fix(synthesis): make coverage-map gaps opt-in (audit R-8)

find_gaps defaulted its probe topics to paper titles, so a title — which
retrieves mostly its own single bibkey — fell below synthesis_gap_min_coverage
and flagged almost every paper as a 'gap'. Coverage-map gaps now run only for
explicitly-requested topics; the CLI gains a repeatable --topic option. The
code-vs-corpus gap path (precise) is unchanged and still default.

Tests: the two cases that relied on default-title coverage gaps now pass explicit
topics; added a regression that no coverage gaps appear without --topic.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Tarik Moussa
2026-06-15 16:32:01 +02:00
parent cfbfa53398
commit c4106b0f51
4 changed files with 46 additions and 11 deletions

View File

@@ -422,7 +422,15 @@ def synthesis_leads(
"--lib-path",
help=(
"Path to a C++ source tree. Enables improvement leads (@cite-aware) and "
"code-vs-corpus gap detection. Without it only connection + topic gaps run."
"code-vs-corpus gap detection."
),
),
topic: list[str] | None = typer.Option( # noqa: B008
None,
"--topic",
help=(
"Topic to check for coverage gaps (repeatable). Opt-in: without --topic "
"no coverage-map gaps run (avoids per-paper false positives, audit R-8)."
),
),
output_dir: Optional[str] = typer.Option( # noqa: UP045
@@ -444,7 +452,7 @@ def synthesis_leads(
llm = default_llm_client()
connections = find_connections(top_k=settings.synthesis_top_k, llm=llm)
gaps = find_gaps(lib_path=lib_path, llm=llm)
gaps = find_gaps(lib_path=lib_path, llm=llm, topics=topic or None)
improvements = (
find_improvements(lib_path, top_k=settings.synthesis_top_k, llm=llm) if lib_path else []
)

View File

@@ -416,11 +416,12 @@ def find_gaps(
Two complementary signals are used:
* **Topic coverage map** — for each topic in ``topics`` (or, if
``None``, the union of paper titles), retrieve chunks and check
bibkey coverage. A topic covered by < ``synthesis_gap_min_coverage``
bibkeys is reported as a gap candidate and the LLM is asked to
articulate it.
* **Topic coverage map** — for each **explicitly-requested** topic in
``topics``, retrieve chunks and check bibkey coverage. A topic covered by
< ``synthesis_gap_min_coverage`` bibkeys is a gap candidate and the LLM is
asked to articulate it. Defaulting to paper titles flagged almost every
paper as a gap (a title retrieves mostly its own single bibkey), so the
coverage map is opt-in now; pass ``topics`` / CLI ``--topic`` (audit R-8).
* **Code-vs-corpus gap** — when ``lib_path`` is given, any @cite
bibkey scanned from code that is NOT present in the corpus is
flagged. This catches the "cited in code, never ingested" case.
@@ -429,9 +430,9 @@ def find_gaps(
seq = 1
leads: list[Lead] = []
# Coverage-map probes are the explicit topics only — no default-to-titles,
# which turned every paper into a spurious gap (audit R-8).
probe_topics: list[str] = list(topics or [])
if not probe_topics:
probe_topics = [title for _, title in _list_paper_titles(db_conn)]
known_bibkeys = {bk for bk, _ in _list_paper_titles(db_conn)}