feat: Link-Taxonomie abgeschlossen - Lint hart ab kb_version 4.0.0, outbound: an Type-Spec gebunden, part-of/composition als Inversenpaar (4.1.0, #40)
CI / verify (push) Successful in 53s
Release / release (push) Successful in 36s

Files changed:
- CHANGES.md
- VERSION
- instructions/link-taxonomy.md
- instructions/migrations/4.0.0-link-taxonomy.md
- kb/comparisons/amd-pstate vs acpi-cpufreq.md
- kb/concepts/Episodic Memory.md
- kb/concepts/Memory Lifecycle.md
- kb/concepts/Mesh Sync.md
- kb/concepts/Procedural Memory.md
- kb/concepts/Reciprocal Rank Fusion.md
- kb/concepts/Semantic Memory.md
- kb/concepts/Shared vs Private.md
- kb/concepts/Split Threshold.md
- kb/concepts/Stub Threshold.md
- kb/concepts/Supersession.md
- kb/concepts/Typed Relationships.md
- kb/concepts/Vector Search.md
- kb/concepts/Work Coordination.md
- kb/concepts/Working Memory.md
- kb/entities/technologies/Wine-Staging.md
- kb/entities/tools/pascalandy schema.md
- kb/index.md
- kb/log.md
- kb/sources/COLLECTION.md
- tools/CONTRACT.md
- tools/chemenu/commands/lint.py
- tools/chemenu/kb_collections.py
- tools/chemenu/lint_core.py
- tools/chemenu/tests/test_conventions.py
- tools/chemenu/tests/test_lint.py
- tools/chemenu/tests/test_type_resolver.py
- types/comparison.md
- types/comparison.schema.yaml
This commit is contained in:
2026-09-03 06:19:56 +02:00
parent 23e34a940c
commit cfe925a76c
33 changed files with 379 additions and 66 deletions
+62
View File
@@ -20,6 +20,68 @@ their date-only headings.
---
## 4.1.0 - 2026-09-03 - Link-Taxonomie: Lint-Findings hart ab kb_version 4.0.0, outbound: an das Type-Spec gebunden, part-of/composition als Inversenpaar
**Author:** Torben Nehmer
Der Rest von Issue #40, nachdem die Korpus-Migration durch ist: die beiden aufgeschobenen
Lint-Findings werden hart, und die drei Befunde aus dem Abschlusskommentar des Migrationslaufs
werden aufgelöst.
**`unlabelled_edges` und `unauthorised_labels` sind harte Fehler — aber an `kb_version`
gebunden, nicht an ein Datum.** Der Weg, den `legacy_citation_markers` genommen hat, war ein
Umlegen in einer späteren Version: eine Instanz, die die Zitat-Migration noch schuldete, lebte
danach mit rotem Lint. Das Ledger kann die Frage inzwischen beantworten, also tut es das.
Unterhalb `kb_version` 4.0.0 bleiben beide beratend — genau das Fenster, in dem
`instructions/migrations/4.0.0-link-taxonomy.md` der Instanz sagt, sie solle den halb
konvertierten Korpus Einheit für Einheit publizieren; ein Check, der dabei fehlschlägt, würde
den Korpus verweigern, dessen Fortschritt er misst. Ab 4.0.0 ist eine kahle Titelangabe in
`related:` keine Seite mehr, die auf ihre Umstellung wartet, sondern eine Kante, deren Autor
nicht gesagt hat, was sie behauptet. `hard_error_keys()` liefert die jeweils geltende Menge,
`HARD_ERROR_KEYS` bleibt die vollständige.
**`outbound:` ist an das Type-Spec gebunden.** `kb/sources/` und `kb/comparisons/`
autorisierten Label, die dort strukturell nicht schreibbar waren: keiner der beiden Type-Specs
führte ein `related:`. Folgenlos war das nicht — die einzige Comparison-Seite des Korpus trug
`- **compares-with:** [[amd-pstate]]` als *handgeschriebene Prosa*, ohne Marker-Region, ohne
Frontmatter, für `lint` unsichtbar. Also ein Identifier zurück im Fließtext, gut vier Stunden
nachdem 4.0.0 genau das beendet hatte. Eine leere Autorisierung liest sich als Lizenz.
Aufgelöst nach dem, was die beiden Contracts jeweils selbst sagen: `comparison` bekommt ein
`related:` (die `compares-with`-Kante gegen jedes Subjekt ist die eine Aussage, für die die
Seite existiert), `kb/sources/` verliert seinen `outbound:`-Block ersatzlos (dessen Contract
sagt ausdrücklich, seine Verknüpfungen seien der mechanische Provenance-Pfad und keine
Autorenkanten). Neu prüft `docs verify` die Kombination: ein `outbound:`-Block auf einer
Collection, in die kein Typ mit `related:` schreibt, ist ein Befund und nennt beide Richtungen
der Reparatur.
**`composition` / `part-of` ist das dritte Inversenpaar**, neben `depends-on` / `required-by`
und `runs-on` / `hosts`. Aus der Messung, nicht vom Schreibtisch: der u3-Lauf hatte entschieden,
die Gegenseite eines `composition` bekomme `see-also`, weil `part-of` ein Spiegel wäre. Ist es
nicht — der Satz des Elternteils zählt seine Teile auf, der des Kindes benennt das Ganze, zu
dem es gehört, und ein Leser, der auf dem Kind landet, braucht den zweiten. Übrig blieben 16
`see-also`-Kanten für eine Beziehung, für die der Katalog ein Wort hat; sie sind auf `part-of`
umgestellt. Ein Inversenpaar macht die Gegenkante weiterhin **nicht** zur Pflicht — Richtung
wird verfasst, nicht gespiegelt —, es legt nur fest, welches Label sie trägt, wenn jemand sie
schreibt.
**Stack- und Korpusänderung laufen hier in einem Zug**, entgegen der sonstigen Trennung. Der
neue `docs verify`-Check würde eine bestehende 4.0.x-Instanz beim bloßen Kopieren der neuen
Maschinerie fehlschlagen lassen, weil deren `kb/sources/COLLECTION.md` den `outbound:`-Block
noch trägt — nach [instructions/dev/version-parts.md](instructions/dev/version-parts.md)
Schritt 1 ein Grenzübertritt. Statt dafür eine `5.0.0` zu lösen, ist die Ursache mitbeseitigt:
die Collection-Contracts dieser Instanz sind angepasst, und `dist export` leitet die
`COLLECTION.md.template` daraus ab, also liefert jede neue Distribution die korrigierte Form
aus. Für eine bereits bestehende 4.0.x-Instanz bleibt eine Handbewegung übrig, und sie wird
hier benannt statt versteckt: die zwei `outbound:`-Zeilen aus `kb/sources/COLLECTION.md`
löschen. Das neue `related:` im `comparison`-Type-Spec erreicht sie ohnehin nicht — die vier
Page-Type-Specs gehören seit 4.0.0 der Instanz und werden nur als `.template` ausgeliefert.
Offen aus #40 bleibt nichts mehr; Befund 2 des Migrationslaufs (dem Katalog fehlt ein Register
für Urheberschaft) ist als eigenes Issue erfasst.
---
## 4.0.1 - 2026-09-02 - Issue-Board: vier Pflicht-Label-Familien und Body-als-Wahrheit
**Author:** Torben Nehmer
+1 -1
View File
@@ -1 +1 @@
4.0.1
4.1.0
+18 -2
View File
@@ -40,9 +40,18 @@ edge merely to mirror the first one.** The inbound view is rendered from the gra
`index rebuild` and `search`, so a reader landing on the target sees what points at it whether
or not anyone wrote a second edge.
That is why most labels below have no inverse. Only two pairs do, because in each the reverse
That is why most labels below have no inverse. Only three pairs do, because in each the reverse
direction is a genuine primary statement someone would write on its own: `depends-on` /
`required-by` and `runs-on` / `hosts`.
`required-by`, `runs-on` / `hosts`, and `composition` / `part-of`.
The third was added after the 4.0.0 migration, from measurement rather than from the desk. A
parent-child structure - a tier list and its tiers, a spectrum and its levels - produces the
question on nearly every page: the parent writes `composition`, and the child then reaches for
either `part-of` or `see-also`. The migration run answered `see-also`, on the reading that
`part-of` would be a mirror, and left sixteen edges saying "these two are related" about a
relationship the catalogue already had a word for. It is not a mirror: the parent's sentence
lists its parts, the child's names the whole it belongs to, and a reader landing on the child
needs the second one.
## When to run
@@ -129,6 +138,13 @@ Inference and comparison between ideas.
| `composition` | is composed of the target |
| `part-of` | is a component of the target |
`composition` / `part-of` is the third **inverse pair**, alongside `depends-on` / `required-by`
and `runs-on` / `hosts` in the operational register. Being a pair does not make the second edge
obligatory - direction is still authored - it settles *which label* the second edge takes when
someone does write it. The child of a `composition` writes `part-of`, not `see-also`: what it
is a component of is a primary statement about the child, and `see-also` says strictly less
about the same fact.
`grounds` / `rests-on` is a genuine pair and both directions are primary statements; they are
listed separately rather than as inverses because either page may legitimately carry only its
own side.
+12 -5
View File
@@ -36,8 +36,9 @@ 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.
has to stay readable while it is being converted - and it is why the two lint findings stay
advisory for as long as `kb_version` is below 4.0.0, which is exactly as long as this document
is outstanding.
## Steps
@@ -102,7 +103,7 @@ advisory until step 6 promotes them.
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:**
6. **Record it. The checks tighten themselves:**
```bash
tools/wikitool lint # unlabelled_edges and unauthorised_labels must be 0
@@ -110,8 +111,14 @@ advisory until step 6 promotes them.
```
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.
while `kb_version` is below 4.0.0 and hard from the moment `migrate done` records it -
nothing to flip by hand, and no window in which a half-converted corpus is refused by the
check that is measuring its progress.
Do not record the migration to silence the findings. The promotion is what makes the run
stick: after it, a bare title in `related:` is a hard error rather than a page still
waiting, so a corpus recorded early fails its next lint instead of quietly keeping the old
shape.
## How to tell a migrated page from an unmigrated one
+6 -3
View File
@@ -3,8 +3,10 @@ type: types/comparison.md
tags: [kernel, power-management, amd, cpu, driver]
created: 2026-07-31
entities: [amd-pstate, acpi-cpufreq]
summary: "Vergleich zweier AMD-CPU-Power-Management-Treiber: CPPC-basiertes amd-pstate gegen\xFC\
ber ACPI-basiertem acpi-cpufreq."
summary: "Vergleich zweier AMD-CPU-Power-Management-Treiber: CPPC-basiertes amd-pstate gegen\xFCber ACPI-basiertem acpi-cpufreq."
related:
- compares-with: amd-pstate
- compares-with: acpi-cpufreq
---
# Comparison: amd-pstate vs acpi-cpufreq
@@ -131,8 +133,9 @@ ls /sys/devices/system/cpu/cpu0/cpufreq/cppc_*
**amd-pstate** stellt einen bedeutenden Fortschritt in der CPU-Energieverwaltung für AMD-Prozessoren dar und bietet fein-körnige Steuerung, bessere Effizienz und verbessertes Batterielebensdauer. **acpi-cpufreq** bleibt ein zuverlässiger Fallback und dient weiterhin älterer Hardware. Die Wahl zwischen ihnen hängt hauptsächlich von Hardware-Unterstützung und Kernel-Version ab, wobei amd-pstate die klare Präferenz für moderne AMD-Systeme ist.
<!-- wikitool:links -->
## Beziehungen
- **compares-with:** [[amd-pstate]]
- **compares-with:** [[acpi-cpufreq]]
<!-- /wikitool:links -->
+2 -2
View File
@@ -5,7 +5,7 @@ tags: []
created: 2026-08-02
modified: 2026-08-29
related:
- see-also: Consolidation Tiers
- part-of: Consolidation Tiers
sources: []
confidence: 0.50
confidence_base: 0.50
@@ -41,5 +41,5 @@ TODO
<!-- wikitool:links -->
## Beziehungen
- **see-also:** [[Consolidation Tiers]]
- **part-of:** [[Consolidation Tiers]]
<!-- /wikitool:links -->
+2 -2
View File
@@ -5,7 +5,7 @@ tags: [memory, lifecycle, confidence, knowledge-management]
created: 2026-07-26
modified: 2026-08-29
related:
- see-also: LLM Wiki Pattern
- part-of: LLM Wiki Pattern
- see-also: Confidence Scoring
- composition: Supersession
- see-also: Consolidation Tiers
@@ -126,7 +126,7 @@ Basierend auf [[Agent Memory]]-Erfahrung:
<!-- wikitool:links -->
## Beziehungen
- **see-also:** [[LLM Wiki Pattern]]
- **part-of:** [[LLM Wiki Pattern]]
- **see-also:** [[Confidence Scoring]]
- **composition:** [[Supersession]]
- **see-also:** [[Consolidation Tiers]]
+2 -2
View File
@@ -6,7 +6,7 @@ created: 2026-08-02
modified: 2026-08-29
related:
- exemplifies: Implementation Spectrum
- see-also: Multi-Agent Collaboration
- part-of: Multi-Agent Collaboration
- evidenced-by: Source - LLM Wiki v2
sources: []
confidence: 0.50
@@ -46,6 +46,6 @@ TODO
## Beziehungen
- **exemplifies:** [[Implementation Spectrum]]
- **see-also:** [[Multi-Agent Collaboration]]
- **part-of:** [[Multi-Agent Collaboration]]
- **evidenced-by:** [[Source - LLM Wiki v2]]
<!-- /wikitool:links -->
+2 -2
View File
@@ -5,7 +5,7 @@ tags: []
created: 2026-08-02
modified: 2026-08-29
related:
- see-also: Consolidation Tiers
- part-of: Consolidation Tiers
sources: []
confidence: 0.50
confidence_base: 0.50
@@ -43,5 +43,5 @@ TODO
<!-- wikitool:links -->
## Beziehungen
- **see-also:** [[Consolidation Tiers]]
- **part-of:** [[Consolidation Tiers]]
<!-- /wikitool:links -->
+2 -2
View File
@@ -5,7 +5,7 @@ tags: []
created: 2026-08-02
modified: 2026-08-29
related:
- see-also: Hybrid Search
- part-of: Hybrid Search
- exemplifies: LLM Wiki Pattern
- evidenced-by: Source - LLM Wiki v2
sources: []
@@ -45,7 +45,7 @@ TODO
<!-- wikitool:links -->
## Beziehungen
- **see-also:** [[Hybrid Search]]
- **part-of:** [[Hybrid Search]]
- **exemplifies:** [[LLM Wiki Pattern]]
- **evidenced-by:** [[Source - LLM Wiki v2]]
<!-- /wikitool:links -->
+2 -2
View File
@@ -5,7 +5,7 @@ tags: []
created: 2026-08-02
modified: 2026-08-29
related:
- see-also: Consolidation Tiers
- part-of: Consolidation Tiers
sources: []
confidence: 0.50
confidence_base: 0.50
@@ -43,5 +43,5 @@ TODO
<!-- wikitool:links -->
## Beziehungen
- **see-also:** [[Consolidation Tiers]]
- **part-of:** [[Consolidation Tiers]]
<!-- /wikitool:links -->
+2 -2
View File
@@ -5,7 +5,7 @@ tags: []
created: 2026-08-02
modified: 2026-08-29
related:
- see-also: Multi-Agent Collaboration
- part-of: Multi-Agent Collaboration
sources: []
confidence: 0.50
confidence_base: 0.50
@@ -43,5 +43,5 @@ TODO
<!-- wikitool:links -->
## Beziehungen
- **see-also:** [[Multi-Agent Collaboration]]
- **part-of:** [[Multi-Agent Collaboration]]
<!-- /wikitool:links -->
+2 -2
View File
@@ -5,7 +5,7 @@ tags: [split, threshold, lines, pages]
created: 2026-08-03
modified: 2026-08-29
related:
- see-also: Content Quality Control
- part-of: Content Quality Control
- see-also: Stub Threshold
- see-also: Index Scaling
sources: [Source - LLM Improvements Sonnet Analysis]
@@ -69,7 +69,7 @@ Split Threshold definiert die maximale Größe, die eine Wiki-Seite erreichen so
<!-- wikitool:links -->
## Beziehungen
- **see-also:** [[Content Quality Control]]
- **part-of:** [[Content Quality Control]]
- **see-also:** [[Stub Threshold]]
- **see-also:** [[Index Scaling]]
<!-- /wikitool:links -->
+2 -2
View File
@@ -5,7 +5,7 @@ tags: [stub, minimum, quality, lines]
created: 2026-08-03
modified: 2026-08-29
related:
- see-also: Content Quality Control
- part-of: Content Quality Control
- see-also: Split Threshold
- see-also: Semantic Lint Automation
sources: [Source - LLM Improvements Sonnet Analysis, Source - LLM Wiki v2]
@@ -80,7 +80,7 @@ It provides comprehensive information about the topic. It clearly exceeds the st
<!-- wikitool:links -->
## Beziehungen
- **see-also:** [[Content Quality Control]]
- **part-of:** [[Content Quality Control]]
- **see-also:** [[Split Threshold]]
- **see-also:** [[Semantic Lint Automation]]
<!-- /wikitool:links -->
+2 -2
View File
@@ -5,7 +5,7 @@ tags: [versioning, knowledge, updates, lifecycle]
created: 2026-07-26
modified: 2026-08-29
related:
- see-also: Memory Lifecycle
- part-of: Memory Lifecycle
- rests-on: Confidence Scoring
- see-also: Knowledge Graph
- exemplifies: LLM Wiki Pattern
@@ -138,7 +138,7 @@ Wenn Aussage B Aussage A ersetzt:
<!-- wikitool:links -->
## Beziehungen
- **see-also:** [[Memory Lifecycle]]
- **part-of:** [[Memory Lifecycle]]
- **rests-on:** [[Confidence Scoring]]
- **see-also:** [[Knowledge Graph]]
- **exemplifies:** [[LLM Wiki Pattern]]
+2 -2
View File
@@ -6,7 +6,7 @@ created: 2026-08-02
modified: 2026-08-29
related:
- exemplifies: Implementation Spectrum
- see-also: Knowledge Graph
- part-of: Knowledge Graph
- evidenced-by: Source - LLM Wiki v2
sources: []
confidence: 0.50
@@ -46,6 +46,6 @@ TODO
## Beziehungen
- **exemplifies:** [[Implementation Spectrum]]
- **see-also:** [[Knowledge Graph]]
- **part-of:** [[Knowledge Graph]]
- **evidenced-by:** [[Source - LLM Wiki v2]]
<!-- /wikitool:links -->
+2 -2
View File
@@ -5,7 +5,7 @@ tags: []
created: 2026-08-02
modified: 2026-08-29
related:
- see-also: Hybrid Search
- part-of: Hybrid Search
- exemplifies: LLM Wiki Pattern
- evidenced-by: Source - LLM Wiki v2
sources: []
@@ -45,7 +45,7 @@ TODO
<!-- wikitool:links -->
## Beziehungen
- **see-also:** [[Hybrid Search]]
- **part-of:** [[Hybrid Search]]
- **exemplifies:** [[LLM Wiki Pattern]]
- **evidenced-by:** [[Source - LLM Wiki v2]]
<!-- /wikitool:links -->
+2 -2
View File
@@ -6,7 +6,7 @@ created: 2026-08-02
modified: 2026-08-29
related:
- exemplifies: Implementation Spectrum
- see-also: Multi-Agent Collaboration
- part-of: Multi-Agent Collaboration
sources: []
confidence: 0.50
confidence_base: 0.50
@@ -45,5 +45,5 @@ TODO
## Beziehungen
- **exemplifies:** [[Implementation Spectrum]]
- **see-also:** [[Multi-Agent Collaboration]]
- **part-of:** [[Multi-Agent Collaboration]]
<!-- /wikitool:links -->
+2 -2
View File
@@ -5,7 +5,7 @@ tags: []
created: 2026-08-02
modified: 2026-08-29
related:
- see-also: Consolidation Tiers
- part-of: Consolidation Tiers
sources: []
confidence: 0.50
confidence_base: 0.50
@@ -43,5 +43,5 @@ TODO
<!-- wikitool:links -->
## Beziehungen
- **see-also:** [[Consolidation Tiers]]
- **part-of:** [[Consolidation Tiers]]
<!-- /wikitool:links -->
+2 -2
View File
@@ -7,7 +7,7 @@ modified: 2026-08-29
related:
- depends-on: Wine
- see-also: Proton
- see-also: Wine GE
- part-of: Wine GE
- see-also: Arch Linux
sources: [Source - Wine]
confidence: 0.85
@@ -68,6 +68,6 @@ Wine-Staging-Patches enthalten typischerweise:
- **depends-on:** [[Wine]]
- **see-also:** [[Proton]]
- **see-also:** [[Wine GE]]
- **part-of:** [[Wine GE]]
- **see-also:** [[Arch Linux]]
<!-- /wikitool:links -->
+2 -2
View File
@@ -5,7 +5,7 @@ tags: [schema, taxonomy, external, farzaa-gist]
created: 2026-08-03
modified: 2026-08-29
related:
- see-also: farzaa gist
- part-of: farzaa gist
- see-also: AGENTS.md
- evidenced-by: Source - LLM Improvements Sonnet Analysis
sources: [Source - LLM Improvements Sonnet Analysis]
@@ -79,7 +79,7 @@ Dies sind handlungsfähige Empfehlungen, die in der Sonnet-Analyse als wertvoll
<!-- wikitool:links -->
## Beziehungen
- **see-also:** [[farzaa gist]]
- **part-of:** [[farzaa gist]]
- **see-also:** [[AGENTS.md]]
- **evidenced-by:** [[Source - LLM Improvements Sonnet Analysis]]
<!-- /wikitool:links -->
+1 -1
View File
@@ -18,7 +18,7 @@ The page tables live in a generated `INDEX.md` inside each collection, linked be
- **Concepts:** 80
- **Entities:** 72
- **Sources:** 28
- **Last Updated:** 2026-09-02
- **Last Updated:** 2026-09-03
---
+10
View File
@@ -127,3 +127,13 @@ Abschluss der Korpus-Migration auf die Link-Taxonomie (Gitea #40, Abschnitte 2 u
Gitea-Issue #41 als raw/notes/Gitea Issue 41 - Issue Management and Label Scheme 2026-09-02.md aufgenommen und als Source-Seite erfasst. kb/concepts/Issue Label Scheme.md auf den Stand vom 2026-09-02 gebracht: vier Pflicht-Achsen (area/kind/prio/size), zwei optionale status/-Flags, Body-als-Wahrheit-Konvention. Das abgeloeste Zweiachsen-Schema steht als Abschnitt Historie mit Diff-Tabelle in der Seite, nicht geloescht. confidence_base 0.70 -> 0.85 (zweite unabhaengige Quelle, Bestaetigung juenger als 30 Tage).
---
## [2026-09-03] update | Link-Taxonomie 4.1.0: 16 see-also-Kanten auf part-of, Comparison-Seite auf gelabelte Kanten
Befund 3 aus #40: composition/part-of ist jetzt das dritte Inversenpaar. Die 16 Gegenkanten eines composition, die im u3-Lauf auf see-also gesetzt wurden, sind per xref add auf part-of relabelt - betroffen sind Consolidation Tiers, Content Quality Control, Hybrid Search, Knowledge Graph, LLM Wiki Pattern, Memory Lifecycle, Multi-Agent Collaboration, Wine GE und farzaa gist samt ihrer Kinder.
Befund 1 aus #40: die einzige Comparison-Seite (amd-pstate vs acpi-cpufreq) trug ihre compares-with-Bullets als handgeschriebene Prosa ohne Frontmatter-Deckung, weil types/comparison.md kein related: fuehrte. Der Type-Spec hat es jetzt; die beiden Kanten stehen als deklarierte Kanten in einer wikitool:links-Region. kb/sources/COLLECTION.md hat seinen inerten outbound:-Block verloren.
migrate verify --from HEAD: 181 Seiten, 0 hinzugefuegt, 0 entfernt, 18 Befunde - alle Label-Wechsel im Frontmatter, keine Aenderung an Wikilink- oder Zitatzahlen. lint --fail-on-error gruen.
---
+9 -10
View File
@@ -1,7 +1,5 @@
---
profile: sources
outbound:
any: [is-evidence-for, defined-in, see-also]
required_by_stack: true
---
@@ -40,16 +38,17 @@ The `raw_files:`/`source_url:`/citation rules are shared and live in
- `tools/wikitool sources trace --raw <path>` answers "what did we learn from this?";
`tools/wikitool sources coverage` lists raw files no source page claims yet.
## Authorised labels
## No authorised labels
The `outbound:` block above is what `wikitool lint` and `xref add` check: which labels a page in
this collection may use, per destination. The catalogue they are drawn from - and what each one
asserts - is [instructions/link-taxonomy.md](../../instructions/link-taxonomy.md), which binds
nothing on its own.
This collection has **no `outbound:` block**, and that is the declaration rather than an
omission: the `source` type-spec offers no `related:` field, so a source page has nowhere to
put a labelled edge. Everything it would want to assert is already carried by `raw_files:`,
`entities:`, `concepts:` and `[^cite-id]` - the mechanical provenance path, not authored edges.
Deliberately narrow. A source page is evidence *about* a source; almost everything it would want to say is already carried by `raw_files:`, `sources:` and `[^cite-id]`, which are the mechanical provenance path rather than authored edges.
Adding a label here is a deliberate contract change, not a way around a refusal.
An `outbound:` block here would authorise labels that no page in this collection can write.
`wikitool docs verify` refuses that combination, so the two cannot drift apart: giving source
pages labelled edges means giving the type-spec a `related:` field first, which is a deliberate
contract change and not a way around a refusal.
## Outbound linking
+1 -1
View File
@@ -50,7 +50,7 @@ tools/wikitool <command> --help
| `index rebuild [--dry-run]` | Regenerate the catalog from every page's frontmatter: `kb/index.md` becomes a map (statistics, one row per collection and per area, links to the shards) and the page tables are written to a generated `INDEX.md` in each collection. An area past 50 rows gets its own shard. Stale shards from removed collections/areas are deleted in the same pass |
| `log append --op ingest\|query\|lint\|create\|update\|delete\|rename --title "..." [--body "..."\|--body-file path]` | Append a formatted entry to `kb/log.md` |
| `log status` | Read-only: count `ingest` entries logged since the last `lint` entry - the deterministic trigger behind the Maintenance Schedule's "every 10 sources" full-lint cadence |
| `lint [--json] [--markdown out.md] [--full] [--fail-on-error]` | Structural + provenance checks: broken wikilinks, dangling frontmatter references, orphan pages, index drift, schema gaps, duplicate titles, title mismatches, uncovered raw files, broken `raw_files:` refs, raw files claimed by more than one source page, unmarked provenance, citation/frontmatter drift, quote-limit overages (>2 blockquoted lines/page, advisory only). Prints only the sections that found something and always writes the full report to `reports/Lint Report <date>.md` (or `--markdown`), naming the path - `--full` prints everything, `--json` prints the findings and writes nothing |
| `lint [--json] [--markdown out.md] [--full] [--fail-on-error]` | Structural + provenance checks: broken wikilinks, dangling frontmatter references, orphan pages, index drift, schema gaps, duplicate titles, title mismatches, uncovered raw files, broken `raw_files:` refs, raw files claimed by more than one source page, unmarked provenance, citation/frontmatter drift, unbalanced generated-region markers, edges whose label is missing or not authorised by the source collection's `outbound:` (both hard once `kb_version` has reached the release that introduced labelled edges - advisory below it, so a corpus mid-migration is not refused by the check measuring it), quote-limit overages (>2 blockquoted lines/page, advisory only). Prints only the sections that found something and always writes the full report to `reports/Lint Report <date>.md` (or `--markdown`), naming the path - `--full` prints everything, `--json` prints the findings and writes nothing |
| `search ["<text>"] [--field <predicate> ...] [--kind/--subtype/--collection/--tag <v>] [--regex] [--limit N] [--sort [-]<field>] [--backend <name>] [--matches] [--json]` | Find pages in `kb/` without reading the index. Text search runs through a pluggable backend (`rg` today); `--field` predicates are evaluated on frontmatter - `f=v`, `f~substring`, `'f>=v'`, `'f:*'` (present), `'!f'` (absent), repeatable and ANDed. With no text this is a pure structured query. Results carry kind/summary/confidence so a hit can be judged without opening the page. A page whose frontmatter does not parse can match no positive predicate, so it is **named** rather than dropped: `--json` always carries an `unreadable` list of `{path, reason}` (usually empty), and the table form writes the same lines to stderr. `--regex` is applied by `rg` alone, whose engine is linear; the ranking boosts for title and summary are literal-containment only, so a non-literal pattern is ranked by match count. `rg` is killed after 30 s and reported as a failure. Read-only, and **exempt from the Iteration Budget Gate** |
| `confidence decay [--apply]` | Recompute every page's derived `confidence` as `confidence_base * (1 - 0.01/month)`, floored at 0.2; dry-run by default |
| `confidence init-base [--apply]` | One-time backfill: set `confidence_base` from the current `confidence` on pages that predate the derived-confidence model |
+4
View File
@@ -16,10 +16,12 @@ from chemenu import config
from chemenu.commands._util import rel_path, success
from chemenu.lint_core import (
HARD_ERROR_KEYS,
MIGRATION_GATED_KEYS,
MOST_LINKED_COUNT,
QUOTE_LIMIT,
count_quote_blocks,
default_report_path,
hard_error_keys,
has_hard_errors,
render_markdown,
render_summary,
@@ -30,10 +32,12 @@ from chemenu.lint_core import (
# so does every other name the tests and sibling commands already import.
__all__ = [
"HARD_ERROR_KEYS",
"MIGRATION_GATED_KEYS",
"MOST_LINKED_COUNT",
"QUOTE_LIMIT",
"count_quote_blocks",
"default_report_path",
"hard_error_keys",
"has_hard_errors",
"render_markdown",
"render_summary",
+42
View File
@@ -183,6 +183,33 @@ def authorised_labels(source: str, destination: str, kb_dir: Path | None = None)
return labels
LABELLED_EDGE_FIELD = "related"
def collections_that_can_carry_labelled_edges() -> set[str]:
"""Collection names whose offered page types actually have somewhere to put
a labelled edge.
Derived from `page_ref_fields:`, the same way `stack_required_collections()`
is derived from `base_dir:`: a type that does not offer `related:` cannot
carry a label, no matter what its collection's contract authorises.
"""
from chemenu.type_resolver import resolver
names: set[str] = set()
for type_path, _frontmatter in resolver.list_type_specs():
try:
if resolver.get_root(type_path) != "kb":
continue
base_dir = resolver.get_base_dir(type_path)
fields = resolver.get_page_ref_fields(type_path)
except (ValueError, OSError):
continue
if base_dir and LABELLED_EDGE_FIELD in fields:
names.add(str(base_dir).strip("/"))
return names
def declaration_issues(kb_dir: Path | None = None) -> list[str]:
"""What each `COLLECTION.md` fails to declare about itself.
@@ -200,6 +227,7 @@ def declaration_issues(kb_dir: Path | None = None) -> list[str]:
issues: list[str] = []
required = stack_required_collections()
can_label = collections_that_can_carry_labelled_edges()
present = {path.name for path in iter_kb_collections(root)}
for name in required:
if name not in present:
@@ -244,6 +272,20 @@ def declaration_issues(kb_dir: Path | None = None) -> list[str]:
)
+ f" - it must be {str(expected).lower()}"
)
# An `outbound:` block on a collection whose types offer no `related:`
# authorises labels that no page there can write. That is not a harmless
# extra: it reads as a licence, so the label gets written into the prose
# by hand instead - an identifier back in free text, which is the exact
# thing the labelled-edge model exists to end. The two halves have to
# move together, so the check names both directions of the fix.
if declared.get(OUTBOUND_FIELD) and collection.name not in can_label:
issues.append(
f"{relative}: `{OUTBOUND_FIELD}:` authorises labels, but no page type writing "
f"into kb/{collection.name}/ offers a `{LABELLED_EDGE_FIELD}:` field - so no "
f"page here can carry a labelled edge. Either drop the block, or give the "
f"type-spec a `{LABELLED_EDGE_FIELD}:` in its `page_ref_fields:` and schema"
)
return issues
+48 -8
View File
@@ -27,6 +27,7 @@ from chemenu.provenance import legacy_source_pages as find_legacy_source_pages
from chemenu.provenance import orphan_footnote_defs as find_orphan_footnote_defs
from chemenu.provenance import uncovered_raw_files as find_uncovered_raw_files
from chemenu.provenance import undefined_footnote_refs as find_undefined_footnote_refs
from chemenu.version import Version
from chemenu.kb_scan import (
GENERATED_INDEX,
WIKILINK_RE,
@@ -478,13 +479,6 @@ def default_report_path(report: dict) -> Path:
# through the index or navigation only. `quote_limit_violations` is advisory
# too - it flags a habit, not a broken tree.
#
# `unlabelled_edges` and `unauthorised_labels` are advisory **for now**, and
# that is a dated decision rather than a judgment about severity: they describe
# exactly the state a corpus is in between the 4.0.0 machinery landing and the
# migration reaching each page, which is the window `.wikitool-kb.json` exists
# to represent. They become hard errors once the migration is recorded - the
# same path `legacy_citation_markers` took.
#
# `malformed_edges` and `unbalanced_markers` are hard from the start: neither
# describes an unconverted page, only a broken one.
#
@@ -505,11 +499,57 @@ HARD_ERROR_KEYS = (
"dangling_frontmatter_refs",
"malformed_edges",
"unbalanced_markers",
"unlabelled_edges",
"unauthorised_labels",
"invalid_type_paths",
"type_resolution_errors",
"schema_validation_errors",
)
# Findings that only become hard once the corpus has reached a given shape.
#
# `unlabelled_edges` and `unauthorised_labels` describe exactly the state a
# corpus is in between the 4.0.0 machinery landing and the migration reaching
# each page - the window `.wikitool-kb.json` exists to represent. Failing on
# them during that window would refuse the very corpus that
# `instructions/migrations/4.0.0-link-taxonomy.md` tells an instance to publish
# unit by unit. So the promotion is tied to `kb_version` rather than to a
# release date: below 4.0.0 they are advisory, at or above it an unlabelled
# edge is no longer a page awaiting conversion but an edge whose author did not
# say what it asserts.
#
# Gated rather than simply promoted, which is where this departs from
# `legacy_citation_markers`: that one was flipped in a later version and any
# instance still owing the citation migration had to live with a red lint. The
# ledger can answer the question now, so it does.
MIGRATION_GATED_KEYS: dict[str, Version] = {
"unlabelled_edges": Version(4, 0, 0),
"unauthorised_labels": Version(4, 0, 0),
}
_ALWAYS_HARD = Version(0, 0, 0)
def hard_error_keys(kb_version: Version | None = None) -> tuple[str, ...]:
"""`HARD_ERROR_KEYS` minus the findings this corpus has not grown into yet.
`kb_version` defaults to what `.wikitool-kb.json` records. A tree without
one - a fresh instance, which starts at the current shape rather than
migrating into it - keeps every key: there is no outstanding migration for
a gated finding to be the noise of.
"""
from chemenu import kb_state
if kb_version is None:
kb_version = kb_state.read_kb_version()
if kb_version is None:
return HARD_ERROR_KEYS
return tuple(
key
for key in HARD_ERROR_KEYS
if kb_version >= MIGRATION_GATED_KEYS.get(key, _ALWAYS_HARD)
)
def has_hard_errors(report: dict) -> bool:
return any(report.get(key) for key in HARD_ERROR_KEYS)
return any(report.get(key) for key in hard_error_keys())
+26
View File
@@ -192,3 +192,29 @@ def test_an_undeclared_destination_authorises_nothing(kb_root):
missing declaration to be filled in with a permissive default."""
_authorising(kb_root, "entities", " concepts: [implements]")
assert kb_collections.authorised_labels("entities", "sources") == set()
def test_outbound_on_a_collection_that_cannot_carry_labels_is_a_finding(kb_root):
"""`kb/sources/` is the live case: the `source` type-spec offers no
`related:`, so an `outbound:` block there authorises labels no page can
write. Left unchecked it reads as a licence and the label gets written into
the prose by hand instead - an identifier back in free text, which is what
labelled edges exist to end."""
_authorising(kb_root, "sources", " any: [is-evidence-for]", required=True)
issues = kb_collections.declaration_issues(kb_root)
assert any(
"kb/sources/COLLECTION.md" in issue and "no page type writing into kb/sources/" in issue
for issue in issues
)
def test_outbound_is_fine_on_a_collection_whose_type_offers_related(kb_root):
_collection(kb_root, "sources", profile="sources", required=True)
_authorising(kb_root, "entities", " any: [uses]")
assert kb_collections.declaration_issues(kb_root) == []
def test_a_collection_without_outbound_is_not_a_finding(kb_root):
"""Absence is the declaration `kb/sources/` makes: no authored edges here."""
_collection(kb_root, "sources", profile="sources", required=True)
assert kb_collections.declaration_issues(kb_root) == []
+87 -1
View File
@@ -1,8 +1,12 @@
import json
from datetime import date
from chemenu import config
import pytest
from chemenu import config, kb_state
from chemenu.commands.lint import (
HARD_ERROR_KEYS,
hard_error_keys,
has_hard_errors,
lint_command,
render_markdown,
@@ -11,6 +15,8 @@ from chemenu.commands.lint import (
)
from chemenu.frontmatter_io import write_page
from chemenu.provenance import cite_id, render_cite_block
from chemenu.tests.conftest import use_shipped_type_specs
from chemenu.version import Version
def test_lint_detects_unparsable_frontmatter(kb_dir):
@@ -472,3 +478,83 @@ def test_lint_does_not_count_a_shell_prompt_as_a_quote(kb_dir):
)
report = run_lint(kb_dir)
assert [i for i in report["quote_limit_violations"] if i["page"] == "shelly"] == []
# --- the migration gate on `unlabelled_edges` / `unauthorised_labels` -------
#
# These four repoint `config.ROOT` at the fixture tree, which the `kb_dir`
# fixture alone does not do. Two things need it: `write_kb_state()` writes
# `.wikitool-kb.json` relative to `ROOT`, and `lint`'s collection lookup
# resolves a page against `config.KB_DIR` rather than against the directory it
# was handed - so without the repoint the gate would be read off the real
# repository's state file.
@pytest.fixture
def rooted_kb(kb_dir, tmp_path, monkeypatch):
monkeypatch.setattr(config, "ROOT", tmp_path)
use_shipped_type_specs(monkeypatch)
return kb_dir
def _page_with_an_unlabelled_edge(rooted_kb):
"""A `related:` entry that is a bare title rather than a `label: title`
mapping - the shape every page was in before the 4.0.0 migration."""
write_page(
rooted_kb / "entities/tools/bare-edge.md",
{"type": "types/entity.md", "entity_type": "tool", "tags": [], "created": "2026-09-03",
"modified": "2026-09-03", "related": ["Modbus"], "sources": [], "confidence": 0.8,
"provenance": "general", "summary": "One edge whose label was never declared."},
"\n# bare-edge\n\nAn edge without a label.\n",
)
def test_unlabelled_edge_is_advisory_below_kb_version_4(rooted_kb):
"""The window the migration document describes: the machinery has landed,
the corpus has not been converted yet, and `lint --fail-on-error` must not
refuse the very tree the migration tells the instance to publish unit by
unit."""
_page_with_an_unlabelled_edge(rooted_kb)
kb_state.write_kb_state(Version(3, 0, 0), [])
report = run_lint(rooted_kb)
assert report["unlabelled_edges"] != []
assert "unlabelled_edges" not in hard_error_keys()
# Narrowed to the finding under test: the fixture corpus carries unrelated
# hard errors of its own, so asserting on the whole report would prove
# nothing about the gate.
assert has_hard_errors({"unlabelled_edges": report["unlabelled_edges"]}) is False
def test_unlabelled_edge_is_hard_at_kb_version_4(rooted_kb):
"""Once the migration is recorded, a bare title is no longer a page waiting
its turn - it is an edge whose author did not say what it asserts."""
_page_with_an_unlabelled_edge(rooted_kb)
kb_state.write_kb_state(Version(4, 0, 0), [])
report = run_lint(rooted_kb)
assert report["unlabelled_edges"] != []
assert "unlabelled_edges" in hard_error_keys()
assert has_hard_errors({"unlabelled_edges": report["unlabelled_edges"]}) is True
def test_unauthorised_label_is_hard_at_kb_version_4(rooted_kb):
"""The fixture contracts authorise `depends-on` but not `contradicts`."""
write_page(
rooted_kb / "entities/tools/off-menu.md",
{"type": "types/entity.md", "entity_type": "tool", "tags": [], "created": "2026-09-03",
"modified": "2026-09-03", "related": [{"contradicts": "Modbus"}], "sources": [],
"confidence": 0.8, "provenance": "general", "summary": "A label off this menu."},
"\n# off-menu\n\nA label the source collection never authorised.\n",
)
kb_state.write_kb_state(Version(4, 0, 0), [])
report = run_lint(rooted_kb)
assert report["unauthorised_labels"] != []
assert "unauthorised_labels" in hard_error_keys()
assert has_hard_errors({"unauthorised_labels": report["unauthorised_labels"]}) is True
def test_a_tree_that_never_declared_a_kb_version_keeps_every_key(rooted_kb):
"""No `.wikitool-kb.json` means a fresh instance, which starts at the
current shape rather than migrating into it - so there is no outstanding
migration for a gated finding to be the noise of."""
assert kb_state.read_kb_version() is None
assert hard_error_keys() == HARD_ERROR_KEYS
+1 -1
View File
@@ -45,7 +45,7 @@ def test_get_page_ref_fields_reads_the_type_spec():
assert resolver.get_page_ref_fields("types/entity.md") == ["related", "sources"]
assert resolver.get_page_ref_fields("types/concept.md") == ["related", "sources"]
assert resolver.get_page_ref_fields("types/source.md") == ["entities", "concepts"]
assert resolver.get_page_ref_fields("types/comparison.md") == ["entities"]
assert resolver.get_page_ref_fields("types/comparison.md") == ["entities", "related"]
def test_page_ref_fields_exist_in_the_type_schema():
+2 -1
View File
@@ -4,7 +4,7 @@ name: comparison
description: Strukturierter Typ für Vergleichsseiten, die mehrere Entities oder Ansätze gegenüberstellen
schema: types/comparison.schema.yaml
base_dir: comparisons
page_ref_fields: [entities]
page_ref_fields: [entities, related]
---
# Comparison
@@ -32,6 +32,7 @@ page_ref_fields: [entities]
| `tags` | Nein | Navigations-Tags zur Kategorisierung |
| `created` | Ja | Erstellungsdatum (YYYY-MM-DD) |
| `entities` | Ja | Titel der verglichenen Entities |
| `related` | Nein | Deklarierte ausgehende Kanten - je Subjekt eine `compares-with`-Kante, geschrieben von `wikitool xref add` |
| `summary` | Ja | Einzeiler für `kb/index.md` |
## Autorenanweisungen
+17
View File
@@ -21,6 +21,23 @@ properties:
type: string
description: Entity titles being compared
minItems: 2
related:
type: array
items:
oneOf:
- type: string
- type: object
minProperties: 1
maxProperties: 1
additionalProperties:
type: string
description: >-
Declared outbound edges, in the same shape entity and concept pages use.
A comparison's own assertion is `compares-with` against each subject: the
titles are already in `entities:`, but that field is the untyped
provenance-style list, so without this one the edge the page exists to
make would live only in hand-written prose - which is the thing
instructions/link-taxonomy.md was built to end.
summary:
type: string
description: 1-line summary for index.md