test(spec): make the pulled method spec load-bearing at rule level, not just section level

The spec arrives by subtree pull from commons, so drift is something we RECEIVE
rather than author. The existing guard only asserted the section HEADINGS were
present: a pull could empty a section of its normative content and stay green.
Measured, not assumed — with `escape, not depth` deleted from Step 1, the old
skeleton test still passes (M5 below).

Two new guards, one file:

- Rule level (test 6): 25 normative rules, each bound to the section that OWNS
  it, so a phrase surviving elsewhere in the document does not count as the spec
  still stating the rule. Selection is principled rather than taste — every entry
  anchors a §11 conformance seam or a CLAUDE.md invariant. Matching is
  whitespace- and emphasis-insensitive: re-wrapping a paragraph upstream is an
  editorial change, not a rule change, and must not cost a false red.
- §11 table (test 7): the conformance table still names the seams this suite
  implements, and every test it cites exists here. One-directional by design —
  the repo carries 26 load-bearing tests, the spec anchors 10, because the spec
  governs the METHOD, not this repo.

Five measurements, all as expected: M1 delete a rule → RED · M2 RELOCATE a rule
out of its section while leaving it in the document → RED (this is what the
section binding buys) · M3 drop a seam row from §11 → RED · M4 harmless re-wrap
+ de-bolding → GREEN · M5 the old skeleton test does NOT catch M1 → GREEN.
Spec restored from a byte copy after each mutation, shasum verified.

Known gap, deliberately not patched here: §11 has no row for the portfolio-wide
budget seam (S3.4/F10) — the pulled spec predates it. That is a commons
amendment; a consumer editing shared/ is exactly the drift this guards against.

553 → 555 tests, ruff + mypy green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aw9TECznT5b5H6374CBTvV
This commit is contained in:
Kjell Tore Guttormsen 2026-08-01 19:57:33 +02:00
commit 8e9f385d17

View file

