"""Load-bearing tests for the shared method specification (S2, målbilde §8 — the fourth shared 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), 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 import dataclasses import json import re from pathlib import Path from portfolio_optimiser.ingest import Extraction, FileSource, HttpSource, ManifestV1, SqlSource from portfolio_optimiser.verdicts import ProposalFeatures, capture_verdict, verdict_to_dict REPO_ROOT = Path(__file__).resolve().parents[1] SPEC_PATH = REPO_ROOT / "shared" / "method-spec.md" INGEST_SPEC_PATH = REPO_ROOT / "shared" / "ingest-spec.md" SKILL_DIR = REPO_ROOT / "shared" / "skills" / "expert-reviewer" EXAMPLE_VERDICT = SKILL_DIR / "references" / "example-verdict.json" BUNDLE_DIR = REPO_ROOT / "shared" / "examples" / "bygg-energi-mikro" # Name-shaped framework guard (stricter than the persona test's import-shaped guard): the spec's # PROSE must never name a concrete agent framework or vendor stack — the same rule the persona # SKILL.md follows (CLAUDE.md §delt ekspert-persona). Scoped to the specs + the persona skill tree; # shared/README.md and the bundle legitimately NAME the two implementations and are exempt. _FRAMEWORK_NAMES = re.compile( r"\bagent[_ -]framework\b|\bMAF\b|\bClaude\b|\bAnthropic\b|\bMicrosoft\b|\bAzure\b" r"|\bFoundry\b|\bOpenAI\b|\bOllama\b|\bLangChain\b|\bAutoGen\b|\bSemantic Kernel\b" r"|\bCrewAI\b|\bMagentic\b", re.IGNORECASE, ) # The spec's required skeleton: the 8-step loop normative + one section per contract the session # plan names (verdict JSON, inbox/outbox, promotion gate, IR+golden as fasit, budget, provenance) # + the cross-check table the verification criterion requires. _REQUIRED_MARKERS = [ "# Method specification", "## 1. Scope", "## 3. The loop", "## 4. The verdict contract", "## 5. The inbox/outbox folder contract", "## 6. The promotion gate", "## 7. Ground truth", "## 8. Budget and stop criteria", "## 9. Provenance", "## 12. Cross-check table", ] # The ingest spec's required skeleton (I1, ingest-målbilde §2–§11): the contract sections both # stacks implement from — manifest, materialization, index generation, provenance, layer # separation, golden format — plus the cross-check table. _INGEST_REQUIRED_MARKERS = [ "# Ingest specification", "## 1. Scope", "## 2. Architecture", "## 3. Layer separation", "## 4. The ingest manifest", "## 5. Materialization", "## 6. Index generation", "## 7. Provenance", "## 8. Security", "## 9. The HITL gate", "## 10. Re-ingest", "## 11. Determinism", "## 12. Cross-check table", ] # 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 section is dropped.""" text = _spec_text() for marker in _REQUIRED_MARKERS: assert marker in text, f"method-spec.md is missing required section marker: {marker!r}" for step in range(1, 9): assert f"### Step {step} — " in text, f"method-spec.md must specify loop step {step}" # Normative, not descriptive: RFC-2119-style requirement language must be present. assert "MUST" in text, "the spec must be normative (RFC-2119 MUST language)" def test_method_spec_is_framework_neutral() -> None: """Test 2 (grep-shaped guard): the specs — and the persona skill tree they sit beside — never NAME a concrete agent framework or vendor stack. Mirrors the persona SKILL.md rule; stricter than the import-shaped guard because the specs are pure prose (no AST to parse). RED the moment a framework name leaks into the shared method prose. Covers BOTH shared specs: the method spec and the ingest spec (I1) — a new spec file is guarded explicitly, never implicitly.""" for f in [SPEC_PATH, INGEST_SPEC_PATH, *sorted(p for p in SKILL_DIR.rglob("*") if p.is_file())]: assert f.is_file(), f"expected shared artifact missing: {f}" hit = _FRAMEWORK_NAMES.search(f.read_text(encoding="utf-8")) assert hit is None, f"framework name {hit.group(0)!r} in shared method prose: {f}" def test_spec_documents_every_contract_field() -> None: """Test 3 (cross-check completeness): every field of the machine-readable contracts the pipeline consumes is documented in the spec — driven from the REAL artifacts and the REAL serializer, so the test goes RED when either the persona example, the golden suite, the IR projection, or the code's verdict serialization drifts away from the spec.""" text = _spec_text() def documented(field: str, source: str) -> None: assert f"`{field}`" in text, f"spec does not document {source} field `{field}`" # Persona-artifact contract: the canonical example verdict's fields, read from the artifact. example = json.loads(EXAMPLE_VERDICT.read_text(encoding="utf-8")) for field in example: documented(field, "example-verdict.json") # Serialized verdict contract: emitted by the REAL serializer (red on code drift). verdict = capture_verdict( ProposalFeatures( affected_codes=frozenset({"X.1"}), measure_type="m", claimed_saving_nok=1.0, description="d", ), "approved", "r", ) payload = verdict_to_dict(verdict) for field in payload: documented(field, "verdict file") for field in payload["proposal_features"]: documented(field, "verdict proposal_features") # IR projection + golden suite: the ground-truth contracts, read from the shared fixture # ("_"-prefixed keys are annotations, not contract). ir = json.loads((BUNDLE_DIR / "validator-input.json").read_text(encoding="utf-8")) for field in (k for k in ir if not k.startswith("_")): documented(field, "validator-input.json") for field in ir["affected_items"][0]: documented(field, "affected_items") golden = json.loads((BUNDLE_DIR / "golden.json").read_text(encoding="utf-8")) for section in ("validator", "learning_surface"): for field in (k for k in golden[section] if not k.startswith("_")): documented(field, f"golden.json {section}") # The decision vocabulary: binary on the run path; the gate additionally admits the seed's # approved_with_adjustment (and nothing else may claim otherwise). for value in ("approved", "rejected", "approved_with_adjustment"): 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 reservation (ingest-målbilde §3: manifest mapping may NEVER produce the promotion gate's layer). RED before the artifact exists, when a required section is dropped, or when the reservation stops being stated. The field-level cross-check (spec ↔ manifest contract code) arrives with the I2 contracts, mirroring test 3.""" assert INGEST_SPEC_PATH.is_file(), "shared/ingest-spec.md missing (the I1 shared artifact)" text = INGEST_SPEC_PATH.read_text(encoding="utf-8") for marker in _INGEST_REQUIRED_MARKERS: assert marker in text, f"ingest-spec.md is missing required section marker: {marker!r}" # Normative, not descriptive: RFC-2119-style requirement language must be present. assert "MUST" in text, "the ingest spec must be normative (RFC-2119 MUST language)" # The verdict-layer reservation is load-bearing prose: both the reserved OKF type and the # reserved filename namespace must be stated verbatim. assert "`type: verdict`" in text, "ingest spec must state the reserved verdict OKF type" assert "promoted-verdict-" in text, "ingest spec must state the reserved filename namespace" # The determinism anchor: the explicit-timestamp rule must be visible at field level. assert "`ingested_at`" in text, "ingest spec must document the explicit timestamp field" def test_ingest_spec_documents_every_contract_field() -> None: """Test 5 (I2 — discharges test 4's deferred note): every field of the REAL ingest manifest contract is documented in the ingest spec — driven from the pydantic models' ``model_fields`` (all three polymorphic source variants + the extraction shape), the seven §5/§7 provenance frontmatter keys, and the §11 golden-case entries. Mirrors test 3: the test goes RED the moment the contract code and the spec's §12 cross-check table drift apart (a new model field without a spec entry, a renamed frontmatter key, a changed golden layout).""" text = INGEST_SPEC_PATH.read_text(encoding="utf-8") def documented(field: str, source: str) -> None: assert f"`{field}`" in text, f"ingest spec does not document {source} field `{field}`" for field in (f.name for f in dataclasses.fields(ManifestV1)): documented(field, "manifest top-level") for model, source in ( (FileSource, "file source"), (SqlSource, "sql source"), (HttpSource, "http source"), (Extraction, "extraction"), ): for field in (f.name for f in dataclasses.fields(model)): documented(field, source) # The library's source models consume the `type` discriminator during validation dispatch # instead of storing it as a field, so it is no longer reachable by introspection. It is a # REAL §4 contract field, so it is asserted explicitly — without this line the swap from # `model_fields` to `dataclasses.fields` would silently drop it from the cross-check. documented("type", "source discriminator") # The §5/§7 provenance layer — exactly the keys the materializer stamps. for key in ( "type", "title", "source_system", "source_query", "ingested_at", "ingest_manifest", "generated", ): documented(key, "provenance frontmatter") # The §11 golden extraction case layout. for entry in ("manifest.json", "fixture/", "ingested-at.txt", "expected-bundle/"): documented(entry, "golden extraction case")