feat!: docs/ sorted into tutorials, how-to, reference, explanation plus unshipped docs/dev/; explanation pages and developer guide moved, human docs English (#187)
The seven explanation pages move to docs/explanation/ without a stub, DEVELOPMENT.md becomes docs/dev/development.md (translated to English), dist export leaves docs/dev/ out, docs verify rejects a file outside the five areas, and AGENTS.md states that the stack's human documentation is English. Required migration 9.0.0-docs-diataxis rewrites the old path in an instance's own kb/CONVENTIONS.md. Files changed: - .wikitool-kb.json - AGENTS.md - CHANGES.md - CLAUDE.md - DEVELOPMENT.md - INSTALL.md - README.md - VERSION - docs/dev/development.md - docs/explanation/README.md - docs/explanation/knowledge-and-commitment.md - docs/explanation/language-boundaries.md - docs/explanation/model-and-effort-selection.md - docs/explanation/ownership-and-templates.md - docs/explanation/pipeline-rationale.md - docs/explanation/version-model.md - docs/explanation/why-gates-are-code.md - docs/how-to/README.md - docs/knowledge-and-commitment.md - docs/language-boundaries.md - docs/model-and-effort-selection.md - docs/ownership-and-templates.md - docs/pipeline-rationale.md - docs/reference/README.md - docs/tutorials/README.md - docs/version-model.md - docs/why-gates-are-code.md - instructions/CONTRACT.md - instructions/dev/dev-setup.md - instructions/dev/doc-pull-through.md - instructions/dev/stack-close/SKILL.md - instructions/dev/stack-mode.md - instructions/gtd-weekly-review/SKILL.md - instructions/ingest-queue.md - instructions/migrations/6.0.0-type-guidance-split.md - instructions/migrations/9.0.0-docs-diataxis.md - instructions/upgrade-instance.md - kb/CONVENTIONS.md - kb/CONVENTIONS.md.template - tools/CONTRACT.md - tools/chemenu/commands/dist_cmd.py - tools/chemenu/commands/docs_verify.py - tools/chemenu/commands/new_page.py - tools/chemenu/commands/task_cmd.py - tools/chemenu/tasks/protocol.py - tools/chemenu/tests/test_dist_cmd.py - tools/chemenu/tests/test_docs_verify.py - tools/chemenu/tests/test_git_publish.py - tools/chemenu/tests/test_toc.py - types/type-guidance.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SnAJ7Z3CpVD3PRbN73QtU2
This commit is contained in:
1 parent
0e15972ee7
commit
d66a3f65f4
43 files changed
+756
-324
No files matched your search
+7
-4
@@ -241,7 +241,7 @@ Scaffold a new wiki page of any type.
|
||||
- `wikitool types describe <type>` - what a type requires and where it lands
|
||||
- `wikitool touch` - changes a page's own frontmatter afterwards
|
||||
- `wikitool task new` - a tracker item without a page
|
||||
- `docs/knowledge-and-commitment.md` - why a project is a page and a tracker project
|
||||
- `docs/explanation/knowledge-and-commitment.md` - why a project is a page and a tracker project
|
||||
|
||||
#### `task new`
|
||||
|
||||
@@ -307,7 +307,7 @@ Create one open item in the configured task tracker - never a kb/ page.
|
||||
- `wikitool task list` - a project's open items and their ids
|
||||
- `wikitool task close` - marks an item done
|
||||
- `wikitool new project` - a project page and its tracker project
|
||||
- `docs/knowledge-and-commitment.md` - why commitments live in the tracker, not in `kb/`
|
||||
- `docs/explanation/knowledge-and-commitment.md` - why commitments live in the tracker, not in `kb/`
|
||||
|
||||
#### `task list`
|
||||
|
||||
@@ -2511,6 +2511,7 @@ Check the docs that mirror the code.
|
||||
- 1 A subtype template `types/<type>.<value>.md` has no type-spec with a `subtype_field:` beside it, names a value outside that field's enum, or carries frontmatter
|
||||
- 1 A shipped `.md`/`.template` cites an issue number
|
||||
- 1 A reference file's table-of-contents region is missing or stale
|
||||
- 1 A file sits directly in `docs/`, or in a directory there that is not one of its five areas
|
||||
- 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
|
||||
@@ -2526,6 +2527,7 @@ Check the docs that mirror the code.
|
||||
- A subtype template `types/<type>.<value>.md` has no type-spec with a `subtype_field:` beside it, names a value outside that field's enum, or carries frontmatter -> Rename the file to the type and value it was meant for, delete it, or remove its frontmatter block
|
||||
- A shipped `.md`/`.template` cites an issue number -> Say what was decided instead of pointing at where, or move the pointer behind a `<!-- dist:strip-start/end -->` block
|
||||
- A reference file's table-of-contents region is missing or stale -> Run `docs toc --apply`, then re-run
|
||||
- A file sits directly in `docs/`, or in a directory there that is not one of its five areas -> Move it (`git mv`) into the area its kind of documentation belongs to, and fix the links that pointed at the old path
|
||||
- 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`
|
||||
@@ -2537,7 +2539,7 @@ Check the docs that mirror the code.
|
||||
|
||||
**NOTES**
|
||||
|
||||
- Checks the docs that mirror the code. The name is about documentation parity, not the `docs/` directory - it neither reads nor requires one.
|
||||
- Checks the docs that mirror the code. The name is about documentation parity, not the `docs/` directory - it reads that only for its layout, and never requires one.
|
||||
- Commands: every command has a `cli_contract` record and is listed in `cli_contract.GROUPS`, in both directions; every command's non-hidden flags appear in its record's SYNOPSIS and vice versa; `tools/CONTRACT.md`'s generated `<!-- wikitool:commands -->` region matches what `docs contract` would write; no command's rendered `--help`/`-h` text cites an issue number.
|
||||
- Collections: every directory under `kb/` has a `COLLECTION.md` and no directory outside it does; every collection declares `profile:` and a `required_by_stack:` that agrees with the stack's own list.
|
||||
- Types: every type the stack lists (currently `source` and `project`) has a type-spec of that name whose schema requires the field the stack list names (`raw_files:`/`state:`); every file under `types/` declaring `type: types/type-spec.md` validates against `types/type-spec.schema.yaml`; every subtype template `types/<type>.<value>.md` (any `<value>` but `guidance`) sits beside a type-spec declaring `subtype_field:`, names a value that field's schema enum allows, and carries no frontmatter; no pre-migration `type: entity` blocks are left in the contracts.
|
||||
@@ -2546,6 +2548,7 @@ 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.
|
||||
- Every file under `docs/` sits in one of its areas - `tutorials/`, `how-to/`, `reference/`, `explanation/` or `dev/` - never directly in `docs/` or in another directory.
|
||||
- `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.
|
||||
- `VERSION` parses and the newest versioned `CHANGES.md` entry names it. An entry that crosses the compatibility boundary from the last release carries `**Breaking Change:**`, has a migration document or `**Migration:** none required`, and its `**Migration:**` line names every required migration document targeting it.
|
||||
- The newest versioned `CHANGES.md` entry is at most 32 000 bytes (UTF-8) as `version notes` prints it, and each of its `###` sections is exactly one paragraph of at most 1 000 bytes, heading line excluded. Older entries and a changelog with no versioned entry are not checked.
|
||||
@@ -2818,7 +2821,7 @@ Write a contentless, distributable copy of this repo's machinery.
|
||||
**NOTES**
|
||||
|
||||
- Writes a contentless, distributable copy of this repo's machinery into an empty or new `<target>` directory.
|
||||
- Ships `AGENTS.md`/`README.md`/`EVALS.md` with any `dist:strip-start`...`dist:strip-end` marker region removed, `instructions/` (minus `instructions/dev/`), `types/` (the `root: kb` page type-specs, their schemas and their subtype templates `types/<type>.<value>.md` re-keyed as `.template`, the stack's own verbatim), `docs/` verbatim, `tools/` (no venv/caches, and not the dev-only test suite `tools/chemenu/tests/` with its `pytest.ini`/`.coveragerc`), the `.github/hooks/`+`.vibe/` session-tracing config plus `.claude/settings.json`, `kb/CONTRACT.md` (no pages, no areas) and `VERSION`.
|
||||
- Ships `AGENTS.md`/`README.md`/`EVALS.md` with any `dist:strip-start`...`dist:strip-end` marker region removed, `instructions/` (minus `instructions/dev/`), `types/` (the `root: kb` page type-specs, their schemas and their subtype templates `types/<type>.<value>.md` re-keyed as `.template`, the stack's own verbatim), `docs/` verbatim (minus `docs/dev/`), `tools/` (no venv/caches, and not the dev-only test suite `tools/chemenu/tests/` with its `pytest.ini`/`.coveragerc`), the `.github/hooks/`+`.vibe/` session-tracing config plus `.claude/settings.json`, `kb/CONTRACT.md` (no pages, no areas) and `VERSION`.
|
||||
- Ships `raw/` and `incoming/` as flat roots, each with a `.gitkeep` and no subdirectories. `incoming/.gitkeep` is trackable and survives becoming a git repository, so a plain clone gets the directory without any bootstrap step.
|
||||
- Ships templates, never the filled files: `USER.md.template`/`SOUL.md.template`, `kb/CONVENTIONS.md.template`, and each collection's contract re-keyed as `kb/<name>/COLLECTION.md.template`. The filled `USER.md`/`SOUL.md`/`kb/CONVENTIONS.md`/`kb/<name>/COLLECTION.md`/`types/<page-type>.md`/`types/<page-type>.<value>.md` bind their instance; `find_leaks` refuses a plan carrying one.
|
||||
- Writes a generated `.wikitool-release.json` stamp: version, export date, origin, and a sha256 per exported file - the base a later upgrade compares against.
|
||||
|
||||
@@ -93,14 +93,10 @@ DIST_TEMPLATES_DIR = Path(__file__).resolve().parent.parent / "dist_templates"
|
||||
# optional. A distribution whose server is present but undocumented is one
|
||||
# whose operator finds the module by reading the source.
|
||||
#
|
||||
# `DEVELOPMENT.md` is deliberately **absent** from this tuple, unlike every
|
||||
# other root doc above. It documents the release workflow (`version bump` ->
|
||||
# `version release` -> `publish` -> CI tags) and points at `instructions/dev/`,
|
||||
# which this same function excludes wholesale a few lines down - a distributed
|
||||
# instance has no release workflow, no CI and no issue board, so it has
|
||||
# nothing for that document to describe. Do not "fix" this by adding it back:
|
||||
# a root file absent from ROOT_FILES is silently skipped by every export, and
|
||||
# that silence is the correct behaviour here, not a gap.
|
||||
# The developer documentation (the release workflow, the development
|
||||
# environment) is not a root file at all: it lives under `docs/dev/`, which
|
||||
# `build_plan` excludes the way it excludes `instructions/dev/` - see
|
||||
# DOCS_DEV_ONLY.
|
||||
ROOT_FILES = (
|
||||
"AGENTS.md", "CLAUDE.md", "README.md", "EVALS.md", "INSTALL.md", "INSTALL-MCP.md",
|
||||
".gitignore", ".gitattributes", "VERSION",
|
||||
@@ -156,6 +152,15 @@ def _is_coverage_output(filename: str) -> bool:
|
||||
# reconstructs it afterwards, unlike the marker-block content below.
|
||||
INSTRUCTIONS_EXCLUDE_DIRS = {"dev"}
|
||||
|
||||
# docs/dev/ is the human counterpart of instructions/dev/: the release workflow
|
||||
# (`version bump` -> `version release` -> `publish` -> CI tags) and the
|
||||
# development environment, linking into instructions/dev/ and citing issue
|
||||
# numbers. A distributed instance has no release workflow, no CI and no issue
|
||||
# board, so it has nothing for those pages to describe. Excluded by exact path,
|
||||
# not by name: only the top-level `dev` area is developer material, and a
|
||||
# directory of that name deeper in another area would be ordinary documentation.
|
||||
DOCS_DEV_ONLY = frozenset({"dev"})
|
||||
|
||||
# Stage contracts that are not collections and carry no pages: copied as a
|
||||
# single file each, nothing else from their directory. `kb/` is excluded here
|
||||
# - it is a content stage too, but it has collections underneath it, so its
|
||||
@@ -341,7 +346,7 @@ def instance_owned_type_stems() -> set[str]:
|
||||
authoring guidance around them addresses an agent and stays English like the
|
||||
rest of the control plane (AGENTS.md § File naming, types/type-spec.md
|
||||
§ Who owns a type-spec). That the two halves share one file, and what it
|
||||
costs, is docs/ownership-and-templates.md § Where the file boundary strains.
|
||||
costs, is docs/explanation/ownership-and-templates.md § Where the file boundary strains.
|
||||
|
||||
Read from `types/` rather than listed, so an instance adding its own page
|
||||
type gets the same treatment without a code change.
|
||||
@@ -462,11 +467,12 @@ def build_plan(origin: Optional[Origin] = None) -> dict[str, PlannedFile]:
|
||||
for hook_dir in HOOK_DIRS:
|
||||
plan.update(_copy_tree(config.ROOT / hook_dir, hook_dir, frozenset()))
|
||||
|
||||
# docs/ is stack background - why the stack is built the way it is - and
|
||||
# ships verbatim like instructions/ and types/: it carries no page, no
|
||||
# frontmatter, and (AGENTS.md § File naming) no normative sentence, so
|
||||
# there is nothing instance-owned in it to split off as a .template.
|
||||
plan.update(_copy_tree(config.ROOT / "docs", "docs", frozenset()))
|
||||
# docs/ is the stack's human documentation - tutorials, how-to, reference
|
||||
# and explanation - and ships verbatim like instructions/ and types/: it
|
||||
# carries no page, no frontmatter, and (AGENTS.md § File naming) no
|
||||
# normative sentence, so there is nothing instance-owned in it to split off
|
||||
# as a .template. Its developer area is the one exception, see DOCS_DEV_ONLY.
|
||||
plan.update(_copy_tree(config.ROOT / "docs", "docs", frozenset(), exclude_paths=DOCS_DEV_ONLY))
|
||||
|
||||
# `kb/CONTRACT.md` is stack-owned and ships verbatim; everything beside it
|
||||
# under `kb/` is the instance's own and ships only as a `.template`. That is
|
||||
@@ -629,9 +635,9 @@ def _write_plan(target: Path, plan: dict[str, PlannedFile]) -> None:
|
||||
"Ships `AGENTS.md`/`README.md`/`EVALS.md` with any `dist:strip-start`...`dist:strip-end` "
|
||||
"marker region removed, `instructions/` (minus `instructions/dev/`), `types/` (the "
|
||||
"`root: kb` page type-specs, their schemas and their subtype templates "
|
||||
"`types/<type>.<value>.md` re-keyed as `.template`, the stack's own verbatim), `docs/` verbatim, `tools/` (no "
|
||||
"venv/caches, and not the dev-only test suite `tools/chemenu/tests/` with its "
|
||||
"`pytest.ini`/`.coveragerc`), the "
|
||||
"`types/<type>.<value>.md` re-keyed as `.template`, the stack's own verbatim), `docs/` "
|
||||
"verbatim (minus `docs/dev/`), `tools/` (no venv/caches, and not the dev-only test "
|
||||
"suite `tools/chemenu/tests/` with its `pytest.ini`/`.coveragerc`), the "
|
||||
"`.github/hooks/`+`.vibe/` session-tracing config plus `.claude/settings.json`, "
|
||||
"`kb/CONTRACT.md` (no pages, no areas) and `VERSION`.",
|
||||
"Ships `raw/` and `incoming/` as flat roots, each with a `.gitkeep` and no "
|
||||
@@ -709,7 +715,7 @@ def export_command(
|
||||
):
|
||||
"""Export a contentless, distributable copy of this repo's machinery:
|
||||
AGENTS.md/README.md (dev-instance-only marker blocks removed),
|
||||
instructions/ (no instructions/dev/), types/, docs/ verbatim,
|
||||
instructions/ (no instructions/dev/), types/, docs/ verbatim (no docs/dev/),
|
||||
tools/ (no venv/caches),
|
||||
the .github/hooks/+.vibe session-tracing config plus .claude/settings.json,
|
||||
kb/CONTRACT.md plus a COLLECTION.md.template per collection and
|
||||
|
||||
@@ -56,6 +56,12 @@ contract nothing enforced - the exact "checked or absent" failure this file's
|
||||
opening paragraph names, just one level up, for the schema that describes the
|
||||
type layer instead of a copy the type layer's code produces (Gitea #105).
|
||||
|
||||
A ninth checks a layout rather than a copy or a reference: every file under
|
||||
`docs/` sits in exactly one of its five areas - the four kinds of
|
||||
documentation, plus the unshipped `docs/dev/`. A page dropped straight into
|
||||
`docs/` belongs to no kind, and its reader cannot tell from the path what it
|
||||
is for.
|
||||
|
||||
Everything here is a hard oracle: a set comparison or a regex, no judgment.
|
||||
Content quality of the contracts themselves stays with the LLM.
|
||||
"""
|
||||
@@ -202,16 +208,17 @@ ROOT_README = config.ROOT / "README.md"
|
||||
# same reason: it is human-facing prose about installing an instance, and the
|
||||
# command reference lives exactly once, in tools/CONTRACT.md.
|
||||
#
|
||||
# DEVELOPMENT.md joined them after it drifted the same way (Gitea #47): it grew
|
||||
# a table describing what each verify command checks, which had to be removed by
|
||||
# hand because nothing compared it to anything. It is not shipped - dist_cmd
|
||||
# .ROOT_FILES excludes it - and that is not an argument against listing it here:
|
||||
# The developer guide joined them after it drifted the same way (Gitea #47, as
|
||||
# the root `DEVELOPMENT.md` it was then): it grew a table describing what each
|
||||
# verify command checks, which had to be removed by hand because nothing
|
||||
# compared it to anything. It is not shipped - `dist export` excludes
|
||||
# `docs/dev/` - and that is not an argument against listing it here:
|
||||
# `check_readmes_have_no_command_table` skips a file that does not exist, so in
|
||||
# a distributed instance this entry is simply inert, while in the dev checkout
|
||||
# (the only place the file exists, and the only place it can drift) it is
|
||||
# checked. The name is now narrower than the tuple - these are the human-facing
|
||||
# prose docs that must not re-list commands, stage README or not.
|
||||
STAGE_READMES = ("tools/README.md", "INSTALL.md", "DEVELOPMENT.md")
|
||||
STAGE_READMES = ("tools/README.md", "INSTALL.md", "docs/dev/development.md")
|
||||
|
||||
# Docs that must not re-introduce the pre-migration bare-enum `type:` form.
|
||||
# The per-collection contracts are appended at call time, since which ones exist
|
||||
@@ -686,6 +693,40 @@ def check_legacy_type_blocks() -> list[str]:
|
||||
return issues
|
||||
|
||||
|
||||
# The areas of `docs/`: the four kinds of documentation every instance ships,
|
||||
# and the developer area `dist export` leaves out (dist_cmd.DOCS_DEV_ONLY).
|
||||
# AGENTS.md § File naming defines them; this is the mechanical half of it.
|
||||
DOCS_AREAS = ("tutorials", "how-to", "reference", "explanation", "dev")
|
||||
|
||||
|
||||
def check_docs_layout() -> list[str]:
|
||||
"""Every file under `docs/` sits in exactly one of `DOCS_AREAS`.
|
||||
|
||||
A file directly in `docs/`, or in a directory that is not an area, is
|
||||
reported. Area membership is read off the first path segment, so a file
|
||||
can be in at most one area by construction; what this catches is a file
|
||||
in none. A tree without `docs/` passes - the check says where a page
|
||||
goes, not that any page must exist.
|
||||
"""
|
||||
docs_dir = config.ROOT / "docs"
|
||||
if not docs_dir.is_dir():
|
||||
return []
|
||||
issues = []
|
||||
for path in sorted(p for p in docs_dir.rglob("*") if p.is_file()):
|
||||
relative = path.relative_to(docs_dir)
|
||||
if len(relative.parts) == 1:
|
||||
issues.append(
|
||||
f"{rel_path(path)} sits directly in docs/ - move it into one of "
|
||||
f"{', '.join(f'docs/{area}/' for area in DOCS_AREAS)}"
|
||||
)
|
||||
elif relative.parts[0] not in DOCS_AREAS:
|
||||
issues.append(
|
||||
f"{rel_path(path)} is in docs/{relative.parts[0]}/, which is not a documentation "
|
||||
f"area - move it into one of {', '.join(f'docs/{area}/' for area in DOCS_AREAS)}"
|
||||
)
|
||||
return issues
|
||||
|
||||
|
||||
def check_toc_regions() -> list[str]:
|
||||
"""Every reference file over the line threshold carries a current TOC.
|
||||
|
||||
@@ -1167,7 +1208,7 @@ def check_breaking_change_for_boundary() -> list[str]:
|
||||
),
|
||||
notes=(
|
||||
"Checks the docs that mirror the code. The name is about documentation parity, not the "
|
||||
"`docs/` directory - it neither reads nor requires one.",
|
||||
"`docs/` directory - it reads that only for its layout, and never requires one.",
|
||||
"Commands: every command has a `cli_contract` record and is listed in "
|
||||
"`cli_contract.GROUPS`, in both directions; every command's non-hidden flags appear in "
|
||||
"its record's SYNOPSIS and vice versa; `tools/CONTRACT.md`'s generated "
|
||||
@@ -1198,6 +1239,9 @@ 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.",
|
||||
"Every file under `docs/` sits in one of its areas - `tutorials/`, `how-to/`, "
|
||||
"`reference/`, `explanation/` or `dev/` - never directly in `docs/` or in another "
|
||||
"directory.",
|
||||
"`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 "
|
||||
@@ -1254,6 +1298,12 @@ def check_breaking_change_for_boundary() -> list[str]:
|
||||
cause="A reference file's table-of-contents region is missing or stale",
|
||||
reaction="Run `docs toc --apply`, then re-run",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="A file sits directly in `docs/`, or in a directory there that is not one of "
|
||||
"its five areas",
|
||||
reaction="Move it (`git mv`) into the area its kind of documentation belongs to, and "
|
||||
"fix the links that pointed at the old path",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="A reference file's relative markdown link does not resolve to an existing "
|
||||
"file",
|
||||
@@ -1308,6 +1358,7 @@ def verify():
|
||||
+ check_breaking_change_for_boundary()
|
||||
+ check_changelog_budget()
|
||||
+ check_no_issue_references()
|
||||
+ check_docs_layout()
|
||||
+ check_toc_regions()
|
||||
+ check_reference_targets()
|
||||
+ install_doc.check_prerequisite_regions()
|
||||
@@ -1326,6 +1377,7 @@ def verify():
|
||||
f"{len(resolver.list_type_specs())} type-spec(s) validating against their own schema, "
|
||||
f"{len(IGNORE_CANARIES)} ignore canaries clear, "
|
||||
f"no issue references in {len(shipped_prose())} shipped document(s) or command help, "
|
||||
f"every docs/ file in one of its {len(DOCS_AREAS)} areas, "
|
||||
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 "
|
||||
|
||||
@@ -507,7 +507,7 @@ def _ensure_tracker_project(page_title: str, *, resume: bool) -> Optional[str]:
|
||||
"`wikitool types describe <type>` - what a type requires and where it lands",
|
||||
"`wikitool touch` - changes a page's own frontmatter afterwards",
|
||||
"`wikitool task new` - a tracker item without a page",
|
||||
"`docs/knowledge-and-commitment.md` - why a project is a page and a tracker project",
|
||||
"`docs/explanation/knowledge-and-commitment.md` - why a project is a page and a tracker project",
|
||||
),
|
||||
))
|
||||
def new_page_command(
|
||||
|
||||
@@ -4,9 +4,9 @@ read surface outside `new project` (Gitea #132, #138; #119 D1/D2/D4/D5/D6/D9).
|
||||
`task new` is the second write path into the task tracker, alongside `new
|
||||
project`'s own (`chemenu.commands.new_page._ensure_tracker_project`) - and
|
||||
the last creation command that pairing needed, per
|
||||
`docs/knowledge-and-commitment.md`. `task close` (#138) is the one closing
|
||||
`docs/explanation/knowledge-and-commitment.md`. `task close` (#138) is the one closing
|
||||
write: never a delete, only "mark done" (`TaskWriter.close_item`) - see that
|
||||
protocol method's docstring and `docs/knowledge-and-commitment.md` for why
|
||||
protocol method's docstring and `docs/explanation/knowledge-and-commitment.md` for why
|
||||
the surface stops there. `task list` (#138) is the read half a caller needs
|
||||
to get an item's id before it can close it, without first running
|
||||
`wikitool review`. None of the three ever touch `kb/`: an ingest that finds
|
||||
@@ -150,7 +150,7 @@ def _read_config():
|
||||
"`wikitool task list` - a project's open items and their ids",
|
||||
"`wikitool task close` - marks an item done",
|
||||
"`wikitool new project` - a project page and its tracker project",
|
||||
"`docs/knowledge-and-commitment.md` - why commitments live in the tracker, not in `kb/`",
|
||||
"`docs/explanation/knowledge-and-commitment.md` - why commitments live in the tracker, not in `kb/`",
|
||||
),
|
||||
))
|
||||
def task_new_command(
|
||||
@@ -192,7 +192,7 @@ def task_new_command(
|
||||
Tracker-only by design (#132 D1): a source that carries both knowledge
|
||||
and a commitment gets this command for the commitment and the normal
|
||||
page-creation commands for the knowledge, run as two separate steps by
|
||||
the calling skill - see `docs/knowledge-and-commitment.md`.
|
||||
the calling skill - see `docs/explanation/knowledge-and-commitment.md`.
|
||||
"""
|
||||
if bool(project) == inbox:
|
||||
fail(
|
||||
@@ -381,7 +381,7 @@ def task_close_command(
|
||||
\f
|
||||
The only closing write this stack makes (Gitea #138); see
|
||||
`chemenu.tasks.protocol.TaskWriter.close_item` and
|
||||
`docs/knowledge-and-commitment.md` for why."""
|
||||
`docs/explanation/knowledge-and-commitment.md` for why."""
|
||||
cfg = _read_config()
|
||||
if cfg is None:
|
||||
fail(
|
||||
|
||||
@@ -21,7 +21,7 @@ cached from before that step.
|
||||
An item's own id (#138) is read-only data, like everything else here - it is
|
||||
never stored by `wikitool`, only ever passed straight back into
|
||||
`TaskWriter.close_item` within the same invocation. That keeps the "one name
|
||||
is the only coupling" decision (`docs/knowledge-and-commitment.md` § "One
|
||||
is the only coupling" decision (`docs/explanation/knowledge-and-commitment.md` § "One
|
||||
name, carrying the duties of an identifier") intact: no id-to-anything
|
||||
mapping is ever written down, so there is nothing to keep in sync.
|
||||
|
||||
|
||||
@@ -149,8 +149,12 @@ def repo(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
|
||||
)
|
||||
|
||||
docs_dir = root / "docs"
|
||||
docs_dir.mkdir()
|
||||
(docs_dir / "why-gates-are-code.md").write_text("# Why gates are code\n", encoding="utf-8")
|
||||
(docs_dir / "explanation").mkdir(parents=True)
|
||||
(docs_dir / "explanation" / "why-gates-are-code.md").write_text(
|
||||
"# Why gates are code\n", encoding="utf-8"
|
||||
)
|
||||
(docs_dir / "dev").mkdir()
|
||||
(docs_dir / "dev" / "development.md").write_text("# Developing this stack\n", encoding="utf-8")
|
||||
|
||||
for relative in ("raw/CONTRACT.md", "reports/CONTRACT.md", "work/CONTRACT.md"):
|
||||
path = root / relative
|
||||
@@ -175,7 +179,16 @@ def test_plan_never_includes_commonplace(repo):
|
||||
|
||||
def test_plan_ships_docs_verbatim(repo):
|
||||
plan = dist_cmd.build_plan()
|
||||
assert plan["docs/why-gates-are-code.md"].content == "# Why gates are code\n"
|
||||
assert plan["docs/explanation/why-gates-are-code.md"].content == "# Why gates are code\n"
|
||||
|
||||
|
||||
def test_plan_never_includes_docs_dev(repo):
|
||||
"""docs/dev/ is the human half of instructions/dev/ - the release workflow
|
||||
and the development environment - and is pruned with it, while the other
|
||||
docs/ areas still ship."""
|
||||
plan = dist_cmd.build_plan()
|
||||
assert not any(relative.startswith("docs/dev/") for relative in plan)
|
||||
assert "docs/explanation/why-gates-are-code.md" in plan
|
||||
|
||||
|
||||
def test_plan_never_includes_instructions_dev(repo):
|
||||
|
||||
@@ -290,12 +290,13 @@ def test_install_md_is_checked_too(tmp_path, monkeypatch):
|
||||
assert any("`doctor`" in issue for issue in issues)
|
||||
|
||||
|
||||
def test_development_md_is_checked_too(tmp_path, monkeypatch):
|
||||
"""DEVELOPMENT.md drifted exactly this way once (Gitea #47): a table
|
||||
describing what each verify command checks, removed by hand because nothing
|
||||
compared it to anything."""
|
||||
def test_the_developer_guide_is_checked_too(tmp_path, monkeypatch):
|
||||
"""The developer guide drifted exactly this way once (Gitea #47, as the root
|
||||
DEVELOPMENT.md it was then): a table describing what each verify command
|
||||
checks, removed by hand because nothing compared it to anything."""
|
||||
root = tmp_path
|
||||
(root / "DEVELOPMENT.md").write_text(
|
||||
(root / "docs" / "dev").mkdir(parents=True)
|
||||
(root / "docs" / "dev" / "development.md").write_text(
|
||||
"| Command | Purpose |\n| `docs verify` | checks docs |\n", encoding="utf-8"
|
||||
)
|
||||
monkeypatch.setattr(docs_verify.config, "ROOT", root)
|
||||
@@ -305,7 +306,7 @@ def test_development_md_is_checked_too(tmp_path, monkeypatch):
|
||||
|
||||
|
||||
def test_an_absent_listed_doc_is_skipped_not_reported(tmp_path, monkeypatch):
|
||||
"""The distributed-instance case: DEVELOPMENT.md is not shipped, so listing
|
||||
"""The distributed-instance case: docs/dev/ is not shipped, so listing
|
||||
it must stay inert where the file does not exist rather than failing a tree
|
||||
that is correct."""
|
||||
root = tmp_path
|
||||
@@ -906,6 +907,113 @@ def test_a_target_with_neither_the_file_nor_a_template_is_still_dead(tmp_path, m
|
||||
assert "CONVENTIONS.md" in issues[0]
|
||||
|
||||
|
||||
# --- docs/ layout ------------------------------------------------------------
|
||||
|
||||
|
||||
def test_this_repos_docs_are_laid_out_in_areas():
|
||||
assert docs_verify.check_docs_layout() == []
|
||||
|
||||
|
||||
def _docs_tree(tmp_path, monkeypatch, files: tuple[str, ...]):
|
||||
from chemenu import config
|
||||
|
||||
monkeypatch.setattr(config, "ROOT", tmp_path)
|
||||
for relative in files:
|
||||
path = tmp_path / relative
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text("# Page\n", encoding="utf-8")
|
||||
|
||||
|
||||
def test_a_file_directly_in_docs_is_reported(tmp_path, monkeypatch):
|
||||
"""The layout before 9.0.0, when every page sat directly in docs/: a page
|
||||
there belongs to no kind of documentation."""
|
||||
_docs_tree(tmp_path, monkeypatch, ("docs/explanation/a.md", "docs/stray.md"))
|
||||
issues = docs_verify.check_docs_layout()
|
||||
assert len(issues) == 1
|
||||
assert "docs/stray.md" in issues[0] and "directly in docs/" in issues[0]
|
||||
|
||||
|
||||
def test_a_file_in_a_directory_that_is_no_area_is_reported(tmp_path, monkeypatch):
|
||||
_docs_tree(tmp_path, monkeypatch, ("docs/guides/a.md",))
|
||||
issues = docs_verify.check_docs_layout()
|
||||
assert len(issues) == 1
|
||||
assert "docs/guides/" in issues[0]
|
||||
|
||||
|
||||
def test_every_area_and_nesting_inside_one_is_accepted(tmp_path, monkeypatch):
|
||||
_docs_tree(tmp_path, monkeypatch, tuple(
|
||||
f"docs/{area}/README.md" for area in docs_verify.DOCS_AREAS
|
||||
) + ("docs/how-to/mcp/setup.md",))
|
||||
assert docs_verify.check_docs_layout() == []
|
||||
|
||||
|
||||
def test_a_tree_without_docs_passes_the_layout_check(tmp_path, monkeypatch):
|
||||
_docs_tree(tmp_path, monkeypatch, ())
|
||||
assert docs_verify.check_docs_layout() == []
|
||||
|
||||
|
||||
def test_verify_raises_when_a_docs_file_is_outside_every_area(monkeypatch):
|
||||
monkeypatch.setattr(docs_verify, "check_docs_layout", lambda: ["stray"])
|
||||
with pytest.raises(typer.Exit):
|
||||
docs_verify.verify()
|
||||
|
||||
|
||||
# The rewrite instructions/migrations/9.0.0-docs-diataxis.md tells an instance
|
||||
# to run, as the regex it spells out for `sed -E`.
|
||||
_DIATAXIS_NAMES = (
|
||||
"pipeline-rationale|ownership-and-templates|language-boundaries|why-gates-are-code|"
|
||||
"version-model|knowledge-and-commitment|model-and-effort-selection"
|
||||
)
|
||||
_DIATAXIS_SED = f"s#docs/({_DIATAXIS_NAMES})\\.md#docs/explanation/\\1.md#g"
|
||||
|
||||
|
||||
def test_the_9_0_0_upgrade_breaks_an_old_conventions_link_until_the_migration_ran(
|
||||
tmp_path, monkeypatch
|
||||
):
|
||||
"""The breaking change 9.0.0 declares, end to end on the check that reports
|
||||
it: an instance adopted `kb/CONVENTIONS.md` from a template linking
|
||||
`../docs/language-boundaries.md`, and the upgrade never touches that
|
||||
instance-owned file while it deletes the page at its old path. After the
|
||||
swap `docs verify` reports the link; after the migration document's
|
||||
rewrite it does not."""
|
||||
import re
|
||||
|
||||
from chemenu import config
|
||||
|
||||
document = (
|
||||
config.ROOT / "instructions" / "migrations" / "9.0.0-docs-diataxis.md"
|
||||
).read_text(encoding="utf-8")
|
||||
monkeypatch.setattr(config, "ROOT", tmp_path)
|
||||
(tmp_path / "kb").mkdir()
|
||||
conventions = tmp_path / "kb" / "CONVENTIONS.md"
|
||||
conventions.write_text(
|
||||
"# Conventions\n\nWhy the KB language is a value: "
|
||||
"[docs/language-boundaries.md](../docs/language-boundaries.md).\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
# The machinery as 9.0.0 installs it: the page only under its new path.
|
||||
(tmp_path / "docs" / "explanation").mkdir(parents=True)
|
||||
(tmp_path / "docs" / "explanation" / "language-boundaries.md").write_text(
|
||||
"# Language boundaries\n", encoding="utf-8"
|
||||
)
|
||||
|
||||
issues = docs_verify.check_reference_targets()
|
||||
assert len(issues) == 1
|
||||
assert "kb/CONVENTIONS.md:3" in issues[0]
|
||||
assert "../docs/language-boundaries.md" in issues[0]
|
||||
|
||||
# The migration document's rewrite, the same expression it gives to `sed -E`.
|
||||
assert _DIATAXIS_SED in document
|
||||
_, pattern, replacement, _ = _DIATAXIS_SED.split("#")
|
||||
conventions.write_text(
|
||||
re.sub(pattern, replacement, conventions.read_text(encoding="utf-8")),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
assert "../docs/explanation/language-boundaries.md" in conventions.read_text(encoding="utf-8")
|
||||
assert docs_verify.check_reference_targets() == []
|
||||
|
||||
|
||||
def test_every_reference_files_link_targets_resolve():
|
||||
"""Forward direction, against the real tree: every relative link in a file
|
||||
`toc.target_files()` covers must resolve - this is what a `../` count
|
||||
|
||||
@@ -289,7 +289,7 @@ def test_stack_machinery_excludes_ordinary_content():
|
||||
"""kb/ content, docs/ prose and work/ scratch carry no normative sentence
|
||||
and are not what the closing-phase reminder is about."""
|
||||
assert not touches_stack_machinery(["kb/entities/systems/Foo.md"])
|
||||
assert not touches_stack_machinery(["docs/why-gates-are-code.md"])
|
||||
assert not touches_stack_machinery(["docs/explanation/why-gates-are-code.md"])
|
||||
assert not touches_stack_machinery(["work/ingest-x/extract-0.md"])
|
||||
assert not touches_stack_machinery([])
|
||||
|
||||
|
||||
@@ -166,7 +166,7 @@ def test_target_files_matches_the_documented_scope():
|
||||
assert "instructions/CONTRACT.md" in relatives
|
||||
assert "instructions/dev/version-parts.md" in relatives # flat, still instructions/**.md
|
||||
assert "types/source.md" in relatives # a page type-spec, not only the stage contract
|
||||
assert "docs/version-model.md" in relatives
|
||||
assert "docs/explanation/version-model.md" in relatives
|
||||
assert not any(rel.endswith("SKILL.md") for rel in relatives)
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user