fix(synthesis): namespace lead ids by kind to stop collisions (audit C-11)

Each stage (find_connections/gaps/improvements/propose_conjectures) numbered its
leads from seq=1, so connection L-0001, gap L-0001 and improvement L-0001 all
resolved to grounded/L-0001.md. The CLI concatenates them
(connections + gaps + improvements) and write_leads writes in order, so later
kinds silently overwrote earlier ones — survivors = max(per-kind count), not the
sum. Reproduced: 4 distinct leads -> 2 files.

_make_lead_id now takes the kind and emits L-C-/L-G-/L-I-/L-X- prefixes, keeping
same-seq ids distinct across stages. Regression tests cover the format and the
no-collision invariant.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Tarik Moussa
2026-06-15 11:09:24 +02:00
parent 115bb63f2d
commit 3092f1814e
3 changed files with 38 additions and 12 deletions

View File

@@ -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,