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:
parent
a831aa1e3b
commit
8e9f385d17
1 changed files with 127 additions and 4 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue