test(storyboards): mirror canonical storyboard source#5243
Conversation
# Conflicts: # package-lock.json # package.json # scripts/overlay-compliance-cache.sh # scripts/run-storyboards-matrix.sh # server/src/training-agent/comply-test-controller.ts # server/src/training-agent/product-factory.ts # server/src/training-agent/task-handlers.ts
There was a problem hiding this comment.
LGTM. Follow-ups noted below. Right shape: canonical source under static/compliance/source/, generated artifacts under dist/compliance/{version}/, a lint that fails closed in both directions, and the overlay script clears the SDK cache before applying source so deletes can't survive. Matches the existing schema authority pattern.
Things I checked
lintSourceAuthorityrules and walker inscripts/lint-compliance-source-authority.cjs:352-394— generated artifacts (domains/,index.json) are banned in source, canonical top-level set enforced,phases:-bearing YAML count required so an empty mirror tripssource_empty.lintBuiltMirror+lintIndexMembershipatscripts/lint-compliance-source-authority.cjs:396-430,310-350— byte-identical compare via SHA-256, three-way membership compare againstindex.json'suniversal/protocols/specialismsarrays.assertComplianceSourceAuthorityandassertComplianceBuiltMirrorhook points inscripts/build-compliance.cjs:128-144,547,559-565— source lint runs before the build, mirror lint runs afterbuildTowritesindex.json.if (require.main === module) main()at the foot preserves CLI behavior while exposingbuildTo/generateIndexfor tests.scripts/overlay-compliance-cache.sh:147-156—rm -rfis guarded by exact-string match[ "${SRC}" = "dist/compliance/latest" ], same conditional used elsewhere in the script. Order is correct: build → clear DST canonical dirs → overlay → run mirror lint against DST.server/src/training-agent/task-handlers.ts:2666,2672,3011contextecho —static/schemas/source/core/context.jsondefines context as "echoed unchanged in responses" andget-products-response.jsonhascontextas a first-class response field. Conformant, not drift.- Changeset tier —
patchis correct perdocs/reference/versioning.mdx:148-166: "The conformance suite … versions independently and is patch-level by default." Nostatic/schemas/source/**change, no task/error-code change. - Forward-drift allowlist pattern — every new entry carries the same expiry pointer ("Remove when 3.1.0-rc.4 schema lands"), the new test at
tests/lint-storyboard-validations-paths.test.cjs:414-435asserts each entry still reproduces the underlying violation, so stale entries fail closed. Matches the shrink-only convention already documented intests/storyboard-sample-request-schema-allowlist.json's$comment. - CI wiring in
.github/workflows/build-check.yml:38-45— runs afternpm run build, before the dist-assets assertion.
Follow-ups (non-blocking — file as issues)
lintBuiltMirrorignores stray top-level entries in the target. Only the fiveCANONICAL_TOP_LEVEL_DIRSplusdomains/andindex.jsonare inspected. An extra leftover directory in the mirror (e.g. an abandoned cache subtree from a previous bundle layout) silently survives. Tighten by asserting the target's top-level set is exactly{...CANONICAL_TOP_LEVEL_DIRS, 'domains', 'index.json'}.lintIndexMembershipwill false-positive the first time a specialism references a protocol without a directory.scripts/lint-compliance-source-authority.cjs:322-336derivesexpected.protocolsfromlistDirs(protocols/), butscripts/build-compliance.cjs:discoverProtocolssynthesizes protocol ids from specialismprotocol:fields. Today every reference has a baseline dir so the sets agree; the first orphan reference tripsindex_membership_drift. Either reusediscoverProtocolsor take the union.overlay-compliance-cache.shrm-rf list is hard-coded. If the SDK's bundled cache ever grows a new generated subtree (a futureindex/, per-version dir, etc.), it'll survive the cleanup and mask drift. Discovery beats a hand-maintained list here.- The
AAO Verified (Spec)→AgenticAdvertising.org Verified (Spec)rename is half-applied. Lines 28 and 65 ofdocs/building/verification/storyboards-vs-scenarios.mdxflip the name; ~20 other sites includingdocs/building/verification/conformance.mdx:16,25,27,29,31,198,232and the canonical/docs/building/verification/aao-verifiedpage still say AAO Verified. Either revert in this PR (cosmetic isn't worth the inconsistency) or sweep separately. Notable that this brand rename rode in on a patch-tier changeset with no mention in the changeset body. - The
comply()link disappeared from the badge-issuance sentence. Pre-PR line 28 said the badge "is issued by running these YAML files throughcomply()and observing every assertion pass" with a hyperlink — clean operational definition. Post-PR it routes authority through AAO's heartbeat without naming the reproducible CLI. Restore thecomply()link. - Rollout-order paragraph at
docs/building/verification/storyboards-vs-scenarios.mdx:42-48uses lowercase "must". If this is meant to bind the working group, make it RFC 2119 MUST and cross-link fromdocs/reference/versioning.mdx. If it's operational guidance, lowercase "should" reads cleaner.
Minor nits (non-blocking)
contextEchoshape mismatch with prevailing pattern.server/src/training-agent/task-handlers.ts:2665usesreq.context ? { context: req.context } : {}; every other context-echo site in the same file uses...(req.context !== undefined && { context: req.context }). Match the existing pattern so falsy-but-present context values echo identically across handlers.- PR title doesn't follow the conventional-commits format used in recent history.
[codex] Mirror canonical storyboard sourcevs e.g.fix(release): …,chore: …. Cosmetic.
Safe to merge.
# Conflicts: # server/src/training-agent/task-handlers.ts
There was a problem hiding this comment.
LGTM. Follow-ups noted below. Source-authority shift is the right shape: protocols/, specialisms/, test-kits/, test-vectors/, universal/ as authored source vs. domains/ and index.json as generated, mechanically enforced by scripts/lint-compliance-source-authority.cjs running pre-build, post-build, and post-overlay. That's the load-bearing piece.
Things I checked
scripts/lint-compliance-source-authority.cjs:166-208—lintSourceAuthorityrejectsdomains/andindex.jsonin source and requires the five canonical top-level dirs. Correct split.scripts/lint-compliance-source-authority.cjs:210-244—lintBuiltMirrorSHA-256s every source file against its mirror underdist/compliance/<target>/<dir>/, plus assertsdomains/is byte-for-byte equal toprotocols/. Drift, missing-in-mirror, and stale-in-mirror all surface.scripts/build-compliance.cjs:548,560-565— source-authority lint runs before main build, built-mirror lint runs at the end ofbuildTo. Both fail-closed. Theif (require.main === module) main()guard plusmodule.exportsis safe — no existing requirer relies on side-effects.scripts/overlay-compliance-cache.sh:147-156—rm -rfof canonical entries before overlay is gated toSRC == "dist/compliance/latest", then the new lint runs against$DST. Stale SDK-cache entries can't survive a rename or delete from source..github/workflows/build-check.yml:38-44— new lint wired into CI after build. Five storyboard lints chained into one job step..changeset/5016-canonical-storyboard-source.md—patchis defensible because zero files understatic/compliance/source/**are modified in this PR (the canonical mirror was already published via@adcp/sdk@8.1.0-beta.19). Worth saying so in the changeset body so future readers don't assume new wire constraints.server/src/training-agent/task-handlers.ts:2744,3095—contextEchois additive andcontextis already declared onget-products-response.jsonascore/context.json. Not a smuggled wire change.- Test-plan checklist in the PR body — every storyboard matrix path green (
/signals,/sales,/governance,/creative,/creative-builder,/brand) plus the newtest:compliance-source-authorityran clean. The stale-allowlist-entry tests added intests/lint-storyboard-{context-output,validations}-paths.test.cjsare in the validation list and passed, which confirms the lint's path resolver still can't traverse the polymorphism the allowlist documents — see below.
Follow-ups (non-blocking — file as issues)
- Allowlist
reasonfield is mis-titled. The 19 entries inscripts/storyboard-validations-paths-allowlist.jsonand the 3 inscripts/storyboard-context-output-paths-allowlist.jsonare labeled "Forward schema drift from the @adcp/sdk@8.1.0-beta.19 storyboard mirror (#5016). Remove when the matching 3.1.0-rc.4 schema changes land in static/schemas/source." Butsignal_targeting_rules,signal_targeting_groups,signal_ref.scope,format_option_id,task_completion.media_buy_id, and theaudit_observations[].*paths are all already present instatic/schemas/source/. The real gap is the static path resolver — oneOf-arm traversal,details.claimed_value.*nested-object resolution,core/context.jsonextension-point fall-through. Same shape as the legacyidempotency_keyentry this PR removed. Re-classify with per-entry specificity (e.g.static_path_resolver_limitation: oneOf-arm,core/context.json extension point) so the "remove when" condition is actionable. As-is, a future maintainer will grep, find the field already in the schema, and either delete the entry (re-introducing a lint failure) or leave a misleading reason in perpetuity. enum@/scenarioallowlist entries intests/storyboard-sample-request-schema-allowlist.jsonare Ajvif/thenconstnoise, not missing scenarios.compliance/comply-test-controller-request.jsonalready definesforce_creative_purged,query_directed_audit_observation,query_edited_audit_observation,force_upstream_unavailable, andseed_measurement_catalog. The errors are Ajv reporting each non-matchingif.constbranch in the cascadingallOfas an enum-class failure. Either filter that error class at the validator, or rewrite the reason to say so explicitly.- Governance rename is buried.
docs/building/verification/storyboards-vs-scenarios.mdxchanges "AdCP Verified (Spec)" → "AgenticAdvertising.org Verified (Spec)" in two places, with the claim that AAO issues the qualifier when its compliance heartbeat runs the storyboards. That's a governance/brand change about who runs and stamps conformance — affects every implementer's marketing surface. Move to a separate PR with its own changeset and AAO sign-off rather than landing inside a tooling PR titled "Mirror canonical storyboard source." domains/alias has no sunset. The lint enforcesdomains/is byte-for-byte equal toprotocols/, which is the right shape today, but it's now permanent generated debt. Either declare a target version for removal or call it out instoryboards-vs-scenarios.mdxas permanent compatibility scaffolding so future contributors don't have to re-derive the rationale.normalizeThreeZeroIdempotencyDatesonly covers one of the dates in the storyboard.server/tests/manual/run-storyboards.ts:244-260rewrites2026-06-01and2026-06-30to 2099, but the currentstatic/compliance/source/universal/idempotency.yamlflight dates are2026-07-01..2026-09-30and2026-07-01..2026-07-31. Only the2026-09-30end-time branch matches today (2026-06-02). The2026-06-*branches are dead code; the July dates are unnormalized. The 3.0-compat run will start failing on past dates after 2026-07-01 unless the date table is updated to match the file actually in tree.
Minor nits (non-blocking)
yamlHasPhasesis a regex against raw text.scripts/lint-compliance-source-authority.cjs:103-105uses/^phases:\s*$/m— false-positives on a multi-line string containing exactly that line at column 0, false-negatives onphases: []inline. Ajs-yaml.load+Array.isArray(doc.phases)check is one line and removes the foot-gun.listYamlBasenamesaccepts.yml,generateIndexonly emits.yaml.scripts/lint-compliance-source-authority.cjs:159-165strips both extensions;scripts/build-compliance.cjsonly lists.yaml. A future.ymlunderuniversal/would triggerindex_membership_driftfor a reason that's actually two listing rules disagreeing inside this PR's own scripts. Pick one.SRCoverride inscripts/overlay-compliance-cache.shbypasses both cleanup and lint. Both therm -rfblock and the trailing lint invocation are gated toSRC == "dist/compliance/latest". A non-defaultSRCwould overlay onto the SDK cache with neither safety net. Either drop the override (the script is single-purpose) or run cleanup+lint unconditionally.
Safe to merge once the allowlist reason wording lands as a follow-up.
There was a problem hiding this comment.
Approving. New linter wires the source-of-truth boundary into both build and overlay, and the empty-allowlist test pattern keeps the suppressions honest.
Things I checked
scripts/lint-compliance-source-authority.cjs:75-95—compareDirectoriessha256s every file, catches drift either direction,walkFilesreturns sorted output. Violation order is deterministic.scripts/lint-compliance-source-authority.cjs:131-170—lintIndexMembershipbuilds expected fromlistDirs/listYamlBasenamesand actual fromindex.protocols[*].id. Latent divergence withdiscoverProtocolsnoted below.scripts/overlay-compliance-cache.sh:147-154—rm -rf $DST/<dir>is bounded by the[ -z "$DST" ] || [ ! -d "$DST" ]guard at lines 45-48;$DSTonly resolves undernode_modules/@adcp/sdk/compliance/cache/<version>. Safe.scripts/build-compliance.cjs:790-797—if (require.main === module) main()plus exportedbuildTo/generateIndexis the right shape for the test harness; no other in-repo callerrequires this file with side-effect expectations.server/src/training-agent/task-handlers.ts:2744—req.contextecho is conformant withstatic/schemas/source/media-buy/get-products-response.json:339-341(refscore/context.json). Not an undocumented runtime convention.- The
idempotency_keyallowlist removal inscripts/storyboard-context-output-paths-allowlist.jsonpairs withuniversal/idempotency.yaml'screate_media_buy_initialstep no longer capturing it. tests/lint-storyboard-context-output-paths.test.cjs:213-237+tests/lint-storyboard-validations-paths.test.cjs:414-438— the stale-allowlist test re-runs the lint with the allowlist disabled per entry. Right pattern; allowlists rot otherwise..changeset/5016-canonical-storyboard-source.mdispatch. Harness-additive per playbook §"Conformance harness". Sound.
Follow-ups (non-blocking — file as issues)
lintIndexMembershipshould mirrordiscoverProtocols' union.scripts/build-compliance.cjs:174-198synthesizes protocols fromlistDirs(protocols/) ∪ {s.protocol for s in specialisms}, butscripts/lint-compliance-source-authority.cjs:147only doeslistDirs. The first contributor who lands a specialism whoseprotocol:has no matchingprotocols/<id>/directory will trip a phantomindex_membership_drifton a green build. Today every specialism resolves cleanly (brand/creative/governance/media-buy/signals/sponsored-intelligence, all exist as dirs), so this is latent — but the linter shouldn't itself drift from the generator it's checking.yamlHasPhasesregex won't matchphases: []inline.scripts/lint-compliance-source-authority.cjs:99. Tighten/^phases:\s*$/mto/^phases\s*:/m.normalizeThreeZeroIdempotencyDatesis keyed to the 3.0 SDK cache dates, not the current source bundle.server/tests/manual/run-storyboards.ts:244-261. A one-line comment stating that pins it against a future source-date refresh silently no-opping.@adcp/sdkrelease cadence vs. spec-CI overlay window. Per the rollout doc, downstream consumers grade against the bundled snapshot until the next SDK ships. Worth a tracking issue on keeping that lag short for stricter storyboard changes —ad-tech-protocol-expertflagged this as the load-bearing hazard of the source-authority migration.
Minor nits (non-blocking)
req.context ? { context: req.context } : {}echoes{}for a truthy empty-object request.server/src/training-agent/task-handlers.ts:2744. Hold off tightening unless a storyboard actually exercises the empty case.- PR body overstates the dep change. "Pin the local runner dependency to
@adcp/sdk@8.1.0-beta.19" —package.jsononly adds the newtest:compliance-source-authorityscript; the SDK was already at^8.1.0-beta.19onmain. A drifted summary line, not a code issue.
Approving on the strength of the source-vs-mirror gate plus the stale-allowlist tests.
Summary
static/compliance/sourceas the repo-owned canonical source for Phase 3: move storyboard scenarios into spec repo as canonical contract #5016.@adcp/sdk@8.1.0-beta.19and add compatibility fixes for the stricter 3.1 RC generated types.get_products.cache_scope, wholesale feed unchanged probes,filters.is_fixed_price, and seededfilters.pricing_currenciesbehavior.Closes #5016.
Closes #5228.
Validation
npm run test:compliance-source-authoritybash scripts/overlay-compliance-cache.shnpm run test:storyboard-validations-pathsnpm run test:storyboard-context-output-pathsnpm run test:storyboard-sample-request-schemanpm run test:storyboard-response-schemanpm run test:storyboard-doc-paritynpm run test:storyboard-check-enumnpm run build:compliancenode scripts/lint-compliance-source-authority.cjs --dist-latestnpm run typechecktest:unit,test:test-dynamic-imports,test:callapi-state-change,typecheckbash scripts/run-storyboards-matrix.shStoryboard matrix:
Pre-push also passed version sync, storyboard matrix, schema-link convention, and Mintlify broken-links validation.