page move: eine kb-Seite folgt ihrem Subtype ins Verzeichnis, das ihr Type-Spec berechnet #56

Closed
opened 2026-09-04 19:25:03 +00:00 by torben · 2 comments
Owner

Umgesetzt und publiziert am 2026-09-04, Commit 9e41431, Stack-Kandidat 4.8.0-beta.1. Aufgeworfen in der Sitzung zur Ordnerorganisation; Nachbarn: #57 (Katalogtiefe), #59 (layout: für concept, hing hieran), #58 (incoming/), #16 (raw rename).

Was das Problem war

Nichts im Stack bewegte eine Seite zwischen Verzeichnissen. rename schrieb nie über eine Verzeichnisgrenze (target.path.parent / f"{new}.md"), und new_page._target_dir rechnete die Platzierung ausschließlich beim Anlegen. Änderte sich entity_type später, blieb die Datei still am alten Ort — und nichts meldete das: weder lint_core.py noch doctor.py verglichen den Ist-Ort einer Seite mit dem, was ihr Type-Spec berechnen würde. Der Zustand war unbehebbar und unsichtbar.

Gegen den realen Bestand geprüft: 3 Seiten lagen tatsächlich falsch — die drei aus #57 (kb/entities/projects/{kfchou,vanillaflava,yugasun}/*.md).

Was gebaut wurde

Eine Platzierungslogik, ein Ort. TypeResolver.compute_target_dir (plus TypeResolver.subtype_dir für den layout:-Teil) in tools/chemenu/type_resolver.py; new_page._target_dir und _page_subdir delegieren nur noch dorthin. lint und move rufen dieselbe Methode.

wikitool move — top-level registriert, wie rename/rm:

  • move --page "<Titel>" bewegt eine Seite an den berechneten Ort. Kein --to <dir>; das Ziel ist abgeleitet, nicht wählbar.
  • move --reconcile wendet dieselbe Regel auf den ganzen Bestand an, über lint_core.find_misplaced — dieselbe Funktion, die der Lint-Befund nutzt.
  • Beide Modi fassen weder Body noch Frontmatter an; der Titel ändert sich nie, also folgt kein Referenz-Update.
  • Idempotent: ein zweiter --reconcile-Lauf meldet „Nothing to move".
  • Ein belegtes Ziel (Rest einer Stem-Kollision) wird verweigert statt still überschrieben.

lint-Befund misplaced_pages mit Ist- und Soll-Pfad, plus dem Kommando zum Beheben. Advisory, nicht in HARD_ERROR_KEYS.

migrate verify auf Titel-Schlüsselung. Siehe „Der Fund unterwegs" unten.

