feat: INSTALL.md held to the installation instructions - prerequisites lists generated from the manifest, setup questions checked by docs verify (#154)
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:
1 parent
b33088f64e
commit
c77bda2004
12 files changed
+633
-69
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in new issue
Block a user