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

+54
View File
@@ -132,6 +132,7 @@ instructions verify read idempotent budget:counted exit:0,1
instructions list read idempotent budget:counted exit:0 List the flat instructions with their descriptions.
docs verify read idempotent budget:counted exit:0,1 Check the docs that mirror the code.
docs toc write idempotent budget:counted exit:0 Create, refresh or remove the generated table-of-contents region.
docs prerequisites write idempotent budget:counted exit:0,1 Regenerate `INSTALL.md`'s prerequisites lists from `tools/prerequisites.txt`.
docs contract write idempotent budget:counted exit:0,1 Regenerate `tools/CONTRACT.md`'s `<!-- wikitool:commands -->` region.
eval sessions read idempotent budget:exempt exit:0 List the sessions that have a trace under `reports/telemetry/`.
eval score read idempotent budget:exempt exit:0,1 Score one traced session.
@@ -2182,6 +2183,9 @@ Check the docs that mirror the code.
- 1 A shipped `.md`/`.template` cites an issue number
- 1 A reference file's table-of-contents region is missing or stale
- 1 A reference file's relative markdown link does not resolve to an existing file
- 1 An `INSTALL.md` prerequisites region is stale
- 1 An `INSTALL.md` prerequisites region is missing, or names a platform no tool has
- 1 A setup question is marked in one of `instructions/setup-instance.md` and `INSTALL.md` but not the other
**ON FAILURE**
@@ -2191,6 +2195,9 @@ Check the docs that mirror the code.
- A shipped `.md`/`.template` cites an issue number -> Say what was decided instead of pointing at where, or move the pointer behind a `<!-- dist:strip-start/end -->` block
- A reference file's table-of-contents region is missing or stale -> Run `docs toc --apply`, then re-run
- A reference file's relative markdown link does not resolve to an existing file -> Fix the `../` count or the target's name
- An `INSTALL.md` prerequisites region is stale -> Run `docs prerequisites --apply`, then re-run
- An `INSTALL.md` prerequisites region is missing, or names a platform no tool has -> Add the marker pair where that list belongs (or remove the orphaned region and its introducing prose), then run `docs prerequisites --apply`
- A setup question is marked in one of `instructions/setup-instance.md` and `INSTALL.md` but not the other -> Describe the question for the human in `INSTALL.md` with the same marker, or remove the bullet for a question no longer asked
**NEVER**
@@ -2207,12 +2214,14 @@ Check the docs that mirror the code.
- No `.md`/`.template` file `dist export` would ship cites an issue number. A `<!-- dist:strip-start/end -->` region is exempt: the check reads the export plan's text, from which it is already gone.
- Every reference file `docs toc` covers carries the current table-of-contents region for its own headings - missing and stale are one check.
- Every relative markdown link in one of those reference files resolves to an existing file. A target's `#anchor` suffix is stripped first, and code fences and inline code spans are masked before scanning, so link syntax shown as an example is not mistaken for a real reference.
- `INSTALL.md` carries one generated `<!-- wikitool:prerequisites -->` region per platform value of `tools/prerequisites.txt` (`prerequisites-<platform>` for a platform-specific one), each current; and the `<!-- setup-question: <key> -->` markers in `instructions/setup-instance.md` and `INSTALL.md` name the same set of keys, so a question the agent asks is never one the human guide leaves out, nor the reverse.
- Read-only.
**SEE ALSO**
- `wikitool docs toc` - regenerates tables of contents
- `wikitool docs contract` - regenerates the commands region
- `wikitool docs prerequisites` - regenerates `INSTALL.md`'s prerequisites lists
- `wikitool instructions verify` - the same kind of check for `instructions/`
#### `docs toc`
@@ -2257,6 +2266,51 @@ Create, refresh or remove the generated table-of-contents region.
- `wikitool docs verify` - checks every region is current
#### `docs prerequisites`
Regenerate `INSTALL.md`'s prerequisites lists from `tools/prerequisites.txt`.
**SYNOPSIS**
- `wikitool docs prerequisites [--apply]`
**PROPERTIES**
- effect: write
- idempotent: yes
- atomic: Yes - every region is rewritten in one file write
- budget: counted
- network: no
**EXAMPLES**
- `tools/wikitool docs prerequisites`
- `tools/wikitool docs prerequisites --apply`
**EXIT STATUS**
- 0 success
- 1 `INSTALL.md` is missing, or lacks a region the manifest calls for
**ON FAILURE**
- `INSTALL.md` is missing, or lacks a region the manifest calls for -> Not transient - add the marker pair the message names where that list belongs (restore the file if it is gone), then retry
**NEVER**
- Never hand-edit a prerequisites region - change `tools/prerequisites.txt` and re-run this.
**NOTES**
- Rewrites each `<!-- wikitool:prerequisites -->` region in `INSTALL.md` (tools every platform needs) and `<!-- wikitool:prerequisites-<platform> -->` region (tools only that platform needs) from the manifest: one list item per tool, its label and minimum version. The manifest's reason field stays out - it is English prose, and the region sits in a document that need not be.
- Never places a region: where a list belongs in the human guide is that guide's own decision. A region the manifest calls for but the file lacks is an error naming the marker pair to add.
- Dry-run by default (says whether the file would change); `--apply` writes.
- `docs verify` checks the result stays current.
**SEE ALSO**
- `wikitool docs verify` - checks every region is current
#### `docs contract`
Regenerate `tools/CONTRACT.md`'s `<!-- wikitool:commands -->` region.
+1
View File
@@ -85,6 +85,7 @@ tools/
toolpaths.py where git and rg are started from: .wikitool-tools.json, bare name only without the file
filelock.py an exclusive lock on an open file, flock on POSIX and msvcrt on Windows - the only module that imports either
prerequisites.py prerequisites.txt read from Python, plus the platform and long-path questions `doctor` asks
install_doc.py INSTALL.md held to the instructions: its prerequisites lists generated from prerequisites.txt, its setup questions matched to setup-instance.md's markers
corpus_cache.py one parsed corpus per commit, never cached while the tree is dirty
kb_scan.py page iteration/loading over kb/
blocks.py generated regions in a page body, found by marker rather than by heading
+1 -1
View File
@@ -270,7 +270,7 @@ GROUPS: tuple[tuple[str, tuple[str, ...]], ...] = (
("Types, instructions and docs", (
"types list", "types describe",
"instructions sync", "instructions verify", "instructions list",
"docs verify", "docs toc", "docs contract",
"docs verify", "docs toc", "docs prerequisites", "docs contract",
)),
("Telemetry", (
"eval sessions", "eval score",
+100 -1
View File
@@ -68,7 +68,7 @@ from typing import Optional
import typer
from chemenu import blocks, cli_contract, config, conventions, kb_collections, markdown_code, toc, toolpaths, version as version_mod
from chemenu import blocks, cli_contract, config, conventions, install_doc, kb_collections, markdown_code, toc, toolpaths, version as version_mod
from chemenu.commands import dist_cmd
from chemenu.commands._util import fail, rel_path, success
@@ -1078,6 +1078,11 @@ def check_breaking_change_for_boundary() -> list[str]:
"file. A target's `#anchor` suffix is stripped first, and code fences and inline code "
"spans are masked before scanning, so link syntax shown as an example is not mistaken "
"for a real reference.",
"`INSTALL.md` carries one generated `<!-- wikitool:prerequisites -->` region per "
"platform value of `tools/prerequisites.txt` (`prerequisites-<platform>` for a "
"platform-specific one), each current; and the `<!-- setup-question: <key> -->` markers "
"in `instructions/setup-instance.md` and `INSTALL.md` name the same set of keys, so a "
"question the agent asks is never one the human guide leaves out, nor the reverse.",
"Read-only.",
),
failures=(
@@ -1108,6 +1113,22 @@ def check_breaking_change_for_boundary() -> list[str]:
"file",
reaction="Fix the `../` count or the target's name",
),
cli_contract.Failure(
cause="An `INSTALL.md` prerequisites region is stale",
reaction="Run `docs prerequisites --apply`, then re-run",
),
cli_contract.Failure(
cause="An `INSTALL.md` prerequisites region is missing, or names a platform no tool "
"has",
reaction="Add the marker pair where that list belongs (or remove the orphaned region "
"and its introducing prose), then run `docs prerequisites --apply`",
),
cli_contract.Failure(
cause="A setup question is marked in one of `instructions/setup-instance.md` and "
"`INSTALL.md` but not the other",
reaction="Describe the question for the human in `INSTALL.md` with the same marker, "
"or remove the bullet for a question no longer asked",
),
),
examples=(
"tools/wikitool docs verify",
@@ -1118,6 +1139,7 @@ def check_breaking_change_for_boundary() -> list[str]:
see_also=(
"`wikitool docs toc` - regenerates tables of contents",
"`wikitool docs contract` - regenerates the commands region",
"`wikitool docs prerequisites` - regenerates `INSTALL.md`'s prerequisites lists",
"`wikitool instructions verify` - the same kind of check for `instructions/`",
),
))
@@ -1139,6 +1161,8 @@ def verify():
+ check_no_issue_references()
+ check_toc_regions()
+ check_reference_targets()
+ install_doc.check_prerequisite_regions()
+ install_doc.check_setup_questions()
)
if issues:
@@ -1155,6 +1179,8 @@ def verify():
f"no issue references in {len(shipped_prose())} shipped document(s) or command help, "
f"tables of contents current and every link resolving on "
f"{len(toc.target_files())} reference file(s), "
f"{install_doc.INSTALL_DOC} in step with tools/prerequisites.txt and "
f"{install_doc.SETUP_INSTRUCTION}'s questions, "
f"{version_mod.CHANGES_FILENAME} documents version "
f"{(config.ROOT / version_mod.VERSION_FILENAME).read_text(encoding='utf-8').strip()}."
)
@@ -1235,6 +1261,79 @@ def toc_command(
typer.echo(f"\n{len(changed)} file(s) would change. Re-run with --apply to write.")
@app.command("prerequisites")
@cli_contract.record(cli_contract.CommandRecord(
path="docs prerequisites",
summary="Regenerate `INSTALL.md`'s prerequisites lists from `tools/prerequisites.txt`.",
synopsis=(cli_contract.Variant(usage="docs prerequisites [--apply]"),),
properties=cli_contract.Properties(
effect=cli_contract.Effect.WRITE,
idempotent=cli_contract.Idempotent.YES,
atomic="Yes - every region is rewritten in one file write",
budget=cli_contract.Budget.COUNTED,
),
notes=(
"Rewrites each `<!-- wikitool:prerequisites -->` region in `INSTALL.md` (tools every "
"platform needs) and `<!-- wikitool:prerequisites-<platform> -->` region (tools only that "
"platform needs) from the manifest: one list item per tool, its label and minimum "
"version. The manifest's reason field stays out - it is English prose, and the region "
"sits in a document that need not be.",
"Never places a region: where a list belongs in the human guide is that guide's own "
"decision. A region the manifest calls for but the file lacks is an error naming the "
"marker pair to add.",
"Dry-run by default (says whether the file would change); `--apply` writes.",
"`docs verify` checks the result stays current.",
),
failures=(cli_contract.Failure(
cause="`INSTALL.md` is missing, or lacks a region the manifest calls for",
reaction="Not transient - add the marker pair the message names where that list "
"belongs (restore the file if it is gone), then retry",
),),
examples=(
"tools/wikitool docs prerequisites",
"tools/wikitool docs prerequisites --apply",
),
never=(
"Never hand-edit a prerequisites region - change `tools/prerequisites.txt` and re-run "
"this.",
),
see_also=(
"`wikitool docs verify` - checks every region is current",
),
))
def prerequisites_command(
apply: bool = typer.Option(False, "--apply", help="Write changes; default is dry-run (preview only)"),
):
"""Regenerate `INSTALL.md`'s prerequisites lists from `tools/prerequisites.txt`."""
from chemenu import prerequisites
path = install_doc.install_doc_path()
if not path.is_file():
fail(f"{install_doc.INSTALL_DOC} is missing.")
text = path.read_text(encoding="utf-8")
after, missing = install_doc.refresh(text, prerequisites.load_manifest())
if missing:
fail(
f"{install_doc.INSTALL_DOC} lacks "
+ ", ".join(f"`{blocks.open_marker(name)}`" for name in missing)
+ " - add each marker pair (with its closing marker) where that list belongs, "
"then re-run."
)
if after == text:
success(f"{install_doc.INSTALL_DOC}'s prerequisites lists are already current.")
return
if not apply:
typer.echo(f"{install_doc.INSTALL_DOC} would change.")
typer.echo("Re-run with --apply to write.")
return
path.write_text(after, encoding="utf-8", newline="\n")
success(f"Regenerated the prerequisites lists in {install_doc.INSTALL_DOC}.")
@app.command("contract")
@cli_contract.record(cli_contract.CommandRecord(
path="docs contract",
+178
View File
@@ -0,0 +1,178 @@
"""The two places `INSTALL.md` overlaps the installation instructions, held to them.
`INSTALL.md` is written for a human and in the instance's KB language;
`instructions/setup-instance.md` is written for an agent and in English. A
single file serving both was rejected: an agent reads every sentence as an
instruction, and the human needs preparation and decisions where the agent
needs steps and exit codes. So the human document does not retell the
procedure, and what it still shares with the instructions is two enumerable
lists - both checked here, so neither can drift silently:
1. **What the machine needs.** `tools/prerequisites.txt` is the one list; the
preflight and `doctor` read it. `INSTALL.md` carries it as a generated
region per platform value (`<!-- wikitool:prerequisites -->` for `all`,
`<!-- wikitool:prerequisites-<platform> -->` otherwise), rendered from the
label and minimum version only. The manifest's `why` field stays out: it is
English prose, and the region sits in a document that is not. A region is
never placed by the tool - where it goes is the human document's own
decision - so a missing one is reported, not appended.
2. **What the agent asks the user.** Every place `setup-instance.md` puts a
question to the user carries `<!-- setup-question: <key> -->`, and the
bullet in `INSTALL.md` that tells the human about it carries the same
marker. The two key sets must be equal: a question the human guide does not
mention catches them unprepared, and one it mentions that is no longer asked
is a claim about a procedure that no longer exists. The key is the shared
identifier precisely because the surrounding prose is in two languages.
Everything else in `INSTALL.md` is prose no check reads;
`instructions/dev/doc-pull-through.md` names it as session work.
"""
from __future__ import annotations
import re
from pathlib import Path
from chemenu import blocks, config, prerequisites
INSTALL_DOC = "INSTALL.md"
SETUP_INSTRUCTION = "instructions/setup-instance.md"
REGION_PREFIX = "prerequisites"
QUESTION_RE = re.compile(r"<!-- setup-question: ([a-z][a-z0-9-]*) -->")
def install_doc_path() -> Path:
return config.ROOT / INSTALL_DOC
def setup_instruction_path() -> Path:
return config.ROOT / SETUP_INSTRUCTION
def region_name(platform: str) -> str:
"""`prerequisites` for the tools every platform needs, `prerequisites-<platform>`
for the ones only that platform does."""
return REGION_PREFIX if platform == "all" else f"{REGION_PREFIX}-{platform}"
def render_lines(tools: tuple[prerequisites.Tool, ...]) -> list[str]:
"""One list item per tool - label and minimum version, nothing in a language."""
return [
f"- **{tool.label}**" + (f" ≥ {tool.minimum}" if tool.minimum else "")
for tool in tools
]
def expected_regions(manifest: prerequisites.Manifest) -> dict[str, str]:
"""Region name -> the whole region, markers included, in manifest order of
first appearance of each platform value."""
platforms: list[str] = []
for tool in manifest.tools:
if tool.platforms not in platforms:
platforms.append(tool.platforms)
regions = {}
for platform in platforms:
name = region_name(platform)
tools = tuple(t for t in manifest.tools if t.platforms == platform)
regions[name] = "\n".join(
[blocks.open_marker(name), *render_lines(tools), blocks.close_marker(name)]
)
return regions
def _region_span(text: str, name: str) -> tuple[int, int] | None:
"""Start and end offset of `name`'s region, markers included - without the
blank lines `blocks` takes along, so a refresh leaves the prose around it
exactly as it was."""
start = text.find(blocks.open_marker(name))
if start < 0:
return None
close = blocks.close_marker(name)
end = text.find(close, start)
if end < 0:
return None
return start, end + len(close)
def refresh(text: str, manifest: prerequisites.Manifest) -> tuple[str, list[str]]:
"""`text` with every prerequisites region it carries rewritten from the
manifest, plus the names of the regions it lacks.
A region the manifest no longer has a platform for is left alone and
reported by `check_prerequisite_regions`, not deleted here: the prose
introducing it ("Unter Windows zusätzlich:") would otherwise be left
standing over nothing.
"""
missing = []
for name, region in expected_regions(manifest).items():
span = _region_span(text, name)
if span is None:
missing.append(name)
continue
text = text[: span[0]] + region + text[span[1]:]
return text, missing
def _present_region_names(text: str) -> set[str]:
pattern = re.compile(
rf"<!-- wikitool:({re.escape(REGION_PREFIX)}(?:-[a-z0-9-]+)?) -->"
)
return set(pattern.findall(text))
def check_prerequisite_regions() -> list[str]:
"""`INSTALL.md` carries one current region per platform value of the manifest."""
path = install_doc_path()
if not path.is_file():
return [f"{INSTALL_DOC} is missing - the human installation guide is a stack file"]
text = path.read_text(encoding="utf-8")
manifest = prerequisites.load_manifest()
refreshed, missing = refresh(text, manifest)
issues = [
f"{INSTALL_DOC} has no `{blocks.open_marker(name)}` region - add the marker pair "
f"where that list belongs, then run `wikitool docs prerequisites --apply`"
for name in missing
]
issues += [
f"{INSTALL_DOC} carries `{blocks.open_marker(name)}`, but no tool in "
"tools/prerequisites.txt has that platform - remove the region and the prose "
"introducing it"
for name in sorted(_present_region_names(text) - set(expected_regions(manifest)))
]
if refreshed != text:
issues.append(
f"{INSTALL_DOC}'s prerequisites list no longer matches tools/prerequisites.txt - "
"run `wikitool docs prerequisites --apply`"
)
return issues
def question_keys(text: str) -> set[str]:
return set(QUESTION_RE.findall(text))
def check_setup_questions() -> list[str]:
"""Every question `setup-instance.md` asks is named in `INSTALL.md`, and
`INSTALL.md` names no question that is no longer asked."""
setup = setup_instruction_path()
install = install_doc_path()
if not setup.is_file() or not install.is_file():
# A missing INSTALL.md is check_prerequisite_regions' finding; a
# missing setup instruction leaves nothing to compare against.
return []
asked = question_keys(setup.read_text(encoding="utf-8"))
named = question_keys(install.read_text(encoding="utf-8"))
issues = [
f"{SETUP_INSTRUCTION} asks the user `{key}`, but {INSTALL_DOC} § \"Was der Agent dich "
f"fragt\" does not name it - add a bullet carrying `<!-- setup-question: {key} -->`"
for key in sorted(asked - named)
]
issues += [
f"{INSTALL_DOC} names the setup question `{key}`, which {SETUP_INSTRUCTION} no longer "
"asks - remove the bullet, or restore the marker where the question is asked"
for key in sorted(named - asked)
]
return issues
+173
View File
@@ -0,0 +1,173 @@
from typer.testing import CliRunner
from chemenu import config, install_doc, prerequisites
from chemenu.cli import app
runner = CliRunner()
MANIFEST = prerequisites.Manifest(
limits={},
tools=(
prerequisites.Tool("python", "3.11", "all", "Python", "why"),
prerequisites.Tool("git", None, "all", "Git", "why"),
prerequisites.Tool("pwsh", "7", "windows", "PowerShell 7 (pwsh)", "why"),
),
)
CURRENT = """# Installation
Vorher:
<!-- wikitool:prerequisites -->
- **Python** ≥ 3.11
- **Git**
<!-- /wikitool:prerequisites -->
Unter Windows zusätzlich:
<!-- wikitool:prerequisites-windows -->
- **PowerShell 7 (pwsh)** ≥ 7
<!-- /wikitool:prerequisites-windows -->
## Was der Agent dich fragt
- <!-- setup-question: identity --> **Autor-Identität** - Name und E-Mail.
"""
SETUP = """# Set up
2. <!-- setup-question: identity --> **Decision point - identity.** Ask the user.
"""
def _tree(tmp_path, monkeypatch, install=CURRENT, setup=SETUP, manifest=MANIFEST):
monkeypatch.setattr(config, "ROOT", tmp_path)
monkeypatch.setattr(prerequisites, "load_manifest", lambda path=None: manifest)
(tmp_path / "instructions").mkdir()
(tmp_path / "INSTALL.md").write_text(install, encoding="utf-8")
(tmp_path / "instructions" / "setup-instance.md").write_text(setup, encoding="utf-8")
return tmp_path
def test_this_repos_install_doc_matches_the_manifest():
assert install_doc.check_prerequisite_regions() == []
def test_this_repos_install_doc_names_every_setup_question():
assert install_doc.check_setup_questions() == []
def test_this_repos_setup_instruction_marks_its_questions():
"""The check compares two sets; an instruction that lost every marker would
compare empty against empty and pass. Guard the real file's own count."""
text = install_doc.setup_instruction_path().read_text(encoding="utf-8")
assert install_doc.question_keys(text) >= {
"identity", "remote", "kb-language", "domain",
"personalization", "environment", "telemetry", "task-tracker",
}
def test_render_lines_are_label_and_minimum_only():
assert install_doc.render_lines(MANIFEST.tools) == [
"- **Python** ≥ 3.11",
"- **Git**",
"- **PowerShell 7 (pwsh)** ≥ 7",
]
def test_a_current_install_doc_passes(tmp_path, monkeypatch):
_tree(tmp_path, monkeypatch)
assert install_doc.check_prerequisite_regions() == []
assert install_doc.check_setup_questions() == []
def test_a_manifest_tool_missing_from_install_doc_fails(tmp_path, monkeypatch):
"""Acceptance criterion: a tool in the manifest that INSTALL.md lacks fails `docs verify`."""
_tree(tmp_path, monkeypatch, install=CURRENT.replace("- **Git**\n", ""))
issues = install_doc.check_prerequisite_regions()
assert any("no longer matches tools/prerequisites.txt" in issue for issue in issues)
def test_a_new_platform_without_a_region_fails(tmp_path, monkeypatch):
manifest = prerequisites.Manifest(
limits={},
tools=MANIFEST.tools + (prerequisites.Tool("brew", None, "macos", "Homebrew", "why"),),
)
_tree(tmp_path, monkeypatch, manifest=manifest)
issues = install_doc.check_prerequisite_regions()
assert any("<!-- wikitool:prerequisites-macos -->" in issue for issue in issues)
def test_a_region_for_a_platform_no_tool_has_fails(tmp_path, monkeypatch):
manifest = prerequisites.Manifest(limits={}, tools=MANIFEST.tools[:2])
_tree(tmp_path, monkeypatch, manifest=manifest)
issues = install_doc.check_prerequisite_regions()
assert any("prerequisites-windows" in issue and "no tool" in issue for issue in issues)
def test_a_missing_install_doc_is_reported(tmp_path, monkeypatch):
_tree(tmp_path, monkeypatch)
(tmp_path / "INSTALL.md").unlink()
assert any("missing" in issue for issue in install_doc.check_prerequisite_regions())
assert install_doc.check_setup_questions() == []
def test_refresh_touches_nothing_outside_the_regions(tmp_path):
stale = CURRENT.replace("≥ 3.11", "≥ 3.9")
refreshed, missing = install_doc.refresh(stale, MANIFEST)
assert missing == []
assert refreshed == CURRENT
def test_refresh_is_idempotent():
once, _ = install_doc.refresh(CURRENT, MANIFEST)
twice, _ = install_doc.refresh(once, MANIFEST)
assert once == twice == CURRENT
def test_a_question_the_install_doc_does_not_name_fails(tmp_path, monkeypatch):
"""Acceptance criterion: a new question in setup-instance.md that INSTALL.md
does not name fails `docs verify`."""
setup = SETUP + "\n3. <!-- setup-question: remote --> **Decision point - remote.**\n"
_tree(tmp_path, monkeypatch, setup=setup)
issues = install_doc.check_setup_questions()
assert len(issues) == 1
assert "`remote`" in issues[0] and "does not name it" in issues[0]
def test_a_question_no_longer_asked_fails(tmp_path, monkeypatch):
install = CURRENT + "- <!-- setup-question: telemetry --> **Telemetrie**\n"
_tree(tmp_path, monkeypatch, install=install)
issues = install_doc.check_setup_questions()
assert len(issues) == 1
assert "`telemetry`" in issues[0] and "no longer asks" in issues[0]
def test_docs_prerequisites_is_a_dry_run_by_default(tmp_path, monkeypatch):
stale = CURRENT.replace("≥ 3.11", "≥ 3.9")
root = _tree(tmp_path, monkeypatch, install=stale)
result = runner.invoke(app, ["docs", "prerequisites"])
assert result.exit_code == 0, result.output
assert "would change" in result.output
assert (root / "INSTALL.md").read_text(encoding="utf-8") == stale
def test_docs_prerequisites_apply_rewrites_the_regions(tmp_path, monkeypatch):
root = _tree(tmp_path, monkeypatch, install=CURRENT.replace("≥ 3.11", "≥ 3.9"))
result = runner.invoke(app, ["docs", "prerequisites", "--apply"])
assert result.exit_code == 0, result.output
assert (root / "INSTALL.md").read_text(encoding="utf-8") == CURRENT
assert install_doc.check_prerequisite_regions() == []
def test_docs_prerequisites_never_places_a_missing_region(tmp_path, monkeypatch):
without_windows = CURRENT.replace(
"<!-- wikitool:prerequisites-windows -->\n- **PowerShell 7 (pwsh)** ≥ 7\n"
"<!-- /wikitool:prerequisites-windows -->\n",
"",
)
root = _tree(tmp_path, monkeypatch, install=without_windows)
result = runner.invoke(app, ["docs", "prerequisites", "--apply"])
assert result.exit_code == 1
assert "prerequisites-windows" in result.output
assert (root / "INSTALL.md").read_text(encoding="utf-8") == without_windows