diff --git a/CHANGES.md b/CHANGES.md index 13971a3..e488fbd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -59,7 +59,7 @@ concern - readable here, never shipped as something to parse. --- -## 7.1.0-beta.13 - 2026-09-26 - Command records, Provenance group: examples, exit lines per cause +## 7.1.0-beta.14 - 2026-09-26 - Command records, Raw material and uploads group: one line per cause, examples, prohibitions **Author:** Torben Nehmer @@ -82,6 +82,7 @@ concern - readable here, never shipped as something to parse. - Command records, Links and citations group: one line per cause, examples, prohibitions - Command records, Finding and checking group: one line per cause, examples, prohibitions - Command records, Provenance group: examples, exit lines per cause +- Command records, Raw material and uploads group: one line per cause, examples, prohibitions ### CalDAV task-tracker provider (Nextcloud Tasks, iOS Reminders); review reports unknown-value findings instead of skipping them @@ -269,6 +270,15 @@ only. `sources trace` separates an argument error from the uncovered-file findin 1 on, since the second is a result to act on rather than an argument to fix; `sources rebuild-index` names `kb/provenance.md` as generated in NEVER. +### Command records, Raw material and uploads group: one line per cause, examples, prohibitions + +`raw accept` and the four `upload` commands rewritten the same way; text only. `raw accept`'s +two paragraph-long variant notes became one line each, with the behaviour in NOTES bullets and +its two exit-1 lists split into seven causes. The name-occupied refusal now carries, in its own +reaction and in NEVER, what `raw/CONTRACT.md` already asks of an agent: show the message and +wait, since only the user can tell a new edition from a second source. `upload accept` states its +gate's shape itself instead of pointing at the Mass-Update Gate's. + --- ## 7.0.0 - 2026-09-22 - Task-Tracker-Anbindung: Vorhaben als Seitenart, Verpflichtungsschicht, Weekly Review als Read-Time-Join diff --git a/VERSION b/VERSION index 0b07751..29f89c6 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -7.1.0-beta.13 +7.1.0-beta.14 diff --git a/tools/CONTRACT.md b/tools/CONTRACT.md index a254d08..b435153 100644 --- a/tools/CONTRACT.md +++ b/tools/CONTRACT.md @@ -1357,8 +1357,8 @@ Promote one or more files from `incoming/` into `raw/`. **SYNOPSIS** -- `wikitool raw accept [ ...] --fidelity --authority [--page ""] [--dry-run]` - Promote one or more files from `incoming/` into `raw/<YYYY>/<MM>/`, computed from the accept date rather than chosen by hand (`raw/CONTRACT.md` "Getting a file in"): a subdirectory under `incoming/` is tolerated and ignored, not inspected - `raw/` no longer addresses by type. One file promoted alone lands with no directory of its own; several files in one call nest under `raw/<YYYY>/<MM>/<stem>/`, named after the first file's stem. `--fidelity`/`--authority` are required here (see `types describe source`; `unknown` is refused, backfill-only) - the one moment both are knowable. `--page "<Title>"` additionally extends that existing source page's `raw_files:` in the same call and writes both capture fields onto it (refused if it already carries a different value - a capture field is fixed once); if that raises the page past one file, its already-promoted file is folded into a bundle at *its own* parent directory, not today's shard, so a bundle never mixes an old and a new capture date, after checking it has no other owner (`provenance.duplicate_raw_file_owners`). The set of names occupied anywhere under `raw/` - file stems and bundle directory names alike, old type directories and date shards together - must stay unique: a promote whose target name already belongs to something this call does not itself own is refused, naming both `--replaces` and renaming-in-`incoming/` without recommending either -- `wikitool raw accept <file> --replaces <raw-path> [--fidelity <v>] [--authority <v>] [--dry-run]` - The one sanctioned way past that uniqueness rule, and the one sanctioned way to correct an already-set capture field: overwrites `<raw-path>` in place with the single incoming file (same filename required; there is no type directory left to match), leaving every page's `raw_files:` untouched and writing no `kb/` page - the previous edition survives only in `git log --follow <raw-path>`. `--fidelity`/`--authority` are optional here, and passing one overwrites the owning page's already-set value - the one path fill-once does not block, because a corrected capture is a new edition of the source, not an edit of the page describing it. Refuses if the target has more than one owning source page; if it has none, replaces anyway and says so. Cannot be combined with `--page` or with more than one incoming file - a replacement is one file for one file. Prints the source page (if any) and its citing pages, so their update lands in the same commit as the replacement +- `wikitool raw accept <file> [<file> ...] --fidelity <v> --authority <v> [--page "<Title>"] [--dry-run]` - Promote one or more files from `incoming/` into today's `raw/<YYYY>/<MM>/` shard +- `wikitool raw accept <file> --replaces <raw-path> [--fidelity <v>] [--authority <v>] [--dry-run]` - Overwrite one existing raw file in place with a new edition **PROPERTIES** @@ -1368,20 +1368,58 @@ Promote one or more files from `incoming/` into `raw/`. - budget: counted - network: no +**EXAMPLES** + +- `tools/wikitool raw accept incoming/docker-cheatsheet.md --fidelity verbatim --authority reporting` +- `tools/wikitool raw accept incoming/part-2.md --fidelity verbatim --authority reporting --page "Source - Docker Cheatsheet"` +- `tools/wikitool raw accept incoming/cluster.md --replaces raw/documents/cluster.md` + **EXIT STATUS** - 0 success -- 1 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 -- 1 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 +- 1 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; or a target path already exists +- 1 raw accept: `--fidelity`/`--authority` is missing, or names `unknown` or a value outside the schema's enum +- 1 raw accept: The target name is already occupied anywhere under `raw/` by something the call does not own +- 1 raw accept: `--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 +- 1 raw accept --replaces: More than one incoming file, or `--page` also given +- 1 raw accept --replaces: 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, or the target does not lie under `raw/` or does not exist +- 1 raw accept --replaces: `--fidelity`/`--authority` names `unknown` or a value outside the schema's enum, or the target has more than one owning source page **ON FAILURE** -- 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: 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 +- 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; or a target path already exists -> Fix the named argument and retry once +- raw accept: `--fidelity`/`--authority` is missing, or names `unknown` or a value outside the schema's enum -> Pass both with a valid value, then retry once +- raw accept: The target name is already occupied anywhere under `raw/` by something the call does not own -> Not fixed by retrying: the refusal names `--replaces` (same source, new edition) and renaming in `incoming/` (a separate source) as the two routes, and neither is the tool's to pick. Show the message to the user and wait +- raw accept: `--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; a different capture value on an existing page is a new edition - `--replaces` +- raw accept --replaces: More than one incoming file, or `--page` also given -> A replacement is one file for one file - fix the call and retry once +- raw accept --replaces: 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, or the target does not lie under `raw/` or does not exist -> Fix the named argument and retry once - every check runs before the filesystem is touched, so both files are exactly as they were +- raw accept --replaces: `--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; nothing was touched + +**NEVER** + +- Never choose the destination under `raw/` by hand, and never move a file into `raw/` yourself. +- Never pick between `--replaces` and renaming on your own initiative after a name-occupied refusal - the user tells the two intents apart. **NOTES** -See `raw/CONTRACT.md` "Getting a file in: incoming/". +- Promotes files from `incoming/` into `raw/<YYYY>/<MM>/`, computed from the accept date rather than chosen by hand. A subdirectory under `incoming/` is tolerated and ignored, not inspected - `raw/` does not address by type. +- One file promoted alone lands with no directory of its own; several files in one call nest under `raw/<YYYY>/<MM>/<stem>/`, named after the first file's stem. +- `--fidelity`/`--authority` are required on a plain accept (`types describe source` lists the values); `unknown` is refused - it is backfill-only. +- `--page "<Title>"` additionally extends that existing source page's `raw_files:` in the same call and writes both capture fields onto it - refused if it already carries a different value, since a capture field is fixed once. +- If `--page` raises the page past one file, its already-promoted file is folded into a bundle at *its own* parent directory, not today's shard, so a bundle never mixes an old and a new capture date - after checking that file has no other owner. +- Every name occupied anywhere under `raw/` - file stems and bundle directory names alike, old type directories and date shards together - stays unique: a promote whose target name already belongs to something this call does not itself own is refused, naming `--replaces` and renaming in `incoming/` as the two routes, without recommending either. +- `--replaces <raw-path>` overwrites that file in place with the single incoming file (same filename required), leaves every page's `raw_files:` untouched and writes no `kb/` page; the previous edition survives only in `git log --follow <raw-path>`. +- With `--replaces`, `--fidelity`/`--authority` are optional, and passing one overwrites the owning page's already-set value - the one path the fixed-once rule does not block. +- `--replaces` refuses a target with more than one owning source page; with none, it replaces anyway and says so. It cannot be combined with `--page` or with more than one incoming file. +- `--replaces` prints the source page (if any) and its citing pages, so their update lands in the same commit as the replacement. +- A file already at its computed destination is what "already exists" reports, not a partial prior run to resume - safe to retry as-is once a cause is fixed. +- `--dry-run` reports the moves without making them. + +**SEE ALSO** + +- `raw/CONTRACT.md` "Getting a file in: incoming/" - the rules and why +- `wikitool types describe source` - the capture field values +- `wikitool new source` - the source page for a promoted file #### `upload list` @@ -1399,13 +1437,26 @@ List every MCP submission currently waiting in the quarantine (`mcp-upload/`). - budget: counted - network: no +**EXAMPLES** + +- `tools/wikitool upload list` +- `tools/wikitool upload list --json` + **EXIT STATUS** - 0 success **NOTES** -Oldest id first - id, filename, size, submitter. Only ever non-empty when `.wikitool-upload.json` opts a checkout into the MCP server's `submit` tool (see the MCP read server design note). Never fails - a submission directory with a corrupt manifest is silently skipped. Safe to retry freely. +- Lists every submission waiting in `mcp-upload/`, oldest id first: id, filename, size, submitter. +- Only ever non-empty when `.wikitool-upload.json` opts the checkout into the MCP server's `submit` tool. +- A submission directory with a corrupt manifest is silently skipped. +- Never fails; read-only and safe to retry freely. + +**SEE ALSO** + +- `wikitool upload show` - one submission's manifest +- `INSTALL-MCP.md` § "Schritt 7: Optional - den `submit`-Pfad freischalten" #### `upload show` @@ -1423,6 +1474,10 @@ Print one submission's manifest in full. - budget: counted - network: no +**EXAMPLES** + +- `tools/wikitool upload show <id>` + **EXIT STATUS** - 0 success @@ -1434,7 +1489,14 @@ Print one submission's manifest in full. **NOTES** -Filename, size, sha256, submitter, submitter source (the header name, not a claim the header was honest), submission time. What a reviewer reads before `accept` +- Prints one submission's manifest in full: filename, size, sha256, submitter, submitter source (the header name, not a claim the header was honest), submission time. +- What a reviewer reads before `upload accept`. +- Read-only. + +**SEE ALSO** + +- `wikitool upload accept` - promotes it after review +- `wikitool upload reject` - declines it #### `upload accept` @@ -1453,19 +1515,40 @@ Filename, size, sha256, submitter, submitter source (the header name, not a clai - network: no - gates: upload-review +**EXAMPLES** + +- `tools/wikitool upload accept <id>` +- `tools/wikitool upload accept <id> --confirm <token> # re-run after exit 42, once the user approved the manifest` + **EXIT STATUS** - 0 success -- 1 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 -- 42 needs clearance - upload-review (see AGENTS.md § Gates) +- 1 Unknown or malformed submission id, or the submission's file is missing from `mcp-upload/<id>/` +- 1 `incoming/<filename>` already exists +- 42 Upload Review Gate: `--confirm` is absent or does not match the manifest's current token **ON FAILURE** -- 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 +- Unknown or malformed submission id, or the submission's file is missing from `mcp-upload/<id>/` -> Fix the named argument and retry once +- `incoming/<filename>` already exists -> Not fixed by retrying unchanged - rename or clear it first +- Upload Review Gate: `--confirm` is absent or does not match the manifest's current token -> 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 + +**NEVER** + +- Never pass a `--confirm` token the user has not seen and approved. **NOTES** -Promote a submission's file from `mcp-upload/<id>/` into `incoming/`, delete the quarantine directory, and append an `accepted` event to `mcp-upload/ledger.jsonl`. Without a matching `--confirm`, exits **42** and prints the manifest in full plus the exact re-run line - the same shape as the Mass-Update Gate's clearance, one submission at a time. The token digests id/filename/size/sha256/submitter, so an edited or superseded manifest invalidates it. Refuses (without the gate - these are ordinary validation errors) when `incoming/<filename>` already exists +- Promotes a submission's file from `mcp-upload/<id>/` into `incoming/`, deletes the quarantine directory, and appends an `accepted` event to `mcp-upload/ledger.jsonl`. +- Upload Review Gate: without a matching `--confirm`, exits 42 and prints the manifest in full plus the exact re-run line - one submission at a time. The gate check runs before anything is moved. +- The token digests id, filename, size, sha256 and submitter, so an edited or superseded manifest invalidates it. +- An occupied `incoming/<filename>` is an ordinary validation error, not the gate. + +**SEE ALSO** + +- `wikitool upload show` - the manifest to review +- `wikitool raw accept` - the next step for the file in `incoming/` +- `instructions/gates.md` - the gate procedure #### `upload reject` @@ -1483,6 +1566,10 @@ Delete a submission's material, keeping only its ledger trail. - budget: counted - network: no +**EXAMPLES** + +- `tools/wikitool upload reject <id> --reason "duplicate of raw/articles/llm-wiki.md"` + **EXIT STATUS** - 0 success @@ -1490,11 +1577,18 @@ Delete a submission's material, keeping only its ledger trail. **ON 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 +- Unknown or malformed submission id, or an empty `--reason` -> Fix the argument and retry once. After a first successful call, "unknown id" is confirmation, not a failure **NOTES** -An append-only `rejected` event naming the reason and the sha256 of what was declined. No gate - rejecting needs no clearance, only accepting does +- Appends an append-only `rejected` event naming the reason and the sha256 of what was declined, then deletes the submission's material. +- The ledger write happens first, so an interruption still leaves the reason on record. +- No gate - rejecting needs no clearance, only accepting does. +- Not idempotent: a second call with the same id reports "unknown id", which confirms the first call worked. + +**SEE ALSO** + +- `wikitool upload show` - the manifest to review ### Git diff --git a/tools/chemenu/commands/raw_cmd.py b/tools/chemenu/commands/raw_cmd.py index 998c019..873b30e 100644 --- a/tools/chemenu/commands/raw_cmd.py +++ b/tools/chemenu/commands/raw_cmd.py @@ -316,42 +316,13 @@ def _replace( cli_contract.Variant( usage='raw accept <file> [<file> ...] --fidelity <v> --authority <v> ' '[--page "<Title>"] [--dry-run]', - notes="Promote one or more files from `incoming/` into `raw/<YYYY>/<MM>/`, " - "computed from the accept date rather than chosen by hand " - "(`raw/CONTRACT.md` \"Getting a file in\"): a subdirectory under `incoming/` is " - "tolerated and ignored, not inspected - `raw/` no longer addresses by type. One " - "file promoted alone lands with no directory of its own; several files in one call " - "nest under `raw/<YYYY>/<MM>/<stem>/`, named after the first file's stem. " - "`--fidelity`/`--authority` are required here (see `types describe source`; " - "`unknown` is refused, backfill-only) - the one moment both are knowable. " - '`--page "<Title>"` additionally extends that existing source page\'s ' - "`raw_files:` in the same call and writes both capture fields onto it (refused if " - "it already carries a different value - a capture field is fixed once); if that " - "raises the page past one file, its already-promoted file is folded into a bundle " - "at *its own* parent directory, not today's shard, so a bundle never mixes an old " - "and a new capture date, after checking it has no other owner " - "(`provenance.duplicate_raw_file_owners`). The set of names occupied anywhere " - "under `raw/` - file stems and bundle directory names alike, old type directories " - "and date shards together - must stay unique: a promote whose target name already " - "belongs to something this call does not itself own is refused, naming both " - "`--replaces` and renaming-in-`incoming/` without recommending either", + notes="Promote one or more files from `incoming/` into today's `raw/<YYYY>/<MM>/` " + "shard", ), cli_contract.Variant( usage='raw accept <file> --replaces <raw-path> [--fidelity <v>] [--authority <v>] ' "[--dry-run]", - notes="The one sanctioned way past that uniqueness rule, and the one sanctioned " - "way to correct an already-set capture field: overwrites `<raw-path>` in place with " - "the single incoming file (same filename required; there is no type directory left " - "to match), leaving every page's `raw_files:` untouched and writing no `kb/` page - " - "the previous edition survives only in `git log --follow <raw-path>`. " - "`--fidelity`/`--authority` are optional here, and passing one overwrites the " - "owning page's already-set value - the one path fill-once does not block, because " - "a corrected capture is a new edition of the source, not an edit of the page " - "describing it. Refuses if the target has more than one owning source page; if it " - "has none, replaces anyway and says so. Cannot be combined with `--page` or with " - "more than one incoming file - a replacement is one file for one file. Prints the " - "source page (if any) and its citing pages, so their update lands in the same " - "commit as the replacement", + notes="Overwrite one existing raw file in place with a new edition", ), ), properties=cli_contract.Properties( @@ -362,35 +333,108 @@ def _replace( "`--fidelity`/`--authority` was given) one page write", budget=cli_contract.Budget.COUNTED, ), - notes="See `raw/CONTRACT.md` \"Getting a file in: incoming/\".", + notes=( + "Promotes files from `incoming/` into `raw/<YYYY>/<MM>/`, computed from the accept " + "date rather than chosen by hand. A subdirectory under `incoming/` is tolerated and " + "ignored, not inspected - `raw/` does not address by type.", + "One file promoted alone lands with no directory of its own; several files in one call " + "nest under `raw/<YYYY>/<MM>/<stem>/`, named after the first file's stem.", + "`--fidelity`/`--authority` are required on a plain accept (`types describe source` " + "lists the values); `unknown` is refused - it is backfill-only.", + "`--page \"<Title>\"` additionally extends that existing source page's `raw_files:` in " + "the same call and writes both capture fields onto it - refused if it already carries a " + "different value, since a capture field is fixed once.", + "If `--page` raises the page past one file, its already-promoted file is folded into a " + "bundle at *its own* parent directory, not today's shard, so a bundle never mixes an " + "old and a new capture date - after checking that file has no other owner.", + "Every name occupied anywhere under `raw/` - file stems and bundle directory names " + "alike, old type directories and date shards together - stays unique: a promote whose " + "target name already belongs to something this call does not itself own is refused, " + "naming `--replaces` and renaming in `incoming/` as the two routes, without " + "recommending either.", + "`--replaces <raw-path>` overwrites that file in place with the single incoming file " + "(same filename required), leaves every page's `raw_files:` untouched and writes no " + "`kb/` page; the previous edition survives only in `git log --follow <raw-path>`.", + "With `--replaces`, `--fidelity`/`--authority` are optional, and passing one overwrites " + "the owning page's already-set value - the one path the fixed-once rule does not " + "block.", + "`--replaces` refuses a target with more than one owning source page; with none, it " + "replaces anyway and says so. It cannot be combined with `--page` or with more than one " + "incoming file.", + "`--replaces` prints the source page (if any) and its citing pages, so their update " + "lands in the same commit as the replacement.", + "A file already at its computed destination is what \"already exists\" reports, not a " + "partial prior run to resume - safe to retry as-is once a cause is fixed.", + "`--dry-run` reports the moves without making them.", + ), failures=( cli_contract.Failure( label="raw accept", - 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 " - "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", - 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\" " - "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", + 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; or a target path already " + "exists", + reaction="Fix the named argument and retry once", + ), + cli_contract.Failure( + label="raw accept", + cause="`--fidelity`/`--authority` is missing, or names `unknown` or a value outside " + "the schema's enum", + reaction="Pass both with a valid value, then retry once", + ), + cli_contract.Failure( + label="raw accept", + cause="The target name is already occupied anywhere under `raw/` by something the " + "call does not own", + reaction="Not fixed by retrying: the refusal names `--replaces` (same source, new " + "edition) and renaming in `incoming/` (a separate source) as the two routes, and " + "neither is the tool's to pick. Show the message to the user and wait", + ), + cli_contract.Failure( + label="raw accept", + cause="`--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", + reaction="Fix the named argument and retry once; a different capture value on an " + "existing page is a new edition - `--replaces`", ), cli_contract.Failure( label="raw accept --replaces", - 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 " - "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", - 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", + cause="More than one incoming file, or `--page` also given", + reaction="A replacement is one file for one file - fix the call and retry once", ), + cli_contract.Failure( + label="raw accept --replaces", + cause="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, or the " + "target does not lie under `raw/` or does not exist", + reaction="Fix the named argument and retry once - every check runs before the " + "filesystem is touched, so both files are exactly as they were", + ), + cli_contract.Failure( + label="raw accept --replaces", + cause="`--fidelity`/`--authority` names `unknown` or a value outside the schema's " + "enum, or the target has more than one owning source page", + reaction="Fix the named argument and retry once; nothing was touched", + ), + ), + examples=( + "tools/wikitool raw accept incoming/docker-cheatsheet.md --fidelity verbatim " + "--authority reporting", + 'tools/wikitool raw accept incoming/part-2.md --fidelity verbatim --authority reporting ' + '--page "Source - Docker Cheatsheet"', + "tools/wikitool raw accept incoming/cluster.md --replaces raw/documents/cluster.md", + ), + never=( + "Never choose the destination under `raw/` by hand, and never move a file into `raw/` " + "yourself.", + "Never pick between `--replaces` and renaming on your own initiative after a " + "name-occupied refusal - the user tells the two intents apart.", + ), + see_also=( + "`raw/CONTRACT.md` \"Getting a file in: incoming/\" - the rules and why", + "`wikitool types describe source` - the capture field values", + "`wikitool new source` - the source page for a promoted file", ), )) @app.command("accept") diff --git a/tools/chemenu/commands/upload_cmd.py b/tools/chemenu/commands/upload_cmd.py index b1ffc62..faea117 100644 --- a/tools/chemenu/commands/upload_cmd.py +++ b/tools/chemenu/commands/upload_cmd.py @@ -43,11 +43,23 @@ def _call(fn, *args, **kwargs): atomic="Read-only", budget=cli_contract.Budget.COUNTED, ), - notes="Oldest id first - id, filename, size, submitter. Only ever non-empty when " - "`.wikitool-upload.json` opts a checkout into the MCP server's `submit` tool (see the MCP " - "read server design note). Never fails - a submission directory with a corrupt manifest is " - "silently skipped. Safe to retry freely.", + notes=( + "Lists every submission waiting in `mcp-upload/`, oldest id first: id, filename, size, " + "submitter.", + "Only ever non-empty when `.wikitool-upload.json` opts the checkout into the MCP " + "server's `submit` tool.", + "A submission directory with a corrupt manifest is silently skipped.", + "Never fails; read-only and safe to retry freely.", + ), failures=(), + examples=( + "tools/wikitool upload list", + "tools/wikitool upload list --json", + ), + see_also=( + "`wikitool upload show` - one submission's manifest", + "`INSTALL-MCP.md` § \"Schritt 7: Optional - den `submit`-Pfad freischalten\"", + ), )) def upload_list_command( json_out: bool = typer.Option(False, "--json", help="Print every waiting submission as JSON"), @@ -78,13 +90,24 @@ def upload_list_command( atomic="Read-only", budget=cli_contract.Budget.COUNTED, ), - notes="Filename, size, sha256, submitter, submitter source (the header name, not a claim " - "the header was honest), submission time. What a reviewer reads before `accept`", + notes=( + "Prints one submission's manifest in full: filename, size, sha256, submitter, " + "submitter source (the header name, not a claim the header was honest), submission " + "time.", + "What a reviewer reads before `upload accept`.", + "Read-only.", + ), failures=(cli_contract.Failure( - label="", cause="Unknown or malformed submission id", reaction="Fix the id (see `upload list`) and retry", ),), + examples=( + "tools/wikitool upload show <id>", + ), + see_also=( + "`wikitool upload accept` - promotes it after review", + "`wikitool upload reject` - declines it", + ), )) def upload_show_command( submission_id: str = typer.Argument(..., help="A submission id from `upload list`"), @@ -141,25 +164,46 @@ def _clearance_message(manifest: dict, token: str, stale: Optional[str]) -> str: budget=cli_contract.Budget.COUNTED, gates=("upload-review",), ), - notes="Promote a submission's file from `mcp-upload/<id>/` into `incoming/`, delete the " - "quarantine directory, and append an `accepted` event to `mcp-upload/ledger.jsonl`. Without " - "a matching `--confirm`, exits **42** and prints the manifest in full plus the exact re-run " - "line - the same shape as the Mass-Update Gate's clearance, one submission at a time. The " - "token digests id/filename/size/sha256/submitter, so an edited or superseded manifest " - "invalidates it. Refuses (without the gate - these are ordinary validation errors) when " - "`incoming/<filename>` already exists", - failures=(cli_contract.Failure( - label="", - 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 " - "`--confirm` is absent or does not match the manifest's current token - the Upload " - "Review Gate, not a validation error", - 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 " - "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=( + "Promotes a submission's file from `mcp-upload/<id>/` into `incoming/`, deletes the " + "quarantine directory, and appends an `accepted` event to `mcp-upload/ledger.jsonl`.", + "Upload Review Gate: without a matching `--confirm`, exits 42 and prints the manifest " + "in full plus the exact re-run line - one submission at a time. The gate check runs " + "before anything is moved.", + "The token digests id, filename, size, sha256 and submitter, so an edited or " + "superseded manifest invalidates it.", + "An occupied `incoming/<filename>` is an ordinary validation error, not the gate.", + ), + failures=( + cli_contract.Failure( + cause="Unknown or malformed submission id, or the submission's file is missing from " + "`mcp-upload/<id>/`", + reaction="Fix the named argument and retry once", + ), + cli_contract.Failure( + cause="`incoming/<filename>` already exists", + reaction="Not fixed by retrying unchanged - rename or clear it first", + ), + cli_contract.Failure( + cause="Upload Review Gate: `--confirm` is absent or does not match the manifest's " + "current token", + reaction=cli_contract.token_gate_reaction("--confirm"), + code=42, + ), + ), + examples=( + "tools/wikitool upload accept <id>", + "tools/wikitool upload accept <id> --confirm <token> # re-run after exit 42, once " + "the user approved the manifest", + ), + never=( + "Never pass a `--confirm` token the user has not seen and approved.", + ), + see_also=( + "`wikitool upload show` - the manifest to review", + "`wikitool raw accept` - the next step for the file in `incoming/`", + "`instructions/gates.md` - the gate procedure", + ), )) def upload_accept_command( submission_id: str = typer.Argument(..., help="A submission id from `upload list`"), @@ -190,15 +234,25 @@ def upload_accept_command( "first, so an interruption still leaves the reason on record", budget=cli_contract.Budget.COUNTED, ), - notes="An append-only `rejected` event naming the reason and the sha256 of what was " - "declined. No gate - rejecting needs no clearance, only accepting does", + notes=( + "Appends an append-only `rejected` event naming the reason and the sha256 of what was " + "declined, then deletes the submission's material.", + "The ledger write happens first, so an interruption still leaves the reason on record.", + "No gate - rejecting needs no clearance, only accepting does.", + "Not idempotent: a second call with the same id reports \"unknown id\", which " + "confirms the first call worked.", + ), failures=(cli_contract.Failure( - label="", cause="Unknown or malformed submission id, or an empty `--reason`", - 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 " - "id\" - that is confirmation, not a failure", + reaction="Fix the argument and retry once. After a first successful call, \"unknown " + "id\" is confirmation, not a failure", ),), + examples=( + 'tools/wikitool upload reject <id> --reason "duplicate of raw/articles/llm-wiki.md"', + ), + see_also=( + "`wikitool upload show` - the manifest to review", + ), )) def upload_reject_command( submission_id: str = typer.Argument(..., help="A submission id from `upload list`"),