Chemenu 2.1.0 - deterministischer Wissenskompiler
CI / verify (push) Failing after 32s
Release / release (push) Successful in 38s

Chemenu kompiliert Rohnotizen zu einem verlinkten, quellengebundenen Wiki:
raw/ -> types/ + tools/ -> kb/ -> reports/. Was mechanisch ist, macht
tools/wikitool; was Urteil braucht, macht ein Agent unter Contracts, deren
Grenzen in Code durchgesetzt sind statt im Prompt.

Dieser Commit ist der Startpunkt der oeffentlichen Historie. Die vorherige
Entwicklung fand in einer privaten Instanz statt und ist nicht Teil dieses
Repositorys; ihre Erzaehlung steht vollstaendig in CHANGES.md, das mit 44
Eintraegen von 0.1.0 bis 2.1.0 erhalten geblieben ist.

Der mitgelieferte Korpus ist ein Testbett und eine Demo: 170 Seiten ueber den
Stack selbst - Gates, Lint, Versionierung, Suche, das Wiki-Muster. Er
dokumentiert das Werkzeug mit den eigenen Mitteln des Werkzeugs.

Lizenz: AGPL-3.0 fuer den Stack (tools/, types/), CC-BY-4.0 fuer die Inhalte.
Die Grenze zwischen beiden ist der Dateiplan, den dist export berechnet -
siehe NOTICE.
This commit is contained in:
2026-09-01 16:24:34 +02:00
commit 18ae28f918
368 changed files with 50628 additions and 0 deletions
+204
View File
@@ -0,0 +1,204 @@
from pathlib import Path
import pytest
from chemenu.commands.index_build import (
SHARD_THRESHOLD,
_anchor,
build_index,
plan_index,
stale_shards,
)
from chemenu.frontmatter_io import write_page
from chemenu.kb_scan import GENERATED_INDEX, iter_kb_pages
from chemenu.type_resolver import resolver
def _area_title(subtype: str) -> str:
"""The display title `index rebuild` will use for an entity subtype.
Read from the type-spec rather than written out, because these titles follow
the KB language: hard-coding them made translating the wiki fail tests that
are not about wording at all.
"""
return resolver.get_layout("types/entity.md")[subtype]["title"]
@pytest.fixture
def plan(kb_dir: Path):
return plan_index(kb_dir)
def _shard(plan: dict, kb_dir: Path, *parts: str) -> str:
return plan[kb_dir.joinpath(*parts, GENERATED_INDEX)]
def _section(content: str, heading: str) -> str:
"""Text from a heading line up to the next heading of any level."""
start = content.index(heading) + len(heading)
rest = content[start:]
for line in rest.splitlines(keepends=True):
if line.startswith("#"):
return rest[: rest.index(line)]
return rest
# --- the map ----------------------------------------------------------------
def test_map_reports_totals_per_collection(kb_dir):
content = build_index(kb_dir)
assert "**Total Pages:** 5" in content
assert "**Entities:** 3" in content
assert "**Concepts:** 1" in content
assert "**Sources:** 1" in content
def test_map_lists_an_empty_collection_rather_than_hiding_it(kb_dir):
"""comparisons/ holds no pages but is a real collection - a reader must
still be able to see that it exists."""
assert "| `comparisons/` | 0 |" in build_index(kb_dir)
def test_map_lists_every_area_with_its_count(kb_dir):
content = build_index(kb_dir)
assert f"| {_area_title('system')} | 2 |" in content
assert f"| {_area_title('tool')} | 1 |" in content
# Areas that exist as directories but hold no pages are simply absent.
assert f"| {_area_title('person')} |" not in content
def test_map_carries_no_page_rows(kb_dir):
"""The whole point of the map: reading it must not cost one row per page."""
content = build_index(kb_dir)
assert "[[aurora]]" not in content
assert "[[Modbus]]" not in content
assert len(content.splitlines()) < 60
def test_map_points_at_search_first(kb_dir):
assert "wikitool search" in build_index(kb_dir)
def test_map_links_to_each_collection_shard(kb_dir):
content = build_index(kb_dir)
assert f"[entities/{GENERATED_INDEX}](entities/{GENERATED_INDEX})" in content
def test_map_deep_links_an_inlined_area_by_anchor(kb_dir):
# The anchor is derived from the area's display title, so it follows the KB
# language along with it. Both sides of the link are generated in the same
# run, so they stay consistent; only a bookmark to an old anchor would break.
anchor = _anchor(_area_title("system"))
assert f"entities/{GENERATED_INDEX}#{anchor}" in build_index(kb_dir)
# --- collection shards ------------------------------------------------------
def test_collection_shard_holds_the_page_rows(plan, kb_dir):
entities = _shard(plan, kb_dir, "entities")
assert "[[aurora]]" in entities
assert "[[Nathan]]" in entities
assert "[[gdeploy]]" in entities
def test_pages_are_grouped_by_area_and_sorted_case_insensitively(plan, kb_dir):
systems = _section(_shard(plan, kb_dir, "entities"), f"## {_area_title('system')}")
assert systems.index("[[aurora]]") < systems.index("[[Nathan]]")
assert "[[gdeploy]]" not in systems
def test_area_titles_come_from_the_entity_type_spec_layout(plan, kb_dir):
entities = _shard(plan, kb_dir, "entities")
assert f"## {_area_title('system')}" in entities
assert f"## {_area_title('tool')}" in entities
def test_summary_prefers_frontmatter_then_falls_back_to_body(plan, kb_dir):
entities = _shard(plan, kb_dir, "entities")
assert "Server hosting DocStore with ZFS storage" in entities # frontmatter
assert "Deploy tool." in entities # first line of ## Description
def test_shards_are_marked_generated(plan, kb_dir):
assert all(content.startswith("<!-- Generated by") for content in plan.values())
# --- sharding threshold -----------------------------------------------------
def test_area_over_the_threshold_gets_its_own_shard(kb_dir):
for n in range(SHARD_THRESHOLD + 1):
write_page(
kb_dir / f"entities/tools/tool-{n:03d}.md",
{
"type": "types/entity.md", "entity_type": "tool",
"created": "2026-08-01", "modified": "2026-08-01",
"summary": f"Tool {n}",
},
f"\n# tool-{n:03d}\n",
)
plan = plan_index(kb_dir)
tools_shard = kb_dir / "entities" / "tools" / GENERATED_INDEX
assert tools_shard in plan
assert "[[tool-000]]" in plan[tools_shard]
# The collection shard links to it instead of inlining the rows again.
entities = _shard(plan, kb_dir, "entities")
assert "[[tool-000]]" not in entities
assert f"tools/{GENERATED_INDEX}" in entities
# ...and the map points straight at the area's own shard.
assert f"entities/tools/{GENERATED_INDEX}" in plan[kb_dir / "index.md"]
def test_area_at_the_threshold_stays_inlined(kb_dir):
for n in range(SHARD_THRESHOLD - 1): # +1 existing tool = exactly threshold
write_page(
kb_dir / f"entities/tools/tool-{n:03d}.md",
{
"type": "types/entity.md", "entity_type": "tool",
"created": "2026-08-01", "modified": "2026-08-01", "summary": "x",
},
f"\n# tool-{n:03d}\n",
)
plan = plan_index(kb_dir)
assert kb_dir / "entities" / "tools" / GENERATED_INDEX not in plan
assert "[[tool-000]]" in _shard(plan, kb_dir, "entities")
# --- stale shards -----------------------------------------------------------
def test_stale_shard_is_detected(kb_dir, plan):
orphan = kb_dir / "entities" / "people" / GENERATED_INDEX
orphan.write_text("# leftover\n", encoding="utf-8")
assert stale_shards(kb_dir, plan) == [orphan]
def test_planned_shards_are_not_stale(kb_dir, plan):
for path, content in plan.items():
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")
assert stale_shards(kb_dir, plan_index(kb_dir)) == []
# --- interaction with the scanner -------------------------------------------
def test_scanner_ignores_generated_shards_at_any_depth(kb_dir, plan):
"""A shard lists every page in its subtree as a wikilink. Counted as a page
it would make every page look linked-to and silence the orphan check."""
for path, content in plan.items():
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")
(kb_dir / "entities" / "systems" / GENERATED_INDEX).write_text("# x\n", encoding="utf-8")
scanned = {p.name for p in iter_kb_pages(kb_dir)}
assert GENERATED_INDEX not in scanned
assert "COLLECTION.md" not in scanned
def test_rebuild_is_idempotent(kb_dir):
assert plan_index(kb_dir) == plan_index(kb_dir)