Der Katalog bildet nur Tiefe 1 ab, aber es liegen Seiten auf Tiefe 2 #57

Closed
opened 2026-09-04 19:25:47 +00:00 by torben · 1 comment
Owner

Umgesetzt und publiziert am 2026-09-04, Commit 251e597, Stack-Kandidat 4.8.0-beta.2. Aufgeworfen in der Sitzung zur Ordnerorganisation, neben #56 (move, erledigt), #59 (layout: für concept) und #58 (incoming/).

Was das Problem war

index_build.group_pages las genau zwei Ebenen unter kb/:

parts = page.path.relative_to(kb_dir).parts
collection_name = parts[0]
area_name = parts[1] if len(parts) > 2 else ""

Alles unterhalb von parts[1] kollabierte in die Level-1-Area. Betroffen waren drei reale Seiten:

kb/entities/projects/kfchou/wiki-skills.md
kb/entities/projects/vanillaflava/wiki-skills-vanillaflava.md
kb/entities/projects/yugasun/llm-wiki-skills.md

Diese drei Verzeichnisse erschienen im Katalog nirgends; ihre Seiten standen in der Tabelle von kb/entities/projects/, als lägen sie direkt darin. Ein eigener Shard war für sie strukturell unerreichbar — area.own_shard wird nur für Areas auf Ebene 1 gesetzt.

Warum das ein Defekt und keine Wunschfunktion war: Doku und Realität widersprachen sich. kb/CONTRACT.md § Collections beschrieb genau eine Ebene und sagte nichts über eine zweite; der Baum hatte trotzdem eine. Der Katalog, der laut AGENTS.md Invariante 1 generiert und verlässlich ist, beschrieb den Baum an dieser Stelle falsch. Einen unterstützten Weg, wie die Verzeichnisse entstanden sein können, gab es nicht — sie waren von Hand angelegt.

Ein Detail, das den Befund eingegrenzt hat: die Katalogzeilen sind [[wikilinks]], keine Pfade (index_build._table). Der Katalog verlinkte also nicht ins Leere, er gruppierte nur falsch. Belegt durch den Publish: kb/index.md und die INDEX.md-Shards haben sich beim Hochziehen der drei Seiten nicht geändert.

Entscheidung: (b), die drei Verzeichnisse auflösen

Der Alternativentwurf (a) — group_pages rekursiv über beliebige Tiefe, Shard-Schwelle pro Ebene — ist verworfen. Die drei Verzeichnisse gruppierten nach Owner (kfchou, vanillaflava, yugasun), eine Achse, die der Katalog an keiner anderen Stelle kennt und die aus keinem Frontmatter-Feld folgt. Jede andere Area im Baum kommt aus einem Subtype (entity_type über layout:); diese drei kamen aus einer Ad-hoc-Entscheidung beim Anlegen. Den Katalog rekursiv zu machen, um eine Achse abzubilden, die nichts erzeugt und nichts prüft, kauft Komplexität für einen Einzelfall.

Tiefe 1 ist damit geschriebene Grenze in kb/CONTRACT.md § Collections. Falls später eine echte zweite Ebene auftaucht — eine Collection, deren Areas selbst über die Schwelle wachsen — wird (a) neu aufgemacht, dann mit einem Grund, der aus dem Schema kommt.

Was gebaut wurde

kb_scan.find_nested_pages(kb_dir, pages)(title, page, depth) für jede Seite mehr als ein Verzeichnis unterhalb ihrer Collection. Reine Pfadtiefe, bewusst unabhängig von der Typauflösung: find_misplaced überspringt eine Seite ohne oder mit unauflösbarem type:, und genau die wäre sonst die verschachtelte Seite, die niemand meldet.

Lint-Befund nested_pages, mit Ist-Pfad und Tiefe — und hart, in HARD_ERROR_KEYS, anders als misplaced_pages. Begründung steht im Kommentarblock über HARD_ERROR_KEYS: eine fehlplatzierte Seite katalogisiert noch korrekt von der falschen Stelle aus, eine verschachtelte macht den generierten Katalog selbst falsch. Nicht migrations-gegatet, weil es keine Version gibt, ab der das erst falsch wird.

index rebuild warnt, statt abzulehnen. group_pages faltet unverändert wie zuvor; neu ist nur die Sichtbarkeit. Hart abzulehnen hätte auf jeder Fremdinstanz mit handverschachtelter Seite den Katalog blockiert — Handarbeit beim Upgrade, also die Major-Zeile aus version-parts.md statt des geplanten PATCH, und ein Widerspruch zu #56s Entscheidung 2.

