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
9 changes: 9 additions & 0 deletions .changeset/beta7-async-response-refs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"adcontextprotocol": patch
---

Build schema bundles with `core/async-response-refs/` copies so SDK validators
can pre-register async response refs before compiling
`core/async-response-data.json` in the next release candidate.

Refs #5161.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
"test:sign-protocol-tarball": "node --test --test-force-exit --test-timeout=30000 tests/sign-protocol-tarball.test.cjs",
"test:build-schemas-hoist-enums": "node --test --test-force-exit --test-timeout=30000 tests/build-schemas-hoist-enums.test.cjs",
"test:build-schemas-hoist-marked": "node --test --test-force-exit --test-timeout=30000 tests/build-schemas-hoist-marked.test.cjs",
"test:build-schemas-async-response-refs": "node --test --test-force-exit --test-timeout=30000 tests/build-schemas-async-response-refs.test.cjs",
"test:error-codes": "node scripts/lint-error-codes.cjs",
"test:error-code-drift": "node scripts/lint-error-code-drift.cjs",
"test:substitution-vector-names": "node scripts/lint-substitution-vector-names.cjs",
Expand All @@ -85,7 +86,7 @@
"audit:oneof": "node scripts/audit-oneof.mjs",
"test:schema-utf8": "node scripts/normalize-schema-utf8.mjs --check",
"fix:schema-utf8": "node scripts/normalize-schema-utf8.mjs",
"test": "npm run test:docs-nav && npm run test:schemas && npm run test:examples && npm run test:extensions && npm run test:extension-schemas && npm run test:error-handling && npm run test:json-schema && npm run test:canonical-reference-resolver && npm run test:composed && npm run test:rejection-arm-mutex && npm run test:migrations && npm run test:hmac-vectors && npm run test:hmac-signer-conformance && npm run test:transport-errors && npm run test:targeting-overlay-vectors && npm run test:storyboard-scoping && npm run test:storyboard-branch-sets && npm run test:storyboard-provides-state-for && npm run test:storyboard-contradictions && npm run test:storyboard-context-entity && npm run test:storyboard-auth-shape && npm run test:storyboard-test-kits && npm run test:storyboard-sample-request-schema && npm run test:storyboard-response-schema && npm run test:storyboard-context-output-paths && npm run test:storyboard-validations-paths && npm run test:storyboard-check-enum && npm run test:storyboard-advisory-expiry && npm run test:storyboard-raw-mode-required && npm run test:storyboard-upstream-traffic-paths && npm run test:storyboard-doc-parity && npm run test:pagination-invariant && npm run test:version-envelope && npm run test:test-dynamic-imports && npm run test:callapi-state-change && npm run test:sign-protocol-tarball && npm run test:build-schemas-hoist-enums && npm run test:build-schemas-hoist-marked && npm run test:error-codes && npm run test:substitution-vector-names && npm run test:platform-agnostic && npm run test:oneof-discriminators && npm run test:schema-utf8 && npm run test:unit && npm run test:server-unit && npm run test:openapi && npm run typecheck",
"test": "npm run test:docs-nav && npm run test:schemas && npm run test:examples && npm run test:extensions && npm run test:extension-schemas && npm run test:error-handling && npm run test:json-schema && npm run test:canonical-reference-resolver && npm run test:composed && npm run test:rejection-arm-mutex && npm run test:migrations && npm run test:hmac-vectors && npm run test:hmac-signer-conformance && npm run test:transport-errors && npm run test:targeting-overlay-vectors && npm run test:storyboard-scoping && npm run test:storyboard-branch-sets && npm run test:storyboard-provides-state-for && npm run test:storyboard-contradictions && npm run test:storyboard-context-entity && npm run test:storyboard-auth-shape && npm run test:storyboard-test-kits && npm run test:storyboard-sample-request-schema && npm run test:storyboard-response-schema && npm run test:storyboard-context-output-paths && npm run test:storyboard-validations-paths && npm run test:storyboard-check-enum && npm run test:storyboard-advisory-expiry && npm run test:storyboard-raw-mode-required && npm run test:storyboard-upstream-traffic-paths && npm run test:storyboard-doc-parity && npm run test:pagination-invariant && npm run test:version-envelope && npm run test:test-dynamic-imports && npm run test:callapi-state-change && npm run test:sign-protocol-tarball && npm run test:build-schemas-hoist-enums && npm run test:build-schemas-hoist-marked && npm run test:build-schemas-async-response-refs && npm run test:error-codes && npm run test:substitution-vector-names && npm run test:platform-agnostic && npm run test:oneof-discriminators && npm run test:schema-utf8 && npm run test:unit && npm run test:server-unit && npm run test:openapi && npm run typecheck",
"test:all": "npm run test:schemas && npm run test:examples && npm run test:extensions && npm run test:error-handling && npm run test:snippets && npm run typecheck",
"precommit": "bash scripts/with-timeout.sh 60 npm run test:unit && npm run test:test-dynamic-imports && npm run test:callapi-state-change && npm run typecheck",
"test:storyboards": "bash scripts/run-storyboards-matrix.sh",
Expand Down
46 changes: 45 additions & 1 deletion scripts/build-schemas.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -1018,6 +1018,45 @@ function copyAndTransformSchemas(sourceDir, targetDir, version) {
}
}

