Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3916cb9541 |
+81
@@ -35,6 +35,87 @@ dev-checkout concern - readable here, never shipped as something to parse.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 -->
|
||||||
|
|||||||
@@ -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 -->
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ ADVISORY_KEYS = (
|
|||||||
"unmarked_provenance",
|
"unmarked_provenance",
|
||||||
"missing_from_index",
|
"missing_from_index",
|
||||||
"title_mismatches",
|
"title_mismatches",
|
||||||
|
"redundant_see_also",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user