From 177c7e9ce846b48a8e4fa6b32dca1f7dbb0add69 Mon Sep 17 00:00:00 2001 From: Torben Nehmer Date: Wed, 2 Sep 2026 18:39:22 +0200 Subject: [PATCH] feat: Prosa ist kein Identifier - Link-Taxonomie als Enum, generierte Regionen mit Markern (4.0.0) Files changed: - .gitea/workflows/ci.yml - AGENTS.md - CHANGES.md - VERSION - instructions/CONTRACT.md - instructions/link-taxonomy.md - instructions/migrations/4.0.0-link-taxonomy.md - instructions/setup-instance.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/blocks.py - tools/chemenu/cli.py - tools/chemenu/commands/cite_cmd.py - tools/chemenu/commands/dist_cmd.py - tools/chemenu/commands/docs_verify.py - tools/chemenu/commands/doctor.py - tools/chemenu/commands/links_cmd.py - tools/chemenu/commands/migrate_cmd.py - tools/chemenu/commands/new_page.py - tools/chemenu/commands/page_ops.py - tools/chemenu/commands/run_budget.py - tools/chemenu/commands/xref.py - tools/chemenu/conventions.py - tools/chemenu/corpus_diff.py - tools/chemenu/frontmatter_io.py - tools/chemenu/kb_collections.py - tools/chemenu/kb_state.py - tools/chemenu/links.py - tools/chemenu/lint_core.py - tools/chemenu/provenance.py - tools/chemenu/sections.py - tools/chemenu/tests/conftest.py - tools/chemenu/tests/test_blocks.py - tools/chemenu/tests/test_cite_cmd.py - tools/chemenu/tests/test_conventions.py - tools/chemenu/tests/test_dist_cmd.py - tools/chemenu/tests/test_doctor.py - tools/chemenu/tests/test_migrate_cmd.py - tools/chemenu/tests/test_new_page.py - tools/chemenu/tests/test_pipeline_l0.py - tools/chemenu/tests/test_types_cmd.py - tools/chemenu/tests/test_xref.py - types/concept.schema.yaml - types/entity.md - types/entity.schema.yaml - types/instruction.schema.yaml - types/type-spec.md - work/link-taxonomy-migration/README.md - work/link-taxonomy-migration/plan.md --- .gitea/workflows/ci.yml | 2 +- AGENTS.md | 2 +- CHANGES.md | 73 +++++ VERSION | 2 +- instructions/CONTRACT.md | 24 +- instructions/link-taxonomy.md | 199 +++++++++++++ .../migrations/4.0.0-link-taxonomy.md | 146 ++++++++++ instructions/setup-instance.md | 25 +- kb/CONTRACT.md | 80 ++++-- kb/CONVENTIONS.md | 51 ++-- kb/CONVENTIONS.md.template | 37 +-- kb/comparisons/COLLECTION.md | 19 +- kb/concepts/COLLECTION.md | 20 +- kb/entities/COLLECTION.md | 16 ++ kb/sources/COLLECTION.md | 13 + tools/CONTRACT.md | 24 +- tools/README.md | 39 +-- tools/chemenu/blocks.py | 136 +++++++++ tools/chemenu/cli.py | 2 + tools/chemenu/commands/cite_cmd.py | 5 +- tools/chemenu/commands/dist_cmd.py | 63 +++- tools/chemenu/commands/docs_verify.py | 43 +++ tools/chemenu/commands/doctor.py | 25 +- tools/chemenu/commands/links_cmd.py | 95 +++++++ tools/chemenu/commands/migrate_cmd.py | 94 +++++- tools/chemenu/commands/new_page.py | 9 +- tools/chemenu/commands/page_ops.py | 19 +- tools/chemenu/commands/run_budget.py | 4 + tools/chemenu/commands/xref.py | 212 +++++++------- tools/chemenu/conventions.py | 121 +++----- tools/chemenu/corpus_diff.py | 21 +- tools/chemenu/frontmatter_io.py | 33 ++- tools/chemenu/kb_collections.py | 107 +++++-- tools/chemenu/kb_state.py | 107 ++++++- tools/chemenu/links.py | 143 ++++++++++ tools/chemenu/lint_core.py | 88 +++++- tools/chemenu/provenance.py | 199 ++++++------- tools/chemenu/sections.py | 84 ------ tools/chemenu/tests/conftest.py | 14 +- tools/chemenu/tests/test_blocks.py | 90 ++++++ tools/chemenu/tests/test_cite_cmd.py | 39 ++- tools/chemenu/tests/test_conventions.py | 141 +++++---- tools/chemenu/tests/test_dist_cmd.py | 37 +++ tools/chemenu/tests/test_doctor.py | 10 +- tools/chemenu/tests/test_migrate_cmd.py | 145 +++++++++- tools/chemenu/tests/test_new_page.py | 37 +-- tools/chemenu/tests/test_pipeline_l0.py | 28 +- tools/chemenu/tests/test_types_cmd.py | 13 +- tools/chemenu/tests/test_xref.py | 269 +++++++++++++----- types/concept.schema.yaml | 17 +- types/entity.md | 19 +- types/entity.schema.yaml | 17 +- types/instruction.schema.yaml | 15 + types/type-spec.md | 33 ++- work/link-taxonomy-migration/README.md | 88 ++++++ work/link-taxonomy-migration/plan.md | 48 ++++ 56 files changed, 2692 insertions(+), 750 deletions(-) create mode 100644 instructions/link-taxonomy.md create mode 100644 instructions/migrations/4.0.0-link-taxonomy.md create mode 100644 tools/chemenu/blocks.py create mode 100644 tools/chemenu/commands/links_cmd.py create mode 100644 tools/chemenu/links.py delete mode 100644 tools/chemenu/sections.py create mode 100644 tools/chemenu/tests/test_blocks.py create mode 100644 work/link-taxonomy-migration/README.md create mode 100644 work/link-taxonomy-migration/plan.md diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 88ca9a4..022b9db 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -228,7 +228,7 @@ jobs: # 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 + for template in kb/*/COLLECTION.md.template types/*.template; do cp "$template" "${template%.template}" done python3 -m venv tools/.venv diff --git a/AGENTS.md b/AGENTS.md index fd9f4fd..58e3b0f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -81,7 +81,7 @@ What a file is called says who it is for and how it is loaded. This is a rule, n | `kb//COLLECTION.md` | Agents | When writing in that collection. Instance-owned in the same way, and declares in frontmatter which profile it adopted | | `instructions/.md` | Agents | By link, or on explicit request | | `instructions//SKILL.md` | Agents | By the harness, once published | -| `types/.md` | Agents + validator | Via `tools/wikitool types describe` | +| `types/.md` | Agents + validator | Via `tools/wikitool types describe`. Split by `root:`: a page type-spec (`root: kb`) belongs to the instance and ships as `.template`; one describing a stack artifact ships verbatim | | `INDEX.md` | Both | Generated - never hand-edited | A stage may carry both a `README.md` and a `CONTRACT.md`: different readers, different diff --git a/CHANGES.md b/CHANGES.md index 14e7c93..9a0b018 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -18,6 +18,79 @@ heading, and `wikitool docs verify` refuses a tree whose `VERSION` and newest versioned entry disagree. Entries below `0.1.0` predate versioning and keep their date-only headings. +--- + +## 4.0.0 - 2026-09-02 - Prosa ist kein Identifier: Link-Taxonomie als Enum, generierte Regionen mit Markern + +**Author:** Torben Nehmer + +**Breaking Change:** Beziehungslabel sind Enum-Werte in related: statt Freitext im Body-Bullet, toolgefuehrte Abschnitte liegen zwischen Marker-Paaren statt hinter ihrer Ueberschrift, und xref add schreibt nur noch eine Kante statt beider Richtungen. tools/chemenu/sections.py ist geloescht. Eine bestehende Instanz muss sections: in kb/CONVENTIONS.md auf links/footnotes umstellen, outbound: in jede COLLECTION.md eintragen, die {section.*}-Variablen aus ihren Page-Type-Templates entfernen und den Korpus umstellen - sonst scaffoldet new die Variablen woertlich in neue Seiten. Ablauf: instructions/migrations/4.0.0-link-taxonomy.md + +Der Stack benutzte an drei Stellen **Prosa als Identifier**, und jede hat messbar etwas +gekostet. Die Überschrift eines Abschnitts war seine Adresse (`^## Beziehungen$`), was die +KB-Sprache zu einer Compiler-Konstante machte *und* das Ende der Region zur Schätzung - sie lief +bis zur nächsten Überschrift, davor bis zum Dateiende, und hat auf acht Seiten still Inhalt +gelöscht. Das Beziehungslabel stand nur im Body-Bullet, also konnte nichts das Vokabular prüfen: +gemessen am Korpus **152 distinkte Label in 337 Bullets** gegen dreizehn dokumentierte, 102 davon +genau einmal vorkommend. Und `xref add` spiegelte jede Kante, was `## Siehe auch` mit 555 +Bullets ohne Label füllte - 353 davon beweisbar redundant. + +**Was jetzt Identifier ist.** Eine Region liegt zwischen `` bzw. +`` und wird vollständig aus dem Frontmatter gerendert, Überschrift +eingeschlossen. Ein Label ist ein Maschinenwert in `related:` (`- depends-on: Hermes`), gezogen +aus `instructions/link-taxonomy.md` und **pro Ziel autorisiert von der Quell-Collection** +(`outbound:` im `COLLECTION.md`, Commonplaces ADR-019). Der Body-Bullet ist eine Darstellung +dieser Daten, nicht ihr zweiter Aufbewahrungsort. + +**Gelöscht, ersatzlos:** `tools/chemenu/sections.py` komplett, `heading_re`, der +Alias-Mechanismus, `PRE_CONVENTIONS_NAMES`, `cite_block_heading`, `provenance.__getattr__`, die +`{section.*}`-Template-Variablen, `xref`s Abschnittssuche. Kein Überschriftentext liegt mehr in +Python - bis auf zwei kosmetische Fallbacks, und die sind harmlos geworden: der Marker trägt die +Identität, also rendert ein falscher Default falsche Wörter statt Struktur zu zerlegen, und der +nächste Write repariert es. + +**Kanten sind direktional, und das war keine Geschmacksfrage.** Die per-Collection-Autorisierung +ist mit einer automatisch gespiegelten Gegenkante logisch unverträglich: die Spiegelhälfte +entsteht in einer Collection, deren Regeln der Autor nie gelesen hat. Entweder schriebe das +Werkzeug unautorisierte Kanten, oder die Regel "die Quellcollection entscheidet" löst sich auf. +Der Navigationseinwand wird dabei *besser* beantwortet als vorher: `wikitool links show --page` +berechnet die Eingangssicht über den Korpus, vollständig und ohne Pflege, und das gerenderte +Bullet ist ein gewöhnlicher `[[wikilink]]` - ein Backlink-Panel zeigt es ohnehin. Die erzwungene +Gegenkante garantierte nie Vollständigkeit, nur dass jemand daran gedacht hat. + +**Der Orphan-Check meldet dadurch mehr,** und das ist die Prüfung bei der Arbeit: sie misst jetzt +Erreichbarkeit statt "ist `xref` gelaufen". + +**`obligation:` trennt zwei Achsen, die vorher eine waren.** `migration_kind:` sagt *wie* +gearbeitet wird, neu `obligation: required|offered` *ob* überhaupt. Eine `offered`-Migration ist +ein Angebot für eine Datei, die der Instanz gehört - sie blockiert nie, steht nicht in der Kette, +und `migrate done` verbucht sie im Ledger, **ohne** `kb_version` zu bewegen. Genau daran hing ein +Entwurfsfehler, den erst der Test gezeigt hat: Offers gegen `kb_version` zu filtern hätte jede +Offer verschwinden lassen, sobald irgendein unbeteiligter Pflichtschritt lief. Dazu ist die +Erkennungshälfte aktiviert, die seit ihrer Einführung ungelesen dalag - die sha256 pro Datei in +`.wikitool-release.json` beantwortet jetzt "editiert oder nur empfangen", also ob eine Offer +kopiert werden darf oder von Hand abgeglichen werden muss. + +**`types/` teilt sich entlang `root:`.** `root: kb` heißt Wissensseite heißt Instanz: die vier +Page-Type-Specs samt Schemas gehen als `.template`, `instruction`/`lint-report`/`type-spec` +verbatim. Damit ist die deutsche Prosa in jenen vier Dateien **korrekt statt Migrationsschuld** - +es war die richtige Sprache an einem Ort mit falsch deklariertem Eigentümer. Was der Stack von +der Type-Schicht noch verlangt, ist eine Zeile: ein Type-Spec `name: source`, dessen Schema +`raw_files` fordert. `STACK_REQUIRED_COLLECTIONS` entfällt als separate Liste - die pflichtige +Collection wird aus dem `base_dir` dieses Typs abgeleitet. + +**Warum das MAJOR ist.** Vorwärts: `sections:` hat eine andere Form, `outbound:` fehlt, und die +in 3.0.0 übernommenen Page-Type-Templates enthalten `{section.*}`-Variablen, die es nicht mehr +gibt - `new` schriebe sie wörtlich in neue Seiten. Rückwärts: 4.0.0 schreibt gelabelte Kanten, +die 3.0.0s Schema als `type: string` ablehnt. Beide Hälften des Drop-in-Tests fallen. + +**Der Korpus dieser Instanz ist noch nicht umgestellt.** Diese Version liefert die Maschinerie; +`lint` meldet die 480 noch ungelabelten Kanten als Findings, nicht als Fehler, weil das genau das +Fenster ist, für das `.wikitool-kb.json` existiert. `malformed_edges` und `unbalanced_markers` +sind dagegen sofort hart - keines beschreibt eine unkonvertierte Seite, nur eine kaputte. Die +Beförderung der beiden anderen kommt, wenn der Korpus sie bestehen kann. + + --- ## 3.0.0 - 2026-09-02 - Autorenkonventionen nach Eigentum geschnitten: kb/CONVENTIONS.md, deklarierte Collections diff --git a/VERSION b/VERSION index 4a36342..fcdb2e1 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.0.0 +4.0.0 diff --git a/instructions/CONTRACT.md b/instructions/CONTRACT.md index bbec30c..a65e3d8 100644 --- a/instructions/CONTRACT.md +++ b/instructions/CONTRACT.md @@ -65,11 +65,31 @@ catalogue of authoring profiles an instance may adopt into its own `kb/CONVENTIO ## `instructions/migrations/` -A content migration is a Manual instruction with two extra frontmatter fields +A content migration is a Manual instruction with three extra frontmatter fields (`types/instruction.schema.yaml`): `migrates_to:`, the stack version whose content shape it -produces, and `migration_kind:` (`mechanical` | `assisted`). It lives at +produces; `migration_kind:` (`mechanical` | `assisted`); and `obligation:` +(`required` | `offered`, default `required`). It lives at `instructions/migrations/-.md`. +`migration_kind:` and `obligation:` are **two axes, not one**. The first says how the work is +carried out, the second whether it has to happen at all: + +| `obligation:` | Means | `migrate status` | +|---|---|---| +| `required` | The content must reach the new shape or it no longer fits the machinery | Counted as outstanding; `migrate done` advances `kb_version` through it, in chain order | +| `offered` | A file the instance owns still works as it is, and the stack proposes a better default | Listed separately, never blocks, no ordering rule. `migrate done` records it in the applied ledger and leaves `kb_version` where it is | + +Keeping them apart is what stops `migrate status` crying wolf: an instance nagged about an +improvement it declined stops reading the nag that means its content no longer fits its +machinery. And because taking an offer deliberately does not move the version, the **applied +ledger** - not `kb_version` - is what makes an offer stop being offered; without that record +there is no way to tell a taken offer from an ignored one. + +An `offered` migration is what makes an instance-owned file upgradeable at all. `dist export` +records a sha256 per shipped file in `.wikitool-release.json`, so `migrate status` can say which +of those files the instance edited and which it merely received - the first have to be +reconciled by a person, the second can simply be copied over. + The tier fits exactly: a migration must never be picked up implicitly - it rewrites the corpus - and it is referenced by nothing, because `tools/wikitool migrate status` finds it by reading the directory and comparing `migrates_to:` against this instance's `kb_version`. That is also why diff --git a/instructions/link-taxonomy.md b/instructions/link-taxonomy.md new file mode 100644 index 0000000..6bd7308 --- /dev/null +++ b/instructions/link-taxonomy.md @@ -0,0 +1,199 @@ +--- +type: types/instruction.md +name: link-taxonomy +description: The link-label catalogue - every relationship label a page may declare in related:, grouped by register, with the reader need each one names. A palette to authorise from in a COLLECTION.md, never binding on its own. +manual: true +--- +# Pick a link label + +**This page is a palette, not an enum.** It lists every label this stack ships with and what +each one asserts. What a page may actually *use* is decided by its own collection: each +`kb//COLLECTION.md` authorises a subset per destination, and `wikitool lint` checks +`related:` against that authorisation rather than against this file. A collection that +authorises six labels has six, however long this list gets. + +A label is an **identifier, not prose**. It is written into `related:` as a machine value and +rendered verbatim into the page body, so it is never translated - not in a German wiki, not in +any other. Which words a page is *written* in stays [kb/CONVENTIONS.md](../kb/CONVENTIONS.md)'s; +this is not one of them. + +## The invariant every label obeys + +Every label completes, with the page carrying the link as the grammatical subject: + +> `[source]