Skip to content

fix(spec): the remaining six recursive schemas name both type parameters, and the authoring artifacts stop spelling out defaults (#4195) - #4227

Merged
os-zhuang merged 3 commits into
mainfrom
claude/exported-types-any-resolution-1x1erg
Jul 30, 2026
Merged

fix(spec): the remaining six recursive schemas name both type parameters, and the authoring artifacts stop spelling out defaults (#4195)#4227
os-zhuang merged 3 commits into
mainfrom
claude/exported-types-any-resolution-1x1erg

Conversation

@os-zhuang

@os-zhuangos-zhuang commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

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 (defineApp compiling navigation: [{ totally: 'made up' }, 42, 'nonsense']). Both branches derived NavigationItemInput independently 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 references app.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.ZodType takes <Output, Input> and Inputdefaults to unknown, so naming only the first parameter leaves z.input of anything embedding that schema at unknown. NavigationItemSchema was one of seven. Measured with a type probe:

wasnow
QueryInput['joins']unknown[]JoinNodeInput[]
QueryInput['fields']unknown[]FieldNode[]
z.input<typeof FormFieldSchema>unknownFormFieldInput
z.input<typeof QuerySchema>unknownQueryInput
z.input<typeof StateNodeSchema>unknownStateNodeConfig
z.input<typeof ValidationRuleSchema>unknownBaseValidationRuleShape

New exported types: FormFieldInput, JoinNodeInput, NavigationContributionInput. FilterCondition, NormalizedFilter and FieldNode carry 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 NavigationContributionInput is needed.

#4171 had to spell out expanded: false (×16) and target: '_self' (×10) across setup.app.ts, studio.app.ts and setup-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 NavigationItemInput landed and NavigationContributionInput added here, they are annotated AppInput / 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/target compiles, and one writing defaultOpen — the non-spec key #4171 found in account.app.ts — is a compile error whose suggestion list names expanded.

Two typed with a documented caveat

StateNodeSchema and ValidationRuleSchema reuse their hand-written type for both parameters: exact on the input side, 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 — input went from unknown (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-any to fail on "output precise but input unknown". I measured before building it: after this change exactly two schemas match, TranslationItemSchema and InlineActionSchema, and both are correct. They are z.preprocess(...), where an unknown input 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.ts is 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)clean
turbo run build --filter='./packages/*' --filter='./examples/*^...'66/66, zero errors
turbo run test --filter='./packages/*'74/74
examples typecheck (4 apps, every defineApp/defineView call site)clean
downstream-contract typecheckclean
check:generatedall 8 artifacts up to date, api-surface.json no drift after regen
spec pure gatescheck:exported-anycheck:livenesscheck:empty-statecheck:react-conformancecheck:skill-examplescheck:variant-docs PASS
root gateslint + 11 check:* PASS

The 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

🤖 Generated with Claude Code

https://claude.ai/code/session_01XY3swmCsdYx6AMGiRmFt1H

claude added 2 commits July 30, 2026 16:19
…, 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
@vercel

vercelBot commented Jul 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredJul 30, 2026 5:25pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/platform-objects, @objectstack/spec.

106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx(via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx(via @objectstack/spec)
  • content/docs/ai/skills.mdx(via @objectstack/spec)
  • content/docs/api/client-sdk.mdx(via @objectstack/spec)
  • content/docs/api/environment-routing.mdx(via @objectstack/spec)
  • content/docs/api/error-catalog.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx(via @objectstack/spec)
  • content/docs/api/index.mdx(via @objectstack/spec)
  • content/docs/automation/approvals.mdx(via packages/spec)
  • content/docs/automation/flows.mdx(via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx(via packages/spec)
  • content/docs/automation/hooks.mdx(via @objectstack/spec)
  • content/docs/automation/index.mdx(via @objectstack/spec)
  • content/docs/automation/webhooks.mdx(via @objectstack/spec)
  • content/docs/automation/workflows.mdx(via @objectstack/spec)
  • content/docs/concepts/architecture.mdx(via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx(via packages/spec)
  • content/docs/concepts/index.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx(via packages/spec)
  • content/docs/concepts/north-star.mdx(via packages/spec)
  • content/docs/data-modeling/analytics.mdx(via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx(via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx(via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx(via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx(via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx(via @objectstack/spec)
  • content/docs/data-modeling/index.mdx(via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx(via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx(via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx(via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx(via @objectstack/spec)
  • content/docs/deployment/cli.mdx(via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx(via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx(via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx(via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx(via @objectstack/spec)
  • content/docs/getting-started/examples.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx(via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx(via @objectstack/spec)
  • content/docs/kernel/cluster.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx(via packages/spec)
  • content/docs/kernel/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx(via packages/spec)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/spec)
  • content/docs/kernel/services.mdx(via @objectstack/spec)
  • content/docs/permissions/authorization.mdx(via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx(via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx(via @objectstack/spec)
  • content/docs/permissions/positions.mdx(via @objectstack/spec)
  • content/docs/permissions/rls.mdx(via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx(via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx(via @objectstack/spec)
  • content/docs/plugins/development.mdx(via @objectstack/spec)
  • content/docs/plugins/index.mdx(via @objectstack/spec)
  • content/docs/plugins/packages.mdx(via @objectstack/platform-objects, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx(via @objectstack/spec)
  • content/docs/protocol/diagram.mdx(via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx(via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx(via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx(via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx(via @objectstack/spec)
  • content/docs/releases/index.mdx(via @objectstack/spec)
  • content/docs/releases/v12.mdx(via @objectstack/spec)
  • content/docs/releases/v13.mdx(via @objectstack/spec)
  • content/docs/releases/v16.mdx(via @objectstack/spec)
  • content/docs/releases/v17.mdx(via @objectstack/spec)
  • content/docs/releases/v9.mdx(via @objectstack/spec)
  • content/docs/ui/actions.mdx(via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx(via @objectstack/spec)
  • content/docs/ui/dashboards.mdx(via @objectstack/spec)
  • content/docs/ui/forms.mdx(via @objectstack/spec)
  • content/docs/ui/index.mdx(via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx(via @objectstack/spec)
  • content/docs/ui/setup-app.mdx(via @objectstack/platform-objects, @objectstack/spec)
  • content/docs/ui/translations.mdx(via @objectstack/spec)
  • content/docs/ui/views.mdx(via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

…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
@os-zhuangos-zhuang changed the title fix(spec): the authoring side of every recursive schema was unknown, so nothing checked what an author wrote (#4195)fix(spec): the remaining six recursive schemas name both type parameters, and the authoring artifacts stop spelling out defaults (#4195)Jul 30, 2026
@os-zhuang
os-zhuang merged commit d6938bf into mainJul 30, 2026
18 checks passed
@os-zhuang
os-zhuang deleted the claude/exported-types-any-resolution-1x1erg branch July 30, 2026 17:42
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.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:dataprotocol:uisize/mtooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[P3] spec: z.input of every z.ZodType<T>-annotated recursive schema is unknown, so the authoring surface under it is unchecked

2 participants

@os-zhuang@claude