Skip to content

fix(types): restore typed Zod for per-asset-type AssetRequirements#1660

Merged
bokelley merged 2 commits into
mainfrom
bokelley/fix-1659-requirements-zod-codegen
May 11, 2026
Merged

fix(types): restore typed Zod for per-asset-type AssetRequirements#1660
bokelley merged 2 commits into
mainfrom
bokelley/fix-1659-requirements-zod-codegen

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

Closes #1659.

Why

#1654 introduced a 6.19.0 regression on the runtime-Zod side of the per-asset-type requirements bag: the 12 *AssetRequirementsSchema exports (ImageAssetRequirementsSchema, TextAssetRequirementsSchema, …) and the parent AssetRequirementsSchema union were emitted as z.any() stubs. The requirements field on every Individual*AssetSchema / Group*AssetSchema slot collapsed to z.optional(z.any()). TypeScript types were unaffected — only Zod regressed.

Concretely, IndividualImageAssetSchema accepted { asset_type: 'image', requirements: { max_animation_duration_ms: 'not-a-number' } } cleanly in 6.19, even though max_animation_duration_ms is typed as number. Downstream consumers that imported *AssetRequirementsSchema (e.g. agentic-api in scope3data/agentic-api#2366) saw the exports vanish.

Root cause

scripts/generate-types.ts::applyIndividualAssetDiscriminators (added in #1654) injects an import type { ImageAssetRequirements, … } from './core.generated'; block at the top of tools.generated.ts so the file typechecks standalone. The Zod codegen step at scripts/generate-zod-from-ts.ts then concatenates core.generated.ts + tools.generated.ts and passes the combined source to ts-to-zod. But ts-to-zod still parses that cross-file import type declaration and treats those names as external — emitting z.any() stubs even though the matching interfaces are inlined in the same source.

What

Strip cross-file import type { … } from './core.generated'; declarations from tools.generated.ts before merging into the combined source. The imported types are already inlined from core.generated.ts, so the declarations are redundant in the merged context — and removing them lets ts-to-zod resolve the inline definitions normally.

The 12 per-asset-type schemas now emit as typed z.object({...}), e.g. ImageAssetRequirementsSchema carries min_width, aspect_ratio, max_animation_duration_ms, the bleed union, etc. AssetRequirementsSchema is restored to a real z.union of those 12 schemas. The requirements field on every slot points at the matching typed schema again.

Test plan

  • Regression test added in test/lib/zod-schemas.test.js — asserts each *AssetRequirementsSchema rejects wrong-typed fields (a z.any() regression would silently accept them) and that AssetRequirementsSchema rejects non-object values.
  • npx tsc --noEmit — clean
  • npm run format:check — clean
  • node --test test/lib/zod-schemas.test.js — 42/42 pass
  • Format/validation suite (zod-schemas + format-assets + format-asset-slot-builders + validation) — 91/91 pass

🤖 Generated with Claude Code

bokelley and others added 2 commits May 11, 2026 04:50
…1659)

Strip the cross-file `import type { ... } from './core.generated';` block
from tools.generated.ts before merging into the combined source passed to
ts-to-zod. With that import in place, ts-to-zod treated the 12
*AssetRequirements names as external and emitted z.any() stubs — even
though the actual interfaces were inlined in the merged source.

The import was added in #1654 so tools.generated.ts typechecks standalone;
it's redundant inside the merged Zod codegen source, where core.generated
is already concatenated.

Restores typed Zod for ImageAssetRequirementsSchema, TextAssetRequirements-
Schema, and the 10 siblings, plus the AssetRequirementsSchema union and the
`requirements` field on every Individual*AssetSchema / Group*AssetSchema
slot. Adds a regression test that probes wrong-typed fields — a z.any()
regression would silently accept them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two cheap build-time defenses, both class-level not instance-level:

1. After stripping `import type { ... } from './core.generated'`, assert
   the strip actually removed it. If the injector in generate-types.ts
   ever changes shape (different specifier, single-line form), the silent
   no-op would regress to z.any() stubs.

2. After ts-to-zod runs, scan for `const FooSchema = z.any();` lines and
   throw. This catches any future codegen path that degrades a named
   schema to z.any(), not just the import-shadow class.

Both surface re-regressions at the codegen step rather than in a
downstream consumer's test suite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley merged commit 94fc0e5 into main May 11, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Regression in 6.19.0: per-asset-type requirements codegen as z.any() (was typed in 6.18)

1 participant