215 lines
10 KiB
Python
215 lines
10 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.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")
|