Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d26698cd0 | |||
| bb097f614b | |||
| 55f65c1ab1 | |||
| 6eb3f84256 | |||
| d49513bda6 | |||
| 90ce41964f | |||
| f350999053 | |||
| 05a75065ba | |||
| ef60e2984c | |||
| c64479fe02 | |||
| c0dc2129bb | |||
| f140e26a4c | |||
| 0fb8fd6122 | |||
| dc688e5726 |
@@ -99,6 +99,7 @@ What a file is called says who it is for and how it is loaded. This is a rule, n
|
||||
| `instructions/<name>.md` | Agents | By link, or on explicit request |
|
||||
| `instructions/<name>/SKILL.md` | Agents | By the harness, once published |
|
||||
| `types/<name>.md` | Agents + validator | Via `tools/wikitool types describe`. Split by `root:`: a page type-spec (`root: kb`) belongs to the instance and ships as `.template`; one describing a stack artifact ships verbatim |
|
||||
| `types/<name>.guidance.md` | Agents + validator | Via `tools/wikitool types describe`, composed with the `types/<name>.md` it documents. Stack-owned regardless of the type-spec's own `root:` - it ships verbatim and is optional, present only where the type-spec declares `guidance:` |
|
||||
| `docs/<name>.md` | Agents and humans | By link, or on explicit request - never automatically, and never as instruction |
|
||||
| `INDEX.md` | Both | Generated - never hand-edited |
|
||||
|
||||
@@ -107,20 +108,53 @@ documents. What it may not carry is the same content twice - a README that resta
|
||||
contract is a second copy that drifts. `docs verify` enforces the specific case that already
|
||||
happened once: no README may hold a copy of the `wikitool` command table.
|
||||
|
||||
**Two languages, and which is which.** Which one a line is written in follows from the *For*
|
||||
column above - who reads it - and from nothing else: not from who owns the file, and not from
|
||||
whether it ever leaves this checkout.
|
||||
|
||||
1. **The control plane is written in English** - this file, `CLAUDE.md`, every `CONTRACT.md`,
|
||||
everything under `instructions/`, and the type-specs for non-page artifacts. Quoted
|
||||
vocabulary is not prose and stays as it is: a section name, a relationship label or a
|
||||
translated term cited as evidence. What addresses the *page* goes the other way - `kb/` pages,
|
||||
and inside a page type-spec the parts that become page text - and follows
|
||||
`kb/CONVENTIONS.md`, which is also where the instance's own terminology material is reached
|
||||
from.
|
||||
|
||||
This holds for a control-plane file an instance writes **only for itself** and never ships:
|
||||
an instruction of its own, a page type it added (`types/` takes one without a code change),
|
||||
a further stage contract. Such a file is instance-owned end to end, which settles who may
|
||||
change it, not who reads it - and the reader is still an agent. There is deliberately no
|
||||
second language value beside `kb/CONVENTIONS.md`'s `language:`, and no instance setting that
|
||||
moves this rule; [docs/language-boundaries.md](docs/language-boundaries.md) has the reasoning.
|
||||
2. **An agent speaks the instance's KB language**, whatever this file is written in. The value
|
||||
lives in `kb/CONVENTIONS.md`'s `language:` and nowhere else; an instruction that models a
|
||||
sentence for the user writes it in English like the rest of the control plane, and the agent
|
||||
says it in that language.
|
||||
|
||||
Nothing checks either mechanically - a stop-word scan would flag the quoted vocabulary above
|
||||
and miss a translated paragraph that reads cleanly. They are held up by whoever writes an
|
||||
instruction, which is why [instructions/CONTRACT.md](instructions/CONTRACT.md) § "Writing an
|
||||
instruction" names them at the step where that happens.
|
||||
|
||||
**`docs/` carries no normative sentence.** It holds why the stack is built the way it is -
|
||||
background consulted in passing, not a rule to follow; anything that would bind belongs in a
|
||||
`CONTRACT.md` instead, which is what keeps invariant 8 intact here. It carries no frontmatter,
|
||||
type, index, lint or provenance; `dist export` ships it verbatim and no other `tools/wikitool`
|
||||
command touches it.
|
||||
|
||||
Four pages exist today, each read by link rather than automatically:
|
||||
Five pages are reached from this file, each by link rather than automatically:
|
||||
[docs/pipeline-rationale.md](docs/pipeline-rationale.md) (why the pipeline has four stages),
|
||||
[docs/ownership-and-templates.md](docs/ownership-and-templates.md) (why a `.template` split
|
||||
exists, and why silent overwrite is the failure it guards against),
|
||||
[docs/why-gates-are-code.md](docs/why-gates-are-code.md) (why the four gates in
|
||||
[docs/language-boundaries.md](docs/language-boundaries.md) (why the control plane is English
|
||||
everywhere and the KB language is a value, and why the axis is the reader rather than the
|
||||
owner), [docs/why-gates-are-code.md](docs/why-gates-are-code.md) (why the four gates in
|
||||
[Gates](#gates) are code rather than instruction), and
|
||||
[docs/version-model.md](docs/version-model.md) (why a version number answers a compatibility
|
||||
question and a migration question separately).
|
||||
question and a migration question separately). A sixth,
|
||||
`docs/model-and-effort-selection.md`, is deliberately not linked here but from `CLAUDE.md`: it
|
||||
decides something only that harness has to decide, and a link here would load it into the other
|
||||
three.
|
||||
|
||||
## Personalization
|
||||
|
||||
@@ -217,6 +251,13 @@ tools/wikitool search --field entity_type=system --field '!sources'
|
||||
|
||||
`search` is read-only and exempt from the iteration budget.
|
||||
|
||||
**It is also exhaustive, so do not grep `kb/` yourself.** `search` *is* a `rg` run over `kb/`,
|
||||
enriched with each hit's frontmatter and ranked; a grep of your own can therefore surface no
|
||||
page it missed, only the generated files it deliberately excludes - `kb/index.md`, `kb/log.md`,
|
||||
`kb/provenance.md`, every `INDEX.md` - which invariant 1 forbids acting on anyway. Each hit
|
||||
carries the page's full path and full title, so it can be opened and passed to the commands
|
||||
that take a title. A result cut short by `--limit` says so and names the total.
|
||||
|
||||
## Gates
|
||||
|
||||
Four limits are enforced in code rather than by instruction, because a prompt-level limit is
|
||||
|
||||
+359
@@ -28,6 +28,13 @@ opening another: the heading's version/date/title move, and the bump's
|
||||
the entry's `**Author:**`/`**Breaking Change:**`/`**Migration:**` lines -
|
||||
written and read by `wikitool version bump`, never by hand.
|
||||
|
||||
`**Breaking Change:**` accumulates, because one candidate can cross the
|
||||
compatibility boundary more than once and each crossing is a separate thing an
|
||||
operator has to act on: one reason stays on the marker line, a second and
|
||||
further ones move to bullets beneath a bare marker. `**Migration:**` does not -
|
||||
it answers one yes/no question about the candidate as a whole, so a later
|
||||
answer replaces the earlier one.
|
||||
|
||||
That list is graded, not a flat chronological dump: each bump carries an
|
||||
impact (`--impact high|medium|low`, default `medium`), and the list renders
|
||||
grouped under `**High/Medium/Low impact**` headings - except when every bump
|
||||
@@ -52,6 +59,358 @@ concern - readable here, never shipped as something to parse.
|
||||
|
||||
---
|
||||
|
||||
## 6.0.0 - 2026-09-15 - search: Pfad und Titel vollstaendig, Trunkierung sichtbar
|
||||
|
||||
**Author:** Torben Nehmer
|
||||
|
||||
**Breaking Change:**
|
||||
- docs verify loest ab dieser Version jeden relativen Markdown-Link in den Referenzdateien auf und meldet ein totes Ziel als Fehler - auch in kb/CONVENTIONS.md und kb/<collection>/COLLECTION.md, die eine Instanz selbst besitzt und die ein Drop-in-Copy der Maschinerie nicht ersetzt. Eine Instanz, deren eigene Konventions- oder Collection-Datei einen relativen Link mit falscher ../-Tiefe oder auf eine inzwischen geloeschte Datei traegt, sieht docs verify nach dem Update fehlschlagen, wo es vorher durchlief. Reparatur: den in der Meldung genannten Datei:Zeile-Link korrigieren - kein Werkzeuglauf, keine Inhaltsmigration.
|
||||
- docs verify prueft die TOC-Region ab dieser Version auch auf types/*.md und docs/*.md. Eine Instanz, die die Page-Type-Spec-Templates adoptiert hat, traegt types/source.md und types/concept.md ohne Region und sieht docs verify nach dem Update fehlschlagen, wo es vorher durchlief; dasselbe gilt fuer eine selbst angelegte oder lokal geaenderte docs/-Seite ueber 100 Zeilen. Reparatur: tools/wikitool docs toc --apply - ein Werkzeuglauf, keine Inhaltsmigration.
|
||||
|
||||
**Migration:** none required - Keine kb/-Seite aendert ihre Form. Der Grenzuebertritt ist ein strengerer Check auf instanz-eigener Prosa, keine Schema- oder Frontmatteraenderung.
|
||||
|
||||
<!-- wikitool:bumps -->
|
||||
**High impact**
|
||||
- SKILL.md: relative Links durch repo-root-relative Pfade ersetzt, docs verify/instructions verify pruefen Linkziele
|
||||
- docs verify: der Linkziel-Check erreicht auch die instanz-eigenen kb/CONVENTIONS.md und COLLECTION.md - daher Grenzuebertritt
|
||||
- TOC-Scope auf types/ und docs/ erweitert, Sprachregeln zentralisiert, --breaking akkumuliert
|
||||
- types/: Seiten-Type-Spec-Anleitungsprosa in stackeigene guidance-Datei ausgelagert
|
||||
|
||||
**Medium impact**
|
||||
- docs verify: ein nur als .template ausgeliefertes Linkziel gilt als aufgeloest
|
||||
- Control-Plane-Sprache universell: Achse ist das Publikum, kein Instanz-Schalter
|
||||
- types/type-spec.schema.yaml enforced against real type-spec frontmatter
|
||||
- search: Pfad und Titel vollstaendig, Trunkierung sichtbar
|
||||
|
||||
**Low impact**
|
||||
- gates.md/session-setup.md: die Budget-Ausnahme von version regrade haengt an der Aufrufform
|
||||
- types/type-spec.md: Ownership und Sprache getrennt benannt (Nachzug zu #99)
|
||||
<!-- /wikitool:bumps -->
|
||||
|
||||
Ausgangspunkt war ein realer Bruch: `instructions sync` kopiert jede `SKILL.md` in eine andere
|
||||
Verzeichnistiefe, und 52 von 58 relativen Links darin zeigten in der publizierten Kopie ins Leere,
|
||||
unbemerkt, weil kein Check je ein Linkziel gelesen hat. Die Reparatur - repo-root-relative Pfade
|
||||
statt `../`-Links - zieht zwei neue mechanische Checks nach sich (`instructions verify` verbietet
|
||||
relative Links in `SKILL.md`, `docs verify` loest Linkziele in allen Referenzdateien auf), und der
|
||||
zweite Check erreicht auch instanz-eigene Prosa (`kb/CONVENTIONS.md`, `COLLECTION.md`), die ein
|
||||
Drop-in-Copy nicht ersetzt - der Grenzuebertritt, der diesen Kandidaten auf `6.0.0` eskaliert hat.
|
||||
Denselben Linkziel-Check bekommt die TOC-Pflicht gleich mit auf `types/` und `docs/` erweitert, und
|
||||
die Sprachregeln fuer die Control-Plane sind zu einer einzigen, publikumsbasierten Regel in
|
||||
`AGENTS.md` zentralisiert statt eines Instanz-Schalters. Daneben, unabhaengig vom Linkproblem: die
|
||||
Seiten-Type-Spec-Anleitungsprosa ist in eine stackeigene Guidance-Datei ausgelagert,
|
||||
`type-spec.schema.yaml` wird jetzt gegen echte Type-Spec-Frontmatter durchgesetzt, und `search`
|
||||
zeigt Pfad und Titel eines Treffers vollstaendig statt trunkiert.
|
||||
|
||||
### gates.md/session-setup.md: die Budget-Ausnahme von version regrade haengt an der Aufrufform
|
||||
|
||||
Doku-Nachzug zu `5.1.0`. Beide Dateien beschrieben die Budget-Ausnahme als feste Liste pro
|
||||
Kommandoname ("fixed allowlist"); `version regrade` ist die erste Ausnahme, die nur in *einer*
|
||||
Aufrufform liest - bar listet sie, mit Positionen schreibt sie `CHANGES.md`. Die Liste selbst
|
||||
bleibt an ihrem einen Ort (`tools/CONTRACT.md`), beide Stellen benennen jetzt aber, dass dort ein
|
||||
Eintrag pro Aufruf statt pro Namen gilt. Aufgefallen in der Schlussphase derselben Arbeit, deshalb
|
||||
ein eigener Patch-Bump: der Pfad `instructions/` liegt im Version-Gate der CI.
|
||||
|
||||
### SKILL.md: relative Links durch repo-root-relative Pfade ersetzt, docs verify/instructions verify pruefen Linkziele
|
||||
|
||||
`instructions sync` kopiert jede `SKILL.md` byteidentisch in `.agents/skills/` und
|
||||
`.claude/skills/` - eine andere Verzeichnistiefe als die Quelle, ohne deren Nachbardateien. 52 von
|
||||
58 relativen Markdown-Links in den sieben Skills zeigten deshalb in der publizierten Kopie ins
|
||||
Leere, unbemerkt, weil kein Check je ein Linkziel gelesen hat (Gitea-Meldung: ein `session-setup.md`-Read
|
||||
schlug in einer ausgelieferten Instanz fehl). Alle 58 Links sind jetzt repo-root-relative
|
||||
Klartextpfade (`instructions/session-setup.md` statt `[session-setup.md](../session-setup.md)`) -
|
||||
sie ueberleben die Kopie unveraendert, weil sie nicht von der Position der lesenden Datei abhaengen.
|
||||
`instructions/CONTRACT.md` § "A skill's outbound reference is a plain path, not a link" traegt die
|
||||
Regel.
|
||||
|
||||
Zwei neue mechanische Checks verhindern das Wiederauftreten: `instructions verify` verbietet jeden
|
||||
relativen Markdown-Link in einer `SKILL.md` (`check_skill_reference_paths`), `docs verify` loest
|
||||
jeden relativen Link in den flachen Instructions und Contracts gegen den Arbeitsbaum auf
|
||||
(`check_reference_targets`, ueber denselben Dateisatz wie `docs toc`). Nebenbei behoben:
|
||||
`instructions/dev/doc-pull-through.md` hatte zwei Links mit falscher `../`-Tiefe, unabhaengig vom
|
||||
Skill-Copy-Problem.
|
||||
|
||||
### docs verify: der Linkziel-Check erreicht auch die instanz-eigenen kb/CONVENTIONS.md und COLLECTION.md - daher Grenzuebertritt
|
||||
|
||||
Nachtraegliche Neueinstufung des Bumps darueber, kein zusaetzlicher Code. `check_reference_targets`
|
||||
laeuft ueber den Dateisatz von `docs toc`, und fuenf Dateien darin gehoeren der Instanz statt dem
|
||||
Stack: `kb/CONVENTIONS.md` und die vier `kb/<collection>/COLLECTION.md`. Ein Drop-in-Copy von
|
||||
`tools/`, `types/`, `instructions/` und `AGENTS.md` ersetzt sie nicht - ein toter relativer Link
|
||||
darin laesst `docs verify` nach dem Update fehlschlagen, wo es vorher durchlief. Genau die Form,
|
||||
die `tools/README.md` § Adding a command Schritt 5 als MAJOR-Zeile benennt ("a stricter check that
|
||||
newly fails on shipped content an instance already had"), und `instructions/dev/version-parts.md`
|
||||
entscheidet den Zweifelsfall zugunsten des Grenzuebertritts.
|
||||
|
||||
Gemessen bricht heute nichts: die zweite bekannte Instanz traegt 25 relative Links in diesen fuenf
|
||||
Dateien, davon null tote; dieses Repo ebenso. Die Einstufung folgt der Reichweite des Checks, nicht
|
||||
einem beobachteten Schaden - der Preis einer unnoetigen MAJOR ist eine Release-Notiz, der Preis
|
||||
einer MINOR, die doch bricht, ist eine Instanz mit fehlschlagendem Update-Pfad unter einer
|
||||
Versionsnummer, die Drop-in versprochen hat. Aufgefallen ist es in der Schlussphase beim Lesen der
|
||||
eigenen Regel in `tools/README.md`, nicht durch einen Check - was `docs/version-model.md` ueber
|
||||
genau diese Stelle sagt ("a person looking at the diff ... not a validator"), hat sich hier
|
||||
wiederholt.
|
||||
|
||||
### docs verify: ein nur als .template ausgeliefertes Linkziel gilt als aufgeloest
|
||||
|
||||
Defektbehebung am Check aus den beiden Bumps darueber, gefunden unmittelbar nach deren Publish.
|
||||
`check_reference_targets` meldete auf einem frisch exportierten Baum 13 tote Links - `kb/CONTRACT.md`
|
||||
neunmal, dazu `german-terminology.md`, `kb-profiles.md` und `link-taxonomy.md` - und zwar dafuer,
|
||||
dass der Export tut, was er soll. `kb/CONVENTIONS.md` und die vier `kb/<name>/COLLECTION.md` sind
|
||||
instanz-eigen: die Distribution traegt `<name>.template`, und die Instanz uebernimmt sie erst im
|
||||
Personalisierungsschritt von `instructions/setup-instance.md` durch Umbenennen. Zwischen
|
||||
`dist export` und diesem Schritt existiert die fertige Datei berechtigterweise nicht, waehrend die
|
||||
stack-eigenen Dateien sie unter ihrem kuenftigen Namen verlinken - korrekt, denn so wird sie heissen.
|
||||
|
||||
Ein Linkziel gilt jetzt auch dann als aufgeloest, wenn daneben `<ziel>.template` liegt. Die
|
||||
Ausnahme ist eng: fehlt beides, bleibt es ein Befund. Damit beschreibt der Check nicht laenger
|
||||
"noch nicht personalisiert" als "kaputter Link" - diesen Zustand meldet `doctor` unter
|
||||
`conventions` praezise und zustaendig.
|
||||
|
||||
CI war davon nie rot: der Replay in `.gitea/workflows/ci.yml` uebernimmt die Templates, bevor er
|
||||
`docs verify` aufruft, und der dokumentierte Weg in `setup-instance.md` stellt die Personalisierung
|
||||
(Schritt 5/6) ebenfalls vor die Verifikation (Schritt 13). Getroffen haette es jeden, der nach dem
|
||||
Export einmal zur Kontrolle `docs verify` aufruft. Aufgefallen ist es, weil die Verifikation des
|
||||
vorherigen Publishes den Arbeitsbaum geprueft hatte und nicht den exportierten - ausgerechnet bei
|
||||
einer Aenderung, deren ganzer Gegenstand Kopien in anderer Verzeichnistiefe sind.
|
||||
|
||||
### TOC-Scope auf types/ und docs/ erweitert, Sprachregeln zentralisiert, --breaking akkumuliert
|
||||
|
||||
Drei Straenge, ausgeloest von einer Beobachtung: manche agentengeladene Referenzdatei trug keine
|
||||
TOC, und manche Instruction war teilweise deutsch.
|
||||
|
||||
**TOC-Scope.** Die Pflicht aus `5.0.0` galt fuer `AGENTS.md`, die Stage-Contracts,
|
||||
`kb/CONVENTIONS.md`, jede `COLLECTION.md` und die flache `instructions/**.md`-Form. `docs/` und die
|
||||
Seiten-Type-Specs fielen ohne genannten Grund heraus - waehrend `SKILL.md` und die Menschendoku
|
||||
ihren Ausschlussgrund im Docstring stehen hatten, was die beiden anderen Luecken wie Absicht
|
||||
aussehen liess. `docs/` ist dabei genau der Fall, fuer den die Schwelle existiert:
|
||||
`AGENTS.md` § File naming fuehrt es als agentengeladen per Link, also am zweiten Hop. Beide sind
|
||||
jetzt drin; vier Dateien haben eine Region bekommen. `SKILL.md` bleibt die **eine** Ausnahme, und
|
||||
zwar belegt statt behauptet: die vendorte Guidance setzt den SKILL.md-Body auf die Ladeebene, die
|
||||
beim Triggern ganz gelesen wird, und richtet ihren eigenen TOC-Rat an die gebuendelten
|
||||
Referenzdateien *daneben*. Ein Type-Spec wird zwar auch ganz geladen, aber eben auch als Datei
|
||||
gelesen - deshalb traegt es eine Region, und `types describe` strippt sie aus seiner Ausgabe, weil
|
||||
dort der ganze Body ohnehin mitkommt.
|
||||
|
||||
**Sprache.** Die Regel gab es schon ("the control plane stays English"), sie stand nur in
|
||||
`kb/CONVENTIONS.md` - einer Datei, die der Instanz gehoert und die sie umschreiben darf, waehrend
|
||||
die Regel stackeigene Dateien bindet. Sie ist nach `AGENTS.md` § File naming gezogen, zusammen mit
|
||||
einer zweiten, die vorher gar nicht geschrieben stand: **ein Agent spricht die KB-Sprache der
|
||||
Instanz**. Der Wert dafuer lebt weiter in `kb/CONVENTIONS.md`s `language:`; `SOUL.md`s eigene
|
||||
`Sprache:`-Zeile war damit eine Dublette und ist weg. `instructions/setup-instance.md` - 297 Zeilen,
|
||||
die einzige vollstaendig deutsche Instruction, verbatim an jede Instanz ausgeliefert - ist
|
||||
uebersetzt, samt `description:`. Die zwei deutschen Blockquotes in den Dev-Skills sind es auch; sie
|
||||
lesen sich jetzt als englisches *Modell* der Nachricht, die der Agent in der KB-Sprache ausspricht.
|
||||
|
||||
Dieselbe Regel gilt fuer **alles, was als Template ausgeliefert wird** - eine Instanz adoptiert es,
|
||||
bevor sie ihre Sprache ueberhaupt gewaehlt hat. `USER.md.template`, `SOUL.md.template` und
|
||||
`ENVIRONMENT.md.template` waren vollstaendig deutsch und sind uebersetzt; `kb/sources/` und
|
||||
`kb/concepts/COLLECTION.md` waren es in Teilen und ziehen jetzt mit `kb/entities/` und
|
||||
`kb/comparisons/` gleich, die es laengst waren. Bei den vier Seiten-Type-Specs laeuft der Schnitt
|
||||
mitten durch die Datei, und zwar entlang derselben Prosa/Identifier-Grenze, die `kb/CONTRACT.md`
|
||||
schon *innerhalb* einer Seite zieht: die Anleitungsprosa ist Anweisung an einen Agenten und damit
|
||||
Control Plane, der `## Template`-Block und die `layout:`-Titel sind Seitentext und bleiben in der
|
||||
KB-Sprache - `wikitool new entity` scaffoldet also weiter deutsche Ueberschriften.
|
||||
`kb/CONVENTIONS.md` behauptete bis hierher, die Type-Specs folgten als Ganzes der KB-Sprache; der
|
||||
Satz ist auf den tatsaechlichen Schnitt nachgezogen.
|
||||
Mechanisch geprueft wird nichts davon: ein Stoppwort-Scan schluege auf dem zitierten Vokabular in
|
||||
`kb-profiles.md` und `link-taxonomy.md` falsch an. Stattdessen nennen
|
||||
`instructions/CONTRACT.md` § "Writing an instruction" und `stack-dev` die Regel an der Stelle, an
|
||||
der sie befolgt oder verloren wird.
|
||||
|
||||
**`--breaking` akkumuliert.** Bis hierher ersetzte ein zweites `--breaking` die Zeile des
|
||||
Kandidaten - der Eintrag versprach dann einen Bruch und lieferte zwei. Genau dieser Kandidat ist der
|
||||
Fall: sein Linkziel-Uebertritt aus `beta.1` und der TOC-Uebertritt von hier sind zwei Dinge, auf die
|
||||
ein Betreiber getrennt reagieren muss. Eine Begruendung bleibt flach auf der Markerzeile, ab der
|
||||
zweiten werden es Bullets; eine vor dieser Aenderung geschriebene Einzelzeile liest sich unveraendert
|
||||
als Ein-Element-Liste zurueck, also musste kein bestehender Eintrag angefasst werden. `--migration:`
|
||||
bleibt bewusst eine Einzelzeile - sie beantwortet eine Ja/Nein-Frage ueber den Kandidaten als Ganzes,
|
||||
und `--migration-required` ist ihr Ruecknahmepfad. Fuer eine falsche Breaking-Begruendung gibt es
|
||||
keinen; der Kandidat ist bis zum Release dev-lokal.
|
||||
|
||||
Nebenbefund, den die Scope-Erweiterung sofort aufgedeckt hat: `docs/version-model.md` verlinkte nach
|
||||
`instructions/dev/version-parts.md`, das `dist export` wegschneidet - im Ursprungs-Repo gruen, in
|
||||
jeder ausgelieferten Instanz ein toter Link. Jetzt ein Klartextpfad mit dem Satz, warum er keiner
|
||||
ist.
|
||||
|
||||
### types/type-spec.md: Ownership und Sprache getrennt benannt (Nachzug zu #99)
|
||||
|
||||
`types/type-spec.md` sagte weiterhin, Prosa, `## Template`-Body **und Sprache** eines
|
||||
Seiten-Type-Specs gehoerten der Instanz, die bei anderer KB-Sprache "einfach die Datei
|
||||
uebersetzt" - genau das Gegenteil des Schnitts, den der Bump davor ausgeliefert hat. Aufgefallen
|
||||
in der Schlussphase, beim Nachdenken darueber, welche Sprachregel fuer einen *instanz-eigenen*
|
||||
neuen Seitentyp gilt.
|
||||
|
||||
Der Abschnitt trennt die zwei Fragen jetzt: Ownership sagt, wer eine Zeile aendern darf, die
|
||||
Sprache folgt davon unabhaengig dem Publikum der Zeile - Anleitungsprosa an einen Agenten ist
|
||||
Control Plane und englisch, `## Template`-Body und `layout:`-Titel sind Seitentext in der
|
||||
KB-Sprache, Feldnamen und Enum-Werte sind Identifier und werden nie uebersetzt. Als Tabelle, weil
|
||||
der Schnitt mitten durch eine Datei laeuft und eine Aufzaehlung im Fliesstext ihn genau deshalb
|
||||
nicht haelt. Der Satz bindet ausdruecklich auch einen Type-Spec, den eine Instanz sich selbst
|
||||
schreibt: der ist zwar durchgaengig instanzeigen, aber seine Anleitungshaelfte hat trotzdem einen
|
||||
Agenten als Leser.
|
||||
|
||||
### Control-Plane-Sprache universell: Achse ist das Publikum, kein Instanz-Schalter
|
||||
|
||||
Die Sprachregel in `AGENTS.md` ruhte auf einer Begruendung, die schmaler war als sie selbst:
|
||||
"Every file in the table above belongs to the stack and **ships to instances** that share none of
|
||||
this instance's language choices, so:". Das traegt nur fuer ausgeliefertes Material und laesst
|
||||
offen, was fuer ein Control-Plane-Dokument gilt, das eine Instanz nur fuer sich selbst schreibt -
|
||||
eine eigene Instruction, ein selbst angelegter Seitentyp (`types/` nimmt einen ohne Code-Aenderung
|
||||
auf), ein weiterer Stage-Contract. Genau dort fallen Ownership und Publikum auseinander: die Datei
|
||||
ist durchgaengig instanzeigen, ihre Anleitungshaelfte hat trotzdem einen Agenten als Leser.
|
||||
|
||||
Der Vorsatz nennt jetzt die tatsaechliche Achse - die *For*-Spalte der Tabelle darueber, also wer
|
||||
die Zeile liest, und weder wem die Datei gehoert noch ob sie den Checkout je verlaesst. Regel 1
|
||||
sagt ausdruecklich, dass sie auch fuer ein nie ausgeliefertes Control-Plane-Dokument gilt und dass
|
||||
es neben `kb/CONVENTIONS.md`s `language:` bewusst keinen zweiten Sprachwert gibt.
|
||||
`kb/CONVENTIONS.md` und ihr `.template` sagen dasselbe von ihrer Seite aus: die
|
||||
Control-Plane-Sprache ist keine Einstellung, die diese Datei zurueckhaelt - es ist gar keine.
|
||||
|
||||
Die Begruendung dazu steht als neue `docs/`-Seite
|
||||
([docs/language-boundaries.md](docs/language-boundaries.md)), weil sie sonst in einem Jahr neu
|
||||
verhandelt wird: warum Englisch (der Stack redet fast nur ueber Identifier, und die sind
|
||||
englisch), warum kein Parameter (die Kosten traegt jede Datei, den Nutzen haette ein Dokument, das
|
||||
ohnehin nur ein Agent liest), und was die Entscheidung wieder aufmachen wuerde. Die Seite haelt
|
||||
zugleich fest, welches Argument falsch war: "Sprache folgt der Ownership" hat funktioniert,
|
||||
solange nur ausgeliefertes Material betrachtet wurde, und faellt am instanz-eigenen Typ.
|
||||
|
||||
Nebenbei zwei Befunde derselben Ecke behoben. Der Docstring von
|
||||
`dist_cmd.instance_owned_type_stems()` behauptete weiter, "its prose, its template and its
|
||||
**language** are the instance's business" - Stand vor dem TOC-/Sprach-Bump oben. Und die
|
||||
Aufzaehlung der `docs/`-Seiten in `AGENTS.md` sagte "Four pages exist today", waehrend das
|
||||
Verzeichnis fuenf trug: `docs/model-and-effort-selection.md` fehlte, und zwar absichtlich, weil
|
||||
ein Link dorthin die Claude-Code-eigene Entscheidung in die anderen drei Harnesses laden wuerde.
|
||||
Der Satz zaehlt jetzt, was von hier aus verlinkt ist, und benennt die sechste Seite samt Grund.
|
||||
|
||||
### types/: Seiten-Type-Spec-Anleitungsprosa in stackeigene guidance-Datei ausgelagert
|
||||
|
||||
Ein `root: kb` Type-Spec (`entity`, `concept`, `source`, `comparison`) hatte zwei Publika in
|
||||
einer Datei: Anleitungsprosa fuer den Agenten (When to use/When NOT to use/Authoring guidance),
|
||||
und Seitenmaterial (`## Template`-Block, `layout:`-Titel). Ownership gilt pro Datei, also wurde
|
||||
die ganze Datei beim Setup als `.template` adoptiert und danach nie wieder angefasst - eine
|
||||
Instanz, die ihre Type-Specs frueh adoptiert hat, las bis in alle Zukunft die Anleitung vom Tag
|
||||
ihrer Erzeugung, weil `dist upgrade` das `.template` neben die adoptierte Datei schrieb, nie die
|
||||
Datei selbst (`docs/ownership-and-templates.md` § "Where the file boundary strains").
|
||||
|
||||
Der urspruengliche Vorschlag drehte den Schnitt um (Type-Spec stackeigen, Seitenmaterial heraus)
|
||||
und wurde beim Pruefen gegen `setup-instance.md` und `evolve-subtypes.md` verworfen: die
|
||||
Frontmatter-Konfiguration (`layout:`, Enum-Werte, `base_dir`) ist instanzeigener Inhalt, keine
|
||||
Stack-Maschinerie - beide Instructions weisen die Instanz an, Enum und `layout:`-Eintrag in
|
||||
derselben Aenderung zu setzen. Stattdessen bleibt der Type-Spec instanzeigen, und nur die
|
||||
maschinenabgeleitete Anleitungsprosa zieht in eine neue, stackeigene `types/<name>.guidance.md`,
|
||||
verknuepft ueber ein optionales `guidance:`-Frontmatterfeld (neuer, nicht instanziierbarer Typ
|
||||
`type-guidance`, wie `lint-report` ohne `base_dir:`). `tools/wikitool types describe <name>`
|
||||
komponiert beide Haelften weiterhin zu einer Antwort - ein Agent muss nie wissen, dass ein Typ aus
|
||||
zwei Dateien besteht. `type_resolver.extract_template()` liest das Template unveraendert allein
|
||||
aus `types/<name>.md`; kein zweiter Ladepfad fuer `wikitool new`.
|
||||
|
||||
`dist_cmd._plan_types()`/`find_leaks()` teilten sich vorher `name.split(".", 1)[0]` als
|
||||
Stamm-Berechnung - beides haette `entity.guidance.md` faelschlich als instanzeigenen Stamm
|
||||
"entity" erkannt (die eine haette sie zum `.template` gemacht, die andere sie als Leak gemeldet).
|
||||
Neuer gemeinsamer Prädikat `_owned_type_stem()` prueft die exakte Endung (`<stem>.md` oder
|
||||
`<stem>.schema.yaml`), nicht den ersten Punkt.
|
||||
|
||||
Grenzuebertritt-Frage bewusst geprueft und verneint: Drop-in in beide Richtungen (ein Type-Spec
|
||||
ohne `guidance:` verhaelt sich unveraendert, eine alte Maschinerie liest `types/<name>.md` wie
|
||||
zuvor und die Guidance-Datei ist fuer sie inert), also `--minor` statt `--major`. Die einmalige
|
||||
Adoption in einer bestehenden Instanz ist als `instructions/migrations/6.0.0-type-guidance-split.md`
|
||||
dokumentiert - `obligation: offered`, der erste Gebrauch dieses seit 4.0.0 existierenden, bis jetzt
|
||||
unbenutzten Mechanismus fuer ein instanzeigenes, upgradebares Machinery-File.
|
||||
|
||||
Verifiziert: `tools/wikitool docs verify`/`instructions verify` gruen, 1261 Tests gruen (8 neu:
|
||||
`get_guidance`, das Template bleibt auf `types/<name>.md` allein geladen, die Guidance-Datei
|
||||
schifft verbatim neben einem `.template`-adoptierten Type-Spec statt als weiteres `.template`,
|
||||
ein `dist upgrade` schreibt verbesserte Guidance-Prosa in eine adoptierte Instanz obwohl deren
|
||||
Type-Spec selbst nie im Stamp stand, `types describe` komponiert beide Haelften in JSON und
|
||||
Textausgabe getrennt nachweisbar).
|
||||
|
||||
### types/type-spec.schema.yaml enforced against real type-spec frontmatter
|
||||
|
||||
Bei der Vorbereitung der Aenderung oben fiel auf: `types/type-spec.schema.yaml` traegt
|
||||
`additionalProperties: false`, kannte aber `root:` und `capture_fields:` nicht, obwohl
|
||||
`types/instruction.md` bzw. `types/source.md` beide Felder tragen und `type_resolver.get_root()`/
|
||||
`get_capture_fields()` sie lesen. Gegen das Schema validiert waeren beide Type-Specs ungueltig
|
||||
gewesen. Dass es niemandem auffiel, war der eigentliche Befund: Type-Spec-Frontmatter wurde
|
||||
nirgends gegen sein eigenes Schema validiert - `resolver.validate_frontmatter()` lief nur ueber
|
||||
`kb/`-Seiten, neu erzeugte Seiten und Instruktionsdateien, nie ueber die Type-Specs selbst.
|
||||
`TypeResolver._validate_type_spec()`, der einzige Weg, den der Selbstbezug `type: types/type-spec.md`
|
||||
nimmt, prueft ausschliesslich, ob `type`/`name`/`description` vorhanden sind.
|
||||
|
||||
Beide fehlenden Felder ergaenzt (`root:` als Enum `kb`/`repo`, `capture_fields:` als Liste wie
|
||||
`page_ref_fields:`), dazu `guidance:` (seit der Aenderung oben real benutzt, aber noch nie im
|
||||
Schema). `docs verify` bekommt eine neue Pruefung: jede Datei unter `types/` mit
|
||||
`type: types/type-spec.md` validiert jetzt gegen `types/type-spec.schema.yaml`
|
||||
(`check_type_spec_frontmatter()`, wiederverwendet `resolver.list_type_specs()` statt eines zweiten
|
||||
Parse-Durchlaufs). `types/type-spec.md` § Validation Contract und die beiden `docs verify`-Zeilen
|
||||
in `tools/CONTRACT.md` nennen das jetzt.
|
||||
|
||||
Daneben ein zweiter, unabhaengiger Befund derselben Aufraeumrunde behoben:
|
||||
`instructions/dev/doc-pull-through.md` verwies fuer `docs/`-Seiten weiter auf "AGENTS.md § File
|
||||
naming lists all four" - der Zaehler in AGENTS.md selbst war beim vorigen Bump schon auf fuenf
|
||||
(plus eine sechste, nur von `CLAUDE.md` aus verlinkte) korrigiert worden, diese eine verbliebene
|
||||
Stelle nicht.
|
||||
|
||||
Grenzuebertritt-Frage geprueft und verneint: additiv in beide Richtungen - eine bestehende Instanz
|
||||
validiert bereits (0 Befunde gegen den realen Baum), und ein Type-Spec ohne die drei neuen Felder
|
||||
bleibt unveraendert gueltig. `--patch`, kein `--breaking`, keine neue Migration noetig.
|
||||
|
||||
Verifiziert: `tools/wikitool docs verify`/`instructions verify` gruen, 1263 Tests gruen (2 neu:
|
||||
alle Type-Specs dieses Repos validieren gegen ihr eigenes Schema; ein Type-Spec mit einem dem
|
||||
Schema unbekannten Feld wird gemeldet, mit Dateiname und Feldname in der Meldung).
|
||||
|
||||
### search: Pfad und Titel vollstaendig, Trunkierung sichtbar
|
||||
|
||||
Gemeldet wurde eine Sitzung, die nach `wikitool search` zusaetzlich `grep -rl` ueber `kb/`
|
||||
laufen liess. Der Grep war redundant - `search` *ist* ein `rg`-Lauf ueber `kb/` und kann keine
|
||||
Seite verfehlen, die ein Grep findet -, aber die Ausgabe gab ihr drei Gruende dafuer, und die
|
||||
sind der eigentliche Befund.
|
||||
|
||||
Die Tabelle nannte **keinen Pfad**, obwohl `wiki-query` verlangt, nur die Seiten zu lesen, auf
|
||||
die die Suche zeigt. Sie kappte ausserdem den **Titel** auf 34 Zeichen - im gemeldeten
|
||||
Transkript vier von fuenf Treffern -, und der Titel ist nach Invariante 2 der einzige
|
||||
Identifier einer Seite und das Argument, das `touch`, `xref add` und `cite add` nehmen. Die
|
||||
Sitzung hatte also weder etwas zum Oeffnen noch etwas zum Weiterreichen; `grep -rl` lieferte
|
||||
genau beides.
|
||||
|
||||
Drittens war `N result(s).` die *gekappte* Zahl: `run_search` gab nur die beschnittene Liste
|
||||
zurueck, also konnte kein Adapter die Gesamtzahl melden, und `20 result(s).` auf einer Anfrage
|
||||
mit 182 Treffern war von einem vollstaendigen Ergebnis nicht zu unterscheiden. Eine
|
||||
Vollstaendigkeitsaussage, zu der die Ausgabe nicht berechtigt war - der staerkste denkbare
|
||||
Anlass, ihr zu misstrauen.
|
||||
|
||||
Die Zeile hat jetzt die Form `score | kind/subtype | titel | pfad | summary`, ohne
|
||||
Spaltenauffuellung. Titel und Pfad werden nie gekappt; die Summary ist das einzige verlustige
|
||||
Feld und steht deshalb am Ende, wo ein `|` in Prosa beim Trennen mit `maxsplit=4` folgenlos
|
||||
bleibt (ein `|` im Titel schliesst die Wikilink-Syntax ohnehin aus). JSON als Default-Ausgabe
|
||||
wurde erwogen und verworfen: ein Treffer ist flach, JSON kostet dafuer ein Vielfaches an Tokens,
|
||||
und `search` existiert dafuer, Retrieval billig zu machen - der Fehler war ein fehlendes Feld,
|
||||
kein Parse-Problem. Wer Struktur braucht, hat `--json`, `api.search` und MCP.
|
||||
|
||||
`run_search` gibt jetzt ein `SearchResult` mit Treffern, Gesamtzahl und Limit zurueck. Die
|
||||
Tabelle schreibt `50 of 182 result(s) - raise --limit (0 for all) or narrow the query.`, das
|
||||
JSON traegt `total`/`truncated`/`limit` neben `count`, dessen Bedeutung unveraendert bleibt
|
||||
(`len(results)`), und `api.search` sowie der MCP-`search`-Tool tragen dieselben Felder. Das
|
||||
Default-Limit steigt von 20 auf 50 und liegt als eine Konstante `DEFAULT_LIMIT` statt als drei
|
||||
Literale in drei Adaptern: gekappt wurden bisher vor allem die *strukturellen* Sweeps
|
||||
(`--field '!sources'`), die alphabetisch und nicht nach Relevanz sortiert sind, wo die Kappung
|
||||
also eine beliebige Scheibe der Antwort wegwirft statt ihres schwaechsten Endes. Sichtbar zu
|
||||
sein ist es, was ein endliches Default ueberhaupt erst unbedenklich macht.
|
||||
|
||||
`AGENTS.md` § Routing traegt die Regel an genau einer Stelle - `search` ist erschoepfend, ein
|
||||
eigener Grep ueber `kb/` fuegt nur die generierten Dateien hinzu, die Invariante 1 ohnehin
|
||||
verbietet; `tools/CONTRACT.md` traegt daneben nur den Mechanismus.
|
||||
|
||||
Grenzuebertritt-Frage geprueft und verneint: kein Flag entfernt oder umbenannt, keine
|
||||
Umgebungsvariable, keine maschinengelesene Datei in ihrer Form veraendert, JSON rein additiv.
|
||||
Die Tabelle liest ein Agent, kein Skript, und ihre Aenderung verlangt keiner Instanz Handarbeit
|
||||
ab.
|
||||
|
||||
Verifiziert: `docs verify`/`instructions verify` gruen, 1272 Tests gruen (9 neu: Pfad vorhanden;
|
||||
Titel und Pfad ungekappt bei langem Titel; eine Trefferzeile zerfaellt trotz `|` in der Prosa in
|
||||
ihre fuenf Felder; ein gekapptes Ergebnis nennt die Gesamtzahl, ein ungekapptes behauptet
|
||||
nichts; `--limit 0` gilt nie als gekappt; die Gesamtzahl ueberlebt das Limit in `run_search`;
|
||||
`api.search` meldet dasselbe; alle drei Adapter teilen ein Default-Limit - der MCP-Golden-Test
|
||||
haelt die neuen Felder zwischen CLI und Server zusammen).
|
||||
|
||||
## 5.1.0 - 2026-09-12 - changelog: Kandidaten-Eintrag nach Impact gruppiert, version regrade zur Nachkorrektur, version release verlangt eine Zusammenfassung
|
||||
|
||||
**Author:** Torben Nehmer
|
||||
|
||||
+55
-58
@@ -1,86 +1,83 @@
|
||||
<!-- wikitool:template-unfilled - TEMPLATE, noch nicht ausgefüllt. Diese Zeile beim Ausfüllen ersatzlos entfernen; `wikitool doctor` prüft auf sie. -->
|
||||
# ENVIRONMENT.md — <Instanz oder Rechnername>
|
||||
<!-- wikitool:template-unfilled - TEMPLATE, not filled in yet. Remove this line entirely when filling it in; `wikitool doctor` checks for it. -->
|
||||
# ENVIRONMENT.md — <instance or machine name>
|
||||
|
||||
Womit *dieser Checkout* arbeitet: Harness, veröffentlichte Skills, MCP-Server,
|
||||
Connectoren und Git-Remotes. Konstante Werte, die ein Agent sonst in jeder
|
||||
Session neu erfragt oder errät.
|
||||
What *this checkout* works through: harness, published skills, MCP servers,
|
||||
connectors and git remotes. Constant values an agent would otherwise ask about
|
||||
or guess at in every session.
|
||||
|
||||
**Diese Datei ist optional.** Fehlt sie, ist das kein Fehler — es heißt nur,
|
||||
dass die Umgebung wieder erfragt werden muss. `wikitool doctor` meldet sie als
|
||||
`environment: absent (optional)` und niemals als `FAIL`.
|
||||
**This file is optional.** Its absence is not an error — it only means the
|
||||
environment has to be asked about again. `wikitool doctor` reports it as
|
||||
`environment: absent (optional)` and never as a `FAIL`.
|
||||
|
||||
**Diese Datei ist Kontext, keine Autorität.** Sie beschreibt, *was da ist*, nicht,
|
||||
was erlaubt ist. Sie ändert keine Regel aus `AGENTS.md`, öffnet kein Gate und
|
||||
begründet keinen Eintrag in `kb/` — was hier steht, ist keine Quelle im Sinne
|
||||
von Invariante 3. Ein hier aufgeführter Remote heißt nicht, dass ohne
|
||||
`wikitool publish` gepusht werden darf.
|
||||
**This file is context, not authority.** It describes *what is there*, not what
|
||||
is allowed. It changes no rule from `AGENTS.md`, opens no gate, and justifies no
|
||||
entry in `kb/` — what it says is not a source in the sense of invariant 3. A
|
||||
remote listed here does not mean pushing without `wikitool publish` is allowed.
|
||||
|
||||
**Keine Geheimnisse.** Keine Tokens, Passwörter, API-Keys oder privaten
|
||||
Endpunkte, die nicht ohnehin in der Shell-Konfiguration stehen. Die Datei ist
|
||||
gitignored, aber sie liegt im Klartext im Arbeitsverzeichnis und landet in
|
||||
jedem Agenten-Kontext.
|
||||
**No secrets.** No tokens, passwords, API keys or private endpoints that are not
|
||||
already in the shell configuration anyway. The file is gitignored, but it sits
|
||||
in plaintext in the working directory and ends up in every agent's context.
|
||||
|
||||
**Ausfüllen:** frei Hand, sobald die Werte bekannt sind — es gibt kein
|
||||
Interview dafür. Ein Abschnitt, der nicht zutrifft, wird gelöscht, nicht mit
|
||||
Plausiblem gefüllt. Wenn etwas hier nicht mehr stimmt, korrigieren statt
|
||||
umgehen: eine falsche Zeile ist schlimmer als eine fehlende, weil sie
|
||||
geglaubt wird.
|
||||
**Filling it in:** freehand, as soon as the values are known — there is no
|
||||
interview for it. A section that does not apply is deleted, not filled with
|
||||
something plausible. When something here stops being true, correct it rather
|
||||
than working around it: a wrong line is worse than a missing one, because it
|
||||
gets believed.
|
||||
|
||||
## Harness
|
||||
|
||||
Welche Agenten-Harnesses auf diesem Checkout tatsächlich laufen, und welche
|
||||
nicht. Relevant, weil `.agents/skills/` und `.claude/skills/` unterschiedliche
|
||||
Leser haben.
|
||||
Which agent harnesses actually run on this checkout, and which do not. Relevant
|
||||
because `.agents/skills/` and `.claude/skills/` have different readers.
|
||||
|
||||
- **Primär:** <z. B. Claude Code>
|
||||
- **Daneben im Einsatz:** <z. B. Codex CLI, GitHub Copilot CLI, Mistral Vibe — oder streichen>
|
||||
- **Nicht im Einsatz:** <was bewusst nicht benutzt wird, damit niemand es vorschlägt>
|
||||
- **Primary:** <e.g. Claude Code>
|
||||
- **Also in use:** <e.g. Codex CLI, GitHub Copilot CLI, Mistral Vibe — or delete>
|
||||
- **Not in use:** <what is deliberately not used, so nobody proposes it>
|
||||
|
||||
## Skills
|
||||
|
||||
Nur was von der veröffentlichten Liste abweicht — der Normalfall (`wiki-ingest`,
|
||||
`wiki-query`, `wiki-manage`, `wiki-lint`, `wiki-status`) steht in `AGENTS.md`
|
||||
und gehört nicht noch einmal hierher.
|
||||
Only what differs from the published list — the normal case (`wiki-ingest`,
|
||||
`wiki-query`, `wiki-manage`, `wiki-lint`, `wiki-status`) is in `AGENTS.md` and
|
||||
does not belong here a second time.
|
||||
|
||||
- **Zusätzlich vorhanden:** <z. B. stack-dev in der Entwickler-Instanz>
|
||||
- **Bekannt fehlend:** <z. B. noch nicht gesynct, Harness neu gestartet nötig — oder streichen>
|
||||
- **Additionally present:** <e.g. stack-dev in the developer instance>
|
||||
- **Known missing:** <e.g. not synced yet, harness restart needed — or delete>
|
||||
|
||||
## MCP-Server
|
||||
## MCP servers
|
||||
|
||||
Welche MCP-Server in diesem Checkout erreichbar sind und wofür sie zuständig
|
||||
sind. Ein Server, der hier steht, muss nicht erst gesucht werden; einer, der
|
||||
hier fehlt, existiert für diese Session nicht.
|
||||
Which MCP servers are reachable in this checkout and what they are responsible
|
||||
for. A server listed here does not have to be looked for first; one missing
|
||||
here does not exist for this session.
|
||||
|
||||
| Server | Wofür | Anmerkung |
|
||||
|--------|-------|-----------|
|
||||
| `<name>` | <z. B. Issues, CI-Runs, Releases> | <z. B. bevorzugt gegenüber curl> |
|
||||
| Server | For what | Note |
|
||||
|--------|----------|------|
|
||||
| `<name>` | <e.g. issues, CI runs, releases> | <e.g. preferred over curl> |
|
||||
|
||||
## Connectoren und Integrationen
|
||||
## Connectors and integrations
|
||||
|
||||
Alles, was kein MCP-Server ist, aber trotzdem an dieser Instanz hängt:
|
||||
Dokument-Connectoren, Chat-Anbindungen, Notiz-Systeme.
|
||||
Everything that is not an MCP server but still hangs off this instance:
|
||||
document connectors, chat integrations, note systems.
|
||||
|
||||
- <z. B. Obsidian-Vault unter ~/..., liest kb/ read-only — oder streichen>
|
||||
- <e.g. Obsidian vault under ~/..., reads kb/ read-only — or delete>
|
||||
|
||||
## Git-Remotes
|
||||
## Git remotes
|
||||
|
||||
Wohin dieser Checkout veröffentlicht, und was sonst noch als Remote eingetragen
|
||||
ist. `wikitool publish` und `wikitool sync` sprechen genau einen davon an.
|
||||
Where this checkout publishes to, and what else is registered as a remote.
|
||||
`wikitool publish` and `wikitool sync` address exactly one of them.
|
||||
|
||||
| Remote | URL | Rolle |
|
||||
|--------|-----|-------|
|
||||
| `origin` | <URL> | <z. B. Publish-Ziel, CI läuft dort> |
|
||||
| Remote | URL | Role |
|
||||
|--------|-----|------|
|
||||
| `origin` | <URL> | <e.g. publish target, CI runs there> |
|
||||
|
||||
## CI
|
||||
|
||||
Wo die Pipeline läuft und wie ihre Läufe gelesen werden — nicht *was* sie
|
||||
prüft, das steht in `.gitea/workflows/`.
|
||||
Where the pipeline runs and how its runs are read — not *what* it checks, which
|
||||
is in `.gitea/workflows/`.
|
||||
|
||||
- **Läuft auf:** <z. B. Gitea Actions, Runner-Label linux-docker — oder streichen>
|
||||
- **Läufe lesen über:** <z. B. den Gitea-MCP-Server, nicht curl>
|
||||
- **Runs on:** <e.g. Gitea Actions, runner label linux-docker — or delete>
|
||||
- **Runs read via:** <e.g. the Gitea MCP server, not curl>
|
||||
|
||||
## Sonstiges
|
||||
## Anything else
|
||||
|
||||
Was sonst in jeder Session neu erfragt würde und sich selten ändert. Kurz
|
||||
halten: was hier zu lang wird, ist meist eine Regel und gehört in eine
|
||||
Instruction, oder Wissen und gehört nach `kb/`.
|
||||
Whatever else would be asked about in every session and rarely changes. Keep it
|
||||
short: what grows long here is usually a rule, and belongs in an instruction, or
|
||||
knowledge, and belongs in `kb/`.
|
||||
|
||||
@@ -89,10 +89,12 @@ chemenu/
|
||||
│ └── assets/
|
||||
├── types/ # SCHEMA: the global type surface. Not a collection
|
||||
│ ├── type-spec.md # Root contract: anatomy, placement, adding a type
|
||||
│ ├── entity.md # Entity type contract + template (+ .schema.yaml)
|
||||
│ ├── concept.md # Concept type contract + template
|
||||
│ ├── source.md # Source type contract + template
|
||||
│ ├── comparison.md # Comparison type contract + template
|
||||
│ ├── type-guidance.md # Contract for the *.guidance.md files below
|
||||
│ ├── entity.md # Entity type config + template (+ .schema.yaml)
|
||||
│ ├── entity.guidance.md # Its stack-owned authoring prose, shipped verbatim
|
||||
│ ├── concept.md # Concept type config + template (+ .guidance.md)
|
||||
│ ├── source.md # Source type config + template (+ .guidance.md)
|
||||
│ ├── comparison.md # Comparison type config + template (+ .guidance.md)
|
||||
│ ├── instruction.md # Instruction type - lives outside kb/ via `root: repo`
|
||||
│ └── lint-report.md # Contract-only: describes reports/, owns no directory
|
||||
├── kb/ # OUTPUT: compiled knowledge. A namespace, not a collection
|
||||
|
||||
@@ -62,7 +62,6 @@ Optionsliste.
|
||||
- **Register:** inhaltlich klar, direkt; technische Präzision vor Höflichkeitsfloskeln
|
||||
- **Länge:** kurz per Default, lang nur wenn der Inhalt es rechtfertigt
|
||||
- **Form:** Fließtext zuerst; Tabellen nur für echte Vergleiche, nicht als Dekoration
|
||||
- **Sprache:** Deutsch als Standard, wenn auf Deutsch geschrieben wird
|
||||
- **Humor:** trocken, sparsam, nie auf Kosten des Nutzers — ein Schreiber, der
|
||||
gelegentlich eine Randnotiz macht, aber die Akte nicht zur Bühne erklärt
|
||||
|
||||
|
||||
+40
-43
@@ -1,85 +1,82 @@
|
||||
<!-- wikitool:template-unfilled - TEMPLATE, noch nicht ausgefüllt. Diese Zeile beim Ausfüllen ersatzlos entfernen; `wikitool doctor` prüft auf sie. -->
|
||||
# SOUL.md — <Persona-Name>
|
||||
<!-- wikitool:template-unfilled - TEMPLATE, not filled in yet. Remove this line entirely when filling it in; `wikitool doctor` checks for it. -->
|
||||
# SOUL.md — <persona name>
|
||||
|
||||
`AGENTS.md` legt fest, *was* zu tun ist (Pipeline, Invarianten, Gates, Tools).
|
||||
Diese Datei legt fest, *wie* gute Arbeit an diesem Wiki aussieht. Wo beides
|
||||
kollidiert, gewinnt `AGENTS.md` — diese Datei ändert nie eine Regel, nur den
|
||||
Ton, in dem sie befolgt wird.
|
||||
`AGENTS.md` sets out *what* to do (pipeline, invariants, gates, tools). This
|
||||
file sets out *what good work on this wiki looks like*. Where the two collide,
|
||||
`AGENTS.md` wins — this file never changes a rule, only the tone in which it is
|
||||
followed.
|
||||
|
||||
**Ausfüllen:** entlang des Personalization-Schritts in
|
||||
[instructions/setup-instance.md](instructions/setup-instance.md). Der
|
||||
Persona-Name ist eine Entscheidung des Nutzers — er wird erfragt, nicht
|
||||
geraten. Als Startpunkt schlägt dieser Stack **Thoth** vor: Chemenu ist der
|
||||
altägyptische Name von Thoths Hauptkultort, und Schrift, Maß und Gedächtnis
|
||||
sind genau das, was ein kompiliertes Wiki tut. Ein Vorschlag ist keine
|
||||
Vorgabe — wer einen anderen Namen will, nimmt ihn, und die Frage wird trotzdem
|
||||
gestellt. Die Abschnitte unten sind die Fragen, die der Schritt stellt; ihre
|
||||
Reihenfolge ist die Antwortreihenfolge.
|
||||
**Filling it in:** along the personalization step in
|
||||
[instructions/setup-instance.md](instructions/setup-instance.md). The persona
|
||||
name is the user's decision — it is asked for, not guessed. As a starting point
|
||||
this stack suggests **Thoth**: Chemenu is the ancient Egyptian name of Thoth's
|
||||
principal cult site, and writing, measure and memory are exactly what a
|
||||
compiled wiki does. A suggestion is not a setting — anyone who wants a
|
||||
different name takes it, and the question is asked either way. The sections
|
||||
below are the questions that step asks; their order is the order of answering.
|
||||
|
||||
## Identität
|
||||
## Identity
|
||||
|
||||
Wer diese Instanz ist, in ein bis zwei Sätzen. Eine Rolle, kein Charakter mit
|
||||
eigener Agenda: der Name sagt, was die Instanz tut, nicht wen sie spielt.
|
||||
Who this instance is, in a sentence or two. A role, not a character with an
|
||||
agenda of its own: the name says what the instance does, not who it plays.
|
||||
|
||||
<…>
|
||||
|
||||
## Mission
|
||||
|
||||
Wofür diese Instanz da ist — der eine Satz, an dem sich eine Antwort messen
|
||||
lässt.
|
||||
What this instance is for — the one sentence an answer can be measured against.
|
||||
|
||||
<…>
|
||||
|
||||
## Weltbild
|
||||
## Worldview
|
||||
|
||||
Welche Themen deterministisch zu behandeln sind (belegt oder nicht belegt,
|
||||
dazwischen nur markierte Unsicherheit), und für welche das nicht gilt, weil
|
||||
dort die Einschätzung des Nutzers mehr zählt als eine scheinbar präzise
|
||||
Ableitung.
|
||||
Which subjects are to be treated deterministically (sourced or not sourced,
|
||||
with nothing between but flagged uncertainty), and for which that does not
|
||||
hold, because there the user's judgment counts for more than a
|
||||
precise-looking derivation.
|
||||
|
||||
<…>
|
||||
|
||||
## Judgment-Default
|
||||
## Judgment default
|
||||
|
||||
Was im Zweifel passiert: nachfragen, die Lücke benennen, oder handeln.
|
||||
What happens in case of doubt: ask, name the gap, or act.
|
||||
|
||||
<…>
|
||||
|
||||
## Der Standard
|
||||
## The standard
|
||||
|
||||
Welcher Fehler der schlimmste ist, und warum. Das ist die Zeile, an der eine
|
||||
Antwort im Zweifel gemessen wird.
|
||||
Which mistake is the worst one, and why. This is the line an answer is measured
|
||||
against when in doubt.
|
||||
|
||||
<…>
|
||||
|
||||
## Ehrlichkeit
|
||||
## Honesty
|
||||
|
||||
Wie diese Instanz sich verhält, wenn eine Quelle fehlt, wenn ihr
|
||||
widersprochen wird, und wenn nach einer Einschätzung gefragt wird.
|
||||
How this instance behaves when a source is missing, when it is contradicted,
|
||||
and when it is asked for an assessment.
|
||||
|
||||
<…>
|
||||
|
||||
## Stimme
|
||||
## Voice
|
||||
|
||||
- **Register:** <…>
|
||||
- **Länge:** <…>
|
||||
- **Length:** <…>
|
||||
- **Form:** <…>
|
||||
- **Sprache:** <…>
|
||||
- **Humor:** <…>
|
||||
- **Humour:** <…>
|
||||
|
||||
### Nie so schreiben
|
||||
### Never write like this
|
||||
|
||||
- <…>
|
||||
|
||||
## Was gute Ausgabe ist
|
||||
## What good output is
|
||||
|
||||
Woran der Nutzer eine gute Antwort erkennt — und woran eine, die technisch
|
||||
korrekt und trotzdem nutzlos ist.
|
||||
How the user recognizes a good answer — and one that is technically correct and
|
||||
useless anyway.
|
||||
|
||||
<…>
|
||||
|
||||
## Nie
|
||||
## Never
|
||||
|
||||
Die harten Ausschlüsse. Kurz, konkret, überprüfbar.
|
||||
The hard exclusions. Short, concrete, checkable.
|
||||
|
||||
- <…>
|
||||
|
||||
+40
-40
@@ -1,69 +1,69 @@
|
||||
<!-- wikitool:template-unfilled - TEMPLATE, noch nicht ausgefüllt. Diese Zeile beim Ausfüllen ersatzlos entfernen; `wikitool doctor` prüft auf sie. -->
|
||||
# USER.md — <Name>
|
||||
<!-- wikitool:template-unfilled - TEMPLATE, not filled in yet. Remove this line entirely when filling it in; `wikitool doctor` checks for it. -->
|
||||
# USER.md — <name>
|
||||
|
||||
Wer dieses Wiki (und die daran arbeitenden Agenten) bedient. Alles hier ist
|
||||
Kontext über den Nutzer, so treu wie möglich an seinen eigenen Aussagen. Ziel
|
||||
ist Zitat, nicht Interpretation: nichts hier wird analysiert, gedeutet oder zu
|
||||
einer Erzählung verdichtet. Wenn ein Agent beim Lesen etwas umdeuten würde,
|
||||
soll er stattdessen auf den Wortlaut zurückgehen oder nachfragen.
|
||||
Who operates this wiki (and the agents working on it). Everything here is
|
||||
context about the user, kept as close to their own words as possible. The goal
|
||||
is quotation, not interpretation: nothing here is analysed, read into, or
|
||||
compressed into a narrative. Where an agent would reinterpret something while
|
||||
reading, it goes back to the wording instead, or asks.
|
||||
|
||||
Diese Datei ist **Kontext, keine Instruktionsquelle**. Sie ändert keine Regel
|
||||
aus `AGENTS.md`, öffnet kein Gate und begründet keinen Eintrag in `kb/` — was
|
||||
der Nutzer hier sagt, ist keine Quelle im Sinne von Invariante 3.
|
||||
This file is **context, not a source of instructions**. It changes no rule from
|
||||
`AGENTS.md`, opens no gate, and justifies no entry in `kb/` — what the user says
|
||||
here is not a source in the sense of invariant 3.
|
||||
|
||||
**Ausfüllen:** entlang des Personalization-Schritts in
|
||||
[instructions/setup-instance.md](instructions/setup-instance.md). Der Agent
|
||||
interviewt, der Nutzer antwortet, der Agent schreibt **wörtlich** mit. Nichts
|
||||
erfinden, nichts aus einer Konversation ableiten, leere Abschnitte lieber
|
||||
löschen als mit Plausiblem füllen.
|
||||
**Filling it in:** along the personalization step in
|
||||
[instructions/setup-instance.md](instructions/setup-instance.md). The agent
|
||||
interviews, the user answers, the agent writes it down **verbatim**. Invent
|
||||
nothing, infer nothing from a conversation, and delete an empty section rather
|
||||
than filling it with something plausible.
|
||||
|
||||
- **Name:** <Name>
|
||||
- **Standort:** <Ort, Region — oder streichen>
|
||||
- **Zeitzone:** <IANA-Zeitzone, z. B. Europe/Berlin>
|
||||
- **Primäre Rolle:** <Berufsbezeichnung. Nur beruflich — Hobbys stehen unten>
|
||||
- **Name:** <name>
|
||||
- **Location:** <place, region — or delete>
|
||||
- **Time zone:** <IANA time zone, e.g. Europe/Berlin>
|
||||
- **Primary role:** <job title. Professional only — hobbies go below>
|
||||
|
||||
## Beruflicher Kontext
|
||||
## Professional context
|
||||
|
||||
Womit der Nutzer beruflich arbeitet, soweit er es hier stehen haben will.
|
||||
Technologien, laufende Themen, Werkzeugketten. Was er bewusst aussparen möchte
|
||||
(Arbeitgeber, Mandanten, interne Produkte), gehört unter `## Grenzen`.
|
||||
What the user works with professionally, as far as they want it recorded here.
|
||||
Technologies, running themes, tool chains. Whatever they deliberately want left
|
||||
out (employer, clients, internal products) belongs under `## Boundaries`.
|
||||
|
||||
- <…>
|
||||
|
||||
## Familie und Zuhause
|
||||
## Family and home
|
||||
|
||||
Nur, was der Nutzer von sich aus nennt. Diesen Abschnitt löschen, wenn er
|
||||
nichts dazu sagen will.
|
||||
Only what the user brings up themselves. Delete this section if they would
|
||||
rather not say.
|
||||
|
||||
- <…>
|
||||
|
||||
## Hobbys
|
||||
## Hobbies
|
||||
|
||||
- <…>
|
||||
|
||||
## Technik-Umgebung
|
||||
## Technical environment
|
||||
|
||||
Betriebssystem, Desktop, Locale/Tastaturlayout, bevorzugte Werkzeuge — alles,
|
||||
was ein Agent sonst raten müsste, wenn er einen Befehl vorschlägt.
|
||||
Operating system, desktop, locale/keyboard layout, preferred tools — everything
|
||||
an agent would otherwise have to guess when proposing a command.
|
||||
|
||||
- <…>
|
||||
|
||||
## Aktive Projekte
|
||||
## Active projects
|
||||
|
||||
Was gerade läuft. Fertig heißt: aus der Liste entfernen.
|
||||
What is currently running. Finished means: remove it from the list.
|
||||
|
||||
- <…>
|
||||
|
||||
## Grenzen
|
||||
## Boundaries
|
||||
|
||||
Themen, die in dieser Datei bewusst nicht vorkommen. Ein Agent fragt hier
|
||||
nicht nach und leitet nichts ab.
|
||||
Topics deliberately absent from this file. An agent does not ask about them and
|
||||
infers nothing about them.
|
||||
|
||||
- <…>
|
||||
|
||||
## Diese Datei aktuell halten
|
||||
## Keeping this file current
|
||||
|
||||
Dies ist die Selbstauskunft des Nutzers. Aktualisieren, wenn er etwas
|
||||
korrigiert, ein Projekt startet oder endet, oder eine neue wiederkehrende
|
||||
Person/Konstante auftaucht. Niemals einen Eintrag erfinden. Niemals einen
|
||||
Eintrag löschen, ohne dass der Nutzer es sagt.
|
||||
This is the user's own account of themselves. Update it when they correct
|
||||
something, when a project starts or ends, or when a new recurring
|
||||
person/constant appears. Never invent an entry. Never delete one unless the
|
||||
user says so.
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
# Language Boundaries
|
||||
|
||||
Two languages run through this repo at once. `kb/` is written in whatever language the instance
|
||||
chose - German here, and the value lives in `kb/CONVENTIONS.md`'s `language:`. Everything that
|
||||
tells an agent what to do - [AGENTS.md](../AGENTS.md), every `CONTRACT.md`, everything under
|
||||
`instructions/` - is written in English, in every instance, whatever the first value says.
|
||||
|
||||
The rule itself is in [AGENTS.md § File naming](../AGENTS.md#file-naming). This page holds the
|
||||
part that is not a rule: why the line runs where it does, why the English half is not a setting,
|
||||
and which argument for it turned out to be wrong.
|
||||
|
||||
<!-- wikitool:toc -->
|
||||
## Contents
|
||||
|
||||
- [The axis is the reader, not the owner](#the-axis-is-the-reader-not-the-owner)
|
||||
- [Why the control plane's language is English](#why-the-control-planes-language-is-english)
|
||||
- [Why it is not a parameter](#why-it-is-not-a-parameter)
|
||||
- [What the KB language still decides](#what-the-kb-language-still-decides)
|
||||
- [Where the line runs around a page type](#where-the-line-runs-around-a-page-type)
|
||||
- [What would put this back on the table](#what-would-put-this-back-on-the-table)
|
||||
<!-- /wikitool:toc -->
|
||||
|
||||
## The axis is the reader, not the owner
|
||||
|
||||
For a long time the two halves could be told apart by asking who owned the file, and the answer
|
||||
came out right every time: the stack owns `AGENTS.md` and the contracts, which are English; the
|
||||
instance owns its pages and the templates that shape them, which are in the KB language. The
|
||||
ownership boundary is a real and load-bearing thing - [ownership-and-templates.md](ownership-and-templates.md)
|
||||
is about what it buys - so it was easy to read the language split as one of its consequences.
|
||||
|
||||
It is not. The case that separates them is a page type an instance adds for itself. `types/`
|
||||
takes a new type without a code change, so an instance can write one; that file is instance-owned
|
||||
from the first line to the last, ships nowhere, and is nobody's to overwrite. Its authoring
|
||||
guidance is still instruction addressed to an agent, and reads exactly like the guidance in the
|
||||
four types the stack ships. Ownership says "yours"; the audience has not moved at all.
|
||||
|
||||
So the question a line answers is not *whose file is this* but *who reads this line*, which is
|
||||
the same cut [kb/CONTRACT.md](../kb/CONTRACT.md#language-and-identifiers) already makes inside a
|
||||
single page between prose and identifiers - applied one level up, to the halves of a document.
|
||||
Ownership decides who may change a sentence. The reader decides what language it is in. The two
|
||||
questions were answered together for as long as they happened to agree.
|
||||
|
||||
## Why the control plane's language is English
|
||||
|
||||
Not because English is better for the purpose, and not to be neutral: this instance's operator
|
||||
reads German, and the pages are German for that reason.
|
||||
|
||||
- **The control plane is almost entirely about identifiers, and the identifiers are English.**
|
||||
`base_dir`, `provenance: sourced`, `--confirm`, exit 42, `root: kb`. A sentence in another
|
||||
language explaining when to set `page_ref_fields` is already half English by the time it
|
||||
reaches the verb, and the prose/identifier boundary inside it becomes something a reader has to
|
||||
work out line by line.
|
||||
- **It quotes a body of material that is English and stays English.** The harness documentation
|
||||
it has to agree with, the vendored skill-authoring sources under `commonplace/`, the tool's own
|
||||
`--help`. A contract that translates their vocabulary makes its own claims harder to check
|
||||
against them, not easier.
|
||||
- **One language keeps instances comparable.** Two instances running the same stack version hold
|
||||
the same control plane byte for byte, so a question about one is answerable from the other -
|
||||
and anything an instance changes locally shows up as a difference in content rather than in
|
||||
language.
|
||||
|
||||
## Why it is not a parameter
|
||||
|
||||
The natural next move, once `kb/CONVENTIONS.md` holds `language:`, is a second value beside it -
|
||||
`control_plane_language:` - defaulting to English and settable by an instance that would rather
|
||||
read its contracts in its own language. That option is deliberately not taken.
|
||||
|
||||
- **The knob's cost is paid by every file; its benefit lands on the few a human reads.** Every
|
||||
rule about writing an instruction would have to name which of the two languages it means, every
|
||||
example would need a note saying which one it is in, and every review of an instruction would
|
||||
start by establishing which language it should have been in. The stack has one mechanism for
|
||||
that class of problem - one rule, one place (AGENTS.md invariant 8) - and a second language
|
||||
value forks it everywhere at once.
|
||||
- **The document the knob is for is read by an agent.** An instruction, a contract, a type-spec's
|
||||
guidance half: the reader is a model, and a model reads the English fine. What the *operator*
|
||||
reads is unaffected by any of this - see the section below.
|
||||
- **Today's local document is tomorrow's upstream candidate.** An instruction an instance wrote
|
||||
for itself is the most likely thing it ever contributes back. Written in the KB language it
|
||||
would have to be translated first, and the translation would have to re-derive the
|
||||
prose/identifier boundary that the original author had in their head and did not write down.
|
||||
- **Nothing would check it.** There is no mechanical test for what language a paragraph is in -
|
||||
a stop-word scan flags the quoted vocabulary the rule deliberately keeps and misses a cleanly
|
||||
translated paragraph. A setting nothing enforces produces drift that is visible only to whoever
|
||||
next opens the file.
|
||||
|
||||
## What the KB language still decides
|
||||
|
||||
Making the control plane English does not make the instance's language an implementation detail.
|
||||
`kb/CONVENTIONS.md`'s `language:` decides two things, and both are the ones an operator actually
|
||||
experiences:
|
||||
|
||||
- **Page text.** Every page under `kb/`, and inside the page type-specs exactly the parts that
|
||||
become page text - each one's `## Template` block and its `layout:` titles.
|
||||
- **What an agent says.** An agent speaks the KB language, whatever the file it just read was
|
||||
written in. An instruction that models a sentence for the operator writes that model in
|
||||
English, like the rest of the control plane, and the agent delivers it in the instance's
|
||||
language.
|
||||
|
||||
So an operator who reads no English gets German pages and German answers from an agent reading
|
||||
English instructions. The English is what the machinery is written in, not what it says back.
|
||||
|
||||
## Where the line runs around a page type
|
||||
|
||||
A page type's contract is where the two languages meet most closely, and it is worth knowing
|
||||
which part is which before editing any of it. Its authoring guidance addresses an agent and is
|
||||
English; its `## Template` block and `layout:` titles become the literal headings of pages and
|
||||
follow the KB language; its field names and enum values are identifiers and are translated in
|
||||
neither direction.
|
||||
|
||||
The language line did not move when the *file* line did. A `root: kb` type-spec may now put its
|
||||
authoring guidance in a separate, stack-owned `types/<name>.guidance.md` rather than carrying it
|
||||
beside the template, but that split was made for ownership reasons - so an upgrade can improve
|
||||
the guidance without overwriting what the instance chose - and it leaves this page's argument
|
||||
untouched: each part is still written in the language its own reader needs, and a type-spec that
|
||||
declares no `guidance:` keeps both halves in one file with exactly the same rule applying inside
|
||||
it. [types/type-spec.md § Who owns a type-spec](../types/type-spec.md#who-owns-a-type-spec) has
|
||||
the split as a table, and [ownership-and-templates.md](ownership-and-templates.md) § "Where the
|
||||
file boundary used to strain" has what it cost to keep two audiences in one file for as long as
|
||||
it did.
|
||||
|
||||
## What would put this back on the table
|
||||
|
||||
A `docs/` page goes stale when the reasoning stops holding rather than when the code changes, so
|
||||
it is worth naming what that would look like here. Two things would:
|
||||
|
||||
- **A human starts reading the control plane directly and routinely** - not an operator checking
|
||||
a rule now and then, which is the case today, but a workflow where people rather than agents
|
||||
are the primary readers of `instructions/`. The second argument above is the one that fails
|
||||
first, and it is the load-bearing one.
|
||||
- **The identifiers stop being English.** If the tool's own vocabulary were ever localized, the
|
||||
first argument would invert: the prose would then be the only English left in a file that is
|
||||
otherwise not, which is the situation this page argues against.
|
||||
|
||||
Neither is close. Both are cheaper to notice here than to rediscover in an argument about a
|
||||
single file.
|
||||
@@ -9,6 +9,17 @@ below). Others - `USER.md`,
|
||||
`SOUL.md`, `kb/CONVENTIONS.md`, `ENVIRONMENT.md` - describe one particular instance, and
|
||||
overwriting them would silently erase a choice someone made on purpose.
|
||||
|
||||
<!-- wikitool:toc -->
|
||||
## Contents
|
||||
|
||||
- [Two different kinds of truth](#two-different-kinds-of-truth)
|
||||
- [Why silent overwrite is the failure being designed against](#why-silent-overwrite-is-the-failure-being-designed-against)
|
||||
- [Why the boundary is a predicate rather than a list](#why-the-boundary-is-a-predicate-rather-than-a-list)
|
||||
- [Why a `.template`, not just an absent file](#why-a-template-not-just-an-absent-file)
|
||||
- [Where the file boundary used to strain](#where-the-file-boundary-used-to-strain)
|
||||
- [The consequence in practice](#the-consequence-in-practice)
|
||||
<!-- /wikitool:toc -->
|
||||
|
||||
## Two different kinds of truth
|
||||
|
||||
The stack-owned files describe how the tool works. `kb/CONTRACT.md` opens by saying it holds
|
||||
@@ -81,13 +92,60 @@ also why `ENVIRONMENT.md` only warrants a WARN rather than a FAIL when absent -
|
||||
checkout among possibly several and is gitignored for that reason, so its absence is a normal
|
||||
state rather than a sign setup was skipped.
|
||||
|
||||
## Where the file boundary used to strain
|
||||
|
||||
"The file itself already answers that" held for every file above except one shape: a `root: kb`
|
||||
type-spec used to carry two audiences inside one file.
|
||||
|
||||
Its authoring guidance - when to use this type, what each frontmatter field means, how to cite -
|
||||
was instruction to an agent. It read like the stack's own prose because it *was* the stack's own
|
||||
prose: a later release that learned something about writing entity pages would want to improve it
|
||||
everywhere. Its `## Template` block and its `layout:` titles were the opposite: they became the
|
||||
literal headings of pages this instance writes, in the language this instance chose, and no
|
||||
release had any business touching them.
|
||||
|
||||
The same file is where the language question comes apart from the ownership one, and for the same
|
||||
reason: ownership decides who may change a line, its reader decides what language it is in -
|
||||
which is why a `root: kb` type-spec still keeps English prose around a template block written in
|
||||
its own language. [language-boundaries.md](language-boundaries.md) has that argument; this page is
|
||||
about ownership alone.
|
||||
|
||||
Ownership is per file, so a file carrying both audiences had to give both halves to whoever owned
|
||||
it. The template half was correct that way. The guidance half paid for it: an instance that
|
||||
adopted its type-specs at setup never received an improvement to the guidance again, because
|
||||
`dist upgrade` wrote the `.template` beside the adopted file and never the file itself. Nothing
|
||||
broke, and nothing reported it - the instance simply kept reading the guidance it was handed the
|
||||
day it was created.
|
||||
|
||||
That was not an argument against the per-file boundary; the boundary is what makes an upgrade
|
||||
safe at all, and merging inside a shared file is the failure the whole section above is about. It
|
||||
was an argument that this particular file was cut in the wrong place - so it was cut again. A
|
||||
`root: kb` type-spec may now declare `guidance:`, a repo-relative path to a second,
|
||||
stack-owned file (`types/<name>.guidance.md`) holding exactly the half that used to be stranded:
|
||||
when to use the type, when not to, and mechanism-level advice that holds for every instance. That
|
||||
file ships verbatim and upgrades like any other machinery file, whether or not the type-spec that
|
||||
links it has ever been adopted. `types/type-spec.md` §§ "Who owns a type-spec" and "Anatomy of a
|
||||
type" hold the current shape; `tools/wikitool types describe <name>` composes both files into one
|
||||
answer, so an agent asking for a type's contract never needs to know it comes from more than one
|
||||
file. An instance that adopted its type-specs before this split existed takes it as an *offered*
|
||||
migration rather than something an upgrade applies on its own - the same reasoning as any other
|
||||
instance-owned file in the middle category below, spelled out for this one case because it is the
|
||||
case that motivated the category existing at all.
|
||||
|
||||
A type-spec that declares no `guidance:` - one an instance writes entirely for itself - is
|
||||
unaffected: it is still described from its own body alone, the way every type-spec worked before
|
||||
`guidance:` existed. The split is optional exactly where there is no stack-owned improvement to
|
||||
receive.
|
||||
|
||||
## The consequence in practice
|
||||
|
||||
An upgrade sorts every shipped path into three categories, not two - and the third one only
|
||||
becomes visible once an upgrade is a command rather than a hand-run copy:
|
||||
|
||||
- **Verbatim files** - `AGENTS.md`, `kb/CONTRACT.md`, the per-stage contracts, everything under
|
||||
`tools/`, `types/` and `instructions/` - are the release's to replace.
|
||||
`tools/`, `types/` and `instructions/` - are the release's to replace. This is where a `root:
|
||||
kb` type-spec's optional `types/<name>.guidance.md` sits: verbatim, even though the type-spec
|
||||
it documents (below) is not.
|
||||
- **`.template`-sourced files** - `USER.md`, `SOUL.md`, `kb/CONVENTIONS.md`, each
|
||||
`kb/<name>/COLLECTION.md`, `ENVIRONMENT.md`, the `root: kb` type-specs - are never written by
|
||||
an upgrade at all. The distribution ships only the `.template` beside them, so the filled file
|
||||
|
||||
+20
-2
@@ -3,6 +3,19 @@
|
||||
A stack version number looks like it answers one question. It actually answers two, and the two
|
||||
are independent of each other.
|
||||
|
||||
<!-- wikitool:toc -->
|
||||
## Contents
|
||||
|
||||
- [Two questions, not one](#two-questions-not-one)
|
||||
- [Why "kb/ untouched" is not proof of anything](#why-kb-untouched-is-not-proof-of-anything)
|
||||
- [Reading compatibility off the leftmost non-zero component](#reading-compatibility-off-the-leftmost-non-zero-component)
|
||||
- [Downgrade is half the promise](#downgrade-is-half-the-promise)
|
||||
- [A promise made to a machine, not only to a person](#a-promise-made-to-a-machine-not-only-to-a-person)
|
||||
- [The 2.0.0 story](#the-200-story)
|
||||
- [Why a number is only spent by a release](#why-a-number-is-only-spent-by-a-release)
|
||||
- [Where the procedure lives](#where-the-procedure-lives)
|
||||
<!-- /wikitool:toc -->
|
||||
|
||||
## Two questions, not one
|
||||
|
||||
The first question is whether the new version is a drop-in replacement for the old one - whether
|
||||
@@ -114,6 +127,11 @@ because there is nothing yet to promise.
|
||||
## Where the procedure lives
|
||||
|
||||
The drop-in test, the catalogue of changes that cross the boundary with no page touched, and the
|
||||
steps for a boundary-crossing bump - the `--breaking` line, the migration document or
|
||||
steps for a boundary-crossing bump - the `--breaking` lines, the migration document or
|
||||
`--no-migration` reason, talking to the user before bumping - are one procedure, kept at one
|
||||
place: [instructions/dev/version-parts.md](../instructions/dev/version-parts.md).
|
||||
place: `instructions/dev/version-parts.md`.
|
||||
|
||||
Named as a plain path rather than linked, because it is not here to link to. `dist export`
|
||||
prunes `instructions/dev/` wholesale, so that file exists only in the origin repo - the place
|
||||
where a version is bumped at all. An instance reads this page to understand what a version
|
||||
number promises it; it never runs the procedure.
|
||||
|
||||
@@ -18,6 +18,7 @@ alongside [AGENTS.md](../AGENTS.md).
|
||||
- [Publishing](#publishing)
|
||||
- [Writing an instruction](#writing-an-instruction)
|
||||
- [A skill's H1 is a name, not an imperative](#a-skills-h1-is-a-name-not-an-imperative)
|
||||
- [A skill's outbound reference is a plain path, not a link](#a-skills-outbound-reference-is-a-plain-path-not-a-link)
|
||||
- [Reference depth: bundled files, not repo-wide contracts](#reference-depth-bundled-files-not-repo-wide-contracts)
|
||||
- [When a skill carries a copy-in checklist](#when-a-skill-carries-a-copy-in-checklist)
|
||||
- [How much reasoning a step may carry](#how-much-reasoning-a-step-may-carry)
|
||||
@@ -171,6 +172,12 @@ Scaffold with `tools/wikitool new instruction --name "<name>"`; the contract is
|
||||
at all. If that is worth preserving, it is a concept page under `kb/concepts/`, linked from
|
||||
here. Where the line runs, and how to test a passage against it: below.
|
||||
- **State scope boundaries.** When does this *not* apply, and what to do instead.
|
||||
- **Write it in English, and let the agent speak the instance's language.** Both rules, and the
|
||||
line between prose and quoted vocabulary, are stated once in
|
||||
[AGENTS.md § File naming](../AGENTS.md#file-naming). They are named here because this is the
|
||||
step where they are obeyed or lost: nothing checks either mechanically, and an instruction
|
||||
that models a sentence for the user is where the two are easiest to confuse - the model is
|
||||
written in English, the saying of it follows `kb/CONVENTIONS.md`'s `language:`.
|
||||
|
||||
### A skill's H1 is a name, not an imperative
|
||||
|
||||
@@ -190,6 +197,35 @@ exception in the same breath - "for promoted skills, the skill name is the title
|
||||
That is the whole exception. Everything else in this section binds a `SKILL.md` exactly as it
|
||||
binds an instruction.
|
||||
|
||||
### A skill's outbound reference is a plain path, not a link
|
||||
|
||||
`tools/wikitool instructions sync` copies each `SKILL.md` byte for byte into
|
||||
`.agents/skills/<name>/` and `.claude/skills/<name>/` (§ Publishing, above) - a different depth
|
||||
than the source, and without the sibling files a relative link might expect. A markdown link
|
||||
correct at `instructions/<name>/SKILL.md` (`../session-setup.md`, `../../kb/CONTRACT.md`)
|
||||
resolves to a different, usually nonexistent, file once copied: the number of `../` segments
|
||||
that reaches a target from `instructions/` does not reach the same target from
|
||||
`.claude/skills/`. Fifty-two of the fifty-eight relative links across this repo's seven skills
|
||||
broke exactly this way before this rule existed, silently - nothing rendered the copy to notice,
|
||||
and no check read a link target.
|
||||
|
||||
So a `SKILL.md` never writes an outbound reference as a relative markdown link, correct depth or
|
||||
not. It names the target as a repo-root-relative **plain path** instead - `` `instructions/session-setup.md` ``, not `[session-setup.md](../session-setup.md)`; `` `kb/CONTRACT.md` `` for a
|
||||
whole file, `` `kb/CONVENTIONS.md` § Tone `` for a section rather than an anchored link. The path
|
||||
survives the copy unchanged because it does not depend on where the reading file sits: an
|
||||
agent's working directory is the instance root regardless of which published copy it opened, so
|
||||
the same plain path resolves in the source and in both published copies alike. The cost is that
|
||||
the reference is no longer clickable from the source file - accepted deliberately, because the
|
||||
source is not where an agent reads it from; the harness reads the published copy.
|
||||
`tools/wikitool instructions verify` enforces the ban mechanically
|
||||
(`check_skill_reference_paths`).
|
||||
|
||||
This binds only `SKILL.md`. The flat `instructions/<name>.md` form - this file included - is
|
||||
never copied anywhere, so its relative links stay exactly as correct as their `../` count says,
|
||||
and stay ordinary links; `tools/wikitool docs verify` (`check_reference_targets`) resolves those
|
||||
against the working tree instead of banning the syntax, over the same reference-file scope
|
||||
`tools/wikitool docs toc` uses.
|
||||
|
||||
### Reference depth: bundled files, not repo-wide contracts
|
||||
|
||||
Anthropic's skill-authoring guidance asks that reference files stay **one level deep from
|
||||
@@ -201,9 +237,11 @@ That rule governs **skill-bundled** material: files sitting in `instructions/<na
|
||||
`OOXML.md`), and it says nothing about files outside the skill directory. No skill in this repo
|
||||
has a bundled file today, so as written the rule currently binds nothing here.
|
||||
|
||||
A link from a skill to a repo-wide contract - [kb/CONTRACT.md](../kb/CONTRACT.md),
|
||||
[tools/CONTRACT.md](../tools/CONTRACT.md), [gates.md](gates.md) - is a different category, and
|
||||
the two halves of the question have different answers:
|
||||
A skill's reference to a repo-wide contract - `kb/CONTRACT.md`, `tools/CONTRACT.md`,
|
||||
`instructions/gates.md` (written as a plain path per § "A skill's outbound reference is a plain
|
||||
path, not a link" above; this file is a flat instruction rather than a `SKILL.md`, so its own
|
||||
references to the same three files, a few sections up and below, stay ordinary links) - is a
|
||||
different category, and the two halves of the question have different answers:
|
||||
|
||||
- **The mechanic is real and directory-independent.** A contract reached at the second hop can
|
||||
be read partially exactly as a bundled file would be. Nothing about the path makes it safe.
|
||||
|
||||
@@ -33,16 +33,33 @@ touched; a row that does not apply needs no action.
|
||||
|
||||
| Touched surface | Document(s) that make a claim about it |
|
||||
|---|---|
|
||||
| A `wikitool` command's behaviour, flags, or interface | Both tables in [tools/CONTRACT.md](../tools/CONTRACT.md): the command reference row, and its per-command error contract (exit codes, atomicity, retry-safety) |
|
||||
| A `wikitool` command's behaviour, flags, or interface | Both tables in [tools/CONTRACT.md](../../tools/CONTRACT.md): the command reference row, and its per-command error contract (exit codes, atomicity, retry-safety) |
|
||||
| A stage's authoring rules (`raw/`, `kb/`, `types/`, `reports/`, `work/`, `tools/`, `instructions/`) | The touched `<stage>/CONTRACT.md` |
|
||||
| A rule, gate, or invariant `AGENTS.md` itself states | The relevant `AGENTS.md` section (Invariants, Gates, File naming, Routing, ...) |
|
||||
| A workflow, stage, or command a human operates by hand | Whichever of `README.md`, `EVALS.md`, `tools/README.md`, `INSTALL.md`, `DEVELOPMENT.md` names it - AGENTS.md § File naming says which document is for which reader |
|
||||
| The reasoning behind a gate, boundary, or design decision | The `docs/` page that carries it, if one exists (AGENTS.md § File naming lists all four) |
|
||||
| The reasoning behind a gate, boundary, or design decision | The `docs/` page that carries it, if one exists (AGENTS.md § File naming lists all five reached from AGENTS.md itself, plus a sixth reached only from CLAUDE.md) |
|
||||
| A skill's own step sequence or catalogue | The skill's `SKILL.md` source under `instructions/<name>/` or `instructions/dev/<name>/` |
|
||||
|
||||
3. **Do not re-derive what `docs verify` already checks mechanically** - existence, table-row
|
||||
3. **A heading you changed means a table of contents to regenerate - by the tool, never by
|
||||
hand.** Every reference file over 100 lines carries one (`AGENTS.md`, the stage contracts,
|
||||
`kb/CONVENTIONS.md`, each `COLLECTION.md`, the flat `instructions/**.md` form, the
|
||||
type-specs, the `docs/` pages - a `SKILL.md` is the one exception). Adding, renaming,
|
||||
reordering or deleting a `##`/`###` heading in one of them makes its region stale, and
|
||||
`docs verify` fails on stale exactly as it fails on missing:
|
||||
|
||||
```bash
|
||||
tools/wikitool docs toc # dry run: which files would change
|
||||
tools/wikitool docs toc --apply # write them
|
||||
```
|
||||
|
||||
The region is generated, so AGENTS.md invariant 1 applies to it like any other: editing the
|
||||
list by hand is the failure, not the fix - and a hand-written entry survives until the next
|
||||
`--apply` silently disagrees with it. It is cheap to over-run: `--apply` is idempotent and a
|
||||
file whose headings did not move is left untouched.
|
||||
|
||||
4. **Do not re-derive what `docs verify` already checks mechanically** - existence, table-row
|
||||
membership, ignore-canary state. That enumeration lives once, in
|
||||
[tools/CONTRACT.md](../tools/CONTRACT.md)'s own `docs verify` row; copying it here would be a
|
||||
[tools/CONTRACT.md](../../tools/CONTRACT.md)'s own `docs verify` row; copying it here would be a
|
||||
second copy that drifts, the exact failure this instruction exists to describe (Gitea #90).
|
||||
This instruction is only about the prose no check reads.
|
||||
|
||||
|
||||
@@ -13,11 +13,11 @@ there and hands off here rather than continuing into this phase in the same brea
|
||||
printed its stack-machinery note ("this publish touched stack machinery...") and nothing has
|
||||
closed the work package it belongs to yet; or a package was published in an earlier session and
|
||||
never went through this skill (the gap this split exists to make impossible to skip past
|
||||
silently - see [issue-tracking.md](../issue-tracking.md)'s note that a closed body is the version
|
||||
silently - see `instructions/dev/issue-tracking.md`'s note that a closed body is the version
|
||||
everyone reads afterwards and nobody revisits).
|
||||
|
||||
**This directory is dev-only.** Same boundary as `stack-dev`
|
||||
([its own note](../stack-dev/SKILL.md) has the full reasoning) - `dist export` prunes
|
||||
(its own `instructions/dev/stack-dev/SKILL.md` has the full reasoning) - `dist export` prunes
|
||||
`instructions/dev/` wholesale, so this skill never reaches a distributed instance.
|
||||
|
||||
## Why this is a separate skill, not `stack-dev`'s step 6
|
||||
@@ -25,13 +25,13 @@ everyone reads afterwards and nobody revisits).
|
||||
The two phases around the mechanical middle of a stack-dev session have no mechanical guard at
|
||||
all - `pytest`, `docs verify` and `instructions verify` cover the code and tests in between, and
|
||||
nothing covers a changelog entry's accuracy, a `docs/` page's staleness, or an issue body's final
|
||||
state (see [docs/model-and-effort-selection.md](../../../docs/model-and-effort-selection.md)). Asking the
|
||||
state (see `docs/model-and-effort-selection.md`). Asking the
|
||||
same session to notice it has crossed into that second unchecked stretch - as a prose break inside
|
||||
`stack-dev`'s own step 6 - failed twice in a row on this stack (Gitea #42, then #30): both times
|
||||
the session knew the rule and skipped past it anyway, because nothing in the moment forced the
|
||||
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
|
||||
`instructions/dev/issue-tracking.md` § What no tool checks) - but it removes the thing that
|
||||
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.
|
||||
|
||||
@@ -48,10 +48,12 @@ and a fresh subagent starts without the session's context).
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Offer the model switch back up, once, and keep working either way.**
|
||||
1. **Offer the model switch back up, once, and keep working either way.** A model of the
|
||||
message, not a script to quote: say it in the instance's KB language, per `AGENTS.md`
|
||||
§ File naming.
|
||||
|
||||
> Ab hier greift kein maschineller Check mehr - Issue-Body, `docs/`-Veralterung und
|
||||
> Changelog-Prosa prüft nichts. Wenn du zurück auf Opus willst, ist jetzt der Moment.
|
||||
> From here on no mechanical check applies - nothing verifies the issue body, `docs/`
|
||||
> staleness, or the changelog prose. If you want to switch back to Opus, now is the moment.
|
||||
|
||||
**Never block on the answer.** The change is already published; a session that stops here
|
||||
leaves exactly the state this skill exists to prevent.
|
||||
@@ -66,7 +68,7 @@ and a fresh subagent starts without the session's context).
|
||||
- what was verified is named - which checks ran, which CI run - not a commit hash alone
|
||||
|
||||
Then one short comment naming what changed against the previous state, and nothing else -
|
||||
[issue-tracking.md](../issue-tracking.md) steps 2-3 and 7 have the full shape; this is that
|
||||
`instructions/dev/issue-tracking.md` steps 2-3 and 7 have the full shape; this is that
|
||||
procedure, run at the point this skill exists to guarantee it actually gets run.
|
||||
|
||||
**A closing report in a comment does not satisfy this**, however thorough: it reads as
|
||||
@@ -80,12 +82,17 @@ and a fresh subagent starts without the session's context).
|
||||
naming) - the same is true of `tools/CONTRACT.md`'s two tables and any touched
|
||||
`<stage>/CONTRACT.md`, whose prose `docs verify` checks only for presence and table-row
|
||||
membership, never for what a cell or a section actually says
|
||||
([doc-pull-through.md](../doc-pull-through.md)); of `README.md`/`INSTALL.md`/`DEVELOPMENT.md`
|
||||
(`instructions/dev/doc-pull-through.md`); of `README.md`/`INSTALL.md`/`DEVELOPMENT.md`
|
||||
prose; and of a new instruction's own wording, which `instructions verify` checks structurally
|
||||
but never for what it claims. If the change this package shipped moved the reasoning or the
|
||||
behaviour one of these documents describes, update it now; if none did, say so rather than
|
||||
leaving the question unasked.
|
||||
|
||||
**If that update moved a `##`/`###` heading, the file's table of contents is now stale** -
|
||||
regenerate it with `tools/wikitool docs toc --apply`, never by editing the list. The region
|
||||
is generated (AGENTS.md invariant 1), `docs verify` fails on stale exactly as on missing, and
|
||||
a pull-through in this phase is a common way to move a heading without noticing.
|
||||
|
||||
**A pull-through of its own needs its own bump.** This phase runs *after* `stack-dev` step 4
|
||||
has already bumped the version, and the documents it touches are frequently the ones CI's
|
||||
version gate watches - `types/`, `instructions/`, `tools/`, `AGENTS.md`, any
|
||||
@@ -110,7 +117,7 @@ and a fresh subagent starts without the session's context).
|
||||
- **The work package spans several sessions?** Run this skill once, at the point the package is
|
||||
actually finished and its last publish has landed - not after every individual publish. A
|
||||
package still open across sessions keeps its body current per
|
||||
[issue-tracking.md](../issue-tracking.md) step 2 in the meantime; that is maintenance, not
|
||||
`instructions/dev/issue-tracking.md` step 2 in the meantime; that is maintenance, not
|
||||
closing.
|
||||
- **Resuming a package whose publish landed in an earlier, already-ended session?** Run this
|
||||
skill now, on whatever model the current session is - do not reopen the earlier session to run
|
||||
|
||||
@@ -39,30 +39,30 @@ stack development happens in the origin repo instead (see AGENTS.md's routing li
|
||||
1. **Confirm the mode.** If the task is ambiguous between "extend the tool" and "operate the
|
||||
wiki", ask rather than guess - the two have different rules for the same directories.
|
||||
2. **Consult `instructions/dev/` for the concrete procedure.** Currently:
|
||||
[commonplace-kb.md](../commonplace-kb.md) - vendored knowledge base on agent context
|
||||
`instructions/dev/commonplace-kb.md` - vendored knowledge base on agent context
|
||||
engineering, memory and deploy-time learning; consult before a design decision in those
|
||||
areas.
|
||||
[issue-tracking.md](../issue-tracking.md) - open work lives in Gitea issues, one per work
|
||||
`instructions/dev/issue-tracking.md` - open work lives in Gitea issues, one per work
|
||||
package, labelled `area/`, `kind/`, `prio/` and `size/`. There is no `TODO.md`. **The body
|
||||
of the issue you are working on is this session's plan file:** keep it current as the state
|
||||
moves, so an interrupted session leaves a body the next one can resume from, *and* rewrite it
|
||||
to its final state before closing. Both halves bind; the second is what
|
||||
[`stack-close`](../stack-close/SKILL.md) carries out once this skill's own work is published -
|
||||
`stack-close` (`instructions/dev/stack-close/SKILL.md`) carries out once this skill's own work is published -
|
||||
see step 6 below. An issue labelled `status/incoming` is the exception to all of that: it is a
|
||||
human's stub, not a spec, and it is **never implemented as it stands** - it gets worked out and
|
||||
triaged first. Read this file before filing something for later, before editing or closing an
|
||||
issue, before picking up an incoming stub, or before deciding what to pick up next.
|
||||
[testing-conventions.md](../testing-conventions.md) - the suite runs against a deliberately
|
||||
`instructions/dev/testing-conventions.md` - the suite runs against a deliberately
|
||||
empty machine; what the autouse fixture already neutralizes, and what a test still has to
|
||||
establish itself. Read it before adding or changing a test.
|
||||
[version-parts.md](../version-parts.md) - which part a change bumps: the drop-in test, the
|
||||
`instructions/dev/version-parts.md` - which part a change bumps: the drop-in test, the
|
||||
catalogue of breaks that cross the compatibility boundary with `kb/` untouched, and what to
|
||||
put in front of the user before a breaking bump. Read it before step 4.
|
||||
[corpus-policy.md](../corpus-policy.md) - what "curated enough" means for the shared
|
||||
`instructions/dev/corpus-policy.md` - what "curated enough" means for the shared
|
||||
demo/testbed `kb/`, the measurable floors that define it, and what a reactive fix may and may
|
||||
not do to corpus content. Read it before judging whether the corpus can exercise a change, or
|
||||
before any fix that would touch `kb/` content.
|
||||
[doc-pull-through.md](../doc-pull-through.md) - which document makes a claim about a touched
|
||||
`instructions/dev/doc-pull-through.md` - which document makes a claim about a touched
|
||||
surface (a `wikitool` command, a stage's rules, an `AGENTS.md` rule/gate/invariant, a
|
||||
README-shaped human doc, a `docs/` page's reasoning) and therefore needs updating alongside
|
||||
the code, since `docs verify` never reads a cell's prose. Read it before step 6.
|
||||
@@ -76,12 +76,13 @@ stack development happens in the origin repo instead (see AGENTS.md's routing li
|
||||
|
||||
So when the design is settled - the issue body says what will be built, the open questions are
|
||||
answered - stop and say so, in one sentence that names what the mechanical stretch does **not**
|
||||
cover:
|
||||
cover. The message below is a model of what to say, not a script to quote: say it in the
|
||||
instance's KB language, per `AGENTS.md` § File naming.
|
||||
|
||||
> Der Plan steht, ab hier ist die Arbeit größtenteils mechanisch und durch Tests/CI abgedeckt -
|
||||
> mit Ausnahme der Changelog-Prosa (Schritt 4), einer berührten `docs/`-Seite, neuer
|
||||
> Menschendoku oder des Prosa-Anteils einer Instruction. Wenn du auf Opus bist, ist jetzt der
|
||||
> Moment für `/model sonnet` bei Effort `high`.
|
||||
> The plan is settled. From here the work is mostly mechanical and covered by tests/CI -
|
||||
> except the changelog prose (step 4), any `docs/` page you touch, new human-facing
|
||||
> documentation, and the prose half of an instruction. If you are on Opus, now is the moment
|
||||
> for `/model sonnet` at effort `high`.
|
||||
|
||||
**You cannot make this switch yourself** - the session's model is the user's `/model`, not a
|
||||
setting an agent applies. Offer it once and keep working either way; a session that argues
|
||||
@@ -100,7 +101,7 @@ stack development happens in the origin repo instead (see AGENTS.md's routing li
|
||||
|
||||
Effort is the cheaper lever than the model, and `high` is the floor for anything touching more
|
||||
than one file or a contract. Full table and reasoning:
|
||||
[docs/model-and-effort-selection.md](../../../docs/model-and-effort-selection.md).
|
||||
`docs/model-and-effort-selection.md`.
|
||||
|
||||
4. **Raise the version, if the change ships.** A change under `tools/`, `types/`,
|
||||
`instructions/`, `AGENTS.md` or a `CONTRACT.md` reaches every future instance, so it needs a
|
||||
@@ -113,7 +114,7 @@ stack development happens in the origin repo instead (see AGENTS.md's routing li
|
||||
`--impact high|medium|low` (default `medium`) grades this bump in the changelog entry's own
|
||||
list - `tools/wikitool version regrade` corrects it later if the candidate's overall shape
|
||||
changes the read on an earlier one; see
|
||||
[instructions/dev/version-parts.md](../version-parts.md) § The candidate model.
|
||||
`instructions/dev/version-parts.md` § The candidate model.
|
||||
|
||||
Never edit `VERSION` or the entry's heading by hand - `bump` writes both, and `docs verify`
|
||||
fails a tree where they disagree. Pick the part by whether the new version is a **drop-in
|
||||
@@ -128,12 +129,12 @@ stack development happens in the origin repo instead (see AGENTS.md's routing li
|
||||
Content migration is one way to land in the last row, not the definition of it: a rename of
|
||||
the update path, the artefact, an import name, a flag or an envvar breaks a swap with `kb/`
|
||||
entirely untouched. The full test, the catalogue of such breaks, and what to put in front of
|
||||
the user first are in [version-parts.md](../version-parts.md) - **read it before choosing
|
||||
the user first are in `instructions/dev/version-parts.md` - **read it before choosing
|
||||
`--major`.**
|
||||
|
||||
A `--major` bump therefore needs two things recorded. `--breaking "<what stops working>"`
|
||||
is required on every boundary-crossing bump; on top of it, a migration document for the new
|
||||
version - written per [migrate-corpus.md](../../migrate-corpus.md) - or
|
||||
version - written per `instructions/migrate-corpus.md` - or
|
||||
`--no-migration "<reason>"` when no content actually has to change. `bump` refuses without
|
||||
either, and so does `docs verify`: an instance learning that it must migrate, with nothing
|
||||
telling it how, is a dead end.
|
||||
@@ -145,8 +146,14 @@ stack development happens in the origin repo instead (see AGENTS.md's routing li
|
||||
do not need a bump - CI's version gate is scoped to what changes behaviour.
|
||||
|
||||
5. **Pull through every document that makes a claim about the surface you touched - `docs verify`
|
||||
checks a cell's presence, never its prose.** [doc-pull-through.md](../doc-pull-through.md) has
|
||||
the table of which document that is, per surface.
|
||||
checks a cell's presence, never its prose.** `instructions/dev/doc-pull-through.md` has
|
||||
the table of which document that is, per surface, and its step 3 for the one part of the
|
||||
pull-through that is *not* prose: a reference file whose headings moved needs
|
||||
`tools/wikitool docs toc --apply`, never a hand-written list.
|
||||
|
||||
Prose you write here is English, whatever language the session is being held in -
|
||||
`AGENTS.md` § File naming has both language rules and the line between prose and quoted
|
||||
vocabulary.
|
||||
|
||||
6. **Verify, then publish.** `tools/wikitool docs verify`, `tools/wikitool instructions verify`,
|
||||
and the relevant `pytest` run in `tools/` - the same checks any stack change must pass, run
|
||||
@@ -162,7 +169,7 @@ stack development happens in the origin repo instead (see AGENTS.md's routing li
|
||||
|
||||
**This skill stops here.** The closing phase - rewriting the issue body to its final state,
|
||||
checking for `docs/` staleness, and naming which model ran which phase of the session - lives
|
||||
in [`stack-close`](../stack-close/SKILL.md), not in a further step of this one. Invoke it now;
|
||||
in `stack-close` (`instructions/dev/stack-close/SKILL.md`), not in a further step of this one. Invoke it now;
|
||||
do not fold its work into this session under this skill's rules, and do not treat "the change
|
||||
is published" as this work package being done.
|
||||
|
||||
@@ -176,7 +183,7 @@ stack development happens in the origin repo instead (see AGENTS.md's routing li
|
||||
user decides whether it is worth that: show them what breaks, what an instance has to do about
|
||||
it, and the alternatives (avoid the break with a shim, defer and batch it with the next one,
|
||||
or split it behind a deprecation window), then recommend one and wait for a go-ahead.
|
||||
[version-parts.md](../version-parts.md) step 4 has the full shape. A surfacing boundary crossing
|
||||
`instructions/dev/version-parts.md` step 4 has the full shape. A surfacing boundary crossing
|
||||
is also a reason to offer the model switch back up (step 3): the judgment it needs has no
|
||||
mechanical guard, and `docs verify` only checks that a crossing documents itself, never that the
|
||||
part was chosen correctly.
|
||||
@@ -186,4 +193,4 @@ stack development happens in the origin repo instead (see AGENTS.md's routing li
|
||||
Not for wiki content work - use `wiki-ingest`/`wiki-query`/`wiki-manage`/`wiki-lint`/
|
||||
`wiki-status` for that. Not for setting up a new instance (`instructions/setup-instance.md`) or
|
||||
a fresh clone of this repo (`instructions/bootstrap.md`). Not for closing a work package after
|
||||
its publish has landed - that is [`stack-close`](../stack-close/SKILL.md).
|
||||
its publish has landed - that is `stack-close` (`instructions/dev/stack-close/SKILL.md`).
|
||||
|
||||
@@ -66,7 +66,12 @@ a new one, and only `version release` turns it into something the release workfl
|
||||
|
||||
1. **Heading, author, breaking/migration lines** - written by `version bump`, anchored right
|
||||
above the bump list so the line an operator most needs to act on never sits beneath a list
|
||||
that can run long.
|
||||
that can run long. The breaking line **accumulates** across a candidate's crossings - one
|
||||
reason on the marker line, bullets under a bare marker from the second onward - because a
|
||||
long-running candidate can break compatibility more than once and each break is its own
|
||||
thing to act on. The migration line does not: it answers one yes/no about the candidate as
|
||||
a whole, and `--migration-required` is its retraction path. Nothing retracts a breaking
|
||||
reason; a wrong one is rare enough, and the candidate is dev-local until release.
|
||||
2. **The bump list**, grouped `**High/Medium/Low impact**` (empty groups omitted) - rendered by
|
||||
`version bump`'s `--impact` (default `medium`), corrected after the fact by `version regrade`.
|
||||
Flat and ungrouped, exactly as before this layering existed, when every bump is `medium` - the
|
||||
|
||||
@@ -193,10 +193,17 @@ command you actually need to run, and only with the user's approval.
|
||||
|
||||
A dozen commands are exempt from this budget entirely - `search` and `doctor` because retrieval
|
||||
and diagnosis are reading, not iterating, plus the read-only forms of `links`, `cite`, `budget`,
|
||||
`eval`, `version`, `migrate` and `upstream verify`. The exemption is that fixed allowlist in
|
||||
`eval`, `version`, `migrate` and `upstream verify`. The exemption is that allowlist in
|
||||
[tools/CONTRACT.md](../tools/CONTRACT.md), not a "does not change the wiki" rule of thumb: `lint`
|
||||
only writes to gitignored `reports/` and still counts, because it is not on the list.
|
||||
|
||||
**One entry is read-only only in one of its two forms.** `version regrade` lists the running
|
||||
candidate's graded bump titles when called bare, and writes `CHANGES.md` when called with
|
||||
positions to regrade - so the exemption is per *invocation* there, not per command name. It is
|
||||
the only such case; every other row on the list is exempt however it is called. Its
|
||||
`tools/CONTRACT.md` row says which form is which, which is still the single place that list
|
||||
lives.
|
||||
|
||||
### Taking a new session id
|
||||
|
||||
The budget is scoped by `WIKITOOL_SESSION_ID` ([session-setup.md](session-setup.md)), so a new
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
---
|
||||
type: types/instruction.md
|
||||
name: 6.0.0-type-guidance-split
|
||||
description: "Add a guidance: field to an adopted root:kb type-spec so it starts receiving the stack's authoring-prose improvements again, without touching the type-spec's own frontmatter or template."
|
||||
manual: true
|
||||
migrates_to: 6.0.0
|
||||
migration_kind: assisted
|
||||
obligation: offered
|
||||
---
|
||||
# Link an adopted type-spec to its stack-owned guidance file (6.0.0)
|
||||
|
||||
Before 6.0.0, a `root: kb` type-spec (`entity`, `concept`, `source`, `comparison`, or one this
|
||||
instance added itself) carried its generic authoring prose - when to use the type, when not to,
|
||||
mechanism-level advice such as citation and provenance rules - in the same file as its frontmatter
|
||||
configuration and its `## Template` block. Adopting the type-spec at setup meant adopting all of
|
||||
it at once, and an upgrade never touched the adopted file again: the prose an instance received
|
||||
was frozen at the day it ran `setup-instance.md`, while every later improvement shipped only in
|
||||
the `.template` beside it (`docs/ownership-and-templates.md` § "Where the file boundary used to
|
||||
strain").
|
||||
|
||||
6.0.0 splits that prose into a separate, stack-owned `types/<name>.guidance.md`, linked from the
|
||||
type-spec via an optional `guidance:` frontmatter field. The new file ships verbatim and upgrades
|
||||
like any other machinery file from here on - but only once a type-spec actually points at it.
|
||||
Taking this offer is exactly that: adding one frontmatter line per adopted type-spec. It is
|
||||
`assisted`, not `mechanical`, because whether this instance's own copy of the prose has diverged
|
||||
from the shipped default is a judgment call a script cannot make.
|
||||
|
||||
This migration is **offered, not required**. A type-spec with no `guidance:` keeps working
|
||||
exactly as it did before 6.0.0 - it is described from its own body alone. Declining costs nothing
|
||||
except future improvements to the prose half; nothing about the machinery stops fitting.
|
||||
|
||||
<!-- wikitool:toc -->
|
||||
## Contents
|
||||
|
||||
- [When to run](#when-to-run)
|
||||
- [Steps](#steps)
|
||||
- [How to tell a migrated type-spec from an unmigrated one](#how-to-tell-a-migrated-type-spec-from-an-unmigrated-one)
|
||||
- [Decision points](#decision-points)
|
||||
- [Scope](#scope)
|
||||
<!-- /wikitool:toc -->
|
||||
|
||||
## When to run
|
||||
|
||||
Any time after installing 6.0.0 machinery over an instance that adopted at least one `root: kb`
|
||||
type-spec before this migration existed. `tools/wikitool migrate status` lists it under "optional
|
||||
upgrade(s) available"; taking it is not gated on anything else being current.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Confirm the new guidance files actually arrived.** `dist upgrade` writes `types/<name>.guidance.md`
|
||||
as an ordinary new/unchanged file - it does not depend on this migration at all. If
|
||||
`ls types/*.guidance.md` shows nothing, the machinery upgrade has not landed yet; run that
|
||||
first.
|
||||
|
||||
2. **For each adopted `root: kb` type-spec, decide whether its authoring prose still matches the
|
||||
shipped default.** Compare the type-spec's current prose (everything outside `## Frontmatter`
|
||||
and `## Template`) against the corresponding `types/<name>.guidance.md`:
|
||||
|
||||
- **Unchanged, or changed only in ways this instance is happy to lose:** proceed to step 3
|
||||
directly - the new guidance file already carries the improved version.
|
||||
- **Locally edited in a way worth keeping** (a house style note, an extra rule specific to
|
||||
this corpus): that edit has to move somewhere before the old prose is dropped. Either fold
|
||||
it into a local copy of the guidance file this instance then owns for itself (any path is
|
||||
valid for `guidance:`, not only the shipped one), or keep it in the type-spec's own body
|
||||
instead of adding `guidance:` at all - both are legitimate; declining the stack default for
|
||||
one type is not an error.
|
||||
|
||||
3. **Add `guidance: types/<name>.guidance.md` to the type-spec's frontmatter** - by hand, the same
|
||||
way any other type-spec frontmatter field is written (a type-spec is machinery, not a `kb/`
|
||||
page, so this is not a `wikitool touch` call). Do not remove `## Frontmatter` or `## Template`;
|
||||
only the generic prose around them is what the guidance file now carries.
|
||||
|
||||
4. **Delete the now-duplicated prose from the type-spec**, keeping the H1, a short pointer to the
|
||||
guidance file (`types/entity.md`'s own current text is the worked example), `## Frontmatter`
|
||||
and `## Template`. Where step 2 found a local edit worth keeping and it lives in the
|
||||
type-spec's own body rather than a private guidance file, leave that part exactly where it is.
|
||||
|
||||
5. **Verify:**
|
||||
|
||||
```bash
|
||||
tools/wikitool types describe <name>
|
||||
```
|
||||
|
||||
The output must read the same as it did before this migration - the guidance prose composed
|
||||
ahead of the type-spec's own body, in one answer. A diff against the pre-migration output of
|
||||
the same command, restricted to wording, is expected only where step 2 found something to
|
||||
drop or fold in; the structure (frontmatter fields, template block) must be byte-identical.
|
||||
|
||||
6. **Record it:**
|
||||
|
||||
```bash
|
||||
tools/wikitool migrate done 6.0.0 --pages 0
|
||||
```
|
||||
|
||||
`--pages 0` because no `kb/` page changes - this migration touches machinery under `types/`
|
||||
only. This does **not** advance `kb_version`, per `obligation: offered` above; it only marks
|
||||
the offer as taken so `migrate status` stops listing it.
|
||||
|
||||
## How to tell a migrated type-spec from an unmigrated one
|
||||
|
||||
`grep -L '^guidance:' types/*.md` (excluding `.guidance.md` files themselves, which never carry
|
||||
the field) lists every `root: kb` type-spec that has not taken the offer yet.
|
||||
|
||||
## Decision points
|
||||
|
||||
- **A type this instance wrote entirely for itself?** No `types/<name>.guidance.md` exists for
|
||||
it and none should be authored to match this migration artificially - `guidance:` is for
|
||||
receiving a *stack* default, and a self-written type has none to receive. Leave it as it is.
|
||||
- **Local prose worth keeping, but no interest in maintaining a private guidance file?** Skip
|
||||
`guidance:` for that one type-spec. Nothing forces uniformity across an instance's own types.
|
||||
|
||||
## Scope
|
||||
|
||||
For `types/` machinery, not `kb/` content - the one migration document in this directory that
|
||||
is. No page's frontmatter or body changes, `sources coverage`/`lint`/`kb_version` are all
|
||||
unaffected, and `migrate done`'s `--pages` is `0` for exactly that reason.
|
||||
@@ -57,11 +57,12 @@ refusal. See [gates.md](gates.md).
|
||||
|
||||
## Scope
|
||||
|
||||
**The exemption is a fixed allowlist, not "read-only" or "does not change the wiki."** A command
|
||||
**The exemption is an allowlist, not "read-only" or "does not change the wiki."** A command
|
||||
needs this setup unless it is one of the dozen `tools/CONTRACT.md` marks exempt in its command
|
||||
table (`search`, `doctor`, `links show`, `cite id`, `budget status`, the read-only forms of
|
||||
`eval`, `version`, `migrate` and `upstream verify`) - that table, not a rule of thumb here, is
|
||||
the single list.
|
||||
the single list. One entry on it, `version regrade`, is exempt only in its bare listing form and
|
||||
counted when it is given positions to regrade; every other entry is exempt however it is called.
|
||||
|
||||
`lint` is the case that breaks the "changes the wiki" reading: it only writes to `reports/`,
|
||||
which is gitignored, so it looks side-effect-free - but it is not on the allowlist and is counted
|
||||
|
||||
+180
-180
@@ -1,83 +1,83 @@
|
||||
---
|
||||
type: types/instruction.md
|
||||
name: setup-instance
|
||||
description: Eine frische Distribution (aus `dist export`) in eine funktionsfähige, eigenständige Wiki-Instanz verwandeln - Git-Repo, Identität/Autor, optionaler Remote, Bootstrap, erster Commit.
|
||||
description: Turn a fresh distribution (from `dist export`) into a working, self-contained wiki instance - git repo, identity/author, optional remote, bootstrap, first commit.
|
||||
---
|
||||
|
||||
# Neue Wiki-Instanz einrichten
|
||||
# Set up a new wiki instance
|
||||
|
||||
Diese Anweisung führt eine leere, per `tools/wikitool dist export <ziel>` erzeugte Distribution
|
||||
zu einer funktionsfähigen, eigenständigen Wiki-Instanz - mit eigenem Git-Repo, eigener Autor-
|
||||
Identität und (optional) eigenem Remote. Am Ende ist die Instanz committet, verifiziert und
|
||||
bereit für den ersten `Ingest`.
|
||||
This instruction takes an empty distribution produced by `tools/wikitool dist export <target>`
|
||||
and turns it into a working, self-contained wiki instance - with its own git repo, its own
|
||||
author identity and (optionally) its own remote. At the end the instance is committed, verified
|
||||
and ready for its first ingest.
|
||||
|
||||
<!-- wikitool:toc -->
|
||||
## Contents
|
||||
|
||||
- [Wann anwenden](#wann-anwenden)
|
||||
- [Schritte](#schritte)
|
||||
- [When to run](#when-to-run)
|
||||
- [Steps](#steps)
|
||||
- [Scope](#scope)
|
||||
<!-- /wikitool:toc -->
|
||||
|
||||
## Wann anwenden
|
||||
## When to run
|
||||
|
||||
- Der Nutzer möchte eine neue, leere Wiki-Instanz aufsetzen (eigenes Thema, anderer Nutzer).
|
||||
- Nicht für einen bestehenden Clone dieses (Quell-)Repos - siehe [bootstrap.md](bootstrap.md).
|
||||
- Es gibt keinen Weg zurück: `dist export` lässt `instructions/dev/` (die Stack-Entwicklung
|
||||
selbst, inkl. der vendorten `commonplace/`-Wissensbasis) bewusst und dauerhaft weg. Wer den
|
||||
entstehenden Instanz-Stack weiterentwickeln will, tut das im Ursprungs-Repo (oder einer neuen
|
||||
Dev-Instanz daraus) - nicht durch Nachrüsten in dieser Instanz.
|
||||
- The user wants to set up a new, empty wiki instance (their own subject, a different person).
|
||||
- Not for an existing clone of this (source) repo - see [bootstrap.md](bootstrap.md).
|
||||
- There is no way back: `dist export` deliberately and permanently leaves out
|
||||
`instructions/dev/` (stack development itself, including the vendored `commonplace/` knowledge
|
||||
base). Anyone who wants to develop the resulting instance's stack further does that in the
|
||||
origin repo (or a new dev instance made from it) - not by retrofitting it into this instance.
|
||||
|
||||
## Schritte
|
||||
## Steps
|
||||
|
||||
1. **Distribution exportieren**, im Quell-Repo:
|
||||
1. **Export the distribution**, in the source repo:
|
||||
|
||||
```bash
|
||||
tools/wikitool dist export <ziel>
|
||||
tools/wikitool dist export <target>
|
||||
```
|
||||
|
||||
`<ziel>` muss nicht existieren oder leer sein; der Befehl bricht sonst mit `ERROR` ab. Danach
|
||||
für alle folgenden Schritte in `<ziel>` arbeiten.
|
||||
`<target>` must not exist, or must be empty; otherwise the command aborts with `ERROR`. Work
|
||||
inside `<target>` for every step that follows.
|
||||
|
||||
2. **Git-Repo initialisieren:**
|
||||
2. **Initialize the git repo:**
|
||||
|
||||
```bash
|
||||
git init -b main
|
||||
```
|
||||
|
||||
`-b main` ist Pflicht: `tools/wikitool publish` prüft beim tatsächlichen Push, ob der
|
||||
ausgecheckte Branch dem Ziel-Branch entspricht (Default `main`), und lehnt sonst ab, um
|
||||
nicht den falschen Branch zu veröffentlichen.
|
||||
`-b main` is mandatory: on the actual push, `tools/wikitool publish` checks that the
|
||||
checked-out branch matches the target branch (default `main`) and refuses otherwise, so that
|
||||
the wrong branch is never published.
|
||||
|
||||
3. **Entscheidungspunkt - Identität.** Frage den Nutzer nach Namen und E-Mail-Adresse; rate sie
|
||||
nie, und übernimm sie nie stillschweigend aus dem Quell-Repo (das ist eine andere Person, ein
|
||||
anderes Projekt):
|
||||
3. **Decision point - identity.** Ask the user for their name and email address; never guess
|
||||
them, and never quietly carry them over from the source repo (that is a different person and
|
||||
a different project):
|
||||
|
||||
```bash
|
||||
git config user.name "<Name>"
|
||||
git config user.email "<E-Mail>"
|
||||
git config user.name "<name>"
|
||||
git config user.email "<email>"
|
||||
```
|
||||
|
||||
Das setzt zugleich den Autor jeder künftig angelegten Wiki-Seite: `tools/wikitool new`
|
||||
löst `author:` über `$WIKI_AUTHOR` (Override) oder sonst `git config user.name` auf und
|
||||
bricht mit `ERROR` ab, wenn beides fehlt - es gibt keinen stillen Platzhalter.
|
||||
This also sets the author of every wiki page created from now on: `tools/wikitool new`
|
||||
resolves `author:` from `$WIKI_AUTHOR` (an override) or else from `git config user.name`, and
|
||||
aborts with `ERROR` when both are missing - there is no silent placeholder.
|
||||
|
||||
4. **Entscheidungspunkt - Remote.** Frage den Nutzer nach einer Remote-URL; ein rein lokales
|
||||
Repo ist ein gültiger Endzustand:
|
||||
- Genannt: `git remote add origin <url>`
|
||||
- Nicht genannt: lokal bleiben - dann braucht **jeder** spätere `tools/wikitool publish`
|
||||
ein `--no-push` (dessen Branch-Prüfung dabei ohnehin entfällt, siehe Schritt 2).
|
||||
4. **Decision point - remote.** Ask the user for a remote URL; a purely local repo is a valid
|
||||
end state:
|
||||
- Given: `git remote add origin <url>`
|
||||
- Not given: stay local - then **every** later `tools/wikitool publish` needs a `--no-push`
|
||||
(which also drops its branch check, see step 2).
|
||||
|
||||
5. **Entscheidungspunkt - Autorenkonventionen.** Die Distribution bringt keine ausgefüllten
|
||||
Konventionen mit, sondern `kb/CONVENTIONS.md.template` und je Collection ein
|
||||
`kb/<name>/COLLECTION.md.template`. Beide **binden**, sobald sie übernommen sind, und beide
|
||||
gehören dieser Instanz - deshalb liefert der Stack nur die Vorlage. Die eine Entscheidung
|
||||
dahinter ist: **in welcher Sprache und in welchem Ton schreibt diese Instanz ihre Seiten?**
|
||||
5. **Decision point - authoring conventions.** The distribution ships no filled-in conventions,
|
||||
only `kb/CONVENTIONS.md.template` and one `kb/<name>/COLLECTION.md.template` per collection.
|
||||
Both **bind** once adopted, and both belong to this instance - which is why the stack ships
|
||||
the template alone. The one decision behind them is: **in which language and in what tone
|
||||
does this instance write its pages?**
|
||||
|
||||
Ablauf:
|
||||
Procedure:
|
||||
|
||||
1. Die Collection-Contracts **und die Page-Type-Specs** übernehmen - Kopien, keine Frage an
|
||||
den Nutzer, denn was dort steht ist als Ausgangspunkt unabhängig von der Sprache brauchbar:
|
||||
1. Adopt the collection contracts **and the page type-specs** - copies, no question to the
|
||||
user, because what they say is usable as a starting point regardless of language:
|
||||
|
||||
```bash
|
||||
for template in kb/*/COLLECTION.md.template types/*.template; do
|
||||
@@ -85,110 +85,114 @@ bereit für den ersten `Ingest`.
|
||||
done
|
||||
```
|
||||
|
||||
Die `.template`-Dateien bleiben liegen; sie sind die Vorlage für den nächsten Export.
|
||||
The `.template` files stay where they are; they are the source for the next export.
|
||||
|
||||
Unter `types/` betrifft das genau die Type-Specs mit `root: kb` - `entity`, `concept`,
|
||||
`source`, `comparison` - samt ihrer `.schema.yaml`. Sie beschreiben Seiten, die *diese*
|
||||
Instanz schreibt, also gehören sie ihr: Prosa, Template und Sprache dürfen umgeschrieben
|
||||
werden. `instruction`, `lint-report` und `type-spec` beschreiben Stack-Artefakte und
|
||||
kommen unverändert.
|
||||
Under `types/` this covers exactly the type-specs with `root: kb` - `entity`, `concept`,
|
||||
`source`, `comparison` - along with their `.schema.yaml`. They describe pages *this*
|
||||
instance writes, so they belong to it: frontmatter, template and language may all be
|
||||
rewritten. `instruction`, `lint-report`, `type-spec` and `type-guidance` describe stack
|
||||
artifacts and arrive unchanged - the glob above never matches them because none of them
|
||||
ships as a `.template` in the first place.
|
||||
|
||||
2. Den Nutzer nach der KB-Sprache fragen. `kb/CONVENTIONS.md.template` ist auf **Englisch**
|
||||
voreingestellt; [kb-profiles.md](kb-profiles.md) hält daneben ein vollständiges
|
||||
deutsches Profil bereit, und dessen Volltext ist die `kb/CONVENTIONS.md` des Quell-Repos.
|
||||
Der Profilkatalog ist eine **Palette, kein Enum**: übernommen wird der Text *in* die
|
||||
Instanzdatei, nicht ein Verweis auf den Katalog.
|
||||
A `root: kb` type-spec's generic authoring guidance (when to use the type, when not to)
|
||||
is not part of this adoption at all: it lives in a sibling `types/<name>.guidance.md`
|
||||
this instance never renames, the same as `instruction.md` - it ships verbatim and a later
|
||||
`dist upgrade` improves it directly, without the type-spec that links it needing to be
|
||||
touched. `types/type-spec.md` § "Anatomy of a type" has the shape.
|
||||
|
||||
3. `kb/CONVENTIONS.md.template` nach `kb/CONVENTIONS.md` kopieren, entlang des gewählten
|
||||
Profils ausfüllen - Sprache, Abschnittsnamen, Namensformen, Ton, Beziehungslabels,
|
||||
Hedging-Regel - und dabei die Sentinel-Zeile (`wikitool:template-unfilled`) entfernen.
|
||||
Die Platzhalter in geschweiften Klammern **sind** der Fragenkatalog.
|
||||
2. Ask the user for the KB language. `kb/CONVENTIONS.md.template` defaults to **English**;
|
||||
[kb-profiles.md](kb-profiles.md) additionally holds a complete German profile, whose full
|
||||
text is the source repo's own `kb/CONVENTIONS.md`. The profile catalogue is a **palette,
|
||||
not an enum**: what gets adopted is the text *into* the instance file, not a reference to
|
||||
the catalogue.
|
||||
|
||||
4. Bei einer anderen Sprache als der des Quell-Repos: `german-terminology.md` löschen oder
|
||||
durch das eigene Vokabular ersetzen - sie ist Material des deutschen Profils, nicht des
|
||||
Stacks.
|
||||
3. Copy `kb/CONVENTIONS.md.template` to `kb/CONVENTIONS.md`, fill it in along the chosen
|
||||
profile - language, section names, naming forms, tone, relationship labels, hedging rule -
|
||||
and remove the sentinel line (`wikitool:template-unfilled`) while doing so. The
|
||||
placeholders in curly braces **are** the list of questions.
|
||||
|
||||
5. Den Nutzer nach dem Anwendungsgebiet fragen und daraus einen `source_type`-Vorschlag
|
||||
ableiten. [kb-profiles.md](kb-profiles.md) hält dafür zwei ausformulierte Domänenprofile
|
||||
als Anschauung bereit, neben dem Wert, den dieses Repo selbst nutzt. Der Vorschlag ist ein
|
||||
**Startpunkt, keine Festlegung** - zum Setup-Zeitpunkt hat der Betreiber null Quellen und
|
||||
rät seine Taxonomie, bevor er auch nur eine Datei gesehen hat, und das ist der
|
||||
schlechteste Moment, ein Enum festzuzurren. Vorschlag umgesetzt heißt: das Enum in
|
||||
`types/source.schema.yaml` **und** die passende `layout:`-Zeile je Wert in
|
||||
`types/source.md` in derselben Bearbeitung setzen - eine ohne die andere lässt einen Wert
|
||||
ohne Zielverzeichnis zurück. Der sichtbare Auffangwert (`unclassified`) bleibt in jedem
|
||||
Vorschlag erhalten; er ist kein Sammelbecken, sondern das Fach für eine Quelle, deren
|
||||
Kategorie noch nicht feststeht. Die Liste später erweitern oder das Fach leeren:
|
||||
[evolve-subtypes.md](evolve-subtypes.md) - nicht Teil dieses Schritts, aber der Weg dahin,
|
||||
sobald echtes Material vorliegt.
|
||||
4. For a language other than the source repo's: delete `german-terminology.md` or replace it
|
||||
with your own vocabulary - it is material belonging to the German profile, not to the
|
||||
stack.
|
||||
|
||||
**Unverändert lassen:** `fidelity` und `authority` auf `source`-Seiten. Die sind
|
||||
Stack-Vokabular, keine Instanzentscheidung - [kb-profiles.md](kb-profiles.md) sagt das im
|
||||
selben Abschnitt.
|
||||
5. Ask the user about the subject area and derive a `source_type` proposal from it.
|
||||
[kb-profiles.md](kb-profiles.md) holds two worked domain profiles as illustration, beside
|
||||
the value this repo uses itself. The proposal is a **starting point, not a commitment** -
|
||||
at setup time the operator has zero sources and is guessing a taxonomy before having seen
|
||||
a single file, which is the worst possible moment to pin an enum down. Carrying out the
|
||||
proposal means setting the enum in `types/source.schema.yaml` **and** the matching
|
||||
`layout:` line per value in `types/source.md` in the same edit - one without the other
|
||||
leaves a value with no target directory. The visible catch-all (`unclassified`) survives
|
||||
every proposal; it is not a dumping ground but the slot for a source whose category is not
|
||||
settled yet. Extending the list later, or emptying that slot:
|
||||
[evolve-subtypes.md](evolve-subtypes.md) - not part of this step, but the way there once
|
||||
real material exists.
|
||||
|
||||
**Vor dem ersten Ingest entscheiden.** Die `sections:`-Namen in `kb/CONVENTIONS.md` sind die
|
||||
Überschriften, die `xref` und `cite` in jede Seite schreiben; sie danach zu ändern ist eine
|
||||
Migration jeder vorhandenen Seite (`section_aliases:` trägt die alten Namen, siehe
|
||||
**Leave unchanged:** `fidelity` and `authority` on `source` pages. Those are stack
|
||||
vocabulary, not an instance decision - [kb-profiles.md](kb-profiles.md) says so in the
|
||||
same section.
|
||||
|
||||
**Decide before the first ingest.** The `sections:` names in `kb/CONVENTIONS.md` are the
|
||||
headings `xref` and `cite` write into every page; changing them afterwards is a migration of
|
||||
every existing page (`section_aliases:` carries the old names, see
|
||||
[migrate-corpus.md](migrate-corpus.md)).
|
||||
|
||||
**Nichts davon liegt in einer Stack-Datei.** Der Compiler liest die Abschnittsnamen aus
|
||||
`kb/CONVENTIONS.md`; die vier Page-Type-Specs gehören ab Schritt 1 dieser Instanz. Eine
|
||||
anderssprachige Instanz übersetzt sie einfach - das ist kein lokaler Patch an etwas
|
||||
Ausgeliefertem mehr, sondern Arbeit an den eigenen Dateien, und ein Upgrade nimmt sie ihr
|
||||
nicht wieder weg.
|
||||
**None of this lives in a stack file.** The compiler reads the section names from
|
||||
`kb/CONVENTIONS.md`; the four page type-specs have belonged to this instance since step 1. An
|
||||
instance in another language simply translates them - that is no longer a local patch to
|
||||
something shipped, but work on its own files, and an upgrade does not take it away again.
|
||||
|
||||
Was der Stack von `types/` überhaupt noch verlangt, ist eine Zeile: es muss einen Type-Spec
|
||||
mit `name: source` geben, dessen Schema `raw_files` fordert. Daran hängt der gesamte
|
||||
`raw/`→`kb/`-Provenance-Pfad (`sources coverage`, `[^cite-id]`-Auflösung, `kb/provenance.md`),
|
||||
und `docs verify` prüft genau das - nicht mehr.
|
||||
What the stack still requires of `types/` is one line: there must be a type-spec with
|
||||
`name: source` whose schema requires `raw_files`. The entire `raw/`→`kb/` provenance path
|
||||
hangs on it (`sources coverage`, `[^cite-id]` resolution, `kb/provenance.md`), and
|
||||
`docs verify` checks exactly that - no more.
|
||||
|
||||
Unverändert bleibt in jedem Fall die Regel, die dem Stack gehört: **jede Zeile einer Seite
|
||||
ist Prosa oder Identifier, und nur Prosa wird übersetzt** ([kb/CONTRACT.md § Language and
|
||||
identifiers](../kb/CONTRACT.md#language-and-identifiers)). Titel, Wikilink-Ziele, Cite-IDs,
|
||||
Enum-Werte, Tags, Befehle und Pfade folgen keiner KB-Sprache.
|
||||
What stays untouched in every case is the rule the stack owns: **every line of a page is
|
||||
either prose or an identifier, and only prose is translated** ([kb/CONTRACT.md § Language and
|
||||
identifiers](../kb/CONTRACT.md#language-and-identifiers)). Titles, wikilink targets, cite ids,
|
||||
enum values, tags, commands and paths follow no KB language.
|
||||
|
||||
`tools/wikitool doctor` prüft das Ergebnis in Schritt 13 (`conventions`): eine fehlende
|
||||
Datei ist ein `FAIL`, eine mit Sentinel oder ohne vollständigen `sections:`-Block ebenso.
|
||||
`docs verify` prüft zusätzlich `profile:` und `required_by_stack:` auf jedem
|
||||
`tools/wikitool doctor` checks the result in step 13 (`conventions`): a missing file is a
|
||||
`FAIL`, and so is one carrying the sentinel or lacking a complete `sections:` block.
|
||||
`docs verify` additionally checks `profile:` and `required_by_stack:` on every
|
||||
`COLLECTION.md`.
|
||||
|
||||
6. **Entscheidungspunkt - Personalization.** Die Distribution bringt
|
||||
`USER.md.template` und `SOUL.md.template` mit, aber keine ausgefüllten Fassungen: wer diese
|
||||
Instanz bedient und wie sie klingt, ist Eigentum genau dieser Instanz und wird nie aus dem
|
||||
Quell-Repo übernommen. Beide Dateien werden ab jetzt in **jeder** Session gelesen, also
|
||||
entstehen sie hier - nicht später bei Gelegenheit.
|
||||
6. **Decision point - personalization.** The distribution ships `USER.md.template` and
|
||||
`SOUL.md.template`, but no filled-in versions: who operates this instance and how it sounds
|
||||
is the property of this instance alone and is never carried over from the source repo. Both
|
||||
files are read in **every** session from now on, so they come into being here - not later,
|
||||
when the occasion arises.
|
||||
|
||||
Ablauf, für `USER.md` und `SOUL.md` je einmal:
|
||||
Procedure, once each for `USER.md` and `SOUL.md`:
|
||||
|
||||
1. Das Template lesen. Seine Abschnitte **sind** der Fragenkatalog, in der Reihenfolge, in
|
||||
der sie dort stehen.
|
||||
2. Den Nutzer entlang dieser Abschnitte befragen - `USER.md`: Name, Standort, Zeitzone,
|
||||
primäre Rolle (rein beruflich), beruflicher Kontext, Familie/Zuhause, Hobbys,
|
||||
Technik-Umgebung, aktive Projekte, bewusste Grenzen. `SOUL.md`: Persona-Name, Identität,
|
||||
Mission, Weltbild, Judgment-Default, Standard, Ehrlichkeit, Stimme, Ausschlüsse.
|
||||
3. Die Antworten **wörtlich** übernehmen. Nicht deuten, nicht zu einer Erzählung
|
||||
verdichten, nicht aus dem Gesprächsverlauf ableiten. Was der Nutzer nicht sagt, steht
|
||||
nicht drin: einen Abschnitt lieber löschen als mit Plausiblem füllen.
|
||||
4. Das Ergebnis als `USER.md` bzw. `SOUL.md` schreiben und die Sentinel-Zeile
|
||||
(`wikitool:template-unfilled`) dabei entfernen. Die `.template`-Dateien bleiben liegen -
|
||||
sie sind die Vorlage für den nächsten Export, nicht Abfall dieses Schritts.
|
||||
1. Read the template. Its sections **are** the list of questions, in the order they appear.
|
||||
2. Interview the user along those sections - `USER.md`: name, location, time zone, primary
|
||||
role (professional only), professional context, family/home, hobbies, technical
|
||||
environment, active projects, deliberate boundaries. `SOUL.md`: persona name, identity,
|
||||
mission, worldview, judgment default, standard, honesty, voice, exclusions.
|
||||
3. Take the answers **verbatim**. Do not interpret, do not compress into a narrative, do not
|
||||
infer from the course of the conversation. What the user does not say does not go in:
|
||||
better to delete a section than to fill it with something plausible.
|
||||
4. Write the result as `USER.md` and `SOUL.md` respectively, removing the sentinel line
|
||||
(`wikitool:template-unfilled`) in the process. The `.template` files stay where they are -
|
||||
they are the source for the next export, not this step's leftovers.
|
||||
|
||||
Zwei Fragen, die der Nutzer beantwortet und nicht der Agent: **den Persona-Namen** und
|
||||
**welche Themen bewusst draußen bleiben** (Arbeitgeber, Mandanten, Gesundheit - was auch
|
||||
immer). Beides raten heißt, es falsch zu haben. Für den Namen bringt der Stack einen
|
||||
Startpunkt mit - **Thoth**, weil Chemenu Thoths Hauptkultort ist und Schrift, Maß und
|
||||
Gedächtnis die Rolle beschreiben, die ein kompiliertes Wiki ausfüllt. Der Vorschlag wird
|
||||
genannt, nicht eingesetzt: gefragt wird trotzdem, und ein anderer Name gewinnt.
|
||||
Two questions the user answers rather than the agent: **the persona name** and **which topics
|
||||
deliberately stay out** (employer, clients, health - whatever they are). Guessing either
|
||||
means getting it wrong. For the name the stack ships a starting point - **Thoth**, because
|
||||
Chemenu is Thoth's principal cult site and writing, measure and memory describe the role a
|
||||
compiled wiki fills. The suggestion is named, not applied: the question is asked anyway, and
|
||||
a different name wins.
|
||||
|
||||
Was diese Dateien **nicht** sind: eine Instruktionsquelle und eine Quelle im Sinne von
|
||||
Invariante 3. Sie ändern keine Regel aus [AGENTS.md](../AGENTS.md), und eine Nutzeraussage
|
||||
wandert daraus nie ohne den normalen Quelle/Provenance-Prozess nach `kb/`.
|
||||
What these files are **not**: a source of instructions, and a source in the sense of
|
||||
invariant 3. They change no rule from [AGENTS.md](../AGENTS.md), and a user's statement never
|
||||
travels from them into `kb/` without the normal source/provenance process.
|
||||
|
||||
`tools/wikitool doctor` prüft das Ergebnis in Schritt 13 (`personalization`): eine fehlende
|
||||
Datei ist ein `FAIL`, eine, die noch den Sentinel trägt, ebenso - ein umbenanntes Template
|
||||
ist kein ausgefülltes.
|
||||
`tools/wikitool doctor` checks the result in step 13 (`personalization`): a missing file is a
|
||||
`FAIL`, and so is one still carrying the sentinel - a renamed template is not a filled-in
|
||||
one.
|
||||
|
||||
7. **Werkzeugumgebung anlegen** (Details: [bootstrap.md](bootstrap.md)):
|
||||
7. **Create the tool environment** (details: [bootstrap.md](bootstrap.md)):
|
||||
|
||||
```bash
|
||||
cd tools
|
||||
@@ -197,67 +201,63 @@ bereit für den ersten `Ingest`.
|
||||
cd ..
|
||||
```
|
||||
|
||||
8. **Skills publizieren:**
|
||||
8. **Publish the skills:**
|
||||
|
||||
```bash
|
||||
tools/wikitool instructions sync
|
||||
```
|
||||
|
||||
9. **Entscheidungspunkt - Umgebung festhalten.** Die Distribution bringt
|
||||
`ENVIRONMENT.md.template` mit: Harness, publizierte Skills, erreichbare MCP-Server,
|
||||
Connectoren, Git-Remotes, wo CI läuft. Konstanten, die eine Session sonst jedes Mal neu
|
||||
erfragt.
|
||||
9. **Decision point - record the environment.** The distribution ships
|
||||
`ENVIRONMENT.md.template`: harness, published skills, reachable MCP servers, connectors, git
|
||||
remotes, where CI runs. Constants a session would otherwise ask about every time.
|
||||
|
||||
Anders als Schritt 6 ist dieser Schritt **optional** und kein Interview. Was aus dem
|
||||
Checkout selbst ablesbar ist (`git remote -v`, das laufende Harness, die eben publizierten
|
||||
Skills), trägt der Agent ein; nach dem Rest fragt er einmal und akzeptiert "weiß ich nicht"
|
||||
als Antwort - ein leerer Abschnitt wird gelöscht, nicht mit Plausiblem gefüllt. Beim
|
||||
Schreiben die Sentinel-Zeile (`wikitool:template-unfilled`) entfernen; das `.template`
|
||||
bleibt liegen.
|
||||
Unlike step 6, this step is **optional** and not an interview. Whatever can be read off the
|
||||
checkout itself (`git remote -v`, the running harness, the skills just published) the agent
|
||||
fills in; for the rest it asks once and accepts "I don't know" as an answer - an empty
|
||||
section is deleted, not filled with something plausible. Remove the sentinel line
|
||||
(`wikitool:template-unfilled`) when writing; the `.template` stays where it is.
|
||||
|
||||
Wird der Schritt übersprungen, läuft alles weiter: `doctor` meldet in Schritt 13
|
||||
`environment: absent (optional)`, kein `FAIL`. Die Datei ist gitignored und geht in keinen
|
||||
Commit ein - sie beschreibt diesen Checkout, nicht das Repo.
|
||||
If the step is skipped, everything still works: `doctor` reports
|
||||
`environment: absent (optional)` in step 13, not a `FAIL`. The file is gitignored and enters
|
||||
no commit - it describes this checkout, not the repo.
|
||||
|
||||
10. **Entscheidungspunkt - Telemetrie.** Der Default hängt am Installationsweg, nicht an
|
||||
diesem Schritt: eine per `dist export` ausgelieferte Instanz - jede, die hier ankommt, ohne
|
||||
Weg C (direkter Klon des Ursprungs-Repos) genommen zu haben - trägt eine
|
||||
`.wikitool-release.json` und startet mit Telemetrie **aus**; niemand hat sie bestellt, und
|
||||
`EVALS.md` liest ohnehin niemand, bevor die erste Datei geschrieben ist. Dieser Schritt
|
||||
fragt nur, ob der Betreiber das umdrehen will.
|
||||
10. **Decision point - telemetry.** The default follows the installation path, not this step: an
|
||||
instance delivered via `dist export` - every instance that arrives here without having taken
|
||||
route C (a direct clone of the origin repo) - carries a `.wikitool-release.json` and starts
|
||||
with telemetry **off**; nobody asked for it, and nobody reads `EVALS.md` before the first
|
||||
file is written anyway. This step only asks whether the operator wants to reverse that.
|
||||
|
||||
Den Nutzer einmal fragen: Telemetrie an? Falls ja, `.wikitool-telemetry.json` im
|
||||
Repo-Root anlegen (pro Checkout, gitignored, kein `.template` - wie
|
||||
`.wikitool-remotes.json`):
|
||||
Ask the user once: telemetry on? If yes, create `.wikitool-telemetry.json` in the repo root
|
||||
(per checkout, gitignored, no `.template` - like `.wikitool-remotes.json`):
|
||||
|
||||
```json
|
||||
{ "enabled": true }
|
||||
```
|
||||
|
||||
`max_session_bytes` (Default 5 MiB) und `keep_sessions` (Default 250) sind optional in
|
||||
derselben Datei; die meisten Instanzen brauchen sie nicht anzufassen. Falls nein, nichts
|
||||
tun - der Default steht bereits auf aus, und keine Datei entsteht. `WIKI_TRACE`
|
||||
überschreibt beide Richtungen weiterhin, falls eine einzelne Session abweichen soll.
|
||||
`max_session_bytes` (default 5 MiB) and `keep_sessions` (default 250) are optional in the
|
||||
same file; most instances need not touch them. If no, do nothing - the default is already
|
||||
off, and no file is created. `WIKI_TRACE` still overrides in both directions, should a
|
||||
single session need to differ.
|
||||
|
||||
`tools/wikitool doctor` meldet das Ergebnis in Schritt 13 (`telemetry`): an/aus, warum
|
||||
(Installationsform, diese Datei, oder `WIKI_TRACE`), und die aktuelle Menge gegen beide
|
||||
Deckel - nie ein `FAIL`, in beide Richtungen ist das ein gültiger Zustand. Mehr dazu:
|
||||
`tools/wikitool doctor` reports the result in step 13 (`telemetry`): on/off, why
|
||||
(installation form, this file, or `WIKI_TRACE`), and the current volume against both caps -
|
||||
never a `FAIL`, since both directions are a valid state. More on this:
|
||||
[EVALS.md](../EVALS.md) § "Whether it runs at all".
|
||||
|
||||
11. **Session-Budget scopen** (Details: [session-setup.md](session-setup.md)):
|
||||
11. **Scope the session budget** (details: [session-setup.md](session-setup.md)):
|
||||
|
||||
```bash
|
||||
export WIKITOOL_SESSION_ID="wiki-$(date +%s)"
|
||||
```
|
||||
|
||||
12. **Generierte Indizes erzeugen** - `dist export` liefert sie bewusst nicht mit:
|
||||
12. **Build the generated indexes** - `dist export` deliberately does not ship them:
|
||||
|
||||
```bash
|
||||
tools/wikitool index rebuild
|
||||
tools/wikitool sources rebuild-index
|
||||
```
|
||||
|
||||
13. **Verifizieren**, in dieser Reihenfolge:
|
||||
13. **Verify**, in this order:
|
||||
|
||||
```bash
|
||||
tools/wikitool doctor
|
||||
@@ -266,32 +266,32 @@ bereit für den ersten `Ingest`.
|
||||
tools/wikitool lint
|
||||
```
|
||||
|
||||
`doctor` muss ohne `FAIL` durchlaufen, bevor es weitergeht - ein `WARN` (z. B. kein Remote,
|
||||
keine `WIKITOOL_SESSION_ID`) ist kein Blocker. Ein `FAIL` benennt sein eigenes Fix-Kommando;
|
||||
das ausführen und `doctor` erneut aufrufen.
|
||||
`doctor` must run through without a `FAIL` before anything continues - a `WARN` (no remote,
|
||||
no `WIKITOOL_SESSION_ID`, say) is not a blocker. A `FAIL` names its own fix command; run it
|
||||
and call `doctor` again.
|
||||
|
||||
14. **Ersten Commit anstoßen:**
|
||||
14. **Make the first commit:**
|
||||
|
||||
```bash
|
||||
tools/wikitool publish --message "chore: initial instance setup"
|
||||
```
|
||||
|
||||
Das Mass-Update-Gate greift hier erwartungsgemäß: eine frische Distribution besteht aus weit
|
||||
mehr als den zehn gezählten Dateien, die den Schwellwert auslösen, also endet der Aufruf mit
|
||||
Exit-Code 42. Die Ausgabe dem Nutzer **vollständig zeigen** und warten; sie enthält die
|
||||
Dateiliste und die exakte `--confirm <token>`-Zeile, die nach seiner Freigabe
|
||||
veröffentlicht. Details zum Gate: [gates.md](gates.md).
|
||||
The Mass-Update Gate fires here as expected: a fresh distribution consists of far more than
|
||||
the ten counted files that trip the threshold, so the call ends with exit code 42. Show the
|
||||
output to the user **in full** and wait; it contains the file list and the exact
|
||||
`--confirm <token>` line that publishes once they approve. Details on the gate:
|
||||
[gates.md](gates.md).
|
||||
|
||||
15. **Agent-Session neu starten.** Harnesses lesen die Skill-Verzeichnisse beim Start; erst
|
||||
danach sind `wiki-ingest`, `wiki-query`, `wiki-manage`, `wiki-lint` und `wiki-status`
|
||||
verfügbar.
|
||||
15. **Restart the agent session.** Harnesses read the skill directories at startup; only
|
||||
afterwards are `wiki-ingest`, `wiki-query`, `wiki-manage`, `wiki-lint` and `wiki-status`
|
||||
available.
|
||||
|
||||
## Scope
|
||||
|
||||
Gilt nur für eine per `dist export` erzeugte, leere Distribution. Für einen bestehenden Clone
|
||||
dieses Quell-Repos siehe [bootstrap.md](bootstrap.md) - dort existieren Git-Repo, Autor und
|
||||
Inhalt bereits, und nur die Werkzeugumgebung (Schritt 7) plus die Skills (Schritt 8) fehlen.
|
||||
Applies only to an empty distribution produced by `dist export`. For an existing clone of this
|
||||
source repo see [bootstrap.md](bootstrap.md) - there the git repo, author and content already
|
||||
exist, and only the tool environment (step 7) plus the skills (step 8) are missing.
|
||||
|
||||
Eine Ausnahme: Schritt 6 (Personalization) gilt auch für einen bestehenden Clone, der noch
|
||||
kein `USER.md`/`SOUL.md` hat - dort als einzelner nachgeholter Schritt, nicht als ganzer
|
||||
Ablauf. `bootstrap.md` verweist dafür hierher.
|
||||
One exception: step 6 (personalization) also applies to an existing clone that has no
|
||||
`USER.md`/`SOUL.md` yet - there as a single catch-up step, not as a whole procedure.
|
||||
`bootstrap.md` points here for it.
|
||||
|
||||
@@ -10,7 +10,7 @@ description: Process a new source file into the LLM wiki - extract entities and
|
||||
**Trigger:** User drops a file into `incoming/` (the normal path - see step 1) or directly into
|
||||
`raw/`, or explicitly requests ingestion.
|
||||
|
||||
**Before the first `wikitool` call:** [session-setup.md](../session-setup.md).
|
||||
**Before the first `wikitool` call:** `instructions/session-setup.md`.
|
||||
|
||||
Contracts are read **when the step needs them**, not upfront: a source that produces no concept
|
||||
pages should never have cost the concept contract. Field-level requirements always come from
|
||||
@@ -40,7 +40,7 @@ validator complains - and the ticked list is the only record that they happened.
|
||||
## Steps
|
||||
|
||||
1. **Promote from `incoming/` if that is where the file sits.** Read
|
||||
[raw/CONTRACT.md](../../raw/CONTRACT.md) "Getting a file in" and "Capture fields" if you have
|
||||
`raw/CONTRACT.md` "Getting a file in" and "Capture fields" if you have
|
||||
not this session - the directory and any bundling are computed, never chosen by hand, but the
|
||||
two capture flags are not: `raw accept` refuses without them.
|
||||
|
||||
@@ -63,7 +63,7 @@ validator complains - and the ticked list is the only record that they happened.
|
||||
**A file that arrived through the MCP `submit` tool is not yet in `incoming/`** - it sits in
|
||||
`mcp-upload/<id>/`, a quarantine no command in this step reads. A reviewer promotes it first
|
||||
with `wikitool upload accept <id> --confirm <token>`, per
|
||||
[instructions/ingest-queue.md](../ingest-queue.md); once accepted it is an ordinary file in
|
||||
`instructions/ingest-queue.md`; once accepted it is an ordinary file in
|
||||
`incoming/` and this step applies to it exactly as to anything dropped there by hand.
|
||||
|
||||
**If this refuses because the name is already claimed** (a file stem or a bundle directory
|
||||
@@ -79,7 +79,7 @@ validator complains - and the ticked list is the only record that they happened.
|
||||
|
||||
**Check the size first, on both axes.** *Volume* - how many raw files this ingest covers -
|
||||
and *breadth* - how many entities and concepts this one source would produce or update.
|
||||
Either one past the thresholds in [ingest-large-tree.md](../ingest-large-tree.md) § When to
|
||||
Either one past the thresholds in `instructions/ingest-large-tree.md` § When to
|
||||
run is that procedure, not this one: stop and follow it. There, volume is cut into units;
|
||||
breadth cannot be cut at all (`raw/` keeps a file whole, and one raw file has one owning
|
||||
source page) and buys an extract pass instead, before any page is written. Skipping either
|
||||
@@ -107,7 +107,7 @@ validator complains - and the ticked list is the only record that they happened.
|
||||
which entities/concepts to create or update, any specific emphasis.
|
||||
|
||||
6. **Create the source page.** Read
|
||||
[kb/sources/COLLECTION.md](../../kb/sources/COLLECTION.md) first - it holds what this
|
||||
`kb/sources/COLLECTION.md` first - it holds what this
|
||||
instance expects of a source page's sections and how it names one.
|
||||
|
||||
```bash
|
||||
@@ -140,18 +140,18 @@ validator complains - and the ticked list is the only record that they happened.
|
||||
article also pass `--set source_url=<upstream URL>`; `raw_files:` must still point at the
|
||||
local copy. Then write the Summary / Key Takeaways / Action Items prose from step 5 - in the
|
||||
KB language, whatever the source's own language is, quoting verbatim passages in the
|
||||
original. Which language that is: [kb/CONVENTIONS.md](../../kb/CONVENTIONS.md#language).
|
||||
original. Which language that is: `kb/CONVENTIONS.md` § Language.
|
||||
What is exempt from it, in any language:
|
||||
[kb/CONTRACT.md](../../kb/CONTRACT.md#language-and-identifiers).
|
||||
`kb/CONTRACT.md` § Language and identifiers.
|
||||
|
||||
Fill `## Not Extracted` in the same pass: what you read and deliberately did not promote,
|
||||
with the reason. Nothing in the repository can re-derive that judgment, and without it the
|
||||
same source gets re-litigated on the next pass.
|
||||
|
||||
7. **Create or update entity pages.** Read
|
||||
[kb/entities/COLLECTION.md](../../kb/entities/COLLECTION.md) and
|
||||
[kb/CONTRACT.md](../../kb/CONTRACT.md) plus
|
||||
[kb/CONVENTIONS.md](../../kb/CONVENTIONS.md) first - the second is where provenance and
|
||||
`kb/entities/COLLECTION.md` and
|
||||
`kb/CONTRACT.md` plus
|
||||
`kb/CONVENTIONS.md` first - the second is where provenance and
|
||||
citation are defined, the third where this instance's tone and naming forms are.
|
||||
|
||||
**A subject earns a page when the source carries material for one.** A name the source
|
||||
@@ -186,7 +186,7 @@ validator complains - and the ticked list is the only record that they happened.
|
||||
|
||||
8. **Create or update concept pages** - only if the source produced any. Same pattern, including
|
||||
step 7's rule about which subjects earn a page at all, reading
|
||||
[kb/concepts/COLLECTION.md](../../kb/concepts/COLLECTION.md) first:
|
||||
`kb/concepts/COLLECTION.md` first:
|
||||
|
||||
```bash
|
||||
tools/wikitool new concept --name "<Name>" \
|
||||
@@ -211,7 +211,7 @@ validator complains - and the ticked list is the only record that they happened.
|
||||
The new raw file(s) must no longer be listed as uncovered, and no `raw_files:` entry may be
|
||||
broken.
|
||||
|
||||
11. **Close out.** Follow [publish-cycle.md](../publish-cycle.md) with `--op ingest` and a
|
||||
11. **Close out.** Follow `instructions/publish-cycle.md` with `--op ingest` and a
|
||||
message of the form `ingest: <raw path>`.
|
||||
|
||||
12. **Check the lint cadence.**
|
||||
@@ -230,16 +230,16 @@ validator complains - and the ticked list is the only record that they happened.
|
||||
Two pages on one subject is the failure this step exists to prevent.
|
||||
- **One source names far more subjects than usual?** That is breadth, not volume. It is not
|
||||
split into several sources - it cannot be - and it does not get a page per name either:
|
||||
[ingest-large-tree.md](../ingest-large-tree.md) § A broad source is not cut.
|
||||
`instructions/ingest-large-tree.md` § A broad source is not cut.
|
||||
- **No raw file backs a claim you want to write?** Leave it out, or mark the page
|
||||
`provenance: mixed` and put it under `## General Guidance (unsourced)`.
|
||||
- **`publish` exited 42?** A single ingest is normally well under the Mass-Update Gate
|
||||
threshold. If it trips - a source touching many entities - show the user the output and stop;
|
||||
see [gates.md](../gates.md).
|
||||
- **A gate or the loop-breaker refuses anything?** Stop and follow [gates.md](../gates.md).
|
||||
see `instructions/gates.md`.
|
||||
- **A gate or the loop-breaker refuses anything?** Stop and follow `instructions/gates.md`.
|
||||
A multi-tool ingest should land in roughly 20-35 `wikitool` calls; needing far more is a sign
|
||||
the source should be split into several ingests - which is
|
||||
[ingest-large-tree.md](../ingest-large-tree.md), not a bigger budget.
|
||||
`instructions/ingest-large-tree.md`, not a bigger budget.
|
||||
|
||||
## wikitool commands used
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ description: Health-check the LLM wiki - broken links, orphan pages, uncovered r
|
||||
threshold reached - `wiki-ingest`'s last step checks it after every publish, so the count is
|
||||
never something an agent has to remember.
|
||||
|
||||
**Before the first `wikitool` call:** [session-setup.md](../session-setup.md).
|
||||
**Before the first `wikitool` call:** `instructions/session-setup.md`.
|
||||
|
||||
## Run checklist
|
||||
|
||||
@@ -51,7 +51,7 @@ mechanical half looks exactly like a complete one.
|
||||
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)),
|
||||
clears the reference in *both* directions (see `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
|
||||
@@ -90,7 +90,7 @@ mechanical half looks exactly like a complete one.
|
||||
7. **Repair what is mechanical.** A dangling frontmatter reference is either a page that should
|
||||
exist (`tools/wikitool new ...`) or a reference that should not
|
||||
(`tools/wikitool xref remove --a "<Page>" --b "<Missing>"`). A title that changed is
|
||||
`tools/wikitool rename` - see [page-lifecycle.md](../page-lifecycle.md). Never hand-edit a
|
||||
`tools/wikitool rename` - see `instructions/page-lifecycle.md`. Never hand-edit a
|
||||
frontmatter array to clear one.
|
||||
|
||||
8. **Verify the stack.**
|
||||
@@ -129,7 +129,7 @@ mechanical half looks exactly like a complete one.
|
||||
|
||||
- **Publish?** Lint does not auto-publish. Run `tools/wikitool publish` only if asked.
|
||||
- **Bulk fixes touched 10+ files?** Expected for a lint pass: `publish` exits 42. Show the
|
||||
user its output and stop; see [gates.md](../gates.md). Consider `--path` batches instead.
|
||||
user its output and stop; see `instructions/gates.md`. Consider `--path` batches instead.
|
||||
- **The gate or loop-breaker keeps tripping?** That is a signal to stop and re-plan with the
|
||||
user, not to pass `--override-budget`. A full pass should land in roughly 20-35 calls.
|
||||
|
||||
@@ -140,7 +140,7 @@ mechanical half looks exactly like a complete one.
|
||||
`publish` (only if asked)
|
||||
|
||||
**Deliberately absent:** `rm` - a lint pass never deletes a page, and
|
||||
[page-lifecycle.md](../page-lifecycle.md) is where a deletion belongs. `log status` - it decides
|
||||
`instructions/page-lifecycle.md` is where a deletion belongs. `log status` - it decides
|
||||
this skill's *trigger*, but `wiki-ingest`'s last step is what runs it.
|
||||
|
||||
## Output
|
||||
|
||||
@@ -11,11 +11,11 @@ catalog and the audit log in sync.
|
||||
**Trigger:** User requests a new entity/concept/comparison page, or new information needs
|
||||
integrating into an existing one.
|
||||
|
||||
**Before the first `wikitool` call:** [session-setup.md](../session-setup.md).
|
||||
**Before the first `wikitool` call:** `instructions/session-setup.md`.
|
||||
|
||||
**Read before drafting:** [kb/CONTRACT.md](../../kb/CONTRACT.md) - linking and provenance,
|
||||
**Read before drafting:** `kb/CONTRACT.md` - linking and provenance,
|
||||
both of which the tool enforces - and
|
||||
[kb/CONVENTIONS.md](../../kb/CONVENTIONS.md), which is where this instance's language, naming
|
||||
`kb/CONVENTIONS.md`, which is where this instance's language, naming
|
||||
forms, tone and relationship labels are, together with the target collection's own
|
||||
`COLLECTION.md`, which carries its quality goal and what is local to that subtree. Field-level
|
||||
requirements come from `tools/wikitool types describe <type>`.
|
||||
@@ -48,7 +48,7 @@ requirements come from `tools/wikitool types describe <type>`.
|
||||
subjects - so the prose connects to existing pages instead of restating them.
|
||||
|
||||
5. **Draft.** Fill in the generated skeleton's TODO sections, following the tone rules in
|
||||
[kb/CONVENTIONS.md](../../kb/CONVENTIONS.md#tone). If `provenance:` is `sourced` or `mixed`, cite
|
||||
`kb/CONVENTIONS.md` § Tone. If `provenance:` is `sourced` or `mixed`, cite
|
||||
hard facts as you write them with `tools/wikitool cite add --page "<Title>" --source
|
||||
"Source - X"`, which also adds `X` to `sources:` - paste the `[^cite-id]` marker it prints.
|
||||
|
||||
@@ -60,7 +60,7 @@ requirements come from `tools/wikitool types describe <type>`.
|
||||
|
||||
One per relationship. Never hand-edit `related:`.
|
||||
|
||||
7. **Close out.** [publish-cycle.md](../publish-cycle.md), `--op create`.
|
||||
7. **Close out.** `instructions/publish-cycle.md`, `--op create`.
|
||||
|
||||
## Updating a page
|
||||
|
||||
@@ -84,11 +84,11 @@ requirements come from `tools/wikitool types describe <type>`.
|
||||
|
||||
Never hand-edit `modified:`, `summary:` or `provenance:`.
|
||||
|
||||
7. **Close out.** [publish-cycle.md](../publish-cycle.md), `--op update`.
|
||||
7. **Close out.** `instructions/publish-cycle.md`, `--op update`.
|
||||
|
||||
## Renaming, deleting, or unlinking
|
||||
|
||||
That is [page-lifecycle.md](../page-lifecycle.md). A title is the wiki's only identifier for a
|
||||
That is `instructions/page-lifecycle.md`. A title is the wiki's only identifier for a
|
||||
page, so none of it is a file operation.
|
||||
|
||||
## Decision points
|
||||
@@ -98,7 +98,7 @@ page, so none of it is a file operation.
|
||||
- **Entity or concept?** A thing you can point at is an entity; a *why* or *how* is a concept.
|
||||
The collection contracts draw the line.
|
||||
- **`publish` refused?** A single page is normally well under the threshold. If it trips,
|
||||
[gates.md](../gates.md).
|
||||
`instructions/gates.md`.
|
||||
|
||||
## wikitool commands used
|
||||
|
||||
@@ -106,7 +106,7 @@ page, so none of it is a file operation.
|
||||
`sources rebuild-index`, `index rebuild`, `log append`, `publish`
|
||||
|
||||
`xref remove` belongs to the unlinking case, which this skill delegates whole to
|
||||
[page-lifecycle.md](../page-lifecycle.md) rather than describing in a step of its own.
|
||||
`instructions/page-lifecycle.md` rather than describing in a step of its own.
|
||||
|
||||
## Output
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ description: Answer a question using the LLM wiki's compiled knowledge - read-on
|
||||
|
||||
**Trigger:** User asks a question.
|
||||
|
||||
**Before the first `wikitool` call:** [session-setup.md](../session-setup.md).
|
||||
**Before the first `wikitool` call:** `instructions/session-setup.md`.
|
||||
|
||||
**Hard rule:** read-only with respect to wiki *content*. Never modify, hand-edit, or scaffold a
|
||||
page while answering. Two exceptions, both mechanical: step 6 (filing a valuable answer through
|
||||
@@ -34,19 +34,22 @@ invariant - rather than synthesizing a plausible-sounding answer from general kn
|
||||
```bash
|
||||
tools/wikitool search "backup" --kind entity --subtype system
|
||||
tools/wikitool search --field entity_type=system --field '!sources' --sort -modified
|
||||
tools/wikitool search --field tags=k8s --limit 30
|
||||
tools/wikitool search "Longhorn" --matches # show the matching lines
|
||||
tools/wikitool search --field tags=k8s --limit 0 # a sweep: every match, not the first 50
|
||||
tools/wikitool search "Longhorn" --matches # show the matching lines
|
||||
```
|
||||
|
||||
`search` is read-only and exempt from the iteration budget, so searching again is always
|
||||
cheaper than reading more.
|
||||
cheaper than reading more. A result that hit `--limit` says so and names the total, so read
|
||||
the last line before treating a list as the whole answer - and do not grep `kb/` yourself,
|
||||
per AGENTS.md § Routing.
|
||||
|
||||
3. **Read only the pages the search points at**, then follow their `related:` and `sources:`
|
||||
entries. Check `kb/sources/` when the question is about what a specific source said.
|
||||
3. **Read only the pages the search points at** - each hit carries its full path - then follow
|
||||
their `related:` and `sources:` entries. Check `kb/sources/` when the question is about what
|
||||
a specific source said.
|
||||
|
||||
4. **Answer and cite.** Name the wiki pages the answer came from, and the sources behind them.
|
||||
Hedge to what those sources carry, not to a number - see
|
||||
[kb/CONVENTIONS.md § Hedging](../../kb/CONVENTIONS.md#hedging).
|
||||
`kb/CONVENTIONS.md` § Hedging.
|
||||
|
||||
5. **Decide what earns a page - before the first `new`.** Name every page you are considering,
|
||||
then hold each one on its own against all three criteria: the answer required synthesis
|
||||
@@ -75,9 +78,9 @@ invariant - rather than synthesizing a plausible-sounding answer from general kn
|
||||
exist under different words. Then say the wiki has no confident source, and offer to ingest
|
||||
one.
|
||||
- **Filed a page?** Query does **not** auto-publish. Run `tools/wikitool publish` only if asked;
|
||||
the sequence is in [publish-cycle.md](../publish-cycle.md).
|
||||
the sequence is in `instructions/publish-cycle.md`.
|
||||
- **Several answers filed at once?** That can trip the Mass-Update Gate - see
|
||||
[gates.md](../gates.md). The gate is a brake, not the check: it counts files and knows nothing
|
||||
`instructions/gates.md`. The gate is a brake, not the check: it counts files and knows nothing
|
||||
about whether any of them earned a page. Step 5 is what decides that, and a batch small enough
|
||||
to pass the gate has not been cleared by it.
|
||||
|
||||
|
||||
@@ -10,14 +10,14 @@ semantic review a lint pass does.
|
||||
|
||||
**Trigger:** User asks for wiki statistics, "what's new", or a quick health snapshot.
|
||||
|
||||
**Before the first `wikitool` call:** [session-setup.md](../session-setup.md) - step 2's `lint` is
|
||||
**Before the first `wikitool` call:** `instructions/session-setup.md` - step 2's `lint` is
|
||||
not on the budget's exemption allowlist and is counted like any other call, gitignored report or
|
||||
not (§ Scope there).
|
||||
|
||||
**Hard rule:** read-only with respect to wiki *content*. Never create, modify, or scaffold a
|
||||
page, never repair a finding, never publish. One file does get written: the report `lint`
|
||||
produces in step 2. That is not an exception being stretched - `reports/` is gitignored and holds
|
||||
no wiki page ([reports/CONTRACT.md](../../reports/CONTRACT.md)), so the write leaves nothing
|
||||
no wiki page (`reports/CONTRACT.md`), so the write leaves nothing
|
||||
behind that the wiki ships. If something looks wrong, point the user at `wiki-lint` or
|
||||
`wiki-manage` instead of fixing it here.
|
||||
|
||||
|
||||
+18
-5
@@ -40,11 +40,24 @@ those regions and nothing else. Nothing matches on this text.
|
||||
|
||||
## Language
|
||||
|
||||
Pages are written in **German**. This binds `kb/` and the authoring surface that shapes it -
|
||||
the page type-specs `types/entity.md`, `types/concept.md`, `types/source.md` and
|
||||
`types/comparison.md`. `raw/` is untouched ([raw/CONTRACT.md](../raw/CONTRACT.md)), and the
|
||||
control plane stays English: `AGENTS.md`, the stage contracts, this file, `instructions/`, and
|
||||
the type-specs for non-page artifacts.
|
||||
Pages are written in **German** - the `language:` in this file's own frontmatter, and the one
|
||||
place that value is written down. This binds `kb/`, and inside the page type-specs
|
||||
(`types/entity.md`, `types/concept.md`, `types/source.md`, `types/comparison.md`) exactly the
|
||||
parts that become page text: each one's `## Template` block, and the `layout:` titles that head a
|
||||
catalog section. Their authoring guidance around those is instruction to an agent, so it follows
|
||||
the control plane and stays English - the same prose/identifier cut
|
||||
[kb/CONTRACT.md](CONTRACT.md#language-and-identifiers) makes inside a page, applied one level up.
|
||||
`raw/` is untouched ([raw/CONTRACT.md](../raw/CONTRACT.md)).
|
||||
|
||||
Two things follow from that value rather than being decided here, both stated once in
|
||||
[AGENTS.md § File naming](../AGENTS.md#file-naming): the control plane stays English whatever an
|
||||
instance writes its pages in, and an agent *speaks* the language named above.
|
||||
|
||||
The first of those is not a setting this file withholds - it is not a setting at all. `language:`
|
||||
above is the only language value in the tree, and what it binds is page text; a control-plane
|
||||
document is English even when this instance wrote it for itself and never ships it. Why that is
|
||||
an architecture decision rather than an unset parameter:
|
||||
[docs/language-boundaries.md](../docs/language-boundaries.md).
|
||||
|
||||
Which line is prose and which is an identifier - and therefore what is translated at all - is
|
||||
the contract's rule, not this file's: see
|
||||
|
||||
@@ -27,12 +27,26 @@ marker pair, so a rename re-renders words and nothing else.
|
||||
|
||||
## Language
|
||||
|
||||
Pages are written in **{language}**. This binds `kb/` and the authoring surface that shapes it -
|
||||
the page type-specs `types/entity.md`, `types/concept.md`, `types/source.md` and
|
||||
`types/comparison.md`, whose `## Template` blocks are the body skeleton every new page starts
|
||||
from. `raw/` is untouched ([raw/CONTRACT.md](../raw/CONTRACT.md)), and the control plane stays
|
||||
English: `AGENTS.md`, the stage contracts, this file, `instructions/`, and the type-specs for
|
||||
non-page artifacts.
|
||||
Pages are written in **{language}** - the `language:` in this file's own frontmatter, and the
|
||||
one place that value is written down. This binds `kb/`, and inside the page type-specs
|
||||
(`types/entity.md`, `types/concept.md`, `types/source.md`, `types/comparison.md`) exactly the
|
||||
parts that become page text: each one's `## Template` block - the body skeleton every new page
|
||||
starts from - and the `layout:` titles that head a catalog section. Their authoring guidance
|
||||
around those is instruction to an agent, so it follows the control plane and stays English - the
|
||||
same prose/identifier cut [kb/CONTRACT.md](CONTRACT.md#language-and-identifiers) makes inside a
|
||||
page, applied one level up. Adopting this template into a non-English instance therefore means
|
||||
translating those blocks, not the whole file. `raw/` is untouched
|
||||
([raw/CONTRACT.md](../raw/CONTRACT.md)).
|
||||
|
||||
Two things follow from that value rather than being decided here, both stated once in
|
||||
[AGENTS.md § File naming](../AGENTS.md#file-naming): the control plane stays English whatever an
|
||||
instance writes its pages in, and an agent *speaks* the language named above.
|
||||
|
||||
The first of those is not a setting this file withholds - it is not a setting at all. `language:`
|
||||
above is the only language value in the tree, and what it binds is page text; a control-plane
|
||||
document is English even when this instance wrote it for itself and never ships it. Why that is
|
||||
an architecture decision rather than an unset parameter:
|
||||
[docs/language-boundaries.md](../docs/language-boundaries.md).
|
||||
|
||||
Which line is prose and which is an identifier - and therefore what is translated at all - is
|
||||
the contract's rule, not this file's: see
|
||||
|
||||
+21
-22
@@ -35,32 +35,31 @@ tone, relationship labels, the confidence rubric. Neither is restated here.
|
||||
|
||||
## Types offered
|
||||
|
||||
`concept` (`tools/wikitool types describe concept`). Das Feld `concept_type:`
|
||||
wählt die Area:
|
||||
`concept` (`tools/wikitool types describe concept`). The `concept_type:` field
|
||||
picks the area:
|
||||
|
||||
| Area | Hält |
|
||||
|------|------|
|
||||
| `architectures/` | Aufbau und Struktur: wie ein System geschnitten ist und warum die Schnitte dort liegen |
|
||||
| `patterns/` | Wiederverwendbare Lösungsformen, die über mehr als einen Gegenstand hinweg gelten |
|
||||
| `protocols/` | Kommunikationsprotokolle und Standards, in ihrer üblichen Schreibweise benannt |
|
||||
| `workflows/` | Abläufe und Prozesse, die projektübergreifend wiederkehren |
|
||||
| `decisions/` | Architektur- und Entwurfsentscheidungen (siehe unten) |
|
||||
| `problems/` | Wiederkehrende Problemstellungen und ihre Lösungsansätze |
|
||||
| Area | Holds |
|
||||
|------|-------|
|
||||
| `architectures/` | Shape and structure: how a system is cut up, and why the cuts fall where they do |
|
||||
| `patterns/` | Reusable solution shapes that hold across more than one subject |
|
||||
| `protocols/` | Communication protocols and standards, named in their usual spelling |
|
||||
| `workflows/` | Procedures and processes that recur across projects |
|
||||
| `decisions/` | Architectural and design decisions (see below) |
|
||||
| `problems/` | Recurring problems and the approaches taken to them |
|
||||
|
||||
Das sind Areas, keine Collections: sie erben diesen Contract und tragen keine
|
||||
eigene `COLLECTION.md`.
|
||||
These are areas, not collections: they inherit this contract and carry no
|
||||
`COLLECTION.md` of their own.
|
||||
|
||||
Die Zuordnung trifft niemand von Hand — sie steht als `layout:` in
|
||||
`types/concept.md`, und `wikitool new` legt eine neue Seite direkt dort ab.
|
||||
Eine Seite, die anderswo liegt, meldet `wikitool lint` als *misplaced*;
|
||||
`wikitool move --page "<Titel>"` bringt sie an ihren berechneten Ort.
|
||||
Nobody assigns them by hand — the mapping is the `layout:` in
|
||||
`types/concept.md`, and `wikitool new` puts a new page straight there. A page
|
||||
sitting anywhere else is reported by `wikitool lint` as *misplaced*;
|
||||
`wikitool move --page "<title>"` moves it to its computed location.
|
||||
|
||||
Die Aufteilung ist keine Geschmacksfrage, sondern das, was die Shard-Schwelle
|
||||
des Katalogs überhaupt wirksam macht: `index rebuild` teilt **pro Area**, und
|
||||
eine Collection ohne Areas teilt sich nie — mit 80 Seiten in einer einzigen
|
||||
Tabelle war die Schwelle hier ein toter Wert. Keine der sechs
|
||||
Areas liegt derzeit über der Schwelle, also bekommt auch keine einen eigenen
|
||||
Shard; wächst eine hinein, passiert das ohne Zutun.
|
||||
The split is not a matter of taste but what makes the catalog's shard threshold
|
||||
effective at all: `index rebuild` splits **per area**, and a collection without
|
||||
areas never splits — with 80 pages in a single table the threshold was a dead
|
||||
value here. None of the six areas is currently above it, so none gets a shard of
|
||||
its own; when one grows into it, that happens without anyone acting.
|
||||
|
||||
## Decisions
|
||||
|
||||
|
||||
+26
-28
@@ -26,38 +26,36 @@ renamed or dropped - its authoring rules below are the instance's, its existence
|
||||
## Types offered
|
||||
|
||||
`source` (`tools/wikitool types describe source`). Page titles carry the `Source - ` prefix,
|
||||
applied automatically by `wikitool new source`. Das Feld `source_type:` wählt die Area - **ohne
|
||||
Default**: `wikitool new source` verweigert ohne einen expliziten Wert.
|
||||
applied automatically by `wikitool new source`. The `source_type:` field picks the area - **with
|
||||
no default**: `wikitool new source` refuses without an explicit value.
|
||||
|
||||
| Area | Hält |
|
||||
|------|------|
|
||||
| `transcripts/` | Session-Transkripte: mitgeschriebener Dialog zwischen Mensch und Agent, oder zwischen Menschen |
|
||||
| `analyses/` | Analyse-Output eines Modells über einen Gegenstand - kein Dialog, kein Protokoll, sondern eine eigenständige Einschätzung |
|
||||
| `articles/` | Externe Artikel und Blogposts, mit `source_url:` |
|
||||
| `documents/` | Eingelesene Dokumente, Handbücher, Spezifikationen |
|
||||
| `notes/` | Echte eigene Notizen ohne Dialogform - Cheat Sheets, Merkzettel |
|
||||
| `trackers/` | Exporte aus einem Issue-Tracker oder vergleichbaren System |
|
||||
| `unclassified/` | Sichtbares Fach für eine Quelle, deren Kategorie noch nicht feststeht - beratender `lint`-Befund, kein Sammelbecken. Es wieder zu leeren, oder das Enum um einen neuen Wert zu erweitern: [instructions/evolve-subtypes.md](../../instructions/evolve-subtypes.md) |
|
||||
| Area | Holds |
|
||||
|------|-------|
|
||||
| `transcripts/` | Session transcripts: recorded dialogue between a human and an agent, or between humans |
|
||||
| `analyses/` | A model's analytical output about a subject - not dialogue, not a record, but an assessment in its own right |
|
||||
| `articles/` | External articles and blog posts, with `source_url:` |
|
||||
| `documents/` | Ingested documents, manuals, specifications |
|
||||
| `notes/` | Genuinely own notes in no dialogue form - cheat sheets, reminders |
|
||||
| `trackers/` | Exports from an issue tracker or comparable system |
|
||||
| `unclassified/` | The visible slot for a source whose category is not settled yet - an advisory `lint` finding, not a dumping ground. Emptying it again, or extending the enum by a new value: [instructions/evolve-subtypes.md](../../instructions/evolve-subtypes.md) |
|
||||
|
||||
Das sind Areas, keine Collections: sie erben diesen Contract und tragen keine eigene
|
||||
`COLLECTION.md`. Die Zuordnung trifft niemand von Hand - sie steht als `layout:` in
|
||||
`types/source.md`, und `wikitool new` legt eine neue Seite direkt dort ab. Eine Seite, die
|
||||
anderswo liegt, meldet `wikitool lint` als *misplaced*; `wikitool move --page "<Titel>"` bringt
|
||||
sie an ihren berechneten Ort.
|
||||
These are areas, not collections: they inherit this contract and carry no `COLLECTION.md` of
|
||||
their own. Nobody assigns them by hand - the mapping is the `layout:` in `types/source.md`, and
|
||||
`wikitool new` puts a new page straight there. A page sitting anywhere else is reported by
|
||||
`wikitool lint` as *misplaced*; `wikitool move --page "<title>"` moves it to its computed
|
||||
location.
|
||||
|
||||
**`analysis` gegen `document`:** die Unterscheidung läuft über die Autorschaft, nicht über den
|
||||
Inhalt. Ein Modell, das über einen Gegenstand urteilt oder ihn zusammenfasst, ohne dass ein
|
||||
Mensch oder eine Organisation dafür geradesteht, ist `analysis` - unabhängig davon, wie
|
||||
artikelförmig der Text wirkt. Ein Handbuch, eine Spezifikation, eine Herstellerdoku ist
|
||||
`document`, auch wenn ein Werkzeug sie generiert hat, solange eine Organisation die Aussage
|
||||
verantwortet. Die Frage ist also "wer haftet für die Behauptung", nicht "wie liest sich der
|
||||
Text".
|
||||
**`analysis` versus `document`:** the distinction runs on authorship, not on content. A model
|
||||
judging or summarizing a subject with no human or organization answering for it is `analysis` -
|
||||
however article-shaped the text looks. A manual, a specification, a vendor document is
|
||||
`document`, even where a tool generated it, as long as an organization is accountable for what
|
||||
it says. The question is "who is liable for the claim", not "how does the text read".
|
||||
|
||||
Solange es diesen Default noch gab, fiel fast alles hierher in `notes/`, weil
|
||||
`types/source.schema.yaml` `notes` als `default:` gesetzt hatte - der Compiler wählte das
|
||||
Sammelbecken, sobald niemand widersprach.
|
||||
22 der 29 damaligen Seiten waren tatsächlich Transkripte, Analysen oder Tracker-Exporte und
|
||||
wurden per `wikitool touch --set source_type=…` umklassifiziert, bevor die Areas entstanden.
|
||||
While that default still existed, nearly everything landed here in `notes/`, because
|
||||
`types/source.schema.yaml` had `notes` as its `default:` - the compiler picked the dumping
|
||||
ground whenever nobody objected. 22 of the 29 pages there at the time were in fact transcripts,
|
||||
analyses or tracker exports, and were reclassified with `wikitool touch --set source_type=…`
|
||||
before the areas existed.
|
||||
|
||||
## Provenance rules
|
||||
|
||||
|
||||
+8
-8
@@ -124,7 +124,7 @@ tools/wikitool <command> --help
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `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, pages nested more than one directory below their collection (hard - the generated catalog folds these into their area silently rather than merely reading it), 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), a collection past the catalog's per-area shard threshold that has no areas to shard (advisory only - sharding is automatic but per *area*, so a collection nobody gave areas keeps one table however large it grows; reported with the split its subtype field would produce, and only when that split puts every resulting area at or under the threshold, so a lopsided or small collection stays silent), source pages sitting in the `unclassified` catalog slot (advisory only - `unclassified` is the visible fallback for a genuinely unclear source, not a defect), 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 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. One hit per line, ` | `-separated as `score \| kind/subtype \| title \| path \| summary`, so a hit can be judged without opening the page and then opened without looking it up: **title and path are never truncated** (the title is the identifier `touch`/`xref`/`cite` take), and the summary - the one lossy field, and the only one that may contain the separator - goes last, so splitting on `" \| "` with `maxsplit=4` is unambiguous. Scope is pages: the backend walks `kb/` but drops anything `kb_scan.iter_kb_pages` excludes (the kb-root meta files, every `COLLECTION.md`, every generated `INDEX.md`), which is why a hand-run grep over `kb/` can add none of them but those. `--limit` defaults to 50 (`0` for no limit) and **a truncated result says so** - `50 of 182 result(s)` in the table, `total`/`truncated`/`limit` beside `count` in `--json`, where `count` stays the number of results in the payload; the same default and the same fields are what `api.search` and the MCP `search` tool carry, from one constant. 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** |
|
||||
|
||||
### Provenance
|
||||
|
||||
@@ -166,12 +166,12 @@ tools/wikitool <command> --help
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `types list [--json]` | List every type-spec under `types/` (name, schema path, subtype field, description) - discover what page types exist without reading `types/*.md` directly |
|
||||
| `types describe <name> [--json]` | Print one type's full contract: required/optional frontmatter fields with enums, its subtype field (if any), and its authoring body |
|
||||
| `types describe <name> [--json]` | Print one type's full contract: required/optional frontmatter fields with enums, its subtype field (if any), and its authoring body - composed with the stack-owned `types/<name>.guidance.md` where the type-spec declares `guidance:` (`--json` reports it separately as `guidance`/`guidance_path`, absent for a type with none), so a `root: kb` type's contract reads as one answer even though it may live in two files. A type-spec (or its guidance file) over the `docs toc` threshold carries a generated table-of-contents region; it is stripped from this output rather than echoed, since the whole body is being handed over and a navigation aid into it would be noise |
|
||||
| `instructions sync [--force]` | Publish every `instructions/<name>/SKILL.md` into `.agents/skills/` and `.claude/skills/` as **copies**, and delete published skills whose source is gone. Both targets are gitignored, so a fresh clone runs this once - see `instructions/bootstrap.md`. Re-running is also how a drifted copy is repaired: the source always wins. `--force` is required only to replace a target directory that is not a published skill at all (no `SKILL.md` in it) |
|
||||
| `instructions verify` | Check the instruction layer: flat instructions validate against `types/instruction.schema.yaml`, each `SKILL.md` carries the frontmatter its harness reads, every published copy is byte-identical to its source, no instruction is left that nothing references, and nothing under `instructions/dev/` is referenced from outside it (a `<!-- dist:strip-start/end -->` block is exempt - see [instructions/CONTRACT.md](../instructions/CONTRACT.md)). Missing *every* copy is reported as "run sync", not as drift - that is a clean checkout |
|
||||
| `instructions verify` | Check the instruction layer: flat instructions validate against `types/instruction.schema.yaml`, each `SKILL.md` carries the frontmatter its harness reads, no `SKILL.md` carries a relative markdown link (`sync` copies it to a different depth than the source, so a `SKILL.md` references a target as a repo-root-relative plain path instead - see [instructions/CONTRACT.md](../instructions/CONTRACT.md) § "A skill's outbound reference is a plain path, not a link"), every published copy is byte-identical to its source, no instruction is left that nothing references, and nothing under `instructions/dev/` is referenced from outside it (a `<!-- dist:strip-start/end -->` block is exempt - see [instructions/CONTRACT.md](../instructions/CONTRACT.md)). Missing *every* copy is reported as "run sync", not as drift - that is a clean checkout |
|
||||
| `instructions list [--json]` | List the flat instructions with their descriptions. This is how the layer is discovered; `search` deliberately covers `kb/` only |
|
||||
| `docs verify` | Check the docs that mirror the code: every CLI command documented in this file's own § Commands table and, separately, in its § Error contracts table (both directions, checked per table, so a row dropped from one is not hidden by the same name surviving in the other, and only a name's presence in a row is checked, never the rest of that row's text), every directory under `kb/` has a `COLLECTION.md` and no directory outside it does, every collection declaring `profile:` and a `required_by_stack:` that agrees with the stack's own list, `kb/CONVENTIONS.md` naming all three tool-owned section headings if it exists at all, every stage contract present, no pre-migration `type: entity` blocks left in the contracts, the `.gitignore` canaries clear in both directions (nothing ignored under `raw/`/`kb/`, `incoming/` ignored, everything ignored under `reports/` and the published skill directories), and no `.md`/`.template` file `dist export` would ship citing an issue number - the tracker exists only in the origin repo, so such a number in a distributed instance is a reference its reader can neither resolve nor recognise as unresolvable (a `<!-- dist:strip-start/end -->` region is exempt: it is already gone from the text the check reads, which is the export plan's, not the working tree's), and every reference file `docs toc` covers carrying the current table-of-contents region for its own headings - missing and stale are one check, because the generator is idempotent. The name is about documentation parity, not about the `docs/` directory - it neither reads nor requires one, the same way `kb/` predates the collection it now checks |
|
||||
| `docs toc [--apply]` | Create, refresh or remove the generated table-of-contents region (`<!-- wikitool:toc -->` ... `<!-- /wikitool:toc -->`, placed after the title and before the first `##`) on every reference file over 100 lines that `AGENTS.md`, a stage/collection contract, or the flat `instructions/**.md` form covers - the scope Anthropic's skill-authoring guidance names for a file previewed rather than read in full. Dry-run by default (prints which files would change); `--apply` writes. `docs verify` checks the result stays current the same way it checks every other generated-from-code copy |
|
||||
| `docs verify` | Check the docs that mirror the code: every CLI command documented in this file's own § Commands table and, separately, in its § Error contracts table (both directions, checked per table, so a row dropped from one is not hidden by the same name surviving in the other, and only a name's presence in a row is checked, never the rest of that row's text), every directory under `kb/` has a `COLLECTION.md` and no directory outside it does, every collection declaring `profile:` and a `required_by_stack:` that agrees with the stack's own list, `kb/CONVENTIONS.md` naming all three tool-owned section headings if it exists at all, every stage contract present, every file under `types/` declaring `type: types/type-spec.md` validating against `types/type-spec.schema.yaml`, no pre-migration `type: entity` blocks left in the contracts, the `.gitignore` canaries clear in both directions (nothing ignored under `raw/`/`kb/`, `incoming/` ignored, everything ignored under `reports/` and the published skill directories), and no `.md`/`.template` file `dist export` would ship citing an issue number - the tracker exists only in the origin repo, so such a number in a distributed instance is a reference its reader can neither resolve nor recognise as unresolvable (a `<!-- dist:strip-start/end -->` region is exempt: it is already gone from the text the check reads, which is the export plan's, not the working tree's), every reference file `docs toc` covers carrying the current table-of-contents region for its own headings - missing and stale are one check, because the generator is idempotent - and every relative markdown link in one of those same reference files resolving to a file that actually exists (a target's `#anchor` suffix is stripped first; code fences and inline code spans are masked before scanning, so a passage showing link syntax as an example is not mistaken for a real reference). The name is about documentation parity, not about the `docs/` directory - it neither reads nor requires one, the same way `kb/` predates the collection it now checks |
|
||||
| `docs toc [--apply]` | Create, refresh or remove the generated table-of-contents region (`<!-- wikitool:toc -->` ... `<!-- /wikitool:toc -->`, placed after the title and before the first `##`) on every reference file over 100 lines, in the scope Anthropic's skill-authoring guidance names for a file previewed rather than read in full: `AGENTS.md`, every stage contract, `kb/CONVENTIONS.md`, every `kb/*/COLLECTION.md`, every flat `instructions/**.md` file, every `types/*.md` type-spec, and every `docs/` page. Computed from those categories rather than listed, so a file added later is in scope without a code change. `SKILL.md` is the one exception, and the same guidance is why: it places a skill body on the loading level that is read whole when the skill triggers, and aims its own TOC advice at the bundled reference files a skill points *at*. Human docs (`README.md`, `CHANGES.md`, `EVALS.md`, `INSTALL.md`, `tools/README.md`) are out of scope because AGENTS.md § File naming says no agent loads them as instruction. Dry-run by default (prints which files would change); `--apply` writes. `docs verify` checks the result stays current the same way it checks every other generated-from-code copy |
|
||||
|
||||
### Telemetry
|
||||
|
||||
@@ -189,7 +189,7 @@ tools/wikitool <command> --help
|
||||
| `version show [--json]` | Print this instance's stack version and where it came from (development tree, or a distribution with its export date and origin). Bare `wikitool version` is an alias for this. Read-only, offline, and **exempt from the Iteration Budget Gate** |
|
||||
| `version check [--url U] [--timeout S] [--json]` | Ask the origin's release feed whether a newer stack exists, and whether the step crosses a compatibility boundary (`state: current\|update\|migration\|ahead`). **The only command in `wikitool` that makes a network call** - never reached implicitly from another command, needs no key, times out, and reports an unreachable feed as an error rather than as "up to date". The feed is `$WIKITOOL_UPDATE_URL`, else the release stamp's, else the built-in origin; `$WIKITOOL_UPDATE_TOKEN` is only needed if that feed is not readable anonymously. Read-only and exempt from the budget gate |
|
||||
| `version notes [--version X.Y.Z]` | Print one version's `CHANGES.md` entry, for use as release notes (default: this tree's `VERSION`). Read-only and exempt from the budget gate |
|
||||
| `version bump --major\|--minor\|--patch --title "<...>" [--impact high\|medium\|low] [--breaking "<what breaks>"] [--no-migration "<reason>"] [--migration-required] [--dry-run]` | Raise or continue the **one running candidate** between two releases - `VERSION` gets a `-beta.N` suffix, never a second fresh number per bump. `--major/--minor/--patch` is **max-wins escalation** against the last release (patch < minor < major): a `--patch` on a MINOR candidate only advances `N`, and escalation never steps back down. Opens the matching `CHANGES.md` entry on the first bump of a candidate (heading, date, author, and a machine-managed `<!-- wikitool:bumps -->` list of every `--title` collected so far, graded by `--impact`, default `medium`) and updates that same entry in place on every later bump of the same candidate - one entry per candidate, not one per bump. The list renders grouped under `**High/Medium/Low impact**` headings (empty groups omitted), except when every bump so far is `medium`, where it stays the flat, ungrouped list the region always had - `version regrade` corrects a grade after the fact. Refuses more or fewer than one part, an empty title, an unknown `--impact`, and a `VERSION`/newest-changelog-entry mismatch. Compatibility follows the **leftmost non-zero component** of the candidate's base, which for this stack (at `1.0.0` and up) means MAJOR: PATCH is a fix, MINOR a compatible capability, MAJOR a version that is **not a drop-in replacement** - any hand-work on update, or a downgrade that no longer works. Whether content must be migrated is a second, independent question. The bump that first escalates a candidate past the boundary requires `--breaking "<what stops working>"` and, on top of it, a migration document targeting the candidate's base or `--no-migration "<reason>"`; both lines are written once, anchored just above the bump list, and persist over later bumps of the same candidate without being repeated, and both are refused on a bump that crosses nothing at all. A later bump of the same candidate that finds out `--no-migration` was wrong after all retracts that line with `--migration-required` instead of restating `--no-migration` - refused without a migration document already targeting the new base, and without an existing `--no-migration` line to retract. Which part a change earns stays a judgment call: the command enforces that a crossing documents itself, never that the part was chosen correctly |
|
||||
| `version bump --major\|--minor\|--patch --title "<...>" [--impact high\|medium\|low] [--breaking "<what breaks>"] [--no-migration "<reason>"] [--migration-required] [--dry-run]` | Raise or continue the **one running candidate** between two releases - `VERSION` gets a `-beta.N` suffix, never a second fresh number per bump. `--major/--minor/--patch` is **max-wins escalation** against the last release (patch < minor < major): a `--patch` on a MINOR candidate only advances `N`, and escalation never steps back down. Opens the matching `CHANGES.md` entry on the first bump of a candidate (heading, date, author, and a machine-managed `<!-- wikitool:bumps -->` list of every `--title` collected so far, graded by `--impact`, default `medium`) and updates that same entry in place on every later bump of the same candidate - one entry per candidate, not one per bump. The list renders grouped under `**High/Medium/Low impact**` headings (empty groups omitted), except when every bump so far is `medium`, where it stays the flat, ungrouped list the region always had - `version regrade` corrects a grade after the fact. Refuses more or fewer than one part, an empty title, an unknown `--impact`, and a `VERSION`/newest-changelog-entry mismatch. Compatibility follows the **leftmost non-zero component** of the candidate's base, which for this stack (at `1.0.0` and up) means MAJOR: PATCH is a fix, MINOR a compatible capability, MAJOR a version that is **not a drop-in replacement** - any hand-work on update, or a downgrade that no longer works. Whether content must be migrated is a second, independent question. The bump that first escalates a candidate past the boundary requires `--breaking "<what stops working>"` and, on top of it, a migration document targeting the candidate's base or `--no-migration "<reason>"`; both are anchored just above the bump list, persist over later bumps of the same candidate without being repeated, and are refused on a bump that crosses nothing at all. The two then behave differently on a *second* crossing, because they answer different questions: a further `--breaking` **joins** the ones already recorded (one reason per crossing - rendered flat on the marker line while there is only one, as bullets under a bare marker from the second onward, and repeating a reason verbatim is a no-op), while a further `--no-migration` **replaces** the single line that says whether content has to change. A candidate crossing the boundary twice is the normal shape of a long-running one, and each crossing is a separate thing an operator has to act on; whether content migrates stays one yes/no about the candidate as a whole. There is deliberately no retraction path for a single accumulated `--breaking` reason - `--migration-required` retracts the migration line, and nothing retracts a breaking one. A later bump of the same candidate that finds out `--no-migration` was wrong after all retracts that line with `--migration-required` instead of restating `--no-migration` - refused without a migration document already targeting the new base, and without an existing `--no-migration` line to retract. Which part a change earns stays a judgment call: the command enforces that a crossing documents itself, never that the part was chosen correctly |
|
||||
| `version regrade [INDICES...] [--impact high\|medium\|low]` | List the running candidate's bump titles with their impact grade and 1-based rendered position (no arguments - the correction path for a `--impact` judgement made at bump time), or change one or more of them in a single call: `version regrade 3 7 --impact high` grades both against a single read of today's list, not position 3 first and then position 7 against whatever that produced. Touches only the topmost entry's bump list - never `VERSION`, never any other part of `CHANGES.md`. The bare listing is read-only and exempt from the Iteration Budget Gate, like `version notes`; a call with indices writes `CHANGES.md` and is counted like `version bump`. Refuses an index outside the rendered list's range, an unknown `--impact`, indices given without `--impact`, a missing `VERSION`/`CHANGES.md`, a `VERSION`/newest-changelog-entry mismatch, or a topmost entry with no bump list at all |
|
||||
| `version release [--title "<...>"] [--dry-run]` | Fix the running candidate: strip `VERSION`'s `-beta.N` suffix and close its `CHANGES.md` entry, ending the pre-release phase `version bump` started. Without `--title` the heading keeps whichever bump last set it; with it, the heading's title is replaced - the normal case for a candidate that collected several bump titles, since the entry wants a summarising heading rather than the most recent one. Leaves the entry's machine-managed bump-title list untouched, as the record of what happened. Refuses when the candidate collected two or more bumps and the entry still carries no summary paragraph (at least 200 non-whitespace characters) between the bump list and the first `### <bump title>` changeset heading; a candidate with exactly one bump is exempt, since there its own changeset already is the summary. `--dry-run` runs this check too and reports the same refusal. Commits nothing and pushes nothing (invariant 5) - the following `publish` moves `VERSION` onto `main`, which `release.yml` reacts to. Refuses when `VERSION` is already a release (no running candidate to fix), or when the changelog's newest entry does not match `VERSION` |
|
||||
|
||||
@@ -379,9 +379,9 @@ is atomic, and whether a retry is safe.
|
||||
| `types list` | Never fails | Read-only | Safe to retry freely |
|
||||
| `types describe` | Unknown type name | Read-only | Fix the name and retry |
|
||||
| `instructions sync` | No skills found under `instructions/`, or a target directory is not a published skill (no `SKILL.md`) and `--force` was not passed | No - one directory copy per skill per target (`.agents/skills/`, `.claude/skills/`); each copy is idempotent, so a re-run converges even after a partial failure | Check whether the flagged target holds anything worth keeping, then re-run with `--force` if not; otherwise fix the named cause and retry |
|
||||
| `instructions verify` | Nothing found under `instructions/` at all, a malformed instruction or `SKILL.md`, a published copy that drifted from its source, an instruction nothing references (or, for `manual: true`, one that IS linked from AGENTS.md, CLAUDE.md, or a skill and so risks running implicitly), or something under `instructions/dev/` referenced from outside it and outside a `dist:strip` block | Read-only | Fix the flagged file, then re-run. For drift, re-run `sync` instead of hand-editing the published copy - the source under `instructions/` always wins |
|
||||
| `instructions verify` | Nothing found under `instructions/` at all, a malformed instruction or `SKILL.md`, a `SKILL.md` carrying a relative markdown link, a published copy that drifted from its source, an instruction nothing references (or, for `manual: true`, one that IS linked from AGENTS.md, CLAUDE.md, or a skill and so risks running implicitly), or something under `instructions/dev/` referenced from outside it and outside a `dist:strip` block | Read-only | Fix the flagged file, then re-run. For a relative link in a `SKILL.md`, rewrite it as a repo-root-relative plain path instead. For drift, re-run `sync` instead of hand-editing the published copy - the source under `instructions/` always wins |
|
||||
| `instructions list` | Never fails - an empty `instructions/` prints "No instructions found." | Read-only | Safe to retry freely |
|
||||
| `docs verify` | A command, contract, or type-form mismatch was found, a shipped `.md`/`.template` cites an issue number, or a reference file's table-of-contents region is missing or stale | Read-only | Fix the documentation it names, then re-run. For an issue reference: say what was decided instead of pointing at where, or move the pointer behind a `<!-- dist:strip-start/end -->` block. For a table of contents: run `docs toc --apply` - never hand-write the region |
|
||||
| `docs verify` | A command, contract, or type-form mismatch was found, a type-spec's own frontmatter fails its schema, a shipped `.md`/`.template` cites an issue number, a reference file's table-of-contents region is missing or stale, or a reference file's relative markdown link does not resolve to an existing file | Read-only | Fix the documentation it names, then re-run. For a type-spec's own frontmatter: fix the field, or add a matching line to `types/type-spec.schema.yaml` if the field is legitimately new. For an issue reference: say what was decided instead of pointing at where, or move the pointer behind a `<!-- dist:strip-start/end -->` block. For a table of contents: run `docs toc --apply` - never hand-write the region. For a dead link: fix the `../` count or the target's name |
|
||||
| `docs toc` | Never fails on content: a file with no `##` heading, or one at or under the threshold, is simply left without a region | `--apply` rewrites each named file in place, one at a time and idempotently, so a re-run after an interruption converges rather than doubling a region; the dry-run form is read-only | Nothing to fix - re-run with `--apply` to write what the dry run listed. If `docs verify` still reports a stale region afterwards, the file's `##` headings changed in between; run it again |
|
||||
|
||||
### Telemetry
|
||||
|
||||
+10
-5
@@ -38,7 +38,7 @@ from chemenu.lint_core import run_lint
|
||||
from chemenu.search import filters
|
||||
from chemenu.search.registry import resolve
|
||||
from chemenu.search.service import run_search, unreadable_pages
|
||||
from chemenu.search.types import Predicate, SearchQuery
|
||||
from chemenu.search.types import DEFAULT_LIMIT, Predicate, SearchQuery
|
||||
from chemenu.types_core import describe_type, list_types
|
||||
|
||||
# Distinguishes "the caller did not pass a revision" from "the caller passed
|
||||
@@ -120,7 +120,7 @@ class Corpus:
|
||||
text: Optional[str] = None,
|
||||
predicates: Iterable[str] = (),
|
||||
regex: bool = False,
|
||||
limit: int = 20,
|
||||
limit: int = DEFAULT_LIMIT,
|
||||
sort: Optional[str] = None,
|
||||
backend: Optional[str] = None,
|
||||
) -> dict[str, Any]:
|
||||
@@ -142,13 +142,18 @@ class Corpus:
|
||||
|
||||
with self._rooted():
|
||||
pages, revision = self._cache.load()
|
||||
hits = run_search(query, pages, backends, self.kb_dir)
|
||||
result = run_search(query, pages, backends, self.kb_dir)
|
||||
return self._stamp({
|
||||
"query": text,
|
||||
"predicates": [p.render() for p in parsed],
|
||||
"backend": ",".join(b.name for b in backends),
|
||||
"count": len(hits),
|
||||
"results": [hit.as_dict() for hit in hits],
|
||||
# Same shape the CLI's `--json` prints: `count` is what came back,
|
||||
# `total` is how many matched before `limit` cut it.
|
||||
"count": len(result.hits),
|
||||
"total": result.total,
|
||||
"truncated": result.truncated,
|
||||
"limit": result.limit,
|
||||
"results": [hit.as_dict() for hit in result.hits],
|
||||
"unreadable": unreadable_pages(pages),
|
||||
}, revision)
|
||||
|
||||
|
||||
@@ -308,9 +308,17 @@ def instance_owned_type_stems() -> set[str]:
|
||||
|
||||
The line is `root:`, and it was already in the frontmatter before anyone
|
||||
drew it: `root: kb` means the type describes a page the instance writes, so
|
||||
its prose, its template and its language are the instance's business.
|
||||
Anything else - `instruction` (`root: repo`), `lint-report` (no `base_dir`
|
||||
at all), `type-spec` itself - describes a stack artifact and ships verbatim.
|
||||
the file is the instance's to change. Anything else - `instruction`
|
||||
(`root: repo`), `lint-report` (no `base_dir` at all), `type-spec` itself -
|
||||
describes a stack artifact and ships verbatim.
|
||||
|
||||
Ownership, not language. What such a file is *written in* is decided by who
|
||||
reads each half, not by who owns the file: its `## Template` block and its
|
||||
`layout:` titles become page text and follow `kb/CONVENTIONS.md`, while the
|
||||
authoring guidance around them addresses an agent and stays English like the
|
||||
rest of the control plane (AGENTS.md § File naming, types/type-spec.md
|
||||
§ Who owns a type-spec). That the two halves share one file, and what it
|
||||
costs, is docs/ownership-and-templates.md § Where the file boundary strains.
|
||||
|
||||
Read from `types/` rather than listed, so an instance adding its own page
|
||||
type gets the same treatment without a code change.
|
||||
@@ -330,6 +338,32 @@ def instance_owned_type_stems() -> set[str]:
|
||||
return stems
|
||||
|
||||
|
||||
# The suffix a type-spec's own two files carry - `<stem>.md` and
|
||||
# `<stem>.schema.yaml` - as opposed to a sibling file that merely starts with
|
||||
# the same stem, such as `<stem>.guidance.md` (Gitea #104). Checked as an
|
||||
# exact suffix rather than by splitting on the first `.`, which is what let
|
||||
# `entity.guidance.md` be mistaken for the `entity` type-spec's own file
|
||||
# before this existed - a stack-owned file re-keyed as though it were the
|
||||
# instance's `.template` to adopt, and flagged as a leak by the other call
|
||||
# site for not being one.
|
||||
_TYPE_SCHEMA_SUFFIX = ".schema.yaml"
|
||||
|
||||
|
||||
def _owned_type_stem(relative: str) -> Optional[str]:
|
||||
"""The type stem `relative` (a path under `types/`, no `.template`
|
||||
suffix) names, if it is exactly that type-spec's own `<stem>.md` or
|
||||
`<stem>.schema.yaml` - `None` for anything else under `types/`,
|
||||
including a `<stem>.guidance.md` file. `_plan_types()` and `find_leaks()`
|
||||
both ask this instead of computing their own stem, so the two answer the
|
||||
same question about the same path (AGENTS.md invariant 8)."""
|
||||
name = relative.rsplit("/", 1)[-1]
|
||||
if name.endswith(_TYPE_SCHEMA_SUFFIX):
|
||||
return name[: -len(_TYPE_SCHEMA_SUFFIX)]
|
||||
if name.endswith(".md") and not name.endswith(".guidance.md"):
|
||||
return name[: -len(".md")]
|
||||
return None
|
||||
|
||||
|
||||
def _plan_types() -> dict[str, PlannedFile]:
|
||||
"""`types/`, with the page type-specs re-keyed as templates.
|
||||
|
||||
@@ -339,6 +373,11 @@ def _plan_types() -> dict[str, PlannedFile]:
|
||||
to be adopted before it counts. A type-spec's `.schema.yaml` travels with
|
||||
it, because the two are one type (see types/type-spec.md § Anatomy) and
|
||||
adopting half of it would leave a spec validated by a file it does not own.
|
||||
|
||||
A type-spec's optional `<name>.guidance.md` (Gitea #104) is the opposite:
|
||||
stack-owned even where the type-spec itself is instance-owned, and ships
|
||||
verbatim beside the `.template` - `_owned_type_stem` is what keeps it out
|
||||
of this re-keying despite sharing the type-spec's own stem.
|
||||
"""
|
||||
plan = _copy_tree(config.TYPES_DIR, "types", frozenset())
|
||||
stems = instance_owned_type_stems()
|
||||
@@ -347,9 +386,8 @@ def _plan_types() -> dict[str, PlannedFile]:
|
||||
|
||||
rekeyed: dict[str, PlannedFile] = {}
|
||||
for relative, planned in plan.items():
|
||||
name = relative.rsplit("/", 1)[-1]
|
||||
stem = name.split(".", 1)[0]
|
||||
if stem in stems:
|
||||
stem = _owned_type_stem(relative)
|
||||
if stem is not None and stem in stems:
|
||||
rekeyed[f"{relative}.template"] = planned
|
||||
else:
|
||||
rekeyed[relative] = planned
|
||||
@@ -504,7 +542,8 @@ def find_leaks(plan: dict[str, PlannedFile]) -> list[str]:
|
||||
elif (
|
||||
relative.startswith("types/")
|
||||
and not relative.endswith(".template")
|
||||
and name.split(".", 1)[0] in owned_types
|
||||
and (owned_stem := _owned_type_stem(relative)) is not None
|
||||
and owned_stem in owned_types
|
||||
):
|
||||
leaks.append(f"{relative} (this instance's page type-spec; ship the .template)")
|
||||
elif relative.startswith("instructions/dev/"):
|
||||
|
||||
@@ -32,6 +32,27 @@ A sixth checks a *reference* rather than a copy: no document `dist export`
|
||||
ships may cite an issue number, because the board those numbers live on
|
||||
exists only in the origin repo.
|
||||
|
||||
A seventh checks the other half of the same reference problem: every relative
|
||||
markdown link in a reference file - `toc.target_files()`'s scope, the same one
|
||||
the table-of-contents check uses - must resolve to a file that actually
|
||||
exists. A link with the wrong `../` count is invisible to every check above:
|
||||
it is present, it names an existing command or contract by title, and nothing
|
||||
renders it to notice the target is unreachable. The complementary half - that
|
||||
`instructions/<name>/SKILL.md` never carries a relative markdown link at all,
|
||||
because `instructions sync` copies it to a different depth than its links
|
||||
assume - is `instructions verify`'s job, not this one, since that module
|
||||
already owns the Skill/Instruction split (`skill_dirs()` vs
|
||||
`instruction_files()`).
|
||||
|
||||
An eighth checks the type layer against its own schema: every file under
|
||||
`types/` declaring `type: types/type-spec.md` must validate against
|
||||
`types/type-spec.schema.yaml`. Before this check existed the schema had
|
||||
already drifted behind two fields real type-specs carry (`root:`,
|
||||
`capture_fields:`) while `additionalProperties: false` sat there describing a
|
||||
contract nothing enforced - the exact "checked or absent" failure this file's
|
||||
opening paragraph names, just one level up, for the schema that describes the
|
||||
type layer instead of a copy the type layer's code produces (Gitea #105).
|
||||
|
||||
Everything here is a hard oracle: a set comparison or a regex, no judgment.
|
||||
Content quality of the contracts themselves stays with the LLM.
|
||||
"""
|
||||
@@ -44,7 +65,7 @@ from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
from chemenu import config, conventions, kb_collections, toc, version as version_mod
|
||||
from chemenu import config, conventions, kb_collections, markdown_code, toc, version as version_mod
|
||||
from chemenu.commands import dist_cmd
|
||||
from chemenu.commands._util import fail, rel_path, success
|
||||
|
||||
@@ -410,6 +431,37 @@ def check_stack_required_types() -> list[str]:
|
||||
return issues
|
||||
|
||||
|
||||
def check_type_spec_frontmatter() -> list[str]:
|
||||
"""Every type-spec's own frontmatter must validate against
|
||||
`types/type-spec.schema.yaml` - the schema that describes the type layer
|
||||
gets the same enforcement any other type's schema gets (Gitea #105).
|
||||
|
||||
Before this check nothing ever called `validate_frontmatter` against a
|
||||
type-spec's own frontmatter, so the schema had quietly drifted behind two
|
||||
fields real type-specs actually carry (`root:`, `capture_fields:`)
|
||||
without anything failing - `additionalProperties: false` described a
|
||||
contract that bound nothing. `resolver.list_type_specs()` already reads
|
||||
every file's frontmatter once for `wikitool types list`; reusing it here
|
||||
means this check costs no second parse pass.
|
||||
"""
|
||||
from chemenu.type_resolver import resolver
|
||||
|
||||
issues: list[str] = []
|
||||
for type_path, frontmatter in resolver.list_type_specs():
|
||||
try:
|
||||
resolver.validate_frontmatter(
|
||||
frontmatter, "types/type-spec.md", source_file=config.ROOT / type_path
|
||||
)
|
||||
except ValueError as exc:
|
||||
# `validate_frontmatter`'s own message names the type path it
|
||||
# validated *against* (always `types/type-spec.md` here, since
|
||||
# every type-spec is validated against the same schema) rather
|
||||
# than the specific file that failed - prefix that file's own
|
||||
# path so two failures in one run stay distinguishable.
|
||||
issues.append(f"{type_path}: {exc}")
|
||||
return issues
|
||||
|
||||
|
||||
def check_legacy_type_blocks() -> list[str]:
|
||||
issues = []
|
||||
guarded = [
|
||||
@@ -452,6 +504,83 @@ def check_toc_regions() -> list[str]:
|
||||
return issues
|
||||
|
||||
|
||||
# A markdown link, `[text](target)`. The target excludes `)` and whitespace -
|
||||
# the same restriction every link in this repo's own instructions already
|
||||
# follows; a target needing either would need CommonMark's <angle-bracket>
|
||||
# escaping, which nothing here uses.
|
||||
MARKDOWN_LINK_RE = re.compile(r"\[[^\]]*\]\(([^)\s]+)\)")
|
||||
|
||||
# The suffix `dist export` re-keys an instance-owned file to, and the one
|
||||
# `setup-instance.md` renames away again. Spelled here rather than imported
|
||||
# from `ownership`, whose own `.template` handling answers a different
|
||||
# question (which side an upstream merge keeps) over a narrower scope
|
||||
# (paths under a content stage).
|
||||
TEMPLATE_SUFFIX = ".template"
|
||||
|
||||
|
||||
def is_external_or_anchor(target: str) -> bool:
|
||||
"""A link this check does not resolve as a filesystem path: an absolute
|
||||
URL, a `mailto:`, or a pure in-page `#anchor`.
|
||||
|
||||
Public (not `_`-prefixed): `instructions_cmd.check_skill_reference_paths`
|
||||
imports this alongside `MARKDOWN_LINK_RE` rather than keeping a second
|
||||
copy - the two checks classify the same link shape, just over different
|
||||
file sets (AGENTS.md invariant 8)."""
|
||||
return target.startswith(("http://", "https://", "mailto:", "#"))
|
||||
|
||||
|
||||
def check_reference_targets() -> list[str]:
|
||||
"""Every relative markdown link in a reference file resolves to a real file.
|
||||
|
||||
Scoped to `toc.target_files()` - AGENTS.md, the stage and collection
|
||||
contracts, and every flat `instructions/**.md` file - the same scope the
|
||||
table-of-contents check uses. That scope already excludes `SKILL.md`
|
||||
(banned from carrying a markdown link at all - `instructions verify`'s
|
||||
`check_skill_reference_paths`), `commonplace/` (vendored, not stack
|
||||
material) and `raw/`/`kb/` page content (data, not documentation) beyond
|
||||
the two files that are themselves reference material.
|
||||
|
||||
A target's `#anchor` suffix is stripped before resolving - CommonMark
|
||||
anchors are not filesystem paths, and nothing here renders one to notice
|
||||
a stale one anyway. Code fences and inline code spans are masked first
|
||||
(`markdown_code.strip_code_spans`), so a passage that shows link syntax
|
||||
as an example is not mistaken for a real reference.
|
||||
|
||||
**A target the stack ships only as a `.template` counts as resolving.**
|
||||
`kb/CONVENTIONS.md` and every `kb/<name>/COLLECTION.md` are instance-owned:
|
||||
a distribution carries `<name>.template` and the instance adopts it by
|
||||
renaming, during `instructions/setup-instance.md`'s personalization step.
|
||||
Between `dist export` and that step the real file legitimately does not
|
||||
exist yet - while `kb/CONTRACT.md` and three flat instructions link to it
|
||||
by its adopted name, correctly, because that is the name it will have.
|
||||
Reporting those as dead links would fail a fresh export for doing exactly
|
||||
what it is supposed to do, and would describe "not personalized yet" as a
|
||||
broken link when `doctor`'s `conventions` check already says it precisely.
|
||||
"""
|
||||
issues = []
|
||||
for path in toc.target_files():
|
||||
text = path.read_text(encoding="utf-8")
|
||||
masked = markdown_code.strip_code_spans(text)
|
||||
for line_number, masked_line in enumerate(masked.splitlines(), start=1):
|
||||
for match in MARKDOWN_LINK_RE.finditer(masked_line):
|
||||
target = match.group(1)
|
||||
if is_external_or_anchor(target):
|
||||
continue
|
||||
target_path = target.split("#", 1)[0]
|
||||
if not target_path:
|
||||
continue
|
||||
resolved = (path.parent / target_path).resolve()
|
||||
if resolved.exists():
|
||||
continue
|
||||
if resolved.with_name(resolved.name + TEMPLATE_SUFFIX).exists():
|
||||
continue
|
||||
issues.append(
|
||||
f"{rel_path(path)}:{line_number} links to `{target}`, which does not "
|
||||
"resolve to an existing file"
|
||||
)
|
||||
return issues
|
||||
|
||||
|
||||
def command_table_free_readmes() -> list[Path]:
|
||||
"""Every README that must not carry a copy of the command table.
|
||||
|
||||
@@ -768,11 +897,12 @@ def check_breaking_change_for_boundary() -> list[str]:
|
||||
|
||||
@app.command("verify")
|
||||
def verify():
|
||||
"""Check the CLI/README command tables, contract presence, type-form drift, ignore rules, version/changelog agreement, and issue references in shipped documents."""
|
||||
"""Check the CLI/README command tables, contract presence, type-form drift, every type-spec's frontmatter against its own schema, ignore rules, version/changelog agreement, issue references, and link targets in shipped documents."""
|
||||
issues = (
|
||||
check_cli_readme()
|
||||
+ check_readmes_have_no_command_table()
|
||||
+ check_collection_contracts()
|
||||
+ check_type_spec_frontmatter()
|
||||
+ check_legacy_type_blocks()
|
||||
+ check_ignored_content()
|
||||
+ check_version_changelog()
|
||||
@@ -780,18 +910,23 @@ def verify():
|
||||
+ check_breaking_change_for_boundary()
|
||||
+ check_no_issue_references()
|
||||
+ check_toc_regions()
|
||||
+ check_reference_targets()
|
||||
)
|
||||
|
||||
if issues:
|
||||
fail("Documentation issues found:\n" + "\n".join(f"- {i}" for i in issues))
|
||||
|
||||
from chemenu.type_resolver import resolver
|
||||
|
||||
success(
|
||||
f"Docs verified: {len(registered_commands())} command(s) documented, "
|
||||
f"{len(kb_collections.iter_kb_collections())} collection(s) and "
|
||||
f"{len(STAGE_CONTRACTS)} stage contract(s) present, no legacy type blocks, "
|
||||
f"{len(resolver.list_type_specs())} type-spec(s) validating against their own schema, "
|
||||
f"{len(IGNORE_CANARIES)} ignore canaries clear, "
|
||||
f"no issue references in {len(shipped_prose())} shipped document(s), "
|
||||
f"tables of contents current on {len(toc.target_files())} reference file(s), "
|
||||
f"tables of contents current and every link resolving on "
|
||||
f"{len(toc.target_files())} reference file(s), "
|
||||
f"{version_mod.CHANGES_FILENAME} documents version "
|
||||
f"{(config.ROOT / version_mod.VERSION_FILENAME).read_text(encoding='utf-8').strip()}."
|
||||
)
|
||||
|
||||
@@ -20,6 +20,19 @@ Both target directories are gitignored. A fresh clone has no skills until `sync`
|
||||
runs; `instructions/bootstrap.md` is the procedure, and `verify` says so rather
|
||||
than reporting an error when *every* copy is missing, because that is the
|
||||
expected state of a clean checkout rather than a fault.
|
||||
|
||||
The copy is also a different depth than the source, and without the sibling
|
||||
files a relative link might expect - a plain `shutil.copytree` per skill
|
||||
directory, not a mirror of the whole `instructions/` tree. A relative markdown
|
||||
link correct at `instructions/<name>/SKILL.md` therefore resolves to a
|
||||
different, usually nonexistent, file in the published copy the harness
|
||||
actually reads. `verify` forbids the shape outright
|
||||
(`check_skill_reference_paths`) rather than checking depth arithmetic, and a
|
||||
`SKILL.md` writes an outbound reference as a repo-root-relative plain path
|
||||
instead - see instructions/CONTRACT.md § "A skill's outbound reference is a
|
||||
plain path, not a link". `docs_verify.check_reference_targets` is the
|
||||
complementary check, over the flat instructions and contracts that are still
|
||||
allowed to link normally because nothing ever copies them elsewhere.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -31,8 +44,8 @@ from pathlib import Path
|
||||
import typer
|
||||
import yaml
|
||||
|
||||
from chemenu import config
|
||||
from chemenu.commands import dist_cmd
|
||||
from chemenu import config, markdown_code
|
||||
from chemenu.commands import dist_cmd, docs_verify
|
||||
from chemenu.commands._util import fail, rel_path, success
|
||||
from chemenu.type_resolver import resolver
|
||||
|
||||
@@ -313,6 +326,45 @@ def dev_only_forbidden_references(instructions_dir: Path | None = None) -> set[s
|
||||
return referenced
|
||||
|
||||
|
||||
def check_skill_reference_paths() -> list[str]:
|
||||
"""No `SKILL.md` may carry a relative markdown link.
|
||||
|
||||
`sync` copies each skill directory verbatim into `.agents/skills/<name>/`
|
||||
and `.claude/skills/<name>/` - a different depth than
|
||||
`instructions/<name>/SKILL.md` itself, and without the sibling files a
|
||||
relative link might expect. A markdown link that resolves correctly at
|
||||
the source (`../session-setup.md`, `../../kb/CONTRACT.md`) resolves to a
|
||||
different, usually nonexistent, file once copied: the number of `../`
|
||||
segments that reaches a target from `instructions/<name>/` does not reach
|
||||
the same target from `.claude/skills/<name>/`.
|
||||
|
||||
So a `SKILL.md` never writes an outbound reference as a relative markdown
|
||||
link - it names the target as a repo-root-relative plain path instead
|
||||
(`` `instructions/session-setup.md` ``, not
|
||||
`[session-setup.md](../session-setup.md)`). See instructions/CONTRACT.md
|
||||
§ "A skill's outbound reference is a plain path, not a link" for why that
|
||||
form survives the copy unchanged.
|
||||
`docs_verify.check_reference_targets` is the complementary check, over the
|
||||
flat instructions and contracts that are still allowed to link normally
|
||||
because nothing ever copies them elsewhere."""
|
||||
issues: list[str] = []
|
||||
for source in skill_dirs():
|
||||
path = source / SKILL_FILE
|
||||
text = path.read_text(encoding="utf-8")
|
||||
masked = markdown_code.strip_code_spans(text)
|
||||
for line_number, masked_line in enumerate(masked.splitlines(), start=1):
|
||||
for match in docs_verify.MARKDOWN_LINK_RE.finditer(masked_line):
|
||||
target = match.group(1)
|
||||
if docs_verify.is_external_or_anchor(target):
|
||||
continue
|
||||
issues.append(
|
||||
f"{rel_path(path)}:{line_number} carries a relative markdown link to "
|
||||
f"`{target}` - `instructions sync` copies this file to a different depth, "
|
||||
"so write the target as a plain repo-root-relative path instead"
|
||||
)
|
||||
return issues
|
||||
|
||||
|
||||
@app.command("sync")
|
||||
def sync(
|
||||
force: bool = typer.Option(
|
||||
@@ -351,7 +403,7 @@ def sync(
|
||||
|
||||
@app.command("verify")
|
||||
def verify():
|
||||
"""Check instructions/ against its type, and every published copy against its source."""
|
||||
"""Check instructions/ against its type, that no skill carries a relative markdown link, and every published copy against its source."""
|
||||
sources = skill_dirs()
|
||||
instructions = instruction_files()
|
||||
if not sources and not instructions:
|
||||
@@ -398,7 +450,11 @@ def verify():
|
||||
if not frontmatter.get("description"):
|
||||
issues.append(f"{source.name}: SKILL.md is missing (or has an empty) `description`")
|
||||
|
||||
# 3. Published copies match their sources. Missing *everywhere* is a clean
|
||||
# 3. No skill carries a relative markdown link - see
|
||||
# check_skill_reference_paths's own docstring for why the copy breaks it.
|
||||
issues.extend(check_skill_reference_paths())
|
||||
|
||||
# 4. Published copies match their sources. Missing *everywhere* is a clean
|
||||
# checkout, not a fault - say what to run instead of reporting drift.
|
||||
expected = len(sources) * len(target_dirs())
|
||||
missing = 0
|
||||
@@ -419,7 +475,7 @@ def verify():
|
||||
if missing and not bootstrap_needed:
|
||||
issues.append(f"{missing} published copy/copies missing - run `wikitool instructions sync`")
|
||||
|
||||
# 4. An instruction nothing loads is inert. Nothing else would report it -
|
||||
# 5. An instruction nothing loads is inert. Nothing else would report it -
|
||||
# unless it is `manual: true`, which inverts the rule over a narrower
|
||||
# haystack: that instruction must not be linked from AGENTS.md or a
|
||||
# skill (automatic pickup), though a CONTRACT.md mentioning it by name
|
||||
@@ -441,7 +497,7 @@ def verify():
|
||||
"Link it from a skill, a contract, AGENTS.md, or CLAUDE.md, or delete it."
|
||||
)
|
||||
|
||||
# 5. instructions/dev/ is a hard boundary: `dist export` prunes it whole,
|
||||
# 6. instructions/dev/ is a hard boundary: `dist export` prunes it whole,
|
||||
# so nothing outside it may depend on something inside it staying
|
||||
# around in a distributed instance. See dev_only_forbidden_references's
|
||||
# docstring for the dist:strip exemption.
|
||||
|
||||
@@ -35,7 +35,7 @@ from chemenu.search.service import (
|
||||
sort_hits,
|
||||
unreadable_pages,
|
||||
)
|
||||
from chemenu.search.types import Predicate, SearchHit, SearchQuery
|
||||
from chemenu.search.types import DEFAULT_LIMIT, Predicate, SearchQuery, SearchResult
|
||||
|
||||
# Re-exported so `from chemenu.commands.search import run_search` keeps
|
||||
# resolving. The core lives in `chemenu/search/service.py`, which imports no
|
||||
@@ -49,32 +49,76 @@ __all__ = [
|
||||
"search_command",
|
||||
]
|
||||
|
||||
TITLE_WIDTH = 34
|
||||
SUMMARY_WIDTH = 84
|
||||
|
||||
# One hit per line, ` | `-separated, in the order score, kind, title, path,
|
||||
# summary. Three properties are load-bearing and should survive any edit here:
|
||||
#
|
||||
# 1. **The path is present.** It was not, and the instructions that drive this
|
||||
# command tell an agent to "read only the pages the search points at" - which
|
||||
# it could not do, because nothing here pointed anywhere. What a session did
|
||||
# instead was run `grep -rl` over `kb/` for the filenames, a second search
|
||||
# that can find no page this one missed (the backend *is* `rg` over `kb/`).
|
||||
# 2. **Title and path are never truncated.** The title is the wiki's only
|
||||
# identifier for a page (AGENTS.md invariant 2) and the argument `xref add`,
|
||||
# `cite add` and `touch` all take; a title clipped to a column width is not
|
||||
# one. The old fixed 34-char field clipped four of five hits in the report
|
||||
# that prompted this. Only the summary is lossy, which is why it goes last.
|
||||
# 3. **The separator is unambiguous.** A `|` cannot occur in a title - the
|
||||
# wikilink syntax reserves it, so a page carrying one could not be linked at
|
||||
# all - and a `|` in the summary is harmless, because the summary is the
|
||||
# final field: split on " | " with maxsplit=4 and prose cannot shift a
|
||||
# column.
|
||||
#
|
||||
# Column padding is gone with the widths: it aligned the table for an eye, and
|
||||
# the reader here is an agent that pays for the spaces by the token.
|
||||
SEPARATOR = " | "
|
||||
|
||||
|
||||
def _truncate(text: str, width: int) -> str:
|
||||
text = " ".join(text.split())
|
||||
return text if len(text) <= width else text[: width - 1] + "\u2026"
|
||||
|
||||
|
||||
def render_table(hits: list[SearchHit], show_matches: bool) -> str:
|
||||
if not hits:
|
||||
def _count_line(result: SearchResult) -> str:
|
||||
"""The last line: how many hits, and whether that is all of them.
|
||||
|
||||
A bare `N result(s).` reads as the whole answer, so it is only used when it
|
||||
is one. A capped search says what it capped, which is the number the caller
|
||||
would otherwise have to run a second, unlimited search to learn.
|
||||
"""
|
||||
if not result.truncated:
|
||||
return f"{len(result.hits)} result(s)."
|
||||
return (
|
||||
f"{len(result.hits)} of {result.total} result(s) - "
|
||||
f"raise --limit (0 for all) or narrow the query."
|
||||
)
|
||||
|
||||
|
||||
def render_table(result: SearchResult, show_matches: bool) -> str:
|
||||
if not result.hits:
|
||||
return "No matches."
|
||||
lines = []
|
||||
for hit in hits:
|
||||
for hit in result.hits:
|
||||
kind = hit.kind or "?"
|
||||
if hit.subtype:
|
||||
kind = f"{kind}/{hit.subtype}"
|
||||
lines.append(
|
||||
f"{hit.score:6.1f} {_truncate(hit.title, TITLE_WIDTH):<{TITLE_WIDTH}} "
|
||||
f"{kind:<18} {_truncate(hit.summary, SUMMARY_WIDTH)}"
|
||||
SEPARATOR.join(
|
||||
(
|
||||
f"{hit.score:.1f}",
|
||||
kind,
|
||||
hit.title,
|
||||
hit.path,
|
||||
_truncate(hit.summary, SUMMARY_WIDTH),
|
||||
)
|
||||
)
|
||||
)
|
||||
if show_matches:
|
||||
for match in hit.matches:
|
||||
lines.append(f" {hit.path}:{match.line}: {_truncate(match.text, 100)}")
|
||||
lines.append(f" {hit.path}:{match.line}: {_truncate(match.text, 100)}")
|
||||
lines.append("")
|
||||
lines.append(f"{len(hits)} result(s).")
|
||||
lines.append(_count_line(result))
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
@@ -99,7 +143,11 @@ def search_command(
|
||||
regex: bool = typer.Option(
|
||||
False, "--regex", help="Treat the query as a regex. Off by default: terms are literal."
|
||||
),
|
||||
limit: int = typer.Option(20, "--limit", help="Maximum number of results. 0 for no limit."),
|
||||
limit: int = typer.Option(
|
||||
DEFAULT_LIMIT,
|
||||
"--limit",
|
||||
help="Maximum number of results. 0 for no limit. A capped result says so.",
|
||||
),
|
||||
sort: str = typer.Option(
|
||||
None, "--sort", help="Sort by a result field; prefix with '-' to reverse, e.g. -modified."
|
||||
),
|
||||
@@ -146,7 +194,7 @@ def search_command(
|
||||
|
||||
pages = load_pages_by_path()
|
||||
try:
|
||||
hits = run_search(query, pages, backends)
|
||||
result = run_search(query, pages, backends)
|
||||
except PredicateError as exc:
|
||||
fail(str(exc))
|
||||
except RipgrepMissing as exc:
|
||||
@@ -162,8 +210,15 @@ def search_command(
|
||||
"query": text,
|
||||
"predicates": [p.render() for p in predicates],
|
||||
"backend": ",".join(b.name for b in backends),
|
||||
"count": len(hits),
|
||||
"results": [hit.as_dict() for hit in hits],
|
||||
# `count` keeps its meaning - how many results are in this payload -
|
||||
# so a consumer written against the old shape reads the same number
|
||||
# it always did. `total`/`truncated`/`limit` are what it could not
|
||||
# ask before.
|
||||
"count": len(result.hits),
|
||||
"total": result.total,
|
||||
"truncated": result.truncated,
|
||||
"limit": result.limit,
|
||||
"results": [hit.as_dict() for hit in result.hits],
|
||||
# Always present, usually empty. A caller that has to look for the
|
||||
# key to learn whether it should worry will not look.
|
||||
"unreadable": unreadable,
|
||||
@@ -171,7 +226,7 @@ def search_command(
|
||||
typer.echo(json.dumps(payload, indent=2))
|
||||
return
|
||||
|
||||
typer.echo(render_table(hits, show_matches))
|
||||
typer.echo(render_table(result, show_matches))
|
||||
for entry in unreadable:
|
||||
typer.echo(
|
||||
f"WARN unreadable frontmatter: {entry['path']} ({entry['reason']}) - "
|
||||
|
||||
@@ -17,6 +17,7 @@ import json
|
||||
|
||||
import typer
|
||||
|
||||
from chemenu import toc
|
||||
from chemenu.commands._util import fail
|
||||
from chemenu.types_core import UnknownType, describe_type, list_types
|
||||
|
||||
@@ -54,7 +55,10 @@ def describe_type_command(
|
||||
"""Print one type's full contract: frontmatter fields (required/optional,
|
||||
with enums where declared), its subtype field if any, and its authoring
|
||||
body - the same information an LLM would otherwise gather by reading the
|
||||
raw type-spec and `.schema.yaml` files directly."""
|
||||
raw type-spec and `.schema.yaml` files directly. Where the type-spec
|
||||
declares `guidance:`, that stack-owned file's prose is composed in ahead
|
||||
of the type-spec's own body, so a `root: kb` type's contract still reads
|
||||
as one answer even though it lives in two files (Gitea #104)."""
|
||||
try:
|
||||
described = describe_type(name)
|
||||
except UnknownType as exc:
|
||||
@@ -88,4 +92,13 @@ def describe_type_command(
|
||||
typer.echo("")
|
||||
|
||||
typer.echo("## Authoring guidance")
|
||||
typer.echo(described["body"])
|
||||
# A type-spec (and its guidance file) over 100 lines carries a generated
|
||||
# table-of-contents region (`chemenu/toc.py`), which serves whoever opens
|
||||
# the file directly. Here it would be noise: this command already hands
|
||||
# over the whole body, so there is nothing left for a navigation aid to
|
||||
# navigate - only markers and a list of headings the reader is about to
|
||||
# see anyway.
|
||||
if described["guidance"]:
|
||||
typer.echo(toc.strip_region(described["guidance"]))
|
||||
typer.echo("")
|
||||
typer.echo(toc.strip_region(described["body"]))
|
||||
|
||||
@@ -204,7 +204,7 @@ def bump_command(
|
||||
breaking: Optional[str] = typer.Option(
|
||||
None,
|
||||
"--breaking",
|
||||
help="What stops working, for the bump that first escalates to a boundary crossing (recorded in CHANGES.md). Required there, refused on a bump that crosses nothing",
|
||||
help="What stops working (recorded in CHANGES.md). Required on the bump that first escalates to a boundary crossing, optional on a later bump of the same crossing candidate - where it joins the reasons already recorded rather than replacing them. Refused on a bump that crosses nothing",
|
||||
),
|
||||
no_migration: Optional[str] = typer.Option(
|
||||
None,
|
||||
@@ -241,9 +241,16 @@ def bump_command(
|
||||
version is not a drop-in replacement, whether or not any content moves -
|
||||
requires `--breaking "<what stops working>"`, and on top of that either a
|
||||
migration document for the new base or `--no-migration "<reason>"`. Both
|
||||
lines are written into the entry once and then persist across every later
|
||||
bump at the same stage: a follow-up bump need not repeat them, and passing
|
||||
either on a bump that crosses nothing at all is refused.
|
||||
are written into the entry and persist across every later bump at the same
|
||||
stage, so a follow-up bump need not repeat them, and passing either on a
|
||||
bump that crosses nothing at all is refused.
|
||||
|
||||
A candidate can cross the boundary more than once, and the two flags part
|
||||
ways there. A further `--breaking` **joins** the reasons already recorded -
|
||||
each crossing is its own thing an operator has to act on, and replacing
|
||||
meant the second one silently deleted the first. A further
|
||||
`--no-migration` **replaces**: whether content has to change is one
|
||||
question about the candidate as a whole, not one per crossing.
|
||||
|
||||
A later bump of the same candidate that finds out `--no-migration` was
|
||||
wrong after all retracts it with `--migration-required` - write the
|
||||
|
||||
@@ -56,6 +56,7 @@ from mcp.server.mcpserver.exceptions import ToolError
|
||||
from chemenu import config, upload
|
||||
from chemenu.api import Corpus
|
||||
from chemenu.errors import ChemenuError
|
||||
from chemenu.search.types import DEFAULT_LIMIT
|
||||
from chemenu.telemetry import policy
|
||||
|
||||
SERVER_NAME = "chemenu"
|
||||
@@ -158,14 +159,16 @@ def build_server(
|
||||
"Find pages in kb/ by text, by frontmatter, or by both. Returns "
|
||||
"title, path, kind and summary per hit, so a result can be judged "
|
||||
"without fetching the page. Prefer this over listing files: the "
|
||||
"answer is a few hundred tokens instead of a whole index."
|
||||
"answer is a few hundred tokens instead of a whole index. "
|
||||
"'count' is how many hits came back and 'total' how many matched; "
|
||||
"when 'truncated' is true, raise 'limit' (0 for all) to see the rest."
|
||||
),
|
||||
)
|
||||
def search(
|
||||
query: str | None = None,
|
||||
predicates: list[str] | None = None,
|
||||
regex: bool = False,
|
||||
limit: int = 20,
|
||||
limit: int = DEFAULT_LIMIT,
|
||||
sort: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Search the wiki.
|
||||
|
||||
@@ -28,7 +28,7 @@ from chemenu.search import filters
|
||||
from chemenu.search.base import page_key
|
||||
from chemenu.search.fuse import reciprocal_rank_fusion
|
||||
from chemenu.search.ripgrep import build_hit
|
||||
from chemenu.search.types import SearchHit, SearchQuery
|
||||
from chemenu.search.types import SearchHit, SearchQuery, SearchResult
|
||||
|
||||
|
||||
def load_pages_by_path(kb_dir: Path | None = None, root: Path | None = None) -> dict[str, Page]:
|
||||
@@ -92,8 +92,13 @@ def run_search(
|
||||
pages: dict[str, Page],
|
||||
backends: list,
|
||||
kb_dir: Path | None = None,
|
||||
) -> list[SearchHit]:
|
||||
"""Answer a query. Pure: no I/O beyond whatever a backend does."""
|
||||
) -> SearchResult:
|
||||
"""Answer a query. Pure: no I/O beyond whatever a backend does.
|
||||
|
||||
Returns the truncated hits *and* the number there were before the limit,
|
||||
because the caller cannot recover the second from the first - see
|
||||
`SearchResult`.
|
||||
"""
|
||||
filters.validate_fields(query.predicates, pages)
|
||||
|
||||
if query.text:
|
||||
@@ -110,4 +115,9 @@ def run_search(
|
||||
hits.sort(key=lambda h: h.title.lower())
|
||||
|
||||
hits = sort_hits(hits, query.sort)
|
||||
return hits[: query.limit] if query.limit else hits
|
||||
total = len(hits)
|
||||
return SearchResult(
|
||||
hits=hits[: query.limit] if query.limit else hits,
|
||||
total=total,
|
||||
limit=query.limit,
|
||||
)
|
||||
|
||||
@@ -4,6 +4,18 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Optional
|
||||
|
||||
# How many hits a caller gets when it asks for no particular number. Defined
|
||||
# once, here, because three adapters offer the same knob - the CLI's `--limit`,
|
||||
# `api.search(limit=...)` and the MCP `search` tool - and three literals is how
|
||||
# they start disagreeing about what "the default search" returns.
|
||||
#
|
||||
# 50 rather than a smaller number because the queries that actually hit the cap
|
||||
# are the *structured* sweeps (`--field '!sources'`), which are ordered
|
||||
# alphabetically rather than by relevance: truncating those throws away an
|
||||
# arbitrary slice of the answer rather than its weakest tail. A capped result
|
||||
# is only safe at all because it now says so - see `SearchResult.truncated`.
|
||||
DEFAULT_LIMIT = 50
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Predicate:
|
||||
@@ -30,7 +42,7 @@ class SearchQuery:
|
||||
text: Optional[str] = None
|
||||
predicates: tuple[Predicate, ...] = ()
|
||||
regex: bool = False
|
||||
limit: int = 20
|
||||
limit: int = DEFAULT_LIMIT
|
||||
sort: Optional[str] = None
|
||||
|
||||
|
||||
@@ -77,3 +89,34 @@ class SearchHit:
|
||||
"backend": self.backend,
|
||||
"matches": [m.as_dict() for m in self.matches],
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SearchResult:
|
||||
"""The hits a caller gets back, plus how many there were before the limit.
|
||||
|
||||
`run_search` used to return the truncated list alone, which made the
|
||||
truncation impossible to report: every adapter counted `len(hits)` and
|
||||
printed it as the answer, so `20 result(s).` on a query matching 182 pages
|
||||
was indistinguishable from a query that really matched twenty. That is a
|
||||
completeness claim none of them were in a position to make, and the only
|
||||
way to find out was to ask again with `--limit 0` - a second full search to
|
||||
learn a number the first one already knew.
|
||||
|
||||
So the total travels with the hits. Nothing here decides how to say it;
|
||||
that belongs to each adapter (`render_table`, the `--json` payload,
|
||||
`api.search`).
|
||||
"""
|
||||
|
||||
hits: list[SearchHit]
|
||||
total: int
|
||||
limit: int
|
||||
|
||||
@property
|
||||
def truncated(self) -> bool:
|
||||
"""Whether the limit actually cut something off.
|
||||
|
||||
`limit=0` means "no limit", so it never truncates however large the
|
||||
corpus is.
|
||||
"""
|
||||
return bool(self.limit) and self.total > len(self.hits)
|
||||
|
||||
@@ -47,6 +47,21 @@ def test_a_corpus_can_be_named_and_is_the_one_that_answers(foreign_corpus):
|
||||
assert result["results"][0]["path"] == "kb/entities/Peregrine.md"
|
||||
|
||||
|
||||
def test_a_capped_answer_says_how_much_it_left_out(foreign_corpus):
|
||||
"""`count` keeps meaning "what is in this payload", so a consumer written
|
||||
against the old shape reads the number it always did; `total` is what it
|
||||
could not ask before, and without it a truncated answer is indistinguishable
|
||||
from a complete one."""
|
||||
corpus = Corpus(foreign_corpus)
|
||||
|
||||
whole = corpus.search("Peregrine")
|
||||
assert (whole["count"], whole["total"], whole["truncated"]) == (1, 1, False)
|
||||
assert whole["count"] == len(whole["results"])
|
||||
|
||||
capped = corpus.search("Peregrine", limit=0)
|
||||
assert capped["truncated"] is False, "limit 0 means no limit, so it caps nothing"
|
||||
|
||||
|
||||
def test_no_path_of_this_checkout_is_read_while_a_foreign_root_is_set(foreign_corpus):
|
||||
"""The acceptance criterion, asserted rather than argued.
|
||||
|
||||
|
||||
@@ -81,7 +81,16 @@ def repo(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
|
||||
# the instance's; `instruction` writes into the repo and is the stack's.
|
||||
(types_dir / "entity.md").write_text(
|
||||
"---\ntype: types/type-spec.md\nname: entity\ndescription: d\n"
|
||||
"schema: types/entity.schema.yaml\nbase_dir: entities\n---\n\n# Entity\n",
|
||||
"schema: types/entity.schema.yaml\nbase_dir: entities\n"
|
||||
"guidance: types/entity.guidance.md\n---\n\n# Entity\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
# entity's guidance: stack-owned even though entity.md itself is
|
||||
# instance-owned - the file `_owned_type_stem` must not mistake for
|
||||
# entity's own `.md`/`.schema.yaml` despite sharing its stem (Gitea #104).
|
||||
(types_dir / "entity.guidance.md").write_text(
|
||||
"---\ntype: types/type-guidance.md\nname: entity\ndescription: When to use entity.\n"
|
||||
"---\n\n# Entity Guidance\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(types_dir / "instruction.md").write_text(
|
||||
@@ -394,6 +403,20 @@ def test_page_type_specs_ship_as_templates_and_stack_types_do_not(repo, monkeypa
|
||||
assert "types/instruction.md.template" not in plan
|
||||
|
||||
|
||||
def test_guidance_file_ships_verbatim_beside_a_templated_type_spec(repo, monkeypatch):
|
||||
"""Gitea #104: `types/entity.guidance.md` is stack-owned even though
|
||||
`types/entity.md` (same stem) is instance-owned - it must cross like
|
||||
`types/instruction.md` above, never re-keyed as though it were the
|
||||
type-spec's own `.template`."""
|
||||
from chemenu.type_resolver import resolver
|
||||
|
||||
monkeypatch.setattr(resolver, "_repo_root", config.ROOT)
|
||||
plan = dist_cmd.build_plan()
|
||||
|
||||
assert "types/entity.guidance.md" in plan
|
||||
assert "types/entity.guidance.md.template" not in plan
|
||||
|
||||
|
||||
def test_plan_creates_empty_raw_and_incoming_not_real_content(repo):
|
||||
"""Both flat since Gitea #67: `raw/` addresses a file by its accept date,
|
||||
never by a hand-picked type, so there is nothing left to seed per type."""
|
||||
|
||||
@@ -223,6 +223,56 @@ def test_seeded_once_paths_are_never_written_even_if_the_release_stamp_lists_the
|
||||
assert not (instance / preserved).exists()
|
||||
|
||||
|
||||
# --- a root:kb type-spec's guidance half upgrades like any other file -------
|
||||
#
|
||||
# Gitea #104: before the split, `types/<name>.md` carried both the
|
||||
# instance-owned template and the stack-owned authoring prose in one file, so
|
||||
# an instance that had adopted it (renamed the `.template`) never received a
|
||||
# prose improvement again - `dist upgrade` only ever wrote the `.template`
|
||||
# beside the adopted file, never the file itself. Splitting the prose into a
|
||||
# sibling `.guidance.md` that ships verbatim (never `.template`-sourced) means
|
||||
# it upgrades through the ordinary unchanged/new path below, even though the
|
||||
# type-spec it documents is never in the stamp at all and therefore never
|
||||
# touched.
|
||||
|
||||
|
||||
def test_upgrade_writes_improved_guidance_prose_over_an_adopted_type_spec(instance, tmp_path):
|
||||
(instance / "types").mkdir()
|
||||
(instance / "types" / "entity.md").write_text(
|
||||
# Adopted from `types/entity.md.template` at some earlier setup - this
|
||||
# file was never part of any release stamp and `dist upgrade` must
|
||||
# never touch it.
|
||||
"---\ntype: types/type-spec.md\nname: entity\ndescription: d\n"
|
||||
"schema: types/entity.schema.yaml\nbase_dir: entities\n"
|
||||
"guidance: types/entity.guidance.md\n---\n\n# Entity\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(instance / "types" / "entity.guidance.md").write_text("old guidance\n", encoding="utf-8")
|
||||
stamp = json.loads((instance / version_mod.RELEASE_STAMP_FILENAME).read_text())
|
||||
stamp["files"]["types/entity.guidance.md"] = _digest("old guidance\n")
|
||||
(instance / version_mod.RELEASE_STAMP_FILENAME).write_text(json.dumps(stamp), encoding="utf-8")
|
||||
|
||||
release = _release(
|
||||
tmp_path, "release", "1.1.0",
|
||||
{
|
||||
"AGENTS.md": "core\n",
|
||||
"tools/wikitool": "#!/bin/sh\n",
|
||||
"types/entity.guidance.md": "improved guidance\n",
|
||||
},
|
||||
)
|
||||
|
||||
dist_cmd.run_upgrade(release)
|
||||
|
||||
assert (instance / "types" / "entity.guidance.md").read_text(encoding="utf-8") == (
|
||||
"improved guidance\n"
|
||||
)
|
||||
# The adopted type-spec itself was never in either stamp, so it is
|
||||
# completely untouched by the upgrade.
|
||||
assert "guidance: types/entity.guidance.md" in (
|
||||
instance / "types" / "entity.md"
|
||||
).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
# --- migration chain: reported, never run -----------------------------------
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import pytest
|
||||
import typer
|
||||
|
||||
from chemenu import config
|
||||
from chemenu import config, type_resolver
|
||||
from chemenu.commands import dist_cmd, docs_verify
|
||||
from chemenu.type_resolver import TypeResolver
|
||||
|
||||
|
||||
def test_every_registered_command_is_documented():
|
||||
@@ -289,6 +290,51 @@ def test_an_absent_listed_doc_is_skipped_not_reported(tmp_path, monkeypatch):
|
||||
assert docs_verify.check_readmes_have_no_command_table() == []
|
||||
|
||||
|
||||
def test_this_repos_type_specs_validate_against_their_own_schema():
|
||||
"""Regression guard for Gitea #105: types/type-spec.schema.yaml declared
|
||||
`additionalProperties: false` while real type-specs already carried
|
||||
`root:` and `capture_fields:`, and nothing ever validated a type-spec's
|
||||
own frontmatter against it - so the mismatch shipped silently."""
|
||||
assert docs_verify.check_type_spec_frontmatter() == []
|
||||
|
||||
|
||||
def test_an_unknown_type_spec_field_is_reported(tmp_path, monkeypatch):
|
||||
"""Once the schema is enforced, a type-spec frontmatter field its own
|
||||
schema does not know about must fail loudly rather than validating
|
||||
silently - the other direction of the #105 regression guard above."""
|
||||
types_dir = tmp_path / "types"
|
||||
types_dir.mkdir()
|
||||
(types_dir / "type-spec.md").write_text(
|
||||
"---\n"
|
||||
"type: types/type-spec.md\n"
|
||||
"name: type-spec\n"
|
||||
"description: Authoring and validation contract for type specs\n"
|
||||
"schema: types/type-spec.schema.yaml\n"
|
||||
"---\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(types_dir / "type-spec.schema.yaml").write_text(
|
||||
(config.ROOT / "types" / "type-spec.schema.yaml").read_text(encoding="utf-8"),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(types_dir / "widget.md").write_text(
|
||||
"---\n"
|
||||
"type: types/type-spec.md\n"
|
||||
"name: widget\n"
|
||||
"description: A type-spec with a field its own schema does not know\n"
|
||||
"schema: null\n"
|
||||
"not_a_real_field: true\n"
|
||||
"---\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
monkeypatch.setattr(config, "ROOT", tmp_path)
|
||||
monkeypatch.setattr(config, "TYPES_DIR", types_dir)
|
||||
monkeypatch.setattr(type_resolver, "resolver", TypeResolver(repo_root=tmp_path))
|
||||
|
||||
issues = docs_verify.check_type_spec_frontmatter()
|
||||
assert any("widget.md" in issue and "not_a_real_field" in issue for issue in issues)
|
||||
|
||||
|
||||
def test_legacy_type_blocks_are_absent():
|
||||
assert docs_verify.check_legacy_type_blocks() == []
|
||||
|
||||
@@ -533,6 +579,100 @@ def test_no_shipped_document_cites_an_issue():
|
||||
assert docs_verify.check_no_issue_references() == []
|
||||
|
||||
|
||||
def test_a_dead_relative_link_is_reported(tmp_path, monkeypatch):
|
||||
"""Regression guard for the bug this check exists to catch: a `../` count
|
||||
wrong for the file's own depth is invisible to every other check - the
|
||||
name it links to is real, the text renders, and nothing resolves the
|
||||
target to notice it lands nowhere."""
|
||||
fake = tmp_path / "example.md"
|
||||
fake.write_text("See [tools/CONTRACT.md](../tools/CONTRACT.md) for the command table.\n", encoding="utf-8")
|
||||
monkeypatch.setattr(docs_verify.toc, "target_files", lambda: [fake])
|
||||
issues = docs_verify.check_reference_targets()
|
||||
assert len(issues) == 1
|
||||
assert "example.md:1" in issues[0]
|
||||
assert "../tools/CONTRACT.md" in issues[0]
|
||||
|
||||
|
||||
def test_a_resolving_relative_link_is_not_reported(tmp_path, monkeypatch):
|
||||
(tmp_path / "tools").mkdir()
|
||||
(tmp_path / "tools" / "CONTRACT.md").write_text("# Contract\n", encoding="utf-8")
|
||||
fake = tmp_path / "example.md"
|
||||
fake.write_text("See [tools/CONTRACT.md](tools/CONTRACT.md) for the command table.\n", encoding="utf-8")
|
||||
monkeypatch.setattr(docs_verify.toc, "target_files", lambda: [fake])
|
||||
assert docs_verify.check_reference_targets() == []
|
||||
|
||||
|
||||
def test_an_absolute_url_is_not_resolved_as_a_path(tmp_path, monkeypatch):
|
||||
fake = tmp_path / "example.md"
|
||||
fake.write_text("See [Anthropic](https://www.anthropic.com).\n", encoding="utf-8")
|
||||
monkeypatch.setattr(docs_verify.toc, "target_files", lambda: [fake])
|
||||
assert docs_verify.check_reference_targets() == []
|
||||
|
||||
|
||||
def test_a_section_anchor_is_stripped_before_resolving(tmp_path, monkeypatch):
|
||||
"""CommonMark anchors are not filesystem paths - only the path part of
|
||||
`target#anchor` is checked for existence."""
|
||||
(tmp_path / "kb").mkdir()
|
||||
(tmp_path / "kb" / "CONVENTIONS.md").write_text("## Tone\n", encoding="utf-8")
|
||||
fake = tmp_path / "example.md"
|
||||
fake.write_text("See [kb/CONVENTIONS.md § Tone](kb/CONVENTIONS.md#tone).\n", encoding="utf-8")
|
||||
monkeypatch.setattr(docs_verify.toc, "target_files", lambda: [fake])
|
||||
assert docs_verify.check_reference_targets() == []
|
||||
|
||||
|
||||
def test_link_syntax_shown_as_an_example_in_a_fence_is_not_flagged(tmp_path, monkeypatch):
|
||||
"""A passage documenting bad link syntax must not be mistaken for a real
|
||||
reference - code fences are masked before scanning, mirroring `toc.py`."""
|
||||
fake = tmp_path / "example.md"
|
||||
fake.write_text(
|
||||
"Do not write it like this:\n\n```markdown\n[gates.md](../nonexistent.md)\n```\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
monkeypatch.setattr(docs_verify.toc, "target_files", lambda: [fake])
|
||||
assert docs_verify.check_reference_targets() == []
|
||||
|
||||
|
||||
def test_a_target_shipped_only_as_a_template_is_not_dead(tmp_path, monkeypatch):
|
||||
"""Regression guard for a defect this check shipped with. A fresh
|
||||
`dist export` carries `kb/CONVENTIONS.md.template`, not
|
||||
`kb/CONVENTIONS.md` - the instance adopts it by renaming, during
|
||||
`setup-instance.md`'s personalization step. `kb/CONTRACT.md` and three
|
||||
flat instructions link to the adopted name, correctly. Before this
|
||||
exemption the check reported 13 dead links on a just-exported tree, for
|
||||
doing exactly what a fresh export is supposed to do."""
|
||||
(tmp_path / "kb").mkdir()
|
||||
(tmp_path / "kb" / "CONVENTIONS.md.template").write_text("# Conventions\n", encoding="utf-8")
|
||||
fake = tmp_path / "kb" / "CONTRACT.md"
|
||||
fake.write_text("What this instance decided: [kb/CONVENTIONS.md](CONVENTIONS.md).\n", encoding="utf-8")
|
||||
monkeypatch.setattr(docs_verify.toc, "target_files", lambda: [fake])
|
||||
assert docs_verify.check_reference_targets() == []
|
||||
|
||||
|
||||
def test_a_target_with_neither_the_file_nor_a_template_is_still_dead(tmp_path, monkeypatch):
|
||||
"""The exemption is narrow: it covers a file the stack ships as a
|
||||
template, not any missing target."""
|
||||
(tmp_path / "kb").mkdir()
|
||||
fake = tmp_path / "kb" / "CONTRACT.md"
|
||||
fake.write_text("See [kb/CONVENTIONS.md](CONVENTIONS.md).\n", encoding="utf-8")
|
||||
monkeypatch.setattr(docs_verify.toc, "target_files", lambda: [fake])
|
||||
issues = docs_verify.check_reference_targets()
|
||||
assert len(issues) == 1
|
||||
assert "CONVENTIONS.md" in issues[0]
|
||||
|
||||
|
||||
def test_every_reference_files_link_targets_resolve():
|
||||
"""Forward direction, against the real tree: every relative link in a file
|
||||
`toc.target_files()` covers must resolve - this is what a `../` count
|
||||
wrong for the file's own depth is caught by."""
|
||||
assert docs_verify.check_reference_targets() == []
|
||||
|
||||
|
||||
def test_verify_raises_when_a_reference_target_is_dead(monkeypatch):
|
||||
monkeypatch.setattr(docs_verify, "check_reference_targets", lambda: ["dangling"])
|
||||
with pytest.raises(typer.Exit):
|
||||
docs_verify.verify()
|
||||
|
||||
|
||||
def test_a_cited_issue_number_is_reported(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
docs_verify,
|
||||
|
||||
@@ -341,6 +341,123 @@ def test_a_self_mention_does_not_count_as_a_reference(layer):
|
||||
assert "gates.md" not in instructions_cmd.referenced_names()
|
||||
|
||||
|
||||
# --- skill reference paths ---------------------------------------------------
|
||||
|
||||
|
||||
def test_the_fixture_skill_carries_no_relative_link(layer):
|
||||
"""Forward direction: the fixture's own `wiki-demo/SKILL.md` mentions
|
||||
`gates.md` as a bare word (`referenced_names()` relies on exactly that
|
||||
substring match), never as a markdown link - so it must not trip the ban."""
|
||||
assert instructions_cmd.check_skill_reference_paths() == []
|
||||
|
||||
|
||||
def test_a_relative_markdown_link_in_a_skill_is_reported(layer):
|
||||
"""`sync` copies `wiki-demo/SKILL.md` to `.claude/skills/wiki-demo/SKILL.md`
|
||||
and `.agents/skills/wiki-demo/SKILL.md` - a different depth than the
|
||||
source - so a link written `../gates.md` here would resolve to a
|
||||
different, usually nonexistent, file once published."""
|
||||
(layer / "instructions" / "wiki-demo" / "SKILL.md").write_text(
|
||||
"---\nname: wiki-demo\ndescription: Demo skill.\n---\n\n"
|
||||
"# Demo\n\nSee [gates.md](../gates.md).\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
issues = instructions_cmd.check_skill_reference_paths()
|
||||
assert len(issues) == 1
|
||||
assert "wiki-demo" in issues[0] and "../gates.md" in issues[0]
|
||||
|
||||
|
||||
def test_a_plain_root_relative_path_is_not_a_reported_link(layer):
|
||||
"""The decided fix: a `SKILL.md` names its target as a repo-root-relative
|
||||
plain path, not a link - even one written correctly with brackets and
|
||||
parens. `instructions/gates.md` on its own, with no `[...]`, must pass."""
|
||||
(layer / "instructions" / "wiki-demo" / "SKILL.md").write_text(
|
||||
"---\nname: wiki-demo\ndescription: Demo skill.\n---\n\n"
|
||||
"# Demo\n\nSee `instructions/gates.md`.\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
assert instructions_cmd.check_skill_reference_paths() == []
|
||||
|
||||
|
||||
def test_an_absolute_url_in_a_skill_is_not_reported(layer):
|
||||
"""The ban is about relative paths breaking under the copy - an external
|
||||
link is not affected by where the file sits, so it stays a normal link."""
|
||||
(layer / "instructions" / "wiki-demo" / "SKILL.md").write_text(
|
||||
"---\nname: wiki-demo\ndescription: Demo skill.\n---\n\n"
|
||||
"# Demo\n\nSee [the spec](https://example.com/spec).\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
assert instructions_cmd.check_skill_reference_paths() == []
|
||||
|
||||
|
||||
def test_a_pure_anchor_link_in_a_skill_is_not_reported(layer):
|
||||
"""A same-page `#anchor` link is not a filesystem reference and does not
|
||||
move when the file is copied."""
|
||||
(layer / "instructions" / "wiki-demo" / "SKILL.md").write_text(
|
||||
"---\nname: wiki-demo\ndescription: Demo skill.\n---\n\n"
|
||||
"# Demo\n\n## Steps\n\nSee [Steps](#steps) above.\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
assert instructions_cmd.check_skill_reference_paths() == []
|
||||
|
||||
|
||||
def test_link_syntax_shown_as_an_example_in_a_fence_is_not_flagged(layer):
|
||||
"""A skill documenting the banned syntax as an example (rather than using
|
||||
it) must not trip its own ban - code fences are masked before scanning,
|
||||
the same way `toc.py` masks them before hunting for headings."""
|
||||
(layer / "instructions" / "wiki-demo" / "SKILL.md").write_text(
|
||||
"---\nname: wiki-demo\ndescription: Demo skill.\n---\n\n"
|
||||
"# Demo\n\nDo not write it like this:\n\n"
|
||||
"```markdown\n[gates.md](../gates.md)\n```\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
assert instructions_cmd.check_skill_reference_paths() == []
|
||||
|
||||
|
||||
def test_verify_rejects_a_skill_with_a_relative_markdown_link(layer):
|
||||
(layer / "instructions" / "wiki-demo" / "SKILL.md").write_text(
|
||||
"---\nname: wiki-demo\ndescription: Demo skill.\n---\n\n"
|
||||
"# Demo\n\nSee [gates.md](../gates.md).\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
instructions_cmd.sync(force=False)
|
||||
with pytest.raises(typer.Exit):
|
||||
instructions_cmd.verify()
|
||||
|
||||
|
||||
def test_a_plain_path_resolves_the_same_regardless_of_which_copy_reads_it(layer):
|
||||
"""The property the whole fix rests on. A repo-root-relative plain path
|
||||
(`instructions/gates.md`) names the same file whether it is read from the
|
||||
source (`instructions/wiki-demo/SKILL.md`) or from either published copy
|
||||
(`.claude/skills/wiki-demo/SKILL.md`, `.agents/skills/wiki-demo/SKILL.md`),
|
||||
because it is resolved against the instance root, never against the
|
||||
reading file's own directory.
|
||||
|
||||
A relative link has no such property, which is the defect this whole
|
||||
check exists to prevent: the same `../gates.md` means
|
||||
`instructions/gates.md` from the source but a nonexistent
|
||||
`.claude/skills/gates.md` / `.agents/skills/gates.md` from either
|
||||
published copy - one directory short of the real file, because `sync`
|
||||
copies the skill one level shallower than `instructions/<name>/` sits."""
|
||||
instructions_cmd.sync(force=False)
|
||||
target = layer / "instructions" / "gates.md"
|
||||
assert target.is_file()
|
||||
|
||||
source_dir = layer / "instructions" / "wiki-demo"
|
||||
claude_copy_dir = _skill_copy(layer, ".claude")
|
||||
agents_copy_dir = _skill_copy(layer, ".agents")
|
||||
|
||||
# The plain path resolves against the instance root, regardless of which
|
||||
# of the three directories above is doing the reading.
|
||||
for _ in (source_dir, claude_copy_dir, agents_copy_dir):
|
||||
assert (layer / "instructions" / "gates.md").is_file()
|
||||
|
||||
# The equivalent relative link would not: correct from the source, wrong
|
||||
# from both copies.
|
||||
assert (source_dir / ".." / "gates.md").resolve() == target.resolve()
|
||||
assert not (claude_copy_dir / ".." / "gates.md").resolve().is_file()
|
||||
assert not (agents_copy_dir / ".." / "gates.md").resolve().is_file()
|
||||
|
||||
|
||||
# --- instructions/dev/ boundary ----------------------------------------------
|
||||
|
||||
|
||||
|
||||
@@ -125,6 +125,24 @@ def test_the_four_tools_are_there_and_nothing_that_writes(corpus):
|
||||
assert names.isdisjoint({"new", "touch", "xref", "cite", "publish", "migrate", "rm"})
|
||||
|
||||
|
||||
def test_all_three_adapters_offer_the_same_default_limit(corpus):
|
||||
"""The CLI's `--limit`, `api.search(limit=...)` and this tool are three
|
||||
knobs on one search, and three literals is how they start disagreeing about
|
||||
what "the default search" returns. They read one constant."""
|
||||
import inspect
|
||||
|
||||
from chemenu.api import Corpus
|
||||
from chemenu.commands.search import search_command
|
||||
from chemenu.search.types import DEFAULT_LIMIT
|
||||
|
||||
server = build_server(corpus, check_trace=False)
|
||||
tool = next(t for t in asyncio.run(server.list_tools()) if t.name == "search")
|
||||
|
||||
assert tool.input_schema["properties"]["limit"]["default"] == DEFAULT_LIMIT
|
||||
assert inspect.signature(Corpus.search).parameters["limit"].default == DEFAULT_LIMIT
|
||||
assert inspect.signature(search_command).parameters["limit"].default.default == DEFAULT_LIMIT
|
||||
|
||||
|
||||
def test_no_tool_writes_anything_into_the_corpus_or_git(corpus):
|
||||
server = build_server(corpus, check_trace=False)
|
||||
before = _tree(corpus)
|
||||
@@ -176,7 +194,8 @@ def test_the_wire_format_is_the_clis_json_form(corpus):
|
||||
# `generated` is the CLI's date stamp and `commit`/`as_of` are the server's
|
||||
# revision stamp - two answers to "when", neither of them a finding. What
|
||||
# has to match is everything that describes the *corpus*.
|
||||
shared = ("query", "predicates", "backend", "count", "results", "unreadable")
|
||||
shared = ("query", "predicates", "backend", "count", "total", "truncated", "limit",
|
||||
"results", "unreadable")
|
||||
assert {key: served[key] for key in shared} == {key: from_cli[key] for key in shared}
|
||||
|
||||
|
||||
|
||||
@@ -17,7 +17,14 @@ from chemenu.search.filters import PredicateError, parse_predicate
|
||||
from chemenu.search.fuse import reciprocal_rank_fusion
|
||||
from chemenu.search.registry import UnknownBackend, resolve
|
||||
from chemenu.search.ripgrep import RipgrepBackend, build_argv
|
||||
from chemenu.search.types import Match, Predicate, SearchHit, SearchQuery
|
||||
from chemenu.search.types import (
|
||||
DEFAULT_LIMIT,
|
||||
Match,
|
||||
Predicate,
|
||||
SearchHit,
|
||||
SearchQuery,
|
||||
SearchResult,
|
||||
)
|
||||
from chemenu.frontmatter_io import write_page
|
||||
|
||||
|
||||
@@ -32,8 +39,12 @@ def backend(kb_dir: Path, tmp_path: Path):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def search(kb_dir: Path, pages):
|
||||
"""Run a query against the fixture kb rather than the real one."""
|
||||
def search_result(kb_dir: Path, pages):
|
||||
"""Run a query against the fixture kb and return the whole `SearchResult`.
|
||||
|
||||
For the tests that care about the limit and the total; most only want the
|
||||
hits, and use `search` below.
|
||||
"""
|
||||
|
||||
def _search(query: SearchQuery, backends=()):
|
||||
return run_search(query, pages, list(backends), kb_dir)
|
||||
@@ -41,6 +52,16 @@ def search(kb_dir: Path, pages):
|
||||
return _search
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def search(search_result):
|
||||
"""The hits alone, for tests whose subject is ranking or filtering."""
|
||||
|
||||
def _search(query: SearchQuery, backends=()):
|
||||
return search_result(query, backends).hits
|
||||
|
||||
return _search
|
||||
|
||||
|
||||
def _titles(hits):
|
||||
return [hit.title for hit in hits]
|
||||
|
||||
@@ -211,6 +232,20 @@ def test_limit_and_sort(search):
|
||||
assert len(search(_q("kind=entity", limit=2))) == 2
|
||||
|
||||
|
||||
def test_the_total_survives_the_limit_so_no_second_search_is_needed(search_result):
|
||||
"""The caller cannot recover the total from a truncated list, and asking
|
||||
again with `--limit 0` is a second full search to learn a number the first
|
||||
one already had."""
|
||||
capped = search_result(_q("kind=entity", limit=2))
|
||||
|
||||
assert len(capped.hits) == 2
|
||||
assert capped.total == 3
|
||||
assert capped.truncated is True
|
||||
|
||||
whole = search_result(_q("kind=entity"))
|
||||
assert (whole.total, whole.truncated) == (3, False)
|
||||
|
||||
|
||||
def test_sort_puts_missing_values_last():
|
||||
hits = [
|
||||
SearchHit(title="b", path="b", modified=None),
|
||||
@@ -240,18 +275,79 @@ def test_resolve_defaults_to_rg_and_rejects_unknown():
|
||||
# --- output -----------------------------------------------------------------
|
||||
|
||||
|
||||
def _result(hits, total=None, limit=DEFAULT_LIMIT):
|
||||
return SearchResult(hits=hits, total=total if total is not None else len(hits), limit=limit)
|
||||
|
||||
|
||||
def test_render_table_is_compact_and_reports_the_count():
|
||||
hit = SearchHit(title="aurora", path="kb/x.md", kind="entity", subtype="system",
|
||||
summary="Server hosting DocStore", score=8.0, matches=[Match(3, "DocStore")])
|
||||
out = render_table([hit], show_matches=False)
|
||||
out = render_table(_result([hit]), show_matches=False)
|
||||
assert "aurora" in out and "entity/system" in out
|
||||
assert "kb/x.md:3" not in out
|
||||
assert "1 result(s)." in out
|
||||
assert "kb/x.md:3" in render_table([hit], show_matches=True)
|
||||
assert "kb/x.md:3" in render_table(_result([hit]), show_matches=True)
|
||||
|
||||
|
||||
def test_render_table_says_so_when_nothing_matched():
|
||||
assert render_table([], show_matches=False) == "No matches."
|
||||
assert render_table(_result([]), show_matches=False) == "No matches."
|
||||
|
||||
|
||||
def test_every_hit_carries_the_path_so_the_page_can_be_opened():
|
||||
"""The reason this format changed: the instructions tell an agent to read
|
||||
the pages the search points at, and the table pointed nowhere. A session
|
||||
that needed filenames ran `grep -rl` over kb/ for them - a second search
|
||||
that can find no page this one missed."""
|
||||
hit = SearchHit(title="aurora", path="kb/entities/systems/aurora.md", kind="entity")
|
||||
assert "kb/entities/systems/aurora.md" in render_table(_result([hit]), show_matches=False)
|
||||
|
||||
|
||||
def test_title_and_path_are_never_truncated_only_the_summary_is():
|
||||
"""A clipped title is not an identifier. It is what `xref add`, `cite add`
|
||||
and `touch` take as an argument, and the old fixed 34-char column cut four
|
||||
of five hits in the report that prompted this."""
|
||||
title = "Source - Pelletofenkondensator und Verkabelung Recherche"
|
||||
path = f"kb/sources/llm-sessions/{title}.md"
|
||||
hit = SearchHit(title=title, path=path, kind="source", summary="x" * 400)
|
||||
line = render_table(_result([hit]), show_matches=False).splitlines()[0]
|
||||
|
||||
assert title in line
|
||||
assert path in line
|
||||
assert "…" in line, "the summary is still the one lossy field"
|
||||
|
||||
|
||||
def test_a_hit_line_parses_into_its_five_fields_even_with_prose_pipes():
|
||||
"""The separator has to survive a summary that contains one. It does,
|
||||
because the summary is last: a `|` there cannot shift a column. A `|` in a
|
||||
title is impossible - the wikilink syntax reserves it."""
|
||||
hit = SearchHit(title="aurora", path="kb/x.md", kind="entity", subtype="system",
|
||||
summary="Runs `a | b` nightly", score=8.0)
|
||||
line = render_table(_result([hit]), show_matches=False).splitlines()[0]
|
||||
score, kind, title, path, summary = line.split(" | ", 4)
|
||||
|
||||
assert (score, kind, title, path) == ("8.0", "entity/system", "aurora", "kb/x.md")
|
||||
assert summary == "Runs `a | b` nightly"
|
||||
|
||||
|
||||
def test_a_capped_result_says_what_it_capped():
|
||||
"""`20 result(s).` on a query matching 182 pages is a completeness claim
|
||||
the output was in no position to make, and the only way to find the real
|
||||
number was a second, unlimited search."""
|
||||
hits = [SearchHit(title=f"p{i}", path=f"kb/p{i}.md") for i in range(20)]
|
||||
out = render_table(_result(hits, total=182, limit=20), show_matches=False)
|
||||
|
||||
assert "20 of 182 result(s)" in out
|
||||
assert "--limit" in out
|
||||
|
||||
|
||||
def test_an_uncapped_result_claims_nothing_about_a_limit():
|
||||
hits = [SearchHit(title=f"p{i}", path=f"kb/p{i}.md") for i in range(3)]
|
||||
assert "3 result(s)." in render_table(_result(hits), show_matches=False)
|
||||
assert " of " not in render_table(_result(hits), show_matches=False)
|
||||
|
||||
|
||||
def test_limit_zero_never_counts_as_truncated_however_large_the_corpus():
|
||||
assert _result([SearchHit(title="a", path="a")] * 182, total=182, limit=0).truncated is False
|
||||
|
||||
|
||||
def test_hit_serialises_for_json():
|
||||
|
||||
@@ -150,9 +150,10 @@ def test_stale_regions_is_false_right_after_upsert():
|
||||
|
||||
|
||||
def test_target_files_matches_the_documented_scope():
|
||||
"""Integration check against the real repo: the scope is AGENTS.md, every
|
||||
stage contract, kb/CONVENTIONS.md, every COLLECTION.md, and the flat
|
||||
`instructions/**.md` form - never a `SKILL.md`."""
|
||||
"""Integration check against the real repo: every agent-loaded category
|
||||
AGENTS.md § File naming names - AGENTS.md itself, every stage contract,
|
||||
kb/CONVENTIONS.md, every COLLECTION.md, the flat `instructions/**.md` form,
|
||||
every type-spec, every `docs/` page - with `SKILL.md` the one exception."""
|
||||
from chemenu import config
|
||||
|
||||
files = toc.target_files()
|
||||
@@ -164,5 +165,35 @@ def test_target_files_matches_the_documented_scope():
|
||||
assert "types/type-spec.md" in relatives
|
||||
assert "instructions/CONTRACT.md" in relatives
|
||||
assert "instructions/dev/version-parts.md" in relatives # flat, still instructions/**.md
|
||||
assert "types/source.md" in relatives # a page type-spec, not only the stage contract
|
||||
assert "docs/version-model.md" in relatives
|
||||
assert not any(rel.endswith("SKILL.md") for rel in relatives)
|
||||
assert not any(rel.startswith("types/") and rel != "types/type-spec.md" for rel in relatives)
|
||||
|
||||
|
||||
def test_target_files_lists_type_spec_once_despite_two_sources():
|
||||
"""`types/type-spec.md` is both a stage contract and a `types/*.md` file;
|
||||
the set in `target_files` is what keeps that from being a special case."""
|
||||
from chemenu import config
|
||||
|
||||
relatives = [f.relative_to(config.ROOT).as_posix() for f in toc.target_files()]
|
||||
assert relatives.count("types/type-spec.md") == 1
|
||||
|
||||
|
||||
def test_strip_region_removes_what_types_describe_would_otherwise_echo():
|
||||
body = (
|
||||
"# A type\n\n"
|
||||
f"{blocks.open_marker(toc.REGION_NAME)}\n## Contents\n\n- [X](#x)\n"
|
||||
f"{blocks.close_marker(toc.REGION_NAME)}\n\n## X\n\nProse.\n"
|
||||
)
|
||||
stripped = toc.strip_region(body)
|
||||
assert toc.REGION_NAME not in stripped
|
||||
assert "## Contents" not in stripped
|
||||
assert "## X" in stripped and "Prose." in stripped
|
||||
# The heading keeps the blank line the region used to sit in, rather than
|
||||
# being welded to the line above it.
|
||||
assert "# A type\n\n## X" in stripped
|
||||
|
||||
|
||||
def test_strip_region_leaves_a_body_that_never_had_one_alone():
|
||||
body = "# A type\n\n## X\n\nProse.\n"
|
||||
assert toc.strip_region(body) == body
|
||||
|
||||
@@ -64,6 +64,54 @@ def test_get_page_ref_fields_defaults_to_empty():
|
||||
assert resolver.get_page_ref_fields("types/type-spec.md") == []
|
||||
|
||||
|
||||
def test_get_guidance_reads_the_linked_type_guidance_file():
|
||||
"""Gitea #104: a root:kb type-spec's generic authoring prose lives in a
|
||||
separate, stack-owned `<name>.guidance.md`, linked via `guidance:` -
|
||||
unlike `schema:`, this one's absence is the common case (an instance
|
||||
typed for itself), not an error."""
|
||||
guidance = resolver.get_guidance("types/entity.md")
|
||||
assert guidance is not None
|
||||
assert "When to use" in guidance or "When NOT to use" in guidance
|
||||
|
||||
|
||||
def test_get_guidance_is_none_when_the_type_spec_declares_none():
|
||||
"""`instruction` and `lint-report` describe stack artifacts and have
|
||||
never carried a `guidance:` field - this is the type with no linked
|
||||
guidance at all, not a broken link."""
|
||||
assert resolver.get_guidance("types/instruction.md") is None
|
||||
assert resolver.get_guidance("types/lint-report.md") is None
|
||||
|
||||
|
||||
def test_extract_template_reads_only_the_type_spec_never_the_guidance_file(tmp_path):
|
||||
"""`wikitool new` must keep exactly one load path for its scaffold - the
|
||||
first ```markdown block of `types/<name>.md` itself - even though the
|
||||
type-spec now optionally links a second file. A ```markdown block placed
|
||||
in the guidance file instead must never be picked up."""
|
||||
from chemenu.type_resolver import TypeResolver
|
||||
|
||||
types_dir = tmp_path / "types"
|
||||
types_dir.mkdir()
|
||||
(types_dir / "widget.md").write_text(
|
||||
# Self-referential `type:` (like the badtype fixture above), purely so
|
||||
# this narrow fixture needs no real `types/type-spec.md` on disk -
|
||||
# `extract_template` never reads the `type:` field at all.
|
||||
"---\ntype: types/widget.md\nname: widget\ndescription: A widget type.\n"
|
||||
"schema: null\nbase_dir: widgets\nguidance: types/widget.guidance.md\n---\n\n"
|
||||
"# Widget\n\n## Template\n\n```markdown\n# {name}\n```\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(types_dir / "widget.guidance.md").write_text(
|
||||
"---\ntype: types/type-guidance.md\nname: widget\ndescription: Guidance for widget.\n"
|
||||
"---\n\n# Widget Guidance\n\n```markdown\nTHIS MUST NEVER BE THE SCAFFOLD\n```\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
bad_resolver = TypeResolver(repo_root=tmp_path)
|
||||
type_spec = bad_resolver.load_type_spec("types/widget.md")
|
||||
template = bad_resolver.extract_template(type_spec)
|
||||
assert template == "# {name}"
|
||||
assert "THIS MUST NEVER BE THE SCAFFOLD" not in template
|
||||
|
||||
|
||||
def test_get_capture_fields_reads_the_type_spec():
|
||||
"""`fidelity`/`authority` are fixed once, at capture time (Gitea #67) -
|
||||
`raw accept`, `new source` and `touch` all read the field list from here
|
||||
@@ -221,6 +269,7 @@ def test_list_type_specs_finds_every_type_spec():
|
||||
names = {fm.get("name") for _, fm in resolver.list_type_specs()}
|
||||
assert names == {
|
||||
"type-spec", "entity", "concept", "source", "comparison", "lint-report", "instruction",
|
||||
"type-guidance",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ def test_types_list_finds_all_current_type_specs():
|
||||
names = {row["name"] for row in rows}
|
||||
assert names == {
|
||||
"type-spec", "entity", "concept", "source", "comparison", "lint-report", "instruction",
|
||||
"type-guidance",
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +53,42 @@ def test_types_describe_entity_reports_schema_and_body():
|
||||
assert "wikitool:footnotes" not in data["body"]
|
||||
|
||||
|
||||
def test_types_describe_entity_composes_guidance_and_body_separately():
|
||||
"""Gitea #104: the generic authoring prose (When to use / When NOT to
|
||||
use) now lives in the stack-owned `entity.guidance.md`, reported under
|
||||
its own JSON keys, while `body` stays exactly what it was - the
|
||||
instance-owned type-spec's own text (frontmatter table + template)."""
|
||||
result = runner.invoke(app, ["types", "describe", "entity", "--json"])
|
||||
assert result.exit_code == 0, result.output
|
||||
import json
|
||||
data = json.loads(result.output)
|
||||
assert data["guidance_path"] == "types/entity.guidance.md"
|
||||
assert data["guidance"] is not None
|
||||
assert "## When to use" in data["guidance"]
|
||||
assert "## When to use" not in data["body"]
|
||||
assert "## Kerndaten" in data["body"]
|
||||
|
||||
|
||||
def test_types_describe_composes_guidance_ahead_of_body_in_text_output():
|
||||
result = runner.invoke(app, ["types", "describe", "entity"])
|
||||
assert result.exit_code == 0, result.output
|
||||
guidance_at = result.output.index("## When to use")
|
||||
template_at = result.output.index("## Kerndaten")
|
||||
assert guidance_at < template_at
|
||||
|
||||
|
||||
def test_types_describe_a_type_with_no_guidance_omits_it_cleanly():
|
||||
"""`instruction` describes a stack artifact and has never carried a
|
||||
`guidance:` field - this must read exactly as it did before the split
|
||||
existed, not print an empty section."""
|
||||
result = runner.invoke(app, ["types", "describe", "instruction", "--json"])
|
||||
assert result.exit_code == 0, result.output
|
||||
import json
|
||||
data = json.loads(result.output)
|
||||
assert data["guidance"] is None
|
||||
assert data["guidance_path"] is None
|
||||
|
||||
|
||||
def test_types_describe_unknown_name_fails_cleanly():
|
||||
result = runner.invoke(app, ["types", "describe", "bogus"])
|
||||
assert result.exit_code != 0
|
||||
|
||||
@@ -444,6 +444,107 @@ def test_breaking_line_sits_above_the_bumps_region(tree):
|
||||
assert changes.index(version_mod.BREAKING_CHANGE_MARKER) < changes.index(version_mod._BUMPS_OPEN)
|
||||
|
||||
|
||||
# --- version bump: accumulating --breaking ----------------------------------
|
||||
|
||||
|
||||
def test_a_single_breaking_reason_stays_on_the_marker_line(tree):
|
||||
"""The shape every entry written before accumulation existed carries."""
|
||||
version_cmd.bump_command(
|
||||
major=True, minor=False, patch=False, title="Breaking",
|
||||
breaking="the feed moved", no_migration="kb/ untouched", migration_required=False,
|
||||
impact=None, dry_run=False,
|
||||
)
|
||||
changes = (tree / "CHANGES.md").read_text(encoding="utf-8")
|
||||
assert f"{version_mod.BREAKING_CHANGE_MARKER} the feed moved" in changes
|
||||
|
||||
|
||||
def test_a_second_breaking_reason_joins_the_first_instead_of_replacing_it(tree):
|
||||
"""The defect this exists for: the second --breaking used to delete the
|
||||
first, leaving the entry promising one break while shipping two."""
|
||||
version_cmd.bump_command(
|
||||
major=True, minor=False, patch=False, title="First crossing",
|
||||
breaking="the feed moved", no_migration="kb/ untouched", migration_required=False,
|
||||
impact=None, dry_run=False,
|
||||
)
|
||||
version_cmd.bump_command(
|
||||
major=False, minor=True, patch=False, title="Second crossing",
|
||||
breaking="the import name changed", no_migration=None, migration_required=False,
|
||||
impact=None, dry_run=False,
|
||||
)
|
||||
changes = (tree / "CHANGES.md").read_text(encoding="utf-8")
|
||||
section = version_mod.changes_section(changes, Version.parse("2.0.0-beta.2"))
|
||||
assert version_mod.breaking_reasons(section) == ["the feed moved", "the import name changed"]
|
||||
assert "- the feed moved" in changes and "- the import name changed" in changes
|
||||
# Still the line an operator acts on first, above a list that can run long.
|
||||
assert changes.index(version_mod.BREAKING_CHANGE_MARKER) < changes.index(version_mod._BUMPS_OPEN)
|
||||
|
||||
|
||||
def test_repeating_a_breaking_reason_verbatim_is_a_no_op(tree):
|
||||
"""So a re-run after an interrupted bump converges instead of writing the
|
||||
same sentence twice."""
|
||||
for title in ("First", "Second"):
|
||||
version_cmd.bump_command(
|
||||
major=True, minor=False, patch=False, title=title,
|
||||
breaking="the feed moved", no_migration="kb/ untouched", migration_required=False,
|
||||
impact=None, dry_run=False,
|
||||
)
|
||||
changes = (tree / "CHANGES.md").read_text(encoding="utf-8")
|
||||
section = version_mod.changes_section(changes, Version.parse("2.0.0-beta.2"))
|
||||
assert version_mod.breaking_reasons(section) == ["the feed moved"]
|
||||
|
||||
|
||||
def test_breaking_reasons_reads_a_pre_accumulation_single_line_back(tree):
|
||||
"""An entry written by a build that predates accumulation round-trips: it
|
||||
parses as one reason, and a later bump grows it to a list rather than
|
||||
rewriting what was already there."""
|
||||
section = "## 2.0.0-beta.1 - 2026-09-01 - T\n\n**Breaking Change:** the feed moved\n\n"
|
||||
assert version_mod.breaking_reasons(section) == ["the feed moved"]
|
||||
grown = version_mod._add_breaking_reason(section, "the import name changed")
|
||||
assert version_mod.breaking_reasons(grown) == ["the feed moved", "the import name changed"]
|
||||
assert f"{version_mod.BREAKING_CHANGE_MARKER}\n- the feed moved\n" in grown
|
||||
|
||||
|
||||
def test_no_migration_still_replaces_rather_than_accumulating(tree):
|
||||
"""It answers one yes/no question about the candidate as a whole, so a
|
||||
second answer supersedes the first - unlike --breaking beside it."""
|
||||
version_cmd.bump_command(
|
||||
major=True, minor=False, patch=False, title="First",
|
||||
breaking="the feed moved", no_migration="kb/ untouched", migration_required=False,
|
||||
impact=None, dry_run=False,
|
||||
)
|
||||
version_cmd.bump_command(
|
||||
major=False, minor=True, patch=False, title="Second",
|
||||
breaking=None, no_migration="still nothing to change", migration_required=False,
|
||||
impact=None, dry_run=False,
|
||||
)
|
||||
changes = (tree / "CHANGES.md").read_text(encoding="utf-8")
|
||||
assert changes.count(version_mod.MIGRATION_NONE_MARKER) == 1
|
||||
assert "kb/ untouched" not in changes
|
||||
assert "still nothing to change" in changes
|
||||
|
||||
|
||||
def test_retracting_no_migration_leaves_an_accumulated_breaking_list_intact(tree):
|
||||
version_cmd.bump_command(
|
||||
major=True, minor=False, patch=False, title="First",
|
||||
breaking="the feed moved", no_migration="kb/ untouched", migration_required=False,
|
||||
impact=None, dry_run=False,
|
||||
)
|
||||
version_cmd.bump_command(
|
||||
major=False, minor=True, patch=False, title="Second",
|
||||
breaking="the import name changed", no_migration=None, migration_required=False,
|
||||
impact=None, dry_run=False,
|
||||
)
|
||||
_migration_document(tree, "2.0.0")
|
||||
version_cmd.bump_command(
|
||||
major=False, minor=False, patch=True, title="Turns out it migrates",
|
||||
breaking=None, no_migration=None, migration_required=True, impact=None, dry_run=False,
|
||||
)
|
||||
changes = (tree / "CHANGES.md").read_text(encoding="utf-8")
|
||||
section = version_mod.changes_section(changes, Version.parse("2.0.0-beta.3"))
|
||||
assert version_mod.MIGRATION_NONE_MARKER not in changes
|
||||
assert version_mod.breaking_reasons(section) == ["the feed moved", "the import name changed"]
|
||||
|
||||
|
||||
# --- version regrade --------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
+84
-11
@@ -20,16 +20,49 @@ the stage/collection contracts, the flat `instructions/**.md` files) is a
|
||||
precedent first.
|
||||
|
||||
**Scope is computed, never a hand-picked list** - the same principle that
|
||||
governs `wikitool` itself. `target_files()` walks the file-naming categories
|
||||
AGENTS.md's own table calls agent-loaded reference material: `AGENTS.md`,
|
||||
every stage contract, `kb/CONVENTIONS.md`, every `kb/*/COLLECTION.md`, and
|
||||
every flat `instructions/**.md` file (a `SKILL.md` is excluded - it is loaded
|
||||
whole by the harness, not previewed at a second hop; `instructions/CONTRACT.md`
|
||||
§ "How much reasoning a step may carry" already treats a checklist read once
|
||||
as the table of contents it replaced). Human docs (`README.md`, `CHANGES.md`,
|
||||
`EVALS.md`, `INSTALL.md`, `tools/README.md`) are excluded too: the file-naming
|
||||
table says they are "Never loaded by an agent as instruction," so the preview
|
||||
mechanic this exists to mitigate does not apply to them.
|
||||
governs `wikitool` itself. `target_files()` walks every file-naming category
|
||||
AGENTS.md's own table calls agent-loaded: `AGENTS.md`, every stage contract,
|
||||
`kb/CONVENTIONS.md`, every `kb/*/COLLECTION.md`, every flat `instructions/**.md`
|
||||
file, every `types/*.md` type-spec, and every `docs/` page. One rule, and
|
||||
exactly one exception below it - which is the whole point, because a scope
|
||||
carrying several unexplained absences reads as an accident rather than a
|
||||
decision, and did: `docs/` and the page type-specs sat outside it for no
|
||||
recorded reason at all.
|
||||
|
||||
`docs/` belongs in for the reason the threshold exists. AGENTS.md § File naming
|
||||
calls it "Agents and humans | By link, or on explicit request" - agent-loaded,
|
||||
at a second hop, which is precisely the preview mechanic
|
||||
`instructions/CONTRACT.md` § "Reference depth" treats as real. The exclusion of
|
||||
the human docs (`README.md`, `CHANGES.md`, `EVALS.md`, `INSTALL.md`,
|
||||
`tools/README.md`) rests on a sentence that does not stretch to cover it: those
|
||||
are "Never loaded by an agent as instruction," while a `docs/` page is not
|
||||
loaded *as instruction* but is very much loaded.
|
||||
|
||||
**The one exception is `SKILL.md`**, and the vendored skill-authoring guidance
|
||||
is what puts it there rather than a judgment of ours. `commonplace/kb/work/
|
||||
skill-creator-distillation/sources/claude-code-skill-creator/SKILL.md:88-99`
|
||||
sets out three loading levels and places the SKILL.md body on the second - "In
|
||||
context whenever skill triggers (<500 lines ideal)" - while aiming its own TOC
|
||||
advice at the third, "large reference files (>300 lines)", i.e. bundled
|
||||
resources. The Codex source agrees by placement: its TOC bullet sits directly
|
||||
under "Keep references one level deep from SKILL.md. All reference files should
|
||||
link directly from SKILL.md" (`.../codex-skill-creator/SKILL.md:221-222`).
|
||||
Neither asks a skill body to carry a table of contents, because neither expects
|
||||
one to be previewed. `instructions/CONTRACT.md` § "How much reasoning a step
|
||||
may carry" arrives at the same place from the other side, treating a checklist
|
||||
read once as the table of contents it replaced. (Our longest skill is 254
|
||||
lines, so the <500 guidance costs us nothing either.)
|
||||
|
||||
A type-spec is loaded whole too - `tools/wikitool types describe` prints its
|
||||
entire authoring body - but it is *also* read as a file, by whoever edits it,
|
||||
and that is the reading the threshold is about. So it carries a region like any
|
||||
other reference file, and `types_cmd` strips the region back out of what
|
||||
`describe` prints: the command already hands over the whole body, so a
|
||||
navigation aid into it would be noise in the output and nothing else.
|
||||
|
||||
The threshold's own provenance is worth recording, because the two sources
|
||||
disagree: Codex says 100 lines, Claude Code says 300. This stack took the
|
||||
stricter number.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -80,7 +113,13 @@ def target_files() -> list[Path]:
|
||||
`instructions/<name>.md` form (`instructions/CONTRACT.md` says the `dev/`
|
||||
split is orthogonal to Linked/Manual, not a different file shape), so
|
||||
there is no separate rule for them to fall out of - and no hand-picking
|
||||
for a future file under either to be missed.
|
||||
for a future file under either to be missed. `types/` and `docs/` are
|
||||
walked for the same reason: a type or a rationale page added later is in
|
||||
scope by construction, not by someone remembering this function.
|
||||
|
||||
`types/type-spec.md` arrives twice - once as a stage contract, once from
|
||||
the `types/` walk - and the set at the bottom is what makes that a
|
||||
non-issue rather than something to special-case.
|
||||
"""
|
||||
files: list[Path] = [config.ROOT / "AGENTS.md"]
|
||||
files += [config.ROOT / rel for rel in _STAGE_CONTRACTS]
|
||||
@@ -96,6 +135,10 @@ def target_files() -> list[Path]:
|
||||
for path in instructions_dir.rglob("*.md")
|
||||
if path.name != "SKILL.md"
|
||||
)
|
||||
for subdir in ("types", "docs"):
|
||||
directory = config.ROOT / subdir
|
||||
if directory.is_dir():
|
||||
files += sorted(directory.rglob("*.md"))
|
||||
return sorted({f for f in files if f.is_file()})
|
||||
|
||||
|
||||
@@ -116,6 +159,36 @@ def body_without_region(text: str) -> str:
|
||||
return stripped.rstrip("\n") + "\n" if stripped else stripped
|
||||
|
||||
|
||||
_REGION_WITH_PADDING_RE = re.compile(
|
||||
r"\n*"
|
||||
+ re.escape(blocks.open_marker(REGION_NAME))
|
||||
+ r".*?"
|
||||
+ re.escape(blocks.close_marker(REGION_NAME))
|
||||
+ r"\n*",
|
||||
re.DOTALL,
|
||||
)
|
||||
|
||||
|
||||
def strip_region(text: str) -> str:
|
||||
"""`text` with the generated TOC region removed, for a caller that is
|
||||
handing the whole body over anyway.
|
||||
|
||||
`types describe` is the one such caller: it prints a type-spec's entire
|
||||
authoring body, so the region's markers and heading list would be noise in
|
||||
its output rather than a way into anything. Distinct from
|
||||
`body_without_region`, which exists to *measure* a body against the line
|
||||
threshold and therefore also canonicalizes the trailing newline.
|
||||
|
||||
Takes the surrounding blank lines with it and puts one back, rather than
|
||||
calling `blocks.strip`: that collapses the padding to a single newline,
|
||||
which is the right answer when the result is about to be rebuilt from
|
||||
scratch (`upsert` does exactly that) and the wrong one here, where the
|
||||
output is printed as-is - it would leave the following `##` heading welded
|
||||
to the paragraph above it.
|
||||
"""
|
||||
return _REGION_WITH_PADDING_RE.sub("\n\n", text)
|
||||
|
||||
|
||||
def needs_toc(text: str) -> bool:
|
||||
return len(body_without_region(text).splitlines()) > THRESHOLD
|
||||
|
||||
|
||||
@@ -436,6 +436,33 @@ class TypeResolver:
|
||||
type_spec = self.load_type_spec(type_path, source_file)
|
||||
return type_spec['frontmatter'].get('title_prefix') or ""
|
||||
|
||||
def get_guidance(self, type_path: str, source_file: Path = None) -> Optional[str]:
|
||||
"""Return the stack-owned guidance body a type-spec's `guidance:`
|
||||
field points to, or None if it declares none - the common case for a
|
||||
type an instance writes entirely for itself (Gitea #104).
|
||||
|
||||
The linked file is loaded as a type-spec-shaped document (`type:
|
||||
types/type-guidance.md`, `name:`, `description:`) the same way
|
||||
`schema:` is resolved, so a broken link or a malformed guidance file
|
||||
fails the same way a broken `schema:` would rather than silently
|
||||
returning nothing.
|
||||
|
||||
Args:
|
||||
type_path: The type path to resolve, e.g. 'types/entity.md'
|
||||
source_file: The source file path (for relative type resolution)
|
||||
|
||||
Raises:
|
||||
ValueError: If the type path cannot be resolved, or `guidance:`
|
||||
names a path that cannot be resolved or does not validate as
|
||||
a type-guidance document.
|
||||
"""
|
||||
type_spec = self.load_type_spec(type_path, source_file)
|
||||
guidance_path = type_spec['frontmatter'].get('guidance')
|
||||
if not guidance_path:
|
||||
return None
|
||||
guidance_spec = self.load_type_spec(guidance_path, type_spec['path'])
|
||||
return guidance_spec['body']
|
||||
|
||||
def get_page_ref_fields(self, type_path: str, source_file: Path = None) -> list:
|
||||
"""Return the frontmatter fields whose entries are wiki page titles
|
||||
(e.g. `['related', 'sources']` for an entity), as declared by the
|
||||
|
||||
@@ -59,6 +59,8 @@ def describe_type(name: str) -> Dict[str, Any]:
|
||||
type_spec = resolver.load_type_spec(type_path)
|
||||
frontmatter = type_spec["frontmatter"]
|
||||
schema = resolver.get_schema(type_path)
|
||||
guidance_path = frontmatter.get("guidance")
|
||||
guidance_body = resolver.get_guidance(type_path) if guidance_path else None
|
||||
|
||||
fields: list[Dict[str, Any]] = []
|
||||
if schema is not None:
|
||||
@@ -86,5 +88,10 @@ def describe_type(name: str) -> Dict[str, Any]:
|
||||
"base_dir": frontmatter.get("base_dir"),
|
||||
"title_prefix": frontmatter.get("title_prefix"),
|
||||
"fields": fields,
|
||||
# `body` stays the type-spec's own body, unchanged - additive fields
|
||||
# below it keep the MCP wire contract readable for an older client
|
||||
# that has never heard of the guidance split (Gitea #104).
|
||||
"body": type_spec["body"].strip(),
|
||||
"guidance_path": guidance_path,
|
||||
"guidance": guidance_body.strip() if guidance_body is not None else None,
|
||||
}
|
||||
|
||||
@@ -583,9 +583,11 @@ def regrade(text: str, version: "Version", updates: dict[int, str]) -> str:
|
||||
def _set_marker_line(section: str, marker: str, line: str) -> str:
|
||||
"""Add or replace the one-line `marker ...` paragraph in `section`.
|
||||
|
||||
Used for the breaking-change and no-migration lines, which - unlike the
|
||||
bumps list - are not accumulated: a later bump that repeats `--breaking`
|
||||
restates it rather than growing a list nobody would read as history.
|
||||
Used for the no-migration line, which - unlike the bumps list and unlike
|
||||
the breaking-change paragraph below - is **not** accumulated: it answers
|
||||
one yes/no question about the candidate as a whole ("does content have to
|
||||
change?"), so a second answer replaces the first rather than joining it,
|
||||
and `_clear_marker_line` is its retraction path.
|
||||
Anchored just above the bumps region (not below it, as before Gitea #95):
|
||||
with a graded, potentially 30-line list, the line an operator most needs
|
||||
to act on stayed the deepest thing in the entry otherwise.
|
||||
@@ -599,6 +601,73 @@ def _set_marker_line(section: str, marker: str, line: str) -> str:
|
||||
return section.rstrip() + f"\n\n{line}\n"
|
||||
|
||||
|
||||
# The breaking-change paragraph, matched whole: the marker line plus any `- `
|
||||
# bullets under it. `_set_marker_line`'s `^marker.*$` reaches the first line
|
||||
# only, which is exactly wrong for a form that can carry bullets beneath it.
|
||||
_BREAKING_PARAGRAPH_RE = re.compile(
|
||||
rf"^{re.escape(BREAKING_CHANGE_MARKER)}.*(?:\n-[ \t].*)*$", re.MULTILINE
|
||||
)
|
||||
|
||||
|
||||
def breaking_reasons(section: str) -> list[str]:
|
||||
"""The breaking-change paragraph parsed back into one reason per crossing,
|
||||
in written order.
|
||||
|
||||
Two shapes read the same way, which is what lets an entry written before
|
||||
accumulation existed round-trip untouched: `**Breaking Change:** <reason>`
|
||||
is one reason, and a bare `**Breaking Change:**` followed by `- ` bullets
|
||||
is one reason per bullet. Same "flat while there is only one of them"
|
||||
trick `_bumps_block` plays with its impact groups, and for the same
|
||||
reason - the common case keeps the shape it always had.
|
||||
"""
|
||||
match = _BREAKING_PARAGRAPH_RE.search(section)
|
||||
if not match:
|
||||
return []
|
||||
lines = match.group(0).splitlines()
|
||||
head = lines[0][len(BREAKING_CHANGE_MARKER):].strip()
|
||||
reasons = [head] if head else []
|
||||
reasons += [line.strip()[2:].strip() for line in lines[1:]]
|
||||
return [reason for reason in reasons if reason]
|
||||
|
||||
|
||||
def _breaking_paragraph(reasons: list[str]) -> str:
|
||||
"""Render the breaking-change paragraph from one reason per crossing.
|
||||
|
||||
One reason stays on the marker line - byte-identical to what every entry
|
||||
written before accumulation carries. Two or more move to bullets under a
|
||||
bare marker, because a single line holding two unrelated breakages reads
|
||||
as one run-on sentence and an operator has to act on each separately.
|
||||
"""
|
||||
if len(reasons) == 1:
|
||||
return f"{BREAKING_CHANGE_MARKER} {reasons[0]}"
|
||||
bullets = "\n".join(f"- {reason}" for reason in reasons)
|
||||
return f"{BREAKING_CHANGE_MARKER}\n{bullets}"
|
||||
|
||||
|
||||
def _add_breaking_reason(section: str, reason: str) -> str:
|
||||
"""Append `reason` to the breaking-change paragraph, or start one.
|
||||
|
||||
Accumulates rather than replaces: a candidate can cross the compatibility
|
||||
boundary more than once (this is the normal shape of a long-running one),
|
||||
and each crossing is a separate thing the operator of an existing instance
|
||||
has to act on. Replacing meant the second `--breaking` silently deleted
|
||||
the first - the entry then promised a single break while shipping two.
|
||||
|
||||
Repeating a reason verbatim is a no-op, so a re-run after an interrupted
|
||||
bump converges instead of writing the same sentence twice.
|
||||
"""
|
||||
existing = breaking_reasons(section)
|
||||
if reason in existing:
|
||||
return section
|
||||
paragraph = _breaking_paragraph(existing + [reason])
|
||||
if existing:
|
||||
return _BREAKING_PARAGRAPH_RE.sub(lambda _m: paragraph, section, count=1)
|
||||
anchor = section.find(_BUMPS_OPEN)
|
||||
if anchor != -1:
|
||||
return section[:anchor] + f"{paragraph}\n\n" + section[anchor:]
|
||||
return section.rstrip() + f"\n\n{paragraph}\n"
|
||||
|
||||
|
||||
def _clear_marker_line(section: str, marker: str) -> str:
|
||||
"""Remove the one-line `marker ...` paragraph from `section`, if present.
|
||||
|
||||
@@ -633,10 +702,15 @@ def _update_open_candidate(
|
||||
impact: str = DEFAULT_IMPACT,
|
||||
) -> str:
|
||||
"""Move the topmost entry's heading to `version`/`date`/`title`, append
|
||||
`(impact, title)` to its machine-managed bump list, and set the
|
||||
`(impact, title)` to its machine-managed bump list, and record the
|
||||
breaking/no-migration lines only where this call supplies them - see
|
||||
`insert_changes_entry`.
|
||||
|
||||
The two are recorded differently on purpose: a `breaking_reason` **joins**
|
||||
whatever crossings the candidate already recorded (`_add_breaking_reason`),
|
||||
a `no_migration_reason` **replaces** the single line that answers whether
|
||||
content has to change (`_set_marker_line`).
|
||||
|
||||
`migration_required` retracts an earlier `--no-migration` line instead of
|
||||
setting one - the two are mutually exclusive on a single bump, enforced by
|
||||
the caller (`version_cmd.bump_command`), not here."""
|
||||
@@ -653,7 +727,7 @@ def _update_open_candidate(
|
||||
)
|
||||
|
||||
if breaking_reason:
|
||||
section = _set_marker_line(section, BREAKING_CHANGE_MARKER, f"{BREAKING_CHANGE_MARKER} {breaking_reason}")
|
||||
section = _add_breaking_reason(section, breaking_reason)
|
||||
if no_migration_reason:
|
||||
section = _set_marker_line(section, MIGRATION_NONE_MARKER, f"{MIGRATION_NONE_MARKER} - {no_migration_reason}")
|
||||
elif migration_required:
|
||||
@@ -684,7 +758,9 @@ def insert_changes_entry(
|
||||
candidate still open" here.
|
||||
|
||||
A fresh entry gets the skeleton only: heading, date, author, - when a
|
||||
compatibility boundary is crossed - the line saying what breaks, plus the
|
||||
compatibility boundary is crossed - the line saying what breaks (one
|
||||
crossing, so the flat one-line form; a candidate that crosses again
|
||||
accumulates bullets there, see `_add_breaking_reason`), plus the
|
||||
line saying no content has to change where that applies, and then the
|
||||
machine-managed bump list (started with this one `(impact, title)` pair,
|
||||
for a candidate). The break comes first, above the bump list rather than
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
type: types/type-guidance.md
|
||||
name: comparison
|
||||
description: When to write a comparison page instead of a neighboring type, and how to write a conforming one - structure and analysis rules that hold regardless of what this instance is comparing
|
||||
---
|
||||
|
||||
# Comparison Guidance
|
||||
|
||||
`comparison` is the type for pages that set several entities, tools, technologies or approaches
|
||||
systematically against one another along fixed criteria. Comparison pages support decisions and
|
||||
make the trade-offs between alternatives visible.
|
||||
|
||||
## When to use
|
||||
|
||||
- Comparing several technologies or frameworks for a concrete purpose
|
||||
- Analysing the advantages and disadvantages of different tools or libraries
|
||||
- Evaluating alternative ways of solving a problem
|
||||
- Documenting a decision process with several options examined
|
||||
|
||||
## When NOT to use
|
||||
|
||||
- For documenting a single entity or concept (use `entity` or `concept` respectively)
|
||||
- For summaries of source material (use `source`)
|
||||
- For pages covering a single subject rather than comparing several
|
||||
|
||||
## Authoring guidance
|
||||
|
||||
- A title that names the comparison (e.g. "Go vs Rust", "Kubernetes vs Docker Swarm"); it follows the established names of the subjects compared, not the KB language (`kb/CONTRACT.md` § "Titles are identifiers", `kb/CONVENTIONS.md` §§ "Naming" and "Language")
|
||||
- State clearly what is being compared and why
|
||||
- Use a comparison table with the criteria as rows
|
||||
- An analysis that evaluates the table rather than repeating it
|
||||
- A recommendation wherever one can be justified
|
||||
|
||||
---
|
||||
|
||||
Relevant Notes:
|
||||
|
||||
- Comparison pages support technology selection and architecture decisions
|
||||
- The entities compared should all exist as wiki pages
|
||||
- The criteria should fit the decision at hand
|
||||
- A comparison stays focused on one concrete purpose or decision context
|
||||
+21
-43
@@ -1,50 +1,37 @@
|
||||
---
|
||||
type: types/type-spec.md
|
||||
name: comparison
|
||||
description: Strukturierter Typ für Vergleichsseiten, die mehrere Entities oder Ansätze gegenüberstellen
|
||||
description: Structured type for comparison pages that set several entities or approaches against one another
|
||||
schema: types/comparison.schema.yaml
|
||||
base_dir: comparisons
|
||||
page_ref_fields: [entities, related]
|
||||
guidance: types/comparison.guidance.md
|
||||
---
|
||||
|
||||
# Comparison
|
||||
|
||||
`comparison` ist der Typ für Seiten, die mehrere Entities, Werkzeuge, Technologien oder Ansätze systematisch anhand festgelegter Kriterien gegenüberstellen. Vergleichsseiten stützen Entscheidungen und machen Zielkonflikte zwischen Alternativen sichtbar.
|
||||
|
||||
## Wann zu verwenden
|
||||
|
||||
- Vergleich mehrerer Technologien oder Frameworks für einen konkreten Einsatzzweck
|
||||
- Analyse der Vor- und Nachteile verschiedener Werkzeuge oder Bibliotheken
|
||||
- Bewertung alternativer Lösungswege für ein Problem
|
||||
- Dokumentation eines Entscheidungsprozesses mit mehreren geprüften Optionen
|
||||
|
||||
## Wann NICHT zu verwenden
|
||||
|
||||
- Für die Dokumentation einer einzelnen Entity oder eines Concepts (dann `entity` bzw. `concept`)
|
||||
- Für Zusammenfassungen von Quellmaterial (dann `source`)
|
||||
- Für Seiten, die einen einzelnen Gegenstand behandeln statt mehrere zu vergleichen
|
||||
This instance's configuration for the `comparison` type: its frontmatter fields as this schema
|
||||
requires them, and the page skeleton `wikitool new comparison` scaffolds. When to use this type
|
||||
and how to write a conforming page is [types/comparison.guidance.md](comparison.guidance.md) -
|
||||
`tools/wikitool types describe comparison` composes both into one answer.
|
||||
|
||||
## Frontmatter
|
||||
|
||||
| Feld | Pflicht | Verwendung |
|
||||
| Field | Required | Use |
|
||||
|---|---:|---|
|
||||
| `type` | Ja | `types/comparison.md` |
|
||||
| `tags` | Nein | Navigations-Tags zur Kategorisierung |
|
||||
| `created` | Ja | Erstellungsdatum (YYYY-MM-DD) |
|
||||
| `entities` | Ja | Titel der verglichenen Entities |
|
||||
| `related` | Nein | Deklarierte ausgehende Kanten - je Subjekt eine `compares-with`-Kante, geschrieben von `wikitool xref add` |
|
||||
| `summary` | Ja | Einzeiler für `kb/index.md` |
|
||||
|
||||
## Autorenanweisungen
|
||||
|
||||
- Ein Titel, der den Vergleich benennt (z. B. "Go vs Rust", "Kubernetes vs Docker Swarm"); er folgt den etablierten Namen der verglichenen Gegenstände, nicht der KB-Sprache (`kb/CONTRACT.md` § "Titles are identifiers", `kb/CONVENTIONS.md` §§ "Naming" und "Language")
|
||||
- Klar darlegen, was verglichen wird und warum
|
||||
- Eine Vergleichstabelle mit den Kriterien als Zeilen verwenden
|
||||
- Eine Analyse, die die Tabelle auswertet statt sie zu wiederholen
|
||||
- Eine Empfehlung, wo eine begründbar ist
|
||||
| `type` | Yes | `types/comparison.md` |
|
||||
| `tags` | No | Navigation tags for categorization |
|
||||
| `created` | Yes | Creation date (YYYY-MM-DD) |
|
||||
| `entities` | Yes | Titles of the entities compared |
|
||||
| `related` | No | Declared outbound edges - one `compares-with` edge per subject, written by `wikitool xref add` |
|
||||
| `summary` | Yes | One-liner for `kb/index.md` |
|
||||
|
||||
## Template
|
||||
|
||||
The block below is page material, so it is written in this instance's KB language
|
||||
(`kb/CONVENTIONS.md` `language:`) rather than in the control plane's English - its headings
|
||||
become the headings of every page `wikitool new comparison` scaffolds.
|
||||
|
||||
```markdown
|
||||
# Comparison: {name}
|
||||
|
||||
@@ -67,16 +54,7 @@ TODO: Auswertung des Vergleichs. Welche Muster zeigen sich? Was sind die entsche
|
||||
TODO: Falls möglich - was wann und für wen zu verwenden ist. Unter welchen Umständen ist welche Option die beste?
|
||||
```
|
||||
|
||||
`# Comparison:` bleibt als Präfix stehen - anders als bei `source` ist es kein `title_prefix`,
|
||||
sondern reine Template-Konvention, und der Seitentitel selbst (`Go vs Rust`) trägt es nicht.
|
||||
Fügt `wikitool xref` eine Beziehung hinzu, entsteht der toolgeführte Querverweis-Abschnitt; wie
|
||||
er heißt, entscheidet die Instanz in `kb/CONVENTIONS.md` (`sections:`).
|
||||
|
||||
---
|
||||
|
||||
Ergänzende Hinweise:
|
||||
|
||||
- Vergleichsseiten stützen Technologieauswahl und Architekturentscheidungen
|
||||
- Die verglichenen Entities sollten alle als Wiki-Seiten existieren
|
||||
- Die Kriterien sollten zur anstehenden Entscheidung passen
|
||||
- Ein Vergleich bleibt auf einen konkreten Einsatzzweck oder Entscheidungskontext fokussiert
|
||||
`# Comparison:` stays as a prefix - unlike `source` it is no `title_prefix` but a pure template
|
||||
convention, and the page title itself (`Go vs Rust`) does not carry it. When `wikitool xref` adds
|
||||
a relationship, the tool-managed cross-reference section appears; what it is called is the
|
||||
instance's decision in `kb/CONVENTIONS.md` (`sections:`).
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
type: types/type-guidance.md
|
||||
name: concept
|
||||
description: When to write a concept page instead of a neighboring type, and how to write a conforming one - citation, linking and register rules that hold regardless of which concept_type values this instance chose
|
||||
---
|
||||
|
||||
# Concept Guidance
|
||||
|
||||
`concept` is the type for abstract ideas, methodologies, patterns and architectural decisions.
|
||||
Concepts provide the theoretical and methodological ground that entities implement or follow.
|
||||
|
||||
## When to use
|
||||
|
||||
- Documenting an architectural pattern or design approach
|
||||
- Explaining a design pattern or a programming methodology
|
||||
- Describing a communication protocol or standard
|
||||
- Recording a workflow or process that recurs across projects
|
||||
- Documenting an architectural decision (ADR) or design decision
|
||||
- Analysing a recurring problem and its solutions
|
||||
|
||||
## When NOT to use
|
||||
|
||||
- For concrete things such as projects, systems or tools (use `entity`)
|
||||
- For source documents or ingested material (use `source`)
|
||||
- For comparative analyses of concrete subjects (use `comparison`)
|
||||
|
||||
## Authoring guidance
|
||||
|
||||
- The title is the concept's canonical name and follows the established term of art, not the KB language (`kb/CONTRACT.md` § "Titles are identifiers", `kb/CONVENTIONS.md` §§ "Naming" and "Language")
|
||||
- Open with a clear definition: what the concept is
|
||||
- Give examples wherever they carry the understanding
|
||||
- Link to the entities that implement or use the concept
|
||||
- Under `provenance: sourced` or `mixed`, back hard facts inline with a `[^cite-id]` footnote -
|
||||
`tools/wikitool cite add --page "<Title>" --source "Source - X"` mints the id and its definition
|
||||
- Wikipedia register: factual, neutral, no marketing language
|
||||
|
||||
---
|
||||
|
||||
Relevant Notes:
|
||||
|
||||
- Concept pages carry the wiki's theoretical underpinning
|
||||
- Each concept type stands for a different category of abstract knowledge
|
||||
- Concepts should be linked to from the entities that implement or use them
|
||||
+23
-48
@@ -1,11 +1,12 @@
|
||||
---
|
||||
type: types/type-spec.md
|
||||
name: concept
|
||||
description: Basistyp für Concept-Seiten - Architekturen, Muster, Protokolle, Abläufe, Entscheidungen oder Problemstellungen
|
||||
description: Base type for concept pages - architectures, patterns, protocols, workflows, decisions or problems
|
||||
schema: types/concept.schema.yaml
|
||||
subtype_field: concept_type
|
||||
base_dir: concepts
|
||||
page_ref_fields: [related, sources]
|
||||
guidance: types/concept.guidance.md
|
||||
layout:
|
||||
architecture: {dir: architectures, title: Architekturen}
|
||||
pattern: {dir: patterns, title: Muster}
|
||||
@@ -17,49 +18,31 @@ layout:
|
||||
|
||||
# Concept
|
||||
|
||||
`concept` ist der Typ für abstrakte Ideen, Methodiken, Muster und Architekturentscheidungen. Concepts liefern die theoretische und methodische Grundlage, die Entities umsetzen oder befolgen.
|
||||
|
||||
## Wann zu verwenden
|
||||
|
||||
- Dokumentation eines Architekturmusters oder Entwurfsansatzes
|
||||
- Erläuterung eines Entwurfsmusters oder einer Programmiermethodik
|
||||
- Beschreibung eines Kommunikationsprotokolls oder Standards
|
||||
- Festhalten eines Ablaufs oder Prozesses, der über Projekte hinweg wiederkehrt
|
||||
- Dokumentation einer Architekturentscheidung (ADR) oder Entwurfsentscheidung
|
||||
- Analyse eines wiederkehrenden Problems und seiner Lösungen
|
||||
|
||||
## Wann NICHT zu verwenden
|
||||
|
||||
- Für konkrete Dinge wie Projekte, Systeme oder Werkzeuge (dann `entity`)
|
||||
- Für Quelldokumente oder eingelesenes Material (dann `source`)
|
||||
- Für vergleichende Analysen konkreter Gegenstände (dann `comparison`)
|
||||
This instance's configuration for the `concept` type: its frontmatter fields as this schema
|
||||
requires them, and the page skeleton `wikitool new concept` scaffolds. When to use this type and
|
||||
how to write a conforming page is [types/concept.guidance.md](concept.guidance.md) -
|
||||
`tools/wikitool types describe concept` composes both into one answer.
|
||||
|
||||
## Frontmatter
|
||||
|
||||
| Feld | Pflicht | Verwendung |
|
||||
| Field | Required | Use |
|
||||
|---|---:|---|
|
||||
| `type` | Ja | `types/concept.md` |
|
||||
| `concept_type` | Ja | Eines von: architecture, pattern, protocol, workflow, decision, problem |
|
||||
| `tags` | Nein | Navigations-Tags zur Kategorisierung |
|
||||
| `created` | Ja | Erstellungsdatum (YYYY-MM-DD) |
|
||||
| `modified` | Ja | Datum der letzten Änderung (YYYY-MM-DD) |
|
||||
| `related` | Nein | Titel verwandter Concepts und Entities |
|
||||
| `sources` | Nein | Titel der Source-Seiten, die die Aussagen dieser Seite stützen |
|
||||
| `provenance` | Ja | sourced, general oder mixed |
|
||||
| `summary` | Ja | Einzeiler für `kb/index.md` |
|
||||
|
||||
## Autorenanweisungen
|
||||
|
||||
- Der Titel ist der kanonische Name des Concepts und folgt der etablierten Fachbezeichnung, nicht der KB-Sprache (`kb/CONTRACT.md` § "Titles are identifiers", `kb/CONVENTIONS.md` §§ "Naming" und "Language")
|
||||
- Mit einer klaren Definition beginnen: was das Concept ist
|
||||
- Beispiele geben, wo sie das Verständnis tragen
|
||||
- Auf Entities verlinken, die das Concept umsetzen oder verwenden
|
||||
- Bei `provenance: sourced` oder `mixed` harte Fakten inline mit einer `[^cite-id]`-Fußnote belegen -
|
||||
`tools/wikitool cite add --page "<Title>" --source "Source - X"` erzeugt Id und Definition
|
||||
- Wikipedia-Tonfall: sachlich, neutral, keine Werbesprache
|
||||
| `type` | Yes | `types/concept.md` |
|
||||
| `concept_type` | Yes | One of: architecture, pattern, protocol, workflow, decision, problem |
|
||||
| `tags` | No | Navigation tags for categorization |
|
||||
| `created` | Yes | Creation date (YYYY-MM-DD) |
|
||||
| `modified` | Yes | Date last changed (YYYY-MM-DD) |
|
||||
| `related` | No | Titles of related concepts and entities |
|
||||
| `sources` | No | Titles of the source pages backing this page's claims |
|
||||
| `provenance` | Yes | sourced, general or mixed |
|
||||
| `summary` | Yes | One-liner for `kb/index.md` |
|
||||
|
||||
## Template
|
||||
|
||||
The block below is page material, so it is written in this instance's KB language
|
||||
(`kb/CONVENTIONS.md` `language:`) rather than in the control plane's English - its headings
|
||||
become the headings of every page `wikitool new concept` scaffolds.
|
||||
|
||||
```markdown
|
||||
# {name}
|
||||
|
||||
@@ -94,15 +77,7 @@ TODO: Anti-Muster, Warnungen oder Situationen, in denen es fehl am Platz ist
|
||||
- [[Related Concept 2]]
|
||||
```
|
||||
|
||||
Der Wert hinter `**Typ:**` bleibt der englische Enum-Wert - danach filtert `search --field`.
|
||||
Fügt `wikitool xref` eine Beziehung hinzu, entstehen zusätzlich die beiden toolgeführten
|
||||
Abschnitte für Beziehungen und Querverweise; wie sie heißen, entscheidet die Instanz in
|
||||
The value behind `**Typ:**` stays the English enum value - that is what `search --field` filters
|
||||
on. When `wikitool xref` adds a relationship, the two tool-managed sections for relationships and
|
||||
cross-references appear as well; what they are called is the instance's decision in
|
||||
`kb/CONVENTIONS.md` (`sections:`).
|
||||
|
||||
---
|
||||
|
||||
Ergänzende Hinweise:
|
||||
|
||||
- Concept-Seiten tragen den theoretischen Unterbau des Wikis
|
||||
- Jeder Concept-Typ steht für eine andere Kategorie abstrakten Wissens
|
||||
- Concepts sollten von den Entities aus verlinkt sein, die sie umsetzen oder verwenden
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
type: types/type-guidance.md
|
||||
name: entity
|
||||
description: When to write an entity page instead of a neighboring type, and how to write a conforming one - citation, linking and register rules that hold regardless of which entity_type values this instance chose
|
||||
---
|
||||
|
||||
# Entity Guidance
|
||||
|
||||
`entity` is the type for concrete things: projects, systems, tools, technologies or people.
|
||||
Entities are the primary building blocks of the knowledge graph.
|
||||
|
||||
## When to use
|
||||
|
||||
- Representing a software project, an initiative or a piece of work
|
||||
- Documenting a running system, service or infrastructure component
|
||||
- Describing a CLI tool, utility or software library
|
||||
- Recording information about a language, a framework or a protocol
|
||||
- Documenting a person, a team or an organization
|
||||
|
||||
## When NOT to use
|
||||
|
||||
- For abstract concepts, patterns or methodologies (use `concept`)
|
||||
- For source documents or ingested material (use `source`)
|
||||
- For comparative analyses (use `comparison`)
|
||||
|
||||
## Authoring guidance
|
||||
|
||||
- The title is the entity's canonical name and follows the subject's established designation, not the KB language (`kb/CONTRACT.md` § "Titles are identifiers", `kb/CONVENTIONS.md` §§ "Naming" and "Language")
|
||||
- The main description goes near the top
|
||||
- Link to related entities and concepts wherever relationships exist
|
||||
- Under `provenance: sourced` or `mixed`, back hard facts inline with a `[^cite-id]` footnote -
|
||||
`tools/wikitool cite add --page "<Title>" --source "Source - X"` mints the id and its definition
|
||||
- Wikipedia register: factual, neutral, no marketing language
|
||||
|
||||
---
|
||||
|
||||
Relevant Notes:
|
||||
|
||||
- Entity pages form the concrete inventory of the knowledge base
|
||||
- Every entity type (project, system, tool and so on) is a variant of this base type
|
||||
- The relationships between entities carry the knowledge graph's interconnection
|
||||
+24
-47
@@ -1,11 +1,12 @@
|
||||
---
|
||||
type: types/type-spec.md
|
||||
name: entity
|
||||
description: Basistyp für Entity-Seiten - Projekte, Systeme, Werkzeuge, Technologien oder Personen
|
||||
description: Base type for entity pages - projects, systems, tools, technologies or people
|
||||
schema: types/entity.schema.yaml
|
||||
subtype_field: entity_type
|
||||
base_dir: entities
|
||||
page_ref_fields: [related, sources]
|
||||
guidance: types/entity.guidance.md
|
||||
layout:
|
||||
project: {dir: projects, title: Projekte}
|
||||
system: {dir: systems, title: Systeme}
|
||||
@@ -16,47 +17,31 @@ layout:
|
||||
|
||||
# Entity
|
||||
|
||||
`entity` ist der Typ für konkrete Dinge: Projekte, Systeme, Werkzeuge, Technologien oder Personen. Entities sind die primären Bausteine des Wissensgraphen.
|
||||
|
||||
## Wann zu verwenden
|
||||
|
||||
- Darstellung eines Softwareprojekts, einer Initiative oder eines Arbeitsvorhabens
|
||||
- Dokumentation eines betriebenen Systems, Dienstes oder einer Infrastrukturkomponente
|
||||
- Beschreibung eines CLI-Werkzeugs, Hilfsprogramms oder einer Programmbibliothek
|
||||
- Festhalten von Informationen zu einer Sprache, einem Framework oder einem Protokoll
|
||||
- Dokumentation einer Person, eines Teams oder einer Organisation
|
||||
|
||||
## Wann NICHT zu verwenden
|
||||
|
||||
- Für abstrakte Konzepte, Muster oder Methodiken (dann `concept`)
|
||||
- Für Quelldokumente oder eingelesenes Material (dann `source`)
|
||||
- Für vergleichende Analysen (dann `comparison`)
|
||||
This instance's configuration for the `entity` type: its frontmatter fields as this schema
|
||||
requires them, and the page skeleton `wikitool new entity` scaffolds. When to use this type and
|
||||
how to write a conforming page is [types/entity.guidance.md](entity.guidance.md) -
|
||||
`tools/wikitool types describe entity` composes both into one answer.
|
||||
|
||||
## Frontmatter
|
||||
|
||||
| Feld | Pflicht | Verwendung |
|
||||
| Field | Required | Use |
|
||||
|---|---:|---|
|
||||
| `type` | Ja | `types/entity.md` |
|
||||
| `entity_type` | Ja | Eines von: project, system, tool, technology, person |
|
||||
| `tags` | Nein | Navigations-Tags zur Kategorisierung |
|
||||
| `created` | Ja | Erstellungsdatum (YYYY-MM-DD) |
|
||||
| `modified` | Ja | Datum der letzten Änderung (YYYY-MM-DD) |
|
||||
| `related` | Nein | Titel verwandter Seiten (Entities, Concepts, Sources) |
|
||||
| `sources` | Nein | Titel der Source-Seiten, die die Aussagen dieser Seite stützen |
|
||||
| `provenance` | Ja | sourced, general oder mixed |
|
||||
| `summary` | Ja | Einzeiler für `kb/index.md` |
|
||||
|
||||
## Autorenanweisungen
|
||||
|
||||
- Der Titel ist der kanonische Name der Entity und folgt der etablierten Bezeichnung des Gegenstands, nicht der KB-Sprache (`kb/CONTRACT.md` § "Titles are identifiers", `kb/CONVENTIONS.md` §§ "Naming" und "Language")
|
||||
- Die Hauptbeschreibung steht weit oben
|
||||
- Auf verwandte Entities und Concepts verlinken, wo Beziehungen bestehen
|
||||
- Bei `provenance: sourced` oder `mixed` harte Fakten inline mit einer `[^cite-id]`-Fußnote belegen -
|
||||
`tools/wikitool cite add --page "<Title>" --source "Source - X"` erzeugt Id und Definition
|
||||
- Wikipedia-Tonfall: sachlich, neutral, keine Werbesprache
|
||||
| `type` | Yes | `types/entity.md` |
|
||||
| `entity_type` | Yes | One of: project, system, tool, technology, person |
|
||||
| `tags` | No | Navigation tags for categorization |
|
||||
| `created` | Yes | Creation date (YYYY-MM-DD) |
|
||||
| `modified` | Yes | Date last changed (YYYY-MM-DD) |
|
||||
| `related` | No | Titles of related pages (entities, concepts, sources) |
|
||||
| `sources` | No | Titles of the source pages backing this page's claims |
|
||||
| `provenance` | Yes | sourced, general or mixed |
|
||||
| `summary` | Yes | One-liner for `kb/index.md` |
|
||||
|
||||
## Template
|
||||
|
||||
The block below is page material, so it is written in this instance's KB language
|
||||
(`kb/CONVENTIONS.md` `language:`) rather than in the control plane's English - its headings
|
||||
become the headings of every page `wikitool new entity` scaffolds.
|
||||
|
||||
```markdown
|
||||
# {name}
|
||||
|
||||
@@ -84,15 +69,7 @@ TODO: Ausführliche Informationen, nach sinnvollen Abschnitten gegliedert
|
||||
- [{today}] - Page created via wikitool
|
||||
```
|
||||
|
||||
Der Beziehungsabschnitt steht bewusst **nicht** im Template: er ist eine generierte Region, die
|
||||
`wikitool xref` beim ersten Kanteneintrag zwischen Markern anlegt und aus `related:` neu
|
||||
rendert. Ein Autor schreibt dort nie hinein. Der Wert hinter `**Typ:**` bleibt der englische
|
||||
Enum-Wert - danach filtert `search --field`.
|
||||
|
||||
---
|
||||
|
||||
Ergänzende Hinweise:
|
||||
|
||||
- Entity-Seiten bilden den konkreten Bestand der Wissensbasis
|
||||
- Jeder Entity-Typ (project, system, tool usw.) ist eine Ausprägung dieses Basistyps
|
||||
- Die Beziehungen zwischen Entities tragen die Vernetzung des Wissensgraphen
|
||||
The relationships section is deliberately **not** in the template: it is a generated region that
|
||||
`wikitool xref` creates between markers on the first edge entry and re-renders from `related:`.
|
||||
An author never writes into it. The value behind `**Typ:**` stays the English enum value - that is
|
||||
what `search --field` filters on.
|
||||
|
||||
@@ -65,4 +65,4 @@ Relevant Notes:
|
||||
- This type declares no `page_ref_fields:`: a report references pages in prose
|
||||
only, never through frontmatter.
|
||||
- It also declares no `base_dir:`, which is what keeps `reports/` out of `kb/`
|
||||
and out of the index.
|
||||
and out of the index.
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
type: types/type-guidance.md
|
||||
name: source
|
||||
description: When to write a source page instead of a neighboring type, and how to write a conforming one - capture-field, provenance and not-extracted rules that hold regardless of which source_type values this instance chose
|
||||
---
|
||||
|
||||
# Source Guidance
|
||||
|
||||
`source` is the type for pages that summarize and catalogue ingested raw material. Source pages
|
||||
are the bridge between the `raw/` layer (immutable source files) and the `kb/` layer (compiled
|
||||
knowledge). One source page stands for **one logical source**, which may span several raw files.
|
||||
|
||||
## When to use
|
||||
|
||||
- Summarizing a single external article, document or specification
|
||||
- Recording several related notes or meeting records as one source
|
||||
- Documenting an ingested PDF, manual or other document
|
||||
- Capturing information about an image or a diagram
|
||||
|
||||
## When NOT to use
|
||||
|
||||
- For compiled knowledge (use `entity` or `concept`)
|
||||
- For comparative analyses (use `comparison`)
|
||||
- For original wiki content not derived from raw material
|
||||
|
||||
## Authoring guidance
|
||||
|
||||
- `source_type` has **no default** - `wikitool new source` refuses without an explicit value. Where the category is unclear, set `unclassified` rather than guessing; that is a visible catalog slot with an advisory `lint` finding, not a dumping ground. What separates `analysis` from `document`, and which area holds which value, is a call this instance's own collection contract makes - see `kb/sources/COLLECTION.md`
|
||||
- `raw_files` lists every raw file this source covers (one source page per logical source, not per file)
|
||||
- `fidelity` and `authority` are **capture fields** (`capture_fields:` on this type): recorded at the drop point and not freely changeable afterwards. `wikitool raw accept --fidelity <value> --authority <value>` refuses without both; without `--page` it instead prints the ready-made `new source --set fidelity=... --set authority=...` line, which `new source` in turn refuses without both values. `wikitool touch --set fidelity=<value>` only writes while the field is absent - where a value already stands, it refuses and points at `raw accept --replaces` as the one correction path (a corrected capture is a new edition, not an edit). `unknown` is backfill-only: only `wikitool touch` may write it, never `raw accept` or `new source` - the same construction as `source_language` uses for pages that predate this rule. What the values mean and how they differ: [raw/CONTRACT.md](../raw/CONTRACT.md#getting-a-file-in-incoming)
|
||||
- For external articles, always set `source_url` to the origin URL
|
||||
- Set `source_language` to the raw material's language, not the page's
|
||||
- The page is written in the KB language, whatever language the source is in; verbatim passages are quoted in the original (`kb/CONVENTIONS.md` § "Language")
|
||||
- Summarize the key claims in the summary section
|
||||
- Put anything actionable in the action items section
|
||||
- Put deliberate omissions in the not-extracted section - see below
|
||||
- Link the entities and concepts mentioned under related entities/concepts
|
||||
|
||||
## Not Extracted
|
||||
|
||||
The decision that material should *not* be taken over cannot be reconstructed: nothing in the
|
||||
repository can re-derive it, and `sources coverage` only knows whether a raw file is claimed by
|
||||
some source page - never whether anyone decided about its contents. Left unwritten, the same
|
||||
source is renegotiated on every later pass.
|
||||
|
||||
- Record every deliberate omission with a **reason**, not just a filename.
|
||||
- Mandatory where the ingest ran through `instructions/ingest-large-tree.md` - on both axes: for
|
||||
a tree ingest the section records what was not taken from the tree; for a thematically broad
|
||||
single source, which named subjects got no page of their own, and why. Optional for a single
|
||||
small file - but an empty section still beats a missing one.
|
||||
- Belongs on the source page, not in `kb/log.md`: it is a statement about *this* source, and the
|
||||
log is chronological rather than per-source.
|
||||
|
||||
---
|
||||
|
||||
Relevant Notes:
|
||||
|
||||
- Source pages are the authoritative catalogue of what raw material has been ingested
|
||||
- **One raw file, one owner.** A raw file appears in exactly one `raw_files:` - that page is responsible for keeping it summarized. Any number of pages may **cite** it via `[^cite-id]`; a citation is reuse, `raw_files:` is a maintenance responsibility. With two claimants it is undefined which page has to be brought up to date when the raw file changes - and then both rot quietly
|
||||
- Source pages make knowledge traceable back to the original raw material
|
||||
- `raw_files:` holds concrete existing file paths, never directories
|
||||
- A `raw_files:` list beyond roughly 15 entries indicates the cut was too coarse - the source should have been split into several source pages via `instructions/ingest-large-tree.md`
|
||||
- `entities:` plus `concepts:` beyond roughly 20 entries is the counterpart on the other axis: not cut too little, but compiled too much at once. Such a source is not split - a raw file has one owner - it needed the extract pass from `instructions/ingest-large-tree.md` § "A broad source is not cut", so that not every named subject gets a page
|
||||
+30
-78
@@ -1,13 +1,14 @@
|
||||
---
|
||||
type: types/type-spec.md
|
||||
name: source
|
||||
description: Strukturierter Typ für Source-Seiten, die eingelesenes Rohmaterial erfassen und zusammenfassen
|
||||
description: Structured type for source pages that record and summarize ingested raw material
|
||||
schema: types/source.schema.yaml
|
||||
subtype_field: source_type
|
||||
base_dir: sources
|
||||
title_prefix: "Source - "
|
||||
page_ref_fields: [entities, concepts]
|
||||
capture_fields: [fidelity, authority]
|
||||
guidance: types/source.guidance.md
|
||||
layout:
|
||||
transcript: {dir: transcripts, title: Transkripte}
|
||||
analysis: {dir: analyses, title: Analysen}
|
||||
@@ -20,71 +21,39 @@ layout:
|
||||
|
||||
# Source
|
||||
|
||||
`source` ist der Typ für Seiten, die eingelesenes Rohmaterial zusammenfassen und katalogisieren. Source-Seiten sind die Brücke zwischen der `raw/`-Schicht (unveränderliche Quelldateien) und der `kb/`-Schicht (kompiliertes Wissen). Eine Source-Seite steht für **eine logische Quelle**, die mehrere Raw-Dateien umfassen kann.
|
||||
|
||||
## Wann zu verwenden
|
||||
|
||||
- Zusammenfassung eines einzelnen externen Artikels, Dokuments oder einer Spezifikation
|
||||
- Erfassung mehrerer zusammengehöriger Notizen oder Gesprächsprotokolle als eine Quelle
|
||||
- Dokumentation eines eingelesenen PDFs, Handbuchs oder sonstigen Dokuments
|
||||
- Festhalten von Informationen zu einem Bild oder Diagramm
|
||||
|
||||
## Wann NICHT zu verwenden
|
||||
|
||||
- Für kompiliertes Wissen (dann `entity` oder `concept`)
|
||||
- Für vergleichende Analysen (dann `comparison`)
|
||||
- Für originären Wiki-Inhalt, der nicht aus Rohmaterial abgeleitet ist
|
||||
This instance's configuration for the `source` type: its frontmatter fields as this schema
|
||||
requires them, and the page skeleton `wikitool new source` scaffolds. When to use this type and
|
||||
how to write a conforming page is [types/source.guidance.md](source.guidance.md) -
|
||||
`tools/wikitool types describe source` composes both into one answer.
|
||||
|
||||
## Frontmatter
|
||||
|
||||
| Feld | Pflicht | Verwendung |
|
||||
| Field | Required | Use |
|
||||
|---|---:|---|
|
||||
| `type` | Ja | `types/source.md` |
|
||||
| `source_type` | Ja | Eines von: transcript, analysis, article, document, notes, tracker, unclassified - kein Default, siehe unten |
|
||||
| `author` | Ja | Urheber des Quellmaterials |
|
||||
| `raw_files` | Ja | Raw-Dateipfade, die diese Quelle **besitzt** - siehe "Eine Raw-Datei, ein Besitzer" unten |
|
||||
| `fidelity` | Ja (im Werkzeug, nicht im Schema) | Wie treu die *Erfassung* ist: `verbatim`, `published`, `secondhand`, `nontextual` - Capture-Feld, siehe unten |
|
||||
| `authority` | Ja (im Werkzeug, nicht im Schema) | Was das Material über seinen *Gegenstand* behaupten darf: `normative`, `reporting`, `opinion` - Capture-Feld, siehe unten |
|
||||
| `source_url` | Nein | Ursprungs-URL bei externen Quellen |
|
||||
| `source_language` | Nein | ISO-639-1-Code der Sprache des Rohmaterials, z. B. `de`, `en`, `fr` |
|
||||
| `date` | Ja | Veröffentlichungs- oder Erstellungsdatum (YYYY-MM-DD) |
|
||||
| `tags` | Nein | Navigations-Tags zur Kategorisierung |
|
||||
| `entities` | Nein | Titel der in dieser Quelle erwähnten Entities |
|
||||
| `concepts` | Nein | Titel der in dieser Quelle erwähnten Concepts |
|
||||
| `summary` | Ja | Einzeiler für `kb/index.md` |
|
||||
| `type` | Yes | `types/source.md` |
|
||||
| `source_type` | Yes | One of: transcript, analysis, article, document, notes, tracker, unclassified - no default, see below |
|
||||
| `author` | Yes | Originator of the source material |
|
||||
| `raw_files` | Yes | Raw file paths this source **owns** - see "One raw file, one owner" below |
|
||||
| `fidelity` | Yes (in the tool, not in the schema) | How faithful the *capture* is: `verbatim`, `published`, `secondhand`, `nontextual` - a capture field, see below |
|
||||
| `authority` | Yes (in the tool, not in the schema) | What the material may claim about its *subject*: `normative`, `reporting`, `opinion` - a capture field, see below |
|
||||
| `source_url` | No | Origin URL for external sources |
|
||||
| `source_language` | No | ISO 639-1 code of the raw material's language, e.g. `de`, `en`, `fr` |
|
||||
| `date` | Yes | Publication or creation date (YYYY-MM-DD) |
|
||||
| `tags` | No | Navigation tags for categorization |
|
||||
| `entities` | No | Titles of the entities mentioned in this source |
|
||||
| `concepts` | No | Titles of the concepts mentioned in this source |
|
||||
| `summary` | Yes | One-liner for `kb/index.md` |
|
||||
|
||||
## Autorenanweisungen
|
||||
## Authoring guidance
|
||||
|
||||
- Der Titel beginnt mit "Source - ", gefolgt vom Namen der Quelle
|
||||
- `source_type` hat **keinen Default** - `wikitool new source` verweigert ohne einen expliziten Wert. Ist die Kategorie unklar, `unclassified` setzen statt zu raten; das ist ein sichtbares Katalogfach mit beratendem `lint`-Befund, kein Sammelbecken. Was `analysis` von `document` trennt und welche Area welchen Wert hält: [kb/sources/COLLECTION.md](../kb/sources/COLLECTION.md)
|
||||
- `raw_files` listet jede Raw-Datei, die diese Quelle abdeckt (eine Source-Seite pro logischer Quelle, nicht pro Datei)
|
||||
- `fidelity` und `authority` sind **Capture-Felder** (`capture_fields:` oben): am Drop-Punkt erhoben, danach nicht mehr frei änderbar. `wikitool raw accept --fidelity <wert> --authority <wert>` verweigert ohne beide; ohne `--page` druckt es stattdessen die fertige `new source --set fidelity=... --set authority=...`-Zeile, die `new source` seinerseits ohne beide Werte verweigert. `wikitool touch --set fidelity=<wert>` schreibt nur, solange das Feld fehlt - steht bereits ein Wert, verweigert es und verweist auf `raw accept --replaces` als einzigen Korrekturweg (eine korrigierte Erfassung ist eine neue Edition, keine Bearbeitung). `unknown` ist backfill-only: nur `wikitool touch` darf es schreiben, nie `raw accept` oder `new source` - dieselbe Konstruktion wie bei `source_language` für Seiten, die vor dieser Regel entstanden sind. Was die Werte bedeuten und wie sie sich unterscheiden: [raw/CONTRACT.md](../raw/CONTRACT.md#getting-a-file-in-incoming)
|
||||
- Bei externen Artikeln immer `source_url` auf die Ursprungs-URL setzen
|
||||
- `source_language` auf die Sprache des Rohmaterials setzen, nicht auf die der Seite
|
||||
- Die Seite wird in der KB-Sprache geschrieben, unabhängig von der Sprache der Quelle; wörtliche Passagen werden im Original zitiert (`kb/CONVENTIONS.md` § "Language")
|
||||
- Kernaussagen im Abschnitt Summary zusammenfassen
|
||||
- Handlungsbedarf in den Abschnitt Action Items
|
||||
- Bewusst Weggelassenes in den Abschnitt Not Extracted - siehe unten
|
||||
- Erwähnte Entities und Concepts unter Related Entities/Concepts verlinken
|
||||
|
||||
## Not Extracted
|
||||
|
||||
Die Entscheidung, dass Material *nicht* übernommen werden soll, ist nicht rekonstruierbar: nichts
|
||||
im Repository kann sie neu herleiten, und `sources coverage` weiß nur, ob eine Raw-Datei von
|
||||
irgendeiner Source-Seite beansprucht wird - nie, ob jemand über ihren Inhalt entschieden hat.
|
||||
Bleibt das unaufgeschrieben, wird dieselbe Quelle bei jedem späteren Durchgang neu verhandelt.
|
||||
|
||||
- Jede bewusste Auslassung mit **Begründung** festhalten, nicht nur mit Dateinamen.
|
||||
- Pflicht, wenn der Ingest über `instructions/ingest-large-tree.md` lief - auf beiden Achsen:
|
||||
beim Tree-Ingest hält der Abschnitt fest, was aus dem Baum nicht übernommen wurde, bei einer
|
||||
thematisch breiten Einzelquelle, welche genannten Gegenstände keine eigene Seite bekommen
|
||||
haben und warum. Optional bei einer einzelnen kleinen Datei - aber ein leerer Abschnitt ist
|
||||
immer noch besser als ein fehlender.
|
||||
- Gehört auf die Source-Seite, nicht in `kb/log.md`: es ist eine Aussage über *diese* Quelle,
|
||||
und das Log ist chronologisch, nicht quellenbezogen.
|
||||
- The title starts with "Source - ", followed by the name of the source
|
||||
|
||||
## Template
|
||||
|
||||
The block below is page material, so it is written in this instance's KB language
|
||||
(`kb/CONVENTIONS.md` `language:`) rather than in the control plane's English - its headings
|
||||
become the headings of every page `wikitool new source` scaffolds.
|
||||
|
||||
```markdown
|
||||
# Source: {name}
|
||||
|
||||
@@ -121,24 +90,7 @@ TODO: 2-3 Absätze zu den Kernaussagen des Quellmaterials.
|
||||
{concepts|bullets}
|
||||
```
|
||||
|
||||
`# Source:` bleibt als Präfix stehen - es spiegelt den `title_prefix` und damit den Titel, unter
|
||||
dem die Seite verlinkt und zitiert wird. Der Wert hinter `**Typ:**` bleibt der englische
|
||||
Enum-Wert. Fügt `wikitool cite` ein Zitat hinzu, entsteht am Seitenende der toolgeführte
|
||||
Fußnoten-Block; wie er heißt, entscheidet die Instanz in `kb/CONVENTIONS.md` (`sections:`).
|
||||
|
||||
---
|
||||
|
||||
Ergänzende Hinweise:
|
||||
|
||||
- Source-Seiten sind der maßgebliche Katalog dessen, was an Rohmaterial eingelesen wurde
|
||||
- **Eine Raw-Datei, ein Besitzer.** Eine Raw-Datei steht in genau einem `raw_files:` - diese Seite ist dafür verantwortlich, sie zusammengefasst zu halten. Beliebig viele Seiten dürfen sie per `[^cite-id]` **zitieren**; ein Zitat ist Wiederverwendung, `raw_files:` ist eine Wartungszuständigkeit. Bei zwei Anspruchstellern ist undefiniert, welche Seite bei einer Änderung der Raw-Datei nachgezogen werden muss - dann verrotten beide still
|
||||
- Source-Seiten machen Wissen bis zum ursprünglichen Rohmaterial rückverfolgbar
|
||||
- `raw_files:` enthält konkrete existierende Dateipfade, nie Verzeichnisse
|
||||
- Eine `raw_files:`-Liste jenseits von etwa 15 Einträgen zeigt an, dass der Schnitt zu grob war -
|
||||
die Quelle hätte per `instructions/ingest-large-tree.md` in mehrere Source-Seiten geteilt
|
||||
werden müssen
|
||||
- `entities:` plus `concepts:` jenseits von etwa 20 Einträgen ist das Gegenstück auf der anderen
|
||||
Achse: nicht zu wenig geschnitten, sondern zu viel auf einmal kompiliert. Geteilt wird eine
|
||||
solche Quelle nicht - eine Raw-Datei hat einen Besitzer -, sie hätte den Extract-Pass aus
|
||||
`instructions/ingest-large-tree.md` § "A broad source is not cut" gebraucht, damit nicht jeder
|
||||
genannte Gegenstand eine Seite bekommt
|
||||
`# Source:` stays as a prefix - it mirrors the `title_prefix` and with it the title the page is
|
||||
linked and cited under. The value behind `**Typ:**` stays the English enum value. When
|
||||
`wikitool cite` adds a citation, the tool-managed footnote block appears at the end of the page;
|
||||
what it is called is the instance's decision in `kb/CONVENTIONS.md` (`sections:`).
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
type: types/type-spec.md
|
||||
name: type-guidance
|
||||
description: "Stack-owned authoring prose for a root:kb page type - the machine-independent half a type-spec's optional guidance: field links to"
|
||||
schema: types/type-guidance.schema.yaml
|
||||
---
|
||||
|
||||
# Type Guidance
|
||||
|
||||
`type-guidance` describes the shape of one `types/<name>.guidance.md` file: the stack-owned half
|
||||
of a `root: kb` page type's contract. A page type-spec ([types/type-spec.md](type-spec.md) §
|
||||
"Who owns a type-spec") splits at the audience line - what an agent needs to know to write a
|
||||
conforming page, versus what this instance's schema and template concretely are - and a guidance
|
||||
file is the machine-independent half of that split, linked from the type-spec it documents via
|
||||
that file's own `guidance:` frontmatter field.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Writing or improving the generic authoring rules for a `root: kb` type: when to choose it, when
|
||||
not to, and mechanism-level advice that holds regardless of which enum values, directory names
|
||||
or template text this instance chose - citation mechanics, provenance rules, what a conforming
|
||||
page does or does not claim
|
||||
- Anything a later stack release would want every adopting instance to receive automatically,
|
||||
the way a fix to `tools/` already does
|
||||
|
||||
## When NOT to Use
|
||||
|
||||
- Enum values, `layout:` titles, directory names or template text. Those name *this* instance's
|
||||
choices and stay in the type-spec itself - the file `guidance:` points *from*, never the one it
|
||||
points *to*
|
||||
- A type an instance writes entirely for itself. `guidance:` is optional; a type-spec that omits
|
||||
it is described from its own body alone, exactly as every type-spec worked before this type
|
||||
existed
|
||||
|
||||
## Frontmatter
|
||||
|
||||
| Field | Required | Use |
|
||||
|---|---:|---|
|
||||
| `type` | Yes | `types/type-guidance.md` |
|
||||
| `name` | Yes | The page type this documents, e.g. `entity` - matches that type-spec's own `name:` |
|
||||
| `description` | Yes | Retrieval description for this guidance document |
|
||||
|
||||
## Conventions
|
||||
|
||||
- Linked from the type-spec it documents via that file's `guidance:` frontmatter field - a
|
||||
repo-relative path, resolved the same way `schema:` is
|
||||
- `tools/wikitool types describe <name>` composes both halves into one answer; an agent asking
|
||||
for a type's contract never needs to know it comes from two files
|
||||
- Written in the control plane's English ([AGENTS.md](../AGENTS.md) § File naming), like a
|
||||
type-spec's own authoring prose - only the type-spec's `## Template` block and its `layout:`
|
||||
titles are page material
|
||||
|
||||
---
|
||||
|
||||
Relevant Notes:
|
||||
|
||||
- Ships verbatim with every distribution; the type-spec it documents ships only as a `.template`
|
||||
where `root: kb` - see `docs/ownership-and-templates.md` § "Where the file boundary used to
|
||||
strain"
|
||||
- No `## Template` section here: a guidance file's body is prose an author reads, not a body
|
||||
structure `wikitool new` would scaffold. `type-guidance` declares no `base_dir:` for the same
|
||||
reason `lint-report` does, and `wikitool new type-guidance` refuses for the same reason
|
||||
- An instance that adopted its type-specs before this type existed sees nothing change until it
|
||||
takes the offered migration that adds `guidance:` to each one - see
|
||||
`instructions/migrations/` for the document
|
||||
@@ -0,0 +1,23 @@
|
||||
# YAML Schema for type-guidance documents
|
||||
type: object
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
const: "types/type-guidance.md"
|
||||
description: Must reference the type-guidance type-spec
|
||||
name:
|
||||
type: string
|
||||
description: >-
|
||||
The page type this guidance documents, e.g. 'entity' - matches that
|
||||
type-spec's own `name:` field
|
||||
minLength: 1
|
||||
pattern: "^[a-z][a-z0-9-]*$"
|
||||
description:
|
||||
type: string
|
||||
description: Retrieval description for this guidance document
|
||||
minLength: 10
|
||||
required:
|
||||
- type
|
||||
- name
|
||||
- description
|
||||
additionalProperties: false
|
||||
+46
-10
@@ -33,6 +33,7 @@ A type-spec doc is the authoring and validation contract for one Chemenu artifac
|
||||
- `description`: retrieval description for the type contract.
|
||||
- `schema`: repo-relative path to the `.schema.yaml` file that validates artifacts of this type, or `null` when the type has no schema.
|
||||
- `subtype_field` (optional): the frontmatter field name on instances of this type that carries a subtype/category (e.g. `entity_type`), or omitted if this type has no such field.
|
||||
- `guidance` (optional): repo-relative path to a `types/type-guidance.md`-shaped document holding this type's stack-owned authoring prose - see § "Anatomy of a type" below. Meaningful only on a `root: kb` type-spec; omit for a type an instance writes entirely for itself.
|
||||
|
||||
## Validation Contract
|
||||
|
||||
@@ -43,6 +44,8 @@ Two systems check conformance to a type:
|
||||
|
||||
Do not restate a schema rule in body text. The validator already enforces it, so the restatement adds no protection — it only spends the reviewer's judgment re-confirming something already guaranteed, instead of on the properties only a reviewer can check.
|
||||
|
||||
A type-spec's own frontmatter is checked the same way a page's is: `docs verify` validates every file under `types/` declaring `type: types/type-spec.md` against `types/type-spec.schema.yaml`. A frontmatter field used here needs a matching line in that schema, or `docs verify` fails.
|
||||
|
||||
## Writing Shape
|
||||
|
||||
- Write the body as a **content contract**: checkable properties of a conforming instance, in a form the type-conformance gate can apply directly. Prefer "what a conforming instance contains or claims" over "how to produce one" — production process is invisible to the reviewer, so it belongs in a skill or instruction, linked rather than embedded.
|
||||
@@ -65,13 +68,41 @@ frontmatter before anyone drew it:
|
||||
| Type-spec | Describes | Owned by | Ships as |
|
||||
|---|---|---|---|
|
||||
| `root: kb` (`entity`, `concept`, `source`, `comparison`) | A page **this instance** writes | The instance | `types/<name>.md.template` plus its `.schema.yaml.template`, adopted by a rename |
|
||||
| `root: repo` (`instruction`), no `base_dir` (`lint-report`), and `type-spec` itself | A stack artifact | The stack | Verbatim |
|
||||
| `root: repo` (`instruction`), no `base_dir` (`lint-report`), and `type-spec`/`type-guidance` themselves | A stack artifact | The stack | Verbatim |
|
||||
|
||||
A page type-spec's prose, its `## Template` body and its language are therefore the instance's
|
||||
to rewrite — an instance writing its pages in another language simply translates the file, and
|
||||
an upgrade does not take that back. Improvements to a shipped default reach it as an *offered*
|
||||
migration ([instructions/CONTRACT.md](../instructions/CONTRACT.md#instructionsmigrations)),
|
||||
never by overwriting.
|
||||
A page type-spec's frontmatter configuration and its `## Template` body are therefore the
|
||||
instance's to rewrite, and an upgrade does not take that back. Its generic authoring prose is the
|
||||
opposite: where the type-spec declares `guidance:`, that prose lives in a separate, stack-owned
|
||||
`types/<name>.guidance.md` (§ "Anatomy of a type" below) that ships verbatim and is overwritten
|
||||
by an upgrade like any other machinery file - the type-spec it documents does not have to be
|
||||
re-adopted, or even touched, for that improvement to arrive. A type-spec that declares no
|
||||
`guidance:` (a type an instance writes entirely for itself) has no stack half to receive; it is
|
||||
described from its own body alone, exactly as every type-spec worked before this split existed.
|
||||
An *existing* instance that adopted its type-specs before `guidance:` existed takes this as an
|
||||
*offered* migration ([instructions/CONTRACT.md](../instructions/CONTRACT.md#instructionsmigrations))
|
||||
rather than something an upgrade applies on its own - see `instructions/migrations/` for the
|
||||
document.
|
||||
|
||||
**Ownership and language still do not line up with each other**, and that stays true even though
|
||||
the authoring prose now has its own file rather than sharing one with the template. Owning the
|
||||
type-spec means the instance may rewrite its frontmatter and its `## Template` body; a guidance
|
||||
file it links is never the instance's to rewrite at all - it is the stack's, the same way
|
||||
`tools/CONTRACT.md` is. What each part is *written in* still follows what the line is *for*, per
|
||||
[AGENTS.md § File naming](../AGENTS.md#file-naming), independent of which file it lives in or who
|
||||
owns that file:
|
||||
|
||||
| Part of a `root: kb` type's contract | Lives in | Audience | Language |
|
||||
|---|---|---|---|
|
||||
| When to use / not to use, authoring guidance | The `guidance:` file, where declared | An agent writing a page | The control plane's — English |
|
||||
| The frontmatter table documenting this instance's own fields | The type-spec itself | An agent writing a page | The control plane's — English |
|
||||
| The `## Template` body, and the `layout:` titles that head a catalog section | The type-spec itself | The page itself | The instance's KB language (`kb/CONVENTIONS.md` `language:`) |
|
||||
| Field names, enum values, `dir:` values, `type:` paths | Either file's frontmatter | The machine | Neither — identifiers, never translated |
|
||||
|
||||
That is the same prose/identifier cut [kb/CONTRACT.md](../kb/CONTRACT.md#language-and-identifiers)
|
||||
makes *inside* a page, applied one level up. It binds a type-spec an instance writes for itself
|
||||
exactly as it binds the four shipped ones: a new page type is instance-owned end to end, which
|
||||
settles who may change it, not which language each half is written in - and it may declare its
|
||||
own `guidance:` file if it wants the same shape, though nothing requires it to.
|
||||
|
||||
**What the stack still requires of the type layer is one line.** There must be a type-spec
|
||||
declaring `name: source` whose schema requires `raw_files:` — the whole `raw/` → `kb/`
|
||||
@@ -87,14 +118,19 @@ Python. Adding a type must require no code change.
|
||||
|
||||
### Anatomy of a type
|
||||
|
||||
Each type is two files:
|
||||
Each type is at least two files, and a `root: kb` type may be three:
|
||||
|
||||
| File | Owns |
|
||||
|------|------|
|
||||
| `types/<name>.md` | The authoring contract: when to use the type, when not to, guidance, and the `## Template` block used to scaffold new pages |
|
||||
| `types/<name>.md` | This instance's configuration: its frontmatter fields as this schema requires them, and the `## Template` block used to scaffold new pages. For a type with no `guidance:` (below), its own prose also carries the authoring contract - when to use the type, when not to |
|
||||
| `types/<name>.schema.yaml` | The machine-checkable half: fields, types, enums, defaults, required-ness, `additionalProperties: false` |
|
||||
| `types/<name>.guidance.md` (optional, `root: kb` only) | The stack-owned authoring contract: when to use the type, when not to, and mechanism-level advice that holds regardless of this instance's own enum values or template text - linked from the type-spec's own `guidance:` field. `types/type-guidance.md` is its contract |
|
||||
|
||||
This file is the self-referential root contract that both are validated against.
|
||||
This file is the self-referential root contract every type-spec is validated against, and
|
||||
`type-guidance.md` is validated against it the same way `lint-report.md` is - itself a
|
||||
non-instantiable, contract-only type. `tools/wikitool types describe <name>` composes all of a
|
||||
type's files into one answer regardless of how many there are; an agent asking for a type's
|
||||
contract never needs to know it came from more than one file.
|
||||
|
||||
### Placement frontmatter
|
||||
|
||||
@@ -175,7 +211,7 @@ replace anyway. See `tools/chemenu/blocks.py`.
|
||||
| Frontmatter fields, enums, defaults, required-ness | Provenance and citation mechanics | Quality goal and tone |
|
||||
| Directory placement and title prefix | Linking mechanics and the orphan check | Naming conventions |
|
||||
| Body skeleton (template) | The prose/identifier rule | Relationship vocabulary |
|
||||
| When to use / not use this type | — | The KB language and its section-heading names |
|
||||
| When to use / not use this type | — | The KB language value, and its section-heading names |
|
||||
|
||||
If a rule would be identical for every type *and* every instance, it belongs in
|
||||
`kb/CONTRACT.md`. If every instance would answer it differently, it belongs in
|
||||
|
||||
@@ -69,6 +69,33 @@ properties:
|
||||
items:
|
||||
type: string
|
||||
pattern: "^[a-z][a-z0-9_]*$"
|
||||
capture_fields:
|
||||
type: array
|
||||
description: >-
|
||||
Frontmatter fields on instances of this type that are fixed at capture
|
||||
time and never correctable afterwards except by re-capturing the source
|
||||
(e.g. `fidelity`, `authority` for a source page). `raw accept`,
|
||||
`new source` and `touch --set` read this instead of a hardcoded field
|
||||
list. Omit for types with no capture-time fields.
|
||||
items:
|
||||
type: string
|
||||
pattern: "^[a-z][a-z0-9_]*$"
|
||||
root:
|
||||
type: string
|
||||
description: >-
|
||||
Which root a `base_dir:` resolves against - `kb` (the default, so
|
||||
omitting this field is equivalent to `kb`) for a page this instance
|
||||
writes, or `repo` for a type whose artifacts are legitimately not
|
||||
knowledge (e.g. `instruction`).
|
||||
enum: [kb, repo]
|
||||
guidance:
|
||||
type: string
|
||||
description: >-
|
||||
Repo-relative path to a `types/type-guidance.md`-shaped document
|
||||
holding this type's stack-owned authoring prose. Meaningful only on a
|
||||
`root: kb` type-spec; omit for a type an instance writes entirely for
|
||||
itself.
|
||||
pattern: "^types/.*\\.md$"
|
||||
required:
|
||||
- type
|
||||
- name
|
||||
|
||||
Reference in New Issue
Block a user