feat: Prosa ist kein Identifier - Link-Taxonomie als Enum, generierte Regionen mit Markern (4.0.0)
CI / verify (push) Successful in 55s
Release / release (push) Successful in 38s

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
This commit is contained in:
2026-09-02 18:39:22 +02:00
parent 502971d147
commit 177c7e9ce8
56 changed files with 2692 additions and 750 deletions
@@ -0,0 +1,146 @@
---
type: types/instruction.md
name: 4.0.0-link-taxonomy
description: Move every relationship from free-text prose in a body bullet to a labelled edge in related:, and every tool-owned body region from heading-matching to a marker pair.
manual: true
migrates_to: 4.0.0
migration_kind: assisted
obligation: required
---
# Move relationships into the data, and generated regions behind markers (4.0.0)
Until 4.0.0 the stack used **prose as an identifier** in three places, and each one cost
something measurable:
| Was the identifier | Cost |
|---|---|
| A section's heading text (`## Beziehungen`) | The KB language was a compiler constant, and the region's *end* was a guess. Content sitting after it was silently deleted on eight pages |
| A relationship label in a body bullet (`- **hängt ab von:**`) | Nothing could check the vocabulary, so it drifted to **152 distinct labels** across 337 bullets against thirteen that were documented |
| The reciprocal half of every edge | `xref add` mirrored every link, which made per-collection label authorisation impossible and filled `## Siehe auch` with 555 unlabelled bullets, 353 of them provably redundant |
4.0.0 replaces all three. A region is delimited by a marker pair and rendered from frontmatter;
a label is a machine value in `related:`, drawn from a catalogue and authorised per destination
by the source collection; an edge is authored in one direction and the inbound view is computed.
**This one touches pages.** Unlike 3.0.0 it is not a contract reshuffle: every `related:` entry
and every tool-owned body region changes. It is `assisted` because there is no mapping table -
mapping free-text German onto a 35-label catalogue is a judgment call per edge, and a large
minority of the old labels are reverse directions that under the new model are not stored at all.
## When to run
After installing 4.0.0 over an instance on 3.x. `tools/wikitool migrate status` names it, and
`lint` reports `unlabelled_edges` for every unconverted edge - that count reaching zero is how
you know the run is finished.
**Nothing breaks while it is outstanding.** Unlabelled edges and undelimited regions are read,
not rejected: `links.py` treats a bare title as an edge whose label is not declared yet, and
`provenance.split_cite_block` falls back to the pre-marker layout. That is deliberate - a corpus
has to stay readable while it is being converted - and it is why the two lint findings are
advisory until step 6 promotes them.
## Steps
1. **Rewrite `kb/CONVENTIONS.md`'s `sections:` block.** Three slots become two, because the
See Also region is gone:
```yaml
sections:
links: <your heading for declared relationships>
footnotes: <your heading for citation definitions>
```
Delete `section_aliases:` if you have one - nothing matches on heading text any more, so
there is nothing to alias. The heading is now a *rendering* value: changing it re-renders
the words above each region on the next write and can no longer split a page.
2. **Add an `outbound:` block to every `kb/<name>/COLLECTION.md`.** Which labels a page may use,
per destination collection, with `any` as a wildcard:
```yaml
outbound:
entities: [depends-on, runs-on, uses, see-also]
concepts: [implements, see-also]
```
The catalogue to draw from is [link-taxonomy.md](../link-taxonomy.md); the four contracts in
the origin repo are worked examples. **The source collection decides** - that is what makes a
35-label palette usable, and it is why the reverse edge can no longer be written
automatically. A destination you do not list authorises nothing, which is a real answer.
3. **Fix your page type-spec templates.** If you adopted the 3.0.0 templates, they contain
`## {section.relationships}` and `## {section.see_also}`. Those variables no longer exist and
would be written into new pages literally. **Delete both sections from the `## Template`
block** - a template must not scaffold a tool-owned region at all: it is generated between
markers on the first `xref add` / `cite add` and re-rendered on every write.
4. **Convert the corpus**, following [migrate-corpus.md](../migrate-corpus.md). Cut it into
units sized against the iteration budget; the origin repo used four, ~45 pages each. Per page:
- For each labelled bullet under the old relationships heading: say the sentence
`[this page] <label> [target]` and pick the catalogue label that makes it true. If it only
reads true **backwards**, the edge belongs on the other page - move it there rather than
inventing an inverse label the catalogue does not have.
- For each bare `- [[X]]` bullet under the old See Also heading: drop it if a labelled edge
already connects the pair. Otherwise decide - a real label, or dropped with the reason
recorded. **Do not convert them to `see-also` in bulk.** That is the one shortcut this
migration explicitly refuses: it would start the new taxonomy with most of its edges on its
weakest label, which is the sediment the change exists to remove.
- Write edges with `tools/wikitool xref add --a "<A>" --b "<B>" --rel <label>`, never by
hand. The body region is rendered from `related:`; editing inside a marker pair is
overwritten without warning.
- `cite sync` converts a page's old footnote block into a marked region in passing.
5. **Check each unit mechanically before anything else:**
```bash
tools/wikitool migrate verify --from <pre-migration rev> --path kb/<area> --fail-on-error
```
It compares wikilink and citation **counts**, footnote definitions, H1, structural
frontmatter, and - new in 4.0.0 - the **count of marker pairs per region**. A dropped marker
is otherwise silent: the region becomes ordinary prose and the next write appends a second
one beside it.
6. **Record it, then tighten the checks:**
```bash
tools/wikitool lint # unlabelled_edges and unauthorised_labels must be 0
tools/wikitool migrate done 4.0.0 --pages <N>
```
Only once `lint` reports zero of both is the run finished. The two findings are advisory
during the window and become hard errors afterwards - the same path
`legacy_citation_markers` took after the citation migration.
## How to tell a migrated page from an unmigrated one
Its `related:` entries are `- <label>: <title>` rather than bare titles, and its relationship
and footnote sections sit between `<!-- wikitool:links -->` / `<!-- wikitool:footnotes -->`
marker pairs. `tools/wikitool links show --page "<Title>"` prints `unlabelled` for every edge
still waiting, and `lint`'s `unlabelled_edges` count is the corpus-wide version of the same
question.
## Decision points
- **A label you want is not in the catalogue?** Add it - a row in `link-taxonomy.md` and an
entry in the authorising `COLLECTION.md`. No code change is involved, and the registers are
advisory groupings rather than a schema. Do not stretch a label whose sentence reads false: a
wrong edge is worse than a weak one, because it is machine-readable and will be believed.
- **`related:` holds an entry with no body bullet to derive a label from?** Expected - the
origin repo found 480 edges against 337 bullets, because frontmatter and body had already
drifted apart while the label lived only in prose. Read the page and decide; that drift is
itself part of what this migration repairs.
- **A page loses its last inbound edge?** The orphan check will now report it, and that is the
check working: directional edges mean a page nothing points at is genuinely unreachable, where
the old mirrored model always manufactured a back-link. Either something should point at it,
or it is reached through the catalog and that is fine.
- **Tempted to keep writing reverse edges for navigation?** Do not. `links show` computes the
inbound view, and the rendered bullet on the asserting page is an ordinary `[[wikilink]]`, so
a backlink panel in an editor already shows it.
## Scope
The corpus under `kb/`, plus the three instance-owned declarations in steps 1-3. It does not
touch `raw/`, and it learns nothing new: the same knowledge is restated in a form that can be
checked. Installing the 4.0.0 machinery itself is `INSTALL.md`'s and must have happened first.