82a22eaa93
Files changed: - .gitea/workflows/ci.yml - .gitignore - CHANGES.md - EVALS.md - INSTALL-MCP.md - INSTALL.md - VERSION - instructions/setup-instance.md - reports/CONTRACT.md - tools/CONTRACT.md - tools/chemenu/commands/doctor.py - tools/chemenu/config.py - tools/chemenu/mcp/server.py - tools/chemenu/telemetry/policy.py - tools/chemenu/telemetry/schema.py - tools/chemenu/telemetry/writer.py - tools/chemenu/tests/conftest.py - tools/chemenu/tests/test_doctor.py - tools/chemenu/tests/test_mcp_server.py - tools/chemenu/tests/test_telemetry_emit.py - tools/chemenu/tests/test_telemetry_policy.py - tools/chemenu/version.py
5075 lines
327 KiB
Markdown
5075 lines
327 KiB
Markdown
# Changelog
|
||
|
||
This file tracks changes to the **wiki stack itself** - `AGENTS.md`, the
|
||
`instructions/` layer, `tools/wikitool`, and the contracts. It is distinct from
|
||
`kb/log.md`, which is the audit trail of *wiki content* operations (ingests,
|
||
queries, lints, page creates/updates) performed by the LLM against `kb/`.
|
||
|
||
Previously each of `AGENTS.md` and `README.md` carried its own "Version
|
||
History" table. Those have been consolidated here so there is one place to
|
||
look for "what changed in the tooling/schema, and when." From now on,
|
||
document any change to the stack (schema, instructions, `wikitool` commands,
|
||
contracts) as a new entry at the top of this file instead of editing inline
|
||
version history tables.
|
||
|
||
Since `0.1.0` an entry's heading also carries the stack version it describes
|
||
(`## <version> - <date> - <title>`). `wikitool version bump` writes that
|
||
heading, and `wikitool docs verify` refuses a tree whose `VERSION` and newest
|
||
versioned entry disagree. Entries below `0.1.0` predate versioning and keep
|
||
their date-only headings.
|
||
|
||
Since `4.4.0` the stack carries **one running candidate** between two
|
||
releases rather than a fresh version per bump - see
|
||
`instructions/dev/version-parts.md`. While a candidate is open its heading
|
||
names it with a `-beta.N` suffix (`## 4.4.0-beta.2 - <date> - <title>`), and
|
||
every bump of that same candidate updates this one entry in place rather than
|
||
opening another: the heading's version/date/title move, and the bump's
|
||
`--title` joins a machine-managed `<!-- wikitool:bumps -->` list right under
|
||
the entry's `**Author:**` line - written and read by `wikitool version bump`,
|
||
never by hand. `wikitool version release` is what closes a candidate: it
|
||
strips the suffix and turns the entry into an ordinary, suffix-free one,
|
||
leaving the bump-title list as the record of what happened. A distributed
|
||
instance never sees a `-beta.` version at all (`release.yml` only ever
|
||
releases a fixed one), so this suffix and the list beneath it are a
|
||
dev-checkout concern - readable here, never shipped as something to parse.
|
||
|
||
---
|
||
|
||
## 5.0.0-beta.9 - 2026-09-10 - Telemetrie-Default nach Installationsform, Byte-Deckel und Session-Retention
|
||
|
||
**Author:** Torben Nehmer
|
||
|
||
<!-- wikitool:bumps -->
|
||
- status/incoming: menschliche Stubs werden ausgearbeitet, nie so umgesetzt
|
||
- page move: eine kb-Seite folgt ihrem Subtype ins Verzeichnis, das ihr Type-Spec berechnet
|
||
- kb/CONTRACT.md: Tiefe 1 als Grenze - Katalog liest nur eine Area-Ebene
|
||
- raw accept: incoming/ als abgeleiteter Rohablage-Eingang (schliesst #58)
|
||
- raw accept: Stem-Eindeutigkeit im Typverzeichnis erzwingen, --replaces als einziger Weg daran vorbei (schliesst #64)
|
||
- update entity naming conventions to use singular form for consistency
|
||
- kb/concepts/ bekommt Areas: layout: fuer concept, Area-Titel aus jedem Type-Spec, Schwellen-Empfehlung im lint (schliesst #59)
|
||
- source_type: Default streichen, unclassified als sichtbares Fach, layout: fuer source (schliesst #66)
|
||
- raw accept: Datums-Shard statt Typverzeichnis, fidelity/authority am Drop-Punkt (schliesst #67)
|
||
- source_type ist Instanzsache: Profilkatalog, Setup-Frage, evolve-subtypes-Instruction (#68)
|
||
- instructions/CONTRACT.md: Skill-H1, Referenztiefe und Begruendungsprosa praezisiert (#71, #72, #79)
|
||
- SKILL.md-Sanierung: Checklisten, Kommandolisten, wiki-status-Hard-Rule, wiki-query-Filingpruefung (#70, #74, #75, #78)
|
||
- Ausgelieferte Doku zitiert keine Issue-Nummern mehr, docs verify prueft es (schliesst #77)
|
||
- TOC-Pflicht fuer Referenzdateien ueber 100 Zeilen; session-setup.md/gates.md nennen die tatsaechliche Budget-Ausnahmeliste (schliesst #73, #76)
|
||
- tools/CONTRACT.md: docs toc im Fehlerkontrakt, docs-verify-Zeile nennt die TOC-Pruefung; tools/README.md korrigiert das --major-Kriterium
|
||
- dist export erzeugt die TOC-Region nach dem Marker-Strip neu (CI-Fund im Export-Replay)
|
||
- Konfidenz-Mechanismus ersatzlos entfernt
|
||
- Konfidenz-Mechanismus ersatzlos entfernt
|
||
- version-parts.md dokumentiert den --migration-required-Ruecknahmepfad
|
||
- wiki-status verweist auf session-setup.md (schliesst #84)
|
||
- CLAUDE.md-Importkette entdrifted, Modellwahl nach docs/ verschoben (schliesst #81)
|
||
- Telemetrie-Default nach Installationsform, Byte-Deckel und Session-Retention
|
||
<!-- /wikitool:bumps -->
|
||
|
||
|
||
**Breaking Change:** docs verify verlangt eine aktuelle Inhaltsverzeichnis-Region (<!-- wikitool:toc -->) auf AGENTS.md, jedem Stage-/Collection-Contract und jeder flachen instructions/**.md-Datei ueber 100 Zeilen - eine bestehende Instanz mit einer eigenen instructions/*.md-Datei ueber 100 Zeilen ohne TOC sieht docs verify nach dem Tool-Update neu fehlschlagen, bis einmalig 'wikitool docs toc --apply' laeuft und der Diff committet wird. Ausserdem verschwinden wikitool confidence decay und confidence init-base ersatzlos, touch --confidence-base ebenso, und die Schemas fuer entity/concept verlieren confidence/confidence_base vollstaendig (additionalProperties: false) - jede bestehende Instanz muss den Korpus migrieren, sonst werden alle Entity-/Concept-Seiten beim Schema-Update sofort schemainvalide. Ablauf: instructions/migrations/5.0.0-confidence-removal.md
|
||
|
||
Das Label `status/incoming` gibt es seit heute in Gitea: der Mensch legt einen
|
||
Stub an — zwei Sätze, ein Verdacht, ein „wäre interessant" — und der Stack
|
||
komplettiert ihn. `instructions/dev/issue-tracking.md` beschrieb es nicht, und
|
||
das ist die gefährlichere Hälfte: ein Stub *sieht aus wie* ein Body, und der
|
||
Body ist genau das, was eine Sitzung nach Schritt 2 als Spec glaubt. Zwei
|
||
solche Issues lagen bereits offen auf dem Board (#60, #61).
|
||
|
||
Neu in der Instruction ist deshalb ein eigener Abschnitt „Incoming stubs" mit
|
||
dem Verbot als erstem Satz — ein `status/incoming`-Issue wird nie so umgesetzt,
|
||
wie es dasteht — und der Ausarbeitung als sechsschrittigem Ablauf: den Wortlaut
|
||
als Absichtserklärung lesen, gegen den Baum prüfen, den Originaltext wörtlich in
|
||
den Kommentar retten, bevor der Rewrite ihn überschreibt, offene Fragen benennen
|
||
statt beantworten (`kind/decision`), erst dann die vier Pflichtlabel, dann das
|
||
Flag entfernen. Zwei Ausgänge wie bei `status/unconfirmed`: ausgearbeitet oder
|
||
mit Begründung geschlossen.
|
||
|
||
Der interessante Punkt ist Schritt 4: `status/incoming` ist das einzige Flag,
|
||
das Schritt 4 nicht qualifiziert, sondern **aussetzt**. Die vier Pflichtachsen
|
||
fehlen einem Stub nicht, sie sind noch nicht fällig — `kind/`, `prio/` und
|
||
`size/` sind Antworten auf Fragen, die niemand gegen den Baum geprüft hat. Ein
|
||
Stub auf Sicht durchzulabeln ist der Fehler, nicht das Weglassen: es lässt
|
||
Ungeprüftes triagiert aussehen.
|
||
|
||
Der Schaden, den das verhindert, ist derselbe wie bei #30, nur eine Stufe
|
||
früher: dort schrieb ein Body einen Mechanismus vor und bekam dessen Bugs
|
||
gebaut, hier schreibt ein Body gar nichts vor und bekommt die Lücke von
|
||
demjenigen gefüllt, der ihn am schnellsten gelesen hat — inklusive Close, womit
|
||
die Frage, für die der Stub stand, nie wieder gestellt wird.
|
||
|
||
Keine neue Schrittnummer, bewusst: `stack-close/SKILL.md` und ältere
|
||
`CHANGES.md`-Einträge verweisen namentlich auf „Schritte 2-3 und 7". Eine
|
||
Umnummerierung hätte diese Verweise still falsch gemacht — genau der Zerfall,
|
||
den dieselbe Datei in § Renames beschreibt.
|
||
|
||
Geändert: [instructions/dev/issue-tracking.md](instructions/dev/issue-tracking.md)
|
||
(Frontmatter, § When to run, Schritt 5, neuer § Incoming stubs, § What no tool
|
||
checks, § Decision points) und die Routing-Zeile in
|
||
`instructions/dev/stack-dev/SKILL.md`. Dev-only — `dist export` schließt
|
||
`instructions/dev/` aus, eine ausgelieferte Instanz sieht davon nichts, deshalb
|
||
PATCH.
|
||
|
||
Nachgezogen in derselben Sitzung, aber als eigenes Paket (#63, Commit
|
||
`a0aecfc`): `kb/concepts/Issue Label Scheme.md` beschrieb weiter nur die beiden
|
||
alten `status/`-Flags und zählte sechzehn statt siebzehn Labels. Das ist
|
||
`kb/`-Inhalt und brauchte nach Invariante 3 erst eine Quelle — die Threadkopie
|
||
von #62/#63 in `raw/notes/` — also eine `wiki-manage`-Sitzung statt dieser
|
||
hier. Die Seite trägt jetzt die dritte `status/`-Zeile samt Verbot und einen
|
||
Kernpunkt dazu, dass dieses Flag die Vier-Achsen-Pflicht aussetzt.
|
||
|
||
Offen bleibt die erste tatsächliche Anwendung der Regel: #60 und #61 sind
|
||
weiterhin unausgearbeitete Stubs.
|
||
|
||
**`wikitool move`** (#56): Nichts im Stack bewegte bisher eine Seite über eine
|
||
Verzeichnisgrenze — `rename` schreibt laut eigenem Docstring nie über
|
||
`target.path.parent` hinaus, und `new_page._target_dir` rechnete die
|
||
Platzierung nur beim Anlegen. Ändert sich `entity_type` später, blieb die
|
||
Datei still am alten Ort liegen, und nichts prüfte das nach: weder
|
||
`lint_core.py` noch `doctor.py` verglichen den Ist-Ort einer Seite mit dem,
|
||
was ihr Type-Spec berechnen würde.
|
||
|
||
Die Platzierungslogik selbst gibt es jetzt genau einmal:
|
||
`TypeResolver.compute_target_dir` (plus `TypeResolver.subtype_dir` für den
|
||
`layout:`-Teil), `new_page._target_dir` delegiert nur noch dorthin. Darauf
|
||
aufbauend zwei neue Stücke:
|
||
|
||
- `wikitool move --page "<Titel>"` bewegt eine Seite an den berechneten Ort;
|
||
`--reconcile` wendet dieselbe Regel auf den ganzen Bestand an und ist
|
||
idempotent (ein zweiter Lauf meldet nichts mehr zu tun). Beide Modi fassen
|
||
weder Body noch Frontmatter an, und der Titel — die einzige Identität einer
|
||
Seite — ändert sich nie, also folgt kein Referenz-Update. Ein bereits
|
||
belegtes Ziel (ein alter Stem-Kollisionsrest) wird verweigert statt still
|
||
überschrieben. Top-level registriert, wie `rename`/`rm`, nicht unter einem
|
||
`page`-Unterbefehl.
|
||
- `lint` bekommt einen neuen Befund, **Misplaced Pages**, mit Ist- und
|
||
Soll-Pfad. Bewusst nicht in `HARD_ERROR_KEYS`: ein von Hand platzierter
|
||
Bestand ist keine kaputte Seite, nur eine, die `move` aufräumen könnte — auf
|
||
dieser Instanz sind das aktuell die drei `kb/entities/projects/*/`-Seiten,
|
||
die #57 separat behandelt.
|
||
|
||
Ein Fund unterwegs, der ohne #56 unsichtbar geblieben wäre: `migrate verify`
|
||
schlüsselte Seiten über den repo-relativen **Pfad**. Ein reiner Move ergab
|
||
„N removed, N added, 0 compared" und lief grün durch — der eine mechanische
|
||
Check, für den `instructions/migrate-corpus.md` existiert, hätte bei genau der
|
||
Operation nichts geprüft, die dieses Issue einführt. Behoben: `PageShape`
|
||
trägt jetzt zusätzlich den Pfad, `_shapes_at_revision`/`_shapes_now`
|
||
schlüsseln über den Titel (die einzige Identität einer Seite), und
|
||
`CorpusDiff.moved` meldet einen reinen Ortswechsel separat — informativ,
|
||
niemals als Finding. Regressionstest deckt drei verschobene Seiten mit
|
||
`compared == 3, added == 0, removed == 0` ab.
|
||
|
||
Bewusst nicht Teil dieses Pakets: die drei realen Seiten aus #57 bleiben
|
||
liegen (kein Korpus-Publish hier, nur Stack), und `raw rename` (#16) — das
|
||
`git mv`-mit-`mv`-Fallback aus #56s Entwurf war für Rohdateien gedacht;
|
||
`rename`/`rm`/`move` bewegen kb-Seiten über ein einfaches `Path.rename`, weil
|
||
`publish` ohnehin über `git add -A` staged.
|
||
|
||
Geändert: `tools/chemenu/type_resolver.py` (`compute_target_dir`,
|
||
`subtype_dir`), `tools/chemenu/commands/new_page.py` (delegiert),
|
||
`tools/chemenu/commands/page_ops.py` (`move_command`),
|
||
`tools/chemenu/lint_core.py` (`find_misplaced`, `misplaced_pages`),
|
||
`tools/chemenu/corpus_diff.py` und `tools/chemenu/commands/migrate_cmd.py`
|
||
(Titel-Schlüsselung, `moved`), `tools/chemenu/commands/log_append.py` (`--op
|
||
move`), `tools/CONTRACT.md`, `instructions/page-lifecycle.md`,
|
||
`instructions/publish-cycle.md`. MINOR: rückwärts liest ein älterer Stack eine
|
||
verschobene Seite unverändert (Identität ist der Titel, nicht der Ort),
|
||
vorwärts reines Überkopieren.
|
||
|
||
**Katalogtiefe** (#57): `index_build.group_pages` las bislang genau zwei
|
||
Pfadsegmente unter `kb/` (`parts[0]` als Collection, `parts[1]` als Area) und
|
||
faltete alles darunter still in die Level-1-Area. Real betroffen waren die
|
||
drei Seiten aus #56s Befund, `kb/entities/projects/{kfchou,vanillaflava,
|
||
yugasun}/*.md` — im generierten Katalog nicht als eigener Ort sichtbar,
|
||
sondern als läge jede direkt in `entities/projects/`. `kb/CONTRACT.md` §
|
||
Collections beschrieb bis heute nur eine Ebene, ohne eine zweite
|
||
auszuschließen; der Baum hatte trotzdem eine, handplatziert, ohne
|
||
unterstützten Weg dorthin.
|
||
|
||
Entscheidung war (b) aus dem Issue: die drei Verzeichnisse auflösen statt den
|
||
Katalog rekursiv zu machen. Die Gruppierungsachse dahinter — Owner
|
||
(`kfchou`/`vanillaflava`/`yugasun`) — kommt aus keinem Frontmatter-Feld und
|
||
aus keinem Type-Spec, sondern aus einer Ad-hoc-Entscheidung beim Anlegen; sie
|
||
verdient keine zweite Verzeichnisebene. Tiefe 1 ist jetzt geschriebene Regel
|
||
in `kb/CONTRACT.md` § Collections, mit dieser Begründung.
|
||
|
||
Vier Stücke setzen das um:
|
||
|
||
- **`kb_scan.find_nested_pages`** liefert `(title, page, depth)` für jede
|
||
Seite mehr als ein Verzeichnis unterhalb ihrer Collection — reine
|
||
Pfadtiefe, unabhängig davon, ob der Typ auflöst, damit auch eine Seite mit
|
||
kaputtem `type:` nicht durchrutscht.
|
||
- **`lint`** bekommt den Befund **Nested Pages**, und anders als
|
||
`misplaced_pages` **hart**: eine fehlplatzierte Seite katalogisiert noch
|
||
korrekt von der falschen Stelle aus, eine verschachtelte macht den
|
||
*generierten* Katalog selbst falsch, und es gibt keine Version, ab der das
|
||
toleriert würde.
|
||
- **`index rebuild`** lehnt eine verschachtelte Seite nicht ab, sondern warnt
|
||
(Entscheidung aus der Session: melden statt verweigern, damit ein
|
||
Fremdinstanz-Upgrade mit handverschachtelten Seiten nicht hart bricht) —
|
||
`group_pages` faltet weiterhin wie zuvor, die Warnung ist die neue
|
||
Sichtbarkeit, nicht eine Verhaltensänderung der Faltung selbst.
|
||
- **`TypeResolver.get_layout`** validiert `layout: {dir: ...}` jetzt auf
|
||
einen einzelnen Pfadabschnitt (kein `/`, kein `\`, kein `.`/`..`, nicht
|
||
leer) und schlägt fehl statt eine zweite Ebene über den einzigen
|
||
unterstützten Weg — ein Type-Spec — entstehen zu lassen.
|
||
|
||
Ein fünftes Stück, das das Issue selbst nicht explizit forderte, aber die
|
||
`move`-Mechanik aus #56 sonst mit toten Verzeichnissen zurückgelassen hätte:
|
||
**`move`** (`--page` wie `--reconcile`) entfernt jetzt ein Verzeichnis, das es
|
||
durch den Wegzug seiner letzten Seite geleert hat — symmetrisch zum
|
||
`mkdir(parents=True)` auf der Zielseite. Ohne das hätten `kfchou/`,
|
||
`vanillaflava/`, `yugasun/` den eigenen Fix überlebt, leer und für git
|
||
unsichtbar, aber für einen verzeichnisbasierten Test sichtbar.
|
||
|
||
Auf dieser Instanz angewendet: `wikitool move --reconcile` hat die drei
|
||
Seiten nach `kb/entities/projects/` gezogen und die drei leeren
|
||
Owner-Verzeichnisse mitentfernt. `migrate verify --from HEAD` bestätigt
|
||
`compared == 182, added == 0, removed == 0`, alle drei als `moved` markiert.
|
||
Titelkollision trat wie im Issue erwartet keine auf.
|
||
|
||
Geändert: `tools/chemenu/kb_scan.py` (`find_nested_pages`),
|
||
`tools/chemenu/lint_core.py` (`nested_pages`, `HARD_ERROR_KEYS`),
|
||
`tools/chemenu/commands/index_build.py` (Rebuild-Warnung),
|
||
`tools/chemenu/type_resolver.py` (`get_layout`-Validierung),
|
||
`tools/chemenu/commands/page_ops.py` (`_rmdir_if_emptied`), `tools/CONTRACT.md`,
|
||
`kb/CONTRACT.md` § Collections, plus die drei realen Seiten unter
|
||
`kb/entities/projects/`. PATCH: reine Codeänderung ohne Schnittstellenwechsel,
|
||
gefaltet in den offenen `4.8.0`-Kandidaten (max-wins gegen die MINOR-Bewegung
|
||
aus #56); die drei bewegten Seiten sind Korpus dieser Instanz, kein
|
||
ausgelieferter Inhalt.
|
||
|
||
Schließt #57.
|
||
|
||
**`raw accept`** (#58): `raw/CONTRACT.md`s Routing-Tabelle war bislang eine Regel für Menschen —
|
||
wer eine Datei ablegt, wählt `articles/`/`documents/`/`notes/`/`assets/` selbst, und mehrere
|
||
Dateien einer logischen Quelle waren im Dateisystem nicht als zusammengehörig erkennbar. Neu ist
|
||
ein gitignorierter Eingang `incoming/`, der dieselben vier Typverzeichnisse spiegelt: der Mensch
|
||
klassifiziert nur, indem er dort ablegt, `tools/wikitool raw accept <datei> [<datei> ...]
|
||
[--page "<Titel>"]` berechnet die Beförderung nach `raw/`.
|
||
|
||
Zwei Entscheidungen, gegen die ursprüngliche Skizze im Issue: ein Bundle-Verzeichnis
|
||
(`raw/<typ>/<stamm>/`, benannt nach der ersten Datei) entsteht erst ab der zweiten Datei, nie
|
||
einheitlich — damit sind die 29 heute flach liegenden Bestandsdateien keine Ausnahme, sondern
|
||
bereits die Regelform, und die Frage „was passiert mit dem Bestand" beantwortet sich von selbst.
|
||
Und der Typ wird über das Eingangs-Unterverzeichnis deklariert, nicht über ein `--type`-Flag: die
|
||
Erklärung wird abgegeben, wenn der Mensch die Datei in der Hand hat, statt im Moment des
|
||
`accept`-Aufrufs neu geraten werden zu müssen.
|
||
|
||
`--page` deckt den Wachstumsfall ab: erweitert `raw_files:` einer bestehenden Source-Seite und
|
||
faltet deren schon abgelegte Einzeldatei ins neue Bundle, sobald das die Seite über eine Datei
|
||
hinaus wachsen lässt — ohne ein Fenster, in dem `raw_files:` ins Leere zeigt. Die dafür nötige
|
||
Rückwärtssuche und der Mehrfach-Owner-Schutz sind keine neue Mechanik, sondern
|
||
`provenance.source_pages_by_raw_file`, das `lint` schon für `duplicate_raw_file_owners` benutzt —
|
||
ein Owner-Konflikt lehnt die Beförderung ab, statt eine andere Seite unbemerkt zu brechen.
|
||
|
||
`incoming/` ist für `sources coverage` und `lint` unsichtbar (beide laufen ausschließlich über
|
||
`config.iter_raw_files(config.RAW_DIR)`), und dass keine Datei dort je committet werden kann, ist
|
||
über `docs_verify.REQUIRED_IGNORE_CANARIES` bewiesen, nicht nur zugesichert. `RAW_SUBDIRS`
|
||
(`dist_cmd.py`) bleibt die einzige Quelle der Vier-Verzeichnis-Liste: `docs verify`
|
||
(`check_raw_subdirs`) hält `raw/CONTRACT.md`s Tabelle jetzt in beiden Richtungen dagegen, und
|
||
`dist export` sät `incoming/<typ>/.gitkeep` neben `raw/<typ>/.gitkeep`; `instructions/bootstrap.md`
|
||
legt den Eingang für einen bestehenden Klon nach, da er dort nie aus git kommt.
|
||
|
||
Geändert: `tools/chemenu/commands/raw_cmd.py` (neu, `raw accept`), `tools/chemenu/cli.py`,
|
||
`tools/chemenu/commands/dist_cmd.py` (`RAW_SUBDIRS`-Kommentar, `incoming/*/.gitkeep`),
|
||
`tools/chemenu/commands/docs_verify.py` (`check_raw_subdirs`, `incoming/`-Ignore-Kanarie),
|
||
`.gitignore`, `raw/CONTRACT.md`, `tools/CONTRACT.md`, `instructions/bootstrap.md`,
|
||
`instructions/wiki-ingest/SKILL.md`. MINOR: eine Umsortierung des Bestands wäre die Grenze
|
||
gewesen, findet aber unter der Bundle-erst-ab-zwei-Regel nicht statt — der Bestand bleibt
|
||
unangetastet, keine fremde Instanz muss migrieren, vorwärts wie rückwärts reines Überkopieren.
|
||
|
||
Schließt #58.
|
||
|
||
`raw accept` prüfte Kollisionen bisher nur auf einzelnen Dateipfaden
|
||
(`dst.exists()`), nie auf dem Bundle-Verzeichnis selbst. Weil der Bundle-Name
|
||
aus dem Stem der Primärdatei entsteht, konnte eine zweite, unabhängige Quelle
|
||
wortlos in das Bundle einer ersten wandern, sobald die Dateinamen zufällig
|
||
nicht kollidierten — verifiziert mit einem Wegwerf-Test: `handbuch.txt` +
|
||
`anhang.md` ohne `--page` landeten unbemerkt in einem bestehenden
|
||
`raw/documents/handbuch/`. `lint` meldete nichts, weil beide Quellen ihre
|
||
Dateien korrekt abdeckten.
|
||
|
||
Die Menge der Namen auf `raw/<typ>/`-Ebene — Dateistämme plus
|
||
Bundle-Verzeichnisnamen — ist jetzt eindeutig erzwungen (`_occupied_stems` in
|
||
`raw_cmd.py`), unter Ausnahme dessen, was der Aufruf selbst schon besitzt: ein
|
||
Bundle, das über `--page` wächst, oder ein bereits registriertes, sich
|
||
fortsetzendes Bundle. Ein belegter Stem wird mit Exit 1 abgelehnt und nennt
|
||
beide Auswege, ohne einen zu empfehlen — `--replaces` oder Umbenennen in
|
||
`incoming/`.
|
||
|
||
`--replaces <raw-pfad>` ist der einzige sanktionierte Weg, eine Rohdatei
|
||
wortwörtlich zu ersetzen: genau eine eingehende Datei, identischer Dateiname,
|
||
gleiches Typverzeichnis, Ablehnung bei mehr als einem Owner. `raw_files:`
|
||
bleibt unverändert, es wird keine `kb/`-Seite geschrieben, und die Altfassung
|
||
lebt ausschließlich in `git log --follow` weiter — kein Archivverzeichnis,
|
||
kein Hash im Dateinamen, kein neues Frontmatter-Feld. Nach einem Ersatz nennt
|
||
das Kommando die Source-Seite und ihre zitierenden Seiten, damit deren
|
||
Nachzug im selben Commit passiert wie die Ersetzung.
|
||
|
||
`raw/CONTRACT.md` § Rules trägt beide Regeln aus dieser Entscheidung
|
||
(unveränderlich, aber ersetzbar als Ganzes); `instructions/wiki-ingest/SKILL.md`
|
||
benennt den Kollisionsfall als Haltepunkt, an dem eine Sitzung die Meldung dem
|
||
Menschen vorlegt statt selbst zu entscheiden — dieselbe Klasse wie AGENTS.md
|
||
Invariante 6, auch ohne dass hier ein Exit-42-Gate greift.
|
||
|
||
Geändert: `tools/chemenu/commands/raw_cmd.py` (`_occupied_stems`,
|
||
`_stem_collision_message`, `--replaces`/`_replace`),
|
||
`tools/chemenu/tests/test_raw_cmd.py` (16 neue Tests), `raw/CONTRACT.md`,
|
||
`tools/CONTRACT.md`, `instructions/wiki-ingest/SKILL.md`. MINOR: `raw accept`
|
||
wurde nie released (letztes Release `v4.7.4`), die Verschärfung kostet also
|
||
keine Kompatibilitätsfrage, solange sie vor `4.8.0` landet.
|
||
|
||
Schließt #64.
|
||
|
||
**`kb/concepts/` bekommt Areas** (#59): Sharden ist längst automatisch —
|
||
`index_build.SHARD_THRESHOLD = 50`, hergeleitet aus der wikieigenen Seite
|
||
`Index Scaling` — aber es passiert **pro Area**, und eine Area legt niemand an.
|
||
`kb/concepts/` hatte keine, also war die Schwelle dort ein toter Wert: 80 Seiten
|
||
in einer einzigen Tabelle, weit über der eigenen Grenze, ohne dass je etwas
|
||
gefeuert hätte. Die Ursache war eine Asymmetrie in den Type-Specs — `entity`
|
||
deklarierte ein `layout:`, `concept` nicht, obwohl das Subtype-Feld fertig dalag.
|
||
|
||
`types/concept.md` deklariert es jetzt für alle sechs `concept_type`-Werte
|
||
(`architectures/`, `patterns/`, `protocols/`, `workflows/`, `decisions/`,
|
||
`problems/`). Für `source` bewusst **nicht**: 25 von 29 Seiten sind `notes`, die
|
||
Aufteilung ergäbe eine Area und vier Splitter, und `kb/sources/` liegt mit 29
|
||
Seiten ohnehin unter der Schwelle. Ein Subtype-Feld zu haben ist kein Grund, es
|
||
als Achse zu benutzen.
|
||
|
||
Zwei Dinge im Code, beide Folgen desselben Befunds. `_area_titles()` in
|
||
`index_build.py` löste `entity` fest über `find_type_by_name("entity")` auf und
|
||
las nur dessen `layout:` — jeder zweite Typ mit einem `layout:` hätte
|
||
`.title()`-Namen auf dem Verzeichnisnamen bekommen statt der deklarierten Titel.
|
||
Es liest jetzt jedes Type-Spec, und zwar **pro Collection** geschlüsselt, damit
|
||
zwei Typen denselben Area-Namen für Verschiedenes benutzen dürfen. Und der neue
|
||
`lint`-Befund meldet eine Collection über der Schwelle **ohne** Areas, mit der
|
||
Verteilung ihres Subtype-Felds — als Empfehlung, nicht als Failure, und nur
|
||
dann, wenn die Aufteilung jede entstehende Area unter die Schwelle drückt. Das
|
||
begrenzt sich selbst in beide Richtungen: `kb/comparisons/` mit einer Seite
|
||
feuert nie, und die schlechte Aufteilung nach `source_type` unterbleibt von
|
||
allein, ohne dass der Check etwas über Sources wüsste.
|
||
|
||
Zwei Dinge fielen unterwegs an, die das Issue nicht vorhergesehen hatte.
|
||
`lint_core.py` durfte `SHARD_THRESHOLD`/`group_pages` nicht aus
|
||
`commands/index_build.py` importieren — `test_api.py` prüft strukturell, dass
|
||
`chemenu.api` kein Modul unter `chemenu.commands` lädt, und der Import hätte den
|
||
ganzen CLI-Kopf mitgezogen. Die Gruppierung liegt deshalb neu in
|
||
`tools/chemenu/catalog.py`, entlang derselben Linie wie `lint_core.py`:
|
||
Korpusform hier, Darstellung dort. Und `_anchor()` strich mit `[^a-z0-9\s-]`
|
||
jeden Nicht-ASCII-Buchstaben ersatzlos — die Karte verlinkte auf `#ablufe`,
|
||
während die Überschrift im Shard `#abläufe` heißt. Vorher fiel das keinem auf,
|
||
weil alle Entity-Area-Titel zufällig ASCII sind; `Abläufe` ist der erste, der es
|
||
nicht ist.
|
||
|
||
Auf dieser Instanz angewendet: `wikitool move --reconcile` hat alle 80
|
||
Concept-Seiten in ihre Area gezogen, `migrate verify --from HEAD` bestätigt
|
||
`182 compared, 0 added, 0 removed, 80 moved, 0 findings` — kein Titel, kein
|
||
Body, kein Frontmatter-Feld angefasst. `index rebuild` erzeugt sechs Areas
|
||
(Abläufe 28, Architekturen 20, Muster 17, Entscheidungen 7, Problemstellungen 5,
|
||
Protokolle 3); keine über der Schwelle, also kein eigener Shard, und die
|
||
Schwelle wirkt wieder als Schwelle.
|
||
|
||
Geändert: `types/concept.md` (`layout:`), `types/type-spec.md` (wann ein
|
||
`layout:` sich lohnt), `tools/chemenu/catalog.py` (neu),
|
||
`tools/chemenu/commands/index_build.py` (`area_titles`, `_anchor`),
|
||
`tools/chemenu/lint_core.py` (`unsharded_collections`),
|
||
`tools/chemenu/tests/` (Fixture-Concept liegt jetzt in seiner Area, plus neun
|
||
neue Tests), `tools/CONTRACT.md`, `tools/README.md`, `README.md`,
|
||
`kb/concepts/COLLECTION.md`, sowie die 80 bewegten Seiten unter `kb/concepts/`.
|
||
|
||
**MINOR**, nicht MAJOR: der Umzug ist ein **Angebot**, kein Zwang. Eine
|
||
bestehende Instanz, die `move --reconcile` nicht laufen lässt, bleibt
|
||
funktionsfähig — `group_pages` liest das Dateisystem, nicht das `layout:`, also
|
||
landen flache Bestandsseiten in der Area „All" und neu angelegte in ihrer
|
||
eigenen; beides rendert. Der gemischte Zustand meldet sich als `lint`-Befund
|
||
*Misplaced Pages*, der seit jeher advisory ist. Und ein Downgrade auf einen
|
||
Stack ohne dieses `layout:` funktioniert weiter: die Verzeichnisse bleiben
|
||
Verzeichnisse, nur die Anzeigetitel fallen auf `.title()` zurück. Kosmetik, kein
|
||
Bruch der Austauschbarkeit in beiden Richtungen.
|
||
|
||
Schließt #59.
|
||
|
||
**Nachzug an `63b4bb8`:** die Umstellung der Namenskonvention auf `HA Integration`
|
||
hatte in `README.md` das Gegenbeispiel verloren — die Zeile las
|
||
``Use singular for entities: `HA Integration.md` (not `HA Integration.md`)``, beide
|
||
Seiten des „not" identisch, also eine Regel ohne Fall, an dem sie greift.
|
||
`kb/CONVENTIONS.md` und `kb/entities/COLLECTION.md` hatten im selben Commit das
|
||
korrekte Paar bekommen; `README.md` zieht jetzt mit `HA Integrations.md` nach.
|
||
|
||
`source_type` hatte in `types/source.schema.yaml` ein `default: notes` — der Compiler wählte
|
||
das Sammelbecken, sobald niemand widersprach, nicht ein Mensch. #59 hatte den Bestand deshalb
|
||
für lopsided gehalten (25 von 29 Seiten `notes`) und `source` bewusst flach gelassen; nachgezählt
|
||
nach dem, was die Seiten tatsächlich sind (Dateiname, `author:`, Rohdatei), waren es 16
|
||
Session-Transkripte, 4 LLM-Analysen, 2 Tracker-Exporte und nur 3 echte Notizen. Der Default war
|
||
der Fehler, nicht das Enum.
|
||
|
||
Umgesetzt: `default:` gestrichen, `wikitool new source` verweigert jetzt ohne expliziten Wert.
|
||
Enum neu: `transcript`, `analysis`, `article`, `document`, `notes`, `tracker`, `unclassified` —
|
||
`spec` und `image` entfallen (null Seiten, nie am echten Material bewährt). `unclassified` ist
|
||
das neue, sichtbare Fach für eine Quelle, deren Kategorie noch nicht feststeht — eigene Area,
|
||
beratender `lint`-Befund (`unclassified_source_pages`), keine harte Fehlerklasse. `types/source.md`
|
||
deklariert jetzt ein `layout:` für alle sieben Werte.
|
||
|
||
Auf dieser Instanz angewendet, in einem eigenen `work/reclassify-source-types/`-Lauf: 22 der 29
|
||
Source-Seiten per `wikitool touch --set source_type=<wert>` auf ihren tatsächlichen Wert
|
||
korrigiert (16 transcript, 4 analysis, 2 tracker), 7 unverändert. `wikitool move --reconcile` hat
|
||
alle 29 danach in ihre Area gezogen (transcripts 16, analyses 4, articles 3, notes 3, trackers 2,
|
||
documents 1, unclassified 0). `migrate verify --from HEAD` bestätigt `182 compared, 0 added,
|
||
0 removed, 29 moved, 22 findings` — die 22 sind exakt die beabsichtigten `source_type`-Änderungen,
|
||
kein Titel, kein Body, keine Wikilink- oder Zitatzahl angefasst. `lint --fail-on-error` grün,
|
||
insbesondere ohne *Misplaced Pages*, *Nested Pages* oder den neuen *Unclassified Source Pages*.
|
||
|
||
Geändert: `types/source.schema.yaml` (Enum, kein Default), `types/source.md` (`layout:`),
|
||
`types/type-spec.md` (`source` als Beispiel für ein bewusst fehlendes `layout:` ersetzt —
|
||
ein lopsided Feld wird repariert, nicht dauerhaft flach gelassen), `tools/chemenu/lint_core.py`
|
||
(`unclassified_source_pages`, neu), `tools/CONTRACT.md`, `instructions/wiki-ingest/SKILL.md`
|
||
(`--set source_type=`, `unclassified` als Ausweg), `instructions/dev/corpus-policy.md`
|
||
(Floor-Ausnahme für `unclassified`), `kb/sources/COLLECTION.md` (Areas, Autorschaft trennt
|
||
`analysis` von `document`), fünf Tests umgehängt (davon einer auf einen neuen
|
||
Fixture-Type-Spec, weil `source` als „hat Subtype-Feld, kein `layout:`"-Beispiel wegfällt),
|
||
sowie die 29 bewegten und 22 reklassifizierten Seiten unter `kb/sources/`.
|
||
|
||
**MINOR**, geprüft am Drop-in-Test: eine bestehende Instanz besitzt ihr eigenes
|
||
`types/source.md` (Auslieferung nur als `.template`), kopiert `tools/`/`types/`/`instructions/`
|
||
über sich und bleibt unverändert funktionsfähig — kein Downgrade-Bruch, kein umgeschriebenes
|
||
maschinengelesenes Format. Kein `--breaking`, kein Migrationsdokument.
|
||
|
||
Schließt #66.
|
||
|
||
**`raw accept`: Datums-Shard statt Typverzeichnis, `fidelity`/`authority` am Drop-Punkt** (#67,
|
||
Paket B von vier — A ist #66 oben, C ist #68, D ist #69): zwei unabhängige Befunde, ein
|
||
Codepfad.
|
||
|
||
Befund 1: `raw/CONTRACT.md`s vier Typverzeichnisse (`articles/`, `documents/`, `notes/`,
|
||
`assets/`) lösten keinen der drei Gründe ein, die einen Verzeichnis-Split rechtfertigen —
|
||
`raw/` wird nie durchgeblättert, keine Klausel dieser Datei galt je pro Verzeichnis, alle vier
|
||
verrotten gleich (unveränderlich, nie gelöscht). Der Split kostete real: der Mensch trifft beim
|
||
Ablegen in `incoming/<typ>/` eine Routing-Entscheidung, die später blind nach `source_type:`
|
||
abgeschrieben wird — genau darüber entstand der in #66 korrigierte Bias (`raw/notes/` hielt laut
|
||
altem Contract-Text „Gesprächsprotokolle", 16 der 25 Dateien dort waren tatsächlich Transkripte).
|
||
|
||
Befund 2: was am Drop-Punkt bekannt ist und danach nirgends mehr — wie treu eine Erfassung ist
|
||
und was das Material über seinen Gegenstand behaupten darf. Zwei neue, unabhängige Achsen auf
|
||
`types/source.md`: `fidelity` (`verbatim`/`published`/`secondhand`/`nontextual`) und `authority`
|
||
(`normative`/`reporting`/`opinion`), beide mit `unknown` als backfill-only-Wert.
|
||
|
||
Umgesetzt:
|
||
|
||
- `raw accept` adressiert eine Datei jetzt über `raw/<YYYY>/<MM>/`, berechnet aus dem
|
||
Annahmedatum — eine reine Funktion von etwas Unveränderlichem, kann also nie rebalancieren und
|
||
keinen `[^cite-id]`-Anker brechen. `incoming/` wird flach; ein Unterverzeichnis wird toleriert
|
||
und ignoriert statt inspiziert (alte `incoming/<typ>/`-Skripte laufen unverändert weiter).
|
||
Bestandsdateien in `raw/articles|documents|notes|assets/` bleiben unbewegt und weiter gültige
|
||
`--replaces`-Ziele — das Layout war nirgends versioniert, es gibt also keine „zwei Korpusformen".
|
||
Wächst eine bereits promotete Einzeldatei zum Bündel, entsteht das Bündel an ihrem eigenen
|
||
Speicherort, nie im heutigen Shard — ein Bündel aus altem und neuem Datum hätte keine
|
||
eindeutig richtige Adresse.
|
||
- Stem-Eindeutigkeit (#64) gilt jetzt global über `raw/`, nicht mehr pro Typverzeichnis —
|
||
ohne Typverzeichnisse als Grenze wäre die Prüfung sonst wirkungslos gegen ein Bündel in einem
|
||
anderen Shard oder einem Alt-Verzeichnis.
|
||
- `fidelity`/`authority`: neu in `types/source.schema.yaml`, ohne `default:` und bewusst
|
||
**nicht** in `required:` (sonst bricht jede bestehende Instanz an der Validierung — die
|
||
MINOR-Einstufung unten hängt daran). Erzwungen stattdessen im Werkzeug: `raw accept` verlangt
|
||
beide Flags immer; trägt der Aufruf `--page`, schreibt es sie direkt auf die Zielseite, sonst
|
||
druckt es die fertige `new source --set fidelity=... --set authority=...`-Folgezeile, und
|
||
`new source` verweigert seinerseits ohne beide Werte. `unknown` ist backfill-only — weder
|
||
`raw accept` noch `new source` dürfen es schreiben.
|
||
- Capture-Felder sind **fill-once**, nicht auf `touch.py`s `UNSETTABLE`-Denylist: eine Denylist
|
||
hätte auch den ersten (Backfill-)Schreibzugriff verboten, den der Migrationslauf braucht.
|
||
`touch --set <feld>=<wert>` schreibt nur, solange das Feld fehlt, und verweist danach auf
|
||
`raw accept --replaces` als einzigen Korrekturweg — der einzige Aufruf, der einen bereits
|
||
gesetzten Capture-Wert überschreiben darf, weil eine korrigierte Erfassung eine neue Edition
|
||
der Quelle ist, keine Bearbeitung der Seite. `types/source.md` deklariert die Feldliste selbst
|
||
(`capture_fields:`), gelesen über `TypeResolver.get_capture_fields` statt an drei Stellen
|
||
hartkodiert.
|
||
- `lint` bekommt einen neuen beratenden Befund, *Confidence Above Source Standing*: die
|
||
Autoritätsbewertung, die `kb/CONVENTIONS.md`s Confidence-Rubrik seit je verlangt („+0.1 für
|
||
offizielle Doku"), aber nirgends festhielt. Eine stackseitige Obergrenzentabelle in
|
||
`kb/CONTRACT.md` (`reporting` 0.8, `opinion` 0.6, `secondhand`/`nontextual` 0.7, `normative`/
|
||
`verbatim`/`published`/`unknown` ohne Obergrenze) begrenzt `confidence_base`, ersetzt es aber
|
||
nicht — eine Formel hätte zwei widersprechende Ableitungen derselben Zahl, und Autorität ist
|
||
eine Obergrenze, kein Determinant. Nicht in `HARD_ERROR_KEYS`. Auf dieser Instanz meldet der
|
||
Befund aktuell nichts: kein Bestand trägt die neuen Felder, das ist erwartet, nicht geprüft.
|
||
- Aufgeräumt: `dist_cmd.RAW_SUBDIRS` und der darauf laufende `docs verify`-Check
|
||
(`check_raw_subdirs`) entfallen ersatzlos, `raw/CONTRACT.md`s Routing-Tabelle beschreibt
|
||
stattdessen den Shard, die Ignore-Kanarie wandert von `incoming/documents/probe.pdf` auf
|
||
`incoming/probe.pdf`.
|
||
|
||
**MINOR**, `4.8.0-beta.8` desselben Kandidaten — drei geprüfte Bedingungen: `raw accept` nimmt
|
||
weiterhin Dateien aus `incoming/<irgendwas>/` an, statt sie zu verweigern; `fidelity`/`authority`
|
||
stehen nicht in `required:`; die zwei neuen Pflichtflags an `raw accept` sind eine
|
||
Verhaltensänderung, aber dieselbe Einstufung, die #66s `new source`-Verweigerung im selben
|
||
Kandidaten schon bekam. Kein `--breaking`, kein Migrationsdokument nötig — kein Bestand wird
|
||
durch diesen Bump ungültig.
|
||
|
||
Geändert: `raw/CONTRACT.md`, `types/source.md`, `types/source.schema.yaml`,
|
||
`tools/chemenu/commands/raw_cmd.py` (Neufassung), `tools/chemenu/commands/new_page.py`
|
||
(Capture-Feld-Pflicht), `tools/chemenu/commands/touch.py` (`_capture_field_or_fail`,
|
||
Fill-once), `tools/chemenu/type_resolver.py` (`get_capture_fields`),
|
||
`tools/chemenu/commands/dist_cmd.py`/`docs_verify.py` (Aufräumen), `tools/chemenu/lint_core.py`
|
||
(neuer Befund), `kb/CONTRACT.md` (Obergrenzentabelle), `tools/CONTRACT.md`, `.gitignore`,
|
||
`instructions/bootstrap.md`, `instructions/wiki-ingest/SKILL.md` (Schritt 1 und 6), zugehörige
|
||
Tests.
|
||
|
||
Der Backfill über den Bestand lief als eigener `work/`-Lauf (`backfill-capture-fields`,
|
||
Commit `00220f8`) hinterher, nach `instructions/migrate-corpus.md` und durch das
|
||
Mass-Update-Gate: **29 Source-Seiten**, nicht 31 wie zwischenzeitlich im Issue notiert — die
|
||
höhere Zahl zählte `INDEX.md` und `COLLECTION.md` mit.
|
||
|
||
Die Regel dieses Laufs war enger als die des laufenden Betriebs, weil ein nachgetragener
|
||
Capture-Wert *erschlossen* ist und nicht *erhoben*: ein echter Wert nur dort, wo die Art des
|
||
Artefakts ihn aus dem Material selbst festlegt, sonst `unknown`. Ergebnis: `verbatim`+`reporting`
|
||
18 (16 Gesprächstranskripte, 2 Tracker-Exporte — beide wörtliche Mitschnitte, beide Protokoll
|
||
statt Festlegung), `secondhand`+`opinion` 3 (LLM-Analysen), `published`+`reporting` 2,
|
||
`published`+`normative` 1 (Karpathys Idea-File, das definierende Dokument seines eigenen
|
||
Gegenstands), `verbatim`+`normative` 1 (das qmd-README, dessen Rohdatei ihre Treue selbst
|
||
deklariert), `unknown`+`normative` 1, `unknown`+`reporting` 3.
|
||
|
||
**`fidelity: unknown` steht auf 4 der 29 Seiten, `authority: unknown` auf keiner.** Die
|
||
Asymmetrie ist der interessante Teil: wer für einen Gegenstand zuständig war, ließ sich überall
|
||
aus dem Material beantworten — wie treu ein selbstverfasstes Cheat Sheet oder ein Anweisungsdokument
|
||
„erfasst", nicht, weil der Enum für ein originär geschriebenes Artefakt keinen Wert hat. Das ist
|
||
kein Backfill-Fehler, sondern genau die Grenze, die `unknown` markieren soll.
|
||
|
||
Und der `lint`-Befund hat einen Fall — 67 sogar: nach dem Backfill melden **67 von 152 Seiten**
|
||
mit `confidence_base` mehr Konfidenz, als die Quellenlage trägt (47 gegen die 0.8-Grenze für
|
||
`reporting`, 20 gegen die 0.6-Grenze für `opinion`). Das ist kein Fehlalarm und auch keine
|
||
Nacharbeit dieses Eintrags: der Korpus ist zu gut der Hälfte aus Gesprächstranskripten kompiliert,
|
||
und die Rubrik in `kb/CONVENTIONS.md` lässt Quellenzahl und Aktualität allein bis 0.95 laufen,
|
||
während der Autoritätsterm der kleinste Summand ist. Ob daraus folgt, dass 67 Seiten überbewertet
|
||
sind oder dass die 0.8-Grenze für einen selbstdokumentierenden Korpus zu eng ist, ist eine
|
||
Entscheidung und keine Korrektur — sie hängt als Messung am Stub #60, der genau diesen Verdacht
|
||
ohne Zahlen aufgeschrieben hatte. Die Transkripte wurden ausdrücklich **nicht** auf `normative`
|
||
hochgestuft, nur damit der Report leiser wird.
|
||
|
||
Schließt #67.
|
||
|
||
**`source_type` ist Instanzsache: Profilkatalog, Setup-Frage, `evolve-subtypes`-Instruction**
|
||
(#68, Paket C von vier — A ist #66, B ist #67, D ist #69): reine Doku- und Instruction-Arbeit,
|
||
kein Korpus-Sweep.
|
||
|
||
Befund: `source_type`s sieben Werte (`transcript`, `analysis`, `article`, `document`, `notes`,
|
||
`tracker`, `unclassified`) beschreiben *diese* Instanz, nicht den Stack — gegen drei
|
||
hypothetische Zielinstanzen (Handball-Verein, Produktentwicklung, Pen-&-Paper) hat die Liste
|
||
fast nichts gemeinsam, während `fidelity`/`authority` (#67) in allen vieren dieselben Werte
|
||
bleiben. Architektonisch war das längst wahr (`types/source.md` trägt `root: kb`, `dist export`
|
||
liefert es nur als `.template`), nur stellte nichts die Frage: `instructions/kb-profiles.md`
|
||
riet im `entities`-Abschnitt „Adapt the area list first", sagte im `sources`-Abschnitt aber kein
|
||
Wort zu `source_type`. Zweiter Befund, aus #66 mitgenommen: das `unclassified`-Fach bekam einen
|
||
beratenden `lint`-Befund, aber nie eine Prozedur, es wieder zu leeren.
|
||
|
||
Umgesetzt:
|
||
|
||
- `kb-profiles.md`s `sources`-Abschnitt behandelt `source_type` jetzt wie `entities` seine Area-
|
||
Liste: als das, was zuerst anzupassen ist, mit zwei ausformulierten Domänenprofilen
|
||
(Handball-Verein, Pen-&-Paper) als Anschauung, und dem ausdrücklichen Gegenbeispiel
|
||
`fidelity`/`authority` — die sind Stack-Vokabular und stehen nicht zur Wahl.
|
||
- `instructions/setup-instance.md` Schritt 5 bekommt einen neuen Unterschritt: nach dem
|
||
Anwendungsgebiet fragen, `source_type`-Vorschlag ableiten, Enum und `layout:` in derselben
|
||
Bearbeitung setzen. Mit der Ansage, dass der Betreiber zum Setup-Zeitpunkt null Quellen hat und
|
||
seine Taxonomie vor jedem Material rät — das Ergebnis ist ein Startpunkt, keine Festlegung, und
|
||
`unclassified` bleibt in jedem Vorschlag erhalten.
|
||
- Neue Instruction `instructions/evolve-subtypes.md`, `manual: true`: benennt die
|
||
Weiterentwicklungsschleife, die werkzeugseitig schon vollständig existierte (Fach sehen →
|
||
Wert samt `layout:` ergänzen → `touch`/`move --reconcile` → `index rebuild`/`migrate verify`),
|
||
über alle drei Subtype-Achsen (`entity_type`, `concept_type`, `source_type` — alle drei tragen
|
||
`subtype_field:` **und** `layout:`; `comparison` keins von beidem). Zwei Regeln im Body: Wert
|
||
und Sweep sind untrennbar (ein deklarierter Wert ohne Seite lädt zum Raten ein — genau der
|
||
`notes`-Fall aus #66), und eine Aufnahmeschwelle von ≥3 Seiten, mit `spec`/`image` aus #66 als
|
||
Gegenbeispiel und einer benannten-Ausnahme-Klausel für Fälle wie `tracker` bei zwei Seiten.
|
||
`manual: true` verhindert, dass eine Taxonomie-Änderung in einen laufenden Ingest hineinstolpert
|
||
— erwähnt aus `kb-profiles.md`, `setup-instance.md` und `kb/sources/COLLECTION.md`, aus keinem
|
||
Skill, keiner AGENTS.md, keiner CLAUDE.md verlinkt.
|
||
- `kb/sources/COLLECTION.md` benennt `evolve-subtypes.md` an der `unclassified/`-Zeile.
|
||
|
||
Die vom Vorbereitungs-Body übernommene, ursprünglich vierte Maßnahme entfiel: die
|
||
`corpus-policy.md`-Floor-Ausnahme für `unclassified` steht dort bereits seit #66.
|
||
|
||
**MINOR**, geprüft gegen den Drop-in-Test: eine Instanz kopiert `instructions/` und `types/`
|
||
über sich, nichts wird umbenannt oder entfernt, kein Kommando, kein Flag, kein
|
||
maschinengelesenes Format. Kein `--breaking`, kein Migrationsdokument. `4.8.0-beta.9` desselben
|
||
Kandidaten.
|
||
|
||
Geändert: `instructions/kb-profiles.md`, `instructions/setup-instance.md`,
|
||
`instructions/evolve-subtypes.md` (neu), `kb/sources/COLLECTION.md`.
|
||
|
||
Schließt #68.
|
||
|
||
### `instructions/CONTRACT.md` § Writing an instruction: drei offene Fragen entschieden
|
||
|
||
Drei Issues aus der #65-Analyse zeigten auf denselben Abschnitt. Alle drei
|
||
enden dort, wo die Regel steht, nicht in einem Issue-Kommentar.
|
||
|
||
**Der Imperativ-Titel bindet eine Instruction, nicht ein `SKILL.md` (#71).**
|
||
Die Regel griff dem Wortlaut nach auf alle fünf Skills durch, deren H1
|
||
Nomenphrasen sind. Geprüft gegen die Primärquelle: Anthropic normiert `name`
|
||
und `description` und sagt zur Body-Überschrift nichts; die eigenen
|
||
Beispiel-Skills heißen `# PDF Processing`, `# BigQuery Data Analysis`. Dazu
|
||
das Sachargument — der H1 liegt auf keinem Retrieval-Pfad, weil über die
|
||
Aufnahme eines Skills die `description` entscheidet, die ab Sessionstart im
|
||
Kontext steht, während der Body erst beim Zugriff gelesen wird. In der Sitzung
|
||
kam ein Beleg dazu, den keines der Issues kannte: der vendorierte
|
||
`commonplace`-Korpus trägt dieselbe Imperativ-Titel-Regel, unabhängig
|
||
entstanden, und macht im selben Absatz dieselbe Ausnahme („for promoted skills,
|
||
the skill name is the title"). Die fünf Titel bleiben unverändert.
|
||
|
||
**Referenztiefe: Anthropics „one level deep" gilt gebündeltem Material (#72).**
|
||
Weg 2 der drei zur Wahl stehenden. Der Beleg für die Reichweite steht im
|
||
vendorierten `codex-skill-creator/SKILL.md`: die Beispiele der Regel sind
|
||
`DOCX-JS.md`, `REDLINING.md`, `OOXML.md` — alles Dateien *im Skill-Bündel*. Kein
|
||
Skill dieses Repos hat heute eine solche Datei, die Regel bindet hier also
|
||
wörtlich nichts. Für den Link von einem Skill auf einen repo-weiten Contract
|
||
fallen die beiden Hälften der Frage auseinander: die Mechanik (Zweit-Hop wird
|
||
womöglich nur angelesen) ist real und verzeichnisunabhängig, die Vorschrift ist
|
||
für diesen Fall von Anthropic nicht belegt. Die geteilten Contracts bleiben
|
||
geteilt — Invariante 8 hat sie dorthin gestellt, und § Frontload verlangt, dass
|
||
ein *Schritt* ohne Vorkontext entscheidbar ist, nicht dass jede Regel an ihm
|
||
wiederholt wird. Als Auflage bleibt das Billigere: ein Link sagt, was der
|
||
Schritt aus der Datei braucht.
|
||
|
||
Ob die Mechanik hier überhaupt beißt, wurde vor der Entscheidung im Eval-Aufbau
|
||
nachgesehen, und die Antwort ist: nicht messbar. Die L2-Trajectory-Regeln lesen
|
||
ausschließlich `wikitool.call`, `gate.*`, `publish.commit` und
|
||
`prompt.submitted` — keine Dateizugriffe eines Agenten; auf Claude Code ist
|
||
überhaupt kein Tool-Hook verdrahtet, ein `head -100` hinterlässt also keine
|
||
Spur. Die zweite Hälfte der Behauptung, was am Ende im Kontextfenster stand,
|
||
erzeugt konstruktionsbedingt nirgends ein Event. Ein kausaler A/B-Vergleich
|
||
bräuchte den L3-Runner, der entworfen und nicht gebaut ist. Das steht jetzt im
|
||
Contract: eine Festlegung über Reichweite, keine Messung. Der Nebenfund — L2
|
||
sieht auf dem primären Harness gar keine Tool-Calls — ist ein eigenes Issue
|
||
wert und nicht Teil dieser Änderung.
|
||
|
||
**Wieviel Begründung ein Schritt tragen darf, ist jetzt messbar (#79).** Der
|
||
alte Satz („Keep reasoning out of the body […] keep only enough reasoning to
|
||
decide edge cases") zog in zwei Richtungen, und die größte Instruction des
|
||
Repos lebte in der Lücke. Neu sind eine Keep/Cut-Tabelle und zwei Tests:
|
||
*Substitution* — die Passage streichen und den Schritt noch einmal lesen; rät
|
||
ein Agent ohne Vorkontext jetzt, war es eine Entscheidungshilfe und sie bleibt,
|
||
egal wie lang. *Once* — eine Entscheidungshilfe steht an dem Schritt, an dem die
|
||
Entscheidung fällt, und an genau einem solchen (Invariante 8). Danach gemessen
|
||
stand die `source_type`/Capture-Asymmetrie in `wiki-ingest` zweimal; sie steht
|
||
jetzt einmal, in Schritt 1, und Schritt 6 trägt die Anweisung plus Verweis.
|
||
Die beiden anderen in #79 genannten Stellen — Namenskollision in Schritt 1,
|
||
`## Not Extracted` in Schritt 6 — bestehen den Substitutionstest und bleiben.
|
||
214 → 211 Zeilen; die Kürzung ist nicht der Zweck, die Eindeutigkeit ist es.
|
||
|
||
**PATCH**, geprüft gegen den Drop-in-Test: eine Instanz kopiert
|
||
`instructions/` über sich, nichts wird umbenannt oder entfernt, kein Kommando,
|
||
kein Flag, kein maschinengelesenes Format, und der Rückweg funktioniert
|
||
genauso. Kein `--breaking`, kein Migrationsdokument.
|
||
|
||
Geändert: `instructions/CONTRACT.md` (§ Writing an instruction, drei neue
|
||
Unterabschnitte), `instructions/wiki-ingest/SKILL.md` (Schritte 1 und 6).
|
||
|
||
Schließt #71, #72 und #79.
|
||
|
||
### Vier Befunde in der Skill-Prosa, ein Publish
|
||
|
||
Der Rest der #65-Analyse, soweit er die fünf `SKILL.md` selbst betrifft. Vier
|
||
Issues, fünf Dateien, kein Codeanteil.
|
||
|
||
**Die Hard Rule von `wiki-status` war falsch (#70).** Sie sagte „read-only.
|
||
Never writes, scaffolds, or modifies any file" — und Schritt 2 ruft `lint`,
|
||
schreibt also einen Report, was Schritt 2 sogar selbst beschreibt. Ein Agent,
|
||
der die Regel wörtlich nimmt, kann den Skill nicht ausführen; einer, der ihn
|
||
ausführt, hat die stärkste Aussage des Dokuments gebrochen, bevor er Schritt 5
|
||
erreicht. Das ist die teurere Sorte Widerspruch, weil die Hard Rule genau die
|
||
Stelle ist, an der ein Konflikt entschieden wird. Sie lautet jetzt wie die von
|
||
`wiki-query` — read-only gegenüber Wiki-*Inhalt* — und benennt den einen Write
|
||
mitsamt Grund: `reports/` ist gitignored und trägt keine Wiki-Seite. Schritt 5
|
||
behauptet nicht mehr, es sei keine Datei geschrieben worden, sondern sagt, was
|
||
mit der geschriebenen *nicht* passiert (Semantic Review bleibt leer, nichts
|
||
wird ausgetragen — das ist `wiki-lint` Schritt 9). Der Decision Point „Never
|
||
publishes — nothing was written" trägt jetzt den wahren Grund: unter `kb/` hat
|
||
sich nichts geändert, und der Report kann gar nicht in einen Commit geraten.
|
||
|
||
**`wiki-ingest` und `wiki-lint` bekommen einen Abhak-Block (#74).** Anthropics
|
||
Skill-Doku empfiehlt für „particularly complex workflows" eine Checkliste, die
|
||
der Agent in die Antwort kopiert und mitführt. Zwölf Schritte fallen
|
||
unzweifelhaft darunter. Der Ausschlag gibt aber nicht die Länge, sondern was
|
||
still ausfällt: `## Not Extracted` in Schritt 6, die Coverage-Prüfung in
|
||
Schritt 10, die Lint-Kadenz in Schritt 12 — keiner davon erzeugt eine
|
||
Fehlermeldung, wenn er ausbleibt.
|
||
|
||
Die offene Frage des Issues — ob `wiki-lint` denselben Block bekommt — ist mit
|
||
ja beantwortet: neun Schritte, davon 3-6 reines Judgment, und ein Lauf, der
|
||
leise nur seine mechanische Hälfte gemacht hat, sieht aus wie ein
|
||
vollständiger. Damit haben zwei von fünf Skills einen Block und drei nicht, und
|
||
genau das wäre ohne festgeschriebenes Kriterium die nächste strukturelle
|
||
Ungleichheit im Sinne von #78. `instructions/CONTRACT.md` § Writing an
|
||
instruction trägt sie deshalb jetzt: ein Block, wenn **ein** Ablauf acht
|
||
Schritte oder mehr hat **und** darin still ausfallende Schritte stehen. Beide
|
||
Hälften nötig — ein langer Ablauf aus reinen Tool-Calls meldet seine Lücken
|
||
selbst, weil der nächste Call ohne den vorigen scheitert. Der Abschnitt nennt
|
||
die drei anderen Skills mit ihren Schrittzahlen, damit niemand aus Symmetrie
|
||
einen vierten Block nachrüstet.
|
||
|
||
**`wiki-query` prüfte nicht, bevor es filete (#75).** Die drei Kriterien
|
||
(Synthese über mehrere Seiten, etwas noch nicht Dokumentiertes, wird wieder
|
||
gefragt) standen im Filing-Schritt selbst, und der Skill darf mehrere Seiten
|
||
je Sitzung anlegen — es gab also keine Stelle, an der *jede* geplante Seite
|
||
einzeln gemessen wurde. Neuer Schritt 5 vor dem ersten `new`: Kandidaten
|
||
benennen, jeden für sich gegen alle drei halten, ein Stapel wird nie als Stapel
|
||
beurteilt. Wer durchfällt, wird nicht angelegt, sondern in der Antwort mit
|
||
einem Satz genannt — der Nutzer kann ihn trotzdem verlangen. Der bisherige
|
||
Filing-Schritt ist Schritt 6, `log append` Schritt 7, die Hard Rule zieht mit.
|
||
Der Mass-Update-Gate-Hinweis bleibt, sagt aber jetzt dazu, dass er keine
|
||
Ersatzprüfung ist: das Gate zählt Dateien und weiß nichts über Berechtigung,
|
||
und ein Stapel unter der Schwelle ist von ihm nicht freigegeben, nur nicht
|
||
angehalten worden. Dazu die `session-setup.md`-Zeile in derselben Form wie in
|
||
den drei anderen — Schritt 7 läuft *immer* und der Filing-Pfad zieht `new`,
|
||
`xref add` und die Rebuilds nach sich.
|
||
|
||
**Die Kommandolisten gingen mit den Schritten auseinander (#78).**
|
||
`cite add` fehlte in `wiki-ingest` und `wiki-manage`, obwohl beide es
|
||
ausdrücklich vorschreiben; `types describe` fehlte in `wiki-ingest`, wo
|
||
Schritt 6 die `source_type`-Werte daraus zieht; `xref add` fehlte in
|
||
`wiki-lint`, wo Schritt 1 das Umlabeln einer schwachen Kante darauf stützt;
|
||
`publish` fehlte in `wiki-lint` und `wiki-query`, wo je ein Decision Point es
|
||
beim Namen nennt. In die andere Richtung: `rm` stand in `wiki-lint`s Liste,
|
||
ohne dass ein Schritt es begründet — die gefährlichere Richtung der Drift, weil
|
||
`rm` Seiten löscht. Dazu `log status` (entscheidet den Trigger, gelaufen wird
|
||
es von `wiki-ingest`) und die nie benutzten Flags `lint --markdown` und
|
||
`lint --json`. Beide Streichungen stehen jetzt als **Deliberately absent** unter
|
||
der Liste, mit Grund — sonst trägt sie jemand aus Vollständigkeit wieder ein.
|
||
|
||
Nicht als Drift gezählt und bewusst gelistet geblieben: `publish`,
|
||
`log append`, `index rebuild` und `sources rebuild-index`, wo ein Skill sie
|
||
über `publish-cycle.md` delegiert. Ebenso `xref remove` in `wiki-manage`, das
|
||
zum Unlinking-Fall gehört, den der Skill als Ganzes an `page-lifecycle.md`
|
||
abgibt; auch das steht jetzt als Satz dort, nicht als stille Annahme.
|
||
|
||
Der zweite Teil von #78 ist die Symmetrie: `wiki-manage` und `wiki-status`
|
||
tragen jetzt einen Beispielblock wie die drei anderen. Fünf Skills mit
|
||
demselben Aufbau sollten denselben Aufbau haben — ein fehlender Abschnitt liest
|
||
sich sonst als Aussage („hier gibt es keine typischen Fälle"), die niemand
|
||
gemeint hat.
|
||
|
||
Die offene Frage aus #78 — ob `instructions verify` diesen Abgleich künftig
|
||
selbst macht — ist mit ja beantwortet und als #83 ausgelagert. Trivial ist er
|
||
nicht: die drei Ausnahmen oben (Delegation über `publish-cycle.md`, benannte
|
||
Delegation an eine andere Instruction, „Deliberately absent") müsste ein Prüfer
|
||
alle kennen, sonst meldet er bei jedem Lauf dieselben Stellen. Nur die dritte
|
||
hat heute einen maschinenlesbaren Anker — den Absatz, den dieser Eintrag oben
|
||
eingeführt hat.
|
||
|
||
**PATCH**, geprüft gegen den Drop-in-Test: eine Instanz kopiert `instructions/`
|
||
über sich, nichts wird umbenannt oder entfernt, kein Kommando, kein Flag, kein
|
||
maschinengelesenes Format, und der Rückweg funktioniert genauso. Kein
|
||
`--breaking`, kein Migrationsdokument.
|
||
|
||
Geändert: `instructions/wiki-status/SKILL.md`, `instructions/wiki-query/SKILL.md`,
|
||
`instructions/wiki-ingest/SKILL.md`, `instructions/wiki-lint/SKILL.md`,
|
||
`instructions/wiki-manage/SKILL.md`, `instructions/CONTRACT.md` (§ Writing an
|
||
instruction, neuer Unterabschnitt „When a skill carries a copy-in checklist").
|
||
`wiki-ingest` bleibt mit 233 Zeilen unter Anthropics 500er-Schwelle.
|
||
|
||
Schließt #70, #74, #75 und #78.
|
||
|
||
**Issue-Nummern in ausgelieferter Doku** (#77): `dist export` lieferte
|
||
Dateien aus, die im Fließtext auf Issue-Nummern dieses Trackers verwiesen —
|
||
„flat since Gitea #67", „`new source` refuses without it (Gitea #66)". In einer
|
||
verteilten Instanz zeigt das auf nichts. Der Leser kann den Verweis weder
|
||
auflösen noch als unauflösbar erkennen, und eine Regel sieht damit so aus, als
|
||
stütze sie sich auf einen Beleg, den niemand beibringen kann. Das Board liegt
|
||
im Ursprungs-Repo, und `instructions/dev/issue-tracking.md` — die einzige Datei,
|
||
die das überhaupt sagt — wird von `dist export` mit dem Rest von
|
||
`instructions/dev/` weggeschnitten. Gegenprobe zum eigenen Anspruch aus
|
||
`instructions/CONTRACT.md` § „Writing an instruction": „self-contained enough
|
||
for an agent with no prior context".
|
||
|
||
Gemessen statt geschätzt: ein Export in ein leeres Verzeichnis, `grep -rn
|
||
'#[0-9]'`, ergab **43 Treffer in 16 Dateien** außerhalb von `tools/**/*.py` —
|
||
`raw/CONTRACT.md` allein acht. Das Issue hatte zehn gelistet.
|
||
|
||
Aufgelöst wurde nicht durch eine Markierung, sondern durch Umformulierung:
|
||
**die Nummer fällt weg, die Datierung geht in Worte.** Aus „flat since Gitea
|
||
#67" wird „flat since the addressing scheme dropped type directories", aus
|
||
„**Pre-#67 files are not moved**" wird „**Files promoted under the old type
|
||
directories are not moved**". Der Satz trägt sich damit selbst — es gibt keine
|
||
repoweite Notation zu definieren und an genau einer Stelle zu halten
|
||
(Invariante 8), und kein Leser von `README.md` muss `AGENTS.md` geladen haben,
|
||
um sie aufzulösen. Rückverfolgbar bleibt es hier über `git blame` → Commit-
|
||
Message; die tragen die Nummern ohnehin.
|
||
|
||
Zwei Stellen, an denen der Zeiger *der ganze Wert* des Satzes war und in Worten
|
||
nichts übrig geblieben wäre, stehen jetzt in einem
|
||
`<!-- dist:strip-start/end -->`-Block: in `instructions/CONTRACT.md` (was ein
|
||
Test der Kontextfenster-Behauptung kosten würde) und in `EVALS.md` (wo die
|
||
Coverage-Lücken geschlossen werden). Im Dev-Repo sichtbar, im Export weg — die
|
||
bestehende Konvention aus `instructions/CONTRACT.md` § `instructions/dev/`, hier
|
||
zum zweiten Mal angewandt statt neu erfunden.
|
||
|
||
**`docs verify` prüft es jetzt** — die offene Frage des Issues, mit Ja
|
||
beantwortet. `check_no_issue_references` liest nicht den Arbeitsbaum, sondern
|
||
den Text, den `dist_cmd.build_plan()` schreiben würde: dort leben `ROOT_FILES`,
|
||
der `instructions/dev/`-Ausschluss und das `.template`-Rekeying schon, und der
|
||
Text hat seine Marker-Blöcke bereits verloren. Deshalb ist ein Strip-Block
|
||
automatisch exemptiert, ohne dass der Check ihn kennen müsste.
|
||
|
||
Der Einwand aus `instructions/dev/issue-tracking.md` § „What no tool checks" —
|
||
`wikitool` soll den Tracker nicht kennen — trägt hier nicht, und das ist die
|
||
Grenze, die der Abschnitt jetzt selbst zieht: `re.compile(r"#\d+")` hat keinen
|
||
Client, keine URL und keinen Begriff vom Zustand eines Issues. Der Check sieht
|
||
eine Eigenschaft des *Dokuments*, nicht des Boards. Gemessen: null False
|
||
Positives über den gesamten Export, weil Markdown-Anker aus Wortzeichen
|
||
bestehen (`](#gates)` matcht nicht). Der erste Fund war prompt der Satz, den
|
||
diese Sitzung selbst in `tools/CONTRACT.md` geschrieben hatte, um die Regel zu
|
||
erklären.
|
||
|
||
**`tools/**/*.py` bleibt bewusst außen vor**, mit ~90 Treffern in Docstrings und
|
||
Kommentaren. Ein Code-Kommentar adressiert, wer die Zeile editiert, und das
|
||
passiert ausschließlich im Ursprungs-Repo: `dist export` schneidet den
|
||
`stack-dev`-Skill mit `instructions/dev/` weg. Ein ausgeliefertes `tools/` ist
|
||
Laufzeit-Maschinerie, keine Lektüre. `.gitignore` und `tools/.coveragerc` sind
|
||
aus demselben Grund nicht im Check — von Hand mitgezogen wurden sie trotzdem,
|
||
sodass der Export heute in *keiner* Datei außerhalb `.py` eine Nummer trägt.
|
||
|
||
**MINOR**, geprüft gegen den Drop-in-Test: kein Kommando, kein Flag, kein
|
||
Dateiformat, keine Umbenennung; der Rückweg funktioniert unverändert, die alte
|
||
Version führt den Check schlicht nicht aus. Kein `--breaking`, kein
|
||
Migrationsdokument. Eine Konsequenz ist zu kennen: der Check liest auch die
|
||
instanzeigenen `kb/CONVENTIONS.md` und `kb/<collection>/COLLECTION.md`, weil ein
|
||
Export sie als `.template` mitnimmt. Eine Instanz, die dort ihre eigene
|
||
Ticket-Nummer zitiert, bekommt beim nächsten `docs verify` ein Finding. Das ist
|
||
kein Fehlalarm — ein Export dieser Instanz würde den Verweis weitergeben —
|
||
aber es ist neu.
|
||
|
||
Geändert: `tools/chemenu/commands/docs_verify.py` (neuer Check plus
|
||
`shipped_prose()`), `tools/chemenu/tests/test_docs_verify.py` (sechs Tests:
|
||
sauberer Baum, präparierte Datei, Anker-Nicht-Treffer, `.py` außerhalb des
|
||
Scans, Strip-Block unsichtbar, `verify` bricht ab), `tools/CONTRACT.md`
|
||
(Kommandotabelle und Fehlerkontrakt-Zeile), `tools/README.md`,
|
||
`instructions/dev/issue-tracking.md` (neuer § Citing an issue in the repo, und
|
||
§ What no tool checks zieht die Grenze zwischen „was dieses Repo über den
|
||
Tracker schreibt" und „dem Tracker selbst"), sowie die 16 Doku-Dateien:
|
||
`raw/CONTRACT.md`, `kb/CONTRACT.md`, `tools/CONTRACT.md`, `types/type-spec.md`,
|
||
`types/source.schema.yaml`, `kb/sources/COLLECTION.md`,
|
||
`kb/concepts/COLLECTION.md`, `instructions/wiki-ingest/SKILL.md`,
|
||
`instructions/evolve-subtypes.md`, `instructions/bootstrap.md`,
|
||
`instructions/kb-profiles.md`, `instructions/mcp-read-server.md`,
|
||
`instructions/CONTRACT.md`, `README.md`, `EVALS.md`, `INSTALL.md`,
|
||
`docs/pipeline-rationale.md`, `.gitignore`, `tools/.coveragerc`.
|
||
|
||
Schließt #77.
|
||
|
||
**Warum das eine MAJOR ist, obwohl `kb/` unberührt bleibt** (#73, #76): Anthropics
|
||
Skill-Authoring-Doku verlangt für Referenzdateien über 100 Zeilen ein
|
||
Inhaltsverzeichnis, damit ein Agent, der eine solche Datei nur mit `head -100`
|
||
anliest, trotzdem die volle Abschnittsübersicht sieht — dieselbe Vorschau-Mechanik,
|
||
die #72 schon für die Referenztiefe als real anerkannt hat. Ein von Hand
|
||
gepflegtes Inhaltsverzeichnis wäre die nächste Drift-Quelle; also ist es jetzt eine
|
||
dritte generierte Region neben `xref`s und `cite`s (`<!-- wikitool:toc -->` ...
|
||
`<!-- /wikitool:toc -->`, `tools/chemenu/toc.py`), erzeugt und geprüft wie jede
|
||
andere abgeleitete Kopie. `wikitool docs toc [--apply]` schreibt sie; `docs verify`
|
||
prüft jetzt, dass sie auf jeder Datei aktuell ist, die `AGENTS.md`, ein
|
||
Stage-/Collection-Contract oder die flache `instructions/**.md`-Form abdeckt (25
|
||
Dateien in diesem Repo, `instructions/dev/` eingeschlossen — strukturell dieselbe
|
||
Dateiform, nur von `dist export` ausgenommen). Der Umfang ist berechnet, nie eine
|
||
Handliste: er folgt AGENTS.md § File naming, nicht einer Link-Traversierung ab den
|
||
fünf Content-Skills, und schließt `types/<name>.md`-Einzelspecs bewusst aus — die
|
||
laufen über `wikitool types describe`, das den Inhalt neu rendert statt die Datei
|
||
roh auszugeben, sodass die Vorschau-Mechanik dort gar nicht greift.
|
||
|
||
Das ist grenzüberschreitend, weil `docs verify` damit eine neue Pflichtprüfung
|
||
über *bestehenden* Inhalt bekommt: eine Instanz mit einer eigenen
|
||
`instructions/*.md`-Datei über 100 Zeilen, an der nichts geändert wurde, sieht
|
||
`docs verify` nach reinem Tool-Update neu fehlschlagen, bis einmalig
|
||
`wikitool docs toc --apply` läuft und der Diff committet wird — derselbe
|
||
Bruchtyp wie ein verschärftes Type-Spec-Pflichtfeld. Keine Migration nötig, weil
|
||
kein `kb/`-Inhalt betroffen ist; der einmalige `docs toc --apply`-Lauf ist der
|
||
volle Reparaturweg.
|
||
|
||
Zwei Nebenfunde beim Bauen der TOC-Regel, beide vor dem Bump behoben, weil sie
|
||
sonst denselben Bump falsch aussehen ließen: `docs verify`s
|
||
`ISSUE_REFERENCE_RE` (`#\d+`) hielt numerierte-Schritt-Anker wie
|
||
`#2-fix-the-fidelity-before-writing-a-word` für Issue-Zitate — die Regel nahm
|
||
bisher an, dass ein Anker immer mit einem Buchstaben beginnt, was für
|
||
nummerierte Überschriften (`instructions/capture-session.md`) nicht mehr gilt;
|
||
behoben durch einen Lookbehind, der genau die `](#...`-Linkfragment-Form
|
||
ausschließt, ohne ein echtes `(#66)`-Zitat zu übersehen. Und
|
||
`instructions_cmd.dev_only_forbidden_references` prüfte mit blankem
|
||
`name in text`: ein TOC-Anker wie `#where-stack-development-happens`
|
||
(`instructions/private-instance.md`) enthält „stack-dev" als reine Teilzeichenkette,
|
||
ohne den Skill zu meinen — behoben durch eine wortgrenzengebundene
|
||
Regex-Suche.
|
||
|
||
Geändert: `tools/chemenu/toc.py` (neu), `tools/chemenu/commands/docs_verify.py`
|
||
(`check_toc_regions`, `docs toc`-Kommando, `ISSUE_REFERENCE_RE`-Lookbehind),
|
||
`tools/chemenu/commands/instructions_cmd.py`
|
||
(`dev_only_forbidden_references` wortgrenzengebunden), `tools/CONTRACT.md`
|
||
(`docs toc`-Zeile), `tools/chemenu/tests/test_toc.py` (neu, 15 Tests),
|
||
`tools/chemenu/tests/test_docs_verify.py` (drei neue Tests: nummerierter Anker,
|
||
geklammertes echtes Zitat, TOC-Region auf dem realen Baum),
|
||
`tools/chemenu/tests/test_instructions_cmd.py` (ein neuer Test für die
|
||
Teilzeichenketten-Kollision), sowie die 25 Referenzdateien, die jetzt eine
|
||
TOC-Region tragen: `AGENTS.md`, `kb/CONTRACT.md`, `kb/CONVENTIONS.md`,
|
||
`kb/concepts/COLLECTION.md`, `raw/CONTRACT.md`, `tools/CONTRACT.md`,
|
||
`types/type-spec.md`, `instructions/CONTRACT.md`, `instructions/gates.md`,
|
||
`instructions/setup-instance.md`, `instructions/private-instance.md`,
|
||
`instructions/link-taxonomy.md`, `instructions/kb-profiles.md`,
|
||
`instructions/ingest-large-tree.md`, `instructions/capture-session.md`,
|
||
`instructions/claude-code-model-selection.md`, `instructions/german-terminology.md`,
|
||
`instructions/evolve-subtypes.md`, `instructions/mcp-read-server.md`,
|
||
`instructions/migrate-corpus.md`, `instructions/migrations/3.0.0-authoring-conventions.md`,
|
||
`instructions/migrations/4.0.0-link-taxonomy.md`, `instructions/dev/issue-tracking.md`,
|
||
`instructions/dev/testing-conventions.md`, `instructions/dev/version-parts.md`.
|
||
|
||
Zusätzlich, unabhängig davon (#76): `instructions/session-setup.md` § Scope und
|
||
`instructions/gates.md` behaupteten, die Budget-Ausnahme richte sich danach, ob
|
||
ein Kommando das Wiki verändert. Tatsächlich zählt `run_budget.py` eine feste
|
||
Allowlist (`SKIP_COMMANDS`/`SKIP_COMMAND_PATHS`) — `lint` schreibt nur ins
|
||
gitignorte `reports/`, sieht also lesend aus, steht aber nicht auf der Liste und
|
||
zählt wie jedes mutierende Kommando. Beide Dateien verweisen jetzt auf die Liste
|
||
in `tools/CONTRACT.md`, statt sie mit einer falschen Faustregel zu umschreiben.
|
||
Kein Versionsbezug — reine Prosa-Korrektur, im selben Bump mitgeführt.
|
||
|
||
Nachgezogen in `-beta.3`: `dist export` erzeugt die TOC-Region jetzt **nach**
|
||
dem Marker-Strip neu. Ein `<!-- dist:strip-start/end -->`-Block kann eine ganze
|
||
Sektion umschließen — der in `AGENTS.md` umschließt `## Developing this stack` —,
|
||
sodass die ausgelieferte Datei eine Überschrift weniger hat, als das im
|
||
Arbeitsbaum erzeugte Inhaltsverzeichnis auflistet. Die frische Instanz wäre
|
||
damit beim allerersten `docs verify` über eine Datei gefallen, die niemand
|
||
angefasst hat. Gefunden hat das die CI im Export-Replay („The distribution works
|
||
as a fresh instance"), nicht `pytest` und nicht `docs verify` im Arbeitsbaum —
|
||
beide sehen den gestrippten Text nie. Der Regressionstest sitzt jetzt in
|
||
`test_dist_cmd.py`.
|
||
|
||
Nachgezogen in `-beta.2`, weil `docs verify` die eigene Dokumentationstreue nur
|
||
für die *Existenz* einer Kommandozeile prüft, nicht für deren Inhalt: die
|
||
`docs verify`-Zeile in `tools/CONTRACT.md` nennt jetzt die TOC-Prüfung, die
|
||
Fehlerkontrakt-Tabelle bekommt die fehlende `docs toc`-Zeile (Schritt 3 in
|
||
`tools/README.md` § Adding a command verlangt beide Tabellen, geprüft wird nur
|
||
eine), und `tools/README.md` § Adding a command Schritt 5 nannte als
|
||
`--major`-Kriterium „wenn bestehender Inhalt migriert werden muss" — was
|
||
`instructions/dev/version-parts.md` ausdrücklich verneint und was dieser Bump
|
||
selbst widerlegt: grenzüberschreitend mit `--no-migration`.
|
||
|
||
Schließt #73, #76.
|
||
|
||
**Konfidenz-Mechanismus ersatzlos entfernt (schließt #60).** `confidence`,
|
||
`confidence_base`, der Zeit-Decay und die Konfidenz-Rubrik sind aus Schema,
|
||
Kommandos (`confidence decay`/`init-base`, `touch --confidence-base`), Lint
|
||
(`confidence_exceeds_source_standing`), Suche (`--sort -confidence`,
|
||
Konfidenzspalte) und jeder Doku-Stelle entfernt, die sie erwähnte.
|
||
|
||
Der Mechanismus wurde gemessen, nicht nur für unschön befunden: 43 der 152
|
||
betroffenen Seiten trugen nie mehr als den Schema-Vorgabewert 0.5, der Decay
|
||
hat seit seiner Einführung keinen einzigen Wert bewegt (`confidence_decay.py`
|
||
übersprang `concept_type: decision`, `touch --confidence-base` zog das
|
||
abgeleitete Feld nie mit), und kein Konsument im Stack hing außer über
|
||
`corpus_diff.STRUCTURAL_FIELDS` — eine Abhängigkeit von der Existenz des
|
||
Feldes, nicht von seinem Wert — überhaupt an ihm. Vier Entwürfe für eine
|
||
Reparatur (Rubrik nachjustieren, Quellenautorität anheben, aus
|
||
`authority × fidelity` berechnen, zwei Schubladen für Reifegrad und
|
||
Volatilität) scheiterten an denselben Messwerten. Die vollständige Studienlage
|
||
steht in #85, wo das Thema als zurückgestellt geführt wird, nicht als
|
||
verworfen.
|
||
|
||
An die Stelle tritt nichts Neues: `kb/CONVENTIONS.md` bekommt eine
|
||
Prosa-Hedging-Regel — nach Quellenlage hedgen statt nach Schwellenwert, siehe
|
||
`kb/CONVENTIONS.md § Hedging` — und die Arbeitsliste ersetzt
|
||
`--field 'confidence<0.6'` durch Prädikate auf tatsächlich aufgezeichneten
|
||
Feldern (`--field '!sources'`, `--field provenance=general`). Mit dem Feld
|
||
stirbt auch `confidence_exceeds_source_standing`, der einzige automatische
|
||
Abgleich zwischen einer Seite und der Standing ihrer Quellen; ein Ersatz ohne
|
||
Zahl ist als Kandidat in #85 vorgemerkt, aber bewusst nicht Teil dieses Pakets.
|
||
|
||
`version bump` bekommt dabei ein neues Flag, `--migration-required`: der
|
||
laufende Kandidat hatte in einem früheren Bump `--no-migration` erklärt, und
|
||
dieses Paket macht die Erklärung falsch. Die `**Migration:** none
|
||
required`-Zeile ist maschinengeschrieben (Invariante 1 verbietet den
|
||
Handgriff), und bislang gab es keinen Weg, sie zurückzunehmen, sobald ein
|
||
späterer Bump doch eine Migration braucht. Das Flag entfernt die Zeile
|
||
stattdessen und verlangt ein Migrationsdokument, das die neue Basisversion
|
||
referenziert, bevor es das tut.
|
||
|
||
Was das für eine bestehende Instanz bricht, steht in der `**Breaking
|
||
Change:**`-Zeile oben. `instructions/migrations/5.0.0-confidence-removal.md`
|
||
ist der mechanische Strip der zwei Felder über den betroffenen Korpus,
|
||
mitsamt der Invariante, die ein automatisierter Lauf einhalten muss (kein
|
||
`modified:`-Bump, byte-identischer Body, unveränderte Referenzarrays).
|
||
|
||
Nachgezogen in `-beta.6`: `instructions/dev/version-parts.md` Schritt 6
|
||
beschreibt den Rücknahmepfad jetzt selbst. `tools/CONTRACT.md` führte das neue
|
||
Flag bereits, aber die Instruction, die eine Sitzung *vor* einem Bump liest,
|
||
kannte den Fall nicht — dieselbe Sitzung ist genau darüber gestolpert. Der
|
||
Abschnitt nennt auch, warum ihn nichts meldet: die Zeile ist
|
||
maschinengeschrieben, `docs verify` genügt ihr bloßes Vorhandensein, und die
|
||
Eskalationsprüfungen laufen nur auf dem Bump, der die Grenze *zuerst*
|
||
überschreitet.
|
||
|
||
Nachgezogen in `-beta.7`: `instructions/wiki-status/SKILL.md` trägt jetzt den
|
||
`session-setup.md`-Verweis, den die anderen vier Content-Skills längst haben.
|
||
Der Skill ruft in Schritt 2 `lint` auf, und `lint` steht nicht auf der
|
||
Ausnahme-Allowlist in `run_budget.py` — er zählt wie jedes mutierende
|
||
Kommando. Ohne exportierte `WIKITOOL_SESSION_ID` fällt die Zählung auf
|
||
`getppid()` zurück, die Sitzung erbt also den Stand irgendeiner fremden Shell.
|
||
Bis `-beta.6` war das Fehlen des Verweises durch die falsche Regel gedeckt, die
|
||
`session-setup.md` § Scope selbst aufstellte („verändert das Wiki"); seit sie
|
||
die tatsächliche Allowlist nennt, ist es schlicht ein Loch.
|
||
|
||
Die Alternative — `lint` durch etwas Befreites ersetzen und `wiki-status`
|
||
wirklich budgetfrei machen — scheidet an der Sache aus: der Skill liest aus dem
|
||
Report die Graph-Auswertung (Broken Links, Orphans, Most-Linked Pages,
|
||
uncovered raw files), und kein befreites Kommando liefert die. `doctor` prüft
|
||
die Installation, nicht den Korpus; `search` ist Retrieval. Ein `wiki-status`
|
||
ohne `lint` wäre kein leichterer Skill, sondern ein leerer.
|
||
|
||
Damit gilt über alle fünf Content-Skills dieselbe Aussage: ein Skill verlinkt
|
||
`session-setup.md` genau dann, wenn er mindestens ein nicht-befreites
|
||
`wikitool`-Kommando aufruft. Geprüft wird sie nicht — `instructions verify`
|
||
kennt weder die Kommandolisten der Skills noch die Allowlist. Das wäre ein
|
||
eigener Schnitt.
|
||
|
||
Nachgezogen in `-beta.8`: `CLAUDE.md` importierte bislang `USER.md`, `SOUL.md`,
|
||
`ENVIRONMENT.md` und `instructions/claude-code-model-selection.md` zusätzlich
|
||
zu `AGENTS.md` — eine Harness-Drift, denn dieselben drei
|
||
Personalisierungsdateien werden auf den anderen drei Harnesses (Codex CLI,
|
||
Copilot, Vibe) allein durch `AGENTS.md`s eigene Anweisung gelesen, nie
|
||
injiziert. `CLAUDE.md` importiert jetzt nur noch `AGENTS.md`; die Bedingung
|
||
„if the runtime has not already injected them" in `AGENTS.md` §
|
||
Personalization entfällt, weil kein Runtime mehr injiziert.
|
||
|
||
`instructions/claude-code-model-selection.md` ist entfernt und als
|
||
`docs/model-and-effort-selection.md` neu geschrieben, in Empfehlungsstimme
|
||
statt als Instruktion: die Datei beschrieb überwiegend Handlungen, die eine
|
||
Sitzung nicht selbst ausführen kann (das eigene Modell, `/code-review`-Stufen),
|
||
und wurde im ganzen Baum nur von den beiden dev-only Skills `stack-dev` und
|
||
`stack-close` referenziert, deren Links jetzt dorthin zeigen. **Eine
|
||
bestehende Instanz behält die entfernte Datei als Überbleibsel**, bis sie
|
||
`wikitool dist upgrade --prune` laufen lässt oder die Datei von Hand löscht —
|
||
`instructions verify` meldet sie sonst neu als verwaist.
|
||
|
||
`AGENTS.md` §§ Personalization, Environment und File naming sind an den
|
||
Stellen gekürzt, die eine zweite Kopie einer Regel waren, die
|
||
`docs/ownership-and-templates.md` oder eine Invariante schon trägt; § File
|
||
naming verlinkt jetzt alle vier `docs/`-Seiten namentlich, was vorher
|
||
nirgends geschah. `USER.md` und `SOUL.md` verlieren an derselben Stelle
|
||
Rahmen- bzw. Herkunftsprosa, die `USER.md.template` bzw. ein Kommentar in
|
||
`SOUL.md` selbst schon trägt.
|
||
|
||
**Telemetrie-Default nach Installationsform** (#55): `chemenu.telemetry.writer.enabled()` war
|
||
eine Zeile - immer an, `WIKI_TRACE=0` das einzige Opt-out. Richtig für dieses Repo, dessen
|
||
Traces das Messinstrument sind, mit dem der Stack sich selbst bewertet, aber die falsche
|
||
Voreinstellung für eine ausgelieferte Instanz: dort hat niemand Telemetrie bestellt, und
|
||
niemand liest `EVALS.md`, bevor die erste Datei geschrieben ist. Dazu kam eine zweite Lücke:
|
||
keine Mengenbegrenzung irgendeiner Art - `reports/telemetry/<session>/trace.jsonl` wächst,
|
||
solange die Instanz läuft, und nichts räumt je etwas weg.
|
||
|
||
`tools/chemenu/telemetry/policy.py` (neu) löst jetzt beides an einer Stelle, gekeyt auf den
|
||
aufgelösten Root, damit `wikitool doctor`, der Writer und der MCP-Server-Start-Guard dieselbe
|
||
Antwort für denselben Checkout geben. Ein Git-Clone dieses Repos bleibt beim alten Verhalten
|
||
(an, `WIKI_TRACE=0` schaltet ab); eine per `dist export` ausgelieferte Instanz startet ab jetzt
|
||
mit Telemetrie **aus** - erkannt an der ohnehin vorhandenen, maschinengeschriebenen
|
||
`.wikitool-release.json` (Invariante 1). Wer sie dort anschalten will, legt eine
|
||
`.wikitool-telemetry.json` an (pro Checkout, gitignored, kein `.template` - wie
|
||
`.wikitool-remotes.json`); `WIKI_TRACE` überschreibt weiterhin beide Richtungen und schlägt die
|
||
Datei.
|
||
|
||
Zwei unabhängige, fail-silent durchgesetzte Mengendeckel greifen in beiden Installationsformen:
|
||
ein Byte-Deckel pro Session-Trace (Default 5 MiB, ein `stat` vor jedem Append) und eine
|
||
Retention über die Anzahl der Session-Verzeichnisse (Default 250). Die Retention reserviert den
|
||
Platz der gerade entstehenden Session, statt sie mitzuzählen - sonst pendelt der Bestand
|
||
dauerhaft bei `keep+1` statt bei `keep`, weil jeder Lauf immer nur das räumt, was der *vorige*
|
||
Lauf über dem Limit gelassen hat. Am Byte-Limit schreibt ein weiterer Aufruf nichts mehr außer
|
||
einem einmaligen `telemetry.limit`-Event, per Exclusive-Create auf eine `.limit`-Sentinel-Datei
|
||
ausgelost - derselbe Ein-Schreiber-Trick wie beim `session.start`-Header, für den Fall, dass
|
||
mehrere Prozesse gleichzeitig auf denselben Trace schreiben. Eine Retention-Runde löscht
|
||
ausschließlich `trace.jsonl`/`.limit` der überzähligen Verzeichnisse und `rmdir`t nur, wenn
|
||
danach leer - nie `rmtree`, aus demselben Grund wie bei `upstream merge` (#30):
|
||
`reports/telemetry/` hält lokale, nicht rekonstruierbare Daten, die `eval score` liest.
|
||
|
||
`wikitool doctor` bekommt einen neuen `telemetry`-Check (an/aus, warum, Menge gegen beide
|
||
Deckel, nie `FAIL`, wie `publish-remotes` und `environment`). Der MCP-Server-Start-Guard
|
||
(`check_trace_destination`) las `WIKI_TRACE` bisher selbst statt den Writer zu fragen - eine
|
||
zweite Kopie derselben Regel, die Invariante 8 verletzte, bevor diese Änderung sie schließt; er
|
||
fragt jetzt dieselbe Policy. `instructions/setup-instance.md` bekommt einen neuen
|
||
Entscheidungspunkt (Schritt 10).
|
||
|
||
**MINOR, kein neuer Boundary-Crossing:** additiv in beide Richtungen - eine bestehende Instanz
|
||
kopiert die neue Maschinerie über sich und hört still auf zu schreiben, ohne Hand-Arbeit oder
|
||
Migration; die alte Version zurücklegen stellt den alten Default wieder her, weil sie die neue
|
||
Datei und die neuen Variablen schlicht ignoriert. Der Kandidat trägt seine `--breaking`-Zeile
|
||
bereits aus einem früheren Bump (Confidence-Entfernung); diese Änderung fügt keine neue hinzu.
|
||
|
||
Geändert: `tools/chemenu/telemetry/policy.py` (neu), `tools/chemenu/telemetry/writer.py`,
|
||
`tools/chemenu/telemetry/schema.py`, `tools/chemenu/commands/doctor.py`,
|
||
`tools/chemenu/mcp/server.py`, `tools/chemenu/config.py`, `tools/chemenu/version.py`,
|
||
`.gitignore`, `tools/chemenu/tests/conftest.py` und die Telemetrie-/Doctor-/MCP-Server-Tests,
|
||
`.gitea/workflows/ci.yml`, `EVALS.md`, `INSTALL.md`, `INSTALL-MCP.md`, `reports/CONTRACT.md`,
|
||
`tools/CONTRACT.md`, `instructions/setup-instance.md`. Schließt #55.
|
||
|
||
---
|
||
|
||
## 4.7.4 - 2026-09-04 - bootstrap.md nennt den session-id-WARN nach frischem Bootstrap explizit als erwartet
|
||
|
||
**Author:** Torben Nehmer
|
||
|
||
<!-- wikitool:bumps -->
|
||
- bootstrap.md nennt den session-id-WARN nach frischem Bootstrap explizit als erwartet
|
||
<!-- /wikitool:bumps -->
|
||
|
||
Nach einem frischen Clone plus `instructions/bootstrap.md` zeigte `tools/wikitool doctor`
|
||
durchgehend `OK`, außer `session-id: WARN` - ohne Einordnung, ob das ein Bootstrap-Defekt ist.
|
||
`WIKITOOL_SESSION_ID` wird laut `instructions/session-setup.md` bewusst pro Arbeitssitzung
|
||
gesetzt, nicht pro Clone; ein Export in `bootstrap.md` selbst würde nur für den Bootstrap-Lauf
|
||
gelten, nicht für die tatsächliche Arbeitssitzung danach (die nach dem Neustart in Schritt 6 in
|
||
einer neuen Shell beginnt). `bootstrap.md` bekommt deshalb einen neuen Schritt 7, der den WARN
|
||
als erwarteten Zustand benennt - analog zum bereits dokumentierten `personalization: FAIL` in
|
||
Schritt 4 - und auf `session-setup.md` verweist, statt den Export in Bootstrap nachzubauen.
|
||
Schließt #54.
|
||
|
||
---
|
||
|
||
## 4.7.3 - 2026-09-04 - eval: gate-not-self-opened prueft REMOVED_FLAGS gegen das eigene Kommando
|
||
|
||
**Author:** Torben Nehmer
|
||
|
||
Die Trajektorien-Regel `gate-not-self-opened` (`tools/chemenu/evals/trajectory.py`) hat jedes
|
||
Argument jedes `wikitool.call` gegen `REMOVED_FLAGS = {"--yes": "publish", "-y": "publish"}`
|
||
geprüft, ohne je das eigene `command`-Feld des Aufrufs gegenzulesen. `--yes`/`-y` sind nur auf
|
||
`publish` entfernt worden - auf `rm --page <Titel> --yes` sind sie ein gültiger, dokumentierter
|
||
Flag. Ergebnis: jeder `rm --yes`-Aufruf wurde als Invarianten-Verstoß gemeldet ("an agent
|
||
inventing a flag the tool never accepts"), obwohl das Tool ihn akzeptiert hatte.
|
||
|
||
In den vorhandenen Telemetrie-Traces unter `reports/telemetry/` betraf das 111 `rm`-Aufrufe
|
||
über 8 Sessions, davon 27 allein in `publish-cleanup/u3` - jede davon fälschlich `FAILED`
|
||
gescort. Kein bestehender Test hätte das gefangen: `tools/chemenu/tests/test_evals.py` prüfte
|
||
`REMOVED_FLAGS` ausschließlich über `publish --yes`, nie über ein anderes Kommando.
|
||
|
||
Fix: die Bedingung liest jetzt `attrs.get("command") == REMOVED_FLAGS[arg]` mit. Neuer
|
||
Regressionstest `test_yes_on_a_command_that_still_has_it_is_not_a_finding` deckt genau den
|
||
`rm --yes`-Fall ab und schlägt gegen den unfixed Code nachweislich fehl.
|
||
|
||
Keine Verhaltensänderung an `wikitool` selbst - ausschließlich an der Scoring-Logik unter
|
||
`tools/chemenu/evals/`.
|
||
|
||
<!-- wikitool:bumps -->
|
||
- eval: gate-not-self-opened prueft REMOVED_FLAGS gegen das eigene Kommando
|
||
<!-- /wikitool:bumps -->
|
||
|
||
---
|
||
|
||
## 4.7.2 - 2026-09-04 - Coverage-Untergrenze bei 85 %, gegen beobachtete 87,0 %
|
||
|
||
**Author:** Torben Nehmer
|
||
|
||
<!-- wikitool:bumps -->
|
||
- Coverage-Untergrenze 85 % in tools/.coveragerc
|
||
<!-- /wikitool:bumps -->
|
||
|
||
Die Suite hat jetzt einen Boden: `fail_under = 85` in `tools/.coveragerc`, gemessen gegen 87,0 %
|
||
(CI-Lauf 163, 6498 Statements, 975 Tests). Damit ist Gitea #10 geschlossen — das Issue, das die
|
||
Messung eingerichtet und die Schwelle danach **absichtlich** zurückgehalten hat, bis die Zahl
|
||
beobachtet war.
|
||
|
||
Die Beobachtung ist der eigentliche Inhalt dieses Bumps. Zwischen der ersten Messung (86,9 % von
|
||
5105 Statements, 730 Tests, Lauf 87, Stack 1.8.1) und heute ist der gemessene Code um ein Viertel
|
||
gewachsen und die Suite um ein Drittel, über 38 grüne Läufe — und die Quote hat sich um einen
|
||
Zehntelpunkt bewegt. Eine Untergrenze, die auf dieser Beobachtung steht, ist etwas anderes als
|
||
eine gegriffene Zahl.
|
||
|
||
**85 und nicht 87, und das ist keine Bequemlichkeit.** Der Coverage-Bericht unterscheidet drei
|
||
Sorten ungedeckter Zeilen, und nur eine davon bedeutet Arbeit (`EVALS.md` § „How much of the
|
||
stack the suite reaches"). Ein neuer dünner Typer-Wrapper senkt den Gesamtwert, ohne dass
|
||
irgendetwas schlechter geworden wäre — seine Logik liegt daneben und ist dort getestet. Eine
|
||
Schwelle auf dem gemessenen Wert würde genau an diesem Commit rot, und eine Schwelle, die aus
|
||
einem Nicht-Grund rot wird, wird gesenkt statt verdient. Das ist die Fehlerweise, die #10
|
||
verhindern wollte, nur von der anderen Seite. Die zwei Punkte sind der Platz, den die Taxonomie
|
||
verlangt.
|
||
|
||
`fail_under` steht in der Konfiguration und nicht als `--cov-fail-under` im CI-Schritt: so sitzt
|
||
die Zahl neben der Begründung, die sie erzeugt hat, und gilt für jeden `--cov`-Lauf statt nur für
|
||
den einen, den CI schreibt.
|
||
|
||
Was der Boden **nicht** tut: die drei echten Lücken schließen (`provenance_cmd.py` 44 %,
|
||
`migrate_cmd.py` 65 %, `type_resolver.py` 79 %). Er friert den erreichten Stand ein. Diese Liste
|
||
ist die einzige, die sich nicht bewegt hat, während alles um sie herum wuchs — `migrate_cmd.py`
|
||
ist sogar von 71 % gefallen, weil das Modul gewachsen ist und die neuen Zeilen ungetestet ankamen.
|
||
Das ist Gitea #51.
|
||
|
||
Mitgenommen, weil es dieselbe Frage beantwortet: der Coverage-Bericht **ist** als Artefakt
|
||
abrufbar, über die Run-Seite. Die Actions-Artefakt-Endpunkte melden dafür `total_count: 0`, weil
|
||
`upload-artifact@v3` über die ältere Artifact-API ablegt, die diese Endpunkte nicht lesen. Eine
|
||
leere Liste ist kein fehlgeschlagener Upload — steht jetzt in `EVALS.md` und im Kommentar an der
|
||
`Coverage report`-Stufe, damit die naheliegende „Korrektur" auf v4 (hier eingeschränkt) niemandem
|
||
mehr einfällt.
|
||
|
||
---
|
||
|
||
## 4.7.1 - 2026-09-04 - redundant_see_also in tools/CONTRACT.md und wiki-lint dokumentiert; xref-remove-Falle benannt
|
||
|
||
**Author:** Torben Nehmer
|
||
|
||
<!-- wikitool:bumps -->
|
||
- redundant_see_also in tools/CONTRACT.md und wiki-lint; xref-remove-Falle beim Aufräumen benannt
|
||
<!-- /wikitool:bumps -->
|
||
|
||
Die Doku-Hälfte von `4.7.0`, beim Abschluss nachgezogen. Der Befund war ausgeliefert, aber
|
||
`tools/CONTRACT.md`s `lint`-Zeile zählt die Befunde auf und kannte ihn nicht - eine Instanz hätte
|
||
eine Sektion im Report gefunden, die ihr Contract nicht erklärt. `docs verify` prüft, dass die
|
||
Kommandotabelle existiert, nie was in einer Zeile steht; genau die Lücke, für die AGENTS.md
|
||
„a stack change is not finished until the human docs describe it" geschrieben ist.
|
||
|
||
**Die eigentliche Änderung ist aber die Warnung in `wiki-lint`**, und sie ist keine Prosa-Politur.
|
||
Der neue Befund liest sich wie etwas, das Schritt 7 („repariere, was mechanisch ist") abräumt, und
|
||
der naheliegende Griff wäre `xref remove` - das die Referenz **beidseitig** löscht. Angewandt auf
|
||
`Wine see-also Wine GE` neben `Wine GE depends-on Wine` hätte das die schwache *und* die
|
||
spezifische Kante entfernt, und das Paar sagte danach gar nichts mehr. Ein Befund, dessen
|
||
offensichtliche Reparatur Daten zerstört, ist schlechter als kein Befund: Schritt 1 nennt die
|
||
Falle jetzt beim Namen und verweist auf `xref add` (fasst nur die Quellseite an) oder aufs
|
||
Berichten. Dieselbe Asymmetrie hat in #30 schon einmal Daten gekostet.
|
||
|
||
---
|
||
|
||
## 4.7.0 - 2026-09-04 - Link-Katalog: authored, alternative-to, addresses; entity→entity-Lineage; Lint-Befund gegen redundante see-also
|
||
|
||
**Author:** Torben Nehmer
|
||
|
||
<!-- wikitool:bumps -->
|
||
- Link-Katalog: authored, alternative-to, addresses; Lint-Befund gegen redundante see-also
|
||
<!-- /wikitool:bumps -->
|
||
|
||
Drei neue Label, zwei geschlossene Autorisierungslücken und ein Lint-Befund - ausgelöst von der
|
||
anstehenden ersten produktiven Instanz. Katalog und `COLLECTION.md` gehen über `dist export` in
|
||
jede neue Instanz; was hier fehlt, fehlt dort ab Tag eins, und nachträglich ist eine
|
||
Katalogerweiterung eine Korpus-Migration statt einer Datenzeile. Gitea #43 und #49.
|
||
|
||
**Die Messung, die den Zuschnitt bestimmt hat.** Die 180 `see-also`-Kanten des Korpus zerfallen
|
||
exakt in drei Klassen: 57 (32 %) sind Spiegel einer bereits typisierten Gegenkante, 70 (39 %)
|
||
sind wechselseitige `see-also`-Paare, 53 (29 %) stehen einseitig. Die erste Klasse ist kein
|
||
Vokabularproblem - `Wine see-also Wine GE` steht neben `Wine GE depends-on Wine`, `RAG see-also
|
||
NotebookLM` neben `NotebookLM implements RAG`. Der Katalog war für ein Drittel der Fälle längst
|
||
ausreichend; es hat sie nur nichts gemeldet. Genau dafür ist der Lint-Befund unten da, und er ist
|
||
der Grund, warum diese Version mehr ist als zwei Katalogzeilen.
|
||
|
||
**`authored`** (operationales Register). „hat das Ziel als einmaligen Akt geschaffen." Der Katalog
|
||
kannte fortlaufende Rechenschaft (`owns`) und fortlaufende Arbeit (`maintains`), aber nicht den
|
||
historischen Ursprung - Urheberschaft stand im Korpus deshalb in vier unvereinbaren Formen
|
||
nebeneinander: `source.author` als Freitext, `owns`, `see-also` und ein Prosa-Bullet. Eine davon
|
||
war sachlich falsch: `Vannevar Bush owns Memex` behauptet laufende Rechenschaft für einen 1974
|
||
Verstorbenen, und eine falsche maschinenlesbare Kante ist schlechter als eine schwache, weil sie
|
||
geglaubt wird. Geschrieben wird das Label auf der Entity-Seite (`Andrej Karpathy authored LLM
|
||
Wiki Pattern`) - die Gegenrichtung `authored-by` auf der Concept-Seite hätte `xref remove`
|
||
gebraucht, das beidseitig abräumt, statt `xref add`, das relabelt. Vier Kanten im Korpus
|
||
umgestellt, keine verloren.
|
||
|
||
**`alternative-to`** (operationales Register, selbst-dual). „erfüllt denselben Zweck wie das Ziel,
|
||
so dass ein Leser, der zwischen beiden wählt, beide will." Belegt durch rund 30 Paare, darunter
|
||
die sieben Agent-CLIs, die untereinander *ausschließlich* `see-also` tragen - keine einzige
|
||
typisierte Kante. Abgegrenzt gegen `contrasts` (behauptet einen lesenswerten Unterschied) und
|
||
`compares-with` (wiegt auf benannten Dimensionen ab und führt in dieser Instanz auf eine
|
||
`kb/comparisons/`-Seite): zwei Agent-CLIs sind austauschbar, zwei gegensätzliche Entwurfsprinzipien
|
||
sind es nicht.
|
||
|
||
Der Katalog sagt jetzt ausdrücklich, dass ein selbst-duales Label **einmal pro Paar** geschrieben
|
||
wird. Ohne diesen Satz wäre aus einer 22-Kanten-`see-also`-Clique eine 22-Kanten-
|
||
`alternative-to`-Clique geworden und nichts gewonnen: sieben austauschbare Werkzeuge sind 21
|
||
Paare, beidseitig deklariert 42 Kanten, von denen die zweiten 21 nichts sagen.
|
||
|
||
**`addresses`** (konzeptionelles Register). „ist eine Antwort auf das Problem, das das Ziel
|
||
beschreibt." `types/concept.md` deklariert `problem` und `decision` als eigene Subtypen, und der
|
||
Katalog hatte kein Label, das eine Entscheidung mit dem Problem verbindet, das sie löst - eine
|
||
Collection konnte ein Problem benennen und nie sagen, was dagegen unternommen wurde. Abgegrenzt
|
||
gegen `rests-on`, das das Ziel als *Prämisse* nimmt statt als zu lösendes Problem. Im Korpus nur
|
||
vier belegte Paare, also dünn nach dem sonst geltenden „erst der Anwendungsfall"-Maßstab; die
|
||
Ausnahme ist bewusst und gilt dem Auslieferungszeitpunkt, der die Kosten umdreht.
|
||
|
||
**Zwei Autorisierungslücken entity→entity.** `kb/entities/COLLECTION.md` erlaubte bisher keine
|
||
Lineage zwischen zwei Entities - ein Fork, eine Neuimplementierung, ein Nachbau war nicht
|
||
ausdrückbar; `derived-from` und `adapted-from` sind jetzt freigegeben. Ebenso `implements`, für
|
||
eine Entity, die eine als Entity geführte Konvention umsetzt. Ein autorisiertes Label ohne
|
||
Live-Nutzung ist ausdrücklich in Ordnung (`instructions/dev/corpus-policy.md`).
|
||
|
||
**Lint-Befund `redundant_see_also`.** Meldet eine `see-also`-Kante, deren Gegenrichtung bereits
|
||
ein typisiertes Label trägt. Gegen den Korpus dieser Instanz meldet er genau die gemessenen 57.
|
||
**Advisory, nicht hart**, aus zwei Gründen zugleich: eine schwache Kante neben einer spezifischen
|
||
ist redundant, nicht kaputt - und der Befund kommt lange nach den Korpora, die er beurteilt, also
|
||
würde eine harte Einstufung jede bestehende Instanz mit dem Upgrade rot schalten, das ihn
|
||
ausliefert. Anders als `unlabelled_edges` ist er auch nicht migrations-gegatet: es gibt keine
|
||
Version, ab der die Redundanz zum Fehler wird, nur einen Sweep, zu dem jemand kommt oder nicht.
|
||
|
||
`links.SEE_ALSO` ist damit das einzige Katalog-Label, das das Werkzeug beim Namen kennt. Das ist
|
||
eine begründete Ausnahme, keine Aufweichung: `see-also` ist der erklärte letzte Ausweg des
|
||
Katalogs und behauptet nur, dass nichts Besseres passte - was der einzige Grund ist, warum `lint`
|
||
eine Kante als *schwächer als* eine andere über demselben Paar beurteilen kann. Alles andere am
|
||
Vokabular bleibt in `instructions/link-taxonomy.md` und den `outbound:`-Blöcken.
|
||
|
||
**Nicht dabei, bewusst.** Der Sweep der 180 bestehenden Kanten (#48) - diese Version ändert außer
|
||
den vier Urheberschaftskanten keine Korpus-Kante. Verworfen wurden außerdem `variant-of` (die
|
||
Wine-Forks tragen bereits `depends-on Wine`), `implemented-by` (Spiegel von `implements`, den die
|
||
Inbound-View rendert), `sibling-of` für die Concept-Cliquen (Über-Verlinkung, kein fehlendes Wort)
|
||
und `builds-on` (Vokabularkollision mit `extends`/`derived-from`/`adapted-from`).
|
||
|
||
---
|
||
|
||
## 4.6.1 - 2026-09-04 - DEVELOPMENT.md im Kommandotabellen-Check, selbstbeschriftete Release-Notes, Prosa-Korrekturen (#47 Block 3)
|
||
|
||
**Author:** Torben Nehmer
|
||
|
||
<!-- wikitool:bumps -->
|
||
- DEVELOPMENT.md im Kommandotabellen-Check, selbstbeschriftete Release-Notes, Prosa-Korrekturen (#47 Block 3)
|
||
<!-- /wikitool:bumps -->
|
||
|
||
Block 3 aus #47 - die beiden Nebenbefunde entschieden und umgesetzt - plus drei Prosa-Korrekturen
|
||
an `4.6.0`, die eine Bewertung des eigenen Ergebnisses gefunden hat.
|
||
|
||
**`DEVELOPMENT.md` gehört in `docs_verify.STAGE_READMES`** (entschieden: ja). Das Gegenargument
|
||
bei der Aufnahme war, die Liste führe bisher nur ausgelieferte Dokumente, und `DEVELOPMENT.md`
|
||
wird von `dist_cmd.ROOT_FILES` bewusst nicht ausgeliefert. Beim Hinsehen löst es sich auf:
|
||
`check_readmes_have_no_command_table` überspringt eine Datei, die nicht existiert. In einer
|
||
ausgelieferten Instanz ist der Eintrag damit schlicht wirkungslos, im Entwicklungs-Checkout - dem
|
||
einzigen Ort, an dem die Datei existiert und also driften kann - greift er. Dafür spricht der
|
||
Anlass: genau diese Datei trug einmal eine Tabelle, die für jeden Verify-Befehl ein zweites Mal
|
||
beschrieb, was er prüft, und sie musste von Hand entfernt werden, weil nichts sie mit etwas
|
||
verglich. Zwei Tests: einer, der die Tabelle in `DEVELOPMENT.md` meldet, und einer, der
|
||
festhält, dass eine fehlende gelistete Datei übersprungen und nicht als Fund gemeldet wird - der
|
||
Instanz-Fall, an dem die Entscheidung hing. Der Konstantenname ist jetzt enger als sein Inhalt;
|
||
das steht als Kommentar daneben, statt eine Umbenennung durch zwei Aufrufstellen zu ziehen.
|
||
|
||
**Veröffentlichte Release-Notes veralten weiter - sie sagen es jetzt selbst** (entschieden:
|
||
Schnappschuss akzeptieren, statt einen Korrekturweg zu bauen). Eine nach dem Tag korrigierte
|
||
`CHANGES.md` erreicht die Release-Seite nicht: `gitea-mcp` kennt kein Release-Edit, und
|
||
Löschen-und-neu-Anlegen würde die angehängten Tarball-Assets vernichten, auf die `INSTALL.md` und
|
||
`version check` zeigen. Bei `v4.4.0` ist das real eingetreten. Statt eines Korrekturwegs für einen
|
||
Text, den niemand editieren kann, trägt der Schnappschuss jetzt eine Fußzeile, die sagt, dass er
|
||
einer ist und wo die gepflegte Fassung liegt - eine veraltete Notiz kostet einen Leser damit einen
|
||
Klick statt einer falschen Überzeugung. Angehängt in `release.yml` und nicht in `version notes`:
|
||
das Kommando ist ein allgemeiner Extraktor, dessen andere Aufrufer (lokale Vorschau, eine Pipe)
|
||
keine Release-Seiten-Fußzeile erben sollen. `.gitea/`-Änderung, also ohne eigenen Bump-Anspruch -
|
||
sie fährt hier mit.
|
||
|
||
**Drei Prosa-Korrekturen an `4.6.0`.** Der `4.6.0`-Eintrag und der Docstring von
|
||
`touches_stack_machinery` behaupteten, das Prädikat prüfe „denselben Pfad-Umfang, den der
|
||
CI-Versions-Gate selbst verwendet". Das stimmt nicht: CI matcht `[^/]+/CONTRACT\.md$`, also genau
|
||
eine Pfadebene, das Prädikat matcht `CONTRACT.md` in jeder Tiefe. Folgenlos im Verhalten - ein
|
||
Über-Match druckt eine Zeile zu viel, nie eine zu wenig -, aber es war eine behauptete Äquivalenz,
|
||
die keine ist, geschrieben in genau der ungeprüften Prosa-Phase, um die #47 sich dreht. Docstring
|
||
und `tools/CONTRACT.md` benennen die Differenz jetzt und begründen sie (bei einer Erinnerung ist
|
||
Über-Matchen die richtige Richtung). Drittens: `stack-close` beschrieb den eigenen Skill-Schnitt
|
||
zu stark („es gibt keinen nächsten Schritt mehr, an dem vorbei zu rutschen wäre"). Wahr für die
|
||
*Prozedur*, die nicht mehr im Kontext steht; nicht wahr für den *Auslöser* - `stack-dev`s „invoke
|
||
it now" ist weiterhin ein Satz, und die `publish`-Notiz nennt den Skill bewusst nicht beim Namen.
|
||
Zwei der drei Kettenglieder bleiben Selbstdisziplin. Der Skill sagt das jetzt selbst, statt sich
|
||
als Garantie zu verkaufen, die er nicht ist.
|
||
|
||
Verifiziert: `tools/wikitool docs verify`, `tools/wikitool instructions verify`,
|
||
`.venv/bin/python -m pytest -q` (969 passed, 2 davon neu), `release.yml` gegen den YAML-Parser
|
||
und das Heredoc als Trockenlauf gegen eine Beispiel-Notiz.
|
||
|
||
---
|
||
|
||
## 4.6.0 - 2026-09-04 - stack-dev/stack-close skill split, publish stack-machinery note, model-selection fix (#47 Block 2)
|
||
|
||
**Author:** Torben Nehmer
|
||
|
||
<!-- wikitool:bumps -->
|
||
- stack-dev/stack-close skill split, publish stack-machinery note, model-selection fix (#47 Block 2)
|
||
<!-- /wikitool:bumps -->
|
||
|
||
Block 2 aus #47 (Vorschlag E, am 2026-09-04 entschieden): die ungeprüfte Schlussphase einer
|
||
Stack-Sitzung - Issue-Body-Rewrite, `docs/`-Veralterung, Changelog-Prosa - hatte bisher keinen
|
||
eigenen Haltepunkt, sondern einen Prosa-Break in `stack-dev` Schritt 6. Der ist zweimal
|
||
hintereinander verschluckt worden (#42, #30), beide Male mit echtem Fund im nachgeholten
|
||
Durchgang. Ein dritter Prosa-Haltepunkt hätte dieselbe Wette verloren, die
|
||
`docs/why-gates-are-code.md` für Gates schon verliert - also keine Prosa-Lösung mehr, sondern ein
|
||
struktureller Schnitt.
|
||
|
||
**Neuer Skill `stack-close`**, dev-only wie `stack-dev`. `stack-dev` endet nach `tools/wikitool
|
||
publish` mit einem Stop statt mit einem sechsten Schritt; die Schlussphase existiert nur noch als
|
||
eigener Skill, den eine Sitzung aufrufen muss - es gibt keinen „nächsten Schritt" mehr, an dem
|
||
vorbei sie rutschen könnte. `stack-close` trägt drei Dinge: den Modell-Rückwechsel-Hinweis (wie
|
||
zuvor), die Body-Rewrite-Disziplin aus `issue-tracking.md` Schritte 2-3 und 7, und neu die
|
||
**Handover-Pflicht über die ganze Sitzung**: benannt wird das Modell für Design/Versionsstelle
|
||
(Schritt 3), für die mechanische Mitte, und für diese Schlussphase - alle drei, auch wenn sie
|
||
identisch sind. Eine Handover-Zeile, die nur eine billige Schlussphase meldet, schweigt genau
|
||
dann, wenn die ebenso ungeprüfte Design-Phase auch billig lief und niemand dort gewechselt hat.
|
||
|
||
Ein Agenten-Zuschnitt (Schlussphase als eigener Subagent mit eigenem Modell) wurde geprüft und
|
||
verworfen: ein Fork erbt in Claude Code zwingend das Elternmodell, ein frischer Subagent den
|
||
Sitzungskontext nicht - die Kombination, die der Zuschnitt bräuchte, gibt es nicht, und selbst
|
||
wenn: der Input der Schlussphase *ist* das akkumulierte Sitzungswissen, das ein kalter Agent aus
|
||
Diff und Issue neu ableiten müsste. Volle Begründung im Body von #47.
|
||
|
||
**`instructions/claude-code-model-selection.md` korrigiert**, im dist-strip-Block: die
|
||
Übersicht „stack-dev Schritt 3 und 6" ist falsch geworden, seit Schritt 6 nicht mehr existiert.
|
||
Sie benennt jetzt beide Haltepunkte an ihrem tatsächlichen Ort - Schritt 3 in `stack-dev`,
|
||
der zweite am Anfang von `stack-close`.
|
||
|
||
**`stack-dev` Schritt 3 ehrlicher formuliert** (Vorschlag C): nicht mehr „ab hier alles
|
||
mechanisch", sondern mit benannter Ausnahme - Changelog-Prosa (Schritt 4), eine berührte
|
||
`docs/`-Seite, neue Menschendoku, der Prosa-Anteil einer Instruction. Dazu die Einschränkung aus
|
||
#30: „durch Tests abgedeckt" gilt nur für das, was die Tests *treffen* - zwei
|
||
datenvernichtende Bugs in `upstream merge` liefen an einem grünen `pytest`/`docs
|
||
verify`/`instructions verify`/CI vorbei, weil kein Test den Fall traf, nicht weil ein
|
||
schwächeres Modell schlechteren Code für den getesteten Fall geschrieben hätte.
|
||
|
||
**Neu: `tools/wikitool publish` selbst erinnert an die Phasengrenze.** Berührt das Changeset
|
||
`tools/`, `types/`, `instructions/`, `AGENTS.md` oder ein `<stage>/CONTRACT.md` - derselbe
|
||
Umfang, den ein Versions-Bump selbst abdeckt -, druckt `publish` nach der Erfolgsmeldung eine
|
||
Zeile, dass die folgende Phase von keinem der drei Checks abgedeckt ist. Kein Gate, keine
|
||
Änderung am Exit-Code, für eine gewöhnliche Content-Publish stumm; harness- und
|
||
instanzneutral formuliert, ohne jede Erwähnung eines Trackers, weil `publish` von jedem
|
||
Skill genutzt wird, nicht nur von `stack-dev`. `git_publish.touches_stack_machinery()` plus
|
||
vier neue Tests (`test_git_publish.py`): zwei für die reine Klassifikationsfunktion
|
||
(positiv/negativ), zwei Integrationstests gegen einen echten Publish - die Notiz erscheint genau
|
||
einmal bei einer `instructions/`-Änderung und bleibt aus bei einer gewöhnlichen `kb/`-Änderung.
|
||
`tools/CONTRACT.md`s `publish`-Zeile trägt die Kurzfassung, absichtlich ohne den Dateinamen
|
||
`version-parts.md` zu nennen - die Datei liegt unter `instructions/dev/` und würde in einer
|
||
ausgelieferten Instanz ins Leere zeigen, während `tools/CONTRACT.md` selbst ausgeliefert wird.
|
||
|
||
Verifiziert: `tools/wikitool instructions sync` (7 Skills, `stack-close` neu), `tools/wikitool
|
||
docs verify`, `tools/wikitool instructions verify`, `.venv/bin/python -m pytest -q` (967
|
||
passed, 4 davon neu).
|
||
|
||
#47 bleibt offen für Block 3 (`DEVELOPMENT.md` in `STAGE_READMES`, veraltete Release-Notes).
|
||
|
||
---
|
||
|
||
## 4.5.1 - 2026-09-04 - issue-tracking - destructive-step invariants, comment-vs-body authority, rename sweep
|
||
|
||
**Author:** Torben Nehmer
|
||
|
||
<!-- wikitool:bumps -->
|
||
- issue-tracking - destructive-step invariants, comment-vs-body authority, rename sweep
|
||
<!-- /wikitool:bumps -->
|
||
|
||
Block 1 aus #47 (gemeinsam mit #29): drei Ergänzungen an
|
||
`instructions/dev/issue-tracking.md`, ausgelöst durch zwei Fehlerklassen, die
|
||
in derselben Sitzung am Stack aufgetreten waren.
|
||
|
||
- **Schritt 1** trägt jetzt, dass destruktive Schritte im Body die Invariante
|
||
nennen müssen, die sie nicht verletzen dürfen, und dass ein
|
||
Akzeptanzkriterium eine prüfbare Eigenschaft ist, keine Tätigkeit. Auslöser
|
||
war #30: der Body schrieb wörtlich "Arbeitsverzeichnis entfernen" für den
|
||
`upstream merge`-Ablauf, und genau das wurde zum datenvernichtenden Bug -
|
||
ein `shutil.rmtree` auf eine Stage mit gitignorierten, nicht
|
||
rekonstruierbaren Daten.
|
||
- **Schritt 2** trägt jetzt die Lesesicht auf Body und Kommentare, die es
|
||
bisher nur aus Autorensicht gab: der Body ist der Stand, Kommentare sind
|
||
Historie; ein erkennbar veralteter Body wird richtiggestellt statt
|
||
umgangen; widersprüchliche Kommentare werden nach Beleg aufgelöst, nicht
|
||
nach Datum. Auslöser war ebenfalls #30 (ein Kommentar empfahl das Gegenteil
|
||
dessen, was der Body später festlegte) und #10 (ein seit Tagen veralteter
|
||
Body gegen drei widersprechende Kommentare, zwei davon sich selbst
|
||
widersprechend).
|
||
- Neuer Abschnitt **"Renames and other decay in the tracker"** nach Schritt 7:
|
||
ein Rename ist erst fertig, wenn auch die offenen Issues nachgezogen sind,
|
||
weil `wikitool` diesen Tracker nicht kennt und nicht kennenlernen soll. Mit
|
||
der Wegweiser-vs-Beleg-Unterscheidung aus #29 und dem Hinweis, dass auch
|
||
verschwundene `kb/`-Seiten und private Infrastrukturangaben Issue-Texte
|
||
altern lassen. Ein neuer Trigger in "When to run" verweist darauf.
|
||
|
||
Keine der drei Ergänzungen verschiebt die bestehende Nummerierung der
|
||
Schritte 1-7 - die Querverweise darauf (u. a. aus
|
||
`instructions/dev/stack-dev/SKILL.md` auf Schritt 7, aus
|
||
`kb/concepts/Issue Label Scheme.md` auf Schritt 2) bleiben also gültig, ohne
|
||
angefasst zu werden.
|
||
|
||
Verifiziert: `tools/wikitool instructions verify`, `tools/wikitool docs
|
||
verify`, beide grün (Prosa-only, kein Interface geändert, PATCH).
|
||
|
||
#47 bleibt offen (Block 2: der Skill-Schnitt aus Vorschlag E; Block 3: die
|
||
beiden Nebenbefunde). #29 bleibt ebenfalls offen: dieser Block deckte nur den
|
||
Regelabsatz, nicht den noch ausstehenden Pfad-Durchgang durch #4, #5, #15,
|
||
#21, #16, #26 - der war nicht Teil des Auftrags für diesen Block.
|
||
|
||
---
|
||
|
||
## 4.5.0 - 2026-09-04 - Beide Update-Wege in Code: upstream merge fuer Clones, dist upgrade fuer Tarball-Instanzen
|
||
|
||
**Author:** Torben Nehmer
|
||
|
||
<!-- wikitool:bumps -->
|
||
- Faktenkorrekturen in der 4.4.0-Prosa; DEVELOPMENT.md ohne zweite Kommandobeschreibung
|
||
- wikitool upstream merge/verify: code procedure for taking a stack update, ownership.py as the shared stack/instance boundary
|
||
- upstream merge: combined-commit regression test (edit+add+delete+contract+template+contract-delete in one commit)
|
||
- upstream merge: keep gitignored local data under a content stage, refuse a merge git never opened, report what actually changed
|
||
- dist upgrade: apply a stack update, not just detect one (#7)
|
||
- dist upgrade: Stamp-Semantik nach --keep-local benannt, docs/ownership-and-templates.md auf drei Eigentumsklassen nachgezogen
|
||
<!-- /wikitool:bumps -->
|
||
|
||
Die Prosa zu 4.4.0 - Changelog-Eintrag, `docs/version-model.md`, `instructions/dev/version-parts.md`,
|
||
`DEVELOPMENT.md` - entstand ungeprüft: kein maschineller Check liest sie, und die Sitzung, die sie
|
||
schrieb, hat den dafür vorgesehenen Modellwechsel aus `stack-dev` Schritt 6 übersprungen. Ein
|
||
nachgeholter Durchgang hat drei Fehler gefunden, von denen der erste der teuerste war.
|
||
|
||
**Der Befund war an einer Stelle schlicht falsch.** Changelog und `docs/version-model.md`
|
||
behaupteten, unter dem alten Modell seien Nummern vergeben worden, die „nie ausgeliefert" wurden -
|
||
im Changelog sogar mit einer erfundenen Zahl („fünf Minor-Bumps ... vier nie ausgeliefert"). Das
|
||
Gegenteil stimmt: weil `release.yml` auf jede `VERSION`-Bewegung feuerte, wurde **jeder** Bump zu
|
||
einem echten, getaggten Release. Der 2026-09-03 hat vier davon in sechs Stunden erzeugt (`4.3.0`
|
||
bis `4.3.3`), zwei für reine Prosa-Änderungen. Der Schaden war nie ein Phantom-Release, sondern
|
||
dass „Release" aufhörte, etwas zu bedeuten - vier Upgrades an einem Nachmittag sind für einen
|
||
Konsumenten kein kleineres Versprechen, sondern ein unleserliches. Beide Stellen tragen jetzt den
|
||
tatsächlichen Vorgang samt Beleg.
|
||
|
||
Dazu zwei kleinere Korrekturen: `version-parts.md` nannte den Stack weiterhin `2.x` (er steht bei
|
||
`4.x`), und `DEVELOPMENT.md` trug eine Tabelle, die für jeden Verify-Befehl ein zweites Mal
|
||
beschrieb, was er prüft - eine Kopie dessen, was `tools/CONTRACT.md` hält und `docs verify` dort
|
||
gegen die CLI prüft. Die Tabelle ist raus; dass die Datei selbst außerhalb der von diesem Check
|
||
abgedeckten Dokumente liegt, steht jetzt an ihrer Stelle. Der Code-seitige Teil davon - ob
|
||
`DEVELOPMENT.md` in `docs_verify.STAGE_READMES` gehört - hängt an #47, zusammen mit der Lücke im
|
||
Skill, die den übersprungenen Modellwechsel überhaupt erst unauffällig gemacht hat.
|
||
|
||
Kein Verhalten geändert, nur Prosa - und bewusst als laufender Kandidat gelassen statt fixiert:
|
||
CIs Version-Gate verlangt die `VERSION`-Bewegung, ein Release verlangt sie nicht. Genau dafür gibt
|
||
es das Modell aus 4.4.0.
|
||
|
||
**Zweiter Bump auf demselben Kandidaten (#30):** `git merge upstream/main` behandelt einen
|
||
bewegten Korpus asymmetrisch - eine gelöschte, upstream-geänderte Seite meldet sich als Konflikt,
|
||
eine neu angelegte Seite wird still gestaged, nur eine beidseitig gelöschte Seite ist harmlos. Die
|
||
Prosa-Prozedur in `private-instance.md` § "Taking a stack update" hat das geschlossen, aber mit
|
||
vier eigenen Fehlern: der Pfadsatz stand dreifach (im Skript, im Kontroll-Grep, implizit in
|
||
`dist_cmd.py`); eine vom Upstream **gelöschte** Maschinerie-Datei wurde von `git checkout
|
||
MERGE_HEAD -- <pfad>` still ignoriert, weil das Skript kein `set -e` hatte; ein echter Konflikt in
|
||
`tools/`/`types/`/`instructions/` endete in einem von der Prosa nirgends erwähnten offenen Merge;
|
||
und ein *neuer* Maschinerie-Pfad unter einer Content-Stage hätte die Literal-Liste nie erreicht.
|
||
|
||
Die Eigentumsgrenze ist jetzt ein Prädikat statt einer Liste: `chemenu/ownership.py`,
|
||
`is_stack_owned(relative)`, wahr für `<stage>/CONTRACT.md` direkt unter einer Content-Stage
|
||
(`kb`, `raw`, `work`, `reports`) und für jeden Pfad, der dort auf `.template` endet - nicht
|
||
rekursiv, `kb/<collection>/COLLECTION.md` bleibt seit #39 instanzeigen. `dist_cmd.py` liest das
|
||
Modul jetzt statt einer eigenen `_CONTENT_ALLOWED_NAMES`-Liste zu pflegen, und
|
||
`CONTRACT_ONLY_STAGES` leitet sich aus `ownership.CONTENT_STAGES` ab statt die drei Stage-Pfade
|
||
ein zweites Mal aufzuschreiben - ein Test hält fest, dass beide Sichten nicht auseinanderlaufen
|
||
können.
|
||
|
||
Neu: `wikitool upstream merge [--remote upstream] [--branch main] [--no-fetch]` und `wikitool
|
||
upstream verify --since <rev> [--until HEAD]`. `merge` prüft Vorbedingungen (sauberer Baum, kein
|
||
laufendes Merge, Remote löst auf), warnt statt zu blockieren, wenn `.wikitool-remotes.json` fehlt,
|
||
hält den Merge offen (`--no-commit --no-ff`), zwingt jede Content-Stage auf die lokale Seite
|
||
zurück, holt dann über die Vereinigungsmenge der Bäume von `MERGE_HEAD` und `HEAD` genau die
|
||
stack-eigenen Pfade zurück - inklusive einer Löschung, falls der Upstream einen Maschinerie-Pfad
|
||
entfernt hat -, verweigert bei verbliebenen unaufgelösten Pfaden ohne zu committen, committet
|
||
sonst und verifiziert den entstandenen Commit mit derselben Logik wie `verify` - ein Fund dort
|
||
wird laut gemeldet und **nicht** automatisch zurückgerollt. Nicht idempotent (AGENTS.md § Tool
|
||
error contract), nicht budget-exempt; `verify` liest nur und ist wie `migrate verify` von der
|
||
Budget-Gate ausgenommen. Die Mass-Update-Gate greift bei einem Merge-Commit strukturell nicht -
|
||
das steht jetzt als eigener Absatz in `instructions/gates.md`, mit `upstream merge`s eigener
|
||
Nachkontrolle als der Sicherung, die hier tatsächlich trägt.
|
||
|
||
`private-instance.md` § "Taking a stack update" verweist jetzt auf den Befehl statt das Skript
|
||
auszuschreiben; die Pfadtabelle bleibt als Erklärung stehen, ist aber nicht mehr die operative
|
||
Liste. Vorschlag B (eigenes Demo-Repo) bleibt zurückgestellt, siehe #30.
|
||
|
||
23 neue Tests unter `test_upstream_cmd.py` (35 Fälle mit der parametrisierten
|
||
`is_stack_owned`-Tabelle) decken die Fälle aus der Spezifikation ab:
|
||
gelöschte vs. geänderte vs. neu angelegte Seiten, `kb/CONTRACT.md`- und Template-Änderungen,
|
||
`kb/entities/COLLECTION.md` bleibt lokal, eine gelöschte `raw/CONTRACT.md` landet, ein neuer
|
||
Template-Pfad landet, ein offener `work/`-Lauf landet nicht, ein echter `tools/`-Konflikt lässt
|
||
den Merge offen, ein schmutziger Baum wird unberührt abgewiesen, "bereits aktuell" ist ein No-op,
|
||
die Publish-Remote-Gate-Warnung, `verify` gegen einen von Hand verpfuschten Merge, und die
|
||
`dist_cmd`/`ownership`-Konsistenz.
|
||
|
||
**Dritter Bump auf demselben Kandidaten:** die im Akzeptanzkriterium geforderte Kombinationsprobe
|
||
fehlte noch - ein einzelner Upstream-Commit, der Editieren, Anlegen, Löschen einer Seite, eine
|
||
Contract-Änderung, eine Template-Änderung und eine Contract-Löschung gleichzeitig bewegt. Jetzt
|
||
als `test_one_upstream_commit_mixing_every_case_at_once` nachgetragen; alle sechs Erwartungen in
|
||
einem `upstream merge`-Aufruf verifiziert.
|
||
|
||
**Vierter Bump: zwei Fehler, die ein Review-Durchgang nach dem Publish gefunden hat.** Beide
|
||
waren in der ersten Fassung enthalten, beide hätten Daten vernichtet, und keiner der bestehenden
|
||
Tests hat sie berührt.
|
||
|
||
*Erstens: die Content-Stage wurde als Ganzes gelöscht.* `_restore_stage_to_local` hieß in der
|
||
ersten Fassung `shutil.rmtree(stage_dir)` — die wörtliche Übersetzung des `rm -rf kb raw` aus der
|
||
Prosa-Prozedur. Für `kb/` und `raw/` ist das harmlos, weil dort nichts Ignoriertes liegt. Für die
|
||
beiden Stages, die dieses Issue *neu* in den Satz aufgenommen hat, ist es das nicht: `reports/`
|
||
ist bis auf seinen Contract komplett gitignored und trägt genau die Daten, die nirgends sonst
|
||
existieren — die Telemetrie-Traces, aus denen `eval score` liest, gespeicherte Eval-Berichte,
|
||
alte Lint-Reports. In dieser Instanz standen zum Zeitpunkt des Fundes 497 Trace-Verzeichnisse
|
||
unter `reports/telemetry/`; ein einziger `upstream merge` hätte sie alle gelöscht, und zwar
|
||
stillschweigend, weil git von ignorierten Dateien nichts meldet. Die Stage wird jetzt über die
|
||
**getrackten** Pfade beider Bäume zurückgesetzt statt über das Verzeichnis; ignorierte lokale
|
||
Daten bleiben unberührt. Leergewordene Verzeichnisse werden aufgeräumt, aber nur wirklich leere.
|
||
|
||
*Zweitens: ein Merge, den git nie eröffnet hat, hätte die Maschinerie gelöscht.* Der Exit-Code
|
||
von `git merge --no-commit --no-ff` wird bewusst ignoriert (Konflikte unter den Content-Stages
|
||
sind erwartet). Nur: wenn git das Merge gar nicht erst eröffnet — unverwandte Historien, eine
|
||
ignorierte Datei im Weg —, gibt es kein `MERGE_HEAD`, `_tree_paths("MERGE_HEAD")` liefert die
|
||
leere Menge, und **jeder** stack-eigene Pfad in `HEAD` fällt damit in den Zweig „der Upstream hat
|
||
ihn gelöscht": `kb/CONTRACT.md`, `raw/CONTRACT.md` und sämtliche Templates werden entfernt. Der
|
||
Kommando-Ablauf prüft jetzt nach dem Merge-Aufruf, dass tatsächlich ein Merge offen ist, und
|
||
bricht sonst ab, ohne den Baum angefasst zu haben. Beide Fehler haben je einen Regressionstest,
|
||
und beide Tests wurden gegen die alte Fassung laufen gelassen, um zu zeigen, dass sie sie
|
||
tatsächlich fangen.
|
||
|
||
Dazu eine Ehrlichkeitskorrektur an der Erfolgsmeldung: sie zählte die *wiederhergestellten*
|
||
Pfade, nicht die geänderten — ein Merge, der eine Datei bewegt, meldete vier oder fünf. Sie fragt
|
||
jetzt `git diff` zwischen Vor- und Nach-Commit, kennzeichnet Löschungen, und stimmt damit mit dem
|
||
überein, was ein Leser nachprüfen würde. `docs/ownership-and-templates.md` hat einen Abschnitt
|
||
bekommen, warum die Grenze ein Prädikat und keine Liste ist — die Begründung, die dieses Issue
|
||
erarbeitet hat, gehörte in die Hintergrunddoku und nicht nur in einen Changelog-Eintrag.
|
||
|
||
**Fünfter Bump: `wikitool dist upgrade` (#7), der zweite der beiden Update-Wege.** `upstream
|
||
merge` oben bedient eine Instanz mit gemeinsamer Git-History; `dist upgrade` bedient eine
|
||
Instanz aus einem Tarball, ohne History, die bislang eine rein manuelle Prozedur in
|
||
`INSTALL.md` durchlaufen musste - Schritt 4 verlangte einen sha256-Vergleich von Hand gegen den
|
||
`files`-Block der alten `.wikitool-release.json`.
|
||
|
||
Die tragende Regel: die Schreibmenge ist genau der `files`-Block der *neuen*
|
||
`.wikitool-release.json`, minus was ein Export aus einer leeren Vorlage neu sät
|
||
(`chemenu.ownership.is_export_stub`, wie bisher schon für `kb/log.md`/`.gitkeep`) oder einmalig
|
||
sät und danach der Instanz gehört (`chemenu.ownership.is_upgrade_preserved`, neu für
|
||
`.wikitool-kb.json` und `CHANGES.md`), plus der Stamp selbst. Jeder Kandidatpfad wird gegen die
|
||
*alte* Instanz-Summe klassifiziert: unverändert wird geräuschlos überschrieben, neu im Release
|
||
wird angelegt, lokal verändert oder gelöscht wird **nie** still überschrieben - der Lauf bricht
|
||
mit der vollständigen Liste ab, außer `--keep-local` sagt ausdrücklich, dass die Dateien liegen
|
||
bleiben sollen. `--prune` entfernt zusätzlich aus dem Release entfallene Dateien, aber nur
|
||
solche, die seit der Installation unverändert sind.
|
||
|
||
Die Migrationskette nach dem Tausch wird aus den `instructions/migrations/` des *neuen* Baums
|
||
ermittelt (`kb_state.load_migrations` bekam dafür einen `directory`-Parameter) und nur
|
||
gemeldet, nie ausgeführt - es gibt bewusst kein `migrate run`. Eine bereits gegen die
|
||
*installierte* Maschinerie offene Kette lässt den Befehl abbrechen, bevor er die Quelle
|
||
überhaupt öffnet. `kb_state.divergent_files()` (bisher nur von `migrate status` gelesen) ist
|
||
jetzt eine dünne Hülle um das neue, zwei-Baum-fähige `compare_against_stamp()` - gleiches
|
||
Verhalten für den bestehenden Aufrufer, wiederverwendbar für `dist upgrade`s eigenen Vergleich.
|
||
|
||
Quelle ist immer ein bereits vorhandenes Verzeichnis oder `.tar.gz` - kein Download, das bleibt
|
||
allein `version check`s Sache. Ein Tarball muss genau ein Top-Level-Verzeichnis enthalten (die
|
||
Form, in der `release.yml` es baut) und wird gegen eine `.sha256`-Beidatei geprüft, falls eine
|
||
danebenliegt (fehlt sie: WARN, kein Abbruch). Weitere Abbruchgründe vor jedem Schreiben: fehlende
|
||
lokale `VERSION`/`.wikitool-kb.json`/Stamp mit `files`-Block, ein schmutziger Arbeitsbaum (kein
|
||
Git-Repo ist ein WARN, keine Sperre), eine Vorab-Version (`-beta.N`) ohne `--pre`, sowie ein
|
||
Downgrade; Gleichstand ist ein No-op. Ein Grenzübertritt der Kompatibilität wird laut gemeldet,
|
||
blockiert aber nicht. Committet und pusht nichts (Invariante 5).
|
||
|
||
Gegenüber dem ersten Entwurf des Issues zwei Korrekturen, die dort auch nachgetragen sind: der
|
||
`files`-Block wurde entgegen der ursprünglichen Annahme bereits vor diesem Bump gelesen
|
||
(`divergent_files`/`migrate status`), und die Migrationskette war ursprünglich falsch begründet
|
||
- sie kann nur aus dem *neuen* Baum kommen, nicht durch eine andere Abfragereihenfolge aus der
|
||
alten Instanz. 24 neue Tests unter `test_dist_upgrade.py` decken die Klassifikation, alle
|
||
Abbruchgründe, `--keep-local`, `--prune` und beide Quellformen (Verzeichnis und Tarball,
|
||
inklusive der sha256- und Top-Level-Prüfung) ab.
|
||
|
||
Bewusst nicht angetastet: `instructions/private-instance.md` (der Clone-Weg ändert sich nicht,
|
||
`INSTALL.md` benennt jetzt beide Wege nebeneinander) und die Frage, wie `dist upgrade` mit
|
||
Collection-Templates umgeht, deren Namen eine fremde Instanz gar nicht hat - es verhält sich wie
|
||
`upstream merge` und schreibt sie, was ein eigenes Issue gegen den Export wäre, keins gegen das
|
||
Upgrade.
|
||
|
||
**Sechster Bump: die ungeprüfte Phase nachgeholt.** Der Abschluss des vorigen Bumps lief auf
|
||
Sonnet, und `stack-dev` Schritt 6 verlangt dort genau zwei Dinge, die kein Check erzwingt:
|
||
Issue-Body und `docs/`-Veralterung. Der Body war gemacht, die `docs/`-Prüfung nicht - sie wurde
|
||
benannt statt durchgeführt. Nachgeholt auf Opus, mit einem Fund.
|
||
|
||
`docs/ownership-and-templates.md` § „The consequence in practice" beschrieb ein Upgrade als
|
||
Zweiteilung: verbatim überschreiben, `.template`-gestützte Dateien liegen lassen - und begründete
|
||
den ersten Teil damit, dass verbatim ausgelieferte Dateien „safe to replace wholesale" seien,
|
||
weil sie „never instance-specific to begin with" waren. Genau diese Annahme trifft `dist upgrade`
|
||
nicht: eine Instanz *kann* eine verbatim ausgelieferte Datei angefasst haben, und die sha256 je
|
||
Datei existiert, um das zu erkennen, statt es vorauszusetzen. Dazu fehlte die dritte Klasse ganz -
|
||
die einmalig gesäten, danach instanzeigenen Pfade (`.wikitool-kb.json`, `CHANGES.md`,
|
||
`kb/log.md`, `raw/*/.gitkeep`), die im Stamp stehen wie jede andere Datei und deshalb aktiv
|
||
ausgeschlossen werden müssen. Die Seite nennt jetzt drei Klassen und die engere praktische Regel:
|
||
überschreibe die verbatim ausgelieferten Dateien, *die diese Instanz nicht angefasst hat*.
|
||
|
||
Dazu eine Präzisierung in `tools/CONTRACT.md`, die vorher nirgends stand: nach `--keep-local`
|
||
wird der neue Stamp trotzdem vollständig geschrieben, trägt also die Release-Summe auch für
|
||
Dateien, die bewusst *nicht* geschrieben wurden. Der Stamp ist die Vergleichsbasis für den
|
||
nächsten Lauf, kein wörtliches Inventar der Platte - und genau das hält eine übersprungene Datei
|
||
bei jedem weiteren Lauf als abweichend gemeldet, statt sie nach einmaligem Überspringen still als
|
||
aktuell zu führen.
|
||
|
||
---
|
||
|
||
## 4.4.0 - 2026-09-03 - Versionskandidat statt Bump-pro-Release: VERSION traegt -beta.N, version release fixiert
|
||
|
||
**Author:** Torben Nehmer
|
||
|
||
<!-- wikitool:bumps -->
|
||
- Versionskandidat statt Bump-pro-Release: VERSION traegt -beta.N, version release fixiert
|
||
<!-- /wikitool:bumps -->
|
||
|
||
Bisher bekam jeder `version bump` sofort eine fixierte Nummer, und weil `release.yml` auf jede
|
||
`VERSION`-Bewegung feuert, wurde daraus sofort ein Release: Nummern entstanden in
|
||
Commit-Granularität statt in Release-Granularität. Der 2026-09-03 hat so vier Releases in sechs
|
||
Stunden erzeugt (`4.3.0` bis `4.3.3`), zwei davon für reine Prosa-Änderungen - alle vier echt,
|
||
keines davon eine Einheit, an der ein Konsument sich hätte orientieren können. `VERSION` trägt
|
||
jetzt zwischen zwei Releases **einen** laufenden Kandidaten (`X.Y.Z-beta.N`):
|
||
`--major/--minor/--patch` eskaliert diesen Kandidaten max-wins gegen den letzten Release, statt
|
||
eine neue Nummer danebenzustellen, und geht dabei nie zurück.
|
||
|
||
`Version` versteht den Suffix, mit einer expliziten Ordnung
|
||
(`4.4.0-beta.1 < 4.4.0-beta.2 < 4.4.0`, numerisch nach `N`, nicht lexikografisch). `CHANGES.md`
|
||
trägt genau einen offenen Eintrag pro Kandidat: der erste Bump eröffnet ihn, jeder weitere
|
||
aktualisiert Heading und die maschinenverwaltete Bump-Titel-Liste in
|
||
`<!-- wikitool:bumps -->` (Marker-Konvention aus `blocks.py`, aber bewusst nicht in
|
||
`blocks.BLOCKS` - diese Region gehört zu `CHANGES.md`, nicht zu einer Seite). `version release`
|
||
ist neu und fixiert einen Kandidaten: Suffix weg, Eintrag geschlossen, committet und pusht nichts.
|
||
|
||
Vier Stellen am Bestand angepasst, die das Kandidatenmodell sonst still beschädigt hätten:
|
||
`release.yml` überspringt einen suffixbehafteten `VERSION`-Push sauber, bevor die Releases-API
|
||
gefragt wird, statt jeden Beta-Bump zu veröffentlichen; die Grenzübertritts-Checks in
|
||
`docs verify` (`check_migration_for_boundary`, `check_breaking_change_for_boundary`) messen jetzt
|
||
gegen den **letzten Release** (`version_mod.last_release`) statt gegen den zweitobersten Eintrag,
|
||
der zwischen zwei Betas keine Grenze mehr hergibt; `kb_state.chain()`/`next_link()` vergleichen
|
||
gegen die **Kandidatenbasis**, weil eine Migration mit Ziel `4.4.0` sonst bei installiertem
|
||
`4.4.0-beta.1` aus dem Intervall fällt (`4.4.0-beta.1 < 4.4.0`); `read_kb_version()` verweigert
|
||
einen Prerelease, weil eine Inhaltsform kein Beta kennt. `dist export` schreibt `VERSION` und den
|
||
Stamp weiterhin ehrlich mit Suffix, aber `.wikitool-kb.json` bekommt die Basis.
|
||
|
||
Menschendoku für die Erzeuger-Seite: `DEVELOPMENT.md` im Repo-Root, bewusst nicht in
|
||
`dist_cmd.ROOT_FILES` (Begründung als Kommentar dort), mit Zeile in `AGENTS.md` § File naming und
|
||
Zeiger aus `README.md`. `docs/version-model.md` hat einen neuen Abschnitt, warum eine Nummer erst
|
||
durch ein Release verbraucht wird.
|
||
|
||
---
|
||
|
||
## 4.3.3 - 2026-09-03 - Modellwahl nach Pruefbarkeit statt nach Aufgabenname; stack-dev bricht an den Phasenwechseln fuer den Model-Switch
|
||
|
||
**Author:** Torben Nehmer
|
||
|
||
`instructions/claude-code-model-selection.md` routete bisher nach Skill: eine Zeile "Stack
|
||
development -> Opus/high" fuer alles, was `tools/`, `types/` oder `instructions/` anfasst. Das ist
|
||
zu grob in beide Richtungen - es verteuert die lange, mechanische Mitte einer Stack-Sitzung, und es
|
||
sagt nichts darueber, dass Anfang und Ende derselben Sitzung anders zu behandeln sind.
|
||
|
||
**Die neue Achse ist "was faengt hier einen Fehler ab".** Wo ein Check in Code steht - `pytest`,
|
||
`docs verify`, `instructions verify`, CI, die Gates - kostet der Fehler eines schwaecheren Modells
|
||
eine Runde und faellt auf. Wo die einzige Durchsetzung eine Sitzung ist, die Prosa liest, faellt
|
||
derselbe Fehler gar nicht auf: er wird ausgeliefert und bleibt stehen. Das ist dasselbe Argument,
|
||
das `docs/why-gates-are-code.md` fuer Gates fuehrt, angewandt auf die Modellwahl.
|
||
|
||
Stack-Entwicklung ist damit **nicht mehr eine Zeile, sondern drei**:
|
||
|
||
| Phase | Was einen Fehler faengt | Modell |
|
||
|---|---|---|
|
||
| Design, Versionsstelle, Grenzuebertritts-Urteil | nichts | Opus/high |
|
||
| Code, Tests, mechanische Doku-Synchronisation | pytest, CI, `docs verify` | Sonnet/high |
|
||
| Issue-Abschluss, `docs/`-Veralterung, Changelog-Prosa | nichts, per Konstruktion | Opus/high |
|
||
|
||
Die Mitte ist die lange Phase und die mit den Checks - dort liegt die Ersparnis. Die beiden
|
||
Raender sind kurz (Minuten, nicht Stunden), haben aber keinen maschinellen Waechter: `wikitool`
|
||
kennt den Issue-Tracker bewusst nicht, und eine `docs/`-Seite traegt keinen normativen Satz, also
|
||
gibt es dort nichts zu verifizieren. Sie oben zu lassen ist billig und schuetzt genau die Arbeit,
|
||
die still scheitert.
|
||
|
||
Zwei Praezisierungen dazu: **Effort ist der billigere Hebel als das Modell** - `medium` steht fuer
|
||
Stack-Arbeit bewusst in keiner Zeile, weil Mehrdatei-Konsistenz das ist, was ein reduzierter
|
||
Effort zuerst aufgibt; `high` ist die Untergrenze, sobald mehr als eine Datei oder ein Contract
|
||
betroffen ist. Und die Asymmetrie ist benannt: eine unnoetige Opus-Phase kostet einmal Geld, eine
|
||
ungepruefte Sonnet-Phase kann etwas ausliefern, das nie wieder jemand ansieht.
|
||
|
||
**Damit die Tabelle ueberhaupt wirksam wird, braucht sie Haltepunkte.** Eine Sitzung kann ihr
|
||
eigenes Modell nicht wechseln - das ist `/model` und gehoert dem Nutzer. Eine Empfehlung, die
|
||
niemand zum richtigen Zeitpunkt ausspricht, aendert nichts. `instructions/dev/stack-dev/SKILL.md`
|
||
bekommt deshalb zwei ausdrueckliche Breaks:
|
||
|
||
- **Neuer Schritt 3** - "Settle the design before building", mit dem Angebot zum Wechsel nach
|
||
unten, sobald der Plan steht und die Arbeit mechanisch wird. Einmal aussprechen, dann so oder
|
||
so weiterarbeiten.
|
||
- **Schritt 6 (Abschluss) bricht in die Gegenrichtung** - ab dort greift wieder kein Check. Mit
|
||
der ausdruecklichen Auflage, die Arbeit **unabhaengig von der Antwort** zu tun: nach dem Publish
|
||
auf einen Modellwechsel zu blockieren wuerde genau den Zustand hinterlassen, den Schritt 6
|
||
verhindern soll. Lief die Phase auf dem billigeren Modell, gehoert das in die Uebergabe statt
|
||
ins Schweigen.
|
||
|
||
Ein auftauchender Grenzuebertritt ist unter den Decision points ebenfalls als Anlass zum Wechsel
|
||
nach oben benannt: `docs verify` prueft, dass ein Uebertritt sich dokumentiert, nie dass die
|
||
Stelle richtig gewaehlt war.
|
||
|
||
Die uebrigen Schritte sind unveraendert und nur umnummeriert (alt 3-5 -> neu 4-6).
|
||
|
||
---
|
||
|
||
## 4.3.2 - 2026-09-03 - stack-dev: Issue-Abschluss ist ein nummerierter Schritt, kein Zeiger in einer Routing-Liste
|
||
|
||
**Author:** Torben Nehmer
|
||
|
||
Nachfassen zu 4.1.2 (#44), das die Regel geschaerft, aber den Weg zu ihr nicht geaendert hat.
|
||
`instructions/dev/issue-tracking.md` bekam damals Schritt 7 ("Closing is the last body update,
|
||
not a comment"); `instructions/dev/stack-dev/SKILL.md` bekam nur eine umformulierte Zeile in
|
||
seiner Routing-Liste. Eine Stunde spaeter schloss #45 auf exakt dieselbe Weise: gruendlicher
|
||
Abschlusskommentar ueber einem Body mit unangehakten Kriterien.
|
||
|
||
**Die Ursache lag nicht am Text der Regel, sondern an ihrer Erreichbarkeit.** Die nummerierten
|
||
Schritte des Skills endeten bei "Verify before publishing". Ein Issue zu schliessen war ueberhaupt
|
||
kein Schritt - es hing an einem Zeiger *innerhalb* von Schritt 2, und Schritt 2 ist eine
|
||
Routing-Tabelle aus fuenf "read X before Y"-Eintraegen, keine Checkliste. Eine Sitzung folgt dem
|
||
Spine, den sie im Kontext hat; was nur hinter einem Link steht, wird genau in dem Moment nicht
|
||
aufgeschlagen, in dem es greift - am Ende einer langen Sitzung, wenn der Kontext am vollsten und
|
||
die verbleibende Instruktionsflaeche am duennsten ist.
|
||
|
||
Verschaerfend arbeitete der Blurb gegen seine eigene Regel: fett gesetzt war "keep it current as
|
||
the state moves, **not at the end**". Wer den Body unterwegs ungefaehr gepflegt hatte, las daraus
|
||
Konformitaet - der eigentliche Abschlusstest stand nur in der verlinkten Datei.
|
||
|
||
Geaendert:
|
||
|
||
- **Neuer Schritt 5 in `stack-dev/SKILL.md`** - "Close the issue with a body rewrite, not a
|
||
comment", mit dem Test inline (Kriterien abgehakt oder mit Begruendung gestrichen,
|
||
Entscheidungen als entschieden formuliert, kein Praesens ueber einen behobenen Defekt,
|
||
Verifikation benannt) und dem Verweis auf Schritt 7 fuer die volle Form. Damit steht der
|
||
Abschluss auf dem Spine.
|
||
- **Schritt-2-Blurb rebalanciert** - beide Haelften binden jetzt sichtbar: fortlaufende Pflege
|
||
*und* der Rewrite vor dem Schliessen, mit Verweis auf Schritt 5.
|
||
|
||
Nichts davon ist maschinell pruefbar, und das bleibt richtig so: `wikitool` kennt den Tracker
|
||
nicht und darf ihn nicht lernen, weil es an Instanzen ausliefert, die kein Board haben
|
||
(`issue-tracking.md` § "What no tool checks"). Der Skill-Spine ist die einzige Durchsetzung, die
|
||
es geben kann - was der Grund ist, den Schritt zu nummerieren statt ihn zu verlinken.
|
||
|
||
Verallgemeinerbar: eine Regel, die in eine verlinkte Instruction geschrieben wird, erreicht
|
||
Sitzungen nur, wenn die nummerierten Schritte des zustaendigen Skills sie in dem Moment
|
||
ansteuern, in dem sie greift.
|
||
|
||
---
|
||
|
||
## 4.3.1 - 2026-09-03 - docs/ befuellt - Stack-Hintergrund fuer vier Themen, Pflegeklausel in AGENTS.md ergaenzt
|
||
|
||
**Author:** Torben Nehmer
|
||
|
||
Gitea #45: die von #38 angelegte, bis dahin leere `docs/` bekommt ihre ersten vier Seiten - frisch
|
||
geschrieben, nicht durch Umzug aus `kb/` befuellt, jede ohne normativen Satz und mit Verweis auf
|
||
das bindende Dokument statt einer Wiederholung seiner Regeln:
|
||
|
||
- `docs/pipeline-rationale.md` - warum `raw -> types/tools -> kb -> reports` vier getrennte Stufen
|
||
sind und was "never re-derive, always compile" praktisch bedeutet
|
||
- `docs/why-gates-are-code.md` - warum Mass-Update-, Publish-Remote- und Iteration-Budget-Gate in
|
||
`tools/wikitool` statt in einer Instruktion stehen
|
||
- `docs/ownership-and-templates.md` - der Unterschied zwischen stack-eigenen, verbatim
|
||
ausgelieferten Dateien und instanz-eigenen `.template`-Dateien
|
||
- `docs/version-model.md` - warum Drop-in-Kompatibilitaet und Migrationsbedarf zwei unabhaengige
|
||
Fragen sind, illustriert an der 2.0.0-Fallstudie
|
||
|
||
**AGENTS.md § Changelog:** neue Klausel zur Pflege von `docs/`, ergaenzt neben der bestehenden
|
||
Regel zu `README.md`/`EVALS.md`/`tools/README.md`. Eine `docs/`-Seite veraltet nicht wie ein
|
||
README bei jedem neuen Flag, sondern nur, wenn die aufgeschriebene Begruendung selbst nicht mehr
|
||
traegt - per Konstruktion ungeprueft, da die Seite keinen normativen Satz enthaelt, den
|
||
`docs verify` pruefen koennte.
|
||
|
||
---
|
||
|
||
## 4.3.0 - 2026-09-03 - docs/ als ausgelieferter Hintergrund-Ort; Decision-Seiten bleiben in kb/, Decay-Skip fuer concept_type: decision
|
||
|
||
**Author:** Torben Nehmer
|
||
|
||
Gitea #38: `dist export` lieferte bislang keine einzige `kb/`-Seite aus - eine frische Instanz
|
||
bekam den Stack, aber keinen Grund für seine Form. Die dokumentierte `adr-NNN-`-Konvention in
|
||
`kb/concepts/COLLECTION.md` existierte zudem nur auf Papier: keine der sieben
|
||
`concept_type: decision`-Seiten folgte ihr, und `confidence_decay()` lief bedingungslos über sie
|
||
- ein Kategorienfehler, weil Zeitablauf eine Entscheidung nicht falscher macht, nur Supersession
|
||
tut das.
|
||
|
||
**Neu:** `docs/` - ein inertes Verzeichnis für Stack-Hintergrund (warum der Stack so gebaut ist,
|
||
nicht was diese Instanz entschieden hat). Keine Frontmatter, kein Typ, kein Index, kein Lint,
|
||
keine Decay, keine Provenance, keine `COLLECTION.md`. `dist export` liefert es verbatim aus, wie
|
||
`instructions/` und `types/`. Befüllung folgt in Gitea #45.
|
||
|
||
**Verworfen, nach Prüfung:** ein Umzug der sieben Decision-Seiten nach `decisions/`. Der
|
||
Subtyp-Floor aus #28 verlangt mindestens eine Seite je deklariertem `concept_type`, und ein
|
||
Umzug hätte `decision` auf null gebracht; dazu zeigen 89 Wikilinks aus `kb/` sowie
|
||
tool-eigene Frontmatter-Arrays auf die sieben, und `links.py`/`xref add` kennen kein Ziel
|
||
außerhalb `kb/`. Die sieben bleiben in `kb/concepts/`, ebenso ein zweiter, separat erwogener
|
||
Rename (`docs verify` → `parity verify`) - der wäre nur nötig gewesen, wenn ein Befehl auf das
|
||
Verzeichnis `docs/` wirkt, und keiner tut das.
|
||
|
||
**Geändert:**
|
||
- `confidence_decay()` überspringt `concept_type: decision` strukturell (kategorische Ausnahme,
|
||
nicht als Brücke gebaut - Begründung im Docstring).
|
||
- `kb/concepts/COLLECTION.md` § Decisions ersetzt die tote ADR-Vorlage durch die real gelebte
|
||
Form: eine Entscheidung ist eine gewöhnliche Concept-Seite, organische Prosa, kein
|
||
`adr-NNN-`-Präfix, `**Status:**` optional, Supersession per `supersedes`-Link.
|
||
- `kb/CONVENTIONS.md` § Naming und `instructions/kb-profiles.md` (Profil `german`) korrigiert -
|
||
beide dokumentierten noch die verworfene `adr-NNN-`-Namensregel.
|
||
- `AGENTS.md` § File naming und § Routing: `docs/`-Zeile, plus die Regel, dass `docs/` keinen
|
||
normativen Satz trägt (das hält Invariante 8 heil - was binden würde, gehört in einen
|
||
Contract).
|
||
- `tools/CONTRACT.md`: Klarstellung, dass `docs verify` Dokumentations-Parität prüft, nicht das
|
||
`docs/`-Verzeichnis, sowie `docs/` in der `dist export`-Zeile ergänzt.
|
||
|
||
Additiv und in beide Richtungen drop-in: eine bestehende Instanz ohne `docs/` exportiert
|
||
weiterhin identisch (leerer `_copy_tree`-Treffer), eine Instanz mit `docs/` bekommt es ab jetzt
|
||
mitgeliefert. Kein Feld, kein Kommando ändert sein Verhalten für bestehenden Inhalt.
|
||
|
||
**Migration:** none required.
|
||
|
||
Berührt: `tools/chemenu/commands/confidence_decay.py`, `tools/chemenu/commands/dist_cmd.py`,
|
||
`tools/chemenu/tests/test_confidence_decay.py`, `tools/chemenu/tests/test_dist_cmd.py`,
|
||
`kb/concepts/COLLECTION.md`, `kb/CONVENTIONS.md`, `instructions/kb-profiles.md`, `AGENTS.md`,
|
||
`tools/CONTRACT.md`.
|
||
|
||
---
|
||
|
||
## 4.2.0 - 2026-09-03 - Korpus-Kuratierungsrichtlinie: Untergrenzen und Leitplanke für reaktive Fixes
|
||
|
||
**Author:** Torben Nehmer
|
||
|
||
Ein Demo-Korpus will klein und stabil sein, ein Testbett groß, unordentlich und in Bewegung -
|
||
dieses Repo verlangt seit der Veröffentlichung beides vom selben `kb/` (Gitea #28). Die Sitzung
|
||
vom 2026-09-02 hatte Fixture, `--with-demo` und ein zweites Repo bereits verworfen; offen blieb
|
||
nur, wie kuratiert "kuratiert genug" heißt und welche Leitplanke reaktive Fixes bekommen.
|
||
|
||
**Neu:** `instructions/dev/corpus-policy.md`. Fünf Untergrenzen, jede mit einer bestehenden
|
||
`wikitool`-Prüfung messbar, keine davon durch neuen Tool-Code: jeder Seitentyp und jeder
|
||
deklarierte Subtyp mit mindestens einer Seite, mindestens fünf Seiten mit mindestens drei
|
||
Quellen, ein bis zehn Orphan-Seiten, im Schnitt mindestens vier ausgehende Wikilinks pro Seite.
|
||
Gemessen am 2026-09-03: 181 Seiten, alle Typ-/Subtyp-Floors erfüllt, 12 Seiten mit ≥3 Quellen, 3
|
||
Orphans, Ø 6,2 ausgehende Links - der Korpus war bereits groß genug, ohne dass eine einzige
|
||
Seite eigens dafür angelegt werden musste. Eine Untergrenze wird nie durch eine erfundene Seite
|
||
gefüllt, sondern durch eine echte Quelle beim nächsten passenden Ingest - Invariante 3 gilt
|
||
unverändert.
|
||
|
||
**Die Leitplanke für reaktive Fixes** unterscheidet drei Stufen: punktuelle Änderungen (immer
|
||
erlaubt, gewöhnliche Arbeit), korpusweite Änderungen (nur geplant, mit eigenem Issue und
|
||
`work/`-Run - trifft eine Session das Mass-Update-Gate während sie etwas anderes tat, holt sie
|
||
sich nicht den `--confirm`-Token, sondern stoppt und legt ein Issue an) und reaktive Eingriffe
|
||
in Korpusinhalt, um einen Test grün zu machen oder einen Tool-Bug zu umgehen (nie erlaubt,
|
||
Invariante 7). Das Verhältnis zu `kb_dir`/`raw_dir` und `test_pipeline_l0.py` bleibt wie im
|
||
ursprünglichen Befund: kleiner, isolierter Fall in der Fixture, großer, vernetzter Fall in
|
||
`kb/` - keine Fixture-Extraktion aus dem Korpus.
|
||
|
||
Dev-only und rein additiv - kein Feld, kein Kommando, keine Datei außerhalb von
|
||
`instructions/dev/` ändert sich, daher `--minor` ohne `--breaking`.
|
||
|
||
**Migration:** none required.
|
||
|
||
Berührt: `instructions/dev/corpus-policy.md` (neu),
|
||
`instructions/dev/stack-dev/SKILL.md` (Schritt 2, Routing-Zeile).
|
||
|
||
---
|
||
|
||
## 4.1.2 - 2026-09-03 - Issue-Abschluss ist ein Body-Rewrite, nicht nur ein Kommentar
|
||
|
||
**Author:** Torben Nehmer
|
||
|
||
Aufgefallen beim Schließen von #44: der Abschlussbericht stand als Kommentar da, der Body
|
||
darunter weiterhin als offene Arbeit — Abschnitt „Zu entscheiden" über eine längst getroffene
|
||
Entscheidung, ungehakte Checkliste, Präsens über einen Defekt, den es nicht mehr gab.
|
||
|
||
Die Regel gab es dafür schon: Schritt 2 von `instructions/dev/issue-tracking.md` sagt, der Body
|
||
ist die aktuelle Wahrheit und wird umgeschrieben, wenn sich der Stand ändert. Nur ließ die
|
||
Formulierung offen, *wann* — und Schritt 7 („Close with what actually happened") war vollständig
|
||
erfüllbar, ohne den Body anzufassen. Ein Abschlussbericht im Kommentar fühlt sich beim Schreiben
|
||
vollständig an; dass der Body dabei zurückbleibt, merkt erst der nächste Leser.
|
||
|
||
**Schritt 2 ist deshalb schärfer geworden: der Body ist das Plan-File dieses Stacks.** Dasselbe,
|
||
was das Plan-Dokument eines Harness ist, und genauso gepflegt — fortlaufend, sobald etwas darin
|
||
nicht mehr stimmt, nicht am Ende. Der Maßstab ist der Abbruch, nicht der Meilenstein: eine
|
||
Session kann jederzeit enden, und was der Body in diesem Moment sagt, ist die vollständige
|
||
Übergabe. Eine frische Session muss zu **jedem** Zeitpunkt allein aus dem Body weiterarbeiten
|
||
können, ohne Kommentare rückwärts zu lesen und ohne einen Menschen, der es neu erklärt. Entschieden
|
||
ersetzt die Frage, erledigt hakt das Kriterium ab, verworfen steht mit Begründung dort, wo das
|
||
Kriterium stand.
|
||
|
||
Schritt 7 ist damit kein Sonderakt mehr, sondern die letzte dieser Aktualisierungen: erst Body
|
||
auf den Endstand, dann schließen, dann die Changelog-Zeile aus Schritt 3. Wer Schritt 2 befolgt
|
||
hat, ist fast fertig; wer nicht, zahlt die ganze Schuld im schlechtesten Moment — der
|
||
geschlossene Body ist die Fassung, die danach alle lesen und niemand mehr aufsucht. #44 steht
|
||
als Beispiel drin.
|
||
|
||
Schritt 3 zieht die Konsequenz: **ein Kommentar pro Session-Umfang, nicht pro Edit.** Ein
|
||
fortlaufend gepflegter Body mit einem Changelog-Kommentar je Änderung wäre Lärm; triviale Pflege
|
||
braucht gar keinen. Der `stack-dev`-Skill sagt es beim Aufgreifen mit, weil dort die Entscheidung
|
||
fällt, ob eine Session den Body überhaupt anfasst.
|
||
|
||
**Und die ehrliche Antwort auf die Frage nach dem Tooling: es gibt keins, und es soll keins
|
||
geben.** `wikitool` kennt diesen Tracker nicht. Es wird an Instanzen ausgeliefert, die unter
|
||
dieser URL keine Issues haben, während `instructions/dev/` von `dist export` gepruned wird —
|
||
ein Gitea-Client im ausgelieferten Tool wäre eine Dev-Abhängigkeit, die jede Instanz mitträgt,
|
||
um ein Board zu prüfen, das keine von ihnen hat. Der Tracker ist ausschließlich über
|
||
`gitea-mcp` erreichbar, also in einer Session, durch einen Agenten.
|
||
|
||
Kein `docs verify` fängt also einen geschlossenen Issue, dessen Body offen klingt, einen Body,
|
||
der seinen eigenen Kommentaren widerspricht, oder ein fehlendes Pflichtlabel. Das steht jetzt
|
||
als eigener Abschnitt „What no tool checks" in der Instruktion — nicht als Bedauern, sondern als
|
||
Begründung dafür, warum die Reihenfolge in Schritt 7 ausgeschrieben ist statt aus Schritt 2
|
||
erschlossen zu werden.
|
||
|
||
---
|
||
|
||
## 4.1.1 - 2026-09-03 - Testisolation: kb_dir repointet config.ROOT, lint löst Kollektionen gegen den übergebenen Baum auf
|
||
|
||
**Author:** Torben Nehmer
|
||
|
||
Issue #44, gefunden beim Bau der Migrations-Gate-Tests für 4.1.0: die `kb_dir`-Fixture baute
|
||
ihren Baum unter `tmp_path`, ließ `config.ROOT` aber auf dem echten Checkout stehen. Jeder
|
||
Codepfad, der eine Datei über `config.ROOT`/`config.KB_DIR` auflöst statt über das übergebene
|
||
Verzeichnis, traf damit das echte Repository.
|
||
|
||
**Der laute Fall** war ein Test, der `kb_state.write_kb_state()` rief und dabei das
|
||
`.wikitool-kb.json` des Repos überschrieb — Applied-Ledger leer statt zwei Einträgen. In
|
||
`git status` sofort sichtbar und reversibel; bei einer gitignorierten Datei wäre es das nicht
|
||
gewesen.
|
||
|
||
**Der stillere Fall** ist der teurere. `lint`s Kollektions-Lookup löste eine Seite gegen
|
||
`config.KB_DIR` auf. Für eine Seite unter `tmp_path/kb/` warf das `ValueError`, die Funktion
|
||
antwortete „keine Kollektion", und die Label-Autorisierung übersprang die Kante wortlos.
|
||
`unauthorised_labels` war damit faktisch ungetestet — jeder Test, der das Finding hätte
|
||
auslösen können, bekam eine leere Liste und behauptete nichts. Ein grüner Lauf, der wie eine
|
||
Zusicherung aussah.
|
||
|
||
**Der Fix ist der Codepfad, nicht die Fixture.** `run_lint()` bekommt ein Verzeichnis
|
||
übergeben und löst jetzt auch intern dagegen auf; `authorised_labels()` bekommt denselben Baum
|
||
gereicht, statt auf `config.KB_DIR` zurückzufallen. Der Regressionstest lintet einen Baum, von
|
||
dem `ROOT` bewusst wegzeigt — genau der Fall, den die alte Auflösung verschluckte. Eine Funktion,
|
||
die ein Verzeichnis entgegennimmt, löst dagegen auf: keine Fixture kann diese Form von außen
|
||
reparieren.
|
||
|
||
**Beide Korpus-Fixturen repointen jetzt.** `kb_dir` tut, was `raw_dir` längst tat — `ROOT` auf
|
||
das eigene `tmp_path`, plus `use_shipped_type_specs()`. Der Suite-Lauf kippte dadurch keinen
|
||
einzigen Test. Die lokale `rooted_kb`-Umgehung aus 4.1.0 entfällt damit; die Auswahl zwischen
|
||
zwei fast gleichen Fixturen war Wissen, das nirgends stand.
|
||
|
||
**Und ein Wächter für die ganze Klasse.** `repository_tree_guard` (session-scoped, autouse)
|
||
vergleicht `git status --porcelain` vor und nach dem Lauf und lässt die Suite scheitern, wenn
|
||
sich im Checkout etwas bewegt hat — zwei `git status`-Aufrufe pro Lauf, deshalb per Default an.
|
||
Er vergleicht vorher gegen nachher statt einen sauberen Baum zu verlangen, sagt also nichts über
|
||
die unveröffentlichte Arbeit des Entwicklers. Den Verursacher benennt er nicht;
|
||
`CHEMENU_TREE_GUARD=each` prüft nach jedem Test und tut es. Ohne git oder außerhalb eines
|
||
Repositorys sind beide still.
|
||
|
||
Was der Wächter nicht sieht: eine Prüfung, die unter Test nichts tut, schreibt keine Datei.
|
||
Dagegen hilft nur ein Test, der das Finding tatsächlich auslöst — der neue tut das.
|
||
|
||
`instructions/dev/testing-conventions.md` hat dafür einen eigenen Abschnitt („Which tree a test
|
||
writes into"), einen Schritt in der Checkliste und die Regel für neue Fixturen.
|
||
|
||
---
|
||
|
||
## 4.1.0 - 2026-09-03 - Link-Taxonomie: Lint-Findings hart ab kb_version 4.0.0, outbound: an das Type-Spec gebunden, part-of/composition als Inversenpaar
|
||
|
||
**Author:** Torben Nehmer
|
||
|
||
Der Rest von Issue #40, nachdem die Korpus-Migration durch ist: die beiden aufgeschobenen
|
||
Lint-Findings werden hart, und die drei Befunde aus dem Abschlusskommentar des Migrationslaufs
|
||
werden aufgelöst.
|
||
|
||
**`unlabelled_edges` und `unauthorised_labels` sind harte Fehler — aber an `kb_version`
|
||
gebunden, nicht an ein Datum.** Der Weg, den `legacy_citation_markers` genommen hat, war ein
|
||
Umlegen in einer späteren Version: eine Instanz, die die Zitat-Migration noch schuldete, lebte
|
||
danach mit rotem Lint. Das Ledger kann die Frage inzwischen beantworten, also tut es das.
|
||
Unterhalb `kb_version` 4.0.0 bleiben beide beratend — genau das Fenster, in dem
|
||
`instructions/migrations/4.0.0-link-taxonomy.md` der Instanz sagt, sie solle den halb
|
||
konvertierten Korpus Einheit für Einheit publizieren; ein Check, der dabei fehlschlägt, würde
|
||
den Korpus verweigern, dessen Fortschritt er misst. Ab 4.0.0 ist eine kahle Titelangabe in
|
||
`related:` keine Seite mehr, die auf ihre Umstellung wartet, sondern eine Kante, deren Autor
|
||
nicht gesagt hat, was sie behauptet. `hard_error_keys()` liefert die jeweils geltende Menge,
|
||
`HARD_ERROR_KEYS` bleibt die vollständige.
|
||
|
||
**`outbound:` ist an das Type-Spec gebunden.** `kb/sources/` und `kb/comparisons/`
|
||
autorisierten Label, die dort strukturell nicht schreibbar waren: keiner der beiden Type-Specs
|
||
führte ein `related:`. Folgenlos war das nicht — die einzige Comparison-Seite des Korpus trug
|
||
`- **compares-with:** [[amd-pstate]]` als *handgeschriebene Prosa*, ohne Marker-Region, ohne
|
||
Frontmatter, für `lint` unsichtbar. Also ein Identifier zurück im Fließtext, gut vier Stunden
|
||
nachdem 4.0.0 genau das beendet hatte. Eine leere Autorisierung liest sich als Lizenz.
|
||
|
||
Aufgelöst nach dem, was die beiden Contracts jeweils selbst sagen: `comparison` bekommt ein
|
||
`related:` (die `compares-with`-Kante gegen jedes Subjekt ist die eine Aussage, für die die
|
||
Seite existiert), `kb/sources/` verliert seinen `outbound:`-Block ersatzlos (dessen Contract
|
||
sagt ausdrücklich, seine Verknüpfungen seien der mechanische Provenance-Pfad und keine
|
||
Autorenkanten). Neu prüft `docs verify` die Kombination: ein `outbound:`-Block auf einer
|
||
Collection, in die kein Typ mit `related:` schreibt, ist ein Befund und nennt beide Richtungen
|
||
der Reparatur.
|
||
|
||
**`composition` / `part-of` ist das dritte Inversenpaar**, neben `depends-on` / `required-by`
|
||
und `runs-on` / `hosts`. Aus der Messung, nicht vom Schreibtisch: der u3-Lauf hatte entschieden,
|
||
die Gegenseite eines `composition` bekomme `see-also`, weil `part-of` ein Spiegel wäre. Ist es
|
||
nicht — der Satz des Elternteils zählt seine Teile auf, der des Kindes benennt das Ganze, zu
|
||
dem es gehört, und ein Leser, der auf dem Kind landet, braucht den zweiten. Übrig blieben 16
|
||
`see-also`-Kanten für eine Beziehung, für die der Katalog ein Wort hat; sie sind auf `part-of`
|
||
umgestellt. Ein Inversenpaar macht die Gegenkante weiterhin **nicht** zur Pflicht — Richtung
|
||
wird verfasst, nicht gespiegelt —, es legt nur fest, welches Label sie trägt, wenn jemand sie
|
||
schreibt.
|
||
|
||
**Stack- und Korpusänderung laufen hier in einem Zug**, entgegen der sonstigen Trennung. Der
|
||
neue `docs verify`-Check würde eine bestehende 4.0.x-Instanz beim bloßen Kopieren der neuen
|
||
Maschinerie fehlschlagen lassen, weil deren `kb/sources/COLLECTION.md` den `outbound:`-Block
|
||
noch trägt — nach [instructions/dev/version-parts.md](instructions/dev/version-parts.md)
|
||
Schritt 1 ein Grenzübertritt. Statt dafür eine `5.0.0` zu lösen, ist die Ursache mitbeseitigt:
|
||
die Collection-Contracts dieser Instanz sind angepasst, und `dist export` leitet die
|
||
`COLLECTION.md.template` daraus ab, also liefert jede neue Distribution die korrigierte Form
|
||
aus. Für eine bereits bestehende 4.0.x-Instanz bleibt eine Handbewegung übrig, und sie wird
|
||
hier benannt statt versteckt: die zwei `outbound:`-Zeilen aus `kb/sources/COLLECTION.md`
|
||
löschen. Das neue `related:` im `comparison`-Type-Spec erreicht sie ohnehin nicht — die vier
|
||
Page-Type-Specs gehören seit 4.0.0 der Instanz und werden nur als `.template` ausgeliefert.
|
||
|
||
Offen aus #40 bleibt nichts mehr; Befund 2 des Migrationslaufs (dem Katalog fehlt ein Register
|
||
für Urheberschaft) ist als eigenes Issue erfasst.
|
||
|
||
---
|
||
|
||
## 4.0.1 - 2026-09-02 - Issue-Board: vier Pflicht-Label-Familien und Body-als-Wahrheit
|
||
|
||
**Author:** Torben Nehmer
|
||
|
||
Das Issue-Schema aus 1.2.1 hatte zwei Pflichtachsen und einen ausdrücklich begründeten Verzicht
|
||
auf eine dritte: eine Taxonomie mit mehr Achsen brauche eigene Pflege, und das Board habe einen
|
||
einzigen Betreuer. Diese Begründung ist entfallen, weil die Pflege inzwischen maschinell
|
||
passiert - Body-Rewrites und Kommentare laufen über eine LLM-Sitzung, Menschen fassen in der
|
||
Regel nur Labels an. Damit sind vier Achsen bezahlbar (Issue #41).
|
||
|
||
**Pflicht auf jedem offenen Issue sind jetzt vier Label:** `area/` (`kb`, `distribution`,
|
||
`corpus`, `workflow`, `process` - kein `area/tools`, Tooling wird nach der bedienten Domäne
|
||
einsortiert, nicht nach Codeort), `kind/` (`decision`, `build`, `defect`), `prio/`
|
||
(`blocking`, `planned`, `waiting` - reine Umbenennung von `1`/`2`/`3`) und `size/` (`S`, `M`,
|
||
`L`; `XS` entfällt). Dazu zwei optionale Flags: `status/blocked` für Abhängigkeit von einem
|
||
anderen offenen Issue, `status/unconfirmed` für einen ungeprüften Verdacht, unter dem `size`
|
||
und `prio` vorläufig sind. Ein `unconfirmed`-Issue endet in der Triage entweder ohne Flag und
|
||
mit verbindlichen Werten oder geschlossen mit Begründung - die Prozessentsprechung zu
|
||
Invariante 3.
|
||
|
||
**Der Issue-Body ist ab jetzt aktuelle Wahrheit, nicht Ursprungstext.** Die Umsetzung eines
|
||
Issues zieht sich über mehrere, zeitlich getrennte Sitzungen, und der Body ist das einzige, was
|
||
sie verbindet: eine Sitzung muss aus ihm allein rekonstruieren können, was entschieden und was
|
||
offen ist. Er wird deshalb umgeschrieben statt ergänzt. Jeder Rewrite bekommt einen Kommentar,
|
||
der ausschließlich benennt, was sich geändert hat - keine Vollkopie des alten Stands, weil ein
|
||
Mensch zwei Fließtexte nicht diffen kann und eine Kopie pro Revision damit keine Historie ist,
|
||
sondern nur eine weitere Kopie.
|
||
|
||
Geändert: [instructions/dev/issue-tracking.md](instructions/dev/issue-tracking.md) (Schritte 2,
|
||
3, 5 neu; Schritt 4 um `area/` und `kind/` erweitert; der Entscheidungspunkt „Two labels feel
|
||
too coarse?" entfällt) und die Beschreibungszeile in `instructions/dev/stack-dev/SKILL.md`. Für
|
||
eine ausgelieferte Instanz ändert sich nichts: `dist export` schließt `instructions/dev/`
|
||
vollständig aus, weshalb dies ein PATCH ist und kein MINOR - dieselbe Begründung wie bei
|
||
`1.2.1`. Noch offen aus #41: `kb/concepts/Issue Label Scheme.md` beschreibt weiterhin das
|
||
zweiachsige Schema und braucht eine eigene `wiki-manage`-Sitzung.
|
||
|
||
---
|
||
|
||
## 4.0.0 - 2026-09-02 - Prosa ist kein Identifier: Link-Taxonomie als Enum, generierte Regionen mit Markern
|
||
|
||
**Author:** Torben Nehmer
|
||
|
||
**Breaking Change:** Beziehungslabel sind Enum-Werte in related: statt Freitext im Body-Bullet, toolgefuehrte Abschnitte liegen zwischen Marker-Paaren statt hinter ihrer Ueberschrift, und xref add schreibt nur noch eine Kante statt beider Richtungen. tools/chemenu/sections.py ist geloescht. Eine bestehende Instanz muss sections: in kb/CONVENTIONS.md auf links/footnotes umstellen, outbound: in jede COLLECTION.md eintragen, die {section.*}-Variablen aus ihren Page-Type-Templates entfernen und den Korpus umstellen - sonst scaffoldet new die Variablen woertlich in neue Seiten. Ablauf: instructions/migrations/4.0.0-link-taxonomy.md
|
||
|
||
Der Stack benutzte an drei Stellen **Prosa als Identifier**, und jede hat messbar etwas
|
||
gekostet. Die Überschrift eines Abschnitts war seine Adresse (`^## Beziehungen$`), was die
|
||
KB-Sprache zu einer Compiler-Konstante machte *und* das Ende der Region zur Schätzung - sie lief
|
||
bis zur nächsten Überschrift, davor bis zum Dateiende, und hat auf acht Seiten still Inhalt
|
||
gelöscht. Das Beziehungslabel stand nur im Body-Bullet, also konnte nichts das Vokabular prüfen:
|
||
gemessen am Korpus **152 distinkte Label in 337 Bullets** gegen dreizehn dokumentierte, 102 davon
|
||
genau einmal vorkommend. Und `xref add` spiegelte jede Kante, was `## Siehe auch` mit 555
|
||
Bullets ohne Label füllte - 353 davon beweisbar redundant.
|
||
|
||
**Was jetzt Identifier ist.** Eine Region liegt zwischen `<!-- wikitool:links -->` bzw.
|
||
`<!-- wikitool:footnotes -->` und wird vollständig aus dem Frontmatter gerendert, Überschrift
|
||
eingeschlossen. Ein Label ist ein Maschinenwert in `related:` (`- depends-on: Hermes`), gezogen
|
||
aus `instructions/link-taxonomy.md` und **pro Ziel autorisiert von der Quell-Collection**
|
||
(`outbound:` im `COLLECTION.md`, Commonplaces ADR-019). Der Body-Bullet ist eine Darstellung
|
||
dieser Daten, nicht ihr zweiter Aufbewahrungsort.
|
||
|
||
**Gelöscht, ersatzlos:** `tools/chemenu/sections.py` komplett, `heading_re`, der
|
||
Alias-Mechanismus, `PRE_CONVENTIONS_NAMES`, `cite_block_heading`, `provenance.__getattr__`, die
|
||
`{section.*}`-Template-Variablen, `xref`s Abschnittssuche. Kein Überschriftentext liegt mehr in
|
||
Python - bis auf zwei kosmetische Fallbacks, und die sind harmlos geworden: der Marker trägt die
|
||
Identität, also rendert ein falscher Default falsche Wörter statt Struktur zu zerlegen, und der
|
||
nächste Write repariert es.
|
||
|
||
**Kanten sind direktional, und das war keine Geschmacksfrage.** Die per-Collection-Autorisierung
|
||
ist mit einer automatisch gespiegelten Gegenkante logisch unverträglich: die Spiegelhälfte
|
||
entsteht in einer Collection, deren Regeln der Autor nie gelesen hat. Entweder schriebe das
|
||
Werkzeug unautorisierte Kanten, oder die Regel "die Quellcollection entscheidet" löst sich auf.
|
||
Der Navigationseinwand wird dabei *besser* beantwortet als vorher: `wikitool links show --page`
|
||
berechnet die Eingangssicht über den Korpus, vollständig und ohne Pflege, und das gerenderte
|
||
Bullet ist ein gewöhnlicher `[[wikilink]]` - ein Backlink-Panel zeigt es ohnehin. Die erzwungene
|
||
Gegenkante garantierte nie Vollständigkeit, nur dass jemand daran gedacht hat.
|
||
|
||
**Der Orphan-Check meldet dadurch mehr,** und das ist die Prüfung bei der Arbeit: sie misst jetzt
|
||
Erreichbarkeit statt "ist `xref` gelaufen".
|
||
|
||
**`obligation:` trennt zwei Achsen, die vorher eine waren.** `migration_kind:` sagt *wie*
|
||
gearbeitet wird, neu `obligation: required|offered` *ob* überhaupt. Eine `offered`-Migration ist
|
||
ein Angebot für eine Datei, die der Instanz gehört - sie blockiert nie, steht nicht in der Kette,
|
||
und `migrate done` verbucht sie im Ledger, **ohne** `kb_version` zu bewegen. Genau daran hing ein
|
||
Entwurfsfehler, den erst der Test gezeigt hat: Offers gegen `kb_version` zu filtern hätte jede
|
||
Offer verschwinden lassen, sobald irgendein unbeteiligter Pflichtschritt lief. Dazu ist die
|
||
Erkennungshälfte aktiviert, die seit ihrer Einführung ungelesen dalag - die sha256 pro Datei in
|
||
`.wikitool-release.json` beantwortet jetzt "editiert oder nur empfangen", also ob eine Offer
|
||
kopiert werden darf oder von Hand abgeglichen werden muss.
|
||
|
||
**`types/` teilt sich entlang `root:`.** `root: kb` heißt Wissensseite heißt Instanz: die vier
|
||
Page-Type-Specs samt Schemas gehen als `.template`, `instruction`/`lint-report`/`type-spec`
|
||
verbatim. Damit ist die deutsche Prosa in jenen vier Dateien **korrekt statt Migrationsschuld** -
|
||
es war die richtige Sprache an einem Ort mit falsch deklariertem Eigentümer. Was der Stack von
|
||
der Type-Schicht noch verlangt, ist eine Zeile: ein Type-Spec `name: source`, dessen Schema
|
||
`raw_files` fordert. `STACK_REQUIRED_COLLECTIONS` entfällt als separate Liste - die pflichtige
|
||
Collection wird aus dem `base_dir` dieses Typs abgeleitet.
|
||
|
||
**Warum das MAJOR ist.** Vorwärts: `sections:` hat eine andere Form, `outbound:` fehlt, und die
|
||
in 3.0.0 übernommenen Page-Type-Templates enthalten `{section.*}`-Variablen, die es nicht mehr
|
||
gibt - `new` schriebe sie wörtlich in neue Seiten. Rückwärts: 4.0.0 schreibt gelabelte Kanten,
|
||
die 3.0.0s Schema als `type: string` ablehnt. Beide Hälften des Drop-in-Tests fallen.
|
||
|
||
**Der Korpus dieser Instanz ist noch nicht umgestellt.** Diese Version liefert die Maschinerie;
|
||
`lint` meldet die 480 noch ungelabelten Kanten als Findings, nicht als Fehler, weil das genau das
|
||
Fenster ist, für das `.wikitool-kb.json` existiert. `malformed_edges` und `unbalanced_markers`
|
||
sind dagegen sofort hart - keines beschreibt eine unkonvertierte Seite, nur eine kaputte. Die
|
||
Beförderung der beiden anderen kommt, wenn der Korpus sie bestehen kann.
|
||
|
||
|
||
---
|
||
|
||
## 3.0.0 - 2026-09-02 - Autorenkonventionen nach Eigentum geschnitten: kb/CONVENTIONS.md, deklarierte Collections
|
||
|
||
**Author:** Torben Nehmer
|
||
|
||
**Breaking Change:** kb/CONTRACT.md ist um alles gekuerzt, was eine Instanz selbst entscheidet; das steht jetzt in einer neuen, instanzeigenen kb/CONVENTIONS.md, aus der der Compiler die drei toolgefuehrten Abschnittsnamen liest. Eine bestehende Instanz muss diese Datei anlegen, auf jedem kb/*/COLLECTION.md profile: und required_by_stack: deklarieren und kb/CONTRACT.md aus dem Release nachziehen - sonst FAILt doctor und docs verify bricht. Ablauf: instructions/migrations/3.0.0-authoring-conventions.md
|
||
|
||
`kb/CONTRACT.md` war eine Datei mit zwei Autoritäten. Der eine Teil ist code-erzwungen und in
|
||
jeder Instanz gleich; der andere - **§ Language komplett**, das Beziehungslabel-Vokabular, die
|
||
Tonfall-Beispiele samt deutscher Buzzword-Liste, die Confidence-Rubrik, das ADR-Präfix - ist
|
||
Konvention, die jede Instanz für sich entscheidet, und wurde trotzdem als bindender Contract
|
||
verbatim ausgeliefert. Wer bei Schritt 5 von `setup-instance.md` "Englisch" antwortete, hatte
|
||
danach `kb/CONTRACT.md`, vier Type-Specs **und `tools/chemenu/sections.py`** lokal geändert -
|
||
und `private-instance.md`s Decision Point sagt für so einen Merge-Konflikt: Upstream-Seite
|
||
nehmen. Für diese Instanz hieß das: KB-Sprache zurück auf Deutsch.
|
||
|
||
**Der Schnitt läuft jetzt danach, wer den Satz ändern darf.** `kb/CONTRACT.md` behält, was
|
||
`wikitool` erzwingt; neu daneben liegt `kb/CONVENTIONS.md`, die **genauso bindet** und der
|
||
Instanz gehört. Unterschied ist Eigentum, nicht Autorität - deshalb liefert die Distribution nur
|
||
`kb/CONVENTIONS.md.template`, exakt der `USER.md`/`SOUL.md`-Split ein Verzeichnis tiefer. Dazu
|
||
`instructions/kb-profiles.md`: der Katalog erprobter Profile, ausdrücklich **Palette und kein
|
||
Enum**. Übernommen wird der *Text* in die Instanzdatei, nie ein Verweis auf den Katalog - ein
|
||
Verweis wäre wieder genau die Konstruktion, die dieser Release beendet.
|
||
|
||
**`sections.py` hält keine Überschrift mehr.** `RELATIONSHIPS = "Beziehungen"` war die Stelle,
|
||
an der die Konvention in Code übergelaufen war: solange sie dort stand, konnte kein Template die
|
||
Sprache umstellen. Neu ist `tools/chemenu/conventions.py`, das die drei Namen aus
|
||
`kb/CONVENTIONS.md` liest; `sections.py` löst sie per PEP 562 bei jedem Zugriff auf, wie
|
||
`config` seine Pfade - ein Modulkonstante hätte den Wert an den Baum gebunden, in dem der Prozess
|
||
gestartet ist. Aus demselben Grund ist `provenance.CITE_BLOCK_HEADING` ein `__getattr__` und
|
||
`render_cite_block(heading=None)` löst innerhalb des Aufrufs auf. Der Alias-Mechanismus, den das
|
||
Modul schon hatte, **ist** der Migrationspfad: erkannt wird die kanonische Form plus die
|
||
deklarierten `section_aliases:` plus das, was dieser Stack vor der Konventionsdatei geschrieben
|
||
hat. Ohne Datei antwortet dieser Fallback - richtig für jeden Korpus, der ihn erreichen kann,
|
||
denn der wurde unter genau diesen Namen geschrieben; `doctor` ist die laute Hälfte davon.
|
||
|
||
**Die vier Page-Type-Specs schreiben `## {section.relationships}`** statt einer Überschrift.
|
||
Neue Template-Variablen `{section.relationships}` / `{section.see_also}` / `{section.footnotes}`,
|
||
gefüllt aus der Instanzdeklaration. Damit ändert eine anderssprachige Instanz **keine Datei unter
|
||
`tools/` oder `types/`** mehr - was Schritt 5 von `setup-instance.md` von fünf Editierstellen
|
||
über drei Schichten auf eine Entscheidung reduziert.
|
||
|
||
**`COLLECTION.md` bekommt Frontmatter.** Bisher wurde eine Collection rein an der Dateipräsenz
|
||
erkannt; die Deklaration brauchte einen Träger, sonst wäre der Ortsschnitt nur durch einen
|
||
Prosaschnitt ersetzt worden. `profile:` nennt den übernommenen Katalogeintrag (Freitext - eine
|
||
selbst angelegte Collection hat dort keinen), `required_by_stack:` sagt, ob `wikitool` die
|
||
Collection *namentlich* auflöst. Das zweite ist **nicht** die Wahl der Instanz: `docs verify`
|
||
prüft es beidseitig gegen `kb_collections.STACK_REQUIRED_COLLECTIONS`. Heute steht dort genau
|
||
`sources` - `sources coverage`, die `[^cite-id]`-Auflösung und `kb/provenance.md` hängen an dem
|
||
Namen, `entities` an keinem.
|
||
|
||
**Das zweite Leck der Merge-Prozedur ist zu.** `git checkout HEAD -- kb raw` holte *alles* unter
|
||
beiden Stages auf den Vor-Merge-Stand - auch `kb/CONTRACT.md` und `raw/CONTRACT.md`. Änderte der
|
||
Upstream einen davon, warf die Prozedur das Update still weg, und die Kontrollzeile meldete dabei
|
||
*leer*, bestätigte den Fehler also, statt ihn zu fangen. `private-instance.md` nimmt die
|
||
Upstream-Seite jetzt für die drei Maschinerie-Pfade unter den Content-Stages zurück
|
||
(`kb/CONTRACT.md`, `kb/CONVENTIONS.md.template`, `raw/CONTRACT.md`) und schließt sie aus der
|
||
Kontrollzeile aus. Dieselbe Altlast in der Tarball-Richtung: `INSTALL.md` Schritt 3 fasste `kb/`
|
||
gar nicht an und zog `kb/CONTRACT.md` damit nie nach - jetzt ausdrücklich benannt.
|
||
|
||
**Verworfen, gemessen: `sources/` aus `kb/` herausziehen.** Der Graph ist einwurzelig
|
||
(`kb_scan.iter_kb_pages` macht ein `rglob` über `kb/`, darauf sitzen Link-Graph, Orphan-Check,
|
||
`index rebuild` und `search`), und Source-Seiten sind darin der dichteste Knotentyp. Ein Hoist
|
||
machte jede Graph-Operation dauerhaft zweiwurzelig, um ein Verzeichnis umzubenennen. Vor allem
|
||
aber kann der *Ort* Eigentum ohnehin nicht kodieren, sobald Collections offen sind: eine selbst
|
||
angelegte liegt im selben `kb/` wie die Defaults. Eigentum ist eine deklarierte Eigenschaft -
|
||
daher das Frontmatter oben. Gitea #39 trägt die Ablehnung im Volltext.
|
||
|
||
**Warum das MAJOR ist.** Die Rückwärtshälfte des Drop-in-Tests hält - 2.5.0 ignoriert beide neuen
|
||
Deklarationen folgenlos. Die Vorwärtshälfte nicht: nach dem Kopieren der Maschinerie FAILt
|
||
`doctor` auf der fehlenden `kb/CONVENTIONS.md`, `docs verify` bricht auf den undeklarierten
|
||
Collections, und `kb/CONTRACT.md` muss aus dem Release nachgezogen werden. Ein Shim war die
|
||
Alternative (`doctor` nur WARN, Pflichtfelder tolerant) und wurde verworfen: er hätte genau den
|
||
Zustand normalisiert, in dem eine Instanz glaubt, sie habe entschieden, während in Wahrheit der
|
||
Fallback antwortet - für eine englische Instanz hieße das `## Beziehungen` in englischen Seiten.
|
||
Die Handarbeit ist eine Datei und zwei Frontmatter-Zeilen je Collection; keine einzige `kb/`-Seite
|
||
ändert sich, weshalb `migrate done 3.0.0 --pages 0` ehrlich und kein Platzhalter ist.
|
||
|
||
---
|
||
|
||
## 2.5.0 - 2026-09-02 - Versionsstelle: Kompatibilitaet statt Inhaltsmigration, Breaking-Change-Vermerk erzwungen
|
||
|
||
**Author:** Torben Nehmer
|
||
|
||
Die Doku des Stacks führte für die Wahl der Versionsstelle zwei Fragen zusammen, die nicht
|
||
dieselbe sind: *muss der Korpus migriert werden?* und *ist die neue Version ein
|
||
Drop-in-Ersatz?* An allen drei Stellen, die einer Session vor dem Bump begegnen - die Tabelle
|
||
in `stack-dev`, der Modul-Docstring in `version.py`, `INSTALL.md` § Version und Updates -
|
||
stand nur die erste. Nur `version bump --help` formulierte es richtig, also ausgerechnet die
|
||
Stelle, die man erst liest, wenn die Entscheidung gefallen ist. Bei `2.0.0` hat genau diese
|
||
Lücke zu `1.9.0` geführt (Gitea #26; der Eintrag zu `2.0.0` trägt den Fall im Volltext).
|
||
|
||
**Die Regel, jetzt an einer Stelle.** Neu ist
|
||
`instructions/dev/version-parts.md`: der Drop-in-Test in zwei Hälften - vorwärts (kopieren,
|
||
fertig, keine Handarbeit durch Nutzer oder Migrationsskript) und rückwärts (die alte Version
|
||
lässt sich noch zurückinstallieren) -, der Katalog der Brüche, die die Grenze bei völlig
|
||
unangetastetem `kb/` überschreiten (Update-Pfad, Artefaktname, Import-Name, Flags, Envvars,
|
||
Shape maschinengelesener Dateien), und der Fall `2.0.0` als Fallbeispiel. `instructions/dev/`,
|
||
also nicht in einer ausgelieferten Instanz - die Datei entscheidet, wer den Stack entwickelt.
|
||
Die ausgelieferten Artefakte tragen deshalb die Kurzform der Regel, aber keinen Verweis auf
|
||
sie: `instructions verify` meldet einen solchen Verweis, weil er nach `dist export` ins Leere
|
||
zeigen würde.
|
||
|
||
**Breaking Changes sind teuer, also genehmigungspflichtig.** `stack-dev` hat einen
|
||
Entscheidungspunkt bekommen: stellt sich eine Änderung als nicht drop-in heraus, wird nicht
|
||
aus eigener Initiative über die Grenze gebumpt. Vorgelegt werden der konkrete Bruch, was jede
|
||
bestehende Instanz von Hand tun muss, und die Alternativen - Bruch per Shim vermeiden,
|
||
aufschieben und mit dem nächsten bündeln, oder in einen kompatiblen und einen brechenden Teil
|
||
mit Deprecation-Fenster spalten -, dazu eine Empfehlung. Dann Freigabe abwarten.
|
||
|
||
**`--breaking` ist neu und bei jedem Grenzübertritt Pflicht.** Es schreibt eine
|
||
`**Breaking Change:**`-Zeile in den Eintrag, vor die Migrationszeile: das ist, was der Operator
|
||
einer bestehenden Instanz beim Lesen der Release-Notes zuerst braucht. `docs verify` prüft den
|
||
neuesten grenzüberschreitenden Eintrag darauf, analog zu `**Migration:** none required` und aus
|
||
demselben Grund - eine Prosa-Regel driftet, ein Marker nicht. Auf einem kompatiblen Bump wird
|
||
`--breaking` verweigert, damit es keine Gewohnheit wird. Beide Prüfungen bleiben getrennt:
|
||
`check_migration_for_boundary` fragt, ob der Inhalt sich bewegt, `check_breaking_change_for_boundary`,
|
||
ob dem Operator gesagt wurde, dass der Tausch keiner zum Drüberkopieren ist. Ein Rebranding
|
||
besteht die erste und fällt durch die zweite.
|
||
|
||
**Was ausdrücklich nicht passiert:** `docs verify` prüft weiterhin nicht, *ob die Stelle
|
||
richtig gewählt* wurde. Kein Validator unterscheidet ein umbenanntes Flag von einem neuen; das
|
||
bleibt Urteil, und dieser Eintrag existiert, weil das Urteil eine Anleitung braucht, keinen
|
||
Automaten.
|
||
|
||
Berührt: `instructions/dev/version-parts.md` (neu), `instructions/dev/stack-dev/SKILL.md`,
|
||
`tools/chemenu/version.py`, `tools/chemenu/commands/version_cmd.py`,
|
||
`tools/chemenu/commands/docs_verify.py`, `tools/CONTRACT.md`, `INSTALL.md`. Sechs neue Tests.
|
||
|
||
---
|
||
|
||
## 2.4.1 - 2026-09-02 - INSTALL-MCP.md: Menschendoku fuer den Leseserver, und sie wird mit ausgeliefert
|
||
|
||
**Author:** Torben Nehmer
|
||
|
||
Nachtrag zu 2.4.0. Der Server war ausgeliefert und für Menschen nur über `tools/CONTRACT.md` und
|
||
eine Instruktion beschrieben — beide für Agenten geschrieben. `INSTALL-MCP.md` schließt das,
|
||
neben `INSTALL.md` und in derselben Form: Voraussetzungen, sechs Schritte, Verifikation,
|
||
Troubleshooting.
|
||
|
||
**Was drinsteht, das sonst nirgends zusammen stand:** die Client-Konfiguration als JSON-Block;
|
||
dass `WIKI_TRACE=0` nicht optional ist und wie die Startverweigerung aussieht; dass
|
||
`--host 0.0.0.0` bewusst nicht der Default ist; und der Verweis auf die
|
||
Traefik-ForwardAuth-Middleware unter <https://gitea.nehmer.net/torben/gitea-mcp-forward-auth>
|
||
samt dem, was sie erwartet (Bearer-Token gegen SHA-256-Hashes, `AUTH_PROXY_TOKEN_HASHES_DIR`).
|
||
Der Verifikationsabschnitt ist ein lauffähiges Skript — wörtlich so ausgeführt, bevor es hier
|
||
landete.
|
||
|
||
**`dist export` liefert die Datei mit.** `ROOT_FILES` ist eine Allowlist, also hätte sie sonst
|
||
keine Instanz erreicht: ein ausgelieferter Server, dessen Bedienungsanleitung im Ursprungs-Repo
|
||
zurückbleibt, ist einer, den der Empfänger durch Quelltextlesen findet. `INSTALL.md` bekommt
|
||
zusätzlich `CHEMENU_ROOT` und `WIKI_TRACE`/`WIKI_TRACE_DIR` in seine Konfigurationstabelle —
|
||
beide waren seit 2.3.0 bzw. seit jeher wirksam und dort nicht aufgeführt.
|
||
|
||
**Dateien:** `INSTALL-MCP.md` (neu), `INSTALL.md`, `README.md`,
|
||
`tools/chemenu/commands/dist_cmd.py`.
|
||
|
||
---
|
||
|
||
## 2.4.0 - 2026-09-02 - MCP-Leseserver: zweiter Konsument auf demselben Kern
|
||
|
||
**Author:** Torben Nehmer
|
||
|
||
Letzter Schritt der Sequenz aus #36, inhaltlich Issue #19. `chemenu` bekommt einen zweiten
|
||
Konsumenten: `search`, `types`, `describe_type`, `lint` und `status` über MCP. Kein CLI mit
|
||
angeschraubter Netzwerkschnittstelle — CLI und Server sind zwei Adapter auf dem Kern, den 2.3.0
|
||
freigelegt hat.
|
||
|
||
**`tools/chemenu/mcp/`**, im Repo statt als eigenes Artefakt. Der Golden-Test, der die
|
||
Serverantworten gegen die `--json`-Formen der CLI hält, läuft nur mit beiden Seiten in einer
|
||
Testsuite; getrennt würde aus einem Contract eine Versionsabsprache. Der Test ruft `wikitool` als
|
||
Subprozess gegen denselben Baum auf, über `CHEMENU_ROOT` — womit er nebenbei die Root-Auflösung
|
||
von außen mitprüft.
|
||
|
||
**Zwei Transports.** `stdio` zum Entwickeln und Testen ohne Netz, `streamable-http` für die
|
||
Auslieferung — der einzige, vor den sich die Authentifizierungs-Middleware überhaupt setzen kann,
|
||
weil sie ein HTTP-Reverse-Proxy ist. `sse` ist über das SDK erreichbar und wird bewusst nicht
|
||
angeboten: der abgelöste Remote-Transport, jetzt darauf zu bauen verschiebt den Wechsel nur.
|
||
`--host`/`--port` gibt es, weil der Default auf Loopback bindet und ein Container hinter einem
|
||
Proxy eine Adresse braucht, die der Proxy erreicht — eine Eigenschaft der Software, nicht einer
|
||
Installation. Beide Transports sind gegen den echten Korpus gegengeprüft.
|
||
|
||
**Kein Schreibpfad, strukturell.** Weder der Server noch `chemenu.api` importiert irgendetwas
|
||
unter `chemenu.commands`, also existieren `new`, `touch`, `xref`, `cite`, `publish`, `migrate`
|
||
und `version bump` in dieser Reichweite gar nicht, statt aus einer Liste gefiltert zu werden. Ein
|
||
Test importiert das Servermodul in einem frischen Interpreter und sieht in `sys.modules` nach;
|
||
ein zweiter ruft alle fünf Tools auf und vergleicht den Dateibaum, `HEAD` und
|
||
`git status --porcelain` vorher/nachher.
|
||
|
||
**Jede Antwort trägt ihren Commit.** `commit` und `as_of` in jedem Payload; `null` heißt, der
|
||
bediente Baum hat uncommittete Änderungen und die Antwort entspricht keiner Revision. Der Stempel
|
||
ist die Revision, aus der die Seiten *tatsächlich* gelesen wurden — zwischen Laden und Stempeln
|
||
kann der Baum sich bewegen, deshalb reicht der Ladepfad seine Revision durch, statt noch einmal
|
||
zu fragen. Das war beim ersten Durchlauf falsch: `types`/`lint`/`status` lasen die zuletzt
|
||
*gecachte* Revision und stempelten `null`, obwohl der Baum sauber war.
|
||
|
||
**Telemetrie in den bedienten Baum wird beim Start verweigert**, nicht stillschweigend
|
||
umgeleitet. Tracing ist per Default an und schreibt nach `reports/telemetry/` im Repo — genau das
|
||
Verzeichnis, das der Sync per `git reset --hard` wegräumen darf. `WIKI_TRACE=0` oder
|
||
`WIKI_TRACE_DIR` außerhalb des Korpus. Heute schreibt auf diesem Pfad nichts (der Emitter hängt an
|
||
`cli.main()` und den Gates), die Sperre ist gegen später.
|
||
|
||
**Fehler an der Protokollgrenze.** Ein `ChemenuError` wird zum `ToolError` des SDK — eine
|
||
absichtliche Ablehnung, deren Text den Aufrufer erreicht. Alles andere bleibt ein Absturz, dessen
|
||
Text auf dem Server bleibt. Ein kaputtes Prädikat ist das Argument des Aufrufers, also muss die
|
||
Zeile mitreisen, die sagt, was stattdessen zu schreiben ist.
|
||
|
||
**Bewusst nicht enthalten:** Authentifizierung und Rate Limiting (Middleware vor dem Prozess),
|
||
Deployment (private Infrastruktur), der Iteration Budget Gate — er begrenzt eine Agenten-Session
|
||
und nicht einen Nutzer, weshalb Retrieval von ihm befreit ist; ihn hier als Rate Limiter zu
|
||
benutzen würde ihn dazu verwässern.
|
||
|
||
**Die Abhängigkeit ist optional** (`tools/requirements-mcp.txt`): eine Instanz, die nur die CLI
|
||
benutzt, soll dafür nicht pydantic, starlette, uvicorn und cryptography installieren müssen. CI
|
||
installiert sie, denn ein übersprungener Golden-Test ist genau der Weg, auf dem Server und CLI
|
||
unbemerkt auseinanderlaufen.
|
||
|
||
Betrieb und Sync-Mechanismus: [instructions/mcp-read-server.md](instructions/mcp-read-server.md).
|
||
Polling (`git fetch && git reset --hard`) statt Webhook — kein eingehender Endpunkt, keine
|
||
Signaturprüfung. `reset --hard` ist dort tragend und keine Bequemlichkeit: ein abgedrifteter Baum
|
||
antwortet zwar richtig, parst aber bei jeder Anfrage neu und stempelt jede Antwort mit `null`.
|
||
|
||
**Dateien:** `chemenu/mcp/` (neu: `server.py`, `__main__.py`), `chemenu/api.py`,
|
||
`tools/requirements-mcp.txt` (neu), `instructions/mcp-read-server.md` (neu), `tools/CONTRACT.md`,
|
||
`tools/README.md`, `.gitea/workflows/ci.yml`, `tests/test_mcp_server.py` (neu).
|
||
|
||
---
|
||
|
||
## 2.3.0 - 2026-09-02 - Bibliotheksgrenze: chemenu laesst sich auf einen Korpus zeigen
|
||
|
||
**Author:** Torben Nehmer
|
||
|
||
Dritter Schritt der Sequenz aus #36, inhaltlich Issue #31. Der Schritt, der `chemenu` von einem
|
||
Skript, das in seinem eigenen Verzeichnis lebt, zu einer Bibliothek macht, auf die man einen
|
||
Korpus *zeigen* kann. Rückwärtskompatibel: ohne gesetzte Variable verhält sich `tools/wikitool`
|
||
unverändert.
|
||
|
||
**Root-Auflösung nach Präzedenz statt nach Dateilage.** `config.resolve_root()`: expliziter
|
||
Parameter → `$CHEMENU_ROOT` → Walk-up wie bisher. Der Walk-up bleibt Default. Wichtiger als die
|
||
neue Fähigkeit ist die beseitigte Fehlerklasse: `ROOT` und alles darunter waren
|
||
Modulkonstanten, also **zur Importzeit gebunden**, und
|
||
`monkeypatch.setattr(config, "ROOT", ziel)` zeigte `ROOT` um, aber nicht `KB_DIR`/`RAW_DIR`. Wer
|
||
sich darauf verließ, baute etwas, das scheinbar auf einem Zielbaum arbeitete und in Wahrheit aus
|
||
dem Entwickler-Checkout antwortete. Die abgeleiteten Pfade werden jetzt bei jedem Zugriff
|
||
aufgelöst (PEP 562 `__getattr__`) und folgen `ROOT` — der halb-umgezeigte Zustand ist nicht mehr
|
||
konstruierbar. `CHEMENU_ROOT` ist in `_WIKITOOL_ENV` registriert, #23-konform.
|
||
|
||
**`config.reset()` und `config.rooted()`.** `reset()` nimmt Zuweisungen auf die verwalteten
|
||
Pfadnamen zurück; die Testsuite ruft es zwischen Tests, und das ist dort nicht optional:
|
||
`monkeypatch` merkt sich den alten Wert, indem es ihn *liest* — also auflöst — und schreibt ihn
|
||
beim Aufräumen als echtes Attribut zurück. Genau die stale Bindung, die der Umbau unmöglich
|
||
machen sollte, vom Cleanup wieder aufgebaut. `rooted(root)` setzt den Root für die Dauer eines
|
||
Blocks, prozessweit und damit nicht thread-sicher — der Aufrufer hält das Lock, dieselbe
|
||
Disziplin wie beim Korpus-Cache. Nötig, weil nicht alles einen Root als Argument nimmt: der
|
||
modulglobale `TypeResolver` muss `types/` finden, und ohne ihn läse ein fremder Korpus mit den
|
||
Type-Specs *dieses* Checkouts.
|
||
|
||
**Die Naht ist gezogen.** `run_search`/`run_lint`/`types` lagen in Modulen, die `typer` auf
|
||
Modulebene importieren und über `_util` auch `rich` — wer sie in-process aufrief, zog den
|
||
kompletten CLI-Kopf mit. Der reine Kern liegt jetzt in `search/service.py`, `lint_core.py` und
|
||
`types_core.py`; `commands/` sind die Terminal-Adapter darüber und re-exportieren die Namen, damit
|
||
kein bestehender Import bricht. Ein Test importiert `chemenu.api` in einem frischen Interpreter
|
||
und prüft, dass weder `chemenu.commands.*` noch `typer`/`rich`/`click` geladen werden.
|
||
|
||
**`chemenu.api.Corpus` als In-Process-Einstieg.** Nimmt einen Root, liefert exakt die
|
||
`--json`-Formen der CLI zurück — ein Wire-Contract statt zwei — und stempelt jede Antwort mit dem
|
||
Commit-SHA und einem Zeitstempel (`commit`, `as_of`), so dass aus einer stillen veralteten Antwort
|
||
eine sichtbare wird. `search`/`lint`/`types`/`describe_type`/`status`; `status` ist bewusst
|
||
serverseitig **komponiert** und kein Wrapper, weil es kein `wikitool status` gibt. Lesend
|
||
strukturell: nichts unter `chemenu.commands` wird importiert, die Schreibfunktionen existieren in
|
||
dieser Oberfläche also gar nicht, statt gefiltert zu werden. Das ist die Grenze, auf der #19
|
||
aufsetzt.
|
||
|
||
**Exceptions statt Exit-Codes an der Grenze.** `chemenu/errors.py`: `ChemenuError` mit
|
||
`ValidationError` (Eingabe abgelehnt) und `BackendError` (Abhängigkeit fehlt oder scheitert).
|
||
`PredicateError`, `FrontmatterError`, `UnknownBackend` und die beiden `Ripgrep*` hängen jetzt
|
||
darunter; `ValidationError` erbt zusätzlich von `ValueError`, weil `PredicateError` vorher eines
|
||
war und Aufrufer es so fangen. Das CLI-Verhalten ist unverändert: `fail()` → `ERROR`-Zeile,
|
||
Exit 1, Budget-Refund.
|
||
|
||
**`resolve()` reicht den Root an das Backend durch.** Vorher konnte ein Aufrufer `run_search` einen
|
||
Korpus übergeben, während `RipgrepBackend` weiter `config.KB_DIR` durchlief — die Anfrage aus dem
|
||
einen Baum beantwortet, die Seiten aus dem anderen gelesen, ohne dass irgendetwas das gesagt
|
||
hätte.
|
||
|
||
**Zwei Abhängigkeiten, die durch Zufall hielten, stehen jetzt da.** `TypeResolver.repo_root`
|
||
folgt `ROOT`, statt beim Import zu binden — womit Fixtures, die `ROOT` auf einen tmp-Baum zeigen,
|
||
die mitgelieferten Type-Specs ausdrücklich benennen müssen (`use_shipped_type_specs`). Dieselbe
|
||
Form wie das Loch, für das `raw_dir` geschrieben wurde, eine Ebene tiefer. Und
|
||
`types describe --json` trug `root:` nicht im Payload, obwohl `types list --json` es tut:
|
||
`types/instruction.md` deklariert `root: repo`, die Renderer-Zeile las es direkt aus dem
|
||
Frontmatter. Jetzt im Payload, in derselben Form wie bei `list`.
|
||
|
||
**Dateien:** `config.py`, `errors.py` (neu), `api.py` (neu), `search/service.py` (neu),
|
||
`lint_core.py` (neu), `types_core.py` (neu), `search/registry.py`, `type_resolver.py`,
|
||
`commands/search.py`, `commands/lint.py`, `commands/types_cmd.py`, `tools/CONTRACT.md`, dazu
|
||
`tests/conftest.py`, `tests/test_api.py` (neu), `tests/test_new_page.py`,
|
||
`tests/test_instructions_cmd.py`.
|
||
|
||
---
|
||
|
||
## 2.2.4 - 2026-09-02 - Haertung des Lesepfads: ReDoS, Subprozess-Timeout, YAML-Alias-Budget, Korpus-Cache
|
||
|
||
**Author:** Torben Nehmer
|
||
|
||
Zweiter Schritt der Sequenz aus #36, inhaltlich Issue #33. Limits vor der Exposition: der
|
||
Lesepfad bekommt mit dem MCP-Server (#19) einen Konsumenten, der nicht der Operator ist. Alle
|
||
sechs Befunde waren reproduziert, nicht vermutet; jeder hat jetzt eine Regression.
|
||
|
||
**ReDoS über `--regex` beseitigt.** `search/ripgrep.py:_contains` gab nutzergesteuerten Regex an
|
||
Pythons Backtracking-Engine: `(\w+\s?)+$` gegen 114 Zeichen gewöhnlichen Seiteninhalts terminiert
|
||
nicht in acht Sekunden, ein deterministisch scheiterndes Muster braucht 0,2 ms — der Unterschied
|
||
ist das Muster, nicht der Heuhaufen. `build_hit` ruft die Funktion je Treffer zweimal auf, und
|
||
`\w` matcht jede Seite: eine Anfrage kaufte zwei unbegrenzte Suchen pro Korpusseite. Der Zweig ist
|
||
**gelöscht**, nicht begrenzt. `rg` hat das Muster mit einer linearen Engine längst angewendet, wenn
|
||
die Funktion überhaupt läuft; verloren geht nur der zusätzliche Titel-/Summary-Bonus für
|
||
nicht-literale Muster, und Summary wie H1 sind selbst Zeilen in der Datei, die `rg` zählt.
|
||
|
||
**Subprozess-Timeout.** `rg` wird nach 30 s abgeräumt und über den vorhandenen
|
||
`RipgrepFailed`-Pfad gemeldet. Kein Performance-Budget — eine Fixed-String-Suche kostet hier 6 ms —
|
||
sondern ein Hänger-Abbruch, damit ein Aufruf als Fehler endet statt den Aufrufer offenzuhalten,
|
||
während seine Ausgabe in den Heap puffert.
|
||
|
||
**YAML-Anchors und -Aliases im Frontmatter werden verweigert, nicht budgetiert.** Gemessen:
|
||
267 Byte werden in 0,2 ms zu 672.603 Knoten beim Traversal, Wachstum 9ⁿ bei konstanter Parse-Zeit
|
||
— ein Größenlimit fasst das nicht an, weil die Eingabe klein bleibt. Die Prüfung läuft auf dem
|
||
*Event*-Strom (`yaml.parse`), der nichts auflöst, kostet also O(Text) und löst nie aus, wonach sie
|
||
fragt; `*` ist in jedem Alias-Knoten notwendig, seine Abwesenheit beweist Abwesenheit ohne jeden
|
||
Parse — der Weg, den jede echte Seite nimmt. Dazu ein Größenlimit von 64 KiB und ein Abfangen von
|
||
`RecursionError` (PyYAML komponiert rekursiv, tiefe Verschachtelung ist kein `YAMLError`). Heute
|
||
nicht erreichbar, weil `kb/` der Operator committet; erreichbar mit der Ingest-Queue (#32).
|
||
|
||
**`CSafeLoader` statt `SafeLoader`, mit Fallback.** Gemessen über diesen Korpus (176 Seiten,
|
||
best of 5): **265 ms → 54 ms**. Kein Mikro-Tuning — der Korpus-Parse war der größte Einzelposten
|
||
eines `search`-Aufrufs und skaliert linear mit der Korpusgröße. End-to-end fällt ein
|
||
`wikitool search` damit von 593 ms auf **347 ms**; die verbleibenden 262 ms sind Modulimport und
|
||
entfallen erst im residenten Prozess (#19).
|
||
|
||
**Stiller Frontmatter-Verlust wird gemeldet.** Entschieden: der Lesepfad *nennt* die Seite, statt
|
||
sie zu schlucken. Kaputtes YAML wird weiterhin zu `{}` — Massenoperationen dürfen an einer Seite
|
||
nicht scheitern —, aber der Grund wird mitgeführt (`Page.frontmatter_error`) und ausgegeben:
|
||
`search --json` trägt immer eine `unreadable`-Liste aus `{path, reason}`, die Tabellenform
|
||
schreibt dieselben Zeilen nach stderr. Das war nötig, weil so eine Seite weder `confidence` noch
|
||
`kind` hat und damit aus jedem positiven `--field`-Prädikat fällt — ausgerechnet aus der
|
||
Low-Confidence-Suche, die Seiten in genau diesem Zustand finden soll — und dabei aussieht wie eine
|
||
Seite, die nicht gematcht hat. Für Frontmatter, das diese Instanz nicht selbst geschrieben hat,
|
||
steht `read_page_strict()` bereit: die Quarantäne aus #32 muss strikt lesen, wo ein
|
||
unlesbares Dokument das Dokument stoppen und nicht leeren soll.
|
||
|
||
**Ein Parser statt zwei.** `read_page()` und `frontmatter_error()` liefen bisher getrennt durch
|
||
`safe_load` — so konnte der permissive Weg zu `{}` degradieren aus einem Grund, den der strikte
|
||
Weg anders beschrieb, und jeder Aufrufer, der beide Antworten wollte, las die Datei zweimal.
|
||
Beide gehen jetzt durch `_load_frontmatter()`; ein Test hält sie gegeneinander.
|
||
|
||
**Korpus-Cache am Commit-SHA** (`chemenu/corpus_cache.py`), als Objekt, das ein Aufrufer *hält* —
|
||
kein Modul-Dict, das sich hinter allen einschaltet. Die CLI hält keins und verhält sich unverändert
|
||
(ein Aufruf pro Prozess, nichts wiederzuverwenden); der residente Prozess aus #19 hält eins.
|
||
Entscheidend ist nicht die Geschwindigkeit, sondern dass nichts veraltet: **ein schmutziger
|
||
Arbeitsbaum wird nie gecacht**, sonst bekäme eine Sitzung, die eine Seite schreibt und danach
|
||
sucht, die Analyse von vor dem Schreiben — bei unverändertem SHA. Kann git nicht antworten, gilt
|
||
der Baum als schmutzig. Derselbe SHA ist der Antwort-Stempel aus #19, per Konstruktion also die
|
||
Revision, aus der die Antwort auch wirklich berechnet wurde.
|
||
|
||
**Erhalten geblieben** ist die Eigenschaft, die kein Befund war: kein `shell=True`,
|
||
`--fixed-strings` als Default, `--`-Terminator. Der Modul-Docstring führt sie jetzt als drei
|
||
tragende Zusagen statt zwei — die dritte ist, dass nutzergesteuerte Muster ausschließlich `rg`
|
||
sieht.
|
||
|
||
**Dateien:** `frontmatter_io.py`, `search/ripgrep.py`, `commands/search.py`, `page.py`,
|
||
`corpus_cache.py` (neu), `tools/CONTRACT.md`, dazu `tests/test_frontmatter_io.py`,
|
||
`tests/test_search.py`, `tests/test_corpus_cache.py` (neu).
|
||
|
||
---
|
||
|
||
## 2.2.3 - 2026-09-02 - Publish-Remote-Gate in diesem Checkout scharf, doctor benennt den Zustand
|
||
|
||
**Author:** Torben Nehmer
|
||
|
||
Erster Schritt der Sequenz aus Issue #36 (Weg zum MCP-Leseserver), inhaltlich Issue #34. Das
|
||
Gate war seit 2.2.0 implementiert und in diesem Checkout **inert**: `.wikitool-remotes.json`
|
||
fehlte, und eine fehlende Datei heißt unbeschränkt. `ENVIRONMENT.md` beschrieb `origin`
|
||
gleichzeitig als einziges Publish-Ziel, `AGENTS.md` führt das Gate unter den drei in Code
|
||
erzwungenen Grenzen. Eine dokumentierte Sicherung, die nicht greift, ist schlechter als eine
|
||
fehlende — sie erzeugt genau das Vertrauen, das sie nicht verdient.
|
||
|
||
**Die Datei ist angelegt** (gitignored, per Checkout, reist nicht mit) und gegengeprüft: ein
|
||
`publish --remote` auf ein nicht gelistetes Ziel verweigert mit Exit 42, bevor irgendetwas
|
||
gestaged wird, und der Arbeitsbaum bleibt unberührt. Damit steht die Sicherung **vor** dem Klonen
|
||
der privaten Instanz (#30) — nachträglich gesetzt ließe sie genau das Fenster offen, das sie
|
||
schließt.
|
||
|
||
**`doctor` sagt jetzt, ob das Gate scharf ist, nicht nur ob die Datei da ist.** Alle drei
|
||
Zustände beginnen mit `Gate armed:` bzw. `Gate not armed:`; der einzelne Remote ohne Allowlist
|
||
bleibt `OK` (er hat nichts zu schützen, und ein FAIL machte die Datei durch die Hintertür
|
||
verpflichtend), sagt aber ausdrücklich, dass jedes Push-Ziel durchkommt. Der Fall, der wirklich
|
||
beißt — mehrere Remotes ohne Allowlist — bleibt `WARN`. Der Check hatte bislang **keine Tests**;
|
||
drei sind dazugekommen, einer je Zustand.
|
||
|
||
**Dateien:** `.wikitool-remotes.json` (neu, nicht committet), `doctor.check_publish_remotes()`,
|
||
`tools/chemenu/tests/test_doctor.py`.
|
||
|
||
---
|
||
|
||
## 2.2.2 - 2026-09-01 - Doku-Verdrahtung: Publish-Remote Gate im Werkzeugvertrag, Projektseite auf oeffentlich
|
||
|
||
**Author:** Torben Nehmer
|
||
|
||
Nachziehen dessen, was 2.2.0 und die Veröffentlichung offen gelassen haben. Gefunden durch eine
|
||
Durchsicht auf lose Enden, nicht durch einen Fehlschlag — `docs verify` deckt den Fall nicht ab,
|
||
weil es Kommando-*Namen* gegeneinander prüft, nicht ob ein neuer Fehlerfall beschrieben ist.
|
||
|
||
**`tools/CONTRACT.md` kannte das Publish-Remote-Gate nicht.** Der Werkzeugvertrag ist die Stelle,
|
||
an der pro Kommando steht, was ein Fehlschlag bedeutet und ob ein Retry sicher ist — und
|
||
`publish` hatte seit 2.2.0 einen dritten Weg zu Exit 42, der dort nirgends stand. Ergänzt in
|
||
beiden Richtungen: in der Kommandozeile (URL statt Remote-Name, `pushurl` wird gelesen, fehlende
|
||
Datei heißt unbeschränkt, kaputte Datei ist ein Fehler) und im Fehlerkontrakt, wo der
|
||
entscheidende Unterschied zu den anderen beiden Gates steht — es gibt **keine** `--confirm`-Zeile,
|
||
die der Agent nachreichen könnte.
|
||
|
||
**`instructions/gates.md` verwies nicht auf die Prozedur, für die das Gate gebaut wurde.**
|
||
Jetzt verlinkt, mit dem Hinweis auf Schritt 4: Das Gate gehört vor den ersten `publish`, später
|
||
hinzugefügt schützt es das Fenster nicht, das es schließen soll.
|
||
|
||
**Die Projektseite beschrieb sich selbst falsch.** `kb/entities/projects/Chemenu.md` nannte
|
||
Chemenu ein „persönliches IT-Wissens-Wiki" mit dem Zweck „Persönliche IT-Wissensbasis" — seit
|
||
der Veröffentlichung schlicht unzutreffend, und es ist die Seite, die ein Fremder über das
|
||
Projekt liest. Neu gefasst: deterministischer Wissenskompiler, diese Instanz seit 2026-09-01
|
||
öffentlich als Testbett und Demo, Lizenz benannt.
|
||
|
||
Die historische Aussage über die monolithische `AGENTS.md` (~30 KB) **bleibt** — sie war zu ihrer
|
||
Zeit korrekt und ist belegt. Sie ist jetzt als Ausgangspunkt datiert statt als Gegenwart
|
||
formuliert, dieselbe Unterscheidung, die #29 für die Issue-Texte trifft: ein Pfad als Wegweiser
|
||
wird nachgezogen, ein Pfad als Beleg bleibt stehen und bekommt ein Datum.
|
||
|
||
**Dateien:** `tools/CONTRACT.md`, `instructions/gates.md`,
|
||
`kb/entities/projects/Chemenu.md`, `kb/entities/INDEX.md` (generiert).
|
||
|
||
---
|
||
|
||
## 2.2.1 - 2026-09-01 - private-instance: der Demo-Korpus wandert beim Merge doch mit - Prozedur korrigiert
|
||
|
||
**Author:** Torben Nehmer
|
||
|
||
`instructions/private-instance.md` behauptete in 2.2.0, ein `git merge upstream/main` löse
|
||
Änderungen am Demo-Korpus stillschweigend auf, weil die private Instanz ihn einmal gelöscht hat:
|
||
*deleted-in-ours, unmodified-in-theirs*. Das war **nicht gemessen, sondern angenommen** — und es
|
||
ist falsch. Ein Nachbau mit einem Upstream, der seinen Korpus bewegt, zeigt drei verschiedene
|
||
Verhalten:
|
||
|
||
| Upstream tut | `git merge upstream/main` tut |
|
||
|---|---|
|
||
| ändert eine Seite, die du gelöscht hast | `CONFLICT (modify/delete)` — und **lässt die Upstream-Fassung im Arbeitsbaum liegen**. Ein `git add -A` beim Auflösen holt die Demo-Seite zurück. |
|
||
| legt eine neue Seite an | staged sie **stillschweigend**. Kein Konflikt, keine Meldung. |
|
||
| löscht eine Seite, die du auch gelöscht hast | nichts. Der einzige harmlose Fall. |
|
||
|
||
Die mittlere Zeile ist die gefährliche, weil nichts sie ankündigt. Ein Upstream, der einen
|
||
Demo-Korpus ausliefert **und** ihn als Testbett benutzt, legt Seiten an — und jede einzelne
|
||
landet in der privaten Instanz und taucht dort in `lint`, `index`, `search` und
|
||
`confidence decay` auf. Genau diese Doppelnutzung beschreibt Issue #28.
|
||
|
||
**Korrigiert:** Die Update-Prozedur hält den Merge mit `--no-commit` offen, zwingt `kb/` und
|
||
`raw/` danach auf den eigenen Stand zurück (`git rm --cached`, `rm -rf`, `git checkout HEAD --`)
|
||
und schließt ihn erst dann. Solange der Merge offen ist, zeigt `HEAD` noch auf den Commit davor,
|
||
und genau das macht den Schritt sauber. Anschließend eine Kontrolle, die man nicht überlesen
|
||
kann:
|
||
|
||
```bash
|
||
git diff --name-only $BEFORE HEAD -- kb raw # muss leer sein
|
||
```
|
||
|
||
Das Rezept ist wörtlich so nachgespielt worden, wie es jetzt in der Datei steht — mit einem
|
||
Upstream, der gleichzeitig eine Seite ändert, eine anlegt, eine löscht und dasselbe unter
|
||
`raw/` tut. Ergebnis: Stack aktualisiert, nur eigener Inhalt übrig, Kontrolle leer,
|
||
Arbeitsbaum sauber.
|
||
|
||
**Auch die Decision Points korrigiert.** „Konflikt in `kb/` per Hand als *keep deleted*
|
||
auflösen" stand vorher da und ist der Rat, der in die Falle führt: `git add -A` committet die
|
||
Fassung, die git im Arbeitsbaum liegen gelassen hat.
|
||
|
||
**Dateien:** `instructions/private-instance.md`.
|
||
|
||
---
|
||
|
||
## 2.2.0 - 2026-09-01 - Publish-Remote Gate: publish schreibt nur an erklaerte Ziele
|
||
|
||
**Author:** Torben Nehmer
|
||
|
||
Der Stack bekommt sein drittes Gate. Die beiden bestehenden fragen, ob eine Änderung zu groß
|
||
ist und ob ein Rebase gefährlich ist. Dieses fragt, was darunter liegt: **ob das überhaupt das
|
||
richtige Repository ist.**
|
||
|
||
**Das Problem entsteht erst durch die private Instanz.** Ein Checkout mit eigenem Inhalt hat
|
||
typischerweise zwei Remotes — sein eigenes und das öffentliche Upstream, von dem er
|
||
Stack-Updates zieht. Git unterscheidet die beim Push nicht, also legt ein falsches `--remote`
|
||
einen privaten Korpus auf ein öffentliches Repository. Das ist nicht billig rückholbar, und
|
||
zwar nachweislich: Beim Veröffentlichen dieses Repos blieb die gesamte alte History nach dem
|
||
Force-Push per SHA abrufbar, bis auf dem Server die Reflogs verfielen und `git gc --prune=now`
|
||
lief. Ein Force-Push bewegt den Branch, nicht die Objekte.
|
||
|
||
**`.wikitool-remotes.json` nennt die erlaubten Push-URLs.** Nicht die Remote-*Namen*: Eine
|
||
Namensliste ließe ein `publish` durch, dessen `origin` umgebogen wurde, und genau das ist der
|
||
Fall, den das Gate fangen soll. Gelesen wird die `pushurl`, wenn der Remote eine setzt, denn
|
||
dorthin schreibt `git push` tatsächlich.
|
||
|
||
**Pro Checkout und gitignored**, aus demselben Grund wie `ENVIRONMENT.md`: Zwei Klone pushen an
|
||
zwei verschiedene Orte, eine committete Kopie würde einem privaten Klon also mitteilen, das
|
||
öffentliche Upstream sei ein legitimes Ziel für seinen eigenen Inhalt. **Fehlt die Datei, gilt
|
||
keine Beschränkung** — ein Checkout mit einem Remote und ohne Privates hat nichts zu schützen,
|
||
und eine Pflichtdatei würde aus einer Sicherung Papierkram machen. Eine *kaputte* Datei ist
|
||
dagegen ein Fehler und kein „keine Beschränkung": Eine beschädigte Sicherung darf sich nicht
|
||
wie eine abgeschaltete verhalten.
|
||
|
||
**Kein Token, keine Flagge.** Die anderen beiden Gates lösen sich mit einem `--confirm <token>`,
|
||
weil ihre Frage („ist diese Änderung richtig?") für genau ein Changeset beantwortbar ist. Dieses
|
||
fragt „gehört dieser Inhalt in jenes Repository?", und das ist eine stehende Eigenschaft des
|
||
Checkouts, kein Einzelfallurteil. Der Weg daran vorbei ist ein bewusster Edit des Nutzers.
|
||
Ein Agent, der die Datei anfasst, um an einer Verweigerung vorbeizukommen, öffnet ein Gate aus
|
||
eigenem Antrieb — Invariante 6.
|
||
|
||
**`doctor` meldet den Zustand** statt ihn zu erzwingen: OK mit Anzahl der Ziele, OK bei
|
||
Abwesenheit mit einem Remote, und WARN bei mehr als einem Remote ohne Allowlist — also genau in
|
||
der Form, die eine private Instanz annimmt, sobald sie das Upstream hinzufügt.
|
||
|
||
**Und die Prozedur, für die das Gate gebaut wurde.** `instructions/private-instance.md` (neu)
|
||
beschreibt die dritte Instanz-Form neben „frisch aufsetzen" und „Repo klonen": eine private
|
||
Arbeitsinstanz, die Stack-Updates von einem öffentlichen Upstream per `git merge` zieht und
|
||
deren eigener Inhalt nie zurückwandert. Der Grund, warum das dem Tarball-Weg vorzuziehen ist,
|
||
steht dort ausformuliert — `cp -r` hat keinen Drei-Wege-Merge und keine Konflikterkennung.
|
||
Schritt 4 der Anleitung ist das Gate, und zwar ausdrücklich **vor** dem ersten `publish`:
|
||
später hinzugefügt schützt es das Fenster nicht, das es schließen soll.
|
||
|
||
**Dateien:** `config.PUBLISH_REMOTES_FILENAME`, `git_publish.read_allowed_push_urls()`,
|
||
`push_url_for()`, `publish_remote_refusal()` und die Prüfung vor dem Reconcile-Schritt,
|
||
`doctor.check_publish_remotes()`, `.gitignore`, `instructions/gates.md`,
|
||
`instructions/private-instance.md` (neu), `AGENTS.md` (Gate-Liste und Bootstrap-Routing),
|
||
12 neue Tests in `test_git_publish.py`.
|
||
|
||
---
|
||
|
||
## 2.1.1 - 2026-09-01 - raw_dir-Fixture kappt config.ROOT; letzte private Fixture-Namen ersetzt
|
||
|
||
**Author:** Torben Nehmer
|
||
|
||
Der erste CI-Lauf nach der Korpus-Bereinigung wurde rot, während dieselbe Suite lokal grün
|
||
blieb: `test_legacy_source_pages_flags_url_and_directory` scheiterte mit
|
||
`KeyError: 'Source - DirBacked'`.
|
||
|
||
**Die Ursache ist keine Testschwäche, sondern eine Umgebungsabhängigkeit.** Der Test legt eine
|
||
Quellenseite mit `source: raw/documents` an und erwartet, dass `legacy_source_pages()` sie als
|
||
„directory, not a file" meldet. Die Prüfung dort ist `(config.ROOT / legacy).is_dir()` — sie
|
||
löst also gegen das *echte* Repo-Wurzelverzeichnis auf, während die `raw_dir`-Fixture ihren
|
||
Baum unter `tmp_path` baut. Der Test war nie hermetisch; er bestand, weil dieser Checkout
|
||
zufällig ein `raw/documents/` besaß.
|
||
|
||
Die Korpus-Bereinigung hat dieses Verzeichnis geleert. Git verfolgt keine leeren Verzeichnisse,
|
||
also verschwand es im CI-Checkout und blieb im lokalen Arbeitsbaum stehen — grün hier, rot
|
||
dort, ohne dass sich am Test etwas geändert hätte.
|
||
|
||
**Geschlossen in der Fixture, nicht im Test.** `raw_dir` setzt jetzt `config.ROOT` auf
|
||
`tmp_path`. Das ist dieselbe Bauart und dasselbe Argument wie bei `hermetic_environment`
|
||
(Gitea #8): Wenn eine Umgebungsabhängigkeit lokal unsichtbar ist, gehört sie an der Stelle
|
||
geschlossen, die alle Tests erreicht, statt bei dem einen, der zufällig darüber stolpert.
|
||
Nachgewiesen, indem `raw/documents/` lokal entfernt und die Suite erneut gefahren wurde — 38
|
||
Provenance-Tests grün ohne das Verzeichnis, vorher rot.
|
||
|
||
**Fixture-Namen.** Die Suite benutzte weiterhin reale Systemnamen der Ursprungsinstanz als
|
||
Fixture-Bezeichner. In einem
|
||
öffentlichen Repo beschreiben sie nichts, verraten aber die Namensgebung einer privaten
|
||
Umgebung. Ersetzt durch `aurora`, `almanac`, `Borealis`, `gateway`, `docstore`. `gdeploy`
|
||
bleibt: die Seite existiert im öffentlichen Korpus.
|
||
|
||
**Dateien:** `tools/chemenu/tests/conftest.py` (`raw_dir`), sämtliche Testdateien mit
|
||
Fixture-Namen.
|
||
|
||
---
|
||
|
||
## 2.1.0 - 2026-09-01 - Dual-Lizenz AGPL-3.0/CC-BY-4.0, und ein Leak-Canary fuer dist export
|
||
|
||
**Author:** Torben Nehmer
|
||
|
||
Das Repo hatte keine Lizenz. Für ein Repo, das privat auf der eigenen Gitea liegt, ist das
|
||
folgenlos; für eines, das veröffentlicht werden soll, hebt es den Zweck der Veröffentlichung
|
||
auf — ohne Lizenz gilt „alle Rechte vorbehalten", und niemand darf eine Instanz aufsetzen.
|
||
|
||
**Zwei Hälften, zwei Lizenzen.** `LICENSE` trägt die **AGPL-3.0** und gilt für den Stack
|
||
(`tools/`, `types/`); `LICENSE-CONTENT` trägt **CC-BY-4.0** und gilt für die Inhalte (`kb/`,
|
||
`raw/`, `instructions/`, die `CONTRACT.md`-Ebene, die Prosa im Wurzelverzeichnis). Die
|
||
Aufteilung folgt dem Vorbild der vendorten Commonplace-Basis, kehrt aber deren Dateinamen um:
|
||
Die Copyleft-Hälfte heißt `LICENSE`, weil das die Lizenz ist, die eine Forge für das Repository
|
||
meldet — und wer eine Copyleft-Pflicht übersieht, wird dadurch geschädigt, wer eine zu viel
|
||
sieht, nicht.
|
||
|
||
Die **Affero**-Variante statt der einfachen GPL, weil Issue #19 den Stack als MCP-Frontend
|
||
erreichbar machen will. Bei der GPL schuldet nichts, wer nur einen Dienst betreibt statt
|
||
Software zu verteilen; genau dieser Weg ist der, den dieses Projekt einschlägt.
|
||
|
||
**Die Grenze wird nicht aufgeschrieben.** Welche Datei zu welcher Hälfte gehört, ist der
|
||
Dateiplan, den `dist export` ohnehin berechnet. `NOTICE` verweist darauf, statt eine zweite
|
||
Pfadliste anzulegen, die drifted (Invariante 8). `NOTICE` trägt außerdem die Attribution für
|
||
`commonplace` (zby, CC-BY-4.0 Inhalte + MIT Code), die CC-BY verlangt.
|
||
|
||
**`dist export` liefert die Lizenzen mit — und weigert sich sonst.** Die drei Dateien stehen
|
||
jetzt in `ROOT_FILES`, aber nicht nur dort: `REQUIRED_ROOT_FILES` macht sie zur Bedingung.
|
||
Jeder andere `ROOT_FILES`-Eintrag wird `if source.is_file()` kopiert und sonst stillschweigend
|
||
übersprungen — richtig für `INSTALL.md`, das es lange nicht gab, und falsch für eine Lizenz:
|
||
Ein Export, der sie weglässt, übergibt einen AGPL-`tools/`-Baum ohne Lizenztext, und das ist
|
||
eine Verletzung, sobald die empfangende Instanz irgendwo öffentlich landet.
|
||
|
||
**Der Leak-Canary.** `find_leaks()` prüft den fertigen Plan gegen das, was er nie enthalten
|
||
darf: eine ausgefüllte `USER.md`/`SOUL.md`/`ENVIRONMENT.md`, etwas unter `instructions/dev/`,
|
||
eine `kb/`-Seite oder eine `raw/`-Quelle. Drei getrennte Allowlists entscheiden bisher, was
|
||
`build_plan` kopiert, und jede hält nur, solange sich beim Editieren jemand an die Regel
|
||
erinnert hat — nichts prüfte das Ergebnis nach. `export` bricht jetzt ab, wenn der Plan etwas
|
||
davon trägt, mit dem Hinweis, die Allowlist zu reparieren statt Dateien aus dem Ziel zu löschen.
|
||
|
||
Die Prüfung ist bewusst **strukturell** und nicht textbasiert. Ein Muster-Scan auf Hostnamen
|
||
oder IP-Literale wurde erwogen und verworfen: Der eigene Host steht legitim in `INSTALL.md` und
|
||
`version.py`, ein solcher Scan müsste also genau die Zeichenkette ausnehmen, nach der er sucht —
|
||
oder bei jedem Export Fehlalarm geben.
|
||
|
||
**Beispiele neutralisiert.** Hilfetexte, Docstrings und Contract-Beispiele nannten reale
|
||
Quellenseiten und Pfade der Ursprungsinstanz. In einer
|
||
Distribution ist das nicht nur unpassend, sondern verrät den Inhalt der Instanz, die sie gebaut
|
||
hat. Ersetzt durch Beispiele aus dem verbleibenden öffentlichen Korpus.
|
||
|
||
**Dateien:** `LICENSE`, `LICENSE-CONTENT`, `NOTICE` (neu), `config.LICENSE_FILES`,
|
||
`dist_cmd.REQUIRED_ROOT_FILES`, `dist_cmd.find_leaks()`, Lizenzabschnitt in `README.md`,
|
||
sechs neue Tests in `test_dist_cmd.py`; Beispiel-Neutralisierung in `cite_cmd.py`,
|
||
`page_ops.py`, `work_cmd.py`, `provenance.py`, `search/ripgrep.py`, `work/CONTRACT.md`,
|
||
`instructions/session-setup.md` und zwei Testdateien.
|
||
|
||
---
|
||
|
||
## 2.0.0 - 2026-09-01 - Rebranding zu Chemenu: Repo, Produktname und Python-Paket
|
||
|
||
**Author:** Torben Nehmer
|
||
|
||
**Migration:** none required - Der Bruch liegt im Update-Pfad und im Import-Namen, nicht im Korpus: kb/ behaelt Schema und Shape 1.0.0, keine Seite und kein Frontmatter-Feld aendert sich. Was eine bestehende Instanz tun muss, steht im Eintrag - Release-Feed umstellen, Paketimport nachziehen -, und beides ist Instanz-Konfiguration, keine Inhaltsmigration.
|
||
|
||
Der Stack hieß `llm-wiki-test1`, und das war nie ein Name, sondern ein Platzhalter mit einer
|
||
Ordnungszahl darin. Gitea-Issue #3 setzt **Chemenu** an seine Stelle - der altägyptische Name
|
||
von Hermopolis Magna, Hauptkultort des Thoth und „Stadt der Acht" der Ogdoade. Der Ort, nicht
|
||
der Gott: die Persona heißt weiterhin Thoth, und der Stack ist das, worin sie schreibt.
|
||
|
||
**Was den Namen trägt.** Repo (`torben/chemenu`), Produktname in `README.md`, `AGENTS.md`,
|
||
`.gitignore`, `types/type-spec.md` und `tools/CONTRACT.md`, das Release-Artefakt
|
||
(`chemenu-stack-<version>.tar.gz` statt `llm-wiki-stack-…`), der Release-Feed in
|
||
`version.DEFAULT_UPDATE_URL`, und die Download- und Issue-URLs in `INSTALL.md` und
|
||
`instructions/dev/issue-tracking.md`. Die eine Nennung von „LLM Wiki", die stehen bleibt, ist
|
||
die Attribution an Karpathys ursprüngliches Gist in `README.md` - das ist die Herkunft der
|
||
Idee, nicht der Name dieses Stacks.
|
||
|
||
**Das Python-Paket heißt jetzt `chemenu`**, nicht mehr `wiki_tools`. Der Import-Name eines
|
||
Pakets ist ein flacher globaler Namensraum ohne Kollisionsschutz, und `wiki_tools` ist genau
|
||
die Sorte generischer Name, die dort nichts verloren hat - unabhängig davon, ob je etwas
|
||
veröffentlicht wird, denn es reicht, dass ein zweites `wiki_tools` auf demselben `sys.path`
|
||
liegt. Betroffen sind 93 Dateien unter `tools/chemenu/` plus `pytest.ini`, `.coveragerc`, der
|
||
`tools/wikitool`-Shim und die Pfadangaben in `EVALS.md`, `instructions/dev/`, `types/*.md` und
|
||
`kb/CONTRACT.md`. `dist_cmd.py` brauchte nichts: es läuft `tools/` als Baum ab und nennt das
|
||
Paket nirgends beim Namen.
|
||
|
||
**Das Kommando bleibt `wikitool`**, und mit ihm `.wikitool-release.json`, `.wikitool-kb.json`
|
||
und die `WIKITOOL_*`-Variablen. Distributionsname, Import-Name und Kommandoname sind drei
|
||
unabhängige Dinge, und ein abweichender Kommandoname ist verbreitete Praxis (`httpie` →
|
||
`http`). Was der Stack nach außen anbietet, heißt weiter so, wie es tut, was es tut.
|
||
|
||
**Warum das trotzdem MAJOR ist.** Der erste Anlauf stand auf `1.9.0`, mit dem Argument, dass
|
||
kein Inhalt migriert werden muss. Das beantwortet die falsche Frage. Drei Dinge brechen:
|
||
|
||
1. **Die Update-Erkennung selbst.** Jede bereits exportierte Instanz trägt in
|
||
`.wikitool-release.json` ein `update_url` auf `.../repos/torben/llm-wiki-test1/releases/latest`.
|
||
Nach dem Repo-Rename antwortet Gitea dort bestenfalls per Redirect, schlimmstenfalls mit
|
||
`404` - und ein `404` ist von „kein Release vorhanden" nicht zu unterscheiden, wie
|
||
`INSTALL.md` § Konfiguration für den Token-Fall bereits festhält. Die Instanz kann sich
|
||
nicht selbst reparieren: der Stempel ist maschinengeschrieben, Invariante 1 verbietet den
|
||
Hand-Edit. Es bricht also ausgerechnet der Kanal, über den eine Instanz erführe, dass sie
|
||
etwas zu tun hat. Der Ausweg ist `WIKITOOL_UPDATE_URL` auf den neuen Feed zu setzen, oder
|
||
ein frischer `dist export`.
|
||
2. **Der Artefaktname.** `llm-wiki-stack-<version>.tar.gz` heißt jetzt
|
||
`chemenu-stack-<version>.tar.gz` - jedes Download-Skript und jeder Pin darauf bricht.
|
||
3. **Der Import-Name.** `from wiki_tools import …` außerhalb des ausgelieferten Baums bricht.
|
||
|
||
Keiner dieser drei Punkte verlangt, dass eine Seite in `kb/` angefasst wird - deshalb
|
||
`--no-migration` statt eines Migrationsdokuments. Aber rückwärtskompatibel ist das nicht, und
|
||
`1.9.0` hätte genau das behauptet.
|
||
|
||
**`SOUL.md.template` schlägt jetzt einen Persona-Namen vor.** Bisher stand dort ausdrücklich,
|
||
dass der Stack keinen vorschlägt; ab jetzt nennt er **Thoth** als Startpunkt, mit der
|
||
Begründung, die der neue Repo-Name mitbringt. Die Regel dahinter ändert sich nicht: gefragt
|
||
wird trotzdem, und ein anderer Name gewinnt. Weil das eine bestehende Regel verschiebt, steht
|
||
sie gleichlautend an allen drei Stellen, die sie führen - `SOUL.md.template`, Schritt 6 in
|
||
`instructions/setup-instance.md`, und `SOUL.md` dieser Instanz.
|
||
|
||
**Was den alten Namen behält:** `CHANGES.md`, `kb/log.md`, `kb/provenance.md`, die Prosa unter
|
||
`kb/sources/` und alles unter `raw/`. Das sind abgeschlossene Aufzeichnungen dessen, was
|
||
einmal galt - sie umzuschreiben hieße, die eigene Historie zu fälschen, und `raw/` ist
|
||
ohnehin unveränderlich.
|
||
|
||
---
|
||
|
||
## 1.8.1 - 2026-08-31 - Coverage-Reporting in CI: messen und als Artefakt ausweisen, ohne Schwelle
|
||
|
||
**Author:** Torben Nehmer
|
||
|
||
729 Tests liefen in CI, und wie viel vom Stack sie erreichen, wusste niemand - Coverage war nie
|
||
gemessen (Gitea #10). Der Verdacht war nicht theoretisch: Run 52 hatte zwei Tests gefunden, die
|
||
grün waren, weil die Umgebung zufällig passte. Was gar nicht ausgeführt wird, fällt leichter
|
||
durch.
|
||
|
||
**Gemessen wird jetzt, entschieden wird später.** Der CI-Schritt „Tests" läuft mit
|
||
`--cov --cov-report=term --cov-report=xml --cov-report=html`, ein neuer Schritt lädt XML und
|
||
HTML als Artefakt `coverage-<run id>` hoch (`upload-artifact@v3`, nicht v4 - v4 ist auf dieser
|
||
Gitea-Instanz eingeschränkt). Der Upload steht unter `if: always()`: eine rote Suite ist genau
|
||
der Moment, in dem die Zahlen pro Modul interessant sind.
|
||
|
||
**Kein `--cov-fail-under`, bewusst.** Eine Schwelle vor der Zahl hat zwei Ausgänge, beide
|
||
schlecht: sie liegt zu tief und wirkt nicht, oder sie liegt zu hoch und der nächste ehrliche
|
||
Commit wird rot - worauf jemand die Schwelle senkt statt Tests zu schreiben. Sie kommt in einem
|
||
eigenen, späteren Commit, mit der gemessenen Zahl als Begründung, und friert den erreichten
|
||
Stand ein, statt ihn schönzurechnen.
|
||
|
||
**Erste Messung: 86.9 % von 5105 Statements, 730 Tests** (2026-08-31, CI-Lauf 87). Festgehalten in EVALS.md
|
||
§ „How much of the stack the suite reaches", zusammen mit der Leseanleitung - denn der
|
||
Gesamtwert ist die uninteressanteste Größe. Der Bericht trennt drei Fälle, die verschieden zu
|
||
behandeln sind: dünne Typer-Wrapper (`eval_cmd.py` 36 %, `types_cmd.py` 52 %, `cli.py` 52 %),
|
||
bei denen niedrige Zahlen ein guter Schnitt sind und kein fehlender Test; Code mit einer
|
||
injizierbaren Außengrenze (`version.py`'s `fetch_latest()`), wo die echte Netzzeile absichtlich
|
||
ungetestet bleibt; und echte Lücken (`provenance_cmd.py` 44 %, `migrate_cmd.py` 71 %,
|
||
`type_resolver.py` 79 %). Nur die dritte Liste ist Arbeit.
|
||
|
||
`pytest-cov` steht **nicht** in `tools/requirements.txt`: die Datei beschreibt die
|
||
Laufzeitabhängigkeiten einer Instanz und wird von `dist export` mitgeliefert, und eine Instanz
|
||
misst diese Suite nicht. CI installiert es neben `pytest`. Die Konfiguration liegt in
|
||
**`tools/.coveragerc`** statt in `pytest.ini` - coverage.py liest `.coveragerc`, `setup.cfg`,
|
||
`tox.ini` und `pyproject.toml`, aber kein `pytest.ini`, wo ein `[coverage:*]`-Abschnitt still
|
||
ignoriert würde. Gemessen wird `wiki_tools/` ohne `wiki_tools/tests/`. Die Messung hängt
|
||
ausdrücklich nicht in `addopts`: das würde den nackten `pytest -q` überall dort brechen, wo
|
||
`pytest-cov` fehlt.
|
||
|
||
`.gitignore` und die Kanarienvögel in `docs verify` decken die Ausgabe mit ab
|
||
(`tools/.coverage`, `tools/coverage.xml`, `tools/htmlcov/`) - abgeleitet wie `reports/`, und
|
||
`publish` läuft mit `git add -A`, also hätte ein unignorierter `htmlcov/`-Baum sich beim
|
||
nächsten Content-Publish selbst mitcommittet.
|
||
|
||
**Dabei gefunden und mitbehoben: `dist export` hat die Coverage-Ausgabe mit ausgeliefert.** Der
|
||
erste Export nach der Messung trug 227 statt 162 Dateien, darunter den kompletten
|
||
`htmlcov/`-Baum - eine Messung *dieses* Repos, ausgeliefert in fremde Instanzen. Der Grund war
|
||
struktureller Natur: `TOOLS_EXCLUDE_DIRS` prunet Verzeichnisse, aber zwei Drittel der
|
||
Coverage-Ausgabe (`.coverage`, `coverage.xml`, unter Parallelläufen `.coverage.<host>.<pid>`)
|
||
liegen als Dateien neben dem Code. `_copy_tree` nimmt jetzt zusätzlich ein
|
||
Dateinamen-Prädikat; `htmlcov/` kam in die Verzeichnisliste. `.coveragerc` geht weiter mit -
|
||
Konfiguration ist Maschinerie, wie `pytest.ini`. Damit hat der Kopierplan drei
|
||
Ausschlussmechanismen statt zwei, für drei verschiedene Gründe: dev-only Inhalt (Marker),
|
||
dev-only Dateien (`instructions/dev/`), abgeleitete Ausgabe (Build-Artefakte).
|
||
|
||
---
|
||
|
||
## 1.8.0 - 2026-08-31 - ENVIRONMENT.md: optionale, checkout-spezifische Umgebungsnotiz
|
||
|
||
**Author:** Torben Nehmer
|
||
|
||
Welches Harness läuft hier, welche MCP-Server sind erreichbar, wohin veröffentlicht `publish`,
|
||
wo läuft CI? Werte, die sich über Wochen nicht ändern und die trotzdem jede Session neu
|
||
erfragt hat, weil nichts sie festhielt. `USER.md` beantwortet, *wer* die Instanz bedient,
|
||
`SOUL.md`, *wie* sie klingt - womit sie arbeitet, beantwortete bisher niemand (Gitea #24).
|
||
|
||
**`ENVIRONMENT.md`** schließt die Lücke, als drittes Root-Dokument der Session-Ebene. Es hält
|
||
Harness, publizierte Skills, MCP-Server, Connectoren, Git-Remotes und den CI-Ort fest;
|
||
`ENVIRONMENT.md.template` gibt die Abschnitte vor. Claude Code importiert es über `CLAUDE.md`,
|
||
`AGENTS.md` trägt es in der Namenstabelle und in einem eigenen Abschnitt für die übrigen
|
||
Harnesses.
|
||
|
||
Drei Eigenschaften unterscheiden es von der Personalization Plane, und alle drei sind
|
||
Absicht:
|
||
|
||
**Optional.** Fehlt die Datei, ist das ein gültiger Endzustand, kein Mangel - `doctor` meldet
|
||
`environment: absent (optional)` und niemals `FAIL`. Ein `FAIL` hätte die Datei durch die
|
||
Hintertür verpflichtend gemacht; der Preis ihres Fehlens sind ein paar Fragen, keine falsche
|
||
Ausgabe. Gemeldet wird genau ein Fall, und zwar als `WARN`: ein umbenanntes, nie ausgefülltes
|
||
Template, das in jeder Session mitgeladen wird und nichts beantwortet. Der Sentinel
|
||
`wikitool:template-unfilled` ist derselbe wie bei `USER.md`/`SOUL.md`.
|
||
|
||
**Gitignored statt committet.** Zwei Clones desselben Repos sind zwei verschiedene Umgebungen.
|
||
Eine committete Fassung würde dem zweiten Clone Antworten geben, die falsch sind statt zu
|
||
fehlen - und falsch ist hier schlimmer, weil die Datei geglaubt wird. `docs verify` prüft
|
||
beide Hälften des Musters: `ENVIRONMENT.md` muss ignoriert sein, `ENVIRONMENT.md.template`
|
||
darf es nicht sein. Das nachlässige `ENVIRONMENT.md*` würde beide schlucken, und genau das
|
||
fängt der Kanarienvogel ab.
|
||
|
||
**Kontext, keine Autorität.** Die Datei beschreibt, *was da ist*, nicht, was erlaubt ist. Ein
|
||
dort gelisteter Remote autorisiert kein `git push` - Invariante 5 führt weiter über
|
||
`wikitool publish` -, ein gelisteter MCP-Server öffnet kein Gate, und nichts darin ist eine
|
||
Quelle im Sinne von Invariante 3. Geheimnisse gehören nicht hinein: die Datei liegt im
|
||
Klartext im Arbeitsverzeichnis und in jedem Agenten-Kontext.
|
||
|
||
Warum ein Import in `CLAUDE.md` und kein Link: Was hier steht, wird nebenbei gebraucht - beim
|
||
Griff zum richtigen MCP-Server, mitten in einer Aufgabe -, nicht an einer Stelle, an der
|
||
jemand ein Dokument aufschlägt. Das ist derselbe Maßstab, den die Modellwahl-Instruktion
|
||
erfüllt, und er ist zugleich der Test: Eine Session, die erst nachschlagen müsste, fragt
|
||
stattdessen wieder den Nutzer - genau die Kosten, die die Datei beseitigt. Dass ein
|
||
unaufgelöster Import folgenlos bleibt, war schon vor der Personalization-Einführung so; hier
|
||
wird es zum Dauerzustand statt zum Übergang.
|
||
|
||
Angelegt wird die Datei in `bootstrap.md` (neuer Schritt 5, für einen Clone) und in
|
||
`setup-instance.md` (neuer Schritt 9, für eine frische Instanz). Beide Male gilt: eintragen,
|
||
was aus dem Checkout ablesbar ist, einmal nach dem Rest fragen, "weiß ich nicht" als Antwort
|
||
akzeptieren - und einen leeren Abschnitt löschen statt ihn mit Plausiblem zu füllen. Eine
|
||
geratene Zeile ist schlechter als eine fehlende.
|
||
|
||
---
|
||
|
||
## 1.7.2 - 2026-08-31 - lint liest Code nicht mehr als Wiki-Notation; Zitat-Limit zählt Zitate statt Zeilen
|
||
|
||
**Author:** Torben Nehmer
|
||
|
||
Two lint rules were measuring something other than what they claim to. Both are found the same
|
||
way - by a page that describes the stack rather than using it - and both are hard enough to be
|
||
worth a release of their own.
|
||
|
||
**`lint` read wiki notation inside code as the real thing** (Gitea #20). `CITE_REF_RE`,
|
||
`CITE_DEF_RE`, `LEGACY_CITE_RE` and `WIKILINK_RE` are plain text matches with no idea what a
|
||
Markdown code span is, so a page writing `` `[^cite-id]` `` in backticks, or showing a
|
||
definition line in a fenced block, produced `Undefined Footnote References` - and that is a
|
||
*hard* error, so `lint --fail-on-error` and CI went red. It happened at the 2026-08-31 ingest
|
||
(`c28f8ce`), on concept pages about the citation mechanism itself. The way out that session was
|
||
to write about the notation without writing it ("Zitatdefinitionszeile" instead of the marker),
|
||
which is invisible to whoever reads the page later. A knowledge base about a stack has to be
|
||
able to quote that stack's notation.
|
||
|
||
The new **`wiki_tools/markdown_code.py`** holds that rule once: `strip_code_spans()` replaces
|
||
fenced blocks and inline code spans with spaces of the same length, so offsets survive and a
|
||
caller may match the masked text and slice the original. Every scan now goes through it -
|
||
`provenance.iter_cite_refs()` for references, `split_cite_block()` for definitions,
|
||
`legacy_citation_markers()`, and `kb_scan.extract_wikilinks()`/`count_wikilinks()` for the link
|
||
graph, where the identical blindness made a fenced example link a broken one. Teaching each
|
||
regex its own context logic would have been six copies of one rule.
|
||
|
||
Two boundaries are drawn deliberately, and both are asserted in
|
||
`tests/test_markdown_code.py`. **Indented code blocks are not masked**: in this corpus a
|
||
four-space indent is a nested list continuation far more often than it is code, and masking by
|
||
indentation would have silently deleted real links from the graph. **Inline spans are matched
|
||
within one line**: an unclosed backtick is a common typo, and a matcher that crosses newlines
|
||
turns one typo into a masked paragraph - a failure that hides findings rather than inventing
|
||
them, which is the direction that goes unnoticed. For the same reason `split_cite_block()`
|
||
still locates the Footnotes heading on the *unmasked* body: one unclosed fence would otherwise
|
||
blank the heading and leave the page with no definitions at all.
|
||
|
||
**The corpus had the mirror-image habit.** 12 `[^s-…]` markers sat *inside* fenced blocks,
|
||
appended to a command or a YAML line, across four pages. They never rendered as footnotes -
|
||
GFM shows them literally, so copying the command copied the marker with it - and masking turned
|
||
two of them into orphan definitions. They now sit on a `Quelle: [^s-…]` line under their block:
|
||
vier Infrastruktur- und Konzeptseiten.
|
||
|
||
**The quote limit counted `>` lines, not quotes** (Gitea #22). The rule is about how much
|
||
borrowed wording a page carries; the line count measured the wrap width instead, so one
|
||
quotation counted 1 written long and 4 wrapped at the width the rest of the repo keeps. An
|
||
author who took the finding seriously made the page harder to read to quiet it - and it fired
|
||
on a page with exactly one quotation while verifying 1.7.1. `count_quote_blocks()` now counts
|
||
runs of consecutive quoted lines, with code masked out first so a `>` in a shell transcript is
|
||
a prompt rather than a quotation. `QUOTE_LIMIT` stays at 2 and finally means what its name
|
||
already claimed.
|
||
|
||
Alongside the release, `.gitea/workflows/nightly.yml` runs `doctor`, `docs verify`,
|
||
`instructions verify`, `lint --fail-on-error`, `sources coverage` and `migrate status` on a
|
||
schedule (Gitea #9). It closes the gap `ci.yml`'s content `paths-ignore` opened: since that
|
||
landed, structural lint no longer runs when the corpus changes. Workflows are outside the
|
||
version gate, so this needed no bump of its own.
|
||
|
||
---
|
||
|
||
## 1.7.1 - 2026-08-31 - Testsuite gegen stille Umgebungsabhängigkeiten gehärtet
|
||
|
||
**Author:** Claude Code
|
||
|
||
The suite was green for months on every developer machine while silently reading the machine it
|
||
ran on. `config.default_author()` shells out to `git config user.name` with `cwd=config.ROOT`;
|
||
the fixture root is not a repository, so the answer came from the *global* git configuration of
|
||
whoever started pytest. The first CI run that ever reached `pytest` (run 52) had none, and two
|
||
tests fell over with `ERROR No author configured for this instance.` Both were repaired in
|
||
`1.0.1` - and in `1.2.0` two *new* tests reintroduced exactly the same dependency, written by
|
||
someone who had read the issue first (Gitea #8). Patching cases one at a time loses to the rate
|
||
at which the suite acquires them.
|
||
|
||
**`tools/wiki_tools/tests/conftest.py` now carries an autouse `hermetic_environment` fixture**
|
||
next to `isolated_trace_dir`. Before every test it points `HOME` (and `XDG_CONFIG_HOME`) into
|
||
that test's own `tmp_path`, sets `GIT_CONFIG_GLOBAL` and `GIT_CONFIG_SYSTEM` to `/dev/null`, and
|
||
clears both git's identity/location variables (`GIT_DIR`, `GIT_WORK_TREE`, `GIT_AUTHOR_*`,
|
||
`GIT_COMMITTER_*`, `EMAIL`) and the tool's own (`WIKI_AUTHOR`, `WIKI_TRACE`,
|
||
`WIKI_TRACE_CONTENT`, `WIKI_TRACE_MAX_CONTENT`, `WIKITOOL_SESSION_ID`, `WIKITOOL_UPDATE_URL`,
|
||
`WIKITOOL_UPDATE_TOKEN`). A test can no longer acquire the dependency at all: it either
|
||
establishes an identity itself or the tool refuses, on the author's machine and in CI alike.
|
||
|
||
`WIKI_TRACE_DIR` is deliberately the one variable left set - `isolated_trace_dir` redirects it
|
||
per test, and now declares `hermetic_environment` as a dependency so the ordering between the
|
||
two autouse fixtures is stated rather than inherited from declaration order. Tracing is never
|
||
disabled suite-wide; two telemetry tests assert that a trace is written.
|
||
|
||
**No identity is set for everyone.** A shared default in `conftest.py` would have been the
|
||
cheaper fix and the wrong one: `default_author()`'s `None` branch only exists on a machine that
|
||
knows nobody, so a suite-wide `WIKI_AUTHOR` would have made the fallback untestable while
|
||
looking green. Instead the new `test_hermetic_env.py` asserts the fixture itself - the cleared
|
||
variables, the empty `HOME`, that `git config user.name` answers nothing, that tracing stays on
|
||
and redirected - and covers all three branches of `default_author()`, including the one the
|
||
fixture makes writable for the first time. `test_new_source_fails_hard_without_any_author` keeps
|
||
patching `default_author` directly, so it stays a test of the CLI's error path rather than of
|
||
the environment.
|
||
|
||
Verified in three environments that now agree exactly - the developer shell, a deliberately
|
||
poisoned one (`WIKI_AUTHOR`, `WIKI_TRACE=0`, `WIKITOOL_*` and `GIT_*` all set to junk), and
|
||
`env -i` with an empty `HOME` and no git configuration: 702 passed in each. Before this change
|
||
the third of those was the one that failed.
|
||
|
||
**CI keeps running the suite once.** A second hardened `pytest` step was the issue's fallback
|
||
proposal; it is redundant now and would report the dependency only after a push. The Tests step
|
||
in `.gitea/workflows/ci.yml` says so, so nobody adds it back for the reason it was once needed.
|
||
|
||
Added [instructions/dev/testing-conventions.md](instructions/dev/testing-conventions.md) - what
|
||
the fixture neutralizes, how a test establishes an author when it needs one, and the `env -i`
|
||
counter-check to run before publishing a test change. Linked from the `stack-dev` skill's step
|
||
2, which until now pointed at "the existing test files' own patterns" for exactly this. Also
|
||
pins the fixture repo in `test_new_source_author_falls_back_to_git_config` to
|
||
`git init -q -b main`: without a global configuration git emits an `init.defaultBranch` advisory
|
||
that clutters unrelated failures.
|
||
|
||
No command changes behaviour, so this is a PATCH.
|
||
|
||
---
|
||
|
||
## 1.7.0 - 2026-08-31 - CLAUDE.md as a harness-scoped instruction reference; Claude-Code model-selection guidance
|
||
|
||
**Author:** Claude Code
|
||
|
||
`tools/wikitool instructions verify` only ever recognized `AGENTS.md` as an automatically-loaded
|
||
reference source for `instructions/*.md` - accurate for every other harness (Codex CLI, GitHub
|
||
Copilot CLI, Mistral Vibe read `AGENTS.md` natively), but not for Claude Code, which loads
|
||
`CLAUDE.md` instead and does not read `AGENTS.md` on its own. A Claude-Code-only instruction
|
||
therefore had no valid place to be linked from: linking it from `AGENTS.md` would load it into
|
||
every other harness's session too.
|
||
|
||
`CLAUDE.md` is now a second recognized reference/auto-load source alongside `AGENTS.md`,
|
||
scoped the same way: `instructions_cmd.AGENT_ROOT_FILES` and `automatic_load_paths()` both
|
||
include it, `manual: true` instructions are forbidden from being linked there for the same
|
||
reason they are forbidden from `AGENTS.md`, and `instructions/CONTRACT.md`'s reference-tier
|
||
table documents the split. `AGENTS.md` and `CLAUDE.md` are deliberately kept as two names, not
|
||
folded into one "automatically loaded" concept, because they are automatic for disjoint sets of
|
||
harnesses.
|
||
|
||
**A mention in `README.md` or `CHANGES.md` no longer counts as a reference.** Both were in the
|
||
old `REFERENCE_GLOBS`, so an instruction whose only mention was in one of them passed `verify`'s
|
||
"nothing references it - it deploys to no one" check while being unreachable by any agent -
|
||
`AGENTS.md`'s own file-naming table defines `README.md` as "never by an agent as instruction",
|
||
and `CHANGES.md` is not even shipped (`dist export` replaces it wholesale). The tuple is now
|
||
split by what the two callers actually ask: `AGENT_ROOT_FILES` for "can an agent reach this",
|
||
`AGENT_ROOT_FILES + SHIPPED_DOC_ROOT_FILES` for the `instructions/dev/` boundary check, which
|
||
still scans `README.md` because a dangling path in a shipped document is a defect regardless of
|
||
who reads it. Nothing in the tree relied on the old behaviour - `verify` stays green - so this
|
||
closes a latent false green rather than fixing a live break. It also means this entry's own
|
||
mention of the new instruction below no longer masks a broken `CLAUDE.md` link.
|
||
|
||
Added [instructions/claude-code-model-selection.md](instructions/claude-code-model-selection.md),
|
||
`@`-imported by `CLAUDE.md` as its fourth import: which model (Sonnet vs. Opus) and effort level
|
||
to run a session, a spawned subagent, or a `/code-review` pass at, keyed to the skill or task at
|
||
hand rather than defaulting everything to the most capable model at the highest effort.
|
||
|
||
**Imported, not linked** - the choice matters enough that `instructions/CONTRACT.md` now
|
||
documents both forms and when each applies. Model and effort are decided while spawning a
|
||
subagent or starting a review, not at a point where anyone stops to open a document, so a link
|
||
would be followed only when the user explicitly asked about models. The price is standing
|
||
context in every Claude Code session: an import is the strongest load in this layer, stronger
|
||
than a skill (which contributes only its `description`), and that is the bar each further
|
||
Claude-Code-only import has to clear. `verify` accepts either form - both put the filename in
|
||
`CLAUDE.md`.
|
||
|
||
**Deliberately out of scope, tracked as a follow-up issue instead:** equivalent model-selection
|
||
guidance for the non-Claude-Code harnesses this repo supports. Those harnesses have no
|
||
equivalent of Claude Code's model names, `/code-review` effort dial, or `Agent` tool `model:`
|
||
override, so the question of whether and how they should decide this differently is a separate
|
||
design decision, not answered here.
|
||
|
||
---
|
||
|
||
## 1.6.0 - 2026-08-31 - xref link-source schreibt beide Richtungen; xref add lehnt undeklarierte Ref-Felder ab
|
||
|
||
**Author:** Torben Nehmer
|
||
|
||
Schließt Gitea-Issue #18. Drei Defekte mit einer Ursache, und ein Loch in einer
|
||
Entscheidung aus `1.4.0` desselben Tages: die Sperrliste in `touch --set` lehnt
|
||
Page-Ref-Felder mit dem Verweis auf `xref` ab — für die `entities:`/`concepts:`
|
||
einer Source-Seite konnte `xref` das aber nicht. Der Verweis zeigte ins Leere.
|
||
|
||
### `xref link-source` schreibt jetzt beide Richtungen
|
||
|
||
Bisher schrieb es nur die Zielseiten (`sources:` + See-Also-Bullet) und rührte
|
||
die Arrays der Source-Seite nie an — es prüfte nur, dass sie existiert. Ein
|
||
Ingest legt die Source-Seite aber an, **bevor** die Concept-Seiten existieren,
|
||
weil deren Titel erst beim Extrahieren feststehen. Also blieb `concepts: []`,
|
||
und kein Kommando konnte es füllen.
|
||
|
||
Welches der beiden Felder ein Ziel bekommt, folgt seiner **Collection**:
|
||
`kb/entities/` → `entities:`, `kb/concepts/` → `concepts:`. Das Verzeichnis
|
||
*ist* der Feldname, also braucht eine neue Collection hier keine Code-Änderung —
|
||
sie braucht einen Typ, der das passende Feld deklariert. Ein Ziel, dessen
|
||
Collection zu keinem deklarierten Feld passt, wird einseitig verlinkt und in der
|
||
Ausgabe benannt statt stillschweigend übergangen.
|
||
|
||
### `xref add` lehnt ein nicht deklariertes `related:` ab
|
||
|
||
`add_related()` schrieb das Feld bedingungslos. Auf einer Source-Seite —
|
||
`page_ref_fields: [entities, concepts]` — erzeugte das Frontmatter, das das
|
||
Schema verwirft (`additionalProperties: false`). Genau so entstand der einzige
|
||
Schema-Fehler, der heute auf `main` lag.
|
||
|
||
Die Prüfung läuft für **beide** Seiten, bevor eine davon geschrieben wird: eine
|
||
Ablehnung darf keinen halben Link hinterlassen. Die Meldung nennt die Felder,
|
||
die der Typ kennt, und das Kommando, das sie füllt.
|
||
|
||
### `xref remove` räumt auch undeklarierte Reste
|
||
|
||
`strip_frontmatter_ref()` lief nur über die vom Typ deklarierten Felder. Ein
|
||
`related:` auf einer Source-Seite war damit **nicht entfernbar** — ein Kommando
|
||
erzeugte einen Zustand, den ein anderes nicht rückgängig machen konnte, obwohl
|
||
sein Contract-Eintrag ausdrücklich verspricht, hinterlassene Referenzen ohne
|
||
Handeditierung zu klären.
|
||
|
||
Gesweept wird jetzt zusätzlich jedes auf der Seite vorhandene Feld, das
|
||
*irgendein* Typ als Ref-Feld deklariert — die Namen kommen aus den Type-Specs,
|
||
nicht aus einer Konstante. Ein undeklariertes Feld, das dabei leer wird, fällt
|
||
ganz weg statt als `related: []` stehenzubleiben: der Schlüssel war für diesen
|
||
Typ nie gültig, und ein leeres Array hielte die Seite weiter schemawidrig.
|
||
|
||
Das gilt damit auch für `rename` und `rm`, die denselben Helfer benutzen.
|
||
|
||
### Bestandsfall repariert, mit dem Werkzeug
|
||
|
||
Die Seite auf `main` trug `related: [Write-Once Frontmatter Fields]` und ein
|
||
leeres `concepts:`. `xref remove` hat den Rest geräumt, `xref link-source` hat
|
||
beide Concepts in beide Richtungen eingetragen. `lint` meldet keinen
|
||
Schema-Fehler mehr. Kein `rm --yes`, keine Handeditierung.
|
||
|
||
### Sonst
|
||
|
||
Die Ablehnungsmeldungen der Sperrliste in `touch --set` nennen jetzt für
|
||
`entities:`/`concepts:`/`sources:` konkret `xref link-source` statt pauschal
|
||
`xref add`/`xref remove` — der bisherige Verweis war für genau diese Felder
|
||
falsch.
|
||
|
||
Sechs neue Tests in `test_xref.py`, darunter der, dass eine Ablehnung keine
|
||
Seite anfasst, und die Idempotenz der neuen Richtung.
|
||
|
||
---
|
||
|
||
## 1.5.1 - 2026-08-31 - cite add/sync und rename loeschen keinen Inhalt mehr hinter dem Fussnoten-Block
|
||
|
||
**Author:** Torben Nehmer
|
||
|
||
Schließt Gitea-Issue #17. **Der bisher einzige bekannte Weg, auf dem dieser
|
||
Stack Inhalt verliert statt ihn falsch zu schreiben.**
|
||
|
||
`split_cite_block()` nahm alles ab der Überschrift `## Fußnoten` bis zum
|
||
Dateiende als Zitatblock, behielt daraus nur die `[^id]:`-Zeilen, und jeder
|
||
Aufrufer setzt eine Seite als `Kopf + neu gerenderter Block` wieder zusammen.
|
||
Stand hinter dem Block noch ein Abschnitt, war er nach dem nächsten Aufruf weg —
|
||
ohne Warnung, mit Erfolgsmeldung. Betroffen waren `cite add`, `cite sync` **und
|
||
`rename`**, die alle denselben Pfad benutzen.
|
||
|
||
Systematisch wurde es durch einen Widerspruch zwischen zwei Kommandos:
|
||
`xref add` hängt `## Beziehungen` und `## Siehe auch` ans Dateiende, `cite add`
|
||
hielt den Fußnoten-Block *für* das Dateiende. Wer `cite add` zuerst laufen ließ,
|
||
war sicher; wer die Reihenfolge umdrehte, verlor beim nächsten Zitat alle
|
||
Querverweise. Keiner der Skills schreibt die Reihenfolge vor, es war also Zufall.
|
||
|
||
Gefunden beim Ingest vom 2026-08-31 (`8524bce`), wo
|
||
`kb/concepts/Detect-Repair Asymmetry.md` vier `## Beziehungen`- und fünf
|
||
`## Siehe auch`-Einträge verlor. Dass es auffiel, war Glück: der ausführende
|
||
Agent hatte nach `cite add` zurückgelesen.
|
||
|
||
### Der Fix
|
||
|
||
Der Block endet jetzt an der nächsten Überschrift statt am Dateiende. Alles
|
||
dahinter — und alles im Block, was keine Zitatdefinition ist — wird auf den Kopf
|
||
zurückgefaltet statt verworfen. Der Rückgabetyp bleibt gleich, alle sechs
|
||
Aufrufer profitieren ohne Änderung.
|
||
|
||
Zwei Eigenschaften, die dabei mehr wert sind als die reine Reparatur:
|
||
|
||
- **Die Seite heilt sich selbst.** Weil der gerenderte Block immer zuletzt
|
||
ausgegeben wird, bringt die erste Zitatoperation eine bereits verrutschte
|
||
Seite wieder in die richtige Form. `xref add` darf weiterhin ans Dateiende
|
||
anhängen, ohne Schaden anzurichten.
|
||
- **Loser Text im Block wird gerettet, nicht abgelehnt.** Ein Abbruch wäre die
|
||
naheliegende Härtung gewesen, ist hier aber falsch: derselbe Pfad läuft unter
|
||
`lint` und `corpus_diff`, wo eine Exception das Lesen einer Seite verweigern
|
||
würde, statt sie zu melden.
|
||
|
||
Nebenbei behoben: ein `[^id]`, das nur in einem Abschnitt *hinter* dem Block
|
||
referenziert wurde, galt für `extract_inline_cites` als nicht referenziert — und
|
||
`cite sync` hätte seine Definition als verwaist entfernt.
|
||
|
||
### Korpus repariert
|
||
|
||
Acht Seiten trugen zusammen 74 Zeilen in der gefährdeten Position. `cite sync
|
||
--all` hat sie normalisiert (elf Seiten insgesamt; drei brauchten nur eine
|
||
Neusortierung). Nachgemessen: 0 Seiten mit Inhalt hinter dem Block, und je Seite
|
||
unveränderte Zahl an Zitatdefinitionen und Bullets.
|
||
|
||
### Tests
|
||
|
||
Fünf neue in `test_provenance.py`, darunter der Fall, der zuerst rot sein muss
|
||
(Abschnitt hinter dem Block überlebt den Rundlauf), die Stabilität wiederholter
|
||
Rundläufe, und ein Zitat, das nur im geretteten Abschnitt referenziert wird.
|
||
|
||
Bemerkenswert: **vor diesen Tests liefen alle 678 grün.** Das alte, zerstörende
|
||
Verhalten war von keinem Test festgehalten — so hat es überlebt.
|
||
|
||
---
|
||
|
||
## 1.5.0 - 2026-08-31 - Mass-Update Gate zaehlt generierte Dateien nicht mehr; Kalibrierungsband gemessen statt geerbt
|
||
|
||
**Author:** Torben Nehmer
|
||
|
||
Zwei Kalibrierungen, beide aus derselben Beobachtung: **drei gewöhnliche
|
||
Ingests hintereinander blieben am Mass-Update Gate stehen.** Keiner war ein
|
||
Massen-Update. Das Gate feuerte auf Rauschen, und die Budget-Richtwerte
|
||
beschrieben eine Arbeit, die es so nicht gibt.
|
||
|
||
### Generierte Dateien zählen nicht mehr gegen die Schwelle
|
||
|
||
`kb/index.md`, `kb/log.md`, `kb/provenance.md` und jede `INDEX.md` werden
|
||
committet und gepusht wie bisher, aber vom Gate nicht mehr gezählt und nicht
|
||
mehr zur Freigabe vorgelegt. Der Grund ist derselbe, aus dem `work/` schon
|
||
ausgenommen war: sie tragen keine Entscheidung. Jede davon ist per
|
||
`index rebuild` / `sources rebuild-index` aus dem Baum reproduzierbar, also
|
||
entscheidet ihre Freigabe nichts — sie erzeugt nur Review-Müdigkeit, gegen die
|
||
die Schwelle eigentlich gerichtet ist.
|
||
|
||
Ein gewöhnlicher Ingest baut fünf bis sechs davon neu. Gemessen an den drei
|
||
Changesets vom 2026-08-31:
|
||
|
||
| Ingest | Dateien | vorher gezählt | jetzt gezählt |
|
||
|---|---|---|---|
|
||
| Comma Bug | 14 | 14 → Gate | **9 → läuft durch** |
|
||
| Issue Triage | 16 | 16 → Gate | **9 → läuft durch** |
|
||
| Auto Mode | 11 | 11 → Gate | **5 → läuft durch** |
|
||
|
||
Das Gate bleibt scharf: zehn echte Seiten lösen es weiterhin aus, egal wie
|
||
viel Index-Rauschen mitfährt. Ein Test hält genau das fest.
|
||
|
||
Die Ablehnungszeile weist beide Ausnahmegründe getrennt aus („3 under work/ and
|
||
5 generated by wikitool committed but not counted"), weil ein Prüfer, der „9
|
||
counted" an einem 14-Datei-Commit sieht, sonst die Differenz für einen Fehler
|
||
hält. Und weil Kratzzustand und abgeleitete Ausgabe nicht dasselbe sind.
|
||
|
||
Der `--confirm`-Token deckt jetzt genau das ab, was ein Mensch tatsächlich
|
||
gelesen hat: eine neu gebaute `INDEX.md` macht eine erteilte Freigabe nicht
|
||
mehr ungültig.
|
||
|
||
### Kalibrierungsband: 15-25 → 20-35 für komplexe Workflows
|
||
|
||
Das obere Band stammte aus einer Branchen-Faustregel. Vier reale Ingests
|
||
messen **24, 26, 29 und 30** Aufrufe — jeder einzelne am oberen Rand oder
|
||
darüber, ohne dass etwas Ungewöhnliches passiert wäre. Ein Richtwert, den der
|
||
Normalfall überschreitet, ist keiner; er bringt einem Agenten bei, dass die
|
||
Zahlen Dekoration sind.
|
||
|
||
Neu: ~5-15 für eine einfache Aufgabe (gemessen 5-9), **~20-35** für einen
|
||
komplexen Multi-Tool-Workflow. Nachgezogen in `run_budget.py`,
|
||
`instructions/gates.md` und den Skills `wiki-ingest` und `wiki-lint`. Die
|
||
Obergrenze von 60 bleibt unverändert — sie ist kein Ziel, sondern der Punkt, ab
|
||
dem eine Sitzung als festgefahren gilt.
|
||
|
||
`gates.md` sagt jetzt auch, **woher** die Zahl kommt und wie man sie neu misst:
|
||
`tools/.wikitool_session/budget.json` hält die Aufrufzahlen je Session. Ein
|
||
Richtwert ohne Messverfahren veraltet lautlos.
|
||
|
||
Die zitierte Branchen-Faustregel in `kb/concepts/Iteration and Cost Limits.md`
|
||
bleibt unangetastet — sie ist eine belegte Aussage über den Stand der Technik,
|
||
nicht über diese Instanz. Was diese Instanz misst, ist eine andere Behauptung
|
||
und gehört beim nächsten Ingest dieser Sitzung dort ergänzt.
|
||
|
||
---
|
||
|
||
## 1.4.0 - 2026-08-31 - touch --set/--add/--remove: Frontmatter-Felder nach dem Anlegen korrigierbar
|
||
|
||
**Author:** Torben Nehmer
|
||
|
||
Schließt Gitea-Issue #14. Ein Feld, das `new` einmal schrieb - `tags:`,
|
||
`raw_files:`, `source_url:` - war danach unerreichbar: `touch` kannte es nicht,
|
||
Frontmatter von Hand zu editieren ist das, was dieses Werkzeug verhindern soll,
|
||
und die Seite zu löschen und neu anzulegen bricht jede Referenz, die schon auf
|
||
sie zeigt. Da `new` nicht idempotent ist, war das Fenster, es richtig zu
|
||
treffen, **genau ein Kommando breit.**
|
||
|
||
Drei Fehlschläge in drei aufeinanderfolgenden Ingests am 2026-08-31, an zwei
|
||
Feldern, von drei verschiedenen Agenten - einer davon wegen eines Kommas am
|
||
Ende eines `--set tags=`-Werts. Das ist keine Ausrutscherquote, sondern die
|
||
Fehlerrate einer Schnittstelle, die einen Wert genau einmal entgegennimmt.
|
||
|
||
### Neu an `touch`
|
||
|
||
- **`--set field=value`** ersetzt den Wert auf der Platte. Wiederholtes `--set`
|
||
für dasselbe Arrayfeld hängt *innerhalb eines Aufrufs* an, `\,` ist ein
|
||
literales Komma - dieselben Regeln wie bei `new --set` (1.2.0).
|
||
- **`--add` / `--remove`** ändern einzelne Elemente eines Arrayfelds, ohne dass
|
||
man die bestehende Liste kennen muss. `--add` ist idempotent. `--remove` auf
|
||
ein nicht vorhandenes Element **gelingt und sagt es** - wie `xref remove`
|
||
idempotent, aber nie stillschweigend: ein stiller No-op sieht genauso aus wie
|
||
eine erfolgreiche Entfernung, und genau so verschwindet ein Tippfehler.
|
||
- `--summary`/`--provenance`/`--confidence-base` bleiben als Kurzformen.
|
||
|
||
### Die Sperrliste, und warum es eine ist
|
||
|
||
Gesperrt sind `type:` (ändert Schema *und* Ablageort - das ist
|
||
`page-lifecycle.md`), `confidence:` (abgeleitet; `--confidence-base` setzen und
|
||
`confidence decay --apply` laufen lassen) und die Page-Ref-Arrays `related:`,
|
||
`sources:`, `entities:`, `concepts:` (`xref` pflegt die Gegenrichtung und die
|
||
Body-Bullets mit; ein nackter Frontmatter-Schreibvorgang ließe die andere
|
||
Hälfte veralten). Jede Ablehnung nennt das Kommando, dem das Feld gehört.
|
||
|
||
Bewusst eine Deny- und keine Allowlist: eine Allowlist wäre eine zweite Kopie
|
||
des Schemas, und die Kopie ist die, die driftet - ein neu in einen Type-Spec
|
||
aufgenommenes Feld bliebe stumm unbeschreibbar, bis jemand daran denkt. So
|
||
gilt: was das Schema kennt, ist schreibbar, sofern es keinen Grund gibt.
|
||
|
||
Ein *unbekanntes* Feld wird anders abgelehnt als ein gesperrtes: nicht mit
|
||
einem Verweis auf ein anderes Kommando, sondern mit der Liste dessen, was die
|
||
Seite tatsächlich hat. Bei `--set tag=x` ist der Nutzen zu erfahren, dass
|
||
`tags` gemeint war.
|
||
|
||
### Sonst
|
||
|
||
- `raw_files:` bekommt beim Schreiben durch `touch` dieselbe Existenzprüfung
|
||
wie bei `new` - eine Dateisystemprüfung, die kein Schema ausdrücken kann.
|
||
- `_coerce_set_value`, `_parse_set_fields` und `_check_raw_files_exist` sind
|
||
aus `new_page.py` nach `commands/_util.py` gewandert und heißen jetzt ohne
|
||
Unterstrich. Zwei Kommandos, eine Implementierung - andernfalls hätte
|
||
`touch --set` das Komma-Problem aus #12 gleich mit geerbt.
|
||
- `tests/test_touch.py` ruft den Typer-Callback jetzt über einen Helfer mit
|
||
Vollbelegung auf. Ein direkt aufgerufener Callback bekommt für ausgelassene
|
||
Argumente `OptionInfo`-Objekte, und ohne den Helfer kostet jede neue Option
|
||
eine Änderung an jeder Aufrufstelle.
|
||
|
||
### Nicht enthalten
|
||
|
||
`raw rename` - der Fall, dass die *Datei* sich bewegt. Zweistufig ist er jetzt
|
||
möglich (`git mv`, dann `touch --set raw_files=…`), aber zwischen den beiden
|
||
Schritten zeigt die Referenz ins Leere, und bei mehreren referenzierenden
|
||
Seiten wächst das Fenster. Als eigenes Issue geführt.
|
||
|
||
---
|
||
|
||
## 1.3.0 - 2026-08-31 - capture-session: eine Session als raw/-Transkripte sichern und ingesten
|
||
|
||
**Author:** Torben Nehmer
|
||
|
||
Neue Instruction `instructions/capture-session.md`: eine beendete Claude-Code-
|
||
Session als `raw/notes/`-Transkripte sichern und einzeln ingesten. Das war
|
||
inzwischen dreimal von Hand gemacht worden, jedes Mal etwas anders - und die
|
||
Fehler, die dabei teuer sind, macht man genau einmal.
|
||
|
||
`manual: true`, also bewusst nicht aus `AGENTS.md` oder einem Skill verlinkt.
|
||
Jede Session zu sichern würde `raw/` mit Material füllen, das nie jemand
|
||
zitiert; die Einschätzung „war diese Session es wert" gehört dem Nutzer, nicht
|
||
dem Agenten. Aufruf nur namentlich auf Anforderung.
|
||
|
||
Was die Instruction festhält, statt es beim nächsten Mal wieder herzuleiten:
|
||
|
||
- **Drei Oberflächen, drei Aufgaben.** `raw/` hält die Evidenz, der
|
||
Issue-Tracker den offenen Arbeitsstand, `kb/` das Gelernte. Ein Transkript
|
||
ist keine To-do-Liste: was offen bleibt, wird vor dem Ingest zum Issue, und
|
||
das Transkript hält nur fest, dass es das wurde.
|
||
- **Ein Transkript je Thema.** Eine Rohdatei bekommt eine Source-Seite, und
|
||
deren `summary:`/`entities:`/`concepts:` beschreiben *eine* Sache. Später zu
|
||
schneiden heißt, eine Datei umzubenennen, auf die schon Zitate zeigen.
|
||
- **Fidelity wird bei der Erfassung festgelegt und steigt danach nie wieder.**
|
||
Kein Zitatformat und keine spätere Prüfung befördert eine Paraphrase zum
|
||
Zitat - nur ein erneuter Blick ins Original, den es bei einer beendeten
|
||
Session nicht mehr gibt. Was tragend werden könnte, wird sofort wörtlich
|
||
zitiert; Material aus zweiter Hand (etwa Subagent-Befunde) nennt seinen
|
||
Vermittler.
|
||
- **Ingests laufen streng nacheinander.** Parallele Ingests desselben Korpus
|
||
kollidieren an geteilten Entity-Seiten, an den generierten Dateien und an
|
||
`publish` - jedes Mal als stiller verlorener Schreibvorgang, nicht als
|
||
Fehler. Je Transkript eine eigene `WIKITOOL_SESSION_ID`.
|
||
- **Verworfene Alternativen gehören ins Transkript.** Was der Code tut, steht
|
||
im Code; was er bewusst nicht tut, steht sonst nirgends.
|
||
|
||
Die Punkte zu Fidelity und zur Trennung von Evidenz und aktivem Arbeitsstand
|
||
sind aus der vendorierten `commonplace/`-Wissensbasis übernommen
|
||
(`a-citation-cannot-assert-more-fidelity-than-its-capture-preserved`,
|
||
`active-work-state-is-not-retrospective-memory-or-chat-history`).
|
||
|
||
Mit diesem Commit landen zugleich die drei Transkripte dieser Session unter
|
||
`raw/notes/`; ihre Source-Seiten entstehen in den nachfolgenden Ingests.
|
||
|
||
---
|
||
|
||
## 1.2.1 - 2026-08-31 - Arbeitsplanung: Gitea-Issues mit prio/size-Labels statt TODO.md
|
||
|
||
**Author:** Torben Nehmer
|
||
|
||
`TODO.md` ist entfernt. Nach der Verlagerung der CI/CD-Punkte nach Gitea
|
||
(2026-08-30) und der Recherche-Notiz als Issue #15 stand dort nichts mehr, was
|
||
nicht ohnehin ein Issue war - die Datei war eine zweite Liste über denselben
|
||
Bestand, und die zweite Liste ist immer die, die driftet.
|
||
|
||
Offene Arbeit steht ab jetzt ausschließlich in den Gitea-Issues, ein Issue je
|
||
Arbeitspaket, mit genau zwei Labels:
|
||
|
||
- `prio/1` blockiert oder beschädigt laufende Arbeit, `prio/2` trägt bald
|
||
Zinsen, `prio/3` wartet auf einen benannten Auslöser.
|
||
- `size/XS` bis `size/L` schätzt den Aufwand, nicht die Wichtigkeit.
|
||
|
||
Beide Achsen sind Pflicht: eine Priorität ohne Kosten ist eine halbe
|
||
Entscheidung. Eine dritte Achse gibt es bewusst nicht - dort fängt eine
|
||
Taxonomie an, eigene Pflege zu brauchen, und dieses Board hat einen Betreuer.
|
||
|
||
Die Regel steht in `instructions/dev/issue-tracking.md` und ist aus dem
|
||
`stack-dev`-Skill verlinkt. Sie liegt unter `instructions/dev/`, weil sie nur
|
||
hier gilt: eine verteilte Instanz hat keine Issues unter dieser URL, und
|
||
`dist export` schließt das Verzeichnis vollständig aus. Aus demselben Grund ist
|
||
das ein **PATCH** und kein MINOR - für eine bestehende Instanz ändert sich
|
||
nichts.
|
||
|
||
---
|
||
|
||
## 1.2.0 - 2026-08-31 - --set-Arraywerte mit Komma, Lint-Reportpfad, Budget-Refund, Ceiling 60
|
||
|
||
**Author:** Torben Nehmer
|
||
|
||
Zwei Werkzeugdefekte aus dem Ingest vom 2026-08-30 (Issues #12 und #13). Beide
|
||
haben den Nutzer gegen eine Regel des Stacks gedrückt: einmal gegen die
|
||
Unveränderlichkeit von `raw/`, einmal gegen Invariante 6. Das ist der Grund,
|
||
warum sie zusammen vor allem anderen kommen.
|
||
|
||
### `--set` kann Kommas in Arraywerten ausdrücken (#12)
|
||
|
||
`parse_list` teilte hart an `,` ohne jedes Escape, und Shell-Quoting hilft
|
||
nicht - die Quotes sind lange weg, bevor der Wert dort ankommt. Ein
|
||
`raw_files:`-Pfad mit Komma im Dateinamen war damit nicht darstellbar. Im
|
||
Ingest vom 2026-08-30 wurde deshalb die Rohdatei umbenannt, obwohl
|
||
`raw/CONTRACT.md` die Rohschicht als unveränderlich führt.
|
||
|
||
- `\,` ist jetzt ein literales Komma und überlebt den Split (`parse_list`, also
|
||
auch `xref add --entities`).
|
||
- `--set` **hängt an**, wenn es für dasselbe Arrayfeld wiederholt wird:
|
||
`--set raw_files=a --set raw_files=b`. Das ist die Form ohne Trennzeichen und
|
||
damit die richtige, wenn ein Element ein Komma enthält. Skalare Felder
|
||
behalten "last one wins".
|
||
- Die Fehlermeldung von `_check_raw_files_exist` benennt den Split als Ursache
|
||
und den Ausweg - vorher nannte sie einen Pfad, den niemand getippt hatte.
|
||
|
||
**Dazu die zweite Hälfte desselben Defekts, die beim Testen auffiel:**
|
||
`dump_frontmatter` schreibt Listen in Flow-Syntax (`[a, b]`), quotete aber nach
|
||
Dokument-Kontext. Ein Element mit Komma kam deshalb als *zwei* Elemente zurück -
|
||
`--set` hätte den Wert korrekt geparst, und die Datei hätte ihn trotzdem
|
||
zerlegt. `_round_trips_as_string` prüft den Rundlauf jetzt im selben Kontext,
|
||
in dem geschrieben wird, und `_quote` fragt für beide Kontexte die Bibliothek
|
||
statt selbst zu quoten. Bestehende Ausgabe ändert sich dadurch nicht - nur
|
||
Werte, die vorher stillschweigend kaputtgingen, werden jetzt gequotet.
|
||
|
||
### Iteration Budget: Reibung statt Iteration wurde gezählt (#13)
|
||
|
||
- **Ceiling 30 → 60.** Das Band aus der Kalibrierung (5-15 einfach, 15-25
|
||
komplex) bleibt, wie es ist: es beschreibt die Arbeit. Die Decke beschrieb
|
||
bisher nichts - sie lag so dicht am Band, dass der Overhead eines echten
|
||
Ingests (Bericht nachlesen, ein korrigierter Retry, Baum vor dem Publish
|
||
prüfen) allein sie erreichte. Der Ingest vom 2026-08-30 lief inhaltlich im
|
||
Band und kam trotzdem nur mit `--override-budget` durch.
|
||
- **Ein Aufruf, der abgelehnt hat, wird zurückerstattet.** Wer über
|
||
`_util.fail()` mit Exit 1 endet - abgewiesenes Argument oder ein
|
||
lesender Check, der Befunde meldet - hat nichts getan; und der Tool-Error-
|
||
Contract beantwortet ein abgewiesenes Argument mit "einmal korrigiert
|
||
wiederholen", was sonst zwei Slots für eine Operation kostet. Der Aufruf
|
||
bleibt in der Historie des Loop-Breakers: dreimal dasselbe kaputte Kommando
|
||
bricht weiterhin ab. Ein Kommando, das seine Arbeit schon getan hat und dann
|
||
ungleich null endet (`lint --fail-on-error`), wirft `typer.Exit(1)` direkt
|
||
und bleibt gezählt.
|
||
|
||
### `lint` nennt seinen Reportpfad (#13)
|
||
|
||
`lint` ohne Flags kippte den vollen Bericht nach stdout und schrieb nichts. Wer
|
||
ihn abschnittsweise las, hatte keinen Weg zurück zum übersprungenen Teil außer
|
||
einem zweiten `lint` - zwei Slots für einen Blick auf einen unveränderten
|
||
Korpus.
|
||
|
||
- Der volle Bericht wird jetzt immer geschrieben, standardmäßig nach
|
||
`reports/Lint Report <date>.md`, und der Pfad wird ausgegeben. `--markdown`
|
||
überschreibt das Ziel wie bisher.
|
||
- Gedruckt werden nur die Abschnitte mit Befunden. Bei sauberem Korpus ist der
|
||
Vollbericht zu über 90 % "None found."; `--full` druckt ihn trotzdem.
|
||
- `--json` schreibt weiterhin nichts.
|
||
- `wiki-lint` und `wiki-status` sagen jetzt beide, dass die Datei gelesen wird
|
||
statt `lint` erneut aufgerufen. `wiki-status` Schritt 3 liest die
|
||
Hub-Statistik aus dem Bericht, weil sie kein Befund ist und deshalb nicht
|
||
mehr in der Zusammenfassung steht.
|
||
|
||
### Nicht enthalten
|
||
|
||
`lint` vom Budget zu befreien (dritter Vorschlag aus #13) - das ist eine
|
||
eigene Entscheidung über die Grenze zwischen lesend und schreibend, und `lint`
|
||
schreibt jetzt eine Datei.
|
||
|
||
---
|
||
|
||
## 1.1.1 - 2026-08-30 - CLAUDE.md: AGENTS.md/USER.md/SOUL.md erreichen Claude Code
|
||
|
||
**Author:** Torben Nehmer
|
||
|
||
Defekt, aufgefallen durch Nachfragen des Nutzers: **Claude Code hat `AGENTS.md`
|
||
noch nie geladen.** Das Repo hatte keine `CLAUDE.md`, und Claude Code liest
|
||
genau diese Datei automatisch - `AGENTS.md` nicht. Die anderen Harnesses
|
||
(Codex, Copilot, Vibe) lesen `AGENTS.md` nativ, weshalb es niemandem auffiel.
|
||
Praktisch heißt das: jede Claude-Code-Session lief bisher ohne Invarianten,
|
||
ohne Routing, ohne Gate-Regeln, sofern der Agent die Datei nicht zufällig
|
||
selbst aufmachte.
|
||
|
||
Mit 1.1.0 wurde der Defekt eine Stufe schlimmer: die File-naming-Tabelle
|
||
behauptet seitdem für `USER.md` und `SOUL.md` „Always, every session". Für
|
||
Claude Code war das schlicht unwahr.
|
||
|
||
Behoben durch eine `CLAUDE.md` im Root, die **keine eigenen Regeln trägt** -
|
||
nur drei Importe (`@AGENTS.md`, `@USER.md`, `@SOUL.md`) und die Begründung,
|
||
warum sie existiert. Eine Regel dort wäre die zweite Kopie, die Invariante 8
|
||
verbietet, und zwar die am ehesten driftende: sie stünde in der Datei, die nur
|
||
ein einziges Harness liest.
|
||
|
||
Sie wird von `dist export` mitgeliefert (`ROOT_FILES`), aus demselben Grund wie
|
||
`.claude/settings.json`: eine ausgelieferte Instanz unter Claude Code hätte
|
||
sonst exakt denselben Defekt. Ein Test hält fest, dass sie im Plan landet und
|
||
`@AGENTS.md` enthält.
|
||
|
||
Bewusst *kein* `doctor`-Check dafür: `CLAUDE.md` ist harness-spezifisch, und
|
||
eine Instanz, die nur unter Codex läuft, braucht sie nicht - ein FAIL wäre dort
|
||
schlicht falsch. Anders als bei `USER.md`/`SOUL.md`, die jedes Harness liest.
|
||
|
||
Während des Setups löst nur `@AGENTS.md` auf; `USER.md`/`SOUL.md` entstehen
|
||
erst in Schritt 6. Ab der ersten Session danach greifen alle drei.
|
||
|
||
---
|
||
|
||
## 1.1.0 - 2026-08-30 - Personalization Plane: USER.md/SOUL.md als Setup-Schritt
|
||
|
||
**Author:** Torben Nehmer
|
||
|
||
Zwei neue Dateien, die eine Instanz in jeder Session liest: `USER.md` (wer sie
|
||
bedient) und `SOUL.md` (wie sie klingt). Beide sind reine Kontext- und
|
||
Stildateien - sie bekommen **keine** neue Autorität, kein Gate und keinen
|
||
Zugang nach `kb/`: `SOUL.md` verliert gegen `AGENTS.md`, und eine Nutzeraussage
|
||
ist weiterhin keine Quelle im Sinne von Invariante 3.
|
||
|
||
Der eigentliche Entwurfspunkt liegt woanders. Beide Dateien sind
|
||
**Betriebsvoraussetzung** und zugleich **persönlicher Inhalt**, gehören also
|
||
nicht in jede exportierte Kopie. Sie entstehen deshalb während der
|
||
Installation, nicht vorab befüllt in der Distribution:
|
||
|
||
- `USER.md.template` und `SOUL.md.template` liegen im Root und werden von
|
||
`dist export` mitgeliefert. Dass die *ausgefüllten* Fassungen es nicht
|
||
werden, ist keine zusätzliche Regel, sondern Folge der bestehenden
|
||
Root-Allowlist - ein Name, der dort nicht steht, wird nicht kopiert.
|
||
- Beide Templates tragen eine Sentinel-Zeile (`wikitool:template-unfilled`),
|
||
die beim Ausfüllen entfernt wird.
|
||
- `instructions/setup-instance.md` hat einen neuen Entscheidungspunkt
|
||
(Schritt 6, Personalization); die Folgeschritte verschieben sich um eins.
|
||
Der Agent interviewt entlang der Template-Abschnitte und schreibt die
|
||
Antworten **wörtlich** mit. Zwei Fragen darf er nicht raten: den
|
||
Persona-Namen und die Themen, die bewusst draußen bleiben.
|
||
- `wikitool doctor` bekommt den Check `personalization`: `FAIL` bei fehlender
|
||
Datei **und** bei einer, die noch den Sentinel trägt - ein umbenanntes
|
||
Template ist kein ausgefülltes.
|
||
|
||
**Für bestehende Instanzen:** `doctor` meldet ab hier `personalization: FAIL`,
|
||
bis die beiden Dateien existieren. Das ist einmalig nachzuholen und **keine
|
||
Content-Migration** - `.wikitool-kb.json` bleibt unberührt, keine Seite ändert
|
||
sich. Nachzuholen ist ausschließlich Schritt 6 aus `setup-instance.md`, nicht
|
||
der ganze Ablauf; `instructions/bootstrap.md` und `INSTALL.md` verweisen an
|
||
der jeweils passenden Stelle darauf.
|
||
|
||
Der CI-Replay der Installation stubbt den neuen Entscheidungspunkt so wie die
|
||
Identität: mit einer festen Antwort (Template minus Sentinel-Zeile). Geprüft
|
||
wird damit, dass der Export die Templates trägt - nicht, was ein Mensch
|
||
hineinschreibt.
|
||
|
||
Umgesetzt aus Issue #2.
|
||
|
||
---
|
||
|
||
## 1.0.1 - 2026-08-30 - Tests ohne globale git-Identität; CI-Fixes
|
||
|
||
**Author:** Torben Nehmer
|
||
|
||
Der erste CI-Lauf, der überhaupt bis zu `pytest` kam, hat zwei Tests
|
||
umgeworfen, die auf jeder Entwicklermaschine grün sind: sie hingen an einer
|
||
**globalen git-Identität**. `default_author()` ruft `git config user.name` mit
|
||
`cwd=config.ROOT` auf, und die Fixture-Wurzel ist kein Repository - also
|
||
antwortete bisher die globale Konfiguration dessen, der die Suite laufen ließ.
|
||
Im Job-Container als `root` gibt es keine, und `new source` weigert sich zu
|
||
Recht, einen Platzhalter-Autor zu stempeln.
|
||
|
||
- `test_new_source_author_falls_back_to_git_config` macht die Fixture-Wurzel
|
||
jetzt selbst zu einem Repository mit *lokalem* `user.name`. Damit prüft der
|
||
Test den Fallback statt der Maschine - und prüft ihn schärfer: er behauptet
|
||
jetzt den konkreten Namen, nicht nur, dass irgendeiner dasteht.
|
||
- `test_new_source_with_multiple_raw_files` setzt `WIKI_AUTHOR`. Der Test geht
|
||
über `raw_files:`, Autorschaft ist dort nur Vorbedingung.
|
||
|
||
Nachprüfbar mit `GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null
|
||
pytest`, was die CI-Bedingung lokal herstellt.
|
||
|
||
Dazu die Workflows unter `.gitea/` (ohne eigenen Bump, sie stehen außerhalb
|
||
des Version-Gates): `nodejs` wird jetzt vor dem Checkout installiert, weil
|
||
act_runner JavaScript-Actions mit `node` *im Job-Container* ausführt und
|
||
`debian:trixie-slim` keins mitbringt; `actions/checkout` auf `@v7`; und CI
|
||
läuft nicht mehr auf reinen Inhaltsänderungen.
|
||
|
||
---
|
||
|
||
## 1.0.0 - 2026-08-30 - Content migrations get a version, a chain, and a mechanical check
|
||
|
||
**Author:** Torben Nehmer
|
||
|
||
**Migration:** none required - no distributed instance exists yet; 1.0.0 is the migration baseline
|
||
|
||
`0.1.0` gave an instance a way to learn that it must migrate. This release gives it a way to
|
||
know *what shape its content is in*, which migrations it still owes, in what order - and a
|
||
mechanical check that a bulk rewrite did not quietly drop something.
|
||
|
||
**Two versions, not one.** `VERSION` describes the machinery. What shape the *content* is in is
|
||
a different number, now in `.wikitool-kb.json` and written only by `migrate done`. Without it
|
||
the state every upgrade passes through - machinery already replaced, content not yet migrated -
|
||
cannot be represented at all, and `migrate status` would have to infer the answer from the stack
|
||
version, which is wrong exactly when it matters. It is a separate file from
|
||
`.wikitool-release.json` because the two have opposite rules: that one is generated and must
|
||
never be hand-edited, this one is mutable instance state. `dist export` seeds it with the
|
||
exported version, since a fresh instance's content is empty and therefore trivially current;
|
||
only a tree predating the file has to answer by hand, via `migrate baseline`.
|
||
|
||
**Migrations run as a chain.** An instance rarely moves exactly one version. `migrate status`
|
||
builds the interval `(kb_version, VERSION]` from the documents under `instructions/migrations/`
|
||
and orders it: 1.3.1 to 2.0.0 runs 1.4.0, then 1.7.0, then 2.0.0. That no migration targets
|
||
1.3.x is not a special case - it simply is not in the interval. `migrate done` refuses any
|
||
version that is not the next link, so a step cannot be skipped and an interrupted multi-step
|
||
upgrade resumes rather than has to be reconstructed.
|
||
|
||
**`migrate verify --from <rev>` is the piece that was missing.** It compares every page against
|
||
a git revision on what a content migration must *not* change: wikilink and citation counts,
|
||
footnote definitions, H1, structural frontmatter. `lint` cannot answer this and never could - it
|
||
reads a single revision, so a reference that went missing leaves a corpus that is still
|
||
perfectly consistent. Everything it compares is a `Counter`, never a set: three of the four
|
||
defects found in the 248-page translation had unchanged link *sets* and only changed counts.
|
||
Verified both ways on the real corpus - a clean run over 248 pages produced zero findings in
|
||
2.2 s, and dropping one of two `[[Docker]]` occurrences from one page was reported as
|
||
`'Docker' 2->1` while `lint` passed all 21 checks in silence.
|
||
|
||
**A migration is a `manual: true` instruction**, at `instructions/migrations/<version>-<slug>.md`
|
||
with `migrates_to:` and `migration_kind:`. No new stage and no new type: the Manual tier exists
|
||
for procedures that must never run implicitly, and `dist export` already ships `instructions/`,
|
||
so a migration reaches every distributed instance with no second export path. (The contract's
|
||
claim that the tier had no example was already stale - `german-terminology.md` uses it too.)
|
||
|
||
**The methodology is now written down.** `instructions/migrate-corpus.md` holds what the
|
||
translation run learned and then deleted with its workshop: units sized by the iteration budget
|
||
(~21 pages against the 30-call ceiling), publishes sized by the Mass-Update Gate, the two being
|
||
different boundaries; `migrate verify` before the summaries and before `lint`; read the *whole*
|
||
lint report; carry the vocabulary between units. Preferring a backwards-compatible change over a
|
||
flag day is a decision point of its own, with `sections.py`'s canonical-plus-alias pattern as
|
||
the worked example.
|
||
|
||
**Enforcement, in two places.** `version bump --major` refuses without either a migration
|
||
document for the new version or `--no-migration "<reason>"`, and `docs verify` checks the same
|
||
thing about the newest changelog entry. An instance told to migrate with nothing telling it how
|
||
is the dead end both close - this entry's own `--no-migration` line is the mechanism's first
|
||
real use.
|
||
|
||
**Starting at 1.0.0.** `compat_key` is stated generally (leftmost non-zero component), so
|
||
nothing in the code special-cases a range; but the *guidance* derived from it under 0.x was
|
||
self-contradictory, assigning `--minor` to both "new capability" and "requires migration". From
|
||
1.0.0 the familiar reading holds throughout: PATCH fixes, MINOR adds compatibly, MAJOR migrates.
|
||
Pre-release suffixes stay unsupported - `x.y.z` is the whole grammar, and a second ordering rule
|
||
would have to be honoured by the release feed, the chain and the compatibility check alike.
|
||
|
||
**One bug found by running it for real.** The historical side of the diff listed every `.md`
|
||
under `kb/` while the working-tree side used `iter_kb_pages`, which skips `COLLECTION.md`,
|
||
`INDEX.md` and the kb-root meta files - so a clean run reported 13 phantom removed "pages". Both
|
||
sides now answer with one shared `kb_scan.is_page_path`, and the regression is pinned.
|
||
|
||
**Deliberately not built:** mechanical runner primitives (`migration_kind: mechanical` describes
|
||
the work today, it does not perform it - a DSL for zero migrations is not worth having), and
|
||
`dist upgrade`, still the manual procedure in `INSTALL.md`.
|
||
|
||
**Verification:** 630 tests pass (46 new), `docs verify`, `instructions verify` and
|
||
`lint --fail-on-error` clean, plus the two real-corpus runs described above.
|
||
|
||
---
|
||
|
||
## 0.1.0 - 2026-08-29 - The stack gets a version, a pipeline, and release files
|
||
|
||
**Author:** Claude Code
|
||
|
||
The repo had no CI and no way to say which stack an instance was running. Both are now in
|
||
place, and they are the same mechanism seen from two ends: a version that CI enforces, stamps
|
||
into every export, and publishes as a release an instance can compare itself against.
|
||
|
||
**`VERSION` versions the stack, never the content.** `tools/`, `types/`, `instructions/`,
|
||
`AGENTS.md` and the contracts - not the `kb/` pages sitting in the same repo. That split is why
|
||
the number is set explicitly instead of being derived from commit messages: `publish --message
|
||
"ingest: ..."` writes content commits here too, so any conventional-commit reading would turn
|
||
every ingest into a release.
|
||
|
||
**Compatibility is read off the leftmost non-zero component**, the rule Cargo's caret ranges
|
||
use: `0.1.3 -> 0.1.4` is safe, `0.1.3 -> 0.2.0` is not, and from `1.0.0` on the same rule reads
|
||
as the familiar "MAJOR means migration". Stating it this way is what lets the `0.x` era carry
|
||
the migration signal at all - under plain "MAJOR breaks" semantics every `0.x` release is
|
||
indistinguishable from every other, which is exactly the signal update detection needs. Nothing
|
||
about the mechanism changes at `1.0.0`.
|
||
|
||
**New: `wikitool version`** - `show` (offline: the version plus the release stamp it came
|
||
from), `bump --major|--minor|--patch --title` (writes `VERSION` *and* opens this entry's
|
||
heading, leaving the body to the author - the same structure-by-tool/prose-by-author split as
|
||
`new`), `notes` (prints one entry, so the release workflow never parses markdown in shell), and
|
||
`check`.
|
||
|
||
**`version check` is the one networked command in `wikitool`.** The research note in `TODO.md`
|
||
argues that a network call breaks all four properties every other command has - offline,
|
||
deterministic, free, testable - and it does. It is allowed here on narrow terms: its own
|
||
command that nothing else calls implicitly, no API key, a timeout, the fetch behind an
|
||
injectable function so the tests never touch a network, and a feed that cannot be reached is
|
||
reported as an error rather than silently answered as "up to date". The argument that ruled
|
||
out a Perplexity call was about a *paid, keyed* API; an unauthenticated GET on a release feed
|
||
is a different thing.
|
||
|
||
**`dist export` now ships `VERSION` and writes `.wikitool-release.json`.** The stamp holds the
|
||
version, the export date, the origin (repo, commit, release page, update feed - supplied by
|
||
flags, because `export` still never calls git), and a sha256 per exported file. Nothing reads
|
||
the digests today: they record what the machinery looked like when it was installed, which is
|
||
the only thing a later `dist upgrade` could use to tell a file the instance edited from one it
|
||
merely received. Writing them now is what keeps that upgrade from needing a format change.
|
||
|
||
**CI: `.gitea/workflows/ci.yml`** on push/PR to `main`, `runs-on: linux-docker`, one job that
|
||
stops at the first failure - the four steps `TODO.md` specified (tests, `lint`/`docs
|
||
verify`/`instructions verify` over the dev tree, `dist export`, then `instructions verify`
|
||
against the export) plus two the versioning makes possible:
|
||
|
||
- a **version gate**: if the pushed range touches stack paths, `VERSION` must have moved and
|
||
`CHANGES.md` must say why. It cannot block a push that already landed, but a release can
|
||
never contain a change nobody named.
|
||
- the export smoke test now **replays `setup-instance.md`** end to end (`git init`, identity,
|
||
venv, `instructions sync`, `index rebuild`, `sources rebuild-index`, `doctor`, `docs verify`,
|
||
`instructions verify`, `lint`) instead of running one verify against it. What that tests is
|
||
the artifact as an artifact: the documented path from tarball to working instance.
|
||
|
||
**Release: `.gitea/workflows/release.yml`**, triggered by a push to `main` in which `VERSION`
|
||
changed. It tags `v<version>` (**the CI tags, never an agent** - `AGENTS.md` invariant 5 is
|
||
untouched), exports with the origin flags filled in, and publishes
|
||
`llm-wiki-stack-<version>.tar.gz` plus a `.sha256` as Gitea release assets, with `wikitool
|
||
version notes` as the release body. The tarball is exactly a `dist export` tree, so
|
||
`INSTALL.md`'s Weg A now starts from a download instead of requiring a checkout of this repo.
|
||
|
||
**Both workflows were written against the wiki's own record of the runner** (the `Act Runner`
|
||
and `Gitea Actions` pages), which corrected two guesses. Authentication is
|
||
`${{ gitea.token }}`, the short-lived per-job token this instance issues - not
|
||
`secrets.GITEA_TOKEN`, and not a 1Password secret: nothing in these two workflows leaves the
|
||
instance, so the Zero-Trust secret path the container-build workflows use has nothing to carry.
|
||
And the job image is named explicitly (`container: image: debian:trixie-slim`, the base
|
||
Szenario B already proves works with `actions/checkout@v4`) rather than inherited from the
|
||
`linux-docker` label, because the label-to-image mapping is the one thing the wiki does *not*
|
||
record - and it decides whether `python3` exists at all. Trixie's python3 is 3.13, past the
|
||
3.11 floor `doctor` enforces.
|
||
|
||
**Deliberately not built yet: applying an update.** Detection tells an instance a newer stack
|
||
exists and whether it crosses a compatibility boundary; `INSTALL.md` documents the manual
|
||
procedure. `dist upgrade` is a command that writes into an instance that already has content,
|
||
which is a different risk class and wants the stamp's digests to exist first - they now do.
|
||
|
||
**Also:** `doctor` gained a `stack-version` check (missing `VERSION` is a WARN, since instances
|
||
exported before this release are still perfectly functional; malformed is a FAIL). `docs
|
||
verify` gained the `VERSION`/`CHANGES.md` agreement check. `version show`/`check`/`notes` are
|
||
exempt from the Iteration Budget Gate as read-only; `version bump` is not.
|
||
|
||
**Verification:** 585 tests pass (61 new), and the CI job's own sequence was replayed locally
|
||
end to end against a real export - `instructions sync`, `index rebuild`, `sources
|
||
rebuild-index`, `doctor`, `docs verify`, `instructions verify`, `lint --fail-on-error`,
|
||
`version show` - all clean in a tree with no git history and no content.
|
||
|
||
`version check` was also run against the live feed. It returned 404, which was first read as
|
||
"no release published yet" - wrongly. **This repository is private**, and Gitea answers an
|
||
anonymous caller with the same 404 for an invisible repository as for one that does not exist;
|
||
the repo endpoint returns it for a made-up repository name too. So `$WIKITOOL_UPDATE_TOKEN` is
|
||
not the exception here but the requirement, until the repository is made public. The mechanism
|
||
was unaffected - the variable exists and the 401/403 message already names it - but the two
|
||
documentation claims that said otherwise were wrong and are corrected.
|
||
|
||
That run also found a real bug, now fixed and regression-tested: `version notes`
|
||
terminated an entry at the next *versioned* heading, and every entry below `0.1.0` is headed by
|
||
a date - so the newest entry ran to the end of the file, and the first release would have
|
||
shipped the entire changelog as its notes.
|
||
|
||
What no local run can prove is the runner itself: whether `linux-docker` accepts a job that
|
||
names its own container image, and whether `${{ gitea.token }}` carries the rights to create a
|
||
release. The first run on the server answers both.
|
||
|
||
---
|
||
|
||
## 2026-08-29 - Human docs catch up, and a new instance is asked which language it wants
|
||
|
||
**Author:** Claude Code
|
||
|
||
Auditing README/INSTALL/EVALS/tools/README against the migration turned up one thing that was not
|
||
a documentation gap at all.
|
||
|
||
**A `dist export` instance inherited German silently.** The exported distribution ships
|
||
`kb/CONTRACT.md` § Language, `instructions/german-terminology.md` and the German section names in
|
||
`sections.py` - verified by exporting one and grepping it. `setup-instance.md` has decision points
|
||
for author identity and remote, both of which refuse to be guessed from the source repo on the
|
||
grounds that it is "a different person, a different project". The KB language is the same kind of
|
||
choice and was being passed along without being asked. It is now step 5, placed before the tool
|
||
environment so the answer lands *before the first ingest* - after that, changing it is a migration
|
||
of every existing page rather than an edit to one contract section.
|
||
|
||
The step names what actually has to change for another language (contract section, tone examples
|
||
and hedge words, four page templates, `sections.py` canonical names with the old ones kept as
|
||
aliases, relationship labels) and what does not: the prose/identifier split itself is not a German
|
||
rule. Titles, wikilink targets, cite-ids, enum values, tags, commands and paths follow no KB
|
||
language in any instance.
|
||
|
||
**Documentation brought current:** README gains the language paragraph it lacked entirely, the
|
||
title rule under Tips → Naming, `german-terminology.md` in the layout tree, and German relationship
|
||
labels where it still listed `depends-on, uses, implements`. INSTALL gains the language decision in
|
||
Weg A. `tools/README.md` gains `sections.py` and a design note on why heading names are a
|
||
vocabulary with aliases rather than literals.
|
||
|
||
**One correction worth its own line.** `tools/README.md` claimed tests "use `tmp_path` fixtures and
|
||
monkeypatch `config` paths rather than touching the real `kb/`". Two do not: `test_types_cmd.py`
|
||
and `test_index_build.py` resolve the real `types/` on purpose, since what they assert is that
|
||
behaviour comes from the type-specs. That sentence is why editing `layout: title:` failed six tests
|
||
that looked unrelated - and why the first fix attempt in this session anchored a test on a literal
|
||
heading that the next commit promptly translated. The README now says so, and says what such a test
|
||
may pin: structure, never wording.
|
||
|
||
---
|
||
|
||
## 2026-08-29 - The corpus is German; the glossary outlives the run
|
||
|
||
**Author:** Claude Code
|
||
|
||
All 248 `kb/` pages are translated, no English template heading remains anywhere, and
|
||
`source_language:` is set on all 38 source pages. `wikitool lint` reports clean on all 21 checks.
|
||
The `translate-kb-de` workshop is closed.
|
||
|
||
**What survived the closure, and why.** `work/CONTRACT.md` requires a run's durable conclusions to
|
||
be out of `work/` before the directory is deleted. Of the run's four files, three were bookkeeping -
|
||
unit plan, checklist, subagent brief - and died with it. The glossary did not: it is not a migration
|
||
artifact. Every future ingest of an English source produces German prose and meets the same
|
||
questions, so it is now [instructions/german-terminology.md](instructions/german-terminology.md),
|
||
linked from kb/CONTRACT.md § Language. The rule stays in the contract, the vocabulary moved to the
|
||
instruction layer - a word list is lookup material, and 80 lines of it do not belong in a file
|
||
loaded on every write.
|
||
|
||
**What the migration cost, and where.** The mechanical checks - frontmatter, H1, wikilink and
|
||
cite-id multisets compared against `HEAD` - found four defects in 248 pages: a dropped citation
|
||
that silently unsourced a claim, a dropped wikilink, an invented one, and one translated H1. Three
|
||
of those four would have passed a set-based comparison; only counting occurrences caught them. None
|
||
would have been reported by `lint`, which checks whether references resolve, not whether one went
|
||
missing.
|
||
|
||
The checks found nothing else, and that is the more useful result: **every remaining error was a
|
||
plausible, consistent, wrong word.** „Geheimnis" for `Secret` (96 occurrences), „Fähigkeit" for
|
||
`Skill` and `Capabilities`, „Anspruch" for *claim*, and 182 sentences in the polite „Sie" form the
|
||
English imperative invites. Structural verification cannot see any of these - each page is
|
||
internally perfect - and neither can a subagent that made the choice consistently.
|
||
|
||
**Where the errors actually came from.** Nine glossary revisions were needed, and the pattern held
|
||
throughout: the agents followed the instructions, and the instructions were ambiguous. A
|
||
counter-check for `capabilities` was written backwards and would have propagated through eleven
|
||
units. "Separation of Concerns" was listed as a choice between two options rather than as a fixed
|
||
phrase, and one agent took the other option and reported doing so. "Fehler melden, nicht
|
||
korrigieren" meant errors *in the source*, and an agent applied it to its own bad German. Cheap
|
||
parallel agents do not degrade an instruction gracefully - they execute both readings of it at once,
|
||
in the same batch, which makes every ambiguity immediately visible and immediately expensive.
|
||
|
||
---
|
||
|
||
## 2026-08-29 - Two frontmatter fields no command can write
|
||
|
||
**Author:** Claude Code
|
||
|
||
Both surfaced while closing the translation run, and both are recorded rather than fixed - the
|
||
right shape for either is a design question, not a patch.
|
||
|
||
**`source_language:` has no writer.** The field was added to `types/source.schema.yaml` this
|
||
morning, and `wikitool new source --set source_language=de` sets it at creation. On the 37 source
|
||
pages that predate it there is no path at all: `touch` writes exactly `modified`/`date`, `summary`,
|
||
`provenance` and `confidence_base`, and nothing else offers a generic frontmatter setter. Backfilling
|
||
meant editing the field directly. That is defensible for a plain data field - it is neither generated
|
||
nor a page-reference array, so AGENTS.md invariant 1 does not reach it - but "add a schema field"
|
||
and "be able to set it on the pages that already exist" should not be separate capabilities. Adding
|
||
a field to a schema is currently a half-migration by construction.
|
||
|
||
**`comparison` declares no modification date.** `touch --page "amd-pstate vs acpi-cpufreq"` fails
|
||
with *"Type types/comparison.md declares no modified/date field - pass --no-date to skip it"*,
|
||
because the schema has only `created`. So a comparison page can record when it was made but never
|
||
that it was revised - and `touch --summary` on one requires `--no-date` every time. Either
|
||
`comparison` should gain `modified:` like `entity` and `concept`, or the omission should be
|
||
documented as deliberate; right now it reads as an oversight that the error message papers over.
|
||
|
||
Neither blocked the run. Both are the kind of gap that only shows up when something walks the whole
|
||
corpus, which is exactly what a migration does and what routine per-page work never will.
|
||
|
||
---
|
||
|
||
## 2026-08-29 - Date fields hold dates, and the special case disappears
|
||
|
||
**Author:** Claude Code
|
||
|
||
Supersedes this morning's ISO-date exemption, which is now deleted. That fix worked by teaching
|
||
`_round_trips_as_string` to recognize a date-shaped *string* and leave it bare - a second place
|
||
that had to know what a date looks like, kept in sync by hand with the schemas and with
|
||
`validate_frontmatter`'s normalization.
|
||
|
||
The question that removed it: **why is there a date-shaped string at all?** `_format_scalar` has
|
||
always rendered a `datetime.date` bare, and `yaml.safe_load` yields exactly that for every page on
|
||
disk. Only two places ever put a *string* in a date field - `touch` and `new`, both via
|
||
`today_iso()` - and the guard existed solely to paper over that mismatch. Both now write
|
||
`datetime.date`, the exemption is gone, and no code outside `_format_scalar` needs an opinion about
|
||
date syntax.
|
||
|
||
Three things fell out of it:
|
||
|
||
- **`touch` no longer reports a change on every run.** It compared `frontmatter.get(field)` - a
|
||
`datetime.date` read from disk - against a string, which is never equal, so an unchanged date was
|
||
rewritten and logged as a change every time. With both sides dates, an unchanged page is now
|
||
correctly a no-op.
|
||
- **`--date` and `--set <date field>=` validate their input.** Previously any string was written
|
||
through; `29.08.2026` would have landed in frontmatter and surfaced later as a schema error on a
|
||
page nobody was looking at. Both now fail immediately, naming the expected shape.
|
||
- **`normalize_dates` moved to `frontmatter_io` and is shared.** There were two validators and only
|
||
one of them converted dates: `TypeResolver.validate_frontmatter` (behind `lint`) did, `touch`'s
|
||
`validate_fields` did not. Harmless while `touch` wrote strings, and a latent failure the moment
|
||
it stopped - found by writing the change rather than by the tests.
|
||
|
||
**On the question that prompted this** ("can the quoting be schema-driven - we know what belongs
|
||
there"): the instinct is right, but the schema does not distinguish the two cases. A `tags[]` entry
|
||
and `modified:` are both declared `type: string`; keying on the declared type gives dates the same
|
||
answer as `"1945"`, which is the wrong one. Only `format: date` separates them, and reaching it from
|
||
`frontmatter_io` would mean importing `type_resolver`, which already imports `frontmatter_io`.
|
||
Making the value's own type honest answers the same question without a lookup - and
|
||
`_build_frontmatter` was already schema-driven on `format == "date"`, which is where that knowledge
|
||
belongs.
|
||
|
||
---
|
||
|
||
## 2026-08-29 - The round-trip guard reached date fields it should not have
|
||
|
||
**Author:** Claude Code
|
||
|
||
Follow-up to the frontmatter fix earlier today, and a reminder that a correct-looking rule can
|
||
still be wrong in context. `_round_trips_as_string` quotes anything that would read back as a
|
||
different type - which is exactly right for a `"1945"` tag, and exactly wrong for `modified:`.
|
||
PyYAML reads a bare `2026-08-29` as a `datetime.date`, so the guard started quoting it, and 53
|
||
pages came out of one batch carrying `modified: '2026-08-29'` while the other ~195 still had it
|
||
bare.
|
||
|
||
Nothing broke: `TypeResolver.validate_frontmatter` already converts date objects to strings before
|
||
validating, precisely because the schemas declare `type: string` while YAML hands back a date. That
|
||
normalization *is* the corpus convention, and the guard had quietly voted against it. The result
|
||
was not corruption but something more corrosive - a field with two spellings that alternate
|
||
depending on whether `touch` happened to change the value, so every later diff carries noise
|
||
nobody can attribute.
|
||
|
||
Full ISO dates (`^\d{4}-\d{2}-\d{2}$`) are now exempt from the guard, and the 53 pages are
|
||
normalized back to bare. The exemption is deliberately narrow: `1945` is an int to YAML, not a
|
||
date, so a tag by that name still gets quoted.
|
||
|
||
Found by reading the Mass-Update Gate's per-file churn: two of 32 translated pages had asymmetric
|
||
line counts where every other page was symmetric. Chasing that asymmetry - it turned out to be
|
||
harmless line rewrapping - surfaced the `modified:` line in the same diff.
|
||
|
||
---
|
||
|
||
## 2026-08-29 - Frontmatter round-trip turned string values into other YAML types
|
||
|
||
**Author:** Claude Code
|
||
|
||
`_format_scalar` decided quoting from a hardcoded list: empty, leading special character,
|
||
surrounding whitespace, `": "`, or one of `true/false/null/~`. A string that merely *looks* like a
|
||
number was not on it, so the Python string `"1945"` was written bare and read back as `int` on the
|
||
next load. `wikitool touch` rewrites the whole frontmatter block, so translating a page was enough
|
||
to corrupt a tag it never mentioned: `Vannevar Bush` came out of a `--summary` update failing
|
||
schema validation on `Field 'tags[3]': 1945 is not of type 'string'`.
|
||
|
||
The list was also short in a second direction. `yaml.safe_load` follows YAML 1.1, where `yes`,
|
||
`no`, `on` and `off` are booleans, and it resolves `0x1F`, `1_000`, `1e5` and `+3` as numbers -
|
||
none of which the check knew about.
|
||
|
||
Rather than lengthening the list, `_round_trips_as_string(text)` now asks the loader whether
|
||
`yaml.safe_load(text) == text`, and quotes when it does not. Reader and writer agree by
|
||
construction, and a resolver rule this code has never heard of cannot drift out from under it.
|
||
|
||
Two pages in the corpus were one `touch` away from the same corruption - `Memex` (`1945`) and
|
||
`10.000 Dice Game` (`10000`) - which a translation run touching all 248 pages would have reached.
|
||
`Vannevar Bush` is repaired.
|
||
|
||
**How it was found is the reusable part.** The `wikitool lint` run at the end of a unit reported a
|
||
schema error on a field the unit had not edited. A page that changes in a way nobody asked for is
|
||
worth chasing even when the visible diff looks right, and the check that caught it - lint after
|
||
every unit, before publish - is now in the run's README rather than in anyone's memory.
|
||
|
||
---
|
||
|
||
## 2026-08-29 - `work new --key` for a run that is not an ingest
|
||
|
||
**Author:** Claude Code
|
||
|
||
The workshop layer assumed every task too big for one session was an ingest: `work new` required
|
||
`--input <raw path>` and derived the run key from it. A translation pass over the 248 existing
|
||
`kb/` pages has no raw input at all, so it could open no workshop - and the alternative was worse
|
||
than cosmetic. Without `plan.md` there are no written unit boundaries, and `instructions/gates.md`
|
||
allows a fresh `WIKITOOL_SESSION_ID` **only** at a boundary recorded there; a run with no plan
|
||
either exhausts one 30-call budget or takes new session ids on no authority but its own.
|
||
|
||
`work new --key <run key>` names the key outright. Exactly one of `--input`/`--key` is required,
|
||
and a `--key` may not start with `ingest-`, which stays reserved for derived keys so a directory
|
||
name keeps saying which kind of run made it. `plan.md` gets a second template for these runs -
|
||
units sized by the iteration budget rather than by `raw_files` count, since there is no tree to
|
||
cut.
|
||
|
||
Also worth recording, because it will happen again: adding a parameter broke seven tests that call
|
||
`new_command(...)` directly. Bypassing Typer means an omitted argument keeps its
|
||
`typer.Option(...)` sentinel rather than becoming `None`, so a mutual-exclusion check saw both
|
||
options as supplied. `test_git_publish.py` already documents this trap in `_publish`; the fix is to
|
||
pass every parameter explicitly, not to loosen the check.
|
||
|
||
---
|
||
|
||
## 2026-08-28 - Two bugs the translation sample run surfaced
|
||
|
||
**Author:** Claude Code
|
||
|
||
Both found by translating six pages by hand before turning subagents loose on 248. Neither is
|
||
about language; both were latent and would have been amplified by the bulk run.
|
||
|
||
**`touch` overwrote a source's publication date.** `DATE_FIELDS = ("modified", "date")` picks
|
||
whichever field the type declares, and a `source` declares `date:` - which the schema documents as
|
||
"Publication or creation date". So `touch --summary` on a source page silently rewrote the raw
|
||
material's own date to today, leaving the page contradicting the `**Datum:**` line printed in its
|
||
own body. Rewriting a summary is a statement about the page; the source's date is a fact about the
|
||
world, and today's date is simply wrong for it. `modified:` is still bumped automatically; `date:`
|
||
now moves only on an explicit `--date`. Caught on one page during the sample, it would have hit
|
||
all 38 source pages in the bulk pass.
|
||
|
||
**The Mass-Update Gate misreported any file with a non-ASCII name.** `_changed_files` reads
|
||
`git status --porcelain -z`, which emits raw paths; `_numstat` read `git diff --numstat` *without*
|
||
`-z`, and git renders such a path quoted (`"kb/sources/Source - 10.000 ausw\303\274rfeln…"`). The
|
||
lookup missed, the file fell through to `_untracked_stat`, and a rewrite was reported as a pure
|
||
insertion: `+54/-0` for a change git counted as `+28/-27`. This is the same failure mode as the
|
||
718-line deletion reported as `-174` on 2026-08-27, reached by a different route - the report
|
||
understating removals in the one direction a reviewer most needs it not to. `_numstat` now passes
|
||
`-z` and parses NUL-delimited records, including the empty-path-plus-two-records form git uses for
|
||
a rename. Regression tests for both are in `test_git_publish.py` and `test_touch.py`.
|
||
|
||
Worth noting how this one was found: the gate printed the wrong number in a clearance report that
|
||
was being read carefully because it was about to be approved. The numbers a gate prints are the
|
||
gate.
|
||
|
||
---
|
||
|
||
## 2026-08-28 - Section headings become a named vocabulary instead of hardcoded English literals
|
||
|
||
**Author:** Claude Code
|
||
|
||
The language policy earlier today froze all section headings as English, on the grounds that
|
||
`xref add` and `cite add` match three of them literally and a translated heading would silently
|
||
append a *second* section. That reasoning was sound but the conclusion was too cheap: it left
|
||
every page with an English skeleton around German prose, which is most of what "the wiki is in
|
||
German" is supposed to mean. Tracing the literals showed the constraint was smaller than it
|
||
looked - three constants in two files, with `page_ops.py` matching bullet *shapes* rather than
|
||
headings and not involved at all.
|
||
|
||
**`wiki_tools/sections.py`** now owns the vocabulary. Each heading has one canonical name - what
|
||
the tool writes - and aliases it still recognizes:
|
||
|
||
| Canonical | Recognized alias |
|
||
|---|---|
|
||
| `## Beziehungen` | `## Relationships` |
|
||
| `## Siehe auch` | `## See Also` |
|
||
| `## Fußnoten` | `## Footnotes` |
|
||
|
||
That asymmetry is the point: a 248-page corpus cannot be translated atomically, so a page still
|
||
carrying the English heading has to keep working. `xref add` finds and appends to it rather than
|
||
creating a duplicate, and only a page's own translation changes the name.
|
||
|
||
**A regression the tests caught, worth recording.** Making `render_page_body` write the canonical
|
||
heading turned `cite sync` into a silent migrator: every page with an English Footnotes block came
|
||
back `changed`, so one `cite sync --all` would have rewritten the whole wiki and buried the real
|
||
diff. `split_cite_block`'s counterpart `cite_block_heading(body)` now reports the heading the page
|
||
actually carries, and the three call sites pass it back into `render_page_body`. Renaming a
|
||
heading is the translation pass's job, never a side effect of another command. `cite sync` is a
|
||
no-op on an untranslated page again, and a test pins that.
|
||
|
||
Also here: the four page-type templates now scaffold German sections; `**Type:**` becomes
|
||
`**Typ:**` while the value stays the schema enum that `search --field` filters on; and
|
||
kb/CONTRACT.md's typed relationship labels (`depends on` → `hängt ab von`, …) are German, which is
|
||
safe because no code matches on them - an untranslated label is stale wording, not a broken
|
||
reference.
|
||
|
||
Not covered here: the 248 existing pages still carry English headings and prose. They are
|
||
recognized, not migrated.
|
||
|
||
---
|
||
|
||
## 2026-08-28 - Canonical KB language: German, with an identifier carve-out
|
||
|
||
**Author:** Claude Code
|
||
|
||
The wiki had no language rule anywhere - not in `AGENTS.md`, not in the contracts, not in the
|
||
skills, not in the schema. The 248 existing pages were English purely by LLM habit, and a German
|
||
raw file (`raw/documents/wuerfelspiel.md`) had already been
|
||
summarized into English with its German terms preserved in quotes. That is a defensible outcome
|
||
reached by accident, which means the next ingest could just as well have gone the other way.
|
||
|
||
**The rule** now lives in one place, `kb/CONTRACT.md` § Language: pages are written in German,
|
||
and **every line of a page is either prose or an identifier - only prose is translated.** The
|
||
carve-out is not a list of exceptions bolted onto a language rule; it is the rule's second half,
|
||
and it is what makes the policy mechanically safe:
|
||
|
||
- **Page titles and the H1 that repeats one** follow the subject's own established name, not the
|
||
KB language. `GitOps Ownership Model` and `Act Runner` stay. This keeps titles - and with them
|
||
`[[wikilinks]]`, the `[^cite-id]` ids derived from them, and every frontmatter reference array -
|
||
out of the language question entirely. `lint` already reports an H1 that stops matching its title.
|
||
- **Section headings stay English** because `xref add` and `cite add` locate them by literal name
|
||
(`commands/xref.py:75,79`, `commands/cite_cmd.py`). A translated `## Relationships` does not
|
||
error - it makes `xref add` append a *second* section, silently.
|
||
- **The generated `**Type:**` line** renders a schema enum value that `search --field` filters on.
|
||
- **`tags:`** are search keys; commands, paths, config keys and code are what they are; quotations
|
||
stay verbatim in the source's own language.
|
||
|
||
**Consequential edits in the same contract.** § Confidence prescribed the literal hedge words
|
||
"possibly"/"may"/"uncertain"/"unconfirmed", and § Tone's buzzword and AI-filler lists and its
|
||
good/bad example pair were English. Those are words meant to appear *in pages*, so under a German
|
||
policy the contract was instructing against itself. All now German.
|
||
|
||
**Sources in another language** are summarized in the KB language regardless of what the raw file
|
||
is in - a source page is evidence *about* a source, not a substitute for it. New optional
|
||
`source_language:` (ISO 639-1, `types/source.schema.yaml`) records what had to be translated, and
|
||
makes "which pages are translations" answerable with `search --field source_language=fr`. Optional
|
||
by design: the 38 existing source pages predate the rule and stay valid.
|
||
|
||
**The four page type-specs** (`entity`, `concept`, `source`, `comparison`) have German prose now -
|
||
they are the authoring surface that shapes `kb/`, and `types describe` dumps their body verbatim to
|
||
whoever is writing a page. Their ```markdown template fences are untouched, for the heading reason
|
||
above. Note that `extract_template` (`type_resolver.py:190`) takes the *first* markdown fence in the
|
||
body, not the one under `## Template`, so a translated section must never introduce a markdown
|
||
example block above it. The control plane - `AGENTS.md`, the stage contracts, `instructions/`, and
|
||
the type-specs for non-page artifacts - stays English.
|
||
|
||
Not covered here: the 248 existing pages, which are migrated separately, and `layout: title:` in
|
||
`types/entity.md` (the generated INDEX headings), which moves with that migration since it forces
|
||
an `index rebuild`.
|
||
|
||
---
|
||
|
||
## 2026-08-28 - Mass-Update Gate: `--yes` retired for an exit-42 clearance code and `--confirm <token>`
|
||
|
||
**Author:** Claude Code
|
||
|
||
The 2026-08-23 procedural fix - "run `publish` without `--yes` first, let the gate refuse, then
|
||
re-run with `--yes`" - failed a third time on 2026-08-27: an agent asked "want me to publish?",
|
||
got "Ok", ran `publish` (refused), and ran `publish --yes` in the *same turn*. The documented
|
||
order was followed - refusal before `--yes` - but no human turn happened in between, so the
|
||
approval the trace showed was the agent waving itself through. A procedure written down in
|
||
advance is a ritual an agent can perform without a human in the loop, and `eval score`'s
|
||
`gate-not-self-opened` could not see the difference because it only ever looked at single calls.
|
||
|
||
**What shipped, and the false start that shaped it.** The first attempt made the gate
|
||
un-self-openable: a ticket bound to the changeset, redeemable only after a `prompt.submitted`
|
||
trace event *and* a `wikitool approve <id>` typed in a second terminal. It worked and it was
|
||
unusable - on a harness driving `wikitool` through a shell tool the agent cannot produce the turn
|
||
signal at all, and the human is asked to leave the conversation to approve anything. It was built,
|
||
tested, and then deleted; a gate nobody can pass is not a safe gate, it is an abandoned one. What
|
||
replaced it splits the problem in two: cheap honest enforcement in-process, and the real check in
|
||
the eval layer.
|
||
|
||
- **A distinct exit code.** `EXIT_NEEDS_CLEARANCE = 42` (`commands/_util.py`, via
|
||
`needs_clearance()`) is now a third outcome beside success (0) and validation error (1),
|
||
meaning "a human has to see this output before anything proceeds". An agent, a hook, a CI job
|
||
and a trajectory scorer can each tell it apart from "your input was wrong, fix it and retry" -
|
||
which exit 1 could never express. `AGENTS.md`'s tool error contract lists four outcomes now,
|
||
not three.
|
||
- **The procedure moved into the tool's output.** A tripped gate prints what would change, the
|
||
full counted file list, and the exact copy-pasteable `--confirm <token>` line that publishes it
|
||
(`git_publish.clearance_message` / `rerun_command`, the latter reconstructing the command line
|
||
rather than echoing argv so it is correct however the original was spelled, and `shlex`-quoted).
|
||
`instructions/gates.md` now says only "exit 42 -> show the user the output, stop" and nothing
|
||
about the mechanism - deliberately, since a recipe in the instruction layer is one an agent can
|
||
complete alone.
|
||
- **`--confirm <token>` replaces `--yes`.** The token is a 12-hex digest of the counted file list
|
||
plus the publish target (`changeset_token`), so a clearance never carries to a changeset the
|
||
user did not see - touch one more file and the token changes and the gate asks again. This
|
||
closes a hole `--yes` always had: it published whatever was in the working tree when it ran.
|
||
A wrong, invented or superseded token exits 42 again with the current list. `--yes`/`-y` stay
|
||
registered only to fail with an explicit error instead of a Typer usage error.
|
||
- **Stateless.** No ticket file, no TTL, no session bookkeeping, no `approve`/`approval status`
|
||
commands, and nothing under `tools/.wikitool_session/` to get out of sync - the token is a pure
|
||
function of the working tree and can be recomputed at any time.
|
||
- **The real check is in the eval layer**, where a whole trajectory is visible instead of one
|
||
call (`evals/trajectory.py`): **`clearance-ended-the-turn`** (error) flags a `wikitool.call`
|
||
between an exit-42 call and the next `prompt.submitted` - precisely the "refused, then confirmed
|
||
in the same breath" shape all three incidents had; **`clearance-was-asked-for`** (error) flags a
|
||
`gate.cleared` whose token no `gate.refused` ever issued, catching both an invented token and
|
||
one reused from a different changeset. `gate-not-self-opened` still hard-flags `--yes`/`-y`.
|
||
Rules can now report a third state, **skipped** (`Rule.skipped`/`skip_reason`, rendered `skip`),
|
||
so `clearance-ended-the-turn` says "cannot say" on a harness with no prompt hook rather than
|
||
fabricating a pass - the degradation rule, applied to a rule that genuinely needs an optional
|
||
event.
|
||
- **Telemetry:** `gate.cleared` (token, counted, threshold, files) is new; `gate.refused` gained
|
||
`reason: needs-clearance|stale-token`, `token` and `presented_token`. The ticket-era
|
||
`gate.ticket_issued`/`gate.consent`/`gate.approved` are gone.
|
||
- **Harness adapters.** `.claude/settings.json` (new, tracked, and copied by `dist export` as a
|
||
single file via `dist_cmd.SINGLE_FILES` so the generated `.claude/skills/` is not swept in with
|
||
it) wires `UserPromptSubmit` to `trace_ingest.py`, which is what makes
|
||
`clearance-ended-the-turn` scorable here, and adds a `permissions.ask` rule on
|
||
`Bash(tools/wikitool publish --confirm:*)` so the clearing call prompts in-chat while ordinary
|
||
publishes do not. Verified live against this CLI: a `PreToolUse` hook returning
|
||
`permissionDecision: "ask"` does **not** override a matching `permissions.allow` rule
|
||
(permissions beat hooks), which is why the rule is a `permissions.ask` entry and not a hook -
|
||
and it is a prefix match, so `rerun_command` always emits `--confirm` first. Copilot CLI already
|
||
emits `userPromptSubmitted`, so it needed no change; its decision-document schema stays
|
||
unverified and therefore unwritten, per the rule the Vibe adapter established.
|
||
- **Honest limits, recorded rather than papered over.** None of this proves a human typed
|
||
anything: the token sits in the agent's own context. `prompt.submitted` proves a turn boundary,
|
||
not that the output was relayed or that the reply meant yes. Closing that needs the harness to
|
||
report what the agent actually said, which no adapter here does.
|
||
- **The clearance report is grouped and sized, not a flat list of paths.** A 30-line alphabetical
|
||
list tells a reviewer nothing about what kind of change it is. The refusal now opens with a
|
||
scale line (file count, `+added/-removed`, status breakdown), then only the attention notes that
|
||
actually apply - deletions named individually, how many files touch the agent control plane or
|
||
harness config, how many published pages change, the largest single file, any binary - and then
|
||
every counted path grouped by area (`FILE_GROUPS`), each with its status letter and churn.
|
||
Ordering is by consequence, not alphabet: published knowledge and the control plane first,
|
||
mechanically-regenerated files (`is_generated`) split into their own group at the end and
|
||
labelled as needing no review. Deliberately notes, not a risk score: every line is a checkable
|
||
statement a reader can disagree with by looking, where a number would invite trusting it. Only
|
||
applicable notes are emitted, since a wall of "0 deletions" reassurances is how a reviewer
|
||
learns to skim. Churn comes from `git diff --numstat HEAD` for tracked files and a line count
|
||
for untracked ones, with binaries marked rather than given a fabricated count; a repository with
|
||
no HEAD yet (the first-commit case in `setup-instance.md`) degrades to counts instead of failing.
|
||
- **The token now covers file contents, not just file names.** `changeset_token` digests each
|
||
counted path *with* a hash of its bytes, closing the same "approved A, published B" hole one
|
||
level down: previously a cleared list could be published after rewriting one of those files, so
|
||
the user's approval would have covered text they never saw. Editing a file after clearance now
|
||
invalidates the token exactly as adding one does.
|
||
- **Deletions report their size.** `collect_changes` originally short-circuited a deleted file to
|
||
`0/0` churn instead of asking `git diff --numstat`, which knows it. The first changeset to
|
||
exercise this deleted a 718-line file and went out headlined `-174` against git's own `-891` -
|
||
four fifths of the removals missing, in the one direction a reviewer least wants understated. A
|
||
deletion now carries its real removed-line count and renders as `-718 deleted`, so a one-line
|
||
stub and a long document stop reading identically.
|
||
- **The clearance message's wording, fixed after it failed on first contact.** It originally read
|
||
"SHOW THIS OUTPUT TO THE USER"; the first agent to receive it replied with a file count and a
|
||
pointer to "the output above" - which the user could not see, because on a harness that runs
|
||
`wikitool` through a shell tool a command's stdout lands in the agent's context, not on anyone's
|
||
screen. Printing and showing are different acts there, and an instruction that conflates them
|
||
reads as already satisfied the moment the text exists. The message now leads with "THE USER
|
||
CANNOT SEE THIS OUTPUT", asks for the file paths to be copied into the reply, and names the
|
||
near-misses that do not discharge it (a count, a summary, "the output above"). `gates.md` got
|
||
the same treatment. Worth generalising: an instruction to an agent should name the act, not the
|
||
intent.
|
||
- Also updated: `instructions/gates.md` (rewritten around the exit code, with an exit-code table),
|
||
`instructions/publish-cycle.md`, `tools/CONTRACT.md` (publish command + error-contract rows),
|
||
`AGENTS.md` (invariants 5-6, tool error contract), `README.md`, `INSTALL.md`,
|
||
`instructions/setup-instance.md`, the `wiki-ingest`/`wiki-lint` skills, `tools/README.md`,
|
||
`EVALS.md` (L2 rule table, the worked example rewritten around where the check moved to and why),
|
||
and - as wiki content - `kb/concepts/Mass-Update Gate.md` and
|
||
`kb/concepts/Iteration and Cost Limits.md`.
|
||
|
||
## 2026-08-26 - One-way dev/dist split: `instructions/dev/`, `stack-dev` skill; `dist enable-dev` retired
|
||
|
||
**Author:** Claude Code
|
||
|
||
Replaced the "dev instance" concept from the previous entry with a one-way model: a distributed
|
||
instance never becomes a dev instance again. Instead of restoring dev-only content into a
|
||
distributed instance, stack development happens in the origin repo (or a new dev instance
|
||
exported from it).
|
||
|
||
- **`tools/wikitool dist enable-dev` is gone**, along with
|
||
`tools/wiki_tools/dist_templates/agents_dev_extras.md`/`readme_dev_extras.md` and
|
||
`instructions/enable-dev-extras.md` (the `manual: true` instruction it backed). There is no
|
||
longer a command that reconstructs dev-only content in a distributed instance - `docs verify`'s
|
||
matching drift check (`check_dev_extras_in_sync`) is removed along with it.
|
||
- **New `instructions/dev/`** - a second, purpose-scoped location for material that is
|
||
exclusively about developing the tool stack itself (compiler/schema/instruction-layer
|
||
changes), as opposed to operating on wiki content. `tools/wikitool dist export` now prunes
|
||
this directory wholesale and unconditionally (`dist_cmd.INSTRUCTIONS_EXCLUDE_DIRS`) - a second,
|
||
independent exclusion mechanism alongside the existing `<!-- dist:strip-start/end -->` marker
|
||
convention, which only ever removed content inside an otherwise-shipped file. The commonplace/
|
||
vendored-knowledge-base guidance that used to live inline in AGENTS.md's marker block moved
|
||
into `instructions/dev/commonplace-kb.md`.
|
||
- **New `stack-dev` skill**, nested at `instructions/dev/stack-dev/SKILL.md` - switches a
|
||
session into tool-development mode: source-binding rules for wiki content do not apply to
|
||
code, and the test/review conventions under `instructions/dev/` apply instead. Nested rather
|
||
than a top-level sibling of the five content skills specifically so the one export exclusion
|
||
removes it together with the instructions it routes to, with no separate skill-exclusion list.
|
||
Linked from AGENTS.md via a routing line inside its own `dist:strip` block - normal (not
|
||
`manual: true`), since it is a recurring session type, not a rare explicit request.
|
||
- **`tools/wikitool instructions verify`** gained a boundary check: nothing outside
|
||
`instructions/dev/` may reference something inside it (`instructions_cmd.dev_only_forbidden_references`),
|
||
because such a reference would dangle the moment `dist export` prunes the directory. A mention
|
||
inside a `dist:strip` block is exempt - `dist_cmd.strip_markers()` removes it from the scan
|
||
before the check runs, since `dist export` removes the block and the directory together.
|
||
`instruction_files()`/`skill_dirs()` were extended to discover `instructions/dev/` as a second
|
||
scan location (previously non-recursive by design).
|
||
- Canonical rule: [instructions/CONTRACT.md](instructions/CONTRACT.md#instructionsdev). Also
|
||
updated: `AGENTS.md`, `README.md`, `INSTALL.md`, `instructions/setup-instance.md`,
|
||
`tools/CONTRACT.md`.
|
||
|
||
## 2026-08-25 - Citation syntax migrated to real GFM footnotes; `wikitool cite`
|
||
|
||
**Author:** Claude Code
|
||
|
||
Replaced the inline `^[[Source - X]]` (or `^[[Source - X|file.md]]`) citation marker - a
|
||
Pandoc-style parser reads it as an inline footnote wrapping a broken shortcut link, so it never
|
||
rendered correctly outside this repo's own tooling - with real GFM footnotes: an inline
|
||
`[^cite-id]` reference plus a `[^cite-id]: [[Source - X]]` definition in a tool-owned `##
|
||
Footnotes` block at the end of the page. The qualifier for a multi-file source now lives in the
|
||
wikilink's alias slot (`[[Source - X|storage-model.md]]`).
|
||
|
||
- **`tools/wikitool cite id/add/sync`** - new command group. `cite id` previews the
|
||
deterministic id `cite_id(title, qualifier)` derives (strip the `Source -` prefix, transliterate to
|
||
ASCII, slugify, `--`-join title and qualifier, `-2`/`-3` suffix on a per-page collision); `cite
|
||
add` upserts a page's Footnotes definition and its frontmatter `sources:` entry and prints the
|
||
marker to paste; `cite sync` prunes definitions nothing references any more, re-renders the
|
||
block in first-reference order, and reports any `[^id]` reference left undefined. `cite id` is
|
||
read-only and exempt from the Iteration Budget Gate.
|
||
- **`wikitool lint`** hard-errors on three new findings: `legacy_citation_markers` (a leftover
|
||
pre-migration `^[[...]]`), `undefined_footnote_refs` (a `[^id]` with no definition), and
|
||
`orphan_footnote_defs` (a definition nothing references) - the same class as `broken_links`,
|
||
not an advisory.
|
||
- **`wikitool rename`** now also refreshes a citation id derived from the old title (in both its
|
||
Footnotes definition and every reference to it) alongside the wikilink retargeting it already
|
||
did; a hand-picked or otherwise-derived id is left untouched.
|
||
- **Migration:** all 71 kb/ pages carrying the legacy marker were rewritten by a one-off script
|
||
(not committed - scratch tooling for a single run) that imports `cite_id()` from
|
||
`wiki_tools.provenance` so it can never mint an id the tool itself wouldn't, and refuses to
|
||
write any page where the multiset of (source title, qualifier) pairs cited before and after
|
||
migration doesn't match exactly. `raw/` and the historical entries in `CHANGES.md`/`kb/log.md`
|
||
were deliberately left untouched.
|
||
- Canonical rule: [kb/CONTRACT.md](kb/CONTRACT.md#provenance-and-citation). Also updated:
|
||
`types/entity.md`, `types/concept.md`, `types/source.md`, `kb/sources/COLLECTION.md`,
|
||
`instructions/page-lifecycle.md`, `instructions/wiki-ingest/SKILL.md`,
|
||
`instructions/wiki-manage/SKILL.md`, `AGENTS.md` invariants 1 and 2, `README.md`.
|
||
|
||
## 2026-08-25 - Distributable instances: `dist export`/`dist enable-dev`, `doctor`, per-instance author
|
||
|
||
**Author:** Claude Code
|
||
|
||
Added a reproducible way to turn this repo into a fresh, contentless wiki instance, instead of
|
||
manual copy-and-strip:
|
||
|
||
- **`tools/wikitool dist export <target>`** writes a contentless copy of the machinery
|
||
(`AGENTS.md`/`README.md`/`EVALS.md` with dev-instance-only content removed, `instructions/`,
|
||
`types/`, `tools/` minus venv/caches, the `.github/hooks/`+`.vibe/` session-tracing config,
|
||
every `kb/*/COLLECTION.md` with no pages or areas, empty `raw/{articles,documents,notes,assets}/`)
|
||
into an empty target. Never touches git.
|
||
- A generic `<!-- dist:strip-start -->`/`<!-- dist:strip-end -->` marker convention replaces the
|
||
one-off "strip the commonplace section" idea: `dist export` removes whatever sits in a marked
|
||
region, and **`tools/wikitool dist enable-dev`** is the exact inverse - it adds the vendored
|
||
`commonplace/` knowledge base back into an existing instance from
|
||
`tools/wiki_tools/dist_templates/`, on request. `docs verify` checks the templates never drift
|
||
from what this repo (itself a dev instance) actually has.
|
||
- **`tools/wikitool doctor [--json]`** is a single read-only health check (dependencies, author
|
||
resolution, git identity/branch/remote, published skills, structure, generated files, session
|
||
id) backing the new setup instruction instead of ten checks spelled out in prose.
|
||
- **Per-instance author.** `author:` on a new source page now resolves from `$WIKI_AUTHOR`, else
|
||
this instance's own `git config user.name` (`config.default_author()`) - never a hardcoded
|
||
name. Neither resolving is a hard failure (`ERROR`, exit 1), not a silent placeholder.
|
||
- **A third instruction-reference tier.** `instructions/*.md` frontmatter gained `manual: true`:
|
||
exempt from `instructions verify`'s "must be referenced" rule, and inverted - it must NOT be
|
||
linked from AGENTS.md or a skill (though a CONTRACT.md mentioning it as documentation is fine).
|
||
For a rare, deliberate procedure that must never run implicitly.
|
||
- New instructions: [instructions/setup-instance.md](instructions/setup-instance.md) (the normal
|
||
path for a new instance: git init, author/remote decision points, bootstrap, first commit) and
|
||
[instructions/enable-dev-extras.md](instructions/enable-dev-extras.md) (`manual: true`, adds
|
||
`commonplace/` back on explicit request only).
|
||
- New [INSTALL.md](INSTALL.md) for humans: prerequisites, the two paths (new instance vs.
|
||
cloning this repo), configuration, `doctor` as the verification step, troubleshooting.
|
||
|
||
## 2026-08-24 - README brought current; keeping the human docs current is now written down
|
||
|
||
**Author:** GitHub Copilot
|
||
|
||
`README.md` had drifted behind six stack changes. What was wrong:
|
||
|
||
- The architecture tree was missing `work/`, `EVALS.md`, `.github/hooks/` and
|
||
`.vibe/`, and still described `reports/` as holding only lint reports.
|
||
- It claimed to be "the only README in the repo", which stopped being true when
|
||
the naming rule changed to let a stage carry both a `README.md` and a
|
||
`CONTRACT.md`.
|
||
- Two links sent readers to `tools/README.md` for the command reference and the
|
||
maintenance schedule. Both moved to `tools/CONTRACT.md` when the command table
|
||
was consolidated - and `docs verify` now enforces that the README has no copy
|
||
of it.
|
||
- `wikitool skills sync` was renamed to `instructions sync`.
|
||
- Lint reports were said to land in `kb/reports/`.
|
||
- The Search section advised a third-party tool for wikis past 100 pages,
|
||
written before `wikitool search` existed.
|
||
|
||
Added: a Telemetry and evaluation section pointing at `EVALS.md`.
|
||
|
||
`AGENTS.md`'s Changelog section now says that a stack change is not finished
|
||
until the human docs describe it. It went there rather than into the Invariants
|
||
list on purpose: an invariant holds in every session regardless of task, and this
|
||
one only applies when the stack changes - a `wiki-ingest` or `wiki-query` session
|
||
never touches a README. The Changelog section is already the place where the
|
||
"record what changed" rules live, so it is one sentence in an existing home
|
||
rather than a new rule competing for the always-loaded budget.
|
||
|
||
---
|
||
|
||
## 2026-08-23 - L0: the pipeline against a wiki the tools built themselves
|
||
|
||
**Author:** GitHub Copilot
|
||
|
||
`tools/wiki_tools/tests/test_pipeline_l0.py` builds a wiki through the CLI -
|
||
`new` → write → `xref add` → `index rebuild` - against an empty tree, then
|
||
asserts that `lint` finds no hard errors, that the catalog covers exactly what
|
||
was created, and that rebuilding it again changes nothing on disk.
|
||
|
||
It closes the last gap the eval ladder rested on. Nothing in the suite chained
|
||
the commands before: `test_lint.py` runs lint over a hand-written fixture, and a
|
||
hand-written fixture cannot show that `new`, `xref`, `index` and `lint` agree
|
||
with one another.
|
||
|
||
**L0 belongs in pytest, not in a separate harness.** The plan had it as a case in
|
||
an `evals/` stage, but a fixture wiki has to be driven in-process - `config.ROOT`
|
||
is derived from `wiki_tools/config.py`'s own location, so `wikitool` cannot be
|
||
aimed at another tree from outside - and driving commands in-process against a
|
||
temporary tree is exactly what the test suite already does. A second mechanism
|
||
for it would have duplicated the suite. The `evals/` stage is therefore not
|
||
created: it exists to hold agent cases and fixtures, and there are none yet.
|
||
|
||
Two findings came out of writing it:
|
||
|
||
- **A scaffolded page does not lint clean.** `new` writes placeholder wikilinks
|
||
for the author to replace, so a page that was created but not yet written
|
||
reports broken links. That is the scaffold saying it is unfinished, and it is
|
||
now pinned by a test so it cannot be mistaken for a defect later.
|
||
- **The shared `kb_dir` fixture no longer validates against the type schemas.**
|
||
Five of its pages are missing `provenance`, `summary` or `raw_files`, and one
|
||
carries a `source` key the schema rejects. `test_clean_wiki_has_no_hard_errors`
|
||
passes anyway because it asserts only `broken_links` and `duplicate_titles`
|
||
rather than the whole hard-error set. Left as found: repairing the fixture
|
||
touches every test that reads those pages, and that is its own change.
|
||
|
||
`EVALS.md` records what an agent runner needs - a repo copy rather than a fixture
|
||
directory, an isolated HOME per run, a mandatory `--agent` so Vibe cannot fall
|
||
back to auto-approve, and a run manifest - and why it was not built: no provider
|
||
credentials are configured here, so a live run cannot be verified, and the Vibe
|
||
adapter is the standing lesson about writing against unverified documentation.
|
||
|
||
---
|
||
|
||
## 2026-08-23 - Let the gate refuse first, even when approval already exists
|
||
|
||
**Author:** GitHub Copilot
|
||
|
||
`instructions/gates.md` gains one rule: run `publish` without `--yes` first, let
|
||
the Mass-Update Gate refuse, then re-run. Approval that arrived earlier in the
|
||
conversation is not a licence to pass `--yes` on the first attempt.
|
||
|
||
This is the first change made *because a trace showed something*. `eval score`
|
||
flagged `gate-not-self-opened` on two consecutive sessions - the one that shipped
|
||
the telemetry layer and the one that shipped the scorer. Both times the file list
|
||
had been shown and approved; both times `--yes` went out on the first attempt, so
|
||
the gate never spoke. A trace records tool calls, not conversations, which makes
|
||
that indistinguishable from an agent waving a gate through on its own.
|
||
|
||
Two occurrences of one shape is a pattern, not an incident, so it warranted a
|
||
decision rather than an explanation. The options were to soften the rule, to give
|
||
the rule a way to see approval (an `--approved-by` flag - but a flag an agent can
|
||
set itself is a weaker check than the one it replaces), or to change the
|
||
procedure. The procedure changed: the outcome is identical, the record is not,
|
||
and the cost is one refused call.
|
||
|
||
The check that found it stays exactly as it was.
|
||
|
||
---
|
||
|
||
## 2026-08-23 - `wikitool eval`: scoring a session against what it left behind
|
||
|
||
**Author:** GitHub Copilot
|
||
|
||
`eval score` turns a trace into a scorecard, and `eval sessions` lists what there
|
||
is to score. Both are read-only and exempt from the Iteration Budget Gate, for
|
||
the same reason `search` is: reading back what a session did is not iteration on
|
||
the wiki, and charging for it would discourage checking one's own work.
|
||
|
||
Two levels, both hard-oracle:
|
||
|
||
- **L1, structure** re-runs `lint`'s checks in-process. The definition of a hard
|
||
error moved out of `lint_command` into `HARD_ERROR_KEYS`, so the scorecard and
|
||
`lint --fail-on-error` cannot drift apart - a run must never pass its score
|
||
while lint refuses it.
|
||
- **L2, trajectory** checks three rules over the trace. Each restates an
|
||
invariant the code cannot enforce in-process: `refusal-not-retried`,
|
||
`gate-not-self-opened`, `content-change-logged`. A gate can refuse a call, but
|
||
nothing stops an agent from calling again with the gate's own flag - that gap
|
||
is what a trajectory check closes.
|
||
|
||
The rules were not invented from the contract text. Each one corresponds to a
|
||
refusal an agent can talk its way around, which is why the gates exist at all.
|
||
Further rules wait for observed failures; a scorer built ahead of a failure
|
||
taxonomy improves its own number rather than the behaviour, which is the failure
|
||
mode `commonplace/kb/notes/evaluation-automation-is-phase-gated-by-comprehension.md`
|
||
describes.
|
||
|
||
Scoring the session that shipped the telemetry layer immediately reported a
|
||
violation of `gate-not-self-opened`: `publish --yes` went out on the first
|
||
attempt, so the gate never got to refuse anything, even though the file list had
|
||
been shown and approved in conversation. The finding is true and benign at once -
|
||
the trace records tool calls, not conversations. It stays an error rather than
|
||
being softened, because weakening a check to make a known case pass is how a
|
||
scorecard stops meaning anything. EVALS.md carries the worked example.
|
||
|
||
Two supporting changes:
|
||
|
||
- **Every trace now opens with a `session.start`** naming what its source can
|
||
report. Mistral Vibe has no session hook, so its traces would otherwise carry
|
||
no `completeness` at all and a scorer could not tell "never happened" from "not
|
||
observable here". The header is written under `x` mode, which elects a single
|
||
writer among the processes appending to one trace.
|
||
- **`telemetry/reader.py`** owns reading a trace back - sort order, torn last
|
||
line, the union of `completeness` across sources - because a consumer that
|
||
re-derived those would drift from the writer.
|
||
|
||
---
|
||
|
||
## 2026-08-23 - Mistral Vibe hooks, verified against the installed CLI
|
||
|
||
**Author:** GitHub Copilot
|
||
|
||
`.vibe/hooks.toml` declares the three hooks Vibe has - `pre_tool`, `post_tool`,
|
||
`post_agent` - and `.vibe/config.toml` puts the telemetry policy in the
|
||
repository instead of in someone's shell profile.
|
||
|
||
Vibe is installed here, and it is open source, so neither file was written
|
||
against the documentation: both were validated with the CLI's own loader
|
||
(`vibe.core.hooks.config._load_hooks_file`, `VibeConfigSchema`), and the
|
||
committed `post_tool` command was executed through Vibe's own
|
||
`HookExecutor` with a real `PostToolInvocation`. Four things that changed as a
|
||
result:
|
||
|
||
- **The config shape is an array of tables**, `[[hooks]]` with a unique `name`,
|
||
not a table per event type as the research summary suggested.
|
||
- **A failing hook cannot block anything.** With `strict = false` - the default -
|
||
a crash or timeout is a no-op warning. This is the mirror image of Copilot,
|
||
where a `preToolUse` hook is fail-closed and a missing interpreter would deny
|
||
every tool call. `strict` and `match` are rejected on `post_agent`.
|
||
- **`post_agent` carries no response text**, only the session context, so it maps
|
||
to `turn.end` rather than to `assistant.message`.
|
||
- **`enable_telemetry` defaults to `true`.** Setting it false is a real change,
|
||
not a restatement of the default. It also gates OTel: `vibe/core/tracing.py`
|
||
exports only when `enable_telemetry and enable_otel`. Both are off here,
|
||
because `enable_otel` ships prompts and tool arguments to Mistral Studio and
|
||
Mistral documents no way to point it at a collector you run yourself.
|
||
|
||
Also confirmed from the source: Vibe already reads `.agents/skills/` and
|
||
`AGENTS.md`, so the directory `wikitool instructions sync` publishes is a
|
||
project-scope skill source for it. This repository needs no adaptation to be
|
||
worked on with Vibe beyond trusting the folder.
|
||
|
||
A new test parses every committed hook config - JSON for Copilot, TOML for Vibe -
|
||
and asserts that each `--event` names an event that exists. A typo there would
|
||
otherwise produce a silent gap in a trace that nothing else would notice.
|
||
|
||
---
|
||
|
||
## 2026-08-23 - Copilot CLI hooks and the chronicle importer
|
||
|
||
**Author:** GitHub Copilot
|
||
|
||
The trace layer now has two sources besides `wikitool` itself.
|
||
|
||
**`.github/hooks/wiki-trace.json`** wires eleven Copilot CLI hook events into
|
||
`tools/trace_ingest.py`. It is committed, so a clone brings its own telemetry;
|
||
`disableAllHooks` opts out without deleting anything. Two details in it are
|
||
load-bearing:
|
||
|
||
- **Every command ends in `|| true`.** `preToolUse` hooks are fail-closed: a
|
||
non-zero exit denies the tool call. Without the guard a missing interpreter
|
||
would turn the observer into a blocker that refuses every tool call in the
|
||
session. Verified by running the committed command with `PATH=/nonexistent`.
|
||
- **The event name is passed explicitly.** Copilot serves two payload dialects -
|
||
camelCase event names give camelCase fields, PascalCase names give the VS
|
||
Code/Claude snake_case shape - so `--event` keeps the mapping independent of
|
||
which one a config picked. Both spellings are mapped either way.
|
||
|
||
**`tools/import_chronicle.py`** reconstructs a trace from a chronicle store
|
||
(`session-store.db`, shared schema between VS Code Chat and Copilot CLI) for the
|
||
surface that has no hooks at all. The store is opened read-only, the import is
|
||
idempotent unless `--force`, and the original timestamps are preserved rather
|
||
than replaced by import time - `write_event` gained a `ts` parameter for that.
|
||
|
||
Reconstruction is honest about its limits: those events carry
|
||
`reconstructed: true`, and the `completeness` list for `vscode-chat` names
|
||
`tool.post` but not `tool.pre`, because the store records that a file was
|
||
touched, not that a tool was about to run. Ordering follows the store's own
|
||
timestamps, which puts a touched file after the reply of its turn; reordering it
|
||
would invent a sequence nobody wrote down.
|
||
|
||
Two events joined the vocabulary. `turn.end` replaces the awkward use of
|
||
`assistant.message` for Copilot's `agentStop` and Vibe's `post_agent`, which mark
|
||
the end of a turn and do not always carry text. `session.error` covers Copilot's
|
||
`errorOccurred`, which can fire for a model call or the system rather than for a
|
||
tool.
|
||
|
||
---
|
||
|
||
## 2026-08-23 - Session traces: the telemetry layer under the eval harness
|
||
|
||
**Author:** GitHub Copilot
|
||
|
||
`wikitool` now records what a session did. Every command appends a
|
||
`wikitool.call` event to `reports/telemetry/<session>/trace.jsonl`, and both
|
||
gates record the moment they refuse. `tools/trace_ingest.py` accepts a hook
|
||
payload from Claude Code, Copilot CLI or Mistral Vibe and normalises it into the
|
||
same vocabulary.
|
||
|
||
The unit tests check the compiler; nothing checked the *agent*. Scoring a
|
||
trajectory - did `new` precede the write, was a refused gate simply retried -
|
||
needs a record of the trajectory, and that record has to survive a change of
|
||
harness. So the design puts the truth in the layer nobody can swap out: the
|
||
repo's own tool. Hooks enrich a trace, they do not constitute it.
|
||
|
||
Three design points worth knowing:
|
||
|
||
- **Hooks call `tools/trace_ingest.py`, never `wikitool`.** A hook fires on every
|
||
tool call, and every `wikitool` invocation is counted by the Iteration Budget
|
||
Gate - routing telemetry through the CLI would let the act of observing a
|
||
session end it. The script imports `wiki_tools.telemetry` as a library, which
|
||
is stdlib-only for the same reason: a hook must run without the venv.
|
||
- **The trace records what the budget exempts.** Read-only retrieval is not
|
||
charged, but it *is* traced: what an agent looked at before acting is exactly
|
||
what a trajectory scorer needs.
|
||
- **The core event set is what the weakest harness can produce.** Claude Code
|
||
has ~30 hooks, Mistral Vibe has three. Every `session.start` carries a
|
||
`completeness` list, so a scorer says "not measurable here" instead of
|
||
silently scoring zero.
|
||
|
||
Prompts and assistant replies are recorded in cleartext, locally, because a
|
||
failure taxonomy cannot be read out of hashes. Guards: secret scrubbing, a
|
||
60 KiB cap, `WIKI_TRACE_CONTENT=0`, and the rule that `raw/` file *contents*
|
||
never enter a trace at all. `reports/` stays gitignored and no exporter is
|
||
configured.
|
||
|
||
Also added: `/EVALS.md`, the human-facing hub for the whole thing, registered in
|
||
the `AGENTS.md` file-naming table. `reports/CONTRACT.md` gained a Traces
|
||
section. `wiki_tools/session.py` now holds the single definition of session
|
||
identity, which `run_budget` and the emitter share.
|
||
|
||
---
|
||
|
||
## 2026-08-23 - `lint` detects contested raw-file ownership
|
||
|
||
**Author:** GitHub Copilot
|
||
|
||
New hard check `duplicate_raw_file_owners`: a raw file named in more than one
|
||
source page's `raw_files:`.
|
||
|
||
`uncovered_raw_files` only ever asked whether a raw file is claimed **at all**,
|
||
so the opposite failure was invisible. It had already happened: one ingested manual
|
||
`10-bootstrap-manual/` subtree carried two incompatible cuts at once, leaving
|
||
eight raw files claimed by two pages each, undetected until someone counted by
|
||
hand.
|
||
|
||
The rule it enforces is the one recorded in `types/source.md`: **`raw_files:` is
|
||
a maintenance claim, `^[[Source - X]]` citation is free reuse.** Any number of
|
||
pages may cite a source; with two *owners* it is undefined which page must be
|
||
refreshed when the raw file changes, so both rot silently and neither is
|
||
identifiably the stale one.
|
||
|
||
Repeats inside a single page's own list are untidy, not contested - there is
|
||
still exactly one owner - so they are not reported.
|
||
|
||
---
|
||
|
||
## 2026-08-23 - Workshop layer `work/`; tree ingests; `## Not Extracted`
|
||
|
||
**Author:** GitHub Copilot
|
||
|
||
A tree like `raw/documents/handbook/` (71 files) does not fit in one ingest, and
|
||
the wiki had no place to keep a run that spans sessions. Four changes.
|
||
|
||
### `work/` - a tracked, transient workshop layer
|
||
|
||
New stage beside `kb/`, with [work/CONTRACT.md](work/CONTRACT.md). One directory
|
||
per run, holding the plan and the intermediate extracts on the way to a `kb/`
|
||
page, deleted when the run closes.
|
||
|
||
It is the mirror image of `reports/`: a report is recomputable and therefore
|
||
gitignored, a workshop is judgment in progress that nothing can re-derive and is
|
||
therefore **tracked**. A gitignored workshop would be a `reports/` directory
|
||
with extra steps - the whole point is that a multi-day run survives.
|
||
|
||
`work/` joins `raw/` and `kb/` in `CONTENT_DIRS` and in the `.gitignore`
|
||
backstop: an ignore rule reaching it is a data-loss bug, and `docs verify` now
|
||
carries a canary that says so.
|
||
|
||
### Run keys are derived, and a collision is an error
|
||
|
||
`wikitool work new --input raw/documents/handbook` creates
|
||
`work/ingest-documents-handbook/`. The key comes from the input path *below*
|
||
`raw/` with separators flattened - not the basename, because
|
||
`raw/documents/handbook` and `raw/articles/handbook` would collide.
|
||
|
||
**The directory name is the run key.** No second identifier, for the same
|
||
reason a page title is a page's only identifier (invariant 2). A collision is
|
||
refused rather than suffixed: it means the tree is already being ingested, which
|
||
is the condition the key exists to detect. A genuine re-ingest is `--again`,
|
||
which dates the key.
|
||
|
||
`wikitool work close --run-key <name> --yes` deletes a finished run, listing
|
||
what would be lost first.
|
||
|
||
### The Mass-Update Gate stops counting `work/`
|
||
|
||
`publish` still commits and pushes workshop files, but they no longer count
|
||
toward the threshold. The gate's justification is that a push publishes
|
||
knowledge irreversibly; a directory that is deleted on closure is not that, and
|
||
making a reviewer approve twelve scratch files is exactly the review fatigue the
|
||
threshold exists to avoid.
|
||
|
||
`GATE_EXEMPT_PREFIXES` is a constant, deliberately not a flag - a
|
||
`--gate-exempt` option would be a gate an agent could open on its own
|
||
initiative. The refusal message now reports both counts.
|
||
|
||
### Tree ingests, and recording what was *not* extracted
|
||
|
||
[instructions/ingest-large-tree.md](instructions/ingest-large-tree.md) (a flat
|
||
instruction, not a published skill - it runs rarely, so it earns a link rather
|
||
than a permanent slot in every session's context) cuts a tree into units before
|
||
anything is written, then reads, promotes and publishes one unit at a time
|
||
through a workshop. Triggers: more than ~20 raw files, or a source page that
|
||
would carry more than ~15 `raw_files:` entries. A third tier adds an audit pass
|
||
for units covering secrets, RBAC, ingress or disaster recovery.
|
||
|
||
Each unit takes its own `WIKITOOL_SESSION_ID`, derived from the run key
|
||
(`<runkey>/u<N>`), so the budget is scoped to the unit rather than to the whole
|
||
run. [instructions/gates.md](instructions/gates.md) now states the matching
|
||
rule: **a new session id may only be taken at a unit boundary written down in
|
||
`plan.md`, never in response to a gate refusal.** The plan is the human approval
|
||
the gate would otherwise have to ask for.
|
||
|
||
`types/source.md` gains a `## Not Extracted` section: what was read and
|
||
deliberately not promoted, with the reason. `sources coverage` can only answer
|
||
whether a raw file is *named* by some source page, never whether anyone decided
|
||
what to do with it - so without this, every later pass re-litigates the same
|
||
source.
|
||
|
||
### `README.md` and `CONTRACT.md` are both allowed, and kept apart
|
||
|
||
The naming rule said `README.md` was repo-root only. It is now explicit that a
|
||
stage may carry both: **`README.md` is technical documentation for humans and
|
||
for developing the thing in that directory, `CONTRACT.md` is the agent-facing
|
||
contract for using it.** What a stage may not carry is the same content twice.
|
||
|
||
`tools/README.md` returns under that rule, rewritten as developer
|
||
documentation - layout, how to add a command, design notes, how to run the
|
||
tests - with no command table. The untracked file previously sitting there was
|
||
a leftover copy of the document that became `tools/CONTRACT.md` in 35d5df8, and
|
||
had already drifted (no `search`, no `work`, old gate wording).
|
||
|
||
To stop that recurring, `docs verify`'s command-table guard was generalized
|
||
from the root README to every README (`check_readmes_have_no_command_table`).
|
||
The guard existed because this exact drift happened once; allowing stage
|
||
READMEs without widening it would have re-created the bug.
|
||
|
||
### Found while verifying
|
||
|
||
Running the new procedure against an already-ingested tree reproduced 5
|
||
of 7 existing units exactly and surfaced one defect: **8 raw files under
|
||
`10-bootstrap-manual/` are covered by two source pages each** - an 18-file
|
||
umbrella page and seven per-step pages, two incompatible cuts applied at
|
||
different times. Recorded as a `DECISION NEEDED` in
|
||
`work/ingest-documents-handbook/`; retiring either side is a page-lifecycle
|
||
operation, not part of this change.
|
||
|
||
---
|
||
|
||
## 2026-08-22 - Retrieval instead of catalog reading; one instruction layer; `CONTRACT.md` naming
|
||
|
||
**Author:** GitHub Copilot
|
||
|
||
Four changes, all aimed at the same problem: the routing layer had grown
|
||
redundant, and finding anything meant reading everything.
|
||
|
||
### `wikitool search`
|
||
|
||
New command. Text search runs through a pluggable backend (`rg` today, behind a
|
||
`SearchBackend` protocol with Reciprocal Rank Fusion already in place, so a
|
||
vector backend is a new module rather than a rewrite). Frontmatter predicates -
|
||
`--field entity_type=system`, `--field 'confidence>=0.8'`, `--field tags=k8s`,
|
||
`--field '!source_url'` - are evaluated in-process on the parsed YAML, and work
|
||
with or without a text query.
|
||
|
||
The query is passed as an argv element and `--fixed-strings` is the default, so
|
||
a search term is never a shell command and never an accidental regex. An unknown
|
||
field name fails with the list of fields that do exist, rather than returning an
|
||
empty result that reads as "no such pages".
|
||
|
||
`search` is **exempt from the Iteration Budget Gate**: retrieval is reading, not
|
||
iterating, and charging for it would tax the one habit that lowers token cost.
|
||
|
||
### The catalog is sharded
|
||
|
||
`kb/index.md` was one flat file listing all 243 pages with summaries - roughly
|
||
12-14k tokens to read, which `wiki-query` did on every question. It is now a
|
||
**map**: statistics, one row per collection and per area, and a link to the shard
|
||
that lists those pages (50 lines). The tables live in a generated
|
||
`kb/<collection>/INDEX.md`, and an area past 50 rows gets its own -
|
||
`kb/entities/tools/INDEX.md` split out immediately at 56. The threshold comes
|
||
from the wiki's own `Index Scaling` page, so growth is handled by arithmetic
|
||
rather than by a judgment call. Stale shards are swept on every rebuild.
|
||
|
||
### `instructions/`, and skills published by copy
|
||
|
||
New top-level directory: the single source for everything an agent is told to
|
||
do. Two forms, told apart **structurally** - a subdirectory with a `SKILL.md` is
|
||
published into the harness directories, a flat `.md` never is. That split is what
|
||
keeps a rarely-used procedure from occupying context for a whole session.
|
||
|
||
The five skills moved from `.agents/skills/` into `instructions/`. Four shared
|
||
fragments were extracted from the places that had been repeating them:
|
||
`gates.md` (was restated in 7 places), `page-lifecycle.md` (6),
|
||
`session-setup.md` (4), `publish-cycle.md` (4), plus `bootstrap.md`.
|
||
|
||
`wikitool skills sync|verify` became `wikitool instructions sync|verify|list`,
|
||
and publication now **copies** instead of symlinking. This reverses the earlier
|
||
decision recorded in that module: symlinks cannot go stale, but they are
|
||
unreliable on Windows checkouts and do not survive being archived or copied. The
|
||
price of a copy is drift, so `verify` compares every copy byte for byte against
|
||
its source. Both `.agents/skills/` and `.claude/skills/` are now **gitignored
|
||
build output**; a fresh clone publishes them once via `instructions/bootstrap.md`.
|
||
|
||
`verify` also reports an instruction that nothing references - an instruction
|
||
nothing loads is inert, and nothing else in the stack would ever say so.
|
||
|
||
New `instruction` type-spec, with a new optional placement field `root:` on
|
||
type-specs (`kb` by default, `repo` to opt out). Without it a type outside `kb/`
|
||
could only be contract-only; with it, `wikitool new instruction` scaffolds
|
||
correctly.
|
||
|
||
### `CONTRACT.md` for agents, `README.md` for humans
|
||
|
||
A file's name now says who it is for. `raw/README.md`, `reports/README.md` and
|
||
`tools/README.md` became `CONTRACT.md`; `kb/CONVENTIONS.md` became
|
||
`kb/CONTRACT.md` and absorbed `kb/README.md`, which is gone. `README.md` now
|
||
exists only at the repo root, for humans.
|
||
|
||
Twelve rules that had been stated in two to seven places each were reduced to one
|
||
canonical location apiece, with links elsewhere. This is now AGENTS.md invariant
|
||
8 ("one rule, one place"). It is deliberately **not** machine-checked: prose
|
||
duplication is a judgment call, so `wiki-lint` gained a review step for it
|
||
instead of `docs verify` gaining a validator.
|
||
|
||
Also fixed: `docs verify` said `COLLECTION.md` placement had two rules while
|
||
`kb/README.md` said three; `runs on` was recommended by the entities contract but
|
||
missing from the canonical label list; AGENTS.md implied `budget reset` was the
|
||
way past the Iteration Budget Gate, when `reset` is itself counted and refused at
|
||
the limit.
|
||
|
||
---
|
||
|
||
## 2026-08-21 - Collections under a single `kb/` root; the repo becomes a four-stage pipeline
|
||
|
||
**Author:** GitHub Copilot
|
||
|
||
The wiki layer was a single `wiki/` directory with one contract covering five
|
||
subdirectories. It is now `kb/`, a namespace whose children are first-class
|
||
collections, each with its own `COLLECTION.md`. The structural rule is now
|
||
enforced rather than described:
|
||
|
||
> A directory under `kb/` is a collection exactly when it contains a
|
||
> `COLLECTION.md`. A subdirectory inside one is an *area* and inherits it.
|
||
> `COLLECTION.md` appears nowhere outside `kb/`.
|
||
|
||
**Layout.** `wiki/` -> `kb/`, with `entities/`, `concepts/`, `sources/` and
|
||
`comparisons/` each gaining a contract. `wiki/COLLECTION.md` split into
|
||
`kb/CONVENTIONS.md` (the cross-collection half: naming, tone, linking,
|
||
provenance, confidence) and the four per-collection contracts (types offered,
|
||
what belongs, outbound linking, per-area emphasis). Added `kb/README.md` for
|
||
routing.
|
||
|
||
**`raw/` stayed at the repo root** rather than moving to `kb/raw/`. It has no
|
||
types, frontmatter, wikilinks, provenance or confidence, so none of
|
||
`kb/CONVENTIONS.md` applies to it; keeping it a sibling makes the trust boundary
|
||
structural instead of documented, and leaves every `raw_files:` reference in
|
||
every source page valid. `raw/COLLECTION.md` -> `raw/README.md`.
|
||
|
||
**`types/` stayed at the repo root** as the global type surface and lost its
|
||
`COLLECTION.md`; the content folded into `types/type-spec.md`, which now also
|
||
documents placement frontmatter, adding a type, and the template filters. A
|
||
type-spec is not a collection contract, and `docs verify` now fails if one
|
||
appears there.
|
||
|
||
**Lint reports left the wiki.** `types/lint-report.md` dropped its `base_dir:`
|
||
and is now contract-only, so `wikitool new lint-report` refuses to scaffold one.
|
||
`lint --markdown` writes to a new gitignored `reports/`. A report's structural
|
||
half is recomputable from the tree at any commit, so committing it created a
|
||
second copy that could drift; the semantic review, which is *not* recomputable,
|
||
now has to be carried into `kb/log.md` via `log append --op lint` before a pass
|
||
ends. `index.md` no longer has a Lint Reports section, and `lint` no longer
|
||
carries per-type exemptions for reports.
|
||
|
||
**Tooling.** `repo_scan.py` -> `kb_scan.py`; `iter_wiki_files`/`load_pages` ->
|
||
`iter_kb_pages`/`load_kb_pages`; `config.WIKI_DIR` -> `config.KB_DIR`, plus
|
||
`REPORTS_DIR` and `PROVENANCE_FILE`. The scanner now excludes `COLLECTION.md` at
|
||
any depth, since contracts sit one level below the kb root where the meta-file
|
||
guard does not reach. New `kb_collections.py` discovers collections from the
|
||
filesystem, so `mkdir kb/<name>` + a contract adds one with no code change.
|
||
`docs verify` replaced its hardcoded `("raw","wiki","types")` list with that
|
||
discovery and gained three checks: an unclaimed directory under `kb/`, a nested
|
||
contract, and a contract outside `kb/`. Its `.gitignore` canaries now run in
|
||
both directions - nothing ignored under `raw/`/`kb/`, everything ignored under
|
||
`reports/` except its README.
|
||
|
||
**AGENTS.md routing** was rewritten from a flat "by layer" table into the
|
||
pipeline diagram plus the two collection rules, then *by stage* -> *by
|
||
collection* -> *by task* -> *by question*, with an explicit statement that page
|
||
placement follows `base_dir:` and is never an agent's choice, and a two-line
|
||
"adding a collection" recipe.
|
||
|
||
**Verification:** 208 tests pass (13 new, covering collection discovery, scanner
|
||
exclusions and contract-only types). `lint` is identical to the pre-move
|
||
baseline: 243 pages, zero findings.
|
||
|
||
**Fixed while publishing the above:** `wikitool publish` pushed the *ref named*
|
||
`--branch` (default `main`) rather than the commit it had just made. Run from a
|
||
feature branch, `git push origin main` pushed an unrelated, unchanged ref, exited
|
||
0, and the command reported "Published changes to origin/main" while the new
|
||
commit stayed local - the same class of silent non-publication that the
|
||
`.gitignore` canaries exist to prevent. `publish` now refuses to push when the
|
||
checked-out branch differs from `--branch`, naming both and suggesting
|
||
`--branch <current>`, and it checks this *before* staging so a refusal leaves the
|
||
working tree untouched. Covered by two regression tests.
|
||
|
||
**Known gap, not fixed:** at exactly the iteration limit, `budget reset --yes` is
|
||
itself refused, because `reset` is deliberately excluded from the exemption list
|
||
(`SKIP_COMMAND_PATHS`) so an agent cannot step around the gate by resetting
|
||
first. The consequence is that the only way out is `--override-budget`, while
|
||
AGENTS.md's "Gates" section reads as though `reset` were available at that point.
|
||
The behaviour is intentional; the documentation is what misleads.
|
||
|
||
## Entries
|
||
|
||
| Date | Change | Author |
|
||
|------|--------|--------|
|
||
| 2026-08-13 | **Review remediation: ignore-rule and reference-integrity holes closed, page mutations given commands.** Two defects let the stack report itself healthier than it was. (1) `.gitignore` carried unanchored patterns (`*temp*`, `*scratch*`, `*.log`, `go.mod`, `bin/`, `core`, `tags`) that silently excluded content: `raw/notes/temperature-sensors.md` was gitignored, so `sources coverage` would call it covered while `publish`'s `git add -A` never committed it and a fresh clone got a broken `raw_files:` reference. Every repo-local pattern is now anchored to the root, the content-swallowing ones are gone, and a `!raw/**`/`!wiki/**` backstop closes the rest; `docs verify` gained `check_ignored_content()`, which reports both actually-ignored files under `raw/`/`wiki/` and ten latent canary paths via `git check-ignore --no-index`. (2) `lint`'s broken-link scan only walked page *bodies*, so `related:`/`sources:`/`entities:`/`concepts:` entries naming a non-existent page passed every check - 40 of them existed while lint reported zero findings, including a URL pasted into `sources:` and four pages citing a source title with a stray `.md` suffix the page does not carry. New `dangling_frontmatter_refs` hard-error check; which fields hold page titles is declared by each type-spec's new `page_ref_fields:` frontmatter (read via `TypeResolver.get_page_ref_fields`) rather than hardcoded, so a new type needs no code change. Both defects were reachable because renaming and deleting a page had no command: added `wikitool rename` (repoints body wikilinks, `^[[Title]]` citation markers with aliases/anchors preserved, the page's own H1, and every declared ref array; a `--from` that is not a page repoints references onto an existing `--to` instead of moving anything), `wikitool rm` (refuses while inbound references exist unless `--yes`, strips mechanical links, deliberately leaves prose citations and reports them), and `wikitool xref remove` (inverse of both `xref add` and `xref link-source`; `--b` need not exist, which is what lets a stale reference be cleared without hand-editing frontmatter). `log append` accepts `--op rename`. All 40 dangling references were then cleaned up: 19 repointed onto existing pages, 21 removed as retired or never-created. Also: the `lint_report` pseudo-type - written by `lint --markdown`, special-cased in three places, documented nowhere - became a real `types/lint-report.md` + schema with `base_dir: reports`, so reports live under `wiki/reports/` and are schema-validated like any other page; `budget reset` is no longer exempt from the Iteration Budget Gate and now requires `--yes` of its own (exempting the command that clears the counter made the gate a formality, while `budget status` stays exempt so the situation remains reportable); `skills sync` refuses to `rmtree` a `.claude/skills/` entry that is a real directory unless `--force`; `index_build` no longer hardcodes `types/entity.md`; the root README's duplicate command table, maintenance schedule, and entity-type directory table were replaced with pointers, with a new `docs verify` check enforcing that the command table stays absent there; `wiki/COLLECTION.md` dropped the directory column the type-specs own; and `wiki-lint` now writes its report *after* the fixes rather than before, so it records the wiki's final state. Test suite 139 -> 182. | GitHub Copilot |
|
||
| 2026-08-13 | **Tooling correctness + control-plane restructure.** Fixed `confidence decay` compounding on every run: `confidence` is now derived from a new undecayed `confidence_base` field (added to the entity/concept schemas, backfilled across 204 pages by the new `confidence init-base` command), so repeated runs are idempotent - the previous "never double-decays" claim in AGENTS.md/READMEs was false. Added `wikitool touch` (bump `modified:`/`summary:`/`provenance:`/`confidence_base` through the tool instead of by hand, schema-validating the fields it writes) and `wikitool docs verify` (checks every CLI command is documented in tools/README and vice versa, that each collection has a `COLLECTION.md`, and that no pre-migration `type: <enum>` block survives in the contracts). `lint` gained a hard-error check for unreadable/missing frontmatter - previously such pages silently read back as `{}` and escaped every other check - plus `most_linked`/`inbound_counts` output, which `wiki-status` had been documented to read but which the report never contained. `publish` now evaluates the Mass-Update Gate *before* staging, parses `git status --porcelain -z -uall` (fixing renames and paths with spaces), and takes `--remote`/`--branch`/`--path` (the last scoping gate count, staging and commit to a subtree, so a large change can be published in reviewable batches). Budget gate: refused calls are no longer counted, `--help` anywhere in argv is exempt, stale sessions are pruned from the state file, and `budget status` reports whether the session key came from `WIKITOOL_SESSION_ID` or the pid fallback. Restructured the 648-line AGENTS.md into a ~140-line control plane (invariants, routing, gates, error model) plus per-layer contracts `raw/COLLECTION.md`, `wiki/COLLECTION.md`, `types/COLLECTION.md`, moving the per-command error contracts and maintenance schedule into tools/README.md; deleted the stale `type: comparison` template, the schema-duplicating field tables, and the pseudo-command reference. All five skills now export `WIKITOOL_SESSION_ID`, call `touch` instead of hand-editing frontmatter, and treat `raw/` content as untrusted data. Added tests for decay idempotence, frontmatter errors, porcelain parsing, budget hardening, `touch`, `docs verify`, and `skills sync` (139 total). | GitHub Copilot |
|
||
| 2026-08-08 | Made `wikitool new`'s scaffolding fully schema-driven instead of hand-duplicating the type-specs: added `TypeResolver.get_enum()`/`get_schema()`, removed `config.py`'s hardcoded `ENTITY_TYPES`/`CONCEPT_TYPES`/`PROVENANCE_VALUES` (enum validity and CLI `--help` text now read from each type's `.schema.yaml`), replaced each `new_*` command's hand-built frontmatter dict with a generic schema-driven builder, and extended `_apply_template_variables` with `\|bullets`/`\|join`/`\|capitalize` filters so type-spec templates reference frontmatter fields directly instead of each command precomputing bespoke display variables. Also consolidated `lint`'s two overlapping missing-field checks into one: removed the hand-maintained `REQUIRED_FIELDS`/`schema_issues`, relying solely on the resolver-based `schema_validation_errors`. Directory placement (`ENTITY_SUBDIRS`) stays in `config.py` as wiki layout, not type structure. | GitHub Copilot |
|
||
| 2026-08-08 | Removed the now-unused legacy bare-enum `type:` compatibility shims left over from the type-system migration below (`Page.kind`'s `entity`/`concept`/`source`/`comparison` fallback entries, and `lint`'s duplicate `REQUIRED_FIELDS` keys) since all 238 wiki pages have used path-based `type: types/entity.md` references since that migration; only the current path-based form is recognized now. | GitHub Copilot |
|
||
| 2026-08-08 | **Major: Transformed to Commonplace-style externalized type system.** Created `/types/` directory with type-spec files (`entity.md`, `concept.md`, `source.md`, `comparison.md`, `type-spec.md`) and corresponding `.schema.yaml` files for structural validation. Rewrote `wikitool new` commands to use type-spec templates instead of hardcoded templates. Migrated all 238 wiki pages from enum-style type references (`type: entity`) to path-based references (`type: types/entity.md`). Created `type_resolver.py` module for path resolution, caching, and schema validation. Extended `lint` with type path validation and schema validation checks. Updated AGENTS.md Type System section, Architecture diagrams, and skill files. | Mistral Vibe |
|
||
| 2026-08-07 | Introduced this `CHANGES.md`. Removed the inline "Version History" sections from `AGENTS.md` and `README.md` (and confirmed no skill file under `.agents/skills/` had one); both now point here instead. | GitHub Copilot |
|
||
| 2026-08-07 | Added "Iteration and Cost Limits" section to `AGENTS.md` and a code-enforced `tools/wikitool` Iteration Budget Gate + Loop-Breaker (new `budget status`/`budget reset` commands, `--override-budget` flag): every `wikitool` call is checked against a 30-call/session ceiling and refuses 3x-identical repeats, mirroring the Mass-Update Gate pattern but scoped to iteration volume instead of publish size. | GitHub Copilot |
|
||
| 2026-08-06 | Added "Tool Error Contracts" section to `AGENTS.md`: a general 3-outcome model (success / expected validation error / unexpected error) plus a per-command table covering every `wikitool` command's exit-1 meaning, atomicity, retry policy, and escalation path. Added `--dry-run` to `xref add`/`xref link-source` and made `xref link-source` report partial failures instead of aborting silently mid-loop. | GitHub Copilot |
|
||
| 2026-08-05 | Documented the skills-based workflow split, the full `wikitool` command set, and a dedicated Maintenance section (schedule, confidence decay, provenance) in `README.md`. | GitHub Copilot |
|
||
| 2026-08-04 | Split the 5 step-by-step workflows into independently-discoverable skills under `.agents/skills/` (`wiki-ingest`, `wiki-query`, `wiki-lint`, `wiki-manage`, `wiki-status`), mirrored to `.claude/skills/` for Claude Code via new `tools/wikitool skills sync`/`skills verify` commands. Slimmed root `AGENTS.md` to schema/policy only. | GitHub Copilot |
|
||
| 2026-08-02 | Added "Provenance and Citation" section to `AGENTS.md`: `raw_files:` (replacing `source:`) on source pages, `provenance:` field on entity/concept pages, inline `^[[Source - X]]` citation markers, and the generated `wiki/provenance.md` reverse index. Added `tools/wikitool sources coverage\|trace\|rebuild-index` commands and 4 new `lint` checks. Updated all workflows and page templates accordingly, and added the "no confident answer without a source" rule. | GitHub Copilot |
|
||
| 2026-08-02 | Added `tools/wikitool` deterministic CLI and a new "Tooling" section to `AGENTS.md` mandating its use for scaffolding, cross-references, index rebuilds, log entries, confidence decay, and publishing. Rewrote INGEST/CREATE/UPDATE/LINT workflow steps and Git Automation to call it instead of describing manual edits. Added `summary:` frontmatter field to all page templates. | GitHub Copilot |
|
||
| 2026-08-02 | Added Git Automation: all workflows now automatically commit and push changes to `origin/main` without user confirmation. Added auto-commit/push step to INGEST and CREATE workflows. | Mistral Vibe |
|
||
| 2026-07-26 | Updated naming conventions: changed from kebab-case to human-readable titles with spaces (e.g., `Hybrid Search.md` instead of `hybrid-search.md`). Wikilinks now match file names exactly: `[[Hybrid Search]]` links to `Hybrid Search.md`. Updated all page templates to use spaced names in wikilinks. | Mistral Vibe |
|
||
| 2026-07-25 | Initial schema/scaffold created for the IT-focused LLM Wiki. | Mistral Vibe |
|