feat(persona): load the falsification skill from commons at call time

[skip-docs] — the invariant row for this plan lands in Step 13, after the mutations.

Amendment 2 in the same commit: the framework-neutrality sweep covered only
expert-reviewer, so the skill that arrived by subtree pull had no framework guard
anywhere. GUARDED_SKILL_DIRS names both, and a fail-closed coverage arm turns red
when a future pull brings a third skill that is not listed.

Co-Authored-By: Claude <claude-opus-5>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-02 21:31:32 +02:00
commit 488a0f2b6c
3 changed files with 276 additions and 3 deletions

View file

@ -26,8 +26,15 @@ 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"
SKILLS_ROOT = REPO_ROOT / "shared" / "skills"
SKILL_DIR = SKILLS_ROOT / "expert-reviewer"
EXAMPLE_VERDICT = SKILL_DIR / "references" / "example-verdict.json"
#: Every shared Agent Skill the framework-neutrality sweep covers, ENUMERATED rather than globbed —
#: "a new spec file is guarded explicitly, never implicitly", the rule this file already states for
#: the two specs. The enumeration is kept honest by the coverage arm at the bottom of this module:
#: a third skill arriving in a subtree pull turns THAT red until it is listed here, so explicit
#: never degrades into stale (the ``_LIVE_DOCS`` coverage pattern).
GUARDED_SKILL_DIRS = (SKILL_DIR, SKILLS_ROOT / "falsification-reviewer")
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
@ -190,8 +197,15 @@ def test_method_spec_is_framework_neutral() -> None:
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())]:
and the ingest spec (I1) a new spec file is guarded explicitly, never implicitly.
Amendment 2 (2026-09-02) widened the skill half from ``expert-reviewer`` alone to
``GUARDED_SKILL_DIRS``: the falsification-reviewer skill arrived by subtree pull with NO
framework guard anywhere, and a sweep pinned to one skill silently stops covering the tree it
claims to cover."""
guarded = [f for d in GUARDED_SKILL_DIRS for f in sorted(d.rglob("*")) if f.is_file()]
assert guarded, "the sweep found no skill files — it would be green by construction"
for f in [SPEC_PATH, INGEST_SPEC_PATH, *guarded]:
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}"
@ -342,3 +356,23 @@ def test_ingest_spec_documents_every_contract_field() -> None:
# The §11 golden extraction case layout.
for entry in ("manifest.json", "fixture/", "ingested-at.txt", "expected-bundle/"):
documented(entry, "golden extraction case")
def test_every_shared_skill_is_covered_by_the_framework_sweep() -> None:
"""Fail-closed coverage for Amendment 2's enumeration.
``GUARDED_SKILL_DIRS`` is written out by hand, following this file's own rule that a new shared
artefact is guarded explicitly rather than by a glob that quietly absorbs it. The cost of an
explicit list is that it goes stale, and ``shared/`` is a PULL-ONLY subtree a third skill can
arrive here without anyone in this repo deciding to add it. This arm makes staleness LOUD: the
enumeration must name every directory under ``shared/skills/``, so the next pull that brings a
skill turns this red until it is listed. Mirrors
``test_every_document_is_classified_live_or_archive``.
"""
on_disk = {d for d in SKILLS_ROOT.iterdir() if d.is_dir()}
assert on_disk, "no shared skills found — the control cannot distinguish covered from empty"
missing = sorted(d.name for d in on_disk - set(GUARDED_SKILL_DIRS))
assert not missing, (
"shared skills not named in GUARDED_SKILL_DIRS, so the framework sweep does not reach "
f"them: {missing}"
)