--- name: verify-scenarios description: "Partial Skill: invoke by name only — the Gherkin-scenario-to-test-report bridge verifier — invoked by the impl-judge at the impl gate, not triggered by users directly." user-invocable: false metadata: internal: true --- # Verify Scenarios The concrete engine for the SDD **scenario-bridge** — a language/runner-agnostic bridge from a frozen `.feature` scenario to the test that proves it, so the impl-judge runs the project's own test suite and reads a report instead of re-verifying every scenario by hand. Deterministic test-running is the SDD **default** verification path (not a plugin specialty — an unmatched artifact-type already falls through to SDD defaults). ## Binding convention - **Key = the scenario's `@id:` tag if present, else its verbatim name.** No synthetic ID registry — node-path + name is globally unique in the repo. - **A test declares its node** with a `describe('spec:', …)` wrapper (or the equivalent in a non-JS runner) around its cases. The node segment is found at **any depth** in the test-report's `" > "`-joined name, so nesting the wrapper deeper never breaks the bind. - **A test's leaf title is the exact scenario name**, pasted verbatim from the frozen `.feature` — or `@id:` when the scenario carries that tag. - **A Scenario Outline is ONE key** (its outline name). Give an `it.each`/table-driven leaf a **static** title equal to the outline name (no per-row interpolation) — every row folds into the same key; a failing row fails the whole key. - **Many-to-one is fine.** Two tests can bind the same key; the fold is PASS only if none of them fail. A test that maps to an already-covered key and isn't the canonical rename shows up as an EXTRA (diagnostic, not a failure) — leave it. - **A punctuation-only near-miss still binds.** When no result matches a key exactly, the fold retries on a comparison key that folds curly quotes/apostrophes, dashes, and the ellipsis to their ASCII forms and collapses whitespace — so a pasted `’` for a `'` does not land the same scenario in BOTH the UNBOUND list and the EXTRA list with nothing linking them. The bind is reported as a **PROBABLE TITLE MISMATCH** naming both verbatim titles, so the typo still gets fixed. Nothing rewrites a title, an **exact** match always wins, case is **not** folded, and an **ambiguous** fold (two candidates folding alike) stays UNBOUND rather than binding the wrong one. ## Config schema `.agents/sdd/scenario-bridge.toml`, resolved beneath `--root` (**not** a single repo-root path — see "Monorepo rooting" below) — an array-of-tables of result **sources** (a `.feature` is not assumed to map to one runner: vitest + playwright + pytest can all contribute to one node): ```toml [[source]] adapter = "junit" command = "pnpm build && vitest run src --reporter=junit --outputFile=.agents/.scenario-report.xml" reportPath = ".agents/.scenario-report.xml" ``` - `adapter` — `junit` today; `tap`/`aced` are a `switch` case away, same interface. - `command` — optional; run only with `--run` (omit to just read an already-produced report). - `reportPath` — resolved relative to `--root`. Add `.agents/.scenario-report.xml` (or wherever `reportPath` points) to the project's `.gitignore`. ## JUnit adapter mechanics Hand-rolled regex parse, no xml dependency — verified against vitest 4.1.7's JUnit reporter: - One `` per test file; `classname` = file path; `name` = describe-chain joined by `" > "` + leaf title. - `classname`/`name` are extracted **by attribute name**, not position, and both **unescaped** (`& " ' < >`) — an unescaped apostrophe silently drops a scenario like `whoami prints this session's own identity`. - Outcome: a child ` fail; a child ` skip; otherwise pass. - The `name` is split on `" > "`; the **node** is the capture of whichever segment matches `/^spec:(.+)$/` (any depth); a testcase with no such segment is dropped — it is not bound to any node. The **leaf** is the last segment; the **key** is its `@id:` capture if it matches, else the leaf verbatim. ## Run ```bash node "/scripts/verify-scenarios.mjs" \ --feature --node / \ [--config .agents/sdd/scenario-bridge.toml] [--root .] [--feature-root ] \ [--report ] [--run] [--format toon|json] ``` - `--report ` bypasses `--config` entirely — a single ad-hoc junit source, no command. - `--run` executes each source's `command` first; without it, existing reports are read as-is. - Default output is a readable per-scenario table + a `N/M BOUND, P pass, F fail, U unbound` summary line + any EXTRA keys + any PROBABLE TITLE MISMATCH pairs. `--format json` emits `{node,total,bound,pass,fail,unbound,scenarios[],extras[],mismatches[]}`. `--format toon` emits the repo's TOON tabular form. - Exit code is non-zero when any scenario is UNBOUND or FAIL; zero only at full BOUND+PASS. ## Monorepo rooting — `--feature-root` vs. `--root` `--root` is the **bridge/report root** — where `--config` defaults to, and where every source's `reportPath` and an ad-hoc `--report` resolve. `--feature-root` is where `--feature` resolves; it **defaults to `--root`** when omitted, so a colocated project (spec, config, and report all under one root) needs only `--root` — unchanged from before this option existed. Pass `--feature-root` separately when the frozen `.feature` lives at a **different** root than the project's config + report — the common monorepo shape, where specs sit at a repo-root spec corpus (`.agents/specs//`) but the project's own `.agents/sdd/scenario-bridge.toml` and test report sit under its `project-path` (e.g. `packages//`): ```bash node "/scripts/verify-scenarios.mjs" \ --feature .agents/specs/cyberlegion-plugin/identity/identity.feature \ --node cyberlegion-plugin/identity \ --feature-root . \ --root packages/cyberlegion ``` ## Boundaries Read-only over the `.feature` and test reports; writes nothing. Consumes `gherkin-cli` (via `npx gherkin-cli parse --format json`) for the scenario set — never re-implements a Gherkin parser. Does not reorganize `.feature` files by runner and does not wire itself into the impl-judge (a separate CR grows the default `sdd-impl-judge` to call this for deterministic artifact-types, run through SDD's own spec gate).