diff --git a/CHANGES.md b/CHANGES.md index f4d7a9a..8756188 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -59,7 +59,7 @@ concern - readable here, never shipped as something to parse. --- -## 7.1.0-beta.17 - 2026-09-26 - Command records, Telemetry group: examples, the missing --fail-on-error exit line +## 7.1.0-beta.18 - 2026-09-26 - Command records, Content migrations group: one line per cause, examples, prohibitions **Author:** Torben Nehmer @@ -86,6 +86,7 @@ concern - readable here, never shipped as something to parse. - Command records, Workshop runs and session budget group: examples, prohibitions - Command records, Types, instructions and docs group: one line per cause, examples, prohibitions - Command records, Telemetry group: examples, the missing --fail-on-error exit line +- Command records, Content migrations group: one line per cause, examples, prohibitions ### CalDAV task-tracker provider (Nextcloud Tasks, iOS Reminders); review reports unknown-value findings instead of skipping them @@ -306,6 +307,15 @@ always done. The reasoning `docs toc`'s record carried about its scope already l has always exited 1 on a failed scorecard, but its record only listed the missing-trace case; it now names both. +### Command records, Content migrations group: one line per cause, examples, prohibitions + +The five `migrate` commands rewritten the same way; text only. `migrate done` and +`migrate baseline` state as NEVER what their prose implied - never force the chain's order, +never advance the version with `baseline --force` or by editing `.wikitool-kb.json` - and +`migrate verify` separates a bad `--from` revision from the findings it exits 1 on. The reasoning +behind counting marker pairs rather than comparing their names, and behind an offered migration +ignoring the chain, moved into the `verify` and `done` docstrings. + --- ## 7.0.0 - 2026-09-22 - Task-Tracker-Anbindung: Vorhaben als Seitenart, Verpflichtungsschicht, Weekly Review als Read-Time-Join diff --git a/VERSION b/VERSION index 5b4c988..b69f385 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -7.1.0-beta.17 +7.1.0-beta.18 diff --git a/tools/CONTRACT.md b/tools/CONTRACT.md index c7f1559..1752d16 100644 --- a/tools/CONTRACT.md +++ b/tools/CONTRACT.md @@ -2810,13 +2810,23 @@ List every migration document under `instructions/migrations/`. - budget: exempt - network: no +**EXAMPLES** + +- `tools/wikitool migrate list` + **EXIT STATUS** - 0 success **NOTES** -Oldest target first, with its kind and obligation. Read-only and **exempt from the Iteration Budget Gate**. Never fails. +- Lists every migration document under `instructions/migrations/`, oldest target first, with its kind and obligation. +- Never fails. Read-only and **exempt from the Iteration Budget Gate**. + +**SEE ALSO** + +- `wikitool migrate status` - which of them this instance still owes +- `instructions/migrate-corpus.md` - how a migration is run #### `migrate status` @@ -2834,18 +2844,35 @@ Show the migrations this instance still owes, in the order they must run. - budget: exempt - network: no +**EXAMPLES** + +- `tools/wikitool migrate status` +- `tools/wikitool migrate status --json` + **EXIT STATUS** - 0 success -- 1 `.wikitool-kb.json` is missing (content version undeclared), or `VERSION` is unreadable +- 1 `.wikitool-kb.json` is missing - the content version is undeclared +- 1 `VERSION` is unreadable **ON FAILURE** -- `.wikitool-kb.json` is missing (content version undeclared), or `VERSION` is unreadable -> For a missing declaration: run `migrate baseline ` once, then retry. Safe to retry freely otherwise +- `.wikitool-kb.json` is missing - the content version is undeclared -> Run `migrate baseline ` once, then retry +- `VERSION` is unreadable -> Fix `VERSION`, then retry **NOTES** -Every **required** document whose `migrates_to` lies in `(kb_version, VERSION]`. `offered` documents are listed separately above the chain and never block, never count as owed, and are bounded by the applied ledger rather than by `kb_version` - taking one deliberately does not move the version, so the version cannot say whether it was taken. When a release stamp is present, also reports which shipped files this instance has since edited (from the per-file sha256 in `.wikitool-release.json`), which is what says whether an offer may be copied over or has to be reconciled by hand; without a stamp that question is reported as unanswerable rather than answered. Exits 1 only when `.wikitool-kb.json` is missing - the content's shape is a question the tool refuses to answer by guessing. Read-only and exempt from the budget gate +- Shows every **required** migration whose `migrates_to` lies in `(kb_version, VERSION]`, in the order it must run. +- `offered` migrations are listed separately above the chain: they never block, never count as owed, and are bounded by the applied ledger rather than by `kb_version` - taking one does not move the version. +- With a release stamp present, also reports which shipped files this instance has since edited (from the per-file sha256 in `.wikitool-release.json`) - which says whether an offer may be copied over or has to be reconciled by hand. Without a stamp that question is reported as unanswerable rather than answered. +- Exits 1 only when the content version is undeclared (`.wikitool-kb.json` missing) or `VERSION` is unreadable; it never guesses the content's shape. +- Read-only, safe to retry freely, and exempt from the Iteration Budget Gate. + +**SEE ALSO** + +- `wikitool migrate done` - records one as applied +- `instructions/migrate-corpus.md` - how a migration is run +- `instructions/upgrade-instance.md` - where an upgrade checks this #### `migrate verify` @@ -2863,18 +2890,40 @@ Compare `kb/` against a git revision on the invariants a content migration must - budget: exempt - network: no +**EXAMPLES** + +- `tools/wikitool migrate verify --from HEAD` +- `tools/wikitool migrate verify --from HEAD --path kb/concepts --expect-body-change` + **EXIT STATUS** - 0 success -- 1 Only with `--fail-on-error`: an invariant changed. Also exits 1 if `--from` is not a revision in this repository +- 1 Only with `--fail-on-error`: an invariant changed +- 1 `--from` is not a revision in this repository **ON FAILURE** -- Only with `--fail-on-error`: an invariant changed. Also exits 1 if `--from` is not a revision in this repository -> Exit 1 from `--fail-on-error` means "act on the findings", not "the tool is broken". A finding is never fixed by re-running - it names a page and what changed on it +- Only with `--fail-on-error`: an invariant changed -> Act on the findings - exit 1 here means "act on the findings", not "the tool is broken". A finding names a page and what changed on it; it is never fixed by re-running +- `--from` is not a revision in this repository -> Fix the revision and retry + +**NEVER** + +- Never re-run to make a finding go away - fix the page it names. **NOTES** -Wikilink and citation **counts** (not sets), footnote definitions, H1, structural frontmatter, and the **count of generated-region marker pairs** - a page that went from one links region to two has the same set of region names and a different count, and a lost marker turns a generated region into prose the next write appends a second one beside. Pages are matched by **title**, not path, so a page `wikitool move` (or `move --reconcile`) relocated compares as itself - reported separately as `moved` - rather than as a removed-and-added pair. Reports added/removed pages without failing on them. `--expect-body-change` additionally flags a page whose body did not change at all. Not migration-specific - worth running after any bulk rewrite, and the one question `lint` cannot answer, since it reads a single revision and so cannot see that something went missing. Read-only and exempt from the budget gate +- Compares `kb/` against the revision `--from` on the invariants a content migration must not change: wikilink and citation **counts** (not sets), footnote definitions, H1, structural frontmatter, and the **count of generated-region marker pairs**. +- Pages are matched by **title**, not path, so a page `move` (or `move --reconcile`) relocated compares as itself - reported separately as `moved` - rather than as a removed-and-added pair. +- Reports added and removed pages without failing on them. +- `--expect-body-change` additionally flags a page whose body did not change at all. +- Not migration-specific: worth running after any bulk rewrite. +- Exits 0 whatever it finds unless `--fail-on-error` is passed. +- Read-only and exempt from the Iteration Budget Gate. + +**SEE ALSO** + +- `instructions/migrate-corpus.md` - where a migration runs this +- `wikitool lint` - the single-revision checks #### `migrate done` @@ -2892,18 +2941,39 @@ Record one migration as applied, advancing `kb_version` in `.wikitool-kb.json`. - budget: counted - network: no +**EXAMPLES** + +- `tools/wikitool migrate done 7.0.0 --pages 42 --dry-run` +- `tools/wikitool migrate done 7.0.0 --pages 42` + **EXIT STATUS** - 0 success -- 1 Unknown version, no `.wikitool-kb.json`, nothing outstanding, or a *required* version that is not the next link in the chain +- 1 Unknown version, no `.wikitool-kb.json`, or nothing outstanding +- 1 A *required* version that is not the next link in the chain **ON FAILURE** -- Unknown version, no `.wikitool-kb.json`, nothing outstanding, or a *required* version that is not the next link in the chain -> **Not idempotent** for a required migration: it advances the chain. For "not the next link", run `migrate status` and apply them in the order it prints - never force the order. Recording an `offered` migration *is* idempotent and safe to repeat +- Unknown version, no `.wikitool-kb.json`, or nothing outstanding -> Check `migrate status`, fix the argument, then retry once +- A *required* version that is not the next link in the chain -> Run `migrate status` and apply the migrations in the order it prints + +**NEVER** + +- Never force the order of required migrations. +- Never hand-edit `.wikitool-kb.json` to advance the version. **NOTES** -**Refuses any version that is not the next link in the chain** - skipping one leaves the corpus in a shape no version describes, and an interrupted multi-step upgrade has to be resumable rather than guessable. An `offered` migration is recorded in the applied ledger *without* moving `kb_version` and with no ordering rule applied: it is not a link in the chain, so there is nothing to skip, and requiring the chain first would make an unrelated file upgrade wait on it. Re-recording one already in the ledger is a no-op, not an error +- Records one migration as applied, advancing `kb_version` in `.wikitool-kb.json` to its target. +- **Refuses any required version that is not the next link in the chain.** +- An `offered` migration is recorded in the applied ledger *without* moving `kb_version` and with no ordering rule applied. Re-recording one already in the ledger is a no-op, not an error - idempotent and safe to repeat. +- Not idempotent for a required migration: it advances the chain. +- `--dry-run` reports without writing. + +**SEE ALSO** + +- `wikitool migrate status` - the order to apply them in +- `instructions/migrate-corpus.md` - the migration procedure #### `migrate baseline` @@ -2921,18 +2991,35 @@ Declare `kb_version` once, for an instance predating `.wikitool-kb.json`. - budget: counted - network: no +**EXAMPLES** + +- `tools/wikitool migrate baseline 6.2.0` + **EXIT STATUS** - 0 success -- 1 Unparseable version, or a declaration already exists and `--force` was not passed +- 1 Unparseable version +- 1 A declaration already exists and `--force` was not passed **ON FAILURE** -- Unparseable version, or a declaration already exists and `--force` was not passed -> Safe to re-run with the same version. If a declaration exists, it is almost always `migrate done` that was wanted +- Unparseable version -> Fix the version and retry +- A declaration already exists and `--force` was not passed -> It is almost always `migrate done` that was wanted + +**NEVER** + +- Never use `--force` to advance the version past a migration - that is `migrate done`. **NOTES** -Refuses to overwrite an existing declaration without `--force`: advancing after a migration is `done`, which checks the chain, and this command must not become the quiet way around it +- Declares `kb_version` once, for an instance predating `.wikitool-kb.json`. +- Refuses to overwrite an existing declaration without `--force`. Advancing the version after a migration is `migrate done`, which checks the chain; this command does not. +- Safe to re-run with the same version. + +**SEE ALSO** + +- `wikitool migrate done` - advances the version after a migration +- `wikitool migrate status` - what is owed from the declared version ### Private instances diff --git a/tools/chemenu/commands/migrate_cmd.py b/tools/chemenu/commands/migrate_cmd.py index d2aad22..d4fd951 100644 --- a/tools/chemenu/commands/migrate_cmd.py +++ b/tools/chemenu/commands/migrate_cmd.py @@ -59,9 +59,19 @@ def _versions() -> tuple[Version, Optional[Version]]: atomic="Read-only", budget=cli_contract.Budget.EXEMPT, ), - notes="Oldest target first, with its kind and obligation. Read-only and **exempt from the " - "Iteration Budget Gate**. Never fails.", + notes=( + "Lists every migration document under `instructions/migrations/`, oldest target first, " + "with its kind and obligation.", + "Never fails. Read-only and **exempt from the Iteration Budget Gate**.", + ), failures=(), + examples=( + "tools/wikitool migrate list", + ), + see_also=( + "`wikitool migrate status` - which of them this instance still owes", + "`instructions/migrate-corpus.md` - how a migration is run", + ), )) @app.command("list") def list_command( @@ -158,23 +168,39 @@ def _report_offers( atomic="Read-only", budget=cli_contract.Budget.EXEMPT, ), - notes="Every **required** document whose `migrates_to` lies in `(kb_version, VERSION]`. " - "`offered` documents are listed separately above the chain and never block, never count as " - "owed, and are bounded by the applied ledger rather than by `kb_version` - taking one " - "deliberately does not move the version, so the version cannot say whether it was taken. " - "When a release stamp is present, also reports which shipped files this instance has since " - "edited (from the per-file sha256 in `.wikitool-release.json`), which is what says whether " - "an offer may be copied over or has to be reconciled by hand; without a stamp that question " - "is reported as unanswerable rather than answered. Exits 1 only when `.wikitool-kb.json` is " - "missing - the content's shape is a question the tool refuses to answer by guessing. " - "Read-only and exempt from the budget gate", - failures=(cli_contract.Failure( - label="", - cause="`.wikitool-kb.json` is missing (content version undeclared), or `VERSION` is " - "unreadable", - reaction="For a missing declaration: run `migrate baseline ` once, then retry. " - "Safe to retry freely otherwise", - ),), + notes=( + "Shows every **required** migration whose `migrates_to` lies in " + "`(kb_version, VERSION]`, in the order it must run.", + "`offered` migrations are listed separately above the chain: they never block, never " + "count as owed, and are bounded by the applied ledger rather than by `kb_version` - " + "taking one does not move the version.", + "With a release stamp present, also reports which shipped files this instance has " + "since edited (from the per-file sha256 in `.wikitool-release.json`) - which says " + "whether an offer may be copied over or has to be reconciled by hand. Without a stamp " + "that question is reported as unanswerable rather than answered.", + "Exits 1 only when the content version is undeclared (`.wikitool-kb.json` missing) or " + "`VERSION` is unreadable; it never guesses the content's shape.", + "Read-only, safe to retry freely, and exempt from the Iteration Budget Gate.", + ), + failures=( + cli_contract.Failure( + cause="`.wikitool-kb.json` is missing - the content version is undeclared", + reaction="Run `migrate baseline ` once, then retry", + ), + cli_contract.Failure( + cause="`VERSION` is unreadable", + reaction="Fix `VERSION`, then retry", + ), + ), + examples=( + "tools/wikitool migrate status", + "tools/wikitool migrate status --json", + ), + see_also=( + "`wikitool migrate done` - records one as applied", + "`instructions/migrate-corpus.md` - how a migration is run", + "`instructions/upgrade-instance.md` - where an upgrade checks this", + ), )) @app.command("status") def status_command( @@ -264,21 +290,38 @@ def status_command( atomic="Yes - single file write", budget=cli_contract.Budget.COUNTED, ), - notes="**Refuses any version that is not the next link in the chain** - skipping one leaves " - "the corpus in a shape no version describes, and an interrupted multi-step upgrade has to " - "be resumable rather than guessable. An `offered` migration is recorded in the applied " - "ledger *without* moving `kb_version` and with no ordering rule applied: it is not a link " - "in the chain, so there is nothing to skip, and requiring the chain first would make an " - "unrelated file upgrade wait on it. Re-recording one already in the ledger is a no-op, not " - "an error", - failures=(cli_contract.Failure( - label="", - cause="Unknown version, no `.wikitool-kb.json`, nothing outstanding, or a *required* " - "version that is not the next link in the chain", - reaction="**Not idempotent** for a required migration: it advances the chain. For \"not " - "the next link\", run `migrate status` and apply them in the order it prints - never " - "force the order. Recording an `offered` migration *is* idempotent and safe to repeat", - ),), + notes=( + "Records one migration as applied, advancing `kb_version` in `.wikitool-kb.json` to its " + "target.", + "**Refuses any required version that is not the next link in the chain.**", + "An `offered` migration is recorded in the applied ledger *without* moving " + "`kb_version` and with no ordering rule applied. Re-recording one already in the ledger " + "is a no-op, not an error - idempotent and safe to repeat.", + "Not idempotent for a required migration: it advances the chain.", + "`--dry-run` reports without writing.", + ), + failures=( + cli_contract.Failure( + cause="Unknown version, no `.wikitool-kb.json`, or nothing outstanding", + reaction="Check `migrate status`, fix the argument, then retry once", + ), + cli_contract.Failure( + cause="A *required* version that is not the next link in the chain", + reaction="Run `migrate status` and apply the migrations in the order it prints", + ), + ), + examples=( + "tools/wikitool migrate done 7.0.0 --pages 42 --dry-run", + "tools/wikitool migrate done 7.0.0 --pages 42", + ), + never=( + "Never force the order of required migrations.", + "Never hand-edit `.wikitool-kb.json` to advance the version.", + ), + see_also=( + "`wikitool migrate status` - the order to apply them in", + "`instructions/migrate-corpus.md` - the migration procedure", + ), )) @app.command("done") def done_command( @@ -293,7 +336,9 @@ def done_command( interrupted multi-step upgrade has to be resumable rather than guessable. An `offered` migration is recorded but does not move the version, and no - ordering rule applies to it - it is not a link in the chain. The record is + ordering rule applies to it - it is not a link in the chain, so there is + nothing to skip, and requiring the chain first would make an unrelated + file upgrade wait on it. The record is the only thing that distinguishes an offer someone took from one they ignored, precisely because the version stays put.""" stack, kb_version = _versions() @@ -386,16 +431,32 @@ def done_command( atomic="Yes - single file write", budget=cli_contract.Budget.COUNTED, ), - notes="Refuses to overwrite an existing declaration without `--force`: advancing after a " - "migration is `done`, which checks the chain, and this command must not become the quiet " - "way around it", - failures=(cli_contract.Failure( - label="", - cause="Unparseable version, or a declaration already exists and `--force` was not " - "passed", - reaction="Safe to re-run with the same version. If a declaration exists, it is almost " - "always `migrate done` that was wanted", - ),), + notes=( + "Declares `kb_version` once, for an instance predating `.wikitool-kb.json`.", + "Refuses to overwrite an existing declaration without `--force`. Advancing the version " + "after a migration is `migrate done`, which checks the chain; this command does not.", + "Safe to re-run with the same version.", + ), + failures=( + cli_contract.Failure( + cause="Unparseable version", + reaction="Fix the version and retry", + ), + cli_contract.Failure( + cause="A declaration already exists and `--force` was not passed", + reaction="It is almost always `migrate done` that was wanted", + ), + ), + examples=( + "tools/wikitool migrate baseline 6.2.0", + ), + never=( + "Never use `--force` to advance the version past a migration - that is `migrate done`.", + ), + see_also=( + "`wikitool migrate done` - advances the version after a migration", + "`wikitool migrate status` - what is owed from the declared version", + ), )) @app.command("baseline") def baseline_command( @@ -537,25 +598,42 @@ def _shapes_now(wanted: set[str]) -> dict[str, corpus_diff.PageShape]: atomic="Read-only", budget=cli_contract.Budget.EXEMPT, ), - notes="Wikilink and citation **counts** (not sets), footnote definitions, H1, structural " - "frontmatter, and the **count of generated-region marker pairs** - a page that went from " - "one links region to two has the same set of region names and a different count, and a " - "lost marker turns a generated region into prose the next write appends a second one " - "beside. Pages are matched by **title**, not path, so a page `wikitool move` (or " - "`move --reconcile`) relocated compares as itself - reported separately as `moved` - rather " - "than as a removed-and-added pair. Reports added/removed pages without failing on them. " - "`--expect-body-change` additionally flags a page whose body did not change at all. Not " - "migration-specific - worth running after any bulk rewrite, and the one question `lint` " - "cannot answer, since it reads a single revision and so cannot see that something went " - "missing. Read-only and exempt from the budget gate", - failures=(cli_contract.Failure( - label="", - cause="Only with `--fail-on-error`: an invariant changed. Also exits 1 if `--from` is " - "not a revision in this repository", - reaction="Exit 1 from `--fail-on-error` means \"act on the findings\", not \"the tool is " - "broken\". A finding is never fixed by re-running - it names a page and what changed on " - "it", - ),), + notes=( + "Compares `kb/` against the revision `--from` on the invariants a content migration " + "must not change: wikilink and citation **counts** (not sets), footnote definitions, " + "H1, structural frontmatter, and the **count of generated-region marker pairs**.", + "Pages are matched by **title**, not path, so a page `move` (or `move --reconcile`) " + "relocated compares as itself - reported separately as `moved` - rather than as a " + "removed-and-added pair.", + "Reports added and removed pages without failing on them.", + "`--expect-body-change` additionally flags a page whose body did not change at all.", + "Not migration-specific: worth running after any bulk rewrite.", + "Exits 0 whatever it finds unless `--fail-on-error` is passed.", + "Read-only and exempt from the Iteration Budget Gate.", + ), + failures=( + cli_contract.Failure( + cause="Only with `--fail-on-error`: an invariant changed", + reaction="Act on the findings - exit 1 here means \"act on the findings\", not \"the " + "tool is broken\". A finding names a page and what changed on it; it is never fixed " + "by re-running", + ), + cli_contract.Failure( + cause="`--from` is not a revision in this repository", + reaction="Fix the revision and retry", + ), + ), + examples=( + "tools/wikitool migrate verify --from HEAD", + "tools/wikitool migrate verify --from HEAD --path kb/concepts --expect-body-change", + ), + never=( + "Never re-run to make a finding go away - fix the page it names.", + ), + see_also=( + "`instructions/migrate-corpus.md` - where a migration runs this", + "`wikitool lint` - the single-revision checks", + ), )) @app.command("verify") def verify_command( @@ -575,6 +653,13 @@ def verify_command( must not change: wikilink and citation *counts*, footnote definitions, H1, and structural frontmatter. + Marker pairs are compared by *count*, not by the set of region names: a + page that went from one links region to two has the same set and a + different count, and a lost marker turns a generated region into prose + the next write appends a second one beside. This is the one question + `lint` cannot answer - it reads a single revision, so it cannot see that + something went missing. + Pages are matched by title, not path, so a page that only moved directory (see `wikitool move`) compares as itself rather than as a removed-and-added pair - its path change is reported separately, as