|
| 1 | +--- |
| 2 | +"@objectstack/spec": patch |
| 3 | +--- |
| 4 | + |
| 5 | +test(spec): pin the input half of every recursive schema, so the third omission fails instead of shipping (#3786) |
| 6 | + |
| 7 | +A recursive Zod schema cannot infer its own type, so it carries a hand-written |
| 8 | +`z.ZodType<...>` annotation. `z.ZodType` takes **two** type parameters, |
| 9 | +`<Output, Input>`, and `Input` defaults to `unknown`. Naming only the first |
| 10 | +compiles, validates correctly at runtime, and silently un-types every authoring |
| 11 | +path through the schema — `unknown` accepts everything. |
| 12 | + |
| 13 | +This package has made that mistake twice: |
| 14 | + |
| 15 | +1. **#4171** replaced `z.ZodType<any>` on the nav union with |
| 16 | + `z.ZodType<NavigationItem>`, fixing the output half. `check-exported-any.ts` |
| 17 | + was built to hold that fix and reads output only, so it reported green over |
| 18 | + the half that was still broken. |
| 19 | +2. **#4221** found the consequence — `defineApp`, the documented authoring entry |
| 20 | + point, compiled `navigation: [{ totally: 'made up' }, 42, 'nonsense']` clean — |
| 21 | + and **#4227** then named both parameters on the six remaining recursive |
| 22 | + schemas. |
| 23 | + |
| 24 | +Both fixes are correct and both are currently unpinned. #4227 considered a |
| 25 | +`.d.ts`-level scanner and declined it for a good reason: separating a deliberate |
| 26 | +single-parameter `z.ZodType<T>` (the generic in `contracts/llm-adapter.ts` takes |
| 27 | +a caller-supplied schema, where the input side is nobody's business) from an |
| 28 | +omission needs heuristics on emitted type names, and `check-exported-any.ts`'s |
| 29 | +own rule is zero false positives so red keeps meaning broken. Its commit |
| 30 | +nominated #4221's assertion-file pattern instead. This is that pattern, applied |
| 31 | +to the eight schemas #4221 did not cover: `QuerySchema`, `JoinNodeSchema`, |
| 32 | +`FieldNodeSchema`, `FilterConditionSchema`, `NormalizedFilterSchema`, |
| 33 | +`StateNodeSchema`, `ValidationRuleSchema`, `FormFieldSchema`. |
| 34 | + |
| 35 | +`src/recursive-schema-input-assertions.ts` gives each one a positive probe (the |
| 36 | +authoring shape still compiles — guarding an input type drawn too tight) and a |
| 37 | +negative probe reached **through `z.input<typeof Schema>`**, the way a consumer |
| 38 | +gets there. The negative is load-bearing: it is a value `unknown` would accept |
| 39 | +and the real type rejects, so dropping a type parameter turns the suppression |
| 40 | +unused and `tsc --noEmit` fails on that line, by name. |
| 41 | + |
| 42 | +Verified by mutation in both shapes a regression can take: |
| 43 | + |
| 44 | +- `QuerySchema` back to one parameter → the pin fires **and** `JoinNodeSchema` |
| 45 | + cascades a type error, because `JoinNodeInput.subquery` is `QueryInput`. |
| 46 | +- `StateNodeSchema` back to one parameter → **only** the pin fires. Nothing else |
| 47 | + in the package references its input, so without this file that regression is |
| 48 | + completely silent. That is the case the file exists for. |
| 49 | + |
| 50 | +No runtime change, no new public export (`check:api-surface` reports no diff); |
| 51 | +the module is referenced by no tsup entry and re-exported by no barrel. |
| 52 | + |
| 53 | +Also classifies `check:strictness-ledger` in `check-generated.ts`'s ledger. It |
| 54 | +landed in #4232 without an entry, so `check:generated` was failing on `main` |
| 55 | +itself — the same cross-PR race the `check:variant-docs` entry above it already |
| 56 | +documents, now on its second occurrence. |
0 commit comments