Story S5-03 — vuln-remediation 5 RAG-corpus-derived cases¶
Step: Step 5 — Backfill bench/vuln-remediation/ with ≥10 cases + rubric + taxonomies
Status: HARDENED (phase-story-validator, 2026-06-05)
Effort: M
Depends on: S5-02 HARDENED (rubric exists and is testable end-to-end; without it the cases are inert), S2-02 HARDENED (load_cases walks bench/{tc}/cases/*, BLAKE3-verifies each case against cases/digests.yaml, raises typed errors; the canonical case-dir digest algorithm at S2-02 §AC-3 is the only algorithm allowed for case_digest), S1-02 HARDENED (BenchCase Pydantic wire-type shapes — cassette_canary_pin: str (32 hex), case_digest: str (blake3:<64 hex>), disposition: Literal["positive","negative","ambiguous"], difficulty: Literal["easy","medium","hard"], source: Literal["curated","outcome-ledger-derived","regression-converted"], curation_class: Literal["rag-corpus-derived","held-out"], tz-aware UTC added_at/last_validated_at), S5-01 HARDENED (bench/vuln-remediation/registration.py ships register_task_class("vuln-remediation", ...) so load_task_class("vuln-remediation", bench_root="bench") resolves)
ADRs honored: ADR-0005 (cassette_canary_pin is 32 hex; pin is identity not content; case_digest excludes case.toml — Phase 4 cassettes pre-date the Canary.mint(seed=...) amendment, so the deterministic-derivation path is the always-path and is pinned in AC-3a), ADR-0006 (these 5 cases are curation_class="rag-corpus-derived"; they verify the recipe/RAG pipeline doesn't regress against the corpus it was tuned on; they are not sufficient evidence for silver-tier promotion on their own — that's the held-out 5 from S5-04), Phase 0 ADR-0001 (BLAKE3 hashing chokepoint — for src/codegenie/**/*.py; bench-curation scripts under bench/ and scripts/ are exempt because they live outside the policed runtime closure)
Validation notes¶
Validated: 2026-06-05
Verdict: HARDENED
Findings addressed: 26 total — 6 block, 14 harden, 6 nit
Critic reports: Consistency (9), Coverage (11), Test-Quality (10), Design-Patterns (4). No NEEDS RESEARCH — every pattern is precedented in this repo (S2-02 HARDENED's canonical digest composition, ADR-0005 §Consequences "case_digest excludes case.toml" + pre-amendment fact pattern, S5-01 HARDENED's _HERE-style module-local constants, structlog.testing.capture_logs at tests/unit/parsers/test_safe_yaml.py:32).
Conflict resolutions (priority: Consistency > Coverage > Test-Quality > Design-Patterns):
- B-DIGEST-ALGORITHM (F-CON-1 — BLOCK). Consistency wins. The original §Implementation outline §3 prescribed a digest algorithm that walks
(case_dir/"input").rglob("*")+(case_dir/"expected").rglob("*")separately, sorts bystr(p)(absolute path → cross-machine non-determinism), does not excludecase.toml, follows symlinks, and frames bytes as(relpath || filebytes)per file. The HARDENED S2-02 §AC-3 canonical algorithm walkscase_dir.rglob("*")once, sorts byp.relative_to(case_dir).as_posix(), excludescase.toml(per ADR-0005 §Consequences), rejects symlinks (S2-02 §AC-9), and frames bytes as recordsf"{rel_posix}\x1f{content_hash(p)}".encode()joined by\x1eand BLAKE3'd once. Every byte the story-prescribed algorithm produces would mismatch what the loader computes. §Implementation outline §3 now cites S2-02 §AC-3 verbatim and the integration test imports_compute_case_dir_digestfrom the loader (private helper; thefrom codegenie.eval.loader import _compute_case_dir_digestform is the contract; the helper graduates to a publiccodegenie.eval.digests.compute_case_dir_digestwhen the third non-loader consumer lands per F-DP-1). - B-INPUT-POINTER-DROPPED (F-CON-2 — BLOCK). Consistency wins. The original AC-2 left an
input-pointer.tomlescape hatch ("if pointing into tests/cassettes/phase4/"); the loader does not support pointers — S2-02 §AC-5 raisesBenchCaseLoadError(field="input", reason="input/ directory not found")if(case_dir / "input").is_dir()is False, andinput-pointer.tomlis a file, not a directory. Dropped the escape hatch;input/MUST be a real, populated directory. If a curator's pre-fix snapshot is genuinely large, the resolution is "commit a minimal extracted snapshot covering only the files the recipe needs to read/touch" — defer pointer support to a future story if portfolio scale forces it. - B-CANARY-DERIVATION (F-CON-3 — BLOCK). Consistency wins. The original §Implementation outline §3 said "
cassette_canary_pinextracted from the cassette's canary metadata" — but ADR-0005 itself names ADR-P4-006 (the Phase 4 amendment) as shipping with Phase 6.5 work; no existing Phase 4 cassette undertests/cassettes/phase4/carries the metadata, so the "extract" path is unreachable. The fallback in Notes line 194 (blake3(cassette_path.encode())[:32]) lacks domain separation and an unspecified path encoding (absolute vs repo-relative). Pinned the canonical deterministic derivation in AC-3a:cassette_canary_pin = blake3.blake3(f"phase4-cassette:{cassette_relpath_posix}".encode("utf-8")).hexdigest()[:32], wherecassette_relpath_posixissource_cassette.resolve().relative_to(REPO_ROOT).as_posix(). Thephase4-cassette:prefix domain-separates from future canary schemes; the repo-relative POSIX path is reproducible across contributors and platforms. When ADR-P4-006 ships and Phase 4 cassettes re-cut with metadata, the derivation upgrades to "read metadata if present, else derive" — a one-line strategy upgrade. - B-DIGESTS-YAML-SIGNED-HERE (F-CON-4 — BLOCK). Consistency wins. The original §Implementation outline §5 said "Add a digests-yaml stub entry for each case (the full signing happens in S5-05)" — but S2-02 §AC-6a requires every value match
re.fullmatch(r"blake3:[0-9a-f]{64}", v)AND S2-02 §AC-3 step (c) compares the entry to the canonical recomputation; a literal stub of"blake3:" + "0" * 64passes the regex but raisesBenchCaseDigestMismatchimmediately. There is no "stub" path through the HARDENED loader. This story signs its 5 cases incases/digests.yaml; S5-04 appends its 5; S5-05 shipsscripts/sign_bench_digests.pyfor future re-signing operations and runs the full E2E. Addedbench/vuln-remediation/cases/digests.yamlto Files-to-touch. - B-E2E-DROPPED (F-CON-5 — BLOCK). Consistency wins. The original AC-7 prescribed
codegenie eval run --task-class=vuln-remediation --cases='00{1..5}-*'exits 0 — directly contradicting §Out of scope §3 ("The E2E run. S5-05 exercises the cases throughcodegenie eval run. Here we only assert they load.") which is correct on multiple counts: the realvuln-remediationSUT lives in Phase 6 (not yet shipped); the deterministic stub SUT is built in S5-05 (not yet shipped); the--casesglob filter requires CLI support S4-02 may not provide. Dropped AC-7 entirely; kept the narrower "loader loads all 5 without raising" contract (AC-4). - B-CASE-ID-DIR-INVARIANT (F-CON-6 — BLOCK). Consistency wins. S2-02 §AC-7 raises
BenchCaseLoadError(field="case_id")whencase.case_id != directory.name; a curator typo on case_id silently passes the originaltest_case_ids_follow_naming_convention(the regex matches both correct and typo'd IDs) and only surfaces at the loader. New AC-4 pins the bidirectional invariant; new test assertscase.case_id == case_dir.namefor each of the 5 directly (independent of the loader walk). - Symlink-freeness pinned at story-test boundary (F-CON-7 — HARDEN). Defense-in-depth: even though S2-02 §AC-9 catches symlinks at load time, the story tests assert symlink-freeness directly so curator-time mistakes surface with a path-naming diagnostic at the story-test boundary, not as an opaque loader error.
- commit_sha vs derivation SHA disambiguated (F-CON-8 — HARDEN). Two distinct SHAs were conflated:
BenchCase.commit_sha: str | None(the Pydantic field; staysNoneper ADR-0006 §Consequences forsource="curated") and the source-cassette commit SHA at derivation time (lives in the comment block at top ofcase.toml). Clarified throughout. - rubric ≠ reader-of-expected (F-CON-9 — HARDEN). AC-2 originally said "ground-truth artifacts the rubric reads (e.g.,
expected/diff.patch, ...)". S5-02 HARDENED makes clear the rubric readsharness_outputonly, neverexpected/.expected/is consumed by the SUT (Phase 6'sVulnRemediationSut.run_casereads it to populateharness_output["validator"]["cve_dropped"]etc.). AC-2 rephrased; Notes-for-implementer updated. - disposition / difficulty distribution pinned (F-COV-4 + F-TQ-7 — HARDEN). Original Notes "expect ~5 positive" and "skew easy" weren't enforced. New AC-7: at least 4 of 5 cases have
disposition="positive"; at least 4 of 5 havedifficulty="easy".disposition="negative"rejected entirely for RAG-corpus-derived (the cassette demonstrates a fix; the SUT should not refuse it). - Source-cassette path existence + distinctness (F-COV-6 + F-COV-7 + F-TQ-3 — HARDEN). Original
test_case_toml_documents_source_cassette_pathwas a substring check (assert "tests/cassettes/phase4/" in text) — a typo'd or fake path passes. New test parses# Derived from: tests/cassettes/phase4/<path>line per case, asserts the resolved path exists, is undertests/cassettes/phase4/, and the 5 derived-from paths form a set of size 5 (no duplicate-derivation regression illusion). - Canonical digest recomputation + 3-way consistency (F-COV-8 + F-COV-9 + F-TQ-2 — HARDEN). Original
test_every_case_has_blake3_digest_with_64_hexonly checked prefix+length; a curator hand-writing the wrong digest passes the story test and only fails at load time. New test recomputes via_compute_case_dir_digest(case_dir)and asserts_compute_case_dir_digest(case_dir) == case.toml#case_digest == digests.yaml[case_id]. - Canonical canary-pin recomputation (F-TQ-1 — HARDEN). Original test allowed any 32-hex string. New test recomputes via the canonical formula (per AC-3a) from the comment-block source-cassette path and asserts byte-equality.
- Bidirectional curation-class ↔ directory-naming check (F-COV-10 + F-TQ-5 — HARDEN). New test pins that the set of
cases/*/directories ending in-rag-corpus-derivedequals the set of case_ids whoseBenchCase.curation_class == "rag-corpus-derived", AND that the 5 case_ids are distinct. - input/ not a symlink, no input-pointer.toml present (F-TQ-4 — HARDEN). Original test
c.input_path.is_dir()silently passes symlinks-to-directories. Tightened toc.input_path.is_dir() and not c.input_path.is_symlink() and not (case_dir / "input-pointer.toml").exists(). - README mapping table pinned (F-TQ-8 — HARDEN). §Refactor §1 referenced a mapping table but no AC enforced. New AC-11:
bench/vuln-remediation/README.mdhas a "Case mapping" section containing a markdown table with ≥ 5 RAG-corpus-derived rows; each row's cassette path matches the comment-block in the correspondingcase.toml. - Digests algorithm extraction surfaced (F-DP-1 — surfaced; NOT promoted to AC). Rule-of-three threshold: the canonical digest algorithm lives private in
src/codegenie/eval/loader.py(S2-02 HARDENED). Consumers stacking up: S5-03 (this story), S5-04 (sibling), S5-05'sscripts/sign_bench_digests.py, S5-07's scaffolder. When the second non-loader consumer lands (likely S5-05's signing script), promote_compute_case_dir_digestto publiccodegenie.eval.digests.compute_case_dir_digest(new module). NOT this story's job (Rule 2 — the loader's HARDENED contract is the source of truth; promoting a public surface from S5-03 would be cross-cutting). Surfaced in Notes as the trigger condition for the next consumer story. - Canary derivation strategy seam (F-DP-2 — surfaced). Two paths will eventually exist (deterministic-from-path today, read-from-metadata after ADR-P4-006 lands). For one path today, Rule 2 says no abstraction. When the second path lands, extract a
CanaryPinSourcesum type (Path | CassetteMetadata) +derive_canary_pin(source) -> strsmart-constructor. Surfaced in Notes. - Functional core / imperative shell endorsed (F-DP-4 — surfaced). The canonical S2-02 digest algorithm is already a functional core. The curation work (selecting cassettes, copying snapshots) is imperative-shell territory. No structural change; surfaced.
Full audit log: _validation/S5-03-vuln-rag-corpus-derived-cases.md
Context¶
ADR-0006 splits the bench corpus into two curation classes. The 5 RAG-corpus-derived cases land first and mechanically — they're constructed by extracting solved examples from Phase 4's tests/cassettes/phase4/ cassette tree, which is the same corpus Phase 4's recipe-first/RAG-fallback path was tuned against. The point of these cases is regression coverage: if the pipeline degrades on cases it has already solved once, the harness will surface it. The point is explicitly not judgment evidence — ADR-0006 §Decision is unambiguous that promotion to silver requires 5 held-out cases (S5-04). These 5 are the schedule-permitting half of the 5+5 floor.
Mechanical construction matters because the long-pole curation work (S5-04) is hand-built CVE-fix ground truth. Shipping these 5 mechanically buys schedule margin for S5-04 without compromising the memorization-vs-judgment distinction.
The story executes against a HARDENED S2-02 loader. The canonical case-dir digest algorithm at S2-02 §AC-3 is the only algorithm allowed for case_digest. The cassette_canary_pin derivation is the deterministic-from-path formula (ADR-P4-006 has not shipped yet; every existing Phase 4 cassette pre-dates the amendment). cases/digests.yaml is signed by this story for its 5 cases (S5-04 appends, S5-05 ships the re-sign script). The E2E run is S5-05's job (Phase 6 SUT + deterministic stub).
References — where to look¶
- Architecture:
../phase-arch-design.md §bench/{task-class}/ directory contract(line 698) —case.tomlschema; required keys (case_id,task_class,disposition,difficulty,source,curation_class,added_at,last_validated_at,cassette_canary_pin,case_digest); optionalcommit_sha(required iffsource != "curated").../phase-arch-design.md §Data model → BenchCase(lines 757–773) — required field shapes;case_digest: stris"blake3:<hex>";cassette_canary_pin: stris 32 hex chars; bothinput_pathandexpected_pathare requiredPath.../phase-arch-design.md §Testing strategy → Fixture portfolio— namesbench/vuln-remediation/as the production fixture, with the 5+5 split.- Phase ADRs:
../ADRs/0005-cassette-canary-seed-parameterization.md §Decision §Consequences—Canary.mint(seed=bytes.fromhex(case.cassette_canary_pin))is the per-case binding; ADR-P4-006 ships with this phase as an amendment to Phase 4;case_digestexcludescase.tomlso pin rotation is identity not content.../ADRs/0006-curation-class-split-with-fence-ci-held-out-floor.md §Decision §Consequences— naming convention001-005-rag-corpus-derived-<slug>(advisory; not fence-enforced); the cassette-derivation script is the curator's tool;source="curated"for RAG-corpus-derived cases.- Production ADRs:
../../../production/adrs/0011-recipe-first-rag-llm-fallback-planning.md— the upstream commitment these cases regression-test against. - Sibling HARDENED stories (load-bearing contracts):
S2-02-loader-cases-and-digests.md §AC-3— the canonical case-dir digest algorithm; the only algorithm allowed forcase_digest.S2-02-loader-cases-and-digests.md §AC-5—(case_dir / "input").is_dir()invariant.S2-02-loader-cases-and-digests.md §AC-6a §AC-6b—digests.yamlschema + ↔-filesystem completeness rules.S2-02-loader-cases-and-digests.md §AC-7—case.case_id == case_dir.nameinvariant.S2-02-loader-cases-and-digests.md §AC-9— symlink rejection rule.S5-02-vuln-rubric-and-unit-tests.md(HARDENED) — the rubric readsharness_output, notexpected/.- Source design:
../High-level-impl.md §Step 5— "5 cases mechanically derived fromtests/cassettes/phase4/".
Goal¶
Construct exactly 5 BenchCase directories under bench/vuln-remediation/cases/ with curation_class="rag-corpus-derived", each derived from a distinct tests/cassettes/phase4/ solved-example cassette via the canonical mechanical procedure pinned in AC-3 / AC-3a; each carrying case.toml, a real input/ directory, a real expected/ directory, a deterministically-derived cassette_canary_pin (32 hex), and a case_digest computed via the canonical S2-02 §AC-3 algorithm; the 5 cases are signed in bench/vuln-remediation/cases/digests.yaml; codegenie.eval.loader.load_cases(task_class) returns all 5 without raising; defense-in-depth structural tests pin the bidirectional invariants the loader catches lazily.
Acceptance criteria¶
-
[ ] AC-1 (directory naming + count).
bench/vuln-remediation/cases/contains exactly 5 directories whose names matchre.fullmatch(r"^00[1-5]-[a-z0-9][a-z0-9-]*-rag-corpus-derived$", name). The 5 directory basenames form a set of size 5. The test enumeratessorted(p.name for p in (BENCH_ROOT/"vuln-remediation"/"cases").iterdir() if p.is_dir())— filtering nothing — and asserts the count of names matching the regex equals 5 AND that no other directory's name contains the substring-rag-corpus-derived(defense against006-cve-2024-99999-rag-corpus-derived/index collision with S5-04's territory). -
[ ] AC-2 (each case directory's filesystem shape). Each of the 5 case directories contains:
case.toml(regular file, UTF-8) validating into aBenchCase(viaBenchCase.model_validate(tomllib.loads(text))) with:task_class == "vuln-remediation",curation_class == "rag-corpus-derived",source == "curated",disposition ∈ {"positive", "ambiguous"}(NOT "negative" — see AC-7),difficulty ∈ {"easy", "medium", "hard"},commit_sha is None(per ADR-0006 §Consequences —source="curated"does not require it),added_atandlast_validated_atare tz-aware UTCdatetimes (tzinfo == timezone.utc),cassette_canary_pinis 32 lowercase hex characters,case_digestmatchesre.fullmatch(r"^blake3:[0-9a-f]{64}$", value),input_pathresolves to the string"input"(relative POSIX),expected_pathresolves to the string"expected"(relative POSIX),cassette_path is None(the cassette is referenced from the comment block, not the optionalcassette_pathfield — that's Phase 4's resolution territory).input/is a real directory ((case_dir / "input").is_dir() and not (case_dir / "input").is_symlink()), is non-empty, and contains the pre-fix snapshot of the file(s) the recipe touches (minimal — not the full repo, per F-CON-2 dropping the pointer escape hatch).expected/is a real directory ((case_dir / "expected").is_dir() and not (case_dir / "expected").is_symlink()), is non-empty, and contains the ground-truth artifacts the SUT consults at run time (the rubric readsharness_outputonly, neverexpected/— see Notes-for-implementer). The exact filenames insideexpected/are SUT-contract territory (Phase 6); this story's curator follows Phase 6's contract or, for the Phase-6.5 deterministic-stub SUT (built in S5-05), follows the stub's contract.- No
input-pointer.tomlfile exists at the case_dir root (defense against re-adding the dropped escape hatch — F-CON-2). -
No file or directory anywhere under
case_dir.rglob("*")is a symlink (defense-in-depth on S2-02 §AC-9). -
[ ] AC-3 (case_digest = canonical S2-02 §AC-3 algorithm; no inline reimplementation). Each case.toml's
case_digestfield equals the canonical algorithm S2-02 §AC-3 prescribes: - (a)
paths = sorted(p for p in case_dir.rglob("*") if p.is_file() and not p.is_symlink() and p.name not in {"case.toml"}, key=lambda p: p.relative_to(case_dir).as_posix()). - (b)
records = [f"{p.relative_to(case_dir).as_posix()}\x1f{content_hash(p)}".encode("utf-8") for p in paths]wherecontent_hashiscodegenie.hashing.content_hash(Phase 0 per-file content BLAKE3). -
(c)
case_digest = "blake3:" + blake3(b"\x1e".join(records)).hexdigest(). The integration test imports_compute_case_dir_digestfromcodegenie.eval.loader(private helper; the contracted name isfrom codegenie.eval.loader import _compute_case_dir_digest) and asserts byte-equality withcase.toml#case_digestper case. The story does NOT inline a re-implementation of the algorithm — every consumer goes through the loader's helper. (When the next consumer story promotes the helper to a public name per F-DP-1, this story's test switches to the public name in one line.) -
[ ] AC-3a (
cassette_canary_pin= canonical deterministic-from-path formula). Each case.toml'scassette_canary_pinequals:whereREPO_ROOT = <repo root resolved> cassette_relpath_posix = source_cassette.resolve().relative_to(REPO_ROOT).as_posix() domain_separated = f"phase4-cassette:{cassette_relpath_posix}".encode("utf-8") cassette_canary_pin = blake3.blake3(domain_separated).hexdigest()[:32]source_cassetteis the path parsed from the case.toml comment-block line# Derived from: tests/cassettes/phase4/<path>. Thephase4-cassette:prefix is the domain-separation token (literal; no whitespace; ends with colon). The 32-hex result is lowercased. The integration test re-derives each pin from the comment-block path and asserts byte-equality withcase.toml#cassette_canary_pin. This is the deterministic-derivation path required because all existing Phase 4 cassettes pre-date ADR-P4-006 (the metadata-emitting amendment; landing as part of Phase 6.5 work). When ADR-P4-006 ships and cassettes are re-cut with metadata, a follow-on story upgrades the derivation to a Strategy seam (F-DP-2); for now, the formula is pinned. -
[ ] AC-4 (case_id ↔ directory-name + curation_class ↔ directory-name-suffix invariants; case_id distinctness). For each of the 5 case directories
case_dir:BenchCase.case_id == case_dir.name(byte-equality). Additionally,case_dir.name.endswith("-rag-corpus-derived")⇔BenchCase.curation_class == "rag-corpus-derived". The set{c.case_id for c in rag_cases}has cardinality 5. These are story-test–level defense-in-depth on S2-02 §AC-7 (case_id↔dir) and S2-02 §AC-8 (collision). -
[ ] AC-5 (
loader.load_casessucceeds; returns exactly 5 RAG-corpus-derived cases when filtered).load_cases(load_task_class("vuln-remediation", bench_root=BENCH_ROOT))returns atuple[BenchCase, ...]without raising; the 5 cases whosecuration_class == "rag-corpus-derived"are present (other curation classes may also be present once S5-04 lands; this story's test filters bycuration_classand asserts exactly 5 of that class). All 5 havesource == "curated",task_class == "vuln-remediation",commit_sha is None. Returned tuple is sorted ascending bycase_id. -
[ ] AC-6 (
bench/vuln-remediation/cases/digests.yamlexists, parses, signs 5 cases canonically).bench/vuln-remediation/cases/digests.yamlis committed;yaml.safe_load(text)returns adict[str, str]; for each of the 5 RAG-corpus-derived case_ids, the file contains an entry{case_id: "blake3:<64-hex>"}matchingre.fullmatch(r"^blake3:[0-9a-f]{64}$", value); each digest value equals_compute_case_dir_digest(BENCH_ROOT/"vuln-remediation"/"cases"/case_id)byte-for-byte. The file is sorted alphabetically by key (yaml.safe_dump(data, sort_keys=True)). The 5 entries from this story coexist peacefully with the 5 from S5-04 (S5-04 appends; this story does not pre-allocate held-out entries). -
[ ] AC-6a (3-way digest consistency). For each of the 5 cases:
case.toml#case_digest == digests.yaml[case_id] == _compute_case_dir_digest(case_dir)(all three byte-equal). The integration test asserts this triple-equality per case. Defense-in-depth: catches the curator forgetting to update one of the three after a re-sign. -
[ ] AC-7 (disposition + difficulty distribution). At least 4 of the 5 cases have
disposition == "positive"; at most 1 hasdisposition == "ambiguous". ZERO havedisposition == "negative"(the source cassette demonstrates a fix; an SUT-should-refuse case is structurally incompatible with RAG-corpus-derivation). At least 4 of 5 havedifficulty == "easy"(RAG-derived cases skew easy — the pipeline already solved them). A curator wanting a higher-difficulty mechanically-derived case must justify in the case.toml comment block. -
[ ] AC-8 (source-cassette path traceability: exists + distinctness). Each
case.tomlcontains a comment block at the top with the literal lines (or equivalent, same content)# Derived from: tests/cassettes/phase4/<path>and# Source cassette commit: <40-char lowercase hex SHA>. The integration test parses eachcase.tomltext, extracts both lines per case via regex^# Derived from: (tests/cassettes/phase4/.+)$and^# Source cassette commit: ([0-9a-f]{40})$. For each extractedtests/cassettes/phase4/<path>value: the path resolves under<REPO_ROOT>/tests/cassettes/phase4/, exists on disk, and is either a file or a directory (cassette layout shape is Phase 4's). For each extracted SHA: matches the 40-hex regex. The set of 5 extracted source-cassette paths has cardinality 5 (no duplicate-derivation regression illusion). The set of 5 commit SHAs may have any cardinality (multiple cassettes may share a commit). -
[ ] AC-9 (curation_class-derived-case set ↔ directory-name set).
directory_names_ending_rag = {p.name for p in cases_root.iterdir() if p.is_dir() and p.name.endswith("-rag-corpus-derived")}equals{c.case_id for c in loaded_cases if c.curation_class == "rag-corpus-derived"}(set equality). Catches a case mistaggedcuration_class="held-out"in a*-rag-corpus-derived/directory (and vice-versa). -
[ ] AC-10 (symlink-freeness — story-level defense-in-depth on S2-02 §AC-9). For each of the 5 case directories, walking
rglob("*")yields zero symlinks. Test produces a diagnostic naming the offending case_id and relpath if any symlink is found. -
[ ] AC-11 (README mapping table).
bench/vuln-remediation/README.mdcontains a section whose markdown header text is## Case mapping(or equivalent unambiguous header). Under it: a markdown table with header row| case_id | source cassette | derivation SHA | curation class |(or equivalent; column count = 4, column 3 contains 40-hex SHAs). The table contains a row per RAG-corpus-derived case_id; the column-2 cell value matches the# Derived from:comment block in the correspondingcase.toml. Also includes a "Selection criterion" paragraph naming what made these 5 cassettes the chosen 5 (e.g., "5 most representative across the recipe-first and RAG-fallback paths"). The integration test parses the README, extracts the markdown table, and asserts row count ≥ 5 with the constraints above. -
[ ] AC-12 (typed-error parity surface — no story-introduced regressions). Running the integration test before any of the 5 cases are present (red marker) produces a clear
BenchCaseLoadErrornamingcases/digests.yaml(the file is missing); after step (1) of §Implementation outline (digests.yaml stubbed empty), the test produces a clear failure mentioning the 5 expected case_ids; after each case is signed, the count converges. This pins that no failure path emits anOSError/KeyError/AssertionErrordirectly — all failures route throughcodegenie.eval.errors. -
[ ] AC-13 (lint + typecheck + red→green). Red test from §TDD plan (
tests/integration/test_vuln_rag_corpus_derived_cases_load.py) exists, was committed at red, now green.ruff check tests/integration/test_vuln_rag_corpus_derived_cases_load.py bench/vuln-remediation/,ruff format --check tests/integration/test_vuln_rag_corpus_derived_cases_load.py,pytest tests/integration/test_vuln_rag_corpus_derived_cases_load.py -vall green.mypy --strict tests/integration/test_vuln_rag_corpus_derived_cases_load.pygreen (test file importsBenchCase,load_cases,load_task_class, and_compute_case_dir_digest; all typed surfaces).make fencecontinues to pass — no new closure imports introduced undersrc/codegenie/; the test'simport blake3(for AC-3a recomputation) lives undertests/and is outside the policed runtime closure.
Implementation outline¶
-
Write the red test
tests/integration/test_vuln_rag_corpus_derived_cases_load.pyfirst — see §TDD plan. Commit as the red marker; the test should fail withBenchCaseLoadErrornamingcases/digests.yaml(file not yet present). -
Identify 5 source cassettes under
tests/cassettes/phase4/representing distinct solved examples (CVE fixes the Phase 4 pipeline successfully resolved at recipe-first or RAG-fallback). Document the selection criterion inbench/vuln-remediation/README.md(e.g., "5 most representative cassettes across the recipe-first and RAG-fallback paths, mix of language ecosystems where available"). For each chosen cassette, record its repo-relative POSIX path and the master commit SHA at curation time — these go into thecase.tomlcomment block. -
For each of the 5 cassettes, build the case directory under
bench/vuln-remediation/cases/00N-<slug>-rag-corpus-derived/(where N ∈ {1..5} and<slug>is derived from the CVE id or a short descriptor; lowercase): - case.toml with:
- A comment block at the top with literally:
- All required
BenchCasefields per AC-2.commit_shaisNone(omit the key or setcommit_sha = ""followed by Pydantic coercion — confirm S1-02's Pydantic shape accepts thenull/missing form; safest: do not emit the key whensource = "curated", sincecommit_sha: str | Nonedefaults toNoneif absent). input_path = "input",expected_path = "expected".cassette_canary_pinderived via AC-3a's canonical formula (compute via a one-off curator command or S5-07's scaffolder once it lands).case_digestcomputed via_compute_case_dir_digest(case_dir)after populatinginput/andexpected/— the digest pins content, so it's computed last.
- input/ populated from the cassette's pre-fix snapshot — extract only the files the recipe touches (NOT the whole repo). Files must be regular files (no symlinks). Per AC-2:
input/non-empty. -
expected/ populated from the cassette's post-fix snapshot — the SUT contract decides the filenames (typically
expected/diff.patch+expected/validator_output.jsonfor the Phase-6.5 stub SUT or Phase 6's real SUT; follow S5-05's deterministic-stub-SUT contract when defined). Files must be regular files. Per AC-2:expected/non-empty. -
Sign the 5 cases in
bench/vuln-remediation/cases/digests.yaml:Verify parity per AC-6a:from codegenie.eval.loader import _compute_case_dir_digest import yaml, pathlib cases_root = pathlib.Path("bench/vuln-remediation/cases") rag_dirs = sorted(p for p in cases_root.iterdir() if p.is_dir() and p.name.endswith("-rag-corpus-derived")) digests = {p.name: _compute_case_dir_digest(p) for p in rag_dirs} # If digests.yaml already exists (S5-04 has shipped first), merge: existing = yaml.safe_load((cases_root / "digests.yaml").read_text()) if (cases_root / "digests.yaml").exists() else {} existing.update(digests) (cases_root / "digests.yaml").write_text(yaml.safe_dump(existing, sort_keys=True))case.toml#case_digest == digests.yaml[case_id] == _compute_case_dir_digest(case_dir). -
Update
bench/vuln-remediation/README.mdper AC-11: add the "Case mapping" section with the table (≥ 5 RAG-corpus-derived rows) and the selection-criterion paragraph. -
Iterate test → green. The integration test's failures are typed (per AC-12) — each failure points at a specific case_id and field, not a generic "load failed."
TDD plan — red / green / refactor¶
Red — write the failing test first¶
Test file path: tests/integration/test_vuln_rag_corpus_derived_cases_load.py
# tests/integration/test_vuln_rag_corpus_derived_cases_load.py
"""5 RAG-corpus-derived cases must load cleanly and satisfy ADR-0005/ADR-0006
contracts. ADR-0006 §Consequences names this curation class; we assert structural
shape + cross-field invariants — not scoring correctness (S5-05's E2E job).
Every test is concrete and mutation-resistant: digest re-computation via the
canonical S2-02 §AC-3 algorithm, canary-pin re-derivation via the canonical
AC-3a formula, set-equality on directory-naming↔curation-class, source-cassette
path existence + distinctness, disposition/difficulty distribution caps.
"""
from __future__ import annotations
import re
import tomllib
from datetime import timezone
from pathlib import Path
import blake3 # test-only: AC-3a canonical canary-pin re-derivation
import pytest
import yaml
from codegenie.eval.loader import (
_compute_case_dir_digest, # canonical S2-02 §AC-3 algorithm; private until F-DP-1 promotes
load_cases,
load_task_class,
)
from codegenie.eval.models import BenchCase
REPO_ROOT = Path(__file__).parents[2]
BENCH_ROOT = REPO_ROOT / "bench"
CASES_ROOT = BENCH_ROOT / "vuln-remediation" / "cases"
RAG_NAME_RE = re.compile(r"^00[1-5]-[a-z0-9][a-z0-9-]*-rag-corpus-derived$")
DERIVED_FROM_RE = re.compile(r"^# Derived from: (tests/cassettes/phase4/.+)$", re.MULTILINE)
SOURCE_SHA_RE = re.compile(r"^# Source cassette commit: ([0-9a-f]{40})$", re.MULTILINE)
BLAKE3_DIGEST_RE = re.compile(r"^blake3:[0-9a-f]{64}$")
HEX32_RE = re.compile(r"^[0-9a-f]{32}$")
def _load_rag_cases() -> tuple[BenchCase, ...]:
tc = load_task_class("vuln-remediation", bench_root=BENCH_ROOT)
cases = load_cases(tc)
return tuple(c for c in cases if c.curation_class == "rag-corpus-derived")
def _list_rag_dirs() -> list[Path]:
return sorted(p for p in CASES_ROOT.iterdir() if p.is_dir() and p.name.endswith("-rag-corpus-derived"))
def _read_case_toml_text(case_dir: Path) -> str:
return (case_dir / "case.toml").read_text(encoding="utf-8")
# --- AC-1: directory naming + count ----------------------------------------
def test_exactly_five_rag_corpus_derived_directories_with_canonical_names():
rag_dirs = _list_rag_dirs()
assert len(rag_dirs) == 5, f"expected 5 RAG-corpus-derived dirs, found {len(rag_dirs)}: {[p.name for p in rag_dirs]}"
for p in rag_dirs:
assert RAG_NAME_RE.fullmatch(p.name), f"{p.name!r} does not match the canonical regex"
# No other directory has the substring (catches 006-…-rag-corpus-derived index collision):
all_dirs = [p.name for p in CASES_ROOT.iterdir() if p.is_dir()]
intruders = [name for name in all_dirs if "-rag-corpus-derived" in name and not RAG_NAME_RE.fullmatch(name)]
assert intruders == [], f"directory(ies) with -rag-corpus-derived suffix outside 001-005: {intruders}"
# --- AC-2: per-case filesystem shape + BenchCase field invariants -----------
@pytest.mark.parametrize("case_dir_index", range(5), ids=lambda i: f"case-{i+1}")
def test_each_case_directory_shape_and_bench_case_invariants(case_dir_index):
rag_dirs = _list_rag_dirs()
if len(rag_dirs) <= case_dir_index:
pytest.skip(f"only {len(rag_dirs)} dirs; case {case_dir_index+1} not yet present")
case_dir = rag_dirs[case_dir_index]
# case.toml exists, parses, validates into BenchCase:
case_toml = case_dir / "case.toml"
assert case_toml.is_file() and not case_toml.is_symlink()
parsed = tomllib.loads(_read_case_toml_text(case_dir))
bc = BenchCase.model_validate(parsed)
# Field constraints per AC-2:
assert bc.task_class == "vuln-remediation"
assert bc.curation_class == "rag-corpus-derived"
assert bc.source == "curated"
assert bc.disposition in {"positive", "ambiguous"}, f"{bc.case_id}: disposition={bc.disposition!r}, must be positive or ambiguous"
assert bc.difficulty in {"easy", "medium", "hard"}
assert bc.commit_sha is None, f"{bc.case_id}: commit_sha must be None for source=curated"
assert bc.added_at.tzinfo is not None and bc.added_at.utcoffset() == timezone.utc.utcoffset(None)
assert bc.last_validated_at.tzinfo is not None and bc.last_validated_at.utcoffset() == timezone.utc.utcoffset(None)
assert HEX32_RE.fullmatch(bc.cassette_canary_pin), f"{bc.case_id}: cassette_canary_pin not 32 lowercase hex"
assert BLAKE3_DIGEST_RE.fullmatch(bc.case_digest), f"{bc.case_id}: case_digest not blake3:<64 hex>"
assert str(bc.input_path) == "input"
assert str(bc.expected_path) == "expected"
assert bc.cassette_path is None
# Filesystem shape:
assert (case_dir / "input").is_dir() and not (case_dir / "input").is_symlink(), f"{bc.case_id}: input/ missing or symlink"
assert (case_dir / "expected").is_dir() and not (case_dir / "expected").is_symlink(), f"{bc.case_id}: expected/ missing or symlink"
assert any((case_dir / "input").iterdir()), f"{bc.case_id}: input/ empty"
assert any((case_dir / "expected").iterdir()), f"{bc.case_id}: expected/ empty"
# Negative: no input-pointer.toml (F-CON-2 backstop):
assert not (case_dir / "input-pointer.toml").exists(), f"{bc.case_id}: input-pointer.toml is forbidden (F-CON-2)"
# --- AC-3: case_digest == canonical S2-02 §AC-3 algorithm ------------------
@pytest.mark.parametrize("case_dir_index", range(5), ids=lambda i: f"case-{i+1}")
def test_case_digest_matches_canonical_algorithm(case_dir_index):
rag_dirs = _list_rag_dirs()
if len(rag_dirs) <= case_dir_index:
pytest.skip(f"only {len(rag_dirs)} dirs")
case_dir = rag_dirs[case_dir_index]
parsed = tomllib.loads(_read_case_toml_text(case_dir))
declared = parsed["case_digest"]
canonical = _compute_case_dir_digest(case_dir)
assert declared == canonical, (
f"{case_dir.name}: case.toml#case_digest={declared!r} != "
f"_compute_case_dir_digest={canonical!r}. The curator wrote the wrong "
f"algorithm or forgot to re-sign after editing input/ or expected/."
)
# --- AC-3a: cassette_canary_pin == canonical deterministic-from-path formula
@pytest.mark.parametrize("case_dir_index", range(5), ids=lambda i: f"case-{i+1}")
def test_cassette_canary_pin_matches_canonical_derivation(case_dir_index):
rag_dirs = _list_rag_dirs()
if len(rag_dirs) <= case_dir_index:
pytest.skip(f"only {len(rag_dirs)} dirs")
case_dir = rag_dirs[case_dir_index]
text = _read_case_toml_text(case_dir)
m = DERIVED_FROM_RE.search(text)
assert m, f"{case_dir.name}: case.toml has no `# Derived from: tests/cassettes/phase4/...` line"
cassette_relpath_posix = m.group(1).strip()
# The canonical AC-3a formula:
expected_pin = blake3.blake3(
f"phase4-cassette:{cassette_relpath_posix}".encode("utf-8")
).hexdigest()[:32]
parsed = tomllib.loads(text)
actual_pin = parsed["cassette_canary_pin"]
assert actual_pin == expected_pin, (
f"{case_dir.name}: cassette_canary_pin={actual_pin!r} does not match "
f"the canonical formula blake3('phase4-cassette:{cassette_relpath_posix}')[:32] "
f"= {expected_pin!r}. Until ADR-P4-006 ships, this is the only allowed derivation."
)
# --- AC-4: case_id ↔ directory-name + curation_class ↔ suffix + distinctness
def test_case_id_equals_directory_name_and_curation_class_matches_suffix():
cases = _load_rag_cases()
assert len(cases) == 5, f"loader returned {len(cases)} RAG-corpus-derived cases; expected 5"
# case_id distinctness:
case_ids = [c.case_id for c in cases]
assert len(set(case_ids)) == 5, f"duplicate case_ids: {case_ids}"
# case_id ↔ directory-name (independent of loader's S2-02 §AC-7 check):
for c in cases:
case_dir = CASES_ROOT / c.case_id
assert case_dir.is_dir(), f"case_id {c.case_id!r} has no matching directory"
parsed = tomllib.loads(_read_case_toml_text(case_dir))
assert parsed["case_id"] == case_dir.name, (
f"{case_dir.name}: case.toml#case_id={parsed['case_id']!r} != directory name"
)
# curation_class ↔ suffix:
for c in cases:
assert c.case_id.endswith("-rag-corpus-derived"), f"{c.case_id}: missing suffix"
# --- AC-5: loader returns exactly 5 RAG-corpus-derived cases ----------------
def test_loader_returns_exactly_five_rag_corpus_derived_with_curated_source():
cases = _load_rag_cases()
assert len(cases) == 5
for c in cases:
assert c.source == "curated", f"{c.case_id}: source must be curated"
assert c.task_class == "vuln-remediation"
assert c.commit_sha is None
# Sorted ascending by case_id:
assert list(cases) == sorted(cases, key=lambda c: c.case_id)
# --- AC-6 + AC-6a: digests.yaml exists, parses, signs each case canonically -
def test_digests_yaml_signs_five_rag_corpus_derived_cases_canonically():
digests_path = CASES_ROOT / "digests.yaml"
assert digests_path.is_file(), "bench/vuln-remediation/cases/digests.yaml missing"
parsed = yaml.safe_load(digests_path.read_text())
assert isinstance(parsed, dict), f"digests.yaml root must be a mapping; got {type(parsed).__name__}"
rag_case_ids = {c.case_id for c in _load_rag_cases()}
for case_id in rag_case_ids:
assert case_id in parsed, f"digests.yaml missing entry for {case_id}"
value = parsed[case_id]
assert isinstance(value, str) and BLAKE3_DIGEST_RE.fullmatch(value), (
f"digests.yaml[{case_id}]={value!r}: must be blake3:<64 lowercase hex>"
)
# Canonical recomputation:
canonical = _compute_case_dir_digest(CASES_ROOT / case_id)
assert value == canonical, (
f"digests.yaml[{case_id}]={value!r} != canonical {canonical!r}; "
f"curator must re-sign after editing input/ or expected/"
)
def test_case_toml_and_digests_yaml_and_canonical_are_three_way_consistent():
"""AC-6a: case.toml#case_digest == digests.yaml[case_id] == canonical algorithm."""
digests_yaml = yaml.safe_load((CASES_ROOT / "digests.yaml").read_text())
for c in _load_rag_cases():
case_dir = CASES_ROOT / c.case_id
case_toml_digest = tomllib.loads(_read_case_toml_text(case_dir))["case_digest"]
digests_yaml_digest = digests_yaml[c.case_id]
canonical = _compute_case_dir_digest(case_dir)
assert case_toml_digest == digests_yaml_digest == canonical, (
f"{c.case_id}: 3-way digest divergence — "
f"case.toml={case_toml_digest!r}, digests.yaml={digests_yaml_digest!r}, "
f"canonical={canonical!r}"
)
# --- AC-7: disposition + difficulty distribution ----------------------------
def test_disposition_distribution_at_least_four_positive_none_negative():
cases = _load_rag_cases()
dispositions = [c.disposition for c in cases]
positive_count = dispositions.count("positive")
negative_count = dispositions.count("negative")
ambiguous_count = dispositions.count("ambiguous")
assert negative_count == 0, (
f"RAG-corpus-derived cases must NOT have disposition=negative "
f"(the source cassette demonstrates a fix); found {negative_count}"
)
assert positive_count >= 4, f"≥4 of 5 must be positive; found {positive_count}: {dispositions}"
assert ambiguous_count <= 1, f"≤1 of 5 may be ambiguous; found {ambiguous_count}"
def test_difficulty_distribution_at_least_four_easy():
cases = _load_rag_cases()
easy_count = sum(1 for c in cases if c.difficulty == "easy")
assert easy_count >= 4, (
f"≥4 of 5 RAG-corpus-derived cases must be difficulty=easy "
f"(the pipeline already solved them); found {easy_count}: "
f"{[(c.case_id, c.difficulty) for c in cases]}"
)
# --- AC-8: source-cassette path traceability — exists, distinct, has SHA ----
def test_each_case_documents_existing_source_cassette_and_distinct_paths():
cases = _load_rag_cases()
cassette_paths = []
for c in cases:
text = _read_case_toml_text(CASES_ROOT / c.case_id)
m = DERIVED_FROM_RE.search(text)
assert m, f"{c.case_id}: case.toml missing `# Derived from: tests/cassettes/phase4/...`"
rel = m.group(1).strip()
cassette_paths.append(rel)
resolved = (REPO_ROOT / rel).resolve()
assert resolved.exists(), f"{c.case_id}: source cassette {rel} does not exist"
assert (REPO_ROOT / "tests" / "cassettes" / "phase4") in resolved.parents or resolved == (REPO_ROOT / "tests" / "cassettes" / "phase4"), (
f"{c.case_id}: source cassette {rel} not under tests/cassettes/phase4/"
)
m2 = SOURCE_SHA_RE.search(text)
assert m2, f"{c.case_id}: case.toml missing `# Source cassette commit: <40-char SHA>`"
assert len(set(cassette_paths)) == 5, (
f"5 cases must derive from 5 distinct cassettes; got {len(set(cassette_paths))} distinct: {cassette_paths}"
)
# --- AC-9: bidirectional set equality directory-suffix ↔ curation_class ----
def test_directory_suffix_set_equals_curation_class_rag_set():
suffix_set = {p.name for p in CASES_ROOT.iterdir() if p.is_dir() and p.name.endswith("-rag-corpus-derived")}
class_set = {c.case_id for c in _load_rag_cases()}
assert suffix_set == class_set, (
f"directory-suffix set {suffix_set} != curation_class set {class_set}; "
f"a case has curation_class mistagged or a directory has the wrong suffix"
)
# --- AC-10: symlink-freeness defense-in-depth -------------------------------
def test_no_symlinks_anywhere_under_any_rag_case_directory():
for case_dir in _list_rag_dirs():
symlinks = [p for p in case_dir.rglob("*") if p.is_symlink()]
assert symlinks == [], (
f"{case_dir.name}: forbidden symlinks at "
f"{[p.relative_to(case_dir).as_posix() for p in symlinks]}"
)
# --- AC-11: README mapping table --------------------------------------------
def test_readme_documents_case_mapping_table_with_at_least_five_rag_rows():
readme_path = BENCH_ROOT / "vuln-remediation" / "README.md"
assert readme_path.is_file()
text = readme_path.read_text(encoding="utf-8")
# Locate the "Case mapping" section:
section_match = re.search(r"##\s+Case mapping\b.*?(?=^##\s|\Z)", text, re.DOTALL | re.MULTILINE)
assert section_match, "README missing `## Case mapping` section"
section = section_match.group(0)
# Extract markdown table rows (lines starting with `|` after a `|---|` separator):
table_rows = [line for line in section.splitlines() if line.strip().startswith("|") and "---" not in line and not line.strip().startswith("| case_id")]
# Filter only RAG-corpus-derived rows (defense if S5-04 also adds rows):
rag_rows = [r for r in table_rows if "rag-corpus-derived" in r]
assert len(rag_rows) >= 5, f"Case mapping table has {len(rag_rows)} RAG rows; expected ≥ 5"
# Each row's column-2 (source cassette) must reference tests/cassettes/phase4/:
for row in rag_rows:
assert "tests/cassettes/phase4/" in row, f"README row missing cassette path: {row!r}"
Run it; confirm BenchCaseLoadError (digests.yaml missing) or empty-rag-directories. Commit as the red marker.
Green — smallest impl shape¶
- Hand-build (or scaffold via S5-07 once it merges) 5 case directories under
bench/vuln-remediation/cases/, each withcase.toml,input/,expected/per §Implementation outline §3. - Compute
cassette_canary_pinvia the canonical AC-3a formula; record in eachcase.toml. - Compute
case_digestvia_compute_case_dir_digest(case_dir)(afterinput/+expected/populated); record in eachcase.toml. - Sign
bench/vuln-remediation/cases/digests.yamlper §Implementation outline §4. - Update
bench/vuln-remediation/README.mdper AC-11. - Iterate the integration test until all 13 ACs / 14 named tests are green.
Refactor — clean up¶
bench/vuln-remediation/README.mddocuments the selection criterion + the case-mapping table (per AC-11). Make the table sortable / scannable.- Each
case.toml's comment block follows the canonical 3-line shape (# curation_class per ADR-0006/# Derived from: .../# Source cassette commit: ...). - Sort the case directories alphabetically by
case_idon disk — the loader sorts anyway, but readable directory listing helps reviewers. - Per F-DP-1: when the second non-loader consumer of
_compute_case_dir_digestlands (S5-05'sscripts/sign_bench_digests.pyis the canonical trigger), open a follow-on story to promote it to publiccodegenie.eval.digests.compute_case_dir_digest. This story consumes the private name; the next story promotes.
Files to touch¶
| Path | Why |
|---|---|
bench/vuln-remediation/cases/001-<slug>-rag-corpus-derived/{case.toml, input/*, expected/*} |
New — first RAG-derived case |
bench/vuln-remediation/cases/002-<slug>-rag-corpus-derived/{case.toml, input/*, expected/*} |
New — second case |
bench/vuln-remediation/cases/003-<slug>-rag-corpus-derived/{case.toml, input/*, expected/*} |
New — third case |
bench/vuln-remediation/cases/004-<slug>-rag-corpus-derived/{case.toml, input/*, expected/*} |
New — fourth case |
bench/vuln-remediation/cases/005-<slug>-rag-corpus-derived/{case.toml, input/*, expected/*} |
New — fifth case |
bench/vuln-remediation/cases/digests.yaml |
New — signs the 5 RAG-corpus-derived cases per S2-02 §AC-6a (S5-04 appends its 5; S5-05 ships the re-sign script) |
bench/vuln-remediation/README.md |
Extend — "Case mapping" section + selection-criterion paragraph per AC-11 |
tests/integration/test_vuln_rag_corpus_derived_cases_load.py |
New — 14 named tests pinning all 13 ACs |
Out of scope¶
- The held-out 5 cases. S5-04 owns those (the long-pole). S5-04 will append 5 entries to the same
digests.yamlthis story creates. digests.yamlre-signing tooling. S5-05 shipsscripts/sign_bench_digests.pyas the canonical re-sign script (used after curator edits toinput/orexpected/). This story signs the initial 5 entries inline (because the loader cannot tolerate stubs); future re-signs use S5-05's script.- The E2E run. S5-05 exercises the cases through
codegenie eval runagainst the deterministic stub SUT. This story only asserts the 5 cases load viaload_cases. scripts/scaffold_bench_case.py. Built in S5-07; this story may or may not depend on it depending on merge order. If S5-07 hasn't merged, hand-build the 5 cases. Both paths must produce byte-identical artifacts (the canonical formulas in AC-3 + AC-3a are the contract).- Promotion verdict. With 5/10 cases, fence-CI #3 fails for any
min_cases_for_promotiontier ≥ silver (per ADR-0006); runningcodegenie eval run --task-class=vuln-remediationagainst the full bench requires S5-04 + S5-05's stub SUT. This story does not run the full pipeline. - Loader-side
input-pointer.tomlresolution. The escape hatch was rejected per F-CON-2; pointers are not supported by the HARDENED S2-02 loader. If portfolio-scale snapshots demand pointer support, that's a follow-on story amending S2-02 (an ADR-shaped change). - Promoting
_compute_case_dir_digestto a public name. Per F-DP-1, the promotion happens when the second non-loader consumer lands (likely S5-05). This story consumes the private helper; the promotion is a follow-on story's surgical edit. disposition="negative"RAG-corpus-derived cases. Structurally rejected (the cassette demonstrates a fix). If a curator finds a Phase 4 cassette where the SUT correctly refused to fix something, that's evidence for a held-out (S5-04) case, not a RAG-corpus-derived one.
Notes for the implementer¶
- Canonical case_digest algorithm — load-bearing. Use
_compute_case_dir_digestfromcodegenie.eval.loader(private helper; the import isfrom codegenie.eval.loader import _compute_case_dir_digest). DO NOT inline a re-implementation, no matter how tempting — the S2-02 HARDENED loader algorithm has five subtle invariants (POSIX relpath sort,case.tomlexclusion, symlink rejection,\x1f/\x1eframing, BLAKE3-once over joined records) that a hand-written version will reliably break. The testtest_case_digest_matches_canonical_algorithmwill catch divergence loudly. When the helper graduates to public per F-DP-1, the test's import switches in one line. - Canonical
cassette_canary_pinderivation. Until ADR-P4-006 ships (the Phase 4 amendment addingCanary.mint(seed=...)+ cassette canary-metadata emission), every Phase 4 cassette undertests/cassettes/phase4/lacks the metadata. The deterministic-derivation path is the ONLY path. The formula isblake3.blake3(f"phase4-cassette:{cassette_relpath_posix}".encode("utf-8")).hexdigest()[:32]wherecassette_relpath_posix = source_cassette.resolve().relative_to(REPO_ROOT).as_posix(). Thephase4-cassette:literal is the domain-separation prefix; do NOT change it (a future canary scheme will use a different prefix to avoid collisions). The testtest_cassette_canary_pin_matches_canonical_derivationre-derives and asserts byte-equality. commit_shaisNone; the derivation SHA lives in the comment block.BenchCase.commit_sha: str | NoneisNoneforsource="curated"per ADR-0006 §Consequences. The 40-char SHA documenting when the cassette was extracted into this case lives in the# Source cassette commit: <SHA>comment block at the top ofcase.toml. These are two distinct values; don't conflate them.- The rubric does NOT read
expected/. S5-02 HARDENED's rubric readsharness_outputonly.expected/is consumed by the SUT (Phase 6'sVulnRemediationSut.run_caseor S5-05's deterministic stub SUT). The shape ofexpected/(filenames, contents) is the SUT's contract; this story follows that contract, doesn't invent it. If S5-05's stub SUT hasn't defined the contract yet at the time this story executes, a sensible default isexpected/diff.patch+expected/validator_output.json(the Phase 7 + Phase 6 conventions); document the choice in the case.toml comment block and the README. - Mechanical derivation ≠ no review. Each case is CODEOWNERS-gated under
bench/vuln-remediation/cases/and must reflect a real solved cassette. Synthesized or fabricated inputs are not acceptable. - 5 distinct cassettes. The 5 source cassettes form a set of size 5 (test enforces). Avoid "5 cassettes that all fix the same CVE" — regression coverage is illusory.
- disposition = positive ≥ 4 of 5; never negative. RAG-corpus-derived cassettes demonstrate fixes; a
disposition="negative"(SUT should refuse to fix) is structurally incompatible. If a Phase 4 cassette where the pipeline correctly refused exists, that's S5-04 territory. difficultyskews easy. The cassette tree contains examples the pipeline already solved; honesty requires marking them easy unless a specific cassette captured edge-case behavior worth flagging. At least 4 of 5 must bedifficulty="easy".- Symlink hygiene.
input/andexpected/are real directories with regular files only. The loader will reject symlinks at load time (S2-02 §AC-9); the story'stest_no_symlinks_anywhere_under_any_rag_case_directorycatches it at the story-test boundary with a path-naming diagnostic. - No
input-pointer.toml. The pointer escape hatch was rejected per F-CON-2. Commit a minimal extracted pre-fix snapshot underinput/— only the files the recipe needs to read/touch, not the whole repo. If size becomes a real problem (>>1 MiB), defer the discussion to a follow-on story amending S2-02 to support pointers. - digests.yaml signs 5 here; S5-04 appends 5. The shape is
{case_id: "blake3:<64 hex>"}, sorted alphabetically by key. When S5-04 ships, the file grows to 10 entries; S5-05 ships the canonical re-sign script (scripts/sign_bench_digests.py). For now, sign inline using_compute_case_dir_digest+yaml.safe_dump(data, sort_keys=True). - F-DP-1 hand-off: promote
_compute_case_dir_digestto public when the next consumer lands. This story uses the private name. The next consumer of the canonical algorithm — likely S5-05'sscripts/sign_bench_digests.py— should be the trigger to extract a publiccodegenie.eval.digests.compute_case_dir_digest(new module). Surface this in the S5-05 / S5-04 implementer's notes when picking up those stories. - F-DP-2 hand-off: canary derivation Strategy seam. When ADR-P4-006 ships and Phase 4 cassettes re-cut with metadata, the canary derivation gains a second path (read-from-metadata). At that point: extract a
CanaryPinSourcesum type (Path | CassetteMetadata) and aderive_canary_pin(source) -> strsmart constructor. For now (one path), Rule 2 says no abstraction. yaml.safe_load,yaml.safe_dump. Neveryaml.load/yaml.dump— theforbidden-patternspre-commit hook bans the unsafe variants.- Determinism by construction. Two curators running the same scaffolder against the same source cassette must produce byte-identical
case.toml+input/+expected/.added_atis set once at curation time;last_validated_atis updated only at intentional re-validation. The canonical formulas (AC-3, AC-3a) are deterministic by construction; the curator's job is to not introduce non-determinism (nodatetime.now()baked into snapshot bytes, no machine-specific paths leaked into snapshots, etc.).