TypeResolver.get_layout validiert layout: {dir: …} auf einen einzelnen Pfadabschnitt (kein /, kein \, nicht ./.., nicht leer). Ohne das wäre die neue Regel über den einzigen unterstützten Weg brechbar geblieben: ein Type-Spec mit dir: projects/owner hätte eine zweite Ebene erzeugt, die new selbst schreibt.

move entfernt ein Verzeichnis, das es geleert hat (--page wie --reconcile), symmetrisch zum mkdir(parents=True) auf der Zielseite. Nicht im ursprünglichen Entwurf, aber ohne das hätten kfchou/, vanillaflava/, yugasun/ ihren eigenen Fix überlebt: leer, für git unsichtbar, für einen verzeichnisbasierten Test sichtbar.

Verschiebemechanik

Wie vorgesehen über wikitool move --reconcile aus #56, nicht von Hand. Der Trockenlauf meldete exakt die drei Seiten und keine Kollision; die Stems sind verschieden, sie trafen unter kb/entities/projects/ auf 11 bestehende Dateien ohne Überschneidung. Ein belegtes Ziel hätte move ohnehin verweigert statt still überschrieben.

Was verifiziert wurde

  • pytest: 1008/1008 grün lokal (vorher 998; 10 neue Tests über test_kb_scan.py, test_lint.py, test_index_build.py, test_type_resolver.py, test_page_ops.py).
  • tools/wikitool docs verify und instructions verify: grün (51 Kommandos, 20 Instructions, 7 Skills).
  • lint --fail-on-error: Exit 0, weder nested_pages noch misplaced_pages noch duplicate_titles.
  • migrate verify --from HEAD: compared == 182, added == 0, removed == 0, alle drei Seiten als moved gemeldet. Git hat sie unabhängig davon als 100%-Renames erkannt.
  • find kb -mindepth 3 -type d liefert nichts.
  • CI-Lauf 178 — success, inklusive setup-instance-Replay gegen einen frischen dist export. Der parallele Release-Lauf 179 lief folgenlos durch und legte korrekt keine Release an — 4.8.0-beta.2 ist ein Kandidat, neueste Release bleibt v4.7.4.
  • Publish über das Mass-Update-Gate: 20 gezählte Dateien, Exit 42, vom Nutzer nach Vorlage der vollständigen Dateiliste freigegeben.

