tools: command records, Git group - NOTES as bullets, one exit line per cause, examples and prohibitions (#142)
Files changed: - CHANGES.md - VERSION - tools/CONTRACT.md - tools/README.md - tools/chemenu/cli_contract.py - tools/chemenu/commands/cite_cmd.py - tools/chemenu/commands/dist_cmd.py - tools/chemenu/commands/docs_verify.py - tools/chemenu/commands/doctor.py - tools/chemenu/commands/eval_cmd.py - tools/chemenu/commands/git_publish.py - tools/chemenu/commands/index_build.py - tools/chemenu/commands/instructions_cmd.py - tools/chemenu/commands/links_cmd.py - tools/chemenu/commands/lint.py - tools/chemenu/commands/log_append.py - tools/chemenu/commands/migrate_cmd.py - tools/chemenu/commands/new_page.py - tools/chemenu/commands/page_ops.py - tools/chemenu/commands/provenance_cmd.py - tools/chemenu/commands/raw_cmd.py - tools/chemenu/commands/review_cmd.py - tools/chemenu/commands/run_budget.py - tools/chemenu/commands/search.py - tools/chemenu/commands/task_cmd.py - tools/chemenu/commands/touch.py - tools/chemenu/commands/types_cmd.py - tools/chemenu/commands/upload_cmd.py - tools/chemenu/commands/upstream_cmd.py - tools/chemenu/commands/version_cmd.py - tools/chemenu/commands/work_cmd.py - tools/chemenu/commands/xref.py - tools/chemenu/tests/test_cli_contract.py - tools/chemenu/tests/test_docs_verify.py
This commit is contained in:
1 parent
2a60f3a265
commit
fd0f60b2e7
34 files changed
+601
-285
No files matched your search
+23
-1
@@ -59,7 +59,7 @@ concern - readable here, never shipped as something to parse.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7.1.0-beta.6 - 2026-09-26 - dist export no longer cuts the dist export record out of the shipped tools/CONTRACT.md
|
## 7.1.0-beta.7 - 2026-09-26 - Command records, Git group: NOTES as bullets, one exit line per cause, examples and prohibitions
|
||||||
|
|
||||||
**Author:** Torben Nehmer
|
**Author:** Torben Nehmer
|
||||||
|
|
||||||
@@ -75,6 +75,7 @@ concern - readable here, never shipped as something to parse.
|
|||||||
- version bump no longer points at version release in its output
|
- version bump no longer points at version release in its output
|
||||||
- stack-close: wait for CI through the authenticated Gitea connection, with timings and a give-up point
|
- stack-close: wait for CI through the authenticated Gitea connection, with timings and a give-up point
|
||||||
- wikitool: usage lines name wikitool, and the -h acceptance checks become tests
|
- 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
|
||||||
<!-- /wikitool:bumps -->
|
<!-- /wikitool:bumps -->
|
||||||
|
|
||||||
### CalDAV task-tracker provider (Nextcloud Tasks, iOS Reminders); review reports unknown-value findings instead of skipping them
|
### CalDAV task-tracker provider (Nextcloud Tasks, iOS Reminders); review reports unknown-value findings instead of skipping them
|
||||||
@@ -174,6 +175,27 @@ shipped copy against its source until the region became generated. The record no
|
|||||||
markers without spelling out the pair, and a new test pins that the export plan ships
|
markers without spelling out the pair, and a new test pins that the export plan ships
|
||||||
`tools/CONTRACT.md` byte-identical to the working tree.
|
`tools/CONTRACT.md` byte-identical to the working tree.
|
||||||
|
|
||||||
|
### Command records, Git group: NOTES as bullets, one exit line per cause, examples and prohibitions
|
||||||
|
|
||||||
|
First pass of the editorial rewrite of the command records, starting with `sync` and `publish`.
|
||||||
|
Text only - no command behaves differently. The record model grew what the rewrite needs:
|
||||||
|
`notes` takes a tuple of present-tense bullets (a plain string, the old form, still renders as
|
||||||
|
one paragraph until every group is done), and `Failure` is now one cause with its own reaction
|
||||||
|
and exit code (`cause`, `reaction`, `code` 0/1/42) instead of one lumped "exit 1 means"/"retry
|
||||||
|
policy" pair per command. EXIT STATUS lists one line per cause; ON FAILURE repeats the cause
|
||||||
|
next to its reaction so each line reads on its own; an explicit exit-42 cause replaces the
|
||||||
|
generic gate line, and a record declaring one without a gate is refused at import. The field
|
||||||
|
rename is mechanical across all records, so every not-yet-rewritten command's ON FAILURE line now
|
||||||
|
reads `<old exit-1 text> -> <old retry text>`.
|
||||||
|
|
||||||
|
`sync` and `publish` now carry copyable EXAMPLES (including the re-run after exit 42), a NEVER
|
||||||
|
section (do not retry or force a failed git step, do not pass an unapproved token, do not edit
|
||||||
|
`.wikitool-remotes.json` past a refusal), SEE ALSO, and self-contained NOTES in place of "exactly
|
||||||
|
like `sync`". Sentences that only explained *why* left the records; each was already a comment
|
||||||
|
at the code that implements it. `publish`'s `atomic` property says "every gate" instead of
|
||||||
|
"both gates" - it has three, and all run before staging. How a record's prose is written is now
|
||||||
|
stated once, in the `CommandRecord` docstring, and `tools/README.md` points there.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7.0.0 - 2026-09-22 - Task-Tracker-Anbindung: Vorhaben als Seitenart, Verpflichtungsschicht, Weekly Review als Read-Time-Join
|
## 7.0.0 - 2026-09-22 - Task-Tracker-Anbindung: Vorhaben als Seitenart, Verpflichtungsschicht, Weekly Review als Read-Time-Join
|
||||||
|
|||||||
+122
-60
@@ -174,8 +174,8 @@ Scaffold a new wiki page of any type.
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- new <type>: Not transient; fix the argument and retry once. Never hand-craft the page instead
|
- new <type>: Duplicate page title, unknown type, invalid `--set` value, or a `raw_files` path that doesn't exist -> Not transient; fix the argument and retry once. Never hand-craft the page instead
|
||||||
- new project: A collision, a bad `--set`, or a read-only access path is not transient, same as `new <type>` - the last of those points at the `access: "api"` instance instead and refuses on every `--resume` retry too, since nothing about the config changes by asking again. **Exit 42** (`NEEDS USER CLEARANCE`, not exit 1) is its own separate outcome from the ordinary exit-1 cases above, and is `superproductivity`-only: that provider *can* write but cannot create the project itself and a human must, per the printed instructions; re-run with `--resume` once that is done - it re-verifies via the read path rather than trusting the claim, and exits 42 again unchanged if the tracker still does not have it. `caldav` never produces this outcome - `MKCALENDAR` is a real collection-creation verb, so a valid, non-colliding name always creates the list itself
|
- new project: Everything `new <type>` covers, **plus**: the name is already taken in the tracker (case-insensitively - for `caldav` this is checked against every list in the account, not only the ones counted as projects), `--resume` was passed for a type other than `project`, or the configured provider's access path has no write path at all (Super Productivity's `access: "snapshot"`) -> A collision, a bad `--set`, or a read-only access path is not transient, same as `new <type>` - the last of those points at the `access: "api"` instance instead and refuses on every `--resume` retry too, since nothing about the config changes by asking again. **Exit 42** (`NEEDS USER CLEARANCE`, not exit 1) is its own separate outcome from the ordinary exit-1 cases above, and is `superproductivity`-only: that provider *can* write but cannot create the project itself and a human must, per the printed instructions; re-run with `--resume` once that is done - it re-verifies via the read path rather than trusting the claim, and exits 42 again unchanged if the tracker still does not have it. `caldav` never produces this outcome - `MKCALENDAR` is a real collection-creation verb, so a valid, non-colliding name always creates the list itself
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
@@ -204,7 +204,7 @@ Create one open item in the configured task tracker - never a kb/ page.
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- Not transient; fix the argument, create the missing tracker project or tag first, or point at an `access: "api"` instance, then retry once. **Never exit 42** - unlike `new project`, every provider offering a write path at all has a real item-creation call, so there is no human-clearance step to wait on here
|
- No `.wikitool-tasks.json`, neither or both of `--project`/`--inbox` given, a `--follow-up-at` without `--waiting` or not `YYYY-MM-DD`, a `--project` name matching no tracker project, `--waiting` against a provider with no way to represent it right now (Super Productivity: the `waiting` tag does not exist), or a read-only access path (Super Productivity's `access: "snapshot"`) -> Not transient; fix the argument, create the missing tracker project or tag first, or point at an `access: "api"` instance, then retry once. **Never exit 42** - unlike `new project`, every provider offering a write path at all has a real item-creation call, so there is no human-clearance step to wait on here
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
@@ -233,7 +233,7 @@ List a project's open items - id, title, and whether each carries the WAITING st
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- Not transient; configure a tracker first, then retry once. A `--project` matching no tracker project is not an error here - see its Commands row
|
- No `.wikitool-tasks.json` -> Not transient; configure a tracker first, then retry once. A `--project` matching no tracker project is not an error here - see its Commands row
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
@@ -262,7 +262,7 @@ Mark one tracker item done - never delete it.
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- Not transient; fix the id (re-run `task list` or `review` to get a current one) or point at an `access: "api"` instance, then retry once. **Never exit 42**, same reasoning as `task new`
|
- No `.wikitool-tasks.json`, an `--id` matching no tracker item right now, or a read-only access path (Super Productivity's `access: "snapshot"`) -> Not transient; fix the id (re-run `task list` or `review` to get a current one) or point at an `access: "api"` instance, then retry once. **Never exit 42**, same reasoning as `task new`
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
@@ -291,7 +291,7 @@ Bump a page's `modified:` date and optionally rewrite its other frontmatter fiel
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- Fix the argument and retry once. Safe to re-run as-is: `--set` and `--add` are idempotent, and `--remove` of an already-absent element succeeds while reporting it
|
- Page not found; an invalid value for a field it writes; a field owned by another command (`type:`, a page-ref array) or absent from the type's schema; `--add`/`--remove` on a non-array field; a `raw_files:` path that doesn't exist -> Fix the argument and retry once. Safe to re-run as-is: `--set` and `--add` are idempotent, and `--remove` of an already-absent element succeeds while reporting it
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
@@ -320,7 +320,7 @@ Rename a page, or repoint references that name a page that never existed.
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- Safe to retry once as-is; each page's rewrite is idempotent. Use `--dry-run` first to see the blast radius. Never fix up references by hand instead
|
- Neither `--from` nor `--to` is a page, target title already taken, or `--from` equals `--to` -> Safe to retry once as-is; each page's rewrite is idempotent. Use `--dry-run` first to see the blast radius. Never fix up references by hand instead
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
@@ -349,7 +349,7 @@ Delete a page and mechanically de-link it from the rest of the wiki.
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- For "still referenced": show the user the inbound list, get approval, then re-run with `--yes`. Prose references it reports afterwards are an editorial fix, not a retry
|
- Page not found, **or** other pages still reference it and `--yes` was not passed -> For "still referenced": show the user the inbound list, get approval, then re-run with `--yes`. Prose references it reports afterwards are an editorial fix, not a retry
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
@@ -379,7 +379,7 @@ Move a page (or every misplaced page) to the directory its type-spec computes.
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- Safe to retry once as-is; a page already at its computed location is reported and left alone, and `--reconcile` only re-moves what is still misplaced. Use `--dry-run` first to see the blast radius. Never choose a directory by hand instead
|
- Neither or both of `--page`/`--reconcile` given, the named page not found, it has no `type:` to compute a placement from, or the destination already exists -> Safe to retry once as-is; a page already at its computed location is reported and left alone, and `--reconcile` only re-moves what is still misplaced. Use `--dry-run` first to see the blast radius. Never choose a directory by hand instead
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
@@ -410,7 +410,7 @@ Declare that A <rel> B.
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- Safe to retry once as-is; re-running never duplicates a link. Never create the missing page just to force the link through, and never hand-write a reference field the type does not declare
|
- Page A or B not found, or a page's type declares no `related:` field -> Safe to retry once as-is; re-running never duplicates a link. Never create the missing page just to force the link through, and never hand-write a reference field the type does not declare
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
@@ -439,7 +439,7 @@ Remove a cross-reference: the inverse of `xref add`.
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- Safe to retry freely; removing an absent link is a no-op
|
- Page A not found (B is allowed not to exist) -> Safe to retry freely; removing an absent link is a no-op
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
@@ -468,7 +468,7 @@ Batch-link a source page to every entity/concept it mentions.
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- Use `--dry-run` first; safe to retry. `sources trace --page "<Title>"` shows who was already linked
|
- Source page not found, an entity in `--entities` doesn't exist, or the source page itself could not be written after its targets were -> Use `--dry-run` first; safe to retry. `sources trace --page "<Title>"` shows who was already linked
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
@@ -497,7 +497,7 @@ Show the edges out of and into a page.
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- Check the exact title with `search`; a wikilink target is not always the page's stem
|
- Page not found -> Check the exact title with `search`; a wikilink target is not always the page's stem
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
@@ -550,7 +550,7 @@ Upsert a `[^cite-id]: [[Source - X]]` definition in a page's footnotes region.
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- Safe to retry; upserting the same (page, source, file) pair twice reuses the existing id and changes nothing the second time
|
- Page or source not found -> Safe to retry; upserting the same (page, source, file) pair twice reuses the existing id and changes nothing the second time
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
@@ -579,7 +579,7 @@ Reconcile each page's footnotes region against its actual `[^id]` references.
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- Safe to retry freely. An undefined-reference report is not a failure - fix the reference (or run `cite add`) and re-run
|
- Neither or both of `--page`/`--all` given, or page not found -> Safe to retry freely. An undefined-reference report is not a failure - fix the reference (or run `cite add`) and re-run
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
@@ -610,7 +610,7 @@ Regenerate the catalog from every page's frontmatter.
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- Safe to retry freely - the plan is always recomputed from the pages currently on disk, so a re-run converges
|
- Rare I/O error only -> Safe to retry freely - the plan is always recomputed from the pages currently on disk, so a re-run converges
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
@@ -639,7 +639,7 @@ Append a formatted entry to `kb/log.md`.
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- **Not idempotent.** If the previous run's outcome is uncertain, check the tail of `kb/log.md` before retrying
|
- Invalid `--op` or unreadable `--body-file` -> **Not idempotent.** If the previous run's outcome is uncertain, check the tail of `kb/log.md` before retrying
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
@@ -694,7 +694,7 @@ Run structural lint checks against kb/.
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- Safe to retry freely, but re-run it to re-*measure*, never to re-read: the printed path holds the full report. Exit 1 means "act on the findings", not "the tool is broken"
|
- Only with `--fail-on-error`: hard findings exist -> Safe to retry freely, but re-run it to re-*measure*, never to re-read: the printed path holds the full report. Exit 1 means "act on the findings", not "the tool is broken"
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
@@ -723,7 +723,7 @@ Find pages in `kb/` by text and/or frontmatter.
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- Fix the argument and retry. A timeout is a pathological pattern or an unresponsive corpus directory, not a slow answer - narrow the query or drop `--regex` rather than retrying it unchanged. An unknown field name is reported with the list of fields that do exist - it is never answered with an empty result, because that would read as "no such pages"
|
- `rg` is not installed or did not finish within 30 s, a malformed `--field` predicate, an unknown field name, or an unknown `--backend` -> Fix the argument and retry. A timeout is a pathological pattern or an unresponsive corpus directory, not a slow answer - narrow the query or drop `--regex` rather than retrying it unchanged. An unknown field name is reported with the list of fields that do exist - it is never answered with an empty result, because that would read as "no such pages"
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
@@ -752,7 +752,7 @@ The GTD weekly review.
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- The two exit-1 causes above need different responses: a config problem needs editing `.wikitool-tasks.json`; an unreachable provider (e.g. the tracker app not running) needs starting it, then a plain retry - the command re-reads everything fresh each time, so nothing here is ever stale to re-fetch
|
- Either no `.wikitool-tasks.json` (or a malformed one) - not yours to fix by retrying unchanged, configure or repair it first - **or** the provider was reachable at config-parse time but a read call failed mid-run, in which case the full report (findings plus which checks ran) is printed first and exit 1 follows, never a silent partial success -> The two exit-1 causes above need different responses: a config problem needs editing `.wikitool-tasks.json`; an unreachable provider (e.g. the tracker app not running) needs starting it, then a plain retry - the command re-reads everything fresh each time, so nothing here is ever stale to re-fetch
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
@@ -807,7 +807,7 @@ Trace provenance in either direction: raw file, or page.
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- Fix the argument and retry
|
- Neither or both of `--raw`/`--page` given, `--raw` names a file no source page covers (reported as a plain finding plus exit 1, not the usual `ERROR`-prefixed rejection), or `--page` names an unknown page -> Fix the argument and retry
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
@@ -836,7 +836,7 @@ Regenerate the `kb/provenance.md` reverse index.
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- Safe to retry freely
|
- Rare I/O error only -> Safe to retry freely
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
@@ -869,8 +869,8 @@ Promote one or more files from `incoming/` into `raw/`.
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- raw accept: Fix the named argument and retry once. Safe to retry as-is once the cause is fixed: a file already at its computed destination is what "already exists" reports, not a partial prior run to resume. A stem-occupied refusal is not fixed by retrying at all - it names `--replaces` and renaming in `incoming/` as the two routes and neither is the tool's to pick. Never choose the destination by hand instead - that is the decision this command exists to take away
|
- raw accept: A file does not exist, is not under `incoming/`, or is nested more than one level below it, two files in one call share a filename, a target path already exists, `--fidelity`/`--authority` is missing (unless `--replaces`) or names `unknown` or a value outside the schema's enum, the target name is already occupied anywhere under `raw/` by something the call does not own, `--page` names an unknown page or one with no `raw_files:` yet, an existing `raw_files:` entry is missing on disk, a file to be moved has more than one owning page, or `--page` would overwrite an already-set `fidelity`/`authority` with a different value -> Fix the named argument and retry once. Safe to retry as-is once the cause is fixed: a file already at its computed destination is what "already exists" reports, not a partial prior run to resume. A stem-occupied refusal is not fixed by retrying at all - it names `--replaces` and renaming in `incoming/` as the two routes and neither is the tool's to pick. Never choose the destination by hand instead - that is the decision this command exists to take away
|
||||||
- raw accept --replaces: Fix the named argument and retry once. Every check runs before the filesystem is touched, so a refusal leaves both files exactly as they were
|
- raw accept --replaces: More than one incoming file, `--page` also given, the incoming file does not exist or is not under `incoming/` (or is nested more than one level below it), its filename differs from the target's, the target does not lie under `raw/` or does not exist, `--fidelity`/`--authority` names `unknown` or a value outside the schema's enum, or the target has more than one owning source page -> Fix the named argument and retry once. Every check runs before the filesystem is touched, so a refusal leaves both files exactly as they were
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
@@ -923,7 +923,7 @@ Print one submission's manifest in full.
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- Fix the id (see `upload list`) and retry
|
- Unknown or malformed submission id -> Fix the id (see `upload list`) and retry
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
@@ -954,7 +954,7 @@ Filename, size, sha256, submitter, submitter source (the header name, not a clai
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- For exit 42: show the user the full manifest and the exact `--confirm <token>` re-run line printed, and stop - the same rule as every other exit-42 gate (AGENTS.md invariant 6). For the three exit-1 cases: fix the named argument and retry once; an occupied `incoming/<filename>` is not fixed by retrying unchanged - rename or clear it first
|
- Unknown or malformed submission id, the submission's file is missing from `mcp-upload/<id>/`, or `incoming/<filename>` already exists. **Exit 42, not 1**, when `--confirm` is absent or does not match the manifest's current token - the Upload Review Gate, not a validation error -> For exit 42: show the user the full manifest and the exact `--confirm <token>` re-run line printed, and stop - the same rule as every other exit-42 gate (AGENTS.md invariant 6). For the three exit-1 cases: fix the named argument and retry once; an occupied `incoming/<filename>` is not fixed by retrying unchanged - rename or clear it first
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
@@ -983,7 +983,7 @@ Delete a submission's material, keeping only its ledger trail.
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- Fix the argument and retry once. Not idempotent against a second call with the same id: the first call already deleted the submission, so a retry reports "unknown id" - that is confirmation, not a failure
|
- Unknown or malformed submission id, or an empty `--reason` -> Fix the argument and retry once. Not idempotent against a second call with the same id: the first call already deleted the submission, so a retry reports "unknown id" - that is confirmation, not a failure
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
@@ -1008,19 +1008,41 @@ Fetch `<remote>/<branch>` and bring the local branch up to date with it.
|
|||||||
- network: no
|
- network: no
|
||||||
- gates: rebase-review
|
- gates: rebase-review
|
||||||
|
|
||||||
|
**EXAMPLES**
|
||||||
|
|
||||||
|
- `tools/wikitool sync`
|
||||||
|
- `tools/wikitool sync --confirm-rebase <token> # re-run after exit 42, once the user approved`
|
||||||
|
|
||||||
**EXIT STATUS**
|
**EXIT STATUS**
|
||||||
|
|
||||||
- 0 success
|
- 0 success
|
||||||
- 1 The automatic rebase hit a real conflict (git failed)
|
- 0 No remote configured, or the remote cannot be reached - reported and skipped, not a failure
|
||||||
- 42 needs clearance - rebase-review (see AGENTS.md § Gates)
|
- 1 The automatic rebase hit a real conflict (git failed); it is aborted cleanly
|
||||||
|
- 42 Rebase-review gate: `<remote>/<branch>` moved and both sides changed the same file; the output lists the upstream commits, the overlapping files and their diff
|
||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- For a conflict: **do not retry, do not force** - resolve manually and re-run. **Exit 42, not 1**, when the rebase-review gate needs clearance: show the user the command's full output verbatim (upstream commits, the overlapping files, their diff) and stop; re-running with `--confirm-rebase <token>` clears it, and a wrong, invented, or superseded token exits 42 again with the current state. No remote configured, or one that cannot be reached, is not a failure - reported and skipped
|
- The automatic rebase hit a real conflict (git failed); it is aborted cleanly -> Do not retry and do not force - resolve the conflict manually, then re-run
|
||||||
|
- Rebase-review gate: `<remote>/<branch>` moved and both sides changed the same file; the output lists the upstream commits, the overlapping files and their diff -> Show the user the command's full output verbatim and stop. Once they have approved it, run the re-run line the output prints, which carries `--confirm-rebase <token>`. Without that token, or with a wrong, invented or superseded one, it exits 42 again with the current state
|
||||||
|
|
||||||
|
**NEVER**
|
||||||
|
|
||||||
|
- Never retry a conflict unchanged, and never force past it.
|
||||||
|
- Never pass a `--confirm-rebase` token the user has not seen and approved.
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
Fetch `<remote>/<branch>` and bring the local branch up to date with it: fast-forward when the remote is simply ahead, rebase local commit(s) on top when both sides moved but touch disjoint files (a content conflict is then impossible by construction), and exit **42** for review when they touch the same file (the **rebase-review gate** - see `publish` below). Never commits, never pushes, never force-anything - no remote configured, or one that cannot be reached, is reported and skipped, not a failure. Meant to run once at the start of a writing session (`instructions/session-setup.md`) so the rest of it works against a current tree instead of discovering the drift at the final `publish`
|
- Fetches `<remote>/<branch>`, then: fast-forwards when only the remote moved; rebases the local commits on top when both sides moved but touched disjoint files; exits 42 (rebase-review gate) when both sides touched the same file.
|
||||||
|
- A refused call performs no rebase attempt and leaves the branch where it was.
|
||||||
|
- The `--confirm-rebase` token covers the exact upstream state and the set of files touched on both sides; either one moving makes it stale.
|
||||||
|
- Makes no commit, no push, and no forced operation of any kind.
|
||||||
|
- Run it once at the start of a writing session.
|
||||||
|
|
||||||
|
**SEE ALSO**
|
||||||
|
|
||||||
|
- `wikitool publish` - runs the same reconcile before it commits and pushes
|
||||||
|
- `instructions/session-setup.md` - where a session runs `sync`
|
||||||
|
- `instructions/gates.md` - the gate procedure
|
||||||
|
|
||||||
#### `publish`
|
#### `publish`
|
||||||
|
|
||||||
@@ -1034,24 +1056,64 @@ Reconcile with `<remote>/<branch>`, then stage all changes, commit, and push.
|
|||||||
|
|
||||||
- effect: write
|
- effect: write
|
||||||
- idempotent: no
|
- idempotent: no
|
||||||
- atomic: No - sequential git operations, but both gates run before staging
|
- atomic: No - sequential git operations, but every gate runs before staging
|
||||||
- budget: counted
|
- budget: counted
|
||||||
- network: no
|
- network: no
|
||||||
- gates: mass-update, publish-remote, rebase-review
|
- gates: mass-update, publish-remote, rebase-review
|
||||||
|
|
||||||
|
**EXAMPLES**
|
||||||
|
|
||||||
|
- `tools/wikitool publish --message "ingest: docker-cheatsheet"`
|
||||||
|
- `tools/wikitool publish --confirm <token> --message "ingest: docker-cheatsheet" # re-run after a Mass-Update exit 42, once the user approved`
|
||||||
|
- `tools/wikitool publish --confirm-rebase <token> --message "ingest: docker-cheatsheet" # re-run after a rebase-review exit 42, once the user approved`
|
||||||
|
|
||||||
**EXIT STATUS**
|
**EXIT STATUS**
|
||||||
|
|
||||||
- 0 success
|
- 0 success
|
||||||
- 1 git failed, the push target is not the checked-out branch (including a real detached HEAD - but *not* the unborn branch of a fresh `git init`, which is a normal first publish), **or** `--yes`/`-y` was passed. **Exit 42, not 1**, when the Mass-Update Gate, the rebase-review gate (raised by the same reconcile `sync` performs), or the Publish-Remote Gate refuses
|
- 1 git failed - `git add`, `git commit`, `git push`, or the reconcile's automatic rebase
|
||||||
- 42 needs clearance - mass-update, publish-remote, rebase-review (see AGENTS.md § Gates)
|
- 1 The push target (`--branch`) is not the checked-out branch, or HEAD is detached; the unborn branch of a fresh `git init` is not this case
|
||||||
|
- 1 `--yes`/`-y` was passed - the flag does not exist and fails with an explicit error
|
||||||
|
- 1 `.wikitool-remotes.json` is unreadable or has no usable `allowed_push_urls` list
|
||||||
|
- 42 Mass-Update Gate: `--threshold` (default 10) or more counted files would be committed, or the `--confirm` token does not match this changeset
|
||||||
|
- 42 Rebase-review gate: `<remote>/<branch>` moved and both sides changed the same file; the output lists the upstream commits, the overlapping files and their diff
|
||||||
|
- 42 Publish-Remote Gate: `.wikitool-remotes.json` exists and the push URL of `--remote` is not listed in it, or `--remote` resolves to no push URL
|
||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- For git failures: **do not retry, do not force** - report and ask the user (the reconcile step already retried the push once on its own, if a rebase resolved the rejection). For exit 42: show the user the command's full output verbatim and stop; it names the evidence and the `--confirm <token>` or `--confirm-rebase <token>` line to re-run, and re-running without it exits 42 again. The Publish-Remote Gate is the exception with no such line: it names the push URL that would have been written to and the ones this checkout allows, and only the user resolves it
|
- git failed - `git add`, `git commit`, `git push`, or the reconcile's automatic rebase -> Do not retry and do not force - report and ask the user. `publish` has already made its one retry of a rejected push itself, where a reconcile resolved the rejection
|
||||||
|
- The push target (`--branch`) is not the checked-out branch, or HEAD is detached; the unborn branch of a fresh `git init` is not this case -> Check out the branch you mean to publish, or pass `--branch <checked-out branch>`, then retry once
|
||||||
|
- `--yes`/`-y` was passed - the flag does not exist and fails with an explicit error -> Drop it. The Mass-Update Gate is cleared only with `--confirm <token>` from the gate's own refusal output
|
||||||
|
- `.wikitool-remotes.json` is unreadable or has no usable `allowed_push_urls` list -> Show the error to the user and stop - a malformed file is not permission, and fixing or deleting it is theirs to do
|
||||||
|
- Mass-Update Gate: `--threshold` (default 10) or more counted files would be committed, or the `--confirm` token does not match this changeset -> Show the user the command's full output verbatim and stop. Once they have approved it, run the re-run line the output prints, which carries `--confirm <token>`. Without that token, or with a wrong, invented or superseded one, it exits 42 again with the current state
|
||||||
|
- Rebase-review gate: `<remote>/<branch>` moved and both sides changed the same file; the output lists the upstream commits, the overlapping files and their diff -> Show the user the command's full output verbatim and stop. Once they have approved it, run the re-run line the output prints, which carries `--confirm-rebase <token>`. Without that token, or with a wrong, invented or superseded one, it exits 42 again with the current state
|
||||||
|
- Publish-Remote Gate: `.wikitool-remotes.json` exists and the push URL of `--remote` is not listed in it, or `--remote` resolves to no push URL -> Show the user the push URL it names and the allowed ones, and stop. This gate has no token and no flag: only the user resolves it, by adding the URL to that file
|
||||||
|
|
||||||
|
**NEVER**
|
||||||
|
|
||||||
|
- Never retry a failed git step unchanged, and never force (`--force`, `--force-with-lease`).
|
||||||
|
- Never pass a `--confirm` or `--confirm-rebase` token the user has not seen and approved.
|
||||||
|
- Never edit `.wikitool-remotes.json` to get past a Publish-Remote refusal - that is opening a gate on your own initiative.
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
Reconcile with `<remote>/<branch>` exactly like `sync` (skipped for `--no-push`), then stage all changes, commit, and push. Refuses before staging anything when the push target is not the checked-out branch, so a `git push <branch>` cannot quietly publish a ref other than the commit just made; the *unborn* branch of a fresh `git init -b main` counts as checked out, which is what lets the first publish of a new instance work (`instructions/setup-instance.md` step 14), while a genuine detached HEAD is still refused. If the reconcile step found a still-unpushed local commit and there is nothing new to stage, that commit is pushed anyway - a previous `publish` whose push failed no longer strands it, and neither does a branch the remote has never seen (a newly created, empty remote repository). A remote that cannot be reached at all is deliberately not read that way: it keeps reporting "Nothing to commit" on a clean tree rather than attempting a push, so an offline or local-only instance is unaffected. If the push is rejected despite the pre-check (a genuine race - something landed on the remote in between), one more reconcile-and-retry is attempted before giving up; never more than one. **Mass-Update Gate:** when >= `--threshold` (default 10) *counted* files would be committed, exits **42 (`EXIT_NEEDS_CLEARANCE`)** instead of publishing - a third outcome distinct from success (0) and a validation error (1) - and prints a review report: a scale line (file count, total lines added/removed, status breakdown), only-what-applies attention notes (deletions by name, control-plane and harness-config touches, published pages, the largest single change, binaries), and every counted path grouped by area with its status and churn, generated files split out as needing no review. The token digests each counted path **and its contents** plus the publish target, so a clearance carries neither to a different file list nor to edited contents; a wrong, invented or superseded token exits 42 again with the current state. Two kinds of path are committed but never counted and never shown for approval: anything under `work/`, and the files `wikitool` generates itself (`kb/index.md`, `kb/log.md`, `kb/provenance.md`, every `INDEX.md`) - each is recomputable from the tree, so approving it decides nothing, and a routine ingest rebuilds five or six of them. The refusal line accounts for both, by reason. The gate is evaluated *before* anything is staged, so a refused publish leaves the working tree untouched. **Publish-Remote Gate:** when this checkout carries a `.wikitool-remotes.json` and the resolved push URL of `--remote` is not listed in it, exits **42** before the reconcile step even fetches - the URL is read from `git remote get-url --push`, so a repointed remote does not pass on its name. Unlike the other two gates it has **no token and no flag**: the way past it is the user adding the URL to that file, and an agent editing it to get past a refusal is opening a gate on its own initiative. Absent file means unrestricted; a malformed one is an error, not permission. See `instructions/gates.md`. `--yes`/`-y` are gone and now fail with an explicit error. `--path` (repeatable) scopes the whole operation - gate count, staging, and commit - to a subtree. **Stack-machinery note:** after a successful commit/push whose changed files include `tools/`, `types/`, `instructions/`, `AGENTS.md`, or a path ending `CONTRACT.md` - roughly the scope a stack version bump covers, deliberately a shade broader than CI's version gate, which matches only a `CONTRACT.md` one segment deep - prints one reminder line that the phase past this point (an issue-body rewrite, `docs/` staleness, a changelog entry's accuracy) is not covered by `docs verify`, `instructions verify` or `pytest`. Not a gate: no exit code change, nothing to clear, silent for an ordinary content publish
|
- Order: branch check and Publish-Remote Gate, then the reconcile with `<remote>/<branch>`, then the Mass-Update Gate, then `git add -A`, commit and push. `--no-push` skips all but the Mass-Update Gate and the commit.
|
||||||
|
- Reconcile: fetches `<remote>/<branch>`, fast-forwards when only the remote moved, rebases the local commits on top when both sides moved but touched disjoint files, and exits 42 (rebase-review gate) when both sides touched the same file. A refused reconcile performs no rebase attempt. The `--confirm-rebase` token covers the exact upstream state and the set of files touched on both sides.
|
||||||
|
- The push target must be the checked-out branch; this is checked before anything is staged. The unborn branch of a fresh `git init -b main` counts as checked out, so the first publish of a new instance works; a real detached HEAD is refused.
|
||||||
|
- With nothing new to stage, a local commit the remote lacks is still pushed: one left behind by an earlier publish whose push failed, or every commit when the remote answers but does not have the branch yet (a new, empty remote repository).
|
||||||
|
- A remote that cannot be reached is not read as lacking the branch: on a clean tree `publish` reports "Nothing to commit" and attempts no push.
|
||||||
|
- A rejected push gets exactly one more reconcile-and-push; never more than one.
|
||||||
|
- Mass-Update Gate: counts the files that would be committed, refuses with exit 42 at `--threshold` (default 10) or more, and prints a review report - a scale line (file count, total lines added/removed, status breakdown), attention notes where they apply (deletions by name, control-plane and harness-config touches, published pages, the largest single change, binaries), and every counted path grouped by area with its status and churn. The gate is evaluated before anything is staged, so a refused publish leaves the working tree untouched.
|
||||||
|
- Never counted and never shown for approval, but committed like everything else: anything under `work/`, and the files `wikitool` generates itself (`kb/index.md`, `kb/log.md`, `kb/provenance.md`, every `INDEX.md`). The refusal line accounts for both, by reason.
|
||||||
|
- The `--confirm` token covers each counted path, its contents and the publish target: a different file list or edited contents need a new clearance.
|
||||||
|
- Publish-Remote Gate: when the checkout carries `.wikitool-remotes.json` and the push URL of `--remote` is not listed in it, exits 42 before the reconcile fetches anything. The URL is read with `git remote get-url --push`, so a repointed remote does not pass on its name. An absent file means unrestricted; a malformed one is an error, not permission.
|
||||||
|
- `--path` (repeatable) scopes the whole operation - gate count, staging and commit - to that subtree.
|
||||||
|
- After a successful commit or push whose changed files include `tools/`, `types/`, `instructions/`, `AGENTS.md` or a path ending in `CONTRACT.md`, prints one reminder line: the phase past this point (an issue-body rewrite, `docs/` staleness, a changelog entry's accuracy) is not covered by `docs verify`, `instructions verify` or `pytest`. It is not a gate: no exit code change, nothing to clear, and silent for an ordinary content publish.
|
||||||
|
|
||||||
|
**SEE ALSO**
|
||||||
|
|
||||||
|
- `wikitool sync` - the same reconcile on its own, without committing or pushing
|
||||||
|
- `instructions/gates.md` - the gate procedure
|
||||||
|
- `instructions/setup-instance.md` - the first publish of a new instance
|
||||||
|
|
||||||
### Workshop runs and session budget
|
### Workshop runs and session budget
|
||||||
|
|
||||||
@@ -1078,7 +1140,7 @@ Scaffold `work/<runkey>/` for one workshop run.
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- A collision is not transient: resume the existing run instead, or pass `--again` if the tree itself changed. Never create a numbered variant by hand
|
- Neither or both of `--input`/`--key` given, `--input` outside `raw/`, a `--key` that is empty or starts with `ingest-`, or the workshop already exists -> A collision is not transient: resume the existing run instead, or pass `--again` if the tree itself changed. Never create a numbered variant by hand
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
@@ -1107,7 +1169,7 @@ Delete a finished workshop.
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- For "not confirmed": check the listed files are no longer needed, confirm the conclusions are in `kb/`, then re-run with `--yes`
|
- Unknown run key, or `--yes` was not passed -> For "not confirmed": check the listed files are no longer needed, confirm the conclusions are in `kb/`, then re-run with `--yes`
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
@@ -1160,7 +1222,7 @@ Clear the current session's (or every session's) iteration budget state.
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- Get the user's approval, then re-run with `--yes`
|
- `--yes` not passed -> Get the user's approval, then re-run with `--yes`
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
@@ -1215,7 +1277,7 @@ Print one type's full contract.
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- Fix the name and retry
|
- Unknown type name -> Fix the name and retry
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
@@ -1244,7 +1306,7 @@ Publish every `instructions/<name>/SKILL.md` into the harness skill directories.
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- Check whether the flagged target holds anything worth keeping, then re-run with `--force` if not; otherwise fix the named cause and retry
|
- No skills found under `instructions/`, or a target directory is not a published skill (no `SKILL.md`) and `--force` was not passed -> Check whether the flagged target holds anything worth keeping, then re-run with `--force` if not; otherwise fix the named cause and retry
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
@@ -1273,7 +1335,7 @@ Check the instruction layer.
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- Fix the flagged file, then re-run. For a relative link in a `SKILL.md`, rewrite it as a repo-root-relative plain path instead. For drift, re-run `sync` instead of hand-editing the published copy - the source under `instructions/` always wins
|
- Nothing found under `instructions/` at all, a malformed instruction or `SKILL.md`, a `SKILL.md` carrying a relative markdown link, a published copy that drifted from its source, an instruction nothing references (or, for `manual: true`, one that IS linked from AGENTS.md, CLAUDE.md, or a skill and so risks running implicitly), or something under `instructions/dev/` referenced from outside it and outside a `dist:strip` block -> Fix the flagged file, then re-run. For a relative link in a `SKILL.md`, rewrite it as a repo-root-relative plain path instead. For drift, re-run `sync` instead of hand-editing the published copy - the source under `instructions/` always wins
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
@@ -1326,7 +1388,7 @@ Check the docs that mirror the code.
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- Fix the documentation it names, then re-run. For a type-spec's own frontmatter: fix the field, or add a matching line to `types/type-spec.schema.yaml` if the field is legitimately new. For an issue reference: say what was decided instead of pointing at where, or move the pointer behind a `<!-- dist:strip-start/end -->` block. For a table of contents: run `docs toc --apply` - never hand-write the region. For a dead link: fix the `../` count or the target's name
|
- A command, contract, or type-form mismatch was found, a type-spec's own frontmatter fails its schema, a shipped `.md`/`.template` cites an issue number, a reference file's table-of-contents region is missing or stale, or a reference file's relative markdown link does not resolve to an existing file -> Fix the documentation it names, then re-run. For a type-spec's own frontmatter: fix the field, or add a matching line to `types/type-spec.schema.yaml` if the field is legitimately new. For an issue reference: say what was decided instead of pointing at where, or move the pointer behind a `<!-- dist:strip-start/end -->` block. For a table of contents: run `docs toc --apply` - never hand-write the region. For a dead link: fix the `../` count or the target's name
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
@@ -1355,7 +1417,7 @@ Create, refresh or remove the generated table-of-contents region.
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- Nothing to fix - re-run with `--apply` to write what the dry run listed. If `docs verify` still reports a stale region afterwards, the file's `##` headings changed in between; run it again
|
- Never fails on content: a file with no `##` heading, or one at or under the threshold, is simply left without a region -> Nothing to fix - re-run with `--apply` to write what the dry run listed. If `docs verify` still reports a stale region afterwards, the file's `##` headings changed in between; run it again
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
@@ -1434,7 +1496,7 @@ Score one traced session.
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- Run `eval sessions` to see which ids exist. A session records nothing when telemetry is off - `WIKI_TRACE=0`, or a distributed instance with no opt-in (`wikitool doctor` says which) - so an absent trace is not necessarily a fault. Safe to retry
|
- No trace exists for the named session -> Run `eval sessions` to see which ids exist. A session records nothing when telemetry is off - `WIKI_TRACE=0`, or a distributed instance with no opt-in (`wikitool doctor` says which) - so an absent trace is not necessarily a fault. Safe to retry
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
@@ -1465,7 +1527,7 @@ Write a contentless, distributable copy of this repo's machinery.
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- 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, 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
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
@@ -1494,7 +1556,7 @@ Apply a stack update `dist export` produced - the write half of `version check`.
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- 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
|
- 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
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
@@ -1523,7 +1585,7 @@ Print this instance's stack version and where it came from.
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- Fix `VERSION` and retry
|
- `VERSION` is missing or unparseable -> Fix `VERSION` and retry
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
@@ -1552,7 +1614,7 @@ Ask the origin's release feed whether a newer stack exists.
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- 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
|
- 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
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
@@ -1581,7 +1643,7 @@ Print one version's release notes.
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- 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
|
- 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
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
@@ -1610,7 +1672,7 @@ Raise or continue the one running candidate between two releases.
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- **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
|
- 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
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
@@ -1639,7 +1701,7 @@ List the running candidate's bump titles with their impact grade, or change one
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- 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
|
- 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
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
@@ -1668,7 +1730,7 @@ Fix the running candidate: strip `VERSION`'s `-beta.N` suffix and close its `CHA
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- **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
|
- 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
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
@@ -1723,7 +1785,7 @@ Show the migrations this instance still owes, in the order they must run.
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- For a missing declaration: run `migrate baseline <version>` once, then retry. Safe to retry freely otherwise
|
- `.wikitool-kb.json` is missing (content version undeclared), or `VERSION` is unreadable -> For a missing declaration: run `migrate baseline <version>` once, then retry. Safe to retry freely otherwise
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
@@ -1752,7 +1814,7 @@ Compare `kb/` against a git revision on the invariants a content migration must
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- 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. 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
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
@@ -1781,7 +1843,7 @@ Record one migration as applied, advancing `kb_version` in `.wikitool-kb.json`.
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- **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`, 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
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
@@ -1810,7 +1872,7 @@ Declare `kb_version` once, for an instance predating `.wikitool-kb.json`.
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- Safe to re-run with the same version. If a declaration exists, it is almost always `migrate done` that was wanted
|
- 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
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
@@ -1841,7 +1903,7 @@ Take a stack update into a private instance's branch, machinery only.
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- **Not idempotent, and not safe to retry unchanged.** For a dirty tree or an in-progress merge: fix the named precondition and retry once. For a real conflict: **do not retry, do not force** - resolve the named paths by hand (take the upstream side, or re-file the local change as an issue against the public repo per `instructions/private-instance.md`) and either `git commit --no-edit` yourself or `git merge --abort`. If the postcheck after commit finds a leak, the merge commit already exists and is **not** rolled back automatically - inspect it by hand; this is a bug report, not a retry
|
- Dirty working tree, a merge already in progress, the remote does not resolve, git refused to open the merge at all (unrelated histories), or a real conflict remains in `tools/`/`types/`/`instructions/` after the content stages and stack-owned paths were restored -> **Not idempotent, and not safe to retry unchanged.** For a dirty tree or an in-progress merge: fix the named precondition and retry once. For a real conflict: **do not retry, do not force** - resolve the named paths by hand (take the upstream side, or re-file the local change as an issue against the public repo per `instructions/private-instance.md`) and either `git commit --no-edit` yourself or `git merge --abort`. If the postcheck after commit finds a leak, the merge commit already exists and is **not** rolled back automatically - inspect it by hand; this is a bug report, not a retry
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
@@ -1870,7 +1932,7 @@ Compare two revisions: did anything under a content stage change except through
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- A finding is not fixed by re-running - it names the paths that leaked. Fix the revision argument and retry for the second case
|
- A leak was found (content changed under a content stage through a path that is not stack-owned), or `--since`/`--until` is not a revision in this repository -> A finding is not fixed by re-running - it names the paths that leaked. Fix the revision argument and retry for the second case
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
@@ -1901,7 +1963,7 @@ Check that this instance is correctly configured.
|
|||||||
|
|
||||||
**ON FAILURE**
|
**ON FAILURE**
|
||||||
|
|
||||||
- Each finding names its own fix command; re-run after applying it
|
- At least one check reported `FAIL` (a `WARN`, e.g. no remote or no `WIKITOOL_SESSION_ID`, does not exit 1) -> Each finding names its own fix command; re-run after applying it
|
||||||
|
|
||||||
**NOTES**
|
**NOTES**
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -86,7 +86,10 @@ bound at import time - `KB_DIR` and friends follow whatever `ROOT` currently is.
|
|||||||
<cmd> -h`, the index (`wikitool -h`, and the top of
|
<cmd> -h`, the index (`wikitool -h`, and the top of
|
||||||
[CONTRACT.md](CONTRACT.md)), and CONTRACT.md's generated `#### <path>`
|
[CONTRACT.md](CONTRACT.md)), and CONTRACT.md's generated `#### <path>`
|
||||||
section all render from - see `cli_contract.py`'s own module docstring for
|
section all render from - see `cli_contract.py`'s own module docstring for
|
||||||
the record's shape. `docs verify` fails in both directions - a command with
|
the record's shape, and the `CommandRecord` docstring for how its prose is
|
||||||
|
written (NOTES as present-tense bullets, one `Failure` per cause with its
|
||||||
|
reaction, copyable EXAMPLES, NEVER, SEE ALSO, and no "why" - that goes into
|
||||||
|
a comment next to the code). `docs verify` fails in both directions - a command with
|
||||||
no record and a `GROUPS` entry naming no real command are equally reported -
|
no record and a `GROUPS` entry naming no real command are equally reported -
|
||||||
and also checks that every non-hidden flag appears in the record's SYNOPSIS.
|
and also checks that every non-hidden flag appears in the record's SYNOPSIS.
|
||||||
Then regenerate the copy: `wikitool docs contract --apply`. Write the
|
Then regenerate the copy: `wikitool docs contract --apply`. Write the
|
||||||
|
|||||||
+113
-38
@@ -9,10 +9,13 @@ next to the code it describes. `GROUPS` is the one thing that stays central:
|
|||||||
the `###`-level grouping and rendering order, unchanged from what
|
the `###`-level grouping and rendering order, unchanged from what
|
||||||
`tools/CONTRACT.md` carried before this module existed.
|
`tools/CONTRACT.md` carried before this module existed.
|
||||||
|
|
||||||
Phase 1 (Gitea #121) fills every record mechanically and word-for-word from
|
Phase 1 (Gitea #121) filled every record mechanically and word-for-word from
|
||||||
the two tables `tools/CONTRACT.md` used to carry. Phase 2 (Gitea #142) is what
|
the two tables `tools/CONTRACT.md` used to carry. Phase 2 (Gitea #142)
|
||||||
edits the prose, adds EXAMPLES/NEVER/SEE ALSO, and pulls "why" out of a
|
rewrites them group by group: NOTES into present-tense bullets, one
|
||||||
record's NOTES - not this module's job.
|
`Failure` per cause, EXAMPLES/NEVER/SEE ALSO filled, and "why" moved out to
|
||||||
|
a code comment where the behaviour is implemented. A sentence several
|
||||||
|
records carry verbatim lives here once (`token_gate_reaction`, ...), so the
|
||||||
|
output repeats it and the source does not.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -63,15 +66,27 @@ class Variant:
|
|||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class Failure:
|
class Failure:
|
||||||
"""One exit-1 story - most commands have exactly one, `new` and
|
"""One cause of a non-success exit, and what the caller does about it.
|
||||||
`raw accept` have two (one per `Variant`), because their failure causes
|
|
||||||
differ by usage form. `label` is empty for a command with only one; when
|
EXIT STATUS renders one `<code> <cause>` line per entry, ON FAILURE one
|
||||||
a command carries more than one `Failure`, `label` names which variant it
|
`<cause> -> <reaction>` line - the cause is repeated on purpose, so each
|
||||||
describes (`"new <type>"`, `"new project"`, ...), and EXIT STATUS/ON
|
ON FAILURE line reads on its own. `code` is 1 (validation error) or 42
|
||||||
FAILURE render every label."""
|
(a gate needs clearance; only on a command whose `Properties.gates` is
|
||||||
label: str
|
non-empty), or 0 for an outcome a caller could mistake for a failure but
|
||||||
exit_1: str
|
that is not one (an unreachable remote reported and skipped). An empty
|
||||||
retry: str
|
`reaction` renders the EXIT STATUS line only.
|
||||||
|
|
||||||
|
`label` names the usage form a cause belongs to (`"new project"`) and is
|
||||||
|
rendered as a `<label>: ` prefix on both lines; empty when the command
|
||||||
|
has one form, or when the cause already says it."""
|
||||||
|
cause: str
|
||||||
|
reaction: str
|
||||||
|
code: int = 1
|
||||||
|
label: str = ""
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
if self.code not in (0, 1, 42):
|
||||||
|
raise ValueError(f"cli_contract: Failure.code must be 0, 1 or 42, not {self.code}")
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -87,18 +102,63 @@ class Properties:
|
|||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class CommandRecord:
|
class CommandRecord:
|
||||||
"""The man-page-shaped record for one command path (e.g. `"publish"`,
|
"""The man-page-shaped record for one command path (e.g. `"publish"`,
|
||||||
`"xref add"`). Sections not populated in phase 1 (`examples`, `never`,
|
`"xref add"`). Empty `examples`, `never` and `see_also` render as absent,
|
||||||
`see_also`) render as absent, not empty - see `render_text`."""
|
not empty - see `render_text`.
|
||||||
|
|
||||||
|
How the prose is written - a record is read on its own, by an agent that
|
||||||
|
asked for exactly this command:
|
||||||
|
|
||||||
|
- `notes`: one bullet per behaviour, present tense. What the command does,
|
||||||
|
not why it was built that way - a "why" goes into a comment where the
|
||||||
|
behaviour is implemented, and history into `CHANGES.md` or nowhere.
|
||||||
|
- `failures`: one entry per cause, each with its own reaction. A rule
|
||||||
|
("do not retry", "show the output and stop") belongs in the reaction or
|
||||||
|
in `never`, never only in `notes`.
|
||||||
|
- `examples`: one to three copyable calls, the most common first; a gated
|
||||||
|
command also shows its re-run after exit 42.
|
||||||
|
- `never`: the prohibitions for the caller, one per line.
|
||||||
|
- `see_also`: related commands and the instruction that uses this one.
|
||||||
|
It is the only place another command may be named for context - a
|
||||||
|
behaviour this command shares with another is stated here in full,
|
||||||
|
not as "same as `X`".
|
||||||
|
|
||||||
|
A plain `str` for `notes` is still accepted and renders as one paragraph:
|
||||||
|
the phase-1 form, kept only until every group has been rewritten into
|
||||||
|
bullets (Gitea #142)."""
|
||||||
path: str
|
path: str
|
||||||
summary: str
|
summary: str
|
||||||
synopsis: tuple[Variant, ...]
|
synopsis: tuple[Variant, ...]
|
||||||
properties: Properties
|
properties: Properties
|
||||||
notes: str
|
notes: tuple[str, ...] | str
|
||||||
failures: tuple[Failure, ...]
|
failures: tuple[Failure, ...]
|
||||||
examples: tuple[str, ...] = ()
|
examples: tuple[str, ...] = ()
|
||||||
never: tuple[str, ...] = ()
|
never: tuple[str, ...] = ()
|
||||||
see_also: tuple[str, ...] = ()
|
see_also: tuple[str, ...] = ()
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
if not self.properties.gates and any(f.code == 42 for f in self.failures):
|
||||||
|
raise ValueError(
|
||||||
|
f"cli_contract: {self.path!r} lists an exit-42 cause but declares no gate"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Shared sentences - text more than one record carries word for word.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def token_gate_reaction(flag: str) -> str:
|
||||||
|
"""The ON FAILURE reaction to an exit-42 gate that is cleared by a token
|
||||||
|
(`--confirm`, `--confirm-rebase`): every such gate prints its evidence and
|
||||||
|
the exact re-run line, and refuses a token that does not match the state
|
||||||
|
it was issued for."""
|
||||||
|
return (
|
||||||
|
"Show the user the command's full output verbatim and stop. Once they have approved "
|
||||||
|
f"it, run the re-run line the output prints, which carries `{flag} <token>`. Without "
|
||||||
|
"that token, or with a wrong, invented or superseded one, it exits 42 again with the "
|
||||||
|
"current state"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Registry
|
# Registry
|
||||||
@@ -224,12 +284,22 @@ _SECTION_ORDER = (
|
|||||||
|
|
||||||
|
|
||||||
def _exit_codes(rec: CommandRecord) -> list[int]:
|
def _exit_codes(rec: CommandRecord) -> list[int]:
|
||||||
codes = [0]
|
codes = {0} | {failure.code for failure in rec.failures}
|
||||||
if rec.failures:
|
|
||||||
codes.append(1)
|
|
||||||
if rec.properties.gates:
|
if rec.properties.gates:
|
||||||
codes.append(42)
|
codes.add(42)
|
||||||
return codes
|
return sorted(codes)
|
||||||
|
|
||||||
|
|
||||||
|
def _labelled(failure: Failure, text: str) -> str:
|
||||||
|
return f"{failure.label}: {text}" if failure.label else text
|
||||||
|
|
||||||
|
|
||||||
|
def _notes_lines(notes: tuple[str, ...] | str) -> list[str]:
|
||||||
|
"""NOTES as rendered lines: one `- ` bullet per entry, or the phase-1
|
||||||
|
paragraph unchanged."""
|
||||||
|
if isinstance(notes, str):
|
||||||
|
return [notes]
|
||||||
|
return [f"- {note}" for note in notes]
|
||||||
|
|
||||||
|
|
||||||
def _idempotent_text(idempotent: Idempotent) -> str:
|
def _idempotent_text(idempotent: Idempotent) -> str:
|
||||||
@@ -258,22 +328,24 @@ def render_properties_lines(props: Properties) -> list[str]:
|
|||||||
|
|
||||||
|
|
||||||
def render_exit_status_lines(rec: CommandRecord) -> list[str]:
|
def render_exit_status_lines(rec: CommandRecord) -> list[str]:
|
||||||
|
"""`0 success` first, then one line per `Failure` in code order (stable
|
||||||
|
within a code). A command with gates but no explicit exit-42 cause gets
|
||||||
|
one generic 42 line naming them."""
|
||||||
lines = ["0 success"]
|
lines = ["0 success"]
|
||||||
for failure in rec.failures:
|
for failure in sorted(rec.failures, key=lambda f: f.code):
|
||||||
prefix = f"{failure.label}: " if failure.label else ""
|
lines.append(f"{str(failure.code).ljust(4)} {_labelled(failure, failure.cause)}")
|
||||||
lines.append(f"1 {prefix}{failure.exit_1}")
|
if rec.properties.gates and not any(f.code == 42 for f in rec.failures):
|
||||||
if rec.properties.gates:
|
|
||||||
gate_list = ", ".join(rec.properties.gates)
|
gate_list = ", ".join(rec.properties.gates)
|
||||||
lines.append(f"42 needs clearance - {gate_list} (see AGENTS.md § Gates)")
|
lines.append(f"42 needs clearance - {gate_list} (see AGENTS.md § Gates)")
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
|
|
||||||
def render_on_failure_lines(rec: CommandRecord) -> list[str]:
|
def render_on_failure_lines(rec: CommandRecord) -> list[str]:
|
||||||
lines = []
|
return [
|
||||||
for failure in rec.failures:
|
_labelled(failure, f"{failure.cause} -> {failure.reaction}")
|
||||||
prefix = f"{failure.label}: " if failure.label else ""
|
for failure in sorted(rec.failures, key=lambda f: f.code)
|
||||||
lines.append(f"{prefix}{failure.retry}")
|
if failure.reaction
|
||||||
return lines
|
]
|
||||||
|
|
||||||
|
|
||||||
def render_text(rec: CommandRecord, options_text: str = "") -> str:
|
def render_text(rec: CommandRecord, options_text: str = "") -> str:
|
||||||
@@ -281,8 +353,9 @@ def render_text(rec: CommandRecord, options_text: str = "") -> str:
|
|||||||
`wikitool <path> -h`. `options_text` is Click's own rendered Options
|
`wikitool <path> -h`. `options_text` is Click's own rendered Options
|
||||||
block (already flag-formatted) for this command, spliced in between
|
block (already flag-formatted) for this command, spliced in between
|
||||||
EXAMPLES and EXIT STATUS - see `chemenu.cli` for how it is obtained.
|
EXAMPLES and EXIT STATUS - see `chemenu.cli` for how it is obtained.
|
||||||
Empty sections (EXAMPLES/NEVER/SEE ALSO in phase 1, OPTIONS for a command
|
Empty sections (EXAMPLES/NEVER/SEE ALSO, ON FAILURE with no reaction to
|
||||||
with none) are omitted entirely rather than printed empty."""
|
give, OPTIONS for a command with none) are omitted entirely rather than
|
||||||
|
printed empty."""
|
||||||
blocks: list[str] = []
|
blocks: list[str] = []
|
||||||
|
|
||||||
blocks.append(f"NAME\n wikitool {rec.path} - {rec.summary}")
|
blocks.append(f"NAME\n wikitool {rec.path} - {rec.summary}")
|
||||||
@@ -306,15 +379,16 @@ def render_text(rec: CommandRecord, options_text: str = "") -> str:
|
|||||||
exit_lines = "\n".join(f" {line}" for line in render_exit_status_lines(rec))
|
exit_lines = "\n".join(f" {line}" for line in render_exit_status_lines(rec))
|
||||||
blocks.append(f"EXIT STATUS\n{exit_lines}")
|
blocks.append(f"EXIT STATUS\n{exit_lines}")
|
||||||
|
|
||||||
if rec.failures:
|
on_failure = render_on_failure_lines(rec)
|
||||||
failure_lines = "\n".join(f" {line}" for line in render_on_failure_lines(rec))
|
if on_failure:
|
||||||
|
failure_lines = "\n".join(f" {line}" for line in on_failure)
|
||||||
blocks.append(f"ON FAILURE\n{failure_lines}")
|
blocks.append(f"ON FAILURE\n{failure_lines}")
|
||||||
|
|
||||||
if rec.never:
|
if rec.never:
|
||||||
never_lines = "\n".join(f" - {n}" for n in rec.never)
|
never_lines = "\n".join(f" - {n}" for n in rec.never)
|
||||||
blocks.append(f"NEVER\n{never_lines}")
|
blocks.append(f"NEVER\n{never_lines}")
|
||||||
|
|
||||||
notes_lines = "\n".join(f" {line}" for line in rec.notes.splitlines()) or f" {rec.notes}"
|
notes_lines = "\n".join(f" {line}" for line in _notes_lines(rec.notes))
|
||||||
blocks.append(f"NOTES\n{notes_lines}")
|
blocks.append(f"NOTES\n{notes_lines}")
|
||||||
|
|
||||||
if rec.see_also:
|
if rec.see_also:
|
||||||
@@ -388,10 +462,11 @@ def render_markdown_section(rec: CommandRecord) -> str:
|
|||||||
lines.append(f"- {line}")
|
lines.append(f"- {line}")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
if rec.failures:
|
on_failure = render_on_failure_lines(rec)
|
||||||
|
if on_failure:
|
||||||
lines.append("**ON FAILURE**")
|
lines.append("**ON FAILURE**")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
for line in render_on_failure_lines(rec):
|
for line in on_failure:
|
||||||
lines.append(f"- {line}")
|
lines.append(f"- {line}")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
@@ -404,7 +479,7 @@ def render_markdown_section(rec: CommandRecord) -> str:
|
|||||||
|
|
||||||
lines.append("**NOTES**")
|
lines.append("**NOTES**")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append(rec.notes)
|
lines.extend(_notes_lines(rec.notes))
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
if rec.see_also:
|
if rec.see_also:
|
||||||
|
|||||||
@@ -119,8 +119,8 @@ def upsert_citation(page: Page, source_title: str, qualifier: Optional[str]) ->
|
|||||||
"a manual, editorial step",
|
"a manual, editorial step",
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
label="",
|
||||||
exit_1="Page or source not found",
|
cause="Page or source not found",
|
||||||
retry="Safe to retry; upserting the same (page, source, file) pair twice reuses the "
|
reaction="Safe to retry; upserting the same (page, source, file) pair twice reuses the "
|
||||||
"existing id and changes nothing the second time",
|
"existing id and changes nothing the second time",
|
||||||
),),
|
),),
|
||||||
))
|
))
|
||||||
@@ -214,8 +214,8 @@ def sync_page(page: Page) -> tuple[str, bool, list[str], list[str]]:
|
|||||||
"instance's heading is a repair rather than a rename",
|
"instance's heading is a repair rather than a rename",
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
label="",
|
||||||
exit_1="Neither or both of `--page`/`--all` given, or page not found",
|
cause="Neither or both of `--page`/`--all` given, or page not found",
|
||||||
retry="Safe to retry freely. An undefined-reference report is not a failure - fix the "
|
reaction="Safe to retry freely. An undefined-reference report is not a failure - fix the "
|
||||||
"reference (or run `cite add`) and re-run",
|
"reference (or run `cite add`) and re-run",
|
||||||
),),
|
),),
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -608,9 +608,9 @@ def _write_plan(target: Path, plan: dict[str, PlannedFile]) -> None:
|
|||||||
"repo (or a new dev instance exported from it) instead",
|
"repo (or a new dev instance exported from it) instead",
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
label="",
|
||||||
exit_1="Target exists and is not empty, is not a directory, or the tree has no "
|
cause="Target exists and is not empty, is not a directory, or the tree has no "
|
||||||
"readable `VERSION`",
|
"readable `VERSION`",
|
||||||
retry="Point `<target>` at an empty (or new) directory and retry. Never merge into a "
|
reaction="Point `<target>` at an empty (or new) directory and retry. Never merge into a "
|
||||||
"non-empty one by hand",
|
"non-empty one by hand",
|
||||||
),),
|
),),
|
||||||
))
|
))
|
||||||
@@ -1014,14 +1014,14 @@ def _report_plan(
|
|||||||
"`INSTALL.md` § \"Version und Updates\"",
|
"`INSTALL.md` § \"Version und Updates\"",
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
label="",
|
||||||
exit_1="Missing local `VERSION`/`.wikitool-release.json`(`files`)/`.wikitool-kb.json`, "
|
cause="Missing local `VERSION`/`.wikitool-release.json`(`files`)/`.wikitool-kb.json`, "
|
||||||
"a migration already outstanding against the installed machinery, a dirty working "
|
"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 "
|
"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 "
|
"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 "
|
"`--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 "
|
"`--dry-run` also raises), or one or more locally changed files that neither "
|
||||||
"`--keep-local` nor a `--take-release` answers for",
|
"`--keep-local` nor a `--take-release` answers for",
|
||||||
retry="For every refusal above: fix the named precondition and retry - none of them are "
|
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 "
|
"transient. For a rejected `--take-release` path: correct it against the "
|
||||||
"locally-changed list the refusal prints. For locally changed files, the refusal names "
|
"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 "
|
"all three answers with the re-run line filled in - `--take-release <path>` to write "
|
||||||
|
|||||||
@@ -1074,11 +1074,11 @@ def check_breaking_change_for_boundary() -> list[str]:
|
|||||||
"it now checks",
|
"it now checks",
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
label="",
|
||||||
exit_1="A command, contract, or type-form mismatch was found, a type-spec's own "
|
cause="A command, contract, or type-form mismatch was found, a type-spec's own "
|
||||||
"frontmatter fails its schema, a shipped `.md`/`.template` cites an issue number, a "
|
"frontmatter fails its schema, a shipped `.md`/`.template` cites an issue number, a "
|
||||||
"reference file's table-of-contents region is missing or stale, or a reference file's "
|
"reference file's table-of-contents region is missing or stale, or a reference file's "
|
||||||
"relative markdown link does not resolve to an existing file",
|
"relative markdown link does not resolve to an existing file",
|
||||||
retry="Fix the documentation it names, then re-run. For a type-spec's own frontmatter: "
|
reaction="Fix the documentation it names, then re-run. For a type-spec's own frontmatter: "
|
||||||
"fix the field, or add a matching line to `types/type-spec.schema.yaml` if the field is "
|
"fix the field, or add a matching line to `types/type-spec.schema.yaml` if the field is "
|
||||||
"legitimately new. For an issue reference: say what was decided instead of pointing at "
|
"legitimately new. For an issue reference: say what was decided instead of pointing at "
|
||||||
"where, or move the pointer behind a `<!-- dist:strip-start/end -->` block. For a table "
|
"where, or move the pointer behind a `<!-- dist:strip-start/end -->` block. For a table "
|
||||||
@@ -1157,9 +1157,9 @@ def verify():
|
|||||||
"result stays current the same way it checks every other generated-from-code copy",
|
"result stays current the same way it checks every other generated-from-code copy",
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
label="",
|
||||||
exit_1="Never fails on content: a file with no `##` heading, or one at or under the "
|
cause="Never fails on content: a file with no `##` heading, or one at or under the "
|
||||||
"threshold, is simply left without a region",
|
"threshold, is simply left without a region",
|
||||||
retry="Nothing to fix - re-run with `--apply` to write what the dry run listed. If "
|
reaction="Nothing to fix - re-run with `--apply` to write what the dry run listed. If "
|
||||||
"`docs verify` still reports a stale region afterwards, the file's `##` headings "
|
"`docs verify` still reports a stale region afterwards, the file's `##` headings "
|
||||||
"changed in between; run it again",
|
"changed in between; run it again",
|
||||||
),),
|
),),
|
||||||
|
|||||||
@@ -656,9 +656,9 @@ def run_doctor() -> list[Check]:
|
|||||||
"Exempt from the Iteration Budget Gate",
|
"Exempt from the Iteration Budget Gate",
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
label="",
|
||||||
exit_1="At least one check reported `FAIL` (a `WARN`, e.g. no remote or no "
|
cause="At least one check reported `FAIL` (a `WARN`, e.g. no remote or no "
|
||||||
"`WIKITOOL_SESSION_ID`, does not exit 1)",
|
"`WIKITOOL_SESSION_ID`, does not exit 1)",
|
||||||
retry="Each finding names its own fix command; re-run after applying it",
|
reaction="Each finding names its own fix command; re-run after applying it",
|
||||||
),),
|
),),
|
||||||
))
|
))
|
||||||
def doctor_command(
|
def doctor_command(
|
||||||
|
|||||||
@@ -78,8 +78,8 @@ def sessions_command(
|
|||||||
"and exempt from the budget; see `EVALS.md`",
|
"and exempt from the budget; see `EVALS.md`",
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
label="",
|
||||||
exit_1="No trace exists for the named session",
|
cause="No trace exists for the named session",
|
||||||
retry="Run `eval sessions` to see which ids exist. A session records nothing when "
|
reaction="Run `eval sessions` to see which ids exist. A session records nothing when "
|
||||||
"telemetry is off - `WIKI_TRACE=0`, or a distributed instance with no opt-in "
|
"telemetry is off - `WIKI_TRACE=0`, or a distributed instance with no opt-in "
|
||||||
"(`wikitool doctor` says which) - so an absent trace is not necessarily a fault. Safe "
|
"(`wikitool doctor` says which) - so an absent trace is not necessarily a fault. Safe "
|
||||||
"to retry",
|
"to retry",
|
||||||
|
|||||||
@@ -1012,24 +1012,48 @@ def apply_reconcile(outcome: ReconcileOutcome, remote: str, branch: str, command
|
|||||||
budget=cli_contract.Budget.COUNTED,
|
budget=cli_contract.Budget.COUNTED,
|
||||||
gates=("rebase-review",),
|
gates=("rebase-review",),
|
||||||
),
|
),
|
||||||
notes="Fetch `<remote>/<branch>` and bring the local branch up to date with it: fast-forward "
|
notes=(
|
||||||
"when the remote is simply ahead, rebase local commit(s) on top when both sides moved but "
|
"Fetches `<remote>/<branch>`, then: fast-forwards when only the remote moved; rebases "
|
||||||
"touch disjoint files (a content conflict is then impossible by construction), and exit "
|
"the local commits on top when both sides moved but touched disjoint files; exits 42 "
|
||||||
"**42** for review when they touch the same file (the **rebase-review gate** - see "
|
"(rebase-review gate) when both sides touched the same file.",
|
||||||
"`publish` below). Never commits, never pushes, never force-anything - no remote configured, "
|
"A refused call performs no rebase attempt and leaves the branch where it was.",
|
||||||
"or one that cannot be reached, is reported and skipped, not a failure. Meant to run once "
|
"The `--confirm-rebase` token covers the exact upstream state and the set of files "
|
||||||
"at the start of a writing session (`instructions/session-setup.md`) so the rest of it "
|
"touched on both sides; either one moving makes it stale.",
|
||||||
"works against a current tree instead of discovering the drift at the final `publish`",
|
"Makes no commit, no push, and no forced operation of any kind.",
|
||||||
failures=(cli_contract.Failure(
|
"Run it once at the start of a writing session.",
|
||||||
label="",
|
),
|
||||||
exit_1="The automatic rebase hit a real conflict (git failed)",
|
failures=(
|
||||||
retry="For a conflict: **do not retry, do not force** - resolve manually and re-run. "
|
cli_contract.Failure(
|
||||||
"**Exit 42, not 1**, when the rebase-review gate needs clearance: show the user the "
|
cause="No remote configured, or the remote cannot be reached - reported and "
|
||||||
"command's full output verbatim (upstream commits, the overlapping files, their diff) "
|
"skipped, not a failure",
|
||||||
"and stop; re-running with `--confirm-rebase <token>` clears it, and a wrong, invented, "
|
reaction="",
|
||||||
"or superseded token exits 42 again with the current state. No remote configured, or "
|
code=0,
|
||||||
"one that cannot be reached, is not a failure - reported and skipped",
|
),
|
||||||
),),
|
cli_contract.Failure(
|
||||||
|
cause="The automatic rebase hit a real conflict (git failed); it is aborted cleanly",
|
||||||
|
reaction="Do not retry and do not force - resolve the conflict manually, then re-run",
|
||||||
|
),
|
||||||
|
cli_contract.Failure(
|
||||||
|
cause="Rebase-review gate: `<remote>/<branch>` moved and both sides changed the "
|
||||||
|
"same file; the output lists the upstream commits, the overlapping files and their "
|
||||||
|
"diff",
|
||||||
|
reaction=cli_contract.token_gate_reaction("--confirm-rebase"),
|
||||||
|
code=42,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
examples=(
|
||||||
|
"tools/wikitool sync",
|
||||||
|
"tools/wikitool sync --confirm-rebase <token> # re-run after exit 42, once the user approved",
|
||||||
|
),
|
||||||
|
never=(
|
||||||
|
"Never retry a conflict unchanged, and never force past it.",
|
||||||
|
"Never pass a `--confirm-rebase` token the user has not seen and approved.",
|
||||||
|
),
|
||||||
|
see_also=(
|
||||||
|
"`wikitool publish` - runs the same reconcile before it commits and pushes",
|
||||||
|
"`instructions/session-setup.md` - where a session runs `sync`",
|
||||||
|
"`instructions/gates.md` - the gate procedure",
|
||||||
|
),
|
||||||
))
|
))
|
||||||
def sync_command(
|
def sync_command(
|
||||||
remote: str = typer.Option("origin", "--remote", help="Git remote to reconcile against"),
|
remote: str = typer.Option("origin", "--remote", help="Git remote to reconcile against"),
|
||||||
@@ -1063,71 +1087,123 @@ def sync_command(
|
|||||||
properties=cli_contract.Properties(
|
properties=cli_contract.Properties(
|
||||||
effect=cli_contract.Effect.WRITE,
|
effect=cli_contract.Effect.WRITE,
|
||||||
idempotent=cli_contract.Idempotent.NO,
|
idempotent=cli_contract.Idempotent.NO,
|
||||||
atomic="No - sequential git operations, but both gates run before staging",
|
atomic="No - sequential git operations, but every gate runs before staging",
|
||||||
budget=cli_contract.Budget.COUNTED,
|
budget=cli_contract.Budget.COUNTED,
|
||||||
gates=("mass-update", "publish-remote", "rebase-review"),
|
gates=("mass-update", "publish-remote", "rebase-review"),
|
||||||
),
|
),
|
||||||
notes="Reconcile with `<remote>/<branch>` exactly like `sync` (skipped for `--no-push`), "
|
notes=(
|
||||||
"then stage all changes, commit, and push. Refuses before staging anything when the push "
|
"Order: branch check and Publish-Remote Gate, then the reconcile with "
|
||||||
"target is not the checked-out branch, so a `git push <branch>` cannot quietly publish a "
|
"`<remote>/<branch>`, then the Mass-Update Gate, then `git add -A`, commit and push. "
|
||||||
"ref other than the commit just made; the *unborn* branch of a fresh `git init -b main` "
|
"`--no-push` skips all but the Mass-Update Gate and the commit.",
|
||||||
"counts as checked out, which is what lets the first publish of a new instance work "
|
"Reconcile: fetches `<remote>/<branch>`, fast-forwards when only the remote moved, "
|
||||||
"(`instructions/setup-instance.md` step 14), while a genuine detached HEAD is still "
|
"rebases the local commits on top when both sides moved but touched disjoint files, "
|
||||||
"refused. If the reconcile step found a still-unpushed local commit and there is nothing "
|
"and exits 42 (rebase-review gate) when both sides touched the same file. A refused "
|
||||||
"new to stage, that commit is pushed anyway - a previous `publish` whose push failed no "
|
"reconcile performs no rebase attempt. The `--confirm-rebase` token covers the exact "
|
||||||
"longer strands it, and neither does a branch the remote has never seen (a newly created, "
|
"upstream state and the set of files touched on both sides.",
|
||||||
"empty remote repository). A remote that cannot be reached at all is deliberately not read "
|
"The push target must be the checked-out branch; this is checked before anything is "
|
||||||
"that way: it keeps reporting \"Nothing to commit\" on a clean tree rather than attempting "
|
"staged. The unborn branch of a fresh `git init -b main` counts as checked out, so the "
|
||||||
"a push, so an offline or local-only instance is unaffected. If the push is rejected "
|
"first publish of a new instance works; a real detached HEAD is refused.",
|
||||||
"despite the pre-check (a genuine race - something landed on the remote in between), one "
|
"With nothing new to stage, a local commit the remote lacks is still pushed: one left "
|
||||||
"more reconcile-and-retry is attempted before giving up; never more than one. "
|
"behind by an earlier publish whose push failed, or every commit when the remote "
|
||||||
"**Mass-Update Gate:** when >= `--threshold` (default 10) *counted* files would be "
|
"answers but does not have the branch yet (a new, empty remote repository).",
|
||||||
"committed, exits **42 (`EXIT_NEEDS_CLEARANCE`)** instead of publishing - a third outcome "
|
"A remote that cannot be reached is not read as lacking the branch: on a clean tree "
|
||||||
"distinct from success (0) and a validation error (1) - and prints a review report: a scale "
|
"`publish` reports \"Nothing to commit\" and attempts no push.",
|
||||||
"line (file count, total lines added/removed, status breakdown), only-what-applies "
|
"A rejected push gets exactly one more reconcile-and-push; never more than one.",
|
||||||
"attention notes (deletions by name, control-plane and harness-config touches, published "
|
"Mass-Update Gate: counts the files that would be committed, refuses with exit 42 at "
|
||||||
"pages, the largest single change, binaries), and every counted path grouped by area with "
|
"`--threshold` (default 10) or more, and prints a review report - a scale line (file "
|
||||||
"its status and churn, generated files split out as needing no review. The token digests "
|
"count, total lines added/removed, status breakdown), attention notes where they apply "
|
||||||
"each counted path **and its contents** plus the publish target, so a clearance carries "
|
"(deletions by name, control-plane and harness-config touches, published pages, the "
|
||||||
"neither to a different file list nor to edited contents; a wrong, invented or superseded "
|
"largest single change, binaries), and every counted path grouped by area with its "
|
||||||
"token exits 42 again with the current state. Two kinds of path are committed but never "
|
"status and churn. The gate is evaluated before anything is staged, so a refused "
|
||||||
"counted and never shown for approval: anything under `work/`, and the files `wikitool` "
|
"publish leaves the working tree untouched.",
|
||||||
"generates itself (`kb/index.md`, `kb/log.md`, `kb/provenance.md`, every `INDEX.md`) - each "
|
"Never counted and never shown for approval, but committed like everything else: "
|
||||||
"is recomputable from the tree, so approving it decides nothing, and a routine ingest "
|
"anything under `work/`, and the files `wikitool` generates itself (`kb/index.md`, "
|
||||||
"rebuilds five or six of them. The refusal line accounts for both, by reason. The gate is "
|
"`kb/log.md`, `kb/provenance.md`, every `INDEX.md`). The refusal line accounts for "
|
||||||
"evaluated *before* anything is staged, so a refused publish leaves the working tree "
|
"both, by reason.",
|
||||||
"untouched. **Publish-Remote Gate:** when this checkout carries a "
|
"The `--confirm` token covers each counted path, its contents and the publish target: "
|
||||||
"`.wikitool-remotes.json` and the resolved push URL of `--remote` is not listed in it, "
|
"a different file list or edited contents need a new clearance.",
|
||||||
"exits **42** before the reconcile step even fetches - the URL is read from "
|
"Publish-Remote Gate: when the checkout carries `.wikitool-remotes.json` and the push "
|
||||||
"`git remote get-url --push`, so a repointed remote does not pass on its name. Unlike the "
|
"URL of `--remote` is not listed in it, exits 42 before the reconcile fetches anything. "
|
||||||
"other two gates it has **no token and no flag**: the way past it is the user adding the "
|
"The URL is read with `git remote get-url --push`, so a repointed remote does not pass "
|
||||||
"URL to that file, and an agent editing it to get past a refusal is opening a gate on its "
|
"on its name. An absent file means unrestricted; a malformed one is an error, not "
|
||||||
"own initiative. Absent file means unrestricted; a malformed one is an error, not "
|
"permission.",
|
||||||
"permission. See `instructions/gates.md`. `--yes`/`-y` are gone and now fail with an "
|
"`--path` (repeatable) scopes the whole operation - gate count, staging and commit - "
|
||||||
"explicit error. `--path` (repeatable) scopes the whole operation - gate count, staging, "
|
"to that subtree.",
|
||||||
"and commit - to a subtree. **Stack-machinery note:** after a successful commit/push whose "
|
"After a successful commit or push whose changed files include `tools/`, `types/`, "
|
||||||
"changed files include `tools/`, `types/`, `instructions/`, `AGENTS.md`, or a path ending "
|
"`instructions/`, `AGENTS.md` or a path ending in `CONTRACT.md`, prints one reminder "
|
||||||
"`CONTRACT.md` - roughly the scope a stack version bump covers, deliberately a shade "
|
"line: the phase past this point (an issue-body rewrite, `docs/` staleness, a "
|
||||||
"broader than CI's version gate, which matches only a `CONTRACT.md` one segment deep - "
|
"changelog entry's accuracy) is not covered by `docs verify`, `instructions verify` or "
|
||||||
"prints one reminder line that the phase past this point (an issue-body rewrite, `docs/` "
|
"`pytest`. It is not a gate: no exit code change, nothing to clear, and silent for an "
|
||||||
"staleness, a changelog entry's accuracy) is not covered by `docs verify`, "
|
"ordinary content publish.",
|
||||||
"`instructions verify` or `pytest`. Not a gate: no exit code change, nothing to clear, "
|
),
|
||||||
"silent for an ordinary content publish",
|
failures=(
|
||||||
failures=(cli_contract.Failure(
|
cli_contract.Failure(
|
||||||
label="",
|
cause="git failed - `git add`, `git commit`, `git push`, or the reconcile's "
|
||||||
exit_1="git failed, the push target is not the checked-out branch (including a real "
|
"automatic rebase",
|
||||||
"detached HEAD - but *not* the unborn branch of a fresh `git init`, which is a normal "
|
reaction="Do not retry and do not force - report and ask the user. `publish` has "
|
||||||
"first publish), **or** `--yes`/`-y` was passed. **Exit 42, not 1**, when the "
|
"already made its one retry of a rejected push itself, where a reconcile resolved "
|
||||||
"Mass-Update Gate, the rebase-review gate (raised by the same reconcile `sync` "
|
"the rejection",
|
||||||
"performs), or the Publish-Remote Gate refuses",
|
),
|
||||||
retry="For git failures: **do not retry, do not force** - report and ask the user (the "
|
cli_contract.Failure(
|
||||||
"reconcile step already retried the push once on its own, if a rebase resolved the "
|
cause="The push target (`--branch`) is not the checked-out branch, or HEAD is "
|
||||||
"rejection). For exit 42: show the user the command's full output verbatim and stop; it "
|
"detached; the unborn branch of a fresh `git init` is not this case",
|
||||||
"names the evidence and the `--confirm <token>` or `--confirm-rebase <token>` line to "
|
reaction="Check out the branch you mean to publish, or pass `--branch <checked-out "
|
||||||
"re-run, and re-running without it exits 42 again. The Publish-Remote Gate is the "
|
"branch>`, then retry once",
|
||||||
"exception with no such line: it names the push URL that would have been written to and "
|
),
|
||||||
"the ones this checkout allows, and only the user resolves it",
|
cli_contract.Failure(
|
||||||
),),
|
cause="`--yes`/`-y` was passed - the flag does not exist and fails with an "
|
||||||
|
"explicit error",
|
||||||
|
reaction="Drop it. The Mass-Update Gate is cleared only with `--confirm <token>` "
|
||||||
|
"from the gate's own refusal output",
|
||||||
|
),
|
||||||
|
cli_contract.Failure(
|
||||||
|
cause="`.wikitool-remotes.json` is unreadable or has no usable "
|
||||||
|
"`allowed_push_urls` list",
|
||||||
|
reaction="Show the error to the user and stop - a malformed file is not "
|
||||||
|
"permission, and fixing or deleting it is theirs to do",
|
||||||
|
),
|
||||||
|
cli_contract.Failure(
|
||||||
|
cause="Mass-Update Gate: `--threshold` (default 10) or more counted files would be "
|
||||||
|
"committed, or the `--confirm` token does not match this changeset",
|
||||||
|
reaction=cli_contract.token_gate_reaction("--confirm"),
|
||||||
|
code=42,
|
||||||
|
),
|
||||||
|
cli_contract.Failure(
|
||||||
|
cause="Rebase-review gate: `<remote>/<branch>` moved and both sides changed the "
|
||||||
|
"same file; the output lists the upstream commits, the overlapping files and their "
|
||||||
|
"diff",
|
||||||
|
reaction=cli_contract.token_gate_reaction("--confirm-rebase"),
|
||||||
|
code=42,
|
||||||
|
),
|
||||||
|
cli_contract.Failure(
|
||||||
|
cause="Publish-Remote Gate: `.wikitool-remotes.json` exists and the push URL of "
|
||||||
|
"`--remote` is not listed in it, or `--remote` resolves to no push URL",
|
||||||
|
reaction="Show the user the push URL it names and the allowed ones, and stop. This "
|
||||||
|
"gate has no token and no flag: only the user resolves it, by adding the URL to "
|
||||||
|
"that file",
|
||||||
|
code=42,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
examples=(
|
||||||
|
'tools/wikitool publish --message "ingest: docker-cheatsheet"',
|
||||||
|
'tools/wikitool publish --confirm <token> --message "ingest: docker-cheatsheet" '
|
||||||
|
"# re-run after a Mass-Update exit 42, once the user approved",
|
||||||
|
'tools/wikitool publish --confirm-rebase <token> --message "ingest: docker-cheatsheet" '
|
||||||
|
"# re-run after a rebase-review exit 42, once the user approved",
|
||||||
|
),
|
||||||
|
never=(
|
||||||
|
"Never retry a failed git step unchanged, and never force (`--force`, "
|
||||||
|
"`--force-with-lease`).",
|
||||||
|
"Never pass a `--confirm` or `--confirm-rebase` token the user has not seen and "
|
||||||
|
"approved.",
|
||||||
|
"Never edit `.wikitool-remotes.json` to get past a Publish-Remote refusal - that is "
|
||||||
|
"opening a gate on your own initiative.",
|
||||||
|
),
|
||||||
|
see_also=(
|
||||||
|
"`wikitool sync` - the same reconcile on its own, without committing or pushing",
|
||||||
|
"`instructions/gates.md` - the gate procedure",
|
||||||
|
"`instructions/setup-instance.md` - the first publish of a new instance",
|
||||||
|
),
|
||||||
))
|
))
|
||||||
def publish_command(
|
def publish_command(
|
||||||
message: str = typer.Option(..., "--message", help="Commit message summary, e.g. 'ingest: docker-cheatsheet'"),
|
message: str = typer.Option(..., "--message", help="Commit message summary, e.g. 'ingest: docker-cheatsheet'"),
|
||||||
|
|||||||
@@ -235,8 +235,8 @@ def build_index(kb_dir: Path) -> str:
|
|||||||
"collections/areas are deleted in the same pass",
|
"collections/areas are deleted in the same pass",
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
label="",
|
||||||
exit_1="Rare I/O error only",
|
cause="Rare I/O error only",
|
||||||
retry="Safe to retry freely - the plan is always recomputed from the pages currently "
|
reaction="Safe to retry freely - the plan is always recomputed from the pages currently "
|
||||||
"on disk, so a re-run converges",
|
"on disk, so a re-run converges",
|
||||||
),),
|
),),
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -386,9 +386,9 @@ def check_skill_reference_paths() -> list[str]:
|
|||||||
"`SKILL.md` in it)",
|
"`SKILL.md` in it)",
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
label="",
|
||||||
exit_1="No skills found under `instructions/`, or a target directory is not a "
|
cause="No skills found under `instructions/`, or a target directory is not a "
|
||||||
"published skill (no `SKILL.md`) and `--force` was not passed",
|
"published skill (no `SKILL.md`) and `--force` was not passed",
|
||||||
retry="Check whether the flagged target holds anything worth keeping, then re-run with "
|
reaction="Check whether the flagged target holds anything worth keeping, then re-run with "
|
||||||
"`--force` if not; otherwise fix the named cause and retry",
|
"`--force` if not; otherwise fix the named cause and retry",
|
||||||
),),
|
),),
|
||||||
))
|
))
|
||||||
@@ -449,13 +449,13 @@ def sync(
|
|||||||
"*every* copy is reported as \"run sync\", not as drift - that is a clean checkout",
|
"*every* copy is reported as \"run sync\", not as drift - that is a clean checkout",
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
label="",
|
||||||
exit_1="Nothing found under `instructions/` at all, a malformed instruction or "
|
cause="Nothing found under `instructions/` at all, a malformed instruction or "
|
||||||
"`SKILL.md`, a `SKILL.md` carrying a relative markdown link, a published copy that "
|
"`SKILL.md`, a `SKILL.md` carrying a relative markdown link, a published copy that "
|
||||||
"drifted from its source, an instruction nothing references (or, for `manual: true`, "
|
"drifted from its source, an instruction nothing references (or, for `manual: true`, "
|
||||||
"one that IS linked from AGENTS.md, CLAUDE.md, or a skill and so risks running "
|
"one that IS linked from AGENTS.md, CLAUDE.md, or a skill and so risks running "
|
||||||
"implicitly), or something under `instructions/dev/` referenced from outside it and "
|
"implicitly), or something under `instructions/dev/` referenced from outside it and "
|
||||||
"outside a `dist:strip` block",
|
"outside a `dist:strip` block",
|
||||||
retry="Fix the flagged file, then re-run. For a relative link in a `SKILL.md`, rewrite "
|
reaction="Fix the flagged file, then re-run. For a relative link in a `SKILL.md`, rewrite "
|
||||||
"it as a repo-root-relative plain path instead. For drift, re-run `sync` instead of "
|
"it as a repo-root-relative plain path instead. For drift, re-run `sync` instead of "
|
||||||
"hand-editing the published copy - the source under `instructions/` always wins",
|
"hand-editing the published copy - the source under `instructions/` always wins",
|
||||||
),),
|
),),
|
||||||
|
|||||||
@@ -77,8 +77,8 @@ def inbound(pages: dict[str, Page], title: str) -> list[dict]:
|
|||||||
"come from. Read-only, exempt from the Iteration Budget Gate",
|
"come from. Read-only, exempt from the Iteration Budget Gate",
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
label="",
|
||||||
exit_1="Page not found",
|
cause="Page not found",
|
||||||
retry="Check the exact title with `search`; a wikilink target is not always the page's "
|
reaction="Check the exact title with `search`; a wikilink target is not always the page's "
|
||||||
"stem",
|
"stem",
|
||||||
),),
|
),),
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -81,8 +81,8 @@ __all__ = [
|
|||||||
"prints everything, `--json` prints the findings and writes nothing",
|
"prints everything, `--json` prints the findings and writes nothing",
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
label="",
|
||||||
exit_1="Only with `--fail-on-error`: hard findings exist",
|
cause="Only with `--fail-on-error`: hard findings exist",
|
||||||
retry="Safe to retry freely, but re-run it to re-*measure*, never to re-read: the "
|
reaction="Safe to retry freely, but re-run it to re-*measure*, never to re-read: the "
|
||||||
"printed path holds the full report. Exit 1 means \"act on the findings\", not \"the "
|
"printed path holds the full report. Exit 1 means \"act on the findings\", not \"the "
|
||||||
"tool is broken\"",
|
"tool is broken\"",
|
||||||
),),
|
),),
|
||||||
|
|||||||
@@ -67,8 +67,8 @@ def ingests_since_last_lint(entries: list[tuple[str, str, str]]) -> int:
|
|||||||
notes="Append a formatted entry to `kb/log.md`.",
|
notes="Append a formatted entry to `kb/log.md`.",
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
label="",
|
||||||
exit_1="Invalid `--op` or unreadable `--body-file`",
|
cause="Invalid `--op` or unreadable `--body-file`",
|
||||||
retry="**Not idempotent.** If the previous run's outcome is uncertain, check the tail "
|
reaction="**Not idempotent.** If the previous run's outcome is uncertain, check the tail "
|
||||||
"of `kb/log.md` before retrying",
|
"of `kb/log.md` before retrying",
|
||||||
),),
|
),),
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -170,9 +170,9 @@ def _report_offers(
|
|||||||
"Read-only and exempt from the budget gate",
|
"Read-only and exempt from the budget gate",
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
label="",
|
||||||
exit_1="`.wikitool-kb.json` is missing (content version undeclared), or `VERSION` is "
|
cause="`.wikitool-kb.json` is missing (content version undeclared), or `VERSION` is "
|
||||||
"unreadable",
|
"unreadable",
|
||||||
retry="For a missing declaration: run `migrate baseline <version>` once, then retry. "
|
reaction="For a missing declaration: run `migrate baseline <version>` once, then retry. "
|
||||||
"Safe to retry freely otherwise",
|
"Safe to retry freely otherwise",
|
||||||
),),
|
),),
|
||||||
))
|
))
|
||||||
@@ -273,9 +273,9 @@ def status_command(
|
|||||||
"an error",
|
"an error",
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
label="",
|
||||||
exit_1="Unknown version, no `.wikitool-kb.json`, nothing outstanding, or a *required* "
|
cause="Unknown version, no `.wikitool-kb.json`, nothing outstanding, or a *required* "
|
||||||
"version that is not the next link in the chain",
|
"version that is not the next link in the chain",
|
||||||
retry="**Not idempotent** for a required migration: it advances the chain. For \"not "
|
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 "
|
"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",
|
"force the order. Recording an `offered` migration *is* idempotent and safe to repeat",
|
||||||
),),
|
),),
|
||||||
@@ -391,9 +391,9 @@ def done_command(
|
|||||||
"way around it",
|
"way around it",
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
label="",
|
||||||
exit_1="Unparseable version, or a declaration already exists and `--force` was not "
|
cause="Unparseable version, or a declaration already exists and `--force` was not "
|
||||||
"passed",
|
"passed",
|
||||||
retry="Safe to re-run with the same version. If a declaration exists, it is almost "
|
reaction="Safe to re-run with the same version. If a declaration exists, it is almost "
|
||||||
"always `migrate done` that was wanted",
|
"always `migrate done` that was wanted",
|
||||||
),),
|
),),
|
||||||
))
|
))
|
||||||
@@ -550,9 +550,9 @@ def _shapes_now(wanted: set[str]) -> dict[str, corpus_diff.PageShape]:
|
|||||||
"missing. Read-only and exempt from the budget gate",
|
"missing. Read-only and exempt from the budget gate",
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
label="",
|
||||||
exit_1="Only with `--fail-on-error`: an invariant changed. Also exits 1 if `--from` is "
|
cause="Only with `--fail-on-error`: an invariant changed. Also exits 1 if `--from` is "
|
||||||
"not a revision in this repository",
|
"not a revision in this repository",
|
||||||
retry="Exit 1 from `--fail-on-error` means \"act on the findings\", not \"the tool is "
|
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 "
|
"broken\". A finding is never fixed by re-running - it names a page and what changed on "
|
||||||
"it",
|
"it",
|
||||||
),),
|
),),
|
||||||
|
|||||||
@@ -401,19 +401,19 @@ def _ensure_tracker_project(page_title: str, *, resume: bool) -> Optional[str]:
|
|||||||
failures=(
|
failures=(
|
||||||
cli_contract.Failure(
|
cli_contract.Failure(
|
||||||
label="new <type>",
|
label="new <type>",
|
||||||
exit_1="Duplicate page title, unknown type, invalid `--set` value, or a "
|
cause="Duplicate page title, unknown type, invalid `--set` value, or a "
|
||||||
"`raw_files` path that doesn't exist",
|
"`raw_files` path that doesn't exist",
|
||||||
retry="Not transient; fix the argument and retry once. Never hand-craft the page "
|
reaction="Not transient; fix the argument and retry once. Never hand-craft the page "
|
||||||
"instead",
|
"instead",
|
||||||
),
|
),
|
||||||
cli_contract.Failure(
|
cli_contract.Failure(
|
||||||
label="new project",
|
label="new project",
|
||||||
exit_1="Everything `new <type>` covers, **plus**: the name is already taken in the "
|
cause="Everything `new <type>` covers, **plus**: the name is already taken in the "
|
||||||
"tracker (case-insensitively - for `caldav` this is checked against every list in "
|
"tracker (case-insensitively - for `caldav` this is checked against every list in "
|
||||||
"the account, not only the ones counted as projects), `--resume` was passed for a "
|
"the account, not only the ones counted as projects), `--resume` was passed for a "
|
||||||
"type other than `project`, or the configured provider's access path has no write "
|
"type other than `project`, or the configured provider's access path has no write "
|
||||||
"path at all (Super Productivity's `access: \"snapshot\"`)",
|
"path at all (Super Productivity's `access: \"snapshot\"`)",
|
||||||
retry="A collision, a bad `--set`, or a read-only access path is not transient, "
|
reaction="A collision, a bad `--set`, or a read-only access path is not transient, "
|
||||||
"same as `new <type>` - the last of those points at the `access: \"api\"` instance "
|
"same as `new <type>` - the last of those points at the `access: \"api\"` instance "
|
||||||
"instead and refuses on every `--resume` retry too, since nothing about the config "
|
"instead and refuses on every `--resume` retry too, since nothing about the config "
|
||||||
"changes by asking again. **Exit 42** (`NEEDS USER CLEARANCE`, not exit 1) is its "
|
"changes by asking again. **Exit 42** (`NEEDS USER CLEARANCE`, not exit 1) is its "
|
||||||
|
|||||||
@@ -229,9 +229,9 @@ def inbound_pages(pages: dict[str, Page], title: str) -> list[str]:
|
|||||||
"the page is `Act Runner`",
|
"the page is `Act Runner`",
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
label="",
|
||||||
exit_1="Neither `--from` nor `--to` is a page, target title already taken, or "
|
cause="Neither `--from` nor `--to` is a page, target title already taken, or "
|
||||||
"`--from` equals `--to`",
|
"`--from` equals `--to`",
|
||||||
retry="Safe to retry once as-is; each page's rewrite is idempotent. Use `--dry-run` "
|
reaction="Safe to retry once as-is; each page's rewrite is idempotent. Use `--dry-run` "
|
||||||
"first to see the blast radius. Never fix up references by hand instead",
|
"first to see the blast radius. Never fix up references by hand instead",
|
||||||
),),
|
),),
|
||||||
))
|
))
|
||||||
@@ -341,9 +341,9 @@ def rename_command(
|
|||||||
"citations in place and reports them",
|
"citations in place and reports them",
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
label="",
|
||||||
exit_1="Page not found, **or** other pages still reference it and `--yes` was not "
|
cause="Page not found, **or** other pages still reference it and `--yes` was not "
|
||||||
"passed",
|
"passed",
|
||||||
retry="For \"still referenced\": show the user the inbound list, get approval, then "
|
reaction="For \"still referenced\": show the user the inbound list, get approval, then "
|
||||||
"re-run with `--yes`. Prose references it reports afterwards are an editorial fix, not "
|
"re-run with `--yes`. Prose references it reports afterwards are an editorial fix, not "
|
||||||
"a retry",
|
"a retry",
|
||||||
),),
|
),),
|
||||||
@@ -473,9 +473,9 @@ def _rmdir_if_emptied(directory: Path) -> bool:
|
|||||||
"is refused rather than silently skipped",
|
"is refused rather than silently skipped",
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
label="",
|
||||||
exit_1="Neither or both of `--page`/`--reconcile` given, the named page not found, it "
|
cause="Neither or both of `--page`/`--reconcile` given, the named page not found, it "
|
||||||
"has no `type:` to compute a placement from, or the destination already exists",
|
"has no `type:` to compute a placement from, or the destination already exists",
|
||||||
retry="Safe to retry once as-is; a page already at its computed location is reported "
|
reaction="Safe to retry once as-is; a page already at its computed location is reported "
|
||||||
"and left alone, and `--reconcile` only re-moves what is still misplaced. Use "
|
"and left alone, and `--reconcile` only re-moves what is still misplaced. Use "
|
||||||
"`--dry-run` first to see the blast radius. Never choose a directory by hand instead",
|
"`--dry-run` first to see the blast radius. Never choose a directory by hand instead",
|
||||||
),),
|
),),
|
||||||
|
|||||||
@@ -103,10 +103,10 @@ def coverage(json_out: bool = typer.Option(False, "--json", help="Print raw find
|
|||||||
"page -> its sources -> their raw files",
|
"page -> its sources -> their raw files",
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
label="",
|
||||||
exit_1="Neither or both of `--raw`/`--page` given, `--raw` names a file no source page "
|
cause="Neither or both of `--raw`/`--page` given, `--raw` names a file no source page "
|
||||||
"covers (reported as a plain finding plus exit 1, not the usual `ERROR`-prefixed "
|
"covers (reported as a plain finding plus exit 1, not the usual `ERROR`-prefixed "
|
||||||
"rejection), or `--page` names an unknown page",
|
"rejection), or `--page` names an unknown page",
|
||||||
retry="Fix the argument and retry",
|
reaction="Fix the argument and retry",
|
||||||
),),
|
),),
|
||||||
))
|
))
|
||||||
def trace(
|
def trace(
|
||||||
@@ -219,8 +219,8 @@ def build_provenance_index(kb_dir: Path, raw_dir: Path) -> str:
|
|||||||
"pages)",
|
"pages)",
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
label="",
|
||||||
exit_1="Rare I/O error only",
|
cause="Rare I/O error only",
|
||||||
retry="Safe to retry freely",
|
reaction="Safe to retry freely",
|
||||||
),),
|
),),
|
||||||
))
|
))
|
||||||
def rebuild_index(
|
def rebuild_index(
|
||||||
|
|||||||
@@ -366,7 +366,7 @@ def _replace(
|
|||||||
failures=(
|
failures=(
|
||||||
cli_contract.Failure(
|
cli_contract.Failure(
|
||||||
label="raw accept",
|
label="raw accept",
|
||||||
exit_1="A file does not exist, is not under `incoming/`, or is nested more than "
|
cause="A file does not exist, is not under `incoming/`, or is nested more than "
|
||||||
"one level below it, two files in one call share a filename, a target path already "
|
"one level below it, two files in one call share a filename, a target path already "
|
||||||
"exists, `--fidelity`/`--authority` is missing (unless `--replaces`) or names "
|
"exists, `--fidelity`/`--authority` is missing (unless `--replaces`) or names "
|
||||||
"`unknown` or a value outside the schema's enum, the target name is already "
|
"`unknown` or a value outside the schema's enum, the target name is already "
|
||||||
@@ -374,7 +374,7 @@ def _replace(
|
|||||||
"an unknown page or one with no `raw_files:` yet, an existing `raw_files:` entry is "
|
"an unknown page or one with no `raw_files:` yet, an existing `raw_files:` entry is "
|
||||||
"missing on disk, a file to be moved has more than one owning page, or `--page` "
|
"missing on disk, a file to be moved has more than one owning page, or `--page` "
|
||||||
"would overwrite an already-set `fidelity`/`authority` with a different value",
|
"would overwrite an already-set `fidelity`/`authority` with a different value",
|
||||||
retry="Fix the named argument and retry once. Safe to retry as-is once the cause is "
|
reaction="Fix the named argument and retry once. Safe to retry as-is once the cause is "
|
||||||
"fixed: a file already at its computed destination is what \"already exists\" "
|
"fixed: a file already at its computed destination is what \"already exists\" "
|
||||||
"reports, not a partial prior run to resume. A stem-occupied refusal is not fixed "
|
"reports, not a partial prior run to resume. A stem-occupied refusal is not fixed "
|
||||||
"by retrying at all - it names `--replaces` and renaming in `incoming/` as the two "
|
"by retrying at all - it names `--replaces` and renaming in `incoming/` as the two "
|
||||||
@@ -383,12 +383,12 @@ def _replace(
|
|||||||
),
|
),
|
||||||
cli_contract.Failure(
|
cli_contract.Failure(
|
||||||
label="raw accept --replaces",
|
label="raw accept --replaces",
|
||||||
exit_1="More than one incoming file, `--page` also given, the incoming file does "
|
cause="More than one incoming file, `--page` also given, the incoming file does "
|
||||||
"not exist or is not under `incoming/` (or is nested more than one level below "
|
"not exist or is not under `incoming/` (or is nested more than one level below "
|
||||||
"it), its filename differs from the target's, the target does not lie under `raw/` "
|
"it), its filename differs from the target's, the target does not lie under `raw/` "
|
||||||
"or does not exist, `--fidelity`/`--authority` names `unknown` or a value outside "
|
"or does not exist, `--fidelity`/`--authority` names `unknown` or a value outside "
|
||||||
"the schema's enum, or the target has more than one owning source page",
|
"the schema's enum, or the target has more than one owning source page",
|
||||||
retry="Fix the named argument and retry once. Every check runs before the "
|
reaction="Fix the named argument and retry once. Every check runs before the "
|
||||||
"filesystem is touched, so a refusal leaves both files exactly as they were",
|
"filesystem is touched, so a refusal leaves both files exactly as they were",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -113,12 +113,12 @@ def report_to_dict(report: ReviewReport) -> dict:
|
|||||||
"**exempt from the Iteration Budget Gate**",
|
"**exempt from the Iteration Budget Gate**",
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
label="",
|
||||||
exit_1="Either no `.wikitool-tasks.json` (or a malformed one) - not yours to fix by "
|
cause="Either no `.wikitool-tasks.json` (or a malformed one) - not yours to fix by "
|
||||||
"retrying unchanged, configure or repair it first - **or** the provider was reachable "
|
"retrying unchanged, configure or repair it first - **or** the provider was reachable "
|
||||||
"at config-parse time but a read call failed mid-run, in which case the full report "
|
"at config-parse time but a read call failed mid-run, in which case the full report "
|
||||||
"(findings plus which checks ran) is printed first and exit 1 follows, never a silent "
|
"(findings plus which checks ran) is printed first and exit 1 follows, never a silent "
|
||||||
"partial success",
|
"partial success",
|
||||||
retry="The two exit-1 causes above need different responses: a config problem needs "
|
reaction="The two exit-1 causes above need different responses: a config problem needs "
|
||||||
"editing `.wikitool-tasks.json`; an unreachable provider (e.g. the tracker app not "
|
"editing `.wikitool-tasks.json`; an unreachable provider (e.g. the tracker app not "
|
||||||
"running) needs starting it, then a plain retry - the command re-reads everything fresh "
|
"running) needs starting it, then a plain retry - the command re-reads everything fresh "
|
||||||
"each time, so nothing here is ever stale to re-fetch",
|
"each time, so nothing here is ever stale to re-fetch",
|
||||||
|
|||||||
@@ -375,8 +375,8 @@ def reset_message() -> str:
|
|||||||
"the same explicit human approval",
|
"the same explicit human approval",
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
label="",
|
||||||
exit_1="`--yes` not passed",
|
cause="`--yes` not passed",
|
||||||
retry="Get the user's approval, then re-run with `--yes`",
|
reaction="Get the user's approval, then re-run with `--yes`",
|
||||||
),),
|
),),
|
||||||
))
|
))
|
||||||
def reset_command(
|
def reset_command(
|
||||||
|
|||||||
@@ -160,9 +160,9 @@ def render_table(result: SearchResult, show_matches: bool) -> str:
|
|||||||
"Budget Gate**",
|
"Budget Gate**",
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
label="",
|
||||||
exit_1="`rg` is not installed or did not finish within 30 s, a malformed `--field` "
|
cause="`rg` is not installed or did not finish within 30 s, a malformed `--field` "
|
||||||
"predicate, an unknown field name, or an unknown `--backend`",
|
"predicate, an unknown field name, or an unknown `--backend`",
|
||||||
retry="Fix the argument and retry. A timeout is a pathological pattern or an "
|
reaction="Fix the argument and retry. A timeout is a pathological pattern or an "
|
||||||
"unresponsive corpus directory, not a slow answer - narrow the query or drop `--regex` "
|
"unresponsive corpus directory, not a slow answer - narrow the query or drop `--regex` "
|
||||||
"rather than retrying it unchanged. An unknown field name is reported with the list of "
|
"rather than retrying it unchanged. An unknown field name is reported with the list of "
|
||||||
"fields that do exist - it is never answered with an empty result, because that would "
|
"fields that do exist - it is never answered with an empty result, because that would "
|
||||||
|
|||||||
@@ -88,12 +88,12 @@ def _parse_follow_up_at(text: str) -> datetime.date:
|
|||||||
"created via its API), are ordinary exit-1 refusals instead, creating nothing",
|
"created via its API), are ordinary exit-1 refusals instead, creating nothing",
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
label="",
|
||||||
exit_1="No `.wikitool-tasks.json`, neither or both of `--project`/`--inbox` given, a "
|
cause="No `.wikitool-tasks.json`, neither or both of `--project`/`--inbox` given, a "
|
||||||
"`--follow-up-at` without `--waiting` or not `YYYY-MM-DD`, a `--project` name matching "
|
"`--follow-up-at` without `--waiting` or not `YYYY-MM-DD`, a `--project` name matching "
|
||||||
"no tracker project, `--waiting` against a provider with no way to represent it right "
|
"no tracker project, `--waiting` against a provider with no way to represent it right "
|
||||||
"now (Super Productivity: the `waiting` tag does not exist), or a read-only access path "
|
"now (Super Productivity: the `waiting` tag does not exist), or a read-only access path "
|
||||||
"(Super Productivity's `access: \"snapshot\"`)",
|
"(Super Productivity's `access: \"snapshot\"`)",
|
||||||
retry="Not transient; fix the argument, create the missing tracker project or tag "
|
reaction="Not transient; fix the argument, create the missing tracker project or tag "
|
||||||
"first, or point at an `access: \"api\"` instance, then retry once. **Never exit 42** - "
|
"first, or point at an `access: \"api\"` instance, then retry once. **Never exit 42** - "
|
||||||
"unlike `new project`, every provider offering a write path at all has a real "
|
"unlike `new project`, every provider offering a write path at all has a real "
|
||||||
"item-creation call, so there is no human-clearance step to wait on here",
|
"item-creation call, so there is no human-clearance step to wait on here",
|
||||||
@@ -201,8 +201,8 @@ def task_new_command(
|
|||||||
"(`chemenu.tasks.protocol.TaskReader.open_items`'s own docstring)",
|
"(`chemenu.tasks.protocol.TaskReader.open_items`'s own docstring)",
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
label="",
|
||||||
exit_1="No `.wikitool-tasks.json`",
|
cause="No `.wikitool-tasks.json`",
|
||||||
retry="Not transient; configure a tracker first, then retry once. A `--project` "
|
reaction="Not transient; configure a tracker first, then retry once. A `--project` "
|
||||||
"matching no tracker project is not an error here - see its Commands row",
|
"matching no tracker project is not an error here - see its Commands row",
|
||||||
),),
|
),),
|
||||||
))
|
))
|
||||||
@@ -262,9 +262,9 @@ def task_list_command(
|
|||||||
"per-item write call",
|
"per-item write call",
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
label="",
|
||||||
exit_1="No `.wikitool-tasks.json`, an `--id` matching no tracker item right now, or a "
|
cause="No `.wikitool-tasks.json`, an `--id` matching no tracker item right now, or a "
|
||||||
"read-only access path (Super Productivity's `access: \"snapshot\"`)",
|
"read-only access path (Super Productivity's `access: \"snapshot\"`)",
|
||||||
retry="Not transient; fix the id (re-run `task list` or `review` to get a current one) "
|
reaction="Not transient; fix the id (re-run `task list` or `review` to get a current one) "
|
||||||
"or point at an `access: \"api\"` instance, then retry once. **Never exit 42**, same "
|
"or point at an `access: \"api\"` instance, then retry once. **Never exit 42**, same "
|
||||||
"reasoning as `task new`",
|
"reasoning as `task new`",
|
||||||
),),
|
),),
|
||||||
|
|||||||
@@ -211,10 +211,10 @@ def _apply_remove(frontmatter: Dict[str, Any], field: str, value: Any) -> Option
|
|||||||
"explicitly.",
|
"explicitly.",
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
label="",
|
||||||
exit_1="Page not found; an invalid value for a field it writes; a field owned by "
|
cause="Page not found; an invalid value for a field it writes; a field owned by "
|
||||||
"another command (`type:`, a page-ref array) or absent from the type's schema; "
|
"another command (`type:`, a page-ref array) or absent from the type's schema; "
|
||||||
"`--add`/`--remove` on a non-array field; a `raw_files:` path that doesn't exist",
|
"`--add`/`--remove` on a non-array field; a `raw_files:` path that doesn't exist",
|
||||||
retry="Fix the argument and retry once. Safe to re-run as-is: `--set` and `--add` are "
|
reaction="Fix the argument and retry once. Safe to re-run as-is: `--set` and `--add` are "
|
||||||
"idempotent, and `--remove` of an already-absent element succeeds while reporting it",
|
"idempotent, and `--remove` of an already-absent element succeeds while reporting it",
|
||||||
),),
|
),),
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -82,8 +82,8 @@ def list_types_command(
|
|||||||
"and a navigation aid into it would be noise",
|
"and a navigation aid into it would be noise",
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
label="",
|
||||||
exit_1="Unknown type name",
|
cause="Unknown type name",
|
||||||
retry="Fix the name and retry",
|
reaction="Fix the name and retry",
|
||||||
),),
|
),),
|
||||||
))
|
))
|
||||||
def describe_type_command(
|
def describe_type_command(
|
||||||
|
|||||||
@@ -82,8 +82,8 @@ def upload_list_command(
|
|||||||
"the header was honest), submission time. What a reviewer reads before `accept`",
|
"the header was honest), submission time. What a reviewer reads before `accept`",
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
label="",
|
||||||
exit_1="Unknown or malformed submission id",
|
cause="Unknown or malformed submission id",
|
||||||
retry="Fix the id (see `upload list`) and retry",
|
reaction="Fix the id (see `upload list`) and retry",
|
||||||
),),
|
),),
|
||||||
))
|
))
|
||||||
def upload_show_command(
|
def upload_show_command(
|
||||||
@@ -150,11 +150,11 @@ def _clearance_message(manifest: dict, token: str, stale: Optional[str]) -> str:
|
|||||||
"`incoming/<filename>` already exists",
|
"`incoming/<filename>` already exists",
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
label="",
|
||||||
exit_1="Unknown or malformed submission id, the submission's file is missing from "
|
cause="Unknown or malformed submission id, the submission's file is missing from "
|
||||||
"`mcp-upload/<id>/`, or `incoming/<filename>` already exists. **Exit 42, not 1**, when "
|
"`mcp-upload/<id>/`, or `incoming/<filename>` already exists. **Exit 42, not 1**, when "
|
||||||
"`--confirm` is absent or does not match the manifest's current token - the Upload "
|
"`--confirm` is absent or does not match the manifest's current token - the Upload "
|
||||||
"Review Gate, not a validation error",
|
"Review Gate, not a validation error",
|
||||||
retry="For exit 42: show the user the full manifest and the exact `--confirm <token>` "
|
reaction="For exit 42: show the user the full manifest and the exact `--confirm <token>` "
|
||||||
"re-run line printed, and stop - the same rule as every other exit-42 gate (AGENTS.md "
|
"re-run line printed, and stop - the same rule as every other exit-42 gate (AGENTS.md "
|
||||||
"invariant 6). For the three exit-1 cases: fix the named argument and retry once; an "
|
"invariant 6). For the three exit-1 cases: fix the named argument and retry once; an "
|
||||||
"occupied `incoming/<filename>` is not fixed by retrying unchanged - rename or clear it "
|
"occupied `incoming/<filename>` is not fixed by retrying unchanged - rename or clear it "
|
||||||
@@ -194,8 +194,8 @@ def upload_accept_command(
|
|||||||
"declined. No gate - rejecting needs no clearance, only accepting does",
|
"declined. No gate - rejecting needs no clearance, only accepting does",
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
label="",
|
||||||
exit_1="Unknown or malformed submission id, or an empty `--reason`",
|
cause="Unknown or malformed submission id, or an empty `--reason`",
|
||||||
retry="Fix the argument and retry once. Not idempotent against a second call with the "
|
reaction="Fix the argument and retry once. Not idempotent against a second call with the "
|
||||||
"same id: the first call already deleted the submission, so a retry reports \"unknown "
|
"same id: the first call already deleted the submission, so a retry reports \"unknown "
|
||||||
"id\" - that is confirmation, not a failure",
|
"id\" - that is confirmation, not a failure",
|
||||||
),),
|
),),
|
||||||
|
|||||||
@@ -273,11 +273,11 @@ def _merge_success_message(
|
|||||||
"Never pushes. Not idempotent - see the tool error contract below",
|
"Never pushes. Not idempotent - see the tool error contract below",
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
label="",
|
||||||
exit_1="Dirty working tree, a merge already in progress, the remote does not resolve, "
|
cause="Dirty working tree, a merge already in progress, the remote does not resolve, "
|
||||||
"git refused to open the merge at all (unrelated histories), or a real conflict remains "
|
"git refused to open the merge at all (unrelated histories), or a real conflict remains "
|
||||||
"in `tools/`/`types/`/`instructions/` after the content stages and stack-owned paths "
|
"in `tools/`/`types/`/`instructions/` after the content stages and stack-owned paths "
|
||||||
"were restored",
|
"were restored",
|
||||||
retry="**Not idempotent, and not safe to retry unchanged.** For a dirty tree or an "
|
reaction="**Not idempotent, and not safe to retry unchanged.** For a dirty tree or an "
|
||||||
"in-progress merge: fix the named precondition and retry once. For a real conflict: "
|
"in-progress merge: fix the named precondition and retry once. For a real conflict: "
|
||||||
"**do not retry, do not force** - resolve the named paths by hand (take the upstream "
|
"**do not retry, do not force** - resolve the named paths by hand (take the upstream "
|
||||||
"side, or re-file the local change as an issue against the public repo per "
|
"side, or re-file the local change as an issue against the public repo per "
|
||||||
@@ -437,9 +437,9 @@ def _verify_success_message(stack_moved: list[str], since: str, until: str) -> s
|
|||||||
"Read-only and exempt from the Iteration Budget Gate, like `migrate verify`",
|
"Read-only and exempt from the Iteration Budget Gate, like `migrate verify`",
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
label="",
|
||||||
exit_1="A leak was found (content changed under a content stage through a path that is "
|
cause="A leak was found (content changed under a content stage through a path that is "
|
||||||
"not stack-owned), or `--since`/`--until` is not a revision in this repository",
|
"not stack-owned), or `--since`/`--until` is not a revision in this repository",
|
||||||
retry="A finding is not fixed by re-running - it names the paths that leaked. Fix the "
|
reaction="A finding is not fixed by re-running - it names the paths that leaked. Fix the "
|
||||||
"revision argument and retry for the second case",
|
"revision argument and retry for the second case",
|
||||||
),),
|
),),
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -94,8 +94,8 @@ def _describe_origin(stamp: Optional[dict]) -> str:
|
|||||||
"Iteration Budget Gate**",
|
"Iteration Budget Gate**",
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
label="",
|
||||||
exit_1="`VERSION` is missing or unparseable",
|
cause="`VERSION` is missing or unparseable",
|
||||||
retry="Fix `VERSION` and retry",
|
reaction="Fix `VERSION` and retry",
|
||||||
),),
|
),),
|
||||||
))
|
))
|
||||||
@app.command("show")
|
@app.command("show")
|
||||||
@@ -151,9 +151,9 @@ def show_command(
|
|||||||
"feed is not readable anonymously. Read-only and exempt from the budget gate",
|
"feed is not readable anonymously. Read-only and exempt from the budget gate",
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
label="",
|
||||||
exit_1="The feed could not be reached, answered non-JSON, or carried no `tag_name`. "
|
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",
|
"**Never** answers \"up to date\" for a question it could not ask",
|
||||||
retry="A network failure is transient - retry once, then report it. HTTP 401/403 names "
|
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 "
|
"`$WIKITOOL_UPDATE_TOKEN`; 404 means no release exists yet or the URL points at the "
|
||||||
"wrong repo",
|
"wrong repo",
|
||||||
),),
|
),),
|
||||||
@@ -255,14 +255,14 @@ def check_command(
|
|||||||
"and fails with the stamp's `release_url` instead. Read-only and exempt from the budget gate",
|
"and fails with the stamp's `release_url` instead. Read-only and exempt from the budget gate",
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
label="",
|
||||||
exit_1="An unparseable `--version`, an unreadable `VERSION` when `--version` is "
|
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 "
|
"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 "
|
"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 "
|
"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 "
|
"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 "
|
"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",
|
"the notes is still told where they are",
|
||||||
retry="Fix the named argument or file, then retry. A feed failure is transient - retry "
|
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",
|
"once, then read the release page the error names. Safe to retry",
|
||||||
),),
|
),),
|
||||||
))
|
))
|
||||||
@@ -436,7 +436,7 @@ def _no_entry_message(wanted: Version, has_stamp: bool, release_url: str) -> str
|
|||||||
"part was chosen correctly",
|
"part was chosen correctly",
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
label="",
|
||||||
exit_1="More or fewer than one of `--major/--minor/--patch`, an empty `--title`, an "
|
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 "
|
"unknown `--impact`, a missing `VERSION`/`CHANGES.md`, `VERSION` and the changelog's "
|
||||||
"newest entry naming different versions, an escalation to a boundary crossing without "
|
"newest entry naming different versions, an escalation to a boundary crossing without "
|
||||||
"`--breaking` or with neither a migration document nor `--no-migration`, "
|
"`--breaking` or with neither a migration document nor `--no-migration`, "
|
||||||
@@ -444,7 +444,7 @@ def _no_entry_message(wanted: Version, has_stamp: bool, release_url: str) -> str
|
|||||||
"combined with `--no-migration`, on a bump with no running candidate, with no "
|
"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 "
|
"`--no-migration` line to retract, or without a migration document already targeting "
|
||||||
"the new base",
|
"the new base",
|
||||||
retry="**Not idempotent**: a second run escalates or continues the candidate again. If "
|
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",
|
"the outcome is uncertain, read `VERSION` and the top of `CHANGES.md` before retrying",
|
||||||
),),
|
),),
|
||||||
))
|
))
|
||||||
@@ -668,10 +668,10 @@ def bump_command(
|
|||||||
"`VERSION`",
|
"`VERSION`",
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
label="",
|
||||||
exit_1="A missing `VERSION`/`CHANGES.md`, `VERSION` already a release (no running "
|
cause="A missing `VERSION`/`CHANGES.md`, `VERSION` already a release (no running "
|
||||||
"candidate), `VERSION` and the changelog's newest entry naming different versions, or "
|
"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",
|
"(from two bumps on) an entry with no summary paragraph above the changesets",
|
||||||
retry="**Not idempotent**: a second run fails outright once the suffix is gone. If the "
|
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` "
|
"outcome is uncertain, read `VERSION` before retrying - a release-shaped `VERSION` "
|
||||||
"means it already ran",
|
"means it already ran",
|
||||||
),),
|
),),
|
||||||
@@ -787,10 +787,10 @@ def release_command(
|
|||||||
"or a topmost entry with no bump list at all",
|
"or a topmost entry with no bump list at all",
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
label="",
|
||||||
exit_1="A missing `VERSION`/`CHANGES.md`, `VERSION` and the changelog's newest entry "
|
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 "
|
"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`",
|
"rendered list's range, indices given without `--impact`, or an unknown `--impact`",
|
||||||
retry="The bare listing never writes anything. A write is **not idempotent** against a "
|
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 "
|
"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 "
|
"at those positions *now*, which may no longer be the same bumps - list again before "
|
||||||
"retrying",
|
"retrying",
|
||||||
|
|||||||
@@ -150,9 +150,9 @@ Cut the tree into units. One unit does one job and becomes one source page. A un
|
|||||||
"`work/CONTRACT.md`",
|
"`work/CONTRACT.md`",
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
label="",
|
||||||
exit_1="Neither or both of `--input`/`--key` given, `--input` outside `raw/`, a "
|
cause="Neither or both of `--input`/`--key` given, `--input` outside `raw/`, a "
|
||||||
"`--key` that is empty or starts with `ingest-`, or the workshop already exists",
|
"`--key` that is empty or starts with `ingest-`, or the workshop already exists",
|
||||||
retry="A collision is not transient: resume the existing run instead, or pass "
|
reaction="A collision is not transient: resume the existing run instead, or pass "
|
||||||
"`--again` if the tree itself changed. Never create a numbered variant by hand",
|
"`--again` if the tree itself changed. Never create a numbered variant by hand",
|
||||||
),),
|
),),
|
||||||
))
|
))
|
||||||
@@ -251,8 +251,8 @@ def new_command(
|
|||||||
"from the rest of the repo - the durable conclusions must already be in `kb/`",
|
"from the rest of the repo - the durable conclusions must already be in `kb/`",
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
label="",
|
||||||
exit_1="Unknown run key, or `--yes` was not passed",
|
cause="Unknown run key, or `--yes` was not passed",
|
||||||
retry="For \"not confirmed\": check the listed files are no longer needed, confirm the "
|
reaction="For \"not confirmed\": check the listed files are no longer needed, confirm the "
|
||||||
"conclusions are in `kb/`, then re-run with `--yes`",
|
"conclusions are in `kb/`, then re-run with `--yes`",
|
||||||
),),
|
),),
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -168,8 +168,8 @@ def _check_authorised(source: Page, target: Page, label: str) -> None:
|
|||||||
"`instructions/link-taxonomy.md`",
|
"`instructions/link-taxonomy.md`",
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
label="",
|
||||||
exit_1="Page A or B not found, or a page's type declares no `related:` field",
|
cause="Page A or B not found, or a page's type declares no `related:` field",
|
||||||
retry="Safe to retry once as-is; re-running never duplicates a link. Never create the "
|
reaction="Safe to retry once as-is; re-running never duplicates a link. Never create the "
|
||||||
"missing page just to force the link through, and never hand-write a reference field "
|
"missing page just to force the link through, and never hand-write a reference field "
|
||||||
"the type does not declare",
|
"the type does not declare",
|
||||||
),),
|
),),
|
||||||
@@ -263,8 +263,8 @@ def remove_link_bullets(body: str, other_title: str) -> str:
|
|||||||
"Idempotent.",
|
"Idempotent.",
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
label="",
|
||||||
exit_1="Page A not found (B is allowed not to exist)",
|
cause="Page A not found (B is allowed not to exist)",
|
||||||
retry="Safe to retry freely; removing an absent link is a no-op",
|
reaction="Safe to retry freely; removing an absent link is a no-op",
|
||||||
),),
|
),),
|
||||||
))
|
))
|
||||||
def xref_remove(
|
def xref_remove(
|
||||||
@@ -346,9 +346,9 @@ def xref_remove(
|
|||||||
"and named in the output. Idempotent in both directions",
|
"and named in the output. Idempotent in both directions",
|
||||||
failures=(cli_contract.Failure(
|
failures=(cli_contract.Failure(
|
||||||
label="",
|
label="",
|
||||||
exit_1="Source page not found, an entity in `--entities` doesn't exist, or the source "
|
cause="Source page not found, an entity in `--entities` doesn't exist, or the source "
|
||||||
"page itself could not be written after its targets were",
|
"page itself could not be written after its targets were",
|
||||||
retry="Use `--dry-run` first; safe to retry. `sources trace --page \"<Title>\"` shows "
|
reaction="Use `--dry-run` first; safe to retry. `sources trace --page \"<Title>\"` shows "
|
||||||
"who was already linked",
|
"who was already linked",
|
||||||
),),
|
),),
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ def _fixture_record(**overrides) -> cc.CommandRecord:
|
|||||||
budget=cc.Budget.COUNTED,
|
budget=cc.Budget.COUNTED,
|
||||||
),
|
),
|
||||||
notes="Frobnicates the named widget in place.",
|
notes="Frobnicates the named widget in place.",
|
||||||
failures=(cc.Failure(label="", exit_1="Widget not found", retry="Fix the name and retry once"),),
|
failures=(cc.Failure(label="", cause="Widget not found", reaction="Fix the name and retry once"),),
|
||||||
)
|
)
|
||||||
defaults.update(overrides)
|
defaults.update(overrides)
|
||||||
return cc.CommandRecord(**defaults)
|
return cc.CommandRecord(**defaults)
|
||||||
@@ -219,3 +219,81 @@ def test_render_markdown_section_has_no_options_heading():
|
|||||||
assert "OPTIONS" not in section
|
assert "OPTIONS" not in section
|
||||||
assert "#### `frobnicate`" in section
|
assert "#### `frobnicate`" in section
|
||||||
assert rec.notes in section
|
assert rec.notes in section
|
||||||
|
|
||||||
|
|
||||||
|
def test_notes_tuple_renders_one_bullet_per_entry():
|
||||||
|
rec = _fixture_record(notes=("Frobnicates in place.", "Leaves the widget's name alone."))
|
||||||
|
text = cc.render_text(rec)
|
||||||
|
assert "NOTES\n - Frobnicates in place.\n - Leaves the widget's name alone." in text
|
||||||
|
section = cc.render_markdown_section(rec)
|
||||||
|
assert "**NOTES**\n\n- Frobnicates in place.\n- Leaves the widget's name alone." in section
|
||||||
|
|
||||||
|
|
||||||
|
def test_one_exit_status_and_on_failure_line_per_cause():
|
||||||
|
rec = _fixture_record(
|
||||||
|
properties=cc.Properties(
|
||||||
|
effect=cc.Effect.WRITE,
|
||||||
|
idempotent=cc.Idempotent.NO,
|
||||||
|
atomic="No",
|
||||||
|
budget=cc.Budget.COUNTED,
|
||||||
|
gates=("mass-update",),
|
||||||
|
),
|
||||||
|
failures=(
|
||||||
|
cc.Failure(cause="Mass-Update Gate: too many files", reaction="Show the output and stop", code=42),
|
||||||
|
cc.Failure(cause="Widget not found", reaction="Fix the name and retry once"),
|
||||||
|
cc.Failure(cause="Widget locked", reaction="Report to the user"),
|
||||||
|
cc.Failure(cause="No remote configured - reported and skipped", reaction="", code=0),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
status = cc.render_exit_status_lines(rec)
|
||||||
|
# Sorted by code, stable within a code; the explicit 42 cause replaces
|
||||||
|
# the generic gate line.
|
||||||
|
assert status == [
|
||||||
|
"0 success",
|
||||||
|
"0 No remote configured - reported and skipped",
|
||||||
|
"1 Widget not found",
|
||||||
|
"1 Widget locked",
|
||||||
|
"42 Mass-Update Gate: too many files",
|
||||||
|
]
|
||||||
|
# A cause without a reaction has no ON FAILURE line.
|
||||||
|
assert cc.render_on_failure_lines(rec) == [
|
||||||
|
"Widget not found -> Fix the name and retry once",
|
||||||
|
"Widget locked -> Report to the user",
|
||||||
|
"Mass-Update Gate: too many files -> Show the output and stop",
|
||||||
|
]
|
||||||
|
assert cc._exit_codes(rec) == [0, 1, 42]
|
||||||
|
|
||||||
|
|
||||||
|
def test_generic_gate_line_stays_without_an_explicit_42_cause():
|
||||||
|
rec = _fixture_record(
|
||||||
|
properties=cc.Properties(
|
||||||
|
effect=cc.Effect.WRITE,
|
||||||
|
idempotent=cc.Idempotent.NO,
|
||||||
|
atomic="No",
|
||||||
|
budget=cc.Budget.COUNTED,
|
||||||
|
gates=("mass-update",),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assert cc.render_exit_status_lines(rec)[-1].startswith("42 needs clearance - mass-update")
|
||||||
|
|
||||||
|
|
||||||
|
def test_label_prefixes_both_lines():
|
||||||
|
rec = _fixture_record(failures=(cc.Failure(cause="Bad name", reaction="Fix it", label="frobnicate --b"),))
|
||||||
|
assert "1 frobnicate --b: Bad name" in cc.render_exit_status_lines(rec)
|
||||||
|
assert cc.render_on_failure_lines(rec) == ["frobnicate --b: Bad name -> Fix it"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_on_failure_omitted_when_no_cause_has_a_reaction():
|
||||||
|
rec = _fixture_record(failures=(cc.Failure(cause="Nothing to do", reaction="", code=0),))
|
||||||
|
assert "ON FAILURE" not in cc.render_text(rec)
|
||||||
|
assert "ON FAILURE" not in cc.render_markdown_section(rec)
|
||||||
|
|
||||||
|
|
||||||
|
def test_exit_42_cause_without_a_gate_is_refused():
|
||||||
|
with pytest.raises(ValueError, match="no gate"):
|
||||||
|
_fixture_record(failures=(cc.Failure(cause="Gate", reaction="Stop", code=42),))
|
||||||
|
|
||||||
|
|
||||||
|
def test_failure_code_outside_0_1_42_is_refused():
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
cc.Failure(cause="Crash", reaction="Report", code=2)
|
||||||
@@ -53,7 +53,7 @@ def _fixture_record(path: str) -> cli_contract.CommandRecord:
|
|||||||
budget=cli_contract.Budget.COUNTED,
|
budget=cli_contract.Budget.COUNTED,
|
||||||
),
|
),
|
||||||
notes="Does a thing, mechanically.",
|
notes="Does a thing, mechanically.",
|
||||||
failures=(cli_contract.Failure(label="", exit_1="It broke", retry="Fix and retry"),),
|
failures=(cli_contract.Failure(label="", cause="It broke", reaction="Fix and retry"),),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user