diff --git a/CHANGES.md b/CHANGES.md index f88a9c4..4c53ac4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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 +### 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` - `` for every platform, + `` 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 `` (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 diff --git a/INSTALL.md b/INSTALL.md index 4734d79..aebb1cf 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -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: + + +- **Python** ≥ 3.11 +- **Git** +- **ripgrep (rg)** + + +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. + +- **PowerShell 7 (pwsh)** ≥ 7 + + +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: . 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//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). +- **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//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). 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 diff --git a/VERSION b/VERSION index 3edb8e0..8fe62bc 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.0.0-beta.20 +8.0.0-beta.21 diff --git a/instructions/dev/doc-pull-through.md b/instructions/dev/doc-pull-through.md index cd982f9..6fc9b35 100644 --- a/instructions/dev/doc-pull-through.md +++ b/instructions/dev/doc-pull-through.md @@ -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//` or `instructions/dev//` | | 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 `` 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 diff --git a/instructions/dev/stack-close/SKILL.md b/instructions/dev/stack-close/SKILL.md index ef00246..2d2e834 100644 --- a/instructions/dev/stack-close/SKILL.md +++ b/instructions/dev/stack-close/SKILL.md @@ -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 diff --git a/instructions/setup-instance.md b/instructions/setup-instance.md index 01fe9e1..2bbd1b9 100644 --- a/instructions/setup-instance.md +++ b/instructions/setup-instance.md @@ -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. **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 "" @@ -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. **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 ` - 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. 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. 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. **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. **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. **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. **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 diff --git a/tools/CONTRACT.md b/tools/CONTRACT.md index a2949ca..974e483 100644 --- a/tools/CONTRACT.md +++ b/tools/CONTRACT.md @@ -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 `` 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 `` 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 `` 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 `` region per platform value of `tools/prerequisites.txt` (`prerequisites-` for a platform-specific one), each current; and the `` 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 `` region in `INSTALL.md` (tools every platform needs) and `` 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 `` region. diff --git a/tools/README.md b/tools/README.md index ad88e34..1b0e2e5 100644 --- a/tools/README.md +++ b/tools/README.md @@ -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 diff --git a/tools/chemenu/cli_contract.py b/tools/chemenu/cli_contract.py index 4c136b3..6ff914b 100644 --- a/tools/chemenu/cli_contract.py +++ b/tools/chemenu/cli_contract.py @@ -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", diff --git a/tools/chemenu/commands/docs_verify.py b/tools/chemenu/commands/docs_verify.py index 6e3b606..b159b35 100644 --- a/tools/chemenu/commands/docs_verify.py +++ b/tools/chemenu/commands/docs_verify.py @@ -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 `` region per " + "platform value of `tools/prerequisites.txt` (`prerequisites-` for a " + "platform-specific one), each current; and the `` 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 `` region in `INSTALL.md` (tools every " + "platform needs) and `` 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", diff --git a/tools/chemenu/install_doc.py b/tools/chemenu/install_doc.py new file mode 100644 index 0000000..c8c887a --- /dev/null +++ b/tools/chemenu/install_doc.py @@ -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 (`` for `all`, + `` 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 ``, 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"") + + +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-` + 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"" + ) + 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 ``" + 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 diff --git a/tools/chemenu/tests/test_install_doc.py b/tools/chemenu/tests/test_install_doc.py new file mode 100644 index 0000000..4873252 --- /dev/null +++ b/tools/chemenu/tests/test_install_doc.py @@ -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: + + +- **Python** ≥ 3.11 +- **Git** + + +Unter Windows zusätzlich: + + +- **PowerShell 7 (pwsh)** ≥ 7 + + +## Was der Agent dich fragt + +- **Autor-Identität** - Name und E-Mail. +""" + +SETUP = """# Set up + +2. **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("" 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. **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 + "- **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( + "\n- **PowerShell 7 (pwsh)** ≥ 7\n" + "\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