Akzeptanzkriterien

  • Kein Verzeichnis unter kb/ liegt tiefer als kb/<collection>/<area>/. Als bleibende Eigenschaft über lint statt über pytest — siehe die Umformulierung unten.
  • Die drei Seiten sind unter kb/entities/projects/ erreichbar, mit unveränderten Titeln. find_duplicate_title_paths meldet keine Kollision (lint Exit 0).
  • migrate verify --from HEAD bestätigt, dass die drei Seiten dieselben sind: compared == 182, added == 0, removed == 0, alle drei unter moved. Die Titel-Schlüsselung aus #56 stand, der Lauf hat also tatsächlich verglichen statt gezählt.
  • kb/CONTRACT.md § Collections schreibt Tiefe 1 als Grenze fest, mit der Begründung aus diesem Issue und dem Verweis auf den harten Lint-Befund.
  • group_pages faltet eine Seite auf Tiefe 2 nicht mehr still: index rebuild warnt, lint meldet sie hart. Gewählt wurde „melden", nicht „ablehnen" — Begründung oben unter Was gebaut wurde.
  • Changelog-Eintrag, PATCH. Gefaltet in den offenen 4.8.0-Kandidaten (max-wins gegen #56s MINOR-Bewegung), Heading jetzt 4.8.0-beta.2.

Umformulierung des ersten Kriteriums. Der Originaltext verlangte „ein Test läuft über den Baum". Gebaut ist stattdessen der harte nested_pages-Befund, den CI in .gitea/workflows/ci.yml (Schritt Verify the development tree) bei jedem Push als wikitool lint --fail-on-error gegen den echten Korpus laufen lässt; die Mechanik selbst deckt pytest über Fixtures ab. Grund: dist_cmd schließt tools/chemenu/tests/ nicht aus, ein pytest über config.KB_DIR wäre also in jeder ausgelieferten Instanz ein Korpus-Health-Check und würde dort rot, sobald jemand von Hand verschachtelt. Instanzgesundheit ist lint/doctors Aufgabe, nicht die der Stack-Suite. Die geforderte Eigenschaft — dauerhaft geprüft, nicht einmalig aufgeräumt — ist damit erfüllt, an der richtigen Stelle.

Was das für die Nachbarn heißt

  • #59 ist unberührt: seine sechs Concept-Areas liegen alle auf Ebene 1. Neu zu beachten ist nur, dass das dort einzuführende layout: in types/concept.md je dir: einen einzelnen Pfadabschnitt tragen muss — get_layout weist alles andere jetzt ab.
  • #58 ist unberührt; raw/ kennt diese Tiefengrenze nicht, dort ist die Tiefe ausdrücklich nicht das Problem.
  • #56 hat mit moves Verzeichnisaufräumen eine kleine Nachlieferung bekommen, die in seinem eigenen Paket noch nicht drin war.
Umgesetzt und publiziert am 2026-09-04, Commit `251e597`, Stack-Kandidat `4.8.0-beta.2`. Aufgeworfen in der Sitzung zur Ordnerorganisation, neben #56 (`move`, erledigt), #59 (`layout:` für `concept`) und #58 (`incoming/`). ## Was das Problem war `index_build.group_pages` las genau zwei Ebenen unter `kb/`: ```python parts = page.path.relative_to(kb_dir).parts collection_name = parts[0] area_name = parts[1] if len(parts) > 2 else "" ``` Alles unterhalb von `parts[1]` kollabierte in die Level-1-Area. Betroffen waren drei reale Seiten: ``` kb/entities/projects/kfchou/wiki-skills.md kb/entities/projects/vanillaflava/wiki-skills-vanillaflava.md kb/entities/projects/yugasun/llm-wiki-skills.md ``` Diese drei Verzeichnisse erschienen im Katalog nirgends; ihre Seiten standen in der Tabelle von `kb/entities/projects/`, als lägen sie direkt darin. Ein eigener Shard war für sie strukturell unerreichbar — `area.own_shard` wird nur für Areas auf Ebene 1 gesetzt. **Warum das ein Defekt und keine Wunschfunktion war:** Doku und Realität widersprachen sich. `kb/CONTRACT.md` § Collections beschrieb genau eine Ebene und sagte nichts über eine zweite; der Baum hatte trotzdem eine. Der Katalog, der laut AGENTS.md Invariante 1 generiert und verlässlich ist, beschrieb den Baum an dieser Stelle falsch. Einen unterstützten Weg, wie die Verzeichnisse entstanden sein können, gab es nicht — sie waren von Hand angelegt. Ein Detail, das den Befund eingegrenzt hat: die Katalogzeilen sind `[[wikilinks]]`, keine Pfade (`index_build._table`). Der Katalog verlinkte also nicht ins Leere, er gruppierte nur falsch. Belegt durch den Publish: `kb/index.md` und die `INDEX.md`-Shards haben sich beim Hochziehen der drei Seiten **nicht** geändert. ## Entscheidung: (b), die drei Verzeichnisse auflösen Der Alternativentwurf (a) — `group_pages` rekursiv über beliebige Tiefe, Shard-Schwelle pro Ebene — ist verworfen. Die drei Verzeichnisse gruppierten nach **Owner** (`kfchou`, `vanillaflava`, `yugasun`), eine Achse, die der Katalog an keiner anderen Stelle kennt und die aus keinem Frontmatter-Feld folgt. Jede andere Area im Baum kommt aus einem Subtype (`entity_type` über `layout:`); diese drei kamen aus einer Ad-hoc-Entscheidung beim Anlegen. Den Katalog rekursiv zu machen, um eine Achse abzubilden, die nichts erzeugt und nichts prüft, kauft Komplexität für einen Einzelfall. Tiefe 1 ist damit geschriebene Grenze in `kb/CONTRACT.md` § Collections. Falls später eine echte zweite Ebene auftaucht — eine Collection, deren Areas selbst über die Schwelle wachsen — wird (a) neu aufgemacht, dann mit einem Grund, der aus dem Schema kommt. ## Was gebaut wurde **`kb_scan.find_nested_pages(kb_dir, pages)`** — `(title, page, depth)` für jede Seite mehr als ein Verzeichnis unterhalb ihrer Collection. Reine Pfadtiefe, bewusst unabhängig von der Typauflösung: `find_misplaced` überspringt eine Seite ohne oder mit unauflösbarem `type:`, und genau die wäre sonst die verschachtelte Seite, die niemand meldet. **Lint-Befund `nested_pages`**, mit Ist-Pfad und Tiefe — und **hart**, in `HARD_ERROR_KEYS`, anders als `misplaced_pages`. Begründung steht im Kommentarblock über `HARD_ERROR_KEYS`: eine fehlplatzierte Seite katalogisiert noch korrekt von der falschen Stelle aus, eine verschachtelte macht den *generierten* Katalog selbst falsch. Nicht migrations-gegatet, weil es keine Version gibt, ab der das erst falsch wird. **`index rebuild` warnt, statt abzulehnen.** `group_pages` faltet unverändert wie zuvor; neu ist nur die Sichtbarkeit. Hart abzulehnen hätte auf jeder Fremdinstanz mit handverschachtelter Seite den Katalog blockiert — Handarbeit beim Upgrade, also die Major-Zeile aus `version-parts.md` statt des geplanten PATCH, und ein Widerspruch zu #56s Entscheidung 2. **`TypeResolver.get_layout` validiert `layout: {dir: …}`** auf einen einzelnen Pfadabschnitt (kein `/`, kein `\`, nicht `.`/`..`, nicht leer). Ohne das wäre die neue Regel über den einzigen *unterstützten* Weg brechbar geblieben: ein Type-Spec mit `dir: projects/owner` hätte eine zweite Ebene erzeugt, die `new` selbst schreibt. **`move` entfernt ein Verzeichnis, das es geleert hat** (`--page` wie `--reconcile`), symmetrisch zum `mkdir(parents=True)` auf der Zielseite. Nicht im ursprünglichen Entwurf, aber ohne das hätten `kfchou/`, `vanillaflava/`, `yugasun/` ihren eigenen Fix überlebt: leer, für git unsichtbar, für einen verzeichnisbasierten Test sichtbar. ## Verschiebemechanik Wie vorgesehen über `wikitool move --reconcile` aus #56, nicht von Hand. Der Trockenlauf meldete exakt die drei Seiten und keine Kollision; die Stems sind verschieden, sie trafen unter `kb/entities/projects/` auf 11 bestehende Dateien ohne Überschneidung. Ein belegtes Ziel hätte `move` ohnehin verweigert statt still überschrieben. ## Was verifiziert wurde - `pytest`: **1008/1008 grün** lokal (vorher 998; 10 neue Tests über `test_kb_scan.py`, `test_lint.py`, `test_index_build.py`, `test_type_resolver.py`, `test_page_ops.py`). - `tools/wikitool docs verify` und `instructions verify`: grün (51 Kommandos, 20 Instructions, 7 Skills). - `lint --fail-on-error`: **Exit 0**, weder `nested_pages` noch `misplaced_pages` noch `duplicate_titles`. - `migrate verify --from HEAD`: `compared == 182, added == 0, removed == 0`, alle drei Seiten als `moved` gemeldet. Git hat sie unabhängig davon als **100%-Renames** erkannt. - `find kb -mindepth 3 -type d` liefert nichts. - **CI-Lauf [178](https://gitea.nehmer.net/torben/chemenu/actions/runs/178) — success**, inklusive `setup-instance`-Replay gegen einen frischen `dist export`. Der parallele Release-Lauf [179](https://gitea.nehmer.net/torben/chemenu/actions/runs/179) lief folgenlos durch und legte korrekt **keine** Release an — `4.8.0-beta.2` ist ein Kandidat, neueste Release bleibt `v4.7.4`. - Publish über das Mass-Update-Gate: 20 gezählte Dateien, Exit 42, vom Nutzer nach Vorlage der vollständigen Dateiliste freigegeben. ## Akzeptanzkriterien - [x] Kein Verzeichnis unter `kb/` liegt tiefer als `kb/<collection>/<area>/`. **Als bleibende Eigenschaft über `lint` statt über pytest** — siehe die Umformulierung unten. - [x] Die drei Seiten sind unter `kb/entities/projects/` erreichbar, mit unveränderten Titeln. `find_duplicate_title_paths` meldet keine Kollision (`lint` Exit 0). - [x] `migrate verify --from HEAD` bestätigt, dass die drei Seiten dieselben sind: `compared == 182, added == 0, removed == 0`, alle drei unter `moved`. Die Titel-Schlüsselung aus #56 stand, der Lauf hat also tatsächlich verglichen statt gezählt. - [x] `kb/CONTRACT.md` § Collections schreibt Tiefe 1 als Grenze fest, mit der Begründung aus diesem Issue und dem Verweis auf den harten Lint-Befund. - [x] `group_pages` faltet eine Seite auf Tiefe 2 nicht mehr still: `index rebuild` warnt, `lint` meldet sie hart. **Gewählt wurde „melden", nicht „ablehnen"** — Begründung oben unter *Was gebaut wurde*. - [x] Changelog-Eintrag, **PATCH**. Gefaltet in den offenen `4.8.0`-Kandidaten (max-wins gegen #56s MINOR-Bewegung), Heading jetzt `4.8.0-beta.2`. **Umformulierung des ersten Kriteriums.** Der Originaltext verlangte „ein Test läuft über den Baum". Gebaut ist stattdessen der harte `nested_pages`-Befund, den CI in `.gitea/workflows/ci.yml` (Schritt *Verify the development tree*) bei jedem Push als `wikitool lint --fail-on-error` gegen den echten Korpus laufen lässt; die Mechanik selbst deckt pytest über Fixtures ab. Grund: `dist_cmd` schließt `tools/chemenu/tests/` **nicht** aus, ein pytest über `config.KB_DIR` wäre also in jeder ausgelieferten Instanz ein Korpus-Health-Check und würde dort rot, sobald jemand von Hand verschachtelt. Instanzgesundheit ist `lint`/`doctor`s Aufgabe, nicht die der Stack-Suite. Die geforderte Eigenschaft — dauerhaft geprüft, nicht einmalig aufgeräumt — ist damit erfüllt, an der richtigen Stelle. ## Was das für die Nachbarn heißt - **#59** ist unberührt: seine sechs Concept-Areas liegen alle auf Ebene 1. Neu zu beachten ist nur, dass das dort einzuführende `layout:` in `types/concept.md` je `dir:` einen einzelnen Pfadabschnitt tragen muss — `get_layout` weist alles andere jetzt ab. - **#58** ist unberührt; `raw/` kennt diese Tiefengrenze nicht, dort ist die Tiefe ausdrücklich nicht das Problem. - **#56** hat mit `move`s Verzeichnisaufräumen eine kleine Nachlieferung bekommen, die in seinem eigenen Paket noch nicht drin war.
torben added the prio/plannedsize/Sarea/kbkind/defect labels 2026-09-04 19:25:47 +00:00
Author
Owner

Body auf den Endstand umgeschrieben. Gegenüber dem Ausgangstext geändert:

  • Die beiden Wege (a)/(b) stehen nicht mehr als Vorschlag, sondern als getroffene Entscheidung mit Umsetzung.
  • Vier Entwurfspunkte, die der Ausgangstext offen ließ, sind entschieden und im Body begründet: melden statt ablehnen in group_pages/index rebuild, nested_pages hart statt advisory, layout:-dir-Validierung auf einen Pfadabschnitt, move räumt geleerte Verzeichnisse ab.
  • Das erste Akzeptanzkriterium ist umformuliert: der geforderte „Test über den Baum" ist der harte Lint-Befund, den CI gegen den echten Korpus laufen lässt, nicht ein pytest über config.KB_DIR — Grund im Body (die Suite wird mit dist export ausgeliefert).
  • Neu belegt statt vermutet: der Katalog verlinkte nie ins Leere (Zeilen sind Wikilinks), nachgewiesen dadurch, dass kb/index.md und die Shards sich beim Move nicht geändert haben.
  • Verifikationsabschnitt ergänzt (pytest 1008, CI 178/179, migrate verify, Mass-Update-Gate mit Freigabe).
Body auf den Endstand umgeschrieben. Gegenüber dem Ausgangstext geändert: - Die beiden Wege (a)/(b) stehen nicht mehr als Vorschlag, sondern als getroffene Entscheidung mit Umsetzung. - Vier Entwurfspunkte, die der Ausgangstext offen ließ, sind entschieden und im Body begründet: melden statt ablehnen in `group_pages`/`index rebuild`, `nested_pages` hart statt advisory, `layout:`-`dir`-Validierung auf einen Pfadabschnitt, `move` räumt geleerte Verzeichnisse ab. - Das erste Akzeptanzkriterium ist umformuliert: der geforderte „Test über den Baum" ist der harte Lint-Befund, den CI gegen den echten Korpus laufen lässt, nicht ein pytest über `config.KB_DIR` — Grund im Body (die Suite wird mit `dist export` ausgeliefert). - Neu belegt statt vermutet: der Katalog verlinkte nie ins Leere (Zeilen sind Wikilinks), nachgewiesen dadurch, dass `kb/index.md` und die Shards sich beim Move nicht geändert haben. - Verifikationsabschnitt ergänzt (pytest 1008, CI 178/179, `migrate verify`, Mass-Update-Gate mit Freigabe).
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: torben/chemenu#57