feat: INSTALL.md held to the installation instructions - prerequisites lists generated from the manifest, setup questions checked by docs verify (#154)
CI / verify (push) Successful in 5m20s
CI / pwsh (push) Successful in 1m53s
Release / release (push) Successful in 37s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SnAJ7Z3CpVD3PRbN73QtU2

Files changed:
- CHANGES.md
- INSTALL.md
- VERSION
- instructions/dev/doc-pull-through.md
- instructions/dev/stack-close/SKILL.md
- instructions/setup-instance.md
- tools/CONTRACT.md
- tools/README.md
- tools/chemenu/cli_contract.py
- tools/chemenu/commands/docs_verify.py
- tools/chemenu/install_doc.py
- tools/chemenu/tests/test_install_doc.py
This commit is contained in:
torben committed 2026-10-02 07:47:39 +02:00
1 parent b33088f64e
commit c77bda2004
12 files changed
+633 -69

No files matched your search

+30 -1
View File
@@ -59,7 +59,7 @@ concern - readable here, never shipped as something to parse.
---
## 8.0.0-beta.20 - 2026-10-01 - Installation only from a release, into an empty folder; upstream merge/verify and the other install paths removed (#153)
## 8.0.0-beta.21 - 2026-10-02 - INSTALL.md an die Installationsinstruktionen gekoppelt: Voraussetzungen generiert, Setup-Fragen geprüft
**Author:** Torben Nehmer
@@ -95,6 +95,7 @@ concern - readable here, never shipped as something to parse.
- Preflight as a release asset: download, verify and unpack the stack, then run the tree preflight
- trace-hook.ps1: Copilot hooks no longer open Windows' choose-an-app dialog
- Windows-Portabilität: Pfadtrenner, Zeilenenden, Encoding und Locks
- INSTALL.md an die Installationsinstruktionen gekoppelt: Voraussetzungen generiert, Setup-Fragen geprüft
**Low impact**
- version bump no longer points at version release in its output
@@ -132,6 +133,34 @@ concern - readable here, never shipped as something to parse.
- preflight.ps1: the asset-mode error helper is Exit-Asset, so PSScriptAnalyzer passes
<!-- /wikitool:bumps -->
### INSTALL.md held to the installation instructions (#154)
The installation procedure has one source, the instructions under `instructions/`; `INSTALL.md`
is the human guide beside it, in the instance's language, and since #153 it no longer retells
the steps. One shared file was rejected (D12): an agent reads every sentence as an instruction,
and the two readers need different things. What the two still share is two enumerable lists,
and both are now checked by `docs verify`:
- **Prerequisites.** `INSTALL.md` carries one generated region per platform value of
`tools/prerequisites.txt` - `<!-- wikitool:prerequisites -->` for every platform,
`<!-- wikitool:prerequisites-windows -->` for Windows only - rendered as label and minimum
version, without the manifest's English reason field. The new `wikitool docs prerequisites
[--apply]` rewrites them; it never places a missing region, since where a list belongs is the
human guide's decision, and reports it instead. A tool added to the manifest fails `docs
verify` until the region is regenerated.
- **Setup questions.** Every place `instructions/setup-instance.md` asks the user something
carries `<!-- setup-question: <key> -->` (eight today: `identity`, `remote`, `kb-language`,
`domain`, `personalization`, `environment`, `telemetry`, `task-tracker`), and the matching
bullet in `INSTALL.md` § "Was der Agent dich fragt" carries the same marker. `docs verify`
compares the two sets in both directions. Markers rather than a frontmatter list: they sit
where the question is asked, visible to whoever adds the next one, and the instruction schema
stays closed.
The prose that no check reads is session work. `instructions/dev/doc-pull-through.md` gains rows
mapping the installation instructions to `INSTALL.md` and `dev-setup.md` to `DEVELOPMENT.md`,
applied before the publish; `stack-close` step 3 reads the pair again after it and files a
deviation as a follow-up issue.
### Installation only from a release, into an empty folder; upstream merge/verify and the other install paths removed (#153)
The install run analysed in Gitea #140 failed on an instruction that contradicted itself, and the
+48 -30
View File
@@ -18,10 +18,19 @@ Demo-Korpus und keine Instanz; sie steht in [DEVELOPMENT.md](DEVELOPMENT.md).
## Was vorher da sein muss
- **Python 3.11 oder neuer, git und [ripgrep](https://github.com/BurntSushi/ripgrep) (`rg`).**
Die maßgebliche Liste steht in `tools/prerequisites.txt`; der Preflight prüft sie, bevor
irgendein `wikitool`-Befehl läuft. Installieren musst du selbst - der Agent tut es nie, auch
nicht mit deiner Zustimmung.
Diese Programme prüft der Preflight, bevor irgendein `wikitool`-Befehl läuft. Installieren musst
du sie selbst - der Agent tut es nie, auch nicht mit deiner Zustimmung. Wo eines fehlt, nennt
der Preflight den Installationsbefehl für dein System. Die Liste wird aus
`tools/prerequisites.txt` erzeugt, derselben Datei, die der Preflight liest:
<!-- wikitool:prerequisites -->
- **Python** ≥ 3.11
- **Git**
- **ripgrep (rg)**
<!-- /wikitool:prerequisites -->
Dazu:
- **Ein Agent-Harness**: Claude Code, GitHub Copilot (in VS Code oder als CLI), Codex CLI oder
Mistral Vibe.
- **Ein leeres Verzeichnis**, in dem die Instanz liegen soll, und dein Harness darin geöffnet.
@@ -29,10 +38,16 @@ Demo-Korpus und keine Instanz; sie steht in [DEVELOPMENT.md](DEVELOPMENT.md).
also genau richtig - liegt dein Repo `torben/nathan` etwa in `~/src/nathan`, installierst du
dorthin, und der Agent übernimmt dessen `origin` als Ziel für `publish`.
Unter Windows zusätzlich:
Unter Windows zusätzlich, ebenfalls vom Preflight geprüft:
- **PowerShell 7** (`pwsh`), in VS Code als Standardterminal eingestellt. Windows PowerShell 5.1
reicht nicht, und WSL ist nicht vorgesehen.
<!-- wikitool:prerequisites-windows -->
- **PowerShell 7 (pwsh)** ≥ 7
<!-- /wikitool:prerequisites-windows -->
Und außerdem:
- **PowerShell 7 als Standardterminal in VS Code.** Windows PowerShell 5.1 reicht nicht, und WSL
ist nicht vorgesehen.
- **Execution Policy `RemoteSigned`** - auf vielen Rechnern ab Werk gesetzt
(`Get-ExecutionPolicy -List` zeigt es).
- **Git for Windows.** Es bringt Git Bash mit, in dem Claude Code seine Befehle ausführt.
@@ -64,29 +79,32 @@ Die Liste aller Releases: <https://gitea.nehmer.net/torben/chemenu/releases>. Da
Raten darf der Agent keine dieser Antworten, und keine übernimmt er aus einem anderen Repo:
- **Autor-Identität** - Name und E-Mail für `git config`. Das ist zugleich der Autorname jeder
künftig angelegten Wiki-Seite (`$WIKI_AUTHOR` überschreibt ihn bei Bedarf).
- **Remote** - bei einem leeren Klon nur die Bestätigung, dass `origin` stimmt; sonst eine URL,
wenn du auf einen Server pushen willst. Ohne Remote bleibt die Instanz lokal, und jedes
`publish` läuft mit `--no-push`.
- **Sprache und Ton der Seiten** - sie landen in `kb/CONVENTIONS.md`, dazu je Collection
`kb/<name>/COLLECTION.md`. Fertige Profile, darunter ein vollständiges deutsches, hält
`instructions/kb-profiles.md` bereit. Entscheide das **vor dem ersten Ingest**: Danach ist ein
Wechsel der Abschnittsnamen eine Migration jeder bestehenden Seite. Titel, Wikilink-Ziele,
Zitat-IDs, Schema-Werte, Tags, Befehle und Pfade folgen keiner Sprache - `Act Runner` heißt in
jeder Instanz `Act Runner`.
- **Anwendungsgebiet** - woraus dieses Wiki seine Quellen zieht. Daraus schlägt der Agent eine
`source_type`-Liste vor (bei einem Verein etwa Satzung, Protokoll, Spielbericht). Das ist ein
Startpunkt, keine Festlegung: Später wird sie an echtem Bestand korrigiert
(`instructions/evolve-subtypes.md`).
- **Personalisierung** - wer diese Instanz bedient (`USER.md`) und wie sie klingt (`SOUL.md`).
Der Agent interviewt dich entlang der Vorlagen und schreibt deine Antworten wörtlich mit. Zwei
Fragen beantwortest nur du: den **Namen der Persona** und die **Themen, die bewusst draußen
bleiben**.
- **Umgebung** (optional) - Harness, MCP-Server, Remotes, damit spätere Sitzungen nicht erneut
fragen. „Weiß ich nicht“ ist eine gültige Antwort.
- **Telemetrie** - standardmäßig aus; der Agent fragt nur, ob du sie einschalten willst.
- **Aufgaben-Tracker** (optional) - siehe [Konfiguration](#konfiguration).
- <!-- setup-question: identity --> **Autor-Identität** - Name und E-Mail für `git config`. Das ist
zugleich der Autorname jeder künftig angelegten Wiki-Seite (`$WIKI_AUTHOR` überschreibt ihn bei
Bedarf).
- <!-- setup-question: remote --> **Remote** - bei einem leeren Klon nur die Bestätigung, dass
`origin` stimmt; sonst eine URL, wenn du auf einen Server pushen willst. Ohne Remote bleibt die
Instanz lokal, und jedes `publish` läuft mit `--no-push`.
- <!-- setup-question: kb-language --> **Sprache und Ton der Seiten** - sie landen in
`kb/CONVENTIONS.md`, dazu je Collection `kb/<name>/COLLECTION.md`. Fertige Profile, darunter ein
vollständiges deutsches, hält `instructions/kb-profiles.md` bereit. Entscheide das **vor dem
ersten Ingest**: Danach ist ein Wechsel der Abschnittsnamen eine Migration jeder bestehenden
Seite. Titel, Wikilink-Ziele, Zitat-IDs, Schema-Werte, Tags, Befehle und Pfade folgen keiner
Sprache - `Act Runner` heißt in jeder Instanz `Act Runner`.
- <!-- setup-question: domain --> **Anwendungsgebiet** - woraus dieses Wiki seine Quellen zieht.
Daraus schlägt der Agent eine `source_type`-Liste vor (bei einem Verein etwa Satzung, Protokoll,
Spielbericht). Das ist ein Startpunkt, keine Festlegung: Später wird sie an echtem Bestand
korrigiert (`instructions/evolve-subtypes.md`).
- <!-- setup-question: personalization --> **Personalisierung** - wer diese Instanz bedient
(`USER.md`) und wie sie klingt (`SOUL.md`). Der Agent interviewt dich entlang der Vorlagen und
schreibt deine Antworten wörtlich mit. Zwei Fragen beantwortest nur du: den **Namen der Persona**
und die **Themen, die bewusst draußen bleiben**.
- <!-- setup-question: environment --> **Umgebung** (optional) - Harness, MCP-Server, Remotes,
damit spätere Sitzungen nicht erneut fragen. „Weiß ich nicht“ ist eine gültige Antwort.
- <!-- setup-question: telemetry --> **Telemetrie** - standardmäßig aus; der Agent fragt nur, ob du
sie einschalten willst.
- <!-- setup-question: task-tracker --> **Aufgaben-Tracker** (optional) - siehe
[Konfiguration](#konfiguration).
Am Ende legt der Agent den ersten Commit an. Dabei hält das Mass-Update-Gate an (Exit 42), weil
eine neue Instanz aus weit mehr als zehn Dateien besteht. Das ist erwartet: Der Agent zeigt dir
+1 -1
View File
@@ -1 +1 @@
8.0.0-beta.20
8.0.0-beta.21
+2
View File
@@ -41,6 +41,8 @@ touched; a row that does not apply needs no action.
| A task-tracker adapter (`tools/chemenu/tasks/`), its recorded fixtures, or the live suite | [instructions/dev/tracker-testing.md](tracker-testing.md), and `MANIFEST.json` beside the fixtures when they were re-recorded |
| A skill's own step sequence or catalogue | The skill's `SKILL.md` source under `instructions/<name>/` or `instructions/dev/<name>/` |
| A per-checkout configuration file an instance owns (`.wikitool-tasks.json`, `.wikitool-telemetry.json`, `.wikitool-remotes.json`, `.wikitool-upload.json`) | [INSTALL.md](../../INSTALL.md) § Konfiguration, where an operator looks the shape up; the [setup-instance.md](../setup-instance.md) decision point that offers it during setup; and `doctor`'s own row in [tools/CONTRACT.md](../../tools/CONTRACT.md), since `doctor` is what reports the file's state |
| An installation instruction - [preflight.md](../preflight.md), [setup-instance.md](../setup-instance.md), [bootstrap.md](../bootstrap.md), [upgrade-instance.md](../upgrade-instance.md) - or `tools/prerequisites.txt` | [INSTALL.md](../../INSTALL.md), the human guide to the same procedure. Read it against the instruction: what to prepare, the sentence for the agent, what the agent asks, where it stops and why. `docs verify` checks only the two enumerable overlaps - the prerequisites lists, which `wikitool docs prerequisites --apply` regenerates from the manifest, and the setup questions: a question the agent asks the user carries `<!-- setup-question: <key> -->` where it is asked in `setup-instance.md`, and `INSTALL.md` § "Was der Agent dich fragt" names it with the same marker. Every other sentence is this session's to compare. `INSTALL.md` does not retell the steps, so a change to their order or wording alone moves nothing there |
| [dev-setup.md](dev-setup.md) | [DEVELOPMENT.md](../../DEVELOPMENT.md), read against it the same way - nothing checks this pair at all |
| A new page type the stack requires, or a new collection | Its type-spec and `COLLECTION.md` (both as the `.template` an instance adopts), the collection table in [kb/CONTRACT.md](../../kb/CONTRACT.md), and **both adoption paths**: [setup-instance.md](../setup-instance.md) for a fresh instance and [upgrade-instance.md](../upgrade-instance.md) for an existing one, where an unadopted template is what `docs verify` refuses |
3. **A heading you changed means a table of contents to regenerate - by the tool, never by
+8
View File
@@ -105,6 +105,14 @@ and a fresh subagent starts without the session's context).
behaviour one of these documents describes, update it now; if none did, say so rather than
leaving the question unasked.
**If the published diff touches an installation instruction, read the human guide against
it once more.** Which instructions those are and which human document answers for each is
the pull-through table's row in `instructions/dev/doc-pull-through.md` - `stack-dev` step 5
applied it before the publish; this is the second reading, after. A deviation found here is
filed as a follow-up issue naming both files and the sentence that disagrees, rather than
fixed in this phase: the pull-through before the publish missed it, and that miss is worth a
record of its own.
**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
+37 -35
View File
@@ -102,9 +102,9 @@ one it needs before that.
checked-out branch matches the target branch (default `main`) and refuses otherwise, so that
the wrong branch is never published.
2. **Decision point - identity.** Ask the user for their name and email address; never guess
them, and never quietly carry them over from another repository (that is a different person
and a different project):
2. <!-- setup-question: identity --> **Decision point - identity.** Ask the user for their name and
email address; never guess them, and never quietly carry them over from another repository (that
is a different person and a different project):
```bash
git config user.name "<name>"
@@ -115,9 +115,9 @@ one it needs before that.
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.
3. **Decision point - remote.** An empty clone already has one: show the user `git remote -v`
and confirm that `origin` is where this instance is to be published. Otherwise ask for a
remote URL; a purely local repo is a valid end state:
3. <!-- setup-question: remote --> **Decision point - remote.** An empty clone already has one:
show the user `git remote -v` and confirm that `origin` is where this instance is to be
published. Otherwise ask 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 1). Without it, `publish` ends with exit 1
@@ -153,10 +153,11 @@ one it needs before that.
`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.
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. 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.
2. <!-- setup-question: kb-language --> 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. 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.
3. Write `kb/CONVENTIONS.md` from `kb/CONVENTIONS.md.template`, filled in along the chosen
profile - language, section names, naming forms, tone, relationship labels, hedging rule -
@@ -166,13 +167,13 @@ one it needs before that.
4. For a language other than German: delete `german-terminology.md` or replace it with your
own vocabulary - it is material belonging to the German profile, not to the stack.
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. 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
5. <!-- setup-question: domain --> 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. 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
@@ -207,11 +208,11 @@ one it needs before that.
`docs verify` additionally checks `profile:` and `required_by_stack:` on every
`COLLECTION.md`.
5. **Decision point - personalization.** The release 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 anywhere else. Both
files are read in **every** session from now on, so they come into being here - not later,
when the occasion arises.
5. <!-- setup-question: personalization --> **Decision point - personalization.** The release 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 anywhere
else. Both files are read in **every** session from now on, so they come into being here - not
later, when the occasion arises.
Procedure, once each for `USER.md` and `SOUL.md`:
@@ -248,9 +249,9 @@ one it needs before that.
tools/wikitool instructions sync
```
7. **Decision point - record the environment.** The release 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.
7. <!-- setup-question: environment --> **Decision point - record the environment.** The release
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.
Unlike step 5, 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
@@ -262,10 +263,10 @@ one it needs before that.
`environment: absent (optional)` in step 12, not a `FAIL`. The file is gitignored and enters
no commit - it describes this checkout, not the repo.
8. **Decision point - telemetry.** Every instance installed from a release 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.
8. <!-- setup-question: telemetry --> **Decision point - telemetry.** Every instance installed from
a release 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.
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`):
@@ -284,12 +285,13 @@ one it needs before that.
never a `FAIL`, since both directions are a valid state. More on this:
[EVALS.md](../EVALS.md) § "Whether it runs at all".
9. **Decision point - task tracker.** The instance ships the `project` type and the collection
its type-spec's `base_dir:` names (`kb/gtd/` here), so committed initiatives have a page from
the start. What they do *not* have until this step is the other half of the weekly review:
the tracker that owns the open items, which `tools/wikitool review` joins those pages against
over the project name. No tracker configured is a legitimate end state - the pages work
alone, `review` simply says so and refuses - so ask rather than assume.
9. <!-- setup-question: task-tracker --> **Decision point - task tracker.** The instance ships the
`project` type and the collection its type-spec's `base_dir:` names (`kb/gtd/` here), so
committed initiatives have a page from the start. What they do *not* have until this step is the
other half of the weekly review: the tracker that owns the open items, which `tools/wikitool
review` joins those pages against over the project name. No tracker configured is a legitimate
end state - the pages work alone, `review` simply says so and refuses - so ask rather than
assume.
Ask the user once: is there a task tracker to connect? If yes, create `.wikitool-tasks.json`
in the repo root (per checkout, no `.template`, **gitignored once it holds a token** - like
+54
View File
@@ -132,6 +132,7 @@ instructions verify read idempotent budget:counted exit:0,1
instructions list read idempotent budget:counted exit:0 List the flat instructions with their descriptions.
docs verify read idempotent budget:counted exit:0,1 Check the docs that mirror the code.
docs toc write idempotent budget:counted exit:0 Create, refresh or remove the generated table-of-contents region.
docs prerequisites write idempotent budget:counted exit:0,1 Regenerate `INSTALL.md`'s prerequisites lists from `tools/prerequisites.txt`.
docs contract write idempotent budget:counted exit:0,1 Regenerate `tools/CONTRACT.md`'s `<!-- wikitool:commands -->` region.
eval sessions read idempotent budget:exempt exit:0 List the sessions that have a trace under `reports/telemetry/`.
eval score read idempotent budget:exempt exit:0,1 Score one traced session.
@@ -2182,6 +2183,9 @@ Check the docs that mirror the code.
- 1 A shipped `.md`/`.template` cites an issue number
- 1 A reference file's table-of-contents region is missing or stale
- 1 A reference file's relative markdown link does not resolve to an existing file
- 1 An `INSTALL.md` prerequisites region is stale
- 1 An `INSTALL.md` prerequisites region is missing, or names a platform no tool has
- 1 A setup question is marked in one of `instructions/setup-instance.md` and `INSTALL.md` but not the other
**ON FAILURE**
@@ -2191,6 +2195,9 @@ Check the docs that mirror the code.
- A shipped `.md`/`.template` cites an issue number -> Say what was decided instead of pointing at where, or move the pointer behind a `<!-- dist:strip-start/end -->` block
- A reference file's table-of-contents region is missing or stale -> Run `docs toc --apply`, then re-run
- A reference file's relative markdown link does not resolve to an existing file -> Fix the `../` count or the target's name
- An `INSTALL.md` prerequisites region is stale -> Run `docs prerequisites --apply`, then re-run
- An `INSTALL.md` prerequisites region is missing, or names a platform no tool has -> Add the marker pair where that list belongs (or remove the orphaned region and its introducing prose), then run `docs prerequisites --apply`
- A setup question is marked in one of `instructions/setup-instance.md` and `INSTALL.md` but not the other -> Describe the question for the human in `INSTALL.md` with the same marker, or remove the bullet for a question no longer asked
**NEVER**
@@ -2207,12 +2214,14 @@ Check the docs that mirror the code.
- No `.md`/`.template` file `dist export` would ship cites an issue number. A `<!-- dist:strip-start/end -->` region is exempt: the check reads the export plan's text, from which it is already gone.
- Every reference file `docs toc` covers carries the current table-of-contents region for its own headings - missing and stale are one check.
- Every relative markdown link in one of those reference files resolves to an existing file. A target's `#anchor` suffix is stripped first, and code fences and inline code spans are masked before scanning, so link syntax shown as an example is not mistaken for a real reference.
- `INSTALL.md` carries one generated `<!-- wikitool:prerequisites -->` region per platform value of `tools/prerequisites.txt` (`prerequisites-<platform>` for a platform-specific one), each current; and the `<!-- setup-question: <key> -->` markers in `instructions/setup-instance.md` and `INSTALL.md` name the same set of keys, so a question the agent asks is never one the human guide leaves out, nor the reverse.
- Read-only.
**SEE ALSO**
- `wikitool docs toc` - regenerates tables of contents
- `wikitool docs contract` - regenerates the commands region
- `wikitool docs prerequisites` - regenerates `INSTALL.md`'s prerequisites lists
- `wikitool instructions verify` - the same kind of check for `instructions/`
#### `docs toc`
@@ -2257,6 +2266,51 @@ Create, refresh or remove the generated table-of-contents region.
- `wikitool docs verify` - checks every region is current
#### `docs prerequisites`
Regenerate `INSTALL.md`'s prerequisites lists from `tools/prerequisites.txt`.
**SYNOPSIS**
- `wikitool docs prerequisites [--apply]`
**PROPERTIES**
- effect: write
- idempotent: yes
- atomic: Yes - every region is rewritten in one file write
- budget: counted
- network: no
**EXAMPLES**
- `tools/wikitool docs prerequisites`
- `tools/wikitool docs prerequisites --apply`
**EXIT STATUS**
- 0 success
- 1 `INSTALL.md` is missing, or lacks a region the manifest calls for
**ON FAILURE**
- `INSTALL.md` is missing, or lacks a region the manifest calls for -> Not transient - add the marker pair the message names where that list belongs (restore the file if it is gone), then retry
**NEVER**
- Never hand-edit a prerequisites region - change `tools/prerequisites.txt` and re-run this.
**NOTES**
- Rewrites each `<!-- wikitool:prerequisites -->` region in `INSTALL.md` (tools every platform needs) and `<!-- wikitool:prerequisites-<platform> -->` region (tools only that platform needs) from the manifest: one list item per tool, its label and minimum version. The manifest's reason field stays out - it is English prose, and the region sits in a document that need not be.
- Never places a region: where a list belongs in the human guide is that guide's own decision. A region the manifest calls for but the file lacks is an error naming the marker pair to add.
- Dry-run by default (says whether the file would change); `--apply` writes.
- `docs verify` checks the result stays current.
**SEE ALSO**
- `wikitool docs verify` - checks every region is current
#### `docs contract`
Regenerate `tools/CONTRACT.md`'s `<!-- wikitool:commands -->` region.
+1
View File
@@ -85,6 +85,7 @@ tools/
toolpaths.py where git and rg are started from: .wikitool-tools.json, bare name only without the file
filelock.py an exclusive lock on an open file, flock on POSIX and msvcrt on Windows - the only module that imports either
prerequisites.py prerequisites.txt read from Python, plus the platform and long-path questions `doctor` asks
install_doc.py INSTALL.md held to the instructions: its prerequisites lists generated from prerequisites.txt, its setup questions matched to setup-instance.md's markers
corpus_cache.py one parsed corpus per commit, never cached while the tree is dirty
kb_scan.py page iteration/loading over kb/
blocks.py generated regions in a page body, found by marker rather than by heading
+1 -1
View File
@@ -270,7 +270,7 @@ GROUPS: tuple[tuple[str, tuple[str, ...]], ...] = (
("Types, instructions and docs", (
"types list", "types describe",
"instructions sync", "instructions verify", "instructions list",
"docs verify", "docs toc", "docs contract",
"docs verify", "docs toc", "docs prerequisites", "docs contract",
)),
("Telemetry", (
"eval sessions", "eval score",
+100 -1
View File
@@ -68,7 +68,7 @@ from typing import Optional
import typer
from chemenu import blocks, cli_contract, config, conventions, kb_collections, markdown_code, toc, toolpaths, version as version_mod
from chemenu import blocks, cli_contract, config, conventions, install_doc, kb_collections, markdown_code, toc, toolpaths, version as version_mod
from chemenu.commands import dist_cmd
from chemenu.commands._util import fail, rel_path, success
@@ -1078,6 +1078,11 @@ def check_breaking_change_for_boundary() -> list[str]:
"file. A target's `#anchor` suffix is stripped first, and code fences and inline code "
"spans are masked before scanning, so link syntax shown as an example is not mistaken "
"for a real reference.",
"`INSTALL.md` carries one generated `<!-- wikitool:prerequisites -->` region per "
"platform value of `tools/prerequisites.txt` (`prerequisites-<platform>` for a "
"platform-specific one), each current; and the `<!-- setup-question: <key> -->` markers "
"in `instructions/setup-instance.md` and `INSTALL.md` name the same set of keys, so a "
"question the agent asks is never one the human guide leaves out, nor the reverse.",
"Read-only.",
),
failures=(
@@ -1108,6 +1113,22 @@ def check_breaking_change_for_boundary() -> list[str]:
"file",
reaction="Fix the `../` count or the target's name",
),
cli_contract.Failure(
cause="An `INSTALL.md` prerequisites region is stale",
reaction="Run `docs prerequisites --apply`, then re-run",
),
cli_contract.Failure(
cause="An `INSTALL.md` prerequisites region is missing, or names a platform no tool "
"has",
reaction="Add the marker pair where that list belongs (or remove the orphaned region "
"and its introducing prose), then run `docs prerequisites --apply`",
),
cli_contract.Failure(
cause="A setup question is marked in one of `instructions/setup-instance.md` and "
"`INSTALL.md` but not the other",
reaction="Describe the question for the human in `INSTALL.md` with the same marker, "
"or remove the bullet for a question no longer asked",
),
),
examples=(
"tools/wikitool docs verify",
@@ -1118,6 +1139,7 @@ def check_breaking_change_for_boundary() -> list[str]:
see_also=(
"`wikitool docs toc` - regenerates tables of contents",
"`wikitool docs contract` - regenerates the commands region",
"`wikitool docs prerequisites` - regenerates `INSTALL.md`'s prerequisites lists",
"`wikitool instructions verify` - the same kind of check for `instructions/`",
),
))
@@ -1139,6 +1161,8 @@ def verify():
+ check_no_issue_references()
+ check_toc_regions()
+ check_reference_targets()
+ install_doc.check_prerequisite_regions()
+ install_doc.check_setup_questions()
)
if issues:
@@ -1155,6 +1179,8 @@ def verify():
f"no issue references in {len(shipped_prose())} shipped document(s) or command help, "
f"tables of contents current and every link resolving on "
f"{len(toc.target_files())} reference file(s), "
f"{install_doc.INSTALL_DOC} in step with tools/prerequisites.txt and "
f"{install_doc.SETUP_INSTRUCTION}'s questions, "
f"{version_mod.CHANGES_FILENAME} documents version "
f"{(config.ROOT / version_mod.VERSION_FILENAME).read_text(encoding='utf-8').strip()}."
)
@@ -1235,6 +1261,79 @@ def toc_command(
typer.echo(f"\n{len(changed)} file(s) would change. Re-run with --apply to write.")
@app.command("prerequisites")
@cli_contract.record(cli_contract.CommandRecord(
path="docs prerequisites",
summary="Regenerate `INSTALL.md`'s prerequisites lists from `tools/prerequisites.txt`.",
synopsis=(cli_contract.Variant(usage="docs prerequisites [--apply]"),),
properties=cli_contract.Properties(
effect=cli_contract.Effect.WRITE,
idempotent=cli_contract.Idempotent.YES,
atomic="Yes - every region is rewritten in one file write",
budget=cli_contract.Budget.COUNTED,
),
notes=(
"Rewrites each `<!-- wikitool:prerequisites -->` region in `INSTALL.md` (tools every "
"platform needs) and `<!-- wikitool:prerequisites-<platform> -->` region (tools only that "
"platform needs) from the manifest: one list item per tool, its label and minimum "
"version. The manifest's reason field stays out - it is English prose, and the region "
"sits in a document that need not be.",
"Never places a region: where a list belongs in the human guide is that guide's own "
"decision. A region the manifest calls for but the file lacks is an error naming the "
"marker pair to add.",
"Dry-run by default (says whether the file would change); `--apply` writes.",
"`docs verify` checks the result stays current.",
),
failures=(cli_contract.Failure(
cause="`INSTALL.md` is missing, or lacks a region the manifest calls for",
reaction="Not transient - add the marker pair the message names where that list "
"belongs (restore the file if it is gone), then retry",
),),
examples=(
"tools/wikitool docs prerequisites",
"tools/wikitool docs prerequisites --apply",
),
never=(
"Never hand-edit a prerequisites region - change `tools/prerequisites.txt` and re-run "
"this.",
),
see_also=(
"`wikitool docs verify` - checks every region is current",
),
))
def prerequisites_command(
apply: bool = typer.Option(False, "--apply", help="Write changes; default is dry-run (preview only)"),
):
"""Regenerate `INSTALL.md`'s prerequisites lists from `tools/prerequisites.txt`."""
from chemenu import prerequisites
path = install_doc.install_doc_path()
if not path.is_file():
fail(f"{install_doc.INSTALL_DOC} is missing.")
text = path.read_text(encoding="utf-8")
after, missing = install_doc.refresh(text, prerequisites.load_manifest())
if missing:
fail(
f"{install_doc.INSTALL_DOC} lacks "
+ ", ".join(f"`{blocks.open_marker(name)}`" for name in missing)
+ " - add each marker pair (with its closing marker) where that list belongs, "
"then re-run."
)
if after == text:
success(f"{install_doc.INSTALL_DOC}'s prerequisites lists are already current.")
return
if not apply:
typer.echo(f"{install_doc.INSTALL_DOC} would change.")
typer.echo("Re-run with --apply to write.")
return
path.write_text(after, encoding="utf-8", newline="\n")
success(f"Regenerated the prerequisites lists in {install_doc.INSTALL_DOC}.")
@app.command("contract")
@cli_contract.record(cli_contract.CommandRecord(
path="docs contract",
+178
View File
@@ -0,0 +1,178 @@
"""The two places `INSTALL.md` overlaps the installation instructions, held to them.
`INSTALL.md` is written for a human and in the instance's KB language;
`instructions/setup-instance.md` is written for an agent and in English. A
single file serving both was rejected: an agent reads every sentence as an
instruction, and the human needs preparation and decisions where the agent
needs steps and exit codes. So the human document does not retell the
procedure, and what it still shares with the instructions is two enumerable
lists - both checked here, so neither can drift silently:
1. **What the machine needs.** `tools/prerequisites.txt` is the one list; the
preflight and `doctor` read it. `INSTALL.md` carries it as a generated
region per platform value (`<!-- wikitool:prerequisites -->` for `all`,
`<!-- wikitool:prerequisites-<platform> -->` otherwise), rendered from the
label and minimum version only. The manifest's `why` field stays out: it is
English prose, and the region sits in a document that is not. A region is
never placed by the tool - where it goes is the human document's own
decision - so a missing one is reported, not appended.
2. **What the agent asks the user.** Every place `setup-instance.md` puts a
question to the user carries `<!-- setup-question: <key> -->`, and the
bullet in `INSTALL.md` that tells the human about it carries the same
marker. The two key sets must be equal: a question the human guide does not
mention catches them unprepared, and one it mentions that is no longer asked
is a claim about a procedure that no longer exists. The key is the shared
identifier precisely because the surrounding prose is in two languages.
Everything else in `INSTALL.md` is prose no check reads;
`instructions/dev/doc-pull-through.md` names it as session work.
"""
from __future__ import annotations
import re
from pathlib import Path
from chemenu import blocks, config, prerequisites
INSTALL_DOC = "INSTALL.md"
SETUP_INSTRUCTION = "instructions/setup-instance.md"
REGION_PREFIX = "prerequisites"
QUESTION_RE = re.compile(r"<!-- setup-question: ([a-z][a-z0-9-]*) -->")
def install_doc_path() -> Path:
return config.ROOT / INSTALL_DOC
def setup_instruction_path() -> Path:
return config.ROOT / SETUP_INSTRUCTION
def region_name(platform: str) -> str:
"""`prerequisites` for the tools every platform needs, `prerequisites-<platform>`
for the ones only that platform does."""
return REGION_PREFIX if platform == "all" else f"{REGION_PREFIX}-{platform}"
def render_lines(tools: tuple[prerequisites.Tool, ...]) -> list[str]:
"""One list item per tool - label and minimum version, nothing in a language."""
return [
f"- **{tool.label}**" + (f" ≥ {tool.minimum}" if tool.minimum else "")
for tool in tools
]
def expected_regions(manifest: prerequisites.Manifest) -> dict[str, str]:
"""Region name -> the whole region, markers included, in manifest order of
first appearance of each platform value."""
platforms: list[str] = []
for tool in manifest.tools:
if tool.platforms not in platforms:
platforms.append(tool.platforms)
regions = {}
for platform in platforms:
name = region_name(platform)
tools = tuple(t for t in manifest.tools if t.platforms == platform)
regions[name] = "\n".join(
[blocks.open_marker(name), *render_lines(tools), blocks.close_marker(name)]
)
return regions
def _region_span(text: str, name: str) -> tuple[int, int] | None:
"""Start and end offset of `name`'s region, markers included - without the
blank lines `blocks` takes along, so a refresh leaves the prose around it
exactly as it was."""
start = text.find(blocks.open_marker(name))
if start < 0:
return None
close = blocks.close_marker(name)
end = text.find(close, start)
if end < 0:
return None
return start, end + len(close)
def refresh(text: str, manifest: prerequisites.Manifest) -> tuple[str, list[str]]:
"""`text` with every prerequisites region it carries rewritten from the
manifest, plus the names of the regions it lacks.
A region the manifest no longer has a platform for is left alone and
reported by `check_prerequisite_regions`, not deleted here: the prose
introducing it ("Unter Windows zusätzlich:") would otherwise be left
standing over nothing.
"""
missing = []
for name, region in expected_regions(manifest).items():
span = _region_span(text, name)
if span is None:
missing.append(name)
continue
text = text[: span[0]] + region + text[span[1]:]
return text, missing
def _present_region_names(text: str) -> set[str]:
pattern = re.compile(
rf"<!-- wikitool:({re.escape(REGION_PREFIX)}(?:-[a-z0-9-]+)?) -->"
)
return set(pattern.findall(text))
def check_prerequisite_regions() -> list[str]:
"""`INSTALL.md` carries one current region per platform value of the manifest."""
path = install_doc_path()
if not path.is_file():
return [f"{INSTALL_DOC} is missing - the human installation guide is a stack file"]
text = path.read_text(encoding="utf-8")
manifest = prerequisites.load_manifest()
refreshed, missing = refresh(text, manifest)
issues = [
f"{INSTALL_DOC} has no `{blocks.open_marker(name)}` region - add the marker pair "
f"where that list belongs, then run `wikitool docs prerequisites --apply`"
for name in missing
]
issues += [
f"{INSTALL_DOC} carries `{blocks.open_marker(name)}`, but no tool in "
"tools/prerequisites.txt has that platform - remove the region and the prose "
"introducing it"
for name in sorted(_present_region_names(text) - set(expected_regions(manifest)))
]
if refreshed != text:
issues.append(
f"{INSTALL_DOC}'s prerequisites list no longer matches tools/prerequisites.txt - "
"run `wikitool docs prerequisites --apply`"
)
return issues
def question_keys(text: str) -> set[str]:
return set(QUESTION_RE.findall(text))
def check_setup_questions() -> list[str]:
"""Every question `setup-instance.md` asks is named in `INSTALL.md`, and
`INSTALL.md` names no question that is no longer asked."""
setup = setup_instruction_path()
install = install_doc_path()
if not setup.is_file() or not install.is_file():
# A missing INSTALL.md is check_prerequisite_regions' finding; a
# missing setup instruction leaves nothing to compare against.
return []
asked = question_keys(setup.read_text(encoding="utf-8"))
named = question_keys(install.read_text(encoding="utf-8"))
issues = [
f"{SETUP_INSTRUCTION} asks the user `{key}`, but {INSTALL_DOC} § \"Was der Agent dich "
f"fragt\" does not name it - add a bullet carrying `<!-- setup-question: {key} -->`"
for key in sorted(asked - named)
]
issues += [
f"{INSTALL_DOC} names the setup question `{key}`, which {SETUP_INSTRUCTION} no longer "
"asks - remove the bullet, or restore the marker where the question is asked"
for key in sorted(named - asked)
]
return issues
+173
View File
@@ -0,0 +1,173 @@
from typer.testing import CliRunner
from chemenu import config, install_doc, prerequisites
from chemenu.cli import app
runner = CliRunner()
MANIFEST = prerequisites.Manifest(
limits={},
tools=(
prerequisites.Tool("python", "3.11", "all", "Python", "why"),
prerequisites.Tool("git", None, "all", "Git", "why"),
prerequisites.Tool("pwsh", "7", "windows", "PowerShell 7 (pwsh)", "why"),
),
)
CURRENT = """# Installation
Vorher:
<!-- wikitool:prerequisites -->
- **Python** ≥ 3.11
- **Git**
<!-- /wikitool:prerequisites -->
Unter Windows zusätzlich:
<!-- wikitool:prerequisites-windows -->
- **PowerShell 7 (pwsh)** ≥ 7
<!-- /wikitool:prerequisites-windows -->
## Was der Agent dich fragt
- <!-- setup-question: identity --> **Autor-Identität** - Name und E-Mail.
"""
SETUP = """# Set up
2. <!-- setup-question: identity --> **Decision point - identity.** Ask the user.
"""
def _tree(tmp_path, monkeypatch, install=CURRENT, setup=SETUP, manifest=MANIFEST):
monkeypatch.setattr(config, "ROOT", tmp_path)
monkeypatch.setattr(prerequisites, "load_manifest", lambda path=None: manifest)
(tmp_path / "instructions").mkdir()
(tmp_path / "INSTALL.md").write_text(install, encoding="utf-8")
(tmp_path / "instructions" / "setup-instance.md").write_text(setup, encoding="utf-8")
return tmp_path
def test_this_repos_install_doc_matches_the_manifest():
assert install_doc.check_prerequisite_regions() == []
def test_this_repos_install_doc_names_every_setup_question():
assert install_doc.check_setup_questions() == []
def test_this_repos_setup_instruction_marks_its_questions():
"""The check compares two sets; an instruction that lost every marker would
compare empty against empty and pass. Guard the real file's own count."""
text = install_doc.setup_instruction_path().read_text(encoding="utf-8")
assert install_doc.question_keys(text) >= {
"identity", "remote", "kb-language", "domain",
"personalization", "environment", "telemetry", "task-tracker",
}
def test_render_lines_are_label_and_minimum_only():
assert install_doc.render_lines(MANIFEST.tools) == [
"- **Python** ≥ 3.11",
"- **Git**",
"- **PowerShell 7 (pwsh)** ≥ 7",
]
def test_a_current_install_doc_passes(tmp_path, monkeypatch):
_tree(tmp_path, monkeypatch)
assert install_doc.check_prerequisite_regions() == []
assert install_doc.check_setup_questions() == []
def test_a_manifest_tool_missing_from_install_doc_fails(tmp_path, monkeypatch):
"""Acceptance criterion: a tool in the manifest that INSTALL.md lacks fails `docs verify`."""
_tree(tmp_path, monkeypatch, install=CURRENT.replace("- **Git**\n", ""))
issues = install_doc.check_prerequisite_regions()
assert any("no longer matches tools/prerequisites.txt" in issue for issue in issues)
def test_a_new_platform_without_a_region_fails(tmp_path, monkeypatch):
manifest = prerequisites.Manifest(
limits={},
tools=MANIFEST.tools + (prerequisites.Tool("brew", None, "macos", "Homebrew", "why"),),
)
_tree(tmp_path, monkeypatch, manifest=manifest)
issues = install_doc.check_prerequisite_regions()
assert any("<!-- wikitool:prerequisites-macos -->" in issue for issue in issues)
def test_a_region_for_a_platform_no_tool_has_fails(tmp_path, monkeypatch):
manifest = prerequisites.Manifest(limits={}, tools=MANIFEST.tools[:2])
_tree(tmp_path, monkeypatch, manifest=manifest)
issues = install_doc.check_prerequisite_regions()
assert any("prerequisites-windows" in issue and "no tool" in issue for issue in issues)
def test_a_missing_install_doc_is_reported(tmp_path, monkeypatch):
_tree(tmp_path, monkeypatch)
(tmp_path / "INSTALL.md").unlink()
assert any("missing" in issue for issue in install_doc.check_prerequisite_regions())
assert install_doc.check_setup_questions() == []
def test_refresh_touches_nothing_outside_the_regions(tmp_path):
stale = CURRENT.replace("≥ 3.11", "≥ 3.9")
refreshed, missing = install_doc.refresh(stale, MANIFEST)
assert missing == []
assert refreshed == CURRENT
def test_refresh_is_idempotent():
once, _ = install_doc.refresh(CURRENT, MANIFEST)
twice, _ = install_doc.refresh(once, MANIFEST)
assert once == twice == CURRENT
def test_a_question_the_install_doc_does_not_name_fails(tmp_path, monkeypatch):
"""Acceptance criterion: a new question in setup-instance.md that INSTALL.md
does not name fails `docs verify`."""
setup = SETUP + "\n3. <!-- setup-question: remote --> **Decision point - remote.**\n"
_tree(tmp_path, monkeypatch, setup=setup)
issues = install_doc.check_setup_questions()
assert len(issues) == 1
assert "`remote`" in issues[0] and "does not name it" in issues[0]
def test_a_question_no_longer_asked_fails(tmp_path, monkeypatch):
install = CURRENT + "- <!-- setup-question: telemetry --> **Telemetrie**\n"
_tree(tmp_path, monkeypatch, install=install)
issues = install_doc.check_setup_questions()
assert len(issues) == 1
assert "`telemetry`" in issues[0] and "no longer asks" in issues[0]
def test_docs_prerequisites_is_a_dry_run_by_default(tmp_path, monkeypatch):
stale = CURRENT.replace("≥ 3.11", "≥ 3.9")
root = _tree(tmp_path, monkeypatch, install=stale)
result = runner.invoke(app, ["docs", "prerequisites"])
assert result.exit_code == 0, result.output
assert "would change" in result.output
assert (root / "INSTALL.md").read_text(encoding="utf-8") == stale
def test_docs_prerequisites_apply_rewrites_the_regions(tmp_path, monkeypatch):
root = _tree(tmp_path, monkeypatch, install=CURRENT.replace("≥ 3.11", "≥ 3.9"))
result = runner.invoke(app, ["docs", "prerequisites", "--apply"])
assert result.exit_code == 0, result.output
assert (root / "INSTALL.md").read_text(encoding="utf-8") == CURRENT
assert install_doc.check_prerequisite_regions() == []
def test_docs_prerequisites_never_places_a_missing_region(tmp_path, monkeypatch):
without_windows = CURRENT.replace(
"<!-- wikitool:prerequisites-windows -->\n- **PowerShell 7 (pwsh)** ≥ 7\n"
"<!-- /wikitool:prerequisites-windows -->\n",
"",
)
root = _tree(tmp_path, monkeypatch, install=without_windows)
result = runner.invoke(app, ["docs", "prerequisites", "--apply"])
assert result.exit_code == 1
assert "prerequisites-windows" in result.output
assert (root / "INSTALL.md").read_text(encoding="utf-8") == without_windows