feat: Autorenkonventionen nach Eigentum geschnitten - kb/CONVENTIONS.md, deklarierte Collections (3.0.0)
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
This commit is contained in:
@@ -221,6 +221,16 @@ jobs:
|
||||
for personal in USER SOUL; do
|
||||
grep -v 'wikitool:template-unfilled' "$personal.md.template" > "$personal.md"
|
||||
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
|
||||
tools/.venv/bin/pip install --quiet -r tools/requirements.txt
|
||||
tools/wikitool instructions sync
|
||||
|
||||
+8
-2
@@ -1,5 +1,11 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"kb_version": "1.0.0",
|
||||
"applied": []
|
||||
"kb_version": "3.0.0",
|
||||
"applied": [
|
||||
{
|
||||
"migration": "3.0.0-authoring-conventions",
|
||||
"at": "2026-09-02",
|
||||
"pages": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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 |
|
||||
| `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 |
|
||||
| `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>/SKILL.md` | Agents | By the harness, once published |
|
||||
| `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
|
||||
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.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 |
|
||||
| `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` |
|
||||
| `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 |
|
||||
| `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.
|
||||
[kb/CONTRACT.md](kb/CONTRACT.md) routes between `kb/entities/`, `kb/concepts/`, `kb/sources/`
|
||||
and `kb/comparisons/`, and holds the rules they share.
|
||||
[kb/CONTRACT.md](kb/CONTRACT.md) routes between this instance's collections and holds the rules
|
||||
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>/`:
|
||||
|
||||
|
||||
+79
@@ -20,6 +20,85 @@ 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
|
||||
|
||||
+19
-12
@@ -68,11 +68,14 @@ Zwei Schritte, von denen nur der erste rein menschlich ist:
|
||||
Wiki-Seite (`$WIKI_AUTHOR` überschreibt dies bei Bedarf).
|
||||
- **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`.
|
||||
- **KB-Sprache** - die exportierte Distribution bringt **Deutsch** mit: die Regel in
|
||||
`kb/CONTRACT.md`, das Vokabular in `instructions/german-terminology.md` und deutsche
|
||||
Abschnittsnamen in den Seitenvorlagen. Das ist eine Entscheidung dieser Ursprungsinstanz,
|
||||
keine Eigenschaft des Musters. Willst du eine andere Sprache, sag es **vor dem ersten
|
||||
Ingest** - danach ist es eine Migration jeder bereits angelegten Seite.
|
||||
- **Autorenkonventionen** - Sprache, Abschnittsnamen, Namensformen, Ton, Beziehungslabels
|
||||
und Confidence-Rubrik stehen in `kb/CONVENTIONS.md`, dazu je Collection die Regeln in
|
||||
`kb/<name>/COLLECTION.md`. Die Distribution bringt davon nur die `.template`-Dateien mit:
|
||||
das sind Entscheidungen *dieser* Instanz, keine Eigenschaft des Musters, und nichts davon
|
||||
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
|
||||
(`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
|
||||
@@ -177,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.
|
||||
3. Die **Maschinerie** aus dem Tarball über die Instanz kopieren: `tools/`, `types/`,
|
||||
`instructions/`, `AGENTS.md`, `VERSION`, `.wikitool-release.json`. Nicht anfassen: `kb/`,
|
||||
`raw/`, `work/`, `.wikitool-kb.json` und `.git/` - das ist die Instanz selbst.
|
||||
4. Achtung bei lokal angepassten Contract-Dateien: wer z. B. die KB-Sprache umgestellt hat
|
||||
(Schritt 5 in `setup-instance.md`), hat `kb/CONTRACT.md` und die Templates unter `types/`
|
||||
verändert. Diese Änderungen vorher sichern und danach wieder einspielen. Welche Dateien das
|
||||
sind, verrät ein Vergleich gegen die sha256-Summen im `files`-Block der alten
|
||||
`.wikitool-release.json`.
|
||||
`instructions/`, `AGENTS.md`, `VERSION`, `.wikitool-release.json` - **und `kb/CONTRACT.md`**.
|
||||
Die letzte Datei liegt unter einem Content-Verzeichnis, ist aber Stack-Eigentum: sie hält,
|
||||
was `wikitool` erzwingt, und ist in jeder Instanz gleich. Nicht anfassen: alles andere unter
|
||||
`kb/` und `raw/`, `work/`, `.wikitool-kb.json` und `.git/` - das ist die Instanz selbst,
|
||||
`kb/CONVENTIONS.md` und die `kb/*/COLLECTION.md` eingeschlossen.
|
||||
4. Achtung bei lokal angepassten Stack-Dateien. Die Autorenkonventionen gehören **nicht** dazu:
|
||||
`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
|
||||
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
|
||||
|
||||
@@ -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
|
||||
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
|
||||
`## Beziehungen` sit in the same page without contradiction. The rule is
|
||||
[kb/CONTRACT.md § Language](kb/CONTRACT.md#language); the vocabulary behind it is
|
||||
`## Beziehungen` sit in the same page without contradiction. Which lines are identifiers 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).
|
||||
|
||||
This is a per-instance decision, not a property of the pattern. A new instance built with
|
||||
`dist export` starts empty and can pick any language by editing that one contract section before
|
||||
This is a per-instance decision, not a property of the pattern - which is why it lives in a file
|
||||
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.
|
||||
|
||||
## Getting started
|
||||
@@ -401,7 +404,7 @@ This wiki is tailored for IT work with:
|
||||
|
||||
- **Entity types** specific to software development and systems
|
||||
- **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
|
||||
- **Guidelines** for documenting technical decisions
|
||||
- **Cross-reference patterns** for code and architecture
|
||||
|
||||
@@ -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
|
||||
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
|
||||
vocabulary consulted on demand rather than a procedure, and every migration document (below).
|
||||
Three kinds of file use the Manual tier today: [german-terminology.md](german-terminology.md), a
|
||||
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/`
|
||||
|
||||
@@ -153,7 +155,8 @@ What lives where:
|
||||
|-------|------|
|
||||
| [AGENTS.md](../AGENTS.md) | Invariants and routing - what must always hold |
|
||||
| `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* |
|
||||
| [tools/CONTRACT.md](../tools/CONTRACT.md) | What each command does and how it fails |
|
||||
|
||||
|
||||
@@ -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
|
||||
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
|
||||
|
||||
Whenever you add or change a test under `tools/chemenu/tests/`.
|
||||
|
||||
@@ -7,9 +7,13 @@ manual: true
|
||||
|
||||
# German terminology for `kb/`
|
||||
|
||||
Reference vocabulary for [kb/CONTRACT.md](../kb/CONTRACT.md#language)'s rule that pages are
|
||||
written in German. The rule lives there; the word list lives here, because it is lookup material
|
||||
rather than a norm and would otherwise be loaded on every write.
|
||||
Reference vocabulary for [kb/CONVENTIONS.md](../kb/CONVENTIONS.md#language)'s rule that this
|
||||
instance's pages are written in German. The rule lives there; the word list lives here, because
|
||||
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
|
||||
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.
|
||||
|
||||
- **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
|
||||
is there.
|
||||
- Buzzwords and AI filler are banned by [kb/CONVENTIONS.md](../kb/CONVENTIONS.md#tone); the
|
||||
German list is there.
|
||||
- Dash as ` - `, not `—`.
|
||||
- German number formatting only in prose („10.000 Punkte"). Never inside code, version numbers or
|
||||
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
|
||||
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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
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
|
||||
|
||||
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
|
||||
BEFORE=$(git rev-parse HEAD)
|
||||
git fetch upstream
|
||||
|
||||
# --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
|
||||
|
||||
# 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
|
||||
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
|
||||
```
|
||||
|
||||
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
|
||||
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
|
||||
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`,
|
||||
`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.
|
||||
@@ -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.
|
||||
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 diff` after the merge shows something under `kb/` or `raw/`?** Stop. The scoping step
|
||||
did not take. Do not publish; find out which path came through and where from.
|
||||
- **`git diff` after the merge shows something under `kb/` or `raw/`?** Stop - unless it is one
|
||||
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
|
||||
step "Where stack development happens" says not to do. Take the upstream side and re-file the
|
||||
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
|
||||
|
||||
|
||||
@@ -60,24 +60,58 @@ bereit für den ersten `Ingest`.
|
||||
- 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).
|
||||
|
||||
5. **Entscheidungspunkt - KB-Sprache.** Frage den Nutzer, in welcher Sprache die Seiten unter
|
||||
`kb/` geschrieben werden sollen. Diese Instanz erbt aus dem Quell-Repo **Deutsch** - sowohl die
|
||||
Regel in [kb/CONTRACT.md](../kb/CONTRACT.md#language) als auch das Vokabular in
|
||||
[german-terminology.md](german-terminology.md) und die deutschen Abschnittsnamen in
|
||||
`tools/chemenu/sections.py`. Das ist eine Entscheidung der Ursprungsinstanz, keine
|
||||
Eigenschaft des Musters, und sie wird hier nicht stillschweigend weitergereicht.
|
||||
5. **Entscheidungspunkt - Autorenkonventionen.** Die Distribution bringt keine ausgefüllten
|
||||
Konventionen mit, sondern `kb/CONVENTIONS.md.template` und je Collection ein
|
||||
`kb/<name>/COLLECTION.md.template`. Beide **binden**, sobald sie übernommen sind, und beide
|
||||
gehören dieser Instanz - deshalb liefert der Stack nur die Vorlage. Die eine Entscheidung
|
||||
dahinter ist: **in welcher Sprache und in welchem Ton schreibt diese Instanz ihre Seiten?**
|
||||
|
||||
- **Deutsch bestätigt:** nichts zu tun.
|
||||
- **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.
|
||||
Ablauf:
|
||||
|
||||
Unverändert bleibt in jedem Fall die eigentliche Regel: **jede Zeile einer Seite ist Prosa
|
||||
oder Identifier, und nur Prosa wird übersetzt.** Titel, Wikilink-Ziele, Cite-IDs, Enum-Werte,
|
||||
Tags, Befehle und Pfade folgen keiner KB-Sprache.
|
||||
1. Die Collection-Contracts übernehmen - vier Kopien, keine Frage an den Nutzer, denn was
|
||||
dort steht ist unabhängig von der Sprache brauchbar:
|
||||
|
||||
```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
|
||||
`USER.md.template` und `SOUL.md.template` mit, aber keine ausgefüllten Fassungen: wer diese
|
||||
|
||||
@@ -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
|
||||
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
|
||||
original. The rule and what is exempt from it:
|
||||
[kb/CONTRACT.md](../../kb/CONTRACT.md#language).
|
||||
original. Which language that is: [kb/CONVENTIONS.md](../../kb/CONVENTIONS.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,
|
||||
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
|
||||
[kb/entities/COLLECTION.md](../../kb/entities/COLLECTION.md) and
|
||||
[kb/CONTRACT.md](../../kb/CONTRACT.md) first - the second is where tone, naming, provenance
|
||||
and citation are defined.
|
||||
[kb/CONTRACT.md](../../kb/CONTRACT.md) plus
|
||||
[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:
|
||||
|
||||
|
||||
@@ -13,10 +13,12 @@ integrating into an existing one.
|
||||
|
||||
**Before the first `wikitool` call:** [session-setup.md](../session-setup.md).
|
||||
|
||||
**Read before drafting:** [kb/CONTRACT.md](../../kb/CONTRACT.md) - naming, tone, linking,
|
||||
provenance and confidence - 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>`.
|
||||
**Read before drafting:** [kb/CONTRACT.md](../../kb/CONTRACT.md) - linking, provenance and the
|
||||
confidence machinery, all of which the tool enforces - and
|
||||
[kb/CONVENTIONS.md](../../kb/CONVENTIONS.md), which is where this instance's language, naming
|
||||
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
|
||||
|
||||
@@ -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.
|
||||
|
||||
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
|
||||
"Source - X"`, which also adds `X` to `sources:` - paste the `[^cite-id]` marker it prints.
|
||||
|
||||
|
||||
+74
-72
@@ -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
|
||||
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`
|
||||
declares that it inherits them and adds only what is local to its own subtree - read this file
|
||||
together with the target collection's contract before writing or editing a page.
|
||||
This file holds the rules that apply in **every** collection **and in every instance**. That
|
||||
second half is the cut: what is written here is enforced by `tools/wikitool` or follows from
|
||||
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
|
||||
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 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.
|
||||
|
||||
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
|
||||
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/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
|
||||
*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
|
||||
[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
|
||||
`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.
|
||||
- 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`.
|
||||
- The filename stem *is* the page title, and `[[wikilinks]]` must match it exactly.
|
||||
- Prefer readability over convention when the two conflict.
|
||||
**The filename stem *is* the page title, and `[[wikilinks]]` must match it exactly.** That is
|
||||
not a naming preference; it is the wiki's only way to address a page. `wikitool lint` reports an
|
||||
H1 that stops matching its title, `rename`/`rm` rewrite every reference to a stem, and a
|
||||
`[^cite-id]` resolves through one.
|
||||
|
||||
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.
|
||||
Which *form* those titles take - spaces or kebab-case, singular or plural, what prefixes a
|
||||
decision record - is the instance's, in
|
||||
[kb/CONVENTIONS.md § Naming](CONVENTIONS.md#naming).
|
||||
|
||||
## Every page should
|
||||
|
||||
@@ -83,31 +104,20 @@ people a full name or common handle.
|
||||
- [ ] Duplicate no existing page
|
||||
- [ ] 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").
|
||||
- No AI filler ("es sei angemerkt", "es ist wichtig zu betonen", "in der heutigen Zeit").
|
||||
- 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.
|
||||
The register those lines are written in - what counts as a buzzword, what filler is refused -
|
||||
is the instance's, in [kb/CONVENTIONS.md § Tone](CONVENTIONS.md#tone).
|
||||
|
||||
Good: "MQTT ist ein leichtgewichtiges Publish-Subscribe-Protokoll für Geräte mit knappen
|
||||
Ressourcen."
|
||||
## Language and identifiers
|
||||
|
||||
Bad: "MQTT ist ein bahnbrechendes, hochmodernes Protokoll, das die IoT-Kommunikation
|
||||
revolutioniert - und es sei angemerkt, dass es ein Publish-Subscribe-Muster verwendet."
|
||||
|
||||
## 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.
|
||||
*Which* language pages are written in is [kb/CONVENTIONS.md](CONVENTIONS.md)'s to say. What
|
||||
follows here is the part that is not a choice, because the tool resolves against it.
|
||||
|
||||
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 |
|
||||
|---|---|
|
||||
| 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 |
|
||||
| `tags:` | Search keys, not prose |
|
||||
| Commands, paths, config keys, hostnames, code | They are what they are |
|
||||
| Quotations | Quoted verbatim in the source's own language |
|
||||
|
||||
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.
|
||||
Which foreign technical terms stay untranslated inside that prose is a judgment call the
|
||||
instance records - see [kb/CONVENTIONS.md § Language](CONVENTIONS.md#language).
|
||||
|
||||
**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
|
||||
@@ -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`
|
||||
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` -
|
||||
and `tools/chemenu/sections.py` is the single place naming them.
|
||||
follow the KB language like everything else, so **the instance names them**, in
|
||||
`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
|
||||
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
|
||||
retitling it. 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.
|
||||
retitling it. The recognized set is the canonical name, any `section_aliases:` the instance
|
||||
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
|
||||
|
||||
@@ -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
|
||||
hand-editing the `related:` array or the Relationships/See Also bullets.
|
||||
|
||||
Use a typed relationship label 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.
|
||||
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
|
||||
it - so which vocabulary this instance uses is
|
||||
[kb/CONVENTIONS.md § Relationship labels](CONVENTIONS.md#relationship-labels)'s to list.
|
||||
|
||||
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.
|
||||
@@ -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"
|
||||
[--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
|
||||
`## 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
|
||||
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]]`
|
||||
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
|
||||
means a marker appended to a line *inside* a fence cites nothing - put it on a
|
||||
`Quelle: [^cite-id]` line under the block, where it renders as a footnote instead of
|
||||
travelling with the command when someone copies it.
|
||||
means a marker appended to a line *inside* a fence cites nothing - put it on a source line
|
||||
under the block (`<source-word>: [^cite-id]`, in the KB language), where it renders as a
|
||||
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;
|
||||
`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.
|
||||
@@ -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
|
||||
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>`.
|
||||
|
||||
In prose, hedge according to the score: below 0.6 write "möglicherweise"/"kann"; below 0.4
|
||||
write "unsicher"/"unbestätigt".
|
||||
What the number *means* - the base score, what raises it and by how much, and how to hedge in
|
||||
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
|
||||
|
||||
- Raw source material - it stays immutable under `raw/`.
|
||||
- Type definitions, frontmatter contracts, or templates - those live in `types/`.
|
||||
- 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
|
||||
`COLLECTION.md`.
|
||||
- Hand-edited generated files - see [Generated files](#generated-files).
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
profile: comparisons
|
||||
required_by_stack: false
|
||||
---
|
||||
|
||||
# kb/comparisons/ - Collection Contract
|
||||
|
||||
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
|
||||
without saying what they cost has described, not compared.
|
||||
|
||||
Inherits [kb/CONTRACT.md](../CONTRACT.md) - naming, tone, linking, provenance and confidence
|
||||
are defined there and are not restated here.
|
||||
Inherits [kb/CONTRACT.md](../CONTRACT.md) for the rules the stack enforces - linking mechanics,
|
||||
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
|
||||
|
||||
@@ -16,8 +23,9 @@ are defined there and are not restated here.
|
||||
|
||||
## Naming
|
||||
|
||||
The title reads as a comparison: `Go vs Rust.md`, `Traefik vs nginx.md`. Order the subjects as
|
||||
they are most commonly spoken, not alphabetically.
|
||||
The title form is [kb/CONVENTIONS.md § Naming](../CONVENTIONS.md#naming)'s. What is local here
|
||||
is the ordering: name the subjects as they are most commonly spoken together, not
|
||||
alphabetically.
|
||||
|
||||
## Requirements
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
profile: concepts
|
||||
required_by_stack: false
|
||||
---
|
||||
|
||||
# kb/concepts/ - Collection Contract
|
||||
|
||||
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
|
||||
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
|
||||
are defined there and are not restated here.
|
||||
Inherits [kb/CONTRACT.md](../CONTRACT.md) for the rules the stack enforces - linking mechanics,
|
||||
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
|
||||
|
||||
@@ -17,8 +24,8 @@ are defined there and are not restated here.
|
||||
|
||||
## Decisions and ADRs
|
||||
|
||||
An architectural decision is a concept page prefixed `adr-NNN-`, e.g.
|
||||
`adr-001-use-go-modules.md`. It records:
|
||||
An architectural decision is a concept page, prefixed as
|
||||
[kb/CONVENTIONS.md § Naming](../CONVENTIONS.md#naming) says. It records:
|
||||
|
||||
- **Context** - what forced a decision.
|
||||
- **Decision** - what was chosen.
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
profile: entities
|
||||
required_by_stack: false
|
||||
---
|
||||
|
||||
# kb/entities/ - Collection Contract
|
||||
|
||||
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
|
||||
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
|
||||
are defined there and are not restated here.
|
||||
Inherits [kb/CONTRACT.md](../CONTRACT.md) for the rules the stack enforces - linking mechanics,
|
||||
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
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
profile: sources
|
||||
required_by_stack: true
|
||||
---
|
||||
|
||||
# kb/sources/ - Collection Contract
|
||||
|
||||
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
|
||||
evidence for anything.
|
||||
|
||||
Inherits [kb/CONTRACT.md](../CONTRACT.md) - naming, tone, linking, provenance and confidence
|
||||
are defined there and are not restated here.
|
||||
Inherits [kb/CONTRACT.md](../CONTRACT.md) for the rules the stack enforces - linking mechanics,
|
||||
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
|
||||
|
||||
|
||||
+3
-3
@@ -67,10 +67,10 @@ 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 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 |
|
||||
| `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 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 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 |
|
||||
@@ -80,7 +80,7 @@ tools/wikitool <command> --help
|
||||
| `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 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
|
||||
|
||||
|
||||
+19
-10
@@ -39,7 +39,8 @@ tools/
|
||||
errors.py ChemenuError / ValidationError / BackendError
|
||||
corpus_cache.py one parsed corpus per commit, never cached while the tree is dirty
|
||||
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
|
||||
lint_core.py the lint checks and the report, 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
|
||||
the eval layer (`evals/trajectory.py`, `clearance-ended-the-turn`).
|
||||
|
||||
**Section names are a vocabulary, not literals.** `xref add` writes into Relationships and See
|
||||
Also, and `cite add` owns the trailing Footnotes block, so those three headings are structure the
|
||||
tool matches on. They are named once in `sections.py`, and each has one canonical spelling - what
|
||||
the tool writes - plus aliases it still recognizes. 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).
|
||||
**Section names are a vocabulary, not literals - and not the stack's.** `xref add` writes into
|
||||
Relationships and See Also, and `cite add` owns the trailing Footnotes block, so those three
|
||||
headings are structure the tool matches on. *Which words they are* is the corpus's own answer:
|
||||
`conventions.py` reads them from `kb/CONVENTIONS.md`, `sections.py` resolves them on access
|
||||
(PEP 562, the way `config` resolves its paths), and no heading text is written down in Python
|
||||
except the pre-conventions fallback for an instance that has not declared one yet.
|
||||
|
||||
Each slot has one canonical spelling - what the tool writes - plus aliases it still recognizes.
|
||||
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
|
||||
`.claude/skills/` are build output; `docs verify` carries canaries in both
|
||||
|
||||
@@ -2,9 +2,13 @@
|
||||
repo's machinery.
|
||||
|
||||
`export` copies the pipeline's schema/compiler/control-plane layers (types/,
|
||||
tools/, instructions/, the stage contracts, every kb/*/COLLECTION.md) into an
|
||||
empty target, with no kb/ pages, no raw/ content, and no git history - see
|
||||
instructions/setup-instance.md for what happens after. It never calls git.
|
||||
tools/, instructions/, the stage contracts) into an empty target, with no kb/
|
||||
pages, no raw/ content, and no git history - see instructions/setup-instance.md
|
||||
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
|
||||
shapes of "does not belong in someone else's instance":
|
||||
@@ -35,7 +39,7 @@ from typing import Callable, NamedTuple, Optional, Union
|
||||
|
||||
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
|
||||
|
||||
app = typer.Typer(help="Build a distributable copy of the wiki machinery.")
|
||||
@@ -289,12 +293,32 @@ def build_plan(origin: Optional[Origin] = None) -> dict[str, PlannedFile]:
|
||||
for hook_dir in HOOK_DIRS:
|
||||
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"
|
||||
if kb_contract.is_file():
|
||||
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():
|
||||
rel = f"kb/{collection.name}/COLLECTION.md"
|
||||
plan[rel] = _read_planned_file(collection / "COLLECTION.md", rel)
|
||||
source = collection / kb_collections.CONTRACT_NAME
|
||||
rel = f"kb/{collection.name}/{kb_collections.CONTRACT_NAME}.template"
|
||||
plan[rel] = _read_planned_file(source, rel)
|
||||
|
||||
for relative in CONTRACT_ONLY_STAGES:
|
||||
source = config.ROOT / relative
|
||||
@@ -339,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
|
||||
# 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.
|
||||
#
|
||||
# `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_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]:
|
||||
@@ -350,6 +387,8 @@ def find_leaks(plan: dict[str, PlannedFile]) -> list[str]:
|
||||
name = relative.rsplit("/", 1)[-1]
|
||||
if name in config.PERSONALIZATION_FILES or name == config.ENVIRONMENT_FILE:
|
||||
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/"):
|
||||
leaks.append(f"{relative} (stack-development only)")
|
||||
elif relative.startswith(_CONTENT_PREFIXES) and name not in _CONTENT_ALLOWED_NAMES:
|
||||
@@ -394,7 +433,8 @@ def export_command(
|
||||
AGENTS.md/README.md (dev-instance-only marker blocks removed),
|
||||
instructions/ (no 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
|
||||
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
|
||||
personalization templates (never the filled files), and a
|
||||
.wikitool-release.json stamp. The --source-*/--release-url/--update-url
|
||||
|
||||
@@ -37,7 +37,7 @@ from typing import Optional
|
||||
|
||||
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
|
||||
|
||||
app = typer.Typer(help="Verify documentation that mirrors the code or repo layout.")
|
||||
@@ -114,6 +114,12 @@ REQUIRED_TRACKED_PATHS = (
|
||||
"instructions/CONTRACT.md",
|
||||
"instructions/wiki-query/SKILL.md",
|
||||
"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"
|
||||
@@ -207,13 +213,22 @@ def check_cli_readme() -> 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
|
||||
`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
|
||||
contract is an unclaimed subtree whose pages obey no local rules, and a
|
||||
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 = []
|
||||
|
||||
@@ -243,6 +258,9 @@ def check_collection_contracts() -> list[str]:
|
||||
if not (config.ROOT / relative_path).exists():
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ from typing import Optional
|
||||
import typer
|
||||
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._util import rel_path
|
||||
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")
|
||||
|
||||
|
||||
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:
|
||||
"""Whether this checkout records the environment it works through.
|
||||
|
||||
@@ -399,6 +440,7 @@ def run_doctor() -> list[Check]:
|
||||
check_skills(),
|
||||
check_structure(),
|
||||
check_personalization(),
|
||||
check_conventions(),
|
||||
check_environment(),
|
||||
check_publish_remotes(),
|
||||
check_generated_files(),
|
||||
@@ -411,9 +453,9 @@ def doctor_command(
|
||||
json_out: bool = typer.Option(False, "--json", help="Print the checks as JSON"),
|
||||
):
|
||||
"""Check that this instance is correctly configured: dependencies, author,
|
||||
git identity/remote, published skills, structure, personalization,
|
||||
generated files, and session scoping. Read-only. Exits 1 only if a check
|
||||
FAILs."""
|
||||
git identity/remote, published skills, structure, personalization, KB
|
||||
conventions, generated files, and session scoping. Read-only. Exits 1 only
|
||||
if a check FAILs."""
|
||||
checks = run_doctor()
|
||||
|
||||
if json_out:
|
||||
|
||||
@@ -24,7 +24,7 @@ import re
|
||||
|
||||
import typer
|
||||
|
||||
from chemenu import config
|
||||
from chemenu import config, conventions
|
||||
from chemenu.commands._util import (
|
||||
check_collision,
|
||||
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.
|
||||
|
||||
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)
|
||||
to `variables[field]`'s value, so templates can render list/enum
|
||||
frontmatter fields directly instead of the caller precomputing a
|
||||
@@ -324,7 +328,13 @@ def new_page_command(
|
||||
|
||||
path = target_dir / f"{page_title}.md"
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
@@ -20,11 +20,32 @@ Two corollaries are enforced rather than documented:
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from chemenu import config
|
||||
|
||||
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]:
|
||||
"""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)
|
||||
|
||||
|
||||
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:
|
||||
try:
|
||||
relative = path.relative_to(repo_root)
|
||||
|
||||
@@ -14,9 +14,9 @@ WIKILINK_RE = re.compile(r"\[\[([^\]|#]+)")
|
||||
|
||||
|
||||
# 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
|
||||
# living in it.
|
||||
_KB_META_FILES = {"index.md", "log.md", "provenance.md", "CONTRACT.md"}
|
||||
# and provenance index, plus the two documents that constrain the tree rather
|
||||
# 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", "CONVENTIONS.md"}
|
||||
|
||||
# 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
|
||||
|
||||
@@ -64,7 +64,22 @@ LEGACY_CITE_RE = re.compile(r"\^\[\[([^\]|#]+)(?:\|([^\]]+))?\]\]")
|
||||
# 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
|
||||
# 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 - "
|
||||
|
||||
@@ -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
|
||||
it. Translating the heading is the migration's job, not the tool's."""
|
||||
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(
|
||||
definitions: dict[str, tuple[str, Optional[str]]], heading: str = CITE_BLOCK_HEADING
|
||||
definitions: dict[str, tuple[str, Optional[str]]], heading: Optional[str] = None
|
||||
) -> str:
|
||||
"""Render the Footnotes block for `definitions` (cite_id -> (title,
|
||||
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:
|
||||
return ""
|
||||
lines = [heading, ""]
|
||||
lines = [heading or cite_block_heading_default(), ""]
|
||||
for cid, (title, qualifier) in definitions.items():
|
||||
target = f"{title}|{qualifier}" if qualifier else title
|
||||
lines.append(f"[^{cid}]: [[{target}]]")
|
||||
@@ -214,7 +234,7 @@ def render_cite_block(
|
||||
def render_page_body(
|
||||
head: str,
|
||||
definitions: dict[str, tuple[str, Optional[str]]],
|
||||
heading: str = CITE_BLOCK_HEADING,
|
||||
heading: Optional[str] = None,
|
||||
) -> str:
|
||||
"""Reassemble a page body from its non-Footnotes content and citation
|
||||
definitions - the inverse of split_cite_block(). Pass the original body's
|
||||
|
||||
+50
-13
@@ -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
|
||||
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
|
||||
force these three to stay English, because a translated heading did not error -
|
||||
it made `xref add` append a *second* section, silently. This module removes that
|
||||
constraint by making the vocabulary explicit in one place.
|
||||
**Which words they are is the instance's decision, not the stack's.** They
|
||||
follow the KB language, and the KB language is declared in `kb/CONVENTIONS.md`
|
||||
(see `chemenu.conventions`). This module used to hold `RELATIONSHIPS =
|
||||
"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
|
||||
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
|
||||
itself is translated. Removing an alias is therefore a breaking change for every
|
||||
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
|
||||
|
||||
RELATIONSHIPS = "Beziehungen"
|
||||
SEE_ALSO = "Siehe auch"
|
||||
FOOTNOTES = "Fußnoten"
|
||||
from chemenu import conventions
|
||||
|
||||
ALIASES: dict[str, tuple[str, ...]] = {
|
||||
RELATIONSHIPS: ("Relationships",),
|
||||
SEE_ALSO: ("See Also",),
|
||||
FOOTNOTES: ("Footnotes",),
|
||||
# The slots, re-exported so a caller keeps using `sections.RELATIONSHIPS` as an
|
||||
# opaque handle. The value it resolves to is the heading text; the name it is
|
||||
# looked up under is stable.
|
||||
_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, ...]:
|
||||
"""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]:
|
||||
@@ -44,4 +81,4 @@ def heading_re(canonical: str) -> re.Pattern[str]:
|
||||
|
||||
def is_known(heading: str) -> bool:
|
||||
"""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)
|
||||
|
||||
@@ -3,7 +3,7 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from chemenu import config
|
||||
from chemenu import config, conventions
|
||||
from chemenu.frontmatter_io import write_page
|
||||
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
|
||||
# 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.
|
||||
# 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()
|
||||
conventions.reset_cache()
|
||||
yield home
|
||||
config.reset()
|
||||
conventions.reset_cache()
|
||||
|
||||
|
||||
def use_shipped_type_specs(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
|
||||
@@ -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) == []
|
||||
@@ -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" / "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 / "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"):
|
||||
path = root / relative
|
||||
@@ -262,6 +270,10 @@ def test_find_leaks_is_silent_on_a_clean_plan(repo):
|
||||
"instructions/dev/commonplace-kb.md",
|
||||
"kb/entities/aurora.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):
|
||||
@@ -283,12 +295,28 @@ def test_export_refuses_a_plan_that_leaks(repo, tmp_path, monkeypatch):
|
||||
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()
|
||||
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 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):
|
||||
plan = dist_cmd.build_plan()
|
||||
@@ -397,7 +425,7 @@ def test_export_into_a_fresh_directory_works(repo, tmp_path):
|
||||
target = tmp_path / "dist"
|
||||
dist_cmd.run_export(target, dry_run=False)
|
||||
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()
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from chemenu import config
|
||||
from chemenu import config, conventions
|
||||
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 / "provenance.md").write_text("# Provenance\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 / "USER.md").write_text("# USER.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")
|
||||
|
||||
|
||||
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):
|
||||
"""The file is optional, so absence is a healthy end state - a FAIL here
|
||||
would make it mandatory through the back door."""
|
||||
|
||||
@@ -75,6 +75,31 @@ def test_new_entity_creates_page_with_expected_frontmatter(monkeypatch, kb_dir):
|
||||
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):
|
||||
"""provenance and confidence are no longer Typer flag defaults - they
|
||||
come from the schema's own `default:`, so omitting them still yields a
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from chemenu import sections
|
||||
from chemenu import conventions
|
||||
from chemenu.cli import app
|
||||
|
||||
runner = CliRunner()
|
||||
@@ -45,9 +45,10 @@ def test_types_describe_entity_reports_schema_and_body():
|
||||
]
|
||||
assert fields_by_name["tags"]["required"] is False
|
||||
# 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
|
||||
# the section names themselves - does not turn this into a tripwire.
|
||||
assert f"## {sections.RELATIONSHIPS}" in data["body"]
|
||||
# template *variable* rather than on any heading text: the spec no longer names the
|
||||
# tool-owned sections at all - `kb/CONVENTIONS.md` does, and `new` substitutes it - so a
|
||||
# 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():
|
||||
|
||||
+3
-3
@@ -36,7 +36,7 @@ page_ref_fields: [entities]
|
||||
|
||||
## 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
|
||||
- Eine Vergleichstabelle mit den Kriterien als Zeilen verwenden
|
||||
- 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`,
|
||||
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
|
||||
`tools/chemenu/sections.py`.
|
||||
Fügt `wikitool xref` eine Beziehung hinzu, entsteht der toolgeführte Querverweis-Abschnitt; wie
|
||||
er heißt, entscheidet die Instanz in `kb/CONVENTIONS.md` (`sections:`).
|
||||
|
||||
---
|
||||
|
||||
|
||||
+4
-3
@@ -45,7 +45,7 @@ page_ref_fields: [related, sources]
|
||||
|
||||
## 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
|
||||
- Beispiele geben, wo sie das Verständnis tragen
|
||||
- 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`.
|
||||
Fügt `wikitool xref` eine Beziehung hinzu, entstehen zusätzlich `## Beziehungen` und
|
||||
`## Siehe auch`; deren Namen stehen in `tools/chemenu/sections.py`.
|
||||
Fügt `wikitool xref` eine Beziehung hinzu, entstehen zusätzlich die beiden toolgeführten
|
||||
Abschnitte für Beziehungen und Querverweise; wie sie heißen, entscheidet die Instanz in
|
||||
`kb/CONVENTIONS.md` (`sections:`).
|
||||
|
||||
---
|
||||
|
||||
|
||||
+8
-6
@@ -50,7 +50,7 @@ layout:
|
||||
|
||||
## 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
|
||||
- Auf verwandte Entities und Concepts verlinken, wo Beziehungen bestehen
|
||||
- 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)
|
||||
- **Repository:** TODO (falls zutreffend)
|
||||
|
||||
## Beziehungen
|
||||
## {section.relationships}
|
||||
|
||||
- **Hängt ab von:** TODO
|
||||
- **Verwendet von:** TODO
|
||||
@@ -91,14 +91,16 @@ TODO: Ausführliche Informationen, nach sinnvollen Abschnitten gegliedert
|
||||
|
||||
- [{today}] - Page created via wikitool
|
||||
|
||||
## Siehe auch
|
||||
## {section.see_also}
|
||||
|
||||
- TODO: Verwandte Seiten
|
||||
```
|
||||
|
||||
`## Beziehungen` und `## Siehe auch` sind toolgeführt: `wikitool xref` schreibt in genau diese
|
||||
Abschnitte, benannt in `tools/chemenu/sections.py`. Der Wert hinter `**Typ:**` bleibt der
|
||||
englische Enum-Wert - danach filtert `search --field`.
|
||||
Die beiden `{section.…}`-Platzhalter sind toolgeführte Abschnitte: `wikitool xref` schreibt in
|
||||
genau sie hinein, und wie sie heißen, entscheidet die Instanz in `kb/CONVENTIONS.md`
|
||||
(`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
@@ -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)
|
||||
- Bei externen Artikeln immer `source_url` auf die Ursprungs-URL setzen
|
||||
- `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
|
||||
- Handlungsbedarf in den Abschnitt Action Items
|
||||
- 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
|
||||
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
|
||||
steht in `tools/chemenu/sections.py`.
|
||||
Enum-Wert. Fügt `wikitool cite` ein Zitat hinzu, entsteht am Seitenende der toolgeführte
|
||||
Fußnoten-Block; wie er heißt, entscheidet die Instanz in `kb/CONVENTIONS.md` (`sections:`).
|
||||
|
||||
---
|
||||
|
||||
|
||||
+15
-8
@@ -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
|
||||
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
|
||||
|
||||
| Owned here | Owned by `kb/CONTRACT.md` and the collection contracts |
|
||||
|------------|-----------------------------------------------------------|
|
||||
| Frontmatter fields, enums, defaults, required-ness | Quality goal and tone |
|
||||
| Directory placement and title prefix | Naming conventions |
|
||||
| Body skeleton (template) | Linking policy and relationship vocabulary |
|
||||
| When to use / not use this type | Provenance and confidence practice |
|
||||
| Owned here | Owned by `kb/CONTRACT.md` | Owned by `kb/CONVENTIONS.md` and the collection contracts |
|
||||
|------------|--------------------------|-----------------------------------------------------------|
|
||||
| Frontmatter fields, enums, defaults, required-ness | Provenance and citation mechanics | Quality goal and tone |
|
||||
| Directory placement and title prefix | The confidence machinery | Naming conventions and the confidence rubric |
|
||||
| Body skeleton (template) | Linking mechanics and the orphan check | Relationship vocabulary |
|
||||
| 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
|
||||
type-spec. If it is identical for every page in one collection, it belongs in that
|
||||
If a rule would be identical for every type *and* every instance, it belongs in
|
||||
`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`.
|
||||
|
||||
### What does not belong here
|
||||
|
||||
Reference in New Issue
Block a user