Compare commits

..

2 Commits

Author SHA1 Message Date
torben 24593c5608 Doku-Hälfte zu 4.7.0: redundant_see_also in tools/CONTRACT.md und wiki-lint, xref-remove-Falle benannt (4.7.1, #49)
CI / verify (push) Successful in 52s
Release / release (push) Successful in 35s
Files changed:
- CHANGES.md
- VERSION
- instructions/wiki-lint/SKILL.md
- tools/CONTRACT.md
2026-09-04 18:46:55 +02:00
torben 3916cb9541 Link-Katalog: authored/alternative-to/addresses, entity→entity-Lineage, Lint-Befund redundant_see_also (4.7.0, #43 #49)
CI / verify (push) Successful in 1m2s
Release / release (push) Successful in 37s
Files changed:
- CHANGES.md
- VERSION
- instructions/link-taxonomy.md
- kb/concepts/COLLECTION.md
- kb/entities/COLLECTION.md
- kb/entities/people/Andrej Karpathy.md
- kb/entities/people/Vannevar Bush.md
- tools/chemenu/evals/scorecard.py
- tools/chemenu/links.py
- tools/chemenu/lint_core.py
- tools/chemenu/tests/test_lint.py
2026-09-04 18:44:22 +02:00
13 changed files with 323 additions and 19 deletions
+106
View File
@@ -35,6 +35,112 @@ dev-checkout concern - readable here, never shipped as something to parse.
--- ---
## 4.7.1 - 2026-09-04 - redundant_see_also in tools/CONTRACT.md und wiki-lint dokumentiert; xref-remove-Falle benannt
**Author:** Torben Nehmer
<!-- wikitool:bumps -->
- redundant_see_also in tools/CONTRACT.md und wiki-lint; xref-remove-Falle beim Aufräumen benannt
<!-- /wikitool:bumps -->
Die Doku-Hälfte von `4.7.0`, beim Abschluss nachgezogen. Der Befund war ausgeliefert, aber
`tools/CONTRACT.md`s `lint`-Zeile zählt die Befunde auf und kannte ihn nicht - eine Instanz hätte
eine Sektion im Report gefunden, die ihr Contract nicht erklärt. `docs verify` prüft, dass die
Kommandotabelle existiert, nie was in einer Zeile steht; genau die Lücke, für die AGENTS.md
„a stack change is not finished until the human docs describe it" geschrieben ist.
**Die eigentliche Änderung ist aber die Warnung in `wiki-lint`**, und sie ist keine Prosa-Politur.
Der neue Befund liest sich wie etwas, das Schritt 7 („repariere, was mechanisch ist") abräumt, und
der naheliegende Griff wäre `xref remove` - das die Referenz **beidseitig** löscht. Angewandt auf
`Wine see-also Wine GE` neben `Wine GE depends-on Wine` hätte das die schwache *und* die
spezifische Kante entfernt, und das Paar sagte danach gar nichts mehr. Ein Befund, dessen
offensichtliche Reparatur Daten zerstört, ist schlechter als kein Befund: Schritt 1 nennt die
Falle jetzt beim Namen und verweist auf `xref add` (fasst nur die Quellseite an) oder aufs
Berichten. Dieselbe Asymmetrie hat in #30 schon einmal Daten gekostet.
---
## 4.7.0 - 2026-09-04 - Link-Katalog: authored, alternative-to, addresses; entity→entity-Lineage; Lint-Befund gegen redundante see-also
**Author:** Torben Nehmer
<!-- wikitool:bumps -->
- Link-Katalog: authored, alternative-to, addresses; Lint-Befund gegen redundante see-also
<!-- /wikitool:bumps -->
Drei neue Label, zwei geschlossene Autorisierungslücken und ein Lint-Befund - ausgelöst von der
anstehenden ersten produktiven Instanz. Katalog und `COLLECTION.md` gehen über `dist export` in
jede neue Instanz; was hier fehlt, fehlt dort ab Tag eins, und nachträglich ist eine
Katalogerweiterung eine Korpus-Migration statt einer Datenzeile. Gitea #43 und #49.
**Die Messung, die den Zuschnitt bestimmt hat.** Die 180 `see-also`-Kanten des Korpus zerfallen
exakt in drei Klassen: 57 (32 %) sind Spiegel einer bereits typisierten Gegenkante, 70 (39 %)
sind wechselseitige `see-also`-Paare, 53 (29 %) stehen einseitig. Die erste Klasse ist kein
Vokabularproblem - `Wine see-also Wine GE` steht neben `Wine GE depends-on Wine`, `RAG see-also
NotebookLM` neben `NotebookLM implements RAG`. Der Katalog war für ein Drittel der Fälle längst
ausreichend; es hat sie nur nichts gemeldet. Genau dafür ist der Lint-Befund unten da, und er ist
der Grund, warum diese Version mehr ist als zwei Katalogzeilen.
**`authored`** (operationales Register). „hat das Ziel als einmaligen Akt geschaffen." Der Katalog
kannte fortlaufende Rechenschaft (`owns`) und fortlaufende Arbeit (`maintains`), aber nicht den
historischen Ursprung - Urheberschaft stand im Korpus deshalb in vier unvereinbaren Formen
nebeneinander: `source.author` als Freitext, `owns`, `see-also` und ein Prosa-Bullet. Eine davon
war sachlich falsch: `Vannevar Bush owns Memex` behauptet laufende Rechenschaft für einen 1974
Verstorbenen, und eine falsche maschinenlesbare Kante ist schlechter als eine schwache, weil sie
geglaubt wird. Geschrieben wird das Label auf der Entity-Seite (`Andrej Karpathy authored LLM
Wiki Pattern`) - die Gegenrichtung `authored-by` auf der Concept-Seite hätte `xref remove`
gebraucht, das beidseitig abräumt, statt `xref add`, das relabelt. Vier Kanten im Korpus
umgestellt, keine verloren.
**`alternative-to`** (operationales Register, selbst-dual). „erfüllt denselben Zweck wie das Ziel,
so dass ein Leser, der zwischen beiden wählt, beide will." Belegt durch rund 30 Paare, darunter
die sieben Agent-CLIs, die untereinander *ausschließlich* `see-also` tragen - keine einzige
typisierte Kante. Abgegrenzt gegen `contrasts` (behauptet einen lesenswerten Unterschied) und
`compares-with` (wiegt auf benannten Dimensionen ab und führt in dieser Instanz auf eine
`kb/comparisons/`-Seite): zwei Agent-CLIs sind austauschbar, zwei gegensätzliche Entwurfsprinzipien
sind es nicht.
Der Katalog sagt jetzt ausdrücklich, dass ein selbst-duales Label **einmal pro Paar** geschrieben
wird. Ohne diesen Satz wäre aus einer 22-Kanten-`see-also`-Clique eine 22-Kanten-
`alternative-to`-Clique geworden und nichts gewonnen: sieben austauschbare Werkzeuge sind 21
Paare, beidseitig deklariert 42 Kanten, von denen die zweiten 21 nichts sagen.
**`addresses`** (konzeptionelles Register). „ist eine Antwort auf das Problem, das das Ziel
beschreibt." `types/concept.md` deklariert `problem` und `decision` als eigene Subtypen, und der
Katalog hatte kein Label, das eine Entscheidung mit dem Problem verbindet, das sie löst - eine
Collection konnte ein Problem benennen und nie sagen, was dagegen unternommen wurde. Abgegrenzt
gegen `rests-on`, das das Ziel als *Prämisse* nimmt statt als zu lösendes Problem. Im Korpus nur
vier belegte Paare, also dünn nach dem sonst geltenden „erst der Anwendungsfall"-Maßstab; die
Ausnahme ist bewusst und gilt dem Auslieferungszeitpunkt, der die Kosten umdreht.
**Zwei Autorisierungslücken entity→entity.** `kb/entities/COLLECTION.md` erlaubte bisher keine
Lineage zwischen zwei Entities - ein Fork, eine Neuimplementierung, ein Nachbau war nicht
ausdrückbar; `derived-from` und `adapted-from` sind jetzt freigegeben. Ebenso `implements`, für
eine Entity, die eine als Entity geführte Konvention umsetzt. Ein autorisiertes Label ohne
Live-Nutzung ist ausdrücklich in Ordnung (`instructions/dev/corpus-policy.md`).
**Lint-Befund `redundant_see_also`.** Meldet eine `see-also`-Kante, deren Gegenrichtung bereits
ein typisiertes Label trägt. Gegen den Korpus dieser Instanz meldet er genau die gemessenen 57.
**Advisory, nicht hart**, aus zwei Gründen zugleich: eine schwache Kante neben einer spezifischen
ist redundant, nicht kaputt - und der Befund kommt lange nach den Korpora, die er beurteilt, also
würde eine harte Einstufung jede bestehende Instanz mit dem Upgrade rot schalten, das ihn
ausliefert. Anders als `unlabelled_edges` ist er auch nicht migrations-gegatet: es gibt keine
Version, ab der die Redundanz zum Fehler wird, nur einen Sweep, zu dem jemand kommt oder nicht.
`links.SEE_ALSO` ist damit das einzige Katalog-Label, das das Werkzeug beim Namen kennt. Das ist
eine begründete Ausnahme, keine Aufweichung: `see-also` ist der erklärte letzte Ausweg des
Katalogs und behauptet nur, dass nichts Besseres passte - was der einzige Grund ist, warum `lint`
eine Kante als *schwächer als* eine andere über demselben Paar beurteilen kann. Alles andere am
Vokabular bleibt in `instructions/link-taxonomy.md` und den `outbound:`-Blöcken.
**Nicht dabei, bewusst.** Der Sweep der 180 bestehenden Kanten (#48) - diese Version ändert außer
den vier Urheberschaftskanten keine Korpus-Kante. Verworfen wurden außerdem `variant-of` (die
Wine-Forks tragen bereits `depends-on Wine`), `implemented-by` (Spiegel von `implements`, den die
Inbound-View rendert), `sibling-of` für die Concept-Cliquen (Über-Verlinkung, kein fehlendes Wort)
und `builds-on` (Vokabularkollision mit `extends`/`derived-from`/`adapted-from`).
---
## 4.6.1 - 2026-09-04 - DEVELOPMENT.md im Kommandotabellen-Check, selbstbeschriftete Release-Notes, Prosa-Korrekturen (#47 Block 3) ## 4.6.1 - 2026-09-04 - DEVELOPMENT.md im Kommandotabellen-Check, selbstbeschriftete Release-Notes, Prosa-Korrekturen (#47 Block 3)
**Author:** Torben Nehmer **Author:** Torben Nehmer
+1 -1
View File
@@ -1 +1 @@
4.6.1 4.7.1
+33 -2
View File
@@ -44,6 +44,16 @@ That is why most labels below have no inverse. Only three pairs do, because in e
direction is a genuine primary statement someone would write on its own: `depends-on` / direction is a genuine primary statement someone would write on its own: `depends-on` /
`required-by`, `runs-on` / `hosts`, and `composition` / `part-of`. `required-by`, `runs-on` / `hosts`, and `composition` / `part-of`.
**A self-dual label is still written once.** `alternative-to` is its own inverse - the sentence
reads identically from either end - and that makes it the easiest label in the catalogue to
write twice by reflex. Symmetry means the relation holds in both directions, not that both pages
must declare it: one edge per pair, and the other page's inbound view carries it. The difference
is not cosmetic at scale. Seven mutually substitutable tools are 21 pairs; declared once each
that is 21 edges, declared from both ends it is 42, and the second 21 say nothing the first did
not. This is the shape a `see-also` clique already had in this corpus before the labels existed,
and relabelling such a clique without dropping to one edge per pair moves the problem rather
than fixing it.
The third was added after the 4.0.0 migration, from measurement rather than from the desk. A 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 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 question on nearly every page: the parent writes `composition`, and the child then reaches for
@@ -100,10 +110,23 @@ entity to entity.
| `consumes` | — | reads the target as an artifact or data | | `consumes` | — | reads the target as an artifact or data |
| `maintains` | — | carries the upkeep of the target | | `maintains` | — | carries the upkeep of the target |
| `owns` | — | is accountable for the target's existence and decisions | | `owns` | — | is accountable for the target's existence and decisions |
| `authored` | — | created the target as a one-time act |
| `alternative-to` | itself | serves the same purpose as the target, so a reader choosing between them wants both |
`uses` versus `depends-on` is the distinction worth keeping sharp: if removing the target breaks `uses` versus `depends-on` is the distinction worth keeping sharp: if removing the target breaks
this thing, it is `depends-on`. `owns` versus `maintains`: accountability versus labour, and this thing, it is `depends-on`.
they are often different people.
`authored`, `owns` and `maintains` are three different sentences about the same pair, and often
three different people: origination, accountability, labour. `owns` is a *standing* claim - it
says someone answers for this thing now - so it reads false about a person who is dead or long
gone from the project, however plainly they made it. That is the case `authored` exists for, and
picking `owns` for it is not a weaker edge but a wrong one.
`alternative-to` versus `contrasts` versus `compares-with`: `contrasts` asserts a *difference
worth reading both for*, `alternative-to` asserts *substitutability* - two things a reader might
pick between for the same job. `compares-with` weighs them on named dimensions, which in this
instance is what routes to a `kb/comparisons/` page. Two agent CLIs are `alternative-to`; two
opposed design principles are `contrasts`, and swapping the two says something false about both.
### Realization ### Realization
@@ -135,6 +158,7 @@ Inference and comparison between ideas.
| `contrasts` | differs from the target in a way worth reading both for | | `contrasts` | differs from the target in a way worth reading both for |
| `compares-with` | is weighed against the target on shared dimensions | | `compares-with` | is weighed against the target on shared dimensions |
| `contradicts` | asserts something the target denies | | `contradicts` | asserts something the target denies |
| `addresses` | is a response to the problem the target describes |
| `composition` | is composed of the target | | `composition` | is composed of the target |
| `part-of` | is a component of the target | | `part-of` | is a component of the target |
@@ -149,6 +173,13 @@ about the same fact.
listed separately rather than as inverses because either page may legitimately carry only its listed separately rather than as inverses because either page may legitimately carry only its
own side. own side.
`addresses` is the edge from a solution to the problem it answers - a decision to the trouble
that forced it, a mechanism to the failure it prevents. Keep it apart from `rests-on`, which
takes the target as a *premise* the source argues from: a decision usually does both, and the
one worth writing is the one a reader here would follow. `addresses` has no inverse. The problem
page's inbound view already answers "what did anyone do about this?", which is the only reason
someone would want the reverse.
### Lineage ### Lineage
Where something came from, and what replaced it. Where something came from, and what replaced it.
+12 -2
View File
@@ -26,8 +26,18 @@ never something an agent has to remember.
unreadable frontmatter, broken wikilinks, dangling frontmatter references, orphan pages, unreadable frontmatter, broken wikilinks, dangling frontmatter references, orphan pages,
catalog drift, missing fields, duplicate titles, filename/title mismatches, broken catalog drift, missing fields, duplicate titles, filename/title mismatches, broken
`raw_files:` references, raw files claimed by more than one source page, invalid type paths, `raw_files:` references, raw files claimed by more than one source page, invalid type paths,
schema failures and citation/frontmatter drift. **Do not re-derive any of it by reading schema failures, citation/frontmatter drift, and edges whose label is missing, not authorised
pages.** by the source collection, or redundant beside a specific label on the reverse direction.
**Do not re-derive any of it by reading pages.**
The *Redundant see-also* section is the one that looks mechanical and is not - do **not**
clear it under step 7. It names a `see-also` edge standing beside a specific label on the
reverse direction, and the obvious repair destroys the thing worth keeping: `xref remove`
clears the reference in *both* directions (see [tools/CONTRACT.md](../../tools/CONTRACT.md)),
so removing the weak edge takes the labelled one with it and the pair ends up saying nothing
at all. Either relabel the weak edge to something true with `xref add`, which only ever
touches the source page, or leave it and report it at step 9. Clearing a batch of these is a
planned corpus sweep with its own run, never a reaction inside a lint.
**To see more of the report, read the file - never run `lint` again.** A second run costs a **To see more of the report, read the file - never run `lint` again.** A second run costs a
budget slot and re-measures a corpus that has not changed. The file at step 9 overwrites this budget slot and re-measures a corpus that has not changed. The file at step 9 overwrites this
+7 -2
View File
@@ -1,8 +1,8 @@
--- ---
profile: concepts profile: concepts
outbound: outbound:
concepts: [extends, grounds, rests-on, enables, precondition, exemplifies, abstracted-from, contrasts, compares-with, contradicts, composition, part-of, supersedes, derived-from, adapted-from, see-also] concepts: [extends, grounds, rests-on, enables, precondition, exemplifies, abstracted-from, contrasts, compares-with, contradicts, addresses, alternative-to, composition, part-of, supersedes, derived-from, adapted-from, see-also]
entities: [operationalized-from, mechanism, procedure, applies-when, operates-on, invokes, exemplifies, see-also] entities: [operationalized-from, mechanism, procedure, applies-when, operates-on, invokes, exemplifies, alternative-to, see-also]
sources: [evidenced-by, derived-from, adapted-from, defined-in, see-also] sources: [evidenced-by, derived-from, adapted-from, defined-in, see-also]
comparisons: [compares-with, see-also] comparisons: [compares-with, see-also]
required_by_stack: false required_by_stack: false
@@ -58,6 +58,11 @@ nothing on its own.
The widest authorisation in this instance, because argumentation is what concept pages do. Note that the operational labels are absent: a concept does not `depend-on` anything - the entity implementing it does. The widest authorisation in this instance, because argumentation is what concept pages do. Note that the operational labels are absent: a concept does not `depend-on` anything - the entity implementing it does.
`addresses` is the one that pairs with this collection's own subtypes: a `concept_type: decision`
or a mechanism pointing at the `concept_type: problem` it answers. Without it, the collection can
declare a problem and never say what was done about it. `alternative-to` is self-dual and written
once per pair - see [instructions/link-taxonomy.md](../../instructions/link-taxonomy.md).
Adding a label here is a deliberate contract change, not a way around a refusal. Adding a label here is a deliberate contract change, not a way around a refusal.
## Outbound linking ## Outbound linking
+10 -2
View File
@@ -1,8 +1,8 @@
--- ---
profile: entities profile: entities
outbound: outbound:
entities: [depends-on, required-by, runs-on, hosts, uses, produces, consumes, maintains, owns, part-of, composition, supersedes, see-also] entities: [depends-on, required-by, runs-on, hosts, uses, produces, consumes, maintains, owns, authored, alternative-to, implements, part-of, composition, supersedes, derived-from, adapted-from, see-also]
concepts: [implements, exemplifies, rests-on, applies-when, operates-on, invokes, see-also] concepts: [implements, exemplifies, rests-on, applies-when, operates-on, invokes, authored, alternative-to, see-also]
sources: [evidenced-by, defined-in, see-also] sources: [evidenced-by, defined-in, see-also]
comparisons: [compares-with, see-also] comparisons: [compares-with, see-also]
required_by_stack: false required_by_stack: false
@@ -57,6 +57,14 @@ nothing on its own.
Operational labels dominate here because an entity's relationships are mostly to other concrete things. `implements` points *out* to a concept; the concept does not point back unless that direction is a statement of its own. Operational labels dominate here because an entity's relationships are mostly to other concrete things. `implements` points *out* to a concept; the concept does not point back unless that direction is a statement of its own.
Three of these carry a caveat this area produces more often than the others. `authored` belongs
on a person page pointing at what they made, and it is the label a dead or departed creator
takes - `owns` claims someone answers for the thing *now*. `alternative-to` is self-dual and is
written **once per pair**, never from both ends: a set of interchangeable tools is where a
mirrored clique grows fastest. `derived-from` and `adapted-from` are here for the fork and the
re-implementation - one tool worked up out of another - which is a lineage claim the operational
labels cannot make.
Adding a label here is a deliberate contract change, not a way around a refusal. Adding a label here is a deliberate contract change, not a way around a refusal.
## Outbound linking ## Outbound linking
+6 -6
View File
@@ -5,9 +5,9 @@ tags: [researcher, ai, machine-learning, open-source]
created: 2026-07-26 created: 2026-07-26
modified: 2026-08-29 modified: 2026-08-29
related: related:
- see-also: LLM Wiki Pattern - authored: LLM Wiki Pattern
- see-also: Three-Layer Architecture - authored: Three-Layer Architecture
- see-also: Knowledge Compounding - authored: Knowledge Compounding
sources: [Source - LLM Wiki v2, Source - LLM Wiki Pattern] sources: [Source - LLM Wiki v2, Source - LLM Wiki Pattern]
confidence: 0.95 confidence: 0.95
confidence_base: 0.95 confidence_base: 0.95
@@ -42,7 +42,7 @@ Seine ursprüngliche Einsicht - "stop re-deriving, start compiling" - bildet die
<!-- wikitool:links --> <!-- wikitool:links -->
## Beziehungen ## Beziehungen
- **see-also:** [[LLM Wiki Pattern]] - **authored:** [[LLM Wiki Pattern]]
- **see-also:** [[Three-Layer Architecture]] - **authored:** [[Three-Layer Architecture]]
- **see-also:** [[Knowledge Compounding]] - **authored:** [[Knowledge Compounding]]
<!-- /wikitool:links --> <!-- /wikitool:links -->
+2 -2
View File
@@ -6,7 +6,7 @@ created: 2026-07-26
modified: 2026-08-29 modified: 2026-08-29
related: related:
- see-also: LLM Wiki Pattern - see-also: LLM Wiki Pattern
- owns: Memex - authored: Memex
sources: [Source - LLM Wiki Pattern] sources: [Source - LLM Wiki Pattern]
confidence: 0.90 confidence: 0.90
confidence_base: 0.90 confidence_base: 0.90
@@ -72,5 +72,5 @@ Laut dem Artikel [[LLM Wiki Pattern]] war Bushs Memex-Vision:
## Beziehungen ## Beziehungen
- **see-also:** [[LLM Wiki Pattern]] - **see-also:** [[LLM Wiki Pattern]]
- **owns:** [[Memex]] - **authored:** [[Memex]]
<!-- /wikitool:links --> <!-- /wikitool:links -->
+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 | | `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 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 | | `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, 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 | | `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), `see-also` edges whose reverse direction already carries a specific label (advisory only - redundant rather than wrong, and never migration-gated, since no version turns the redundancy into an error), 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** | | `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 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 | | `confidence init-base [--apply]` | One-time backfill: set `confidence_base` from the current `confidence` on pages that predate the derived-confidence model |
+1
View File
@@ -30,6 +30,7 @@ ADVISORY_KEYS = (
"unmarked_provenance", "unmarked_provenance",
"missing_from_index", "missing_from_index",
"title_mismatches", "title_mismatches",
"redundant_see_also",
) )
+11
View File
@@ -33,6 +33,17 @@ from typing import Any, Iterable, Optional
# so `lint` cannot be satisfied by declaring the placeholder legal. # so `lint` cannot be satisfied by declaring the placeholder legal.
UNLABELLED = None UNLABELLED = None
# The one catalogue label this tool knows by name. Everything else about the
# vocabulary lives in `instructions/link-taxonomy.md` and each collection's
# `outbound:` block, on purpose - an instance may authorise any label it likes
# and the tool never has an opinion about which. `see-also` is the exception
# because it is the catalogue's declared last resort: it asserts only that
# nothing better fit, which is what lets `lint` judge it as *weaker than*
# another edge on the same pair rather than merely different. No behaviour
# depends on the string beyond that comparison, and an instance that dropped
# `see-also` from every contract would simply never see the finding.
SEE_ALSO = "see-also"
@dataclass(frozen=True) @dataclass(frozen=True)
class Edge: class Edge:
+43 -1
View File
@@ -181,6 +181,26 @@ def run_lint(kb_dir: Path) -> dict:
malformed_edges: list[dict] = [] malformed_edges: list[dict] = []
unlabelled_edges: list[dict] = [] unlabelled_edges: list[dict] = []
unauthorised_labels: list[dict] = [] unauthorised_labels: list[dict] = []
redundant_see_also: list[dict] = []
# Every specific thing any page asserts about any pair, collected before the
# loop below because the reverse direction of an edge is not known while
# standing on the page that carries it.
#
# What it is for: `see-also` is the catalogue's declared last resort - it
# asserts that nothing better fit. When the *other* page already says
# something specific about the same pair (`Wine GE depends-on Wine` opposite
# `Wine see-also Wine GE`), the weak edge adds nothing a reader did not
# have: direction is authored but the inbound view is rendered, so the
# labelled edge already shows on both pages. Measured once on this corpus,
# that was 57 of 180 `see-also` edges - the largest single class, and none
# of it a vocabulary gap.
typed_edges: dict[tuple[str, str], str] = {}
for source_title, source_page in pages.items():
for edge in links.edges(source_page.frontmatter, "related"):
if edge.is_labelled and edge.label != links.SEE_ALSO:
typed_edges[(source_title, edge.target)] = edge.label
for title, page in sorted(pages.items()): for title, page in sorted(pages.items()):
type_path = page.frontmatter.get("type") type_path = page.frontmatter.get("type")
if not type_path: if not type_path:
@@ -211,6 +231,16 @@ def run_lint(kb_dir: Path) -> dict:
if not edge.is_labelled: if not edge.is_labelled:
unlabelled_edges.append({"page": title, "target": edge.target}) unlabelled_edges.append({"page": title, "target": edge.target})
continue continue
if edge.label == links.SEE_ALSO:
reverse_label = typed_edges.get((edge.target, title))
if reverse_label is not None:
redundant_see_also.append(
{
"page": title,
"target": edge.target,
"reverse_label": reverse_label,
}
)
target_page = pages.get(edge.target) target_page = pages.get(edge.target)
if source_collection is None or target_page is None: if source_collection is None or target_page is None:
continue continue
@@ -301,6 +331,7 @@ def run_lint(kb_dir: Path) -> dict:
"malformed_edges": malformed_edges, "malformed_edges": malformed_edges,
"unlabelled_edges": unlabelled_edges, "unlabelled_edges": unlabelled_edges,
"unauthorised_labels": unauthorised_labels, "unauthorised_labels": unauthorised_labels,
"redundant_see_also": redundant_see_also,
"unbalanced_markers": unbalanced_marker_findings, "unbalanced_markers": unbalanced_marker_findings,
"quote_limit_violations": quote_limit_violations, "quote_limit_violations": quote_limit_violations,
"invalid_type_paths": invalid_type_paths, "invalid_type_paths": invalid_type_paths,
@@ -403,6 +434,11 @@ def render_markdown(report: dict) -> str:
report.get("unauthorised_labels", []), report.get("unauthorised_labels", []),
lambda i: f"[[{i['page']}]] `{i['label']}` -> kb/{i['destination']}/ ([[{i['target']}]])", lambda i: f"[[{i['page']}]] `{i['label']}` -> kb/{i['destination']}/ ([[{i['target']}]])",
) )
_section(
lines, "Redundant see-also (the other page already says something specific)",
report.get("redundant_see_also", []),
lambda i: f"[[{i['page']}]] `see-also` -> [[{i['target']}]], but [[{i['target']}]] already asserts `{i['reverse_label']}` back - drop the weaker edge, the inbound view renders the other one here",
)
_section( _section(
lines, "Dangling Frontmatter References", report["dangling_frontmatter_refs"], lines, "Dangling Frontmatter References", report["dangling_frontmatter_refs"],
lambda i: f"[[{i['page']}]] `{i['field']}:` names `{i['target']}`, which is not a page", lambda i: f"[[{i['page']}]] `{i['field']}:` names `{i['target']}`, which is not a page",
@@ -485,7 +521,13 @@ def default_report_path(report: dict) -> Path:
# Findings that make a tree structurally wrong rather than merely untidy. # Findings that make a tree structurally wrong rather than merely untidy.
# `orphan_pages` is deliberately absent: many pages are validly reachable # `orphan_pages` is deliberately absent: many pages are validly reachable
# through the index or navigation only. `quote_limit_violations` is advisory # through the index or navigation only. `quote_limit_violations` is advisory
# too - it flags a habit, not a broken tree. # too - it flags a habit, not a broken tree. `redundant_see_also` joins them for
# both of those reasons at once: a weak edge beside a specific one is redundant
# rather than wrong, and the check arrived long after the corpora it judges, so
# promoting it would turn every existing instance red on the upgrade that
# shipped it. Unlike `unlabelled_edges` it is not migration-gated either - there
# is no version at which the redundancy becomes an error, only a sweep someone
# does or does not get to.
# #
# `malformed_edges` and `unbalanced_markers` are hard from the start: neither # `malformed_edges` and `unbalanced_markers` are hard from the start: neither
# describes an unconverted page, only a broken one. # describes an unconverted page, only a broken one.
+90
View File
@@ -579,3 +579,93 @@ def test_a_tree_that_never_declared_a_kb_version_keeps_every_key(kb_dir):
migration for a gated finding to be the noise of.""" migration for a gated finding to be the noise of."""
assert kb_state.read_kb_version() is None assert kb_state.read_kb_version() is None
assert hard_error_keys() == HARD_ERROR_KEYS assert hard_error_keys() == HARD_ERROR_KEYS
# --- `redundant_see_also` --------------------------------------------------
#
# `see-also` is the catalogue's declared last resort. The finding is about the
# case where the *other* page already said something specific about the same
# pair, so the weak edge carries nothing the inbound view did not already
# render. Measured once on this repo's corpus, that was 57 of 180 see-also
# edges - which is why it is worth a check rather than a habit.
def _pair(kb_dir, forward, backward):
"""Two tool pages asserting `forward` and `backward` about each other."""
for name, edge in (("nearside", forward), ("farside", backward)):
other = "farside" if name == "nearside" else "nearside"
write_page(
kb_dir / f"entities/tools/{name}.md",
{"type": "types/entity.md", "entity_type": "tool", "tags": [],
"created": "2026-09-04", "modified": "2026-09-04",
"related": [] if edge is None else [{edge: other}],
"sources": [], "confidence": 0.8, "provenance": "general",
"summary": f"One half of a pair, asserting {edge} about the other."},
f"\n# {name}\n\nHalf a pair.\n",
)
def test_see_also_is_redundant_when_the_other_page_asserts_something_specific(kb_dir):
"""`nearside see-also farside` beside `farside depends-on nearside`: the
labelled edge already shows on both pages, so the weak one says nothing."""
_pair(kb_dir, "see-also", "depends-on")
report = run_lint(kb_dir)
assert {
"page": "nearside", "target": "farside", "reverse_label": "depends-on"
} in report["redundant_see_also"]
def test_a_see_also_with_no_reverse_edge_at_all_is_not_redundant(kb_dir):
"""The ordinary case the label exists for - nothing more specific fits, and
the other page says nothing back."""
_pair(kb_dir, "see-also", None)
assert [
i for i in run_lint(kb_dir)["redundant_see_also"] if i["page"] == "nearside"
] == []
def test_a_mutual_see_also_pair_is_not_reported_here(kb_dir):
"""Two weak edges about one pair is a different finding - a mirror, which
the catalogue's 'direction is authored, never mirrored' rule covers and a
corpus sweep resolves. This check must not claim it: it is about a weak
edge standing beside a *specific* one, and reporting the mutual case here
would tell an author to drop an edge without saying which."""
_pair(kb_dir, "see-also", "see-also")
assert [
i for i in run_lint(kb_dir)["redundant_see_also"]
if i["page"] in ("nearside", "farside")
] == []
def test_the_specific_edge_is_never_the_one_reported(kb_dir):
"""Only the `see-also` side is a finding. Reporting the labelled edge too
would make the pair unfixable - dropping both loses the assertion."""
_pair(kb_dir, "see-also", "depends-on")
assert [
i for i in run_lint(kb_dir)["redundant_see_also"] if i["page"] == "farside"
] == []
def test_redundant_see_also_is_advisory_at_every_kb_version(kb_dir):
"""Redundant, not wrong - and the check arrived long after the corpora it
judges, so promoting it would turn every existing instance red on the
upgrade that shipped it. Unlike `unlabelled_edges` there is no version at
which it becomes an error, so it is not migration-gated either."""
_pair(kb_dir, "see-also", "depends-on")
report = run_lint(kb_dir)
assert report["redundant_see_also"] != []
for version in (Version(3, 0, 0), Version(4, 0, 0), Version(5, 0, 0)):
kb_state.write_kb_state(version, [])
assert "redundant_see_also" not in hard_error_keys()
assert has_hard_errors({"redundant_see_also": report["redundant_see_also"]}) is False
def test_redundant_see_also_reaches_the_rendered_report_and_the_summary(kb_dir):
"""A finding nobody prints is a finding nobody acts on. `render_summary`
drops every empty section, so this also proves the section is not empty."""
_pair(kb_dir, "see-also", "depends-on")
report = run_lint(kb_dir)
assert "Redundant see-also" in render_markdown(report)
summary = render_summary(report)
assert "Redundant see-also" in summary
assert "[[nearside]]" in summary and "depends-on" in summary