Compare commits

...

7 Commits

Author SHA1 Message Date
torben 502971d147 feat: Autorenkonventionen nach Eigentum geschnitten - kb/CONVENTIONS.md, deklarierte Collections (3.0.0)
CI / verify (push) Successful in 53s
Release / release (push) Successful in 38s
Files changed:
- .gitea/workflows/ci.yml
- .wikitool-kb.json
- AGENTS.md
- CHANGES.md
- INSTALL.md
- README.md
- VERSION
- instructions/CONTRACT.md
- instructions/dev/testing-conventions.md
- instructions/german-terminology.md
- instructions/kb-profiles.md
- instructions/migrations/3.0.0-authoring-conventions.md
- instructions/private-instance.md
- instructions/setup-instance.md
- instructions/wiki-ingest/SKILL.md
- instructions/wiki-manage/SKILL.md
- kb/CONTRACT.md
- kb/CONVENTIONS.md
- kb/CONVENTIONS.md.template
- kb/comparisons/COLLECTION.md
- kb/concepts/COLLECTION.md
- kb/entities/COLLECTION.md
- kb/sources/COLLECTION.md
- tools/CONTRACT.md
- tools/README.md
- tools/chemenu/commands/dist_cmd.py
- tools/chemenu/commands/docs_verify.py
- tools/chemenu/commands/doctor.py
- tools/chemenu/commands/new_page.py
- tools/chemenu/conventions.py
- tools/chemenu/kb_collections.py
- tools/chemenu/kb_scan.py
- tools/chemenu/provenance.py
- tools/chemenu/sections.py
- tools/chemenu/tests/conftest.py
- tools/chemenu/tests/test_conventions.py
- tools/chemenu/tests/test_dist_cmd.py
- tools/chemenu/tests/test_doctor.py
- tools/chemenu/tests/test_new_page.py
- tools/chemenu/tests/test_types_cmd.py
- types/comparison.md
- types/concept.md
- types/entity.md
- types/source.md
- types/type-spec.md
2026-09-02 15:02:10 +02:00
torben 9843df99d3 ingest: raw/notes/Conversation Transcript - Version Part Nomenclature and Breaking Change Gate Session 2026-09-02.md
Files changed:
- kb/concepts/INDEX.md
- kb/concepts/KB Stack Versioning.md
- kb/entities/INDEX.md
- kb/entities/projects/Chemenu.md
- kb/entities/tools/wikitool.md
- kb/index.md
- kb/log.md
- kb/sources/INDEX.md
- kb/sources/Source - Version Part Nomenclature and Breaking Change Gate Session 2026-09-02.md
- raw/notes/Conversation Transcript - Version Part Nomenclature and Breaking Change Gate Session 2026-09-02.md
2026-09-02 09:25:11 +02:00
torben 31662dc3ff feat: Versionsstelle als Kompatibilitaetsfrage, Breaking-Change-Vermerk erzwungen (2.5.0)
CI / verify (push) Successful in 54s
Release / release (push) Successful in 37s
Files changed:
- CHANGES.md
- INSTALL.md
- VERSION
- instructions/dev/stack-dev/SKILL.md
- instructions/dev/version-parts.md
- tools/CONTRACT.md
- tools/chemenu/commands/docs_verify.py
- tools/chemenu/commands/version_cmd.py
- tools/chemenu/tests/test_docs_verify.py
- tools/chemenu/tests/test_version_cmd.py
- tools/chemenu/version.py
2026-09-02 09:17:36 +02:00
torben 7fbb9a99df update: Korrektur zum stale Transkript-Verweis auf eine nie geschriebene zweite Sitzung
Files changed:
- kb/log.md
- kb/sources/Source - MCP Read Server Implementation Session 2026-09-02.md
2026-09-02 08:14:09 +02:00
torben 778764de4d update: qmd Sprachangabe korrigiert (TypeScript statt Go/Rust-Vermutung)
Files changed:
- kb/concepts/Delete Rather Than Anonymize.md
- kb/concepts/Dual Licensing by File Plan.md
- kb/concepts/Publish-Remote Gate.md
- kb/entities/INDEX.md
- kb/entities/tools/qmd.md
- kb/index.md
- kb/log.md
- kb/provenance.md
- kb/sources/INDEX.md
- kb/sources/Source - qmd - GitHub Repository.md
- raw/documents/qmd - GitHub Repository.md
2026-09-02 07:55:45 +02:00
torben f7597b209c ingest: MCP Read Server Implementation Session 2026-09-02
Files changed:
- kb/concepts/INDEX.md
- kb/concepts/Iteration and Cost Limits.md
- kb/concepts/MCP-Leseserver.md
- kb/concepts/Mass-Update Gate.md
- kb/concepts/Publish-Remote Gate.md
- kb/entities/INDEX.md
- kb/entities/projects/Chemenu.md
- kb/entities/tools/wikitool.md
- kb/index.md
- kb/log.md
- kb/provenance.md
- kb/sources/INDEX.md
- kb/sources/Source - MCP Read Server Implementation Session 2026-09-02.md
- raw/notes/Conversation Transcript - MCP Read Server Implementation Session 2026-09-02.md
2026-09-02 07:51:52 +02:00
torben 83018fcc7d docs: INSTALL-MCP.md fuer Menschen, wird mit dist export ausgeliefert (2.4.1)
CI / verify (push) Successful in 53s
Release / release (push) Successful in 38s
Files changed:
- CHANGES.md
- INSTALL-MCP.md
- INSTALL.md
- README.md
- VERSION
- tools/chemenu/commands/dist_cmd.py
2026-09-02 07:27:57 +02:00
74 changed files with 3651 additions and 322 deletions
+10
View File
@@ -221,6 +221,16 @@ jobs:
for personal in USER SOUL; do for personal in USER SOUL; do
grep -v 'wikitool:template-unfilled' "$personal.md.template" > "$personal.md" grep -v 'wikitool:template-unfilled' "$personal.md.template" > "$personal.md"
done done
# The authoring conventions ride the same split one directory down,
# and are stubbed the same way: what is under test is that the export
# carries the templates and that `doctor`/`docs verify` accept an
# adopted one, not what a person would write into them. The collection
# contracts are adopted verbatim - the shipped text is a working
# default, unlike a personalization file.
grep -v 'wikitool:template-unfilled' kb/CONVENTIONS.md.template > kb/CONVENTIONS.md
for template in kb/*/COLLECTION.md.template; do
cp "$template" "${template%.template}"
done
python3 -m venv tools/.venv python3 -m venv tools/.venv
tools/.venv/bin/pip install --quiet -r tools/requirements.txt tools/.venv/bin/pip install --quiet -r tools/requirements.txt
tools/wikitool instructions sync tools/wikitool instructions sync
+8 -2
View File
@@ -1,5 +1,11 @@
{ {
"schema": 1, "schema": 1,
"kb_version": "1.0.0", "kb_version": "3.0.0",
"applied": [] "applied": [
{
"migration": "3.0.0-authoring-conventions",
"at": "2026-09-02",
"pages": 0
}
]
} }
+17 -4
View File
@@ -77,7 +77,8 @@ What a file is called says who it is for and how it is loaded. This is a rule, n
| `SOUL.md` | Agents | Always, every session | | `SOUL.md` | Agents | Always, every session |
| `ENVIRONMENT.md` | Agents | Every session, **if it exists** - the one optional file in this table. Not committed: it describes one checkout, not the repo | | `ENVIRONMENT.md` | Agents | Every session, **if it exists** - the one optional file in this table. Not committed: it describes one checkout, not the repo |
| `<stage>/CONTRACT.md` | Agents | When writing in that stage | | `<stage>/CONTRACT.md` | Agents | When writing in that stage |
| `kb/<collection>/COLLECTION.md` | Agents | When writing in that collection | | `kb/CONVENTIONS.md` | Agents | When writing any page - it holds what *this* instance decided about authoring (language, section headings, naming, tone, relationship labels, confidence rubric), where `kb/CONTRACT.md` holds what the stack enforces. Instance-owned: a distribution ships only the `.template` |
| `kb/<collection>/COLLECTION.md` | Agents | When writing in that collection. Instance-owned in the same way, and declares in frontmatter which profile it adopted |
| `instructions/<name>.md` | Agents | By link, or on explicit request | | `instructions/<name>.md` | Agents | By link, or on explicit request |
| `instructions/<name>/SKILL.md` | Agents | By the harness, once published | | `instructions/<name>/SKILL.md` | Agents | By the harness, once published |
| `types/<name>.md` | Agents + validator | Via `tools/wikitool types describe` | | `types/<name>.md` | Agents + validator | Via `tools/wikitool types describe` |
@@ -104,6 +105,16 @@ and `SOUL.md.template`; the Personalization step of
writes the real files. `tools/wikitool doctor` FAILs on a missing one, and on one still writes the real files. `tools/wikitool doctor` FAILs on a missing one, and on one still
carrying the template's sentinel. carrying the template's sentinel.
The same `.template` split runs one directory down, for authoring rather than for voice.
`kb/CONVENTIONS.md` and each `kb/<name>/COLLECTION.md` bind every page and belong to the
instance, so a distribution ships them as templates and the KB-language step of
[instructions/setup-instance.md](instructions/setup-instance.md) fills them in, out of a
catalogue of ready-made profiles it routes to; `doctor` FAILs on a missing or unfilled
`kb/CONVENTIONS.md` the same way.
Unlike `USER.md`, these two *are* a source of rules: they are as binding as `kb/CONTRACT.md`.
What differs is ownership, not authority.
## Environment ## Environment
`ENVIRONMENT.md` records what *this checkout* works through - harness, published skills, `ENVIRONMENT.md` records what *this checkout* works through - harness, published skills,
@@ -142,15 +153,17 @@ Alongside it, not part of it: `instructions/` (what agents are told to do) and t
|-------|----------|--------| |-------|----------|--------|
| `raw/` | [raw/CONTRACT.md](raw/CONTRACT.md) | Immutability, directory routing, untrusted-content rule | | `raw/` | [raw/CONTRACT.md](raw/CONTRACT.md) | Immutability, directory routing, untrusted-content rule |
| `types/` | [types/type-spec.md](types/type-spec.md) | Type-spec anatomy, placement, adding a type, template variables | | `types/` | [types/type-spec.md](types/type-spec.md) | Type-spec anatomy, placement, adding a type, template variables |
| `kb/` | [kb/CONTRACT.md](kb/CONTRACT.md) | Collections, naming, tone, linking, provenance, confidence | | `kb/` | [kb/CONTRACT.md](kb/CONTRACT.md) + `kb/CONVENTIONS.md` | What the stack enforces about a page (collections, linking, provenance, confidence machinery), and beside it what this instance decided (language, naming, tone, labels, rubric) |
| `reports/` | [reports/CONTRACT.md](reports/CONTRACT.md) | Why reports and traces are generated, gitignored, and carried into `kb/log.md` | | `reports/` | [reports/CONTRACT.md](reports/CONTRACT.md) | Why reports and traces are generated, gitignored, and carried into `kb/log.md` |
| `work/` | [work/CONTRACT.md](work/CONTRACT.md) | Workshop runs: run keys, required files, why they are tracked, how a run closes | | `work/` | [work/CONTRACT.md](work/CONTRACT.md) | Workshop runs: run keys, required files, why they are tracked, how a run closes |
| `tools/` | [tools/CONTRACT.md](tools/CONTRACT.md) | Full command reference, per-command error contracts, maintenance schedule | | `tools/` | [tools/CONTRACT.md](tools/CONTRACT.md) | Full command reference, per-command error contracts, maintenance schedule |
| `instructions/` | [instructions/CONTRACT.md](instructions/CONTRACT.md) | Instruction vs. skill, publishing, writing standard | | `instructions/` | [instructions/CONTRACT.md](instructions/CONTRACT.md) | Instruction vs. skill, publishing, writing standard |
**By collection** - then read the contract for the collection you are writing in. **By collection** - then read the contract for the collection you are writing in.
[kb/CONTRACT.md](kb/CONTRACT.md) routes between `kb/entities/`, `kb/concepts/`, `kb/sources/` [kb/CONTRACT.md](kb/CONTRACT.md) routes between this instance's collections and holds the rules
and `kb/comparisons/`, and holds the rules they share. the stack enforces across all of them; `kb/CONVENTIONS.md` holds the ones this instance chose.
Both bind. The difference is who may change the sentence - which is also why a distribution
ships the first verbatim and the second only as a `.template`.
**By task** - skills hold the step-by-step procedures. Sources live in `instructions/<name>/`: **By task** - skills hold the step-by-step procedures. Sources live in `instructions/<name>/`:
+158
View File
@@ -20,6 +20,164 @@ their date-only headings.
--- ---
## 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 ## 2.4.0 - 2026-09-02 - MCP-Leseserver: zweiter Konsument auf demselben Kern
**Author:** Torben Nehmer **Author:** Torben Nehmer
+239
View File
@@ -0,0 +1,239 @@
# MCP-Leseserver installieren
Dieses Dokument richtet sich an Menschen. Es beschreibt, wie der MCP-Leseserver eines Chemenu-
Wikis lokal läuft, wie ein Client ihn einbindet, und wie er hinter einer Authentifizierung
erreichbar wird. Der agent-seitige Betriebsablauf steht in
[instructions/mcp-read-server.md](instructions/mcp-read-server.md); die vollständige
Kommandoreferenz in [tools/CONTRACT.md](tools/CONTRACT.md).
**Was der Server ist.** Ein zweiter Konsument desselben Kerns, nicht ein zweites Programm. CLI
und Server rufen dieselben Funktionen auf; ein Golden-Test hält ihre Ausgaben gegeneinander.
Was `tools/wikitool search --json` liefert, liefert das MCP-Tool `search` auch — plus den
Commit, aus dem die Antwort berechnet wurde.
**Was er nicht ist.** Kein Schreibpfad. Es gibt kein Tool, das eine Seite anlegt, ändert oder
publiziert — nicht weil eine Liste gefiltert wird, sondern weil der Server nichts unter
`tools/chemenu/commands/` importiert. Die Funktionen sind aus diesem Prozess heraus nicht
erreichbar.
## Voraussetzungen
- Eine funktionierende Instanz nach [INSTALL.md](INSTALL.md) — inklusive `tools/.venv` und
`ripgrep`
- Python 3.11 oder neuer (wie die CLI)
## Schritt 1: Abhängigkeit installieren
Sie liegt bewusst nicht in `tools/requirements.txt`. Eine Instanz, die nur die CLI benutzt, soll
dafür nicht pydantic, starlette, uvicorn und cryptography mitinstallieren müssen.
```bash
tools/.venv/bin/pip install -r tools/requirements-mcp.txt
```
## Schritt 2: Lokal starten (stdio)
`stdio` ist der Weg zum Ausprobieren und für einen Client auf derselben Maschine: ein Prozess
pro Konsument, lokal gestartet, kein Netzwerk.
```bash
WIKI_TRACE=0 tools/.venv/bin/python -m chemenu.mcp
```
Der Prozess spricht MCP über stdin/stdout und gibt für sich genommen nichts aus — das ist
richtig so. Gestartet wird er normalerweise nicht von Hand, sondern vom Client (Schritt 3).
**`WIKI_TRACE=0` ist nicht optional.** Telemetrie ist per Default an und schreibt nach
`reports/telemetry/` im Repo. Der Server **verweigert den Start**, solange das so ist, statt
still umzuleiten:
```
ERROR Telemetry is on and would write into the served checkout (...). Set WIKI_TRACE=0,
or point WIKI_TRACE_DIR outside the corpus.
```
Beide Auswege sind gleichwertig: `WIKI_TRACE=0` schaltet ab, `WIKI_TRACE_DIR=/var/log/chemenu`
lenkt um. Der Grund steht in Schritt 6 — der Sync darf `reports/` wegräumen.
## Schritt 3: Einen Client einbinden
Die Konfiguration folgt der üblichen MCP-Client-Form. Absolute Pfade, weil der Client kein
Arbeitsverzeichnis erbt:
```json
{
"mcpServers": {
"chemenu": {
"command": "/pfad/zur/instanz/tools/.venv/bin/python",
"args": ["-m", "chemenu.mcp"],
"cwd": "/pfad/zur/instanz/tools",
"env": {
"WIKI_TRACE": "0",
"CHEMENU_ROOT": "/pfad/zur/instanz"
}
}
}
}
```
`CHEMENU_ROOT` sagt, **welches** Wiki bedient wird. Ohne die Variable nimmt der Server den
Checkout, in dem das Paket selbst liegt — für eine einzelne Instanz reicht das, aber wer mehrere
Korpora hat, setzt sie besser immer.
Danach kennt der Client fünf Werkzeuge:
| Tool | Was es beantwortet |
|---|---|
| `search` | Seiten in `kb/` nach Text, nach Frontmatter (`confidence<0.6`, `tags~k8s`) oder beidem |
| `types` | Welche Seitentypen dieses Wiki kennt |
| `describe_type` | Der vollständige Vertrag eines Typs: Felder, Pflichtangaben, Enums |
| `lint` | Strukturelle Befunde: kaputte Wikilinks, Waisen, Index-Drift, Schema-Lücken |
| `status` | Momentaufnahme: Seitenzahl, Verteilung auf Collections, Befundzahlen |
## Schritt 4: Ausgeliefert starten (streamable HTTP)
Der Transport für den Betrieb, und der einzige, vor den sich ein Reverse Proxy setzen kann.
```bash
WIKI_TRACE=0 CHEMENU_ROOT=/srv/chemenu \
tools/.venv/bin/python -m chemenu.mcp \
--transport streamable-http --host 0.0.0.0 --port 8000
```
Der Endpunkt ist dann `http://<host>:8000/mcp`.
`--host 0.0.0.0` ist bewusst nicht der Default. Ohne die Angabe bindet der Server auf Loopback,
was lokal richtig und im Container falsch ist — dort muss der Proxy ihn erreichen können. Wer
`0.0.0.0` setzt, muss also auch dafür sorgen, dass davor etwas steht (Schritt 5).
`sse` wird nicht angeboten. Es ist der abgelöste Remote-Transport; jetzt darauf zu bauen
verschiebt den Wechsel nur.
## Schritt 5: Authentifizierung davor
**Der Server authentifiziert nicht selbst, und das ist Absicht.** Nicht sauber
authentifizierte Zugriffe sollen den Python-Prozess gar nicht erst erreichen. Die
Authentifizierung ist eine Traefik-ForwardAuth-Middleware:
> **<https://gitea.nehmer.net/torben/gitea-mcp-forward-auth>**
Kurz, was sie tut: sie prüft `Authorization: Bearer <token>` gegen SHA-256-Hashes erlaubter
Tokens, antwortet `200` bei gültigem und `401` bei fehlendem oder falschem Token, und hält
`GET /healthz` immer offen. Klartext-Tokens liegen weder in der Konfiguration noch im Log — nur
Hashes und ein kurzer Fingerprint. Konfiguriert wird sie über
`AUTH_PROXY_TOKEN_HASHES_DIR` (ein Verzeichnis, eine Datei je Token-Hash — passend für ein
Kubernetes-Secret-Volume) oder `AUTH_PROXY_TOKEN_HASHES` (kommagetrennte Liste). Einzelheiten,
Referenzmanifeste und ein Testskript stehen im README dort.
Einen Token-Hash erzeugen:
```bash
echo -n "mein-token" | sha256sum | awk '{print $1}'
```
**Rate Limiting gehört an dieselbe Stelle** — vor den Prozess, neben die Authentifizierung.
Nicht in den Iteration Budget Gate: der begrenzt eine *Agenten-Session* am unbemerkten Iterieren
über den Wiki-Zustand, weshalb Retrieval von ihm ausgenommen ist. Ihn als Rate Limiter zu
benutzen würde ihn dazu verwässern.
## Schritt 6: Den Korpus aktuell halten
Der Server liest den Arbeitsbaum. Ein veralteter Checkout antwortet selbstbewusst falsch —
deshalb trägt **jede Antwort den Commit**, aus dem sie berechnet wurde:
```json
{ "commit": "<40-stelliger SHA>", "as_of": "<ISO-8601, UTC>", "count": 3, "...": "..." }
```
Aktuell gehalten wird der Baum durch Polling, aus einem Timer neben dem Server:
```bash
git -C "$CHEMENU_ROOT" fetch --quiet origin && \
git -C "$CHEMENU_ROOT" reset --hard --quiet origin/main
```
Polling statt Webhook, weil es keinen eingehenden Endpunkt und keine Signaturprüfung braucht —
eine kleinere Angriffsfläche als das, was es optimieren würde.
`reset --hard` ist dabei tragend und keine Bequemlichkeit: der Korpus-Cache verwendet einen
Parse wieder, solange der Commit gleich bleibt, und cacht einen **schmutzigen Baum überhaupt
nicht**. Ein abgedrifteter Checkout antwortet also zwar richtig, parst aber bei jeder Anfrage
neu — und stempelt jede Antwort mit `"commit": null`, weil sie keiner Revision entspricht.
## Verifikation
Läuft es? Der schnellste Test ohne Client — startet den Server über stdio, listet die Tools und
stellt eine Frage:
```bash
cd tools && WIKI_TRACE=0 .venv/bin/python - <<'EOF'
import asyncio, os
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
async def main():
params = StdioServerParameters(
command=".venv/bin/python", args=["-m", "chemenu.mcp"],
env={"WIKI_TRACE": "0", "PATH": os.environ["PATH"]},
)
async with stdio_client(params) as (r, w):
async with ClientSession(r, w) as s:
await s.initialize()
print("Tools:", [t.name for t in (await s.list_tools()).tools])
out = await s.call_tool("status", {})
d = getattr(out, "structuredContent", None) or out.structured_content
print("Seiten:", d["pages"], "| Commit:", d["commit"])
asyncio.run(main())
EOF
```
Erwartete Ausgabe, sinngemäß:
```
Tools: ['search', 'types', 'describe_type', 'lint', 'status']
Seiten: 176 | Commit: 576df2cdddc96614a7e6641e562022d52112d411
```
Gegen die CLI gegenprüfen — beide müssen dieselbe Antwort geben:
```bash
tools/wikitool search "<begriff>" --json
```
## Troubleshooting
**`ERROR Telemetry is on and would write into the served checkout`** — erwartetes Verhalten,
kein Fehler in der Installation. `WIKI_TRACE=0` setzen oder `WIKI_TRACE_DIR` aus dem Korpus
heraus zeigen lassen (Schritt 2).
**`ModuleNotFoundError: No module named 'mcp'`** — Schritt 1 fehlt, oder der Client startet ein
anderes Python als das der Instanz. Im Client den absoluten Pfad auf `tools/.venv/bin/python`
setzen.
**`"commit": null` in jeder Antwort** — der bediente Baum hat uncommittete Änderungen. Entweder
läuft der Sync nicht, oder etwas schreibt in den Korpus, das dort nichts zu suchen hat. Der
Server selbst schreibt nie; ein Test prüft das, indem er alle fünf Tools aufruft und Dateibaum,
`HEAD` und `git status --porcelain` vorher/nachher vergleicht.
**`commit` nennt eine alte Revision** — der Sync aus Schritt 6 läuft nicht.
**Der Server antwortet anders als `wikitool`** — das ist ein Defekt, keine
Konfigurationsdifferenz: beide gehen durch dieselben Funktionen, und ein Golden-Test hält sie
zusammen. Zuerst prüfen, ob beide auf denselben Root zeigen; `CHEMENU_ROOT` ist leicht für einen
von beiden gesetzt und für den anderen nicht.
**Von außen nicht erreichbar** — ohne `--host 0.0.0.0` bindet der Server auf Loopback
(Schritt 4). Wenn er dann erreichbar ist, aber jeder Aufruf `401` bekommt, arbeitet die
Middleware aus Schritt 5 korrekt und das Token stimmt nicht.
## Was hier bewusst nicht steht
Deployment — Cluster, Ingress-Hosts, Secret-Store, FluxCD-Quelle. Das ist private Infrastruktur
und dieses Repo ist öffentlich.
Ein **Container-Image** für den Betrieb gibt es noch nicht; es ist als eigenes Vorhaben erfasst,
mitsamt den Entscheidungen, die dafür noch offen sind (Korpus im Image oder als Volume, wer den
Sync ausführt, Basis-Image, Healthcheck):
<https://gitea.nehmer.net/torben/chemenu/issues/37>. Bis dahin ist der Weg oben — venv,
`python -m chemenu.mcp`, Proxy davor — der vollständige.
+40 -14
View File
@@ -7,6 +7,10 @@ Repo als Upstream** aufsetzen. Der agent-seitige Ablauf steckt in `instructions/
nur die menschlichen Teile - für die vollständige Kommandoreferenz siehe nur die menschlichen Teile - für die vollständige Kommandoreferenz siehe
[tools/CONTRACT.md](tools/CONTRACT.md). [tools/CONTRACT.md](tools/CONTRACT.md).
Den optionalen **MCP-Leseserver** installiert und betreibt
[INSTALL-MCP.md](INSTALL-MCP.md): derselbe Korpus, lesend, für einen Konsumenten, der kein
Terminal auf dieser Maschine ist.
## Voraussetzungen ## Voraussetzungen
- Python 3.11 oder neuer - Python 3.11 oder neuer
@@ -64,11 +68,14 @@ Zwei Schritte, von denen nur der erste rein menschlich ist:
Wiki-Seite (`$WIKI_AUTHOR` überschreibt dies bei Bedarf). Wiki-Seite (`$WIKI_AUTHOR` überschreibt dies bei Bedarf).
- **Remote** (optional) - eine URL, wenn du das Repo auf einen Server pushen willst; sonst - **Remote** (optional) - eine URL, wenn du das Repo auf einen Server pushen willst; sonst
bleibt die Instanz lokal, und jedes `publish` läuft mit `--no-push`. bleibt die Instanz lokal, und jedes `publish` läuft mit `--no-push`.
- **KB-Sprache** - die exportierte Distribution bringt **Deutsch** mit: die Regel in - **Autorenkonventionen** - Sprache, Abschnittsnamen, Namensformen, Ton, Beziehungslabels
`kb/CONTRACT.md`, das Vokabular in `instructions/german-terminology.md` und deutsche und Confidence-Rubrik stehen in `kb/CONVENTIONS.md`, dazu je Collection die Regeln in
Abschnittsnamen in den Seitenvorlagen. Das ist eine Entscheidung dieser Ursprungsinstanz, `kb/<name>/COLLECTION.md`. Die Distribution bringt davon nur die `.template`-Dateien mit:
keine Eigenschaft des Musters. Willst du eine andere Sprache, sag es **vor dem ersten das sind Entscheidungen *dieser* Instanz, keine Eigenschaft des Musters, und nichts davon
Ingest** - danach ist es eine Migration jeder bereits angelegten Seite. liegt unter `tools/` oder `types/`. Fertige Profile - darunter ein vollständiges deutsches -
hält `instructions/kb-profiles.md` bereit; es ist eine Palette, kein Enum. Sag die Sprache
**vor dem ersten Ingest** - danach ist ein Wechsel der Abschnittsnamen eine Migration jeder
bereits angelegten Seite.
- **Personalization** - wer diese Instanz bedient (`USER.md`) und wie sie klingt - **Personalization** - wer diese Instanz bedient (`USER.md`) und wie sie klingt
(`SOUL.md`). Die Distribution bringt nur `USER.md.template` und `SOUL.md.template` mit: (`SOUL.md`). Die Distribution bringt nur `USER.md.template` und `SOUL.md.template` mit:
persönlicher Inhalt gehört nicht in jede exportierte Kopie, aber beide Dateien werden in persönlicher Inhalt gehört nicht in jede exportierte Kopie, aber beide Dateien werden in
@@ -142,8 +149,21 @@ nicht erreichbarer Feed wird als Fehler gemeldet - **nie** als „aktuell".
**Was die Versionsnummer aussagt:** kompatibel ist, was in der *linkesten von Null **Was die Versionsnummer aussagt:** kompatibel ist, was in der *linkesten von Null
verschiedenen Stelle* übereinstimmt. `0.1.3 → 0.1.4` ist ein sicheres Update, `0.1.3 → 0.2.0` verschiedenen Stelle* übereinstimmt. `0.1.3 → 0.1.4` ist ein sicheres Update, `0.1.3 → 0.2.0`
verlangt eine Migration, und ab `1.0.0` liest sich dieselbe Regel als das gewohnte „MAJOR heißt nicht, und ab `1.0.0` liest sich dieselbe Regel als das gewohnte „MAJOR bricht". `version check`
Migration". `version check` sagt das direkt (`state: update` vs. `state: migration`). sagt das direkt (`state: update` vs. `state: migration`).
Was diese Stelle beantwortet, ist **ob die neue Version ein Drop-in-Ersatz ist** - ob sich die
Maschinerie einfach darüberkopieren lässt und ob die alte danach noch zurückkann. Ob *Inhalt*
migriert werden muss, ist eine **zweite, unabhängige Frage**. Ein MAJOR-Sprung kann eine leere
Migrationskette haben und trotzdem Handarbeit verlangen: umbenannter Release-Feed, umbenanntes
Artefakt, umbenannter Import- oder Kommandoname, geänderte Envvar - `kb/` bleibt dabei
unangetastet, das Update ist trotzdem keins zum Drüberkopieren. Der Abschnitt „Sonderfall:
Update von 1.x auf 2.0.0" unten ist genau dieser Fall.
Deshalb stehen in den Release-Notes eines MAJOR zwei getrennte Zeilen, und beide sind vor dem
Update zu lesen: **Breaking Change:** sagt, was aufhört zu funktionieren und was diese Instanz
dagegen tun muss; **Migration:** sagt, ob und wie der Korpus umgeschrieben wird (`none required`,
wenn nicht). `tools/wikitool version notes` druckt den Eintrag.
### Eine Instanz aktualisieren ### Eine Instanz aktualisieren
@@ -160,13 +180,17 @@ dieser Unterschied ist der Zustand, in dem sich jede Instanz mitten im Upgrade b
2. Release-Tarball herunterladen und entpacken (Weg A), die Release-Notes lesen. 2. Release-Tarball herunterladen und entpacken (Weg A), die Release-Notes lesen.
3. Die **Maschinerie** aus dem Tarball über die Instanz kopieren: `tools/`, `types/`, 3. Die **Maschinerie** aus dem Tarball über die Instanz kopieren: `tools/`, `types/`,
`instructions/`, `AGENTS.md`, `VERSION`, `.wikitool-release.json`. Nicht anfassen: `kb/`, `instructions/`, `AGENTS.md`, `VERSION`, `.wikitool-release.json` - **und `kb/CONTRACT.md`**.
`raw/`, `work/`, `.wikitool-kb.json` und `.git/` - das ist die Instanz selbst. Die letzte Datei liegt unter einem Content-Verzeichnis, ist aber Stack-Eigentum: sie hält,
4. Achtung bei lokal angepassten Contract-Dateien: wer z. B. die KB-Sprache umgestellt hat was `wikitool` erzwingt, und ist in jeder Instanz gleich. Nicht anfassen: alles andere unter
(Schritt 5 in `setup-instance.md`), hat `kb/CONTRACT.md` und die Templates unter `types/` `kb/` und `raw/`, `work/`, `.wikitool-kb.json` und `.git/` - das ist die Instanz selbst,
verändert. Diese Änderungen vorher sichern und danach wieder einspielen. Welche Dateien das `kb/CONVENTIONS.md` und die `kb/*/COLLECTION.md` eingeschlossen.
sind, verrät ein Vergleich gegen die sha256-Summen im `files`-Block der alten 4. Achtung bei lokal angepassten Stack-Dateien. Die Autorenkonventionen gehören **nicht** dazu:
`.wikitool-release.json`. `kb/CONVENTIONS.md` und die `kb/*/COLLECTION.md` liegen unter `kb/`, werden in Schritt 3
also ohnehin nicht angefasst - genau dafür ist der Schnitt da. Wer darüber hinaus etwas
unter `tools/`, `types/` oder `instructions/` verändert hat, sichert das vorher und spielt
es danach wieder ein. Welche Dateien das sind, verrät ein Vergleich gegen die sha256-Summen
im `files`-Block der alten `.wikitool-release.json`.
5. **Die Migrationskette abarbeiten.** `tools/wikitool migrate status` listet jetzt alle 5. **Die Migrationskette abarbeiten.** `tools/wikitool migrate status` listet jetzt alle
offenen Migrationen in der Reihenfolge, in der sie laufen müssen - bei einem Sprung über offenen Migrationen in der Reihenfolge, in der sie laufen müssen - bei einem Sprung über
mehrere Versionen sind das mehrere. Für jede: das genannte Dokument unter mehrere Versionen sind das mehrere. Für jede: das genannte Dokument unter
@@ -223,6 +247,8 @@ behält Schema und Shape.
| `WIKITOOL_SESSION_ID` | Scopt das Iteration-Budget-Gate auf eine Aufgabe statt auf ein Terminal | Parent-Process-ID (siehe [instructions/session-setup.md](instructions/session-setup.md)) | | `WIKITOOL_SESSION_ID` | Scopt das Iteration-Budget-Gate auf eine Aufgabe statt auf ein Terminal | Parent-Process-ID (siehe [instructions/session-setup.md](instructions/session-setup.md)) |
| `WIKITOOL_UPDATE_URL` | Release-Feed, den `version check` abfragt | Wert aus `.wikitool-release.json`, sonst der Feed der Ursprungs-Instanz | | `WIKITOOL_UPDATE_URL` | Release-Feed, den `version check` abfragt | Wert aus `.wikitool-release.json`, sonst der Feed der Ursprungs-Instanz |
| `WIKITOOL_UPDATE_TOKEN` | Gitea-Token für den Release-Feed | keiner - gegen `torben/chemenu` nicht nötig, nur für einen privaten Fork (siehe unten) | | `WIKITOOL_UPDATE_TOKEN` | Gitea-Token für den Release-Feed | keiner - gegen `torben/chemenu` nicht nötig, nur für einen privaten Fork (siehe unten) |
| `CHEMENU_ROOT` | Auf welchen Korpus das Paket zeigt - für einen Aufrufer, der nicht im Checkout selbst liegt | der Checkout, in dem das Paket liegt (`tools/wikitool` verhält sich ohne die Variable unverändert) |
| `WIKI_TRACE` / `WIKI_TRACE_DIR` | Telemetrie abschalten bzw. aus dem Arbeitsbaum heraus umlenken | an, nach `reports/telemetry/` - der MCP-Server verweigert damit den Start, siehe [INSTALL-MCP.md](INSTALL-MCP.md) |
**Gegen das Ursprungs-Repo braucht es kein Token.** `torben/chemenu` ist öffentlich lesbar; **Gegen das Ursprungs-Repo braucht es kein Token.** `torben/chemenu` ist öffentlich lesbar;
`version check` und der Download in Weg A funktionieren ohne Konfiguration. `version check` und der Download in Weg A funktionieren ohne Konfiguration.
+14 -7
View File
@@ -14,12 +14,15 @@ and maintains a persistent wiki** that compounds over time.
English; the compiled pages under `kb/` are not. What stays English inside them is everything that English; the compiled pages under `kb/` are not. What stays English inside them is everything that
is an *identifier* rather than prose - page titles, section headings, wikilink targets, citation is an *identifier* rather than prose - page titles, section headings, wikilink targets, citation
ids, schema enum values, tags, commands, paths and code - so `GitOps Ownership Model` and ids, schema enum values, tags, commands, paths and code - so `GitOps Ownership Model` and
`## Beziehungen` sit in the same page without contradiction. The rule is `## Beziehungen` sit in the same page without contradiction. Which lines are identifiers is
[kb/CONTRACT.md § Language](kb/CONTRACT.md#language); the vocabulary behind it is [kb/CONTRACT.md § Language and identifiers](kb/CONTRACT.md#language-and-identifiers); *which
language* the prose is in, and what the tool-owned headings are called, is this instance's own
[kb/CONVENTIONS.md](kb/CONVENTIONS.md), and the vocabulary behind it is
[instructions/german-terminology.md](instructions/german-terminology.md). [instructions/german-terminology.md](instructions/german-terminology.md).
This is a per-instance decision, not a property of the pattern. A new instance built with This is a per-instance decision, not a property of the pattern - which is why it lives in a file
`dist export` starts empty and can pick any language by editing that one contract section before the instance owns rather than in one the stack ships. A new instance built with
`dist export` starts empty and picks any language by filling in `kb/CONVENTIONS.md` before
the first ingest. the first ingest.
## Getting started ## Getting started
@@ -51,6 +54,7 @@ chemenu/
├── CLAUDE.md # Claude Code only: imports AGENTS.md/USER.md/SOUL.md/ENVIRONMENT.md + its Claude-Code-only instructions. No rules of its own ├── CLAUDE.md # Claude Code only: imports AGENTS.md/USER.md/SOUL.md/ENVIRONMENT.md + its Claude-Code-only instructions. No rules of its own
├── README.md # This file: human-readable overview of the whole repo ├── README.md # This file: human-readable overview of the whole repo
├── INSTALL.md # Human-readable setup: new instance vs. cloning this one ├── INSTALL.md # Human-readable setup: new instance vs. cloning this one
├── INSTALL-MCP.md # Human-readable setup for the optional MCP read server
├── EVALS.md # Human-readable overview of telemetry and evaluation ├── EVALS.md # Human-readable overview of telemetry and evaluation
├── CHANGES.md # Changelog for the stack itself ├── CHANGES.md # Changelog for the stack itself
├── USER.md # Who operates this instance - context, never instructions ├── USER.md # Who operates this instance - context, never instructions
@@ -363,8 +367,11 @@ WIKI_TRACE=0 tools/.venv/bin/python -m chemenu.mcp \
``` ```
The dependency is deliberately not in `requirements.txt`: an instance that only uses the CLI The dependency is deliberately not in `requirements.txt`: an instance that only uses the CLI
should not have to install a web stack to do it. Running it, keeping its checkout current, and should not have to install a web stack to do it.
where authentication belongs (in front of the process, not in it) are in
Installing it, wiring a client to it, and putting authentication in front of it:
[`INSTALL-MCP.md`](INSTALL-MCP.md), for humans. The agent-side operating procedure - keeping the
checkout current, and what a stale answer looks like - is
[`instructions/mcp-read-server.md`](instructions/mcp-read-server.md). [`instructions/mcp-read-server.md`](instructions/mcp-read-server.md).
### Obsidian ### Obsidian
@@ -397,7 +404,7 @@ This wiki is tailored for IT work with:
- **Entity types** specific to software development and systems - **Entity types** specific to software development and systems
- **Relationship types** like `hängt ab von`, `verwendet`, `implementiert` - the vocabulary is in - **Relationship types** like `hängt ab von`, `verwendet`, `implementiert` - the vocabulary is in
[kb/CONTRACT.md § Linking](kb/CONTRACT.md#linking) [kb/CONVENTIONS.md](kb/CONVENTIONS.md), because it is this instance's rather than the stack's
- **Templates** for projects, systems, tools, technologies, ADRs - **Templates** for projects, systems, tools, technologies, ADRs
- **Guidelines** for documenting technical decisions - **Guidelines** for documenting technical decisions
- **Cross-reference patterns** for code and architecture - **Cross-reference patterns** for code and architecture
+1 -1
View File
@@ -1 +1 @@
2.4.0 3.0.0
+6 -3
View File
@@ -58,8 +58,10 @@ whether an instruction is still reachable, which is exactly why the answer means
`instructions/dev/` boundary check below asks the opposite question - what would *dangle* in a `instructions/dev/` boundary check below asks the opposite question - what would *dangle* in a
distributed instance - and does scan README.md, because `dist export` ships it verbatim. distributed instance - and does scan README.md, because `dist export` ships it verbatim.
Two kinds of file use the Manual tier today: [german-terminology.md](german-terminology.md), a Three kinds of file use the Manual tier today: [german-terminology.md](german-terminology.md), a
vocabulary consulted on demand rather than a procedure, and every migration document (below). vocabulary consulted on demand rather than a procedure; [kb-profiles.md](kb-profiles.md), the
catalogue of authoring profiles an instance may adopt into its own `kb/CONVENTIONS.md` and
`COLLECTION.md` files; and every migration document (below).
## `instructions/migrations/` ## `instructions/migrations/`
@@ -153,7 +155,8 @@ What lives where:
|-------|------| |-------|------|
| [AGENTS.md](../AGENTS.md) | Invariants and routing - what must always hold | | [AGENTS.md](../AGENTS.md) | Invariants and routing - what must always hold |
| `instructions/` | How the tooling is *operated* | | `instructions/` | How the tooling is *operated* |
| [kb/CONTRACT.md](../kb/CONTRACT.md) + each `COLLECTION.md` | How a page is *authored* | | [kb/CONTRACT.md](../kb/CONTRACT.md) | What the stack enforces about a page, in every instance |
| `kb/CONVENTIONS.md` + each `COLLECTION.md` | What *this* instance decided about authoring - owned by the instance, shipped only as a `.template` |
| [types/](../types/type-spec.md) | What a page structurally *is* | | [types/](../types/type-spec.md) | What a page structurally *is* |
| [tools/CONTRACT.md](../tools/CONTRACT.md) | What each command does and how it fails | | [tools/CONTRACT.md](../tools/CONTRACT.md) | What each command does and how it fails |
+25 -7
View File
@@ -48,6 +48,9 @@ stack development happens in the origin repo instead (see AGENTS.md's routing li
[testing-conventions.md](../testing-conventions.md) - the suite runs against a deliberately [testing-conventions.md](../testing-conventions.md) - the suite runs against a deliberately
empty machine; what the autouse fixture already neutralizes, and what a test still has to empty machine; what the autouse fixture already neutralizes, and what a test still has to
establish itself. Read it before adding or changing a test. establish itself. Read it before adding or changing a test.
[version-parts.md](../version-parts.md) - which part a change bumps: the drop-in test, the
catalogue of breaks that cross the compatibility boundary with `kb/` untouched, and what to
put in front of the user before a breaking bump. Read it before step 3.
More instructions are added here incrementally as stack-development needs come up - this More instructions are added here incrementally as stack-development needs come up - this
list grows without needing this skill file to change shape. list grows without needing this skill file to change shape.
3. **Raise the version, if the change ships.** A change under `tools/`, `types/`, 3. **Raise the version, if the change ships.** A change under `tools/`, `types/`,
@@ -59,18 +62,27 @@ stack development happens in the origin repo instead (see AGENTS.md's routing li
``` ```
Never edit `VERSION` or the entry's heading by hand - `bump` writes both, and `docs verify` Never edit `VERSION` or the entry's heading by hand - `bump` writes both, and `docs verify`
fails a tree where they disagree. Pick the part by what an existing instance would have to do: fails a tree where they disagree. Pick the part by whether the new version is a **drop-in
replacement** for the old one - not by whether content has to be migrated:
| Change | Part | | Change | Part |
|--------|------| |--------|------|
| Fix, no interface change | `--patch` | | Fix, no interface change | `--patch` |
| New capability, backwards compatible | `--minor` | | New capability, still drop-in in both directions | `--minor` |
| **Existing content must be migrated** | `--major` | | **Not a drop-in replacement** - any hand-work by the user or a migration script, or a downgrade that no longer works | `--major` |
A `--major` bump additionally needs a migration document for the new version - written per Content migration is one way to land in the last row, not the definition of it: a rename of
[migrate-corpus.md](../../migrate-corpus.md) - or `--no-migration "<reason>"` when no content the update path, the artefact, an import name, a flag or an envvar breaks a swap with `kb/`
actually has to change. `bump` refuses otherwise, and so does `docs verify`: an instance entirely untouched. The full test, the catalogue of such breaks, and what to put in front of
learning that it must migrate, with nothing telling it how, is a dead end. the user first are in [version-parts.md](../version-parts.md) - **read it before choosing
`--major`.**
A `--major` bump therefore needs two things recorded. `--breaking "<what stops working>"`
is required on every boundary-crossing bump; on top of it, a migration document for the new
version - written per [migrate-corpus.md](../../migrate-corpus.md) - or
`--no-migration "<reason>"` when no content actually has to change. `bump` refuses without
either, and so does `docs verify`: an instance learning that it must migrate, with nothing
telling it how, is a dead end.
Then write the entry's body - `bump` deliberately leaves it empty, the same way `new` leaves Then write the entry's body - `bump` deliberately leaves it empty, the same way `new` leaves
the prose. the prose.
@@ -90,6 +102,12 @@ stack development happens in the origin repo instead (see AGENTS.md's routing li
- **Touches both stack code and wiki content in one session?** Apply this skill's rules to the - **Touches both stack code and wiki content in one session?** Apply this skill's rules to the
code changes and the normal content skills' rules to the content changes - they are not code changes and the normal content skills' rules to the content changes - they are not
mutually exclusive within a session, only per change. mutually exclusive within a session, only per change.
- **The change turns out not to be a drop-in replacement?** Do not bump across the boundary on
your own initiative. Every existing instance pays for a breaking change once, by hand, so the
user decides whether it is worth that: show them what breaks, what an instance has to do about
it, and the alternatives (avoid the break with a shim, defer and batch it with the next one,
or split it behind a deprecation window), then recommend one and wait for a go-ahead.
[version-parts.md](../version-parts.md) step 4 has the full shape.
## Scope ## Scope
+5
View File
@@ -35,6 +35,11 @@ Do not re-do any of this per test; it is done for you, per test, via `monkeypatc
fixture redirects it into `tmp_path`. Tracing is never disabled suite-wide, because two fixture redirects it into `tmp_path`. Tracing is never disabled suite-wide, because two
telemetry tests assert that a trace gets written. telemetry tests assert that a trace gets written.
Two in-process caches are cleared alongside the environment, for the same reason: `config`'s
resolved paths and `conventions`' parsed `kb/CONVENTIONS.md`. A test that *rewrites* the
conventions file mid-test calls `conventions.reset_cache()` itself - the fixture answers for the
boundary between tests, not for one inside a test.
## When to run ## When to run
Whenever you add or change a test under `tools/chemenu/tests/`. Whenever you add or change a test under `tools/chemenu/tests/`.
+149
View File
@@ -0,0 +1,149 @@
---
type: types/instruction.md
name: version-parts
description: Which part of the stack version a change bumps - the compatibility question (is the new version a drop-in replacement?) separated from the migration question (must existing content change?), plus what to do before a breaking bump.
---
# Pick the version part for a stack change
Two questions decide a version bump, and they are **not the same question**:
1. **Is the new version a drop-in replacement for the old one?** This is what the version
number itself says. Compatibility is read off the **leftmost non-zero component** - on this
stack (`2.x`) that is MAJOR, on a `0.x` stack it is MINOR. A bump that changes it is called
*boundary-crossing* below, because that is the term `version bump` and `docs verify` use in
their own messages.
2. **Must existing content be migrated?** This is a *consequence* a boundary crossing may or
may not have. `kb/` staying untouched does not make a change compatible, and
`version bump --no-migration` exists precisely because boundary-crossing bumps with an
untouched corpus are a real case.
Getting these backwards is how a genuinely breaking change ships as a MINOR. It happened once
already (see the case study at the end), which is why this file exists.
## When to run
Before every `tools/wikitool version bump` - the `stack-dev` skill's step 3 sends you here.
Read it in full the first time a change looks like it might be boundary-crossing; afterwards
the three-line test below is usually enough.
## Steps
1. **Apply the drop-in test.** The bump is boundary-crossing if **either** half fails:
- **Forward:** an existing instance can install the new machinery by copying `tools/`,
`types/`, `instructions/`, `AGENTS.md`, `VERSION` and `.wikitool-release.json` over itself,
and everything that worked before still works - with **no** hand-work by the user and **no**
migration script. Any step beyond the copy, however small, fails this half.
- **Backward:** having installed the new version, the user can put the old one back and be
where they started. A state file the old version cannot read, a rewritten corpus, a
renamed stamp - anything that makes the downgrade fail or leave a broken instance - fails
this half.
Content migration is one way to fail the forward half, not the definition of it.
2. **Check the catalogue** when the answer still feels like a judgment call. Each of these
crosses the boundary with `kb/` entirely untouched:
| What changed | Why the swap is not drop-in |
|---|---|
| The update path - `update_url`, the release feed, the repo it points at | The instance cannot repair its own `.wikitool-release.json`: it is machine-written, and invariant 1 forbids the hand-edit. The channel that would have told it to update is the channel that broke |
| The release artefact's name | Every download script and every pin against it breaks |
| The Python package's import name | `from <old> import ...` outside the shipped tree breaks |
| A command, subcommand, or flag that was removed or renamed | Scripts, CI workflows and instruction files calling the old spelling break |
| An environment variable's name | An instance configured through it silently loses the configuration |
| The shape of a machine-read file - `.wikitool-kb.json`, `.wikitool-release.json`, a generated index | The old version cannot read what the new one wrote, so the downgrade half fails even if the upgrade half passed |
| A type-spec's required fields | Existing pages stop validating - this one crosses *and* needs a content migration |
The catalogue is illustrative, not exhaustive. When something is not on it, go back to step 1.
3. **Otherwise pick the compatible part:**
| Change | Part |
|---|---|
| Fix, no interface change | `--patch` |
| New capability, drop-in in both directions | `--minor` |
4. **Stop and talk to the user before a boundary-crossing bump.** It is expensive in a way the
other two parts are not: every existing instance pays for it, once, by hand. Put in front of
them, in this order:
- **What breaks**, concretely - which file, which name, which call site.
- **What each existing instance must do**, as the steps they would actually run.
- **The alternatives**, so the break is a choice and not a side effect:
- *Avoid it* - keep the old name as an alias, read both file shapes, accept both flag
spellings. A compatibility shim carried for one release is usually cheaper than a
migration everyone runs.
- *Defer and batch it* - hold the break until the next boundary crossing, so instances pay
once instead of twice.
- *Split it* - ship the compatible half now, the breaking half later behind a deprecation
window that the changelog announces in advance.
- **Your recommendation**, with the trade-off named.
Then wait for an explicit go-ahead. Do not bump across the boundary on your own initiative.
5. **Record the break in the bump itself.** A boundary-crossing bump requires
`--breaking "<what breaks>"`, which writes a `**Breaking Change:**` line into the entry:
```bash
tools/wikitool version bump --major \
--title "<what changed>" \
--breaking "<what stops working, and what an instance must do about it>" \
--no-migration "<why no page has to change>" # only if that is true
```
`--breaking` is refused on a bump that crosses nothing, and required on one that does;
`docs verify` checks the newest boundary-crossing entry still carries the line. Write it for
the operator of an instance that has not read this repository: what stops working, and what
they do about it.
6. **Then answer the migration question separately.** Boundary-crossing and
content-migrating are independent:
- Content must change → write the migration document under `instructions/migrations/` per
[migrate-corpus.md](../migrate-corpus.md). `bump` finds it by its `migrates_to:` field.
- Content need not change → `--no-migration "<reason>"`, which records that in the entry.
Both are also needed by `docs verify`, for the same reason: an instance that learns it must
migrate, with nothing telling it how, is a dead end.
7. **Write the entry's body.** `bump` leaves it empty on purpose. A boundary-crossing entry
earns a paragraph that says *why this is breaking* - it is the one thing a future reader
cannot reconstruct from the diff, and it is what the next session in this position will read
instead of guessing.
## Decision points
- **The change ships no code - only `README.md`, `INSTALL.md`, `EVALS.md`, or `.gitea/`?** No
bump at all; CI's version gate is scoped to what changes behaviour.
- **A break you can see coming but are not making yet?** File it as an issue and let it
accumulate. Boundary crossings are cheaper in batches, and step 4's "defer" alternative is
only real if someone wrote the break down.
- **Unsure between MINOR and boundary-crossing?** It is boundary-crossing. The cost of an
unnecessary MAJOR is one extra release note; the cost of a MINOR that actually breaks is an
instance whose update path fails while its version number promised it would not.
- **The break only affects this repository, not a distributed instance** - something under
`instructions/dev/`, say? Then it is not a stack break at all: `dist export` never shipped it.
Judge by what an *exported* instance sees.
## Scope
Applies to the stack version in `VERSION` - `tools/`, `types/`, `instructions/`, `AGENTS.md`
and the contracts. It says nothing about the content shape in `.wikitool-kb.json`, which is
advanced by `wikitool migrate done` and described by [migrate-corpus.md](../migrate-corpus.md),
and nothing about wiki content operations, which are logged in `kb/log.md` and carry no version
at all.
Choosing the part remains a judgment call, deliberately: `docs verify` checks that a
boundary-crossing entry *documents* its break and its migration, never that the part was chosen
correctly. No validator can tell a renamed flag from a new one.
## Case study: 2.0.0
The Chemenu rebranding renamed the repo, the release artefact and the Python package. No page
in `kb/` changed, so the first attempt was `1.9.0` - the migration question, answered correctly,
substituted for the compatibility question, which was never asked. Three things broke: every
existing instance's `update_url` pointed at a repo path that no longer existed and could not be
hand-repaired; the artefact name changed; the import name changed. The correct bump was
`--major --no-migration`, and the `CHANGES.md` entry for `2.0.0` carries the reasoning in full
under "Warum das trotzdem MAJOR ist". The error was caught by the user, not by the
documentation - which is what step 4 is for.
+10 -6
View File
@@ -7,9 +7,13 @@ manual: true
# German terminology for `kb/` # German terminology for `kb/`
Reference vocabulary for [kb/CONTRACT.md](../kb/CONTRACT.md#language)'s rule that pages are Reference vocabulary for [kb/CONVENTIONS.md](../kb/CONVENTIONS.md#language)'s rule that this
written in German. The rule lives there; the word list lives here, because it is lookup material instance's pages are written in German. The rule lives there; the word list lives here, because
rather than a norm and would otherwise be loaded on every write. it is lookup material rather than a norm and would otherwise be loaded on every write.
**This file belongs to the `german` language profile, not to the stack.** An instance writing in
another language deletes or replaces it - see
[kb-profiles.md](kb-profiles.md).
Derived from translating all 248 pages on 2026-08-29. Every entry below is a decision that was Derived from translating all 248 pages on 2026-08-29. Every entry below is a decision that was
made wrong at least once first - each cost a correction pass across published pages, which is why made wrong at least once first - each cost a correction pass across published pages, which is why
@@ -98,8 +102,8 @@ none of them structural, so no check found them. It is the one thing to watch fo
instructional prose. instructional prose.
- **Quotations are never reworded**, neither translated nor moved into the impersonal register. - **Quotations are never reworded**, neither translated nor moved into the impersonal register.
- Buzzwords and AI filler are banned by [kb/CONTRACT.md](../kb/CONTRACT.md#tone); the German list - Buzzwords and AI filler are banned by [kb/CONVENTIONS.md](../kb/CONVENTIONS.md#tone); the
is there. German list is there.
- Dash as ` - `, not `—`. - Dash as ` - `, not `—`.
- German number formatting only in prose („10.000 Punkte"). Never inside code, version numbers or - German number formatting only in prose („10.000 Punkte"). Never inside code, version numbers or
measurements (`75-85 px`, `10m`, `0.90`). measurements (`75-85 px`, `10m`, `0.90`).
@@ -108,4 +112,4 @@ instructional prose.
This is about prose in `kb/`. What is prose and what is an identifier - titles, headings, wikilink This is about prose in `kb/`. What is prose and what is an identifier - titles, headings, wikilink
targets, cite-ids, enum values, tags, code - is decided by targets, cite-ids, enum values, tags, code - is decided by
[kb/CONTRACT.md](../kb/CONTRACT.md#language), not here. [kb/CONTRACT.md](../kb/CONTRACT.md#language-and-identifiers), not here.
+191
View File
@@ -0,0 +1,191 @@
---
type: types/instruction.md
name: kb-profiles
description: Ready-made answers for kb/CONVENTIONS.md and each COLLECTION.md - the proven collection contracts and language profiles this stack has shipped, offered as a palette to adopt or adapt, never as a binding source.
manual: true
---
# Pick a profile for a collection or for this instance's conventions
**This page is a palette, not an enum.** Each `kb/<name>/COLLECTION.md` stays authoritative for
its own collection and `kb/CONVENTIONS.md` for the instance as a whole; an entry here is a
proven starting point, nothing more. Adopting one means *copying its text into* that file - not
pointing at this page and inheriting whatever it says later. Nothing in the stack reads this
document, and `profile:` in a contract's frontmatter records where the text came from, not where
it lives.
That direction is deliberate and it is the opposite of how this repo used to work. Language,
tone, naming and the relationship vocabulary sat in `kb/CONTRACT.md`, a file `dist export` ships
verbatim - so every instance that wanted something else edited a stack file, and an upstream
merge handed the stack's answer back. What binds is now the instance's; what ships is this
catalogue, and it binds nothing.
## When to run
- Setting up a new instance: the KB-language step of
[setup-instance.md](setup-instance.md) sends you here to fill `kb/CONVENTIONS.md`.
- Adding a collection to an existing instance, and wanting a contract that already works rather
than a blank one.
- Rewriting an existing `COLLECTION.md` or `kb/CONVENTIONS.md` and wanting to see what the
alternatives were.
Not for changing what the *stack* enforces. That is [kb/CONTRACT.md](../kb/CONTRACT.md), and it
is not a profile.
## Steps
1. **Decide what you are filling.** Two different files, and they are not interchangeable:
| File | Holds | Profiles below |
|---|---|---|
| `kb/CONVENTIONS.md` | Language, section headings, naming forms, tone, relationship labels, confidence rubric - once per instance | [Language profiles](#language-profiles) |
| `kb/<name>/COLLECTION.md` | What one collection holds, its quality goal, its local linking and naming rules | [Collection profiles](#collection-profiles) |
2. **Copy the entry's text into the file**, then edit it until it is true of this instance.
A profile you adopted and then changed is still that profile's `profile:` value - the field
records the starting point, not a promise of fidelity.
3. **Record it.** `profile: <name>` in the file's frontmatter, or `profile: none` for a
collection written from scratch. `wikitool docs verify` checks the field is there; it does
not check the value against this page, because a collection an instance invented has no
entry here to name.
4. **Set `required_by_stack:` on a collection - and set it correctly.** This one is *not* a
choice: it says whether `wikitool` resolves against the collection by name, and
`docs verify` checks it against the stack's own list. `sources` is `true`, everything else
is `false`. See [kb/CONTRACT.md § Collections](../kb/CONTRACT.md#collections).
## Language profiles
A language profile answers all of `kb/CONVENTIONS.md` at once. There is one today, because one
is what this repo has actually run.
### `german`
The profile this repo's own instance uses, and the reason this catalogue exists: it was the
stack's hardcoded behaviour until the conventions file existed.
| Decides | Value |
|---|---|
| `language:` | `de` |
| `sections:` | `Beziehungen` / `Siehe auch` / `Fußnoten` |
| Naming | Human-readable titles with spaces; singular for entities; `adr-NNN-` for decisions; `X vs Y` for comparisons |
| Tone | Wikipedia register, with a German buzzword and filler list |
| Relationship labels | `hängt ab von` · `verwendet` · `implementiert` · `erweitert` · `ersetzt` · `steht in Konflikt mit` · `benötigt` · `erzeugt` · `konsumiert` · `besitzt` · `pflegt` · `läuft auf` · `verwandt mit` |
| Confidence rubric | 0.5 base, +0.2 per supporting source (max +0.6), recency and source-quality bonuses; hedge with "möglicherweise"/"kann" below 0.6, "unsicher"/"unbestätigt" below 0.4 |
| Terminology | [german-terminology.md](german-terminology.md) - which English terms stay English, and which have a settled German form |
**The full text to copy** is this repo's own [kb/CONVENTIONS.md](../kb/CONVENTIONS.md). An
instance adopting it takes that file, not this table; the table is what the profile *decides*,
so you can tell at a glance whether it is the one you want.
Adopting it also means keeping `german-terminology.md`. An instance on any other language
deletes or replaces that file - it is the profile's lookup material, not the stack's.
### `english`
What `kb/CONVENTIONS.md.template` ships as its default, so "adopt `english`" means "fill in the
template and change nothing structural". `sections:` are `Relationships` / `See Also` /
`Footnotes`, which are also the names this stack wrote before it had a conventions file - so a
corpus that predates the split needs no translation pass to adopt this profile.
There is no worked text for the rest of it. The template's placeholders are the questions;
`german` above is what a filled answer looks like.
### Writing a third one
A language profile is not a translation of `german`. Two of its sections are judgment about a
language rather than vocabulary in it - which foreign technical terms stay untranslated, and how
to hedge a low-confidence claim - and those are exactly the two that read as awkward when
translated mechanically. Write them, do not convert them.
The one part that is mechanical: `section_aliases:`. Whatever the corpus used before goes in
that list, and the pages then migrate one at a time instead of all at once.
## Collection profiles
The four collections this repo runs. Each is a whole `COLLECTION.md`, and **the text to copy is
the file itself** - `dist export` ships each one as `kb/<name>/COLLECTION.md.template`, which a
new instance adopts by renaming. What follows is what each decides, so you can tell whether you
want it.
### `entities`
Concrete, pointable things: projects, deployed systems, tools, technologies, people.
- **Quality goal:** pointability plus currency - what the thing is, where it actually is, and
whether that is still true.
- **Areas** driven by the `entity_type:` field: `projects/`, `systems/`, `tools/`,
`technologies/`, `people/`. Areas, not collections - they inherit the contract and carry no
`COLLECTION.md`.
- **Per-area emphasis** spelled out, so a system page is not written like a technology page.
- `required_by_stack: false`.
Take it when the wiki is about things that exist. Adapt the area list first: it is the part most
likely to be wrong for another domain.
### `concepts`
Ideas rather than things: architectures, patterns, protocols, workflows, recurring problems,
and the decisions taken about them.
- **Quality goal:** explanatory sufficiency - the page answers *why it is done this way* without
the reader opening the entity pages that use it.
- Carries the **ADR shape**: context, decision, consequences, status, and the rule that a
superseded decision is never rewritten.
- Routes head-to-head arguments out to `comparisons/` rather than hosting them.
- `required_by_stack: false`.
Take it whenever `entities` is taken - the split between the two is what keeps either from
becoming an essay.
### `sources`
One page per ingested source, carrying the `raw_files:` provenance every citation resolves
against.
- **Quality goal:** faithful compression - what *this source* said, not what was concluded from
it. A source page improved beyond its source is no longer evidence.
- Titles carry the `Source - ` prefix, applied by `wikitool new source`.
- `required_by_stack: **true**`. `sources coverage`, `[^cite-id]` resolution and
`kb/provenance.md` resolve against the name `sources`.
Not optional in the way the others are. An instance may rewrite its authoring rules and may not
rename or drop it.
### `comparisons`
Structured head-to-head evaluations of two or more things that already have pages.
- **Quality goal:** decidability - named, checkable dimensions and a stated trade-off, so a
reader with a concrete situation can choose.
- Every subject must already have a page; a comparison is a view over existing knowledge.
- **Exempt from the orphan check** - comparisons are reached through the catalog, not through
inbound prose links.
- `required_by_stack: false`.
Skip it in a wiki that records rather than decides. It is the one of the four that is genuinely
optional.
## Decision points
- **A profile is almost right?** Copy and edit. There is no partial adoption and no override
file - the copy *is* the mechanism, and `profile:` still records where it started.
- **Two collections want the same profile?** Fine. `profile:` is not unique, and two
collections holding different subject matter under the same authoring rules is an ordinary
outcome.
- **Changing `sections:` after pages exist?** That is a corpus migration, not an edit. Put the
old names in `section_aliases:` first, then translate page by page - the tool keeps finding
the old headings for as long as the alias stands. See
[migrate-corpus.md](migrate-corpus.md).
- **Tempted to make this page binding** - to have `COLLECTION.md` say `profile: entities` and
nothing else? Do not. That is the arrangement this split was written to end: the instance
would be bound by a file the stack ships and upgrades, which is how an upstream merge changes
an instance's authoring rules without anyone deciding to.
## Scope
Covers what an instance authors under `kb/`. It says nothing about what the stack enforces
([kb/CONTRACT.md](../kb/CONTRACT.md)), what a page structurally is
([types/type-spec.md](../types/type-spec.md)), or how a command behaves
([tools/CONTRACT.md](../tools/CONTRACT.md)). None of those are profiles, and none of them are
the instance's to change.
@@ -0,0 +1,150 @@
---
type: types/instruction.md
name: 3.0.0-authoring-conventions
description: 'Adopt the instance-owned authoring conventions introduced in 3.0.0 - write kb/CONVENTIONS.md, declare profile:/required_by_stack: on every COLLECTION.md, and replace kb/CONTRACT.md with the shipped one.'
manual: true
migrates_to: 3.0.0
migration_kind: mechanical
---
# Adopt this instance's own authoring conventions (3.0.0)
Until 3.0.0, the rules for writing a page were split by *location*: everything about `kb/` sat
in `kb/CONTRACT.md`, a file every distribution ships verbatim. Half of it was never the stack's
to decide - the language pages are written in, the three tool-owned section headings, the naming
forms, the tone, the relationship labels, the confidence rubric - so an instance that wanted
something else edited a file the stack also ships, and an upstream merge handed the stack's
answer back.
3.0.0 splits it by *ownership* instead. `kb/CONTRACT.md` keeps only what `wikitool` enforces;
everything else moves into a new `kb/CONVENTIONS.md` that belongs to this instance, and each
`kb/<name>/COLLECTION.md` now declares what it is. The compiler reads its section headings from
that file rather than from `tools/chemenu/sections.py`.
**No page changes.** Not one line under `kb/entities/`, `kb/concepts/`, `kb/sources/` or
`kb/comparisons/` is touched. What changes are the contracts beside them, which is why this is
`mechanical` and takes minutes rather than a workshop.
## When to run
After installing 3.0.0 machinery over an instance that was on 2.x, when `tools/wikitool doctor`
reports `FAIL conventions` or `tools/wikitool docs verify` reports a `COLLECTION.md` with no
frontmatter. `tools/wikitool migrate status` names this document.
**Until it has run, the compiler answers out of a fallback.** `xref add` and `cite add` write
`## Beziehungen` / `## Siehe auch` / `## Fußnoten` - what this stack hardcoded before the
conventions file existed. That is correct for a corpus written under them and wrong for any
other, so run this before the next `wiki-ingest` or `wiki-manage`, not afterwards.
## Steps
1. **Replace `kb/CONTRACT.md` from the release.** It is machinery that happens to live under a
content directory, and the tarball update path used to skip it (see `INSTALL.md`, which now
names it explicitly). The 3.0.0 version is roughly half the length of the 2.x one - the
removed half is what step 2 is about to write into a file of yours.
```bash
cp <unpacked-release>/kb/CONTRACT.md kb/CONTRACT.md
```
A private instance cloned from an upstream takes it with the merge instead - see
[private-instance.md](../private-instance.md), whose update procedure now re-takes the
upstream side for exactly this path.
2. **Write `kb/CONVENTIONS.md`.** Two ways in, and the first is almost always right:
- **This instance writes German pages** (it did, unless you changed it): copy the release's
`kb/CONVENTIONS.md.template` and fill it from the `german` profile in
[kb-profiles.md](../kb-profiles.md) - whose worked full text is the origin repo's own
`kb/CONVENTIONS.md`. Everything in it was already true of your corpus; it was simply
written down somewhere you did not own.
- **You had changed the language**, and therefore hold local edits to `kb/CONTRACT.md`,
`types/*.md` and `tools/chemenu/sections.py`: those edits are what this file replaces. Copy
the canonical heading names out of your old `sections.py` into `sections:`, the labels and
tone rules out of your old `kb/CONTRACT.md`, then **discard the local edits under `tools/`
and `types/`** and take the shipped versions. That is the whole point of the change: there
is nothing left to patch there.
The minimum the tool needs is the frontmatter:
```yaml
---
language: de
profile: german
sections:
relationships: Beziehungen
see_also: Siehe auch
footnotes: Fußnoten
---
```
Set `sections:` to the names **your existing pages already carry**, not to what you would
prefer. Changing them is a separate, real corpus migration; `section_aliases:` is how it is
done page by page ([migrate-corpus.md](../migrate-corpus.md)).
Drop the `wikitool:template-unfilled` sentinel line while filling it in - `doctor` FAILs on a
renamed-but-unanswered template exactly as it does for `USER.md`.
3. **Declare each collection.** Two frontmatter lines at the top of every
`kb/<name>/COLLECTION.md`:
```yaml
---
profile: <the entry in instructions/kb-profiles.md this contract came from, or none>
required_by_stack: false
---
```
`required_by_stack: true` on `kb/sources/` and **nowhere else**. It is not a preference:
`sources coverage`, `[^cite-id]` resolution and `kb/provenance.md` resolve against that name,
and `docs verify` checks the field against the stack's own list in both directions.
For the four default collections, the shipped `kb/<name>/COLLECTION.md.template` files carry
the right values already.
4. **Verify.** All three must pass:
```bash
tools/wikitool doctor # `conventions` must be OK
tools/wikitool docs verify
tools/wikitool lint
```
`migrate verify` is deliberately not in that list: it compares pages, and no page changed.
Running it would report nothing and prove nothing.
5. **Record it.**
```bash
tools/wikitool migrate done 3.0.0 --pages 0
```
`--pages 0` is honest, not a placeholder - see the note under step 1.
## How to tell a migrated instance from an unmigrated one
`kb/CONVENTIONS.md` exists, carries no `wikitool:template-unfilled` line, and names all three
slots under `sections:`; every `kb/*/COLLECTION.md` opens with a frontmatter block; and
`kb/CONTRACT.md` has a `## Language and identifiers` heading rather than a `## Language` one.
`doctor` answers all of that in one call.
## Decision points
- **`doctor` says `conventions: FAIL` after step 2?** It prints which slot is missing. The three
keys are `relationships`, `see_also` and `footnotes` - the *slot* names are fixed, only their
values are yours.
- **A collection this instance invented, with no profile behind it?** `profile: none`. The field
records where the text came from; it is free text and `docs verify` does not check it against
the catalogue, because an invented collection has no entry there to name.
- **Tempted to point `profile:` at the catalogue instead of copying the text?** Do not. An
adopted profile is a copy; a reference would put your binding authoring rules in a file the
stack ships and upgrades, which is the arrangement 3.0.0 exists to end.
- **Your old `kb/CONTRACT.md` had local edits you still want?** They belong in
`kb/CONVENTIONS.md` now. If something you edited has no home there, it was a stack rule you
overrode - file it as an issue against the origin repo rather than re-applying it.
## Scope
One instance's contracts, once. It changes no page, no frontmatter on a page, and nothing under
`raw/`. The machinery half of the 3.0.0 upgrade - copying `tools/`, `types/`, `instructions/`,
`AGENTS.md`, `VERSION` and `.wikitool-release.json` - is `INSTALL.md`'s, and has to have
happened before step 1.
+48 -6
View File
@@ -100,17 +100,36 @@ So the merge has to be scoped. That is the procedure below, and it is not option
[setup-instance.md](setup-instance.md), then [bootstrap.md](bootstrap.md) for the venv and [setup-instance.md](setup-instance.md), then [bootstrap.md](bootstrap.md) for the venv and
the skills. the skills.
A clone inherits the upstream's `kb/CONVENTIONS.md` and `kb/*/COLLECTION.md` rather than
templates, because it inherits the upstream's whole tree. They are yours from this point on:
rewrite them if this instance writes its pages differently - the update procedure below
restores them on every merge, so the change sticks. [kb-profiles.md](kb-profiles.md) has the
alternatives.
## Taking a stack update ## Taking a stack update
Take the machinery, never the content. The merge is held open, the content stages are forced Take the machinery, never the content. The merge is held open, the content stages are forced
back to your own state, and only then does it close: back to your own state, and only then does it close.
**Three files under those stages are machinery, not content**, and forcing them back is how an
upstream contract change gets silently discarded:
| Path | Why it must take the upstream side |
|---|---|
| `kb/CONTRACT.md` | The stack's own knowledge-layer contract. Every rule in it is enforced by `wikitool`; an instance never edits it |
| `kb/CONVENTIONS.md.template` | The template your `kb/CONVENTIONS.md` was filled from. The filled file is yours; the template is the stack's |
| `raw/CONTRACT.md` | The raw stage's contract, for the same reason as the first row |
Everything else under `kb/` and `raw/` is yours, `kb/CONVENTIONS.md` and each
`kb/<name>/COLLECTION.md` included - they bind your corpus, and they are exactly what the
restore below is protecting.
```bash ```bash
BEFORE=$(git rev-parse HEAD) BEFORE=$(git rev-parse HEAD)
git fetch upstream git fetch upstream
# --no-commit holds the merge open; it may report conflicts under kb/ or raw/, # --no-commit holds the merge open; it may report conflicts under kb/ or raw/,
# which the next three lines are about to make irrelevant. # which the next four lines are about to make irrelevant.
git merge --no-commit --no-ff upstream/main || true git merge --no-commit --no-ff upstream/main || true
# Whatever the merge did to the content stages, undo it. HEAD is still your # Whatever the merge did to the content stages, undo it. HEAD is still your
@@ -119,18 +138,32 @@ git rm -rq --cached --ignore-unmatch kb raw
rm -rf kb raw rm -rf kb raw
git checkout HEAD -- kb raw git checkout HEAD -- kb raw
# ...then take the upstream side back for the machinery that lives among it.
# MERGE_HEAD is still resolvable while the merge is open.
git checkout MERGE_HEAD -- kb/CONTRACT.md kb/CONVENTIONS.md.template raw/CONTRACT.md
git commit --no-edit git commit --no-edit
``` ```
Then **check that it worked**, rather than trusting that it did: Then **check that it worked**, rather than trusting that it did. The same three paths are
excluded here, spelled out rather than held in a variable so that the check can be read on its
own and copied on its own:
```bash ```bash
git diff --name-only $BEFORE HEAD -- kb raw # must print nothing git diff --name-only "$BEFORE" HEAD -- kb raw \
| grep -vE '^(kb/CONTRACT\.md|kb/CONVENTIONS\.md\.template|raw/CONTRACT\.md)$'
``` ```
Must print nothing.
An empty result is the proof that the update touched machinery only. A non-empty one means a An empty result is the proof that the update touched machinery only. A non-empty one means a
path slipped through - inspect it before going further. path slipped through - inspect it before going further.
**The exclusion is not cosmetic.** Without it the check reports *empty* for an update that just
ate a `kb/CONTRACT.md` change - it would be confirming the failure it exists to catch. If one of
the three paths does not appear in the diff at all, that is fine: it means upstream did not
touch it.
Then, as after any stack change: `doctor`, `docs verify`, `instructions verify`, `migrate status`, Then, as after any stack change: `doctor`, `docs verify`, `instructions verify`, `migrate status`,
`lint`. A `migrate status` with outstanding links means the update crossed a compatibility `lint`. A `migrate status` with outstanding links means the update crossed a compatibility
boundary - follow [migrate-corpus.md](migrate-corpus.md) before doing anything else. boundary - follow [migrate-corpus.md](migrate-corpus.md) before doing anything else.
@@ -157,11 +190,20 @@ merge above. Nothing is lost by the detour: the fix has to pass that CI either w
above overwrites those stages with your own afterwards, so the conflict resolves itself. above overwrites those stages with your own afterwards, so the conflict resolves itself.
Never resolve one by hand with `git add -A` - that is exactly how the upstream version, which Never resolve one by hand with `git add -A` - that is exactly how the upstream version, which
git left sitting in your working tree, gets committed into your instance. git left sitting in your working tree, gets committed into your instance.
- **`git diff` after the merge shows something under `kb/` or `raw/`?** Stop. The scoping step - **`git diff` after the merge shows something under `kb/` or `raw/`?** Stop - unless it is one
did not take. Do not publish; find out which path came through and where from. of the three machinery paths the check excludes, which is the update working as intended. For
anything else the scoping step did not take: do not publish; find out which path came through
and where from.
- **Conflict in `tools/`, `types/` or `instructions/`?** You changed the stack locally, which - **Conflict in `tools/`, `types/` or `instructions/`?** You changed the stack locally, which
step "Where stack development happens" says not to do. Take the upstream side and re-file the step "Where stack development happens" says not to do. Take the upstream side and re-file the
change as an issue there. change as an issue there.
- **...but you changed how *your pages* are written?** That is not a stack change and the rule
above does not apply to it. Language, section headings, naming forms, tone, relationship
labels and the confidence rubric live in `kb/CONVENTIONS.md`, and each collection's authoring
rules in `kb/<name>/COLLECTION.md` - all under `kb/`, all yours, all restored by the merge
procedure rather than overwritten by it. If you find yourself editing `tools/` or `types/` to
change an authoring convention, that is a stack bug: file it, because the split exists
precisely so you do not have to.
## Scope ## Scope
+50 -16
View File
@@ -60,24 +60,58 @@ bereit für den ersten `Ingest`.
- Nicht genannt: lokal bleiben - dann braucht **jeder** spätere `tools/wikitool publish` - Nicht genannt: lokal bleiben - dann braucht **jeder** spätere `tools/wikitool publish`
ein `--no-push` (dessen Branch-Prüfung dabei ohnehin entfällt, siehe Schritt 2). ein `--no-push` (dessen Branch-Prüfung dabei ohnehin entfällt, siehe Schritt 2).
5. **Entscheidungspunkt - KB-Sprache.** Frage den Nutzer, in welcher Sprache die Seiten unter 5. **Entscheidungspunkt - Autorenkonventionen.** Die Distribution bringt keine ausgefüllten
`kb/` geschrieben werden sollen. Diese Instanz erbt aus dem Quell-Repo **Deutsch** - sowohl die Konventionen mit, sondern `kb/CONVENTIONS.md.template` und je Collection ein
Regel in [kb/CONTRACT.md](../kb/CONTRACT.md#language) als auch das Vokabular in `kb/<name>/COLLECTION.md.template`. Beide **binden**, sobald sie übernommen sind, und beide
[german-terminology.md](german-terminology.md) und die deutschen Abschnittsnamen in gehören dieser Instanz - deshalb liefert der Stack nur die Vorlage. Die eine Entscheidung
`tools/chemenu/sections.py`. Das ist eine Entscheidung der Ursprungsinstanz, keine dahinter ist: **in welcher Sprache und in welchem Ton schreibt diese Instanz ihre Seiten?**
Eigenschaft des Musters, und sie wird hier nicht stillschweigend weitergereicht.
- **Deutsch bestätigt:** nichts zu tun. Ablauf:
- **Andere Sprache:** *vor dem ersten Ingest* umstellen, denn danach ist es eine Migration
jeder vorhandenen Seite. Zu ändern sind der Abschnitt "Language" in `kb/CONTRACT.md`, die
Tonfall-Beispiele und Hedge-Wörter darunter, die vier Page-Type-Templates in `types/`, die
kanonischen Namen in `sections.py` (die bisherigen als Alias behalten) und die
Beziehungslabels in `kb/CONTRACT.md` § Linking. `german-terminology.md` wird dann ersetzt
oder gelöscht.
Unverändert bleibt in jedem Fall die eigentliche Regel: **jede Zeile einer Seite ist Prosa 1. Die Collection-Contracts übernehmen - vier Kopien, keine Frage an den Nutzer, denn was
oder Identifier, und nur Prosa wird übersetzt.** Titel, Wikilink-Ziele, Cite-IDs, Enum-Werte, dort steht ist unabhängig von der Sprache brauchbar:
Tags, Befehle und Pfade folgen keiner KB-Sprache.
```bash
for template in kb/*/COLLECTION.md.template; do
cp "$template" "${template%.template}"
done
```
Die `.template`-Dateien bleiben liegen; sie sind die Vorlage für den nächsten Export.
2. Den Nutzer nach der KB-Sprache fragen. `kb/CONVENTIONS.md.template` ist auf **Englisch**
voreingestellt; [kb-profiles.md](kb-profiles.md) hält daneben ein vollständiges
deutsches Profil bereit, und dessen Volltext ist die `kb/CONVENTIONS.md` des Quell-Repos.
Der Profilkatalog ist eine **Palette, kein Enum**: übernommen wird der Text *in* die
Instanzdatei, nicht ein Verweis auf den Katalog.
3. `kb/CONVENTIONS.md.template` nach `kb/CONVENTIONS.md` kopieren, entlang des gewählten
Profils ausfüllen - Sprache, Abschnittsnamen, Namensformen, Ton, Beziehungslabels,
Confidence-Rubrik - und dabei die Sentinel-Zeile (`wikitool:template-unfilled`) entfernen.
Die Platzhalter in geschweiften Klammern **sind** der Fragenkatalog.
4. Bei einer anderen Sprache als der des Quell-Repos: `german-terminology.md` löschen oder
durch das eigene Vokabular ersetzen - sie ist Material des deutschen Profils, nicht des
Stacks.
**Vor dem ersten Ingest entscheiden.** Die `sections:`-Namen in `kb/CONVENTIONS.md` sind die
Überschriften, die `xref` und `cite` in jede Seite schreiben; sie danach zu ändern ist eine
Migration jeder vorhandenen Seite (`section_aliases:` trägt die alten Namen, siehe
[migrate-corpus.md](migrate-corpus.md)).
**Nichts davon liegt unter `tools/` oder `types/`.** Der Compiler liest die Abschnittsnamen
aus `kb/CONVENTIONS.md`, und die vier Page-Type-Templates setzen sie über
`{section.…}`-Variablen ein - eine anderssprachige Instanz ändert dort keine Datei.
Unverändert bleibt in jedem Fall die Regel, die dem Stack gehört: **jede Zeile einer Seite
ist Prosa oder Identifier, und nur Prosa wird übersetzt** ([kb/CONTRACT.md § Language and
identifiers](../kb/CONTRACT.md#language-and-identifiers)). Titel, Wikilink-Ziele, Cite-IDs,
Enum-Werte, Tags, Befehle und Pfade folgen keiner KB-Sprache.
`tools/wikitool doctor` prüft das Ergebnis in Schritt 12 (`conventions`): eine fehlende
Datei ist ein `FAIL`, eine mit Sentinel oder ohne vollständigen `sections:`-Block ebenso.
`docs verify` prüft zusätzlich `profile:` und `required_by_stack:` auf jedem
`COLLECTION.md`.
6. **Entscheidungspunkt - Personalization.** Die Distribution bringt 6. **Entscheidungspunkt - Personalization.** Die Distribution bringt
`USER.md.template` und `SOUL.md.template` mit, aber keine ausgefüllten Fassungen: wer diese `USER.md.template` und `SOUL.md.template` mit, aber keine ausgefüllten Fassungen: wer diese
+6 -4
View File
@@ -61,8 +61,9 @@ pages should never have cost the concept contract. Field-level requirements alwa
article also pass `--set source_url=<upstream URL>`; `raw_files:` must still point at the article also pass `--set source_url=<upstream URL>`; `raw_files:` must still point at the
local copy. Then write the Summary / Key Takeaways / Action Items prose from step 4 - in the local copy. Then write the Summary / Key Takeaways / Action Items prose from step 4 - in the
KB language, whatever the source's own language is, quoting verbatim passages in the KB language, whatever the source's own language is, quoting verbatim passages in the
original. The rule and what is exempt from it: original. Which language that is: [kb/CONVENTIONS.md](../../kb/CONVENTIONS.md#language).
[kb/CONTRACT.md](../../kb/CONTRACT.md#language). What is exempt from it, in any language:
[kb/CONTRACT.md](../../kb/CONTRACT.md#language-and-identifiers).
Fill `## Not Extracted` in the same pass: what you read and deliberately did not promote, Fill `## Not Extracted` in the same pass: what you read and deliberately did not promote,
with the reason. Nothing in the repository can re-derive that judgment, and without it the with the reason. Nothing in the repository can re-derive that judgment, and without it the
@@ -70,8 +71,9 @@ pages should never have cost the concept contract. Field-level requirements alwa
6. **Create or update entity pages.** Read 6. **Create or update entity pages.** Read
[kb/entities/COLLECTION.md](../../kb/entities/COLLECTION.md) and [kb/entities/COLLECTION.md](../../kb/entities/COLLECTION.md) and
[kb/CONTRACT.md](../../kb/CONTRACT.md) first - the second is where tone, naming, provenance [kb/CONTRACT.md](../../kb/CONTRACT.md) plus
and citation are defined. [kb/CONVENTIONS.md](../../kb/CONVENTIONS.md) first - the second is where provenance and
citation are defined, the third where this instance's tone and naming forms are.
New: New:
+7 -5
View File
@@ -13,10 +13,12 @@ integrating into an existing one.
**Before the first `wikitool` call:** [session-setup.md](../session-setup.md). **Before the first `wikitool` call:** [session-setup.md](../session-setup.md).
**Read before drafting:** [kb/CONTRACT.md](../../kb/CONTRACT.md) - naming, tone, linking, **Read before drafting:** [kb/CONTRACT.md](../../kb/CONTRACT.md) - linking, provenance and the
provenance and confidence - together with the target collection's own `COLLECTION.md`, which confidence machinery, all of which the tool enforces - and
carries its quality goal and what is local to that subtree. Field-level requirements come from [kb/CONVENTIONS.md](../../kb/CONVENTIONS.md), which is where this instance's language, naming
`tools/wikitool types describe <type>`. forms, tone and relationship labels are, together with the target collection's own
`COLLECTION.md`, which carries its quality goal and what is local to that subtree. Field-level
requirements come from `tools/wikitool types describe <type>`.
## Creating a page ## Creating a page
@@ -46,7 +48,7 @@ carries its quality goal and what is local to that subtree. Field-level requirem
subjects - so the prose connects to existing pages instead of restating them. subjects - so the prose connects to existing pages instead of restating them.
5. **Draft.** Fill in the generated skeleton's TODO sections, following the tone rules in 5. **Draft.** Fill in the generated skeleton's TODO sections, following the tone rules in
[kb/CONTRACT.md](../../kb/CONTRACT.md#tone). If `provenance:` is `sourced` or `mixed`, cite [kb/CONVENTIONS.md](../../kb/CONVENTIONS.md#tone). If `provenance:` is `sourced` or `mixed`, cite
hard facts as you write them with `tools/wikitool cite add --page "<Title>" --source hard facts as you write them with `tools/wikitool cite add --page "<Title>" --source
"Source - X"`, which also adds `X` to `sources:` - paste the `[^cite-id]` marker it prints. "Source - X"`, which also adds `X` to `sources:` - paste the `[^cite-id]` marker it prints.
+74 -72
View File
@@ -7,12 +7,25 @@ material in `raw/`, and is expected to stay correct without being re-derived.
**Quality goal:** a page should answer a future question *without* re-reading the source it **Quality goal:** a page should answer a future question *without* re-reading the source it
came from. If answering still requires the raw file, the page is incomplete. came from. If answering still requires the raw file, the page is incomplete.
This file holds the rules that apply in **every** collection. Each `kb/<name>/COLLECTION.md` This file holds the rules that apply in **every** collection **and in every instance**. That
declares that it inherits them and adds only what is local to its own subtree - read this file second half is the cut: what is written here is enforced by `tools/wikitool` or follows from
together with the target collection's contract before writing or editing a page. how it works, so it is identical everywhere and `dist export` ships it verbatim.
**What an instance decides for itself is next door, in
[kb/CONVENTIONS.md](CONVENTIONS.md)** - the language pages are written in and its three
tool-owned section headings, the naming forms, the tone, the relationship-label vocabulary, the
confidence rubric. That file binds exactly as this one does; it is simply owned by the instance
rather than by the stack, so the distribution ships only its `.template` and the instance writes
the real one. Read both, plus the target collection's `kb/<name>/COLLECTION.md` (also
instance-owned), before writing or editing a page.
The split is by **who may change the sentence**, not by what it is about. Language, tone and
naming used to sit here, which meant every instance that answered "not German" to
`setup-instance.md` was locally editing a file the stack also ships - and a merge from upstream
would quietly hand it back.
Structural facts (which frontmatter fields exist, which are required, what the body skeleton Structural facts (which frontmatter fields exist, which are required, what the body skeleton
looks like) are *not* here - they belong to the type-specs and are printed by looks like) are in neither - they belong to the type-specs and are printed by
`tools/wikitool types describe <type>`. Never hand-write frontmatter; scaffold with `tools/wikitool types describe <type>`. Never hand-write frontmatter; scaffold with
`tools/wikitool new <type> --name "<Name>" --set field=value ...`. `tools/wikitool new <type> --name "<Name>" --set field=value ...`.
@@ -21,7 +34,15 @@ looks like) are *not* here - they belong to the type-specs and are printed by
`kb/` is a **namespace, not a collection**. It carries no `COLLECTION.md` of its own. `kb/` is a **namespace, not a collection**. It carries no `COLLECTION.md` of its own.
A directory under `kb/` is a **collection** exactly when it contains a `COLLECTION.md`. That A directory under `kb/` is a **collection** exactly when it contains a `COLLECTION.md`. That
file is the local authoring contract for every page in the subtree. file is the local authoring contract for every page in the subtree, and it belongs to the
instance: it declares in its frontmatter which profile from
[instructions/kb-profiles.md](../instructions/kb-profiles.md) it adopted, and whether the stack
resolves against it by name.
| Field | Means |
|---|---|
| `profile:` | Which catalogue entry this contract started from, or `none`. Free text - the catalogue is a palette, not an enum, and a collection an instance invented has no entry to name |
| `required_by_stack:` | Whether `wikitool` itself depends on this collection *by name*. Not the instance's to choose: `docs verify` checks it against the stack's own list. `kb/sources/` is `true` - `sources coverage`, `[^cite-id]` resolution and `kb/provenance.md` all resolve against that name - and everything else is `false` |
- A subdirectory *inside* a collection is an **area**. It inherits the enclosing contract and - A subdirectory *inside* a collection is an **area**. It inherits the enclosing contract and
must not carry a `COLLECTION.md` of its own - `kb/entities/systems/` is an area of must not carry a `COLLECTION.md` of its own - `kb/entities/systems/` is an area of
@@ -40,9 +61,11 @@ file is the local authoring contract for every page in the subtree.
| `kb/sources/` | One summary page per ingested source, carrying its `raw_files:` provenance | [sources/COLLECTION.md](sources/COLLECTION.md) | | `kb/sources/` | One summary page per ingested source, carrying its `raw_files:` provenance | [sources/COLLECTION.md](sources/COLLECTION.md) |
| `kb/comparisons/` | Structured comparisons of two or more existing pages | [comparisons/COLLECTION.md](comparisons/COLLECTION.md) | | `kb/comparisons/` | Structured comparisons of two or more existing pages | [comparisons/COLLECTION.md](comparisons/COLLECTION.md) |
**Adding a collection:** `mkdir kb/<name>` and write a `kb/<name>/COLLECTION.md`. Collections The four rows above are this instance's collections, not a fixed set. **Adding one:**
`mkdir kb/<name>` and write a `kb/<name>/COLLECTION.md` with the two fields above. Collections
are discovered by contract presence, so no code change is needed. A collection only becomes are discovered by contract presence, so no code change is needed. A collection only becomes
*writable* once some type-spec declares a matching `base_dir:`. *writable* once some type-spec declares a matching `base_dir:`. Renaming or dropping one is the
instance's call too - except where `required_by_stack: true` says otherwise.
**Where a page goes** is decided by its type-spec, never by hand - see **Where a page goes** is decided by its type-spec, never by hand - see
[types/type-spec.md](../types/type-spec.md). [types/type-spec.md](../types/type-spec.md).
@@ -61,18 +84,16 @@ Never hand-edit these; they are produced by `tools/wikitool`:
To *find* a page, search rather than read the catalog: `tools/wikitool search "<text>"`, or To *find* a page, search rather than read the catalog: `tools/wikitool search "<text>"`, or
`tools/wikitool search --field <predicate>` for a structured query over frontmatter. `tools/wikitool search --field <predicate>` for a structured query over frontmatter.
## Naming ## Titles are identifiers
- Human-readable titles with spaces: `Hybrid Search.md`, `Gitea Actions.md` - not kebab-case. **The filename stem *is* the page title, and `[[wikilinks]]` must match it exactly.** That is
- Singular for entities: `ha-core.md`, not `ha-cores.md`. not a naming preference; it is the wiki's only way to address a page. `wikitool lint` reports an
- Comparison pages read as a comparison: `Go vs Rust.md`. H1 that stops matching its title, `rename`/`rm` rewrite every reference to a stem, and a
- ADRs are prefixed: `adr-001-use-go-modules.md`. `[^cite-id]` resolves through one.
- The filename stem *is* the page title, and `[[wikilinks]]` must match it exactly.
- Prefer readability over convention when the two conflict.
What to name a thing: projects use their repository or common name; systems a descriptive Which *form* those titles take - spaces or kebab-case, singular or plural, what prefixes a
name; tools the tool's own name; technologies their standard spelling and capitalization; decision record - is the instance's, in
people a full name or common handle. [kb/CONVENTIONS.md § Naming](CONVENTIONS.md#naming).
## Every page should ## Every page should
@@ -83,31 +104,20 @@ people a full name or common handle.
- [ ] Duplicate no existing page - [ ] Duplicate no existing page
- [ ] Appear in the catalog (guaranteed by `wikitool index rebuild`) - [ ] Appear in the catalog (guaranteed by `wikitool index rebuild`)
## Tone ## Quotation cap
Wikipedia style: factual, neutral, specific. At most 2 blockquoted lines per page. `wikitool lint` reports overages as advisory, since
exceeding the cap can be a legitimate judgment call - but the page should carry the knowledge
itself, not delegate it to quotations. The cap is about how much of the page you let quotes
carry; it does not apply to text you are citing verbatim from a source.
- No buzzwords ("bahnbrechend", "hochmodern", "leistungsstark", "revolutioniert"). The register those lines are written in - what counts as a buzzword, what filler is refused -
- No AI filler ("es sei angemerkt", "es ist wichtig zu betonen", "in der heutigen Zeit"). is the instance's, in [kb/CONVENTIONS.md § Tone](CONVENTIONS.md#tone).
- No em-dash asides carrying parenthetical reasoning.
- At most 2 blockquoted lines per page. `wikitool lint` reports overages as advisory, since
exceeding the cap can be a legitimate judgment call - but the page should carry the
knowledge itself, not delegate it to quotations. The cap is about how much of the page you
let quotes carry; it does not apply to text you are citing verbatim from a source.
Good: "MQTT ist ein leichtgewichtiges Publish-Subscribe-Protokoll für Geräte mit knappen ## Language and identifiers
Ressourcen."
Bad: "MQTT ist ein bahnbrechendes, hochmodernes Protokoll, das die IoT-Kommunikation *Which* language pages are written in is [kb/CONVENTIONS.md](CONVENTIONS.md)'s to say. What
revolutioniert - und es sei angemerkt, dass es ein Publish-Subscribe-Muster verwendet." follows here is the part that is not a choice, because the tool resolves against it.
## Language
Pages are written in **German**. This binds `kb/` and the authoring surface that shapes it -
the page type-specs `types/entity.md`, `types/concept.md`, `types/source.md` and
`types/comparison.md`. `raw/` is untouched ([raw/CONTRACT.md](../raw/CONTRACT.md)), and the
control plane stays English: AGENTS.md, the stage contracts including this one, `instructions/`,
and the type-specs for non-page artifacts.
Every line of a page is either **prose** or an **identifier**. Only prose is translated. Every line of a page is either **prose** or an **identifier**. Only prose is translated.
@@ -118,20 +128,14 @@ source page's Summary / Key Takeaways / Action Items / Not Extracted, and `summa
| Identifier | Why | | Identifier | Why |
|---|---| |---|---|
| Page titles, and the H1 that repeats one | A title is the wiki's only identifier for a page and follows the subject's own established name - see [Naming](#naming). `wikitool lint` reports an H1 that stops matching its title | | Page titles, and the H1 that repeats one | A title is the wiki's only identifier for a page and follows the subject's own established name - see [Titles are identifiers](#titles-are-identifiers). `wikitool lint` reports an H1 that stops matching its title |
| The subtype value on the generated `**Typ:**` line | It renders a schema enum value (`technology`, `workflow`), which `search --field` filters on. The label is prose; the value is not | | The subtype value on the generated `**Typ:**` line | It renders a schema enum value (`technology`, `workflow`), which `search --field` filters on. The label is prose; the value is not |
| `tags:` | Search keys, not prose | | `tags:` | Search keys, not prose |
| Commands, paths, config keys, hostnames, code | They are what they are | | Commands, paths, config keys, hostnames, code | They are what they are |
| Quotations | Quoted verbatim in the source's own language | | Quotations | Quoted verbatim in the source's own language |
Established English technical terms stay English inside German prose - "GitOps", "Ownership Which foreign technical terms stay untranslated inside that prose is a judgment call the
Model", "Reverse Proxy", "Pull Request". Translate a term only where the German one is genuinely instance records - see [kb/CONVENTIONS.md § Language](CONVENTIONS.md#language).
the more common usage. A coined German equivalent nobody else writes makes the page harder to
find, not more idiomatic.
Which terms those are, which have a settled German form, and the register the prose is written in:
[instructions/german-terminology.md](../instructions/german-terminology.md). It is lookup material,
not a second rule - every entry in it is a decision that was made wrong once first.
**A source in another language** is still summarized in the KB language: a source page is **A source in another language** is still summarized in the KB language: a source page is
evidence *about* a source, not a substitute for it. Quote verbatim in the original language and evidence *about* a source, not a substitute for it. Quote verbatim in the original language and
@@ -141,14 +145,18 @@ record the raw file's language in `source_language:`.
Three headings are a vocabulary the tool owns rather than prose an author picks: `xref add` Three headings are a vocabulary the tool owns rather than prose an author picks: `xref add`
writes into Relationships and See Also, and `cite add` owns the trailing Footnotes block. They writes into Relationships and See Also, and `cite add` owns the trailing Footnotes block. They
follow the KB language like everything else - `## Beziehungen`, `## Siehe auch`, `## Fußnoten` - follow the KB language like everything else, so **the instance names them**, in
and `tools/chemenu/sections.py` is the single place naming them. `kb/CONVENTIONS.md`'s `sections:` frontmatter. `tools/chemenu/conventions.py` reads that
declaration and `tools/chemenu/sections.py` is what the rest of the compiler asks - there is no
heading text in the compiler itself.
Each has aliases the tool still *recognizes* but no longer writes, which is what lets the corpus Each has aliases the tool still *recognizes* but no longer writes, which is what lets the corpus
be translated page by page: a page still carrying `## Relationships` is found and appended to be translated page by page: a page still carrying `## Relationships` is found and appended to
correctly, and `cite sync` leaves an untranslated `## Footnotes` heading alone rather than correctly, and `cite sync` leaves an untranslated `## Footnotes` heading alone rather than
retitling it. Renaming a heading is the translation pass's job, never a side effect of another retitling it. The recognized set is the canonical name, any `section_aliases:` the instance
command. Any *other* heading an author adds is ordinary prose and is translated with the rest. declared, and the names this stack wrote before the declaration existed. Renaming a heading is
the translation pass's job, never a side effect of another command. Any *other* heading an
author adds is ordinary prose and is translated with the rest.
## Linking ## Linking
@@ -156,14 +164,10 @@ Every page links to what it mentions, in both directions. Cross-references are c
`tools/wikitool xref add --a "<A>" --b "<B>" --rel-a "<label>" --rel-b "<label>"`, never by `tools/wikitool xref add --a "<A>" --b "<B>" --rel-a "<label>" --rel-b "<label>"`, never by
hand-editing the `related:` array or the Relationships/See Also bullets. hand-editing the `related:` array or the Relationships/See Also bullets.
Use a typed relationship label rather than a generic one: Use a typed relationship label rather than a generic one. The label is free text as far as the
tool is concerned - it is written into a `- **label:** [[Title]]` bullet and no code matches on
`hängt ab von` · `verwendet` · `implementiert` · `erweitert` · `ersetzt` · `steht in Konflikt mit` it - so which vocabulary this instance uses is
· `benötigt` · `erzeugt` · `konsumiert` · `besitzt` · `pflegt` · `läuft auf` · `verwandt mit` [kb/CONVENTIONS.md § Relationship labels](CONVENTIONS.md#relationship-labels)'s to list.
(last resort)
The labels are prose written into a `- **label:** [[Title]]` bullet; no code matches on them, so
an untranslated page's English label is stale wording, not a broken reference.
A page is expected to have at least one inbound link; `wikitool lint` reports orphans. A page is expected to have at least one inbound link; `wikitool lint` reports orphans.
Comparison pages are exempt - they are reached through the catalog. Comparison pages are exempt - they are reached through the catalog.
@@ -187,15 +191,16 @@ Every claim is either traceable to a raw file or explicitly marked as not.
command, or config value. `tools/wikitool cite add --page "<Title>" --source "Source - X" command, or config value. `tools/wikitool cite add --page "<Title>" --source "Source - X"
[--file <qualifier>]` mints the id, upserts its `[[Source - X]]` (or [--file <qualifier>]` mints the id, upserts its `[[Source - X]]` (or
`[[Source - X|storage-model.md]]` for a multi-file source) definition in the page's trailing `[[Source - X|storage-model.md]]` for a multi-file source) definition in the page's trailing
`## Footnotes` block, and adds `Source - X` to `sources:` - it prints the marker to paste at Footnotes block (named per [Section headings](#section-headings)), and adds `Source - X` to
`sources:` - it prints the marker to paste at
the fact; placing it is still manual. Never hand-type a cite-id (AGENTS.md invariant 1). This the fact; placing it is still manual. Never hand-type a cite-id (AGENTS.md invariant 1). This
differs from a plain `[[Source - X]]` link, which only means "related to". differs from a plain `[[Source - X]]` link, which only means "related to".
- **Notation inside code is notation, not a reference.** A `[^cite-id]` or a `[[wikilink]]` - **Notation inside code is notation, not a reference.** A `[^cite-id]` or a `[[wikilink]]`
written in backticks or a fenced block is read as an example: the citation does not count and written in backticks or a fenced block is read as an example: the citation does not count and
the link does not exist. That is what lets a page document this stack's own syntax. It also the link does not exist. That is what lets a page document this stack's own syntax. It also
means a marker appended to a line *inside* a fence cites nothing - put it on a means a marker appended to a line *inside* a fence cites nothing - put it on a source line
`Quelle: [^cite-id]` line under the block, where it renders as a footnote instead of under the block (`<source-word>: [^cite-id]`, in the KB language), where it renders as a
travelling with the command when someone copies it. footnote instead of travelling with the command when someone copies it.
- A source cited inline must also appear in the page's frontmatter `sources:` list; - A source cited inline must also appear in the page's frontmatter `sources:` list;
`wikitool lint` checks this in both directions, and hard-errors on a leftover pre-migration `wikitool lint` checks this in both directions, and hard-errors on a leftover pre-migration
`^[[...]]` marker, an undefined `[^cite-id]` reference, or an orphaned Footnotes definition. `^[[...]]` marker, an undefined `[^cite-id]` reference, or an orphaned Footnotes definition.
@@ -215,23 +220,20 @@ one - and never file the synthesized version back into the wiki.
`confidence` is *derived* from it by `tools/wikitool confidence decay` and must never be `confidence` is *derived* from it by `tools/wikitool confidence decay` and must never be
edited directly. edited directly.
Base score for a single source is 0.5, adjusted by:
- **+0.2 per supporting source** (max +0.6)
- **+0.2** if confirmed <30 days ago, **+0.1** if <90 days
- **+0.1** for official documentation, **+0.05** for a reputable secondary source
- **+0.1** if multiple independent sources agree
Re-assess a page with `tools/wikitool touch --page "<Title>" --confidence-base <value>`. Re-assess a page with `tools/wikitool touch --page "<Title>" --confidence-base <value>`.
In prose, hedge according to the score: below 0.6 write "möglicherweise"/"kann"; below 0.4 What the number *means* - the base score, what raises it and by how much, and how to hedge in
write "unsicher"/"unbestätigt". prose below a threshold - is a rubric rather than a mechanism, so it is
[kb/CONVENTIONS.md § Confidence rubric](CONVENTIONS.md#confidence-rubric)'s.
## What does not belong here ## What does not belong here
- Raw source material - it stays immutable under `raw/`. - Raw source material - it stays immutable under `raw/`.
- Type definitions, frontmatter contracts, or templates - those live in `types/`. - Type definitions, frontmatter contracts, or templates - those live in `types/`.
- Procedures for operating the tooling - those live in `instructions/`. - Procedures for operating the tooling - those live in `instructions/`.
- **Anything an instance would have to rewrite for itself** - language, naming forms, tone,
relationship labels, the confidence rubric. Those are `kb/CONVENTIONS.md`'s, and a sentence
of that kind here is a sentence the stack ships over the instance's own answer.
- Rules that apply to only one collection - those belong in that collection's - Rules that apply to only one collection - those belong in that collection's
`COLLECTION.md`. `COLLECTION.md`.
- Hand-edited generated files - see [Generated files](#generated-files). - Hand-edited generated files - see [Generated files](#generated-files).
+126
View File
@@ -0,0 +1,126 @@
---
language: de
profile: german
sections:
relationships: Beziehungen
see_also: Siehe auch
footnotes: Fußnoten
---
# kb/ - Authoring Conventions of This Instance
The decisions [kb/CONTRACT.md](CONTRACT.md) deliberately does not make. The contract holds what
the code enforces and is identical in every instance; this file holds what *this* instance
chose, and no other instance has to agree with a word of it.
**It binds all the same.** Everything below applies to every page under `kb/`, exactly as the
contract does. The difference is ownership, not authority: a rule here is changed by editing
this file, a rule there by changing the stack.
Adopted from the `german` profile in
[instructions/kb-profiles.md](../instructions/kb-profiles.md). That catalogue is a palette, not
an enum - what is written here is what holds, whether or not a profile says the same thing.
The frontmatter above is the one machine-read part. `sections:` names the three headings
`wikitool xref` and `wikitool cite` write into; `tools/chemenu/conventions.py` reads them and
`tools/chemenu/sections.py` is what the rest of the compiler asks. Renaming one here changes
what the tool *writes*; what it still *recognizes* is the union of that name, any
`section_aliases:` declared beside it, and the names this stack wrote before this file existed.
That asymmetry is the translation path: a page keeps working under its old heading until it is
itself translated.
## Language
Pages are written in **German**. This binds `kb/` and the authoring surface that shapes it -
the page type-specs `types/entity.md`, `types/concept.md`, `types/source.md` and
`types/comparison.md`. `raw/` is untouched ([raw/CONTRACT.md](../raw/CONTRACT.md)), and the
control plane stays English: `AGENTS.md`, the stage contracts, this file, `instructions/`, and
the type-specs for non-page artifacts.
Which line is prose and which is an identifier - and therefore what is translated at all - is
the contract's rule, not this file's: see
[kb/CONTRACT.md § Language and identifiers](CONTRACT.md#language-and-identifiers).
Established English technical terms stay English inside German prose - "GitOps", "Ownership
Model", "Reverse Proxy", "Pull Request". Translate a term only where the German one is genuinely
the more common usage. A coined German equivalent nobody else writes makes the page harder to
find, not more idiomatic.
Which terms those are, which have a settled German form, and the register the prose is written
in: [instructions/german-terminology.md](../instructions/german-terminology.md). It is lookup
material, not a second rule - every entry in it is a decision that was made wrong once first.
### Section headings
The canonical names are the frontmatter's: `## Beziehungen`, `## Siehe auch`, `## Fußnoten`.
The English forms this stack wrote before the corpus was translated are still recognized, so a
page carrying `## Relationships` is found and appended to correctly and `cite sync` leaves an
untranslated `## Footnotes` alone. Renaming such a heading is the translation pass's job, never
a side effect of another command. Any *other* heading an author adds is ordinary prose and is
translated with the rest.
## Naming
- Human-readable titles with spaces: `Hybrid Search.md`, `Gitea Actions.md` - not kebab-case.
- Singular for entities: `ha-core.md`, not `ha-cores.md`.
- Comparison pages read as a comparison: `Go vs Rust.md`.
- ADRs are prefixed: `adr-001-use-go-modules.md`.
- Prefer readability over convention when the two conflict.
What to name a thing: projects use their repository or common name; systems a descriptive
name; tools the tool's own name; technologies their standard spelling and capitalization;
people a full name or common handle.
The one naming fact that is *not* a choice, and therefore lives in the contract: the filename
stem is the page title, and `[[wikilinks]]` must match it exactly.
## Tone
Wikipedia style: factual, neutral, specific.
- No buzzwords ("bahnbrechend", "hochmodern", "leistungsstark", "revolutioniert").
- No AI filler ("es sei angemerkt", "es ist wichtig zu betonen", "in der heutigen Zeit").
- No em-dash asides carrying parenthetical reasoning.
Good: "MQTT ist ein leichtgewichtiges Publish-Subscribe-Protokoll für Geräte mit knappen
Ressourcen."
Bad: "MQTT ist ein bahnbrechendes, hochmodernes Protokoll, das die IoT-Kommunikation
revolutioniert - und es sei angemerkt, dass es ein Publish-Subscribe-Muster verwendet."
The blockquote cap is not here: `wikitool lint` reports it, so it is the contract's.
## Relationship labels
`tools/wikitool xref add --rel-a/--rel-b` takes a free-text label. This instance uses a typed
one rather than a generic one:
`hängt ab von` · `verwendet` · `implementiert` · `erweitert` · `ersetzt` · `steht in Konflikt mit`
· `benötigt` · `erzeugt` · `konsumiert` · `besitzt` · `pflegt` · `läuft auf` · `verwandt mit`
(last resort)
The labels are prose written into a `- **label:** [[Title]]` bullet; no code matches on them, so
an untranslated page's English label is stale wording, not a broken reference.
## Confidence rubric
`confidence_base` is set by hand and `confidence` is derived from it - that mechanism is the
contract's. What the number *means* is this instance's:
Base score for a single source is 0.5, adjusted by:
- **+0.2 per supporting source** (max +0.6)
- **+0.2** if confirmed <30 days ago, **+0.1** if <90 days
- **+0.1** for official documentation, **+0.05** for a reputable secondary source
- **+0.1** if multiple independent sources agree
In prose, hedge according to the score: below 0.6 write "möglicherweise"/"kann"; below 0.4
write "unsicher"/"unbestätigt".
## Keeping this file honest
Change it when a convention actually changes, and treat a change to `sections:` as a corpus
migration rather than an edit: existing pages keep their old headings until something translates
them, and the alias list is what carries them in the meantime. `wikitool doctor` FAILs on a
missing or unfilled file, and `wikitool docs verify` refuses a `sections:` block that does not
name all three slots.
+104
View File
@@ -0,0 +1,104 @@
---
# wikitool:template-unfilled - delete this line once the file is answered.
language: en
profile: none
sections:
relationships: Relationships
see_also: See Also
footnotes: Footnotes
# Headings this instance no longer writes but still recognizes, so a corpus can
# be translated page by page instead of all at once. Optional; the names this
# stack wrote before this file existed are always recognized anyway.
# section_aliases:
# relationships: [Beziehungen]
# see_also: [Siehe auch]
# footnotes: [Fußnoten]
---
# kb/ - Authoring Conventions of This Instance
The decisions [kb/CONTRACT.md](CONTRACT.md) deliberately does not make. The contract holds what
the code enforces and is identical in every instance; this file holds what *this* instance
chooses, and no other instance has to agree with a word of it.
**It binds all the same.** Everything below applies to every page under `kb/`, exactly as the
contract does. The difference is ownership, not authority: a rule here is changed by editing
this file, a rule there by changing the stack.
Ready-made answers to every section below - including a complete German profile - are in
[instructions/kb-profiles.md](../instructions/kb-profiles.md). That catalogue is a palette, not
an enum: adopt an entry, adapt it, or write your own. What is written *here* is what holds.
The frontmatter above is the one machine-read part. `sections:` names the three headings
`wikitool xref` and `wikitool cite` write into. Set them before the first page is written:
afterwards, changing one is a corpus migration rather than an edit.
## Language
Pages are written in **{language}**. This binds `kb/` and the authoring surface that shapes it -
the page type-specs `types/entity.md`, `types/concept.md`, `types/source.md` and
`types/comparison.md`, whose `## Template` blocks are the body skeleton every new page starts
from. `raw/` is untouched ([raw/CONTRACT.md](../raw/CONTRACT.md)), and the control plane stays
English: `AGENTS.md`, the stage contracts, this file, `instructions/`, and the type-specs for
non-page artifacts.
Which line is prose and which is an identifier - and therefore what is translated at all - is
the contract's rule, not this file's: see
[kb/CONTRACT.md § Language and identifiers](CONTRACT.md#language-and-identifiers).
{Which established foreign-language technical terms stay untranslated inside this instance's
prose, and where the vocabulary for that is looked up. Delete this paragraph if the KB language
is the one those terms are already in.}
### Section headings
The canonical names are the frontmatter's. Any name this instance previously wrote stays
recognized through `section_aliases:`, which is what lets a corpus be translated page by page.
Renaming such a heading is the translation pass's job, never a side effect of another command.
Any *other* heading an author adds is ordinary prose.
## Naming
- {Title form - words and spaces, or kebab-case, or the subject's own spelling.}
- {Singular or plural for entities.}
- {How a comparison page's title reads.}
- {The ADR prefix, if this instance files decisions as pages.}
- {What to name a thing: projects, systems, tools, technologies, people.}
The one naming fact that is *not* a choice, and therefore lives in the contract: the filename
stem is the page title, and `[[wikilinks]]` must match it exactly.
## Tone
{The register pages are written in, in one line.}
- {Words and constructions this instance refuses, with examples in the KB language.}
Good: {one sentence that is what this instance wants.}
Bad: {the same sentence written the way it must not be.}
## Relationship labels
`tools/wikitool xref add --rel-a/--rel-b` takes a free-text label. Listing the ones this
instance uses is what keeps a graph typed rather than a wiki full of "related to":
{the label vocabulary, in the KB language}
No code matches on these, so an old label on an untranslated page is stale wording, not a
broken reference.
## Confidence rubric
`confidence_base` is set by hand and `confidence` is derived from it - that mechanism is the
contract's. What the number *means* is this instance's:
{the base score, what raises it, and by how much}
{How to hedge in prose at a low score, in the KB language.}
## Keeping this file honest
Change it when a convention actually changes, and treat a change to `sections:` as a corpus
migration rather than an edit. `wikitool doctor` FAILs on a missing or unfilled file, and
`wikitool docs verify` refuses a `sections:` block that does not name all three slots.
+12 -4
View File
@@ -1,3 +1,8 @@
---
profile: comparisons
required_by_stack: false
---
# kb/comparisons/ - Collection Contract # kb/comparisons/ - Collection Contract
Structured head-to-head evaluations of two or more things that already have pages here. A Structured head-to-head evaluations of two or more things that already have pages here. A
@@ -7,8 +12,10 @@ comparison exists so that neither subject's own page has to argue against the ot
That needs named, checkable dimensions and a stated trade-off; a page that lists differences That needs named, checkable dimensions and a stated trade-off; a page that lists differences
without saying what they cost has described, not compared. without saying what they cost has described, not compared.
Inherits [kb/CONTRACT.md](../CONTRACT.md) - naming, tone, linking, provenance and confidence Inherits [kb/CONTRACT.md](../CONTRACT.md) for the rules the stack enforces - linking mechanics,
are defined there and are not restated here. provenance, citation, the confidence machinery - and
[kb/CONVENTIONS.md](../CONVENTIONS.md) for what this instance decided: language, naming forms,
tone, relationship labels, the confidence rubric. Neither is restated here.
## Types offered ## Types offered
@@ -16,8 +23,9 @@ are defined there and are not restated here.
## Naming ## Naming
The title reads as a comparison: `Go vs Rust.md`, `Traefik vs nginx.md`. Order the subjects as The title form is [kb/CONVENTIONS.md § Naming](../CONVENTIONS.md#naming)'s. What is local here
they are most commonly spoken, not alphabetically. is the ordering: name the subjects as they are most commonly spoken together, not
alphabetically.
## Requirements ## Requirements
+11 -4
View File
@@ -1,3 +1,8 @@
---
profile: concepts
required_by_stack: false
---
# kb/concepts/ - Collection Contract # kb/concepts/ - Collection Contract
Ideas rather than things: architectures, patterns, protocols, workflows, recurring problems, Ideas rather than things: architectures, patterns, protocols, workflows, recurring problems,
@@ -8,8 +13,10 @@ records *what*.
without the reader having to open the entity pages that use it. If the explanation only makes without the reader having to open the entity pages that use it. If the explanation only makes
sense once you already know the system, it is on the wrong page. sense once you already know the system, it is on the wrong page.
Inherits [kb/CONTRACT.md](../CONTRACT.md) - naming, tone, linking, provenance and confidence Inherits [kb/CONTRACT.md](../CONTRACT.md) for the rules the stack enforces - linking mechanics,
are defined there and are not restated here. provenance, citation, the confidence machinery - and
[kb/CONVENTIONS.md](../CONVENTIONS.md) for what this instance decided: language, naming forms,
tone, relationship labels, the confidence rubric. Neither is restated here.
## Types offered ## Types offered
@@ -17,8 +24,8 @@ are defined there and are not restated here.
## Decisions and ADRs ## Decisions and ADRs
An architectural decision is a concept page prefixed `adr-NNN-`, e.g. An architectural decision is a concept page, prefixed as
`adr-001-use-go-modules.md`. It records: [kb/CONVENTIONS.md § Naming](../CONVENTIONS.md#naming) says. It records:
- **Context** - what forced a decision. - **Context** - what forced a decision.
- **Decision** - what was chosen. - **Decision** - what was chosen.
+1 -1
View File
@@ -6,7 +6,7 @@ created: 2026-09-01
modified: 2026-09-01 modified: 2026-09-01
related: [Chemenu] related: [Chemenu]
sources: ['Source - Public Release, Corpus Purge and History Squash Session 2026-09-01', Source - Private-Instance Merge Correction and Issue 30 Session 2026-09-01] sources: ['Source - Public Release, Corpus Purge and History Squash Session 2026-09-01', Source - Private-Instance Merge Correction and Issue 30 Session 2026-09-01]
confidence: 0.90 confidence: 0.70
confidence_base: 0.70 confidence_base: 0.70
provenance: sourced provenance: sourced
summary: 'Private Korpusinhalte per Loeschung entfernen statt zu anonymisieren: ein Seitentitel ist der einzige Identifier eines Wikis, Umbenennen ist die volle page-lifecycle-Prozedur je Seite, Loeschen ist ein unterstuetztes Kommando.' summary: 'Private Korpusinhalte per Loeschung entfernen statt zu anonymisieren: ein Seitentitel ist der einzige Identifier eines Wikis, Umbenennen ist die volle page-lifecycle-Prozedur je Seite, Loeschen ist ein unterstuetztes Kommando.'
+1 -1
View File
@@ -6,7 +6,7 @@ created: 2026-09-01
modified: 2026-09-01 modified: 2026-09-01
related: [Chemenu] related: [Chemenu]
sources: ['Source - Public Release, Corpus Purge and History Squash Session 2026-09-01'] sources: ['Source - Public Release, Corpus Purge and History Squash Session 2026-09-01']
confidence: 0.50 confidence: 0.70
confidence_base: 0.70 confidence_base: 0.70
provenance: sourced provenance: sourced
summary: Ein Repo mit Code- und Inhaltsanteil erhaelt zwei Lizenzen; die Grenze zwischen ihnen ist kein zweiter, gepflegter Pfadkatalog, sondern der ohnehin vorhandene Dateiplan des Distributionswerkzeugs. summary: Ein Repo mit Code- und Inhaltsanteil erhaelt zwei Lizenzen; die Grenze zwischen ihnen ist kein zweiter, gepflegter Pfadkatalog, sondern der ohnehin vorhandene Dateiplan des Distributionswerkzeugs.
+5 -4
View File
@@ -2,7 +2,7 @@
# kb/concepts/ - Index # kb/concepts/ - Index
79 page(s). Regenerated by `wikitool index rebuild`. 80 page(s). Regenerated by `wikitool index rebuild`.
## All ## All
@@ -42,14 +42,15 @@
| [[Implementation Spectrum]] | architecture | Modularer Einführungspfad für die Funktionen von LLM Wiki v2, vom minimal tragfähigen Wiki bis zur vollen Umsetzung mit Automatisierung und Governance. | 2026-08-29 | | [[Implementation Spectrum]] | architecture | Modularer Einführungspfad für die Funktionen von LLM Wiki v2, vom minimal tragfähigen Wiki bis zur vollen Umsetzung mit Automatisierung und Governance. | 2026-08-29 |
| [[Index Scaling]] | workflow | Skalierungsregeln für Indexseiten: Tabellenabschnitte ab 50 Einträgen teilen, ab 200 Seiten _meta/topic-map.md anlegen | 2026-08-29 | | [[Index Scaling]] | workflow | Skalierungsregeln für Indexseiten: Tabellenabschnitte ab 50 Einträgen teilen, ab 200 Seiten _meta/topic-map.md anlegen | 2026-08-29 |
| [[Issue Label Scheme]] | decision | Zweiachsiges Pflicht-Labelschema fuer das Gitea-Board: prio/1..3 und size/XS..L, bewusst keine dritte Achse; die Regel liegt in instructions/dev/, weil sie keine ausgelieferte Instanz erreichen darf | 2026-08-31 | | [[Issue Label Scheme]] | decision | Zweiachsiges Pflicht-Labelschema fuer das Gitea-Board: prio/1..3 und size/XS..L, bewusst keine dritte Achse; die Regel liegt in instructions/dev/, weil sie keine ausgelieferte Instanz erreichen darf | 2026-08-31 |
| [[Iteration and Cost Limits]] | workflow | Im Code durchgesetzte Obergrenze von 60 wikitool-Aufrufen je Session\, Loop-Breaker bei 3 identischen Wiederholungen\, Slot-Erstattung bei abgelehntem Aufruf und ein seit 1.5.0 gemessenes Kalibrierungsband von 5-15 (einfach) bzw. 20-35 (komplex) Aufrufen | 2026-08-31 | | [[Iteration and Cost Limits]] | workflow | Im Code durchgesetzte Obergrenze von 60 wikitool-Aufrufen je Session, Loop-Breaker bei 3 identischen Wiederholungen, Slot-Erstattung, ein gemessenes Kalibrierungsband, und Retrieval sowie der MCP-Leseserver bleiben ausgenommen | 2026-09-02 |
| [[KB Migration]] | workflow | Migration des KB-Inhalts entlang einer geordneten Versionskette; abgegrenzt gegen offene Instanz-Aktionen, die in den doctor-Check gehoeren statt in die Kette | 2026-08-31 | | [[KB Migration]] | workflow | Migration des KB-Inhalts entlang einer geordneten Versionskette; abgegrenzt gegen offene Instanz-Aktionen, die in den doctor-Check gehoeren statt in die Kette | 2026-08-31 |
| [[KB Stack Versioning]] | decision | Semantische Versionierung des Wiki-Stacks: VERSION beschreibt die Maschinerie, Kompatibilitaet ist die linkeste Nicht-Null-Komponente, und drei getrennte Dateien trennen Maschinerie, Herkunft und Content-Form | 2026-08-30 | | [[KB Stack Versioning]] | decision | Semantische Versionierung des Wiki-Stacks: VERSION beschreibt die Maschinerie, Kompatibilitaet (Drop-in-Ersatz) und Inhaltsmigration sind seit 2.5.0 getrennte, unabhaengig geprueft Fragen | 2026-09-02 |
| [[Knowledge Compounding]] | workflow | Effekt, bei dem Wissen im Wiki an Wert gewinnt, weil jede neue Quelle an bestehende, untereinander verwiesene Seiten anknüpft und sie ergänzt. | 2026-08-29 | | [[Knowledge Compounding]] | workflow | Effekt, bei dem Wissen im Wiki an Wert gewinnt, weil jede neue Quelle an bestehende, untereinander verwiesene Seiten anknüpft und sie ergänzt. | 2026-08-29 |
| [[Knowledge Graph]] | architecture | Typisierte Schicht aus Entities und Beziehungen über den Wiki-Seiten, die eine reichere Wissensdarstellung und graphbasierte Abfragen ermöglicht. | 2026-08-29 | | [[Knowledge Graph]] | architecture | Typisierte Schicht aus Entities und Beziehungen über den Wiki-Seiten, die eine reichere Wissensdarstellung und graphbasierte Abfragen ermöglicht. | 2026-08-29 |
| [[Lint Workflow]] | workflow | Deterministischer Health-Check rund um wikitool lint; seit 1.7.2 maskiert es Code vor dem Notation-Match und zaehlt Zitat-Bloecke statt Zeilen | 2026-09-01 | | [[Lint Workflow]] | workflow | Deterministischer Health-Check rund um wikitool lint; seit 1.7.2 maskiert es Code vor dem Notation-Match und zaehlt Zitat-Bloecke statt Zeilen | 2026-09-01 |
| [[LLM Wiki Pattern]] | architecture | Methodik für persönliches Wissensmanagement, bei der ein LLM aus Rohquellen ein dauerhaftes Wiki aufbaut - Wissen wird kompiliert statt per RAG neu hergeleitet. | 2026-08-29 | | [[LLM Wiki Pattern]] | architecture | Methodik für persönliches Wissensmanagement, bei der ein LLM aus Rohquellen ein dauerhaftes Wiki aufbaut - Wissen wird kompiliert statt per RAG neu hergeleitet. | 2026-08-29 |
| [[Mass-Update Gate]] | workflow | Mass-Update Gate: publish endet mit 42 (Freigabe durch den Menschen noetig) ab 10 gezaehlten Dateien; generierte Dateien und work/ werden committet\, aber seit 1.5.0 nicht gezaehlt; freigegeben per --confirm <token> | 2026-09-01 | | [[Mass-Update Gate]] | workflow | Mass-Update Gate: publish endet mit 42 (Freigabe durch den Menschen noetig) ab 10 gezaehlten Dateien; generierte Dateien und work/ werden committet\, aber seit 1.5.0 nicht gezaehlt; freigegeben per --confirm <token> | 2026-09-01 |
| [[MCP-Leseserver]] | architecture | Zweiter Konsument von chemenu ueber MCP: search/types/describe_type/lint/status auf chemenu.api.Corpus, strukturell ohne Schreibpfad, jede Antwort trage einen Commit-Stempel. | 2026-09-02 |
| [[Memory Lifecycle]] | architecture | Architektur des Wissenslebenszyklus mit Confidence Scoring, Supersession, Forgetting und Consolidation Tiers zur Pflege von Fakten über die Zeit. | 2026-08-29 | | [[Memory Lifecycle]] | architecture | Architektur des Wissenslebenszyklus mit Confidence Scoring, Supersession, Forgetting und Consolidation Tiers zur Pflege von Fakten über die Zeit. | 2026-08-29 |
| [[Mesh Sync]] | pattern | Abgleichsmechanismus, der Beobachtungen paralleler Agenten in ein gemeinsames Wiki überführt; Last-Write-Wins mit Konflikterkennung und manuellem Eingriff. | 2026-08-29 | | [[Mesh Sync]] | pattern | Abgleichsmechanismus, der Beobachtungen paralleler Agenten in ein gemeinsames Wiki überführt; Last-Write-Wins mit Konflikterkennung und manuellem Eingriff. | 2026-08-29 |
| [[Modbus]] | protocol | Industrielles Kommunikationsprotokoll von 1979 zur Anbindung speicherprogrammierbarer Steuerungen und Geräte über serielle oder TCP-Netze. | 2026-08-29 | | [[Modbus]] | protocol | Industrielles Kommunikationsprotokoll von 1979 zur Anbindung speicherprogrammierbarer Steuerungen und Geräte über serielle oder TCP-Netze. | 2026-08-29 |
@@ -60,7 +61,7 @@
| [[Personalization Plane]] | architecture | Schicht fuer Instanz-Identitaet: USER.md/SOUL.md werden als Template ausgeliefert, im Setup-Interview woertlich befuellt und vom doctor-Check auf fehlend wie unbefuellt geprueft | 2026-08-31 | | [[Personalization Plane]] | architecture | Schicht fuer Instanz-Identitaet: USER.md/SOUL.md werden als Template ausgeliefert, im Setup-Interview woertlich befuellt und vom doctor-Check auf fehlend wie unbefuellt geprueft | 2026-08-31 |
| [[Privacy and Governance]] | workflow | Rahmenwerk zur Absicherung von Wiki-Inhalten über Datenfilterung beim Ingest, Audit-Trail-Protokollierung und umkehrbare Massenoperationen. | 2026-08-29 | | [[Privacy and Governance]] | workflow | Rahmenwerk zur Absicherung von Wiki-Inhalten über Datenfilterung beim Ingest, Audit-Trail-Protokollierung und umkehrbare Massenoperationen. | 2026-08-29 |
| [[Procedural Memory]] | architecture | Langlebigste Speicherschicht für Abläufe, Muster, bewährte Vorgehensweisen und Rezepte, gewonnen aus wiederholten semantischen Beobachtungen. | 2026-08-29 | | [[Procedural Memory]] | architecture | Langlebigste Speicherschicht für Abläufe, Muster, bewährte Vorgehensweisen und Rezepte, gewonnen aus wiederholten semantischen Beobachtungen. | 2026-08-29 |
| [[Publish-Remote Gate]] | workflow | Drittes, im Code durchgesetztes Gate: publish bricht mit Exit 42 ab, wenn die aufgeloeste Push-URL nicht in einer optionalen, gitignoreten Allowlist steht; anders als die anderen Gates gibt es keinen Freigabe-Token. | 2026-09-01 | | [[Publish-Remote Gate]] | workflow | Drittes, im Code durchgesetztes Gate: publish bricht mit Exit 42 ab, wenn die aufgeloeste Push-URL nicht in einer optionalen, gitignoreten Allowlist steht; doctor benennt seit 2026-09-02 den Gate-Zustand statt nur die Dateiexistenz | 2026-09-02 |
| [[Quality and Self-Correction]] | workflow | Automatische Qualitätssicherung für Wikis mit Inhaltsbewertung, Selbstheilung und Widerspruchserkennung. | 2026-08-29 | | [[Quality and Self-Correction]] | workflow | Automatische Qualitätssicherung für Wikis mit Inhaltsbewertung, Selbstheilung und Widerspruchserkennung. | 2026-08-29 |
| [[Quality Scoring]] | pattern | Quantitative Bewertung aller vom LLM geschriebenen Inhalte nach struktureller Qualität, Vollständigkeit der Quellenangaben, Konsistenz mit dem Wiki und Themenabdeckung. | 2026-08-29 | | [[Quality Scoring]] | pattern | Quantitative Bewertung aller vom LLM geschriebenen Inhalte nach struktureller Qualität, Vollständigkeit der Quellenangaben, Konsistenz mit dem Wiki und Themenabdeckung. | 2026-08-29 |
| [[RAG]] | architecture | Architekturmuster, bei dem LLMs die Generierung um Dokumente aus einer Wissensbasis anreichern. | 2026-08-29 | | [[RAG]] | architecture | Architekturmuster, bei dem LLMs die Generierung um Dokumente aus einer Wissensbasis anreichern. | 2026-08-29 |
+12 -4
View File
@@ -3,13 +3,13 @@ type: types/concept.md
concept_type: workflow concept_type: workflow
tags: [gate, safety, iteration-budget, loop-breaker] tags: [gate, safety, iteration-budget, loop-breaker]
created: 2026-08-07 created: 2026-08-07
modified: 2026-08-31 modified: 2026-09-02
related: [Mass-Update Gate, Anti-Cramming Heuristic, Index Scaling, wikitool, Structural Enforcement over Documented Rule] related: [Mass-Update Gate, Anti-Cramming Heuristic, Index Scaling, wikitool, Structural Enforcement over Documented Rule, MCP-Leseserver]
sources: [Source - LLM Improvements Production Agent Gaps 2026, Source - Conversation - Comma Bug Budget Refund and Lint Report Path Session 2026-08-31, Source - Conversation - Gate Counting and Measured Calibration Session 2026-08-31] sources: [Source - LLM Improvements Production Agent Gaps 2026, Source - Conversation - Comma Bug Budget Refund and Lint Report Path Session 2026-08-31, Source - Conversation - Gate Counting and Measured Calibration Session 2026-08-31, Source - MCP Read Server Implementation Session 2026-09-02]
confidence: 0.88 confidence: 0.88
confidence_base: 0.88 confidence_base: 0.88
provenance: sourced provenance: sourced
summary: Im Code durchgesetzte Obergrenze von 60 wikitool-Aufrufen je Session\, Loop-Breaker bei 3 identischen Wiederholungen\, Slot-Erstattung bei abgelehntem Aufruf und ein seit 1.5.0 gemessenes Kalibrierungsband von 5-15 (einfach) bzw. 20-35 (komplex) Aufrufen summary: Im Code durchgesetzte Obergrenze von 60 wikitool-Aufrufen je Session, Loop-Breaker bei 3 identischen Wiederholungen, Slot-Erstattung, ein gemessenes Kalibrierungsband, und Retrieval sowie der MCP-Leseserver bleiben ausgenommen
--- ---
# Iteration and Cost Limits # Iteration and Cost Limits
@@ -48,6 +48,11 @@ Eine hart in Code durchgesetzte Obergrenze für die Anzahl der Tool-Aufrufe, die
- Einzelne, begrenzte Einmalvorgänge, bei denen die Aufrufen-Anzahl inhärent festgelegt ist (z. B. ein einzelner `new entity`-Aufruf) - das Gate wird dort immer noch gleichmäßig angewendet, wird aber im Wesentlichen nie ausgelöst. - Einzelne, begrenzte Einmalvorgänge, bei denen die Aufrufen-Anzahl inhärent festgelegt ist (z. B. ein einzelner `new entity`-Aufruf) - das Gate wird dort immer noch gleichmäßig angewendet, wird aber im Wesentlichen nie ausgelöst.
- Als Ersatz für das [[Mass-Update Gate]], das auf den *Schadensradius* eines `publish` (Dateien, die von einem einzelnen Push betroffen sind) begrenzt ist, nicht auf die *Iterationsmenge* über eine Sitzung - die beiden Gates beheben unterschiedliche Ausfallmodi und beide bleiben notwendig. - Als Ersatz für das [[Mass-Update Gate]], das auf den *Schadensradius* eines `publish` (Dateien, die von einem einzelnen Push betroffen sind) begrenzt ist, nicht auf die *Iterationsmenge* über eine Sitzung - die beiden Gates beheben unterschiedliche Ausfallmodi und beide bleiben notwendig.
- Im [[MCP-Leseserver]]. Das Gate begrenzt eine Agenten-Session am unbemerkten Iterieren über den
Wiki-Zustand - deshalb ist Retrieval bereits generell ausgenommen (`SKIP_COMMANDS`) -, nicht
einen Nutzer, der oft sucht. Ein zu häufig suchender Nutzer ist ein Ressourcenproblem, das vor
den Serverprozess gehört (Rate Limiting), nicht in dieses Gate - beide zu vermischen würde es
zu einem Rate Limiter verwässern.[^s-mcp-read-server-implementation-session-2026-09-02]
## Verwandte Concepts ## Verwandte Concepts
@@ -63,6 +68,7 @@ Eine hart in Code durchgesetzte Obergrenze für die Anzahl der Tool-Aufrufe, die
- **schützt:** [[Index Scaling]] - **schützt:** [[Index Scaling]]
- **implementiert durch:** [[wikitool]] - **implementiert durch:** [[wikitool]]
- **wendet an:** [[Structural Enforcement over Documented Rule]] - **wendet an:** [[Structural Enforcement over Documented Rule]]
- **nimmt aus:** [[MCP-Leseserver]]
## Siehe auch ## Siehe auch
@@ -73,9 +79,11 @@ Eine hart in Code durchgesetzte Obergrenze für die Anzahl der Tool-Aufrufe, die
- [[Source - LLM Improvements Production Agent Gaps 2026]] - [[Source - LLM Improvements Production Agent Gaps 2026]]
- [[Source - Conversation - Gate Counting and Measured Calibration Session 2026-08-31]] - [[Source - Conversation - Gate Counting and Measured Calibration Session 2026-08-31]]
- [[Structural Enforcement over Documented Rule]] - [[Structural Enforcement over Documented Rule]]
- [[MCP-Leseserver]]
## Fußnoten ## Fußnoten
[^s-llm-improvements-production-agent-gaps-2026]: [[Source - LLM Improvements Production Agent Gaps 2026]] [^s-llm-improvements-production-agent-gaps-2026]: [[Source - LLM Improvements Production Agent Gaps 2026]]
[^s-conversation-gate-counting-and-measured-calibration-session-2026-08-31]: [[Source - Conversation - Gate Counting and Measured Calibration Session 2026-08-31]] [^s-conversation-gate-counting-and-measured-calibration-session-2026-08-31]: [[Source - Conversation - Gate Counting and Measured Calibration Session 2026-08-31]]
[^s-conversation-comma-bug-budget-refund-and-lint-report-path-session-2026-08-31]: [[Source - Conversation - Comma Bug Budget Refund and Lint Report Path Session 2026-08-31]] [^s-conversation-comma-bug-budget-refund-and-lint-report-path-session-2026-08-31]: [[Source - Conversation - Comma Bug Budget Refund and Lint Report Path Session 2026-08-31]]
[^s-mcp-read-server-implementation-session-2026-09-02]: [[Source - MCP Read Server Implementation Session 2026-09-02]]
+21 -6
View File
@@ -3,13 +3,13 @@ type: types/concept.md
concept_type: decision concept_type: decision
tags: [versioning, semver, release, stack] tags: [versioning, semver, release, stack]
created: 2026-08-30 created: 2026-08-30
modified: 2026-08-30 modified: 2026-09-02
related: [wikitool, Issue Label Scheme] related: [wikitool, Issue Label Scheme]
sources: [Source - Conversation - Versioning CI-CD and Content Migration Session 2026-08-30, Source - Conversation - Issue Triage Labels and TODO Retirement Session 2026-08-31] sources: [Source - Conversation - Versioning CI-CD and Content Migration Session 2026-08-30, Source - Conversation - Issue Triage Labels and TODO Retirement Session 2026-08-31, Source - Version Part Nomenclature and Breaking Change Gate Session 2026-09-02]
confidence: 0.70 confidence: 0.70
confidence_base: 0.70 confidence_base: 0.70
provenance: sourced provenance: sourced
summary: 'Semantische Versionierung des Wiki-Stacks: VERSION beschreibt die Maschinerie, Kompatibilitaet ist die linkeste Nicht-Null-Komponente, und drei getrennte Dateien trennen Maschinerie, Herkunft und Content-Form' summary: 'Semantische Versionierung des Wiki-Stacks: VERSION beschreibt die Maschinerie, Kompatibilitaet (Drop-in-Ersatz) und Inhaltsmigration sind seit 2.5.0 getrennte, unabhaengig geprueft Fragen'
--- ---
# KB Stack Versioning # KB Stack Versioning
@@ -42,9 +42,18 @@ deshalb eine ausdrückliche Handlung.
Caret-Ranges Caret-Ranges
verwenden[^s-conversation-versioning-ci-cd-and-content-migration-session-2026-08-30]. Sie gilt verwenden[^s-conversation-versioning-ci-cd-and-content-migration-session-2026-08-30]. Sie gilt
einheitlich für `0.x` und `1.x`, sodass unter `0.x` der Schritt `0.1.x` -> `0.2.0` dasselbe einheitlich für `0.x` und `1.x`, sodass unter `0.x` der Schritt `0.1.x` -> `0.2.0` dasselbe
Migrationssignal trägt wie `MAJOR` ab `1.0.0`. Der Code für den `compat_key` ist deshalb Signal trägt wie `MAJOR` ab `1.0.0`. Der Code für den `compat_key` ist deshalb einheitlich
einheitlich formuliert und musste beim Wechsel auf `1.0.0` nicht angefasst formuliert und musste beim Wechsel auf `1.0.0` nicht angefasst
werden[^s-conversation-versioning-ci-cd-and-content-migration-session-2026-08-30]. werden[^s-conversation-versioning-ci-cd-and-content-migration-session-2026-08-30].
- **Kompatibilität und Inhaltsmigration sind zwei unabhängige Fragen, seit `2.5.0` auch zwei
getrennte Marker.** Kompatibilität fragt, ob die neue Version ein Drop-in-Ersatz ist -
vorwärts ohne Handarbeit, rückwärts noch downgradebar; Inhaltsmigration fragt, ob `kb/` sich
bewegen muss. Ein Grenzübertritt kann `kb/` unangetastet lassen und trotzdem MAJOR sein - der
`2.0.0`-Rebranding-Bump ist das Beispiel: Update-Pfad, Release-Artefaktname und
Paket-Import-Name brachen, keine Seite tat es. `version bump` verlangt deshalb bei jedem
Grenzübertritt `--breaking "<was aufhört zu funktionieren>"`, unabhängig von
`--no-migration`/einem Migrationsdokument; beide Zeilen landen getrennt im
`CHANGES.md`-Eintrag[^s-version-part-nomenclature-and-breaking-change-gate-session-2026-09-02].
- **`x.y.z` ist die maximale Granularität. Keine Pre-Release-Suffixe.** Eine zweite - **`x.y.z` ist die maximale Granularität. Keine Pre-Release-Suffixe.** Eine zweite
Ordnungsregel müsste vom Release-Feed, von der Migrationskette und von der Ordnungsregel müsste vom Release-Feed, von der Migrationskette und von der
Kompatibilitätsprüfung gleichermaßen befolgt Kompatibilitätsprüfung gleichermaßen befolgt
@@ -64,7 +73,11 @@ deshalb eine ausdrückliche Handlung.
Instanz[^s-conversation-versioning-ci-cd-and-content-migration-session-2026-08-30]. Instanz[^s-conversation-versioning-ci-cd-and-content-migration-session-2026-08-30].
- **Die Grenze wird an zwei Stellen erzwungen:** in `version bump` und in `docs verify`, ergänzt - **Die Grenze wird an zwei Stellen erzwungen:** in `version bump` und in `docs verify`, ergänzt
um einen `kb-version`-Check in um einen `kb-version`-Check in
`doctor`[^s-conversation-versioning-ci-cd-and-content-migration-session-2026-08-30]. `doctor`[^s-conversation-versioning-ci-cd-and-content-migration-session-2026-08-30]. Seit
`2.5.0` prüft `docs verify` dort zwei unabhängige Dinge - `check_migration_for_boundary` (hat
der Korpus sich bewegt) und `check_breaking_change_for_boundary` (wurde der Bruch benannt) -,
weil ein Grenzübertritt die eine Prüfung bestehen und an der anderen scheitern
kann[^s-version-part-nomenclature-and-breaking-change-gate-session-2026-09-02].
## Beispiele ## Beispiele
@@ -105,7 +118,9 @@ kann.
- [[wikitool]] - [[wikitool]]
- [[Issue Label Scheme]] - [[Issue Label Scheme]]
- [[Source - Conversation - Issue Triage Labels and TODO Retirement Session 2026-08-31]] - [[Source - Conversation - Issue Triage Labels and TODO Retirement Session 2026-08-31]]
- [[Source - Version Part Nomenclature and Breaking Change Gate Session 2026-09-02]]
## Fußnoten ## Fußnoten
[^s-conversation-versioning-ci-cd-and-content-migration-session-2026-08-30]: [[Source - Conversation - Versioning CI-CD and Content Migration Session 2026-08-30]] [^s-conversation-versioning-ci-cd-and-content-migration-session-2026-08-30]: [[Source - Conversation - Versioning CI-CD and Content Migration Session 2026-08-30]]
[^s-version-part-nomenclature-and-breaking-change-gate-session-2026-09-02]: [[Source - Version Part Nomenclature and Breaking Change Gate Session 2026-09-02]]
+108
View File
@@ -0,0 +1,108 @@
---
type: types/concept.md
concept_type: architecture
tags: [mcp, library-boundary, search, server]
created: 2026-09-02
modified: 2026-09-02
related: [wikitool, Publish-Remote Gate, Mass-Update Gate, Iteration and Cost Limits, Chemenu]
sources: [Source - MCP Read Server Implementation Session 2026-09-02]
confidence: 0.50
confidence_base: 0.50
provenance: sourced
summary: 'Zweiter Konsument von chemenu ueber MCP: search/types/describe_type/lint/status auf chemenu.api.Corpus, strukturell ohne Schreibpfad, jede Antwort trage einen Commit-Stempel.'
---
# MCP-Leseserver
**Typ:** Architecture
## Definition
Ein zweiter Konsument desselben Kerns, nicht ein zweites Programm: `tools/chemenu/mcp/` exponiert
`search`, `types`, `describe_type`, `lint` und `status` über MCP, indem es dieselben Funktionen
aufruft, die `wikitool` auch aufruft - vermittelt durch `chemenu.api.Corpus`, den In-Process-
Einstiegspunkt. Ein Golden-Test hält die Ausgaben beider Wege gegeneinander, statt darauf zu
vertrauen, dass sie übereinstimmen.
## Kernpunkte
- **Kein Schreibpfad, strukturell.** Weder der Server noch `chemenu.api` importiert etwas unter
`chemenu.commands` - `new`, `touch`, `xref`, `publish`, `migrate` sind aus diesem Prozess
heraus nicht erreichbar, statt aus einer Liste gefiltert zu werden. Ein Test importiert das
Servermodul in einem frischen Interpreter und prüft
`sys.modules`.[^s-mcp-read-server-implementation-session-2026-09-02]
- **Zwei Transports.** `stdio` zum Entwickeln und Testen ohne Netz; `streamable-http` für die
Auslieferung, der einzige, vor den sich ein HTTP-Reverse-Proxy setzen kann. `sse` ist über das
SDK erreichbar und wird bewusst nicht angeboten - der abgelöste Remote-Transport, jetzt darauf
zu bauen verschiebt den Wechsel nur.
- **Jede Antwort trägt den Commit, aus dem sie berechnet wurde** (`commit`, `as_of`). Ein
veralteter Checkout antwortet sonst selbstbewusst falsch. `null` heißt: der bediente Baum hat
uncommittete Änderungen, die Antwort entspricht keiner Revision. Der Stempel ist die Revision,
aus der die Seiten *tatsächlich* gelesen wurden, nicht die zum Zeitpunkt des Stempelns aktuelle
- ein Bug, der genau diesen Unterschied überging, wurde beim Schreiben des Golden-Tests selbst
gefunden und behoben.[^s-mcp-read-server-implementation-session-2026-09-02]
- **Telemetrie in den bedienten Baum wird beim Start verweigert**, nicht still umgeleitet. Der
Sync, der den Checkout aktuell hält (`git fetch && git reset --hard`), darf `reports/telemetry/`
wegräumen; ein Trace, der dort landet, wäre ein Verlust und eine stille Möglichkeit, den Baum
zu beschmutzen, dessen Sauberkeit der Korpus-Cache prüft.
- **Kein Iteration Budget Gate im Server.** Das Gate begrenzt eine Agenten-Session am unbemerkten
Iterieren über den Wiki-Zustand, nicht einen Nutzer, der oft sucht - Retrieval ist deshalb
bereits generell davon ausgenommen (siehe [[Iteration and Cost Limits]]). Rate Limiting gehört
stattdessen vor den Prozess, neben die Authentifizierung.
- **Authentifizierung ist Middleware, nicht Servercode.** Eine Traefik-ForwardAuth-Instanz
(Bearer-Token gegen SHA-256-Hashes) sitzt vor dem Prozess; nicht sauber authentifizierte
Zugriffe erreichen Python gar nicht erst.
- **Gemessen:** Korpus-Parse für 176 Seiten 265 ms → 54 ms (`CSafeLoader`),
`wikitool search` end-to-end 593 ms → 347 ms; die verbleibenden ~262 ms sind Modulimport und
entfallen im residenten Serverprozess, weil er ihn einmal pro Start statt pro Aufruf
zahlt.[^s-mcp-read-server-implementation-session-2026-09-02]
## Beispiele
- `search`/`types`/`describe_type`/`lint`/`status` als die fünf Tools - siehe
`tools/chemenu/mcp/server.py`.
- Der Korpus-Cache (`chemenu/corpus_cache.py`) hält einen Parse pro Commit und cacht nie einen
schmutzigen Arbeitsbaum - dieselbe Eigenschaft, die den Antwort-Stempel korrekt hält.
- `chemenu.api.Corpus`: nimmt einen Root, liefert exakt die `--json`-Formen der CLI, raised statt
zu exitieren.
## Wann zu verwenden
- Ein Konsument, der keine Shell auf der bedienenden Maschine ist, soll dieselben Fragen stellen
können wie ein Agent, der `wikitool` direkt aufruft.
- Mehrere gleichzeitige Leser eines Korpus, für die ein Prozess pro CLI-Aufruf (Modulimport,
Korpus-Parse) unnötigen Overhead bedeutet.
## Wann NICHT zu verwenden
- Als Ort für einen Schreibpfad - die Ingest-Queue (geplant, Issue #32) ist ein anderes Design
mit einer Quarantäne davor, nicht eine Erweiterung dieses Servers.
- Als Ersatz für den Iteration Budget Gate oder das Traefik-Rate-Limiting - beide bleiben
notwendig und leben an anderer Stelle.
## Verwandte Concepts
- [[Publish-Remote Gate]]
- [[Mass-Update Gate]]
- [[Iteration and Cost Limits]]
## Fußnoten
[^s-mcp-read-server-implementation-session-2026-09-02]: [[Source - MCP Read Server Implementation Session 2026-09-02]]
## Beziehungen
- **implementiert:** [[wikitool]]
- **verwandt mit:** [[Publish-Remote Gate]]
- **verwandt mit:** [[Mass-Update Gate]]
- **ausgenommen von:** [[Iteration and Cost Limits]]
- **Teil von:** [[Chemenu]]
## Siehe auch
- [[wikitool]]
- [[Publish-Remote Gate]]
- [[Mass-Update Gate]]
- [[Iteration and Cost Limits]]
- [[Chemenu]]
- [[Source - MCP Read Server Implementation Session 2026-09-02]]
+3 -1
View File
@@ -4,7 +4,7 @@ concept_type: workflow
tags: [gate, safety, mass-update, confirmation] tags: [gate, safety, mass-update, confirmation]
created: 2026-08-03 created: 2026-08-03
modified: 2026-09-01 modified: 2026-09-01
related: [Content Quality Control, wikitool, Iteration and Cost Limits, Structural Enforcement over Documented Rule, Bulk Operations, Publish-Remote Gate] related: [Content Quality Control, wikitool, Iteration and Cost Limits, Structural Enforcement over Documented Rule, Bulk Operations, Publish-Remote Gate, MCP-Leseserver]
sources: [Source - LLM Improvements Sonnet Analysis, Source - LLM Improvements Production Agent Gaps 2026, Source - Conversation - Comma Bug Budget Refund and Lint Report Path Session 2026-08-31, Source - Conversation - Gate Counting and Measured Calibration Session 2026-08-31, Source - LLM Improvements Codex Analysis] sources: [Source - LLM Improvements Sonnet Analysis, Source - LLM Improvements Production Agent Gaps 2026, Source - Conversation - Comma Bug Budget Refund and Lint Report Path Session 2026-08-31, Source - Conversation - Gate Counting and Measured Calibration Session 2026-08-31, Source - LLM Improvements Codex Analysis]
confidence: 0.88 confidence: 0.88
confidence_base: 0.88 confidence_base: 0.88
@@ -78,6 +78,7 @@ Das Mass-Update Gate ist ein Sicherheitsmechanismus, der die Ausführung pausier
- **wendet an:** [[Structural Enforcement over Documented Rule]] - **wendet an:** [[Structural Enforcement over Documented Rule]]
- **grenzt ab gegen:** [[Bulk Operations]] - **grenzt ab gegen:** [[Bulk Operations]]
- **verwandtes Gate:** [[Publish-Remote Gate]] - **verwandtes Gate:** [[Publish-Remote Gate]]
- **verwandt mit:** [[MCP-Leseserver]]
## Siehe auch ## Siehe auch
@@ -88,6 +89,7 @@ Das Mass-Update Gate ist ein Sicherheitsmechanismus, der die Ausführung pausier
- [[Structural Enforcement over Documented Rule]] - [[Structural Enforcement over Documented Rule]]
- [[Bulk Operations]] - [[Bulk Operations]]
- [[Publish-Remote Gate]] - [[Publish-Remote Gate]]
- [[MCP-Leseserver]]
## Fußnoten ## Fußnoten
+15 -5
View File
@@ -3,13 +3,13 @@ type: types/concept.md
concept_type: workflow concept_type: workflow
tags: [] tags: []
created: 2026-09-01 created: 2026-09-01
modified: 2026-09-01 modified: 2026-09-02
related: [Mass-Update Gate, Chemenu] related: [Mass-Update Gate, Chemenu, MCP-Leseserver]
sources: [Source - Publish-Remote Gate and Issue Triage Session 2026-09-01, Source - Private-Instance Merge Correction and Issue 30 Session 2026-09-01] sources: [Source - Publish-Remote Gate and Issue Triage Session 2026-09-01, Source - Private-Instance Merge Correction and Issue 30 Session 2026-09-01, Source - MCP Read Server Implementation Session 2026-09-02]
confidence: 0.50 confidence: 0.70
confidence_base: 0.70 confidence_base: 0.70
provenance: sourced provenance: sourced
summary: 'Drittes, im Code durchgesetztes Gate: publish bricht mit Exit 42 ab, wenn die aufgeloeste Push-URL nicht in einer optionalen, gitignoreten Allowlist steht; anders als die anderen Gates gibt es keinen Freigabe-Token.' summary: 'Drittes, im Code durchgesetztes Gate: publish bricht mit Exit 42 ab, wenn die aufgeloeste Push-URL nicht in einer optionalen, gitignoreten Allowlist steht; doctor benennt seit 2026-09-02 den Gate-Zustand statt nur die Dateiexistenz'
--- ---
# Publish-Remote Gate # Publish-Remote Gate
@@ -40,6 +40,12 @@ Remote lokal konfiguriert ist.
ist wichtig: Ein Checkout ohne Beschränkungsbedarf soll nicht gezwungen sein, eine leere ist wichtig: Ein Checkout ohne Beschränkungsbedarf soll nicht gezwungen sein, eine leere
Konfigurationsdatei zu pflegen; eine beschädigte Datei darf aber nicht wie eine abwesende Konfigurationsdatei zu pflegen; eine beschädigte Datei darf aber nicht wie eine abwesende
behandelt werden, sonst wird eine defekte Sicherung zu einer stillschweigend abgeschalteten. behandelt werden, sonst wird eine defekte Sicherung zu einer stillschweigend abgeschalteten.
- **`doctor` benennt seit 2026-09-02 den Gate-*Zustand*, nicht nur, ob die Datei existiert.**
Vorher meldete der Check nur die Anwesenheit von `.wikitool-remotes.json`; ob das
gleichbedeutend mit "scharf" ist, musste der Leser selbst schließen. Alle drei Ausgaben
beginnen jetzt mit `Gate armed:` bzw. `Gate not armed:` - der Ein-Remote-Fall ohne Allowlist
bleibt `OK` (er hat nichts zu schützen), sagt aber ausdrücklich, dass jedes Push-Ziel
durchkommt.[^s-mcp-read-server-implementation-session-2026-09-02]
## Wann zu verwenden ## Wann zu verwenden
@@ -88,11 +94,13 @@ Maschinerie ziehen) stehen in Gitea-Issue #30.
## Verwandte Concepts ## Verwandte Concepts
- [[Mass-Update Gate]] - [[Mass-Update Gate]]
- [[MCP-Leseserver]]
## Beziehungen ## Beziehungen
- **gilt fuer:** [[Chemenu]] - **gilt fuer:** [[Chemenu]]
- **verwandtes Gate:** [[Mass-Update Gate]] - **verwandtes Gate:** [[Mass-Update Gate]]
- **verwandt mit:** [[MCP-Leseserver]]
## Siehe auch ## Siehe auch
@@ -100,7 +108,9 @@ Maschinerie ziehen) stehen in Gitea-Issue #30.
- [[Chemenu]] - [[Chemenu]]
- [[Mass-Update Gate]] - [[Mass-Update Gate]]
- [[Source - Private-Instance Merge Correction and Issue 30 Session 2026-09-01]] - [[Source - Private-Instance Merge Correction and Issue 30 Session 2026-09-01]]
- [[MCP-Leseserver]]
## Fußnoten ## Fußnoten
[^s-private-instance-merge-correction-and-issue-30-session-2026-09-01]: [[Source - Private-Instance Merge Correction and Issue 30 Session 2026-09-01]] [^s-private-instance-merge-correction-and-issue-30-session-2026-09-01]: [[Source - Private-Instance Merge Correction and Issue 30 Session 2026-09-01]]
[^s-mcp-read-server-implementation-session-2026-09-02]: [[Source - MCP Read Server Implementation Session 2026-09-02]]
+9 -2
View File
@@ -1,3 +1,8 @@
---
profile: entities
required_by_stack: false
---
# kb/entities/ - Collection Contract # kb/entities/ - Collection Contract
Concrete things that exist: a project, a deployed system, a CLI tool, a technology, a person or Concrete things that exist: a project, a deployed system, a CLI tool, a technology, a person or
@@ -7,8 +12,10 @@ an organization. If it can be pointed at, it is an entity.
is, where it actually is, and whether that is still true. An entity page that describes a is, where it actually is, and whether that is still true. An entity page that describes a
system correctly but names no host, path, version or status has not earned its keep. system correctly but names no host, path, version or status has not earned its keep.
Inherits [kb/CONTRACT.md](../CONTRACT.md) - naming, tone, linking, provenance and confidence Inherits [kb/CONTRACT.md](../CONTRACT.md) for the rules the stack enforces - linking mechanics,
are defined there and are not restated here. provenance, citation, the confidence machinery - and
[kb/CONVENTIONS.md](../CONVENTIONS.md) for what this instance decided: language, naming forms,
tone, relationship labels, the confidence rubric. Neither is restated here.
## Types offered ## Types offered
+3 -3
View File
@@ -19,7 +19,7 @@
|------|------|---------|----------------| |------|------|---------|----------------|
| [[andybalholm-edl]] | project | Go-basierte EDL-Bibliothek für die Kommunikation mit eingebetteten Geräten. | 2026-08-29 | | [[andybalholm-edl]] | project | Go-basierte EDL-Bibliothek für die Kommunikation mit eingebetteten Geräten. | 2026-08-29 |
| [[BCDModule]] | project | Go-Modul, das die Entscheidungslogik für die Batterieladung umsetzt. | 2026-08-29 | | [[BCDModule]] | project | Go-Modul, das die Entscheidungslogik für die Batterieladung umsetzt. | 2026-08-29 |
| [[Chemenu]] | project | Deterministischer Wissenskompiler (raw/ -> kb/); seit 2.0.0 unter dem Namen Chemenu, seit 2026-09-01 oeffentlich als Testbett und Demo unter AGPL-3.0/CC-BY-4.0 | 2026-09-01 | | [[Chemenu]] | project | Deterministischer Wissenskompiler (raw/ -> kb/); seit 2.0.0 unter dem Namen Chemenu; Issue 26 zur Versionsstellen-Nomenklatur in 2.5.0 geschlossen | 2026-09-02 |
| [[goresponsiveness]] | project | Go-Werkzeug zur Messung von Anwendungsleistung und Responsiveness. | 2026-08-29 | | [[goresponsiveness]] | project | Go-Werkzeug zur Messung von Anwendungsleistung und Responsiveness. | 2026-08-29 |
| [[ha-core]] | project | Kern-Integrationsbibliothek für Home-Assistant-E3DC-Systeme; stellt die E3DC-Kommunikationsprotokolle und den Home-Assistant-Integrationscode bereit. | 2026-08-29 | | [[ha-core]] | project | Kern-Integrationsbibliothek für Home-Assistant-E3DC-Systeme; stellt die E3DC-Kommunikationsprotokolle und den Home-Assistant-Integrationscode bereit. | 2026-08-29 |
| [[hacs-e3dc]] | project | Home Assistant Custom Component zur Überwachung von E3DC-Energiesystemen. | 2026-08-29 | | [[hacs-e3dc]] | project | Home Assistant Custom Component zur Überwachung von E3DC-Energiesystemen. | 2026-08-29 |
@@ -96,8 +96,8 @@
| [[pascalandy schema]] | tool | Von der Community beigesteuertes Wiki Schema (Global) aus pascalandys Kommentar in Farzas Gist, mit alternativer Tag-Taxonomie (area/kind/topic/status/pty) | 2026-08-29 | | [[pascalandy schema]] | tool | Von der Community beigesteuertes Wiki Schema (Global) aus pascalandys Kommentar in Farzas Gist, mit alternativer Tag-Taxonomie (area/kind/topic/status/pty) | 2026-08-29 |
| [[Pi]] | tool | Assistenz-Agent von Inflection AI; kann das LLM-Wiki-Muster wie andere LLM-Agenten umsetzen. | 2026-08-29 | | [[Pi]] | tool | Assistenz-Agent von Inflection AI; kann das LLM-Wiki-Muster wie andere LLM-Agenten umsetzen. | 2026-08-29 |
| [[Proton]] | tool | Wine-basierte Kompatibilitätsschicht von Valve; lässt Windows-Spiele über Steam unter Linux laufen, mit optimierter DirectX-Übersetzung. | 2026-08-29 | | [[Proton]] | tool | Wine-basierte Kompatibilitätsschicht von Valve; lässt Windows-Spiele über Steam unter Linux laufen, mit optimierter DirectX-Übersetzung. | 2026-08-29 |
| [[qmd]] | tool | Lokale Suchmaschine für Markdown-Dateien mit hybrider BM25-Vektor-Suche und LLM-Reranking. | 2026-08-29 | | [[qmd]] | tool | Lokale Suchmaschine fuer Markdown-Dateien: TypeScript/Node.js/Bun, SQLite-FTS5-BM25 plus sqlite-vec-Vektorsuche plus node-llama-cpp-LLM-Reranking. | 2026-09-02 |
| [[Steam]] | tool | Valves Plattform für digitalen Spielevertrieb und Spielebibliothek auf dem PC. | 2026-08-29 | | [[Steam]] | tool | Valves Plattform für digitalen Spielevertrieb und Spielebibliothek auf dem PC. | 2026-08-29 |
| [[wikitool]] | tool | Deterministisches CLI fuer alle mechanischen Wiki-Operationen; seit 2.0.0 liegt es im Python-Paket chemenu, das Kommando heisst weiterhin wikitool | 2026-09-01 | | [[wikitool]] | tool | Deterministisches CLI fuer alle mechanischen Wiki-Operationen; seit 2.0.0 im Paket chemenu, seit 2.4.0 zusaetzlich als MCP-Leseserver erreichbar, seit 2.5.0 mit Breaking-Change-Pflichtmarker | 2026-09-02 |
| [[Wine]] | tool | Kompatibilitätsschicht, die Windows-API-Aufrufe nach POSIX übersetzt und Windows-Anwendungen unter Linux, BSD und macOS ohne Virtualisierung oder Emulation ausführt. | 2026-08-29 | | [[Wine]] | tool | Kompatibilitätsschicht, die Windows-API-Aufrufe nach POSIX übersetzt und Windows-Anwendungen unter Linux, BSD und macOS ohne Virtualisierung oder Emulation ausführt. | 2026-08-29 |
+17 -5
View File
@@ -3,13 +3,13 @@ type: types/entity.md
entity_type: project entity_type: project
tags: [wiki, llm, knowledge-base] tags: [wiki, llm, knowledge-base]
created: 2026-08-04 created: 2026-08-04
modified: 2026-09-01 modified: 2026-09-02
related: [Personalization Plane, Issue Label Scheme, Optional Instance Context File, Delete Rather Than Anonymize, Dual Licensing by File Plan, Publish-Remote Gate] related: [Personalization Plane, Issue Label Scheme, Optional Instance Context File, Delete Rather Than Anonymize, Dual Licensing by File Plan, Publish-Remote Gate, MCP-Leseserver]
sources: [Source - Copilot Skill Restructure Instructions, Source - Conversation - AGENTS.md Skill Restructuring Session 2026-08-04, Source - Conversation - Versioning CI-CD and Content Migration Session 2026-08-30, Source - Conversation - Comma Bug Budget Refund and Lint Report Path Session 2026-08-31, Source - Conversation - Issue Triage Labels and TODO Retirement Session 2026-08-31, Source - Conversation - Write-Once Frontmatter Fields and touch --set Session 2026-08-31, Source - Conversation - Gate Counting and Measured Calibration Session 2026-08-31, Source - Conversation - Two Round-Trip Defects Found by an Ingest Session 2026-08-31, Source - Conversation - Hardening the Test Suite Against Silent Environment Dependencies Session 2026-08-31, 'Source - Public Release, Corpus Purge and History Squash Session 2026-09-01', Source - Publish-Remote Gate and Issue Triage Session 2026-09-01, Source - Private-Instance Merge Correction and Issue 30 Session 2026-09-01] sources: [Source - Copilot Skill Restructure Instructions, Source - Conversation - AGENTS.md Skill Restructuring Session 2026-08-04, Source - Conversation - Versioning CI-CD and Content Migration Session 2026-08-30, Source - Conversation - Comma Bug Budget Refund and Lint Report Path Session 2026-08-31, Source - Conversation - Issue Triage Labels and TODO Retirement Session 2026-08-31, Source - Conversation - Write-Once Frontmatter Fields and touch --set Session 2026-08-31, Source - Conversation - Gate Counting and Measured Calibration Session 2026-08-31, Source - Conversation - Two Round-Trip Defects Found by an Ingest Session 2026-08-31, Source - Conversation - Hardening the Test Suite Against Silent Environment Dependencies Session 2026-08-31, 'Source - Public Release, Corpus Purge and History Squash Session 2026-09-01', Source - Publish-Remote Gate and Issue Triage Session 2026-09-01, Source - Private-Instance Merge Correction and Issue 30 Session 2026-09-01, Source - MCP Read Server Implementation Session 2026-09-02, Source - Version Part Nomenclature and Breaking Change Gate Session 2026-09-02]
confidence: 0.90 confidence: 0.90
confidence_base: 0.90 confidence_base: 0.90
provenance: mixed provenance: mixed
summary: Deterministischer Wissenskompiler (raw/ -> kb/); seit 2.0.0 unter dem Namen Chemenu, seit 2026-09-01 oeffentlich als Testbett und Demo unter AGPL-3.0/CC-BY-4.0 summary: Deterministischer Wissenskompiler (raw/ -> kb/); seit 2.0.0 unter dem Namen Chemenu; Issue 26 zur Versionsstellen-Nomenklatur in 2.5.0 geschlossen
--- ---
# Chemenu # Chemenu
@@ -63,6 +63,7 @@ Das Repository hat bereits ein deterministisches CLI, `tools/wikitool` (Python,
- **wendet an:** [[Delete Rather Than Anonymize]] - **wendet an:** [[Delete Rather Than Anonymize]]
- **wendet an:** [[Dual Licensing by File Plan]] - **wendet an:** [[Dual Licensing by File Plan]]
- **setzt um:** [[Publish-Remote Gate]] - **setzt um:** [[Publish-Remote Gate]]
- **enthaelt:** [[MCP-Leseserver]]
## Details ## Details
@@ -142,6 +143,13 @@ Repositorys selbst und keine Aussagen aus einer Rohdatenquelle; der Änderungsda
## Historie ## Historie
- 2026-09-02 - `2.2.3`-`2.4.1`: Issue #36s vierstufige Sequenz zum [[MCP-Leseserver]] umgesetzt
([[Publish-Remote Gate]] scharf gestellt, Lesepfad gehärtet, Root-Auflösung von der Importzeit
gelöst, der Server selbst), dazu `INSTALL-MCP.md` und Issue #37 für das noch fehlende
Container-Image. Alle vier Sequenz-Issues geschlossen; #36 blieb offen, weil sein eigenes
Abschlusskriterium - ein Konsument, der nachweislich über die
Authentifizierungs-Middleware antwortet - erst mit #37 erfüllbar
ist.[^s-mcp-read-server-implementation-session-2026-09-02]
- 2026-09-01 - `2.0.0` (Commit `9a7abe6`, 121 Dateien, 730 Tests grün): Rebranding von - 2026-09-01 - `2.0.0` (Commit `9a7abe6`, 121 Dateien, 730 Tests grün): Rebranding von
`llm-wiki-test1` auf **Chemenu** nach Gitea-Issue #3 - Repo-Rename, Produktname, `llm-wiki-test1` auf **Chemenu** nach Gitea-Issue #3 - Repo-Rename, Produktname,
Release-Artefakt (`chemenu-stack-<version>.tar.gz`), Release-Feed, und das Python-Paket Release-Artefakt (`chemenu-stack-<version>.tar.gz`), Release-Feed, und das Python-Paket
@@ -151,7 +159,8 @@ Repositorys selbst und keine Aussagen aus einer Rohdatenquelle; der Änderungsda
auf den alten Repo-Pfad und lässt sich per Invariante 1 nicht von Hand reparieren. Die auf den alten Repo-Pfad und lässt sich per Invariante 1 nicht von Hand reparieren. Die
erste Einschätzung lautete `1.9.0` und wurde von Torben korrigiert; die Lücke in der Doku, erste Einschätzung lautete `1.9.0` und wurde von Torben korrigiert; die Lücke in der Doku,
die dazu führte - MAJOR ist dort als Inhaltsmigration statt als Kompatibilitätsbruch die dazu führte - MAJOR ist dort als Inhaltsmigration statt als Kompatibilitätsbruch
beschrieben - liegt als Issue #26. Verzeichnet in `CHANGES.md` (`2.0.0`). beschrieben - war Issue #26 und wurde in `2.5.0` geschlossen (siehe [[KB Stack Versioning]]).
Verzeichnet in `CHANGES.md` (`2.0.0`).
- 2026-08-31 - `1.8.1` (Commit `a243a4a`, Korrektur `2b7b3cb`): Test-Coverage wird in CI - 2026-08-31 - `1.8.1` (Commit `a243a4a`, Korrektur `2b7b3cb`): Test-Coverage wird in CI
gemessen und als Artefakt ausgewiesen, ohne `--cov-fail-under` - siehe gemessen und als Artefakt ausgewiesen, ohne `--cov-fail-under` - siehe
Messen vor Schwelle. Die Messung deckte einen `dist export`-Fehler auf: Coverage-Ausgabe Messen vor Schwelle. Die Messung deckte einen `dist export`-Fehler auf: Coverage-Ausgabe
@@ -219,6 +228,8 @@ Repositorys selbst und keine Aussagen aus einer Rohdatenquelle; der Änderungsda
- [[Source - Publish-Remote Gate and Issue Triage Session 2026-09-01]] - [[Source - Publish-Remote Gate and Issue Triage Session 2026-09-01]]
- [[Publish-Remote Gate]] - [[Publish-Remote Gate]]
- [[Source - Private-Instance Merge Correction and Issue 30 Session 2026-09-01]] - [[Source - Private-Instance Merge Correction and Issue 30 Session 2026-09-01]]
- [[MCP-Leseserver]]
- [[Source - Version Part Nomenclature and Breaking Change Gate Session 2026-09-02]]
## Fußnoten ## Fußnoten
@@ -230,3 +241,4 @@ Repositorys selbst und keine Aussagen aus einer Rohdatenquelle; der Änderungsda
[^s-conversation-gate-counting-and-measured-calibration-session-2026-08-31]: [[Source - Conversation - Gate Counting and Measured Calibration Session 2026-08-31]] [^s-conversation-gate-counting-and-measured-calibration-session-2026-08-31]: [[Source - Conversation - Gate Counting and Measured Calibration Session 2026-08-31]]
[^s-conversation-write-once-frontmatter-fields-and-touch-set-session-2026-08-31]: [[Source - Conversation - Write-Once Frontmatter Fields and touch --set Session 2026-08-31]] [^s-conversation-write-once-frontmatter-fields-and-touch-set-session-2026-08-31]: [[Source - Conversation - Write-Once Frontmatter Fields and touch --set Session 2026-08-31]]
[^s-conversation-comma-bug-budget-refund-and-lint-report-path-session-2026-08-31]: [[Source - Conversation - Comma Bug Budget Refund and Lint Report Path Session 2026-08-31]] [^s-conversation-comma-bug-budget-refund-and-lint-report-path-session-2026-08-31]: [[Source - Conversation - Comma Bug Budget Refund and Lint Report Path Session 2026-08-31]]
[^s-mcp-read-server-implementation-session-2026-09-02]: [[Source - MCP Read Server Implementation Session 2026-09-02]]
+22 -11
View File
@@ -3,13 +3,13 @@ type: types/entity.md
entity_type: tool entity_type: tool
tags: [search, markdown, cli, local] tags: [search, markdown, cli, local]
created: 2026-07-26 created: 2026-07-26
modified: 2026-08-29 modified: 2026-09-02
related: [Obsidian, LLM Wiki Pattern] related: [Obsidian, LLM Wiki Pattern]
sources: [Source - LLM Wiki Pattern] sources: [Source - LLM Wiki Pattern, Source - qmd - GitHub Repository]
confidence: 0.85 confidence: 0.80
confidence_base: 0.85 confidence_base: 0.80
provenance: sourced provenance: sourced
summary: Lokale Suchmaschine für Markdown-Dateien mit hybrider BM25-Vektor-Suche und LLM-Reranking. summary: 'Lokale Suchmaschine fuer Markdown-Dateien: TypeScript/Node.js/Bun, SQLite-FTS5-BM25 plus sqlite-vec-Vektorsuche plus node-llama-cpp-LLM-Reranking.'
--- ---
# qmd # qmd
@@ -22,11 +22,12 @@ qmd ist eine lokale Suchmaschine, die speziell für Markdown-Dateien ausgelegt i
## Kerndaten ## Kerndaten
- **Typ:** CLI-Tool - **Typ:** CLI-Tool
- **Sprache:** Nicht angegeben (wahrscheinlich Go oder Rust) - **Sprache:** TypeScript, Laufzeit Node.js oder Bun
- **Such-Typen:** Hybrid (BM25 + Vector) - **Such-Typen:** Hybrid (SQLite-FTS5-BM25 + `sqlite-vec`-Vektorsuche)
- **Neu-Ranking:** LLM-basiert - **Neu-Ranking:** LLM-basiert über `node-llama-cpp` (lokale GGUF-Modelle)
- **Bereitstellung:** On-device/lokal - **Bereitstellung:** On-device/lokal
- **Repository:** https://github.com/tobi/qmd - **Repository:** https://github.com/tobi/qmd
- **Installation:** `npm install -g @tobilu/qmd` oder `bun install -g @tobilu/qmd`
## Beziehungen ## Beziehungen
@@ -50,11 +51,13 @@ qmd ist eine lokale Suchmaschine, die speziell für Markdown-Dateien ausgelegt i
## Installation und Verwendung ## Installation und Verwendung
```bash ```bash
# Installation (hypothetisch, siehe aktuelles Repo für Details) # Installation (Node oder Bun)
go install github.com/tobi/qmd@latest npm install -g @tobilu/qmd
# oder
bun install -g @tobilu/qmd
# Suche von CLI # Suche von CLI
qmd search "knowledge management" qmd query "knowledge management"
# Als MCP-Server für LLM-Integration verwenden # Als MCP-Server für LLM-Integration verwenden
qmd server qmd server
@@ -74,9 +77,17 @@ qmd server
## Historie ## Historie
- [2026-09-02] - Sprachangabe korrigiert. Die Seite trug seit ihrer Erstellung "Nicht angegeben
(wahrscheinlich Go oder Rust)" bei `confidence: 0.85` - eine Vermutung mit einer Konfidenz, die
für eine Vermutung zu hoch war. Direkt gegen das Repository geprüft: TypeScript, Laufzeit
Node.js/Bun, SQLite-FTS5-BM25 + `sqlite-vec` + `node-llama-cpp`.[^s-qmd-github-repository]
- [2026-07-26] - Entity-Seite während der Aufnahme des LLM-Wiki-Pattern-Artikels erstellt - [2026-07-26] - Entity-Seite während der Aufnahme des LLM-Wiki-Pattern-Artikels erstellt
## Siehe auch ## Siehe auch
- [[LLM Wiki Pattern]] - [[LLM Wiki Pattern]]
- [[Obsidian]] - [[Obsidian]]
## Fußnoten
[^s-qmd-github-repository]: [[Source - qmd - GitHub Repository]]
+49 -6
View File
@@ -3,13 +3,13 @@ type: types/entity.md
entity_type: tool entity_type: tool
tags: [cli, automation, deterministic, wiki-management] tags: [cli, automation, deterministic, wiki-management]
created: 2026-08-03 created: 2026-08-03
modified: 2026-09-01 modified: 2026-09-02
related: [Semantic Lint Automation, Session Orientation, Iteration and Cost Limits, KB Stack Versioning, KB Migration, Personalization Plane, Detect-Repair Asymmetry, Write-Once Frontmatter Fields, Denylist over Allowlist, Command Round-Trip Integrity, Green Suite Blind Spot, Ambient Environment Dependency, Structural Enforcement over Documented Rule, Optional Instance Context File] related: [Semantic Lint Automation, Session Orientation, Iteration and Cost Limits, KB Stack Versioning, KB Migration, Personalization Plane, Detect-Repair Asymmetry, Write-Once Frontmatter Fields, Denylist over Allowlist, Command Round-Trip Integrity, Green Suite Blind Spot, Ambient Environment Dependency, Structural Enforcement over Documented Rule, Optional Instance Context File, MCP-Leseserver]
sources: [Source - LLM Improvements Codex Analysis, Source - LLM Improvements Sonnet Analysis, Source - Copilot Skill Restructure Instructions, Source - Conversation - AGENTS.md Skill Restructuring Session 2026-08-04, Source - LLM Improvements Production Agent Gaps 2026, Source - Conversation - Versioning CI-CD and Content Migration Session 2026-08-30, Source - Conversation - Comma Bug Budget Refund and Lint Report Path Session 2026-08-31, Source - Conversation - Issue Triage Labels and TODO Retirement Session 2026-08-31, Source - Conversation - Auto Mode and Tool Choice Session 2026-08-31, Source - Conversation - Write-Once Frontmatter Fields and touch --set Session 2026-08-31, Source - Conversation - Gate Counting and Measured Calibration Session 2026-08-31, Source - Conversation - Two Round-Trip Defects Found by an Ingest Session 2026-08-31, Source - Conversation - Hardening the Test Suite Against Silent Environment Dependencies Session 2026-08-31, Source - Conversation - ENVIRONMENT.md as an Optional Third Session-Level File Session 2026-08-31, 'Source - Public Release, Corpus Purge and History Squash Session 2026-09-01', Source - Publish-Remote Gate and Issue Triage Session 2026-09-01, Source - Private-Instance Merge Correction and Issue 30 Session 2026-09-01] sources: [Source - LLM Improvements Codex Analysis, Source - LLM Improvements Sonnet Analysis, Source - Copilot Skill Restructure Instructions, Source - Conversation - AGENTS.md Skill Restructuring Session 2026-08-04, Source - LLM Improvements Production Agent Gaps 2026, Source - Conversation - Versioning CI-CD and Content Migration Session 2026-08-30, Source - Conversation - Comma Bug Budget Refund and Lint Report Path Session 2026-08-31, Source - Conversation - Issue Triage Labels and TODO Retirement Session 2026-08-31, Source - Conversation - Auto Mode and Tool Choice Session 2026-08-31, Source - Conversation - Write-Once Frontmatter Fields and touch --set Session 2026-08-31, Source - Conversation - Gate Counting and Measured Calibration Session 2026-08-31, Source - Conversation - Two Round-Trip Defects Found by an Ingest Session 2026-08-31, Source - Conversation - Hardening the Test Suite Against Silent Environment Dependencies Session 2026-08-31, Source - Conversation - ENVIRONMENT.md as an Optional Third Session-Level File Session 2026-08-31, 'Source - Public Release, Corpus Purge and History Squash Session 2026-09-01', Source - Publish-Remote Gate and Issue Triage Session 2026-09-01, Source - Private-Instance Merge Correction and Issue 30 Session 2026-09-01, Source - MCP Read Server Implementation Session 2026-09-02, Source - Version Part Nomenclature and Breaking Change Gate Session 2026-09-02]
confidence: 0.90 confidence: 0.90
confidence_base: 0.90 confidence_base: 0.90
provenance: sourced provenance: sourced
summary: Deterministisches CLI fuer alle mechanischen Wiki-Operationen; seit 2.0.0 liegt es im Python-Paket chemenu, das Kommando heisst weiterhin wikitool summary: Deterministisches CLI fuer alle mechanischen Wiki-Operationen; seit 2.0.0 im Paket chemenu, seit 2.4.0 zusaetzlich als MCP-Leseserver erreichbar, seit 2.5.0 mit Breaking-Change-Pflichtmarker
--- ---
# wikitool # wikitool
@@ -51,6 +51,7 @@ Wie in der Codex-Analyse vermerkt, bietet wikitool die deterministische Grundlag
- **zeigte:** [[Ambient Environment Dependency]] - **zeigte:** [[Ambient Environment Dependency]]
- **setzt um:** [[Structural Enforcement over Documented Rule]] - **setzt um:** [[Structural Enforcement over Documented Rule]]
- **setzt um:** [[Optional Instance Context File]] - **setzt um:** [[Optional Instance Context File]]
- **implementiert:** [[MCP-Leseserver]]
## Befehle ## Befehle
@@ -73,6 +74,9 @@ wikitool bietet die folgenden Befehlskategorien:
Seitenkopf zurückgefaltet statt abgelehnt, und weil der Block immer zuletzt gerendert wird, Seitenkopf zurückgefaltet statt abgelehnt, und weil der Block immer zuletzt gerendert wird,
richtet die erste Zitatoperation eine verrutschte Seite von selbst wieder ein[^s-conversation-two-round-trip-defects-found-by-an-ingest-session-2026-08-31] richtet die erste Zitatoperation eine verrutschte Seite von selbst wieder ein[^s-conversation-two-round-trip-defects-found-by-an-ingest-session-2026-08-31]
- **Abfrage:** `search` - Textsuche über `kb/` durch ein austauschbares Backend (`rg` heute), plus `--field`-Prädikate, die auf Frontmatter evaluiert werden (`entity_type=system`, `confidence>=0.8`, `tags=k8s`, `!source_url`). Ohne Text ist es eine reine strukturierte Abfrage. Schreibgeschützt und ausgenommen von der Iteration-Budget-Gate, da Abfrage das Lesen statt das Iterieren ist - **Abfrage:** `search` - Textsuche über `kb/` durch ein austauschbares Backend (`rg` heute), plus `--field`-Prädikate, die auf Frontmatter evaluiert werden (`entity_type=system`, `confidence>=0.8`, `tags=k8s`, `!source_url`). Ohne Text ist es eine reine strukturierte Abfrage. Schreibgeschützt und ausgenommen von der Iteration-Budget-Gate, da Abfrage das Lesen statt das Iterieren ist
- **MCP-Server:** `python -m chemenu.mcp` (`--transport stdio|streamable-http`) - kein
`wikitool`-Unterbefehl, sondern ein zweiter Einstiegspunkt über `chemenu.api.Corpus`, derselbe
Kern wie `search`/`types`/`lint`. Siehe [[MCP-Leseserver]]
- **Indizierung:** `index rebuild` - regeneriert die `kb/index.md`-Map plus eine pro-Sammlung `INDEX.md`, wobei ein Bereich bei 50 Zeilen in seine eigene Shard aufgeteilt wird - **Indizierung:** `index rebuild` - regeneriert die `kb/index.md`-Map plus eine pro-Sammlung `INDEX.md`, wobei ein Bereich bei 50 Zeilen in seine eigene Shard aufgeteilt wird
- **Herkunft:** `sources coverage`, `sources trace`, `sources rebuild-index` - **Herkunft:** `sources coverage`, `sources trace`, `sources rebuild-index`
- **Protokollierung:** `log append`, `log status` - **Protokollierung:** `log append`, `log status`
@@ -82,8 +86,15 @@ wikitool bietet die folgenden Befehlskategorien:
- **Veröffentlichung:** `publish` - zählt seit `1.5.0` nur noch Dateien, die eine Entscheidung tragen: Pfade unter `work/` und generierte Dateien (`kb/index.md`, `kb/log.md`, `kb/provenance.md`, jede `INDEX.md`) werden committet und gepusht, aber nicht gegen die Schwelle gezählt; die Weigerungszeile weist beide Gründe getrennt aus[^s-conversation-gate-counting-and-measured-calibration-session-2026-08-31]. Siehe [[Mass-Update Gate]] - **Veröffentlichung:** `publish` - zählt seit `1.5.0` nur noch Dateien, die eine Entscheidung tragen: Pfade unter `work/` und generierte Dateien (`kb/index.md`, `kb/log.md`, `kb/provenance.md`, jede `INDEX.md`) werden committet und gepusht, aber nicht gegen die Schwelle gezählt; die Weigerungszeile weist beide Gründe getrennt aus[^s-conversation-gate-counting-and-measured-calibration-session-2026-08-31]. Siehe [[Mass-Update Gate]]
- **Budget:** `budget status`, `budget reset` - Obergrenze seit `1.2.0` 60 Aufrufe je Sitzung; ein Aufruf, der über `_util.fail()` abgelehnt wurde, bekommt seinen Slot zurück und bleibt trotzdem in `recent`, damit der Loop-Breaker ihn sieht[^s-conversation-comma-bug-budget-refund-and-lint-report-path-session-2026-08-31]. Siehe [[Iteration and Cost Limits]] - **Budget:** `budget status`, `budget reset` - Obergrenze seit `1.2.0` 60 Aufrufe je Sitzung; ein Aufruf, der über `_util.fail()` abgelehnt wurde, bekommt seinen Slot zurück und bleibt trotzdem in `recent`, damit der Loop-Breaker ihn sieht[^s-conversation-comma-bug-budget-refund-and-lint-report-path-session-2026-08-31]. Siehe [[Iteration and Cost Limits]]
- **Versionierung:** `version bump`, `version check` - `bump` schreibt die Stack-Version in die - **Versionierung:** `version bump`, `version check` - `bump` schreibt die Stack-Version in die
Wurzeldatei `VERSION` und verweigert einen `MAJOR`-Sprung ohne Migrationsdokument, sofern er Wurzeldatei `VERSION`. Kompatibilität (ist die neue Version ein Drop-in-Ersatz - vorwärts ohne
nicht ausdrücklich mit `--no-migration "<Begründung>"` gesetzt wird; `check` ist der einzige Handarbeit, rückwärts noch downgradebar) und Inhaltsmigration sind seit `2.5.0` zwei getrennte
Fragen: ein Grenzübertritt verlangt zwingend `--breaking "<was aufhört zu funktionieren>"`,
verweigert auf jedem anderen Bump, und *zusätzlich* entweder ein Migrationsdokument oder
`--no-migration "<Begründung>"`, wenn `kb/` unangetastet bleibt. Beide Marker landen als
eigene Zeile im `CHANGES.md`-Eintrag (`**Breaking Change:**` vor `**Migration:**`) und werden
von `docs verify` unabhängig
voneinander geprüft[^s-version-part-nomenclature-and-breaking-change-gate-session-2026-09-02].
Siehe [[KB Stack Versioning]]. `check` ist der einzige
Befehl, der einen Netzaufruf machen darf - ohne Schlüssel, mit Timeout und injizierbarem Befehl, der einen Netzaufruf machen darf - ohne Schlüssel, mit Timeout und injizierbarem
Fetch, damit Tests nie ein Netz Fetch, damit Tests nie ein Netz
berühren[^s-conversation-versioning-ci-cd-and-content-migration-session-2026-08-30] berühren[^s-conversation-versioning-ci-cd-and-content-migration-session-2026-08-30]
@@ -147,6 +158,35 @@ ist[^s-conversation-versioning-ci-cd-and-content-migration-session-2026-08-30].
## Historie ## Historie
- 2026-09-02 - `2.5.0` (Commit `31662dc`, 806 Tests grün, 6 neu): Gitea-Issue #26 geschlossen.
`instructions/dev/version-parts.md` (neu, `instructions/dev/` - kein Verweis aus einem
ausgelieferten Artefakt, `instructions verify` hätte einen dangelnden Verweis nach
`dist export` gemeldet) trennt die Kompatibilitäts- von der Migrationsfrage: Drop-in-Test in
beiden Richtungen, Katalog der Brüche mit unangetastetem `kb/` (Update-Pfad, Artefaktname,
Import-Name, Flag/Envvar, Shape einer maschinengelesenen Datei), `2.0.0` als Fallbeispiel.
`version bump` bekommt `--breaking "<was aufhört zu funktionieren>"`, bei jedem
Grenzübertritt Pflicht und auf jedem anderen Bump verweigert; `docs verify` prüft das über
eine zweite, von der Migrationsprüfung unabhängige Regel. `stack-dev` bekommt einen
Entscheidungspunkt: kein Grenzübertritt aus eigener Initiative, erst Bruch, Handarbeit je
Instanz und Alternativen (Shim, aufschieben/bündeln, kompatibel/brechend mit
Deprecation-Fenster aufspalten) vorlegen, dann
Freigabe[^s-version-part-nomenclature-and-breaking-change-gate-session-2026-09-02].
- 2026-09-02 - `2.2.3`-`2.4.1`: die vierstufige Sequenz aus Issue #36 (Publish-Remote-Gate
scharf, Lesepfad gehärtet, Root-Auflösung und Bibliotheksgrenze, [[MCP-Leseserver]]) plus
Menschendoku. `.wikitool-remotes.json` war trotz gegenteiliger Dokumentation nicht gesetzt -
angelegt und gegen ein erfundenes Ziel gegengeprüft (Exit 42). Sechs Lesepfad-Härtungen:
gelöschter ReDoS-Zweig in der Ranking-Funktion, ein 30-Sekunden-`rg`-Timeout, ein
YAML-Alias-Refusal (267 B → 672.603 Knoten reproduziert), `CSafeLoader` (265 ms → 54 ms),
gemeldetes statt geschlucktes kaputtes Frontmatter, ein Korpus-Cache, der nie einen
schmutzigen Arbeitsbaum cacht. `config.ROOT` und alle abgeleiteten Pfade waren zur Importzeit
gebunden - jetzt lazy aufgelöst (`$CHEMENU_ROOT` → Walk-up), mit einem eigenen
`config.reset()` gegen eine Falle, in die `monkeypatch`s eigener Teardown sonst zurückläuft.
Neuer typerloser Lesekern (`search/service.py`, `lint_core.py`, `types_core.py`) hinter
`chemenu.api.Corpus`, darüber der MCP-Server: `search`/`types`/`describe_type`/`lint`/`status`,
strukturell ohne Schreibpfad, jede Antwort mit Commit-Stempel. `wikitool search` end-to-end
593 ms → 347 ms. Menschendoku `INSTALL-MCP.md`, ausgeliefert über `dist_cmd.ROOT_FILES`.
Siehe [[Publish-Remote Gate]], [[Mass-Update Gate]],
[[MCP-Leseserver]][^s-mcp-read-server-implementation-session-2026-09-02]
- 2026-09-01 - `2.1.0`-`2.2.2`: Vorbereitung und Durchführung der Veröffentlichung. `dist export` - 2026-09-01 - `2.1.0`-`2.2.2`: Vorbereitung und Durchführung der Veröffentlichung. `dist export`
bekommt `REQUIRED_ROOT_FILES` (fehlende Lizenzdateien lassen den Export scheitern statt still bekommt `REQUIRED_ROOT_FILES` (fehlende Lizenzdateien lassen den Export scheitern statt still
weiterzulaufen) und `find_leaks()` (strukturelle Prüfung des fertigen Export-Plans gegen weiterzulaufen) und `find_leaks()` (strukturelle Prüfung des fertigen Export-Plans gegen
@@ -267,6 +307,7 @@ ist[^s-conversation-versioning-ci-cd-and-content-migration-session-2026-08-30].
- [[Source - Public Release, Corpus Purge and History Squash Session 2026-09-01]] - [[Source - Public Release, Corpus Purge and History Squash Session 2026-09-01]]
- [[Source - Publish-Remote Gate and Issue Triage Session 2026-09-01]] - [[Source - Publish-Remote Gate and Issue Triage Session 2026-09-01]]
- [[Source - Private-Instance Merge Correction and Issue 30 Session 2026-09-01]] - [[Source - Private-Instance Merge Correction and Issue 30 Session 2026-09-01]]
- [[MCP-Leseserver]]
## Fußnoten ## Fußnoten
@@ -281,3 +322,5 @@ ist[^s-conversation-versioning-ci-cd-and-content-migration-session-2026-08-30].
[^s-llm-improvements-codex-analysis]: [[Source - LLM Improvements Codex Analysis]] [^s-llm-improvements-codex-analysis]: [[Source - LLM Improvements Codex Analysis]]
[^s-public-release-corpus-purge-and-history-squash-session-2026-09-01]: [[Source - Public Release, Corpus Purge and History Squash Session 2026-09-01]] [^s-public-release-corpus-purge-and-history-squash-session-2026-09-01]: [[Source - Public Release, Corpus Purge and History Squash Session 2026-09-01]]
[^s-publish-remote-gate-and-issue-triage-session-2026-09-01]: [[Source - Publish-Remote Gate and Issue Triage Session 2026-09-01]] [^s-publish-remote-gate-and-issue-triage-session-2026-09-01]: [[Source - Publish-Remote Gate and Issue Triage Session 2026-09-01]]
[^s-mcp-read-server-implementation-session-2026-09-02]: [[Source - MCP Read Server Implementation Session 2026-09-02]]
[^s-version-part-nomenclature-and-breaking-change-gate-session-2026-09-02]: [[Source - Version Part Nomenclature and Breaking Change Gate Session 2026-09-02]]
+6 -6
View File
@@ -13,12 +13,12 @@ The page tables live in a generated `INDEX.md` inside each collection, linked be
## Statistics ## Statistics
- **Total Pages:** 176 - **Total Pages:** 180
- **Comparisons:** 1 - **Comparisons:** 1
- **Concepts:** 79 - **Concepts:** 80
- **Entities:** 72 - **Entities:** 72
- **Sources:** 24 - **Sources:** 27
- **Last Updated:** 2026-09-01 - **Last Updated:** 2026-09-02
--- ---
@@ -27,9 +27,9 @@ The page tables live in a generated `INDEX.md` inside each collection, linked be
| Collection | Pages | Index | | Collection | Pages | Index |
|------------|------:|-------| |------------|------:|-------|
| `comparisons/` | 1 | [comparisons/INDEX.md](comparisons/INDEX.md) | | `comparisons/` | 1 | [comparisons/INDEX.md](comparisons/INDEX.md) |
| `concepts/` | 79 | [concepts/INDEX.md](concepts/INDEX.md) | | `concepts/` | 80 | [concepts/INDEX.md](concepts/INDEX.md) |
| `entities/` | 72 | [entities/INDEX.md](entities/INDEX.md) | | `entities/` | 72 | [entities/INDEX.md](entities/INDEX.md) |
| `sources/` | 24 | [sources/INDEX.md](sources/INDEX.md) | | `sources/` | 27 | [sources/INDEX.md](sources/INDEX.md) |
### entities/ ### entities/
+24
View File
@@ -73,3 +73,27 @@ Source-Seite angelegt, neue Concept-Seite Publish-Remote Gate, wikitool-Historie
Source-Seite angelegt, Publish-Remote-Gate-Seite um die gemessene Merge-Semantik erweitert, quer verlinkt. Source-Seite angelegt, Publish-Remote-Gate-Seite um die gemessene Merge-Semantik erweitert, quer verlinkt.
--- ---
## [2026-09-02] ingest | MCP Read Server Implementation Session 2026-09-02
Source-Seite und neues Concept MCP-Leseserver angelegt; wikitool, Chemenu, Publish-Remote Gate und Iteration and Cost Limits aktualisiert. Deckt Issue #36s Sequenz (#34/#33/#31/#19), INSTALL-MCP.md und Issue #37 ab.
---
## [2026-09-02] update | qmd - Sprachangabe korrigiert
Direkt gegen tobi/qmd auf GitHub geprueft: TypeScript statt der geratenen 'Go oder Rust'-Angabe. Neue Quelle Source - qmd - GitHub Repository (raw/documents/), confidence_base 0.85 -> 0.80 neu begruendet, Installationsbeispiel korrigiert.
---
## [2026-09-02] update | MCP Read Server Implementation Session 2026-09-02 - Korrektur
Aufgaben-Checkbox fuer die qmd.md-Korrektur nachgezogen (war [ ], ist erledigt) und ein Korrektur-Hinweis ergaenzt: der Fidelity-Block des Rohtranskripts kuendigte ein zweites Transkript fuer die qmd-Korrektur an, das nie geschrieben wurde - die Korrektur lief stattdessen als eigene Quellen-Verifikation. raw/ ist unveraenderlich, die Korrektur steht deshalb auf der Source-Seite.
---
## [2026-09-02] ingest | raw/notes/Conversation Transcript - Version Part Nomenclature and Breaking Change Gate Session 2026-09-02.md
Neue Source-Seite, Korrektur der veralteten MAJOR-als-Migration-Formulierung in wikitool.md und KB Stack Versioning.md, Nachtrag in Chemenu.md (Issue #26 geschlossen).
---
+12 -2
View File
@@ -8,8 +8,8 @@ inline `[^cite-id]` footnote).
## Coverage Summary ## Coverage Summary
- **Total raw files:** 24 - **Total raw files:** 26
- **Covered:** 24 - **Covered:** 26
- **Uncovered:** 0 - **Uncovered:** 0
--- ---
@@ -31,6 +31,11 @@ inline `[^cite-id]` footnote).
- Covered by: [[Source - LLM Wiki Pattern]] - Covered by: [[Source - LLM Wiki Pattern]]
- Cited by: [[Andrej Karpathy]], [[ChatGPT]], [[Claude Code]], [[Dataview]], [[Knowledge Compounding]], [[LLM Wiki Pattern]], [[Marp]], [[Memex]], [[NotebookLM]], [[Obsidian]], [[Obsidian Web Clipper]], [[OpenAI Codex]], [[OpenCode]], [[Pi]], [[RAG]], [[Three-Layer Architecture]], [[Tolkien Gateway]], [[Vannevar Bush]], [[qmd]] - Cited by: [[Andrej Karpathy]], [[ChatGPT]], [[Claude Code]], [[Dataview]], [[Knowledge Compounding]], [[LLM Wiki Pattern]], [[Marp]], [[Memex]], [[NotebookLM]], [[Obsidian]], [[Obsidian Web Clipper]], [[OpenAI Codex]], [[OpenCode]], [[Pi]], [[RAG]], [[Three-Layer Architecture]], [[Tolkien Gateway]], [[Vannevar Bush]], [[qmd]]
### `raw/documents/qmd - GitHub Repository.md`
- Covered by: [[Source - qmd - GitHub Repository]]
- Cited by: [[qmd]]
### `raw/notes/Arch Linux Cheat Sheet.md` ### `raw/notes/Arch Linux Cheat Sheet.md`
- Covered by: [[Source - Arch Linux Cheat Sheet]] - Covered by: [[Source - Arch Linux Cheat Sheet]]
@@ -71,6 +76,11 @@ inline `[^cite-id]` footnote).
- Covered by: [[Source - Conversation - Issue Triage Labels and TODO Retirement Session 2026-08-31]] - Covered by: [[Source - Conversation - Issue Triage Labels and TODO Retirement Session 2026-08-31]]
- Cited by: [[Chemenu]], [[Detect-Repair Asymmetry]], [[Gitea]], [[Gitea Actions]], [[Gitea MCP Server]], [[Issue Label Scheme]], [[KB Stack Versioning]], [[wikitool]] - Cited by: [[Chemenu]], [[Detect-Repair Asymmetry]], [[Gitea]], [[Gitea Actions]], [[Gitea MCP Server]], [[Issue Label Scheme]], [[KB Stack Versioning]], [[wikitool]]
### `raw/notes/Conversation Transcript - MCP Read Server Implementation Session 2026-09-02.md`
- Covered by: [[Source - MCP Read Server Implementation Session 2026-09-02]]
- Cited by: [[Chemenu]], [[Iteration and Cost Limits]], [[MCP-Leseserver]], [[Publish-Remote Gate]], [[wikitool]]
### `raw/notes/Conversation Transcript - Nightly Drift-Check Workflow and doctor's Bootstrap Gap Session 2026-08-31.md` ### `raw/notes/Conversation Transcript - Nightly Drift-Check Workflow and doctor's Bootstrap Gap Session 2026-08-31.md`
- Covered by: [[Source - Conversation - Nightly Drift-Check Workflow and doctor's Bootstrap Gap Session 2026-08-31]] - Covered by: [[Source - Conversation - Nightly Drift-Check Workflow and doctor's Bootstrap Gap Session 2026-08-31]]
+13 -2
View File
@@ -1,3 +1,8 @@
---
profile: sources
required_by_stack: true
---
# kb/sources/ - Collection Contract # kb/sources/ - Collection Contract
One page per ingested source. A source page is the bridge between the untrusted material in One page per ingested source. A source page is the bridge between the untrusted material in
@@ -9,8 +14,14 @@ concluded from it. Where the source is wrong, say what it claims and let the sub
carry the correction. A source page that has been improved beyond its source is no longer carry the correction. A source page that has been improved beyond its source is no longer
evidence for anything. evidence for anything.
Inherits [kb/CONTRACT.md](../CONTRACT.md) - naming, tone, linking, provenance and confidence Inherits [kb/CONTRACT.md](../CONTRACT.md) for the rules the stack enforces - linking mechanics,
are defined there and are not restated here. provenance, citation, the confidence machinery - and
[kb/CONVENTIONS.md](../CONVENTIONS.md) for what this instance decided: language, naming forms,
tone, relationship labels, the confidence rubric. Neither is restated here.
**This collection is `required_by_stack`.** `sources coverage`, `[^cite-id]` resolution and
`kb/provenance.md` resolve against it by name, so unlike every other collection it may not be
renamed or dropped - its authoring rules below are the instance's, its existence is not.
## Types offered ## Types offered
+4 -1
View File
@@ -2,7 +2,7 @@
# kb/sources/ - Index # kb/sources/ - Index
24 page(s). Regenerated by `wikitool index rebuild`. 27 page(s). Regenerated by `wikitool index rebuild`.
## All ## All
@@ -28,8 +28,11 @@
| [[Source - LLM Improvements Sonnet Analysis]] | notes | Sonnet-Analyse, die AGENTS.md und wikitool mit Farzas Gist und awesome-llm-wiki vergleicht und die Codex-Analyse um konkrete Empfehlungen zu Qualitätsschwellen, Stilrichtlinie, Auditrhythmus und Skalierung ergänzt | 2026-08-03 | | [[Source - LLM Improvements Sonnet Analysis]] | notes | Sonnet-Analyse, die AGENTS.md und wikitool mit Farzas Gist und awesome-llm-wiki vergleicht und die Codex-Analyse um konkrete Empfehlungen zu Qualitätsschwellen, Stilrichtlinie, Auditrhythmus und Skalierung ergänzt | 2026-08-03 |
| [[Source - LLM Wiki Pattern]] | article | Grundlegendes Muster für persönliche Wissensbasen mit LLMs: ein dauerhaftes Wiki schrittweise pflegen, statt es aus den Quellen neu herzuleiten. | 2026-07-26 | | [[Source - LLM Wiki Pattern]] | article | Grundlegendes Muster für persönliche Wissensbasen mit LLMs: ein dauerhaftes Wiki schrittweise pflegen, statt es aus den Quellen neu herzuleiten. | 2026-07-26 |
| [[Source - LLM Wiki v2]] | article | Erweitertes LLM-Wiki-Muster mit Praxiserfahrungen aus agentmemory zu Memory Lifecycle, Confidence Scoring, Wissensgraph und Automatisierung. | 2026-07-26 | | [[Source - LLM Wiki v2]] | article | Erweitertes LLM-Wiki-Muster mit Praxiserfahrungen aus agentmemory zu Memory Lifecycle, Confidence Scoring, Wissensgraph und Automatisierung. | 2026-07-26 |
| [[Source - MCP Read Server Implementation Session 2026-09-02]] | notes | Sitzung, die die Sequenz aus Issue #36 umsetzt: Publish-Remote Gate scharf, Lesepfad gehaertet, Root-Aufloesung und Bibliotheksgrenze gezogen, MCP-Leseserver gebaut - vier Versionsstufen 2.2.3 bis 2.4.0, dazu INSTALL-MCP.md und Issue #37. | 2026-09-02 |
| [[Source - Private-Instance Merge Correction and Issue 30 Session 2026-09-01]] | notes | Sitzung, die eine ungeprueft niedergeschriebene Merge-Behauptung in private-instance.md durch einen empirischen Test widerlegt, die Prozedur korrigiert (2.2.1) und Issue #30 mit einem getesteten Skript sowie zwei Architekturvorschlaegen anlegt. | 2026-09-01 | | [[Source - Private-Instance Merge Correction and Issue 30 Session 2026-09-01]] | notes | Sitzung, die eine ungeprueft niedergeschriebene Merge-Behauptung in private-instance.md durch einen empirischen Test widerlegt, die Prozedur korrigiert (2.2.1) und Issue #30 mit einem getesteten Skript sowie zwei Architekturvorschlaegen anlegt. | 2026-09-01 |
| [[Source - Public Release, Corpus Purge and History Squash Session 2026-09-01]] | notes | Sitzung, die den Chemenu-Stack von einer privaten Testinstanz in ein oeffentliches Repo ueberfuehrt: Korpus geloescht statt anonymisiert, Git-History auf einen Commit gesquashed, AGPL-3.0/CC-BY-4.0-Dual-Lizenz gewaehlt, dist export um einen Leak-Canary gehaertet. | 2026-09-01 | | [[Source - Public Release, Corpus Purge and History Squash Session 2026-09-01]] | notes | Sitzung, die den Chemenu-Stack von einer privaten Testinstanz in ein oeffentliches Repo ueberfuehrt: Korpus geloescht statt anonymisiert, Git-History auf einen Commit gesquashed, AGPL-3.0/CC-BY-4.0-Dual-Lizenz gewaehlt, dist export um einen Leak-Canary gehaertet. | 2026-09-01 |
| [[Source - Publish-Remote Gate and Issue Triage Session 2026-09-01]] | notes | Sitzung, die ein drittes, Token-loses Gate fuer publish baut, instructions/private-instance.md schreibt, sechs Gitea-Issues auf den Rename und die neue Architektur nachzieht und die Actions-Run-Historie entfernen laesst. | 2026-09-01 | | [[Source - Publish-Remote Gate and Issue Triage Session 2026-09-01]] | notes | Sitzung, die ein drittes, Token-loses Gate fuer publish baut, instructions/private-instance.md schreibt, sechs Gitea-Issues auf den Rename und die neue Architektur nachzieht und die Actions-Run-Historie entfernen laesst. | 2026-09-01 |
| [[Source - qmd - GitHub Repository]] | document | GitHub-API-Metadaten, package.json und README-Auszuege von tobi/qmd: TypeScript/Node/Bun statt Go oder Rust, BM25 (SQLite FTS5) plus Vektor-Suche (sqlite-vec) plus LLM-Reranking ueber node-llama-cpp. | 2026-09-02 |
| [[Source - Version Part Nomenclature and Breaking Change Gate Session 2026-09-02]] | notes | Sitzung, die die Versionsstelle als Kompatibilitaets- statt Migrationsfrage praezisiert und einen Freigabe-Ablauf fuer Breaking Changes in stack-dev einfuehrt | 2026-09-02 |
| [[Source - Wine]] | notes | Wine-Konfiguration für Arch Linux: pacman-NoExtract-Einstellungen und Bottles-Runtime-Optionen einschließlich Proton- und Lutris-Varianten. | 2026-08-01 | | [[Source - Wine]] | notes | Wine-Konfiguration für Arch Linux: pacman-NoExtract-Einstellungen und Bottles-Runtime-Optionen einschließlich Proton- und Lutris-Varianten. | 2026-08-01 |
@@ -0,0 +1,109 @@
---
type: types/source.md
source_type: notes
author: Torben
raw_files: [raw/notes/Conversation Transcript - MCP Read Server Implementation Session 2026-09-02.md]
source_language: de
date: 2026-09-02
tags: []
entities: [wikitool, Chemenu]
concepts: [Publish-Remote Gate, Mass-Update Gate, Iteration and Cost Limits, MCP-Leseserver]
summary: 'Sitzung, die die Sequenz aus Issue #36 umsetzt: Publish-Remote Gate scharf, Lesepfad gehaertet, Root-Aufloesung und Bibliotheksgrenze gezogen, MCP-Leseserver gebaut - vier Versionsstufen 2.2.3 bis 2.4.0, dazu INSTALL-MCP.md und Issue #37.'
---
# Source: MCP Read Server Implementation Session 2026-09-02
**Autor:** Torben
**Datum:** 2026-09-02
**Raw-Dateien:** raw/notes/Conversation Transcript - MCP Read Server Implementation Session 2026-09-02.md
**Typ:** Notes
## Zusammenfassung
Diese Sitzung arbeitet die vierstufige Sequenz aus Issue #36 ab, dem Sammel-Issue für den Weg
zum MCP-Leseserver: #34 (Publish-Remote-Gate scharf stellen), #33 (Lesepfad vor der Exposition
härten), #31 (Root-Auflösung von der Importzeit lösen und eine Bibliotheksgrenze ziehen), #19
(der Leseserver selbst). Jeder Schritt endet mit einem Versions-Bump und einem Testlauf; die
Reihenfolge folgt dem Master-Issue, weil #33 dieselben Dateien anfasst, die #31 strukturell
umbaut, und #31 die Grenze liefert, auf der #19 aufsetzt.
`.wikitool-remotes.json` fehlte in diesem Checkout trotz Dokumentation, die das Gegenteil
behauptete - angelegt und gegen ein erfundenes Ziel gegengeprüft (Exit 42). Der Lesepfad bekam
sechs Fixes gegen einen 267-Byte-YAML-Alias, der zu 672.603 Knoten expandiert, gegen einen
ReDoS-Zweig in der Ranking-Funktion, einen fehlenden Subprozess-Timeout, und einen Korpus-Cache,
der nie einen schmutzigen Arbeitsbaum cacht. `config.ROOT` und alle abgeleiteten Pfade waren zur
Importzeit gebunden; die Auflösung ist jetzt lazy (`CHEMENU_ROOT` → Walk-up), und der reine
Lesekern (`search/service.py`, `lint_core.py`, `types_core.py`) importiert kein `typer` mehr.
Der MCP-Server (`tools/chemenu/mcp/`) exponiert `search`/`types`/`describe_type`/`lint`/`status`
über `chemenu.api.Corpus` - strukturell ohne Schreibpfad, mit Commit-Stempel auf jeder Antwort
und einer Startverweigerung, falls Telemetrie in den bedienten Baum schreiben würde.
Nach Freigabe des 37-Datei-Changesets (Mass-Update-Gate, Token `46442f4419c1`) folgten
`INSTALL-MCP.md` für Menschen, ein Verweis auf die separate Traefik-ForwardAuth-Middleware
(`gitea-mcp-forward-auth`), und Issue #37 für das noch fehlende Container-Image - mit den
konkreten CI-Vorlagen aus `gitea-mcp-forward-auth` (Registry-Push) und `gitea-mcp`
(Dockerfile-Form, aber DockerHub statt der eigenen Registry). Alle vier Sequenz-Issues wurden
geschlossen, #36 blieb offen, weil sein eigenes Abschlusskriterium - ein Konsument, der
nachweislich über die Middleware antwortet - erst mit #37 erfüllbar ist.
## Kernaussagen
- Gemessen: Korpus-Parse 265 ms → 54 ms (`CSafeLoader`), `wikitool search` end-to-end
593 ms → 347 ms; die verbleibenden ~262 ms sind Modulimport und entfallen erst im residenten
MCP-Prozess.
- Der ReDoS-Zweig (`_contains` mit `re.search` gegen nutzergesteuerten Regex) wurde gelöscht,
nicht begrenzt - `rg` wendet das Muster ohnehin mit einer linearen Engine an, bevor die
Funktion je läuft.
- `monkeypatch.setattr(config, "ROOT", ...)` baute nach der lazy-Auflösung die stale Bindung
beim Teardown wieder auf, weil es den *aufgelösten* alten Wert zurückschreibt - `config.reset()`
musste dazukommen, in derselben autouse-Fixture, die das Problem eine Ebene höher (Umgebungsvariablen)
bereits kannte.
- Der MCP-Server hat keinen Schreibpfad, weil `chemenu.api` nichts unter `chemenu.commands`
importiert - nicht, weil eine Liste gefiltert wird. Ein Test importiert das Servermodul in
einem frischen Interpreter und prüft `sys.modules`.
- Ein Stempel-Bug wurde beim Schreiben des Golden-Tests selbst gefunden: `_stamp()` fragte nach
der *aktuellen* statt der beim Laden tatsächlich gelesenen Revision und hätte bei einem
minimal verzögerten zweiten Zugriff `commit: null` auf einem sauberen Baum liefern können.
- `gitea-mcp` ist als Registry-Vorlage ungeeignet - sein Release-Workflow pusht nach DockerHub
(Fork des Upstream), nicht in die eigene Gitea-Registry.
## Aufgaben
- [x] #34, #33, #31, #19 umgesetzt und mit Abschlusskommentar geschlossen
- [x] `INSTALL-MCP.md` geschrieben, in `INSTALL.md`/`README.md` verlinkt, in `dist export` aufgenommen
- [x] Issue #37 (Container-Image) angelegt, mit neun offenen Entscheidungen benannt
- [ ] #37 selbst umsetzen
- [ ] #36 schließen, sobald #37 den Middleware-Nachweis liefert
- [ ] #23 (Env-Var-Erzwingung) - `CHEMENU_ROOT` wurde von Hand in `_WIKITOOL_ENV` eingetragen
- [x] `kb/entities/tools/qmd.md` - falsche Sprachangabe korrigiert (siehe Korrektur unten)
## Korrektur zum Transkript-Kopf
Der Fidelity-Block des Rohtranskripts sagt: "One of two transcripts cut from this session; the
other covers fixing `kb/entities/tools/qmd.md`". Dieses zweite Transkript wurde nie geschrieben
- `raw/` ist unveränderlich, die Korrektur gehört hierher, nicht in die Datei selbst. Tatsächlich
lief die Korrektur ohne eigenes Transkript: direkt gegen `tobi/qmd` auf GitHub geprüft und als
eigene Quelle mit eigenem Raw-Beleg abgelegt (`Source - qmd - GitHub Repository`,
`raw/documents/qmd - GitHub Repository.md`) - eine Quellen-Verifikation statt eines
Gesprächsprotokolls, was für eine Sprachangaben-Korrektur die passendere Belegform ist.
## Nicht übernommen
- Der vollständige Wortlaut der geprüften Docstrings, Kommentare und Testfälle - das Transkript
benennt Dateien und die tragenden Eigenschaften, der Code selbst ist die Quelle.
- Die exakten neun offenen Entscheidungspunkte aus Issue #37 (Korpus im Image vs. Volume,
Basis-Image, Healthcheck etc.) - dort bereits vollständig dokumentiert, hier nicht dupliziert.
- Der Wortlaut der abgerufenen READMEs von `gitea-mcp-forward-auth` und `gitea-mcp` - nur die
für die Entscheidung relevanten Fakten (Config-Variablen, Workflow-Form, Registry-Ziel)
wurden übernommen.
## Verwandte Entities
- [[wikitool]]
- [[Chemenu]]
## Verwandte Concepts
- [[Publish-Remote Gate]]
- [[Mass-Update Gate]]
- [[Iteration and Cost Limits]]
- [[MCP-Leseserver]]
@@ -0,0 +1,75 @@
---
type: types/source.md
source_type: notes
author: Torben Nehmer
raw_files: [raw/notes/Conversation Transcript - Version Part Nomenclature and Breaking Change Gate Session 2026-09-02.md]
source_language: de
date: 2026-09-02
tags: []
entities: [wikitool, Chemenu]
concepts: [KB Stack Versioning]
summary: Sitzung, die die Versionsstelle als Kompatibilitaets- statt Migrationsfrage praezisiert und einen Freigabe-Ablauf fuer Breaking Changes in stack-dev einfuehrt
---
# Source: Version Part Nomenclature and Breaking Change Gate Session 2026-09-02
**Autor:** Torben Nehmer
**Datum:** 2026-09-02
**Raw-Dateien:** raw/notes/Conversation Transcript - Version Part Nomenclature and Breaking Change Gate Session 2026-09-02.md
**Typ:** Notes
## Zusammenfassung
Umsetzung von Gitea-Issue #26: Die Doku des Stacks führte für die Wahl der Versionsstelle zwei
Fragen zusammen, die nicht dieselbe sind - ob der Korpus migriert werden muss, und ob der
Wechsel ein Drop-in-Ersatz ist. Nur `version bump --help` unterschied korrekt; die drei
prosaischen Stellen (`stack-dev`, `version.py`-Docstring, `INSTALL.md`) beschrieben MAJOR als
Migrationsfrage. Der `2.0.0`-Rebranding-Bump hatte genau daran zuerst `1.9.0` statt `--major`
angesetzt.
Der Nutzer schärfte die Regel während der Sitzung zu einem konkreten zweiseitigen Test nach:
"die neue version ist kein drop-in replacement. Sobald irgendwie Hand angelegt werden muss, sei
es durch den user oder durch ein Migrationsscript, ist es ein major version change. selbiges
gilt, wenn ein update nicht rückgängig gemacht werden kann [...] in allen Fällen muss bei einem
Major version change ein 'Breaking Change' vermerkt werden. breaking changes sind damit teuer.
passe stack-dev so an, dass in diesen Fällen zwingend der user informiert, Alternativen
aufgezeigt und eine freigabe eingeholt wird." Zwei Auswahlentscheidungen davor: Durchsetzung im
Code statt reiner Prosa (weil Prosa bereits einmal gedriftet war), und die Freigabe als
"Decision point" statt als Gate-Sprache, um die drei echten code-erzwungenen Gates nicht zu
verwässern.
## Kernaussagen
- Kompatibilität (Drop-in-Ersatz, vorwärts wie rückwärts) und Inhaltsmigration sind zwei
unabhängige Fragen; MAJOR beantwortet die erste, `--no-migration`/ein Migrationsdokument die
zweite.
- Ein Grenzübertritt kann `kb/` völlig unangetastet lassen und trotzdem MAJOR sein - Katalog:
Update-Pfad, Release-Artefaktname, Paket-Import-Name, ein umbenanntes Kommando/Flag/Envvar,
die Shape einer maschinengelesenen Datei.
- Ein Breaking Change ist teuer (jede bestehende Instanz zahlt einmal, von Hand) und deshalb
genehmigungspflichtig: Bruch, Handarbeit je Instanz und Alternativen (Shim, aufschieben und
bündeln, aufspalten mit Deprecation-Fenster) vorlegen, dann Freigabe abwarten.
- Reine Prosa-Regeln drifted - deshalb wurde `--breaking` als Pflichtflag samt zweiter, von der
Migrationsprüfung unabhängiger `docs verify`-Prüfung eingeführt, nicht nur eine Textänderung.
## Aufgaben
Keine offenen Aufgaben aus dieser Sitzung - Issue #26 wurde in derselben Sitzung geschlossen,
mit Verweis auf Commit `31662dc` (`2.5.0`).
## Nicht übernommen
- Die vollständige Katalog-Tabelle und der `2.0.0`-Fallbeispiel-Text aus
`instructions/dev/version-parts.md` werden hier nicht wiederholt - die Datei ist die
autoritative Quelle (Instruktions-Layer, `manual`-artig durch die `instructions/dev/`-Grenze),
diese Source-Seite fasst nur zusammen, was zur Entscheidung führte.
- Der genaue Wortlaut der Tool-Fehlermeldungen (`version bump`-Refusals) steht im Transkript
selbst; hier nur die Regel dahinter.
## Verwandte Entities
- [[wikitool]]
- [[Chemenu]]
## Verwandte Concepts
- [[KB Stack Versioning]]
@@ -0,0 +1,67 @@
---
type: types/source.md
source_type: document
author: Torben
raw_files: [raw/documents/qmd - GitHub Repository.md]
source_url: https://github.com/tobi/qmd
source_language: en
date: 2026-09-02
tags: []
entities: [qmd]
concepts: []
summary: 'GitHub-API-Metadaten, package.json und README-Auszuege von tobi/qmd: TypeScript/Node/Bun statt Go oder Rust, BM25 (SQLite FTS5) plus Vektor-Suche (sqlite-vec) plus LLM-Reranking ueber node-llama-cpp.'
---
# Source: qmd - GitHub Repository
**Autor:** Torben
**Datum:** 2026-09-02
**Raw-Dateien:** raw/documents/qmd - GitHub Repository.md
**Typ:** Document
## Zusammenfassung
Direkter Abruf von `tobi/qmd` auf GitHub (Repo-Metadaten über die GitHub-API, `package.json`,
README-Auszüge), um eine bestehende, unsichere Behauptung auf der Entity-Seite [[qmd]] zu
prüfen. Die Seite trug seit ihrer Erstellung `Sprache: Nicht angegeben (wahrscheinlich Go oder
Rust)` bei `confidence: 0.85` - eine geratene Angabe mit einer Konfidenz, die für eine Vermutung
zu hoch war. Die GitHub-API meldet `"language": "TypeScript"`, mit einem byte-gewichteten
Sprach-Split von 1.597.168 B TypeScript gegenüber 200.880 B Python, 26.136 B Shell,
23.132 B JavaScript und weniger als 6 KB restlicher Sprachen - kein Go, kein Rust im Repository.
Das npm-Paket heißt `@tobilu/qmd`, installiert per `npm install -g` oder `bun install -g`,
ausgeführt per `npx`/`bunx` - keine Go-typische `go install`-Zeile, wie die alte Seite
behauptete. `package.json` listet `node-llama-cpp` (LLM-Inferenz), `sqlite-vec` (Vektorindex)
und `better-sqlite3` (SQLite-Bindung) als direkte Abhängigkeiten; das README bestätigt das
Zusammenspiel explizit: „QMD combines BM25 full-text search, vector semantic search, and LLM
re-ranking—all running locally via node-llama-cpp with GGUF models." Die Scoring-Tabelle des
Architektur-Abschnitts benennt zusätzlich `SQLite FTS5 BM25` als konkreten Volltext-Mechanismus,
nicht nur „BM25" allgemein.
## Kernaussagen
- Sprache: TypeScript (GitHub-API `language`-Feld, bestätigt durch die Byte-Gewichtung).
- Runtime: Node.js oder Bun, nicht Go/Rust - Installationsbefehle im README belegen das direkt.
- Such-Stack: SQLite FTS5 (BM25) + `sqlite-vec` (Vektor-Ähnlichkeit) + `node-llama-cpp`
(LLM-Reranking mit GGUF-Modellen) - deckungsgleich mit dem, was die alte Seite unter
„Such-Capabilities" bereits beschrieb, nur ohne konkrete Technologienamen.
- Enthält einen `@modelcontextprotocol/server`-Abhängigkeit, deckungsgleich mit der bereits auf
der Seite genannten MCP-Server-Funktion.
## Aufgaben
- [x] `kb/entities/tools/qmd.md`s Sprachangabe korrigiert und mit dieser Quelle belegt
## Nicht übernommen
- Der vollständige Funktionsumfang (Library-API, MCP-Server-Details, Tree-Sitter-basiertes
Code-Chunking, Konfigurationsdatei-Referenz, CLI-Unterbefehle) - nur das zur Korrektur der
Sprach-/Stack-Angabe Nötige wurde übernommen. Eine künftige Erweiterung der `qmd`-Seite um
diese Themen bräuchte einen neuen Abruf, nicht diese Quelle.
## Verwandte Entities
- [[qmd]]
## Verwandte Concepts
- None identified
+101
View File
@@ -0,0 +1,101 @@
# qmd - GitHub Repository (tobi/qmd)
> Source: https://github.com/tobi/qmd
> Retrieved: 2026-09-02, via `https://api.github.com/repos/tobi/qmd`,
> `https://api.github.com/repos/tobi/qmd/languages`, and the raw README/`package.json` at the
> `main` branch HEAD (`pushed_at: 2026-08-18T14:26:31Z` at retrieval time).
> Fidelity: the API JSON fields are quoted verbatim; the README/package.json excerpts below are
> quoted verbatim from the fetched files, trimmed to what backs the corrected claims on
> `kb/entities/tools/qmd.md` - not a full copy of either file.
## Repository metadata (GitHub API)
```json
{
"full_name": "tobi/qmd",
"description": "mini cli search engine for your docs, knowledge bases, meeting notes, whatever. Tracking current sota approaches while being all local",
"language": "TypeScript",
"html_url": "https://github.com/tobi/qmd",
"license": {"key": "mit", "name": "MIT License", "spdx_id": "MIT"}
}
```
Byte-weighted language breakdown (`/languages` endpoint):
```json
{
"TypeScript": 1597168,
"Python": 200880,
"Shell": 26136,
"JavaScript": 23132,
"Nix": 5984,
"Just": 1081,
"Dockerfile": 1074
}
```
## `package.json` dependencies
```json
{
"name": "@tobilu/qmd",
"dependencies": {
"@modelcontextprotocol/server": "2.0.0",
"better-sqlite3": "^13.0.3",
"fast-glob": "3.3.3",
"node-llama-cpp": "3.20.0",
"picomatch": "4.0.5",
"sqlite-vec": "0.1.9",
"tree-sitter-go": "0.25.0",
"tree-sitter-python": "0.25.0",
"tree-sitter-rust": "0.24.0",
"tree-sitter-typescript": "0.23.2",
"web-tree-sitter": "0.26.12",
"yaml": "2.9.0",
"zod": "4.2.1"
}
}
```
## README excerpts
Opening description (README.md, line 5):
> QMD combines BM25 full-text search, vector semantic search, and LLM re-ranking—all running
> locally via node-llama-cpp with GGUF models.
Install ("Quick Start" section):
```sh
# Install globally (Node or Bun)
npm install -g @tobilu/qmd
# or
bun install -g @tobilu/qmd
# Or run directly
npx @tobilu/qmd ...
bunx @tobilu/qmd ...
```
Scoring table (architecture section):
| Stage | Method | Formula | Range |
|---|---|---|---|
| FTS (BM25) | SQLite FTS5 BM25 | `Math.abs(score)` | 0 to ~25+ |
| Vector | Cosine distance | `1 / (1 + distance)` | 0.0 to 1.0 |
| Reranker | LLM 0-10 rating | `score / 10` | 0.0 to 1.0 |
Diagnostic command comment, confirming `sqlite-vec` is the vector index used at runtime, not
only a listed dependency:
```
# Diagnose the install (runtime, sqlite-vec, embedding fingerprints, GPU probe)
```
## Not extracted
The full README (1301 lines: library API, MCP server usage, tree-sitter code-chunking options,
config file reference, CLI subcommand reference) - only the excerpts backing the corrected
`qmd.md` claims (language, runtime, install method, search/rerank stack) were pulled. A future
qmd page expansion covering its library API or MCP server would need a fresh fetch, not a reuse
of this file's `## Not extracted` judgment.
@@ -0,0 +1,245 @@
# Conversation Transcript - MCP Read Server Implementation Session
> Source: Claude Code session (`claude-opus-5`), chemenu workspace
> Collected: 2026-09-02
> Participant: Torben
> Fidelity: **faithful summary transcript, not a verbatim log.** Torben's instructions are
> quoted verbatim; the assistant's design reasoning, code, and file contents are condensed to
> what a later reader needs to find the artifact - the artifacts themselves are the code, not
> this transcript. Command outputs shown (test counts, timings, the Mass-Update Gate's printed
> file list, gate refusal messages) are real, copied from the session. No credentials appeared.
> No subagents were used - all work in this transcript is first-hand.
> One of two transcripts cut from this session; the other covers fixing `kb/entities/tools/qmd.md`,
> a documentation defect noted but not fixed during this one.
Covers implementing the four-issue sequence held by #36 (the master issue for "path to the MCP
read server"): #34 (arm the Publish-Remote Gate), #33 (harden the read path), #31 (root
resolution + library boundary), #19 (the MCP server itself), plus the human-facing
`INSTALL-MCP.md` and filing #37 (the container-image follow-up) that #19's closure surfaced as
still missing. Four version bumps in sequence: 2.2.3, 2.2.4, 2.3.0, 2.4.0, then 2.4.1 for the
documentation follow-up. All five touched issues (#34, #33, #31, #19, #36) got closing comments;
#36 was left open pending #37.
---
## Turn 1 - `/stack-dev implementiere #36`
The `stack-dev` skill loaded, switching the session into tool-development rules (ordinary
software-engineering judgment on `tools/`, `types/`, `instructions/` - no source-binding, no
`raw/` citation needed for code).
Read issue #36 (the master issue) via `gitea-mcp`. It holds no implementation itself - a
sequence table (#34#33#31#19), a table of platform/auth/transport decisions already
made and closed to debate, a measured baseline (593 ms per `wikitool search`, broken down:
262 ms module import, 274 ms corpus parse, 7 ms ripgrep), ten verified findings backing the
design, and an explicit "not in scope" list (Go, deployment details, a write path, #15's
research capability). Read #34, #33, #31, #19 in full including comments to get the acceptance
criteria and the reasoning behind each.
Worked the sequence in order, starting a session-scoped `WIKITOOL_SESSION_ID`.
**#34 - Publish-Remote Gate.** `.wikitool-remotes.json` did not exist in this checkout; absence
means unrestricted per `git_publish.read_allowed_push_urls()`. Created it with `origin`'s exact
push URL as the sole allowed target. Counter-checked by adding a second git remote to a made-up
URL and running `publish --remote gatecheck`: refused with **exit 42**, working tree untouched,
before any fetch. Remote removed afterward.
`doctor`'s `check_publish_remotes()` reported only whether the *file* existed, not whether the
gate was actually armed. Rewrote its three outcome messages to lead with `Gate armed:` /
`Gate not armed:` explicitly - the single-remote-no-file case stays `OK` (nothing to protect,
and a FAIL would make the file mandatory through the back door) but now says outright that every
push target passes. The check had **no tests**; added three, one per state.
Bumped `--patch`**2.2.3**.
**#33 - Harden the read path.** Six findings, each addressed with a regression:
1. *ReDoS.* `search/ripgrep.py:_contains` passed `query.regex` to `re.search`. Deleted the
branch entirely rather than bounding it - `rg` already applies the pattern with a linear
engine before this function runs, so nothing is lost except the extra title/summary ranking
boost for a non-literal pattern (and summary/H1 lines are themselves counted by `rg`).
Regression test asserts both the timing (`< 0.5s` against `(\w+\s?)+$`) and the return value,
because a bound alone would pass with a merely-faster engine.
2. *No subprocess timeout.* Added `RIPGREP_TIMEOUT_SECONDS = 30.0`; `TimeoutExpired` translates
to the existing `RipgrepFailed` path.
3. *YAML aliases.* Frontmatter has no legitimate use for anchors/aliases, so they are refused
outright rather than budgeted, checked on the streaming *event* parse (`yaml.parse`) so the
check itself never triggers the expansion it is checking for - `*` is a necessary character
in any alias node, so its absence proves absence with zero parse cost. Constructed a
reproduction: 267 bytes of nested aliases compose into 672,603 nodes on traversal at
constant (0.2 ms) parse time, growth 9^n with nesting depth. Added a 64 KiB frontmatter size
limit and a `RecursionError` catch (PyYAML composes recursively; deep nesting is not a
`YAMLError`).
4. *`CSafeLoader` unused.* Switched to it with a `SafeLoader` fallback. Measured over this
corpus (176 pages, best of 5): **265 ms → 54 ms**.
5. *Silent frontmatter loss.* Decided: the read path now *reports* rather than swallows.
Unparseable YAML still degrades to `{}` for bulk operations, but the reason travels with it
(`Page.frontmatter_error`, set by a single parser shared between `read_page()` and
`frontmatter_error()` - previously two separate `safe_load` call sites that could describe
the same broken file differently). `search --json` now always carries an `unreadable` list
of `{path, reason}`; the table form writes the same lines to stderr. Added `read_page_strict()`
for the future ingest-quarantine path (#32), which must stop on unparseable input rather than
empty it.
6. *Corpus reparse per call.* New `chemenu/corpus_cache.py`: a `CorpusCache` object a caller
*holds* (not a module-global switch). The CLI holds none and is unaffected. Correctness
property, not just speed: **a dirty working tree is never cached** - if git cannot answer
whether the tree is clean, it is treated as dirty. `wikitool search` end-to-end (best of 5):
**593 ms → 347 ms**.
Bumped `--patch`**2.2.4**.
**#31 - Root resolution + library boundary.** `config.ROOT` and everything derived from it were
Python module constants, bound at import time from `Path(__file__).resolve().parents[2]` - so
`monkeypatch.setattr(config, "ROOT", tmp_path)` repointed `ROOT` but left `KB_DIR`/`RAW_DIR`
aimed at the real checkout, the exact failure class `raw_dir`'s own fixture docstring already
warned about one layer up.
Rewrote `config.py`: `resolve_root()` by precedence (explicit argument → `$CHEMENU_ROOT`
walk-up, the walk-up staying default so `tools/wikitool` is unaffected), and every derived path
resolved on *attribute access* via module `__getattr__` (PEP 562) rather than stored - so an
assignment onto `ROOT` (test or otherwise) is honored by every path under it, live. This
surfaced a subtler bug while fixing the first one: `monkeypatch`'s own undo mechanism reads the
old value (which resolves it) and writes it back as a real attribute on teardown, recreating the
stale binding the rewrite was meant to eliminate. Added `config.reset()`, called by the autouse
hermetic-environment fixture on both setup and teardown.
Split the CLI-coupled command modules into a pure core + thin adapter, three times over:
`search/service.py`, `lint_core.py`, `types_core.py` - none of them import `typer` or `rich`.
`commands/search.py`, `commands/lint.py`, `commands/types_cmd.py` became the terminal adapters,
re-exporting the same names so no existing import breaks.
New `chemenu/errors.py`: `ChemenuError``ValidationError` (also inherits `ValueError`, since
`PredicateError` already was one and callers catch it that way) / `BackendError`.
`PredicateError`, `FrontmatterError`, `UnknownBackend`, `RipgrepMissing`, `RipgrepFailed` moved
under this hierarchy.
New `chemenu/api.py`: `Corpus` class, the in-process entry point - takes a root, returns exactly
the CLI's `--json` shapes, raises instead of exiting, stamps every response with `commit`/
`as_of`. Wrote the acceptance test by monkeypatching `Path.read_text`/`Path.rglob` to fail on any
access under the real checkout root while a foreign root is set - this test could not have
passed before the lazy-resolution rewrite.
Two more accidental dependencies surfaced and were fixed: `TypeResolver.repo_root` was also
import-bound (fixed the same way, with `config.rooted()` as a process-wide context manager for
callers that reach `config` directly rather than taking a root argument - and `use_shipped_type_specs()`
added to the test fixtures that had been relying on it silently); and `search/registry.resolve()`
did not pass `kb_dir`/`root` through to the backend, so a caller pointing `run_search` at a
foreign corpus could still have `RipgrepBackend` read `config.KB_DIR` underneath it.
Bumped `--minor`**2.3.0** (new capability, backward-compatible).
**#19 - MCP read server.** New package `tools/chemenu/mcp/` (`server.py`, `__main__.py`).
Installed the `mcp` SDK (`mcp>=2.0`, discovered mid-session that this pulled in the v2 API -
`FastMCP` renamed to `MCPServer`, imported from `mcp.server.mcpserver`). Five tools over
`chemenu.api.Corpus`: `search`, `types`, `describe_type`, `lint`, `status` (`status` is
server-composed, not a wrapper - there is no `wikitool status` command to wrap). No write tool,
structurally: neither the server module nor `chemenu.api` imports anything under
`chemenu.commands`.
Both transports built and smoke-tested end-to-end against the real 176-page corpus: `stdio` via
a hand-written MCP client script, and `streamable-http` (host/port bound explicitly - the
default binds loopback, wrong for a container behind a proxy) via a subprocess + HTTP client
round-trip. `sse` deliberately not offered (superseded transport).
`ChemenuError` translated to the SDK's `ToolError` at the tool-call boundary (a deliberate
refusal whose message reaches the caller) rather than left to become an `UnexpectedToolError`
(a crash whose message stays server-side).
Found and fixed a stamping bug while writing the golden test: `_stamp()` was asking the cache for
the *current* revision after the load had already happened, so a caller with a perfectly clean
tree could see `"commit": null` if the cache's cached revision lagged. Fixed by threading the
revision the load actually returned through to the stamp.
Wrote `tools/chemenu/tests/test_mcp_server.py`: the golden test runs `wikitool ... --json` as a
subprocess against the same fixture tree (via `$CHEMENU_ROOT`) and asserts the server's
structured output matches field-for-field; a before/after test captures file size+contents,
`git rev-parse HEAD`, and `git status --porcelain` around all five tool calls to prove nothing
writes; a structural test imports the server module in a fresh interpreter and checks
`sys.modules` for absence of `chemenu.commands.*`.
Added `check_trace_destination()`: the server **refuses to start** if `WIKI_TRACE` is on and
would write into the served checkout's `reports/telemetry/` - the directory the sync's
`git reset --hard` is entitled to wipe. `WIKI_TRACE=0` or `WIKI_TRACE_DIR` outside the corpus
both satisfy it.
New `instructions/mcp-read-server.md` (agent-facing operating procedure), `tools/requirements-mcp.txt`
(the SDK's dependency, kept out of `requirements.txt` so a CLI-only instance need not install
pydantic/starlette/uvicorn/cryptography), a CI step to install it (so the golden test cannot
silently be skipped), and doc updates (`tools/CONTRACT.md`, `tools/README.md`, `README.md`).
Bumped `--minor`**2.4.0**.
Ran `docs verify`, `instructions verify`, and the full suite (**799 passed**) plus an
empty-machine run (`env -i ... pytest`) with the same result, before presenting the changeset.
`tools/wikitool publish` hit the **Mass-Update Gate**: 37 files, +3,037/-625 lines, exit 42.
Reproduced the full file-by-area breakdown verbatim in the reply (as the gate's message
requires - the output does not reach the user's screen on its own) and stopped without running
further commands, per the gate's instruction and AGENTS.md invariant 6.
## Turn 2 - "Freigegeben. Erstelle eine install-MCP.md für Menschen. Für traefik middleware verweise auf https://gitea.nehmer.net/torben/gitea-mcp-forward-auth ... Wir brauchen ein issue, der uns dicker Container für ein deployment erzeugt. Fasse dort schon mal zusammen, was du bisher weißt... Die ci scripte in https://gitea.nehmer.net/torben/gitea-mcp dürfte ein guter startpunkt sein."
Published with the token from the gate's own printed line
(`--confirm 46442f4419c1`) - commit `576df2c`.
Fetched both referenced repos via `gitea-mcp` (`get_repository_tree`, `get_file_contents`) to
ground the two follow-up documents in what actually exists rather than in assumption:
- `gitea-mcp-forward-auth`: a small Go Traefik ForwardAuth service - Bearer-token-vs-SHA-256-hash
checking, `AUTH_PROXY_TOKEN_HASHES_DIR`/`AUTH_PROXY_TOKEN_HASHES` config, `/healthz` always
open, demo token hashes checked into the repo, a registry-image test script with ephemeral
Docker/Podman login.
- `gitea-mcp`: its Dockerfile (multi-stage, `CGO_ENABLED=0`, distroless non-root, OCI labels) is
a usable pattern, but its release workflow pushes to **DockerHub** (`DOCKER_ORG: gitea`) - it
is a fork of the upstream project, so it is *not* a usable registry-push template for this
repo. Noted explicitly rather than silently copied.
Wrote `install-MCP.md` (later renamed, see Turn 3): six numbered steps (install dependency, run
stdio, wire a client, run streamable-http, put authentication in front, keep the corpus current
via polling), a runnable stdio verification script (executed for real before being written into
the doc, output: `Tools: [...]`, `Seiten: 176 | Commit: <sha>`), and a troubleshooting section
keyed to the server's actual error messages. Linked it from `INSTALL.md` and `README.md`, and
added `CHEMENU_ROOT`/`WIKI_TRACE`/`WIKI_TRACE_DIR` to `INSTALL.md`'s configuration table (both
had been in effect since earlier work but undocumented there). Added the file to `dist_cmd.py`'s
`ROOT_FILES` allowlist and verified with a real `dist export` that it ships.
Filed **issue #37** ("Container-Image für den MCP-Leseserver"): summarized what is already fixed
(startup command, required env vars, the ripgrep-in-the-image trap a naive `pip install`
Dockerfile would miss, where auth/rate-limiting belong) and the concrete templates found above,
then nine explicit open decisions (corpus baked into the image vs. mounted as a volume and
synced by a sidecar; who runs the sync; base image, since Go-style `distroless` does not carry a
Python interpreter or `rg`; how the version reaches the image; whether to build `arm64`;
**a healthcheck endpoint does not exist yet - the one item that implies new code in this repo**;
registry path; OCI labels; whether a smoke test against the built image is added). Labeled
`prio/2 size/M`.
Closed **#34, #33, #31, #19** with detailed comments each restating what was implemented against
the issue's own acceptance criteria, including the two accidental-dependency findings from #31
and the corrected alias-bomb numbers. Commented on **#36** with a summary table across all four
versions, noted its closing criterion (a consumer provably reaching the server through the
Traefik middleware) is not yet met - that needs #37's deployment - and proposed leaving #36 open
until then; also carried forward its two remaining loose ends (the wrong `qmd.md` language claim,
and the still-missing place for architecture decisions).
## Turn 3 - "Freigegeben. Nenne die Datei INSTALL-MCP.md all Caps sonst fahre mit dem Auftrag fort" (interrupting a `dist export` verification call)
Renamed `install-MCP.md``INSTALL-MCP.md`, fixed every reference (`INSTALL.md`, `README.md`,
`tools/chemenu/commands/dist_cmd.py`). Re-ran the full suite (**799 passed**) and `docs verify`,
bumped `--patch`**2.4.1**, wrote the changelog entry, published (commit `83018fc`). Saved a
feedback memory (`root-docs-are-all-caps.md`): root-level human docs in this repo are named in
ALL CAPS, and a new one has to be added to `dist_cmd.ROOT_FILES` or it silently does not ship.
## Outcome
- **Version:** 2.2.2 -> 2.2.3 -> 2.2.4 -> 2.3.0 -> 2.4.0 -> 2.4.1
- **Commits:** `576df2c` (2.4.0, the 37-file changeset cleared through the Mass-Update Gate),
`83018fc` (2.4.1, `INSTALL-MCP.md` and its wiring)
- **Tests:** 776 -> 786 -> 799 passed, green throughout, including on an `env -i` empty machine
- **Measured:** corpus parse 265ms -> 54ms; `wikitool search` end-to-end 593ms -> 347ms
- **Issues:** #34, #33, #31, #19 closed with detailed comments; #36 commented, left open pending
#37; #37 opened (`prio/2 size/M`)
- **CI:** `.gitea/workflows/ci.yml` updated to install `tools/requirements-mcp.txt`, otherwise
unchanged; not separately re-run in this session (publish triggers it)
- **Not done in this session, carried forward:** #37 itself (container image); #23 (env var
registration enforcement - `CHEMENU_ROOT` was added to `_WIKITOOL_ENV` by hand); the `qmd.md`
language-claim fix (separate transcript); an ADR-style home for architecture decisions
@@ -0,0 +1,218 @@
# Conversation Transcript - Version Part Nomenclature and Breaking Change Gate Session
> Source: Claude Code session (`claude-sonnet-5`), chemenu workspace
> Collected: 2026-09-02
> Participant: Torben Nehmer
> Fidelity: **faithful summary transcript, not a verbatim log.** The user's instructions and
> clarifications are quoted verbatim; the agent's own reasoning and file-reading steps are
> paraphrased; tool output blocks (`docs verify`, `pytest`, `version bump --dry-run`, the
> Mass-Update Gate refusal) are real, copied from the actual run, not reconstructed.
> No second-hand material - no subagent was used.
> No credentials appeared.
> Single topic, not cut.
Closes Gitea issue #26 (already closed in-session) with `2.5.0`. Commit `31662dc` on `main`.
Covers picking the version part for a stack change, and adding a user-approval gate for
breaking changes to `stack-dev`.
## Turn 1 - `/stack-dev kümmere dich um #26`
The user invoked the `stack-dev` skill with the argument `kümmere dich um #26`. The agent read
issue #26 via `gitea-mcp` (`torben/chemenu#26`, no comments yet).
**The issue's own account** (quoted from its body, since it is the source of the whole
session): the stack's documentation conflated two questions when choosing a version bump part -
"Muss der Korpus migriert werden?" and "Ist der Wechsel rückwärtskompatibel?" - and every place
an agent would consult before a bump stated only the first:
| Ort | Wortlaut |
|---|---|
| `instructions/dev/stack-dev/SKILL.md`, Schritt 3 | `--major` ⇔ "Existing content must be migrated" |
| `tools/chemenu/version.py`, module docstring | "from `1.0.0` on the same rule reads as the familiar 'MAJOR means migration'" |
| `INSTALL.md` § Version und Updates | "ab `1.0.0` liest sich dieselbe Regel als das gewohnte 'MAJOR heißt Migration'" |
Only `version bump --help` had it right ("A bump that crosses the compatibility boundary
additionally requires a migration document ... or `--no-migration`") - the one place read only
after the decision is already made.
The issue's worked example was the `2.0.0` rebranding (`torben/llm-wiki-test1`
`torben/chemenu`): no page in `kb/` changed, so the first attempt at that bump was `1.9.0`. Three
things broke anyway - the release-feed URL baked into every existing instance's
`.wikitool-release.json` (unrepairable by hand, per invariant 1), the release artefact's
filename, and the Python package's import name. The correct bump was `--major --no-migration`.
The issue's "Was 'fertig' heißt" checklist asked for: rewording the three prose locations to
lead with compatibility rather than migration, a decision aid enumerating which kinds of break
force MAJOR even with `kb/` untouched, and a check that the three locations do not end up
contradicting each other (invariant 8). It explicitly scoped out teaching `docs verify` to judge
*whether* a bump chose the right part - "das ist eine Urteilsfrage und kein hartes Orakel."
The agent read the three prose locations plus `tools/chemenu/commands/version_cmd.py` (the
existing `--no-migration` enforcement) and `docs_verify.py` (`check_migration_for_boundary`), to
scope the fix before writing anything.
## Turn 2 - scope questions, then the user's clarification
Before writing, the agent asked two `AskUserQuestion` questions:
1. Whether the "Breaking Change" marker and approval requirement should be prose-only or also
enforced in the tool. Options offered: "Prosa + Marker im Code (Empfehlung)" vs. "Nur Prosa".
**User picked the enforced option** ("Prosa + Marker im Code (Empfehlung)") - a
`--breaking` flag on `version bump`, checked by `docs verify`, on the reasoning that a
pure-prose rule is exactly what drifted in the first place.
2. Whether the pre-bump approval step should read like one of the codebase's actual gates
(`instructions/gates.md` language: "anhalten ... auf ausdrückliche Freigabe warten") or as a
softer "Decision points" entry. **User picked the softer framing** ("Als Entscheidungspunkt")
- explicitly *not* gate language, to avoid diluting the weight of the three real,
code-enforced gates (Mass-Update, Publish-Remote, Iteration Budget) by association.
The user then added the clarification that drove the rest of the session, quoted in full:
> "genaue nomenklatur für major version change: die neue version ist kein drop-in replacement.
> Sobald irgendwie Hand angelegt werden muss, sei es durch den user oder durch ein
> Migrationsscript, ist es ein major version change. selbiges gilt, wenn ein update nicht
> rückgängig gemacht werden kann (Szenario: User aktualisiert den Stack von vA nach vB, vB hat
> beim User ein Problem, kann jetzt vA nicht mehr einfach so als Downgrade installiert werden,
> ist es ein breaking change)
> in allen Fällen muss bei einem Major version change ein "Breaking Change" vermerkt werden.
> breaking changes sind damit teuer. passe stack-dev so an, dass in diesen Fällen zwingend der
> user informiert, Alternativen aufgezeigt und eine freigabe eingeholt wird."
This sharpened the issue's "compatibility" question into a concrete two-sided test - **forward**
(can the new version be copied over with zero hand-work, by user or script) and **backward** (can
the old version still be reinstalled afterwards) - and made explicit that either half failing,
independent of `kb/`, is what MAJOR actually means.
## Turn 3 - what was written
**`instructions/dev/version-parts.md` (new).** The single source for the rule. Structure:
the two-sided drop-in test (step 1); a catalogue of breaks that cross the boundary with `kb/`
untouched - update path, release-artefact name, package import name, a renamed
command/flag/envvar, the shape of a machine-read file like `.wikitool-kb.json` (step 2); the
plain compatible-part table for patch/minor (step 3); the mandatory stop-and-inform-the-user
step before any boundary-crossing bump, with three named alternatives (avoid via a
compatibility shim, defer and batch with the next crossing, split into a compatible half now
and a deprecated-then-removed half later) plus a recommendation (step 4); `--breaking` usage
(step 5); the pre-existing migration-document-or-`--no-migration` step, now explicitly
independent of compatibility (step 6); writing the entry body (step 7). Closes with the `2.0.0`
case study as the worked example, matching the issue's own account.
Placed under `instructions/dev/` (not linked from any distributed artifact) because
`tools/wikitool dist export` prunes that directory wholesale - the agent's first draft linked to
it from `tools/CONTRACT.md` and the `version.py` docstring, which `tools/wikitool instructions
verify` correctly rejected:
```
ERROR Instruction layer issues:
- version-parts.md: lives under instructions/dev/ but is referenced from
outside it and outside a dist:strip block - `dist export` removes
instructions/dev/ wholesale, so that reference would dangle in a distributed
instance. Remove the reference, or wrap it in a <!-- dist:strip-start/end -->
block if it belongs only to this dev instance.
```
**Rejected approach:** wrapping the reference in `<!-- dist:strip-start/end -->` markers so it
would still resolve in this repo. Not used - the agent instead rewrote the three shipped
locations (`tools/CONTRACT.md`, `version.py` docstring, `version_cmd.py` docstring) to state the
short form of the rule standalone, with no pointer to the dev-only file, since a shipped
instance never has it to point to.
**`instructions/dev/stack-dev/SKILL.md`.** Step 3's table changed from "Existing content must
be migrated → `--major`" to "Not a drop-in replacement ... → `--major`", with a pointer to
`version-parts.md` for the full test and catalogue. A new "Decision points" entry: if a change
turns out not to be a drop-in replacement, stop - do not bump across the boundary on the
session's own initiative; show the user the concrete break, what each instance must do, and the
three alternatives from `version-parts.md` step 4, then wait for a go-ahead. Written in the
softer "Decision points" register per the user's second answer above, not gate language.
**`tools/chemenu/version.py`.** Module docstring reworded: "MAJOR means migration" → "MAJOR
breaks", with a new paragraph stating the two questions are independent and naming both markers.
New constant `BREAKING_CHANGE_MARKER = "**Breaking Change:**"`, alongside the existing
`MIGRATION_NONE_MARKER`. `insert_changes_entry()` gained a `breaking_reason` parameter, writing
the `**Breaking Change:**` line *before* the migration line - the break is what an operator acts
on first.
**`tools/chemenu/commands/version_cmd.py`.** `bump` gained `--breaking TEXT`. Refused on a
non-boundary-crossing bump; required on a boundary-crossing one, with a refusal message that
also second-guesses the part choice itself: *"If that sentence is hard to write because nothing
actually breaks - no hand-work on update, and the old version can still be put back - then the
bump is probably not --major."* Verified live:
```
$ tools/wikitool version bump --major --title "smoke" --dry-run
ERROR 2.4.1 -> 3.0.0 crosses the compatibility boundary, so it is not a drop-in
replacement - re-run with --breaking "<what stops working, and what an instance
must do about it>".
If that sentence is hard to write because nothing actually breaks - no
hand-work on update, and the old version can still be put back - then the
bump is probably not --major.
```
```
$ tools/wikitool version bump --minor --title "smoke" --breaking "x" --dry-run
ERROR --breaking only applies to a bump that crosses the compatibility boundary;
2.4.1 -> 2.5.0 does not.
```
**`tools/chemenu/commands/docs_verify.py`.** New `check_breaking_change_for_boundary()`,
independent of the existing `check_migration_for_boundary()` on purpose - the two check
different things (did the corpus move vs. was the operator told the swap isn't drop-in), and a
crossing with `kb/` untouched can satisfy the first while failing the second. Wired into
`verify()` alongside the existing check.
**Tests.** `test_version_cmd.py`: nine existing calls updated for the new `breaking=` parameter,
plus three new tests (`test_a_boundary_crossing_bump_without_breaking_is_refused`,
`test_breaking_records_what_stops_working_in_the_changelog` - including the ordering assertion
that `BREAKING_CHANGE_MARKER` precedes `MIGRATION_NONE_MARKER` in the rendered entry -
`test_breaking_is_refused_on_a_compatible_bump`). `test_docs_verify.py`: four new tests mirroring
the existing migration-boundary tests but for the breaking-change marker. Full suite: 806 passed.
**`tools/CONTRACT.md` and `INSTALL.md`.** Both reworded to lead with the drop-in question and
treat migration as the second, independent one; `INSTALL.md` additionally tells the reader which
two lines to look for in release notes (`Breaking Change:` and `Migration:`) before applying an
update.
**`CHANGES.md`.** New `2.5.0` entry written after the bump, body filled in by the agent (the
tool leaves it empty by design) - includes the "what deliberately did not change" note that
`docs verify` still does not judge *whether* the chosen part was correct, matching the issue's
explicit scope-out.
## Turn 4 - publish, twice
`tools/wikitool version bump --minor --title "..."` was run first (11 changed files, drop-in in
both directions - the new requirement only binds the *next* boundary crossing, not
retroactively). `tools/wikitool publish --message "..."` then hit the **Mass-Update Gate**
(11 counted files ≥ threshold 10):
```
NEEDS USER CLEARANCE Mass-Update Gate: this publish would commit and push 11
counted files (>= threshold 10) to origin/main. ...
```
Per the gate's own instructions, the agent reproduced the full file-by-area breakdown and the
`--confirm <token>` line in its reply and ran nothing further that turn. The user replied
"freigegeben" for both the earlier publish authorization ("publish ist freigegeben") and, in a
separate turn, this specific token. The agent then ran `tools/wikitool publish --confirm
95ae372d5677 --message '...'`, which pushed commit `31662dc` to `origin/main`, and verified
`git rev-parse HEAD origin/main` matched afterward.
## Turn 5 - issue closeout and this capture
The user asked to update issue #26 "wie vorgeschlagen" (per the agent's own end-of-turn
suggestion), run `instructions/capture-session.md`, and in the same pass correct
`kb/entities/tools/wikitool.md`. The agent posted a comment on #26 summarizing what shipped
(including the parts that went beyond the issue's own checklist - the `--breaking` flag and the
second `docs verify` check, added because plain prose was judged likely to drift again) and
closed the issue. This transcript and the `kb/` correction are the remaining two steps of that
request.
## Outcome
| | |
|---|---|
| Version | `2.4.1``2.5.0` (`--minor`: new capability, still drop-in both directions) |
| Commit | `31662dc` on `main`, pushed to `origin` |
| Files changed | 11 (+432/-33): `instructions/dev/version-parts.md` (new), `instructions/dev/stack-dev/SKILL.md`, `tools/CONTRACT.md`, `tools/chemenu/version.py`, `tools/chemenu/commands/version_cmd.py`, `tools/chemenu/commands/docs_verify.py`, `tools/chemenu/tests/test_version_cmd.py`, `tools/chemenu/tests/test_docs_verify.py`, `CHANGES.md`, `INSTALL.md`, `VERSION` |
| Tests | 806 passed (`tools/chemenu/tests/`), including 6 new |
| Verification | `tools/wikitool docs verify` OK, `tools/wikitool instructions verify` OK (17 instructions, 6 skills, 12 published copies match), `tools/wikitool doctor` clean (only the expected `WIKITOOL_SESSION_ID` WARN) |
| Issues | #26 closed, comment `torben/chemenu#26` (issuecomment-474) |
| CI | Not yet observed in this session - a `VERSION` move on `main` triggers a tagged release per `.gitea/workflows/release.yml`; not polled |
+5 -5
View File
@@ -67,20 +67,20 @@ tools/wikitool <command> --help
| `instructions sync [--force]` | Publish every `instructions/<name>/SKILL.md` into `.agents/skills/` and `.claude/skills/` as **copies**, and delete published skills whose source is gone. Both targets are gitignored, so a fresh clone runs this once - see `instructions/bootstrap.md`. Re-running is also how a drifted copy is repaired: the source always wins. `--force` is required only to replace a target directory that is not a published skill at all (no `SKILL.md` in it) | | `instructions sync [--force]` | Publish every `instructions/<name>/SKILL.md` into `.agents/skills/` and `.claude/skills/` as **copies**, and delete published skills whose source is gone. Both targets are gitignored, so a fresh clone runs this once - see `instructions/bootstrap.md`. Re-running is also how a drifted copy is repaired: the source always wins. `--force` is required only to replace a target directory that is not a published skill at all (no `SKILL.md` in it) |
| `instructions verify` | Check the instruction layer: flat instructions validate against `types/instruction.schema.yaml`, each `SKILL.md` carries the frontmatter its harness reads, every published copy is byte-identical to its source, no instruction is left that nothing references, and nothing under `instructions/dev/` is referenced from outside it (a `<!-- dist:strip-start/end -->` block is exempt - see [instructions/CONTRACT.md](../instructions/CONTRACT.md)). Missing *every* copy is reported as "run sync", not as drift - that is a clean checkout | | `instructions verify` | Check the instruction layer: flat instructions validate against `types/instruction.schema.yaml`, each `SKILL.md` carries the frontmatter its harness reads, every published copy is byte-identical to its source, no instruction is left that nothing references, and nothing under `instructions/dev/` is referenced from outside it (a `<!-- dist:strip-start/end -->` block is exempt - see [instructions/CONTRACT.md](../instructions/CONTRACT.md)). Missing *every* copy is reported as "run sync", not as drift - that is a clean checkout |
| `instructions list [--json]` | List the flat instructions with their descriptions. This is how the layer is discovered; `search` deliberately covers `kb/` only | | `instructions list [--json]` | List the flat instructions with their descriptions. This is how the layer is discovered; `search` deliberately covers `kb/` only |
| `docs verify` | Check the docs that mirror the code: every CLI command documented here (and vice versa), every directory under `kb/` has a `COLLECTION.md` and no directory outside it does, every stage contract present, no pre-migration `type: entity` blocks left in the contracts, and the `.gitignore` canaries clear in both directions (nothing ignored under `raw/`/`kb/`, everything ignored under `reports/` and the published skill directories) | | `docs verify` | Check the docs that mirror the code: every CLI command documented here (and vice versa), every directory under `kb/` has a `COLLECTION.md` and no directory outside it does, every collection declaring `profile:` and a `required_by_stack:` that agrees with the stack's own list, `kb/CONVENTIONS.md` naming all three tool-owned section headings if it exists at all, every stage contract present, no pre-migration `type: entity` blocks left in the contracts, and the `.gitignore` canaries clear in both directions (nothing ignored under `raw/`/`kb/`, everything ignored under `reports/` and the published skill directories) |
| `eval sessions [--json]` | List the sessions that have a trace under `reports/telemetry/`, most recent first. Read-only and exempt from the Iteration Budget Gate | | `eval sessions [--json]` | List the sessions that have a trace under `reports/telemetry/`, most recent first. Read-only and exempt from the Iteration Budget Gate |
| `eval score [--session <id>] [--json] [--markdown out.md] [--save] [--fail-on-error]` | Score one traced session: structural state from `lint`'s own checks (L1) plus trajectory rules over the trace (L2) - was a refused call repeated unchanged, was a gate flag passed without that gate having refused anything, did a publish of `kb/` pages go unlogged. Defaults to the current session. `--save` writes `reports/evals/<date>/<session>.{json,md}`. Read-only over `kb/` and exempt from the budget; see [../EVALS.md](../EVALS.md) | | `eval score [--session <id>] [--json] [--markdown out.md] [--save] [--fail-on-error]` | Score one traced session: structural state from `lint`'s own checks (L1) plus trajectory rules over the trace (L2) - was a refused call repeated unchanged, was a gate flag passed without that gate having refused anything, did a publish of `kb/` pages go unlogged. Defaults to the current session. `--save` writes `reports/evals/<date>/<session>.{json,md}`. Read-only over `kb/` and exempt from the budget; see [../EVALS.md](../EVALS.md) |
| `dist export <target> [--dry-run] [--source-repo U] [--source-commit SHA] [--release-url U] [--update-url U]` | Write a contentless, distributable copy of this repo's machinery into an empty `<target>` directory: `AGENTS.md`/`README.md`/`EVALS.md` with any `<!-- dist:strip-start -->...<!-- dist:strip-end -->` region removed, `instructions/` (minus `instructions/dev/`), `types/`, `tools/` (no venv/caches), the `.github/hooks/`+`.vibe/` session-tracing config plus `.claude/settings.json`, every `kb/*/COLLECTION.md` (no pages, no areas), empty `raw/{articles,documents,notes,assets}/`, `VERSION`, `USER.md.template`/`SOUL.md.template` (the templates ship; a filled `USER.md`/`SOUL.md` never does - the root allowlist is what makes that automatic), and a generated `.wikitool-release.json` stamp (version, export date, origin, and a sha256 per exported file - the base a later upgrade would compare against). The four origin options only fill stamp fields: `export` never calls git and cannot discover them. Refuses a non-empty target, and a tree with no `VERSION`. See `instructions/setup-instance.md`. One-way: there is no command that reconstructs a distributed instance into a dev instance - work on the stack in the origin repo (or a new dev instance exported from it) instead | | `dist export <target> [--dry-run] [--source-repo U] [--source-commit SHA] [--release-url U] [--update-url U]` | Write a contentless, distributable copy of this repo's machinery into an empty `<target>` directory: `AGENTS.md`/`README.md`/`EVALS.md` with any `<!-- dist:strip-start -->...<!-- dist:strip-end -->` region removed, `instructions/` (minus `instructions/dev/`), `types/`, `tools/` (no venv/caches), the `.github/hooks/`+`.vibe/` session-tracing config plus `.claude/settings.json`, `kb/CONTRACT.md` (no pages, no areas), empty `raw/{articles,documents,notes,assets}/`, `VERSION`, `USER.md.template`/`SOUL.md.template` plus `kb/CONVENTIONS.md.template` and each collection's contract re-keyed as `kb/<name>/COLLECTION.md.template` (the templates ship; the filled `USER.md`/`SOUL.md`/`kb/CONVENTIONS.md`/`kb/<name>/COLLECTION.md` never do - all four bind their instance and none of them are the stack's to decide, and `find_leaks` refuses a plan carrying one), and a generated `.wikitool-release.json` stamp (version, export date, origin, and a sha256 per exported file - the base a later upgrade would compare against). The four origin options only fill stamp fields: `export` never calls git and cannot discover them. Refuses a non-empty target, and a tree with no `VERSION`. See `instructions/setup-instance.md`. One-way: there is no command that reconstructs a distributed instance into a dev instance - work on the stack in the origin repo (or a new dev instance exported from it) instead |
| `version show [--json]` | Print this instance's stack version and where it came from (development tree, or a distribution with its export date and origin). Bare `wikitool version` is an alias for this. Read-only, offline, and **exempt from the Iteration Budget Gate** | | `version show [--json]` | Print this instance's stack version and where it came from (development tree, or a distribution with its export date and origin). Bare `wikitool version` is an alias for this. Read-only, offline, and **exempt from the Iteration Budget Gate** |
| `version check [--url U] [--timeout S] [--json]` | Ask the origin's release feed whether a newer stack exists, and whether the step crosses a compatibility boundary (`state: current\|update\|migration\|ahead`). **The only command in `wikitool` that makes a network call** - never reached implicitly from another command, needs no key, times out, and reports an unreachable feed as an error rather than as "up to date". The feed is `$WIKITOOL_UPDATE_URL`, else the release stamp's, else the built-in origin; `$WIKITOOL_UPDATE_TOKEN` is only needed if that feed is not readable anonymously. Read-only and exempt from the budget gate | | `version check [--url U] [--timeout S] [--json]` | Ask the origin's release feed whether a newer stack exists, and whether the step crosses a compatibility boundary (`state: current\|update\|migration\|ahead`). **The only command in `wikitool` that makes a network call** - never reached implicitly from another command, needs no key, times out, and reports an unreachable feed as an error rather than as "up to date". The feed is `$WIKITOOL_UPDATE_URL`, else the release stamp's, else the built-in origin; `$WIKITOOL_UPDATE_TOKEN` is only needed if that feed is not readable anonymously. Read-only and exempt from the budget gate |
| `version notes [--version X.Y.Z]` | Print one version's `CHANGES.md` entry, for use as release notes (default: this tree's `VERSION`). Read-only and exempt from the budget gate | | `version notes [--version X.Y.Z]` | Print one version's `CHANGES.md` entry, for use as release notes (default: this tree's `VERSION`). Read-only and exempt from the budget gate |
| `version bump --major\|--minor\|--patch --title "<...>" [--no-migration "<reason>"] [--dry-run]` | Raise `VERSION` and open the matching `CHANGES.md` entry - heading, date and author only; the body stays the author's to write, the way `new` writes frontmatter and leaves the prose. Refuses more or fewer than one part, an empty title, and a changelog already documenting a version that is not older than the new one. Compatibility follows the **leftmost non-zero component**, which for this stack (at `1.0.0` and up, no pre-release suffixes anywhere) means MAJOR: PATCH is a fix, MINOR a compatible capability, MAJOR requires instances to migrate. A MAJOR bump additionally requires a migration document targeting the new version, or `--no-migration "<reason>"`, which records the reason in the entry | | `version bump --major\|--minor\|--patch --title "<...>" [--breaking "<what breaks>"] [--no-migration "<reason>"] [--dry-run]` | Raise `VERSION` and open the matching `CHANGES.md` entry - heading, date and author only; the body stays the author's to write, the way `new` writes frontmatter and leaves the prose. Refuses more or fewer than one part, an empty title, and a changelog already documenting a version that is not older than the new one. Compatibility follows the **leftmost non-zero component**, which for this stack (at `1.0.0` and up, no pre-release suffixes anywhere) means MAJOR: PATCH is a fix, MINOR a compatible capability, MAJOR a version that is **not a drop-in replacement** - any hand-work on update, or a downgrade that no longer works. Whether content must be migrated is a second, independent question. A MAJOR bump therefore requires `--breaking "<what stops working>"`, which is refused on any other part, and on top of it a migration document targeting the new version or `--no-migration "<reason>"`; both are recorded in the entry. Which part a change earns stays a judgment call: the command enforces that a crossing documents itself, never that the part was chosen correctly |
| `migrate list [--json]` | List every migration document under `instructions/migrations/`, oldest target first, with its kind. Read-only and **exempt from the Iteration Budget Gate** | | `migrate list [--json]` | List every migration document under `instructions/migrations/`, oldest target first, with its kind. Read-only and **exempt from the Iteration Budget Gate** |
| `migrate status [--json]` | Show the migrations this instance still owes, in the order they must run: every document whose `migrates_to` lies in `(kb_version, VERSION]`. Exits 1 only when `.wikitool-kb.json` is missing - the content's shape is a question the tool refuses to answer by guessing. Read-only and exempt from the budget gate | | `migrate status [--json]` | Show the migrations this instance still owes, in the order they must run: every document whose `migrates_to` lies in `(kb_version, VERSION]`. Exits 1 only when `.wikitool-kb.json` is missing - the content's shape is a question the tool refuses to answer by guessing. Read-only and exempt from the budget gate |
| `migrate verify --from <rev> [--path P ...] [--expect-body-change] [--json] [--fail-on-error]` | Compare `kb/` against a git revision on the invariants a content migration must not change: wikilink and citation **counts** (not sets), footnote definitions, H1, and structural frontmatter. Reports added/removed pages without failing on them. `--expect-body-change` additionally flags a page whose body did not change at all. Not migration-specific - worth running after any bulk rewrite, and the one question `lint` cannot answer, since it reads a single revision and so cannot see that something went missing. Read-only and exempt from the budget gate | | `migrate verify --from <rev> [--path P ...] [--expect-body-change] [--json] [--fail-on-error]` | Compare `kb/` against a git revision on the invariants a content migration must not change: wikilink and citation **counts** (not sets), footnote definitions, H1, and structural frontmatter. Reports added/removed pages without failing on them. `--expect-body-change` additionally flags a page whose body did not change at all. Not migration-specific - worth running after any bulk rewrite, and the one question `lint` cannot answer, since it reads a single revision and so cannot see that something went missing. Read-only and exempt from the budget gate |
| `migrate done <version> [--pages N] [--dry-run]` | Record one migration as applied, advancing `kb_version` in `.wikitool-kb.json` to its target. **Refuses any version that is not the next link in the chain** - skipping one leaves the corpus in a shape no version describes, and an interrupted multi-step upgrade has to be resumable rather than guessable | | `migrate done <version> [--pages N] [--dry-run]` | Record one migration as applied, advancing `kb_version` in `.wikitool-kb.json` to its target. **Refuses any version that is not the next link in the chain** - skipping one leaves the corpus in a shape no version describes, and an interrupted multi-step upgrade has to be resumable rather than guessable |
| `migrate baseline <version> [--force]` | Declare `kb_version` once, for an instance predating `.wikitool-kb.json`. Refuses to overwrite an existing declaration without `--force`: advancing after a migration is `done`, which checks the chain, and this command must not become the quiet way around it | | `migrate baseline <version> [--force]` | Declare `kb_version` once, for an instance predating `.wikitool-kb.json`. Refuses to overwrite an existing declaration without `--force`: advancing after a migration is `done`, which checks the chain, and this command must not become the quiet way around it |
| `doctor [--json]` | Check that this instance is correctly configured: dependencies (Python, ripgrep), author resolution, stack version, git identity/branch/remote, published skills, kb/raw/reports/work/instructions structure, personalization (`USER.md`/`SOUL.md` present **and** filled - a file still carrying the template's sentinel is a `FAIL`, since a renamed template is not a filled one), the environment note (`ENVIRONMENT.md` - optional, so absent is `OK`; a still-templated one is a `WARN`), generated files, and `WIKITOOL_SESSION_ID`. Read-only, exit 1 only on a `FAIL` (a missing remote, session id, or `VERSION` is a `WARN`, not a fault). Exempt from the Iteration Budget Gate | | `doctor [--json]` | Check that this instance is correctly configured: dependencies (Python, ripgrep), author resolution, stack version, git identity/branch/remote, published skills, kb/raw/reports/work/instructions structure, personalization (`USER.md`/`SOUL.md` present **and** filled - a file still carrying the template's sentinel is a `FAIL`, since a renamed template is not a filled one), the KB conventions (`kb/CONVENTIONS.md` present, unsentinelled, and naming all three tool-owned section headings - a `FAIL` on any of the three, because `xref`/`cite` write out of it), the environment note (`ENVIRONMENT.md` - optional, so absent is `OK`; a still-templated one is a `WARN`), generated files, and `WIKITOOL_SESSION_ID`. Read-only, exit 1 only on a `FAIL` (a missing remote, session id, or `VERSION` is a `WARN`, not a fault). Exempt from the Iteration Budget Gate |
## Design notes ## Design notes
@@ -190,7 +190,7 @@ is atomic, and whether a retry is safe.
| `dist export` | Target exists and is not empty, is not a directory, or the tree has no readable `VERSION` | Yes - nothing is written until every file is planned | Point `<target>` at an empty (or new) directory and retry. Never merge into a non-empty one by hand | | `dist export` | Target exists and is not empty, is not a directory, or the tree has no readable `VERSION` | Yes - nothing is written until every file is planned | Point `<target>` at an empty (or new) directory and retry. Never merge into a non-empty one by hand |
| `version show` / `version notes` | `VERSION` is missing or unparseable; for `notes`, no `CHANGES.md` entry names the version asked for | Read-only | Fix `VERSION`, or write the changelog entry (`version bump` writes its heading). Safe to retry | | `version show` / `version notes` | `VERSION` is missing or unparseable; for `notes`, no `CHANGES.md` entry names the version asked for | Read-only | Fix `VERSION`, or write the changelog entry (`version bump` writes its heading). Safe to retry |
| `version check` | The feed could not be reached, answered non-JSON, or carried no `tag_name`. **Never** answers "up to date" for a question it could not ask | Read-only, no local writes | A network failure is transient - retry once, then report it. HTTP 401/403 names `$WIKITOOL_UPDATE_TOKEN`; 404 means no release exists yet or the URL points at the wrong repo | | `version check` | The feed could not be reached, answered non-JSON, or carried no `tag_name`. **Never** answers "up to date" for a question it could not ask | Read-only, no local writes | A network failure is transient - retry once, then report it. HTTP 401/403 names `$WIKITOOL_UPDATE_TOKEN`; 404 means no release exists yet or the URL points at the wrong repo |
| `version bump` | More or fewer than one of `--major/--minor/--patch`, an empty `--title`, a missing `VERSION`/`CHANGES.md`, a changelog already documenting a version not older than the new one, a boundary-crossing bump with neither a migration document nor `--no-migration`, or `--no-migration` on a bump that crosses nothing | No - `VERSION` then `CHANGES.md` | **Not idempotent**: a second run bumps again. If the outcome is uncertain, read `VERSION` and the top of `CHANGES.md` before retrying | | `version bump` | More or fewer than one of `--major/--minor/--patch`, an empty `--title`, a missing `VERSION`/`CHANGES.md`, a changelog already documenting a version not older than the new one, a boundary-crossing bump without `--breaking` or with neither a migration document nor `--no-migration`, or `--breaking`/`--no-migration` on a bump that crosses nothing | No - `VERSION` then `CHANGES.md` | **Not idempotent**: a second run bumps again. If the outcome is uncertain, read `VERSION` and the top of `CHANGES.md` before retrying |
| `migrate list` / `migrate status` | `list` never fails; `status` exits 1 when `.wikitool-kb.json` is missing or unreadable, or `VERSION` is | Read-only | For a missing declaration: run `migrate baseline <version>` once, then retry. Safe to retry freely otherwise | | `migrate list` / `migrate status` | `list` never fails; `status` exits 1 when `.wikitool-kb.json` is missing or unreadable, or `VERSION` is | Read-only | For a missing declaration: run `migrate baseline <version>` once, then retry. Safe to retry freely otherwise |
| `migrate verify` | Only with `--fail-on-error`: an invariant changed. Also exits 1 if `--from` is not a revision in this repository | Read-only | Exit 1 from `--fail-on-error` means "act on the findings", not "the tool is broken". A finding is never fixed by re-running - it names a page and what changed on it | | `migrate verify` | Only with `--fail-on-error`: an invariant changed. Also exits 1 if `--from` is not a revision in this repository | Read-only | Exit 1 from `--fail-on-error` means "act on the findings", not "the tool is broken". A finding is never fixed by re-running - it names a page and what changed on it |
| `migrate done` | Unknown version, no `.wikitool-kb.json`, nothing outstanding, or a version that is not the next link in the chain | Yes - single file write | **Not idempotent**: it advances the chain. For "not the next link", run `migrate status` and apply them in the order it prints - never force the order | | `migrate done` | Unknown version, no `.wikitool-kb.json`, nothing outstanding, or a version that is not the next link in the chain | Yes - single file write | **Not idempotent**: it advances the chain. For "not the next link", run `migrate status` and apply them in the order it prints - never force the order |
+19 -10
View File
@@ -39,7 +39,8 @@ tools/
errors.py ChemenuError / ValidationError / BackendError errors.py ChemenuError / ValidationError / BackendError
corpus_cache.py one parsed corpus per commit, never cached while the tree is dirty corpus_cache.py one parsed corpus per commit, never cached while the tree is dirty
kb_scan.py page iteration/loading over kb/ kb_scan.py page iteration/loading over kb/
kb_collections.py collection discovery (a directory with COLLECTION.md) kb_collections.py collection discovery (a directory with COLLECTION.md), and what one declares about itself
conventions.py kb/CONVENTIONS.md: what this instance decided about authoring, as opposed to what the stack enforces
type_resolver.py type-spec loading and schema resolution type_resolver.py type-spec loading and schema resolution
lint_core.py the lint checks and the report, with no CLI attached lint_core.py the lint checks and the report, with no CLI attached
types_core.py type-spec listing/description, with no CLI attached types_core.py type-spec listing/description, with no CLI attached
@@ -110,15 +111,23 @@ procedure written down in advance is one an agent can complete alone. Whether
a human *actually* saw it is not enforced here - that question is answered in a human *actually* saw it is not enforced here - that question is answered in
the eval layer (`evals/trajectory.py`, `clearance-ended-the-turn`). the eval layer (`evals/trajectory.py`, `clearance-ended-the-turn`).
**Section names are a vocabulary, not literals.** `xref add` writes into Relationships and See **Section names are a vocabulary, not literals - and not the stack's.** `xref add` writes into
Also, and `cite add` owns the trailing Footnotes block, so those three headings are structure the Relationships and See Also, and `cite add` owns the trailing Footnotes block, so those three
tool matches on. They are named once in `sections.py`, and each has one canonical spelling - what headings are structure the tool matches on. *Which words they are* is the corpus's own answer:
the tool writes - plus aliases it still recognizes. That asymmetry is what let the wiki be `conventions.py` reads them from `kb/CONVENTIONS.md`, `sections.py` resolves them on access
translated page by page instead of atomically: an untranslated `## Relationships` is still found (PEP 562, the way `config` resolves its paths), and no heading text is written down in Python
and appended to. Dropping an alias is therefore a breaking change for any page not yet converted, except the pre-conventions fallback for an instance that has not declared one yet.
not a cleanup. Renaming a heading is a migration's job; no other command may do it as a side
effect (see `cite_block_heading` in `provenance.py`, which exists solely so `cite sync` stays a Each slot has one canonical spelling - what the tool writes - plus aliases it still recognizes.
no-op on an untranslated page). That asymmetry is what let the wiki be translated page by page instead of atomically: an
untranslated `## Relationships` is still found and appended to. Dropping an alias is therefore a
breaking change for any page not yet converted, not a cleanup. Renaming a heading is a
migration's job; no other command may do it as a side effect (see `cite_block_heading` in
`provenance.py`, which exists solely so `cite sync` stays a no-op on an untranslated page).
Because the value is resolved rather than bound, nothing may capture it at import time - not a
module constant, not an evaluated default argument. That is why `provenance.CITE_BLOCK_HEADING`
is a module `__getattr__` and `render_cite_block(heading=None)` resolves inside the call.
**Generated output is never committed.** `reports/`, `.agents/skills/` and **Generated output is never committed.** `reports/`, `.agents/skills/` and
`.claude/skills/` are build output; `docs verify` carries canaries in both `.claude/skills/` are build output; `docs verify` carries canaries in both
+55 -9
View File
@@ -2,9 +2,13 @@
repo's machinery. repo's machinery.
`export` copies the pipeline's schema/compiler/control-plane layers (types/, `export` copies the pipeline's schema/compiler/control-plane layers (types/,
tools/, instructions/, the stage contracts, every kb/*/COLLECTION.md) into an tools/, instructions/, the stage contracts) into an empty target, with no kb/
empty target, with no kb/ pages, no raw/ content, and no git history - see pages, no raw/ content, and no git history - see instructions/setup-instance.md
instructions/setup-instance.md for what happens after. It never calls git. for what happens after. It never calls git.
The two binding-but-instance-owned documents under kb/ - each collection's
COLLECTION.md and kb/CONVENTIONS.md - cross as `.template` and are adopted by a
rename, the same split USER.md/SOUL.md use at the repo root.
Three independent exclusion mechanisms feed the plan, for three different Three independent exclusion mechanisms feed the plan, for three different
shapes of "does not belong in someone else's instance": shapes of "does not belong in someone else's instance":
@@ -35,7 +39,7 @@ from typing import Callable, NamedTuple, Optional, Union
import typer import typer
from chemenu import config, kb_collections, kb_state, version as version_mod from chemenu import config, conventions, kb_collections, kb_state, version as version_mod
from chemenu.commands._util import fail, rel_path, success, today_iso from chemenu.commands._util import fail, rel_path, success, today_iso
app = typer.Typer(help="Build a distributable copy of the wiki machinery.") app = typer.Typer(help="Build a distributable copy of the wiki machinery.")
@@ -64,8 +68,14 @@ DIST_TEMPLATES_DIR = Path(__file__).resolve().parent.parent / "dist_templates"
# and does not load `AGENTS.md`, so it ships for the same reason # and does not load `AGENTS.md`, so it ships for the same reason
# `.claude/settings.json` does - a distributed instance running that harness # `.claude/settings.json` does - a distributed instance running that harness
# would otherwise start every session without the control plane. # would otherwise start every session without the control plane.
#
# `INSTALL-MCP.md` ships beside `INSTALL.md` and for the same reason: the MCP
# read server is part of what an instance *has*, even though its dependency is
# optional. A distribution whose server is present but undocumented is one
# whose operator finds the module by reading the source.
ROOT_FILES = ( ROOT_FILES = (
"AGENTS.md", "CLAUDE.md", "README.md", "EVALS.md", "INSTALL.md", ".gitignore", "VERSION", "AGENTS.md", "CLAUDE.md", "README.md", "EVALS.md", "INSTALL.md", "INSTALL-MCP.md",
".gitignore", "VERSION",
*config.LICENSE_FILES, *config.LICENSE_FILES,
*config.PERSONALIZATION_TEMPLATES, *config.PERSONALIZATION_TEMPLATES,
config.ENVIRONMENT_TEMPLATE, config.ENVIRONMENT_TEMPLATE,
@@ -283,12 +293,32 @@ def build_plan(origin: Optional[Origin] = None) -> dict[str, PlannedFile]:
for hook_dir in HOOK_DIRS: for hook_dir in HOOK_DIRS:
plan.update(_copy_tree(config.ROOT / hook_dir, hook_dir, frozenset())) plan.update(_copy_tree(config.ROOT / hook_dir, hook_dir, frozenset()))
# `kb/CONTRACT.md` is stack-owned and ships verbatim; everything beside it
# under `kb/` is the instance's own and ships only as a `.template`. That is
# the personalization split (`USER.md`/`SOUL.md`) one directory down, and
# the reason for it is the same: a distribution can say what the file
# decides, never what this instance decided.
kb_contract = config.KB_DIR / "CONTRACT.md" kb_contract = config.KB_DIR / "CONTRACT.md"
if kb_contract.is_file(): if kb_contract.is_file():
plan["kb/CONTRACT.md"] = _read_planned_file(kb_contract, "kb/CONTRACT.md") plan["kb/CONTRACT.md"] = _read_planned_file(kb_contract, "kb/CONTRACT.md")
conventions_template = config.KB_DIR / conventions.CONVENTIONS_TEMPLATE
if conventions_template.is_file():
rel = f"kb/{conventions.CONVENTIONS_TEMPLATE}"
plan[rel] = _read_planned_file(conventions_template, rel)
# A collection contract is instance-owned too, but unlike `USER.md` the
# shipped content is not *wrong* for the receiver - it is the profile this
# repo's own collections adopted, and a fine starting point. So the file
# itself ships, under the template name: one source of truth here, and a
# receiving instance that has to rename it before it counts. Keeping a
# separate `.template` beside each contract would have meant maintaining two
# near-identical copies of the same text, which is the drift AGENTS.md
# invariant 8 exists to prevent.
for collection in kb_collections.iter_kb_collections(): for collection in kb_collections.iter_kb_collections():
rel = f"kb/{collection.name}/COLLECTION.md" source = collection / kb_collections.CONTRACT_NAME
plan[rel] = _read_planned_file(collection / "COLLECTION.md", rel) rel = f"kb/{collection.name}/{kb_collections.CONTRACT_NAME}.template"
plan[rel] = _read_planned_file(source, rel)
for relative in CONTRACT_ONLY_STAGES: for relative in CONTRACT_ONLY_STAGES:
source = config.ROOT / relative source = config.ROOT / relative
@@ -333,8 +363,21 @@ def build_plan(origin: Optional[Origin] = None) -> dict[str, PlannedFile]:
# IP literals) was considered and rejected - the project's own host legitimately # IP literals) was considered and rejected - the project's own host legitimately
# appears in INSTALL.md and version.py, so such a scan would either whitelist # appears in INSTALL.md and version.py, so such a scan would either whitelist
# the very string it is looking for or cry wolf on every export. # the very string it is looking for or cry wolf on every export.
#
# `COLLECTION.md` and `CONVENTIONS.md` are deliberately *not* on the allowed
# list any more. Both bind, and both are the instance's to write, so they cross
# the boundary as `.template` and are adopted by a rename - a plan carrying the
# filled name would hand a new instance this one's authoring conventions as
# though they were the stack's.
_CONTENT_PREFIXES = ("kb/", "raw/") _CONTENT_PREFIXES = ("kb/", "raw/")
_CONTENT_ALLOWED_NAMES = ("CONTRACT.md", "COLLECTION.md", "log.md", ".gitkeep") _CONTENT_ALLOWED_NAMES = (
"CONTRACT.md",
f"{kb_collections.CONTRACT_NAME}.template",
conventions.CONVENTIONS_TEMPLATE,
"log.md",
".gitkeep",
)
_INSTANCE_OWNED_KB_FILES = (kb_collections.CONTRACT_NAME, conventions.CONVENTIONS_FILENAME)
def find_leaks(plan: dict[str, PlannedFile]) -> list[str]: def find_leaks(plan: dict[str, PlannedFile]) -> list[str]:
@@ -344,6 +387,8 @@ def find_leaks(plan: dict[str, PlannedFile]) -> list[str]:
name = relative.rsplit("/", 1)[-1] name = relative.rsplit("/", 1)[-1]
if name in config.PERSONALIZATION_FILES or name == config.ENVIRONMENT_FILE: if name in config.PERSONALIZATION_FILES or name == config.ENVIRONMENT_FILE:
leaks.append(f"{relative} (one instance's own personalization)") leaks.append(f"{relative} (one instance's own personalization)")
elif relative.startswith("kb/") and name in _INSTANCE_OWNED_KB_FILES:
leaks.append(f"{relative} (this instance's authoring conventions; ship the .template)")
elif relative.startswith("instructions/dev/"): elif relative.startswith("instructions/dev/"):
leaks.append(f"{relative} (stack-development only)") leaks.append(f"{relative} (stack-development only)")
elif relative.startswith(_CONTENT_PREFIXES) and name not in _CONTENT_ALLOWED_NAMES: elif relative.startswith(_CONTENT_PREFIXES) and name not in _CONTENT_ALLOWED_NAMES:
@@ -388,7 +433,8 @@ def export_command(
AGENTS.md/README.md (dev-instance-only marker blocks removed), AGENTS.md/README.md (dev-instance-only marker blocks removed),
instructions/ (no instructions/dev/), types/, tools/ (no venv/caches), instructions/ (no instructions/dev/), types/, tools/ (no venv/caches),
the .github/hooks/+.vibe session-tracing config plus .claude/settings.json, the .github/hooks/+.vibe session-tracing config plus .claude/settings.json,
every kb/*/COLLECTION.md (no pages, no areas), empty kb/CONTRACT.md plus a COLLECTION.md.template per collection and
kb/CONVENTIONS.md.template (no pages, no areas), empty
raw/{articles,documents,notes,assets}/, VERSION, the USER.md/SOUL.md raw/{articles,documents,notes,assets}/, VERSION, the USER.md/SOUL.md
personalization templates (never the filled files), and a personalization templates (never the filled files), and a
.wikitool-release.json stamp. The --source-*/--release-url/--update-url .wikitool-release.json stamp. The --source-*/--release-url/--update-url
+58 -2
View File
@@ -37,7 +37,7 @@ from typing import Optional
import typer import typer
from chemenu import config, kb_collections, version as version_mod from chemenu import config, conventions, kb_collections, version as version_mod
from chemenu.commands._util import fail, rel_path, success from chemenu.commands._util import fail, rel_path, success
app = typer.Typer(help="Verify documentation that mirrors the code or repo layout.") app = typer.Typer(help="Verify documentation that mirrors the code or repo layout.")
@@ -114,6 +114,12 @@ REQUIRED_TRACKED_PATHS = (
"instructions/CONTRACT.md", "instructions/CONTRACT.md",
"instructions/wiki-query/SKILL.md", "instructions/wiki-query/SKILL.md",
"ENVIRONMENT.md.template", "ENVIRONMENT.md.template",
# The one `.template` that lives under a content directory. It is what a
# distribution ships in place of this instance's own `kb/CONVENTIONS.md`, so
# an ignore rule reaching it would produce exports whose receiving instance
# has nothing to fill in - and `find_leaks` refuses to substitute the filled
# file, correctly, so the export would simply be missing it.
"kb/CONVENTIONS.md.template",
) )
CLI_README = config.ROOT / "tools" / "CONTRACT.md" CLI_README = config.ROOT / "tools" / "CONTRACT.md"
@@ -207,13 +213,22 @@ def check_cli_readme() -> list[str]:
def check_collection_contracts() -> list[str]: def check_collection_contracts() -> list[str]:
"""The three structural rules that define what a collection is. """The structural rules that define what a collection is, plus what each one
has to declare about itself.
Collections are discovered by contract presence rather than listed here, so Collections are discovered by contract presence rather than listed here, so
`mkdir kb/<name>` + a COLLECTION.md is all it takes to add one. That only `mkdir kb/<name>` + a COLLECTION.md is all it takes to add one. That only
works if the inverse is also checked: a directory under kb/ *without* a works if the inverse is also checked: a directory under kb/ *without* a
contract is an unclaimed subtree whose pages obey no local rules, and a contract is an unclaimed subtree whose pages obey no local rules, and a
contract outside kb/ quietly widens "collection" back out to "any directory". contract outside kb/ quietly widens "collection" back out to "any directory".
Presence alone stopped being enough once the contracts became
instance-owned. A `COLLECTION.md` an instance wrote can be about anything,
so the two facts the stack still needs from it - which profile it adopted,
and whether the stack resolves against it by name - are declared in its
frontmatter and checked here (`kb_collections.declaration_issues`), together
with the shape of `kb/CONVENTIONS.md`, whose section names the compiler
reads.
""" """
issues = [] issues = []
@@ -243,6 +258,9 @@ def check_collection_contracts() -> list[str]:
if not (config.ROOT / relative_path).exists(): if not (config.ROOT / relative_path).exists():
issues.append(f"{relative_path} is missing - it is the authoring contract for its stage") issues.append(f"{relative_path} is missing - it is the authoring contract for its stage")
issues += kb_collections.declaration_issues()
issues += conventions.declaration_issues()
return issues return issues
@@ -473,6 +491,43 @@ def check_migration_for_boundary() -> list[str]:
] ]
def check_breaking_change_for_boundary() -> list[str]:
"""A version that crosses the compatibility boundary must say what breaks.
Separate from `check_migration_for_boundary`, because the two answer
different questions: that one asks whether the *content* has to move, this
one whether the operator was told the swap is not drop-in at all. A
boundary crossing with an untouched corpus - a renamed feed, artefact,
import name or flag - satisfies that check and still leaves every existing
instance with something to do by hand.
Only the newest entry is checked, for the same reason: older crossings are
history, and re-reporting them forever would make the check noise.
"""
changes_path = config.ROOT / version_mod.CHANGES_FILENAME
version_path = config.ROOT / version_mod.VERSION_FILENAME
if not changes_path.is_file() or not version_path.is_file():
return [] # already reported by check_version_changelog
text = changes_path.read_text(encoding="utf-8")
current = version_mod.top_changes_version(text)
previous = _second_changes_version(text)
if current is None or previous is None:
return [] # the first versioned entry has no predecessor to cross from
if current.compat_key == previous.compat_key:
return []
if version_mod.BREAKING_CHANGE_MARKER in (version_mod.changes_section(text, current) or ""):
return []
return [
f"{current} crosses the compatibility boundary from {previous}, so it is not a drop-in "
f"replacement - but its {version_mod.CHANGES_FILENAME} entry carries no "
f"`{version_mod.BREAKING_CHANGE_MARKER}` line saying what stops working. Add it "
"(`version bump --breaking` writes it; see instructions/dev/version-parts.md)"
]
@app.command("verify") @app.command("verify")
def verify(): def verify():
"""Check the CLI/README command tables, contract presence, type-form drift, ignore rules, and version/changelog agreement.""" """Check the CLI/README command tables, contract presence, type-form drift, ignore rules, and version/changelog agreement."""
@@ -484,6 +539,7 @@ def verify():
+ check_ignored_content() + check_ignored_content()
+ check_version_changelog() + check_version_changelog()
+ check_migration_for_boundary() + check_migration_for_boundary()
+ check_breaking_change_for_boundary()
) )
if issues: if issues:
+46 -4
View File
@@ -20,7 +20,7 @@ from typing import Optional
import typer import typer
from rich.console import Console from rich.console import Console
from chemenu import config, kb_collections, version as version_mod from chemenu import config, conventions, kb_collections, version as version_mod
from chemenu.commands import git_publish, instructions_cmd from chemenu.commands import git_publish, instructions_cmd
from chemenu.commands._util import rel_path from chemenu.commands._util import rel_path
from chemenu.session import ENV_VAR as SESSION_ENV_VAR from chemenu.session import ENV_VAR as SESSION_ENV_VAR
@@ -213,6 +213,47 @@ def check_personalization() -> Check:
return Check("personalization", "OK", f"{', '.join(config.PERSONALIZATION_FILES)} present and filled") return Check("personalization", "OK", f"{', '.join(config.PERSONALIZATION_FILES)} present and filled")
def check_conventions() -> Check:
"""Whether this instance has said how its own pages are written.
`kb/CONVENTIONS.md` carries the decisions `kb/CONTRACT.md` deliberately no
longer makes: the KB language and its three tool-owned section headings, the
relationship-label vocabulary, the tone examples, the confidence rubric, the
ADR prefix. The compiler reads the section names out of it, so an instance
without one is not merely undocumented - `xref add` and `cite add` fall back
to the names this stack hardcoded before the file existed, which is right
only for a corpus that was written under them.
Hence `FAIL` rather than `WARN`, and hence the same two failure modes the
personalization pair has: the distribution can ship the template but never
the filled file, so a template renamed and left unanswered looks present and
decides nothing.
"""
path = conventions.conventions_file()
fix = (
"Copy kb/CONVENTIONS.md.template to kb/CONVENTIONS.md and answer it - the KB-language "
"step of instructions/setup-instance.md walks it, and instructions/kb-profiles.md has "
"the ready-made profiles to adopt"
)
if not path.is_file():
return Check(
"conventions", "FAIL",
f"kb/{conventions.CONVENTIONS_FILENAME} is missing - this instance has not "
"declared how its pages are written",
fix,
)
issues = conventions.declaration_issues()
if issues:
return Check("conventions", "FAIL", "; ".join(issues), fix)
declared = conventions.language() or "unspecified"
headings = ", ".join(conventions.canonical(slot) for slot in conventions.SLOTS)
return Check(
"conventions", "OK",
f"kb/{conventions.CONVENTIONS_FILENAME} present, language {declared}, "
f"sections {headings}",
)
def check_environment() -> Check: def check_environment() -> Check:
"""Whether this checkout records the environment it works through. """Whether this checkout records the environment it works through.
@@ -399,6 +440,7 @@ def run_doctor() -> list[Check]:
check_skills(), check_skills(),
check_structure(), check_structure(),
check_personalization(), check_personalization(),
check_conventions(),
check_environment(), check_environment(),
check_publish_remotes(), check_publish_remotes(),
check_generated_files(), check_generated_files(),
@@ -411,9 +453,9 @@ def doctor_command(
json_out: bool = typer.Option(False, "--json", help="Print the checks as JSON"), json_out: bool = typer.Option(False, "--json", help="Print the checks as JSON"),
): ):
"""Check that this instance is correctly configured: dependencies, author, """Check that this instance is correctly configured: dependencies, author,
git identity/remote, published skills, structure, personalization, git identity/remote, published skills, structure, personalization, KB
generated files, and session scoping. Read-only. Exits 1 only if a check conventions, generated files, and session scoping. Read-only. Exits 1 only
FAILs.""" if a check FAILs."""
checks = run_doctor() checks = run_doctor()
if json_out: if json_out:
+13 -3
View File
@@ -24,7 +24,7 @@ import re
import typer import typer
from chemenu import config from chemenu import config, conventions
from chemenu.commands._util import ( from chemenu.commands._util import (
check_collision, check_collision,
check_raw_files_exist, check_raw_files_exist,
@@ -166,7 +166,11 @@ def _apply_template_variables(template: str, variables: Dict[str, Any]) -> str:
"""Apply variable substitutions to a template string. """Apply variable substitutions to a template string.
Supports: Supports:
- `{field}` - plain substitution from `variables[field]` - `{field}` - plain substitution from `variables[field]`, including the
`{section.<slot>}` names this instance gave the three tool-owned
headings (see chemenu.conventions). Those are what took the KB language
out of `types/*.md`: a template writes `## {section.relationships}`, so
scaffolding a page in another language needs no edit under `types/`
- `{field|filter}` - apply a named filter (bullets, join, capitalize) - `{field|filter}` - apply a named filter (bullets, join, capitalize)
to `variables[field]`'s value, so templates can render list/enum to `variables[field]`'s value, so templates can render list/enum
frontmatter fields directly instead of the caller precomputing a frontmatter fields directly instead of the caller precomputing a
@@ -324,7 +328,13 @@ def new_page_command(
path = target_dir / f"{page_title}.md" path = target_dir / f"{page_title}.md"
body = _apply_template_variables( body = _apply_template_variables(
template, {**frontmatter, "name": name, "today": today.isoformat()} template,
{
**frontmatter,
"name": name,
"today": today.isoformat(),
**conventions.section_variables(),
},
) )
write_page(path, frontmatter, body) write_page(path, frontmatter, body)
+31 -4
View File
@@ -189,6 +189,11 @@ def bump_command(
minor: bool = typer.Option(False, "--minor", help="Bump MINOR (resets PATCH)"), minor: bool = typer.Option(False, "--minor", help="Bump MINOR (resets PATCH)"),
patch: bool = typer.Option(False, "--patch", help="Bump PATCH"), patch: bool = typer.Option(False, "--patch", help="Bump PATCH"),
title: str = typer.Option(..., "--title", help="One-line title for the new CHANGES.md entry"), title: str = typer.Option(..., "--title", help="One-line title for the new CHANGES.md entry"),
breaking: Optional[str] = typer.Option(
None,
"--breaking",
help="What stops working, for a boundary-crossing bump (recorded in CHANGES.md). Required on one, refused on any other",
),
no_migration: Optional[str] = typer.Option( no_migration: Optional[str] = typer.Option(
None, None,
"--no-migration", "--no-migration",
@@ -203,10 +208,14 @@ def bump_command(
frontmatter and leaves the prose. `docs verify` afterwards enforces that frontmatter and leaves the prose. `docs verify` afterwards enforces that
the two agree, so a bump with no entry cannot reach a release. the two agree, so a bump with no entry cannot reach a release.
A bump that crosses the compatibility boundary additionally requires a A bump that crosses the compatibility boundary - one whose new version is
migration document for the new version, or `--no-migration "<reason>"`. not a drop-in replacement, whether or not any content moves - requires
An instance learning that it must migrate, with nothing telling it how, is `--breaking "<what stops working>"`, and on top of that either a migration
the gap this closes.""" document for the new version or `--no-migration "<reason>"`. An instance
learning that it must migrate, with nothing telling it what broke or how to
cross, is the gap these close. Which part to pass stays a judgment call
this command does not make - it enforces only that a crossing says what it
costs."""
selected = [name for name, chosen in (("major", major), ("minor", minor), ("patch", patch)) if chosen] selected = [name for name, chosen in (("major", major), ("minor", minor), ("patch", patch)) if chosen]
if len(selected) != 1: if len(selected) != 1:
fail("Pass exactly one of --major / --minor / --patch") fail("Pass exactly one of --major / --minor / --patch")
@@ -239,6 +248,23 @@ def bump_command(
crossing = new_version.compat_key != current.compat_key crossing = new_version.compat_key != current.compat_key
boundary = " (crosses a compatibility boundary - instances must migrate)" if crossing else "" boundary = " (crosses a compatibility boundary - instances must migrate)" if crossing else ""
if crossing and not breaking:
fail(
f"{current} -> {new_version} crosses the compatibility boundary, so it is not a "
f"drop-in replacement - re-run with --breaking \"<what stops working, and what an "
f"instance must do about it>\".\n"
f"If that sentence is hard to write because nothing actually breaks - no hand-work "
f"on update, and the old version can still be put back - then the bump is probably "
f"not --{selected[0]}."
)
return
if breaking and not crossing:
fail(
f"--breaking only applies to a bump that crosses the compatibility boundary; "
f"{current} -> {new_version} does not."
)
return
if crossing and not no_migration: if crossing and not no_migration:
from chemenu import kb_state from chemenu import kb_state
@@ -267,6 +293,7 @@ def bump_command(
version_mod.insert_changes_entry( version_mod.insert_changes_entry(
text, new_version, today_iso(), title.strip(), author, text, new_version, today_iso(), title.strip(), author,
no_migration_reason=no_migration.strip() if no_migration else None, no_migration_reason=no_migration.strip() if no_migration else None,
breaking_reason=breaking.strip() if breaking else None,
), ),
encoding="utf-8", encoding="utf-8",
) )
+224
View File
@@ -0,0 +1,224 @@
"""What this instance decided, read from `kb/CONVENTIONS.md`.
`kb/CONTRACT.md` and this file answer two different questions. The contract
holds what the code enforces - what a collection is, which files are generated,
how `provenance:` and `confidence_base` work - and is identical in every
instance, so `dist export` ships it verbatim. `kb/CONVENTIONS.md` holds what
each instance decides for itself: the language its pages are written in, the
relationship-label vocabulary, the tone examples, the confidence rubric, the
ADR prefix. The distribution ships only `kb/CONVENTIONS.md.template`, exactly
the split `USER.md`/`SOUL.md` already use one directory up.
Only one part of it is machine-read, and it is the part that used to be Python:
the three section headings `xref add` and `cite add` write. While
`RELATIONSHIPS = "Beziehungen"` sat in `sections.py`, an instance writing its
pages in any other language had to edit the compiler to say so - which made the
KB language a stack property in code while every document called it an instance
decision.
**A missing conventions file is not an error here.** It is the state an
instance is in between installing this machinery and running the migration that
writes the file, and every command has to keep working through it. The fallback
is `PRE_CONVENTIONS_NAMES` - not "the stack's language", but *what this stack
hardcoded before the file existed*, which is by construction what any corpus
reaching that state was written with. `wikitool doctor` is what says the file is
missing; degrading loudly here would take out `doctor` itself.
"""
from __future__ import annotations
from pathlib import Path
from typing import Any, Optional
from chemenu import config
from chemenu.frontmatter_io import read_page
CONVENTIONS_FILENAME = "CONVENTIONS.md"
CONVENTIONS_TEMPLATE = f"{CONVENTIONS_FILENAME}.template"
# The three tool-owned headings, by slot name. The slot is the stable
# identifier - it is what code, the type-spec templates and the conventions
# file all key on - while the heading text itself is the instance's to choose.
RELATIONSHIPS = "relationships"
SEE_ALSO = "see_also"
FOOTNOTES = "footnotes"
SLOTS = (RELATIONSHIPS, SEE_ALSO, FOOTNOTES)
# Frontmatter keys read out of kb/CONVENTIONS.md.
SECTIONS_KEY = "sections"
SECTION_ALIASES_KEY = "section_aliases"
LANGUAGE_KEY = "language"
# Every heading name this stack has ever written as canonical, newest first.
# Two jobs, and they are separate: the first entry is the fallback for an
# instance that has no conventions file yet, and the whole tuple is an implicit
# alias set that every instance recognizes regardless of what it declares. The
# second is what makes a corpus translatable page by page - a page still
# carrying `## Footnotes` is untranslated, not broken, and `cite sync` has to
# stay a no-op on it.
PRE_CONVENTIONS_NAMES: dict[str, tuple[str, ...]] = {
RELATIONSHIPS: ("Beziehungen", "Relationships"),
SEE_ALSO: ("Siehe auch", "See Also"),
FOOTNOTES: ("Fußnoten", "Footnotes"),
}
def conventions_file() -> Path:
return config.KB_DIR / CONVENTIONS_FILENAME
# (path, mtime_ns, size) -> frontmatter. `heading_re()` is called once per page
# per lint run, so re-reading the file each time would put a stat+parse on a
# per-page path for a document that changes about once per instance. Keyed on
# the stat rather than on the path alone, so a test that rewrites the file
# inside one process is not answered out of the cache.
_CACHE: dict[tuple[str, int, int], dict[str, Any]] = {}
def read_conventions() -> dict[str, Any]:
"""`kb/CONVENTIONS.md`'s frontmatter, or `{}` if the file is absent.
Permissive on purpose, like `read_page` itself: a conventions file with
broken YAML degrades to the pre-conventions defaults rather than taking
every command down with it. `doctor` and `docs verify` are where that
surfaces as a finding.
"""
path = conventions_file()
if not path.is_file():
return {}
stat = path.stat()
key = (str(path), stat.st_mtime_ns, stat.st_size)
if key not in _CACHE:
frontmatter, _ = read_page(path)
_CACHE.clear()
_CACHE[key] = frontmatter
return _CACHE[key]
def reset_cache() -> None:
"""Drop the parsed conventions. For a caller that rewrote the file and has
to see the new value within the same stat resolution."""
_CACHE.clear()
def _mapping(key: str) -> dict[str, Any]:
value = read_conventions().get(key)
return value if isinstance(value, dict) else {}
def language() -> Optional[str]:
"""The declared KB language tag (e.g. `de`), or None if undeclared.
Nothing in the compiler branches on it - the language is carried by the
prose the instance writes, not by a switch. It is here because the
conventions file is where a human and an agent look the answer up, and
because `doctor` reports it.
"""
value = read_conventions().get(LANGUAGE_KEY)
if value is None:
return None
return str(value).strip() or None
def canonical(slot: str) -> str:
"""The heading name this instance writes for `slot`."""
declared = _mapping(SECTIONS_KEY).get(slot)
if isinstance(declared, str) and declared.strip():
return declared.strip()
return PRE_CONVENTIONS_NAMES[slot][0]
def names(slot: str) -> tuple[str, ...]:
"""Every heading name `slot` is recognized under, canonical first.
The canonical name, then any `section_aliases:` the instance declared, then
the names this stack wrote before the conventions file existed. Deduplicated
while preserving that order, so an instance declaring English does not end
up with `Relationships` listed twice.
"""
declared_aliases = _mapping(SECTION_ALIASES_KEY).get(slot)
extra = declared_aliases if isinstance(declared_aliases, list) else []
ordered = [
canonical(slot),
*(str(name).strip() for name in extra if str(name).strip()),
*PRE_CONVENTIONS_NAMES[slot],
]
seen: dict[str, None] = {}
for name in ordered:
seen.setdefault(name, None)
return tuple(seen)
def section_variables() -> dict[str, str]:
"""The `{section.<slot>}` substitutions a type-spec template can use.
This is what took the three German headings out of `types/*.md`: a template
writes `## {section.relationships}` and the instance's own conventions fill
it in, so scaffolding a page in another language needs no edit under
`types/`.
"""
return {f"section.{slot}": canonical(slot) for slot in SLOTS}
def declaration_issues() -> list[str]:
"""What is wrong with this instance's conventions file, if anything.
Shared by `doctor` (which FAILs on it) and `docs verify` (which refuses a
tree with it), so the two cannot disagree about what a valid declaration
looks like. An absent file is *not* reported here - that is a separate
finding with a separate fix, and only `doctor` makes it one.
"""
path = conventions_file()
if not path.is_file():
return []
issues: list[str] = []
frontmatter, _ = read_page(path)
if not frontmatter:
return [
f"kb/{CONVENTIONS_FILENAME} has no readable frontmatter - it must declare "
f"`{SECTIONS_KEY}:` with the heading names this instance writes"
]
declared = frontmatter.get(SECTIONS_KEY)
if not isinstance(declared, dict):
return [
f"kb/{CONVENTIONS_FILENAME}: `{SECTIONS_KEY}:` must be a mapping of "
f"{'/'.join(SLOTS)} to the heading text this instance writes"
]
for slot in SLOTS:
value = declared.get(slot)
if not isinstance(value, str) or not value.strip():
issues.append(
f"kb/{CONVENTIONS_FILENAME}: `{SECTIONS_KEY}.{slot}` is missing or empty - "
"`xref add` and `cite add` write into a heading this instance has not named"
)
for slot in sorted(set(declared) - set(SLOTS)):
issues.append(
f"kb/{CONVENTIONS_FILENAME}: `{SECTIONS_KEY}.{slot}` is not a section the tool "
f"owns; the slots are {', '.join(SLOTS)}"
)
aliases = frontmatter.get(SECTION_ALIASES_KEY, {})
if not isinstance(aliases, dict):
issues.append(
f"kb/{CONVENTIONS_FILENAME}: `{SECTION_ALIASES_KEY}:` must be a mapping of a "
"slot to the list of headings still recognized under it"
)
else:
for slot, value in sorted(aliases.items()):
if slot not in SLOTS:
issues.append(
f"kb/{CONVENTIONS_FILENAME}: `{SECTION_ALIASES_KEY}.{slot}` is not a "
f"section the tool owns; the slots are {', '.join(SLOTS)}"
)
elif not isinstance(value, list):
issues.append(
f"kb/{CONVENTIONS_FILENAME}: `{SECTION_ALIASES_KEY}.{slot}` must be a list"
)
if config.TEMPLATE_SENTINEL in path.read_text(encoding="utf-8"):
issues.append(
f"kb/{CONVENTIONS_FILENAME} still carries the `{config.TEMPLATE_SENTINEL}` line - "
"a renamed template is not a filled one"
)
return issues
+98
View File
@@ -20,11 +20,32 @@ Two corollaries are enforced rather than documented:
from __future__ import annotations from __future__ import annotations
from pathlib import Path from pathlib import Path
from typing import Any
from chemenu import config from chemenu import config
CONTRACT_NAME = "COLLECTION.md" CONTRACT_NAME = "COLLECTION.md"
# What a collection declares about itself, in `COLLECTION.md`'s frontmatter.
#
# Presence on the filesystem says a collection *exists*; it cannot say who owns
# the rules inside it. A `COLLECTION.md` is instance-owned - the distribution
# ships a `.template` per default collection and the instance writes the real
# one - so the two facts the stack still needs from it have to be declared
# rather than inferred from the directory name, which an instance is free to
# choose.
PROFILE_FIELD = "profile"
REQUIRED_BY_STACK_FIELD = "required_by_stack"
# Collections `wikitool` itself depends on by name, as opposed to ones that
# merely hold pages. `sources` is here because three parts of the stack resolve
# against it rather than against a page's type: `sources coverage` asks which
# raw files no source page claims, every `[^cite-id]` footnote resolves to a
# page in it, and `sources rebuild-index` writes `kb/provenance.md` from it. An
# instance may add, rename or drop any collection that is not on this list;
# renaming one that is leaves those three with nothing to resolve against.
STACK_REQUIRED_COLLECTIONS = ("sources",)
def iter_kb_collections(kb_dir: Path | None = None) -> list[Path]: def iter_kb_collections(kb_dir: Path | None = None) -> list[Path]:
"""Return every collection directory under kb/, sorted by name. """Return every collection directory under kb/, sorted by name.
@@ -82,6 +103,83 @@ def stray_collection_contracts(root: Path | None = None, kb_dir: Path | None = N
return sorted(stray) return sorted(stray)
def collection_declaration(collection: Path) -> dict[str, Any]:
"""A collection's own `COLLECTION.md` frontmatter, or `{}` if it has none.
Permissive like every other frontmatter read in this package: an unreadable
declaration degrades to empty here and is reported by `docs verify`, rather
than taking down the discovery every command starts with.
"""
from chemenu.frontmatter_io import read_page
contract = collection / CONTRACT_NAME
if not contract.is_file():
return {}
frontmatter, _ = read_page(contract)
return frontmatter
def declaration_issues(kb_dir: Path | None = None) -> list[str]:
"""What each `COLLECTION.md` fails to declare about itself.
Two fields, for two questions the filesystem cannot answer. `profile:`
names the entry in `instructions/kb-profiles.md` this collection adopted -
free text, because the profile catalogue is a palette rather than an enum,
and a collection an instance invented has no entry there to name.
`required_by_stack:` is not the instance's to choose at all: it must agree
with `STACK_REQUIRED_COLLECTIONS`, so a collection whose contract claims the
stack depends on it - or one the stack does depend on and that says it does
not - is a finding rather than a preference.
"""
root = kb_dir if kb_dir is not None else config.KB_DIR
issues: list[str] = []
present = {path.name for path in iter_kb_collections(root)}
for name in STACK_REQUIRED_COLLECTIONS:
if name not in present:
issues.append(
f"kb/{name}/ is missing - `sources coverage`, `[^cite-id]` resolution and "
f"`kb/provenance.md` all resolve against it by name"
)
for collection in iter_kb_collections(root):
relative = f"kb/{collection.name}/{CONTRACT_NAME}"
declared = collection_declaration(collection)
if not declared:
issues.append(
f"{relative} has no frontmatter - it must declare `{PROFILE_FIELD}:` and "
f"`{REQUIRED_BY_STACK_FIELD}:` (see instructions/kb-profiles.md)"
)
continue
profile = declared.get(PROFILE_FIELD)
if not isinstance(profile, str) or not profile.strip():
issues.append(
f"{relative}: `{PROFILE_FIELD}:` is missing or empty - name the profile from "
f"instructions/kb-profiles.md this collection adopted, or `none`"
)
required = declared.get(REQUIRED_BY_STACK_FIELD)
expected = collection.name in STACK_REQUIRED_COLLECTIONS
if not isinstance(required, bool):
issues.append(
f"{relative}: `{REQUIRED_BY_STACK_FIELD}:` is missing or not a boolean - "
f"it must be {str(expected).lower()} for this collection"
)
elif required != expected:
issues.append(
f"{relative}: `{REQUIRED_BY_STACK_FIELD}: {str(required).lower()}` contradicts "
f"the stack, which "
+ (
"does depend on this collection by name"
if expected
else "depends on no collection of this name"
)
+ f" - it must be {str(expected).lower()}"
)
return issues
def _is_vendored(path: Path, repo_root: Path) -> bool: def _is_vendored(path: Path, repo_root: Path) -> bool:
try: try:
relative = path.relative_to(repo_root) relative = path.relative_to(repo_root)
+3 -3
View File
@@ -14,9 +14,9 @@ WIKILINK_RE = re.compile(r"\[\[([^\]|#]+)")
# Root-level files under kb/ that are not pages: the generated catalog map, log # Root-level files under kb/ that are not pages: the generated catalog map, log
# and provenance index, plus the contract that constrains the tree rather than # and provenance index, plus the two documents that constrain the tree rather
# living in it. # than living in it - the stack's contract and this instance's own conventions.
_KB_META_FILES = {"index.md", "log.md", "provenance.md", "CONTRACT.md"} _KB_META_FILES = {"index.md", "log.md", "provenance.md", "CONTRACT.md", "CONVENTIONS.md"}
# The per-collection authoring contract. Unlike the meta files above it is never # The per-collection authoring contract. Unlike the meta files above it is never
# at the kb root - it sits one level down, in every collection - so it has to be # at the kb root - it sits one level down, in every collection - so it has to be
+26 -6
View File
@@ -64,7 +64,22 @@ LEGACY_CITE_RE = re.compile(r"\^\[\[([^\]|#]+)(?:\|([^\]]+))?\]\]")
# Written under the canonical name, but split_cite_block() matches the aliases # Written under the canonical name, but split_cite_block() matches the aliases
# too - a page whose block still says "## Footnotes" keeps working until it is # too - a page whose block still says "## Footnotes" keeps working until it is
# translated. See chemenu/sections.py. # translated. See chemenu/sections.py.
CITE_BLOCK_HEADING = f"## {sections.FOOTNOTES}" #
# Resolved on access rather than bound at import (PEP 562), because the
# canonical name is now this instance's own - `kb/CONVENTIONS.md`, via
# chemenu.conventions - and a module constant would freeze whichever corpus the
# process started in. The functions below take it as a default the same way, via
# None rather than an evaluated default argument.
def __getattr__(name: str) -> str:
if name == "CITE_BLOCK_HEADING":
return f"## {sections.FOOTNOTES}"
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
def cite_block_heading_default() -> str:
"""The Footnotes heading this instance writes, `## ` included."""
return f"## {sections.FOOTNOTES}"
_SOURCE_TITLE_PREFIX = "Source - " _SOURCE_TITLE_PREFIX = "Source - "
@@ -193,18 +208,23 @@ def cite_block_heading(body: str) -> str:
alias is untranslated, not broken, and `cite sync` has to stay a no-op on alias is untranslated, not broken, and `cite sync` has to stay a no-op on
it. Translating the heading is the migration's job, not the tool's.""" it. Translating the heading is the migration's job, not the tool's."""
match = sections.heading_re(sections.FOOTNOTES).search(body) match = sections.heading_re(sections.FOOTNOTES).search(body)
return match.group(0).strip() if match else CITE_BLOCK_HEADING return match.group(0).strip() if match else cite_block_heading_default()
def render_cite_block( def render_cite_block(
definitions: dict[str, tuple[str, Optional[str]]], heading: str = CITE_BLOCK_HEADING definitions: dict[str, tuple[str, Optional[str]]], heading: Optional[str] = None
) -> str: ) -> str:
"""Render the Footnotes block for `definitions` (cite_id -> (title, """Render the Footnotes block for `definitions` (cite_id -> (title,
qualifier)), preserving dict order. Empty dict renders "" - a page with qualifier)), preserving dict order. Empty dict renders "" - a page with
no citations carries no block at all.""" no citations carries no block at all.
`heading=None` means this instance's canonical Footnotes heading, resolved
at call time. It cannot be an evaluated default: the name comes from
`kb/CONVENTIONS.md`, so a default bound at import would answer for whichever
corpus the process started in."""
if not definitions: if not definitions:
return "" return ""
lines = [heading, ""] lines = [heading or cite_block_heading_default(), ""]
for cid, (title, qualifier) in definitions.items(): for cid, (title, qualifier) in definitions.items():
target = f"{title}|{qualifier}" if qualifier else title target = f"{title}|{qualifier}" if qualifier else title
lines.append(f"[^{cid}]: [[{target}]]") lines.append(f"[^{cid}]: [[{target}]]")
@@ -214,7 +234,7 @@ def render_cite_block(
def render_page_body( def render_page_body(
head: str, head: str,
definitions: dict[str, tuple[str, Optional[str]]], definitions: dict[str, tuple[str, Optional[str]]],
heading: str = CITE_BLOCK_HEADING, heading: Optional[str] = None,
) -> str: ) -> str:
"""Reassemble a page body from its non-Footnotes content and citation """Reassemble a page body from its non-Footnotes content and citation
definitions - the inverse of split_cite_block(). Pass the original body's definitions - the inverse of split_cite_block(). Pass the original body's
+50 -13
View File
@@ -5,10 +5,12 @@ See Also by name, and `cite add` owns the trailing Footnotes block. An author
may add any other heading they like - only the ones named here are matched by may add any other heading they like - only the ones named here are matched by
the tool, and only these have to stay predictable. the tool, and only these have to stay predictable.
kb/CONTRACT.md's Language rule puts page prose in the KB language. That used to **Which words they are is the instance's decision, not the stack's.** They
force these three to stay English, because a translated heading did not error - follow the KB language, and the KB language is declared in `kb/CONVENTIONS.md`
it made `xref add` append a *second* section, silently. This module removes that (see `chemenu.conventions`). This module used to hold `RELATIONSHIPS =
constraint by making the vocabulary explicit in one place. "Beziehungen"` as a Python constant, which made an instance writing its pages
in any other language edit the compiler to say so - the one place a documented
instance convention had leaked into code.
Each heading has one **canonical** name - what the tool writes - and any number Each heading has one **canonical** name - what the tool writes - and any number
of **aliases** it still recognizes. That asymmetry is what lets a corpus migrate of **aliases** it still recognizes. That asymmetry is what lets a corpus migrate
@@ -16,24 +18,59 @@ page by page instead of all at once: a page still carrying `## Relationships` is
found and appended to correctly, and only takes the canonical name when the page found and appended to correctly, and only takes the canonical name when the page
itself is translated. Removing an alias is therefore a breaking change for every itself is translated. Removing an alias is therefore a breaking change for every
page not yet converted, not a cleanup. page not yet converted, not a cleanup.
The three module attributes below resolve on access (PEP 562), the same way
`config`'s paths do and for the same reason: a caller that repoints `KB_DIR`
must not be answered out of a value bound at import time by whichever tree the
process started in.
""" """
from __future__ import annotations
import re import re
RELATIONSHIPS = "Beziehungen" from chemenu import conventions
SEE_ALSO = "Siehe auch"
FOOTNOTES = "Fußnoten"
ALIASES: dict[str, tuple[str, ...]] = { # The slots, re-exported so a caller keeps using `sections.RELATIONSHIPS` as an
RELATIONSHIPS: ("Relationships",), # opaque handle. The value it resolves to is the heading text; the name it is
SEE_ALSO: ("See Also",), # looked up under is stable.
FOOTNOTES: ("Footnotes",), _SLOT_ATTRS = {
"RELATIONSHIPS": conventions.RELATIONSHIPS,
"SEE_ALSO": conventions.SEE_ALSO,
"FOOTNOTES": conventions.FOOTNOTES,
} }
def __getattr__(name: str) -> str:
slot = _SLOT_ATTRS.get(name)
if slot is None:
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
return conventions.canonical(slot)
def __dir__() -> list[str]:
return sorted([*globals(), *_SLOT_ATTRS])
def _slot_of(canonical: str) -> str:
"""The slot whose current canonical name is `canonical`.
Callers hold on to the resolved heading text (`sections.FOOTNOTES`), not to
the slot, so the lookup has to go back the other way. Falls back to matching
against every name a slot is recognized under, so a caller that resolved the
attribute before the conventions file changed still lands on the right slot.
"""
for slot in conventions.SLOTS:
if canonical == conventions.canonical(slot):
return slot
for slot in conventions.SLOTS:
if canonical in conventions.names(slot):
return slot
raise ValueError(f"{canonical!r} is not a tool-owned section heading")
def names(canonical: str) -> tuple[str, ...]: def names(canonical: str) -> tuple[str, ...]:
"""Every name `canonical` is recognized under, canonical first.""" """Every name `canonical` is recognized under, canonical first."""
return (canonical, *ALIASES.get(canonical, ())) return conventions.names(_slot_of(canonical))
def heading_re(canonical: str) -> re.Pattern[str]: def heading_re(canonical: str) -> re.Pattern[str]:
@@ -44,4 +81,4 @@ def heading_re(canonical: str) -> re.Pattern[str]:
def is_known(heading: str) -> bool: def is_known(heading: str) -> bool:
"""True if `heading` is a canonical name or an alias of one.""" """True if `heading` is a canonical name or an alias of one."""
return any(heading in names(canonical) for canonical in ALIASES) return any(heading in conventions.names(slot) for slot in conventions.SLOTS)
+9 -1
View File
@@ -3,7 +3,7 @@ from pathlib import Path
import pytest import pytest
from chemenu import config from chemenu import config, conventions
from chemenu.frontmatter_io import write_page from chemenu.frontmatter_io import write_page
from chemenu.type_resolver import resolver from chemenu.type_resolver import resolver
@@ -77,9 +77,17 @@ def hermetic_environment(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
# attribute. That binding outlives the test and hands the next one a # attribute. That binding outlives the test and hands the next one a
# corpus directory belonging to the previous tree. Cleared on both sides, # corpus directory belonging to the previous tree. Cleared on both sides,
# so neither a leak from before nor one from this test can be inherited. # so neither a leak from before nor one from this test can be inherited.
# One layer further in again: `conventions` parses `kb/CONVENTIONS.md` once
# and keys the result on the file's own path and stat, so a repointed
# `KB_DIR` cannot be answered out of it. Cleared here anyway, on both sides,
# for the same reason `config.reset()` is - a fixture that leaves state
# behind is the hole this file exists to close, and the cost of proving it
# cannot leak is one function call per test.
config.reset() config.reset()
conventions.reset_cache()
yield home yield home
config.reset() config.reset()
conventions.reset_cache()
def use_shipped_type_specs(monkeypatch: pytest.MonkeyPatch) -> None: def use_shipped_type_specs(monkeypatch: pytest.MonkeyPatch) -> None:
+167
View File
@@ -0,0 +1,167 @@
"""Tests for `kb/CONVENTIONS.md` - the instance-owned half of the authoring rules.
Two things are under test here, and they are the two the split exists for: the
compiler reads its section headings from the corpus rather than from Python, and
a collection declares who owns its rules rather than having it inferred from the
directory name.
"""
from __future__ import annotations
from pathlib import Path
import pytest
from chemenu import config, conventions, kb_collections, sections
GERMAN = (
"---\n"
"language: de\n"
"profile: german\n"
"sections:\n"
" relationships: Beziehungen\n"
" see_also: Siehe auch\n"
" footnotes: Fußnoten\n"
"---\n\n# conventions\n"
)
FRENCH = (
"---\n"
"language: fr\n"
"profile: none\n"
"sections:\n"
" relationships: Relations\n"
" see_also: Voir aussi\n"
" footnotes: Notes\n"
"section_aliases:\n"
" relationships: [Beziehungen]\n"
"---\n\n# conventions\n"
)
@pytest.fixture
def kb_root(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
kb = tmp_path / "kb"
kb.mkdir()
monkeypatch.setattr(config, "ROOT", tmp_path)
monkeypatch.setattr(config, "KB_DIR", kb)
conventions.reset_cache()
yield kb
conventions.reset_cache()
def _write(kb: Path, text: str) -> None:
(kb / conventions.CONVENTIONS_FILENAME).write_text(text, encoding="utf-8")
conventions.reset_cache()
def _collection(kb: Path, name: str, profile: str = "none", required: bool = False) -> Path:
directory = kb / name
directory.mkdir(parents=True, exist_ok=True)
(directory / kb_collections.CONTRACT_NAME).write_text(
f"---\nprofile: {profile}\nrequired_by_stack: {str(required).lower()}\n---\n\n# {name}\n",
encoding="utf-8",
)
return directory
def test_missing_file_falls_back_to_what_the_stack_used_to_hardcode(kb_root):
"""The state between installing this machinery and running the migration
that writes the file. Every command has to keep working through it, and the
only corpus that can be in it was written under these names."""
assert conventions.canonical(conventions.FOOTNOTES) == "Fußnoten"
assert sections.FOOTNOTES == "Fußnoten"
def test_the_compiler_writes_the_headings_the_instance_declared(kb_root):
_write(kb_root, FRENCH)
assert sections.RELATIONSHIPS == "Relations"
assert sections.SEE_ALSO == "Voir aussi"
assert sections.FOOTNOTES == "Notes"
def test_declared_aliases_and_the_pre_conventions_names_are_both_recognized(kb_root):
"""The translation path. A page still carrying the old heading has to be
found and appended to, or a language change would silently split every page
into two Relationships sections."""
_write(kb_root, FRENCH)
pattern = sections.heading_re(sections.RELATIONSHIPS)
for heading in ("## Relations", "## Beziehungen", "## Relationships"):
assert pattern.search(f"# Page\n\n{heading}\n\n- x\n"), heading
def test_the_canonical_name_is_not_duplicated_among_its_aliases(kb_root):
"""An instance declaring the pre-conventions name gets it once, not twice -
otherwise `heading_re`'s alternation carries a redundant branch and
`names()` misreports what a page could be carrying."""
_write(
kb_root,
"---\nsections:\n relationships: Relationships\n"
" see_also: See Also\n footnotes: Footnotes\n---\n",
)
names = conventions.names(conventions.RELATIONSHIPS)
assert names[0] == "Relationships"
assert len(names) == len(set(names))
def test_section_variables_are_what_a_type_spec_template_substitutes(kb_root):
_write(kb_root, GERMAN)
assert conventions.section_variables() == {
"section.relationships": "Beziehungen",
"section.see_also": "Siehe auch",
"section.footnotes": "Fußnoten",
}
def test_a_rewritten_file_is_not_answered_out_of_the_cache(kb_root):
_write(kb_root, GERMAN)
assert sections.FOOTNOTES == "Fußnoten"
_write(kb_root, FRENCH)
assert sections.FOOTNOTES == "Notes"
def test_an_incomplete_sections_block_is_reported(kb_root):
_write(kb_root, "---\nlanguage: de\nsections:\n relationships: Beziehungen\n---\n")
issues = conventions.declaration_issues()
assert any("sections.see_also" in issue for issue in issues)
assert any("sections.footnotes" in issue for issue in issues)
def test_an_unfilled_template_is_reported_like_a_missing_one(kb_root):
_write(kb_root, GERMAN.replace("language: de", f"# {config.TEMPLATE_SENTINEL}\nlanguage: de"))
assert any(config.TEMPLATE_SENTINEL in issue for issue in conventions.declaration_issues())
def test_an_absent_file_is_not_a_declaration_issue(kb_root):
"""`doctor` FAILs on absence; `docs verify` must not, or a fresh export
would be unverifiable before the setup step that writes the file."""
assert conventions.declaration_issues() == []
def test_a_collection_must_declare_its_profile_and_stack_dependence(kb_root):
_collection(kb_root, "sources", profile="sources", required=True)
(kb_root / "notes").mkdir()
(kb_root / "notes" / kb_collections.CONTRACT_NAME).write_text("# notes\n", encoding="utf-8")
issues = kb_collections.declaration_issues(kb_root)
assert any("kb/notes/COLLECTION.md has no frontmatter" in issue for issue in issues)
def test_required_by_stack_is_checked_against_the_stack_not_taken_on_trust(kb_root):
"""The one field an instance may not choose. A collection claiming the stack
depends on it would make a rename look unsafe when it is not - and, worse,
`sources` claiming otherwise would make one look safe when it is not."""
_collection(kb_root, "sources", required=False)
_collection(kb_root, "entities", required=True)
issues = kb_collections.declaration_issues(kb_root)
assert any("kb/sources/COLLECTION.md" in issue and "must be true" in issue for issue in issues)
assert any("kb/entities/COLLECTION.md" in issue and "must be false" in issue for issue in issues)
def test_a_missing_stack_required_collection_is_reported(kb_root):
_collection(kb_root, "entities")
assert any("kb/sources/ is missing" in issue for issue in kb_collections.declaration_issues(kb_root))
def test_a_correct_declaration_reports_nothing(kb_root):
_collection(kb_root, "sources", profile="sources", required=True)
_collection(kb_root, "entities", profile="entities")
assert kb_collections.declaration_issues(kb_root) == []
+31 -3
View File
@@ -113,6 +113,14 @@ def repo(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
(kb / "entities" / "COLLECTION.md").write_text("# entities collection\n", encoding="utf-8") (kb / "entities" / "COLLECTION.md").write_text("# entities collection\n", encoding="utf-8")
(kb / "entities" / "aurora.md").write_text("---\ntype: types/entity.md\n---\n", encoding="utf-8") (kb / "entities" / "aurora.md").write_text("---\ntype: types/entity.md\n---\n", encoding="utf-8")
(kb / "CONTRACT.md").write_text("# kb contract\n", encoding="utf-8") (kb / "CONTRACT.md").write_text("# kb contract\n", encoding="utf-8")
(kb / "CONVENTIONS.md").write_text(
"---\nlanguage: de\nsections:\n relationships: Beziehungen\n"
" see_also: Siehe auch\n footnotes: Fußnoten\n---\n\n# this instance\n",
encoding="utf-8",
)
(kb / "CONVENTIONS.md.template").write_text(
f"<!-- {config.TEMPLATE_SENTINEL} -->\n# conventions template\n", encoding="utf-8"
)
for relative in ("raw/CONTRACT.md", "reports/CONTRACT.md", "work/CONTRACT.md"): for relative in ("raw/CONTRACT.md", "reports/CONTRACT.md", "work/CONTRACT.md"):
path = root / relative path = root / relative
@@ -262,6 +270,10 @@ def test_find_leaks_is_silent_on_a_clean_plan(repo):
"instructions/dev/commonplace-kb.md", "instructions/dev/commonplace-kb.md",
"kb/entities/aurora.md", "kb/entities/aurora.md",
"raw/notes/personal-note.md", "raw/notes/personal-note.md",
# Both bind every page and both are the instance's to write, so the
# filled name must never cross - only the `.template` beside it does.
"kb/CONVENTIONS.md",
"kb/entities/COLLECTION.md",
], ],
) )
def test_find_leaks_catches_one_instance_own_data(repo, relative): def test_find_leaks_catches_one_instance_own_data(repo, relative):
@@ -283,12 +295,28 @@ def test_export_refuses_a_plan_that_leaks(repo, tmp_path, monkeypatch):
assert not target.exists() assert not target.exists()
def test_plan_copies_collection_contracts_not_pages(repo): def test_plan_ships_collection_contracts_as_templates_not_pages(repo):
"""The stack's own contract crosses verbatim; the instance-owned ones cross
under the template name and are adopted by a rename. Shipping
`kb/entities/COLLECTION.md` would hand a new instance this one's authoring
conventions as though the stack had decided them."""
plan = dist_cmd.build_plan() plan = dist_cmd.build_plan()
assert "kb/entities/COLLECTION.md" in plan assert "kb/entities/COLLECTION.md.template" in plan
assert "kb/entities/COLLECTION.md" not in plan
assert "kb/CONTRACT.md" in plan assert "kb/CONTRACT.md" in plan
assert not any(relative.endswith("aurora.md") for relative in plan) assert not any(relative.endswith("aurora.md") for relative in plan)
# The shipped template is the contract's own text - one source of truth in
# the origin repo, renamed across the boundary. A second file kept beside
# each contract would be a near-identical copy, maintained by hand.
assert plan["kb/entities/COLLECTION.md.template"].content == "# entities collection\n"
def test_plan_ships_the_conventions_template_and_not_the_filled_file(repo):
plan = dist_cmd.build_plan()
assert "kb/CONVENTIONS.md.template" in plan
assert "kb/CONVENTIONS.md" not in plan
def test_plan_creates_empty_raw_subdirs_not_real_content(repo): def test_plan_creates_empty_raw_subdirs_not_real_content(repo):
plan = dist_cmd.build_plan() plan = dist_cmd.build_plan()
@@ -397,7 +425,7 @@ def test_export_into_a_fresh_directory_works(repo, tmp_path):
target = tmp_path / "dist" target = tmp_path / "dist"
dist_cmd.run_export(target, dry_run=False) dist_cmd.run_export(target, dry_run=False)
assert (target / "AGENTS.md").is_file() assert (target / "AGENTS.md").is_file()
assert (target / "kb" / "entities" / "COLLECTION.md").is_file() assert (target / "kb" / "entities" / "COLLECTION.md.template").is_file()
assert (target / "raw" / "notes" / ".gitkeep").is_file() assert (target / "raw" / "notes" / ".gitkeep").is_file()
+37
View File
@@ -237,6 +237,43 @@ def test_a_migration_document_satisfies_the_check(tmp_path, monkeypatch):
assert docs_verify.check_migration_for_boundary() == [] assert docs_verify.check_migration_for_boundary() == []
def test_a_breaking_release_without_a_breaking_note_is_reported(tmp_path, monkeypatch):
"""A crossing that migrates nothing still leaves hand-work behind, so the
migration check passing is not evidence that anyone was told."""
from chemenu import version as version_mod
_boundary_tree(
tmp_path, monkeypatch, "2.0.0", "1.4.0",
marker=f"{version_mod.MIGRATION_NONE_MARKER} - nothing to change.\n\n",
)
assert docs_verify.check_migration_for_boundary() == []
issues = docs_verify.check_breaking_change_for_boundary()
assert any("2.0.0" in issue and "drop-in" in issue for issue in issues)
def test_a_compatible_release_needs_no_breaking_note(tmp_path, monkeypatch):
_boundary_tree(tmp_path, monkeypatch, "1.5.0", "1.4.0")
assert docs_verify.check_breaking_change_for_boundary() == []
def test_a_breaking_change_marker_satisfies_the_check(tmp_path, monkeypatch):
from chemenu import version as version_mod
_boundary_tree(
tmp_path, monkeypatch, "2.0.0", "1.4.0",
marker=f"{version_mod.BREAKING_CHANGE_MARKER} the feed moved.\n\n",
)
assert docs_verify.check_breaking_change_for_boundary() == []
def test_verify_raises_when_a_boundary_has_no_breaking_note(monkeypatch):
monkeypatch.setattr(
docs_verify, "check_breaking_change_for_boundary", lambda: ["unannounced"]
)
with pytest.raises(typer.Exit):
docs_verify.verify()
def test_verify_raises_when_a_boundary_has_no_migration(monkeypatch): def test_verify_raises_when_a_boundary_has_no_migration(monkeypatch):
monkeypatch.setattr(docs_verify, "check_migration_for_boundary", lambda: ["unbridged"]) monkeypatch.setattr(docs_verify, "check_migration_for_boundary", lambda: ["unbridged"])
with pytest.raises(typer.Exit): with pytest.raises(typer.Exit):
+33 -1
View File
@@ -8,7 +8,7 @@ from pathlib import Path
import pytest import pytest
from chemenu import config from chemenu import config, conventions
from chemenu.commands import doctor, instructions_cmd from chemenu.commands import doctor, instructions_cmd
@@ -30,6 +30,12 @@ def instance(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
(kb / "log.md").write_text("# Log\n", encoding="utf-8") (kb / "log.md").write_text("# Log\n", encoding="utf-8")
(kb / "provenance.md").write_text("# Provenance\n", encoding="utf-8") (kb / "provenance.md").write_text("# Provenance\n", encoding="utf-8")
(kb / "CONTRACT.md").write_text("# kb contract\n", encoding="utf-8") (kb / "CONTRACT.md").write_text("# kb contract\n", encoding="utf-8")
(kb / "CONVENTIONS.md").write_text(
"---\nlanguage: en\nprofile: none\nsections:\n"
" relationships: Relationships\n see_also: See Also\n footnotes: Footnotes\n"
"---\n\n# conventions\n",
encoding="utf-8",
)
(root / "VERSION").write_text("0.1.0\n", encoding="utf-8") (root / "VERSION").write_text("0.1.0\n", encoding="utf-8")
(root / "USER.md").write_text("# USER.md - Fixture\n", encoding="utf-8") (root / "USER.md").write_text("# USER.md - Fixture\n", encoding="utf-8")
(root / "SOUL.md").write_text("# SOUL.md - Fixture\n", encoding="utf-8") (root / "SOUL.md").write_text("# SOUL.md - Fixture\n", encoding="utf-8")
@@ -194,6 +200,32 @@ def test_a_renamed_but_unfilled_template_fails(instance):
assert "template" in next(c.detail for c in checks if c.name == "personalization") assert "template" in next(c.detail for c in checks if c.name == "personalization")
def test_conventions_are_ok_when_declared(instance):
checks = doctor.run_doctor()
assert _status(checks, "conventions") == "OK"
assert "Relationships" in next(c.detail for c in checks if c.name == "conventions")
def test_missing_conventions_fail(instance):
"""Unlike `ENVIRONMENT.md`, this one is not optional: `xref add` and
`cite add` write headings out of it, so an instance without it is being
answered by whatever the stack hardcoded before the file existed."""
(config.KB_DIR / conventions.CONVENTIONS_FILENAME).unlink()
checks = doctor.run_doctor()
assert _status(checks, "conventions") == "FAIL"
assert "missing" in next(c.detail for c in checks if c.name == "conventions")
def test_conventions_with_an_incomplete_sections_block_fail(instance):
"""Present and deciding nothing - the same failure mode the personalization
sentinel check exists for, one directory down."""
(config.KB_DIR / conventions.CONVENTIONS_FILENAME).write_text(
"---\nlanguage: en\nsections:\n relationships: Relationships\n---\n", encoding="utf-8"
)
conventions.reset_cache()
assert _status(doctor.run_doctor(), "conventions") == "FAIL"
def test_environment_is_ok_when_absent(instance): def test_environment_is_ok_when_absent(instance):
"""The file is optional, so absence is a healthy end state - a FAIL here """The file is optional, so absence is a healthy end state - a FAIL here
would make it mandatory through the back door.""" would make it mandatory through the back door."""
+25
View File
@@ -75,6 +75,31 @@ def test_new_entity_creates_page_with_expected_frontmatter(monkeypatch, kb_dir):
assert "# gateway.example.net" in body assert "# gateway.example.net" in body
def test_scaffolded_body_carries_the_headings_this_instance_declared(monkeypatch, kb_dir):
"""The type-spec writes `## {section.relationships}`, not a heading text, so
an instance in another language scaffolds its own headings without editing
anything under `types/`. This is that path end to end."""
from chemenu import conventions
(kb_dir / conventions.CONVENTIONS_FILENAME).write_text(
"---\nlanguage: fr\nprofile: none\nsections:\n relationships: Relations\n"
" see_also: Voir aussi\n footnotes: Notes\n---\n\n# conventions\n",
encoding="utf-8",
)
conventions.reset_cache()
try:
result = _invoke_new(monkeypatch, kb_dir, [
"new", "entity", "--name", "passerelle", "--set", "entity_type=system",
])
assert result.exit_code == 0, result.output
_fm, body = read_page(kb_dir / "entities/systems/passerelle.md")
assert "## Relations" in body
assert "## Voir aussi" in body
assert "{section." not in body
finally:
conventions.reset_cache()
def test_new_entity_applies_schema_declared_defaults(monkeypatch, kb_dir): def test_new_entity_applies_schema_declared_defaults(monkeypatch, kb_dir):
"""provenance and confidence are no longer Typer flag defaults - they """provenance and confidence are no longer Typer flag defaults - they
come from the schema's own `default:`, so omitting them still yields a come from the schema's own `default:`, so omitting them still yields a
+5 -4
View File
@@ -1,6 +1,6 @@
from typer.testing import CliRunner from typer.testing import CliRunner
from chemenu import sections from chemenu import conventions
from chemenu.cli import app from chemenu.cli import app
runner = CliRunner() runner = CliRunner()
@@ -45,9 +45,10 @@ def test_types_describe_entity_reports_schema_and_body():
] ]
assert fields_by_name["tags"]["required"] is False assert fields_by_name["tags"]["required"] is False
# The body must carry the page skeleton an authoring LLM works from. Anchored on the # The body must carry the page skeleton an authoring LLM works from. Anchored on the
# tool-owned section vocabulary rather than a literal, so that translating the spec - or # template *variable* rather than on any heading text: the spec no longer names the
# the section names themselves - does not turn this into a tripwire. # tool-owned sections at all - `kb/CONVENTIONS.md` does, and `new` substitutes it - so a
assert f"## {sections.RELATIONSHIPS}" in data["body"] # literal here would assert the very coupling that was removed.
assert f"## {{section.{conventions.RELATIONSHIPS}}}" in data["body"]
def test_types_describe_unknown_name_fails_cleanly(): def test_types_describe_unknown_name_fails_cleanly():
+50 -8
View File
@@ -183,7 +183,7 @@ def test_insert_changes_entry_lands_above_the_newest_entry():
def test_bump_writes_both_the_version_and_the_changelog_heading(tree): def test_bump_writes_both_the_version_and_the_changelog_heading(tree):
version_cmd.bump_command( version_cmd.bump_command(
major=False, minor=True, patch=False, title="Something happened", major=False, minor=True, patch=False, title="Something happened",
no_migration=None, dry_run=False, breaking=None, no_migration=None, dry_run=False,
) )
assert (tree / "VERSION").read_text(encoding="utf-8").strip() == "1.1.0" assert (tree / "VERSION").read_text(encoding="utf-8").strip() == "1.1.0"
changes = (tree / "CHANGES.md").read_text(encoding="utf-8") changes = (tree / "CHANGES.md").read_text(encoding="utf-8")
@@ -194,7 +194,7 @@ def test_bump_writes_both_the_version_and_the_changelog_heading(tree):
def test_bump_dry_run_writes_nothing(tree): def test_bump_dry_run_writes_nothing(tree):
version_cmd.bump_command( version_cmd.bump_command(
major=False, minor=False, patch=True, title="Nope", no_migration=None, dry_run=True major=False, minor=False, patch=True, title="Nope", breaking=None, no_migration=None, dry_run=True
) )
assert (tree / "VERSION").read_text(encoding="utf-8").strip() == "1.0.0" assert (tree / "VERSION").read_text(encoding="utf-8").strip() == "1.0.0"
assert "1.0.1" not in (tree / "CHANGES.md").read_text(encoding="utf-8") assert "1.0.1" not in (tree / "CHANGES.md").read_text(encoding="utf-8")
@@ -207,7 +207,7 @@ def test_bump_demands_exactly_one_part(tree, flags):
major, minor, patch = flags major, minor, patch = flags
with pytest.raises(typer.Exit): with pytest.raises(typer.Exit):
version_cmd.bump_command( version_cmd.bump_command(
major=major, minor=minor, patch=patch, title="x", no_migration=None, dry_run=False major=major, minor=minor, patch=patch, title="x", breaking=None, no_migration=None, dry_run=False
) )
assert (tree / "VERSION").read_text(encoding="utf-8").strip() == "1.0.0" assert (tree / "VERSION").read_text(encoding="utf-8").strip() == "1.0.0"
@@ -215,7 +215,7 @@ def test_bump_demands_exactly_one_part(tree, flags):
def test_bump_refuses_an_empty_title(tree): def test_bump_refuses_an_empty_title(tree):
with pytest.raises(typer.Exit): with pytest.raises(typer.Exit):
version_cmd.bump_command( version_cmd.bump_command(
major=False, minor=False, patch=True, title=" ", no_migration=None, dry_run=False major=False, minor=False, patch=True, title=" ", breaking=None, no_migration=None, dry_run=False
) )
@@ -227,7 +227,7 @@ def test_bump_refuses_when_the_changelog_is_already_ahead(tree):
) )
with pytest.raises(typer.Exit): with pytest.raises(typer.Exit):
version_cmd.bump_command( version_cmd.bump_command(
major=False, minor=False, patch=True, title="x", no_migration=None, dry_run=False major=False, minor=False, patch=True, title="x", breaking=None, no_migration=None, dry_run=False
) )
assert (tree / "VERSION").read_text(encoding="utf-8").strip() == "1.0.0" assert (tree / "VERSION").read_text(encoding="utf-8").strip() == "1.0.0"
@@ -241,7 +241,7 @@ def test_a_boundary_crossing_bump_without_a_migration_is_refused(tree):
with pytest.raises(typer.Exit): with pytest.raises(typer.Exit):
version_cmd.bump_command( version_cmd.bump_command(
major=True, minor=False, patch=False, title="Breaking", major=True, minor=False, patch=False, title="Breaking",
no_migration=None, dry_run=False, breaking="the feed moved", no_migration=None, dry_run=False,
) )
assert (tree / "VERSION").read_text(encoding="utf-8").strip() == "1.0.0" assert (tree / "VERSION").read_text(encoding="utf-8").strip() == "1.0.0"
@@ -255,7 +255,8 @@ def test_a_boundary_crossing_bump_passes_with_a_migration_document(tree):
encoding="utf-8", encoding="utf-8",
) )
version_cmd.bump_command( version_cmd.bump_command(
major=True, minor=False, patch=False, title="Breaking", no_migration=None, dry_run=False major=True, minor=False, patch=False, title="Breaking",
breaking="every page is retyped", no_migration=None, dry_run=False,
) )
assert (tree / "VERSION").read_text(encoding="utf-8").strip() == "2.0.0" assert (tree / "VERSION").read_text(encoding="utf-8").strip() == "2.0.0"
@@ -263,6 +264,7 @@ def test_a_boundary_crossing_bump_passes_with_a_migration_document(tree):
def test_no_migration_records_the_reason_in_the_changelog(tree): def test_no_migration_records_the_reason_in_the_changelog(tree):
version_cmd.bump_command( version_cmd.bump_command(
major=True, minor=False, patch=False, title="Breaking", major=True, minor=False, patch=False, title="Breaking",
breaking="the release feed moved",
no_migration="no distributed instance exists yet", dry_run=False, no_migration="no distributed instance exists yet", dry_run=False,
) )
changes = (tree / "CHANGES.md").read_text(encoding="utf-8") changes = (tree / "CHANGES.md").read_text(encoding="utf-8")
@@ -275,10 +277,50 @@ def test_no_migration_is_refused_on_a_compatible_bump(tree):
with pytest.raises(typer.Exit): with pytest.raises(typer.Exit):
version_cmd.bump_command( version_cmd.bump_command(
major=False, minor=False, patch=True, title="Fix", major=False, minor=False, patch=True, title="Fix",
no_migration="not needed", dry_run=False, breaking=None, no_migration="not needed", dry_run=False,
) )
# --- version bump: the breaking-change note --------------------------------
def test_a_boundary_crossing_bump_without_breaking_is_refused(tree):
"""The corpus question and the drop-in question are independent: a bump
can migrate nothing and still leave every instance with hand-work."""
with pytest.raises(typer.Exit):
version_cmd.bump_command(
major=True, minor=False, patch=False, title="Renamed the feed",
breaking=None, no_migration="kb/ keeps its shape", dry_run=False,
)
assert (tree / "VERSION").read_text(encoding="utf-8").strip() == "1.0.0"
def test_breaking_records_what_stops_working_in_the_changelog(tree):
version_cmd.bump_command(
major=True, minor=False, patch=False, title="Renamed the feed",
breaking="update_url points at a repo path that no longer exists",
no_migration="kb/ keeps its shape", dry_run=False,
)
changes = (tree / "CHANGES.md").read_text(encoding="utf-8")
assert version_mod.BREAKING_CHANGE_MARKER in changes
assert "update_url points at a repo path that no longer exists" in changes
# The break comes before the migration note: it is what an operator acts on.
assert changes.index(version_mod.BREAKING_CHANGE_MARKER) < changes.index(
version_mod.MIGRATION_NONE_MARKER
)
def test_breaking_is_refused_on_a_compatible_bump(tree):
"""A compatible bump that claims a break is describing itself wrongly -
one of the two is a mistake, and the command will not guess which."""
with pytest.raises(typer.Exit):
version_cmd.bump_command(
major=False, minor=True, patch=False, title="New command",
breaking="nothing, really", no_migration=None, dry_run=False,
)
assert (tree / "VERSION").read_text(encoding="utf-8").strip() == "1.0.0"
# --- version notes --------------------------------------------------------- # --- version notes ---------------------------------------------------------
+32 -8
View File
@@ -10,11 +10,23 @@ ingest into a release.
**Compatibility is read off the leftmost non-zero component**, the rule Cargo's **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 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 `1.0.0` on the same rule reads as the familiar "MAJOR breaks". Stating it that
it that way is what lets the 0.x era carry the migration signal at all - under way is what lets the 0.x era carry the signal at all - under a rule keyed to
plain "MAJOR breaks" semantics every 0.x release would be indistinguishable the MAJOR component alone, every 0.x release would be indistinguishable from
from every other, which is exactly the signal update detection needs. Nothing every other, which is exactly the signal update detection needs. Nothing about
about the mechanism changes at 1.0.0. the mechanism changes at 1.0.0.
What that component answers is **whether the new version is a drop-in
replacement**: whether an instance can copy the new machinery over itself with
no hand-work and still put the old version back afterwards. Whether *content*
must be migrated is a **second, independent question**. It is one way to fail
the first - but a renamed release feed, artefact, import name, flag or envvar
fails it too, with `kb/` untouched, which is why `--no-migration` exists at all:
boundary-crossing bumps that migrate nothing are a real case, not an escape
hatch. Hence two markers below rather than one - `BREAKING_CHANGE_MARKER`
records the break, `MIGRATION_NONE_MARKER` records the absence of the
migration. Which part a change earns stays a judgment call made before the
bump; this module only enforces that a crossing says what it costs.
Paths are resolved through `config.ROOT` at call time rather than at import, Paths are resolved through `config.ROOT` at call time rather than at import,
because the tests (and `dist export`'s own fixtures) relocate the root. because the tests (and `dist export`'s own fixtures) relocate the root.
@@ -64,6 +76,11 @@ _SEMVER_RE = re.compile(r"^\s*v?(\d+)\.(\d+)\.(\d+)\s*$")
# boundary that needs no content migration. `docs verify` accepts it in place # boundary that needs no content migration. `docs verify` accepts it in place
# of a migration document, so the exact string is a contract between the two. # of a migration document, so the exact string is a contract between the two.
MIGRATION_NONE_MARKER = "**Migration:** none required" MIGRATION_NONE_MARKER = "**Migration:** none required"
# Written into every CHANGES.md entry whose version crosses a compatibility
# boundary, migration or not: the swap is not drop-in, and the operator of an
# existing instance has to be told what stops working. `docs verify` checks the
# newest crossing carries it, so this string too is a contract between the two.
BREAKING_CHANGE_MARKER = "**Breaking Change:**"
# A changelog entry that names a version. Entries predating versioning start # A changelog entry that names a version. Entries predating versioning start
# with a date instead and are deliberately not matched - they are history, not # with a date instead and are deliberately not matched - they are history, not
# a claim about which version the tree is. # a claim about which version the tree is.
@@ -332,15 +349,22 @@ def insert_changes_entry(
title: str, title: str,
author: str, author: str,
no_migration_reason: Optional[str] = None, no_migration_reason: Optional[str] = None,
breaking_reason: Optional[str] = None,
) -> str: ) -> str:
"""Add a heading for `version` above the newest existing entry. """Add a heading for `version` above the newest existing entry.
Only the skeleton: heading, date, author, and - when a compatibility Only the skeleton: heading, date, author, and - when a compatibility
boundary is crossed without a migration - the line that says so. The boundary is crossed - the line saying what breaks, plus the line saying no
entry's actual content is written afterwards by whoever made the change, content has to change where that applies. The entry's actual content is
which is also why `bump` refuses to invent a title. written afterwards by whoever made the change, which is also why `bump`
refuses to invent a title.
The break comes first: it is what an operator reading the release notes has
to act on, and the migration line only qualifies it.
""" """
lines = [f"## {version} - {date} - {title}", "", f"**Author:** {author}", ""] lines = [f"## {version} - {date} - {title}", "", f"**Author:** {author}", ""]
if breaking_reason:
lines += [f"{BREAKING_CHANGE_MARKER} {breaking_reason}", ""]
if no_migration_reason: if no_migration_reason:
lines += [f"{MIGRATION_NONE_MARKER} - {no_migration_reason}", ""] lines += [f"{MIGRATION_NONE_MARKER} - {no_migration_reason}", ""]
entry = "\n".join(lines) + "\n---\n\n" entry = "\n".join(lines) + "\n---\n\n"
+3 -3
View File
@@ -36,7 +36,7 @@ page_ref_fields: [entities]
## Autorenanweisungen ## Autorenanweisungen
- Ein Titel, der den Vergleich benennt (z. B. "Go vs Rust", "Kubernetes vs Docker Swarm"); er folgt den etablierten Namen der verglichenen Gegenstände, nicht der KB-Sprache (`kb/CONTRACT.md`, Abschnitte "Naming" und "Language") - Ein Titel, der den Vergleich benennt (z. B. "Go vs Rust", "Kubernetes vs Docker Swarm"); er folgt den etablierten Namen der verglichenen Gegenstände, nicht der KB-Sprache (`kb/CONTRACT.md` § "Titles are identifiers", `kb/CONVENTIONS.md` §§ "Naming" und "Language")
- Klar darlegen, was verglichen wird und warum - Klar darlegen, was verglichen wird und warum
- Eine Vergleichstabelle mit den Kriterien als Zeilen verwenden - Eine Vergleichstabelle mit den Kriterien als Zeilen verwenden
- Eine Analyse, die die Tabelle auswertet statt sie zu wiederholen - Eine Analyse, die die Tabelle auswertet statt sie zu wiederholen
@@ -68,8 +68,8 @@ TODO: Falls möglich - was wann und für wen zu verwenden ist. Unter welchen Ums
`# Comparison:` bleibt als Präfix stehen - anders als bei `source` ist es kein `title_prefix`, `# Comparison:` bleibt als Präfix stehen - anders als bei `source` ist es kein `title_prefix`,
sondern reine Template-Konvention, und der Seitentitel selbst (`Go vs Rust`) trägt es nicht. sondern reine Template-Konvention, und der Seitentitel selbst (`Go vs Rust`) trägt es nicht.
Fügt `wikitool xref` eine Beziehung hinzu, entsteht `## Siehe auch`; der Name steht in Fügt `wikitool xref` eine Beziehung hinzu, entsteht der toolgeführte Querverweis-Abschnitt; wie
`tools/chemenu/sections.py`. er heißt, entscheidet die Instanz in `kb/CONVENTIONS.md` (`sections:`).
--- ---
+4 -3
View File
@@ -45,7 +45,7 @@ page_ref_fields: [related, sources]
## Autorenanweisungen ## Autorenanweisungen
- Der Titel ist der kanonische Name des Concepts und folgt der etablierten Fachbezeichnung, nicht der KB-Sprache (`kb/CONTRACT.md`, Abschnitte "Naming" und "Language") - Der Titel ist der kanonische Name des Concepts und folgt der etablierten Fachbezeichnung, nicht der KB-Sprache (`kb/CONTRACT.md` § "Titles are identifiers", `kb/CONVENTIONS.md` §§ "Naming" und "Language")
- Mit einer klaren Definition beginnen: was das Concept ist - Mit einer klaren Definition beginnen: was das Concept ist
- Beispiele geben, wo sie das Verständnis tragen - Beispiele geben, wo sie das Verständnis tragen
- Auf Entities verlinken, die das Concept umsetzen oder verwenden - Auf Entities verlinken, die das Concept umsetzen oder verwenden
@@ -90,8 +90,9 @@ TODO: Anti-Muster, Warnungen oder Situationen, in denen es fehl am Platz ist
``` ```
Der Wert hinter `**Typ:**` bleibt der englische Enum-Wert - danach filtert `search --field`. Der Wert hinter `**Typ:**` bleibt der englische Enum-Wert - danach filtert `search --field`.
Fügt `wikitool xref` eine Beziehung hinzu, entstehen zusätzlich `## Beziehungen` und Fügt `wikitool xref` eine Beziehung hinzu, entstehen zusätzlich die beiden toolgeführten
`## Siehe auch`; deren Namen stehen in `tools/chemenu/sections.py`. Abschnitte für Beziehungen und Querverweise; wie sie heißen, entscheidet die Instanz in
`kb/CONVENTIONS.md` (`sections:`).
--- ---
+8 -6
View File
@@ -50,7 +50,7 @@ layout:
## Autorenanweisungen ## Autorenanweisungen
- Der Titel ist der kanonische Name der Entity und folgt der etablierten Bezeichnung des Gegenstands, nicht der KB-Sprache (`kb/CONTRACT.md`, Abschnitte "Naming" und "Language") - Der Titel ist der kanonische Name der Entity und folgt der etablierten Bezeichnung des Gegenstands, nicht der KB-Sprache (`kb/CONTRACT.md` § "Titles are identifiers", `kb/CONVENTIONS.md` §§ "Naming" und "Language")
- Die Hauptbeschreibung steht weit oben - Die Hauptbeschreibung steht weit oben
- Auf verwandte Entities und Concepts verlinken, wo Beziehungen bestehen - Auf verwandte Entities und Concepts verlinken, wo Beziehungen bestehen
- Bei `provenance: sourced` oder `mixed` harte Fakten inline mit einer `[^cite-id]`-Fußnote belegen - - Bei `provenance: sourced` oder `mixed` harte Fakten inline mit einer `[^cite-id]`-Fußnote belegen -
@@ -77,7 +77,7 @@ TODO: 1-2 Absätze dazu, was diese Entity ist und wozu sie dient.
- **Verantwortlich:** TODO (falls zutreffend) - **Verantwortlich:** TODO (falls zutreffend)
- **Repository:** TODO (falls zutreffend) - **Repository:** TODO (falls zutreffend)
## Beziehungen ## {section.relationships}
- **Hängt ab von:** TODO - **Hängt ab von:** TODO
- **Verwendet von:** TODO - **Verwendet von:** TODO
@@ -91,14 +91,16 @@ TODO: Ausführliche Informationen, nach sinnvollen Abschnitten gegliedert
- [{today}] - Page created via wikitool - [{today}] - Page created via wikitool
## Siehe auch ## {section.see_also}
- TODO: Verwandte Seiten - TODO: Verwandte Seiten
``` ```
`## Beziehungen` und `## Siehe auch` sind toolgeführt: `wikitool xref` schreibt in genau diese Die beiden `{section.…}`-Platzhalter sind toolgeführte Abschnitte: `wikitool xref` schreibt in
Abschnitte, benannt in `tools/chemenu/sections.py`. Der Wert hinter `**Typ:**` bleibt der genau sie hinein, und wie sie heißen, entscheidet die Instanz in `kb/CONVENTIONS.md`
englische Enum-Wert - danach filtert `search --field`. (`sections:`) - nicht dieser Type-Spec und nicht der Compiler. `wikitool new` setzt den
aktuellen Namen ein. Der Wert hinter `**Typ:**` bleibt der englische Enum-Wert - danach filtert
`search --field`.
--- ---
+3 -3
View File
@@ -48,7 +48,7 @@ page_ref_fields: [entities, concepts]
- `raw_files` listet jede Raw-Datei, die diese Quelle abdeckt (eine Source-Seite pro logischer Quelle, nicht pro Datei) - `raw_files` listet jede Raw-Datei, die diese Quelle abdeckt (eine Source-Seite pro logischer Quelle, nicht pro Datei)
- Bei externen Artikeln immer `source_url` auf die Ursprungs-URL setzen - Bei externen Artikeln immer `source_url` auf die Ursprungs-URL setzen
- `source_language` auf die Sprache des Rohmaterials setzen, nicht auf die der Seite - `source_language` auf die Sprache des Rohmaterials setzen, nicht auf die der Seite
- Die Seite wird in der KB-Sprache geschrieben, unabhängig von der Sprache der Quelle; wörtliche Passagen werden im Original zitiert (`kb/CONTRACT.md`, Abschnitt "Language") - Die Seite wird in der KB-Sprache geschrieben, unabhängig von der Sprache der Quelle; wörtliche Passagen werden im Original zitiert (`kb/CONVENTIONS.md` § "Language")
- Kernaussagen im Abschnitt Summary zusammenfassen - Kernaussagen im Abschnitt Summary zusammenfassen
- Handlungsbedarf in den Abschnitt Action Items - Handlungsbedarf in den Abschnitt Action Items
- Bewusst Weggelassenes in den Abschnitt Not Extracted - siehe unten - Bewusst Weggelassenes in den Abschnitt Not Extracted - siehe unten
@@ -108,8 +108,8 @@ TODO: 2-3 Absätze zu den Kernaussagen des Quellmaterials.
`# Source:` bleibt als Präfix stehen - es spiegelt den `title_prefix` und damit den Titel, unter `# Source:` bleibt als Präfix stehen - es spiegelt den `title_prefix` und damit den Titel, unter
dem die Seite verlinkt und zitiert wird. Der Wert hinter `**Typ:**` bleibt der englische dem die Seite verlinkt und zitiert wird. Der Wert hinter `**Typ:**` bleibt der englische
Enum-Wert. Fügt `wikitool cite` ein Zitat hinzu, entsteht am Seitenende `## Fußnoten`; der Name Enum-Wert. Fügt `wikitool cite` ein Zitat hinzu, entsteht am Seitenende der toolgeführte
steht in `tools/chemenu/sections.py`. Fußnoten-Block; wie er heißt, entscheidet die Instanz in `kb/CONVENTIONS.md` (`sections:`).
--- ---
+15 -8
View File
@@ -98,17 +98,24 @@ The `## Template` block is filled from the page's own frontmatter, plus `{name}`
`{entities|table_cells}`. `{field|literal text}` falls back to the literal when the field is `{entities|table_cells}`. `{field|literal text}` falls back to the literal when the field is
absent. absent.
Three further variables come from the instance rather than from the page:
`{section.relationships}`, `{section.see_also}` and `{section.footnotes}`, filled from
`kb/CONVENTIONS.md`'s `sections:` declaration. A template writes a tool-owned heading through
one of those and never as literal text - that is what lets an instance change the KB language
without editing anything under `types/`.
### Ownership boundary ### Ownership boundary
| Owned here | Owned by `kb/CONTRACT.md` and the collection contracts | | Owned here | Owned by `kb/CONTRACT.md` | Owned by `kb/CONVENTIONS.md` and the collection contracts |
|------------|-----------------------------------------------------------| |------------|--------------------------|-----------------------------------------------------------|
| Frontmatter fields, enums, defaults, required-ness | Quality goal and tone | | Frontmatter fields, enums, defaults, required-ness | Provenance and citation mechanics | Quality goal and tone |
| Directory placement and title prefix | Naming conventions | | Directory placement and title prefix | The confidence machinery | Naming conventions and the confidence rubric |
| Body skeleton (template) | Linking policy and relationship vocabulary | | Body skeleton (template) | Linking mechanics and the orphan check | Relationship vocabulary |
| When to use / not use this type | Provenance and confidence practice | | When to use / not use this type | The prose/identifier rule | The KB language and its section-heading names |
If a rule would be identical for every type, it belongs in `kb/CONTRACT.md`, not in a If a rule would be identical for every type *and* every instance, it belongs in
type-spec. If it is identical for every page in one collection, it belongs in that `kb/CONTRACT.md`. If every instance would answer it differently, it belongs in
`kb/CONVENTIONS.md`. If it is identical for every page in one collection, it belongs in that
collection's `COLLECTION.md`. collection's `COLLECTION.md`.
### What does not belong here ### What does not belong here