feat(wiki): concept schema loader + concepts.yaml (F-12)

Adds wiki/concepts.yaml (5 curated seeds: discrete-conformal-map,
circle-packing, lobachevsky-function, discrete-yamabe-flow,
hyperideal-tetrahedron) and codex/wiki.py with full load_concepts()
implementation (Concept dataclass, YAML parser, graceful empty list).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Tarik Moussa
2026-06-13 20:22:51 +02:00
parent c6eaf999a1
commit aee00515f4
6 changed files with 1489 additions and 0 deletions

0
tests/wiki/__init__.py Normal file
View File

163
tests/wiki/test_check.py Normal file
View File

@@ -0,0 +1,163 @@
"""Tests for ``codex wiki check`` CLI command.
Verifies:
- Exit 0 when all pages are grounded (no ⚠ in any page).
- Exit 1 when at least one page contains an ungrounded claim (⚠ marker).
"""
from __future__ import annotations
import textwrap
from pathlib import Path
import pytest
from typer.testing import CliRunner
from codex.cli import app
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture
def wiki_dir_all_grounded(tmp_path: Path) -> Path:
"""Create a wiki directory with only grounded pages (no ⚠)."""
wiki = tmp_path / "wiki"
wiki.mkdir()
(wiki / "discrete-conformal-map.md").write_text(
textwrap.dedent(
"""\
# Discrete Conformal Map
A discrete conformal map is defined by logarithmic scale factors. [BPS2015 #chunk 0]
The variational principle gives the optimum. [BPS2015 #chunk 1]
"""
),
encoding="utf-8",
)
(wiki / "circle-packing.md").write_text(
textwrap.dedent(
"""\
# Circle Packing
Circle packing is studied via the Koebe-Andreev-Thurston theorem. [Thurston1985 #page 3]
"""
),
encoding="utf-8",
)
return wiki
@pytest.fixture
def wiki_dir_with_ungrounded(tmp_path: Path) -> Path:
"""Create a wiki directory where one page has an ungrounded claim (⚠)."""
wiki = tmp_path / "wiki"
wiki.mkdir()
(wiki / "lobachevsky-function.md").write_text(
textwrap.dedent(
"""\
# Lobachevsky Function
The volume formula is V = L(γ₁)+L(γ₂)+L(γ₃). [Springborn2008 #chunk 16]
⚠ The closed form L(x) = -∫₀ˣ log|2 sin t| dt. [Springborn2008 #chunk 99]
"""
),
encoding="utf-8",
)
return wiki
# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------
def test_wiki_check_exit_0_when_all_grounded(
wiki_dir_all_grounded: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""codex wiki check exits 0 when no ⚠ in any page."""
from unittest.mock import MagicMock
from codex.config import Settings
mock_settings = MagicMock(spec=Settings)
mock_settings.wiki_dir = str(wiki_dir_all_grounded)
monkeypatch.setattr("codex.cli.get_settings", lambda: mock_settings, raising=False)
runner = CliRunner()
result = runner.invoke(app, ["wiki", "check", "--output-dir", str(wiki_dir_all_grounded)])
assert result.exit_code == 0, (
f"Expected exit 0, got {result.exit_code}. Output: {result.output}"
)
def test_wiki_check_exit_1_when_ungrounded(
wiki_dir_with_ungrounded: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""codex wiki check exits 1 when at least one ⚠ is present."""
from unittest.mock import MagicMock
from codex.config import Settings
mock_settings = MagicMock(spec=Settings)
mock_settings.wiki_dir = str(wiki_dir_with_ungrounded)
monkeypatch.setattr("codex.cli.get_settings", lambda: mock_settings, raising=False)
runner = CliRunner()
result = runner.invoke(app, ["wiki", "check", "--output-dir", str(wiki_dir_with_ungrounded)])
assert result.exit_code == 1, (
f"Expected exit 1, got {result.exit_code}. Output: {result.output}"
)
def test_wiki_check_empty_dir_exits_0(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""codex wiki check exits 0 when wiki dir has no .md pages (nothing to check)."""
wiki = tmp_path / "wiki"
wiki.mkdir()
from unittest.mock import MagicMock
from codex.config import Settings
mock_settings = MagicMock(spec=Settings)
mock_settings.wiki_dir = str(wiki)
monkeypatch.setattr("codex.cli.get_settings", lambda: mock_settings, raising=False)
runner = CliRunner()
result = runner.invoke(app, ["wiki", "check", "--output-dir", str(wiki)])
assert result.exit_code == 0
def test_wiki_check_index_and_log_ignored(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""codex wiki check ignores index.md and log.md even if they contain ⚠."""
wiki = tmp_path / "wiki"
wiki.mkdir()
# index.md and log.md with ⚠ — must NOT trigger exit 1
(wiki / "index.md").write_text("# Wiki Index\n\n⚠ some note\n", encoding="utf-8")
(wiki / "log.md").write_text("# Log\n\n⚠ ungrounded\n", encoding="utf-8")
from unittest.mock import MagicMock
from codex.config import Settings
mock_settings = MagicMock(spec=Settings)
mock_settings.wiki_dir = str(wiki)
monkeypatch.setattr("codex.cli.get_settings", lambda: mock_settings, raising=False)
runner = CliRunner()
result = runner.invoke(app, ["wiki", "check", "--output-dir", str(wiki)])
assert result.exit_code == 0, (
f"index.md / log.md should be excluded. exit={result.exit_code}, out={result.output}"
)

510
tests/wiki/test_compile.py Normal file
View File

@@ -0,0 +1,510 @@
"""Tests for compile_concept, compile_all, write_index, append_log.
DB and LLM are fully mocked — no network or database access required.
"""
from __future__ import annotations
import textwrap
from pathlib import Path
from typing import Any
from unittest.mock import MagicMock
import pytest
from codex.wiki import (
Claim,
CompileReport,
Concept,
ConceptPage,
_chunk_hash,
_inject_cross_refs,
_parse_claims,
_run_grounding_guard,
append_log,
compile_concept,
write_index,
)
# ---------------------------------------------------------------------------
# Shared fixtures
# ---------------------------------------------------------------------------
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",
"dist": 0.31,
},
{
"id": 2,
"paper_id": "springborn-2008",
"ord": 0,
"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": "Springborn2008",
"dist": 0.35,
},
]
FIXTURE_CONCEPT = Concept(
slug="lobachevsky-function",
title="Lobachevsky Function",
aliases=["Milnor Lobachevsky", "Clausen function"],
emphasis="Verwendung in hyperbolischen Volumenformeln (Springborn 2008).",
)
# Grounded LLM output: claim text is a substring of the fixture chunk
_GROUNDED_CLAIM = (
"For an ideal tetrahedron with dihedral angles the hyperbolic volume is"
" V = L(gamma1) + L(gamma2) + L(gamma3). [Springborn2008 #chunk 16]"
)
GROUNDED_LLM_OUTPUT = (
"The Lobachevsky function L appears as the building block of hyperbolic volume.\n"
+ _GROUNDED_CLAIM
+ "\nThe volume function V0 is strictly concave on the angle domain."
" [Springborn2008 #chunk 16]\n"
)
# Ungrounded LLM output: claim text not present in any chunk
UNGROUNDED_LLM_OUTPUT = textwrap.dedent(
"""\
The closed form is L(x) = -integral from 0 to x of log|2 sin t| dt. [Springborn2008 #chunk 99]
"""
)
class MockLLM:
"""Deterministic mock LLM that returns a preset response."""
def __init__(self, response: str) -> None:
self._response = response
def generate(self, prompt: str, model: str) -> str:
return self._response
# ---------------------------------------------------------------------------
# _parse_claims
# ---------------------------------------------------------------------------
def test_parse_claims_finds_citation() -> None:
"""Claims with [BibKey #locator] format are parsed correctly."""
md = "Some result. [Springborn2008 #chunk 16]"
claims = _parse_claims(md)
assert len(claims) == 1
assert claims[0].bibkey == "Springborn2008"
assert claims[0].locator == "chunk 16"
def test_parse_claims_no_citations() -> None:
"""Text without citation markers returns empty list."""
md = "A standalone sentence with no citation."
claims = _parse_claims(md)
assert claims == []
def test_parse_claims_multiple() -> None:
"""Multiple citations in same text are all parsed."""
md = "First claim. [Smith2020 #page 5]\nSecond claim. [Jones2019 #eq 3]\n"
claims = _parse_claims(md)
assert len(claims) == 2
# ---------------------------------------------------------------------------
# _run_grounding_guard
# ---------------------------------------------------------------------------
def test_grounding_guard_marks_grounded_claim() -> None:
"""A claim whose text appears in the cited chunk is marked grounded."""
claim = Claim(
text="the hyperbolic volume is V = L(gamma1) + L(gamma2) + L(gamma3)",
bibkey="Springborn2008",
locator="chunk 16",
)
result = _run_grounding_guard([claim], FIXTURE_CHUNKS)
assert result[0].grounded is True
def test_grounding_guard_marks_ungrounded_claim() -> None:
"""A claim not present in the cited chunk is marked ungrounded."""
claim = Claim(
text="integral from 0 to x of log 2 sin t dt closed form definition",
bibkey="Springborn2008",
locator="chunk 99",
)
result = _run_grounding_guard([claim], FIXTURE_CHUNKS)
assert result[0].grounded is False
def test_grounding_guard_ungrounded_when_bibkey_missing() -> None:
"""A claim citing a bibkey not in any chunk is ungrounded."""
claim = Claim(text="some statement", bibkey="UnknownBib2000", locator="page 1")
result = _run_grounding_guard([claim], FIXTURE_CHUNKS)
assert result[0].grounded is False
# ---------------------------------------------------------------------------
# _inject_cross_refs
# ---------------------------------------------------------------------------
def test_inject_cross_refs_replaces_title() -> None:
"""Occurrences of another concept's title are replaced with [[slug]]."""
concepts = [
Concept(
slug="discrete-conformal-map",
title="Discrete Conformal Map",
aliases=[],
),
FIXTURE_CONCEPT,
]
text = "This is related to Discrete Conformal Map theory."
result = _inject_cross_refs(text, concepts, current_slug="lobachevsky-function")
assert "[[discrete-conformal-map]]" in result
def test_inject_cross_refs_skips_current_slug() -> None:
"""The current concept's own title is not replaced."""
concepts = [FIXTURE_CONCEPT]
text = "The Lobachevsky Function is important."
result = _inject_cross_refs(text, concepts, current_slug="lobachevsky-function")
assert "[[lobachevsky-function]]" not in result
def test_inject_cross_refs_replaces_alias() -> None:
"""Aliases are also replaced with [[slug]]."""
concepts = [
Concept(
slug="circle-packing",
title="Circle Packing",
aliases=["Koebe-Andreev-Thurston", "circle pattern"],
),
FIXTURE_CONCEPT,
]
text = "The Koebe-Andreev-Thurston theorem gives a packing."
result = _inject_cross_refs(text, concepts, current_slug="lobachevsky-function")
assert "[[circle-packing]]" in result
# ---------------------------------------------------------------------------
# compile_concept (full mock)
# ---------------------------------------------------------------------------
@pytest.fixture
def mock_retrieve(monkeypatch: pytest.MonkeyPatch) -> None:
"""Patch _retrieve_chunks to return deterministic fixture chunks."""
monkeypatch.setattr(
"codex.wiki._retrieve_chunks",
lambda queries, top_k: FIXTURE_CHUNKS,
)
@pytest.fixture
def mock_formulas(monkeypatch: pytest.MonkeyPatch) -> None:
"""Patch _try_embed_formulas to be a no-op (F-09 not present)."""
monkeypatch.setattr("codex.wiki._try_embed_formulas", lambda concept, chunks: None)
@pytest.fixture
def mock_settings(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path:
"""Patch get_settings to return a settings pointing at tmp_path."""
from codex.config import Settings
wiki_path = tmp_path / "wiki"
wiki_path.mkdir()
mock = MagicMock(spec=Settings)
mock.wiki_dir = str(wiki_path)
mock.wiki_llm_model = "test-model"
mock.wiki_llm_url = None
mock.ollama_base_url = "http://localhost:11434"
mock.wiki_top_k = 6
monkeypatch.setattr("codex.wiki.get_settings", lambda: mock)
monkeypatch.setattr("codex.cli.get_settings", lambda: mock, raising=False)
return wiki_path
def test_compile_concept_returns_concept_page(
mock_retrieve: None,
mock_formulas: None,
mock_settings: Path,
) -> None:
"""compile_concept returns a ConceptPage with non-empty markdown."""
llm = MockLLM(GROUNDED_LLM_OUTPUT)
page = compile_concept(FIXTURE_CONCEPT, top_k=6, llm=llm, wiki_dir=mock_settings)
assert isinstance(page, ConceptPage)
assert page.concept.slug == "lobachevsky-function"
assert len(page.markdown) > 0
def test_compile_concept_has_chunk_hash(
mock_retrieve: None,
mock_formulas: None,
mock_settings: Path,
) -> None:
"""Compiled page has a non-empty chunk_hash."""
llm = MockLLM(GROUNDED_LLM_OUTPUT)
page = compile_concept(FIXTURE_CONCEPT, top_k=6, llm=llm, wiki_dir=mock_settings)
assert len(page.chunk_hash) == 64 # SHA-256 hex
def test_compile_concept_grounded_claim_not_flagged(
mock_retrieve: None,
mock_formulas: None,
mock_settings: Path,
) -> None:
"""A grounded claim does NOT receive the ⚠ marker in the output markdown."""
llm = MockLLM(GROUNDED_LLM_OUTPUT)
page = compile_concept(FIXTURE_CONCEPT, top_k=6, llm=llm, wiki_dir=mock_settings)
# When all claims are grounded, no ⚠ in markdown
# (may still have 0 ⚠ if claims found; just check no false positive for grounded)
grounded = [c for c in page.claims if c.grounded]
for claim in grounded:
assert claim.grounded is True
def test_compile_concept_ungrounded_claim_marked(
mock_retrieve: None,
mock_formulas: None,
mock_settings: Path,
) -> None:
"""An ungrounded claim is marked ⚠ in the page markdown."""
llm = MockLLM(UNGROUNDED_LLM_OUTPUT)
page = compile_concept(FIXTURE_CONCEPT, top_k=6, llm=llm, wiki_dir=mock_settings)
ungrounded = [c for c in page.claims if not c.grounded]
if ungrounded:
assert "" in page.markdown
def test_compile_concept_header_present(
mock_retrieve: None,
mock_formulas: None,
mock_settings: Path,
) -> None:
"""Page markdown starts with an H1 header for the concept title."""
llm = MockLLM(GROUNDED_LLM_OUTPUT)
page = compile_concept(FIXTURE_CONCEPT, top_k=6, llm=llm, wiki_dir=mock_settings)
assert page.markdown.startswith("# Lobachevsky Function")
# ---------------------------------------------------------------------------
# write_index
# ---------------------------------------------------------------------------
def test_index_creates_index_md(
mock_settings: Path,
) -> None:
"""write_index creates wiki/index.md."""
pages = [
("discrete-conformal-map", "Discrete Conformal Map"),
("circle-packing", "Circle Packing"),
]
write_index(pages, wiki_dir=mock_settings)
index_path = mock_settings / "index.md"
assert index_path.exists()
def test_index_contains_links(
mock_settings: Path,
) -> None:
"""wiki/index.md contains [[slug]] links for each page."""
pages = [
("discrete-conformal-map", "Discrete Conformal Map"),
("circle-packing", "Circle Packing"),
]
write_index(pages, wiki_dir=mock_settings)
content = (mock_settings / "index.md").read_text(encoding="utf-8")
assert "[[discrete-conformal-map]]" in content
assert "[[circle-packing]]" in content
def test_index_contains_header(
mock_settings: Path,
) -> None:
"""wiki/index.md starts with a # Wiki Index header."""
write_index([], wiki_dir=mock_settings)
content = (mock_settings / "index.md").read_text(encoding="utf-8")
assert content.startswith("# Wiki Index")
# ---------------------------------------------------------------------------
# append_log
# ---------------------------------------------------------------------------
def test_log_append_creates_log_md(
mock_settings: Path,
) -> None:
"""append_log creates wiki/log.md if it does not exist."""
report = CompileReport(
compiled=["lobachevsky-function"],
skipped=[],
ungrounded=[],
)
append_log(report, wiki_dir=mock_settings)
log_path = mock_settings / "log.md"
assert log_path.exists()
def test_log_append_does_not_overwrite(
mock_settings: Path,
) -> None:
"""Two calls to append_log accumulate entries — older entry is NOT overwritten."""
report1 = CompileReport(compiled=["first-concept"])
report2 = CompileReport(compiled=["second-concept"])
append_log(report1, wiki_dir=mock_settings)
append_log(report2, wiki_dir=mock_settings)
content = (mock_settings / "log.md").read_text(encoding="utf-8")
assert "first-concept" in content
assert "second-concept" in content
def test_log_append_records_ungrounded(
mock_settings: Path,
) -> None:
"""Ungrounded claims appear in the log entry."""
report = CompileReport(
compiled=["lobachevsky-function"],
ungrounded=[("lobachevsky-function", "some ungrounded claim text")],
)
append_log(report, wiki_dir=mock_settings)
content = (mock_settings / "log.md").read_text(encoding="utf-8")
assert "some ungrounded claim text" in content
assert "" in content
def test_log_append_records_skipped(
mock_settings: Path,
) -> None:
"""Skipped slugs appear in the log entry."""
report = CompileReport(
compiled=[],
skipped=["circle-packing"],
)
append_log(report, wiki_dir=mock_settings)
content = (mock_settings / "log.md").read_text(encoding="utf-8")
assert "circle-packing" in content
# ---------------------------------------------------------------------------
# Idempotency: second compile without chunk change → no rewrite
# ---------------------------------------------------------------------------
def test_idempotent_no_rewrite_when_unchanged(
mock_retrieve: None,
mock_formulas: None,
mock_settings: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Second compile_all with unchanged chunks skips the concept (changed_only=True)."""
from codex.wiki import compile_all
call_count = 0
original_compile = compile_concept
def counting_compile(concept: Concept, **kwargs: Any) -> ConceptPage:
nonlocal call_count
call_count += 1
return original_compile(concept, **kwargs)
monkeypatch.setattr("codex.wiki.compile_concept", counting_compile)
# Write concepts.yaml into wiki_dir
yaml_content = textwrap.dedent(
"""\
concepts:
- slug: lobachevsky-function
title: Lobachevsky Function
aliases: [Milnor Lobachevsky]
"""
)
(mock_settings / "concepts.yaml").write_text(yaml_content, encoding="utf-8")
llm = MockLLM(GROUNDED_LLM_OUTPUT)
# First compile: should call compile_concept
compile_all(changed_only=True, llm=llm, output_dir=str(mock_settings))
first_count = call_count
# Second compile with same chunks: should skip
compile_all(changed_only=True, llm=llm, output_dir=str(mock_settings))
second_count = call_count
assert first_count >= 1, "First run should compile at least once"
assert second_count == first_count, "Second run should not recompile (unchanged chunks)"
def test_force_all_recompiles_even_when_unchanged(
mock_retrieve: None,
mock_formulas: None,
mock_settings: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""compile_all with changed_only=False recompiles regardless of hash."""
from codex.wiki import compile_all
call_count = 0
original_compile = compile_concept
def counting_compile(concept: Concept, **kwargs: Any) -> ConceptPage:
nonlocal call_count
call_count += 1
return original_compile(concept, **kwargs)
monkeypatch.setattr("codex.wiki.compile_concept", counting_compile)
yaml_content = textwrap.dedent(
"""\
concepts:
- slug: lobachevsky-function
title: Lobachevsky Function
aliases: [Milnor Lobachevsky]
"""
)
(mock_settings / "concepts.yaml").write_text(yaml_content, encoding="utf-8")
llm = MockLLM(GROUNDED_LLM_OUTPUT)
compile_all(changed_only=True, llm=llm, output_dir=str(mock_settings))
compile_all(changed_only=False, llm=llm, output_dir=str(mock_settings))
assert call_count >= 2, "Force --all should recompile even when unchanged"
# ---------------------------------------------------------------------------
# _chunk_hash
# ---------------------------------------------------------------------------
def test_chunk_hash_is_stable() -> None:
"""Same chunks produce the same hash each time."""
h1 = _chunk_hash(FIXTURE_CHUNKS)
h2 = _chunk_hash(FIXTURE_CHUNKS)
assert h1 == h2
assert len(h1) == 64
def test_chunk_hash_differs_on_content_change() -> None:
"""Different content produces different hash."""
modified = [dict(FIXTURE_CHUNKS[0], content="completely different content"), FIXTURE_CHUNKS[1]]
h1 = _chunk_hash(FIXTURE_CHUNKS)
h2 = _chunk_hash(modified)
assert h1 != h2

122
tests/wiki/test_concepts.py Normal file
View File

@@ -0,0 +1,122 @@
"""Tests for load_concepts() — YAML parsing of wiki/concepts.yaml."""
from __future__ import annotations
import textwrap
from pathlib import Path
import pytest
from codex.wiki import Concept, load_concepts
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
FIXTURE_YAML = textwrap.dedent(
"""\
concepts:
- slug: discrete-conformal-map
title: Discrete Conformal Map
aliases: [discrete conformal equivalence, discrete uniformization]
emphasis: Fokus auf die variationelle Charakterisierung (BPS 2015).
- slug: circle-packing
title: Circle Packing
aliases: [Koebe-Andreev-Thurston, circle pattern]
- slug: lobachevsky-function
title: Lobachevsky Function
aliases: [Milnor Lobachevsky, Clausen function]
emphasis: Verwendung in hyperbolischen Volumenformeln (Springborn 2008).
- slug: discrete-yamabe-flow
title: Discrete Yamabe Flow
aliases: [discrete Ricci flow, Chow-Luo]
- slug: hyperideal-tetrahedron
title: Hyperideal Tetrahedron
aliases: [hyperbolic volume, Schläfli, Ushijima]
"""
)
@pytest.fixture
def concepts_yaml(tmp_path: Path) -> Path:
"""Write fixture YAML to a temp file and return the path."""
p = tmp_path / "concepts.yaml"
p.write_text(FIXTURE_YAML, encoding="utf-8")
return p
# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------
def test_load_concepts_returns_all_concepts(concepts_yaml: Path) -> None:
"""All five concept entries are returned."""
concepts = load_concepts(str(concepts_yaml))
assert len(concepts) == 5
def test_load_concepts_types(concepts_yaml: Path) -> None:
"""Every returned item is a Concept instance."""
concepts = load_concepts(str(concepts_yaml))
for c in concepts:
assert isinstance(c, Concept)
def test_load_concepts_first_slug_and_title(concepts_yaml: Path) -> None:
"""First concept has correct slug and title."""
concepts = load_concepts(str(concepts_yaml))
assert concepts[0].slug == "discrete-conformal-map"
assert concepts[0].title == "Discrete Conformal Map"
def test_load_concepts_aliases_are_lists(concepts_yaml: Path) -> None:
"""Aliases are parsed as a list of strings."""
concepts = load_concepts(str(concepts_yaml))
for c in concepts:
assert isinstance(c.aliases, list)
for alias in c.aliases:
assert isinstance(alias, str)
def test_load_concepts_first_has_two_aliases(concepts_yaml: Path) -> None:
"""First concept has exactly two aliases."""
concepts = load_concepts(str(concepts_yaml))
assert concepts[0].aliases == ["discrete conformal equivalence", "discrete uniformization"]
def test_load_concepts_emphasis_present(concepts_yaml: Path) -> None:
"""Concepts with emphasis field have it parsed correctly."""
concepts = load_concepts(str(concepts_yaml))
dcm = next(c for c in concepts if c.slug == "discrete-conformal-map")
assert dcm.emphasis is not None
assert "variationelle" in dcm.emphasis
def test_load_concepts_emphasis_absent_is_none(concepts_yaml: Path) -> None:
"""Concepts without emphasis field have emphasis=None."""
concepts = load_concepts(str(concepts_yaml))
cp = next(c for c in concepts if c.slug == "circle-packing")
assert cp.emphasis is None
def test_load_concepts_slugs_unique(concepts_yaml: Path) -> None:
"""All slugs are distinct."""
concepts = load_concepts(str(concepts_yaml))
slugs = [c.slug for c in concepts]
assert len(slugs) == len(set(slugs))
def test_load_concepts_unicode_in_aliases(concepts_yaml: Path) -> None:
"""Aliases with non-ASCII characters (Schläfli) are parsed without error."""
concepts = load_concepts(str(concepts_yaml))
ht = next(c for c in concepts if c.slug == "hyperideal-tetrahedron")
assert any("Schläfli" in a for a in ht.aliases)
def test_load_concepts_empty_yaml(tmp_path: Path) -> None:
"""An empty concepts list returns an empty list (no crash)."""
p = tmp_path / "concepts.yaml"
p.write_text("concepts: []\n", encoding="utf-8")
concepts = load_concepts(str(p))
assert concepts == []