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:
parent
4df2140694
commit
7ba0fae933
1 changed files with 48 additions and 5 deletions
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue