From 232570694e6e132291833a42ce0456ab6511f5dd Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 28 Jul 2026 14:42:16 +0000 Subject: [PATCH 1/2] =?UTF-8?q?fix(docs):=20FieldSchema.extend()=20does=20?= =?UTF-8?q?not=20exist=20=E2=80=94=20the=20FAQ=20recommended=20a=20call=20?= =?UTF-8?q?that=20throws?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #3882 brought content/docs under the example type-check gate and left the open question: of the blocks still unmarked, is any GENUINE ROT rather than a fragment? Swept them. One real one. deployment/troubleshooting.mdx answered "How do I extend a built-in schema?" with `FieldSchema.extend({ … })`. FieldSchema carries a .transform() that lowers author-facing sugar at parse time, so it is a ZodPipe, not a ZodObject — `.extend` is undefined on it (verified against the built package). Anyone following the FAQ got a "not a function" throw. The example also used `z` without importing it. Rewritten to FieldSchema.in.extend({ … }) — `.in` is the input object the pipe wraps — verified to parse, marked so the gate holds it, and it now says why (same for ObjectSchema/ActionSchema) and warns that the extended schema no longer runs the transform. The sweep's METHOD is recorded in the gate docstring, because two traps make a naive pass report a confident "nothing found": 1. tsc stops after syntactic diagnostics and never runs the semantic pass. Marking all 780 blocks at once let ~200 broken fragments suppress type-checking for every other block; that run returned only TS1xxx codes, which reads exactly like "no rot" and proves nothing. Caught by injecting a deliberate type error and watching it go unreported. 2. Unimported type names resolve against the DOM lib — Plugin, Event, Response, Storage all exist there. A block missing its import reports "'version' does not exist in type 'Plugin'" against lib.dom's Plugin: an artefact, not drift. Three of the strongest-looking candidates were exactly this. After both corrections the rest are fragments, plus config-resolution.mdx, whose aspirational snippets a callout on the page already labels as design intent. Gate: 196 -> 197 checked examples. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Ajwvrmd1hDC9RBofYBhGuR --- .changeset/docs-fieldschema-extend-rot.md | 43 +++++++++++++++++++ content/docs/deployment/troubleshooting.mdx | 21 +++++++-- packages/spec/scripts/check-skill-examples.ts | 20 +++++++++ 3 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 .changeset/docs-fieldschema-extend-rot.md diff --git a/.changeset/docs-fieldschema-extend-rot.md b/.changeset/docs-fieldschema-extend-rot.md new file mode 100644 index 0000000000..21404b91e8 --- /dev/null +++ b/.changeset/docs-fieldschema-extend-rot.md @@ -0,0 +1,43 @@ +--- +"@objectstack/spec": patch +--- + +fix(docs): `FieldSchema.extend()` does not exist — the FAQ was recommending a call that throws + +#3882 brought `content/docs` under the example type-check gate and left an open +question: of the blocks still unmarked, is any of it **genuine rot** rather than a +fragment? Swept them. One real one: + +`content/docs/deployment/troubleshooting.mdx` answered *"How do I extend a +built-in schema?"* with + +```ts +const CustomFieldSchema = FieldSchema.extend({ … }); // TypeError +``` + +`FieldSchema` carries a `.transform()` that lowers author-facing sugar at parse +time, which makes it a **`ZodPipe`, not a `ZodObject`** — `.extend` is `undefined` +on it (verified against the built package). Anyone following the FAQ got a +`not a function` throw. The example also used `z` without importing it. + +Rewritten to `FieldSchema.in.extend({ … })` — `.in` is the input object the pipe +wraps — verified to parse, and marked so the gate holds it. It also now names why +(the same applies to `ObjectSchema` / `ActionSchema`) and warns that the extended +schema no longer runs the transform. + +**The sweep's method is recorded in the gate's docstring**, because two traps make +a naive pass report a confident "nothing found": + +1. **`tsc` stops after syntactic diagnostics** — it never reaches the semantic + pass. Marking all 780 blocks at once let ~200 broken fragments suppress + type-checking for every other block; the run came back with only TS1xxx codes, + which reads exactly like "no rot" and proves nothing. Verified by injecting a + deliberate type error and watching it go unreported. +2. **Unimported type names resolve against the DOM lib.** `Plugin`, `Event`, + `Response`, `Storage` all exist there, so a block missing its import reports + *"'version' does not exist in type 'Plugin'"* against `lib.dom`'s `Plugin` — + an artefact, not drift. Three of the strongest-looking candidates were this. + +After both corrections the remaining blocks are fragments, plus +`protocol/kernel/config-resolution.mdx`, whose aspirational snippets are already +labelled as design intent by a callout on the page. diff --git a/content/docs/deployment/troubleshooting.mdx b/content/docs/deployment/troubleshooting.mdx index 5a7de8228c..8c87bd5d09 100644 --- a/content/docs/deployment/troubleshooting.mdx +++ b/content/docs/deployment/troubleshooting.mdx @@ -359,16 +359,31 @@ Yes. All schemas work with plain JavaScript. You lose compile-time type checking ### How do I extend a built-in schema? -Use Zod's `.extend()` or `.merge()`: +Use Zod's `.extend()` — but reach the **object** first. Several built-in schemas +(`FieldSchema`, `ObjectSchema`, `ActionSchema`, …) carry a `.transform()` that +lowers author-facing sugar at parse time, which makes them a `ZodPipe` rather +than a `ZodObject`. `.extend()` does not exist on a pipe; `.in` is the input +object it wraps: +{/* os:check */} ```typescript +import { z } from 'zod'; import { FieldSchema } from '@objectstack/spec/data'; -const CustomFieldSchema = FieldSchema.extend({ - customProperty: z.string().optional() +// `.in` — the ZodObject the pipe wraps. Calling `.extend()` on FieldSchema +// itself throws "not a function": it is a ZodPipe. +const CustomFieldSchema = FieldSchema.in.extend({ + customProperty: z.string().optional(), }); + +CustomFieldSchema.parse({ name: 'code', type: 'text', customProperty: 'x' }); ``` +Note the extended schema no longer runs the transform, so author-facing sugar +that the pipe would have lowered stays raw. When you only need to *validate* an +extra key alongside the real parse, prefer parsing with the built-in schema and +checking your own additions separately. + ### Where are the JSON Schemas for IDE autocomplete? The bundled JSON Schema is at `packages/spec/json-schema/objectstack.json` (its `x-schema-count` field reports the total number of definitions). Per-domain schemas are in subfolders under `packages/spec/json-schema/` (e.g. `data/`, `ui/`, `api/`). diff --git a/packages/spec/scripts/check-skill-examples.ts b/packages/spec/scripts/check-skill-examples.ts index bae62bcc3f..6d27e7ca9a 100644 --- a/packages/spec/scripts/check-skill-examples.ts +++ b/packages/spec/scripts/check-skill-examples.ts @@ -22,6 +22,26 @@ * still sees the block. A fence-meta tag like ` ```ts check ` would have punched * a hole in that gate. * + * ── Sweeping the UNMARKED blocks: two traps ───────────────────────────────── + * Periodically it is worth asking whether any unmarked block is genuine ROT + * rather than a fragment. Two things make a naive sweep report a confident + * "nothing found", and both bit a real attempt: + * + * 1. `tsc` reports syntactic diagnostics and then STOPS — it never runs the + * semantic pass for the program. Mark every block at once and the ~200 + * syntactically-broken fragments suppress type-checking for ALL the rest, + * so a run producing only TS1xxx codes proves nothing. Exclude the + * syntax failures and re-run before concluding anything about rot. + * 2. A block that omits its imports resolves bare type names against the DOM + * lib: `Plugin`, `Event`, `Response`, `Storage`, `Selection` all exist + * there. `const p: Plugin = { version, init }` then reports "version does + * not exist in type Plugin" against lib.dom's Plugin — an artefact, not + * drift. Check what the name actually resolved to before filing it. + * + * The 2026-07 sweep that applied both corrections found exactly one real rot in + * `content/docs` (a FAQ recommending `FieldSchema.extend()`, impossible since + * FieldSchema became a ZodPipe); everything else was fragments. + * * Each marked block is written verbatim to a throwaway build dir and type-checked * with `tsc --noEmit` against the built `@objectstack/spec` declarations — the * exact surface a consumer's `import { … } from '@objectstack/spec'` resolves to. From bf72c41c3c11b74a88e0161a2e26f5bf2be9eb20 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 28 Jul 2026 14:52:31 +0000 Subject: [PATCH 2/2] =?UTF-8?q?fix(docs):=20teach=20composition,=20not=20.?= =?UTF-8?q?extend=20=E2=80=94=20and=20stop=20depending=20on=20which=20Fiel?= =?UTF-8?q?dSchema=20declaration=20you=20get?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI rejected the first fix: `FieldSchema.in.extend(…)` passed locally and failed with "Property 'in' does not exist on type 'ZodObject<…>'" on the runner. The two builds emit DIFFERENT declarations for the same source — locally z.ZodPipe>, in CI a plain z.ZodObject. The runtime is unambiguous: after `rm -rf dist && pnpm build`, FieldSchema is a `bound ZodPipe` with `.extend === undefined` and `.in` a ZodObject. So CI's declaration contradicts the value it describes — `.extend()` type-checks there and throws at runtime. That is a real spec-side divergence (likely inference instability in the DTS bundling of these very large zod types) and deserves its own investigation; it is not something this docs fix should paper over. So the checked example now uses only `.parse()`: parse with FieldSchema, validate the additions alongside it. Correct under either declaration, verified to run, and better advice anyway — composition keeps the transform, which `.extend()` discards. `.in.extend()` stays in prose as the merged-schema route with that caveat. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Ajwvrmd1hDC9RBofYBhGuR --- .changeset/docs-fieldschema-extend-rot.md | 19 +++++++++--- content/docs/deployment/troubleshooting.mdx | 34 ++++++++++++--------- 2 files changed, 34 insertions(+), 19 deletions(-) diff --git a/.changeset/docs-fieldschema-extend-rot.md b/.changeset/docs-fieldschema-extend-rot.md index 21404b91e8..67608f4b17 100644 --- a/.changeset/docs-fieldschema-extend-rot.md +++ b/.changeset/docs-fieldschema-extend-rot.md @@ -20,10 +20,21 @@ time, which makes it a **`ZodPipe`, not a `ZodObject`** — `.extend` is `undefi on it (verified against the built package). Anyone following the FAQ got a `not a function` throw. The example also used `z` without importing it. -Rewritten to `FieldSchema.in.extend({ … })` — `.in` is the input object the pipe -wraps — verified to parse, and marked so the gate holds it. It also now names why -(the same applies to `ObjectSchema` / `ActionSchema`) and warns that the extended -schema no longer runs the transform. +Rewritten to **compose** — parse with `FieldSchema`, validate your additions +alongside it — verified to both type-check and run. `.in.extend()` is mentioned in +prose as the merged-schema route, with the caveat that it skips the transform. + +**A divergence worth knowing about, found while fixing this.** The first fix used +`FieldSchema.in.extend(…)` in the checked block. It passed locally and failed in +CI with `Property 'in' does not exist on type 'ZodObject<…>'` — the two builds +emit **different declarations for the same source**: locally +`z.ZodPipe>`, in CI a plain `z.ZodObject`. The runtime is +unambiguous (`bound ZodPipe`, `.extend === undefined`, verified after a clean +`rm -rf dist && pnpm build`), so **CI's declaration contradicts the value it +describes** — `.extend()` type-checks there and throws at runtime. Probably +inference instability in the DTS bundling of these very large zod types; worth its +own investigation. The example now uses only `.parse()`, so it is correct under +either declaration and the doc is not hostage to which one you get. **The sweep's method is recorded in the gate's docstring**, because two traps make a naive pass report a confident "nothing found": diff --git a/content/docs/deployment/troubleshooting.mdx b/content/docs/deployment/troubleshooting.mdx index 8c87bd5d09..cfee15bfc5 100644 --- a/content/docs/deployment/troubleshooting.mdx +++ b/content/docs/deployment/troubleshooting.mdx @@ -359,30 +359,34 @@ Yes. All schemas work with plain JavaScript. You lose compile-time type checking ### How do I extend a built-in schema? -Use Zod's `.extend()` — but reach the **object** first. Several built-in schemas -(`FieldSchema`, `ObjectSchema`, `ActionSchema`, …) carry a `.transform()` that -lowers author-facing sugar at parse time, which makes them a `ZodPipe` rather -than a `ZodObject`. `.extend()` does not exist on a pipe; `.in` is the input -object it wraps: +**Not with `.extend()` on the schema itself.** Several built-in schemas — +`FieldSchema`, `ObjectSchema`, `ActionSchema` — carry a `.transform()` that lowers +author-facing sugar at parse time, which makes the exported value a **`ZodPipe`, +not a `ZodObject`**. `FieldSchema.extend` is `undefined`, so calling it throws +`is not a function`. + +Compose instead: parse with the built-in schema, and validate your additions +alongside it. This keeps the transform running, which `.extend()` would discard. {/* os:check */} ```typescript import { z } from 'zod'; import { FieldSchema } from '@objectstack/spec/data'; -// `.in` — the ZodObject the pipe wraps. Calling `.extend()` on FieldSchema -// itself throws "not a function": it is a ZodPipe. -const CustomFieldSchema = FieldSchema.in.extend({ - customProperty: z.string().optional(), -}); +const CustomProps = z.object({ customProperty: z.string().optional() }); + +function parseCustomField(input: unknown) { + return { ...FieldSchema.parse(input), ...CustomProps.parse(input) }; +} -CustomFieldSchema.parse({ name: 'code', type: 'text', customProperty: 'x' }); +parseCustomField({ name: 'code', type: 'text', customProperty: 'x' }); ``` -Note the extended schema no longer runs the transform, so author-facing sugar -that the pipe would have lowered stays raw. When you only need to *validate* an -extra key alongside the real parse, prefer parsing with the built-in schema and -checking your own additions separately. +If you genuinely need one merged schema object, `FieldSchema.in` is the +`ZodObject` the pipe wraps, so `FieldSchema.in.extend({ … })` builds one — but +the result **skips the transform**, so author-facing sugar the pipe would have +lowered stays raw. Prefer the composition above unless you specifically want the +untransformed shape. ### Where are the JSON Schemas for IDE autocomplete?