test(spec-guard): extend framework guard + structure test to ingest-spec.md (I1)

The neutrality guard now covers BOTH shared specs explicitly (a new spec
file is never guarded implicitly), and a new structure test pins the
ingest spec's normative skeleton incl. the verdict-layer reservation and
the explicit-timestamp field. Proven RED against a throwaway copy with a
framework name injected and with a required section dropped; field-level
cross-check vs contract code arrives with the I2 contracts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AaQCFnfsh3tfq1VfzdJpoi
This commit is contained in:
Kjell Tore Guttormsen 2026-07-03 14:05:39 +02:00
commit 7ba0fae933

View file

@ -20,13 +20,14 @@ from portfolio_optimiser.verdicts import ProposalFeatures, capture_verdict, verd
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 spec + the persona skill tree;
# 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"
@ -52,6 +53,26 @@ _REQUIRED_MARKERS = [
]
# 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")
@ -71,11 +92,12 @@ def test_method_spec_exists_with_required_structure() -> None:
def test_method_spec_is_framework_neutral() -> None:
"""Test 2 (grep-shaped guard): the spec — and the persona skill tree it sits beside — never
"""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 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())]:
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}"
@ -129,3 +151,24 @@ def test_spec_documents_every_contract_field() -> None:
# 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"