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
5 changes: 5 additions & 0 deletions .changeset/warm-impalas-press.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@adcp/client': minor
---

Close schema pipeline gap: generate TypeScript types and Zod schemas for all missing JSON schemas, add TOOL_REQUEST_SCHEMAS and TOOL_RESPONSE_SCHEMAS exports
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@
"docs:open": "open docs/api/index.html || xdg-open docs/api/index.html || start docs/api/index.html",
"ci:validate": "node scripts/ci-validate.js",
"ci:quick": "npm run format:check && npm run typecheck && npm run build:lib && npm test",
"ci:schema-check": "npm run sync-schemas && npm run generate-types && npm run generate-registry-types && git diff --exit-code src/lib/types/ src/lib/agents/ src/lib/registry/types.generated.ts schemas/registry/registry.yaml || (echo '⚠️ Generated files are out of sync. Run: npm run sync-schemas && npm run generate-types && npm run generate-registry-types' && exit 1)",
"ci:schema-check": "npm run sync-schemas && npm run generate-types && npm run generate-registry-types && git diff --exit-code -I '// Generated at:' src/lib/types/ src/lib/agents/ src/lib/registry/types.generated.ts schemas/registry/registry.yaml || (echo '⚠️ Generated files are out of sync. Run: npm run sync-schemas && npm run generate-types && npm run generate-registry-types' && exit 1)",
"ci:docs-check": "npm run generate-agent-docs && git diff --exit-code -I '> Generated at:' docs/llms.txt docs/TYPE-SUMMARY.md || (echo '⚠️ Agent docs are out of sync. Run: npm run generate-agent-docs' && exit 1)",
"ci:pre-push": "npm run ci:schema-check && npm run ci:quick",
"hooks:install": "node scripts/install-hooks.js",
Expand Down
149 changes: 148 additions & 1 deletion scripts/generate-types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env tsx

import { writeFileSync, mkdirSync, readFileSync, existsSync } from 'fs';
import { writeFileSync, mkdirSync, readFileSync, existsSync, readdirSync, statSync } from 'fs';
import { compile } from 'json-schema-to-typescript';
import path from 'path';
import { removeArrayLengthConstraints } from './schema-utils';
Expand Down Expand Up @@ -1168,6 +1168,144 @@ export class AgentCollection {
return agentClass;
}

/**
* Recursively discover all JSON schema files in the cache directory.
* Returns relative paths from LATEST_CACHE_DIR (e.g., "core/format.json", "enums/channels.json").
*/
function discoverAllSchemaFiles(dir: string, base: string = dir): string[] {
const results: string[] = [];
for (const entry of readdirSync(dir)) {
const fullPath = path.join(dir, entry);
if (statSync(fullPath).isDirectory()) {
// Skip tmp directory
if (entry === 'tmp') continue;
results.push(...discoverAllSchemaFiles(fullPath, base));
} else if (entry.endsWith('.json') && entry !== 'index.json') {
results.push(path.relative(base, fullPath));
}
}
return results;
}

/**
* Convert a schema file path to a PascalCase type name.
* e.g., "core/format.json" -> "Format"
* "enums/pricing-model.json" -> "PricingModel"
* "core/assets/html-asset.json" -> "HtmlAsset"
* "pricing-options/cpm-option.json" -> "CpmOption"
* "brand/rights-pricing-option.json" -> "RightsPricingOption"
*/
function schemaPathToTypeName(relativePath: string): string {
const fileName = path.basename(relativePath, '.json');
return fileName
.split('-')
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
.join('');
}

