Skip to content

compliance(storyboards): schema-driven mutations + contributes migration#2676

Merged
bokelley merged 2 commits into
mainfrom
bokelley/schema-mutations
Apr 21, 2026
Merged

compliance(storyboards): schema-driven mutations + contributes migration#2676
bokelley merged 2 commits into
mainfrom
bokelley/schema-mutations

Conversation

@bokelley

Copy link
Copy Markdown
Contributor

Summary

Bundles two follow-ups from #2661:

  • #2669 — derive `MUTATING_TASKS` from request-schema `required: [idempotency_key]` declarations; replace the hardcoded list in `scripts/lint-storyboard-contradictions.cjs`.
  • #2662 — adopt the `contributes: true` shorthand (adcp-client#693, shipped in `@adcp/client` 5.8.1) on the two universal branch-set storyboards.

Schema-driven mutations

`loadMutatingTasksFromSchemas` walks `static/schemas/source/` for `*-request.json` files whose top-level `required` array lists `idempotency_key`. Mirrors the existing `loadMutatingSchemaRefs` pattern in `scripts/build-compliance.cjs`.

`MUTATING_EXCEPTIONS` retains two carve-outs, each documented in the target schema's description:

  • `comply_test_controller` — naturally idempotent (scenario enum is the dedup boundary).
  • `si_terminate_session` — naturally idempotent (session_id is the dedup boundary). Added after protocol expert review flagged the omission.

Before this PR, the hardcoded set was missing `sync_audiences` entirely. Schema-driven derivation catches it automatically.

Drift guards

Four new tests in `tests/lint-storyboard-contradictions.test.cjs`:

  1. Union invariant — `MUTATING_TASKS` equals `(schema-derived ∪ MUTATING_EXCEPTIONS)`. Breaks if someone adds to one side without the other.
  2. No exception redundancy — `MUTATING_EXCEPTIONS` ∩ schema-derived is empty. Prevents a task accidentally carrying both justifications.
  3. Positive anchor coverage — `create_media_buy`, `update_media_buy`, `sync_creatives`, `sync_audiences` MUST be in the derived set.
  4. Negative anchor coverage — `get_products`, `get_signals`, `list_creative_formats`, `get_adcp_capabilities` MUST NOT be in the derived set. Guards against a read-schema drifting to require idempotency_key.

Also hardened `loadMutatingTasksFromSchemas`: throws on hyphen-to-underscore collisions across subdirs (previously silent via Set dedup), and skips symlinked directories to prevent unbounded recursion.

contributes migration

Four call sites migrated from `contributes_to: ` to `contributes: true`:

  • `universal/schema-validation.yaml` — `past_start_reject_path` / `past_start_adjust_path`
  • `universal/security.yaml` — `probe_invalid_api_key` / `probe_invalid_oauth_token`

Storyboard doc (`universal/storyboard-schema.yaml`) updated: both forms are documented as semantically equivalent, with the shorthand preferred inside branch_set phases.

Test plan

  • `npm run test:storyboard-contradictions` — 19 tests pass (3 new drift guards + 1 negative anchor)
  • `npm run test:storyboard-branch-sets` — 14 tests unchanged, all pass
  • `npm run build:compliance` — all four storyboard lints pass
  • Precommit (`test:unit` + `typecheck`) — 631 tests pass
  • Storyboards run against `@adcp/client` 5.8.1 — 36/56 clean, 295 steps (above 35/279 floors)

Follow-ups

  • #2675 — introduce `x-mutates-state: true` schema annotation to eliminate the `MUTATING_EXCEPTIONS` list entirely; decouples "mutation semantics" from "idempotency mechanism."

🤖 Generated with Claude Code

bokelley and others added 2 commits April 21, 2026 13:47
Closes #2669 and #2662.

Schema-driven MUTATING_TASKS (#2669):
- Derive the set at lint-time from every static/schemas/source/*-request.json
  that lists idempotency_key in required. Mirrors build-compliance.cjs's
  loadMutatingSchemaRefs pattern.
- MUTATING_EXCEPTIONS preserves one carve-out: comply_test_controller is
  naturally idempotent (its schema description justifies the absence of
  idempotency_key) but still mutates controller state the next step
  observes.
- sync_audiences was absent from the hardcoded set and is now covered.
- Three drift-guard tests: union invariant, no exception redundancy,
  anchor-task coverage.

contributes: true migration (#2662):
- Adopt the @adcp/client 5.8.1 boolean shorthand on four contributing
  steps inside branch_set phases:
    schema-validation.yaml  past_start_reject_path / past_start_adjust_path
    security.yaml           probe_invalid_api_key / probe_invalid_oauth_token
- Storyboards pass 36/56 clean, 295 steps against 5.8.1 — both above CI
  regression floors (35/279).
- Schema doc's guidance updated to name both forms as semantically
  equivalent, with the shorthand preferred inside branch_set phases.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Protocol expert caught: si_terminate_session is missing from
MUTATING_EXCEPTIONS. Same shape as comply_test_controller — naturally
idempotent (session_id is the dedup boundary) but does mutate session
state a later si_send_message asserts against (SESSION_TERMINATED).
Without it, contradiction lint can't discriminate pre- vs post-
termination state paths.

Code review should-fixes:
- loadMutatingTasksFromSchemas collects into a Map<task, srcPath> and
  throws on hyphen-to-underscore collisions across subdirs (would
  previously dedupe silently via Set.add).
- walk() skips symlinked directories to prevent unbounded recursion.
- Added negative anchor test: get_products / get_signals /
  list_creative_formats / get_adcp_capabilities MUST NOT be in the
  schema-derived set. Guards against a read schema accidentally listing
  idempotency_key in required.
- Softened storyboard-schema doc: no longer names which storyboards use
  the shorthand (goes stale), points at the lint as the authority.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley merged commit 70b38b0 into main Apr 21, 2026
14 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.

1 participant