diff --git a/codex/synthesis.py b/codex/synthesis.py index dcc1586..6bfb68c 100644 --- a/codex/synthesis.py +++ b/codex/synthesis.py @@ -248,9 +248,23 @@ def _grounded_ratio(claims: list[Claim]) -> float: return n_grounded / len(claims) -def _make_lead_id(seq: int) -> str: - """Format a sequential lead id like ``L-0001``.""" - return f"L-{seq:04d}" +_KIND_PREFIX: dict[LeadKind, str] = { + "connection": "C", + "gap": "G", + "improvement": "I", + "conjecture": "X", +} + + +def _make_lead_id(seq: int, kind: LeadKind) -> str: + """Format a per-kind sequential lead id like ``L-C-0001``. + + Each synthesis stage numbers its leads from 1, so without a kind prefix a + connection, a gap, and an improvement all produce ``L-0001`` and clobber + one another in the shared ``grounded/`` directory (audit C-11). The kind + prefix namespaces the ids so they stay distinct. + """ + return f"L-{_KIND_PREFIX[kind]}-{seq:04d}" def _safe_llm_generate(llm: LLMClient, prompt: str, model: str) -> str: @@ -296,7 +310,7 @@ def _finalise_grounded_lead( return None marked = _mark_ungrounded(body, claims) return Lead( - id=_make_lead_id(seq), + id=_make_lead_id(seq, kind), kind=kind, title=title, body=marked, @@ -428,7 +442,7 @@ def find_gaps( # No coverage at all → record the gap directly (LLM not strictly needed). leads.append( Lead( - id=_make_lead_id(seq), + id=_make_lead_id(seq, "gap"), kind="gap", title=f"Gap: corpus has no material on '{topic}'", body=( @@ -481,7 +495,7 @@ def find_gaps( for bibkey in sorted(missing): leads.append( Lead( - id=_make_lead_id(seq), + id=_make_lead_id(seq, "gap"), kind="gap", title=f"Gap: code cites '{bibkey}' but corpus does not contain it", body=( @@ -658,7 +672,7 @@ def propose_conjectures( continue leads.append( Lead( - id=_make_lead_id(seq), + id=_make_lead_id(seq, "conjecture"), kind="conjecture", title=title, body=body, diff --git a/tests/synthesis/test_grounded.py b/tests/synthesis/test_grounded.py index 17e6fb8..7d27ba7 100644 --- a/tests/synthesis/test_grounded.py +++ b/tests/synthesis/test_grounded.py @@ -76,7 +76,7 @@ def test_finalise_grounded_lead_succeeds_when_grounded( ) assert lead is not None assert lead.kind == "connection" - assert lead.id == "L-0001" + assert lead.id == "L-C-0001" # kind-prefixed id (audit C-11) assert lead.confidence >= 0.5 assert lead.status == "unverified" assert lead.suggested_validation == "" diff --git a/tests/synthesis/test_model.py b/tests/synthesis/test_model.py index e45c18d..b2c991a 100644 --- a/tests/synthesis/test_model.py +++ b/tests/synthesis/test_model.py @@ -92,9 +92,21 @@ def test_lead_empty_provenance_raises() -> None: def test_lead_id_format_helper() -> None: - """The internal _make_lead_id helper produces zero-padded L-XXXX strings.""" + """_make_lead_id produces a kind-prefixed, zero-padded L--XXXX string.""" from codex.synthesis import _make_lead_id - assert _make_lead_id(1) == "L-0001" - assert _make_lead_id(42) == "L-0042" - assert _make_lead_id(9999) == "L-9999" + assert _make_lead_id(1, "connection") == "L-C-0001" + assert _make_lead_id(42, "gap") == "L-G-0042" + assert _make_lead_id(9999, "improvement") == "L-I-9999" + assert _make_lead_id(1, "conjecture") == "L-X-0001" + + +def test_lead_id_namespaced_per_kind_no_collision() -> None: + """Regression for audit C-11: each stage numbers from 1, so without a kind + prefix connection/gap/improvement all collide on L-0001 and overwrite one + another in grounded/. The prefix must keep same-seq ids distinct. + """ + from codex.synthesis import _make_lead_id + + ids = {_make_lead_id(1, k) for k in ("connection", "gap", "improvement", "conjecture")} + assert len(ids) == 4, f"same-seq ids must stay distinct across kinds, got {ids}"