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:
@@ -248,9 +248,23 @@ def _grounded_ratio(claims: list[Claim]) -> float:
|
|||||||
return n_grounded / len(claims)
|
return n_grounded / len(claims)
|
||||||
|
|
||||||
|
|
||||||
def _make_lead_id(seq: int) -> str:
|
_KIND_PREFIX: dict[LeadKind, str] = {
|
||||||
"""Format a sequential lead id like ``L-0001``."""
|
"connection": "C",
|
||||||
return f"L-{seq:04d}"
|
"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:
|
def _safe_llm_generate(llm: LLMClient, prompt: str, model: str) -> str:
|
||||||
@@ -296,7 +310,7 @@ def _finalise_grounded_lead(
|
|||||||
return None
|
return None
|
||||||
marked = _mark_ungrounded(body, claims)
|
marked = _mark_ungrounded(body, claims)
|
||||||
return Lead(
|
return Lead(
|
||||||
id=_make_lead_id(seq),
|
id=_make_lead_id(seq, kind),
|
||||||
kind=kind,
|
kind=kind,
|
||||||
title=title,
|
title=title,
|
||||||
body=marked,
|
body=marked,
|
||||||
@@ -428,7 +442,7 @@ def find_gaps(
|
|||||||
# No coverage at all → record the gap directly (LLM not strictly needed).
|
# No coverage at all → record the gap directly (LLM not strictly needed).
|
||||||
leads.append(
|
leads.append(
|
||||||
Lead(
|
Lead(
|
||||||
id=_make_lead_id(seq),
|
id=_make_lead_id(seq, "gap"),
|
||||||
kind="gap",
|
kind="gap",
|
||||||
title=f"Gap: corpus has no material on '{topic}'",
|
title=f"Gap: corpus has no material on '{topic}'",
|
||||||
body=(
|
body=(
|
||||||
@@ -481,7 +495,7 @@ def find_gaps(
|
|||||||
for bibkey in sorted(missing):
|
for bibkey in sorted(missing):
|
||||||
leads.append(
|
leads.append(
|
||||||
Lead(
|
Lead(
|
||||||
id=_make_lead_id(seq),
|
id=_make_lead_id(seq, "gap"),
|
||||||
kind="gap",
|
kind="gap",
|
||||||
title=f"Gap: code cites '{bibkey}' but corpus does not contain it",
|
title=f"Gap: code cites '{bibkey}' but corpus does not contain it",
|
||||||
body=(
|
body=(
|
||||||
@@ -658,7 +672,7 @@ def propose_conjectures(
|
|||||||
continue
|
continue
|
||||||
leads.append(
|
leads.append(
|
||||||
Lead(
|
Lead(
|
||||||
id=_make_lead_id(seq),
|
id=_make_lead_id(seq, "conjecture"),
|
||||||
kind="conjecture",
|
kind="conjecture",
|
||||||
title=title,
|
title=title,
|
||||||
body=body,
|
body=body,
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ def test_finalise_grounded_lead_succeeds_when_grounded(
|
|||||||
)
|
)
|
||||||
assert lead is not None
|
assert lead is not None
|
||||||
assert lead.kind == "connection"
|
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.confidence >= 0.5
|
||||||
assert lead.status == "unverified"
|
assert lead.status == "unverified"
|
||||||
assert lead.suggested_validation == ""
|
assert lead.suggested_validation == ""
|
||||||
|
|||||||
@@ -92,9 +92,21 @@ def test_lead_empty_provenance_raises() -> None:
|
|||||||
|
|
||||||
|
|
||||||
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."""
|
"""_make_lead_id produces a kind-prefixed, zero-padded L-<K>-XXXX string."""
|
||||||
from codex.synthesis import _make_lead_id
|
from codex.synthesis import _make_lead_id
|
||||||
|
|
||||||
assert _make_lead_id(1) == "L-0001"
|
assert _make_lead_id(1, "connection") == "L-C-0001"
|
||||||
assert _make_lead_id(42) == "L-0042"
|
assert _make_lead_id(42, "gap") == "L-G-0042"
|
||||||
assert _make_lead_id(9999) == "L-9999"
|
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}"
|
||||||
|
|||||||
Reference in New Issue
Block a user