fix(spec): the remaining six recursive schemas name both type parameters, and the authoring artifacts stop spelling out defaults (#4195) - #4227
Merged
Conversation
…, so nothing checked what an author wrote (#4195) #4171 fixed what a consumer READS from these schemas. This is the other half: what an author WRITES into them. Zod 4's `ZodType` is `ZodType<out Output = unknown, out Input = unknown>`, and every recursive schema supplied only the first argument: export const NavigationItemSchema: z.ZodType<NavigationItem> = z.lazy(() => …); // ^ Output only — Input stayed `unknown` That `unknown` propagated to every schema embedding one. Measured before: `AppInput['navigation']` was `unknown[]`, `QueryInput['joins']`/`['fields']` were `unknown[]`, `z.input<typeof FormFieldSchema>` was `unknown`. So `defineApp` accepted any array of anything for `navigation` — the densest hand-authored surface on the platform, and the one #4001 made `.strict()` precisely because an author is most likely to write a key from memory there. The strictness was real at parse time and absent at authoring time. Seven schemas now carry both type arguments. Where input and output genuinely differ, the input shape is derived the way #4171 derived the output — `z.input` of the non-recursive half, recursive knot tied by hand. New exported types: `NavigationItemInput`, `NavigationContributionInput`, `FormFieldInput`, `JoinNodeInput`. `FilterCondition`, `NormalizedFilter` and `FieldNode` carry no `.default()` or `.transform()`, so their input is their output. 26 hand-written defaults deleted. #4171 had to spell out `expanded: false` (x16) and `target: '_self'` (x10) because those artifacts are annotated with the PARSED type, where a `.default()`ed key is required — and retyping them to the input surface would have traded eight loud errors for no checking at all. With the input types real they are annotated `AppInput` / `NavigationContributionInput`, the defaults are defaults again, and the literals are checked for the first time. Verified live, not nominal: a literal omitting `expanded`/`target` compiles, and one writing `defaultOpen` — the non-spec key #4171 found in `account.app.ts` — is now a compile error naming `expanded` in its suggestions. Two schemas are typed with a documented caveat: `StateNodeSchema` and `ValidationRuleSchema` reuse their hand-written type for both arguments, exact on the input side and loose on the output side (`StateNodeConfig` marks `type` optional though `.default('atomic')` makes it always present; `BaseValidationRuleShape` carries a `[key: string]: unknown` index signature). Both were already that loose; making them exact means deriving those types from their schemas rather than maintaining them beside one — separate work. Two remain `unknown` on the input side, correctly: `TranslationItemSchema` and `InlineActionSchema` are `z.preprocess(...)`, where an `unknown` input is zod's semantics, not a missing annotation. That is also why this did not land as a CI gate — "output precise but input `unknown`" cannot be separated from legitimate `preprocess` mechanically, and a gate with false positives is worse than none. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XY3swmCsdYx6AMGiRmFt1H
…-any-resolution-1x1erg
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…arallel #4221 ("type-check defineApp's navigation again — #4171 fixed only half the annotation") fixed `NavigationItemSchema` while this branch was in review. Both declarations of `NavigationItemInput` are byte-identical — two independent derivations of the same union — so the only conflict was the doc comment above it, and #4221's is kept: it is the landed version, it carries the `defineApp(navigation: [42, 'nonsense'])` repro, and it references `app.nav-type-assertions.ts`, the compile-level pin it added. What this branch still carries that #4221 did not touch: - the other six schemas with the same defect — FormFieldSchema, JoinNodeSchema, FieldNodeSchema, QuerySchema, FilterConditionSchema, NormalizedFilterSchema, plus StateNodeSchema and ValidationRuleSchema with a documented caveat. The `z.ZodType<T>` single-parameter form is now absent from the codebase. - `NavigationContributionInput`, which #4221's `NavigationItemInput` needs before a contribution array can be typed as authoring input. - the downstream close-out: 26 `.default()`ed keys that #4171 had to spell out are deleted, and the four authoring artifacts are annotated `AppInput` / `NavigationContributionInput` instead of the parsed types. Changeset rewritten to describe this branch's remaining scope rather than restating #4221's. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XY3swmCsdYx6AMGiRmFt1H
unknown, so nothing checked what an author wrote (#4195)
os-zhuang
added a commit
that referenced
this pull request
Jul 31, 2026
…d omission fails instead of shipping (#3786) (#4252) A recursive Zod schema carries a hand-written `z.ZodType<...>` annotation. `z.ZodType` takes <Output, Input> and Input DEFAULTS TO `unknown`, so naming only the first compiles, validates correctly at runtime, and silently un-types every authoring path through the schema. This package has made that mistake twice. #4171 fixed the output half of the nav annotation, and check-exported-any.ts — built to hold that fix — reads output only, so it reported green over the half still broken. #4221 found the consequence (`defineApp` compiled `navigation: [{ totally: 'made up' }, 42, 'nonsense']`) and #4227 named both parameters on the six remaining recursive schemas. Both fixes are correct; neither was pinned. #4227 declined a .d.ts scanner for a good reason — separating a deliberate single-parameter `z.ZodType<T>` (contracts/llm-adapter.ts takes a caller-supplied schema) from an omission needs heuristics on emitted type names, against check-exported-any.ts's zero-false-positive rule — and nominated #4221's assertion pattern instead. This applies it to the eight schemas #4221 did not cover: Query, JoinNode, FieldNode, FilterCondition, NormalizedFilter, StateNode, ValidationRule, FormField. Each gets a positive probe (the authoring shape still compiles, guarding an input type drawn too tight) and a negative probe reached through `z.input<typeof Schema>` — the way a consumer gets there, so the pin covers the WIRING and not just the type alias. Dropping a type parameter turns the suppression unused and tsc fails by name. Mutation-tested in both shapes a regression can take: - QuerySchema back to one parameter → the pin fires AND JoinNodeSchema cascades, since JoinNodeInput.subquery is QueryInput. - StateNodeSchema back to one parameter → ONLY the pin fires. Nothing else references its input, so without this file that regression is silent. No runtime change and no new public export; the module is referenced by no tsup entry and re-exported by no barrel. Also classifies check:strictness-ledger in check-generated.ts's ledger — it landed in #4232 with no entry, so check:generated was failing on main itself, the same cross-PR race the check:variant-docs entry above it documents.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #4195. Follow-up to #4171 / #4194. Overlaps #4221 — see below.
Relationship to #4221
#4221 landed while this was in review and fixed
NavigationItemSchema— the worst instance, and the one with a reproducible demo (defineAppcompilingnavigation: [{ totally: 'made up' }, 42, 'nonsense']). Both branches derivedNavigationItemInputindependently and the two declarations came out byte-identical, so the merge conflict was only the doc comment above it. I kept #4221's: it is the landed version, it carries the repro, and it referencesapp.nav-type-assertions.ts, the compile-level pin it added.This PR is the rest of the sweep. Nothing here re-does #4221's work.
What this covers that #4221 did not
z.ZodTypetakes<Output, Input>andInputdefaults tounknown, so naming only the first parameter leavesz.inputof anything embedding that schema atunknown.NavigationItemSchemawas one of seven. Measured with a type probe:QueryInput['joins']unknown[]JoinNodeInput[]QueryInput['fields']unknown[]FieldNode[]z.input<typeof FormFieldSchema>unknownFormFieldInputz.input<typeof QuerySchema>unknownQueryInputz.input<typeof StateNodeSchema>unknownStateNodeConfigz.input<typeof ValidationRuleSchema>unknownBaseValidationRuleShapeNew exported types:
FormFieldInput,JoinNodeInput,NavigationContributionInput.FilterCondition,NormalizedFilterandFieldNodecarry no.default()or.transform(), so their input is their output and the second parameter is simply the first.The
z.ZodType<T>single-parameter form is now absent from the codebase.The downstream close-out
This is the half #4221 left on the table, and the reason
NavigationContributionInputis needed.#4171 had to spell out
expanded: false(×16) andtarget: '_self'(×10) acrosssetup.app.ts,studio.app.tsandsetup-nav.contributions.ts, because those artifacts are annotated with the parsed type where a.default()ed key is required — and retyping them to the input surface would have traded eight loud errors for no checking at all.With
NavigationItemInputlanded andNavigationContributionInputadded here, they are annotatedAppInput/NavigationContributionInput, the defaults are defaults again, and the literals are checked for the first time. Net across those four files: 21 lines added, 54 removed.Verified live, not nominal: a literal omitting
expanded/targetcompiles, and one writingdefaultOpen— the non-spec key #4171 found inaccount.app.ts— is a compile error whose suggestion list namesexpanded.Two typed with a documented caveat
StateNodeSchemaandValidationRuleSchemareuse their hand-written type for both parameters: exact on the input side, loose on the output side.StateNodeConfigmarkstypeoptional though.default('atomic')makes it always present;BaseValidationRuleShapecarries a[key: string]: unknownindex signature. Both were already that loose — input went fromunknown(types nothing) to a real type, output is untouched. Making them exact means deriving those types from their schemas instead of maintaining them beside one; that is separate work, and the caveat is written at each declaration rather than left for a reader to discover.Why there is still no CI gate
#4195 proposed one — extend
check:exported-anyto fail on "output precise but inputunknown". I measured before building it: after this change exactly two schemas match,TranslationItemSchemaandInlineActionSchema, and both are correct. They arez.preprocess(...), where anunknowninput is zod's semantics, not a missing annotation.Separating those from a genuinely missing parameter needs heuristics on emitted type names, and per the rule written into that script's own header — zero false positives, so red keeps meaning broken — a gate I can't make reliable is worse than none. #4221's
app.nav-type-assertions.tsis the better pattern where it applies: pin the contract at compile level rather than infer intent from shape.Verification
Re-run in full after merging
main(which brought #4221 and 5 other PRs):tsc --noEmit(spec)turbo run build --filter='./packages/*' --filter='./examples/*^...'turbo run test --filter='./packages/*'defineApp/defineViewcall site)check:generatedapi-surface.jsonno drift after regencheck:exported-anycheck:livenesscheck:empty-statecheck:react-conformancecheck:skill-examplescheck:variant-docsPASSlint+ 11check:*PASSThe workspace built with zero errors on the first attempt after the type change — the existing metadata was already conformant, it had simply never been checked.
Note for anyone who saw the earlier runs:
cloud-connection's marketplace seed test timed out under parallel load 4× across #4194 and this PR. #4217 landed a timeout-budget fix for it in the meantime, and the suite is 74/74 clean here.Follow-ups still open
FieldNode's nested-select object form is declared but nothing produces or consumes it — enforce or remove #4196 —FieldNode's nested-select object form is declared but nothing produces or consumes it (enforce-or-remove). Unaffected by this PR.StateNodeConfig/BaseValidationRuleShapeoutput-side looseness above, if someone wants those derived from their schemas.🤖 Generated with Claude Code
https://claude.ai/code/session_01XY3swmCsdYx6AMGiRmFt1H