feat(bdd): scenario→test linkage runner + CI gate (soc-63xfx #scenario-test-runner) (#503)
## What
Mechanizes the scenario→test linkage gap. 71 `.feature` files existed
under `skills/*/references/` but only a handful had an executing test;
the rest were unverified documentation. This adds a linkage **convention
+ checker + blocking CI gate** — a linkage check, not a full Gherkin
step-interpreter (that scope was deliberately avoided per the bead's
pragmatic-scope guidance).
## Convention
`@covered-by:<test-path>[::<Name>]` Gherkin tag placed directly above a
`Scenario:` (or above `Feature:` for file-wide coverage). Lightest
convention that resolves "scenario X is covered by test Y" without
maintaining a separate registry. The `::<Name>` form additionally
asserts a named test (Go func / bats `@test` / bash func) exists in the
file.
## Deliverables
- **`scripts/check-scenario-test-linkage.sh`** — parses every `.feature`
scenario, resolves `@covered-by` targets, and FAILS on:
unlinked-and-unallowlisted scenarios, dangling links (missing path or
named test), allowlist/tag conflicts, and stale allowlist entries.
Flags: `--warn-only`, `--json`, `--help`. Exit 0/1/2.
- **`scripts/.scenario-linkage-allow`** — 68 doc-only feature files, a
*draining backlog* (drain as skills gain executing tests). The gate is
real: a scenario not here and not tagged fails CI.
- **Starter set** — `rpi`, `goals`, `scenario` features fully linked (11
scenarios) to their `tests/e2e/*` tests, proving the mechanism end to
end.
- **`validate.yml`** — `validate-scenario-test-linkage` T1 blocking job
+ summary `needs` + summary echo; manifest entry in
`docs/contracts/ci-jobs.yaml` + regenerated `AGENTS-CI.md` table (parity
gate green).
- **`docs/contracts/scenario-test-linkage.md`** +
`documentation-index.md` catalog entry.
- **`tests/scripts/check-scenario-test-linkage.bats`** — 13 regression
tests (pass/fail/warn/json/file-level-tag/misuse).
Sibling to `executable-spec-link-integrity` (directive→scenario).
Together they close the GOALS-directive → scenario → test evidence
chain.
## Verification (all green)
- `shellcheck scripts/check-scenario-test-linkage.sh` — clean
- gate run locally — PASS (256 scenarios, 71 files; 11 linked, 245
allowlisted)
- 5 failure paths manually exercised → exit 1; `--warn-only` → exit 0;
misuse → exit 2
- `bats tests/scripts/check-scenario-test-linkage.bats` — 13/13 ok
- `check-contracts-structural-floor.sh` — PASS (new contract catalogued)
- `validate-ci-policy-parity.sh` — PASS (manifest + AGENTS table synced)
- `validate.yml` + `ci-jobs.yaml` YAML parse OK
- diff stat: +639/−2, all-additions, no collateral data loss
Closes-scenario: soc-63xfx#scenario-test-runner
Bounded-context: BC4-Evidence
Evidence: scripts/check-scenario-test-linkage.sh
2026-05-23 23:03:01 -04:00
# Scenario → Test Linkage Contract
Status: active · Bead: soc-63xfx · Gate: `validate-scenario-test-linkage` (T1, blocking)
## Why
71 `.feature` files live under `skills/*/references/` . Historically only a
handful had an executing test; the rest were documentation that drifted free of
any check. A scenario nobody runs is a claim nobody verifies.
This contract makes the scenario→test relationship **mechanical ** : every Gherkin
`Scenario:` in the corpus must either name the test that covers it, or its
feature file must be explicitly declared documentation-only. The gate fails on
any scenario that is neither.
It is the sibling of `executable-spec-link-integrity` (soc-58nt / soc-m8tdn),
which links **GOALS directives → scenarios ** . This one links **scenarios →
tests**. Together they close the spec-to-evidence chain:
```
GOALS directive ──► scenario (.feature) ──► test (executing)
(exec-spec-link-integrity) (scenario-test-linkage)
```
## The convention
Tag a scenario with the test that covers it using a Gherkin tag:
``` gherkin
@covered-by:tests/e2e/rpi-phased-domain.sh
Scenario : Phases run in order and never compress
When / r p i e x e c u t e s
Then i t r u n s R e s e a r c h , t h e n P l a n , t h e n I m p l e m e n t i n o r d e r
```
Rules:
- The tag goes on its **own line directly above ** the `Scenario:` it covers —
the standard Gherkin tag position. A non-tag line (a step, `Background` , a
blank then content) between the tag and the scenario breaks the association.
- **File-level tags** (placed above the `Feature:` line) apply to **every **
scenario in the file.
- Multiple tags may stack; a scenario passes only if **all ** of its
`@covered-by:` targets resolve.
- Two target forms:
- `@covered-by:<test-path>` — the test file must exist (repo-relative path).
- `@covered-by:<test-path>::<Name>` — additionally, `<Name>` (a Go func, a
bats `@test` label, a bash function, etc.) must appear in that file. The
name match is a substring check, which keeps the gate language-agnostic
while still catching a renamed or deleted test.
`<test-path>` is any executing test surface: `tests/e2e/*.sh` ,
`tests/scripts/*.bats` , `cli/**/*_test.go` , and so on. The gate only asserts the
target **exists ** — it does not run it. Running is the test suite's job; linking
is this gate's job.
## The allowlist
`scripts/.scenario-linkage-allow` lists feature files (repo-relative, one per
line, `#` comments allowed) that are intentionally documentation-only for now.
The gate skips scenarios in those files.
The allowlist is a **draining backlog ** , not a parking lot:
- When a skill gains an executing test, tag its scenarios with `@covered-by:`
and **remove the file from the allowlist ** .
- A file may **not ** be both allowlisted **and ** carry `@covered-by:` tags —
that ambiguous intent is a gate error. Pick one.
- A stale allowlist entry (a listed file that no longer exists) is a gate error.
- Do not add new feature files to the allowlist without a tracking bead; new
specs should ship with their covering test.
## Gate behavior
`scripts/check-scenario-test-linkage.sh` :
| Condition | Result |
|---|---|
| Scenario has a resolving `@covered-by:` tag | pass |
| Scenario's feature file is allowlisted | pass (counted as doc-only) |
| Scenario has no tag and file is not allowlisted | **FAIL ** |
| `@covered-by:` path does not exist | **FAIL ** (dangling) |
| `@covered-by:...::Name` not found in the file | **FAIL ** (dangling) |
| Allowlisted file also carries a `@covered-by:` tag | **FAIL ** (ambiguous) |
| Allowlist entry points at a deleted file | **FAIL ** (stale) |
Flags: `--warn-only` (advisory, exit 0), `--json` (machine-readable summary),
`-h` /`--help` . Exit codes: `0` pass, `1` fail, `2` misuse.
Wired into `.github/workflows/validate.yml` as `validate-scenario-test-linkage` ,
gated on changes to `skills/**` , `**/*.sh` , or `.github/**` .
## Starter set (at landing, soc-63xfx)
3 of 71 feature files are fully linked to executing tests:
| Feature | Covering test |
|---|---|
| `skills/rpi/references/rpi.feature` | `tests/e2e/rpi-phased-domain.sh` |
Structural: goals→fitness rename with compatibility alias; shared retired to a declared contract owner (#1007)
## Summary
The two structural beads deferred from the skill-overhaul reboot waves.
**goals → fitness** (`age-skill-overhaul-reboot-sjv7v.10`, 07-24 plan
D9)
- The semantic skill is now `fitness`; the `ao goals` CLI command family
deliberately keeps its name.
- `skills/goals` remains a thin, non-advertised compatibility alias:
user-invocable, zero capabilities/effects/dependencies, an `alias-of`
context_rel edge to fitness, physical deletion only under the
observed-zero policy.
- `alias-of` added to the frontmatter schema enums (the catalog schema's
own description already listed it as valid relationship vocabulary; the
enum lagged).
- bootstrap's `goals` consumer edge migrated; the stale `goals.feature`
row in the scenario-linkage contract corrected (the feature file was
already absent).
**shared retirement** (`age-skill-overhaul-reboot-sjv7v.11`, 07-24 plan
D9)
- The runtime-neutrality contract moved to its declared owner:
`docs/contracts/runtime-neutrality.md`, scope unchanged (shared
references a consuming skill loads).
- The last consumer edge (`bootstrap consumes: shared`) removed;
`skills/shared` reduced to a non-routable tombstone pending the
observed-zero deletion window; the ports-and-adapters contract updated
to executed state.
## Validation
- Strict frontmatter 50/50 (fitness + alias); regen byte-current;
scenario-linkage PASS; liveness/anti-spiral/desc-budget bats green;
`cli/internal/skills` Go tests pass
- Cross-family review, two rounds: round 1 — one finding fixed
(unintended contract-scope broadening reverted), one rejected with
evidence (the reviewer inferred a pre-existing feature file from a stale
doc row; the pre-change tree had none); round 2 delta **VERDICT: PASS**
Tracker: `age-skill-overhaul-reboot-sjv7v.10`, `.11`
2026-07-29 01:03:12 -04:00
| ~~`skills/goals/references/goals.feature`~~ (stale row: the feature file was already absent from the tree before 2026-07-29; the skill is now `fitness` ) | `tests/e2e/goals-measure-scenarios.sh` , `goals-trace-chain.sh` still execute against the `ao goals` CLI |
2026-06-26 22:16:42 -04:00
| ~~`skills/scenario/references/scenario.feature`~~ (removed: `scenario` folded into `eval-outcomes` , 2026-06-12) | `tests/e2e/goals-scenarios-link.sh` |
feat(bdd): scenario→test linkage runner + CI gate (soc-63xfx #scenario-test-runner) (#503)
## What
Mechanizes the scenario→test linkage gap. 71 `.feature` files existed
under `skills/*/references/` but only a handful had an executing test;
the rest were unverified documentation. This adds a linkage **convention
+ checker + blocking CI gate** — a linkage check, not a full Gherkin
step-interpreter (that scope was deliberately avoided per the bead's
pragmatic-scope guidance).
## Convention
`@covered-by:<test-path>[::<Name>]` Gherkin tag placed directly above a
`Scenario:` (or above `Feature:` for file-wide coverage). Lightest
convention that resolves "scenario X is covered by test Y" without
maintaining a separate registry. The `::<Name>` form additionally
asserts a named test (Go func / bats `@test` / bash func) exists in the
file.
## Deliverables
- **`scripts/check-scenario-test-linkage.sh`** — parses every `.feature`
scenario, resolves `@covered-by` targets, and FAILS on:
unlinked-and-unallowlisted scenarios, dangling links (missing path or
named test), allowlist/tag conflicts, and stale allowlist entries.
Flags: `--warn-only`, `--json`, `--help`. Exit 0/1/2.
- **`scripts/.scenario-linkage-allow`** — 68 doc-only feature files, a
*draining backlog* (drain as skills gain executing tests). The gate is
real: a scenario not here and not tagged fails CI.
- **Starter set** — `rpi`, `goals`, `scenario` features fully linked (11
scenarios) to their `tests/e2e/*` tests, proving the mechanism end to
end.
- **`validate.yml`** — `validate-scenario-test-linkage` T1 blocking job
+ summary `needs` + summary echo; manifest entry in
`docs/contracts/ci-jobs.yaml` + regenerated `AGENTS-CI.md` table (parity
gate green).
- **`docs/contracts/scenario-test-linkage.md`** +
`documentation-index.md` catalog entry.
- **`tests/scripts/check-scenario-test-linkage.bats`** — 13 regression
tests (pass/fail/warn/json/file-level-tag/misuse).
Sibling to `executable-spec-link-integrity` (directive→scenario).
Together they close the GOALS-directive → scenario → test evidence
chain.
## Verification (all green)
- `shellcheck scripts/check-scenario-test-linkage.sh` — clean
- gate run locally — PASS (256 scenarios, 71 files; 11 linked, 245
allowlisted)
- 5 failure paths manually exercised → exit 1; `--warn-only` → exit 0;
misuse → exit 2
- `bats tests/scripts/check-scenario-test-linkage.bats` — 13/13 ok
- `check-contracts-structural-floor.sh` — PASS (new contract catalogued)
- `validate-ci-policy-parity.sh` — PASS (manifest + AGENTS table synced)
- `validate.yml` + `ci-jobs.yaml` YAML parse OK
- diff stat: +639/−2, all-additions, no collateral data loss
Closes-scenario: soc-63xfx#scenario-test-runner
Bounded-context: BC4-Evidence
Evidence: scripts/check-scenario-test-linkage.sh
2026-05-23 23:03:01 -04:00
The remaining 68 files are allowlisted in `scripts/.scenario-linkage-allow` with
the standing goal to drain the list as skills gain executing tests.