diff --git a/CHANGES.md b/CHANGES.md index efa1a20..666fc4b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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 diff --git a/VERSION b/VERSION index 1454f6e..ee74734 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.0.1 +4.1.0 diff --git a/instructions/link-taxonomy.md b/instructions/link-taxonomy.md index 6bd7308..87e58a5 100644 --- a/instructions/link-taxonomy.md +++ b/instructions/link-taxonomy.md @@ -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. diff --git a/instructions/migrations/4.0.0-link-taxonomy.md b/instructions/migrations/4.0.0-link-taxonomy.md index 0b3ba42..bf723b1 100644 --- a/instructions/migrations/4.0.0-link-taxonomy.md +++ b/instructions/migrations/4.0.0-link-taxonomy.md @@ -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 diff --git a/kb/comparisons/amd-pstate vs acpi-cpufreq.md b/kb/comparisons/amd-pstate vs acpi-cpufreq.md index 4fb6008..473f38b 100644 --- a/kb/comparisons/amd-pstate vs acpi-cpufreq.md +++ b/kb/comparisons/amd-pstate vs acpi-cpufreq.md @@ -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. + ## Beziehungen - **compares-with:** [[amd-pstate]] - **compares-with:** [[acpi-cpufreq]] - + diff --git a/kb/concepts/Episodic Memory.md b/kb/concepts/Episodic Memory.md index d7f0159..087304c 100644 --- a/kb/concepts/Episodic Memory.md +++ b/kb/concepts/Episodic Memory.md @@ -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 ## Beziehungen -- **see-also:** [[Consolidation Tiers]] +- **part-of:** [[Consolidation Tiers]] diff --git a/kb/concepts/Memory Lifecycle.md b/kb/concepts/Memory Lifecycle.md index 31223f5..85a2822 100644 --- a/kb/concepts/Memory Lifecycle.md +++ b/kb/concepts/Memory Lifecycle.md @@ -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: ## Beziehungen -- **see-also:** [[LLM Wiki Pattern]] +- **part-of:** [[LLM Wiki Pattern]] - **see-also:** [[Confidence Scoring]] - **composition:** [[Supersession]] - **see-also:** [[Consolidation Tiers]] diff --git a/kb/concepts/Mesh Sync.md b/kb/concepts/Mesh Sync.md index 9a5bf7b..19f66da 100644 --- a/kb/concepts/Mesh Sync.md +++ b/kb/concepts/Mesh Sync.md @@ -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]] diff --git a/kb/concepts/Procedural Memory.md b/kb/concepts/Procedural Memory.md index f68e279..7cd55e5 100644 --- a/kb/concepts/Procedural Memory.md +++ b/kb/concepts/Procedural Memory.md @@ -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 ## Beziehungen -- **see-also:** [[Consolidation Tiers]] +- **part-of:** [[Consolidation Tiers]] diff --git a/kb/concepts/Reciprocal Rank Fusion.md b/kb/concepts/Reciprocal Rank Fusion.md index 347ca8c..3fd4d80 100644 --- a/kb/concepts/Reciprocal Rank Fusion.md +++ b/kb/concepts/Reciprocal Rank Fusion.md @@ -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 ## Beziehungen -- **see-also:** [[Hybrid Search]] +- **part-of:** [[Hybrid Search]] - **exemplifies:** [[LLM Wiki Pattern]] - **evidenced-by:** [[Source - LLM Wiki v2]] diff --git a/kb/concepts/Semantic Memory.md b/kb/concepts/Semantic Memory.md index 38c9a3e..90582e4 100644 --- a/kb/concepts/Semantic Memory.md +++ b/kb/concepts/Semantic Memory.md @@ -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 ## Beziehungen -- **see-also:** [[Consolidation Tiers]] +- **part-of:** [[Consolidation Tiers]] diff --git a/kb/concepts/Shared vs Private.md b/kb/concepts/Shared vs Private.md index 8d99946..4517f58 100644 --- a/kb/concepts/Shared vs Private.md +++ b/kb/concepts/Shared vs Private.md @@ -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 ## Beziehungen -- **see-also:** [[Multi-Agent Collaboration]] +- **part-of:** [[Multi-Agent Collaboration]] diff --git a/kb/concepts/Split Threshold.md b/kb/concepts/Split Threshold.md index 6a45e8d..5f6bc35 100644 --- a/kb/concepts/Split Threshold.md +++ b/kb/concepts/Split Threshold.md @@ -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 ## Beziehungen -- **see-also:** [[Content Quality Control]] +- **part-of:** [[Content Quality Control]] - **see-also:** [[Stub Threshold]] - **see-also:** [[Index Scaling]] diff --git a/kb/concepts/Stub Threshold.md b/kb/concepts/Stub Threshold.md index 061ac68..e4e0a94 100644 --- a/kb/concepts/Stub Threshold.md +++ b/kb/concepts/Stub Threshold.md @@ -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 ## Beziehungen -- **see-also:** [[Content Quality Control]] +- **part-of:** [[Content Quality Control]] - **see-also:** [[Split Threshold]] - **see-also:** [[Semantic Lint Automation]] diff --git a/kb/concepts/Supersession.md b/kb/concepts/Supersession.md index 6e6d413..ca71ebf 100644 --- a/kb/concepts/Supersession.md +++ b/kb/concepts/Supersession.md @@ -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: ## Beziehungen -- **see-also:** [[Memory Lifecycle]] +- **part-of:** [[Memory Lifecycle]] - **rests-on:** [[Confidence Scoring]] - **see-also:** [[Knowledge Graph]] - **exemplifies:** [[LLM Wiki Pattern]] diff --git a/kb/concepts/Typed Relationships.md b/kb/concepts/Typed Relationships.md index 705638e..11ad183 100644 --- a/kb/concepts/Typed Relationships.md +++ b/kb/concepts/Typed Relationships.md @@ -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]] diff --git a/kb/concepts/Vector Search.md b/kb/concepts/Vector Search.md index ab41219..572b014 100644 --- a/kb/concepts/Vector Search.md +++ b/kb/concepts/Vector Search.md @@ -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 ## Beziehungen -- **see-also:** [[Hybrid Search]] +- **part-of:** [[Hybrid Search]] - **exemplifies:** [[LLM Wiki Pattern]] - **evidenced-by:** [[Source - LLM Wiki v2]] diff --git a/kb/concepts/Work Coordination.md b/kb/concepts/Work Coordination.md index 536fe46..97e86b7 100644 --- a/kb/concepts/Work Coordination.md +++ b/kb/concepts/Work Coordination.md @@ -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]] diff --git a/kb/concepts/Working Memory.md b/kb/concepts/Working Memory.md index ff38a33..0593b04 100644 --- a/kb/concepts/Working Memory.md +++ b/kb/concepts/Working Memory.md @@ -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 ## Beziehungen -- **see-also:** [[Consolidation Tiers]] +- **part-of:** [[Consolidation Tiers]] diff --git a/kb/entities/technologies/Wine-Staging.md b/kb/entities/technologies/Wine-Staging.md index 8ec25de..9cbe859 100644 --- a/kb/entities/technologies/Wine-Staging.md +++ b/kb/entities/technologies/Wine-Staging.md @@ -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]] diff --git a/kb/entities/tools/pascalandy schema.md b/kb/entities/tools/pascalandy schema.md index d70ed50..c4c17a3 100644 --- a/kb/entities/tools/pascalandy schema.md +++ b/kb/entities/tools/pascalandy schema.md @@ -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 ## Beziehungen -- **see-also:** [[farzaa gist]] +- **part-of:** [[farzaa gist]] - **see-also:** [[AGENTS.md]] - **evidenced-by:** [[Source - LLM Improvements Sonnet Analysis]] diff --git a/kb/index.md b/kb/index.md index 3dd7247..1bda4ad 100644 --- a/kb/index.md +++ b/kb/index.md @@ -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 --- diff --git a/kb/log.md b/kb/log.md index 7a196c1..f6a8515 100644 --- a/kb/log.md +++ b/kb/log.md @@ -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. + +--- diff --git a/kb/sources/COLLECTION.md b/kb/sources/COLLECTION.md index 91d8595..4c74e80 100644 --- a/kb/sources/COLLECTION.md +++ b/kb/sources/COLLECTION.md @@ -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 ` 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 diff --git a/tools/CONTRACT.md b/tools/CONTRACT.md index 03cd9b4..28c55d8 100644 --- a/tools/CONTRACT.md +++ b/tools/CONTRACT.md @@ -50,7 +50,7 @@ tools/wikitool --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 .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 .md` (or `--markdown`), naming the path - `--full` prints everything, `--json` prints the findings and writes nothing | | `search [""] [--field ...] [--kind/--subtype/--collection/--tag ] [--regex] [--limit N] [--sort [-]] [--backend ] [--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 | diff --git a/tools/chemenu/commands/lint.py b/tools/chemenu/commands/lint.py index 570a8b8..0c02070 100644 --- a/tools/chemenu/commands/lint.py +++ b/tools/chemenu/commands/lint.py @@ -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", diff --git a/tools/chemenu/kb_collections.py b/tools/chemenu/kb_collections.py index 0c3dce8..5330646 100644 --- a/tools/chemenu/kb_collections.py +++ b/tools/chemenu/kb_collections.py @@ -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 diff --git a/tools/chemenu/lint_core.py b/tools/chemenu/lint_core.py index 262a601..ad3a89b 100644 --- a/tools/chemenu/lint_core.py +++ b/tools/chemenu/lint_core.py @@ -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()) diff --git a/tools/chemenu/tests/test_conventions.py b/tools/chemenu/tests/test_conventions.py index a08b970..10e9d13 100644 --- a/tools/chemenu/tests/test_conventions.py +++ b/tools/chemenu/tests/test_conventions.py @@ -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) == [] diff --git a/tools/chemenu/tests/test_lint.py b/tools/chemenu/tests/test_lint.py index 0afd00f..61032ad 100644 --- a/tools/chemenu/tests/test_lint.py +++ b/tools/chemenu/tests/test_lint.py @@ -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 diff --git a/tools/chemenu/tests/test_type_resolver.py b/tools/chemenu/tests/test_type_resolver.py index 51571c5..d15326c 100644 --- a/tools/chemenu/tests/test_type_resolver.py +++ b/tools/chemenu/tests/test_type_resolver.py @@ -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(): diff --git a/types/comparison.md b/types/comparison.md index 4edb9d1..8448244 100644 --- a/types/comparison.md +++ b/types/comparison.md @@ -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 diff --git a/types/comparison.schema.yaml b/types/comparison.schema.yaml index 9f67bd3..b18d6fb 100644 --- a/types/comparison.schema.yaml +++ b/types/comparison.schema.yaml @@ -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