From 59763105a5a2891883634fa6272a1c5efda989fe Mon Sep 17 00:00:00 2001 From: Tarik Moussa Date: Sun, 14 Jun 2026 08:52:35 +0200 Subject: [PATCH] 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 --- codex/synthesis.py | 16 +++++++++------- tests/synthesis/test_model.py | 13 +++++++++++++ 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/codex/synthesis.py b/codex/synthesis.py index efdf398..2baba3f 100644 --- a/codex/synthesis.py +++ b/codex/synthesis.py @@ -102,6 +102,13 @@ class Lead: # Derived: claim-level grounding decisions (empty for conjectures). 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 @@ -432,7 +439,7 @@ def find_gaps( f"The retrieval layer returned no chunks for topic '{topic}'.\n" "No grounded statement is possible; this is the absence itself." ), - provenance=[], + provenance=[Provenance(bibkey="(none)", locator=f"topic:{topic}")], confidence=1.0, status="unverified", suggested_validation=( @@ -797,12 +804,7 @@ def write_leads(leads: list[Lead], leads_dir: str) -> None: def _update_index(root: Path) -> None: - """Re-build ``root/INDEX.md`` from the on-disk files. - - Append-only semantics: previously listed ids that no longer have a - file are still recorded under "Removed" so the index never silently - loses provenance. - """ + """Re-build ``root/INDEX.md`` from the on-disk files (full rebuild, not append-only).""" grounded_dir = root / "grounded" conj_dir = root / "conjectures" grounded_files = sorted(grounded_dir.glob("*.md")) if grounded_dir.exists() else [] diff --git a/tests/synthesis/test_model.py b/tests/synthesis/test_model.py index 9528c89..e45c18d 100644 --- a/tests/synthesis/test_model.py +++ b/tests/synthesis/test_model.py @@ -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: """The internal _make_lead_id helper produces zero-padded L-XXXX strings.""" from codex.synthesis import _make_lead_id