function copyAsyncResponseRefsToCore(targetDir) {
const asyncRefDir = path.join(targetDir, 'core', 'async-response-refs');

if (fs.existsSync(asyncRefDir)) {
fs.rmSync(asyncRefDir, { recursive: true, force: true });
}
ensureDir(asyncRefDir);

let count = 0;

function walk(dir) {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
const rel = path.relative(targetDir, fullPath);

if (entry.isDirectory()) {
if (rel === 'bundled' || rel.startsWith(`bundled${path.sep}`)) continue;
if (rel === path.join('core', 'async-response-refs') || rel.startsWith(`${path.join('core', 'async-response-refs')}${path.sep}`)) continue;
walk(fullPath);
continue;
}

if (!entry.isFile()) continue;
if (!entry.name.endsWith('.json')) continue;
if (!/-async-response-(submitted|working|input-required)\.json$/.test(entry.name)) continue;
if (rel.startsWith(`core${path.sep}`)) continue;

const targetPath = path.join(asyncRefDir, rel);
ensureDir(path.dirname(targetPath));
fs.copyFileSync(fullPath, targetPath);
count++;
}
}

walk(targetDir);
return count;
}

function updateSourceRegistry(version) {
const registryPath = path.join(SOURCE_DIR, 'index.json');
const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
Expand Down Expand Up @@ -1879,6 +1918,8 @@ async function main() {
console.log(`📋 Creating release: dist/schemas/${version}/`);
ensureDir(versionDir);
copyAndTransformSchemas(SOURCE_DIR, versionDir, version);
const asyncRefCount = copyAsyncResponseRefsToCore(versionDir);
console.log(` ✓ Copied ${asyncRefCount} async response schemas to core/async-response-refs/`);

// Build extensions (auto-discovered, filtered by version)
console.log(`🔌 Building extensions for ${version}`);
Expand Down Expand Up @@ -1910,6 +1951,7 @@ async function main() {
console.log(`📋 Updating latest/ to match release`);
ensureDir(latestDir);
copyAndTransformSchemas(SOURCE_DIR, latestDir, 'latest');
copyAsyncResponseRefsToCore(latestDir);

// Build extensions for latest (using full version for filtering)
buildExtensions(SOURCE_DIR, latestDir, version);
Expand Down Expand Up @@ -1963,6 +2005,8 @@ async function main() {
console.log(`📋 Building schemas to dist/schemas/latest/`);
ensureDir(latestDir);
copyAndTransformSchemas(SOURCE_DIR, latestDir, 'latest');
const asyncRefCount = copyAsyncResponseRefsToCore(latestDir);
console.log(` ✓ Copied ${asyncRefCount} async response schemas to core/async-response-refs/`);

// Build extensions (auto-discovered, filtered by current version)
console.log(`🔌 Building extensions for ${version}`);
Expand Down Expand Up @@ -2016,7 +2060,7 @@ async function main() {
console.log('📖 See docs/reference/versioning.mdx for guidance on which to use.');
}

module.exports = { hoistDuplicateInlineEnums, hoistMarkedSchemas, resolveRefs, versionInlineSchemaIds, dedupBundledSchemaIds, stripIdsFromSubtreesWithLocalRefs };
module.exports = { hoistDuplicateInlineEnums, hoistMarkedSchemas, resolveRefs, versionInlineSchemaIds, dedupBundledSchemaIds, stripIdsFromSubtreesWithLocalRefs, copyAsyncResponseRefsToCore };

if (require.main === module) {
main().catch(err => {
Expand Down
4 changes: 2 additions & 2 deletions scripts/stage-sdk-schema-bundle.sh
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ mkdir -p "$DST"
(cd "$SRC" && tar -cf - .) | (cd "$DST" && tar -xf -)

# SDK 8.1.0-beta.12's loader deliberately skips response-tool files during
# core pre-registration so response root relaxation can happen lazily. Older
# 3.0.x schema bundles have core/async-response-data.json $ref directly to
# core pre-registration so response root relaxation can happen lazily. Some
# published schema bundles have core/async-response-data.json $ref directly to
# tool async response schemas, so those refs must still be registered before
# core schemas compile. Duplicating them under core/ keeps the original bundle
# intact while placing a copy in a directory the loader treats as fragments.
Expand Down
90 changes: 90 additions & 0 deletions tests/build-schemas-async-response-refs.test.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/usr/bin/env node

'use strict';

const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const Ajv = require('ajv');

const { copyAsyncResponseRefsToCore } = require('../scripts/build-schemas.cjs');

function writeJson(filePath, value) {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, JSON.stringify(value, null, 2), 'utf8');
}

function findJsonFiles(dir) {
const out = [];
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
out.push(...findJsonFiles(fullPath));
} else if (entry.isFile() && entry.name.endsWith('.json')) {
out.push(fullPath);
}
}
return out;
}

function isOriginalAsyncResponse(root, filePath) {
const rel = path.relative(root, filePath);
if (rel.startsWith(`core${path.sep}async-response-refs${path.sep}`)) return false;
return /-async-response-(submitted|working|input-required)\.json$/.test(path.basename(filePath));
}

test('core async-response-refs let async-response-data compile when original async response schemas are skipped', () => {
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'adcp-async-response-refs-'));
try {
writeJson(path.join(tmpRoot, 'core', 'async-response-data.json'), {
$schema: 'http://json-schema.org/draft-07/schema#',
$id: '/schemas/latest/core/async-response-data.json',
title: 'Async Response Data',
description: 'Test async response union.',
anyOf: [
{ $ref: '/schemas/latest/media-buy/example-response.json' },
{ $ref: '/schemas/latest/media-buy/example-async-response-working.json' },
],
});

writeJson(path.join(tmpRoot, 'media-buy', 'example-response.json'), {
$schema: 'http://json-schema.org/draft-07/schema#',
$id: '/schemas/latest/media-buy/example-response.json',
title: 'Example Response',
description: 'Completed response.',
type: 'object',
properties: { ok: { type: 'boolean' } },
});

writeJson(path.join(tmpRoot, 'media-buy', 'example-async-response-working.json'), {
$schema: 'http://json-schema.org/draft-07/schema#',
$id: '/schemas/latest/media-buy/example-async-response-working.json',
title: 'Example Async Working',
description: 'Working response.',
type: 'object',
properties: { percentage: { type: 'number' } },
});

const copied = copyAsyncResponseRefsToCore(tmpRoot);
assert.equal(copied, 1);
assert.ok(fs.existsSync(path.join(tmpRoot, 'core', 'async-response-refs', 'media-buy', 'example-async-response-working.json')));

const ajv = new Ajv({ allErrors: true, strict: false, discriminator: true, validateFormats: false });
const target = path.join(tmpRoot, 'core', 'async-response-data.json');

for (const filePath of findJsonFiles(tmpRoot)) {
if (filePath === target) continue;
if (isOriginalAsyncResponse(tmpRoot, filePath)) continue;
const schema = JSON.parse(fs.readFileSync(filePath, 'utf8'));
ajv.addSchema(schema, schema.$id);
}

assert.doesNotThrow(() => {
ajv.compile(JSON.parse(fs.readFileSync(target, 'utf8')));
});
} finally {
fs.rmSync(tmpRoot, { recursive: true, force: true });
}
});
Loading