Skip to content

feat(compliance): storyboard scoping lint + sessionKeyFromArgs plans fallback (#2527, #2529, #2531)#2563

Merged
bokelley merged 8 commits into
mainfrom
bokelley/storyboard-scoping-lint
Apr 20, 2026
Merged

feat(compliance): storyboard scoping lint + sessionKeyFromArgs plans fallback (#2527, #2529, #2531)#2563
bokelley merged 8 commits into
mainfrom
bokelley/storyboard-scoping-lint

Conversation

@bokelley

Copy link
Copy Markdown
Contributor

Summary

Three guardrails against the class of bug #2236/#2526 exposed — steps that silently write to open:default because sample_request lacks brand/account identity.

Stacked on #2562 (mechanical storyboard cleanup). Merge after that.

#2527 — Scoping lint

scripts/lint-storyboard-scoping.cjs, wired into npm run build:compliance. TENANT_SCOPED_TASKS enumerates every tool whose training-agent handler calls getSession(sessionKeyFromArgs(...)). Each step invoking one must carry one of:

  • sample_request.account.account_id
  • sample_request.account.brand.domain
  • sample_request.brand.domain
  • sample_request.plans[0].brand.domain (sync_plans only)

Per-step opt-out via scoping: global for intentionally cross-tenant probes. Fixed 26 pre-existing violations across 10 storyboards by adding brand: { domain } derived from each storyboard's prerequisites.test_kit.

#2529 — Handler-dispatch parity test

tests/lint-storyboard-scoping.test.cjs parses HANDLER_MAP from server/src/training-agent/task-handlers.ts and asserts every registered task is classified in exactly one of TENANT_SCOPED_TASKS / EXEMPT_FROM_LINT. Also catches stale entries after a rename. Added as npm run test:storyboard-scoping and wired into the top-level npm run test chain.

Prevents silent drift: without this, someone adds get_new_thing to the dispatch table, forgets to classify it, and the lint never sees it.

#2531 — sessionKeyFromArgs plans[0] fallback

sessionKeyFromArgs now falls back to plans[0].brand.domain before open:default. sync_plans calls carry brand identity inside the plans array, not at the envelope level — without this fallback, governance storyboards that only identify the tenant via plans[*].brand.domain land in open:default even though the lint accepts that shape as valid.

Tests added for:

  • plans fallback happy path
  • top-level brand still wins over plans[0].brand.domain
  • malformed / empty plans still route to open:default

Authoring guide

docs/contributing/storyboard-authoring.md documents valid identity shapes, the scoping: global opt-out, which tasks are tenant-scoped vs exempt, and how to run the lint locally.

Closes

Test plan

  • npm run build:compliance → scoping lint passes + clean dist build
  • npm run test:schemas (7/7) and npm run test:examples (31/31)
  • npm run test:storyboard-scoping (3/3)
  • npx vitest run server/tests/unit/training-agent.test.ts (326/326, including 4 new plans-fallback cases)
  • npm run typecheck clean

Stacking

PR A #2562 (mechanical cleanup) → this PR (lint + fallback).

🤖 Generated with Claude Code

…2528, #2530, #2533)

Mechanical storyboard-consistency pass across static/compliance/source/:

- #2530: Normalize to RFC 2606 .example TLD. acmeoutdoor.com →
  acmeoutdoor.example, amsterdam-steakhouse.com → .example,
  pinnacle-agency.com → .example, and fix the acme-outdoor.example.com
  typo in creative-template and creative-generative.

- #2533: Fix operator = brand.domain in property-lists and
  collection-lists. These specialisms are meant to demonstrate an agency
  operating on behalf of a brand, not the brand operating directly.
  Switch to pinnacle-agency.example to match the rest of the buyer-side
  storyboards.

- #2528: Standardize buyer-side storyboards on account { brand, operator }.
  When a step already had an account block, drop the redundant top-level
  brand. Otherwise, promote top-level brand into the account wrapper with
  operator: "pinnacle-agency.example".

No runtime behavior change: both shapes resolve to the same session key
via sessionKeyFromArgs. Fresh build:compliance and test:schemas pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

Schema Link Check Results

Commit: 0079826 - Merge remote-tracking branch 'origin/main' into bokelley/storyboard-scoping-lint

⚠️ Warnings (schema not yet released)

These schemas exist in source but haven't been released yet. The links will be broken until the next version is published:

  • https://adcontextprotocol.org/schemas/v3/enums/specialism.json
    • Schema exists in latest (source) but not yet released in v3
    • Action: This link will work after next 3.x release is published

To fix: Either:

  1. Wait for the next release and merge this PR after the release is published
  2. Use latest instead of a version alias if you need the link to work immediately (note: latest is the development version and may change)
  3. Coordinate with maintainers to cut a new release before merging

bokelley added a commit that referenced this pull request Apr 20, 2026
…view: H1, should-fix)

Review feedback from ad-tech-protocol-expert and code-reviewer on PR #2563:

H1 (blocking): sync-plans-request.json declares plan items with
`required: [plan_id, brand, ...]` and `additionalProperties: false`.
My amendment mis-wrapped plan identity in `account { brand, operator }`,
which AJV rejects. Revert to `brand: { domain }` at the plan-item level
across five storyboards (governance, governance-spend-authority × 2,
governance-delivery-monitor, brand-rights/governance_denied). Strip the
`plans[0].account.brand.domain` shape from the lint, the runtime
sessionKeyFromArgs fallback, tests, and the authoring guide so no-one
gets misled again.

Should-fix (parity test robustness):
- Strip `// comments` before matching handler-map entries so a `// TODO`
  beside a handler line doesn't silently drop the task from lint coverage.
- Drop the end-of-line anchor so an inline type annotation after the
  handler name doesn't fail the match.
- Add a canary: assert the parser captured ≥30 tasks. If the regex ever
  chops the body (e.g. future nested braces), the canary fires before
  the two downstream assertions pass vacuously.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley and others added 5 commits April 20, 2026 12:22
…create_media_buy brand (#2528, #2530)

Review findings from ad-tech-protocol-expert on #2562:

- #2530 follow-up: sales-broadcast-tv/index.yaml missed the initial TLD
  sweep. Normalize `novamotors.com → .example` (6 sites) and
  `videoamp.com → .example` (2 sites) so spec examples don't resolve to
  real third-party domains.

- sales-social/index.yaml: list_accounts had an injected
  `account { brand, operator }` block, but the list-accounts-request
  schema has no `account` field (listing all accounts for the
  authenticated caller). Drop the identity block; keep only the context
  wrapper. additionalProperties: true would silently accept it, but the
  semantics were misleading to spec readers.

- #2528 follow-up (H2): create-media-buy-request schema requires BOTH
  top-level `brand` and `account`. The initial standardization to
  `account { brand, operator }` dropped top-level brand as "redundant";
  it isn't — `brand` and `account` are distinct schema fields. Restore
  top-level `brand: { domain }` alongside the existing account block on
  20 create_media_buy steps so the requests validate against the
  published schema.

build:compliance and test:schemas pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…fallback (#2527, #2529, #2531)

Three guardrails against the class of bug #2236/#2526 exposed — steps that
silently write to open:default because sample_request lacks brand/account.

- #2527: scripts/lint-storyboard-scoping.cjs, wired into
  npm run build:compliance. TENANT_SCOPED_TASKS enumerates every tool that
  calls getSession(sessionKeyFromArgs(...)). Each step invoking one must
  carry sample_request.{account.account_id | account.brand.domain |
  brand.domain | plans[0].brand.domain (sync_plans)}. Per-step opt-out via
  `scoping: global` for intentionally cross-tenant probes. Fixed 26
  pre-existing violations across 10 storyboards.

- #2529: tests/lint-storyboard-scoping.test.cjs parity guard. Parses
  HANDLER_MAP from server/src/training-agent/task-handlers.ts and asserts
  every registered task is classified in exactly one of
  TENANT_SCOPED_TASKS / EXEMPT_FROM_LINT. Also catches stale entries in
  those sets after a rename. Added as npm run test:storyboard-scoping.

- #2531: sessionKeyFromArgs falls back to plans[0].brand.domain before
  open:default. sync_plans carries brand identity inside plans[], not at
  the envelope level — without this fallback, governance storyboards that
  only identify the tenant via plans land in the wrong session. Matches
  what the scoping lint already accepts as a valid identity shape.

- Authoring guide: docs/contributing/storyboard-authoring.md documents
  valid identity shapes, scoping: global opt-out, and local lint usage.

build:compliance, test:schemas, typecheck, and training-agent.test.ts
all pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…d, operator }

Follow-up self-review: the initial scoping-lint fix injected top-level
brand.domain into 47 steps. For create_media_buy/update_media_buy/
sync_creatives that is schema-invalid — AccountRef requires operator
whenever the natural-key (brand) form is used, so the canonical identity
shape is account { brand, operator }. Top-level brand on those tasks is
either a separate schema field (the campaign's brand on create_media_buy)
or not a schema field at all.

Changes:

- Upgrade existing brand-only injections to account { brand, operator }
  across 18 storyboards. Where a step already had an account block, drop
  the redundant brand. Keep top-level brand only where the storyboard
  legitimately expresses the campaign brand (separate from identity).

- Lint: also accept plans[0].account.brand.domain (canonical shape for
  sync_plans inside the plans array) in addition to plans[0].brand.domain.

- Runtime (sessionKeyFromArgs): mirror the lint — check
  plans[0].account.brand.domain before plans[0].brand.domain. New test
  coverage added.

- Authoring guide: stop documenting top-level brand as "shorthand
  identity." Document account { brand, operator } as canonical, with a
  clear note that top-level brand is a training-agent routing fallback,
  not a spec-sanctioned shape.

- Lint error message: recommend the account shape directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…view: H1, should-fix)

Review feedback from ad-tech-protocol-expert and code-reviewer on PR #2563:

H1 (blocking): sync-plans-request.json declares plan items with
`required: [plan_id, brand, ...]` and `additionalProperties: false`.
My amendment mis-wrapped plan identity in `account { brand, operator }`,
which AJV rejects. Revert to `brand: { domain }` at the plan-item level
across five storyboards (governance, governance-spend-authority × 2,
governance-delivery-monitor, brand-rights/governance_denied). Strip the
`plans[0].account.brand.domain` shape from the lint, the runtime
sessionKeyFromArgs fallback, tests, and the authoring guide so no-one
gets misled again.

Should-fix (parity test robustness):
- Strip `// comments` before matching handler-map entries so a `// TODO`
  beside a handler line doesn't silently drop the task from lint coverage.
- Drop the end-of-line anchor so an inline type annotation after the
  handler name doesn't fail the match.
- Add a canary: assert the parser captured ≥30 tasks. If the regex ever
  chops the body (e.g. future nested braces), the canary fires before
  the two downstream assertions pass vacuously.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tions (review: H2)

Review feedback from ad-tech-protocol-expert on #2563:

The scoping-lint fix added `account { brand, operator }` to 11 create_media_buy
steps that lacked any identity, but the create-media-buy-request schema
requires BOTH `brand` (top-level) and `account`. Adding only `account`
makes the probes fail on missing-brand validation before exercising the
error path they're meant to test (negative budget, reversed dates, etc.).

Add top-level `brand: { domain }` alongside the injected account on:
- universal/error-compliance.yaml (5 steps)
- universal/schema-validation.yaml (3 steps)
- protocols/media-buy/state-machine.yaml (1)
- protocols/governance/index.yaml (1)
- specialisms/governance-spend-authority/index.yaml (1)

The companion top-level-brand restoration for PR A's create_media_buy
steps landed in #2562.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley force-pushed the bokelley/storyboard-scoping-lint branch from d058182 to 68a7297 Compare April 20, 2026 16:25
bokelley and others added 2 commits April 20, 2026 13:45
check-seo CI requires description + og:title on every doc page.
Match the sibling testable-*.md frontmatter style.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…coping-lint

# Conflicts:
#	static/compliance/source/protocols/governance/index.yaml
#	static/compliance/source/protocols/media-buy/state-machine.yaml
#	static/compliance/source/specialisms/brand-rights/scenarios/governance_denied.yaml
#	static/compliance/source/specialisms/governance-spend-authority/index.yaml
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