Compare commits

...

4 Commits

Author SHA1 Message Date
torben e00eae08e8 Coverage-Untergrenze 85 % in tools/.coveragerc, gegen beobachtete 87,0 % (4.7.2, schließt #10, eröffnet #51)
CI / verify (push) Successful in 57s
Release / release (push) Successful in 38s
Files changed:
- .gitea/workflows/ci.yml
- CHANGES.md
- EVALS.md
- VERSION
- tools/.coveragerc
2026-09-04 19:19:38 +02:00
torben fe55ad2a9c Coverage-Beobachtung nachgezogen: 87,0 % / 6498 / 975 (Lauf 163), Artefakt-Abruf geklärt (#10)
CI / verify (push) Successful in 58s
Files changed:
- .gitea/workflows/ci.yml
- EVALS.md
2026-09-04 19:12:40 +02:00
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
16 changed files with 438 additions and 43 deletions
+12 -6
View File
@@ -120,12 +120,12 @@ jobs:
# container is no longer a special environment worth a second run. # container is no longer a special environment worth a second run.
# See instructions/dev/testing-conventions.md. # See instructions/dev/testing-conventions.md.
# #
# Coverage is reported, not enforced: there is deliberately no # Coverage is measured and enforced at a floor of 85% against a measured
# `--cov-fail-under` yet (Gitea #10). The threshold gets set in its own # 87.0% - `fail_under` in tools/.coveragerc, not a flag here, so the
# later commit, with the measured number as its justification - one # number sits next to the reasoning that produced it. It was set only
# picked before the number is either too low to bite or too high to # after the number had been watched across 38 runs (Gitea #10, closed).
# survive the next honest commit, and the second kind gets lowered # A red suite from this floor means coverage actually fell; the two
# instead of earned. Config: tools/.coveragerc. # points of headroom already absorb a new thin Typer wrapper.
run: | run: |
set -eu set -eu
cd tools cd tools
@@ -138,6 +138,12 @@ jobs:
# v3, not v4 - v4 is restricted on this Gitea instance; v3 is what is # v3, not v4 - v4 is restricted on this Gitea instance; v3 is what is
# proven here (torben/gitea-mcp@ci-build, ci-build.yaml, runs # proven here (torben/gitea-mcp@ci-build, ci-build.yaml, runs
# 42-45). # 42-45).
#
# The artifact is downloadable from the run page, but the Actions
# artifact REST endpoints report `total_count: 0` for it - v3 writes
# through the older artifact API, which those endpoints do not read.
# An empty list is not a failed upload. See EVALS.md § "How much of the
# stack the suite reaches"; do not re-derive this.
if: always() if: always()
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
+153
View File
@@ -35,6 +35,159 @@ dev-checkout concern - readable here, never shipped as something to parse.
--- ---
## 4.7.2 - 2026-09-04 - Coverage-Untergrenze bei 85 %, gegen beobachtete 87,0 %
**Author:** Torben Nehmer
<!-- wikitool:bumps -->
- Coverage-Untergrenze 85 % in tools/.coveragerc
<!-- /wikitool:bumps -->
Die Suite hat jetzt einen Boden: `fail_under = 85` in `tools/.coveragerc`, gemessen gegen 87,0 %
(CI-Lauf 163, 6498 Statements, 975 Tests). Damit ist Gitea #10 geschlossen — das Issue, das die
Messung eingerichtet und die Schwelle danach **absichtlich** zurückgehalten hat, bis die Zahl
beobachtet war.
Die Beobachtung ist der eigentliche Inhalt dieses Bumps. Zwischen der ersten Messung (86,9 % von
5105 Statements, 730 Tests, Lauf 87, Stack 1.8.1) und heute ist der gemessene Code um ein Viertel
gewachsen und die Suite um ein Drittel, über 38 grüne Läufe — und die Quote hat sich um einen
Zehntelpunkt bewegt. Eine Untergrenze, die auf dieser Beobachtung steht, ist etwas anderes als
eine gegriffene Zahl.
**85 und nicht 87, und das ist keine Bequemlichkeit.** Der Coverage-Bericht unterscheidet drei
Sorten ungedeckter Zeilen, und nur eine davon bedeutet Arbeit (`EVALS.md` § „How much of the
stack the suite reaches"). Ein neuer dünner Typer-Wrapper senkt den Gesamtwert, ohne dass
irgendetwas schlechter geworden wäre — seine Logik liegt daneben und ist dort getestet. Eine
Schwelle auf dem gemessenen Wert würde genau an diesem Commit rot, und eine Schwelle, die aus
einem Nicht-Grund rot wird, wird gesenkt statt verdient. Das ist die Fehlerweise, die #10
verhindern wollte, nur von der anderen Seite. Die zwei Punkte sind der Platz, den die Taxonomie
verlangt.
`fail_under` steht in der Konfiguration und nicht als `--cov-fail-under` im CI-Schritt: so sitzt
die Zahl neben der Begründung, die sie erzeugt hat, und gilt für jeden `--cov`-Lauf statt nur für
den einen, den CI schreibt.
Was der Boden **nicht** tut: die drei echten Lücken schließen (`provenance_cmd.py` 44 %,
`migrate_cmd.py` 65 %, `type_resolver.py` 79 %). Er friert den erreichten Stand ein. Diese Liste
ist die einzige, die sich nicht bewegt hat, während alles um sie herum wuchs — `migrate_cmd.py`
ist sogar von 71 % gefallen, weil das Modul gewachsen ist und die neuen Zeilen ungetestet ankamen.
Das ist Gitea #51.
Mitgenommen, weil es dieselbe Frage beantwortet: der Coverage-Bericht **ist** als Artefakt
abrufbar, über die Run-Seite. Die Actions-Artefakt-Endpunkte melden dafür `total_count: 0`, weil
`upload-artifact@v3` über die ältere Artifact-API ablegt, die diese Endpunkte nicht lesen. Eine
leere Liste ist kein fehlgeschlagener Upload — steht jetzt in `EVALS.md` und im Kommentar an der
`Coverage report`-Stufe, damit die naheliegende „Korrektur" auf v4 (hier eingeschränkt) niemandem
mehr einfällt.
---
## 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
+34 -14
View File
@@ -249,29 +249,49 @@ created but not yet written reports broken links. That is the scaffold saying it
### How much of the stack the suite reaches ### How much of the stack the suite reaches
Coverage is measured in CI and reported, never enforced - `pytest --cov`, config in Coverage is measured in CI - `pytest --cov`, config in `tools/.coveragerc`, HTML and XML
`tools/.coveragerc`, HTML and XML uploaded as the `coverage-<run id>` artifact of every run. uploaded as the `coverage-<run id>` artifact of every run.
There is no `--cov-fail-under`: a threshold is owed (Gitea #10), in its own commit, once the **Fetch that artifact from the run's own page, not from the API**: `upload-artifact@v3` writes
number has been watched long enough to freeze the state it actually reached. through the older artifact API, and the Actions artifact REST endpoints answer `total_count: 0`
for a run whose artifact the run page offers for download. The upload works; only the listing
does not see it. Do not re-derive this, and do not read the empty list as a failed upload.
**First measurement, 2026-08-31, stack 1.8.1: 86.9% of 5105 statements across `chemenu/`, It is enforced at a floor of **85%** (`fail_under` in `tools/.coveragerc`), which is what a red
730 tests** - as reported by CI run 87, not by the local run that preceded the last commit of suite from this axis means: coverage actually fell, not that a wrapper was added. The floor was
that release. Reproduce it with `cd tools && .venv/bin/python -m pytest -q --cov` (needs set only after the number had been watched - Gitea #10 held it back for exactly that, and the
`pytest-cov`, which is CI-only and deliberately absent from `tools/requirements.txt` - an two points between 85 and the measured 87.0% are the room the taxonomy below asks for. A
instance runs the wiki, it does not measure this suite). threshold at the measured number goes red on the next thin Typer wrapper, and a threshold that
goes red for a non-reason gets lowered rather than earned.
The total is the least interesting number here. What the report is for is *which* modules sit **Measured 2026-09-04, stack 4.7.1: 87.0% of 6498 statements across `chemenu/`, 975 tests** -
CI run 163. The first measurement, at stack 1.8.1 on 2026-08-31, was 86.9% of 5105 statements
over 730 tests (CI run 87). Both are what CI reported, never a local run: the local number
preceding a release measures a tree that is one commit short of the published one.
The pair says more than either number does. Between them the measured code grew by a quarter
and the suite by a third, and the quota moved by a tenth of a point - which is the observation a
threshold was waiting for, rather than the total itself. Reproduce either with
`cd tools && .venv/bin/python -m pytest -q --cov` (needs `pytest-cov`, which is CI-only and
deliberately absent from `tools/requirements.txt` - an instance runs the wiki, it does not
measure this suite).
The total stays the least interesting number here. What the report is for is *which* modules sit
low, and three kinds have to be told apart before any of it turns into work: low, and three kinds have to be told apart before any of it turns into work:
- **Thin Typer wrappers**, where the logic lives beside them and is tested there: - **Thin Typer wrappers**, where the logic lives beside them and is tested there:
`eval_cmd.py` (36%), `types_cmd.py` (52%), `cli.py` (52%). Low coverage on a wrapper is `eval_cmd.py` (36%), `types_cmd.py` (40%), `search.py` (49%), `cli.py` (54%),
evidence of a good cut, not of a missing test. `links_cmd.py` (61%). Low coverage on a wrapper is evidence of a good cut, not of a missing
test - `search.py`'s uncovered block is its command body alone, while the backends under
`chemenu/search/` that do the work sit between 91% and 98%.
- **Code that reaches the network or the filesystem's outside**, where the interesting half is - **Code that reaches the network or the filesystem's outside**, where the interesting half is
already injectable and tested through the seam: `version.py`'s `fetch_latest()` takes a already injectable and tested through the seam: `version.py`'s `fetch_latest()` takes a
`fetcher` parameter for exactly that, and the real network line stays uncovered on purpose. `fetcher` parameter for exactly that, and the real network line stays uncovered on purpose.
- **Genuine gaps**, where uncovered lines are logic nobody exercises: `provenance_cmd.py` - **Genuine gaps**, where uncovered lines are logic nobody exercises: `provenance_cmd.py`
(44%), `migrate_cmd.py` (71%), `type_resolver.py` (79%). This is the list worth reading, and (44%), `migrate_cmd.py` (65%), `type_resolver.py` (79%). This is the list worth reading, and
the reason step 2 of #10 is not a formality. the only one of the three that has not moved while everything around it did:
`provenance_cmd.py` sits where it sat, and `migrate_cmd.py` fell from 71% because it grew and
its new lines arrived untested. The floor freezes this; it does not close it. Closing it is
Gitea #51.
## Scoring a session ## Scoring a session
+1 -1
View File
@@ -1 +1 @@
4.6.1 4.7.2
+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 -->
+22 -4
View File
@@ -11,10 +11,27 @@
# measure the stack's tests - so an addopts entry would break the plain # measure the stack's tests - so an addopts entry would break the plain
# `pytest -q` that every local run and the CI "Tests" step use. # `pytest -q` that every local run and the CI "Tests" step use.
# #
# No `fail_under` yet, on purpose: Gitea #10 sets the threshold in a separate, # `fail_under` lives here rather than as a `--cov-fail-under` flag in the CI
# later commit, once the measured number exists to justify it. A threshold # step, so the number sits next to the reasoning that produced it and applies to
# picked before the number is either too low to bite or too high to survive the # any `--cov` run, not just the one CI happens to write.
# next honest commit - and the second kind gets lowered rather than earned. #
# 85, against a measured 87.0% (CI run 163, 6498 statements, 975 tests). Gitea
# #10 held this back until the number had been watched: the first measurement
# was 86.9% of 5105 statements over 730 tests (CI run 87), and between the two
# the measured code grew by a quarter and the suite by a third while the quota
# moved a tenth of a point. That stability is what the threshold rests on.
#
# The two points of headroom are not slack. They are the room the report's own
# taxonomy asks for: a new thin Typer wrapper lowers the total without anything
# having got worse, because its logic is tested beside it (see EVALS.md § "How
# much of the stack the suite reaches"). A threshold at the measured number
# would go red on exactly that commit, and a threshold that goes red for a
# non-reason gets lowered rather than earned - which is the failure mode #10
# existed to avoid, arriving from the other side.
#
# What this number does *not* do is close the genuine gaps - provenance_cmd.py,
# migrate_cmd.py, type_resolver.py. It freezes the state that was reached; the
# gaps are their own work, tracked separately.
[run] [run]
source = chemenu source = chemenu
omit = omit =
@@ -23,3 +40,4 @@ omit =
[report] [report]
show_missing = True show_missing = True
precision = 1 precision = 1
fail_under = 85
+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