portfolio-optimiser/tests/test_method_spec_loadbearing.py

215 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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.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",
]
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 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_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 ManifestV1.model_fields:
documented(field, "manifest top-level")
for model, source in (
(FileSource, "file source"),
(SqlSource, "sql source"),
(HttpSource, "http source"),
(Extraction, "extraction"),
):
for field in model.model_fields:
documented(field, source)
# 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")