STATE pkt. 2 scoped a measurement of the substring guards against tmp_path-
GENERATED artefacts. Measured, not reasoned: every one of the 18 assertions
behind those 11 line refs was detached for real and each is individually
load-bearing. Mutation matrix (src/lib mutated in place, restored + sha-verified,
`git status` clean before and after):
M1 render_table drops rows -> ingest_lb:91, sql_lb:104,105 RED
M2 SQL NULL -> naive str() "None" -> sql_lb:61,62 RED
M3 whole REAL loses its .0 -> sql_lb:69 RED
M4 _update_index_lines over-reaches -> ingest_lb:165,166,189 sql:162 RED
M5 _update_index_lines under-reaches -> ingest_lb:188 (negative) RED
M6 _link_in_index no-op -> ingest_lb:169, sql_lb:164 RED
M7 collision gate clobbers first -> test_ingest:141 RED
M8 index label leaks the rationale -> step8:179,180,194 (negative) RED
M9 index label varies per verdict -> step8:186,187,188 RED
M10 re-promotion double-links -> step8:170 RED
M11 fold drops the rationale prose -> step8:151 RED
M12 seeding re-mints the verdict id -> step8:163,164 RED
A second pass was required because pytest stops at the FIRST failing assert:
six assertions sat behind a failing one and were therefore unmeasured at test
level. Re-run with the preceding assertion neutralised, each of those six is
load-bearing too (ingest_lb:91-B, :166; sql_lb:62, :105; step8:180, :164).
The finding is structural, and it is the reason this commit is not empty. Five
NEGATIVE assertions carried no positive control, so they measure an absence
without ever establishing the presence. Proven by value-proof (not merely a red
proof): under a plausible drift — `_link_in_index` detached, or `description`
stopped carrying the rationale — all three tests stayed GREEN with the control
removed and go RED with it present. green-without / red-with is what makes these
controls value-adding rather than decorative.
test_ingest_loadbearing.py the ingest-edge link is asserted PRESENT, in
exactly the form the removal assertion seeks
test_step8_promotion_loadbearing the marker/rationale are asserted live in the
promoted file before the index/context
exclusions are allowed to mean anything
Next lens, enumerated rather than assumed: the class reaches 23 test files, not
the 4 STATE named — ~34 negative substring assertions in total. "Negative without
a positive control" is the sharp, cheap successor to "substring assertion".
Suite 688 passed; ruff + ruff format + mypy --strict clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PzEtJzL6SKYbYtSQRY5o57
194 lines
9.2 KiB
Python
194 lines
9.2 KiB
Python
"""Load-bearing ingest seams (ingest-spec §11) — D7 mirror of MAF I2's set.
|
|
|
|
Each test must go RED when its seam is detached (the method-spec §11 regime): a
|
|
grønn-men-død test is the failure mode the rule exists for. The seams mirrored here,
|
|
proven through the consumer seam (``portfolio_optimiser_claude.ingest``, backed by
|
|
llm-ingestion-okf since the adoption):
|
|
|
|
- Provenance stamping — a generated file carries the §7 provenance layer, in order.
|
|
- Navigability — the generated bundle is consumable by the UNCHANGED ``okf`` navigation
|
|
(index links included), incl. the frontmatter-less generated index.
|
|
- Verdict reservation — a manifest mapping to ``type: verdict`` is rejected fail-fast,
|
|
before any source call.
|
|
- Re-ingest layer safety — re-materialization over a bundle carrying a promoted verdict
|
|
preserves the verdict file AND its index link (curated content always survives).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
import shutil
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from portfolio_optimiser_claude import okf
|
|
from portfolio_optimiser_claude.ingest import ManifestError, load_manifest, materialize
|
|
|
|
GOLDEN = Path(__file__).resolve().parents[1] / "examples" / "ingest-golden-file"
|
|
INGESTED_AT = (GOLDEN / "ingested-at.txt").read_text(encoding="utf-8").strip()
|
|
|
|
_PROVENANCE_KEYS = ("source_system", "source_query", "ingested_at", "ingest_manifest", "generated")
|
|
|
|
|
|
def _expected_stamp(manifest_path: Path) -> str:
|
|
# The §5 stamp rule, recomputed INDEPENDENTLY of the implementation:
|
|
# ``{manifest stem}@{sha256(raw bytes)[:16]}`` — RED if the stamping detaches.
|
|
digest = hashlib.sha256(manifest_path.read_bytes()).hexdigest()[:16]
|
|
return f"{manifest_path.stem}@{digest}"
|
|
|
|
|
|
def _materialized(tmp_path: Path) -> Path:
|
|
bundle = tmp_path / "bundle"
|
|
bundle.mkdir()
|
|
materialize(GOLDEN / "manifest.json", bundle, INGESTED_AT)
|
|
return bundle
|
|
|
|
|
|
class TestProvenanceStamping:
|
|
"""Seam: a generated file carries the §7 provenance layer (RED if it stops)."""
|
|
|
|
def test_generated_file_carries_the_provenance_layer(self, tmp_path: Path) -> None:
|
|
bundle = _materialized(tmp_path)
|
|
concept = okf.parse_concept_file(bundle / "ingest-costs.md")
|
|
for key in _PROVENANCE_KEYS:
|
|
assert key in concept.frontmatter, f"provenance key {key} detached"
|
|
assert concept.frontmatter["source_system"] == "prosjekt-arkiv"
|
|
assert concept.frontmatter["ingested_at"] == INGESTED_AT
|
|
assert concept.frontmatter["generated"] == "true"
|
|
assert concept.frontmatter["ingest_manifest"] == _expected_stamp(GOLDEN / "manifest.json")
|
|
|
|
def test_provenance_keys_are_in_the_spec_order(self, tmp_path: Path) -> None:
|
|
# §5: exactly these keys, in exactly this order — the chain is a contract.
|
|
bundle = _materialized(tmp_path)
|
|
lines = (bundle / "ingest-costs.md").read_text(encoding="utf-8").splitlines()
|
|
keys = [ln.split(":", 1)[0] for ln in lines[1 : lines.index("---", 1)]]
|
|
assert keys == [
|
|
"type",
|
|
"title",
|
|
"source_system",
|
|
"source_query",
|
|
"ingested_at",
|
|
"ingest_manifest",
|
|
"generated",
|
|
]
|
|
|
|
|
|
class TestNavigability:
|
|
"""Seam: the generated bundle is consumable by the UNCHANGED okf navigation."""
|
|
|
|
def test_generated_bundle_navigates_via_unchanged_okf(self, tmp_path: Path) -> None:
|
|
bundle = _materialized(tmp_path)
|
|
names = [c.path.name for c in okf.navigate_bundle(bundle)]
|
|
# Every generated ingest file must be REACHABLE via index cross-links — an
|
|
# unlinked generated file is unreachable (RED if index linking detaches).
|
|
assert names == ["index.md", "ingest-costs.md", "ingest-edge.md"]
|
|
|
|
def test_generated_context_renders_the_extracted_rows(self, tmp_path: Path) -> None:
|
|
bundle = _materialized(tmp_path)
|
|
context = okf.bundle_context(bundle)
|
|
assert "led-retrofit" in context and "requires vendor quote" in context
|
|
|
|
def test_generated_index_is_frontmatterless_and_still_navigates(self, tmp_path: Path) -> None:
|
|
# The generated index carries no frontmatter (§6 shape) — navigation must
|
|
# still work, exercising the okf entry-point relaxation end to end.
|
|
bundle = _materialized(tmp_path)
|
|
assert not (bundle / "index.md").read_text(encoding="utf-8").startswith("---")
|
|
assert okf.navigate_bundle(bundle)[0].path.name == "index.md"
|
|
|
|
|
|
class TestVerdictReservation:
|
|
"""Seam: an ingest mapping to the verdict layer is rejected fail-fast (§3)."""
|
|
|
|
def _manifest(self, okf_type: str) -> dict:
|
|
return {
|
|
"manifest_version": 1,
|
|
"source": {"type": "file", "id": "x", "root": "fixture"},
|
|
"bundle_summary": "s",
|
|
"extractions": [
|
|
{"id": "e", "title": "T", "query": "e.csv", "okf_type": okf_type, "max_rows": 1}
|
|
],
|
|
}
|
|
|
|
def _write(self, tmp_path: Path, okf_type: str) -> Path:
|
|
manifest = tmp_path / "manifest.json"
|
|
manifest.write_text(json.dumps(self._manifest(okf_type)), encoding="utf-8")
|
|
return manifest
|
|
|
|
def test_verdict_okf_type_is_rejected(self, tmp_path: Path) -> None:
|
|
with pytest.raises(ManifestError):
|
|
load_manifest(self._write(tmp_path, "verdict"))
|
|
|
|
def test_verdict_reservation_is_case_insensitive(self, tmp_path: Path) -> None:
|
|
with pytest.raises(ManifestError):
|
|
load_manifest(self._write(tmp_path, "Verdict"))
|
|
|
|
def test_rejection_is_fail_fast_before_any_source_call(self, tmp_path: Path) -> None:
|
|
# A verdict manifest never touches the source: no bundle is written.
|
|
manifest = self._write(tmp_path, "verdict")
|
|
bundle = tmp_path / "bundle"
|
|
with pytest.raises(ManifestError):
|
|
materialize(manifest, bundle, INGESTED_AT)
|
|
assert not bundle.exists() or not any(bundle.iterdir())
|
|
|
|
|
|
class TestReingestLayerSafety:
|
|
"""Seam: re-ingest preserves a promoted verdict AND its index link (§3, §6)."""
|
|
|
|
def _promote(self, bundle: Path) -> None:
|
|
# Simulate the promotion gate writing a verdict file + its (neutral) index link,
|
|
# plus a curated file + link. Neither carries the ingest stamp.
|
|
(bundle / "promoted-verdict-led.md").write_text(
|
|
"---\ntype: verdict\ndecision: approved\ndescription: LED holds up.\n---\n\nBody.\n",
|
|
encoding="utf-8",
|
|
)
|
|
(bundle / "curated-note.md").write_text(
|
|
"---\ntype: reference\ntitle: Curated\n---\n\nHand-written.\n", encoding="utf-8"
|
|
)
|
|
index = bundle / "index.md"
|
|
index.write_text(
|
|
index.read_text(encoding="utf-8")
|
|
+ "- [promoted](promoted-verdict-led.md)\n"
|
|
+ "- [Curated](curated-note.md)\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
def test_promoted_verdict_and_curated_survive_reingest(self, tmp_path: Path) -> None:
|
|
bundle = _materialized(tmp_path)
|
|
self._promote(bundle)
|
|
materialize(GOLDEN / "manifest.json", bundle, INGESTED_AT) # re-ingest
|
|
|
|
assert (bundle / "promoted-verdict-led.md").is_file() # verdict file survives
|
|
assert (bundle / "curated-note.md").is_file() # curated file survives
|
|
index_text = (bundle / "index.md").read_text(encoding="utf-8")
|
|
assert "- [promoted](promoted-verdict-led.md)" in index_text # verdict link survives
|
|
assert "- [Curated](curated-note.md)" in index_text # curated link survives
|
|
# ...and the ingest files were still refreshed.
|
|
assert (bundle / "ingest-costs.md").is_file()
|
|
assert "](ingest-costs.md)" in index_text
|
|
|
|
def test_reingest_drops_a_stale_ingest_file_and_its_link(self, tmp_path: Path) -> None:
|
|
# A manifest that no longer generates ingest-edge → the stale file AND its index
|
|
# link are removed; the promoted/curated content is untouched (RED if replacement
|
|
# over-reaches or under-reaches).
|
|
bundle = _materialized(tmp_path)
|
|
self._promote(bundle)
|
|
# Positive control for the negative assertion below: the link is present, in
|
|
# EXACTLY the form the removal assertion searches for. Without this the `not in`
|
|
# would also pass if the link form drifted — green for the wrong reason.
|
|
assert "](ingest-edge.md)" in (bundle / "index.md").read_text(encoding="utf-8")
|
|
# The shrunk manifest must sit beside its own fixture/ (root resolves relative to
|
|
# the manifest dir), so copy the golden case and rewrite the manifest there.
|
|
case = tmp_path / "case"
|
|
shutil.copytree(GOLDEN, case)
|
|
shrunk = json.loads((case / "manifest.json").read_text(encoding="utf-8"))
|
|
shrunk["extractions"] = [e for e in shrunk["extractions"] if e["id"] == "costs"]
|
|
(case / "manifest.json").write_text(json.dumps(shrunk), encoding="utf-8")
|
|
materialize(case / "manifest.json", bundle, INGESTED_AT)
|
|
|
|
assert not (bundle / "ingest-edge.md").exists() # stale ingest file removed
|
|
index_text = (bundle / "index.md").read_text(encoding="utf-8")
|
|
assert "](ingest-edge.md)" not in index_text # stale ingest link removed
|
|
assert "- [promoted](promoted-verdict-led.md)" in index_text # promoted survives
|
|
assert (bundle / "ingest-costs.md").is_file() # kept extraction survives
|