4e80a07ac7
Files changed: - AGENTS.md - CHANGES.md - VERSION - docs/ownership-and-templates.md - docs/pipeline-rationale.md - docs/version-model.md - docs/why-gates-are-code.md
3277 lines
209 KiB
Markdown
3277 lines
209 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.
|
|
|
|
---
|
|
|
|
## 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 |
|