Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cc38bcd700 | |||
| 6671af6a60 | |||
| b4e450108e | |||
| e00eae08e8 | |||
| fe55ad2a9c | |||
| 24593c5608 | |||
| 3916cb9541 |
+12
-6
@@ -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:
|
||||||
|
|||||||
+202
@@ -35,6 +35,208 @@ dev-checkout concern - readable here, never shipped as something to parse.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 4.7.4 - 2026-09-04 - bootstrap.md nennt den session-id-WARN nach frischem Bootstrap explizit als erwartet
|
||||||
|
|
||||||
|
**Author:** Torben Nehmer
|
||||||
|
|
||||||
|
<!-- wikitool:bumps -->
|
||||||
|
- bootstrap.md nennt den session-id-WARN nach frischem Bootstrap explizit als erwartet
|
||||||
|
<!-- /wikitool:bumps -->
|
||||||
|
|
||||||
|
Nach einem frischen Clone plus `instructions/bootstrap.md` zeigte `tools/wikitool doctor`
|
||||||
|
durchgehend `OK`, außer `session-id: WARN` - ohne Einordnung, ob das ein Bootstrap-Defekt ist.
|
||||||
|
`WIKITOOL_SESSION_ID` wird laut `instructions/session-setup.md` bewusst pro Arbeitssitzung
|
||||||
|
gesetzt, nicht pro Clone; ein Export in `bootstrap.md` selbst würde nur für den Bootstrap-Lauf
|
||||||
|
gelten, nicht für die tatsächliche Arbeitssitzung danach (die nach dem Neustart in Schritt 6 in
|
||||||
|
einer neuen Shell beginnt). `bootstrap.md` bekommt deshalb einen neuen Schritt 7, der den WARN
|
||||||
|
als erwarteten Zustand benennt - analog zum bereits dokumentierten `personalization: FAIL` in
|
||||||
|
Schritt 4 - und auf `session-setup.md` verweist, statt den Export in Bootstrap nachzubauen.
|
||||||
|
Schließt #54.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4.7.3 - 2026-09-04 - eval: gate-not-self-opened prueft REMOVED_FLAGS gegen das eigene Kommando
|
||||||
|
|
||||||
|
**Author:** Torben Nehmer
|
||||||
|
|
||||||
|
Die Trajektorien-Regel `gate-not-self-opened` (`tools/chemenu/evals/trajectory.py`) hat jedes
|
||||||
|
Argument jedes `wikitool.call` gegen `REMOVED_FLAGS = {"--yes": "publish", "-y": "publish"}`
|
||||||
|
geprüft, ohne je das eigene `command`-Feld des Aufrufs gegenzulesen. `--yes`/`-y` sind nur auf
|
||||||
|
`publish` entfernt worden - auf `rm --page <Titel> --yes` sind sie ein gültiger, dokumentierter
|
||||||
|
Flag. Ergebnis: jeder `rm --yes`-Aufruf wurde als Invarianten-Verstoß gemeldet ("an agent
|
||||||
|
inventing a flag the tool never accepts"), obwohl das Tool ihn akzeptiert hatte.
|
||||||
|
|
||||||
|
In den vorhandenen Telemetrie-Traces unter `reports/telemetry/` betraf das 111 `rm`-Aufrufe
|
||||||
|
über 8 Sessions, davon 27 allein in `publish-cleanup/u3` - jede davon fälschlich `FAILED`
|
||||||
|
gescort. Kein bestehender Test hätte das gefangen: `tools/chemenu/tests/test_evals.py` prüfte
|
||||||
|
`REMOVED_FLAGS` ausschließlich über `publish --yes`, nie über ein anderes Kommando.
|
||||||
|
|
||||||
|
Fix: die Bedingung liest jetzt `attrs.get("command") == REMOVED_FLAGS[arg]` mit. Neuer
|
||||||
|
Regressionstest `test_yes_on_a_command_that_still_has_it_is_not_a_finding` deckt genau den
|
||||||
|
`rm --yes`-Fall ab und schlägt gegen den unfixed Code nachweislich fehl.
|
||||||
|
|
||||||
|
Keine Verhaltensänderung an `wikitool` selbst - ausschließlich an der Scoring-Logik unter
|
||||||
|
`tools/chemenu/evals/`.
|
||||||
|
|
||||||
|
<!-- wikitool:bumps -->
|
||||||
|
- eval: gate-not-self-opened prueft REMOVED_FLAGS gegen das eigene Kommando
|
||||||
|
<!-- /wikitool:bumps -->
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -55,3 +55,12 @@ to feel routine.
|
|||||||
That's the deeper reason these numbers live in a tool rather than in prose: prose is read once
|
That's the deeper reason these numbers live in a tool rather than in prose: prose is read once
|
||||||
and remembered loosely, but a threshold enforced every call is tested by every call, and a
|
and remembered loosely, but a threshold enforced every call is tested by every call, and a
|
||||||
threshold that fails its own test gets noticed and re-measured rather than quietly ignored.
|
threshold that fails its own test gets noticed and re-measured rather than quietly ignored.
|
||||||
|
|
||||||
|
The suite's coverage floor is the same argument run forwards instead of backwards. The ceiling
|
||||||
|
above was wrong first and measured afterwards; the floor was withheld on purpose until the number
|
||||||
|
existed - measured, then watched across 38 runs while the code grew by a quarter, and only then
|
||||||
|
written down as 85 against an observed 87.0%. The two points of daylight are the same
|
||||||
|
consideration as the ceiling's headroom: a limit the ordinary case keeps tripping stops being a
|
||||||
|
limit. A coverage floor set at the measured number goes red on the next thin command wrapper,
|
||||||
|
which is not a regression, and a threshold that goes red for a non-reason gets lowered rather
|
||||||
|
than earned - the failure mode above, reached from the other direction.
|
||||||
|
|||||||
@@ -65,6 +65,14 @@ they are published: the agent harness will not offer `wiki-ingest`, `wiki-query`
|
|||||||
6. **Restart the agent session** if it was already running. Harnesses read the skill
|
6. **Restart the agent session** if it was already running. Harnesses read the skill
|
||||||
directories at startup, so skills published mid-session are not picked up.
|
directories at startup, so skills published mid-session are not picked up.
|
||||||
|
|
||||||
|
7. **Expect a lingering `session-id` WARN.** A `tools/wikitool doctor` run at this point reports
|
||||||
|
`OK` throughout except `session-id: WARN` - that check is scoped to the working session, not
|
||||||
|
the clone, so a freshly bootstrapped checkout with no `WIKITOOL_SESSION_ID` exported yet
|
||||||
|
always shows it. This is expected, not a Bootstrap gap: exporting it here would only be true
|
||||||
|
for this one-off setup run, not for whichever session picks up the actual work next, in a new
|
||||||
|
shell after step 6's restart. Run [session-setup.md](session-setup.md) at the start of that
|
||||||
|
session instead.
|
||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
This does not apply to anything under `kb/`, `raw/` or `reports/`; those are committed and
|
This does not apply to anything under `kb/`, `raw/` or `reports/`; those are committed and
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 -->
|
||||||
|
|||||||
+22
-4
@@ -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
@@ -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 |
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ ADVISORY_KEYS = (
|
|||||||
"unmarked_provenance",
|
"unmarked_provenance",
|
||||||
"missing_from_index",
|
"missing_from_index",
|
||||||
"title_mismatches",
|
"title_mismatches",
|
||||||
|
"redundant_see_also",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ def check_gate_not_self_opened(records: list[dict]) -> Rule:
|
|||||||
"ts": record["ts"], "call": _signature(attrs),
|
"ts": record["ts"], "call": _signature(attrs),
|
||||||
"flag": arg, "reason": "force flag, never permitted",
|
"flag": arg, "reason": "force flag, never permitted",
|
||||||
})
|
})
|
||||||
elif arg in REMOVED_FLAGS:
|
elif arg in REMOVED_FLAGS and attrs.get("command") == REMOVED_FLAGS[arg]:
|
||||||
rule.findings.append({
|
rule.findings.append({
|
||||||
"ts": record["ts"], "call": _signature(attrs), "flag": arg,
|
"ts": record["ts"], "call": _signature(attrs), "flag": arg,
|
||||||
"reason": f"{arg} no longer exists on {REMOVED_FLAGS[arg]} - a stale skill "
|
"reason": f"{arg} no longer exists on {REMOVED_FLAGS[arg]} - a stale skill "
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -76,6 +76,18 @@ def test_yes_is_a_finding_even_after_the_gate_refused():
|
|||||||
assert "no longer exists" in rule.findings[0]["reason"]
|
assert "no longer exists" in rule.findings[0]["reason"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_yes_on_a_command_that_still_has_it_is_not_a_finding():
|
||||||
|
"""`--yes` was removed from `publish` only (chemenu/git_publish.py); `rm
|
||||||
|
--page X --yes` is a live, documented flag. REMOVED_FLAGS names the
|
||||||
|
command the flag was removed from - the rule must check the call's own
|
||||||
|
`command` against it, not just scan every argument for the literal
|
||||||
|
string. A real trace (`publish-cleanup/u3`) shows what missing this
|
||||||
|
check costs: 27 ordinary `rm --yes` calls scored as invariant
|
||||||
|
violations."""
|
||||||
|
records = [call("2026-08-23T10:00:00Z", "rm", "--page", "X", "--yes")]
|
||||||
|
assert rule_by_id(records, "gate-not-self-opened").passed
|
||||||
|
|
||||||
|
|
||||||
def test_override_budget_needs_its_own_gate_not_another_one():
|
def test_override_budget_needs_its_own_gate_not_another_one():
|
||||||
"""Being refused by one gate does not license opening a different one."""
|
"""Being refused by one gate does not license opening a different one."""
|
||||||
records = [
|
records = [
|
||||||
|
|||||||
@@ -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