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-zhuang os-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 Input defaults 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:

was now
QueryInput['joins'] unknown[] JoinNodeInput[]
QueryInput['fields'] unknown[] FieldNode[]
z.input<typeof FormFieldSchema> unknown FormFieldInput
z.input<typeof QuerySchema> unknown QueryInput
z.input<typeof StateNodeSchema> unknown StateNodeConfig
z.input<typeof ValidationRuleSchema> unknown BaseValidationRuleShape

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 typecheck clean
check:generated all 8 artifacts up to date, api-surface.json no drift after regen
spec pure gates check:exported-any check:liveness check:empty-state check:react-conformance check:skill-examples check:variant-docs PASS
root gates lint + 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

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 30, 2026 5:25pm

Request Review

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation protocol:data protocol:ui tooling labels Jul 30, 2026
@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-zhuang os-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 main Jul 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 free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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