Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cc38bcd700 | |||
| 6671af6a60 | |||
| b4e450108e | |||
| e00eae08e8 | |||
| fe55ad2a9c | |||
| 24593c5608 | |||
| 3916cb9541 | |||
| 72b2b4424f | |||
| 91bd430ac8 |
+12
-6
@@ -120,12 +120,12 @@ jobs:
|
||||
# container is no longer a special environment worth a second run.
|
||||
# See instructions/dev/testing-conventions.md.
|
||||
#
|
||||
# Coverage is reported, not enforced: there is deliberately no
|
||||
# `--cov-fail-under` yet (Gitea #10). The threshold gets set in its own
|
||||
# later commit, with the measured number as its justification - one
|
||||
# picked before the number is either too low to bite or too high to
|
||||
# survive the next honest commit, and the second kind gets lowered
|
||||
# instead of earned. Config: tools/.coveragerc.
|
||||
# Coverage is measured and enforced at a floor of 85% against a measured
|
||||
# 87.0% - `fail_under` in tools/.coveragerc, not a flag here, so the
|
||||
# number sits next to the reasoning that produced it. It was set only
|
||||
# after the number had been watched across 38 runs (Gitea #10, closed).
|
||||
# A red suite from this floor means coverage actually fell; the two
|
||||
# points of headroom already absorb a new thin Typer wrapper.
|
||||
run: |
|
||||
set -eu
|
||||
cd tools
|
||||
@@ -138,6 +138,12 @@ jobs:
|
||||
# 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
|
||||
# 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()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
|
||||
@@ -102,11 +102,32 @@ jobs:
|
||||
- name: Release notes from CHANGES.md
|
||||
# `version notes` fails when the changelog has no entry for this
|
||||
# version, which is the last place that mistake can still be caught.
|
||||
#
|
||||
# The footer below settles Gitea #47's second side-finding: a release
|
||||
# note is written once, at tag time, and a later correction to
|
||||
# CHANGES.md never reaches it - `gitea-mcp` has no release-edit method,
|
||||
# and delete-and-recreate would destroy the attached tarball assets that
|
||||
# INSTALL.md and `version check` point at. That happened for real to
|
||||
# v4.4.0, whose note carried a fact that the corpus had already
|
||||
# corrected. Rather than build a correction path for a text nobody can
|
||||
# edit, the snapshot says it is one and names where the maintained
|
||||
# version lives. A stale note then costs a reader one click instead of
|
||||
# a wrong belief. Appended here rather than inside `version notes`,
|
||||
# which is a general-purpose extractor whose other callers (a local
|
||||
# preview, a pipe) should not inherit a release-page footer.
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
run: |
|
||||
set -eu
|
||||
tools/wikitool docs verify
|
||||
tools/wikitool version notes > /tmp/release-notes.md
|
||||
cat >> /tmp/release-notes.md <<'EOF'
|
||||
|
||||
---
|
||||
|
||||
*This note is a snapshot of the `CHANGES.md` entry as it stood when the tag was cut, and
|
||||
is never edited afterwards. The maintained version of this text - including any later
|
||||
correction - is the entry for this version in `CHANGES.md` in the repository.*
|
||||
EOF
|
||||
cat /tmp/release-notes.md
|
||||
|
||||
- name: Build the distribution tarball
|
||||
|
||||
+258
@@ -35,6 +35,264 @@ 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)
|
||||
|
||||
**Author:** Torben Nehmer
|
||||
|
||||
<!-- wikitool:bumps -->
|
||||
- DEVELOPMENT.md im Kommandotabellen-Check, selbstbeschriftete Release-Notes, Prosa-Korrekturen (#47 Block 3)
|
||||
<!-- /wikitool:bumps -->
|
||||
|
||||
Block 3 aus #47 - die beiden Nebenbefunde entschieden und umgesetzt - plus drei Prosa-Korrekturen
|
||||
an `4.6.0`, die eine Bewertung des eigenen Ergebnisses gefunden hat.
|
||||
|
||||
**`DEVELOPMENT.md` gehört in `docs_verify.STAGE_READMES`** (entschieden: ja). Das Gegenargument
|
||||
bei der Aufnahme war, die Liste führe bisher nur ausgelieferte Dokumente, und `DEVELOPMENT.md`
|
||||
wird von `dist_cmd.ROOT_FILES` bewusst nicht ausgeliefert. Beim Hinsehen löst es sich auf:
|
||||
`check_readmes_have_no_command_table` überspringt eine Datei, die nicht existiert. In einer
|
||||
ausgelieferten Instanz ist der Eintrag damit schlicht wirkungslos, im Entwicklungs-Checkout - dem
|
||||
einzigen Ort, an dem die Datei existiert und also driften kann - greift er. Dafür spricht der
|
||||
Anlass: genau diese Datei trug einmal eine Tabelle, die für jeden Verify-Befehl ein zweites Mal
|
||||
beschrieb, was er prüft, und sie musste von Hand entfernt werden, weil nichts sie mit etwas
|
||||
verglich. Zwei Tests: einer, der die Tabelle in `DEVELOPMENT.md` meldet, und einer, der
|
||||
festhält, dass eine fehlende gelistete Datei übersprungen und nicht als Fund gemeldet wird - der
|
||||
Instanz-Fall, an dem die Entscheidung hing. Der Konstantenname ist jetzt enger als sein Inhalt;
|
||||
das steht als Kommentar daneben, statt eine Umbenennung durch zwei Aufrufstellen zu ziehen.
|
||||
|
||||
**Veröffentlichte Release-Notes veralten weiter - sie sagen es jetzt selbst** (entschieden:
|
||||
Schnappschuss akzeptieren, statt einen Korrekturweg zu bauen). Eine nach dem Tag korrigierte
|
||||
`CHANGES.md` erreicht die Release-Seite nicht: `gitea-mcp` kennt kein Release-Edit, und
|
||||
Löschen-und-neu-Anlegen würde die angehängten Tarball-Assets vernichten, auf die `INSTALL.md` und
|
||||
`version check` zeigen. Bei `v4.4.0` ist das real eingetreten. Statt eines Korrekturwegs für einen
|
||||
Text, den niemand editieren kann, trägt der Schnappschuss jetzt eine Fußzeile, die sagt, dass er
|
||||
einer ist und wo die gepflegte Fassung liegt - eine veraltete Notiz kostet einen Leser damit einen
|
||||
Klick statt einer falschen Überzeugung. Angehängt in `release.yml` und nicht in `version notes`:
|
||||
das Kommando ist ein allgemeiner Extraktor, dessen andere Aufrufer (lokale Vorschau, eine Pipe)
|
||||
keine Release-Seiten-Fußzeile erben sollen. `.gitea/`-Änderung, also ohne eigenen Bump-Anspruch -
|
||||
sie fährt hier mit.
|
||||
|
||||
**Drei Prosa-Korrekturen an `4.6.0`.** Der `4.6.0`-Eintrag und der Docstring von
|
||||
`touches_stack_machinery` behaupteten, das Prädikat prüfe „denselben Pfad-Umfang, den der
|
||||
CI-Versions-Gate selbst verwendet". Das stimmt nicht: CI matcht `[^/]+/CONTRACT\.md$`, also genau
|
||||
eine Pfadebene, das Prädikat matcht `CONTRACT.md` in jeder Tiefe. Folgenlos im Verhalten - ein
|
||||
Über-Match druckt eine Zeile zu viel, nie eine zu wenig -, aber es war eine behauptete Äquivalenz,
|
||||
die keine ist, geschrieben in genau der ungeprüften Prosa-Phase, um die #47 sich dreht. Docstring
|
||||
und `tools/CONTRACT.md` benennen die Differenz jetzt und begründen sie (bei einer Erinnerung ist
|
||||
Über-Matchen die richtige Richtung). Drittens: `stack-close` beschrieb den eigenen Skill-Schnitt
|
||||
zu stark („es gibt keinen nächsten Schritt mehr, an dem vorbei zu rutschen wäre"). Wahr für die
|
||||
*Prozedur*, die nicht mehr im Kontext steht; nicht wahr für den *Auslöser* - `stack-dev`s „invoke
|
||||
it now" ist weiterhin ein Satz, und die `publish`-Notiz nennt den Skill bewusst nicht beim Namen.
|
||||
Zwei der drei Kettenglieder bleiben Selbstdisziplin. Der Skill sagt das jetzt selbst, statt sich
|
||||
als Garantie zu verkaufen, die er nicht ist.
|
||||
|
||||
Verifiziert: `tools/wikitool docs verify`, `tools/wikitool instructions verify`,
|
||||
`.venv/bin/python -m pytest -q` (969 passed, 2 davon neu), `release.yml` gegen den YAML-Parser
|
||||
und das Heredoc als Trockenlauf gegen eine Beispiel-Notiz.
|
||||
|
||||
---
|
||||
|
||||
## 4.6.0 - 2026-09-04 - stack-dev/stack-close skill split, publish stack-machinery note, model-selection fix (#47 Block 2)
|
||||
|
||||
**Author:** Torben Nehmer
|
||||
|
||||
+4
-1
@@ -92,6 +92,9 @@ gegen einen Commit. `.gitea/workflows/release.yml` ist Schritt 6 oben.
|
||||
Der `stack-dev`-Skill (`instructions/dev/`, nur in diesem Ursprungs-Repo vorhanden) fasst die
|
||||
Regeln für eine Sitzung, die den Stack selbst statt Wiki-Inhalt bearbeitet: wann
|
||||
Quellenbindung nicht gilt, wo Design endet und die mechanische Phase beginnt (mit dem
|
||||
Modellwechsel-Hinweis), und dass Issue-Abschluss ein Body-Rewrite ist, kein Kommentar. Siehe
|
||||
Modellwechsel-Hinweis), und endet mit dem Publish. Die Schlussphase - Issue-Body als Rewrite
|
||||
statt Kommentar, `docs/`-Veralterung, die Modell-Handover-Zeile über die ganze Sitzung - liegt
|
||||
seit `4.6.0` in einem eigenen Folge-Skill, `stack-close`, den `stack-dev` an dieser Stelle
|
||||
übergibt statt sie als weiteren eigenen Schritt zu führen. Siehe
|
||||
[instructions/dev/issue-tracking.md](instructions/dev/issue-tracking.md) für den
|
||||
Issue-Tracker selbst.
|
||||
|
||||
@@ -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
|
||||
|
||||
Coverage is measured in CI and reported, never enforced - `pytest --cov`, config in
|
||||
`tools/.coveragerc`, HTML and XML 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
|
||||
number has been watched long enough to freeze the state it actually reached.
|
||||
Coverage is measured in CI - `pytest --cov`, config in `tools/.coveragerc`, HTML and XML
|
||||
uploaded as the `coverage-<run id>` artifact of every run.
|
||||
**Fetch that artifact from the run's own page, not from the API**: `upload-artifact@v3` writes
|
||||
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/`,
|
||||
730 tests** - as reported by CI run 87, not by the local run that preceded the last commit of
|
||||
that release. Reproduce it 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).
|
||||
It is enforced at a floor of **85%** (`fail_under` in `tools/.coveragerc`), which is what a red
|
||||
suite from this axis means: coverage actually fell, not that a wrapper was added. The floor was
|
||||
set only after the number had been watched - Gitea #10 held it back for exactly that, and the
|
||||
two points between 85 and the measured 87.0% are the room the taxonomy below asks for. A
|
||||
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:
|
||||
|
||||
- **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
|
||||
evidence of a good cut, not of a missing test.
|
||||
`eval_cmd.py` (36%), `types_cmd.py` (40%), `search.py` (49%), `cli.py` (54%),
|
||||
`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
|
||||
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.
|
||||
- **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
|
||||
the reason step 2 of #10 is not a formality.
|
||||
(44%), `migrate_cmd.py` (65%), `type_resolver.py` (79%). This is the list worth reading, and
|
||||
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
|
||||
|
||||
|
||||
@@ -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
|
||||
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.
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
This does not apply to anything under `kb/`, `raw/` or `reports/`; those are committed and
|
||||
|
||||
@@ -32,8 +32,17 @@ the session knew the rule and skipped past it anyway, because nothing in the mom
|
||||
question. Splitting the phase into its own skill does not add a check either - `wikitool` still
|
||||
does not know this tracker exists and must not learn (see
|
||||
[issue-tracking.md](../issue-tracking.md) § What no tool checks) - but it removes the thing that
|
||||
was actually failing: there is no longer a "next step" to skip past, only a skill nobody has
|
||||
invoked yet. See Gitea #47 for the full incident history and the rejected alternative (a
|
||||
was actually failing: the closing *procedure* is no longer sitting in the session's context as a
|
||||
next step to run past - it exists only inside a skill someone has to invoke.
|
||||
|
||||
**Be precise about what that does and does not buy**, because the honest version is weaker than
|
||||
"now it cannot be skipped". What did **not** change is the trigger: `stack-dev`'s "invoke it now"
|
||||
is still a sentence, and `publish`'s stack-machinery note is deliberately generic enough not to
|
||||
name this skill at all. Two of the three links in that chain remain self-discipline. The split
|
||||
narrows the failure, it does not close it - treat a session that reaches this text as the
|
||||
mechanism having worked *this time*, not as proof that it always will.
|
||||
|
||||
See Gitea #47 for the full incident history and the rejected alternative (a
|
||||
model-switched subagent - not buildable in Claude Code, where a fork inherits the parent's model
|
||||
and a fresh subagent starts without the session's context).
|
||||
|
||||
|
||||
@@ -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` /
|
||||
`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
|
||||
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
|
||||
@@ -100,10 +110,23 @@ entity to entity.
|
||||
| `consumes` | — | reads the target as an artifact or data |
|
||||
| `maintains` | — | carries the upkeep of the target |
|
||||
| `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
|
||||
this thing, it is `depends-on`. `owns` versus `maintains`: accountability versus labour, and
|
||||
they are often different people.
|
||||
this thing, it is `depends-on`.
|
||||
|
||||
`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
|
||||
|
||||
@@ -135,6 +158,7 @@ Inference and comparison between ideas.
|
||||
| `contrasts` | differs from the target in a way worth reading both for |
|
||||
| `compares-with` | is weighed against the target on shared dimensions |
|
||||
| `contradicts` | asserts something the target denies |
|
||||
| `addresses` | is a response to the problem the target describes |
|
||||
| `composition` | is composed 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
|
||||
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
|
||||
|
||||
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,
|
||||
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,
|
||||
schema failures and citation/frontmatter drift. **Do not re-derive any of it by reading
|
||||
pages.**
|
||||
schema failures, citation/frontmatter drift, and edges whose label is missing, not authorised
|
||||
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
|
||||
budget slot and re-measures a corpus that has not changed. The file at step 9 overwrites this
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
profile: concepts
|
||||
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]
|
||||
entities: [operationalized-from, mechanism, procedure, applies-when, operates-on, invokes, exemplifies, 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, alternative-to, see-also]
|
||||
sources: [evidenced-by, derived-from, adapted-from, defined-in, see-also]
|
||||
comparisons: [compares-with, see-also]
|
||||
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.
|
||||
|
||||
`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.
|
||||
|
||||
## Outbound linking
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
profile: entities
|
||||
outbound:
|
||||
entities: [depends-on, required-by, runs-on, hosts, uses, produces, consumes, maintains, owns, part-of, composition, supersedes, see-also]
|
||||
concepts: [implements, exemplifies, rests-on, applies-when, operates-on, invokes, 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, authored, alternative-to, see-also]
|
||||
sources: [evidenced-by, defined-in, see-also]
|
||||
comparisons: [compares-with, see-also]
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## Outbound linking
|
||||
|
||||
@@ -5,9 +5,9 @@ tags: [researcher, ai, machine-learning, open-source]
|
||||
created: 2026-07-26
|
||||
modified: 2026-08-29
|
||||
related:
|
||||
- see-also: LLM Wiki Pattern
|
||||
- see-also: Three-Layer Architecture
|
||||
- see-also: Knowledge Compounding
|
||||
- authored: LLM Wiki Pattern
|
||||
- authored: Three-Layer Architecture
|
||||
- authored: Knowledge Compounding
|
||||
sources: [Source - LLM Wiki v2, Source - LLM Wiki Pattern]
|
||||
confidence: 0.95
|
||||
confidence_base: 0.95
|
||||
@@ -42,7 +42,7 @@ Seine ursprüngliche Einsicht - "stop re-deriving, start compiling" - bildet die
|
||||
<!-- wikitool:links -->
|
||||
## Beziehungen
|
||||
|
||||
- **see-also:** [[LLM Wiki Pattern]]
|
||||
- **see-also:** [[Three-Layer Architecture]]
|
||||
- **see-also:** [[Knowledge Compounding]]
|
||||
- **authored:** [[LLM Wiki Pattern]]
|
||||
- **authored:** [[Three-Layer Architecture]]
|
||||
- **authored:** [[Knowledge Compounding]]
|
||||
<!-- /wikitool:links -->
|
||||
|
||||
@@ -6,7 +6,7 @@ created: 2026-07-26
|
||||
modified: 2026-08-29
|
||||
related:
|
||||
- see-also: LLM Wiki Pattern
|
||||
- owns: Memex
|
||||
- authored: Memex
|
||||
sources: [Source - LLM Wiki Pattern]
|
||||
confidence: 0.90
|
||||
confidence_base: 0.90
|
||||
@@ -72,5 +72,5 @@ Laut dem Artikel [[LLM Wiki Pattern]] war Bushs Memex-Vision:
|
||||
## Beziehungen
|
||||
|
||||
- **see-also:** [[LLM Wiki Pattern]]
|
||||
- **owns:** [[Memex]]
|
||||
- **authored:** [[Memex]]
|
||||
<!-- /wikitool:links -->
|
||||
|
||||
+22
-4
@@ -11,10 +11,27 @@
|
||||
# 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.
|
||||
#
|
||||
# No `fail_under` yet, on purpose: Gitea #10 sets the threshold in a separate,
|
||||
# later commit, once the measured number exists to justify it. A threshold
|
||||
# picked before the number is either too low to bite or too high to survive the
|
||||
# next honest commit - and the second kind gets lowered rather than earned.
|
||||
# `fail_under` lives here rather than as a `--cov-fail-under` flag in the CI
|
||||
# step, so the number sits next to the reasoning that produced it and applies to
|
||||
# any `--cov` run, not just the one CI happens to write.
|
||||
#
|
||||
# 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]
|
||||
source = chemenu
|
||||
omit =
|
||||
@@ -23,3 +40,4 @@ omit =
|
||||
[report]
|
||||
show_missing = True
|
||||
precision = 1
|
||||
fail_under = 85
|
||||
|
||||
+2
-2
@@ -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 |
|
||||
| `log append --op ingest\|query\|lint\|create\|update\|delete\|rename --title "..." [--body "..."\|--body-file path]` | Append a formatted entry to `kb/log.md` |
|
||||
| `log status` | Read-only: count `ingest` entries logged since the last `lint` entry - the deterministic trigger behind the Maintenance Schedule's "every 10 sources" full-lint cadence |
|
||||
| `lint [--json] [--markdown out.md] [--full] [--fail-on-error]` | Structural + provenance checks: broken wikilinks, dangling frontmatter references, orphan pages, index drift, schema gaps, duplicate titles, title mismatches, uncovered raw files, broken `raw_files:` refs, raw files claimed by more than one source page, unmarked provenance, citation/frontmatter drift, 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** |
|
||||
| `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 |
|
||||
@@ -58,7 +58,7 @@ tools/wikitool <command> --help
|
||||
| `sources trace --raw <path>` \| `--page "<Title>"` | Trace provenance in either direction: raw file -> source page(s) -> citing pages, or page -> its sources -> their raw files |
|
||||
| `sources rebuild-index [--dry-run]` | Regenerate the `kb/provenance.md` reverse index (raw file -> source page -> citing pages) |
|
||||
| `sync [--remote origin] [--branch main] [--confirm-rebase TOKEN]` | Fetch `<remote>/<branch>` and bring the local branch up to date with it: fast-forward when the remote is simply ahead, rebase local commit(s) on top when both sides moved but touch disjoint files (a content conflict is then impossible by construction), and exit **42** for review when they touch the same file (the **rebase-review gate** - see `publish` below). Never commits, never pushes, never force-anything - no remote configured, or one that cannot be reached, is reported and skipped, not a failure. Meant to run once at the start of a writing session (`instructions/session-setup.md`) so the rest of it works against a current tree instead of discovering the drift at the final `publish` |
|
||||
| `publish --message "<op>: <desc>" [--no-push] [--confirm TOKEN] [--confirm-rebase TOKEN] [--threshold N] [--remote origin] [--branch main] [--path P ...]` | Reconcile with `<remote>/<branch>` exactly like `sync` (skipped for `--no-push`), then stage all changes, commit, and push. If the reconcile step found a still-unpushed local commit and there is nothing new to stage, that commit is pushed anyway - a previous `publish` whose push failed no longer strands it. If the push is rejected despite the pre-check (a genuine race - something landed on the remote in between), one more reconcile-and-retry is attempted before giving up; never more than one. **Mass-Update Gate:** when >= `--threshold` (default 10) *counted* files would be committed, exits **42 (`EXIT_NEEDS_CLEARANCE`)** instead of publishing - a third outcome distinct from success (0) and a validation error (1) - and prints a review report: a scale line (file count, total lines added/removed, status breakdown), only-what-applies attention notes (deletions by name, control-plane and harness-config touches, published pages, the largest single change, binaries), and every counted path grouped by area with its status and churn, generated files split out as needing no review. The token digests each counted path **and its contents** plus the publish target, so a clearance carries neither to a different file list nor to edited contents; a wrong, invented or superseded token exits 42 again with the current state. Two kinds of path are committed but never counted and never shown for approval: anything under `work/`, and the files `wikitool` generates itself (`kb/index.md`, `kb/log.md`, `kb/provenance.md`, every `INDEX.md`) - each is recomputable from the tree, so approving it decides nothing, and a routine ingest rebuilds five or six of them. The refusal line accounts for both, by reason. The gate is evaluated *before* anything is staged, so a refused publish leaves the working tree untouched. **Publish-Remote Gate:** when this checkout carries a `.wikitool-remotes.json` and the resolved push URL of `--remote` is not listed in it, exits **42** before the reconcile step even fetches - the URL is read from `git remote get-url --push`, so a repointed remote does not pass on its name. Unlike the other two gates it has **no token and no flag**: the way past it is the user adding the URL to that file, and an agent editing it to get past a refusal is opening a gate on its own initiative. Absent file means unrestricted; a malformed one is an error, not permission. See [instructions/gates.md](../instructions/gates.md) `--yes`/`-y` are gone and now fail with an explicit error. `--path` (repeatable) scopes the whole operation - gate count, staging, and commit - to a subtree. **Stack-machinery note:** after a successful commit/push whose changed files include `tools/`, `types/`, `instructions/`, `AGENTS.md`, or any `<stage>/CONTRACT.md` - the same scope a stack version bump itself covers - prints one reminder line that the phase past this point (an issue-body rewrite, `docs/` staleness, a changelog entry's accuracy) is not covered by `docs verify`, `instructions verify` or `pytest`. Not a gate: no exit code change, nothing to clear, silent for an ordinary content publish |
|
||||
| `publish --message "<op>: <desc>" [--no-push] [--confirm TOKEN] [--confirm-rebase TOKEN] [--threshold N] [--remote origin] [--branch main] [--path P ...]` | Reconcile with `<remote>/<branch>` exactly like `sync` (skipped for `--no-push`), then stage all changes, commit, and push. If the reconcile step found a still-unpushed local commit and there is nothing new to stage, that commit is pushed anyway - a previous `publish` whose push failed no longer strands it. If the push is rejected despite the pre-check (a genuine race - something landed on the remote in between), one more reconcile-and-retry is attempted before giving up; never more than one. **Mass-Update Gate:** when >= `--threshold` (default 10) *counted* files would be committed, exits **42 (`EXIT_NEEDS_CLEARANCE`)** instead of publishing - a third outcome distinct from success (0) and a validation error (1) - and prints a review report: a scale line (file count, total lines added/removed, status breakdown), only-what-applies attention notes (deletions by name, control-plane and harness-config touches, published pages, the largest single change, binaries), and every counted path grouped by area with its status and churn, generated files split out as needing no review. The token digests each counted path **and its contents** plus the publish target, so a clearance carries neither to a different file list nor to edited contents; a wrong, invented or superseded token exits 42 again with the current state. Two kinds of path are committed but never counted and never shown for approval: anything under `work/`, and the files `wikitool` generates itself (`kb/index.md`, `kb/log.md`, `kb/provenance.md`, every `INDEX.md`) - each is recomputable from the tree, so approving it decides nothing, and a routine ingest rebuilds five or six of them. The refusal line accounts for both, by reason. The gate is evaluated *before* anything is staged, so a refused publish leaves the working tree untouched. **Publish-Remote Gate:** when this checkout carries a `.wikitool-remotes.json` and the resolved push URL of `--remote` is not listed in it, exits **42** before the reconcile step even fetches - the URL is read from `git remote get-url --push`, so a repointed remote does not pass on its name. Unlike the other two gates it has **no token and no flag**: the way past it is the user adding the URL to that file, and an agent editing it to get past a refusal is opening a gate on its own initiative. Absent file means unrestricted; a malformed one is an error, not permission. See [instructions/gates.md](../instructions/gates.md) `--yes`/`-y` are gone and now fail with an explicit error. `--path` (repeatable) scopes the whole operation - gate count, staging, and commit - to a subtree. **Stack-machinery note:** after a successful commit/push whose changed files include `tools/`, `types/`, `instructions/`, `AGENTS.md`, or a path ending in `CONTRACT.md` - roughly the scope a stack version bump covers, deliberately a shade broader than CI's version gate, which matches only a `CONTRACT.md` one segment deep - prints one reminder line that the phase past this point (an issue-body rewrite, `docs/` staleness, a changelog entry's accuracy) is not covered by `docs verify`, `instructions verify` or `pytest`. Not a gate: no exit code change, nothing to clear, silent for an ordinary content publish |
|
||||
| `work new (--input <raw path> \| --key <run key>) [--again] [--dry-run]` | Scaffold `work/<runkey>/` for one workshop run: refuses a collision instead of suffixing it, and writes the required `README.md` + `plan.md`. `--input` derives the run key from the path below `raw/` (an ingest); `--key` names it outright for a run with no raw input - a migration or a sweep across `kb/` - and may not start with `ingest-`, which stays reserved for derived keys. Exactly one of the two. `--again` opens a dated second pass over a tree that has itself changed. See [work/CONTRACT.md](../work/CONTRACT.md) |
|
||||
| `work close --run-key <name> [--yes] [--dry-run]` | Delete a finished workshop. Lists what would be lost and requires `--yes`, because nothing in it is recoverable from the rest of the repo - the durable conclusions must already be in `kb/` |
|
||||
| `budget status` | Show the current session's `wikitool` call count and recent command history (never counted against the budget) |
|
||||
|
||||
@@ -136,7 +136,17 @@ ROOT_README = config.ROOT / "README.md"
|
||||
# tools/README.md is exactly the file it drifted in. INSTALL.md is here for the
|
||||
# same reason: it is human-facing prose about installing an instance, and the
|
||||
# command reference lives exactly once, in tools/CONTRACT.md.
|
||||
STAGE_READMES = ("tools/README.md", "INSTALL.md")
|
||||
#
|
||||
# DEVELOPMENT.md joined them after it drifted the same way (Gitea #47): it grew
|
||||
# a table describing what each verify command checks, which had to be removed by
|
||||
# hand because nothing compared it to anything. It is not shipped - dist_cmd
|
||||
# .ROOT_FILES excludes it - and that is not an argument against listing it here:
|
||||
# `check_readmes_have_no_command_table` skips a file that does not exist, so in
|
||||
# a distributed instance this entry is simply inert, while in the dev checkout
|
||||
# (the only place the file exists, and the only place it can drift) it is
|
||||
# checked. The name is now narrower than the tuple - these are the human-facing
|
||||
# prose docs that must not re-list commands, stage README or not.
|
||||
STAGE_READMES = ("tools/README.md", "INSTALL.md", "DEVELOPMENT.md")
|
||||
|
||||
# Docs that must not re-introduce the pre-migration bare-enum `type:` form.
|
||||
# The per-collection contracts are appended at call time, since which ones exist
|
||||
|
||||
@@ -441,13 +441,18 @@ STACK_MACHINERY_NOTE = (
|
||||
|
||||
|
||||
def touches_stack_machinery(changed_files: list[str]) -> bool:
|
||||
"""Whether `changed_files` includes a path under `version-parts.md`'s
|
||||
scope for the stack version - `tools/`, `types/`, `instructions/`,
|
||||
`AGENTS.md`, or any `<stage>/CONTRACT.md`. A publish in this class is,
|
||||
by construction of the `stack-dev`/`stack-close` split, always followed
|
||||
by the unchecked closing phase - `STACK_MACHINERY_NOTE` times a reminder
|
||||
to land exactly there, for any session, not only one that read the
|
||||
skill that names it."""
|
||||
"""Whether `changed_files` includes a path under the stack version's own
|
||||
scope - `tools/`, `types/`, `instructions/`, `AGENTS.md`, or a path ending
|
||||
in `CONTRACT.md` at any depth. A publish in this class is, by construction
|
||||
of the `stack-dev`/`stack-close` split, always followed by the unchecked
|
||||
closing phase - `STACK_MACHINERY_NOTE` times a reminder to land exactly
|
||||
there, for any session, not only one that read the skill that names it.
|
||||
|
||||
Deliberately a shade broader than CI's version gate, which matches
|
||||
`<one-segment>/CONTRACT.md` only: this decides whether to print a sentence,
|
||||
so over-matching costs a reminder nobody needed, while under-matching costs
|
||||
the reminder in the one case it was built for. The two are not the same
|
||||
predicate and should not be described as one."""
|
||||
for path in changed_files:
|
||||
if path in STACK_MACHINERY_NAMES:
|
||||
return True
|
||||
|
||||
@@ -30,6 +30,7 @@ ADVISORY_KEYS = (
|
||||
"unmarked_provenance",
|
||||
"missing_from_index",
|
||||
"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),
|
||||
"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({
|
||||
"ts": record["ts"], "call": _signature(attrs), "flag": arg,
|
||||
"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.
|
||||
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)
|
||||
class Edge:
|
||||
|
||||
@@ -181,6 +181,26 @@ def run_lint(kb_dir: Path) -> dict:
|
||||
malformed_edges: list[dict] = []
|
||||
unlabelled_edges: 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()):
|
||||
type_path = page.frontmatter.get("type")
|
||||
if not type_path:
|
||||
@@ -211,6 +231,16 @@ def run_lint(kb_dir: Path) -> dict:
|
||||
if not edge.is_labelled:
|
||||
unlabelled_edges.append({"page": title, "target": edge.target})
|
||||
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)
|
||||
if source_collection is None or target_page is None:
|
||||
continue
|
||||
@@ -301,6 +331,7 @@ def run_lint(kb_dir: Path) -> dict:
|
||||
"malformed_edges": malformed_edges,
|
||||
"unlabelled_edges": unlabelled_edges,
|
||||
"unauthorised_labels": unauthorised_labels,
|
||||
"redundant_see_also": redundant_see_also,
|
||||
"unbalanced_markers": unbalanced_marker_findings,
|
||||
"quote_limit_violations": quote_limit_violations,
|
||||
"invalid_type_paths": invalid_type_paths,
|
||||
@@ -403,6 +434,11 @@ def render_markdown(report: dict) -> str:
|
||||
report.get("unauthorised_labels", []),
|
||||
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(
|
||||
lines, "Dangling Frontmatter References", report["dangling_frontmatter_refs"],
|
||||
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.
|
||||
# `orphan_pages` is deliberately absent: many pages are validly reachable
|
||||
# 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
|
||||
# describes an unconverted page, only a broken one.
|
||||
|
||||
@@ -100,6 +100,30 @@ def test_install_md_is_checked_too(tmp_path, monkeypatch):
|
||||
assert any("`doctor`" in issue for issue in issues)
|
||||
|
||||
|
||||
def test_development_md_is_checked_too(tmp_path, monkeypatch):
|
||||
"""DEVELOPMENT.md drifted exactly this way once (Gitea #47): a table
|
||||
describing what each verify command checks, removed by hand because nothing
|
||||
compared it to anything."""
|
||||
root = tmp_path
|
||||
(root / "DEVELOPMENT.md").write_text(
|
||||
"| Command | Purpose |\n| `docs verify` | checks docs |\n", encoding="utf-8"
|
||||
)
|
||||
monkeypatch.setattr(docs_verify.config, "ROOT", root)
|
||||
monkeypatch.setattr(docs_verify, "ROOT_README", root / "README.md") # doesn't exist here
|
||||
issues = docs_verify.check_readmes_have_no_command_table()
|
||||
assert any("`docs verify`" in issue for issue in issues)
|
||||
|
||||
|
||||
def test_an_absent_listed_doc_is_skipped_not_reported(tmp_path, monkeypatch):
|
||||
"""The distributed-instance case: DEVELOPMENT.md is not shipped, so listing
|
||||
it must stay inert where the file does not exist rather than failing a tree
|
||||
that is correct."""
|
||||
root = tmp_path
|
||||
monkeypatch.setattr(docs_verify.config, "ROOT", root)
|
||||
monkeypatch.setattr(docs_verify, "ROOT_README", root / "README.md")
|
||||
assert docs_verify.check_readmes_have_no_command_table() == []
|
||||
|
||||
|
||||
def test_legacy_type_blocks_are_absent():
|
||||
assert docs_verify.check_legacy_type_blocks() == []
|
||||
|
||||
|
||||
@@ -76,6 +76,18 @@ def test_yes_is_a_finding_even_after_the_gate_refused():
|
||||
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():
|
||||
"""Being refused by one gate does not license opening a different one."""
|
||||
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."""
|
||||
assert kb_state.read_kb_version() is None
|
||||
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