fix(wiki): protect math in cross-refs; drop no-op DB call; fix mark order
- R-9 (MED): _inject_cross_refs now protects LaTeX math spans ($…$, $$…$$, \(…\), \[…\]) like inline code, so a concept name inside a formula is no longer rewritten to a [[slug]] link and corrupting the LaTeX. Regression test added. - C-13 (LOW): mark ungrounded claims on the raw body BEFORE injecting cross-refs (render then inject), so a concept name inside a claim cannot stop the ⚠ regex from matching. - C-12 (LOW): _try_embed_formulas is now a true no-op — it previously issued a per-concept information_schema round-trip that did nothing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -402,6 +402,13 @@ def _detect_conflicts(
|
||||
|
||||
_INLINE_CODE_RE = re.compile(r"`[^`]+`")
|
||||
|
||||
# LaTeX math spans — protected from cross-ref injection so a concept name inside
|
||||
# a formula is not rewritten to a [[slug]] link, corrupting the LaTeX (audit R-9).
|
||||
_MATH_SPAN_RE = re.compile(
|
||||
r"\$\$.*?\$\$|\$[^$\n]*?\$|\\\(.*?\\\)|\\\[.*?\\\]",
|
||||
re.DOTALL,
|
||||
)
|
||||
|
||||
|
||||
def _inject_cross_refs(
|
||||
markdown: str,
|
||||
@@ -411,11 +418,12 @@ def _inject_cross_refs(
|
||||
"""Replace occurrences of other concept titles/aliases with ``[[slug]]`` links.
|
||||
|
||||
Only exact case-insensitive whole-word matches outside of existing
|
||||
``[[…]]`` blocks or inline code spans are replaced.
|
||||
Inline-code spans (`` `…` ``) are temporarily protected by null-byte
|
||||
placeholders and restored after injection.
|
||||
``[[…]]`` blocks, inline code spans, or LaTeX math spans are replaced.
|
||||
Inline-code (`` `…` ``) and math (``$…$``, ``$$…$$``, ``\\(…\\)``,
|
||||
``\\[…\\]``) spans are temporarily protected by null-byte placeholders and
|
||||
restored after injection.
|
||||
"""
|
||||
# Step 1: protect inline-code spans from replacement
|
||||
# Step 1: protect inline-code and math spans from replacement
|
||||
placeholders: dict[str, str] = {}
|
||||
|
||||
def _protect(m: re.Match[str]) -> str:
|
||||
@@ -424,6 +432,7 @@ def _inject_cross_refs(
|
||||
return key
|
||||
|
||||
markdown = _INLINE_CODE_RE.sub(_protect, markdown)
|
||||
markdown = _MATH_SPAN_RE.sub(_protect, markdown) # don't rewrite names inside LaTeX (R-9)
|
||||
|
||||
# Step 2: inject cross-refs on unprotected text
|
||||
for concept in all_concepts:
|
||||
@@ -602,14 +611,14 @@ def compile_concept(
|
||||
claims = _parse_claims(raw_output)
|
||||
claims = _run_grounding_guard(claims, chunks)
|
||||
|
||||
_all_concepts = all_concepts or []
|
||||
raw_output = _inject_cross_refs(raw_output, _all_concepts, concept.slug)
|
||||
|
||||
# Graceful: try to embed formula chunks (F-09) — skip if table missing
|
||||
# Graceful: formula-embedding hook (F-09) — currently a no-op (audit C-12).
|
||||
_try_embed_formulas(concept, chunks)
|
||||
|
||||
compiled_at = datetime.now(UTC)
|
||||
# Mark ungrounded claims on the raw body FIRST, then inject cross-refs: a
|
||||
# concept name inside a claim must not stop the ⚠ regex from matching (C-13).
|
||||
markdown = _render_page_markdown(concept, raw_output, claims, compiled_at)
|
||||
markdown = _inject_cross_refs(markdown, all_concepts or [], concept.slug)
|
||||
|
||||
return ConceptPage(
|
||||
concept=concept,
|
||||
@@ -621,20 +630,13 @@ def compile_concept(
|
||||
|
||||
|
||||
def _try_embed_formulas(concept: Concept, chunks: list[dict[str, Any]]) -> None:
|
||||
"""Attempt to look up formula chunks for the concept — graceful no-op if F-09 absent."""
|
||||
try:
|
||||
from codex.db import get_conn
|
||||
"""Reserved hook for embedding F-09 formula chunks into a page.
|
||||
|
||||
with get_conn() as conn:
|
||||
# Check if formulas table exists
|
||||
row = conn.execute(
|
||||
"SELECT 1 FROM information_schema.tables WHERE table_name = 'formulas'"
|
||||
).fetchone()
|
||||
if row is None:
|
||||
return # F-09 not present
|
||||
# (Future: embed relevant raw_latex into the page)
|
||||
except Exception: # noqa: BLE001
|
||||
return # DB not reachable or other error — degrade gracefully
|
||||
Currently a no-op — formula embedding is not implemented yet. Kept as a stable
|
||||
seam for compile_concept's call site. The previous body issued a per-concept
|
||||
DB round-trip (information_schema lookup) that did nothing (audit C-12).
|
||||
"""
|
||||
return
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -274,6 +274,19 @@ def test_inject_cross_refs_skips_inline_code() -> None:
|
||||
assert "[[circle-packing]]" in result # bare occurrence replaced
|
||||
|
||||
|
||||
def test_inject_cross_refs_skips_math_spans() -> None:
|
||||
"""Concept names inside LaTeX math are NOT rewritten to [[slug]] links (audit R-9)."""
|
||||
concepts = [
|
||||
Concept(slug="energy", title="Energy", aliases=[]),
|
||||
FIXTURE_CONCEPT,
|
||||
]
|
||||
text = "Prose Energy here. Math $x = Energy^2$ and inline \\(Energy\\) stay literal."
|
||||
result = _inject_cross_refs(text, concepts, current_slug="lobachevsky-function")
|
||||
assert "$x = Energy^2$" in result # display/inline $...$ math untouched
|
||||
assert "\\(Energy\\)" in result # \(...\) math untouched
|
||||
assert "Prose [[energy]] here." in result # prose occurrence still linked
|
||||
|
||||
|
||||
def test_inject_cross_refs_full_list_even_when_single_concept() -> None:
|
||||
"""Cross-ref injection uses the full concept list, not just the compiled concept."""
|
||||
concepts = [
|
||||
|
||||
Reference in New Issue
Block a user