/**
* Compile all schemas that weren't already generated by the root schema or tool passes.
* This fills the gap for standalone schemas in core/, enums/, pricing-options/, brand/, etc.
*
* Skips:
* - Task request/response schemas (already generated as tool types)
* - Root aggregation schemas (brand.json, adagents.json at top level)
* - Schemas whose type names were already generated via $ref resolution
* - Async response variant schemas (working/submitted/input-required)
*/
async function compileGapSchemas(generatedTypes: Set<string>, refResolver: any): Promise<string> {
const allFiles = discoverAllSchemaFiles(LATEST_CACHE_DIR);
const gapCode: string[] = [];

// Directories that contain task request/response schemas (already covered by tool generation)
const taskDirs = new Set([
'account',
'media-buy',
'creative',
'signals',
'governance',
'protocol',
'sponsored-intelligence',
'compliance',
'content-standards',
'property',
'collection',
]);

// Patterns that indicate task request/response schemas
const taskSchemaPattern = /-(request|response)\.json$/;
// Async response variants are always generated alongside their parent tool
const asyncVariantPattern = /-async-response-(working|submitted|input-required)\.json$/;

// Top-level aggregation schemas (not standalone types)
const skipFiles = new Set(['adagents.json', 'brand.json']);

let compiledCount = 0;

for (const relPath of allFiles.sort()) {
// Skip top-level aggregation files
if (!relPath.includes('/') && skipFiles.has(relPath)) continue;

const dir = relPath.split('/')[0];

// Skip task request/response schemas in task directories
if (taskDirs.has(dir) && taskSchemaPattern.test(relPath)) continue;

// Skip async response variants
if (asyncVariantPattern.test(relPath)) continue;

const typeName = schemaPathToTypeName(relPath);

// Skip if this type was already generated
if (generatedTypes.has(typeName)) continue;

// Skip deprecated schemas
if (DEPRECATED_SCHEMAS.has(path.basename(relPath, '.json'))) continue;

try {
const schemaPath = path.join(LATEST_CACHE_DIR, relPath);
let schema = JSON.parse(readFileSync(schemaPath, 'utf8'));

// Apply same preprocessing as other schema passes
const fileName = path.basename(relPath, '.json');
if (DEPRECATED_ENUM_VALUES[fileName]) {
schema = removeDeprecatedFields(schema, fileName);
}
const pascalName = schemaPathToTypeName(relPath);
if (DEPRECATED_SCHEMA_FIELDS[pascalName]) {
schema = removeDeprecatedFields(schema, pascalName);
}
if (BACKWARD_COMPAT_OPTIONAL_FIELDS[pascalName]) {
schema = makeFieldsOptional(schema, BACKWARD_COMPAT_OPTIONAL_FIELDS[pascalName]);
}

const strictSchema = enforceStrictSchema(removeArrayLengthConstraints(schema));
const types = await compile(strictSchema, typeName, {
bannerComment: '',
style: { semi: true, singleQuote: true },
additionalProperties: false,
strictIndexSignatures: true,
$refOptions: {
resolve: {
cache: refResolver,
},
},
});

const filtered = filterDuplicateTypeDefinitions(types, generatedTypes);
if (filtered.trim()) {
gapCode.push(`// ${relPath}\n${filtered}`);
compiledCount++;
}
} catch (error: any) {
console.warn(`⚠️ Failed to compile gap schema ${relPath}: ${error.message}`);
}
}

console.log(`📦 Compiled ${compiledCount} gap schemas`);
return gapCode.join('\n\n');
}

async function generateTypes() {
console.log('🔄 Generating AdCP types and fluent API...');

Expand Down Expand Up @@ -1310,6 +1448,15 @@ async function generateTypes() {
toolTypes = removeNumberedTypeDuplicates(toolTypes);
toolTypes = fixTypedIndexSignatures(toolTypes);

// Compile gap schemas: all schemas not already generated by root schema passes.
// Only dedup against core types (not tool types) because gap schemas go into
// core.generated.ts which is a separate file from tools.generated.ts.
console.log('\n🔍 Scanning for gap schemas...');
const gapTypes = await compileGapSchemas(new Set(generatedCoreTypes), refResolver);
if (gapTypes.trim()) {
coreTypes += `\n// GAP SCHEMAS — types not reachable from root schemas or tool definitions\n${gapTypes}\n`;
}

// Generate Agent classes
const agentClasses = generateAgentClasses(tools);

Expand Down
5 changes: 5 additions & 0 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,11 @@ export const PreviewCreativeVariantRequestSchema = _variant;
// Auth utilities for custom integrations
export { getAuthToken, createAdCPHeaders, createMCPAuthHeaders, createAuthenticatedFetch } from './auth';

// ====== TOOL SCHEMA MAPS ======
// Zod schemas keyed by tool name — use with server.tool(name, schema.shape, handler)
export { TOOL_REQUEST_SCHEMAS } from './utils/tool-request-schemas';
export { TOOL_RESPONSE_SCHEMAS } from './utils/response-schemas';

// ====== VALIDATION ======
// Schema validation for requests/responses
export { validateAgentUrl, validateAdCPResponse, getExpectedSchema, handleAdCPResponse } from './validation';
Expand Down
Loading
Loading