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:
Tarik Moussa
2026-06-14 08:37:42 +02:00
parent 967e6baa59
commit 8cf0cc7e01
14 changed files with 2139 additions and 0 deletions

124
tests/synthesis/conftest.py Normal file
View File

@@ -0,0 +1,124 @@
"""Shared synthesis fixtures — keep DB/LLM/embed fully mocked."""
from __future__ import annotations
from pathlib import Path
from typing import Any
from unittest.mock import MagicMock
import pytest
# Shared corpus fixture used across the test suite.
FIXTURE_CHUNKS: list[dict[str, Any]] = [
{
"id": 1,
"paper_id": "springborn-2008",
"ord": 16,
"content": (
"For an ideal tetrahedron with dihedral angles the hyperbolic volume is "
"V = L(gamma1) + L(gamma2) + L(gamma3). "
"The volume function V0 is strictly concave on the angle domain."
),
"bibkey": "Springborn2008",
},
{
"id": 2,
"paper_id": "bps-2015",
"ord": 3,
"content": (
"A discrete conformal map is defined by logarithmic scale factors u_i "
"at each vertex such that the edge lengths satisfy a compatibility condition."
),
"bibkey": "BobenkoPinkallSpringborn2015",
},
{
"id": 3,
"paper_id": "luo-2004",
"ord": 0,
"content": (
"The combinatorial Yamabe flow drives the edge lengths towards a target "
"curvature on a triangulated surface."
),
"bibkey": "Luo2004",
},
]
class StubLLM:
"""Deterministic mock LLM that returns a preset response."""
def __init__(self, response: str | list[str]) -> None:
if isinstance(response, str):
self._responses = [response]
else:
self._responses = list(response)
self._calls: list[tuple[str, str]] = []
def generate(self, prompt: str, model: str) -> str:
self._calls.append((prompt, model))
if not self._responses:
return ""
if len(self._responses) == 1:
return self._responses[0]
return self._responses.pop(0)
@property
def calls(self) -> list[tuple[str, str]]:
return list(self._calls)
@pytest.fixture
def stub_llm() -> StubLLM:
"""Default stub returning empty string (override per test as needed)."""
return StubLLM("")
@pytest.fixture
def fixture_chunks() -> list[dict[str, Any]]:
return [dict(c) for c in FIXTURE_CHUNKS]
@pytest.fixture
def mock_retrieve(monkeypatch: pytest.MonkeyPatch) -> None:
"""Patch _retrieve_chunks to return deterministic fixture chunks (no DB)."""
monkeypatch.setattr(
"codex.synthesis._retrieve_chunks",
lambda queries, top_k: [dict(c) for c in FIXTURE_CHUNKS],
)
@pytest.fixture
def mock_paper_titles(monkeypatch: pytest.MonkeyPatch) -> None:
"""Patch _list_paper_titles to return a deterministic three-paper corpus."""
monkeypatch.setattr(
"codex.synthesis._list_paper_titles",
lambda db_conn: [
("Springborn2008", "A variational principle for weighted Delaunay triangulations"),
(
"BobenkoPinkallSpringborn2015",
"Discrete conformal maps and ideal hyperbolic polyhedra",
),
("Luo2004", "Combinatorial Yamabe Flow on Surfaces"),
],
)
@pytest.fixture
def mock_settings(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path:
"""Settings stub pointing leads_dir at tmp_path/leads."""
from codex.config import Settings
leads_path = tmp_path / "leads"
mock = MagicMock(spec=Settings)
mock.leads_dir = str(leads_path)
mock.synthesis_llm_model = "test-model"
mock.synthesis_llm_url = None
mock.ollama_base_url = "http://localhost:11434"
mock.synthesis_top_k = 6
mock.synthesis_min_grounded_ratio = 0.5
mock.synthesis_gap_min_coverage = 2
mock.wiki_dir = str(tmp_path / "wiki")
monkeypatch.setattr("codex.synthesis.get_settings", lambda: mock)
return leads_path