feat(synthesis): Lead/Provenance model + grounded leads + conjecture generator
- codex/synthesis.py: Lead/Provenance dataclasses, find_connections/gaps/improvements, propose_conjectures (status=unverified, quarantined in leads/conjectures/), write_leads (grounded→leads/grounded/, conjectures→leads/conjectures/ HARD invariant) - codex/cli.py: synthesis leads/conjectures/report command group - codex/config.py: F-13 settings (leads_dir, synthesis_llm_*, synthesis_top_k, synthesis_min_grounded_ratio, synthesis_gap_min_coverage) - tests/synthesis/: 42 tests covering model, grounding, conjecture invariant, quarantine, CLI - spike/: F-13 spike script + output (live run attempted) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
118
codex/cli.py
118
codex/cli.py
@@ -12,6 +12,7 @@ app = typer.Typer(help="codex — personal knowledge base for scientific papers.
|
||||
discover_app = typer.Typer(help="Discovery queries over the citation graph.")
|
||||
prov_app = typer.Typer(help="Provenance: @cite scan, code_links, bib export.")
|
||||
wiki_app = typer.Typer(help="Wiki-compile: grounded concept pages over the RAG substrate.")
|
||||
synth_app = typer.Typer(help="Synthesis: grounded leads and quarantined conjectures (F-13).")
|
||||
# F-09: search sub-app with paper (semantic) and formula (FTS) subcommands
|
||||
search_app = typer.Typer(
|
||||
help="Search: semantic paper search and formula full-text search.",
|
||||
@@ -20,6 +21,7 @@ search_app = typer.Typer(
|
||||
app.add_typer(discover_app, name="discover")
|
||||
app.add_typer(prov_app, name="provenance")
|
||||
app.add_typer(wiki_app, name="wiki")
|
||||
app.add_typer(synth_app, name="synthesis")
|
||||
app.add_typer(search_app, name="search")
|
||||
|
||||
|
||||
@@ -346,3 +348,119 @@ def wiki_check(
|
||||
raise typer.Exit(1)
|
||||
else:
|
||||
typer.echo("All claims grounded.")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# F-13: Synthesis command group
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@synth_app.command("leads")
|
||||
def synthesis_leads(
|
||||
lib_path: Optional[str] = typer.Option( # noqa: UP045
|
||||
None,
|
||||
"--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."
|
||||
),
|
||||
),
|
||||
output_dir: Optional[str] = typer.Option( # noqa: UP045
|
||||
None, "--output-dir", help="Override config.leads_dir."
|
||||
),
|
||||
) -> None:
|
||||
"""Generate grounded leads (stages 2-4) and write them to leads/grounded/."""
|
||||
from codex.config import get_settings
|
||||
from codex.synthesis import (
|
||||
default_llm_client,
|
||||
find_connections,
|
||||
find_gaps,
|
||||
find_improvements,
|
||||
write_leads,
|
||||
)
|
||||
|
||||
settings = get_settings()
|
||||
leads_dir = output_dir or settings.leads_dir
|
||||
llm = default_llm_client()
|
||||
|
||||
connections = find_connections(top_k=settings.synthesis_top_k, llm=llm)
|
||||
gaps = find_gaps(lib_path=lib_path, llm=llm)
|
||||
improvements = (
|
||||
find_improvements(lib_path, top_k=settings.synthesis_top_k, llm=llm) if lib_path else []
|
||||
)
|
||||
|
||||
all_leads = connections + gaps + improvements
|
||||
write_leads(all_leads, leads_dir)
|
||||
|
||||
typer.echo(
|
||||
f"Grounded leads written to {leads_dir}/grounded/: "
|
||||
f"{len(connections)} connection · {len(gaps)} gap · {len(improvements)} improvement"
|
||||
)
|
||||
|
||||
|
||||
@synth_app.command("conjectures")
|
||||
def synthesis_conjectures(
|
||||
output_dir: Optional[str] = typer.Option( # noqa: UP045
|
||||
None, "--output-dir", help="Override config.leads_dir."
|
||||
),
|
||||
) -> None:
|
||||
"""Generate stage-5 conjecture leads — quarantined to leads/conjectures/.
|
||||
|
||||
INVARIANT (F-13 HARD): conjectures are NEVER written to wiki/. They
|
||||
require a ``suggested_validation`` spike before they may be promoted.
|
||||
"""
|
||||
from codex.config import get_settings
|
||||
from codex.synthesis import default_llm_client, propose_conjectures, write_leads
|
||||
|
||||
settings = get_settings()
|
||||
leads_dir = output_dir or settings.leads_dir
|
||||
llm = default_llm_client()
|
||||
|
||||
conjectures = propose_conjectures(top_k=settings.synthesis_top_k, llm=llm)
|
||||
write_leads(conjectures, leads_dir)
|
||||
|
||||
typer.echo(
|
||||
f"Conjectures (UNVERIFIED) written to {leads_dir}/conjectures/: {len(conjectures)}"
|
||||
)
|
||||
|
||||
|
||||
@synth_app.command("report")
|
||||
def synthesis_report(
|
||||
output_dir: Optional[str] = typer.Option( # noqa: UP045
|
||||
None, "--output-dir", help="Override config.leads_dir."
|
||||
),
|
||||
) -> None:
|
||||
"""Show grounded leads and conjectures — visibly separated."""
|
||||
from pathlib import Path
|
||||
|
||||
from codex.config import get_settings
|
||||
|
||||
settings = get_settings()
|
||||
leads_dir = Path(output_dir or settings.leads_dir)
|
||||
grounded_dir = leads_dir / "grounded"
|
||||
conj_dir = leads_dir / "conjectures"
|
||||
|
||||
grounded_files = sorted(grounded_dir.glob("*.md")) if grounded_dir.exists() else []
|
||||
conj_files = sorted(conj_dir.glob("*.md")) if conj_dir.exists() else []
|
||||
|
||||
typer.echo("=" * 72)
|
||||
typer.echo(f"GROUNDED LEADS ({len(grounded_files)}) → {grounded_dir}")
|
||||
typer.echo("=" * 72)
|
||||
if not grounded_files:
|
||||
typer.echo("(none)")
|
||||
for f in grounded_files:
|
||||
first = f.read_text(encoding="utf-8").splitlines()[0]
|
||||
typer.echo(f" {first}")
|
||||
|
||||
typer.echo("")
|
||||
typer.echo("=" * 72)
|
||||
typer.echo(
|
||||
f"CONJECTURES ({len(conj_files)}) → {conj_dir} "
|
||||
"[UNVERIFIED — never in wiki/]"
|
||||
)
|
||||
typer.echo("=" * 72)
|
||||
if not conj_files:
|
||||
typer.echo("(none)")
|
||||
for f in conj_files:
|
||||
first = f.read_text(encoding="utf-8").splitlines()[0]
|
||||
typer.echo(f" {first}")
|
||||
|
||||
Reference in New Issue
Block a user