fix(wiki): pass chunks explicitly to compile_concept (single retrieve)
compile_concept now accepts chunks as a positional parameter; compile_all retrieves chunks once and passes them directly, avoiding double-retrieve. Also separates all_concepts (full list) from compile_concepts (filtered), so cross-ref injection always uses the complete concept list regardless of --concept filter (Fix 6 + Fix 8). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -426,6 +426,7 @@ def _render_page_markdown(
|
|||||||
|
|
||||||
def compile_concept(
|
def compile_concept(
|
||||||
concept: Concept,
|
concept: Concept,
|
||||||
|
chunks: list[dict[str, Any]],
|
||||||
*,
|
*,
|
||||||
top_k: int,
|
top_k: int,
|
||||||
llm: LLMClient,
|
llm: LLMClient,
|
||||||
@@ -434,19 +435,24 @@ def compile_concept(
|
|||||||
) -> ConceptPage:
|
) -> ConceptPage:
|
||||||
"""Compile a single concept page.
|
"""Compile a single concept page.
|
||||||
|
|
||||||
1. Retrieve Top-K chunks (hybrid dense+FTS) for concept title + aliases.
|
Parameters
|
||||||
2. Synthesise via LLM (Ollama) with per-claim citation format.
|
----------
|
||||||
3. Run Grounding-Guard: mark ungrounded claims as ⚠.
|
concept:
|
||||||
4. Inject cross-references to other concepts as [[slug]] links.
|
The concept to compile.
|
||||||
5. Embed formula chunks if ``formulas`` table is present (graceful).
|
chunks:
|
||||||
|
Pre-retrieved source chunks for this concept (from :func:`_retrieve_chunks`).
|
||||||
|
Passing chunks explicitly avoids a second retrieve and ensures the stored
|
||||||
|
hash matches the chunks actually used for synthesis.
|
||||||
|
|
||||||
|
1. Synthesise via LLM (Ollama) with per-claim citation format.
|
||||||
|
2. Run Grounding-Guard: mark ungrounded claims as ⚠.
|
||||||
|
3. Inject cross-references to other concepts as [[slug]] links.
|
||||||
|
4. Embed formula chunks if ``formulas`` table is present (graceful).
|
||||||
|
|
||||||
Returns a :class:`ConceptPage` with full markdown and claim list.
|
Returns a :class:`ConceptPage` with full markdown and claim list.
|
||||||
"""
|
"""
|
||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
_wiki_dir = wiki_dir or Path(settings.wiki_dir)
|
_wiki_dir = wiki_dir or Path(settings.wiki_dir) # noqa: F841 — kept for future use
|
||||||
|
|
||||||
queries = [concept.title] + concept.aliases
|
|
||||||
chunks = _retrieve_chunks(queries, top_k=top_k)
|
|
||||||
|
|
||||||
h = _chunk_hash(chunks)
|
h = _chunk_hash(chunks)
|
||||||
prompt = _build_synthesis_prompt(concept, chunks)
|
prompt = _build_synthesis_prompt(concept, chunks)
|
||||||
@@ -532,10 +538,13 @@ def compile_all(
|
|||||||
if not concepts_path.exists():
|
if not concepts_path.exists():
|
||||||
# Fall back to sibling concepts.yaml next to wiki/ dir
|
# Fall back to sibling concepts.yaml next to wiki/ dir
|
||||||
concepts_path = wiki_dir.parent / "wiki" / "concepts.yaml"
|
concepts_path = wiki_dir.parent / "wiki" / "concepts.yaml"
|
||||||
concepts = load_concepts(str(concepts_path))
|
# Load the FULL concept list — used for cross-reference injection regardless of filter
|
||||||
|
all_concepts = load_concepts(str(concepts_path))
|
||||||
|
|
||||||
if concept_filter:
|
# Apply filter only to the set of concepts that will be (re-)compiled
|
||||||
concepts = [c for c in concepts if c.slug == concept_filter]
|
compile_concepts = (
|
||||||
|
[c for c in all_concepts if c.slug == concept_filter] if concept_filter else all_concepts
|
||||||
|
)
|
||||||
|
|
||||||
state_path = wiki_dir / ".compile-state.json"
|
state_path = wiki_dir / ".compile-state.json"
|
||||||
state = _load_compile_state(state_path)
|
state = _load_compile_state(state_path)
|
||||||
@@ -549,8 +558,8 @@ def compile_all(
|
|||||||
|
|
||||||
report = CompileReport()
|
report = CompileReport()
|
||||||
|
|
||||||
for concept in concepts:
|
for concept in compile_concepts:
|
||||||
# Fast check: retrieve chunks and compare hash
|
# Retrieve chunks once — reuse for hash check and synthesis
|
||||||
queries = [concept.title] + concept.aliases
|
queries = [concept.title] + concept.aliases
|
||||||
chunks = _retrieve_chunks(queries, top_k=k)
|
chunks = _retrieve_chunks(queries, top_k=k)
|
||||||
h = _chunk_hash(chunks)
|
h = _chunk_hash(chunks)
|
||||||
@@ -561,9 +570,10 @@ def compile_all(
|
|||||||
|
|
||||||
page = compile_concept(
|
page = compile_concept(
|
||||||
concept,
|
concept,
|
||||||
|
chunks,
|
||||||
top_k=k,
|
top_k=k,
|
||||||
llm=_llm,
|
llm=_llm,
|
||||||
all_concepts=concepts,
|
all_concepts=all_concepts, # always the full list for cross-refs
|
||||||
wiki_dir=wiki_dir,
|
wiki_dir=wiki_dir,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -259,7 +259,9 @@ def test_compile_concept_returns_concept_page(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""compile_concept returns a ConceptPage with non-empty markdown."""
|
"""compile_concept returns a ConceptPage with non-empty markdown."""
|
||||||
llm = MockLLM(GROUNDED_LLM_OUTPUT)
|
llm = MockLLM(GROUNDED_LLM_OUTPUT)
|
||||||
page = compile_concept(FIXTURE_CONCEPT, top_k=6, llm=llm, wiki_dir=mock_settings)
|
page = compile_concept(
|
||||||
|
FIXTURE_CONCEPT, FIXTURE_CHUNKS, top_k=6, llm=llm, wiki_dir=mock_settings
|
||||||
|
)
|
||||||
assert isinstance(page, ConceptPage)
|
assert isinstance(page, ConceptPage)
|
||||||
assert page.concept.slug == "lobachevsky-function"
|
assert page.concept.slug == "lobachevsky-function"
|
||||||
assert len(page.markdown) > 0
|
assert len(page.markdown) > 0
|
||||||
@@ -272,7 +274,9 @@ def test_compile_concept_has_chunk_hash(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Compiled page has a non-empty chunk_hash."""
|
"""Compiled page has a non-empty chunk_hash."""
|
||||||
llm = MockLLM(GROUNDED_LLM_OUTPUT)
|
llm = MockLLM(GROUNDED_LLM_OUTPUT)
|
||||||
page = compile_concept(FIXTURE_CONCEPT, top_k=6, llm=llm, wiki_dir=mock_settings)
|
page = compile_concept(
|
||||||
|
FIXTURE_CONCEPT, FIXTURE_CHUNKS, top_k=6, llm=llm, wiki_dir=mock_settings
|
||||||
|
)
|
||||||
assert len(page.chunk_hash) == 64 # SHA-256 hex
|
assert len(page.chunk_hash) == 64 # SHA-256 hex
|
||||||
|
|
||||||
|
|
||||||
@@ -283,7 +287,9 @@ def test_compile_concept_grounded_claim_not_flagged(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""A grounded claim does NOT receive the ⚠ marker in the output markdown."""
|
"""A grounded claim does NOT receive the ⚠ marker in the output markdown."""
|
||||||
llm = MockLLM(GROUNDED_LLM_OUTPUT)
|
llm = MockLLM(GROUNDED_LLM_OUTPUT)
|
||||||
page = compile_concept(FIXTURE_CONCEPT, top_k=6, llm=llm, wiki_dir=mock_settings)
|
page = compile_concept(
|
||||||
|
FIXTURE_CONCEPT, FIXTURE_CHUNKS, top_k=6, llm=llm, wiki_dir=mock_settings
|
||||||
|
)
|
||||||
# When all claims are grounded, no ⚠ in markdown
|
# When all claims are grounded, no ⚠ in markdown
|
||||||
# (may still have 0 ⚠ if claims found; just check no false positive for grounded)
|
# (may still have 0 ⚠ if claims found; just check no false positive for grounded)
|
||||||
grounded = [c for c in page.claims if c.grounded]
|
grounded = [c for c in page.claims if c.grounded]
|
||||||
@@ -298,7 +304,9 @@ def test_compile_concept_ungrounded_claim_marked(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""An ungrounded claim is marked ⚠ in the page markdown."""
|
"""An ungrounded claim is marked ⚠ in the page markdown."""
|
||||||
llm = MockLLM(UNGROUNDED_LLM_OUTPUT)
|
llm = MockLLM(UNGROUNDED_LLM_OUTPUT)
|
||||||
page = compile_concept(FIXTURE_CONCEPT, top_k=6, llm=llm, wiki_dir=mock_settings)
|
page = compile_concept(
|
||||||
|
FIXTURE_CONCEPT, FIXTURE_CHUNKS, top_k=6, llm=llm, wiki_dir=mock_settings
|
||||||
|
)
|
||||||
ungrounded = [c for c in page.claims if not c.grounded]
|
ungrounded = [c for c in page.claims if not c.grounded]
|
||||||
if ungrounded:
|
if ungrounded:
|
||||||
assert "⚠" in page.markdown
|
assert "⚠" in page.markdown
|
||||||
@@ -311,7 +319,9 @@ def test_compile_concept_header_present(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Page markdown starts with an H1 header for the concept title."""
|
"""Page markdown starts with an H1 header for the concept title."""
|
||||||
llm = MockLLM(GROUNDED_LLM_OUTPUT)
|
llm = MockLLM(GROUNDED_LLM_OUTPUT)
|
||||||
page = compile_concept(FIXTURE_CONCEPT, top_k=6, llm=llm, wiki_dir=mock_settings)
|
page = compile_concept(
|
||||||
|
FIXTURE_CONCEPT, FIXTURE_CHUNKS, top_k=6, llm=llm, wiki_dir=mock_settings
|
||||||
|
)
|
||||||
assert page.markdown.startswith("# Lobachevsky Function")
|
assert page.markdown.startswith("# Lobachevsky Function")
|
||||||
|
|
||||||
|
|
||||||
@@ -434,10 +444,12 @@ def test_idempotent_no_rewrite_when_unchanged(
|
|||||||
call_count = 0
|
call_count = 0
|
||||||
original_compile = compile_concept
|
original_compile = compile_concept
|
||||||
|
|
||||||
def counting_compile(concept: Concept, **kwargs: Any) -> ConceptPage:
|
def counting_compile(
|
||||||
|
concept: Concept, chunks: list[Any], **kwargs: Any
|
||||||
|
) -> ConceptPage:
|
||||||
nonlocal call_count
|
nonlocal call_count
|
||||||
call_count += 1
|
call_count += 1
|
||||||
return original_compile(concept, **kwargs)
|
return original_compile(concept, chunks, **kwargs)
|
||||||
|
|
||||||
monkeypatch.setattr("codex.wiki.compile_concept", counting_compile)
|
monkeypatch.setattr("codex.wiki.compile_concept", counting_compile)
|
||||||
|
|
||||||
@@ -478,10 +490,12 @@ def test_force_all_recompiles_even_when_unchanged(
|
|||||||
call_count = 0
|
call_count = 0
|
||||||
original_compile = compile_concept
|
original_compile = compile_concept
|
||||||
|
|
||||||
def counting_compile(concept: Concept, **kwargs: Any) -> ConceptPage:
|
def counting_compile(
|
||||||
|
concept: Concept, chunks: list[Any], **kwargs: Any
|
||||||
|
) -> ConceptPage:
|
||||||
nonlocal call_count
|
nonlocal call_count
|
||||||
call_count += 1
|
call_count += 1
|
||||||
return original_compile(concept, **kwargs)
|
return original_compile(concept, chunks, **kwargs)
|
||||||
|
|
||||||
monkeypatch.setattr("codex.wiki.compile_concept", counting_compile)
|
monkeypatch.setattr("codex.wiki.compile_concept", counting_compile)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user