fix(synthesis): enforce non-empty provenance + fix gap lead + clean index docstring

- Lead.__post_init__: raises ValueError on empty provenance list
- find_gaps: no-chunk path now carries topic-marker Provenance instead of []
- _update_index: docstring corrected (rebuilds, not append-only)
- test_model: test_lead_empty_provenance_raises covers the new invariant

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Tarik Moussa
2026-06-14 08:52:35 +02:00
parent 8cf0cc7e01
commit 59763105a5
2 changed files with 22 additions and 7 deletions

View File

@@ -102,6 +102,13 @@ class Lead:
# Derived: claim-level grounding decisions (empty for conjectures). # Derived: claim-level grounding decisions (empty for conjectures).
claims: list[Claim] = field(default_factory=list) claims: list[Claim] = field(default_factory=list)
def __post_init__(self) -> None:
if not self.provenance:
raise ValueError(
f"Lead '{self.id}' must carry at least one Provenance entry. "
"For gap leads with no chunks, use a topic-marker Provenance."
)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Protocols # Protocols
@@ -432,7 +439,7 @@ def find_gaps(
f"The retrieval layer returned no chunks for topic '{topic}'.\n" f"The retrieval layer returned no chunks for topic '{topic}'.\n"
"No grounded statement is possible; this is the absence itself." "No grounded statement is possible; this is the absence itself."
), ),
provenance=[], provenance=[Provenance(bibkey="(none)", locator=f"topic:{topic}")],
confidence=1.0, confidence=1.0,
status="unverified", status="unverified",
suggested_validation=( suggested_validation=(
@@ -797,12 +804,7 @@ def write_leads(leads: list[Lead], leads_dir: str) -> None:
def _update_index(root: Path) -> None: def _update_index(root: Path) -> None:
"""Re-build ``root/INDEX.md`` from the on-disk files. """Re-build ``root/INDEX.md`` from the on-disk files (full rebuild, not append-only)."""
Append-only semantics: previously listed ids that no longer have a
file are still recorded under "Removed" so the index never silently
loses provenance.
"""
grounded_dir = root / "grounded" grounded_dir = root / "grounded"
conj_dir = root / "conjectures" conj_dir = root / "conjectures"
grounded_files = sorted(grounded_dir.glob("*.md")) if grounded_dir.exists() else [] grounded_files = sorted(grounded_dir.glob("*.md")) if grounded_dir.exists() else []

View File

@@ -78,6 +78,19 @@ def test_lead_missing_provenance_field_raises() -> None:
) )
def test_lead_empty_provenance_raises() -> None:
"""Constructing Lead with empty provenance list raises ValueError."""
with pytest.raises(ValueError, match="at least one Provenance"):
Lead(
id="L-0004",
kind="connection",
title="x",
body="y",
provenance=[],
confidence=0.5,
)
def test_lead_id_format_helper() -> None: def test_lead_id_format_helper() -> None:
"""The internal _make_lead_id helper produces zero-padded L-XXXX strings.""" """The internal _make_lead_id helper produces zero-padded L-XXXX strings."""
from codex.synthesis import _make_lead_id from codex.synthesis import _make_lead_id