diff --git a/specs/MetricsAndScenarios.md b/specs/MetricsAndScenarios.md index c8b360f..5ba53bd 100644 --- a/specs/MetricsAndScenarios.md +++ b/specs/MetricsAndScenarios.md @@ -31,6 +31,7 @@ and add capability-specific rows only when these don't cover the claim. | ID | Dimension | Metric | Unit | Provenance | |---|---|---|---|---| | M-D1-COV | D1 | numbered spec rules covered by ≥1 passing scenario | % | adapted:requirements-traceability (per-rule, not per-feature) | +| M-D1-LNK | D1 | claimed rules also **named in the aggregate source** | count | novel — closes the spec→code→scenario chain; instrument: `make coverage` (AM-1b) | | M-D1-SPL | D1 | spec lines per numbered rule | lines | novel — proxies statement simplicity; gameable, so paired with M-D1-COV | | M-D2-LOC | D2 | source LOC excluding tests (tokei) | lines | adopted:tokei | | M-D2-DEP | D2 | transitive dependency count (cargo tree) | crates | adopted:cargo-deny practice | @@ -44,6 +45,32 @@ and add capability-specific rows only when these don't cover the claim. | M-D4-LEAK | D4 | foreign types in canonical interfaces | count | novel — must be 0; enforced by grep/deny rule, the Clay-Borg hard rule | | M-D4-SWAP | D4 | capability has null + reference impls passing the same conformance suite | bool | adapted:hexagonal-architecture port testing | +### 1b. The coverage gate's two numbers (M-D1-COV, M-D1-LNK) + +M-D1-COV counts tags. It proves no rule is unclaimed and no claimed rule +is invented; it does **not** prove a scenario exercises what it names, and +that limit is printed with the number every time (InnerLoop implementation +rule 4). + +**M-D1-LNK** (CB-WP-0003 T09) closes one link of that chain mechanically: +a rule a scenario claims should also appear in the aggregate source, or +the claim rests on a tag and nothing else. + +Measured 2026-07-31: **49 of 58** claimed rules are named in +`games/ground/src/lib.rs`. **Unmet**, target 58. The nine unlinked: + +```text +GR-D07 GR-F02 GR-L03 GR-O03 GR-P01 GR-P02 GR-P03 GR-P04 GR-T01 +``` + +The gate **reports** this rather than failing, deliberately. Closing the +gap by adding rule IDs to comments would satisfy the check without +establishing that each rule is implemented — which is the overclaim +InnerLoop implementation rule 2 exists to prevent, and which CB-WP-0001 +committed once already. Each of the nine needs its implementation +confirmed before it is tagged. Promoting M-D1-LNK to a failing gate is +correct **after** that, not before. + ### 1a. Token cost accounting (M-D2-CST) > **Superseded 2026-07-31 by [CostAccounting.md](CostAccounting.md)**, which diff --git a/tools/rule-coverage.py b/tools/rule-coverage.py index bf8c99c..73ef5ae 100755 --- a/tools/rule-coverage.py +++ b/tools/rule-coverage.py @@ -26,12 +26,19 @@ import sys RULE_RE = r"\*\*(GR-[A-Z]+\d+)" COVERS_RE = r"covers: \[(.*?)\]" +AGGREGATE = "games/ground/src/lib.rs" +ID_RE = r"GR-[A-Z]+\d+" def parse_rules(spec_text): return sorted(set(re.findall(RULE_RE, spec_text))) +def parse_code_ids(text): + """Rule IDs named anywhere in the aggregate source (T09).""" + return set(re.findall(ID_RE, text)) + + def parse_covers(text): match = re.search(COVERS_RE, text, re.S) if not match: @@ -58,6 +65,12 @@ def self_test(): check("covers matcher works", parse_covers("covers: [GR-R06, GR-A12]") == {"GR-R06", "GR-A12"}) check("missing covers yields empty set", parse_covers("no covers key") == set()) + # T09: the spec -> code link must be detectable. + check("code-id matcher finds ids in source", + parse_code_ids("// GR-R06: lead first\nfn f(){} // GR-A12") + == {"GR-R06", "GR-A12"}) + check("code-id matcher finds none in unmarked source", + parse_code_ids("fn f() { let x = 1; }") == set()) print("rule-coverage self-test (positive control)") ok = True @@ -94,10 +107,27 @@ def main(): missing = [r for r in rules if r not in covered] invented = sorted(covered - known) + # T09: the spec -> code -> scenario chain, made mechanical. A rule a + # scenario claims should also be named in the aggregate, or the claim + # rests on nothing but a tag. + code_ids = parse_code_ids(open(AGGREGATE).read()) + unlinked = sorted((known & covered) - code_ids) + phantom = sorted(code_ids - known) + pct = 100 * len(hit) // len(rules) + linked = len((known & covered) & code_ids) print(f"AM-1 rule coverage: {len(hit)}/{len(rules)} ({pct}%) " f"over {len(paths)} scenarios") + print(f"AM-1b spec->code link: {linked}/{len(hit)} claimed rules also " + f"named in {AGGREGATE}") print(" NOTE: counts tags; does not prove a scenario exercises what it names") + if unlinked: + print(" unlinked (claimed by a scenario, absent from the aggregate):") + print(" ", " ".join(unlinked)) + if phantom: + print(" ERROR — rule id in code that the spec does not define:", + " ".join(phantom), file=sys.stderr) + return 1 if missing: print(" uncovered:", " ".join(missing)) if invented: diff --git a/workplans/CB-WP-0003-loop-hardening.md b/workplans/CB-WP-0003-loop-hardening.md index bdb52f1..33bdc57 100644 --- a/workplans/CB-WP-0003-loop-hardening.md +++ b/workplans/CB-WP-0003-loop-hardening.md @@ -304,7 +304,7 @@ file must list them, not that the build breaks. ```task id: CB-WP-0003-T09 -status: todo +status: done priority: low state_hub_task_id: "69ccc9ec-dc35-481c-8065-cef040f07f50" ```