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 **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 - 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 - trace-hook.ps1: Copilot hooks no longer open Windows' choose-an-app dialog
- Windows-Portabilität: Pfadtrenner, Zeilenenden, Encoding und Locks - Windows-Portabilität: Pfadtrenner, Zeilenenden, Encoding und Locks
- INSTALL.md an die Installationsinstruktionen gekoppelt: Voraussetzungen generiert, Setup-Fragen geprüft
**Low impact** **Low impact**
- version bump no longer points at version release in its output - 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 - preflight.ps1: the asset-mode error helper is Exit-Asset, so PSScriptAnalyzer passes
<!-- /wikitool:bumps --> <!-- /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) ### 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 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 ## Was vorher da sein muss
- **Python 3.11 oder neuer, git und [ripgrep](https://github.com/BurntSushi/ripgrep) (`rg`).** Diese Programme prüft der Preflight, bevor irgendein `wikitool`-Befehl läuft. Installieren musst
Die maßgebliche Liste steht in `tools/prerequisites.txt`; der Preflight prüft sie, bevor du sie selbst - der Agent tut es nie, auch nicht mit deiner Zustimmung. Wo eines fehlt, nennt
irgendein `wikitool`-Befehl läuft. Installieren musst du selbst - der Agent tut es nie, auch der Preflight den Installationsbefehl für dein System. Die Liste wird aus
nicht mit deiner Zustimmung. `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 - **Ein Agent-Harness**: Claude Code, GitHub Copilot (in VS Code oder als CLI), Codex CLI oder
Mistral Vibe. Mistral Vibe.
- **Ein leeres Verzeichnis**, in dem die Instanz liegen soll, und dein Harness darin geöffnet. - **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 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`. 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 <!-- wikitool:prerequisites-windows -->
reicht nicht, und WSL ist nicht vorgesehen. - **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 - **Execution Policy `RemoteSigned`** - auf vielen Rechnern ab Werk gesetzt
(`Get-ExecutionPolicy -List` zeigt es). (`Get-ExecutionPolicy -List` zeigt es).
- **Git for Windows.** Es bringt Git Bash mit, in dem Claude Code seine Befehle ausführt. - **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: 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 - <!-- setup-question: identity --> **Autor-Identität** - Name und E-Mail für `git config`. Das ist
künftig angelegten Wiki-Seite (`$WIKI_AUTHOR` überschreibt ihn bei Bedarf). zugleich der Autorname jeder künftig angelegten Wiki-Seite (`$WIKI_AUTHOR` überschreibt ihn bei
- **Remote** - bei einem leeren Klon nur die Bestätigung, dass `origin` stimmt; sonst eine URL, Bedarf).
wenn du auf einen Server pushen willst. Ohne Remote bleibt die Instanz lokal, und jedes - <!-- setup-question: remote --> **Remote** - bei einem leeren Klon nur die Bestätigung, dass
`publish` läuft mit `--no-push`. `origin` stimmt; sonst eine URL, wenn du auf einen Server pushen willst. Ohne Remote bleibt die
- **Sprache und Ton der Seiten** - sie landen in `kb/CONVENTIONS.md`, dazu je Collection Instanz lokal, und jedes `publish` läuft mit `--no-push`.
`kb/<name>/COLLECTION.md`. Fertige Profile, darunter ein vollständiges deutsches, hält - <!-- setup-question: kb-language --> **Sprache und Ton der Seiten** - sie landen in
`instructions/kb-profiles.md` bereit. Entscheide das **vor dem ersten Ingest**: Danach ist ein `kb/CONVENTIONS.md`, dazu je Collection `kb/<name>/COLLECTION.md`. Fertige Profile, darunter ein
Wechsel der Abschnittsnamen eine Migration jeder bestehenden Seite. Titel, Wikilink-Ziele, vollständiges deutsches, hält `instructions/kb-profiles.md` bereit. Entscheide das **vor dem
Zitat-IDs, Schema-Werte, Tags, Befehle und Pfade folgen keiner Sprache - `Act Runner` heißt in ersten Ingest**: Danach ist ein Wechsel der Abschnittsnamen eine Migration jeder bestehenden
jeder Instanz `Act Runner`. Seite. Titel, Wikilink-Ziele, Zitat-IDs, Schema-Werte, Tags, Befehle und Pfade folgen keiner
- **Anwendungsgebiet** - woraus dieses Wiki seine Quellen zieht. Daraus schlägt der Agent eine Sprache - `Act Runner` heißt in jeder Instanz `Act Runner`.
`source_type`-Liste vor (bei einem Verein etwa Satzung, Protokoll, Spielbericht). Das ist ein - <!-- setup-question: domain --> **Anwendungsgebiet** - woraus dieses Wiki seine Quellen zieht.
Startpunkt, keine Festlegung: Später wird sie an echtem Bestand korrigiert Daraus schlägt der Agent eine `source_type`-Liste vor (bei einem Verein etwa Satzung, Protokoll,
(`instructions/evolve-subtypes.md`). Spielbericht). Das ist ein Startpunkt, keine Festlegung: Später wird sie an echtem Bestand
- **Personalisierung** - wer diese Instanz bedient (`USER.md`) und wie sie klingt (`SOUL.md`). korrigiert (`instructions/evolve-subtypes.md`).
Der Agent interviewt dich entlang der Vorlagen und schreibt deine Antworten wörtlich mit. Zwei - <!-- setup-question: personalization --> **Personalisierung** - wer diese Instanz bedient
Fragen beantwortest nur du: den **Namen der Persona** und die **Themen, die bewusst draußen (`USER.md`) und wie sie klingt (`SOUL.md`). Der Agent interviewt dich entlang der Vorlagen und
bleiben**. schreibt deine Antworten wörtlich mit. Zwei Fragen beantwortest nur du: den **Namen der Persona**
- **Umgebung** (optional) - Harness, MCP-Server, Remotes, damit spätere Sitzungen nicht erneut und die **Themen, die bewusst draußen bleiben**.
fragen. „Weiß ich nicht“ ist eine gültige Antwort. - <!-- setup-question: environment --> **Umgebung** (optional) - Harness, MCP-Server, Remotes,
- **Telemetrie** - standardmäßig aus; der Agent fragt nur, ob du sie einschalten willst. damit spätere Sitzungen nicht erneut fragen. „Weiß ich nicht“ ist eine gültige Antwort.
- **Aufgaben-Tracker** (optional) - siehe [Konfiguration](#konfiguration). - <!-- 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 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 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 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 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 | | 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 | | 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 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 behaviour one of these documents describes, update it now; if none did, say so rather than
leaving the question unasked. 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** - **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 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 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 checked-out branch matches the target branch (default `main`) and refuses otherwise, so that
the wrong branch is never published. the wrong branch is never published.
2. **Decision point - identity.** Ask the user for their name and email address; never guess 2. <!-- setup-question: identity --> **Decision point - identity.** Ask the user for their name and
them, and never quietly carry them over from another repository (that is a different person email address; never guess them, and never quietly carry them over from another repository (that
and a different project): is a different person and a different project):
```bash ```bash
git config user.name "<name>" 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 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. 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` 3. <!-- setup-question: remote --> **Decision point - remote.** An empty clone already has one:
and confirm that `origin` is where this instance is to be published. Otherwise ask for a show the user `git remote -v` and confirm that `origin` is where this instance is to be
remote URL; a purely local repo is a valid end state: published. Otherwise ask for a remote URL; a purely local repo is a valid end state:
- Given: `git remote add origin <url>` - Given: `git remote add origin <url>`
- Not given: stay local - then **every** later `tools/wikitool publish` needs a `--no-push` - 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 (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 `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. 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**; 2. <!-- setup-question: kb-language --> Ask the user for the KB language.
[kb-profiles.md](kb-profiles.md) additionally holds a complete German profile. The `kb/CONVENTIONS.md.template` defaults to **English**; [kb-profiles.md](kb-profiles.md)
profile catalogue is a **palette, not an enum**: what gets adopted is the text *into* the additionally holds a complete German profile. The profile catalogue is a **palette, not an
instance file, not a reference to the catalogue. 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 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 - 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 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. 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. 5. <!-- setup-question: domain --> Ask the user about the subject area and derive a
[kb-profiles.md](kb-profiles.md) holds two worked domain profiles as illustration. The `source_type` proposal from it. [kb-profiles.md](kb-profiles.md) holds two worked domain
proposal is a **starting point, not a commitment** - at setup time the operator has zero profiles as illustration. The proposal is a **starting point, not a commitment** - at setup
sources and is guessing a taxonomy before having seen a single file, which is the worst time the operator has zero sources and is guessing a taxonomy before having seen a single
possible moment to pin an enum down. Carrying out the proposal means setting the enum in file, which is the worst possible moment to pin an enum down. Carrying out the proposal means
`types/source.schema.yaml` **and** the matching `layout:` line per value in setting the enum in `types/source.schema.yaml` **and** the matching `layout:` line per value
`types/source.md` in the same edit - one without the other leaves a value with no target 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 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 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 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 `docs verify` additionally checks `profile:` and `required_by_stack:` on every
`COLLECTION.md`. `COLLECTION.md`.
5. **Decision point - personalization.** The release ships `USER.md.template` and 5. <!-- setup-question: personalization --> **Decision point - personalization.** The release ships
`SOUL.md.template`, but no filled-in versions: who operates this instance and how it sounds `USER.md.template` and `SOUL.md.template`, but no filled-in versions: who operates this instance
is the property of this instance alone and is never carried over from anywhere else. Both and how it sounds is the property of this instance alone and is never carried over from anywhere
files are read in **every** session from now on, so they come into being here - not later, else. Both files are read in **every** session from now on, so they come into being here - not
when the occasion arises. later, when the occasion arises.
Procedure, once each for `USER.md` and `SOUL.md`: Procedure, once each for `USER.md` and `SOUL.md`:
@@ -248,9 +249,9 @@ one it needs before that.
tools/wikitool instructions sync tools/wikitool instructions sync
``` ```
7. **Decision point - record the environment.** The release ships `ENVIRONMENT.md.template`: 7. <!-- setup-question: environment --> **Decision point - record the environment.** The release
harness, published skills, reachable MCP servers, connectors, git remotes, where CI runs. ships `ENVIRONMENT.md.template`: harness, published skills, reachable MCP servers, connectors,
Constants a session would otherwise ask about every time. 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 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 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 `environment: absent (optional)` in step 12, not a `FAIL`. The file is gitignored and enters
no commit - it describes this checkout, not the repo. no commit - it describes this checkout, not the repo.
8. **Decision point - telemetry.** Every instance installed from a release carries a 8. <!-- setup-question: telemetry --> **Decision point - telemetry.** Every instance installed from
`.wikitool-release.json` and starts with telemetry **off**; nobody asked for it, and nobody a release carries a `.wikitool-release.json` and starts with telemetry **off**; nobody asked for
reads `EVALS.md` before the first file is written anyway. This step only asks whether the it, and nobody reads `EVALS.md` before the first file is written anyway. This step only asks
operator wants to reverse that. whether the operator wants to reverse that.
Ask the user once: telemetry on? If yes, create `.wikitool-telemetry.json` in the repo root 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`): (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: never a `FAIL`, since both directions are a valid state. More on this:
[EVALS.md](../EVALS.md) § "Whether it runs at all". [EVALS.md](../EVALS.md) § "Whether it runs at all".
9. **Decision point - task tracker.** The instance ships the `project` type and the collection 9. <!-- setup-question: task-tracker --> **Decision point - task tracker.** The instance ships the
its type-spec's `base_dir:` names (`kb/gtd/` here), so committed initiatives have a page from `project` type and the collection its type-spec's `base_dir:` names (`kb/gtd/` here), so
the start. What they do *not* have until this step is the other half of the weekly review: committed initiatives have a page from the start. What they do *not* have until this step is the
the tracker that owns the open items, which `tools/wikitool review` joins those pages against other half of the weekly review: the tracker that owns the open items, which `tools/wikitool
over the project name. No tracker configured is a legitimate end state - the pages work review` joins those pages against over the project name. No tracker configured is a legitimate
alone, `review` simply says so and refuses - so ask rather than assume. 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` 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 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. 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 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 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. 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 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. 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 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 table-of-contents region is missing or stale
- 1 A reference file's relative markdown link does not resolve to an existing file - 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** **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 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 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 - 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** **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. - 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 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. - 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. - Read-only.
**SEE ALSO** **SEE ALSO**
- `wikitool docs toc` - regenerates tables of contents - `wikitool docs toc` - regenerates tables of contents
- `wikitool docs contract` - regenerates the commands region - `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/` - `wikitool instructions verify` - the same kind of check for `instructions/`
#### `docs toc` #### `docs toc`
@@ -2257,6 +2266,51 @@ Create, refresh or remove the generated table-of-contents region.
- `wikitool docs verify` - checks every region is current - `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` #### `docs contract`
Regenerate `tools/CONTRACT.md`'s `<!-- wikitool:commands -->` region. 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 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 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 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 corpus_cache.py one parsed corpus per commit, never cached while the tree is dirty
kb_scan.py page iteration/loading over kb/ kb_scan.py page iteration/loading over kb/
blocks.py generated regions in a page body, found by marker rather than by heading 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, instructions and docs", (
"types list", "types describe", "types list", "types describe",
"instructions sync", "instructions verify", "instructions list", "instructions sync", "instructions verify", "instructions list",
"docs verify", "docs toc", "docs contract", "docs verify", "docs toc", "docs prerequisites", "docs contract",
)), )),
("Telemetry", ( ("Telemetry", (
"eval sessions", "eval score", "eval sessions", "eval score",
+100 -1
View File
@@ -68,7 +68,7 @@ from typing import Optional
import typer 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 import dist_cmd
from chemenu.commands._util import fail, rel_path, success 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 " "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 " "spans are masked before scanning, so link syntax shown as an example is not mistaken "
"for a real reference.", "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.", "Read-only.",
), ),
failures=( failures=(
@@ -1108,6 +1113,22 @@ def check_breaking_change_for_boundary() -> list[str]:
"file", "file",
reaction="Fix the `../` count or the target's name", 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=( examples=(
"tools/wikitool docs verify", "tools/wikitool docs verify",
@@ -1118,6 +1139,7 @@ def check_breaking_change_for_boundary() -> list[str]:
see_also=( see_also=(
"`wikitool docs toc` - regenerates tables of contents", "`wikitool docs toc` - regenerates tables of contents",
"`wikitool docs contract` - regenerates the commands region", "`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/`", "`wikitool instructions verify` - the same kind of check for `instructions/`",
), ),
)) ))
@@ -1139,6 +1161,8 @@ def verify():
+ check_no_issue_references() + check_no_issue_references()
+ check_toc_regions() + check_toc_regions()
+ check_reference_targets() + check_reference_targets()
+ install_doc.check_prerequisite_regions()
+ install_doc.check_setup_questions()
) )
if issues: if issues:
@@ -1155,6 +1179,8 @@ def verify():
f"no issue references in {len(shipped_prose())} shipped document(s) or command help, " 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"tables of contents current and every link resolving on "
f"{len(toc.target_files())} reference file(s), " 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"{version_mod.CHANGES_FILENAME} documents version "
f"{(config.ROOT / version_mod.VERSION_FILENAME).read_text(encoding='utf-8').strip()}." 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.") 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") @app.command("contract")
@cli_contract.record(cli_contract.CommandRecord( @cli_contract.record(cli_contract.CommandRecord(
path="docs contract", 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