diff --git a/.changeset/docs-fieldschema-extend-rot.md b/.changeset/docs-fieldschema-extend-rot.md new file mode 100644 index 0000000000..67608f4b17 --- /dev/null +++ b/.changeset/docs-fieldschema-extend-rot.md @@ -0,0 +1,54 @@ +--- +"@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 **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": + +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..cfee15bfc5 100644 --- a/content/docs/deployment/troubleshooting.mdx +++ b/content/docs/deployment/troubleshooting.mdx @@ -359,16 +359,35 @@ 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()`: +**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'; -const CustomFieldSchema = FieldSchema.extend({ - customProperty: z.string().optional() -}); +const CustomProps = z.object({ customProperty: z.string().optional() }); + +function parseCustomField(input: unknown) { + return { ...FieldSchema.parse(input), ...CustomProps.parse(input) }; +} + +parseCustomField({ name: 'code', type: 'text', customProperty: 'x' }); ``` +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? 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.