Skip to content

Commit 2325706

Browse files
committed
fix(docs): FieldSchema.extend() does not exist — the FAQ recommended a call that throws
#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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ajwvrmd1hDC9RBofYBhGuR
1 parent 93f267f commit 2325706

3 files changed

Lines changed: 81 additions & 3 deletions

File tree

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
fix(docs): `FieldSchema.extend()` does not exist — the FAQ was recommending a call that throws
6+
7+
#3882 brought `content/docs` under the example type-check gate and left an open
8+
question: of the blocks still unmarked, is any of it **genuine rot** rather than a
9+
fragment? Swept them. One real one:
10+
11+
`content/docs/deployment/troubleshooting.mdx` answered *"How do I extend a
12+
built-in schema?"* with
13+
14+
```ts
15+
const CustomFieldSchema = FieldSchema.extend({ … }); // TypeError
16+
```
17+
18+
`FieldSchema` carries a `.transform()` that lowers author-facing sugar at parse
19+
time, which makes it a **`ZodPipe`, not a `ZodObject`**`.extend` is `undefined`
20+
on it (verified against the built package). Anyone following the FAQ got a
21+
`not a function` throw. The example also used `z` without importing it.
22+
23+
Rewritten to `FieldSchema.in.extend({ … })``.in` is the input object the pipe
24+
wraps — verified to parse, and marked so the gate holds it. It also now names why
25+
(the same applies to `ObjectSchema` / `ActionSchema`) and warns that the extended
26+
schema no longer runs the transform.
27+
28+
**The sweep's method is recorded in the gate's docstring**, because two traps make
29+
a naive pass report a confident "nothing found":
30+
31+
1. **`tsc` stops after syntactic diagnostics** — it never reaches the semantic
32+
pass. Marking all 780 blocks at once let ~200 broken fragments suppress
33+
type-checking for every other block; the run came back with only TS1xxx codes,
34+
which reads exactly like "no rot" and proves nothing. Verified by injecting a
35+
deliberate type error and watching it go unreported.
36+
2. **Unimported type names resolve against the DOM lib.** `Plugin`, `Event`,
37+
`Response`, `Storage` all exist there, so a block missing its import reports
38+
*"'version' does not exist in type 'Plugin'"* against `lib.dom`'s `Plugin`
39+
an artefact, not drift. Three of the strongest-looking candidates were this.
40+
41+
After both corrections the remaining blocks are fragments, plus
42+
`protocol/kernel/config-resolution.mdx`, whose aspirational snippets are already
43+
labelled as design intent by a callout on the page.

content/docs/deployment/troubleshooting.mdx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -359,16 +359,31 @@ Yes. All schemas work with plain JavaScript. You lose compile-time type checking
359359

360360
### How do I extend a built-in schema?
361361

362-
Use Zod's `.extend()` or `.merge()`:
362+
Use Zod's `.extend()` — but reach the **object** first. Several built-in schemas
363+
(`FieldSchema`, `ObjectSchema`, `ActionSchema`, …) carry a `.transform()` that
364+
lowers author-facing sugar at parse time, which makes them a `ZodPipe` rather
365+
than a `ZodObject`. `.extend()` does not exist on a pipe; `.in` is the input
366+
object it wraps:
363367

368+
{/* os:check */}
364369
```typescript
370+
import { z } from 'zod';
365371
import { FieldSchema } from '@objectstack/spec/data';
366372

367-
const CustomFieldSchema = FieldSchema.extend({
368-
customProperty: z.string().optional()
373+
// `.in` — the ZodObject the pipe wraps. Calling `.extend()` on FieldSchema
374+
// itself throws "not a function": it is a ZodPipe.
375+
const CustomFieldSchema = FieldSchema.in.extend({
376+
customProperty: z.string().optional(),
369377
});
378+
379+
CustomFieldSchema.parse({ name: 'code', type: 'text', customProperty: 'x' });
370380
```
371381

382+
Note the extended schema no longer runs the transform, so author-facing sugar
383+
that the pipe would have lowered stays raw. When you only need to *validate* an
384+
extra key alongside the real parse, prefer parsing with the built-in schema and
385+
checking your own additions separately.
386+
372387
### Where are the JSON Schemas for IDE autocomplete?
373388

374389
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/`).

packages/spec/scripts/check-skill-examples.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,26 @@
2222
* still sees the block. A fence-meta tag like ` ```ts check ` would have punched
2323
* a hole in that gate.
2424
*
25+
* ── Sweeping the UNMARKED blocks: two traps ─────────────────────────────────
26+
* Periodically it is worth asking whether any unmarked block is genuine ROT
27+
* rather than a fragment. Two things make a naive sweep report a confident
28+
* "nothing found", and both bit a real attempt:
29+
*
30+
* 1. `tsc` reports syntactic diagnostics and then STOPS — it never runs the
31+
* semantic pass for the program. Mark every block at once and the ~200
32+
* syntactically-broken fragments suppress type-checking for ALL the rest,
33+
* so a run producing only TS1xxx codes proves nothing. Exclude the
34+
* syntax failures and re-run before concluding anything about rot.
35+
* 2. A block that omits its imports resolves bare type names against the DOM
36+
* lib: `Plugin`, `Event`, `Response`, `Storage`, `Selection` all exist
37+
* there. `const p: Plugin = { version, init }` then reports "version does
38+
* not exist in type Plugin" against lib.dom's Plugin — an artefact, not
39+
* drift. Check what the name actually resolved to before filing it.
40+
*
41+
* The 2026-07 sweep that applied both corrections found exactly one real rot in
42+
* `content/docs` (a FAQ recommending `FieldSchema.extend()`, impossible since
43+
* FieldSchema became a ZodPipe); everything else was fragments.
44+
*
2545
* Each marked block is written verbatim to a throwaway build dir and type-checked
2646
* with `tsc --noEmit` against the built `@objectstack/spec` declarations — the
2747
* exact surface a consumer's `import { … } from '@objectstack/spec'` resolves to.

0 commit comments

Comments
 (0)