tools: command records, Distribution and versioning group - one line per cause, examples, prohibitions (#142)
Files changed: - CHANGES.md - VERSION - tools/CONTRACT.md - tools/chemenu/commands/dist_cmd.py - tools/chemenu/commands/version_cmd.py
This commit is contained in:
1 parent
df8ff2fa22
commit
9d6ca6b193
5 files changed
+710
-265
No files matched your search
+19
-1
@@ -59,7 +59,7 @@ concern - readable here, never shipped as something to parse.
|
||||
|
||||
---
|
||||
|
||||
## 7.1.0-beta.8 - 2026-09-26 - Command records, Catalog and log group: bullets, examples, prohibitions
|
||||
## 7.1.0-beta.9 - 2026-09-26 - Command records, Distribution and versioning group: one line per cause, examples, prohibitions
|
||||
|
||||
**Author:** Torben Nehmer
|
||||
|
||||
@@ -77,6 +77,7 @@ concern - readable here, never shipped as something to parse.
|
||||
- wikitool: usage lines name wikitool, and the -h acceptance checks become tests
|
||||
- Command records, Git group: NOTES as bullets, one exit line per cause, examples and prohibitions
|
||||
- Command records, Catalog and log group: bullets, examples, prohibitions
|
||||
- Command records, Distribution and versioning group: one line per cause, examples, prohibitions
|
||||
<!-- /wikitool:bumps -->
|
||||
|
||||
### CalDAV task-tracker provider (Nextcloud Tasks, iOS Reminders); review reports unknown-value findings instead of skipping them
|
||||
@@ -206,6 +207,23 @@ prose: check the tail of `kb/log.md` before re-running after an uncertain outcom
|
||||
the command already did. `log status` no longer claims it "reports 0" for a missing log - it
|
||||
reports that nothing is logged yet, which is what it always printed.
|
||||
|
||||
### Command records, Distribution and versioning group: one line per cause, examples, prohibitions
|
||||
|
||||
`dist export`, `dist upgrade` and the six `version` commands rewritten the same way; text only.
|
||||
The long single-paragraph records - `dist upgrade` and `version bump` above all - are now one
|
||||
bullet per behaviour, and each exit-1 cause carries its own reaction, taken from what the
|
||||
command's own error message already tells the caller (`migrate baseline`, `migrate status`,
|
||||
commit or stash, `upstream merge`). Three corrections to what the records claimed: `dist upgrade`
|
||||
listed an equal source version both as a no-op success and as an exit-1 refusal - it is a
|
||||
success, and EXIT STATUS now says so; `dist export` and `dist upgrade` gained the exit-1 causes
|
||||
their code already had but their records omitted (a missing licence, an export plan leaking
|
||||
instance data, a bad archive checksum); and `version regrade`'s NOTES, garbled when they were
|
||||
moved over mechanically, read as sentences again. Two code comments changed with them: the one
|
||||
above `dist export`'s `raw/`/`incoming/` anchors had claimed `.gitignore` drops `incoming/`,
|
||||
which stopped being true when `!/incoming/.gitkeep` was added, and `dist upgrade`'s stamp write
|
||||
now carries the reasoning for writing the stamp whole after `--keep-local`, which used to live
|
||||
in the record.
|
||||
|
||||
---
|
||||
|
||||
## 7.0.0 - 2026-09-22 - Task-Tracker-Anbindung: Vorhaben als Seitenart, Verpflichtungsschicht, Weekly Review als Read-Time-Join
|
||||
|
||||
+238
-23
@@ -1569,18 +1569,46 @@ Write a contentless, distributable copy of this repo's machinery.
|
||||
- budget: counted
|
||||
- network: no
|
||||
|
||||
**EXAMPLES**
|
||||
|
||||
- `tools/wikitool dist export ../my-wiki --dry-run`
|
||||
- `tools/wikitool dist export ../my-wiki`
|
||||
|
||||
**EXIT STATUS**
|
||||
|
||||
- 0 success
|
||||
- 1 Target exists and is not empty, is not a directory, or the tree has no readable `VERSION`
|
||||
- 1 `<target>` exists and is not empty, or is not a directory
|
||||
- 1 The tree has no readable `VERSION`
|
||||
- 1 A licence file is missing from the source tree
|
||||
- 1 The plan would carry this instance's own data (a filled personalization, conventions or page type-spec file)
|
||||
|
||||
**ON FAILURE**
|
||||
|
||||
- Target exists and is not empty, is not a directory, or the tree has no readable `VERSION` -> Point `<target>` at an empty (or new) directory and retry. Never merge into a non-empty one by hand
|
||||
- `<target>` exists and is not empty, or is not a directory -> Point `<target>` at an empty (or new) directory and retry
|
||||
- The tree has no readable `VERSION` -> Fix `VERSION`, then retry
|
||||
- A licence file is missing from the source tree -> Restore it, then retry
|
||||
- The plan would carry this instance's own data (a filled personalization, conventions or page type-spec file) -> Report it: it is an allowlist bug in `dist export`, not something to work around by deleting files from the target
|
||||
|
||||
**NEVER**
|
||||
|
||||
- Never merge an export into a non-empty directory by hand.
|
||||
|
||||
**NOTES**
|
||||
|
||||
Write a contentless, distributable copy of this repo's machinery into an empty `<target>` directory: `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 and their schemas re-keyed as `.template`, the stack's own verbatim), `docs/` verbatim, `tools/` (no venv/caches), the `.github/hooks/`+`.vibe/` session-tracing config plus `.claude/settings.json`, `kb/CONTRACT.md` (no pages, no areas), the two flat anchors `raw/.gitkeep` and `incoming/.gitkeep` (both roots are flat now that a file's location under `raw/` is a date shard rather than a hand-picked type, so a fresh export no longer creates any type subdirectories under either root; `incoming/.gitkeep` is trackable and survives becoming a git repository, so a plain clone gets the directory without any bootstrap step re-creating it), `VERSION`, `USER.md.template`/`SOUL.md.template` plus `kb/CONVENTIONS.md.template` and each collection's contract re-keyed as `kb/<name>/COLLECTION.md.template` (the templates ship; the filled `USER.md`/`SOUL.md`/`kb/CONVENTIONS.md`/`kb/<name>/COLLECTION.md`/`types/<page-type>.md` never do - all of them bind their instance and none are the stack's to decide, and `find_leaks` refuses a plan carrying one), and a generated `.wikitool-release.json` stamp (version, export date, origin, and a sha256 per exported file - the base a later upgrade would compare against). The four origin options only fill stamp fields: `export` never calls git and cannot discover them. Refuses a non-empty target, and a tree with no `VERSION`. See `instructions/setup-instance.md`. One-way: there is no command that reconstructs a distributed instance into a dev instance - work on the stack in the origin repo (or a new dev instance exported from it) instead
|
||||
- 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 and their schemas re-keyed as `.template`, the stack's own verbatim), `docs/` verbatim, `tools/` (no venv/caches), 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` 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.
|
||||
- The four origin options only fill stamp fields: `export` never calls git and cannot discover them.
|
||||
- One-way: no command reconstructs a distributed instance into a dev instance - work on the stack in the origin repo, or in a new dev instance exported from it.
|
||||
- `--dry-run` lists every file it would write, and writes nothing.
|
||||
|
||||
**SEE ALSO**
|
||||
|
||||
- `instructions/setup-instance.md` - what comes after the export
|
||||
- `wikitool dist upgrade` - applies a later export to an existing instance
|
||||
- `wikitool version show` - reads the stamp this writes
|
||||
|
||||
#### `dist upgrade`
|
||||
|
||||
@@ -1594,22 +1622,71 @@ Apply a stack update `dist export` produced - the write half of `version check`.
|
||||
|
||||
- effect: write
|
||||
- idempotent: no
|
||||
- atomic: **Yes for the refusal cases above - nothing is written.** Once writing starts it is a plain sequential file copy with no partial-state cleanup: an interruption mid-copy (killed process, disk full) can leave the tree part-old, part-new
|
||||
- atomic: Yes for every refusal - nothing is written. Once writing starts it is a plain sequential file copy with no partial-state cleanup: an interruption mid-copy (killed process, disk full) can leave the tree part-old, part-new
|
||||
- budget: counted
|
||||
- network: no
|
||||
|
||||
**EXAMPLES**
|
||||
|
||||
- `tools/wikitool dist upgrade ../chemenu-7.1.0.tar.gz --dry-run`
|
||||
- `tools/wikitool dist upgrade ../chemenu-7.1.0.tar.gz`
|
||||
- `tools/wikitool dist upgrade ../chemenu-7.1.0.tar.gz --take-release tools/README.md`
|
||||
|
||||
**EXIT STATUS**
|
||||
|
||||
- 0 success
|
||||
- 1 Missing local `VERSION`/`.wikitool-release.json`(`files`)/`.wikitool-kb.json`, a migration already outstanding against the installed machinery, a dirty working tree, a source with no `VERSION`/stamp/`files` block, a source version that is older than, equal to, or (without `--pre`) a pre-release relative to the installed one, a `--take-release` path that is not classified as locally changed (the one refusal a `--dry-run` also raises), or one or more locally changed files that neither `--keep-local` nor a `--take-release` answers for
|
||||
- 0 The source's version equals the installed one - a no-op success
|
||||
- 1 Local `VERSION` missing, or no local `.wikitool-release.json` with a `files` block
|
||||
- 1 `.wikitool-kb.json` is missing
|
||||
- 1 A migration is already outstanding against the *installed* machinery
|
||||
- 1 The working tree is dirty
|
||||
- 1 `<source>` does not exist, fails its `.sha256`, or does not unpack to exactly one top-level directory
|
||||
- 1 The source carries no `VERSION`, `.wikitool-release.json` or `files` block
|
||||
- 1 The source's version is older than the installed one, or a pre-release without `--pre`
|
||||
- 1 A `--take-release` path this run does not classify as locally changed - the one refusal a `--dry-run` also raises
|
||||
- 1 Locally changed files that neither `--keep-local` nor a `--take-release` answers for; nothing was written
|
||||
|
||||
**ON FAILURE**
|
||||
|
||||
- Missing local `VERSION`/`.wikitool-release.json`(`files`)/`.wikitool-kb.json`, a migration already outstanding against the installed machinery, a dirty working tree, a source with no `VERSION`/stamp/`files` block, a source version that is older than, equal to, or (without `--pre`) a pre-release relative to the installed one, a `--take-release` path that is not classified as locally changed (the one refusal a `--dry-run` also raises), or one or more locally changed files that neither `--keep-local` nor a `--take-release` answers for -> For every refusal above: fix the named precondition and retry - none of them are transient. For a rejected `--take-release` path: correct it against the locally-changed list the refusal prints. For locally changed files, the refusal names all three answers with the re-run line filled in - `--take-release <path>` to write the release's version over it (which ends the divergence), `--keep-local` to leave them untouched (repeatable, and it reports the same files again on every subsequent run until they stop diverging), or reconcile by hand and retry. An interrupted write is not resumed automatically; compare the tree against the printed classification and finish or revert by hand
|
||||
- Local `VERSION` missing, or no local `.wikitool-release.json` with a `files` block -> Not transient - fix the named precondition and retry. A checkout with shared git history takes stack updates with `wikitool upstream merge` instead
|
||||
- `.wikitool-kb.json` is missing -> Run `wikitool migrate baseline <version>`, then retry
|
||||
- A migration is already outstanding against the *installed* machinery -> Finish it first - `wikitool migrate status` names it - then retry
|
||||
- The working tree is dirty -> Commit or stash first, then retry
|
||||
- `<source>` does not exist, fails its `.sha256`, or does not unpack to exactly one top-level directory -> Fix the path or re-download the release archive, then retry
|
||||
- The source carries no `VERSION`, `.wikitool-release.json` or `files` block -> Point `<source>` at a distribution export, then retry
|
||||
- The source's version is older than the installed one, or a pre-release without `--pre` -> Not transient - choose another source, or pass `--pre` for a pre-release
|
||||
- A `--take-release` path this run does not classify as locally changed - the one refusal a `--dry-run` also raises -> Correct it against the locally-changed list the refusal prints, then retry
|
||||
- Locally changed files that neither `--keep-local` nor a `--take-release` answers for; nothing was written -> Choose one of the three answers the refusal names, re-run lines filled in: `--take-release <path>` to write the release's version over it (ends the divergence), `--keep-local` to leave them untouched (reported again on every later run until they stop diverging), or reconcile by hand and retry
|
||||
|
||||
**NEVER**
|
||||
|
||||
- Never treat any of the three answers to locally changed files as the default.
|
||||
|
||||
**NOTES**
|
||||
|
||||
Never downloads anything: `<source>` is an already-fetched export directory or `.tar.gz` release archive (verified against a sibling `.sha256` if one is present; WARNs, does not block, if it is absent), which must unpack to exactly one top-level directory - the shape `.gitea/workflows/release.yml` packs. The write set is exactly the *new* `.wikitool-release.json`'s `files` block, minus what an export re-seeds from a blank template every time (`kb/log.md`, `raw/.gitkeep` - `chemenu.ownership.is_export_stub`) or seeds once and the instance owns from then on (`.wikitool-kb.json`, `CHANGES.md` - `chemenu.ownership.is_upgrade_preserved`), plus the stamp itself, always rewritten. Every candidate path is classified against the *local* `.wikitool-release.json`'s recorded digest for it: unchanged is overwritten silently, absent from the old stamp is created, and locally modified or locally deleted is **never** silently overwritten - the run aborts with the full list, and its text names the three answers with the command line already filled in, so that no reader takes any of them for the default. `--keep-local` proceeds and leaves every one of them untouched; `--take-release <path>` (repeatable) writes the release's version over the named path, discarding the local change, and re-creates it if it was locally deleted. The two are decided per path and compose on one call: without `--keep-local`, a locally changed path that no `--take-release` names still aborts the run. A `--take-release` path that this run does not report as locally changed is refused, in a `--dry-run` as well as a writing run - it is a mistake in the argument rather than a state of the tree, and a path that silently did nothing would report a successful upgrade while keeping the change it was asked to discard. After a `--keep-local` run the new stamp is still written whole, so it records the release's digest for files that were deliberately *not* written: the stamp is the baseline for the next comparison, not a literal inventory of what is on disk. That is what keeps a skipped file diverging - and therefore reported - on every later run, rather than quietly reading as current once it has been skipped once. A path taken with `--take-release` is the opposite case and the reason the flag exists: it was written, so it matches the digest the stamp records and stops being reported at all. A path in the old stamp but not the new one is reported as no longer part of the release and left alone unless `--prune` is passed, which removes it only if it is still unchanged since installation. Reports the migration chain the new machinery would owe (`chemenu.kb_state.chain` over the *new* tree's `instructions/migrations/`, read via a `directory` argument to `load_migrations`) but never runs any of it - there is no `migrate run`. Refuses before touching the source at all when: `VERSION` or `.wikitool-release.json` (with a `files` block) is missing locally, `.wikitool-kb.json` is missing, a migration is already outstanding against the *installed* machinery, or the working tree is dirty (not being a git repository at all is a WARN, not a refusal). Refuses after reading the source when: it carries no `VERSION`/`.wikitool-release.json`/`files` block, its version is older than or equal to the installed one (equal is a no-op success), it is a pre-release (`-beta.N`) without `--pre`, or `--take-release` names a path this run does not classify as locally changed. Reports, but does not block on, a crossed compatibility boundary. Never touches git - no commit, no push (invariant 5). The closing report carries no step list of its own: everything after the swap is one order, written in `instructions/upgrade-instance.md`, which the report names and which resumes at `instructions sync`. What a human decides *before* the swap - which release, whether to take it, where the tarball comes from - is `INSTALL.md` § "Version und Updates"
|
||||
- Never downloads anything: `<source>` is an already-fetched export directory or `.tar.gz` release archive. An archive is verified against a sibling `.sha256` if one is present (a missing one is a WARN, not a block) and must unpack to exactly one top-level directory - the shape `.gitea/workflows/release.yml` packs.
|
||||
- The write set is exactly the *new* `.wikitool-release.json`'s `files` block, minus what an export re-seeds from a blank template every time (`kb/log.md`, `raw/.gitkeep`) or seeds once and the instance owns from then on (`.wikitool-kb.json`, `CHANGES.md`), plus the stamp itself, always rewritten.
|
||||
- Every candidate path is classified against the *local* `.wikitool-release.json`'s recorded digest: unchanged is overwritten silently, absent from the old stamp is created, and locally modified or locally deleted is **never** silently overwritten.
|
||||
- Locally changed files abort the run with the full list; the abort text names the three answers with the command line filled in, and none of them is the default.
|
||||
- `--keep-local` proceeds and leaves every locally changed file untouched. The new stamp is still written whole, recording the release's digest for files that were not written, so a skipped file keeps diverging and is reported again on every later run.
|
||||
- `--take-release <path>` (repeatable) writes the release's version over the named path, discarding the local change, and re-creates it if it was locally deleted. The path then matches the stamp and stops being reported.
|
||||
- The two are decided per path and compose on one call: without `--keep-local`, a locally changed path that no `--take-release` names still aborts the run.
|
||||
- A path in the old stamp but not the new one is reported as no longer part of the release and left alone, unless `--prune` is passed, which removes it only if it is still unchanged since installation.
|
||||
- Reports the migration chain the new machinery would owe, but never runs any of it - there is no `migrate run`.
|
||||
- Reports, but does not block on, a crossed compatibility boundary.
|
||||
- The local preconditions - `VERSION`, the release stamp, `.wikitool-kb.json`, no outstanding migration, a clean working tree - are checked before the source is read. Not being a git repository at all is a WARN, not a refusal.
|
||||
- Never touches git - no commit, no push.
|
||||
- `--dry-run` classifies and reports without writing; a pre-release (`-beta.N`) source needs `--pre`.
|
||||
- An interrupted write is not resumed automatically: compare the tree against the printed classification and finish or revert by hand.
|
||||
- The closing report names `instructions/upgrade-instance.md`, which carries the order for everything after the swap and resumes at `instructions sync`.
|
||||
|
||||
**SEE ALSO**
|
||||
|
||||
- `wikitool version check` - finds out whether an update exists
|
||||
- `instructions/upgrade-instance.md` - the order after the swap
|
||||
- `INSTALL.md` § "Version und Updates" - which release, whether to take it, where the tarball comes from
|
||||
- `wikitool upstream merge` - the update path for a checkout with shared git history
|
||||
- `wikitool migrate status` - the migrations the report names
|
||||
|
||||
#### `version show`
|
||||
|
||||
@@ -1627,6 +1704,11 @@ Print this instance's stack version and where it came from.
|
||||
- budget: exempt
|
||||
- network: no
|
||||
|
||||
**EXAMPLES**
|
||||
|
||||
- `tools/wikitool version show`
|
||||
- `tools/wikitool version show --json`
|
||||
|
||||
**EXIT STATUS**
|
||||
|
||||
- 0 success
|
||||
@@ -1638,7 +1720,15 @@ Print this instance's stack version and where it came from.
|
||||
|
||||
**NOTES**
|
||||
|
||||
Development tree, or a distribution with its export date and origin. Bare `wikitool version` is an alias for this. Read-only, offline, and **exempt from the Iteration Budget Gate**
|
||||
- Prints `VERSION` and where this tree came from: "development tree" without a release stamp, otherwise the export date, source commit and repository from `.wikitool-release.json`, plus the release page when the stamp records one.
|
||||
- `--json` prints the version, its compatibility key, the stamp and the update URL.
|
||||
- Bare `wikitool version` is an alias for this.
|
||||
- Read-only and offline; exempt from the Iteration Budget Gate.
|
||||
|
||||
**SEE ALSO**
|
||||
|
||||
- `wikitool version check` - asks whether a newer stack exists
|
||||
- `wikitool version notes` - prints a version's release notes
|
||||
|
||||
#### `version check`
|
||||
|
||||
@@ -1656,18 +1746,40 @@ Ask the origin's release feed whether a newer stack exists.
|
||||
- budget: exempt
|
||||
- network: yes
|
||||
|
||||
**EXAMPLES**
|
||||
|
||||
- `tools/wikitool version check`
|
||||
- `tools/wikitool version check --json`
|
||||
|
||||
**EXIT STATUS**
|
||||
|
||||
- 0 success
|
||||
- 1 The feed could not be reached, answered non-JSON, or carried no `tag_name`. **Never** answers "up to date" for a question it could not ask
|
||||
- 1 `VERSION` is missing or unparseable
|
||||
- 1 The feed could not be reached, answered non-JSON, or carried no `tag_name`
|
||||
|
||||
**ON FAILURE**
|
||||
|
||||
- The feed could not be reached, answered non-JSON, or carried no `tag_name`. **Never** answers "up to date" for a question it could not ask -> A network failure is transient - retry once, then report it. HTTP 401/403 names `$WIKITOOL_UPDATE_TOKEN`; 404 means no release exists yet or the URL points at the wrong repo
|
||||
- `VERSION` is missing or unparseable -> Fix `VERSION` and retry
|
||||
- The feed could not be reached, answered non-JSON, or carried no `tag_name` -> A network failure is transient - retry once, then report it. HTTP 401/403 names `$WIKITOOL_UPDATE_TOKEN`; 404 means no release exists yet or the URL points at the wrong repo
|
||||
|
||||
**NEVER**
|
||||
|
||||
- Never report "up to date" for a check that failed - an unreachable feed is not that answer.
|
||||
|
||||
**NOTES**
|
||||
|
||||
Ask the origin's release feed whether a newer stack exists, and whether the step crosses a compatibility boundary (`state: current|update|migration|ahead`). One of the **two** commands in `wikitool` that make a network call, and the only one whose whole job it is - `version notes` is the other, and only on a distributed instance. Never reached implicitly from another command, needs no key, times out, and reports an unreachable feed as an error rather than as "up to date". The feed is `$WIKITOOL_UPDATE_URL`, else the release stamp's, else the built-in origin; `$WIKITOOL_UPDATE_TOKEN` is only needed if that feed is not readable anonymously. Read-only and exempt from the budget gate
|
||||
- Asks the release feed for its latest release and compares it with `VERSION`: `state` is `current`, `update`, `migration` (the step crosses a compatibility boundary) or `ahead`.
|
||||
- One of the **two** commands in `wikitool` that make a network call, and the only one whose whole job it is - `version notes` is the other, and only on a distributed instance.
|
||||
- Never reached implicitly from another command, needs no key, and times out after `--timeout` seconds (default 10).
|
||||
- The feed is `--url`, else `$WIKITOOL_UPDATE_URL`, else the release stamp's, else the built-in origin. `$WIKITOOL_UPDATE_TOKEN` is only needed if that feed is not readable anonymously.
|
||||
- For `update` or `migration` it prints that applying the release is a separate, manual step (`INSTALL.md` § "Eine Instanz aktualisieren").
|
||||
- Read-only; exempt from the Iteration Budget Gate.
|
||||
|
||||
**SEE ALSO**
|
||||
|
||||
- `wikitool dist upgrade` - applies the update this reports
|
||||
- `wikitool version notes` - prints the release notes
|
||||
- `INSTALL.md` § "Version und Updates" - what the operator decides before an update
|
||||
|
||||
#### `version notes`
|
||||
|
||||
@@ -1685,18 +1797,41 @@ Print one version's release notes.
|
||||
- budget: exempt
|
||||
- network: yes
|
||||
|
||||
**EXAMPLES**
|
||||
|
||||
- `tools/wikitool version notes`
|
||||
- `tools/wikitool version notes --version 7.0.0`
|
||||
- `tools/wikitool version notes --offline`
|
||||
|
||||
**EXIT STATUS**
|
||||
|
||||
- 0 success
|
||||
- 1 An unparseable `--version`, an unreadable `VERSION` when `--version` is omitted, or a missing `CHANGES.md`. No entry for the requested version is an error only where the feed cannot answer either: in a tree with no release stamp (a dev checkout - write the entry, or `version bump`), with `--offline`, or when the feed could not be reached or returned a release with an empty `body`. Every one of those failures names the stamp's `release_url` where it has one, so a run that cannot read the notes is still told where they are
|
||||
- 1 `--version` is unparseable, `VERSION` is unreadable when `--version` is omitted, or `CHANGES.md` is missing
|
||||
- 1 No entry for the requested version in a tree with no release stamp (a dev checkout)
|
||||
- 1 No entry for the requested version, and `--offline` was passed
|
||||
- 1 No entry for the requested version, and the feed could not be reached or returned a release with an empty `body`
|
||||
|
||||
**ON FAILURE**
|
||||
|
||||
- An unparseable `--version`, an unreadable `VERSION` when `--version` is omitted, or a missing `CHANGES.md`. No entry for the requested version is an error only where the feed cannot answer either: in a tree with no release stamp (a dev checkout - write the entry, or `version bump`), with `--offline`, or when the feed could not be reached or returned a release with an empty `body`. Every one of those failures names the stamp's `release_url` where it has one, so a run that cannot read the notes is still told where they are -> Fix the named argument or file, then retry. A feed failure is transient - retry once, then read the release page the error names. Safe to retry
|
||||
- `--version` is unparseable, `VERSION` is unreadable when `--version` is omitted, or `CHANGES.md` is missing -> Fix the named argument or file, then retry
|
||||
- No entry for the requested version in a tree with no release stamp (a dev checkout) -> Write the entry, or run `version bump`
|
||||
- No entry for the requested version, and `--offline` was passed -> Read the release page the error names
|
||||
- No entry for the requested version, and the feed could not be reached or returned a release with an empty `body` -> A feed failure is transient - retry once, then read the release page the error names
|
||||
|
||||
**NOTES**
|
||||
|
||||
Default: this tree's `VERSION`. The `CHANGES.md` entry where there is one, and where there is not, the feed's latest release notes. The fallback exists because an instance's `CHANGES.md` is a stub `dist upgrade` never overwrites (`chemenu.ownership.is_upgrade_preserved`), so the local file can never carry the entry - not today and not after any future release, which made the command permanently unanswerable exactly where the release notes are most needed. It is reached **only with a release stamp present**, i.e. only from a `dist export` tree: a dev checkout keeps the plain error, which is what keeps the origin repo and CI offline. **stdout carries nothing but the notes**; the line naming the feed being asked, and the one naming the release that answered, go to stderr - `release.yml` redirects stdout into the file it posts as the release body. Only the feed's *latest* release can be asked for (`update_url` is the one URL a stamp records, and composing a by-tag URL out of it would be guessing at an API shape), so a returned version other than the one asked for is named on stderr and printed anyway - the expected shape before an upgrade, where `VERSION` still names the release being left. `--offline` refuses the call and fails with the stamp's `release_url` instead. Read-only and exempt from the budget gate
|
||||
- Prints the `CHANGES.md` entry for `--version` (default: this tree's `VERSION`).
|
||||
- With no such entry, in a tree with a release stamp (a `dist export` tree) and without `--offline`, it prints the release feed's latest release notes instead. A tree without a stamp (a dev checkout) never asks the feed.
|
||||
- Only the feed's latest release can be asked for. When that is a different version than requested, stderr names it and the notes are printed anyway - the expected case before an upgrade, where `VERSION` still names the release being left.
|
||||
- stdout carries nothing but the notes; the line naming the feed being asked and the one naming the release that answered go to stderr. `release.yml` redirects stdout into the file it posts as the release body.
|
||||
- `--offline` never asks the feed and fails with the stamp's `release_url` instead.
|
||||
- Every failure names the stamp's `release_url` where it has one, so a run that cannot read the notes is still told where they are.
|
||||
- Read-only, safe to retry, and exempt from the Iteration Budget Gate.
|
||||
|
||||
**SEE ALSO**
|
||||
|
||||
- `wikitool version check` - asks the same feed whether a newer stack exists
|
||||
- `wikitool version bump` - writes the entry this prints
|
||||
|
||||
#### `version bump`
|
||||
|
||||
@@ -1714,18 +1849,54 @@ Raise or continue the one running candidate between two releases.
|
||||
- budget: counted
|
||||
- network: no
|
||||
|
||||
**EXAMPLES**
|
||||
|
||||
- `tools/wikitool version bump --patch --title "Fix the lint report path" --impact low`
|
||||
- `tools/wikitool version bump --minor --title "New command: wikitool review" --dry-run`
|
||||
- `tools/wikitool version bump --major --title "Rename --gate-file to --remotes-file" --breaking "--gate-file is gone; scripts must pass --remotes-file" --no-migration "no content changes"`
|
||||
|
||||
**EXIT STATUS**
|
||||
|
||||
- 0 success
|
||||
- 1 More or fewer than one of `--major/--minor/--patch`, an empty `--title`, an unknown `--impact`, a missing `VERSION`/`CHANGES.md`, `VERSION` and the changelog's newest entry naming different versions, an escalation to a boundary crossing without `--breaking` or with neither a migration document nor `--no-migration`, `--breaking`/`--no-migration` on a bump that crosses nothing, or `--migration-required` combined with `--no-migration`, on a bump with no running candidate, with no `--no-migration` line to retract, or without a migration document already targeting the new base
|
||||
- 1 Not exactly one of `--major`/`--minor`/`--patch`, an empty `--title`, or an unknown `--impact`
|
||||
- 1 `VERSION` or `CHANGES.md` is missing, or `VERSION` and the changelog's newest entry name different versions
|
||||
- 1 An escalation to a boundary crossing without `--breaking`, or with neither a migration document targeting the new base nor `--no-migration`
|
||||
- 1 `--breaking` or `--no-migration` on a bump that crosses nothing
|
||||
- 1 `--migration-required` combined with `--no-migration`, on a bump with no running candidate, with no `--no-migration` line to retract, or without a migration document already targeting the new base
|
||||
|
||||
**ON FAILURE**
|
||||
|
||||
- More or fewer than one of `--major/--minor/--patch`, an empty `--title`, an unknown `--impact`, a missing `VERSION`/`CHANGES.md`, `VERSION` and the changelog's newest entry naming different versions, an escalation to a boundary crossing without `--breaking` or with neither a migration document nor `--no-migration`, `--breaking`/`--no-migration` on a bump that crosses nothing, or `--migration-required` combined with `--no-migration`, on a bump with no running candidate, with no `--no-migration` line to retract, or without a migration document already targeting the new base -> **Not idempotent**: a second run escalates or continues the candidate again. If the outcome is uncertain, read `VERSION` and the top of `CHANGES.md` before retrying
|
||||
- Not exactly one of `--major`/`--minor`/`--patch`, an empty `--title`, or an unknown `--impact` -> Nothing was written - fix the argument and retry once
|
||||
- `VERSION` or `CHANGES.md` is missing, or `VERSION` and the changelog's newest entry name different versions -> Nothing was written - fix whichever is wrong, then retry
|
||||
- An escalation to a boundary crossing without `--breaking`, or with neither a migration document targeting the new base nor `--no-migration` -> Nothing was written - add what the error asks for, or, if nothing actually breaks, choose a smaller part
|
||||
- `--breaking` or `--no-migration` on a bump that crosses nothing -> Nothing was written - drop the flag and retry
|
||||
- `--migration-required` combined with `--no-migration`, on a bump with no running candidate, with no `--no-migration` line to retract, or without a migration document already targeting the new base -> Nothing was written - write the migration document or fix the combination, then retry
|
||||
|
||||
**NEVER**
|
||||
|
||||
- Never re-run after an uncertain outcome without first reading `VERSION` and the top of `CHANGES.md` - a second run escalates or continues the candidate again.
|
||||
- Never hand-edit `VERSION` or the machine-written parts of the entry (heading, bump list, breaking and migration lines); `--migration-required` is the only way to take the migration line back.
|
||||
|
||||
**NOTES**
|
||||
|
||||
`VERSION` gets a `-beta.N` suffix, never a second fresh number per bump. `--major/--minor/--patch` is **max-wins escalation** against the last release (patch < minor < major): a `--patch` on a MINOR candidate only advances `N`, and escalation never steps back down. Opens the matching `CHANGES.md` entry on the first bump of a candidate (heading, date, author, and a machine-managed `<!-- wikitool:bumps -->` list of every `--title` collected so far, graded by `--impact`, default `medium`) and updates that same entry in place on every later bump of the same candidate - one entry per candidate, not one per bump. The list renders grouped under `**High/Medium/Low impact**` headings (empty groups omitted), except when every bump so far is `medium`, where it stays the flat, ungrouped list the region always had - `version regrade` corrects a grade after the fact. Refuses more or fewer than one part, an empty title, an unknown `--impact`, and a `VERSION`/newest-changelog-entry mismatch. Compatibility follows the **leftmost non-zero component** of the candidate's base, which for this stack (at `1.0.0` and up) means MAJOR: PATCH is a fix, MINOR a compatible capability, MAJOR a version that is **not a drop-in replacement** - any hand-work on update, or a downgrade that no longer works. Whether content must be migrated is a second, independent question. The bump that first escalates a candidate past the boundary requires `--breaking "<what stops working>"` and, on top of it, a migration document targeting the candidate's base or `--no-migration "<reason>"`; both are anchored just above the bump list, persist over later bumps of the same candidate without being repeated, and are refused on a bump that crosses nothing at all. The two then behave differently on a *second* crossing, because they answer different questions: a further `--breaking` **joins** the ones already recorded (one reason per crossing - rendered flat on the marker line while there is only one, as bullets under a bare marker from the second onward, and repeating a reason verbatim is a no-op), while a further `--no-migration` **replaces** the single line that says whether content has to change. A candidate crossing the boundary twice is the normal shape of a long-running one, and each crossing is a separate thing an operator has to act on; whether content migrates stays one yes/no about the candidate as a whole. There is deliberately no retraction path for a single accumulated `--breaking` reason - `--migration-required` retracts the migration line, and nothing retracts a breaking one. A later bump of the same candidate that finds out `--no-migration` was wrong after all retracts that line with `--migration-required` instead of restating `--no-migration` - refused without a migration document already targeting the new base, and without an existing `--no-migration` line to retract. Which part a change earns stays a judgment call: the command enforces that a crossing documents itself, never that the part was chosen correctly
|
||||
- `VERSION` gets a `-beta.N` suffix: one running candidate between two releases, never a fresh number per bump.
|
||||
- `--major`/`--minor`/`--patch` is **max-wins escalation** against the last release (patch < minor < major): a `--patch` on a MINOR candidate only advances `N`, and escalation never steps back down.
|
||||
- The first bump of a candidate opens its `CHANGES.md` entry - heading, date, author, and a machine-managed `<!-- wikitool:bumps -->` list of every `--title` collected so far, graded by `--impact` (default `medium`). Every later bump of the same candidate updates that entry in place: one entry per candidate, not one per bump.
|
||||
- The bump list renders grouped under `**High/Medium/Low impact**` headings, empty groups omitted - except while every bump is `medium`, where it stays one flat list. `version regrade` corrects a grade after the fact.
|
||||
- Writes the heading, the bump list and the breaking/migration lines; the entry's prose is left to the author.
|
||||
- Compatibility follows the **leftmost non-zero component** of the candidate's base, which for this stack (at `1.0.0` and up) means MAJOR: PATCH is a fix, MINOR a compatible capability, MAJOR a version that is **not a drop-in replacement** - any hand-work on update, or a downgrade that no longer works. Whether content must be migrated is a second, independent question.
|
||||
- The bump that first escalates a candidate past the boundary requires `--breaking "<what stops working>"` and, on top of it, a migration document targeting the candidate's base or `--no-migration "<reason>"`. Both are anchored just above the bump list, persist over later bumps of the same candidate without being repeated, and are refused on a bump that crosses nothing at all.
|
||||
- On a later crossing of the same candidate, a further `--breaking` **joins** the reasons already recorded (flat on the marker line while there is one, bullets under a bare marker from the second on; repeating a reason verbatim is a no-op), while a further `--no-migration` **replaces** the single migration line.
|
||||
- `--migration-required` retracts the running candidate's `--no-migration` line; it needs a migration document already targeting the new base. Nothing retracts a recorded `--breaking` reason.
|
||||
- Enforces that a crossing documents itself, never that the part was chosen correctly.
|
||||
- Not idempotent: every successful run escalates or continues the candidate again.
|
||||
- `--dry-run` reports the step without writing.
|
||||
|
||||
**SEE ALSO**
|
||||
|
||||
- `wikitool version regrade` - corrects an `--impact` grade
|
||||
- `wikitool version release` - fixes the candidate into a release
|
||||
- `instructions/migrate-corpus.md` - how a migration document is written
|
||||
|
||||
#### `version regrade`
|
||||
|
||||
@@ -1743,18 +1914,38 @@ List the running candidate's bump titles with their impact grade, or change one
|
||||
- budget: exempt_without_args
|
||||
- network: no
|
||||
|
||||
**EXAMPLES**
|
||||
|
||||
- `tools/wikitool version regrade`
|
||||
- `tools/wikitool version regrade 3 7 --impact high`
|
||||
|
||||
**EXIT STATUS**
|
||||
|
||||
- 0 success
|
||||
- 1 A missing `VERSION`/`CHANGES.md`, `VERSION` and the changelog's newest entry naming different versions, a topmost entry with no bump list, an index outside the rendered list's range, indices given without `--impact`, or an unknown `--impact`
|
||||
- 1 `VERSION` or `CHANGES.md` is missing, `VERSION` and the changelog's newest entry name different versions, or the topmost entry has no bump list
|
||||
- 1 An index outside the rendered list's range, indices given without `--impact`, or an unknown `--impact`
|
||||
|
||||
**ON FAILURE**
|
||||
|
||||
- A missing `VERSION`/`CHANGES.md`, `VERSION` and the changelog's newest entry naming different versions, a topmost entry with no bump list, an index outside the rendered list's range, indices given without `--impact`, or an unknown `--impact` -> The bare listing never writes anything. A write is **not idempotent** against a changed list: re-running the same indices after a first success regrades whatever is at those positions *now*, which may no longer be the same bumps - list again before retrying
|
||||
- `VERSION` or `CHANGES.md` is missing, `VERSION` and the changelog's newest entry name different versions, or the topmost entry has no bump list -> Nothing was written - fix whichever is wrong, then retry
|
||||
- An index outside the rendered list's range, indices given without `--impact`, or an unknown `--impact` -> Nothing was written - list again with the bare command, then retry once with corrected arguments
|
||||
|
||||
**NEVER**
|
||||
|
||||
- Never re-run the same indices after a write without listing again first - the positions may have moved.
|
||||
|
||||
**NOTES**
|
||||
|
||||
1-based rendered position (no arguments - the correction path for a `--impact` judgement made at bump time), or change one or more of them in a single call: `version regrade 3 7 --impact high` grades both against a single read of today's list, not position 3 first and then position 7 against whatever that produced. Touches only the topmost entry's bump list - never `VERSION`, never any other part of `CHANGES.md`. The bare listing is read-only and exempt from the Iteration Budget Gate, like `version notes`; a call with indices writes `CHANGES.md` and is counted like `version bump`. Refuses an index outside the rendered list's range, an unknown `--impact`, indices given without `--impact`, a missing `VERSION`/`CHANGES.md`, a `VERSION`/newest-changelog-entry mismatch, or a topmost entry with no bump list at all
|
||||
- Without arguments: lists the running candidate's bump titles with their grade, numbered by 1-based position in the rendered list (High before Medium before Low, chronological within a grade). Read-only and exempt from the Iteration Budget Gate.
|
||||
- With indices and `--impact`: sets the grade of every named position in one call, all resolved against a single read of the current list - `version regrade 3 7 --impact high` grades what is at 3 and 7 now, not 7 after 3 has moved. Writes `CHANGES.md` and is counted by the Iteration Budget Gate.
|
||||
- The correction path for an `--impact` grade judged at bump time.
|
||||
- Touches only the topmost entry's bump list - never `VERSION`, never any other part of `CHANGES.md`.
|
||||
- A write is not idempotent against a changed list: after a first success, the same indices may name different bumps.
|
||||
|
||||
**SEE ALSO**
|
||||
|
||||
- `wikitool version bump` - sets the grade in the first place
|
||||
- `wikitool version release` - refuses without a summary above this list
|
||||
|
||||
#### `version release`
|
||||
|
||||
@@ -1772,18 +1963,42 @@ Fix the running candidate: strip `VERSION`'s `-beta.N` suffix and close its `CHA
|
||||
- budget: counted
|
||||
- network: no
|
||||
|
||||
**EXAMPLES**
|
||||
|
||||
- `tools/wikitool version release --dry-run`
|
||||
- `tools/wikitool version release --title "Command records rewritten for agents"`
|
||||
|
||||
**EXIT STATUS**
|
||||
|
||||
- 0 success
|
||||
- 1 A missing `VERSION`/`CHANGES.md`, `VERSION` already a release (no running candidate), `VERSION` and the changelog's newest entry naming different versions, or (from two bumps on) an entry with no summary paragraph above the changesets
|
||||
- 1 `VERSION` is already a release - there is no running candidate to fix
|
||||
- 1 `VERSION` or `CHANGES.md` is missing, or `VERSION` and the changelog's newest entry name different versions
|
||||
- 1 Two or more bumps and no summary paragraph above the changesets
|
||||
|
||||
**ON FAILURE**
|
||||
|
||||
- A missing `VERSION`/`CHANGES.md`, `VERSION` already a release (no running candidate), `VERSION` and the changelog's newest entry naming different versions, or (from two bumps on) an entry with no summary paragraph above the changesets -> **Not idempotent**: a second run fails outright once the suffix is gone. If the outcome is uncertain, read `VERSION` before retrying - a release-shaped `VERSION` means it already ran
|
||||
- `VERSION` is already a release - there is no running candidate to fix -> After an uncertain run this means it already ran; otherwise there is nothing to release
|
||||
- `VERSION` or `CHANGES.md` is missing, or `VERSION` and the changelog's newest entry name different versions -> Fix whichever is wrong, then retry
|
||||
- Two or more bumps and no summary paragraph above the changesets -> Write a short summary paragraph right below the bump list, then retry
|
||||
|
||||
**NEVER**
|
||||
|
||||
- Never retry after an uncertain outcome without reading `VERSION` first - a release-shaped `VERSION` means it already ran.
|
||||
|
||||
**NOTES**
|
||||
|
||||
Ends the pre-release phase `version bump` started. Without `--title` the heading keeps whichever bump last set it; with it, the heading's title is replaced - the normal case for a candidate that collected several bump titles, since the entry wants a summarising heading rather than the most recent one. Leaves the entry's machine-managed bump-title list untouched, as the record of what happened. Refuses when the candidate collected two or more bumps and the entry still carries no summary paragraph (at least 200 non-whitespace characters) between the bump list and the first `### <bump title>` changeset heading; a candidate with exactly one bump is exempt, since there its own changeset already is the summary. `--dry-run` runs this check too and reports the same refusal. Commits nothing and pushes nothing (invariant 5) - the following `publish` moves `VERSION` onto `main`, which `release.yml` reacts to. Refuses when `VERSION` is already a release (no running candidate to fix), or when the changelog's newest entry does not match `VERSION`
|
||||
- Strips `VERSION`'s `-beta.N` suffix - the candidate's base becomes the release - and closes the candidate's `CHANGES.md` entry.
|
||||
- Without `--title` the heading keeps whichever bump last set it; `--title` replaces it - the normal case for a candidate that collected several bumps, whose entry wants a summarising heading rather than the most recent one.
|
||||
- Leaves the entry's machine-managed bump list untouched, as the record of what happened.
|
||||
- From two bumps on, requires a summary paragraph (at least 200 non-whitespace characters) between the bump list and the first `### <bump title>` changeset heading. A candidate with exactly one bump is exempt, since there its own changeset already is the summary. `--dry-run` runs this check too.
|
||||
- Commits nothing and pushes nothing. The following `publish` moves `VERSION` onto `main`, which `release.yml` reacts to.
|
||||
- Not idempotent: a second run fails once the suffix is gone.
|
||||
|
||||
**SEE ALSO**
|
||||
|
||||
- `wikitool version bump` - opens and continues the candidate
|
||||
- `wikitool version regrade` - shows the bump list the summary sits under
|
||||
- `wikitool publish` - moves the released `VERSION` onto `main`
|
||||
|
||||
### Content migrations
|
||||
|
||||
|
||||
+180
-102
@@ -467,12 +467,14 @@ def build_plan(origin: Optional[Origin] = None) -> dict[str, PlannedFile]:
|
||||
if source.is_file():
|
||||
plan[relative] = _read_planned_file(source, relative)
|
||||
|
||||
# `raw/` and `incoming/` are both flat now (Gitea #67 removes type
|
||||
# `raw/` and `incoming/` are both flat (Gitea #67 removed type
|
||||
# subdirectories from the addressing scheme entirely - a file's location
|
||||
# under `raw/` is a date shard computed by `raw accept`, never a hand-picked
|
||||
# type). `.gitignore` (also exported, see ROOT_FILES) excludes `incoming/`
|
||||
# again once the instance is a git repo, which is why bootstrap.md
|
||||
# re-creates it for a plain clone that never had this export step at all.
|
||||
# type), so an export creates no subdirectories under either root. The
|
||||
# exported `.gitignore` excludes what lands in `incoming/` but not its
|
||||
# `.gitkeep` (`/incoming/*` plus `!/incoming/.gitkeep`, Gitea #88), so the
|
||||
# anchor is tracked and a plain clone of the instance has the directory
|
||||
# without any bootstrap step re-creating it.
|
||||
plan["raw/.gitkeep"] = PlannedFile("")
|
||||
plan["incoming/.gitkeep"] = PlannedFile("")
|
||||
|
||||
@@ -583,36 +585,63 @@ def _write_plan(target: Path, plan: dict[str, PlannedFile]) -> None:
|
||||
atomic="Yes - nothing is written until every file is planned",
|
||||
budget=cli_contract.Budget.COUNTED,
|
||||
),
|
||||
notes="Write a contentless, distributable copy of this repo's machinery into an empty "
|
||||
"`<target>` directory: `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 and their schemas "
|
||||
"re-keyed as `.template`, the stack's own verbatim), `docs/` verbatim, `tools/` (no "
|
||||
"venv/caches), the `.github/hooks/`+`.vibe/` session-tracing config plus "
|
||||
"`.claude/settings.json`, `kb/CONTRACT.md` (no pages, no areas), the two flat anchors "
|
||||
"`raw/.gitkeep` and `incoming/.gitkeep` (both roots are flat now that a file's location "
|
||||
"under `raw/` is a date shard rather than a hand-picked type, so a fresh export no longer "
|
||||
"creates any type subdirectories under either root; `incoming/.gitkeep` is trackable and "
|
||||
"survives becoming a git repository, so a plain clone gets the directory without any "
|
||||
"bootstrap step re-creating it), `VERSION`, `USER.md.template`/`SOUL.md.template` plus "
|
||||
"`kb/CONVENTIONS.md.template` and each collection's contract re-keyed as "
|
||||
"`kb/<name>/COLLECTION.md.template` (the templates ship; the filled "
|
||||
"`USER.md`/`SOUL.md`/`kb/CONVENTIONS.md`/`kb/<name>/COLLECTION.md`/`types/<page-type>.md` "
|
||||
"never do - all of them bind their instance and none are the stack's to decide, and "
|
||||
"`find_leaks` refuses a plan carrying one), and a generated `.wikitool-release.json` stamp "
|
||||
"(version, export date, origin, and a sha256 per exported file - the base a later upgrade "
|
||||
"would compare against). The four origin options only fill stamp fields: `export` never "
|
||||
"calls git and cannot discover them. Refuses a non-empty target, and a tree with no "
|
||||
"`VERSION`. See `instructions/setup-instance.md`. One-way: there is no command that "
|
||||
"reconstructs a distributed instance into a dev instance - work on the stack in the origin "
|
||||
"repo (or a new dev instance exported from it) instead",
|
||||
failures=(cli_contract.Failure(
|
||||
label="",
|
||||
cause="Target exists and is not empty, is not a directory, or the tree has no "
|
||||
"readable `VERSION`",
|
||||
reaction="Point `<target>` at an empty (or new) directory and retry. Never merge into a "
|
||||
"non-empty one by hand",
|
||||
),),
|
||||
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 and their schemas re-keyed as `.template`, the stack's own "
|
||||
"verbatim), `docs/` verbatim, `tools/` (no venv/caches), 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` 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.",
|
||||
"The four origin options only fill stamp fields: `export` never calls git and cannot "
|
||||
"discover them.",
|
||||
"One-way: no command reconstructs a distributed instance into a dev instance - work on "
|
||||
"the stack in the origin repo, or in a new dev instance exported from it.",
|
||||
"`--dry-run` lists every file it would write, and writes nothing.",
|
||||
),
|
||||
failures=(
|
||||
cli_contract.Failure(
|
||||
cause="`<target>` exists and is not empty, or is not a directory",
|
||||
reaction="Point `<target>` at an empty (or new) directory and retry",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="The tree has no readable `VERSION`",
|
||||
reaction="Fix `VERSION`, then retry",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="A licence file is missing from the source tree",
|
||||
reaction="Restore it, then retry",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="The plan would carry this instance's own data (a filled personalization, "
|
||||
"conventions or page type-spec file)",
|
||||
reaction="Report it: it is an allowlist bug in `dist export`, not something to "
|
||||
"work around by deleting files from the target",
|
||||
),
|
||||
),
|
||||
examples=(
|
||||
"tools/wikitool dist export ../my-wiki --dry-run",
|
||||
"tools/wikitool dist export ../my-wiki",
|
||||
),
|
||||
never=(
|
||||
"Never merge an export into a non-empty directory by hand.",
|
||||
),
|
||||
see_also=(
|
||||
"`instructions/setup-instance.md` - what comes after the export",
|
||||
"`wikitool dist upgrade` - applies a later export to an existing instance",
|
||||
"`wikitool version show` - reads the stamp this writes",
|
||||
),
|
||||
))
|
||||
@app.command("export")
|
||||
def export_command(
|
||||
@@ -961,76 +990,119 @@ def _report_plan(
|
||||
properties=cli_contract.Properties(
|
||||
effect=cli_contract.Effect.WRITE,
|
||||
idempotent=cli_contract.Idempotent.NO,
|
||||
atomic="**Yes for the refusal cases above - nothing is written.** Once writing starts "
|
||||
"it is a plain sequential file copy with no partial-state cleanup: an interruption "
|
||||
"mid-copy (killed process, disk full) can leave the tree part-old, part-new",
|
||||
atomic="Yes for every refusal - nothing is written. Once writing starts it is a plain "
|
||||
"sequential file copy with no partial-state cleanup: an interruption mid-copy "
|
||||
"(killed process, disk full) can leave the tree part-old, part-new",
|
||||
budget=cli_contract.Budget.COUNTED,
|
||||
),
|
||||
notes="Never downloads anything: `<source>` is an already-fetched export directory or "
|
||||
"`.tar.gz` release archive (verified against a sibling `.sha256` if one is present; WARNs, "
|
||||
"does not block, if it is absent), which must unpack to exactly one top-level directory - "
|
||||
"the shape `.gitea/workflows/release.yml` packs. The write set is exactly the *new* "
|
||||
"`.wikitool-release.json`'s `files` block, minus what an export re-seeds from a blank "
|
||||
"template every time (`kb/log.md`, `raw/.gitkeep` - `chemenu.ownership.is_export_stub`) or "
|
||||
"seeds once and the instance owns from then on (`.wikitool-kb.json`, `CHANGES.md` - "
|
||||
"`chemenu.ownership.is_upgrade_preserved`), plus the stamp itself, always rewritten. Every "
|
||||
"candidate path is classified against the *local* `.wikitool-release.json`'s recorded "
|
||||
"digest for it: unchanged is overwritten silently, absent from the old stamp is created, "
|
||||
"and locally modified or locally deleted is **never** silently overwritten - the run aborts "
|
||||
"with the full list, and its text names the three answers with the command line already "
|
||||
"filled in, so that no reader takes any of them for the default. `--keep-local` proceeds "
|
||||
"and leaves every one of them untouched; `--take-release <path>` (repeatable) writes the "
|
||||
"release's version over the named path, discarding the local change, and re-creates it if "
|
||||
"it was locally deleted. The two are decided per path and compose on one call: without "
|
||||
"`--keep-local`, a locally changed path that no `--take-release` names still aborts the "
|
||||
"run. A `--take-release` path that this run does not report as locally changed is refused, "
|
||||
"in a `--dry-run` as well as a writing run - it is a mistake in the argument rather than a "
|
||||
"state of the tree, and a path that silently did nothing would report a successful upgrade "
|
||||
"while keeping the change it was asked to discard. After a `--keep-local` run the new stamp "
|
||||
"is still written whole, so it records the release's digest for files that were "
|
||||
"deliberately *not* written: the stamp is the baseline for the next comparison, not a "
|
||||
"literal inventory of what is on disk. That is what keeps a skipped file diverging - and "
|
||||
"therefore reported - on every later run, rather than quietly reading as current once it "
|
||||
"has been skipped once. A path taken with `--take-release` is the opposite case and the "
|
||||
"reason the flag exists: it was written, so it matches the digest the stamp records and "
|
||||
"stops being reported at all. A path in the old stamp but not the new one is reported as no "
|
||||
"longer part of the release and left alone unless `--prune` is passed, which removes it "
|
||||
"only if it is still unchanged since installation. Reports the migration chain the new "
|
||||
"machinery would owe (`chemenu.kb_state.chain` over the *new* tree's "
|
||||
"`instructions/migrations/`, read via a `directory` argument to `load_migrations`) but "
|
||||
"never runs any of it - there is no `migrate run`. Refuses before touching the source at "
|
||||
"all when: `VERSION` or `.wikitool-release.json` (with a `files` block) is missing locally, "
|
||||
"`.wikitool-kb.json` is missing, a migration is already outstanding against the *installed* "
|
||||
"machinery, or the working tree is dirty (not being a git repository at all is a WARN, not "
|
||||
"a refusal). Refuses after reading the source when: it carries no "
|
||||
"`VERSION`/`.wikitool-release.json`/`files` block, its version is older than or equal to "
|
||||
"the installed one (equal is a no-op success), it is a pre-release (`-beta.N`) without "
|
||||
"`--pre`, or `--take-release` names a path this run does not classify as locally changed. "
|
||||
"Reports, but does not block on, a crossed compatibility boundary. Never touches git - no "
|
||||
"commit, no push (invariant 5). The closing report carries no step list of its own: "
|
||||
"everything after the swap is one order, written in `instructions/upgrade-instance.md`, "
|
||||
"which the report names and which resumes at `instructions sync`. What a human decides "
|
||||
"*before* the swap - which release, whether to take it, where the tarball comes from - is "
|
||||
"`INSTALL.md` § \"Version und Updates\"",
|
||||
failures=(cli_contract.Failure(
|
||||
label="",
|
||||
cause="Missing local `VERSION`/`.wikitool-release.json`(`files`)/`.wikitool-kb.json`, "
|
||||
"a migration already outstanding against the installed machinery, a dirty working "
|
||||
"tree, a source with no `VERSION`/stamp/`files` block, a source version that is older "
|
||||
"than, equal to, or (without `--pre`) a pre-release relative to the installed one, a "
|
||||
"`--take-release` path that is not classified as locally changed (the one refusal a "
|
||||
"`--dry-run` also raises), or one or more locally changed files that neither "
|
||||
"`--keep-local` nor a `--take-release` answers for",
|
||||
reaction="For every refusal above: fix the named precondition and retry - none of them are "
|
||||
"transient. For a rejected `--take-release` path: correct it against the "
|
||||
"locally-changed list the refusal prints. For locally changed files, the refusal names "
|
||||
"all three answers with the re-run line filled in - `--take-release <path>` to write "
|
||||
"the release's version over it (which ends the divergence), `--keep-local` to leave "
|
||||
"them untouched (repeatable, and it reports the same files again on every subsequent "
|
||||
"run until they stop diverging), or reconcile by hand and retry. An interrupted write "
|
||||
"is not resumed automatically; compare the tree against the printed classification and "
|
||||
"finish or revert by hand",
|
||||
),),
|
||||
notes=(
|
||||
"Never downloads anything: `<source>` is an already-fetched export directory or "
|
||||
"`.tar.gz` release archive. An archive is verified against a sibling `.sha256` if one "
|
||||
"is present (a missing one is a WARN, not a block) and must unpack to exactly one "
|
||||
"top-level directory - the shape `.gitea/workflows/release.yml` packs.",
|
||||
"The write set is exactly the *new* `.wikitool-release.json`'s `files` block, minus "
|
||||
"what an export re-seeds from a blank template every time (`kb/log.md`, "
|
||||
"`raw/.gitkeep`) or seeds once and the instance owns from then on "
|
||||
"(`.wikitool-kb.json`, `CHANGES.md`), plus the stamp itself, always rewritten.",
|
||||
"Every candidate path is classified against the *local* `.wikitool-release.json`'s "
|
||||
"recorded digest: unchanged is overwritten silently, absent from the old stamp is "
|
||||
"created, and locally modified or locally deleted is **never** silently overwritten.",
|
||||
"Locally changed files abort the run with the full list; the abort text names the "
|
||||
"three answers with the command line filled in, and none of them is the default.",
|
||||
"`--keep-local` proceeds and leaves every locally changed file untouched. The new "
|
||||
"stamp is still written whole, recording the release's digest for files that were "
|
||||
"not written, so a skipped file keeps diverging and is reported again on every later "
|
||||
"run.",
|
||||
"`--take-release <path>` (repeatable) writes the release's version over the named "
|
||||
"path, discarding the local change, and re-creates it if it was locally deleted. The "
|
||||
"path then matches the stamp and stops being reported.",
|
||||
"The two are decided per path and compose on one call: without `--keep-local`, a "
|
||||
"locally changed path that no `--take-release` names still aborts the run.",
|
||||
"A path in the old stamp but not the new one is reported as no longer part of the "
|
||||
"release and left alone, unless `--prune` is passed, which removes it only if it is "
|
||||
"still unchanged since installation.",
|
||||
"Reports the migration chain the new machinery would owe, but never runs any of it - "
|
||||
"there is no `migrate run`.",
|
||||
"Reports, but does not block on, a crossed compatibility boundary.",
|
||||
"The local preconditions - `VERSION`, the release stamp, `.wikitool-kb.json`, no "
|
||||
"outstanding migration, a clean working tree - are checked before the source is read. "
|
||||
"Not being a git repository at all is a WARN, not a refusal.",
|
||||
"Never touches git - no commit, no push.",
|
||||
"`--dry-run` classifies and reports without writing; a pre-release (`-beta.N`) source "
|
||||
"needs `--pre`.",
|
||||
"An interrupted write is not resumed automatically: compare the tree against the "
|
||||
"printed classification and finish or revert by hand.",
|
||||
"The closing report names `instructions/upgrade-instance.md`, which carries the order "
|
||||
"for everything after the swap and resumes at `instructions sync`.",
|
||||
),
|
||||
failures=(
|
||||
cli_contract.Failure(
|
||||
cause="The source's version equals the installed one - a no-op success",
|
||||
reaction="",
|
||||
code=0,
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="Local `VERSION` missing, or no local `.wikitool-release.json` with a `files` "
|
||||
"block",
|
||||
reaction="Not transient - fix the named precondition and retry. A checkout with "
|
||||
"shared git history takes stack updates with `wikitool upstream merge` instead",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="`.wikitool-kb.json` is missing",
|
||||
reaction="Run `wikitool migrate baseline <version>`, then retry",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="A migration is already outstanding against the *installed* machinery",
|
||||
reaction="Finish it first - `wikitool migrate status` names it - then retry",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="The working tree is dirty",
|
||||
reaction="Commit or stash first, then retry",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="`<source>` does not exist, fails its `.sha256`, or does not unpack to exactly "
|
||||
"one top-level directory",
|
||||
reaction="Fix the path or re-download the release archive, then retry",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="The source carries no `VERSION`, `.wikitool-release.json` or `files` block",
|
||||
reaction="Point `<source>` at a distribution export, then retry",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="The source's version is older than the installed one, or a pre-release "
|
||||
"without `--pre`",
|
||||
reaction="Not transient - choose another source, or pass `--pre` for a pre-release",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="A `--take-release` path this run does not classify as locally changed - the "
|
||||
"one refusal a `--dry-run` also raises",
|
||||
reaction="Correct it against the locally-changed list the refusal prints, then retry",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="Locally changed files that neither `--keep-local` nor a `--take-release` "
|
||||
"answers for; nothing was written",
|
||||
reaction="Choose one of the three answers the refusal names, re-run lines filled in: "
|
||||
"`--take-release <path>` to write the release's version over it (ends the "
|
||||
"divergence), `--keep-local` to leave them untouched (reported again on every "
|
||||
"later run until they stop diverging), or reconcile by hand and retry",
|
||||
),
|
||||
),
|
||||
examples=(
|
||||
"tools/wikitool dist upgrade ../chemenu-7.1.0.tar.gz --dry-run",
|
||||
"tools/wikitool dist upgrade ../chemenu-7.1.0.tar.gz",
|
||||
"tools/wikitool dist upgrade ../chemenu-7.1.0.tar.gz --take-release tools/README.md",
|
||||
),
|
||||
never=(
|
||||
"Never treat any of the three answers to locally changed files as the default.",
|
||||
),
|
||||
see_also=(
|
||||
"`wikitool version check` - finds out whether an update exists",
|
||||
"`instructions/upgrade-instance.md` - the order after the swap",
|
||||
"`INSTALL.md` § \"Version und Updates\" - which release, whether to take it, where "
|
||||
"the tarball comes from",
|
||||
"`wikitool upstream merge` - the update path for a checkout with shared git history",
|
||||
"`wikitool migrate status` - the migrations the report names",
|
||||
),
|
||||
))
|
||||
@app.command("upgrade")
|
||||
def upgrade_command(
|
||||
@@ -1226,6 +1298,12 @@ def run_upgrade(
|
||||
dst = config.ROOT / relative
|
||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(src, dst)
|
||||
# The new stamp is written whole even after `--keep-local` skipped files: it is the
|
||||
# baseline for the next comparison, not a literal inventory of what is on disk. That
|
||||
# is what keeps a skipped file diverging - and therefore reported - on every later
|
||||
# run, rather than quietly reading as current once it has been skipped once. A
|
||||
# `--take-release` path is the opposite case: it was written, so it matches the
|
||||
# digest recorded here and stops being reported at all.
|
||||
shutil.copy2(stamp_path, config.ROOT / version_mod.RELEASE_STAMP_FILENAME)
|
||||
|
||||
pruned: list[str] = []
|
||||
|
||||
@@ -89,14 +89,26 @@ def _describe_origin(stamp: Optional[dict]) -> str:
|
||||
atomic="Read-only",
|
||||
budget=cli_contract.Budget.EXEMPT,
|
||||
),
|
||||
notes="Development tree, or a distribution with its export date and origin. Bare "
|
||||
"`wikitool version` is an alias for this. Read-only, offline, and **exempt from the "
|
||||
"Iteration Budget Gate**",
|
||||
notes=(
|
||||
"Prints `VERSION` and where this tree came from: \"development tree\" without a "
|
||||
"release stamp, otherwise the export date, source commit and repository from "
|
||||
"`.wikitool-release.json`, plus the release page when the stamp records one.",
|
||||
"`--json` prints the version, its compatibility key, the stamp and the update URL.",
|
||||
"Bare `wikitool version` is an alias for this.",
|
||||
"Read-only and offline; exempt from the Iteration Budget Gate.",
|
||||
),
|
||||
failures=(cli_contract.Failure(
|
||||
label="",
|
||||
cause="`VERSION` is missing or unparseable",
|
||||
reaction="Fix `VERSION` and retry",
|
||||
),),
|
||||
examples=(
|
||||
"tools/wikitool version show",
|
||||
"tools/wikitool version show --json",
|
||||
),
|
||||
see_also=(
|
||||
"`wikitool version check` - asks whether a newer stack exists",
|
||||
"`wikitool version notes` - prints a version's release notes",
|
||||
),
|
||||
))
|
||||
@app.command("show")
|
||||
def show_command(
|
||||
@@ -141,22 +153,47 @@ def show_command(
|
||||
budget=cli_contract.Budget.EXEMPT,
|
||||
network=cli_contract.Network.YES,
|
||||
),
|
||||
notes="Ask the origin's release feed whether a newer stack exists, and whether the step "
|
||||
"crosses a compatibility boundary (`state: current|update|migration|ahead`). One of the "
|
||||
"**two** commands in `wikitool` that make a network call, and the only one whose whole job "
|
||||
"it is - `version notes` is the other, and only on a distributed instance. Never reached "
|
||||
"implicitly from another command, needs no key, times out, and reports an unreachable feed "
|
||||
"as an error rather than as \"up to date\". The feed is `$WIKITOOL_UPDATE_URL`, else the "
|
||||
"release stamp's, else the built-in origin; `$WIKITOOL_UPDATE_TOKEN` is only needed if that "
|
||||
"feed is not readable anonymously. Read-only and exempt from the budget gate",
|
||||
failures=(cli_contract.Failure(
|
||||
label="",
|
||||
cause="The feed could not be reached, answered non-JSON, or carried no `tag_name`. "
|
||||
"**Never** answers \"up to date\" for a question it could not ask",
|
||||
reaction="A network failure is transient - retry once, then report it. HTTP 401/403 names "
|
||||
"`$WIKITOOL_UPDATE_TOKEN`; 404 means no release exists yet or the URL points at the "
|
||||
"wrong repo",
|
||||
),),
|
||||
notes=(
|
||||
"Asks the release feed for its latest release and compares it with `VERSION`: `state` "
|
||||
"is `current`, `update`, `migration` (the step crosses a compatibility boundary) or "
|
||||
"`ahead`.",
|
||||
"One of the **two** commands in `wikitool` that make a network call, and the only one "
|
||||
"whose whole job it is - `version notes` is the other, and only on a distributed "
|
||||
"instance.",
|
||||
"Never reached implicitly from another command, needs no key, and times out after "
|
||||
"`--timeout` seconds (default 10).",
|
||||
"The feed is `--url`, else `$WIKITOOL_UPDATE_URL`, else the release stamp's, else the "
|
||||
"built-in origin. `$WIKITOOL_UPDATE_TOKEN` is only needed if that feed is not readable "
|
||||
"anonymously.",
|
||||
"For `update` or `migration` it prints that applying the release is a separate, manual "
|
||||
"step (`INSTALL.md` § \"Eine Instanz aktualisieren\").",
|
||||
"Read-only; exempt from the Iteration Budget Gate.",
|
||||
),
|
||||
failures=(
|
||||
cli_contract.Failure(
|
||||
cause="`VERSION` is missing or unparseable",
|
||||
reaction="Fix `VERSION` and retry",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="The feed could not be reached, answered non-JSON, or carried no `tag_name`",
|
||||
reaction="A network failure is transient - retry once, then report it. HTTP 401/403 "
|
||||
"names `$WIKITOOL_UPDATE_TOKEN`; 404 means no release exists yet or the URL points "
|
||||
"at the wrong repo",
|
||||
),
|
||||
),
|
||||
examples=(
|
||||
"tools/wikitool version check",
|
||||
"tools/wikitool version check --json",
|
||||
),
|
||||
never=(
|
||||
"Never report \"up to date\" for a check that failed - an unreachable feed is not "
|
||||
"that answer.",
|
||||
),
|
||||
see_also=(
|
||||
"`wikitool dist upgrade` - applies the update this reports",
|
||||
"`wikitool version notes` - prints the release notes",
|
||||
"`INSTALL.md` § \"Version und Updates\" - what the operator decides before an update",
|
||||
),
|
||||
))
|
||||
@app.command("check")
|
||||
def check_command(
|
||||
@@ -238,33 +275,53 @@ def check_command(
|
||||
budget=cli_contract.Budget.EXEMPT,
|
||||
network=cli_contract.Network.YES,
|
||||
),
|
||||
notes="Default: this tree's `VERSION`. The `CHANGES.md` entry where there is one, and where "
|
||||
"there is not, the feed's latest release notes. The fallback exists because an instance's "
|
||||
"`CHANGES.md` is a stub `dist upgrade` never overwrites (`chemenu.ownership.is_upgrade_"
|
||||
"preserved`), so the local file can never carry the entry - not today and not after any "
|
||||
"future release, which made the command permanently unanswerable exactly where the release "
|
||||
"notes are most needed. It is reached **only with a release stamp present**, i.e. only from "
|
||||
"a `dist export` tree: a dev checkout keeps the plain error, which is what keeps the origin "
|
||||
"repo and CI offline. **stdout carries nothing but the notes**; the line naming the feed "
|
||||
"being asked, and the one naming the release that answered, go to stderr - `release.yml` "
|
||||
"redirects stdout into the file it posts as the release body. Only the feed's *latest* "
|
||||
"release can be asked for (`update_url` is the one URL a stamp records, and composing a "
|
||||
"by-tag URL out of it would be guessing at an API shape), so a returned version other than "
|
||||
"the one asked for is named on stderr and printed anyway - the expected shape before an "
|
||||
"upgrade, where `VERSION` still names the release being left. `--offline` refuses the call "
|
||||
"and fails with the stamp's `release_url` instead. Read-only and exempt from the budget gate",
|
||||
failures=(cli_contract.Failure(
|
||||
label="",
|
||||
cause="An unparseable `--version`, an unreadable `VERSION` when `--version` is "
|
||||
"omitted, or a missing `CHANGES.md`. No entry for the requested version is an error "
|
||||
"only where the feed cannot answer either: in a tree with no release stamp (a dev "
|
||||
"checkout - write the entry, or `version bump`), with `--offline`, or when the feed "
|
||||
"could not be reached or returned a release with an empty `body`. Every one of those "
|
||||
"failures names the stamp's `release_url` where it has one, so a run that cannot read "
|
||||
"the notes is still told where they are",
|
||||
reaction="Fix the named argument or file, then retry. A feed failure is transient - retry "
|
||||
"once, then read the release page the error names. Safe to retry",
|
||||
),),
|
||||
notes=(
|
||||
"Prints the `CHANGES.md` entry for `--version` (default: this tree's `VERSION`).",
|
||||
"With no such entry, in a tree with a release stamp (a `dist export` tree) and without "
|
||||
"`--offline`, it prints the release feed's latest release notes instead. A tree "
|
||||
"without a stamp (a dev checkout) never asks the feed.",
|
||||
"Only the feed's latest release can be asked for. When that is a different version "
|
||||
"than requested, stderr names it and the notes are printed anyway - the expected case "
|
||||
"before an upgrade, where `VERSION` still names the release being left.",
|
||||
"stdout carries nothing but the notes; the line naming the feed being asked and the "
|
||||
"one naming the release that answered go to stderr. `release.yml` redirects stdout "
|
||||
"into the file it posts as the release body.",
|
||||
"`--offline` never asks the feed and fails with the stamp's `release_url` instead.",
|
||||
"Every failure names the stamp's `release_url` where it has one, so a run that cannot "
|
||||
"read the notes is still told where they are.",
|
||||
"Read-only, safe to retry, and exempt from the Iteration Budget Gate.",
|
||||
),
|
||||
failures=(
|
||||
cli_contract.Failure(
|
||||
cause="`--version` is unparseable, `VERSION` is unreadable when `--version` is "
|
||||
"omitted, or `CHANGES.md` is missing",
|
||||
reaction="Fix the named argument or file, then retry",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="No entry for the requested version in a tree with no release stamp (a dev "
|
||||
"checkout)",
|
||||
reaction="Write the entry, or run `version bump`",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="No entry for the requested version, and `--offline` was passed",
|
||||
reaction="Read the release page the error names",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="No entry for the requested version, and the feed could not be reached or "
|
||||
"returned a release with an empty `body`",
|
||||
reaction="A feed failure is transient - retry once, then read the release page the "
|
||||
"error names",
|
||||
),
|
||||
),
|
||||
examples=(
|
||||
"tools/wikitool version notes",
|
||||
"tools/wikitool version notes --version 7.0.0",
|
||||
"tools/wikitool version notes --offline",
|
||||
),
|
||||
see_also=(
|
||||
"`wikitool version check` - asks the same feed whether a newer stack exists",
|
||||
"`wikitool version bump` - writes the entry this prints",
|
||||
),
|
||||
))
|
||||
@app.command("notes")
|
||||
def notes_command(
|
||||
@@ -398,55 +455,90 @@ def _no_entry_message(wanted: Version, has_stamp: bool, release_url: str) -> str
|
||||
atomic="No - `VERSION` then `CHANGES.md`",
|
||||
budget=cli_contract.Budget.COUNTED,
|
||||
),
|
||||
notes="`VERSION` gets a `-beta.N` suffix, never a second fresh number per bump. "
|
||||
"`--major/--minor/--patch` is **max-wins escalation** against the last release "
|
||||
"(patch < minor < major): a `--patch` on a MINOR candidate only advances `N`, and "
|
||||
"escalation never steps back down. Opens the matching `CHANGES.md` entry on the first bump "
|
||||
"of a candidate (heading, date, author, and a machine-managed `<!-- wikitool:bumps -->` "
|
||||
"list of every `--title` collected so far, graded by `--impact`, default `medium`) and "
|
||||
"updates that same entry in place on every later bump of the same candidate - one entry per "
|
||||
"candidate, not one per bump. The list renders grouped under "
|
||||
"`**High/Medium/Low impact**` headings (empty groups omitted), except when every bump so "
|
||||
"far is `medium`, where it stays the flat, ungrouped list the region always had - "
|
||||
"`version regrade` corrects a grade after the fact. Refuses more or fewer than one part, an "
|
||||
"empty title, an unknown `--impact`, and a `VERSION`/newest-changelog-entry mismatch. "
|
||||
"Compatibility follows the **leftmost non-zero component** of the candidate's base, which "
|
||||
"for this stack (at `1.0.0` and up) means MAJOR: PATCH is a fix, MINOR a compatible "
|
||||
"capability, MAJOR a version that is **not a drop-in replacement** - any hand-work on "
|
||||
"update, or a downgrade that no longer works. Whether content must be migrated is a second, "
|
||||
"independent question. The bump that first escalates a candidate past the boundary requires "
|
||||
"`--breaking \"<what stops working>\"` and, on top of it, a migration document targeting "
|
||||
"the candidate's base or `--no-migration \"<reason>\"`; both are anchored just above the "
|
||||
"bump list, persist over later bumps of the same candidate without being repeated, and are "
|
||||
"refused on a bump that crosses nothing at all. The two then behave differently on a "
|
||||
"*second* crossing, because they answer different questions: a further `--breaking` "
|
||||
"**joins** the ones already recorded (one reason per crossing - rendered flat on the marker "
|
||||
"line while there is only one, as bullets under a bare marker from the second onward, and "
|
||||
"repeating a reason verbatim is a no-op), while a further `--no-migration` **replaces** the "
|
||||
"single line that says whether content has to change. A candidate crossing the boundary "
|
||||
"twice is the normal shape of a long-running one, and each crossing is a separate thing an "
|
||||
"operator has to act on; whether content migrates stays one yes/no about the candidate as a "
|
||||
"whole. There is deliberately no retraction path for a single accumulated `--breaking` "
|
||||
"reason - `--migration-required` retracts the migration line, and nothing retracts a "
|
||||
"breaking one. A later bump of the same candidate that finds out `--no-migration` was wrong "
|
||||
"after all retracts that line with `--migration-required` instead of restating "
|
||||
"`--no-migration` - refused without a migration document already targeting the new base, "
|
||||
"and without an existing `--no-migration` line to retract. Which part a change earns stays "
|
||||
"a judgment call: the command enforces that a crossing documents itself, never that the "
|
||||
"part was chosen correctly",
|
||||
failures=(cli_contract.Failure(
|
||||
label="",
|
||||
cause="More or fewer than one of `--major/--minor/--patch`, an empty `--title`, an "
|
||||
"unknown `--impact`, a missing `VERSION`/`CHANGES.md`, `VERSION` and the changelog's "
|
||||
"newest entry naming different versions, an escalation to a boundary crossing without "
|
||||
"`--breaking` or with neither a migration document nor `--no-migration`, "
|
||||
"`--breaking`/`--no-migration` on a bump that crosses nothing, or `--migration-required` "
|
||||
"combined with `--no-migration`, on a bump with no running candidate, with no "
|
||||
"`--no-migration` line to retract, or without a migration document already targeting "
|
||||
"the new base",
|
||||
reaction="**Not idempotent**: a second run escalates or continues the candidate again. If "
|
||||
"the outcome is uncertain, read `VERSION` and the top of `CHANGES.md` before retrying",
|
||||
),),
|
||||
notes=(
|
||||
"`VERSION` gets a `-beta.N` suffix: one running candidate between two releases, never "
|
||||
"a fresh number per bump.",
|
||||
"`--major`/`--minor`/`--patch` is **max-wins escalation** against the last release "
|
||||
"(patch < minor < major): a `--patch` on a MINOR candidate only advances `N`, and "
|
||||
"escalation never steps back down.",
|
||||
"The first bump of a candidate opens its `CHANGES.md` entry - heading, date, author, "
|
||||
"and a machine-managed `<!-- wikitool:bumps -->` list of every `--title` collected so "
|
||||
"far, graded by `--impact` (default `medium`). Every later bump of the same candidate "
|
||||
"updates that entry in place: one entry per candidate, not one per bump.",
|
||||
"The bump list renders grouped under `**High/Medium/Low impact**` headings, empty "
|
||||
"groups omitted - except while every bump is `medium`, where it stays one flat list. "
|
||||
"`version regrade` corrects a grade after the fact.",
|
||||
"Writes the heading, the bump list and the breaking/migration lines; the entry's prose "
|
||||
"is left to the author.",
|
||||
"Compatibility follows the **leftmost non-zero component** of the candidate's base, "
|
||||
"which for this stack (at `1.0.0` and up) means MAJOR: PATCH is a fix, MINOR a "
|
||||
"compatible capability, MAJOR a version that is **not a drop-in replacement** - any "
|
||||
"hand-work on update, or a downgrade that no longer works. Whether content must be "
|
||||
"migrated is a second, independent question.",
|
||||
"The bump that first escalates a candidate past the boundary requires `--breaking "
|
||||
"\"<what stops working>\"` and, on top of it, a migration document targeting the "
|
||||
"candidate's base or `--no-migration \"<reason>\"`. Both are anchored just above the "
|
||||
"bump list, persist over later bumps of the same candidate without being repeated, and "
|
||||
"are refused on a bump that crosses nothing at all.",
|
||||
"On a later crossing of the same candidate, a further `--breaking` **joins** the "
|
||||
"reasons already recorded (flat on the marker line while there is one, bullets under a "
|
||||
"bare marker from the second on; repeating a reason verbatim is a no-op), while a "
|
||||
"further `--no-migration` **replaces** the single migration line.",
|
||||
"`--migration-required` retracts the running candidate's `--no-migration` line; it "
|
||||
"needs a migration document already targeting the new base. Nothing retracts a "
|
||||
"recorded `--breaking` reason.",
|
||||
"Enforces that a crossing documents itself, never that the part was chosen correctly.",
|
||||
"Not idempotent: every successful run escalates or continues the candidate again.",
|
||||
"`--dry-run` reports the step without writing.",
|
||||
),
|
||||
failures=(
|
||||
cli_contract.Failure(
|
||||
cause="Not exactly one of `--major`/`--minor`/`--patch`, an empty `--title`, or an "
|
||||
"unknown `--impact`",
|
||||
reaction="Nothing was written - fix the argument and retry once",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="`VERSION` or `CHANGES.md` is missing, or `VERSION` and the changelog's "
|
||||
"newest entry name different versions",
|
||||
reaction="Nothing was written - fix whichever is wrong, then retry",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="An escalation to a boundary crossing without `--breaking`, or with neither a "
|
||||
"migration document targeting the new base nor `--no-migration`",
|
||||
reaction="Nothing was written - add what the error asks for, or, if nothing actually "
|
||||
"breaks, choose a smaller part",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="`--breaking` or `--no-migration` on a bump that crosses nothing",
|
||||
reaction="Nothing was written - drop the flag and retry",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="`--migration-required` combined with `--no-migration`, on a bump with no "
|
||||
"running candidate, with no `--no-migration` line to retract, or without a "
|
||||
"migration document already targeting the new base",
|
||||
reaction="Nothing was written - write the migration document or fix the "
|
||||
"combination, then retry",
|
||||
),
|
||||
),
|
||||
examples=(
|
||||
'tools/wikitool version bump --patch --title "Fix the lint report path" --impact low',
|
||||
'tools/wikitool version bump --minor --title "New command: wikitool review" --dry-run',
|
||||
'tools/wikitool version bump --major --title "Rename --gate-file to --remotes-file" '
|
||||
'--breaking "--gate-file is gone; scripts must pass --remotes-file" '
|
||||
'--no-migration "no content changes"',
|
||||
),
|
||||
never=(
|
||||
"Never re-run after an uncertain outcome without first reading `VERSION` and the top "
|
||||
"of `CHANGES.md` - a second run escalates or continues the candidate again.",
|
||||
"Never hand-edit `VERSION` or the machine-written parts of the entry (heading, bump "
|
||||
"list, breaking and migration lines); `--migration-required` is the only way to take "
|
||||
"the migration line back.",
|
||||
),
|
||||
see_also=(
|
||||
"`wikitool version regrade` - corrects an `--impact` grade",
|
||||
"`wikitool version release` - fixes the candidate into a release",
|
||||
"`instructions/migrate-corpus.md` - how a migration document is written",
|
||||
),
|
||||
))
|
||||
@app.command("bump")
|
||||
def bump_command(
|
||||
@@ -653,28 +745,51 @@ def bump_command(
|
||||
atomic="No - `VERSION` then `CHANGES.md`",
|
||||
budget=cli_contract.Budget.COUNTED,
|
||||
),
|
||||
notes="Ends the pre-release phase `version bump` started. Without `--title` the heading "
|
||||
"keeps whichever bump last set it; with it, the heading's title is replaced - the normal "
|
||||
"case for a candidate that collected several bump titles, since the entry wants a "
|
||||
"summarising heading rather than the most recent one. Leaves the entry's machine-managed "
|
||||
"bump-title list untouched, as the record of what happened. Refuses when the candidate "
|
||||
"collected two or more bumps and the entry still carries no summary paragraph (at least 200 "
|
||||
"non-whitespace characters) between the bump list and the first `### <bump title>` "
|
||||
"changeset heading; a candidate with exactly one bump is exempt, since there its own "
|
||||
"changeset already is the summary. `--dry-run` runs this check too and reports the same "
|
||||
"refusal. Commits nothing and pushes nothing (invariant 5) - the following `publish` moves "
|
||||
"`VERSION` onto `main`, which `release.yml` reacts to. Refuses when `VERSION` is already a "
|
||||
"release (no running candidate to fix), or when the changelog's newest entry does not match "
|
||||
"`VERSION`",
|
||||
failures=(cli_contract.Failure(
|
||||
label="",
|
||||
cause="A missing `VERSION`/`CHANGES.md`, `VERSION` already a release (no running "
|
||||
"candidate), `VERSION` and the changelog's newest entry naming different versions, or "
|
||||
"(from two bumps on) an entry with no summary paragraph above the changesets",
|
||||
reaction="**Not idempotent**: a second run fails outright once the suffix is gone. If the "
|
||||
"outcome is uncertain, read `VERSION` before retrying - a release-shaped `VERSION` "
|
||||
"means it already ran",
|
||||
),),
|
||||
notes=(
|
||||
"Strips `VERSION`'s `-beta.N` suffix - the candidate's base becomes the release - and "
|
||||
"closes the candidate's `CHANGES.md` entry.",
|
||||
"Without `--title` the heading keeps whichever bump last set it; `--title` replaces it "
|
||||
"- the normal case for a candidate that collected several bumps, whose entry wants a "
|
||||
"summarising heading rather than the most recent one.",
|
||||
"Leaves the entry's machine-managed bump list untouched, as the record of what "
|
||||
"happened.",
|
||||
"From two bumps on, requires a summary paragraph (at least 200 non-whitespace "
|
||||
"characters) between the bump list and the first `### <bump title>` changeset "
|
||||
"heading. A candidate with exactly one bump is exempt, since there its own changeset "
|
||||
"already is the summary. `--dry-run` runs this check too.",
|
||||
"Commits nothing and pushes nothing. The following `publish` moves `VERSION` onto "
|
||||
"`main`, which `release.yml` reacts to.",
|
||||
"Not idempotent: a second run fails once the suffix is gone.",
|
||||
),
|
||||
failures=(
|
||||
cli_contract.Failure(
|
||||
cause="`VERSION` is already a release - there is no running candidate to fix",
|
||||
reaction="After an uncertain run this means it already ran; otherwise there is "
|
||||
"nothing to release",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="`VERSION` or `CHANGES.md` is missing, or `VERSION` and the changelog's newest "
|
||||
"entry name different versions",
|
||||
reaction="Fix whichever is wrong, then retry",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="Two or more bumps and no summary paragraph above the changesets",
|
||||
reaction="Write a short summary paragraph right below the bump list, then retry",
|
||||
),
|
||||
),
|
||||
examples=(
|
||||
"tools/wikitool version release --dry-run",
|
||||
'tools/wikitool version release --title "Command records rewritten for agents"',
|
||||
),
|
||||
never=(
|
||||
"Never retry after an uncertain outcome without reading `VERSION` first - a "
|
||||
"release-shaped `VERSION` means it already ran.",
|
||||
),
|
||||
see_also=(
|
||||
"`wikitool version bump` - opens and continues the candidate",
|
||||
"`wikitool version regrade` - shows the bump list the summary sits under",
|
||||
"`wikitool publish` - moves the released `VERSION` onto `main`",
|
||||
),
|
||||
))
|
||||
@app.command("release")
|
||||
def release_command(
|
||||
@@ -775,26 +890,45 @@ def release_command(
|
||||
atomic="No - `CHANGES.md` only, and only when indices are given",
|
||||
budget=cli_contract.Budget.EXEMPT_WITHOUT_ARGS,
|
||||
),
|
||||
notes="1-based rendered position (no arguments - the correction path for a `--impact` "
|
||||
"judgement made at bump time), or change one or more of them in a single call: "
|
||||
"`version regrade 3 7 --impact high` grades both against a single read of today's list, not "
|
||||
"position 3 first and then position 7 against whatever that produced. Touches only the "
|
||||
"topmost entry's bump list - never `VERSION`, never any other part of `CHANGES.md`. The "
|
||||
"bare listing is read-only and exempt from the Iteration Budget Gate, like `version notes`; "
|
||||
"a call with indices writes `CHANGES.md` and is counted like `version bump`. Refuses an "
|
||||
"index outside the rendered list's range, an unknown `--impact`, indices given without "
|
||||
"`--impact`, a missing `VERSION`/`CHANGES.md`, a `VERSION`/newest-changelog-entry mismatch, "
|
||||
"or a topmost entry with no bump list at all",
|
||||
failures=(cli_contract.Failure(
|
||||
label="",
|
||||
cause="A missing `VERSION`/`CHANGES.md`, `VERSION` and the changelog's newest entry "
|
||||
"naming different versions, a topmost entry with no bump list, an index outside the "
|
||||
"rendered list's range, indices given without `--impact`, or an unknown `--impact`",
|
||||
reaction="The bare listing never writes anything. A write is **not idempotent** against a "
|
||||
"changed list: re-running the same indices after a first success regrades whatever is "
|
||||
"at those positions *now*, which may no longer be the same bumps - list again before "
|
||||
"retrying",
|
||||
),),
|
||||
notes=(
|
||||
"Without arguments: lists the running candidate's bump titles with their grade, "
|
||||
"numbered by 1-based position in the rendered list (High before Medium before Low, "
|
||||
"chronological within a grade). Read-only and exempt from the Iteration Budget Gate.",
|
||||
"With indices and `--impact`: sets the grade of every named position in one call, all "
|
||||
"resolved against a single read of the current list - `version regrade 3 7 --impact "
|
||||
"high` grades what is at 3 and 7 now, not 7 after 3 has moved. Writes `CHANGES.md` and "
|
||||
"is counted by the Iteration Budget Gate.",
|
||||
"The correction path for an `--impact` grade judged at bump time.",
|
||||
"Touches only the topmost entry's bump list - never `VERSION`, never any other part of "
|
||||
"`CHANGES.md`.",
|
||||
"A write is not idempotent against a changed list: after a first success, the same "
|
||||
"indices may name different bumps.",
|
||||
),
|
||||
failures=(
|
||||
cli_contract.Failure(
|
||||
cause="`VERSION` or `CHANGES.md` is missing, `VERSION` and the changelog's newest "
|
||||
"entry name different versions, or the topmost entry has no bump list",
|
||||
reaction="Nothing was written - fix whichever is wrong, then retry",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="An index outside the rendered list's range, indices given without "
|
||||
"`--impact`, or an unknown `--impact`",
|
||||
reaction="Nothing was written - list again with the bare command, then retry once "
|
||||
"with corrected arguments",
|
||||
),
|
||||
),
|
||||
examples=(
|
||||
"tools/wikitool version regrade",
|
||||
"tools/wikitool version regrade 3 7 --impact high",
|
||||
),
|
||||
never=(
|
||||
"Never re-run the same indices after a write without listing again first - the "
|
||||
"positions may have moved.",
|
||||
),
|
||||
see_also=(
|
||||
"`wikitool version bump` - sets the grade in the first place",
|
||||
"`wikitool version release` - refuses without a summary above this list",
|
||||
),
|
||||
))
|
||||
@app.command("regrade")
|
||||
def regrade_command(
|
||||
|
||||
Reference in new issue
Block a user