Author shared/method-spec.md: the 8-step loop (normative, RFC-2119), the verdict JSON contract incl. the id-minting algorithm and the chosen conflict semantics, the inbox/outbox folder contract, the fail-closed promotion-gate semantics, the IR projection + golden suite as the only ground truth (incl. the reproducible Monte Carlo procedure), and the budget/stop, provenance and startup-contract requirements — every normative claim cross-checked against the load-bearing tests/code. The sibling implementation builds from this spec alone. Load-bearing trio (tests/test_method_spec_loadbearing.py, persona-trio style): required structure, a name-shaped framework-neutrality guard over the spec + the persona skill tree, and a cross-check-completeness test driven from the REAL artifacts and the REAL verdict serializer (red on code drift). All three detach points proven RED (missing file / framework name / dropped field). shared/README.md: the "(planned)" line replaced with the real entry. Suite 152 -> 155 passed / 4 skipped; ruff check+format clean; mypy src clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AaQCFnfsh3tfq1VfzdJpoi
131 lines
6.3 KiB
Python
131 lines
6.3 KiB
Python
"""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), 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.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
from pathlib import Path
|
|
|
|
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"
|
|
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 spec + 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",
|
|
]
|
|
|
|
|
|
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 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 spec — and the persona skill tree it sits beside — never
|
|
NAME a concrete agent framework or vendor stack. Mirrors the persona SKILL.md rule; stricter
|
|
than the import-shaped guard because the spec is pure prose (no AST to parse). RED the moment
|
|
a framework name leaks into the shared method prose."""
|
|
for f in [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")
|