docs verify: DEVELOPMENT.md in STAGE_READMES; self-labelling release notes; prose corrections to 4.6.0 (#47 Block 3)
Files changed: - .gitea/workflows/release.yml - CHANGES.md - VERSION - instructions/dev/stack-close/SKILL.md - tools/CONTRACT.md - tools/chemenu/commands/docs_verify.py - tools/chemenu/commands/git_publish.py - tools/chemenu/tests/test_docs_verify.py
This commit is contained in:
+1
-1
@@ -58,7 +58,7 @@ tools/wikitool <command> --help
|
||||
| `sources trace --raw <path>` \| `--page "<Title>"` | Trace provenance in either direction: raw file -> source page(s) -> citing pages, or page -> its sources -> their raw files |
|
||||
| `sources rebuild-index [--dry-run]` | Regenerate the `kb/provenance.md` reverse index (raw file -> source page -> citing pages) |
|
||||
| `sync [--remote origin] [--branch main] [--confirm-rebase TOKEN]` | Fetch `<remote>/<branch>` and bring the local branch up to date with it: fast-forward when the remote is simply ahead, rebase local commit(s) on top when both sides moved but touch disjoint files (a content conflict is then impossible by construction), and exit **42** for review when they touch the same file (the **rebase-review gate** - see `publish` below). Never commits, never pushes, never force-anything - no remote configured, or one that cannot be reached, is reported and skipped, not a failure. Meant to run once at the start of a writing session (`instructions/session-setup.md`) so the rest of it works against a current tree instead of discovering the drift at the final `publish` |
|
||||
| `publish --message "<op>: <desc>" [--no-push] [--confirm TOKEN] [--confirm-rebase TOKEN] [--threshold N] [--remote origin] [--branch main] [--path P ...]` | Reconcile with `<remote>/<branch>` exactly like `sync` (skipped for `--no-push`), then stage all changes, commit, and push. If the reconcile step found a still-unpushed local commit and there is nothing new to stage, that commit is pushed anyway - a previous `publish` whose push failed no longer strands it. If the push is rejected despite the pre-check (a genuine race - something landed on the remote in between), one more reconcile-and-retry is attempted before giving up; never more than one. **Mass-Update Gate:** when >= `--threshold` (default 10) *counted* files would be committed, exits **42 (`EXIT_NEEDS_CLEARANCE`)** instead of publishing - a third outcome distinct from success (0) and a validation error (1) - and prints a review report: a scale line (file count, total lines added/removed, status breakdown), only-what-applies attention notes (deletions by name, control-plane and harness-config touches, published pages, the largest single change, binaries), and every counted path grouped by area with its status and churn, generated files split out as needing no review. The token digests each counted path **and its contents** plus the publish target, so a clearance carries neither to a different file list nor to edited contents; a wrong, invented or superseded token exits 42 again with the current state. Two kinds of path are committed but never counted and never shown for approval: anything under `work/`, and the files `wikitool` generates itself (`kb/index.md`, `kb/log.md`, `kb/provenance.md`, every `INDEX.md`) - each is recomputable from the tree, so approving it decides nothing, and a routine ingest rebuilds five or six of them. The refusal line accounts for both, by reason. The gate is evaluated *before* anything is staged, so a refused publish leaves the working tree untouched. **Publish-Remote Gate:** when this checkout carries a `.wikitool-remotes.json` and the resolved push URL of `--remote` is not listed in it, exits **42** before the reconcile step even fetches - the URL is read from `git remote get-url --push`, so a repointed remote does not pass on its name. Unlike the other two gates it has **no token and no flag**: the way past it is the user adding the URL to that file, and an agent editing it to get past a refusal is opening a gate on its own initiative. Absent file means unrestricted; a malformed one is an error, not permission. See [instructions/gates.md](../instructions/gates.md) `--yes`/`-y` are gone and now fail with an explicit error. `--path` (repeatable) scopes the whole operation - gate count, staging, and commit - to a subtree. **Stack-machinery note:** after a successful commit/push whose changed files include `tools/`, `types/`, `instructions/`, `AGENTS.md`, or any `<stage>/CONTRACT.md` - the same scope a stack version bump itself covers - prints one reminder line that the phase past this point (an issue-body rewrite, `docs/` staleness, a changelog entry's accuracy) is not covered by `docs verify`, `instructions verify` or `pytest`. Not a gate: no exit code change, nothing to clear, silent for an ordinary content publish |
|
||||
| `publish --message "<op>: <desc>" [--no-push] [--confirm TOKEN] [--confirm-rebase TOKEN] [--threshold N] [--remote origin] [--branch main] [--path P ...]` | Reconcile with `<remote>/<branch>` exactly like `sync` (skipped for `--no-push`), then stage all changes, commit, and push. If the reconcile step found a still-unpushed local commit and there is nothing new to stage, that commit is pushed anyway - a previous `publish` whose push failed no longer strands it. If the push is rejected despite the pre-check (a genuine race - something landed on the remote in between), one more reconcile-and-retry is attempted before giving up; never more than one. **Mass-Update Gate:** when >= `--threshold` (default 10) *counted* files would be committed, exits **42 (`EXIT_NEEDS_CLEARANCE`)** instead of publishing - a third outcome distinct from success (0) and a validation error (1) - and prints a review report: a scale line (file count, total lines added/removed, status breakdown), only-what-applies attention notes (deletions by name, control-plane and harness-config touches, published pages, the largest single change, binaries), and every counted path grouped by area with its status and churn, generated files split out as needing no review. The token digests each counted path **and its contents** plus the publish target, so a clearance carries neither to a different file list nor to edited contents; a wrong, invented or superseded token exits 42 again with the current state. Two kinds of path are committed but never counted and never shown for approval: anything under `work/`, and the files `wikitool` generates itself (`kb/index.md`, `kb/log.md`, `kb/provenance.md`, every `INDEX.md`) - each is recomputable from the tree, so approving it decides nothing, and a routine ingest rebuilds five or six of them. The refusal line accounts for both, by reason. The gate is evaluated *before* anything is staged, so a refused publish leaves the working tree untouched. **Publish-Remote Gate:** when this checkout carries a `.wikitool-remotes.json` and the resolved push URL of `--remote` is not listed in it, exits **42** before the reconcile step even fetches - the URL is read from `git remote get-url --push`, so a repointed remote does not pass on its name. Unlike the other two gates it has **no token and no flag**: the way past it is the user adding the URL to that file, and an agent editing it to get past a refusal is opening a gate on its own initiative. Absent file means unrestricted; a malformed one is an error, not permission. See [instructions/gates.md](../instructions/gates.md) `--yes`/`-y` are gone and now fail with an explicit error. `--path` (repeatable) scopes the whole operation - gate count, staging, and commit - to a subtree. **Stack-machinery note:** after a successful commit/push whose changed files include `tools/`, `types/`, `instructions/`, `AGENTS.md`, or a path ending in `CONTRACT.md` - roughly the scope a stack version bump covers, deliberately a shade broader than CI's version gate, which matches only a `CONTRACT.md` one segment deep - prints one reminder line that the phase past this point (an issue-body rewrite, `docs/` staleness, a changelog entry's accuracy) is not covered by `docs verify`, `instructions verify` or `pytest`. Not a gate: no exit code change, nothing to clear, silent for an ordinary content publish |
|
||||
| `work new (--input <raw path> \| --key <run key>) [--again] [--dry-run]` | Scaffold `work/<runkey>/` for one workshop run: refuses a collision instead of suffixing it, and writes the required `README.md` + `plan.md`. `--input` derives the run key from the path below `raw/` (an ingest); `--key` names it outright for a run with no raw input - a migration or a sweep across `kb/` - and may not start with `ingest-`, which stays reserved for derived keys. Exactly one of the two. `--again` opens a dated second pass over a tree that has itself changed. See [work/CONTRACT.md](../work/CONTRACT.md) |
|
||||
| `work close --run-key <name> [--yes] [--dry-run]` | Delete a finished workshop. Lists what would be lost and requires `--yes`, because nothing in it is recoverable from the rest of the repo - the durable conclusions must already be in `kb/` |
|
||||
| `budget status` | Show the current session's `wikitool` call count and recent command history (never counted against the budget) |
|
||||
|
||||
@@ -136,7 +136,17 @@ ROOT_README = config.ROOT / "README.md"
|
||||
# tools/README.md is exactly the file it drifted in. INSTALL.md is here for the
|
||||
# same reason: it is human-facing prose about installing an instance, and the
|
||||
# command reference lives exactly once, in tools/CONTRACT.md.
|
||||
STAGE_READMES = ("tools/README.md", "INSTALL.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:
|
||||
# `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")
|
||||
|
||||
# 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
|
||||
|
||||
@@ -441,13 +441,18 @@ STACK_MACHINERY_NOTE = (
|
||||
|
||||
|
||||
def touches_stack_machinery(changed_files: list[str]) -> bool:
|
||||
"""Whether `changed_files` includes a path under `version-parts.md`'s
|
||||
scope for the stack version - `tools/`, `types/`, `instructions/`,
|
||||
`AGENTS.md`, or any `<stage>/CONTRACT.md`. A publish in this class is,
|
||||
by construction of the `stack-dev`/`stack-close` split, always followed
|
||||
by the unchecked closing phase - `STACK_MACHINERY_NOTE` times a reminder
|
||||
to land exactly there, for any session, not only one that read the
|
||||
skill that names it."""
|
||||
"""Whether `changed_files` includes a path under the stack version's own
|
||||
scope - `tools/`, `types/`, `instructions/`, `AGENTS.md`, or a path ending
|
||||
in `CONTRACT.md` at any depth. A publish in this class is, by construction
|
||||
of the `stack-dev`/`stack-close` split, always followed by the unchecked
|
||||
closing phase - `STACK_MACHINERY_NOTE` times a reminder to land exactly
|
||||
there, for any session, not only one that read the skill that names it.
|
||||
|
||||
Deliberately a shade broader than CI's version gate, which matches
|
||||
`<one-segment>/CONTRACT.md` only: this decides whether to print a sentence,
|
||||
so over-matching costs a reminder nobody needed, while under-matching costs
|
||||
the reminder in the one case it was built for. The two are not the same
|
||||
predicate and should not be described as one."""
|
||||
for path in changed_files:
|
||||
if path in STACK_MACHINERY_NAMES:
|
||||
return True
|
||||
|
||||
@@ -100,6 +100,30 @@ 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."""
|
||||
root = tmp_path
|
||||
(root / "DEVELOPMENT.md").write_text(
|
||||
"| Command | Purpose |\n| `docs verify` | checks docs |\n", encoding="utf-8"
|
||||
)
|
||||
monkeypatch.setattr(docs_verify.config, "ROOT", root)
|
||||
monkeypatch.setattr(docs_verify, "ROOT_README", root / "README.md") # doesn't exist here
|
||||
issues = docs_verify.check_readmes_have_no_command_table()
|
||||
assert any("`docs verify`" in issue for issue in issues)
|
||||
|
||||
|
||||
def test_an_absent_listed_doc_is_skipped_not_reported(tmp_path, monkeypatch):
|
||||
"""The distributed-instance case: DEVELOPMENT.md 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
|
||||
monkeypatch.setattr(docs_verify.config, "ROOT", root)
|
||||
monkeypatch.setattr(docs_verify, "ROOT_README", root / "README.md")
|
||||
assert docs_verify.check_readmes_have_no_command_table() == []
|
||||
|
||||
|
||||
def test_legacy_type_blocks_are_absent():
|
||||
assert docs_verify.check_legacy_type_blocks() == []
|
||||
|
||||
|
||||
Reference in New Issue
Block a user