--op move in log_append.VALID_OPS, mit den zugehörigen Zeilen in tools/CONTRACT.md, instructions/page-lifecycle.md (neuer Abschnitt „Move") und instructions/publish-cycle.md.

Entscheidungen

  1. Kommandoform: top-level move, kein page-Unterbefehlsbaum. rename und rm — die Geschwister derselben Seitenlebenszyklus-Familie — sind top-level; ein Sub-App für genau ein Kommando daneben wäre eine zweite Konvention für dieselbe Sache. Der ursprüngliche Titel dieses Issues sagt noch page move; gebaut ist wikitool move.
  2. Der Lint-Befund ist advisory. Als Hard Error wäre lint auf jeder Instanz mit handplatzierten Seiten sofort rot gewesen — auf dieser hier ab Tag eins, wegen der drei #57-Seiten. Ein handplatzierter Bestand ist keine kaputte Seite, und es gibt keine Version, ab der „nicht im berechneten Verzeichnis" falsch wird. Begründung steht im Kommentarblock über HARD_ERROR_KEYS, neben orphan_pages/redundant_see_also.
  3. #56 zuerst, ohne den Bestand anzufassen. kb/ ist in diesem Commit unverändert. Die drei realen Seiten bleiben liegen; #57 zieht sie hoch — jetzt mit move --reconcile statt Handarbeit, zusammen mit seinem eigenen kb/CONTRACT.md-Satz und der group_pages-Ablehnung, die #56 nicht mitliefert.
  4. Kein git mv-mit-mv-Fallback. Der ursprüngliche Vorschlag übernahm das aus #16, wo es hingehört: eine frisch abgelegte Rohdatei kann ungetrackt sein. Für kb-Seiten trägt das Argument nicht — rename und rm bewegen bzw. löschen längst über Path.rename/unlink, und publish staged ohnehin über git add -A, womit der Endzustand identisch ist. move folgt dem Präzedenzfall. Damit entfällt auch das gemeinsame Kleinstprimitiv, das dieses Issue und #16 laut ursprünglichem Text geteilt hätten: #16 schreibt es allein, wenn es drankommt.

Der Fund unterwegs

migrate verify schlüsselte Seiten über den repo-relativen Pfad (_shapes_at_revision/_shapes_now). Ein reiner Move ergab „N removed, N added, 0 compared, 0 findings" und lief grün durch — der eine mechanische Check, für den instructions/migrate-corpus.md Schritt 4 existiert, hätte bei genau der Operation nichts geprüft, die dieses Issue einführt. Ohne #59s Vorbehalt wäre das erst bei 80 verschobenen Concept-Seiten aufgefallen, und dann als grüner Lauf.

Behoben: PageShape trägt zusätzlich den Pfad, beide Shape-Dicts schlüsseln über den Titel (die einzige Identität einer Seite, AGENTS.md Invariante 2), und CorpusDiff.moved meldet einen reinen Ortswechsel separat — informativ, nie als Finding. compare() bleibt agnostisch gegenüber dem Schlüssel; die Titel-Schlüsselung sitzt bei den Aufrufern in migrate_cmd.py.

Ein eigener Bug dabei — die „nachher"-Seite lieferte absolute statt repo-relative Pfade in moved — wurde vom neuen Regressionstest gefangen, nicht von Augenmaß.

Was verifiziert wurde

  • pytest: 998/998 grün lokal (vorher 975; 23 neue Tests über test_page_ops.py, test_lint.py, test_type_resolver.py, test_corpus_diff.py, test_migrate_cmd.py).
  • tools/wikitool docs verify: grün (51 Kommandos dokumentiert, beide Richtungen).
  • tools/wikitool instructions verify: grün (20 Instructions, 7 Skills, 14 publizierte Kopien deckungsgleich).
  • CI-Lauf 176 — success, inklusive setup-instance-Replay gegen einen frischen dist export. Der parallele release-Lauf 177 lief folgenlos durch und legte korrekt keine Release an — 4.8.0-beta.1 ist ein Kandidat, neueste Release bleibt v4.7.4.
  • Gegen den echten Bestand trocken geprüft: move --page "wiki-skills" --dry-run und move --reconcile --dry-run melden genau die drei #57-Seiten, ohne zu schreiben. lint --json listet dieselben drei unter misplaced_pages.

Akzeptanzkriterien

  • move --page "<Titel>" bewegt die Datei an den Ort, den new für dieselbe Frontmatter berechnet hätte; die Platzierungslogik existiert genau einmal, geteilt mit new_page._target_dir über TypeResolver.compute_target_dir.
  • --dry-run zeigt Quell- und Zielpfad, ohne zu schreiben.
  • Nach move --reconcile: identische Menge der Seitentitel, jede Seite unter ihrem berechneten Verzeichnis, Body und Frontmatter unverändert — alle drei Eigenschaften geprüft, nicht nur die dritte.
  • --reconcile ist idempotent: ein zweiter Lauf bewegt nichts und meldet das.
  • lint meldet eine falsch platzierte Seite mit Ist- und Soll-Pfad. Zwei Tests: einer feuert, einer schweigt auf korrekt platzierten Seiten. Dazu ein dritter, der festhält, dass der Befund lint nicht rot macht.
  • migrate verify --from HEAD meldet nach einem reinen Move keine Seite als removed/added, sondern vergleicht sie als dieselbe. Regressionstest mit 3 verschobenen Seiten: compared == 3, added == 0, removed == 0.
  • Ein Test deckt die nicht versionierte Datei ab (git mv scheitert, mv greift). Gestrichen — kein git mv/mv-Fallback gebaut, siehe Entscheidung 4. Es gibt keinen Codepfad zu testen.
  • Zeilen in tools/CONTRACT.md für Kommando und Fehlerkontrakt; docs verify erzwingt die Kommandozeile in beide Richtungen. (Anmerkung: die Fehlerkontrakt-Tabelle prüft docs verify nicht mechanisch — das Kriterium überschätzte das Werkzeug. Die Zeile steht, geprüft ist sie von Hand.)
  • instructions/page-lifecycle.md beschreibt den Verschiebefall (neuer Abschnitt „Move", plus --op move im Abschluss).
  • Changelog-Eintrag, MINOR. Drop-in-Test: vorwärts reines Überkopieren, rückwärts liest ein älterer Stack eine verschobene Seite unverändert — die Identität ist der Titel, nicht der Ort.

Was das für die Nachbarn heißt

  • #59 ist nicht mehr auf #56 blockiert: die Stem-Schlüsselung, an der sein --minor-Argument hing, steht. Das status/blocked dort kann weg.
  • #57 hat jetzt sein Werkzeug. Sein eigener Teil — Tiefe 1 als Grenze in kb/CONTRACT.md, group_pages meldet statt still zu falten — ist unberührt.
  • #16 schreibt das git mv-Primitiv allein, falls es eines braucht; dieses Issue liefert keines.

Offen für später: der Kandidat 4.8.0-beta.1 wird mit version release geschlossen, wenn er reif ist — nicht Teil dieses Pakets.

Umgesetzt und publiziert am 2026-09-04, Commit `9e41431`, Stack-Kandidat `4.8.0-beta.1`. Aufgeworfen in der Sitzung zur Ordnerorganisation; Nachbarn: #57 (Katalogtiefe), #59 (`layout:` für `concept`, hing hieran), #58 (`incoming/`), #16 (`raw rename`). ## Was das Problem war Nichts im Stack bewegte eine Seite zwischen Verzeichnissen. `rename` schrieb nie über eine Verzeichnisgrenze (`target.path.parent / f"{new}.md"`), und `new_page._target_dir` rechnete die Platzierung ausschließlich beim Anlegen. Änderte sich `entity_type` später, blieb die Datei still am alten Ort — und nichts meldete das: weder `lint_core.py` noch `doctor.py` verglichen den Ist-Ort einer Seite mit dem, was ihr Type-Spec berechnen würde. Der Zustand war unbehebbar *und* unsichtbar. Gegen den realen Bestand geprüft: **3 Seiten lagen tatsächlich falsch** — die drei aus #57 (`kb/entities/projects/{kfchou,vanillaflava,yugasun}/*.md`). ## Was gebaut wurde **Eine Platzierungslogik, ein Ort.** `TypeResolver.compute_target_dir` (plus `TypeResolver.subtype_dir` für den `layout:`-Teil) in `tools/chemenu/type_resolver.py`; `new_page._target_dir` und `_page_subdir` delegieren nur noch dorthin. `lint` und `move` rufen dieselbe Methode. **`wikitool move`** — top-level registriert, wie `rename`/`rm`: - `move --page "<Titel>"` bewegt eine Seite an den berechneten Ort. Kein `--to <dir>`; das Ziel ist abgeleitet, nicht wählbar. - `move --reconcile` wendet dieselbe Regel auf den ganzen Bestand an, über `lint_core.find_misplaced` — dieselbe Funktion, die der Lint-Befund nutzt. - Beide Modi fassen weder Body noch Frontmatter an; der Titel ändert sich nie, also folgt kein Referenz-Update. - Idempotent: ein zweiter `--reconcile`-Lauf meldet „Nothing to move". - Ein belegtes Ziel (Rest einer Stem-Kollision) wird verweigert statt still überschrieben. **`lint`-Befund `misplaced_pages`** mit Ist- und Soll-Pfad, plus dem Kommando zum Beheben. Advisory, **nicht** in `HARD_ERROR_KEYS`. **`migrate verify` auf Titel-Schlüsselung.** Siehe „Der Fund unterwegs" unten. **`--op move`** in `log_append.VALID_OPS`, mit den zugehörigen Zeilen in `tools/CONTRACT.md`, `instructions/page-lifecycle.md` (neuer Abschnitt „Move") und `instructions/publish-cycle.md`. ## Entscheidungen 1. **Kommandoform: top-level `move`, kein `page`-Unterbefehlsbaum.** `rename` und `rm` — die Geschwister derselben Seitenlebenszyklus-Familie — sind top-level; ein Sub-App für genau ein Kommando daneben wäre eine zweite Konvention für dieselbe Sache. Der ursprüngliche Titel dieses Issues sagt noch `page move`; gebaut ist `wikitool move`. 2. **Der Lint-Befund ist advisory.** Als Hard Error wäre `lint` auf jeder Instanz mit handplatzierten Seiten sofort rot gewesen — auf dieser hier ab Tag eins, wegen der drei #57-Seiten. Ein handplatzierter Bestand ist keine kaputte Seite, und es gibt keine Version, ab der „nicht im berechneten Verzeichnis" falsch wird. Begründung steht im Kommentarblock über `HARD_ERROR_KEYS`, neben `orphan_pages`/`redundant_see_also`. 3. **#56 zuerst, ohne den Bestand anzufassen.** `kb/` ist in diesem Commit unverändert. Die drei realen Seiten bleiben liegen; #57 zieht sie hoch — jetzt mit `move --reconcile` statt Handarbeit, zusammen mit seinem eigenen `kb/CONTRACT.md`-Satz und der `group_pages`-Ablehnung, die #56 nicht mitliefert. 4. **Kein `git mv`-mit-`mv`-Fallback.** Der ursprüngliche Vorschlag übernahm das aus #16, wo es hingehört: eine frisch abgelegte **Rohdatei** kann ungetrackt sein. Für kb-Seiten trägt das Argument nicht — `rename` und `rm` bewegen bzw. löschen längst über `Path.rename`/`unlink`, und `publish` staged ohnehin über `git add -A`, womit der Endzustand identisch ist. `move` folgt dem Präzedenzfall. Damit entfällt auch das gemeinsame Kleinstprimitiv, das dieses Issue und #16 laut ursprünglichem Text geteilt hätten: **#16 schreibt es allein, wenn es drankommt.** ## Der Fund unterwegs `migrate verify` schlüsselte Seiten über den repo-relativen **Pfad** (`_shapes_at_revision`/`_shapes_now`). Ein reiner Move ergab „N removed, N added, 0 compared, 0 findings" und lief grün durch — der eine mechanische Check, für den `instructions/migrate-corpus.md` Schritt 4 existiert, hätte bei genau der Operation nichts geprüft, die dieses Issue einführt. Ohne #59s Vorbehalt wäre das erst bei 80 verschobenen Concept-Seiten aufgefallen, und dann als grüner Lauf. Behoben: `PageShape` trägt zusätzlich den Pfad, beide Shape-Dicts schlüsseln über den Titel (die einzige Identität einer Seite, AGENTS.md Invariante 2), und `CorpusDiff.moved` meldet einen reinen Ortswechsel separat — informativ, nie als Finding. `compare()` bleibt agnostisch gegenüber dem Schlüssel; die Titel-Schlüsselung sitzt bei den Aufrufern in `migrate_cmd.py`. Ein eigener Bug dabei — die „nachher"-Seite lieferte absolute statt repo-relative Pfade in `moved` — wurde vom neuen Regressionstest gefangen, nicht von Augenmaß. ## Was verifiziert wurde - `pytest`: **998/998 grün** lokal (vorher 975; 23 neue Tests über `test_page_ops.py`, `test_lint.py`, `test_type_resolver.py`, `test_corpus_diff.py`, `test_migrate_cmd.py`). - `tools/wikitool docs verify`: grün (51 Kommandos dokumentiert, beide Richtungen). - `tools/wikitool instructions verify`: grün (20 Instructions, 7 Skills, 14 publizierte Kopien deckungsgleich). - **CI-Lauf [176](https://gitea.nehmer.net/torben/chemenu/actions/runs/176) — success**, inklusive `setup-instance`-Replay gegen einen frischen `dist export`. Der parallele `release`-Lauf [177](https://gitea.nehmer.net/torben/chemenu/actions/runs/177) lief folgenlos durch und legte korrekt **keine** Release an — `4.8.0-beta.1` ist ein Kandidat, neueste Release bleibt `v4.7.4`. - Gegen den echten Bestand trocken geprüft: `move --page "wiki-skills" --dry-run` und `move --reconcile --dry-run` melden genau die drei #57-Seiten, ohne zu schreiben. `lint --json` listet dieselben drei unter `misplaced_pages`. ## Akzeptanzkriterien - [x] `move --page "<Titel>"` bewegt die Datei an den Ort, den `new` für dieselbe Frontmatter berechnet hätte; die Platzierungslogik existiert genau einmal, geteilt mit `new_page._target_dir` über `TypeResolver.compute_target_dir`. - [x] `--dry-run` zeigt Quell- und Zielpfad, ohne zu schreiben. - [x] Nach `move --reconcile`: identische Menge der Seitentitel, jede Seite unter ihrem berechneten Verzeichnis, Body und Frontmatter unverändert — alle drei Eigenschaften geprüft, nicht nur die dritte. - [x] `--reconcile` ist idempotent: ein zweiter Lauf bewegt nichts und meldet das. - [x] `lint` meldet eine falsch platzierte Seite mit Ist- und Soll-Pfad. Zwei Tests: einer feuert, einer schweigt auf korrekt platzierten Seiten. Dazu ein dritter, der festhält, dass der Befund `lint` nicht rot macht. - [x] `migrate verify --from HEAD` meldet nach einem reinen Move keine Seite als removed/added, sondern vergleicht sie als dieselbe. Regressionstest mit 3 verschobenen Seiten: `compared == 3, added == 0, removed == 0`. - [x] ~~Ein Test deckt die nicht versionierte Datei ab (`git mv` scheitert, `mv` greift).~~ **Gestrichen** — kein `git mv`/`mv`-Fallback gebaut, siehe Entscheidung 4. Es gibt keinen Codepfad zu testen. - [x] Zeilen in `tools/CONTRACT.md` für Kommando **und** Fehlerkontrakt; `docs verify` erzwingt die Kommandozeile in beide Richtungen. *(Anmerkung: die Fehlerkontrakt-Tabelle prüft `docs verify` **nicht** mechanisch — das Kriterium überschätzte das Werkzeug. Die Zeile steht, geprüft ist sie von Hand.)* - [x] `instructions/page-lifecycle.md` beschreibt den Verschiebefall (neuer Abschnitt „Move", plus `--op move` im Abschluss). - [x] Changelog-Eintrag, **MINOR**. Drop-in-Test: vorwärts reines Überkopieren, rückwärts liest ein älterer Stack eine verschobene Seite unverändert — die Identität ist der Titel, nicht der Ort. ## Was das für die Nachbarn heißt - **#59** ist nicht mehr auf #56 blockiert: die Stem-Schlüsselung, an der sein `--minor`-Argument hing, steht. Das `status/blocked` dort kann weg. - **#57** hat jetzt sein Werkzeug. Sein eigener Teil — Tiefe 1 als Grenze in `kb/CONTRACT.md`, `group_pages` meldet statt still zu falten — ist unberührt. - **#16** schreibt das `git mv`-Primitiv allein, falls es eines braucht; dieses Issue liefert keines. Offen für später: der Kandidat `4.8.0-beta.1` wird mit `version release` geschlossen, wenn er reif ist — nicht Teil dieses Pakets.
torben added the prio/plannedsize/Marea/kbkind/build labels 2026-09-04 19:25:03 +00:00
Author
Owner

Changelog: Umgesetzt (move --page/--reconcile, lint-Befund advisory, migrate verify auf Titel-Schlüsselung umgestellt, CHANGES.md 4.8.0-beta.1 geschrieben). Kein git mv-Fallback gebaut (Path.rename wie rename/rm). Publish steht noch aus, pausiert auf Nutzeranweisung — Schließen folgt nach stack-close.

**Changelog:** Umgesetzt (`move --page`/`--reconcile`, `lint`-Befund advisory, `migrate verify` auf Titel-Schlüsselung umgestellt, `CHANGES.md` 4.8.0-beta.1 geschrieben). Kein `git mv`-Fallback gebaut (Path.rename wie rename/rm). Publish steht noch aus, pausiert auf Nutzeranweisung — Schließen folgt nach `stack-close`.
Author
Owner

Changelog: Abschluss-Rewrite. Body auf Endzustand: Befund in die Vergangenheit gesetzt, die vier Entscheidungen (top-level move statt page move; Lint-Befund advisory; Bestand unangetastet; kein git mv-Fallback) stehen als getroffen statt als Vorschlag, Verifikation benannt (998 Tests, docs verify, instructions verify, CI-Lauf 176 success, Trockenlauf gegen den echten Bestand). Kriterium 7 (git mv/mv-Test) gestrichen mit Begründung; Kriterium 8 um die Anmerkung ergänzt, dass docs verify nur die Kommando-, nicht die Fehlerkontrakt-Tabelle mechanisch prüft. Neuer Abschnitt zu den Folgen für #59/#57/#16. Schließt das Issue.

**Changelog:** Abschluss-Rewrite. Body auf Endzustand: Befund in die Vergangenheit gesetzt, die vier Entscheidungen (top-level `move` statt `page move`; Lint-Befund advisory; Bestand unangetastet; kein `git mv`-Fallback) stehen als getroffen statt als Vorschlag, Verifikation benannt (998 Tests, `docs verify`, `instructions verify`, CI-Lauf 176 success, Trockenlauf gegen den echten Bestand). Kriterium 7 (`git mv`/`mv`-Test) gestrichen mit Begründung; Kriterium 8 um die Anmerkung ergänzt, dass `docs verify` nur die Kommando-, nicht die Fehlerkontrakt-Tabelle mechanisch prüft. Neuer Abschnitt zu den Folgen für #59/#57/#16. Schließt das Issue.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: torben/chemenu#56