Teilpaket von #140. Entschieden: D4 (hart, 2026-09-27), D18 (Umfang, 2026-09-28), D28–D31 (Präzisierungen, 2026-09-29). Erledigt 2026-09-30: veröffentlicht mit 8be5e6e, Version 8.0.0-beta.1 (öffnet den 8.0.0-Kandidaten, erste Grenzüberschreitung; kein version release, das bleibt beim Betreiber).
Warum
Titel werden eins zu eins zu Dateinamen (new_page.py: f"{page_title}.md"). Daraus folgten zwei Probleme:
Unter Linux erlaubt, unter Windows ungültig. Ein Titel mit :, ?, " o. ä. ließ sich unter Linux anlegen und committen, brach danach aber jeden Windows-Klon der Instanz: git checkout scheitert an der Datei.
Groß-/Kleinschreibung und Normalisierung. Zwei Titel, die sich nur darin unterscheiden, kollidieren auf NTFS und auf APFS (Standardeinstellung); APFS behandelt zusätzlich NFC und NFD als denselben Namen. Unter Linux fiel beides nicht auf.
Mit nativem Windows-Support (#140 D8) gilt die Regel deshalb auf allen Plattformen, nicht nur unter Windows.
Nebenbefund, mitbehoben:new prüfte Kollisionen nur für root: kb. tools/wikitool new instruction --name gates überschrieb deshalb instructions/gates.md kommentarlos mit einem Gerüst. new überschreibt jetzt nie eine bestehende Datei, für keine Wurzel.
Regel (D18, präzisiert durch D28–D30)
Normativ steht sie einmal, in kb/CONTRACT.md § Titles are identifiers; umgesetzt in tools/chemenu/titles.py. Ein Titel darf nicht:
leer sein (D30)
eines der Zeichen < > : " / \ | ? * enthalten
Steuerzeichen enthalten (gemeldet als U+XXXX)
ein reservierter Name sein – maßgeblich ist der Teil vor dem ersten Punkt, ohne Rücksicht auf Groß-/Kleinschreibung und ohne abschließende Leerzeichen:
nicht global reserviert: Kein instanziierbarer Typ legt Seiten direkt in kb/ ab, und „Log“ oder „Contract“ sind legitime Seitentitel. Die Verzeichnisprüfung fängt den Fall trotzdem ab.
D30 Reservierte Namen vollständig (2026-09-29):COM0/LPT0 und COM¹²³/LPT¹²³ aufgenommen, dazu die Weigerung bei leerem Titel. Unsicher bleibt, ob jede Windows-Version alle davon sperrt; die strengere Regel kostet nichts. So steht es auch im CHANGES-Eintrag.
D31 #114 (2026-09-29): in diesem Paket aufgegangen und geschlossen; sein Schrägstrich-Kriterium steht unten.
Umsetzung (wie gebaut)
tools/chemenu/titles.py (neu) – reine Funktionen ohne fail(): title_problems(title) liefert jeden Grund konkret (das Zeichen, Steuerzeichen als U+0009, den reservierten Namen); collision_key(name) = casefold(NFC(name)).
commands/_util.py:check_title, check_collision(name, *, ignore=None) über collision_key im ganzen Titel-Namensraum von kb/, target_conflict/check_target_free(path, *, ignore=None) für das Zielverzeichnis jeder Wurzel. Nutzertext in Fehlermeldungen wird für Rich escaped.
new:check_title für jeden Typ und jede Wurzel, check_collision für root: kb, check_target_free für alle – alles vor jedem Schreibzugriff, auch vor dem Tracker-Projekt bei new project.
rename: im Umbenennungsmodus check_title(--to), check_collision und check_target_free mit der Seite selbst ausgenommen (Foo → FOO geht) – auch bei --dry-run. --from wird nie geprüft, damit rename die Abhilfe bleibt (D19).
move:check_target_free statt exaktem exists(), im Einzel- und im --reconcile-Pfad; --reconcile überspringt ein solches Ziel und nennt die Gegenseite.
lint: neuer Befund unportable_titles („Unportable Titles“) mit {title, path, problem}, Kollisionen nennen die Gegenseite, Bericht nennt die rename-Abhilfe. In HARD_ERROR_KEYS, bewusst nicht in MIGRATION_GATED_KEYS (keine Migration nach D19, kb_version rückt nicht vor). duplicate_titles (exakt gleicher Stamm) unverändert; exakte Duplikate meldet nur dieser.
Records:new, rename, move und lint tragen die neuen Ablehnungen, Notes und Verweise; tools/CONTRACT.md per docs contract --apply neu erzeugt.
Doku:kb/CONTRACT.md § Titles are identifiers (einzige normative Stelle), verlinkt aus kb/CONVENTIONS.md und dem Template; instructions/page-lifecycle.md (Rename- und Move-Absatz, ToC neu erzeugt), instructions/wiki-lint/SKILL.md, README.md (Naming Tips). tools/README.md macht keine Aussage über Titel – nichts zu ändern.
Tests:test_titles.py (neu, 57 Fälle), dazu Ergänzungen in test_new_page.py, test_page_ops.py, test_lint.py.
Version:version bump --major 7.1.0-beta.32 → 8.0.0-beta.1, --impact high, mit --breaking und --no-migration wie entworfen. Abweichung vom Entwurf: --title auf Englisch („Page titles must form valid, unique file names on Windows and macOS“), weil CHANGES.md englisch ist. Der Eintrag nennt auch die Korrektur des Überschreibens bei root: repo.
Akzeptanzkriterien
new <typ> --name "A: B" endet mit Exit 1 und nennt das Zeichen; es entsteht keine Datei. (test_new_refuses_a_forbidden_character_and_writes_nothing; im echten Checkout nachgestellt)
new --name "A/B" endet mit Exit 1; es entsteht weder A/ noch eine Datei (aus #114). (test_new_refuses_a_slash_and_creates_no_directory)
new --name "foo" endet unter Linux mit Exit 1, wenn eine Seite „Foo“ existiert; ebenso ein NFD-Titel gegen eine NFC-Seite. (test_new_refuses_a_title_that_differs_only_by_case, …_an_nfd_title_against_an_nfc_page)
new … --name "Index", "collection" und "COM¹" enden mit Exit 1. (test_new_refuses_reserved_and_malformed_names)
new project --name "A: B" legt auch kein Tracker-Projekt an. (test_new_project_with_a_bad_title_never_reaches_the_tracker)
new instruction --name gates endet mit Exit 1, und instructions/gates.md ist danach byte-gleich; ebenso --name contract neben instructions/CONTRACT.md. (test_new_instruction_never_overwrites_an_existing_file; gates im echten Checkout nachgestellt)
rename auf einen verbotenen oder kollidierenden Titel wird genauso verweigert, auch mit --dry-run; rename --from Foo --to FOO geht. (test_rename_refuses_an_unportable_or_colliding_target × dry-run, test_rename_may_change_only_the_case_of_the_page_itself, test_rename_dry_run_accepts_a_case_only_change)
renamevon einem verbotenen Titel auf einen gültigen geht. (test_rename_is_the_remedy_for_a_page_that_breaks_the_rule)
lint meldet eine eingeschleuste Seite kb/entities/CON.md als ERROR; lint --fail-on-error endet mit Exit 1. (test_lint_reports_a_reserved_device_name_as_a_hard_error, test_unportable_titles_is_hard_whatever_the_corpus_version – hart heißt: --fail-on-error endet mit 1)
lint meldet zwei Seiten „Foo“/„foo“ als ERROR mit beiden Pfaden. (test_lint_reports_case_variants_with_both_paths)
Der Demo-Korpus bleibt lint-sauber (182 Seiten, unportable_titles leer).
kb/CONTRACT.md nennt die Regel; tools/CONTRACT.md ist neu erzeugt; docs verify, instructions verify und pytest (1680 passed) sind grün.
Der Major-Bump trägt --breaking und --no-migration nach D19.
Verifiziert
Lokal vor dem Publish: tools/wikitool docs verify OK („CHANGES.md documents version 8.0.0-beta.1“), tools/wikitool instructions verify OK, pytest 1680 passed. CI auf 8be5e6e: Lauf 431 (verify: Tests, Version gate, Dist-Export und Fresh-Instance-Replay) grün, Lauf 432 (Release-Workflow) grün.
Version
major, 8.0.0-beta.1 – öffnet den 8.0.0-Kandidaten (#153 folgt im selben Kandidaten, D19): Korpora mit solchen Titeln lassen sich nicht mehr ohne Handarbeit lint-sauber halten.
Teilpaket von #140. Entschieden: D4 (hart, 2026-09-27), D18 (Umfang, 2026-09-28), D28–D31 (Präzisierungen, 2026-09-29). **Erledigt 2026-09-30:** veröffentlicht mit `8be5e6e`, Version **8.0.0-beta.1** (öffnet den 8.0.0-Kandidaten, erste Grenzüberschreitung; kein `version release`, das bleibt beim Betreiber).
## Warum
Titel werden eins zu eins zu Dateinamen (`new_page.py`: `f"{page_title}.md"`). Daraus folgten zwei Probleme:
- **Unter Linux erlaubt, unter Windows ungültig.** Ein Titel mit `:`, `?`, `"` o. ä. ließ sich unter Linux anlegen und committen, brach danach aber jeden Windows-Klon der Instanz: `git checkout` scheitert an der Datei.
- **Groß-/Kleinschreibung und Normalisierung.** Zwei Titel, die sich nur darin unterscheiden, kollidieren auf NTFS und auf APFS (Standardeinstellung); APFS behandelt zusätzlich NFC und NFD als denselben Namen. Unter Linux fiel beides nicht auf.
Mit nativem Windows-Support (#140 D8) gilt die Regel deshalb auf **allen** Plattformen, nicht nur unter Windows.
**Nebenbefund, mitbehoben:** `new` prüfte Kollisionen nur für `root: kb`. `tools/wikitool new instruction --name gates` überschrieb deshalb `instructions/gates.md` kommentarlos mit einem Gerüst. `new` überschreibt jetzt nie eine bestehende Datei, für keine Wurzel.
## Regel (D18, präzisiert durch D28–D30)
Normativ steht sie einmal, in `kb/CONTRACT.md` § Titles are identifiers; umgesetzt in `tools/chemenu/titles.py`. Ein Titel darf nicht:
- leer sein (D30)
- eines der Zeichen `< > : " / \ | ? *` enthalten
- Steuerzeichen enthalten (gemeldet als `U+XXXX`)
- ein reservierter Name sein – maßgeblich ist der Teil vor dem ersten Punkt, ohne Rücksicht auf Groß-/Kleinschreibung und ohne abschließende Leerzeichen:
- Windows: `CON`, `PRN`, `AUX`, `NUL`, `COM0`–`COM9`, `COM¹`–`COM³`, `LPT0`–`LPT9`, `LPT¹`–`LPT³` (D30)
- Stack (D28): `INDEX`, `COLLECTION`
- auf einen Punkt oder ein Leerzeichen enden
- mit einer bestehenden Seite kollidieren, nachdem beide nach NFC normalisiert und per `casefold` verglichen wurden (D29)
Geprüft wird immer der volle Seitentitel, also nach `title_prefix` (`Source - …`).
## Entscheidungen
- **D28 Stack-eigene Namen (2026-09-29), mit Bestandsaufnahme.** Geprüft wurde, welche Dateien der Stack dort ablegt, wo `new` schreibt:
| Ort | Stack-Dateien | Behandlung |
|---|---|---|
| jedes Sammlungs- und Bereichsverzeichnis unter `kb/` | `INDEX.md` (generiert) | global reserviert |
| jedes Sammlungsverzeichnis | `COLLECTION.md`, in ausgelieferten Instanzen `COLLECTION.md.template` | global reserviert (`COLLECTION` deckt über „Teil vor dem ersten Punkt“ beide ab) |
| `kb/` selbst | `index.md`, `log.md`, `provenance.md`, `CONTRACT.md`, `CONVENTIONS.md(.template)` | **nicht** global reserviert: Kein instanziierbarer Typ legt Seiten direkt in `kb/` ab, und „Log“ oder „Contract“ sind legitime Seitentitel. Die Verzeichnisprüfung fängt den Fall trotzdem ab. |
| `instructions/` (`root: repo`, Typ `instruction`) | `CONTRACT.md`, bestehende Instruktionen, Skill-Verzeichnisse | Verzeichnisprüfung |
| `README.md` in einer Sammlung | ist eine normale Seite | normale Titelkollision |
Aus Titeln entstehen keine Verzeichnisse: Die Unterverzeichnisse kommen aus dem `layout:` des Typs, nicht aus Nutzertext.
- **D29 Normalisierung (2026-09-29):** Kollisionsschlüssel `casefold(NFC(titel))`.
- **D30 Reservierte Namen vollständig (2026-09-29):** `COM0`/`LPT0` und `COM¹²³`/`LPT¹²³` aufgenommen, dazu die Weigerung bei leerem Titel. Unsicher bleibt, ob jede Windows-Version alle davon sperrt; die strengere Regel kostet nichts. So steht es auch im CHANGES-Eintrag.
- **D31 #114 (2026-09-29):** in diesem Paket aufgegangen und geschlossen; sein Schrägstrich-Kriterium steht unten.
## Umsetzung (wie gebaut)
1. **`tools/chemenu/titles.py`** (neu) – reine Funktionen ohne `fail()`: `title_problems(title)` liefert jeden Grund konkret (das Zeichen, Steuerzeichen als `U+0009`, den reservierten Namen); `collision_key(name)` = `casefold(NFC(name))`.
2. **`commands/_util.py`:** `check_title`, `check_collision(name, *, ignore=None)` über `collision_key` im ganzen Titel-Namensraum von `kb/`, `target_conflict`/`check_target_free(path, *, ignore=None)` für das Zielverzeichnis jeder Wurzel. Nutzertext in Fehlermeldungen wird für Rich escaped.
3. **`new`:** `check_title` für jeden Typ und jede Wurzel, `check_collision` für `root: kb`, `check_target_free` für alle – alles vor jedem Schreibzugriff, auch vor dem Tracker-Projekt bei `new project`.
4. **`rename`:** im Umbenennungsmodus `check_title(--to)`, `check_collision` und `check_target_free` mit der Seite selbst ausgenommen (`Foo` → `FOO` geht) – auch bei `--dry-run`. `--from` wird nie geprüft, damit `rename` die Abhilfe bleibt (D19).
5. **`move`:** `check_target_free` statt exaktem `exists()`, im Einzel- und im `--reconcile`-Pfad; `--reconcile` überspringt ein solches Ziel und nennt die Gegenseite.
6. **`lint`:** neuer Befund `unportable_titles` („Unportable Titles“) mit `{title, path, problem}`, Kollisionen nennen die Gegenseite, Bericht nennt die `rename`-Abhilfe. In `HARD_ERROR_KEYS`, bewusst **nicht** in `MIGRATION_GATED_KEYS` (keine Migration nach D19, `kb_version` rückt nicht vor). `duplicate_titles` (exakt gleicher Stamm) unverändert; exakte Duplikate meldet nur dieser.
7. **Records:** `new`, `rename`, `move` und `lint` tragen die neuen Ablehnungen, Notes und Verweise; `tools/CONTRACT.md` per `docs contract --apply` neu erzeugt.
8. **Doku:** `kb/CONTRACT.md` § Titles are identifiers (einzige normative Stelle), verlinkt aus `kb/CONVENTIONS.md` und dem Template; `instructions/page-lifecycle.md` (Rename- und Move-Absatz, ToC neu erzeugt), `instructions/wiki-lint/SKILL.md`, `README.md` (Naming Tips). `tools/README.md` macht keine Aussage über Titel – nichts zu ändern.
9. **Tests:** `test_titles.py` (neu, 57 Fälle), dazu Ergänzungen in `test_new_page.py`, `test_page_ops.py`, `test_lint.py`.
10. **Version:** `version bump --major` 7.1.0-beta.32 → 8.0.0-beta.1, `--impact high`, mit `--breaking` und `--no-migration` wie entworfen. Abweichung vom Entwurf: `--title` auf Englisch („Page titles must form valid, unique file names on Windows and macOS“), weil `CHANGES.md` englisch ist. Der Eintrag nennt auch die Korrektur des Überschreibens bei `root: repo`.
## Akzeptanzkriterien
- [x] `new <typ> --name "A: B"` endet mit Exit 1 und nennt das Zeichen; es entsteht keine Datei. (`test_new_refuses_a_forbidden_character_and_writes_nothing`; im echten Checkout nachgestellt)
- [x] `new --name "A/B"` endet mit Exit 1; es entsteht weder `A/` noch eine Datei (aus #114). (`test_new_refuses_a_slash_and_creates_no_directory`)
- [x] `new --name "foo"` endet unter Linux mit Exit 1, wenn eine Seite „Foo“ existiert; ebenso ein NFD-Titel gegen eine NFC-Seite. (`test_new_refuses_a_title_that_differs_only_by_case`, `…_an_nfd_title_against_an_nfc_page`)
- [x] `new … --name "Index"`, `"collection"` und `"COM¹"` enden mit Exit 1. (`test_new_refuses_reserved_and_malformed_names`)
- [x] `new project --name "A: B"` legt auch kein Tracker-Projekt an. (`test_new_project_with_a_bad_title_never_reaches_the_tracker`)
- [x] `new instruction --name gates` endet mit Exit 1, und `instructions/gates.md` ist danach byte-gleich; ebenso `--name contract` neben `instructions/CONTRACT.md`. (`test_new_instruction_never_overwrites_an_existing_file`; `gates` im echten Checkout nachgestellt)
- [x] `rename` auf einen verbotenen oder kollidierenden Titel wird genauso verweigert, auch mit `--dry-run`; `rename --from Foo --to FOO` geht. (`test_rename_refuses_an_unportable_or_colliding_target` × dry-run, `test_rename_may_change_only_the_case_of_the_page_itself`, `test_rename_dry_run_accepts_a_case_only_change`)
- [x] `rename` **von** einem verbotenen Titel auf einen gültigen geht. (`test_rename_is_the_remedy_for_a_page_that_breaks_the_rule`)
- [x] `lint` meldet eine eingeschleuste Seite `kb/entities/CON.md` als ERROR; `lint --fail-on-error` endet mit Exit 1. (`test_lint_reports_a_reserved_device_name_as_a_hard_error`, `test_unportable_titles_is_hard_whatever_the_corpus_version` – hart heißt: `--fail-on-error` endet mit 1)
- [x] `lint` meldet zwei Seiten „Foo“/„foo“ als ERROR mit beiden Pfaden. (`test_lint_reports_case_variants_with_both_paths`)
- [x] Der Demo-Korpus bleibt lint-sauber (182 Seiten, `unportable_titles` leer).
- [x] `kb/CONTRACT.md` nennt die Regel; `tools/CONTRACT.md` ist neu erzeugt; `docs verify`, `instructions verify` und `pytest` (1680 passed) sind grün.
- [x] Der Major-Bump trägt `--breaking` und `--no-migration` nach D19.
## Verifiziert
Lokal vor dem Publish: `tools/wikitool docs verify` OK („CHANGES.md documents version 8.0.0-beta.1“), `tools/wikitool instructions verify` OK, `pytest` 1680 passed. CI auf `8be5e6e`: [Lauf 431](https://gitea.nehmer.net/torben/chemenu/actions/runs/431) (verify: Tests, Version gate, Dist-Export und Fresh-Instance-Replay) grün, [Lauf 432](https://gitea.nehmer.net/torben/chemenu/actions/runs/432) (Release-Workflow) grün.
## Version
major, 8.0.0-beta.1 – öffnet den 8.0.0-Kandidaten (#153 folgt im selben Kandidaten, D19): Korpora mit solchen Titeln lassen sich nicht mehr ohne Handarbeit lint-sauber halten.
Changelog: Gegen den Baum geprüft (94deccb). Neu: Abschnitt „Umsetzung (Plan)“ (Modul titles.py, new/rename/lint, Records, Doku, Tests, Bump-Entwurf) und vier offene Präzisierungen D28–D31 (stack-eigene Namen INDEX/COLLECTION, Unicode-Normalisierung, vollständige Reserviert-Liste samt leerem Titel, Umgang mit #114). Akzeptanzkriterien ergänzt: Schrägstrich (aus #114), new project ohne Tracker-Seiteneffekt, rename bei --dry-run und von einem verbotenen Titel weg, Foo/foo-Kollision in lint. Korpus-Probelauf auch gegen D28–D30 sauber. Wegen D28–D31 kind/build → kind/decision.
**Changelog:** Gegen den Baum geprüft (`94deccb`). Neu: Abschnitt „Umsetzung (Plan)“ (Modul `titles.py`, `new`/`rename`/`lint`, Records, Doku, Tests, Bump-Entwurf) und vier offene Präzisierungen D28–D31 (stack-eigene Namen `INDEX`/`COLLECTION`, Unicode-Normalisierung, vollständige Reserviert-Liste samt leerem Titel, Umgang mit #114). Akzeptanzkriterien ergänzt: Schrägstrich (aus #114), `new project` ohne Tracker-Seiteneffekt, `rename` bei `--dry-run` und von einem verbotenen Titel weg, Foo/foo-Kollision in `lint`. Korpus-Probelauf auch gegen D28–D30 sauber. Wegen D28–D31 `kind/build` → `kind/decision`.
Changelog: Die Präzisierungen D28–D31 sind entschieden (Betreiber, 2026-09-29, alle wie empfohlen). Darum kind/decision → kind/build. Neu dazu gekommen:
Bestandsaufnahme zu D28: Die Tabelle zeigt, welche Stack-Dateien dort liegen, wo new schreibt.
Nebenbefund, reproduziert:new überschreibt bei root: repo eine bestehende Datei, z. B. new instruction --name gates.
Neue Verzeichnisprüfungcheck_target_free, die für alle Wurzeln und auch für move greift.
Weitere Akzeptanzkriterien: NFD/NFC, Index/collection/COM¹ und das Überschreiben bei root: repo.
Regel: um D28–D30 ergänzt.
#114: geschlossen, weil es in diesem Paket aufgeht.
**Changelog:** Die Präzisierungen D28–D31 sind entschieden (Betreiber, 2026-09-29, alle wie empfohlen). Darum `kind/decision` → `kind/build`. Neu dazu gekommen:
- **Bestandsaufnahme zu D28:** Die Tabelle zeigt, welche Stack-Dateien dort liegen, wo `new` schreibt.
- **Nebenbefund, reproduziert:** `new` überschreibt bei `root: repo` eine bestehende Datei, z. B. `new instruction --name gates`.
- **Neue Verzeichnisprüfung** `check_target_free`, die für alle Wurzeln und auch für `move` greift.
- **Weitere Akzeptanzkriterien:** NFD/NFC, `Index`/`collection`/`COM¹` und das Überschreiben bei `root: repo`.
- **Regel:** um D28–D30 ergänzt.
- **#114:** geschlossen, weil es in diesem Paket aufgeht.
Umgesetzt und veröffentlicht (8be5e6e, 8.0.0-beta.1); CI-Läufe 431/432 grün. Body auf den Endstand gebracht: Plan → „wie gebaut“, alle Akzeptanzkriterien abgehakt mit dem jeweiligen Test, Abschnitt „Verifiziert“ neu. Einzige Abweichung vom Entwurf: der Bump-Titel ist englisch, weil CHANGES.md englisch ist.
Umgesetzt und veröffentlicht (`8be5e6e`, 8.0.0-beta.1); CI-Läufe 431/432 grün. Body auf den Endstand gebracht: Plan → „wie gebaut“, alle Akzeptanzkriterien abgehakt mit dem jeweiligen Test, Abschnitt „Verifiziert“ neu. Einzige Abweichung vom Entwurf: der Bump-Titel ist englisch, weil `CHANGES.md` englisch ist.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Teilpaket von #140. Entschieden: D4 (hart, 2026-09-27), D18 (Umfang, 2026-09-28), D28–D31 (Präzisierungen, 2026-09-29). Erledigt 2026-09-30: veröffentlicht mit
8be5e6e, Version 8.0.0-beta.1 (öffnet den 8.0.0-Kandidaten, erste Grenzüberschreitung; keinversion release, das bleibt beim Betreiber).Warum
Titel werden eins zu eins zu Dateinamen (
new_page.py:f"{page_title}.md"). Daraus folgten zwei Probleme::,?,"o. ä. ließ sich unter Linux anlegen und committen, brach danach aber jeden Windows-Klon der Instanz:git checkoutscheitert an der Datei.Mit nativem Windows-Support (#140 D8) gilt die Regel deshalb auf allen Plattformen, nicht nur unter Windows.
Nebenbefund, mitbehoben:
newprüfte Kollisionen nur fürroot: kb.tools/wikitool new instruction --name gatesüberschrieb deshalbinstructions/gates.mdkommentarlos mit einem Gerüst.newüberschreibt jetzt nie eine bestehende Datei, für keine Wurzel.Regel (D18, präzisiert durch D28–D30)
Normativ steht sie einmal, in
kb/CONTRACT.md§ Titles are identifiers; umgesetzt intools/chemenu/titles.py. Ein Titel darf nicht:< > : " / \ | ? *enthaltenU+XXXX)CON,PRN,AUX,NUL,COM0–COM9,COM¹–COM³,LPT0–LPT9,LPT¹–LPT³(D30)INDEX,COLLECTIONcasefoldverglichen wurden (D29)Geprüft wird immer der volle Seitentitel, also nach
title_prefix(Source - …).Entscheidungen
D28 Stack-eigene Namen (2026-09-29), mit Bestandsaufnahme. Geprüft wurde, welche Dateien der Stack dort ablegt, wo
newschreibt:kb/INDEX.md(generiert)COLLECTION.md, in ausgelieferten InstanzenCOLLECTION.md.templateCOLLECTIONdeckt über „Teil vor dem ersten Punkt“ beide ab)kb/selbstindex.md,log.md,provenance.md,CONTRACT.md,CONVENTIONS.md(.template)kb/ab, und „Log“ oder „Contract“ sind legitime Seitentitel. Die Verzeichnisprüfung fängt den Fall trotzdem ab.instructions/(root: repo, Typinstruction)CONTRACT.md, bestehende Instruktionen, Skill-VerzeichnisseREADME.mdin einer SammlungAus Titeln entstehen keine Verzeichnisse: Die Unterverzeichnisse kommen aus dem
layout:des Typs, nicht aus Nutzertext.D29 Normalisierung (2026-09-29): Kollisionsschlüssel
casefold(NFC(titel)).D30 Reservierte Namen vollständig (2026-09-29):
COM0/LPT0undCOM¹²³/LPT¹²³aufgenommen, dazu die Weigerung bei leerem Titel. Unsicher bleibt, ob jede Windows-Version alle davon sperrt; die strengere Regel kostet nichts. So steht es auch im CHANGES-Eintrag.D31 #114 (2026-09-29): in diesem Paket aufgegangen und geschlossen; sein Schrägstrich-Kriterium steht unten.
Umsetzung (wie gebaut)
tools/chemenu/titles.py(neu) – reine Funktionen ohnefail():title_problems(title)liefert jeden Grund konkret (das Zeichen, Steuerzeichen alsU+0009, den reservierten Namen);collision_key(name)=casefold(NFC(name)).commands/_util.py:check_title,check_collision(name, *, ignore=None)übercollision_keyim ganzen Titel-Namensraum vonkb/,target_conflict/check_target_free(path, *, ignore=None)für das Zielverzeichnis jeder Wurzel. Nutzertext in Fehlermeldungen wird für Rich escaped.new:check_titlefür jeden Typ und jede Wurzel,check_collisionfürroot: kb,check_target_freefür alle – alles vor jedem Schreibzugriff, auch vor dem Tracker-Projekt beinew project.rename: im Umbenennungsmoduscheck_title(--to),check_collisionundcheck_target_freemit der Seite selbst ausgenommen (Foo→FOOgeht) – auch bei--dry-run.--fromwird nie geprüft, damitrenamedie Abhilfe bleibt (D19).move:check_target_freestatt exaktemexists(), im Einzel- und im--reconcile-Pfad;--reconcileüberspringt ein solches Ziel und nennt die Gegenseite.lint: neuer Befundunportable_titles(„Unportable Titles“) mit{title, path, problem}, Kollisionen nennen die Gegenseite, Bericht nennt dierename-Abhilfe. InHARD_ERROR_KEYS, bewusst nicht inMIGRATION_GATED_KEYS(keine Migration nach D19,kb_versionrückt nicht vor).duplicate_titles(exakt gleicher Stamm) unverändert; exakte Duplikate meldet nur dieser.new,rename,moveundlinttragen die neuen Ablehnungen, Notes und Verweise;tools/CONTRACT.mdperdocs contract --applyneu erzeugt.kb/CONTRACT.md§ Titles are identifiers (einzige normative Stelle), verlinkt auskb/CONVENTIONS.mdund dem Template;instructions/page-lifecycle.md(Rename- und Move-Absatz, ToC neu erzeugt),instructions/wiki-lint/SKILL.md,README.md(Naming Tips).tools/README.mdmacht keine Aussage über Titel – nichts zu ändern.test_titles.py(neu, 57 Fälle), dazu Ergänzungen intest_new_page.py,test_page_ops.py,test_lint.py.version bump --major7.1.0-beta.32 → 8.0.0-beta.1,--impact high, mit--breakingund--no-migrationwie entworfen. Abweichung vom Entwurf:--titleauf Englisch („Page titles must form valid, unique file names on Windows and macOS“), weilCHANGES.mdenglisch ist. Der Eintrag nennt auch die Korrektur des Überschreibens beiroot: repo.Akzeptanzkriterien
new <typ> --name "A: B"endet mit Exit 1 und nennt das Zeichen; es entsteht keine Datei. (test_new_refuses_a_forbidden_character_and_writes_nothing; im echten Checkout nachgestellt)new --name "A/B"endet mit Exit 1; es entsteht wederA/noch eine Datei (aus #114). (test_new_refuses_a_slash_and_creates_no_directory)new --name "foo"endet unter Linux mit Exit 1, wenn eine Seite „Foo“ existiert; ebenso ein NFD-Titel gegen eine NFC-Seite. (test_new_refuses_a_title_that_differs_only_by_case,…_an_nfd_title_against_an_nfc_page)new … --name "Index","collection"und"COM¹"enden mit Exit 1. (test_new_refuses_reserved_and_malformed_names)new project --name "A: B"legt auch kein Tracker-Projekt an. (test_new_project_with_a_bad_title_never_reaches_the_tracker)new instruction --name gatesendet mit Exit 1, undinstructions/gates.mdist danach byte-gleich; ebenso--name contractnebeninstructions/CONTRACT.md. (test_new_instruction_never_overwrites_an_existing_file;gatesim echten Checkout nachgestellt)renameauf einen verbotenen oder kollidierenden Titel wird genauso verweigert, auch mit--dry-run;rename --from Foo --to FOOgeht. (test_rename_refuses_an_unportable_or_colliding_target× dry-run,test_rename_may_change_only_the_case_of_the_page_itself,test_rename_dry_run_accepts_a_case_only_change)renamevon einem verbotenen Titel auf einen gültigen geht. (test_rename_is_the_remedy_for_a_page_that_breaks_the_rule)lintmeldet eine eingeschleuste Seitekb/entities/CON.mdals ERROR;lint --fail-on-errorendet mit Exit 1. (test_lint_reports_a_reserved_device_name_as_a_hard_error,test_unportable_titles_is_hard_whatever_the_corpus_version– hart heißt:--fail-on-errorendet mit 1)lintmeldet zwei Seiten „Foo“/„foo“ als ERROR mit beiden Pfaden. (test_lint_reports_case_variants_with_both_paths)unportable_titlesleer).kb/CONTRACT.mdnennt die Regel;tools/CONTRACT.mdist neu erzeugt;docs verify,instructions verifyundpytest(1680 passed) sind grün.--breakingund--no-migrationnach D19.Verifiziert
Lokal vor dem Publish:
tools/wikitool docs verifyOK („CHANGES.md documents version 8.0.0-beta.1“),tools/wikitool instructions verifyOK,pytest1680 passed. CI auf8be5e6e: Lauf 431 (verify: Tests, Version gate, Dist-Export und Fresh-Instance-Replay) grün, Lauf 432 (Release-Workflow) grün.Version
major, 8.0.0-beta.1 – öffnet den 8.0.0-Kandidaten (#153 folgt im selben Kandidaten, D19): Korpora mit solchen Titeln lassen sich nicht mehr ohne Handarbeit lint-sauber halten.
Changelog: D18 ist wie empfohlen entschieden (Betreiber, 2026-09-28). Darum
kind/decision→kind/build.Changelog: Gegen den Baum geprüft (
94deccb). Neu: Abschnitt „Umsetzung (Plan)“ (Modultitles.py,new/rename/lint, Records, Doku, Tests, Bump-Entwurf) und vier offene Präzisierungen D28–D31 (stack-eigene NamenINDEX/COLLECTION, Unicode-Normalisierung, vollständige Reserviert-Liste samt leerem Titel, Umgang mit #114). Akzeptanzkriterien ergänzt: Schrägstrich (aus #114),new projectohne Tracker-Seiteneffekt,renamebei--dry-runund von einem verbotenen Titel weg, Foo/foo-Kollision inlint. Korpus-Probelauf auch gegen D28–D30 sauber. Wegen D28–D31kind/build→kind/decision.Changelog: Die Präzisierungen D28–D31 sind entschieden (Betreiber, 2026-09-29, alle wie empfohlen). Darum
kind/decision→kind/build. Neu dazu gekommen:newschreibt.newüberschreibt beiroot: repoeine bestehende Datei, z. B.new instruction --name gates.check_target_free, die für alle Wurzeln und auch fürmovegreift.Index/collection/COM¹und das Überschreiben beiroot: repo.Umgesetzt und veröffentlicht (
8be5e6e, 8.0.0-beta.1); CI-Läufe 431/432 grün. Body auf den Endstand gebracht: Plan → „wie gebaut“, alle Akzeptanzkriterien abgehakt mit dem jeweiligen Test, Abschnitt „Verifiziert“ neu. Einzige Abweichung vom Entwurf: der Bump-Titel ist englisch, weilCHANGES.mdenglisch ist.torben referenced this issue2026-09-30 20:16:28 +00:00