@ -4,10 +4,13 @@ artifact).
The method spec is the normative, framework-neutral document the sibling implementation is built
FROM it must be implementable from the spec alone, without reverse-engineering this repo's code.
These tests make the artifact load-bearing in the persona-trio's style: structure + framework
neutrality (grep-shaped), and a cross-check that every field of the machine-readable contracts the
pipeline actually consumes is documented in the spec. Each test goes RED when its seam is detached:
the spec goes missing, prose starts naming a concrete framework, or the code's verdict contract
drifts away from what the spec documents.
neutrality (grep-shaped), a cross-check that every field of the machine-readable contracts the
pipeline actually consumes is documented in the spec, and because the spec arrives here by
subtree pull rather than by local edit a rule-level guard that the sections still STATE the
normative rules this repo is built against. Each test goes RED when its seam is detached: the spec
goes missing, prose starts naming a concrete framework, the code's verdict contract drifts away
from what the spec documents, a normative rule is deleted or relocated out of its owning section,
or the §11 conformance table stops naming the seams this suite implements.
"""
from __future__ import annotations
@ -75,11 +78,100 @@ _INGEST_REQUIRED_MARKERS = [
]
# The rules this repo's invariants stand on, each bound to the section that OWNS it. The skeleton
# markers above only prove the HEADINGS survive: a commons pull could empty a section of its
# normative content and stay green, which is exactly how the shared contract drifts away from the
# code built against it. Selection is not taste — every entry anchors either a §11 conformance seam
# or a CLAUDE.md invariant, and the section binding means a phrase that merely survives elsewhere in
# the document does not count as the spec still stating the rule.
#
# A RED here means "read the commons diff", not "the code is broken": re-wrapping and emphasis are
# normalised away (below), so only a changed or deleted RULE trips it.
_REQUIRED_RULES = [
# (section prefix, rule phrase)
("## 1. Scope", "Honesty rule (unwaivable)"),
# §11 "Navigation boundary" — the hierarchical contract F4 re-grounded (CLAUDE.md).
("### Step 1 — ", "bundle root"),
("### Step 1 — ", "escape, not depth"),
("### Step 1 — ", "depth-first in first-seen link order"),
("### Step 1 — ", "de-duplicated on the resolved path"),
("### Step 1 — ", "SOLE in-/out-of-bundle test"),
("### Step 1 — ", "flat regardless of nesting depth"),
# §11 "Verdict-layer exclusion" — a per-file type check, never a link-graph property.
("### Step 1 — ", "type check on each file as it is reached"),
# §11 "Step-1 fold" — structural ranking is what keeps surface text out of similarity.
("### Step 1 — ", "structural, never textual"),
# §11 "Checker gate" — the marker, the mandatory validator, and the two-falsifier separation.
("### Step 3 — ", "VERDICT: REJECT"),
("### Step 4 — ", "mandatory, blocking, never an optional plugin"),
("### Step 4 — ", "opt-in-reject (fail-open)"),
("### Step 4 — ", "mirrors ONLY the validator"),
# §11 "Informed refinement" — most-recent-only, reason-only, under the existing caps.
("### Step 5 — ", "Only the most recent rejection reason"),
("### Step 5 — ", "never the prior proposal JSON"),
("### Step 5 — ", "no new loop may be introduced"),
# §11 "Async file loop" — the role split, merge semantics, and the tolerant raw layer.
("### Step 7 — ", "the system READS the inbox; the expert writes it"),
("## 5. The inbox/outbox", "Merge, never replace"),
("## 5. The inbox/outbox", "SKIPPED, never raised"),
# §11 "Promotion gate" — fail-closed, neutral label, explicit timestamp.
("## 6. The promotion gate", "Fail-closed:"),
("## 6. The promotion gate", "index link label is FIXED and carries NO verdict signal"),
("## 6. The promotion gate", "no wall-clock default"),
# §8 — the normative anchor under the budget invariant (S3.4/F10).
("## 8. Budget", "never a word-count or character proxy"),
("## 8. Budget", "structured stop event"),
# §7.1 — the fail-fast required input, the deliberate contrast to the tolerant inbox above.
("### 7.1", "FAIL-FAST"),
]
# The §11 rows this repo's suite is anchored in. A bare count would be arbitrary AND satisfiable by
# the wrong rows; naming the seams is what makes a dropped row RED. Deliberately one-directional:
# the repo carries far more load-bearing tests than the spec anchors (the spec governs the METHOD,
# not this repo), so only these are required to appear.
_REQUIRED_CONFORMANCE_TESTS = [
"test_bygg_energi_mikro.py",
"test_checker_gate_loadbearing.py",
"test_okf.py",
"test_persona_skill_loadbearing.py",
"test_simulation_loadbearing.py",
"test_step1_expel_loadbearing.py",
"test_step5_refine_loadbearing.py",
"test_step7_async_loop_loadbearing.py",
"test_step8_promotion_loadbearing.py",
]
def _spec_text() -> str:
assert SPEC_PATH.is_file(), "shared/method-spec.md missing (the fourth shared artifact)"
return SPEC_PATH.read_text(encoding="utf-8")
def _flat(text: str) -> str:
"""Whitespace- and emphasis-insensitive view of the prose: re-wrapping a paragraph or bolding a
clause is an editorial change upstream, not a rule change, and must not turn the guard red."""
return re.sub(r"\s+", " ", text.replace("*", ""))
def _section_body(text: str, prefix: str) -> str:
"""The body owned by the heading starting with ``prefix`` (``##`` and ``###`` alike), so a rule
can be anchored to its own section. Asserting exactly one match also catches a duplicated
heading, which would make "which section states this rule?" ambiguous."""
bodies: dict[str, list[str]] = {}
current: str | None = None
for line in text.splitlines():
if line.startswith(("## ", "### ")):
current = line.strip()
bodies.setdefault(current, [])
elif current is not None:
bodies[current].append(line)
matches = [lines for head, lines in bodies.items() if head.startswith(prefix)]
assert len(matches) == 1, (
f"expected exactly one section starting {prefix!r}, got {len(matches)}"
)
return "\n".join(matches[0])
def test_method_spec_exists_with_required_structure() -> None:
"""Test 1: the spec exists and carries the normative skeleton — the 8 steps plus every
contract section the session plan names. RED before the artifact exists, or when a required
@ -155,6 +247,37 @@ def test_spec_documents_every_contract_field() -> None:
documented(value, "decision vocabulary")
def test_method_spec_states_its_load_bearing_rules() -> None:
"""Test 6 (rule-level integrity — closes test 1's gap): the spec still STATES each normative
rule this repo is built against, inside the section that owns it. Test 1 proves only that the
headings survive, so a commons pull could empty a section of its rules and stay green. RED when
a rule is deleted, reworded away, or relocated out of its own section the three ways the
pulled contract silently stops saying what the code assumes."""
text = _spec_text()
for section, rule in _REQUIRED_RULES:
body = _flat(_section_body(text, section))
assert rule in body, (
f"method-spec.md section {section!r} no longer states the rule: {rule!r}"
)
def test_method_spec_conformance_table_cites_the_real_suite() -> None:
"""Test 7 (§11 integrity): the load-bearing conformance table still names the seams this repo's
suite implements, and every test it cites actually exists here. RED when a seam row is dropped
upstream, and RED when a cited test is deleted or renamed here the table is the shared
contract for WHICH seams must be provable, so both directions of drift are findings.
(Known, deliberate gap: the portfolio-wide budget seam (S3.4/F10) has no §11 row yet the
pulled spec predates it. That is a commons amendment, not something this consumer may patch.)
"""
section = _section_body(_spec_text(), "## 11. Load-bearing conformance tests")
cited = set(re.findall(r"`(test_[a-z0-9_]+\.py)`", section))
for name in _REQUIRED_CONFORMANCE_TESTS:
assert name in cited, f"method-spec.md §11 no longer cites the seam test {name!r}"
for name in sorted(cited):
assert (REPO_ROOT / "tests" / name).is_file(), f"§11 cites a test this repo lacks: {name}"
def test_ingest_spec_exists_with_required_structure() -> None:
"""Test 4 (I1): the ingest spec exists in shared/ and carries its normative skeleton — the
contract sections both stacks implement the ingest layer from, incl. the verdict-layer