From 7ba0fae933d90d5c30e9bc39f11183a61acc095c Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Fri, 3 Jul 2026 14:05:39 +0200 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01AaQCFnfsh3tfq1VfzdJpoi --- tests/test_method_spec_loadbearing.py | 53 ++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 5 deletions(-) diff --git a/tests/test_method_spec_loadbearing.py b/tests/test_method_spec_loadbearing.py index 904463f..93903b7 100644 --- a/tests/test_method_spec_loadbearing.py +++ b/tests/test_method_spec_loadbearing.py @@ -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"