Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .changeset/restore-asset-requirements-zod.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
'@adcp/sdk': patch
---

Fix(types): restore typed Zod for per-asset-type `*AssetRequirementsSchema`

Regression in 6.19.0 (introduced by #1654): the 12 `*AssetRequirementsSchema`
exports (`ImageAssetRequirementsSchema`, `TextAssetRequirementsSchema`, …) and
the parent `AssetRequirementsSchema` union were emitted as `z.any()` stubs, and
the `requirements` field on every `Individual*AssetSchema` / `Group*AssetSchema`
slot collapsed to `z.optional(z.any())`. TypeScript types were unaffected.

Root cause: #1654's codegen post-processor injects
`import type { ImageAssetRequirements, … } from './core.generated';` at the top
of `tools.generated.ts` so the file typechecks standalone. The Zod codegen step
concatenates `core.generated.ts` + `tools.generated.ts` and passes the combined
source to `ts-to-zod`, but `ts-to-zod` still parses the cross-file `import type`
block and treats those names as external — emitting `z.any()` stubs even though
the actual interfaces are present in the same source.

Fix: strip cross-file `import type { … } from './core.generated';` declarations
from `tools.generated.ts` before merging into the combined source. The types
are already inlined from `core.generated.ts`, so the import is redundant.

Restores field-level runtime validation on `Individual*AssetSchema.requirements`
and re-exports the typed per-asset-type requirements schemas that consumers
like agentic-api had imported in 6.18. Added a regression test in
`test/lib/zod-schemas.test.js` that asserts the schemas reject wrong-typed
fields (a `z.any()` regression would silently accept them).
40 changes: 39 additions & 1 deletion scripts/generate-zod-from-ts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -504,8 +504,29 @@ async function generateZodSchemas() {
const coreContent = readFileSync(CORE_SOURCE_FILE, 'utf8');
const toolsContent = readFileSync(TOOLS_SOURCE_FILE, 'utf8');

// tools.generated.ts imports a handful of *AssetRequirements types from
// core.generated.ts (injected by scripts/generate-types.ts so the standalone
// file typechecks). Since we concatenate both sources for ts-to-zod, those
// imports are redundant — worse, ts-to-zod treats imported names as external
// and emits `z.any()` stubs even when the actual interfaces are present in
// the combined source. Strip cross-file imports before merging.
const toolsWithoutCrossImports = toolsContent.replace(
/^import type \{[^}]*\} from ['"]\.\/core\.generated['"];?\n+/gm,
''
);
// Defensive: if the injector in scripts/generate-types.ts ever changes shape
// (different specifier, single-line form, etc.), the strip would silently
// no-op and we'd regress back to z.any() stubs. Fail loudly instead.
if (toolsWithoutCrossImports.includes("from './core.generated'")) {
throw new Error(
"generate-zod-from-ts: cross-file `import type { ... } from './core.generated'` " +
'survived the strip. Update the regex in this file or the injector in ' +
'scripts/generate-types.ts — letting it through degrades the matching schemas to z.any().'
);
}

// Merge both sources so cross-file type dependencies can be resolved
const combinedSource = `${coreContent}\n\n// ====== TOOL TYPES ======\n\n${toolsContent}`;
const combinedSource = `${coreContent}\n\n// ====== TOOL TYPES ======\n\n${toolsWithoutCrossImports}`;

console.log('📦 Generating Zod schemas for all types...');

Expand Down Expand Up @@ -573,6 +594,23 @@ async function generateZodSchemas() {
// Post-process: Add explicit z.ZodType annotations to schemas that trip TS7056.
zodSchemas = postProcessTS7056Annotations(zodSchemas);

// Defensive: ts-to-zod emits `const FooSchema = z.any();` stubs when it
// can't resolve a referenced type — usually because a cross-file `import
// type` declaration leaks past the upstream strip (see #1659). A `z.any()`
// stub silently accepts any shape at runtime and erases the per-type Zod
// contract downstream consumers rely on. Fail the build so the regression
// surfaces here, not in a consumer's test suite.
const anyStubs = [...zodSchemas.matchAll(/^const (\w+Schema) = z\.any\(\);$/gm)].map(m => m[1]);
if (anyStubs.length > 0) {
throw new Error(
`generate-zod-from-ts: ${anyStubs.length} schema(s) degenerated to z.any() stubs:\n` +
anyStubs.map(n => ` - ${n}`).join('\n') +
'\nThis usually means a cross-file `import type` declaration leaked past the strip ' +
'in this script. Check that the referenced TypeScript interfaces are inlined in ' +
'the combined source, and update the strip regex if a new cross-file import was added.'
);
}

// Create header with metadata
const header = `// Generated Zod v4 schemas from TypeScript types
// Generated at: ${new Date().toISOString()}
Expand Down
Loading
Loading