tools: command records, Pages group - one line per cause, examples, prohibitions (#142)
Files changed: - CHANGES.md - VERSION - tools/CONTRACT.md - tools/chemenu/commands/new_page.py - tools/chemenu/commands/page_ops.py - tools/chemenu/commands/task_cmd.py - tools/chemenu/commands/touch.py
This commit is contained in:
1 parent
9d6ca6b193
commit
d0a740acc9
7 files changed
+685
-218
No files matched your search
@@ -331,57 +331,32 @@ def _ensure_tracker_project(page_title: str, *, resume: bool) -> Optional[str]:
|
||||
synopsis=(
|
||||
cli_contract.Variant(
|
||||
usage='new <type-name> --name "<Name>" [--type <path>] [--set field=value ...]',
|
||||
notes="Scaffold a page of any type. The type-spec drives fields, directory "
|
||||
"(`base_dir`/`layout`), title prefix, and template - a schema `default:` is "
|
||||
"materialized only for a field the schema also lists in `required:` (an optional "
|
||||
"field's default is a reader-side assumption, not a scaffold-time value) - `--set` "
|
||||
"is repeatable, and comma-separated values fill array fields. An element that itself "
|
||||
"contains a comma is written `\\,`, or passed as its own repeated `--set` for that "
|
||||
"field - repeating an array field appends. See `types list`/`types describe`.",
|
||||
notes="Any type; its type-spec decides fields, directory, title prefix and template.",
|
||||
),
|
||||
cli_contract.Variant(
|
||||
usage='new entity --name "<Name>" --set entity_type=<t> [--set tags=a,b] '
|
||||
"[--set related=X,Y] [--set sources=\"Source - Z\"] "
|
||||
"[--set provenance=sourced|general|mixed]",
|
||||
notes="Scaffold `kb/entities/<subdir>/<Name>.md`",
|
||||
notes="Writes `kb/entities/<subdir>/<Name>.md`",
|
||||
),
|
||||
cli_contract.Variant(
|
||||
usage='new concept --name "<Name>" --set concept_type=<t> ...',
|
||||
notes="Scaffold `kb/concepts/<Name>.md`",
|
||||
notes="Writes `kb/concepts/<Name>.md`",
|
||||
),
|
||||
cli_contract.Variant(
|
||||
usage='new source --name "<Name>" --set raw_files=raw/notes/x.md,raw/notes/y.md '
|
||||
"[--set source_url=<URL>] [--set entities=A,B] [--set concepts=C,D]",
|
||||
notes="Scaffold `kb/sources/Source - <Name>.md` (prefix added automatically) with a "
|
||||
"`raw_files:` list (rejects paths that don't exist)",
|
||||
notes="Writes `kb/sources/Source - <Name>.md` (prefix added automatically) with a "
|
||||
"`raw_files:` list; rejects paths that don't exist",
|
||||
),
|
||||
cli_contract.Variant(
|
||||
usage='new comparison --name "X vs Y" --set entities=X,Y',
|
||||
notes="Scaffold `kb/comparisons/X vs Y.md`",
|
||||
notes="Writes `kb/comparisons/X vs Y.md`",
|
||||
),
|
||||
cli_contract.Variant(
|
||||
usage='new project --name "<Name>" --set responsibility=<bereich> [--resume]',
|
||||
notes="Scaffold `kb/gtd/<bereich>/<Name>.md` **and**, if `.wikitool-tasks.json` "
|
||||
"configures a task tracker, a same-named tracker project - one name, one identity. "
|
||||
"Tracker before page: the tracker side is settled first, so a failure past that "
|
||||
"point leaves a tracker project with no page - a state `review`'s check 3 already "
|
||||
"reports - never a page with no tracker project. No tracker configured is a "
|
||||
"legitimate, explicitly announced state (page only). A name already taken "
|
||||
"(case-insensitively) in `kb/` or the tracker is refused outright, naming where it "
|
||||
"was found, and creates nothing. A provider whose *configured access path* has no "
|
||||
"write path (Super Productivity's `access: \"snapshot\"` - the tracker is read-only "
|
||||
"from there by construction) refuses **entirely**, exit **1**, naming the "
|
||||
"`access: \"api\"` instance to use instead - neither the tracker project nor the "
|
||||
"page is created, and `--resume` behaves the same. A provider that could write but "
|
||||
"has no project-creation endpoint of its own (Super Productivity's `access: \"api\"` "
|
||||
"- `GET /projects` exists, `POST /projects` does not) raises "
|
||||
"`chemenu.errors.HumanInterventionRequired`; the command shows its instructions and "
|
||||
"exits **42** (`needs_clearance()`, same posture as the four named gates, without "
|
||||
"being a fifth one - see that class's docstring), creating nothing. `--resume` is "
|
||||
"how a later run tells the command a human has done what that message asked: it "
|
||||
"re-verifies via the read path (`find_project`) before continuing to page creation, "
|
||||
"rather than trusting the claim, and repeats the same 42 if the tracker still "
|
||||
"doesn't have it. `--resume` on any other type is refused",
|
||||
notes="Writes `kb/gtd/<bereich>/<Name>.md` and, with a task tracker configured, a "
|
||||
"same-named tracker project",
|
||||
),
|
||||
),
|
||||
properties=cli_contract.Properties(
|
||||
@@ -395,36 +370,109 @@ def _ensure_tracker_project(page_title: str, *, resume: bool) -> Optional[str]:
|
||||
budget=cli_contract.Budget.COUNTED,
|
||||
gates=("human-intervention-required (`new project` only)",),
|
||||
),
|
||||
notes="`new`/`xref`/`log append` only produce structurally-correct frontmatter and body "
|
||||
"skeletons/edits - the prose (Description, Summary, judgment calls about relationships) is "
|
||||
"still written by the LLM afterwards.",
|
||||
notes=(
|
||||
"The type-spec drives everything: fields, directory (`base_dir`/`layout`), title "
|
||||
"prefix, and template. `types list`/`types describe` show what a type requires.",
|
||||
"A schema `default:` is materialized only for a field the schema also lists in "
|
||||
"`required:`.",
|
||||
"`--set` is repeatable, and comma-separated values fill array fields. An element that "
|
||||
"itself contains a comma is written `\\,`, or passed as its own repeated `--set` for "
|
||||
"that field - repeating an array field appends.",
|
||||
"A capture field the type-spec requires (a source's `fidelity`/`authority`) must be "
|
||||
"passed with `--set`; `new` never guesses it and refuses `unknown` for it.",
|
||||
"Produces structurally correct frontmatter and a body skeleton only - the prose "
|
||||
"(Description, Summary, judgment calls about relationships) is written afterwards.",
|
||||
"`new project`: with `.wikitool-tasks.json` configuring a task tracker, also makes sure "
|
||||
"a same-named tracker project exists - one name, one identity. No tracker configured is "
|
||||
"a legitimate, explicitly announced state: page only.",
|
||||
"`new project`: tracker before page. The tracker side is settled first, so a failure "
|
||||
"past that point leaves a tracker project with no page - a state `review`'s check 3 "
|
||||
"reports - never a page with no tracker project.",
|
||||
"`new project`: a name already taken, case-insensitively, in `kb/` or the tracker is "
|
||||
"refused outright, naming where it was found, and creates nothing. For `caldav` the "
|
||||
"tracker check covers every list in the account, not only the ones counted as "
|
||||
"projects.",
|
||||
"`new project`: a provider whose configured access path has no write path (Super "
|
||||
"Productivity's `access: \"snapshot\"`) refuses entirely with exit 1, naming the "
|
||||
"`access: \"api\"` instance to use instead - neither the tracker project nor the page "
|
||||
"is created, and `--resume` behaves the same.",
|
||||
"`new project`: a provider that could write but has no project-creation call of its own "
|
||||
"(Super Productivity's `access: \"api\"` - `GET /projects` exists, `POST /projects` "
|
||||
"does not) prints instructions for a human and exits 42, creating nothing. That is not "
|
||||
"one of the named gates, but the same exit code and the same handling. `caldav` never "
|
||||
"does this: `MKCALENDAR` creates the list, so a valid, non-colliding name always "
|
||||
"creates it.",
|
||||
"`--resume` is how a later run tells the command a human has done what that message "
|
||||
"asked: it re-verifies through the tracker's read path before continuing to page "
|
||||
"creation, rather than trusting the claim, and exits 42 again if the tracker still does "
|
||||
"not have the project. `--resume` on any other type is refused.",
|
||||
),
|
||||
failures=(
|
||||
cli_contract.Failure(
|
||||
label="new <type>",
|
||||
cause="Duplicate page title, unknown type, invalid `--set` value, or a "
|
||||
"`raw_files` path that doesn't exist",
|
||||
reaction="Not transient; fix the argument and retry once. Never hand-craft the page "
|
||||
"instead",
|
||||
cause="A page with this title already exists, the type is unknown, or a `--set` "
|
||||
"value is invalid",
|
||||
reaction="Not transient - fix the argument and retry once",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="A `raw_files` path does not exist",
|
||||
reaction="Not transient - fix the path and retry once",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="A capture field the type-spec requires is missing, or set to `unknown`",
|
||||
reaction="Pass it explicitly (e.g. `--set fidelity=verbatim --set "
|
||||
"authority=reporting`), then retry once",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="`--resume` with a type other than `project`",
|
||||
reaction="Drop `--resume` and retry once",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
label="new project",
|
||||
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 "
|
||||
"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\"`)",
|
||||
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 "
|
||||
"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",
|
||||
cause="The name is already taken in the tracker (case-insensitively; for `caldav` "
|
||||
"against every list in the account)",
|
||||
reaction="Not transient - choose another name. If an earlier run of this exact "
|
||||
"command asked a human to create the project and they did, re-run with `--resume`",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
label="new project",
|
||||
cause="The configured access path has no write path (Super Productivity's "
|
||||
"`access: \"snapshot\"`)",
|
||||
reaction="Not transient - point at the `access: \"api\"` instance the error names. "
|
||||
"A `--resume` retry refuses the same way, since nothing about the config changes by "
|
||||
"asking again",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
label="new project",
|
||||
cause="The page write failed after the tracker project was confirmed to exist",
|
||||
reaction="Fix the write error, then re-run with `--resume` - a plain re-run is "
|
||||
"refused as a tracker collision",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
label="new project",
|
||||
cause="The provider cannot create the project itself (Super Productivity's "
|
||||
"`access: \"api\"`); the output says what a human has to create",
|
||||
reaction="Show the user the command's full output verbatim and stop. Once they have "
|
||||
"created the project, re-run the same command with `--resume`; it re-verifies and "
|
||||
"exits 42 again, unchanged, if the tracker still does not have it",
|
||||
code=42,
|
||||
),
|
||||
),
|
||||
examples=(
|
||||
'tools/wikitool new entity --name "Docker" --set entity_type=tool --set tags=containers',
|
||||
'tools/wikitool new source --name "Docker Cheatsheet" '
|
||||
"--set raw_files=raw/2026/09/docker-cheatsheet.md --set fidelity=verbatim "
|
||||
"--set authority=reporting",
|
||||
'tools/wikitool new project --name "Homelab migration" --set responsibility=infrastruktur '
|
||||
"--resume # re-run after exit 42, once the user created the tracker project",
|
||||
),
|
||||
never=(
|
||||
"Never hand-craft the page, or its frontmatter, instead.",
|
||||
),
|
||||
see_also=(
|
||||
"`wikitool types describe <type>` - what a type requires and where it lands",
|
||||
"`wikitool touch` - changes a page's own frontmatter afterwards",
|
||||
"`wikitool task new` - a tracker item without a page",
|
||||
"`docs/knowledge-and-commitment.md` - why a project is a page and a tracker project",
|
||||
),
|
||||
))
|
||||
def new_page_command(
|
||||
|
||||
@@ -220,20 +220,51 @@ def inbound_pages(pages: dict[str, Page], title: str) -> list[str]:
|
||||
atomic="No - one write per referencing page, then the file move",
|
||||
budget=cli_contract.Budget.COUNTED,
|
||||
),
|
||||
notes="Rename a page and repoint every reference to it: body `[[wikilinks]]` (aliases and "
|
||||
"anchors preserved), a `[^cite-id]` whose id was derived from the old title (refreshed to "
|
||||
"match the new one, both in its Footnotes definition and every reference to it), the page's "
|
||||
"own H1, and every page-ref frontmatter array declared by the type's `page_ref_fields:`. If "
|
||||
"`--from` is *not* a page but is referenced, it instead repoints those references onto the "
|
||||
"existing `--to` page and moves nothing - the fix for a reference spelled `act_runner` when "
|
||||
"the page is `Act Runner`",
|
||||
failures=(cli_contract.Failure(
|
||||
label="",
|
||||
cause="Neither `--from` nor `--to` is a page, target title already taken, or "
|
||||
"`--from` equals `--to`",
|
||||
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",
|
||||
),),
|
||||
notes=(
|
||||
"Renames a page and repoints every reference to it: body `[[wikilinks]]` (aliases and "
|
||||
"anchors preserved), a `[^cite-id]` whose id was derived from the old title (refreshed "
|
||||
"to match the new one, both in its Footnotes definition and every reference to it), the "
|
||||
"page's own H1, and every page-ref frontmatter array declared by the type's "
|
||||
"`page_ref_fields:`.",
|
||||
"If `--from` is *not* a page but is referenced, it instead repoints those references "
|
||||
"onto the existing `--to` page and moves nothing - the fix for a reference spelled "
|
||||
"`act_runner` when the page is `Act Runner`.",
|
||||
"Each page's rewrite is idempotent, so a re-run as-is is safe. If a write fails "
|
||||
"midway, nothing is renamed on disk and the error lists what was updated.",
|
||||
"`--dry-run` lists every page it would change; run it first to see the blast radius.",
|
||||
),
|
||||
failures=(
|
||||
cli_contract.Failure(
|
||||
cause="`--from` equals `--to`",
|
||||
reaction="Fix the arguments and retry once",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="Neither `--from` nor `--to` is a page",
|
||||
reaction="Create the page first with `wikitool new`, or drop the reference with "
|
||||
"`wikitool xref remove`",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="The `--to` title is already taken",
|
||||
reaction="Choose another title and retry once",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="A page write failed partway; nothing was renamed on disk",
|
||||
reaction="Check `git status`, resolve the write failure (permissions/disk), then "
|
||||
"re-run the full command - safe, since each page's rewrite is idempotent",
|
||||
),
|
||||
),
|
||||
examples=(
|
||||
'tools/wikitool rename --from "act_runner" --to "Act Runner" --dry-run',
|
||||
'tools/wikitool rename --from "Docker Engine" --to "Docker"',
|
||||
),
|
||||
never=(
|
||||
"Never fix up references by hand instead.",
|
||||
),
|
||||
see_also=(
|
||||
"`instructions/page-lifecycle.md` - renaming, moving and deleting a page",
|
||||
"`wikitool move` - changes a page's directory, not its title",
|
||||
"`wikitool rm` - deletes a page",
|
||||
),
|
||||
))
|
||||
def rename_command(
|
||||
old: str = typer.Option(..., "--from", help="Current page title, exactly as it appears"),
|
||||
@@ -336,17 +367,42 @@ def rename_command(
|
||||
atomic="No - one write per referencing page, then the delete",
|
||||
budget=cli_contract.Budget.COUNTED,
|
||||
),
|
||||
notes="Refuses without `--yes` while other pages still reference it. Strips ref-array "
|
||||
"entries and bare `- [[Title]]` / `- **label:** [[Title]]` bullets; leaves prose and inline "
|
||||
"citations in place and reports them",
|
||||
failures=(cli_contract.Failure(
|
||||
label="",
|
||||
cause="Page not found, **or** other pages still reference it and `--yes` was not "
|
||||
"passed",
|
||||
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 "
|
||||
"a retry",
|
||||
),),
|
||||
notes=(
|
||||
"Deletes a page and de-links it from the rest of the wiki: strips page-ref array "
|
||||
"entries and bare `- [[Title]]` / `- **label:** [[Title]]` bullets pointing at it.",
|
||||
"Leaves prose references and inline citations in place and reports them afterwards; "
|
||||
"those are an editorial fix, not a reason to retry.",
|
||||
"Refuses without `--yes` while other pages still reference it, listing them.",
|
||||
"If a de-link write fails midway, the page is not deleted, so nothing is orphaned and "
|
||||
"a re-run is safe.",
|
||||
"`--dry-run` lists what would change, without writing.",
|
||||
"Not idempotent: once the page is gone, a second run finds nothing to delete.",
|
||||
),
|
||||
failures=(
|
||||
cli_contract.Failure(
|
||||
cause="Page not found",
|
||||
reaction="Fix the title and retry once",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="Other pages still reference it and `--yes` was not passed",
|
||||
reaction="Show the user the inbound list, get approval, then re-run with `--yes`",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="A de-link write failed partway; the page was not deleted",
|
||||
reaction="Check `git status`, resolve the write failure, then re-run",
|
||||
),
|
||||
),
|
||||
examples=(
|
||||
'tools/wikitool rm --page "Old Draft" --dry-run',
|
||||
'tools/wikitool rm --page "Old Draft" --yes # after the user approved the inbound list',
|
||||
),
|
||||
never=(
|
||||
"Never pass `--yes` before the user has seen the inbound list and approved it.",
|
||||
),
|
||||
see_also=(
|
||||
"`instructions/page-lifecycle.md` - when a page is deleted rather than renamed",
|
||||
"`wikitool rename` - repoints references instead of removing them",
|
||||
),
|
||||
))
|
||||
def rm_command(
|
||||
page_title: str = typer.Option(..., "--page", help="Exact title of the page to delete"),
|
||||
@@ -460,25 +516,55 @@ def _rmdir_if_emptied(directory: Path) -> bool:
|
||||
"each idempotent",
|
||||
budget=cli_contract.Budget.COUNTED,
|
||||
),
|
||||
notes="Move a page to the directory its type-spec computes for its current frontmatter "
|
||||
"(`base_dir` + `layout` - the same rule `new` places a page by, via "
|
||||
"`TypeResolver.compute_target_dir`), never a hand-chosen destination - there is no "
|
||||
"`--to <dir>`. `--reconcile` applies it corpus-wide: every misplaced page moves in one "
|
||||
"call, and a second run reports nothing left to do (`lint`'s `Misplaced Pages` finding is "
|
||||
"the advisory that this fixes, and its `Nested Pages` finding the hard one - see `lint`). "
|
||||
"Neither mode touches a body or a frontmatter field, and the page's title (its only "
|
||||
"identity in the wiki) never changes - only the file moves. A directory a move empties is "
|
||||
"removed along with it, so a page that was nested below its area leaves no leftover "
|
||||
"directory behind. A destination already occupied (a pre-existing duplicate-stem collision) "
|
||||
"is refused rather than silently skipped",
|
||||
failures=(cli_contract.Failure(
|
||||
label="",
|
||||
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",
|
||||
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 "
|
||||
"`--dry-run` first to see the blast radius. Never choose a directory by hand instead",
|
||||
),),
|
||||
notes=(
|
||||
"Moves a page to the directory its type-spec computes for its current frontmatter "
|
||||
"(`base_dir` + `layout`, the same rule `new` places a page by) - never to a hand-chosen "
|
||||
"destination; there is no `--to <dir>`.",
|
||||
"`--reconcile` applies it corpus-wide: every misplaced page moves in one call, and a "
|
||||
"second run reports nothing left to do. It fixes `lint`'s `Misplaced Pages` (advisory) "
|
||||
"and `Nested Pages` (hard) findings.",
|
||||
"Neither mode touches a body or a frontmatter field, and the page's title - its only "
|
||||
"identity in the wiki - never changes; only the file moves.",
|
||||
"A directory a move empties is removed with it, so a page nested below its area leaves "
|
||||
"no leftover directory behind.",
|
||||
"A page already at its computed location is reported and left alone, and `--reconcile` "
|
||||
"only re-moves what is still misplaced, so a re-run is safe.",
|
||||
"`--dry-run` (with `--reconcile`) lists the moves; run it first to see the blast "
|
||||
"radius.",
|
||||
"Run `wikitool index rebuild` afterwards.",
|
||||
),
|
||||
failures=(
|
||||
cli_contract.Failure(
|
||||
cause="Neither or both of `--page`/`--reconcile` given",
|
||||
reaction="Fix the arguments and retry once",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="The named page is not found, or has no `type:` to compute a placement from",
|
||||
reaction="Fix the title, or give the page its `type:`, then retry once",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="The destination already exists (a pre-existing duplicate-stem collision) - "
|
||||
"refused rather than silently skipped",
|
||||
reaction="Resolve the collision, then retry",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="`--reconcile` failed partway",
|
||||
reaction="Safe to retry as-is - `--reconcile` only re-moves what is still misplaced",
|
||||
),
|
||||
),
|
||||
examples=(
|
||||
'tools/wikitool move --page "Docker"',
|
||||
"tools/wikitool move --reconcile --dry-run",
|
||||
"tools/wikitool move --reconcile",
|
||||
),
|
||||
never=(
|
||||
"Never choose a directory by hand instead.",
|
||||
),
|
||||
see_also=(
|
||||
"`wikitool lint` - reports Misplaced and Nested Pages",
|
||||
"`wikitool index rebuild` - run after moving",
|
||||
"`instructions/page-lifecycle.md` - moving, renaming and deleting a page",
|
||||
),
|
||||
))
|
||||
def move_command(
|
||||
page_title: Optional[str] = typer.Option(
|
||||
|
||||
@@ -17,6 +17,15 @@ transaction, because nothing here shares state with the page-creation path
|
||||
the way `new project`'s own tracker-then-page order does within a single
|
||||
command.
|
||||
|
||||
Neither write ever exits 42, unlike `new project`: every provider that
|
||||
offers a write path at all has a real per-item call (Super Productivity's
|
||||
`POST /tasks`, where `POST /projects` does not exist), so there is no
|
||||
human-clearance step to wait on - a missing project or tag is an ordinary
|
||||
refusal. `task close` takes the provider's own id, never a title: an item's
|
||||
tracker-side identity is opaque, while a project's *name* is the one coupling
|
||||
`kb/` and the tracker share. `--notes` is stored verbatim and never parsed -
|
||||
the same posture a WAITING item's own title has for the person named in it.
|
||||
|
||||
This module owns only the CLI shape - parsing, the `--project`/`--inbox`
|
||||
exclusivity (#132 D4), the `--follow-up-at` date, and `task list`/`task
|
||||
close`'s rendering. The writes themselves are
|
||||
@@ -66,38 +75,68 @@ def _parse_follow_up_at(text: str) -> datetime.date:
|
||||
budget=cli_contract.Budget.COUNTED,
|
||||
network=cli_contract.Network.YES,
|
||||
),
|
||||
notes="The second creation command alongside `new project`, and the last one their split "
|
||||
"needed - see `docs/knowledge-and-commitment.md`. Exactly one of `--project` (an existing "
|
||||
"tracker project, matched case-insensitively - never created and never searched or "
|
||||
"guessed) or `--inbox` (the tracker's own inbox, a deliberate exit with a cost: an item "
|
||||
"filed there never appears in `review`, since every one of its checks is reached through a "
|
||||
"project name and the inbox has none) is required; an omitted `--project` refuses rather "
|
||||
"than silently falling into the inbox. `--waiting` sets the WAITING status the review's own "
|
||||
"waiting-overdue check reads; `--follow-up-at` is refused without `--waiting`, since it is "
|
||||
"never a due date on its own. `--notes` carries a freetext backref (e.g. to the kb/ source "
|
||||
"page this item came from), stored verbatim, never parsed - the same posture a `WAITING` "
|
||||
"item's own title already has for the person named in it. No `.wikitool-tasks.json` fails "
|
||||
"immediately with the same \"no tracker configured\" message as `review`. A provider whose "
|
||||
"configured access path has no write path (Super Productivity's `access: \"snapshot\"`) "
|
||||
"refuses **entirely**, exit **1**, naming the `access: \"api\"` instance to use instead - "
|
||||
"same posture as `new project`. Unlike `new project`, **never exits 42**: every provider "
|
||||
"offering a write path at all has a real item-creation call (Super Productivity's "
|
||||
"`POST /tasks`, where `POST /projects` does not exist) - a named `--project` that does not "
|
||||
"match any tracker project, or `--waiting` against a provider that cannot represent it "
|
||||
"right now (Super Productivity: the `waiting` tag does not exist yet, and tags cannot be "
|
||||
"created via its API), are ordinary exit-1 refusals instead, creating nothing",
|
||||
failures=(cli_contract.Failure(
|
||||
label="",
|
||||
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 "
|
||||
"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\"`)",
|
||||
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** - "
|
||||
"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=(
|
||||
"Creates one open item in the configured task tracker; never touches `kb/`.",
|
||||
"Exactly one of `--project` or `--inbox` is required; an omitted `--project` refuses "
|
||||
"rather than silently falling into the inbox.",
|
||||
"`--project` names an existing tracker project, matched case-insensitively - never "
|
||||
"created, and never searched or guessed.",
|
||||
"`--inbox` files into the tracker's own inbox. An item filed there never appears in "
|
||||
"`review`, since every one of its checks reaches items through a project name.",
|
||||
"`--waiting` sets the WAITING status `review`'s waiting-overdue check reads. "
|
||||
"`--follow-up-at` is refused without `--waiting` - it is never a due date on its own.",
|
||||
"`--notes` carries a freetext backref (e.g. to the `kb/` source page the item came "
|
||||
"from), stored verbatim, never parsed.",
|
||||
"No `.wikitool-tasks.json` fails immediately with a \"no tracker configured\" message.",
|
||||
"A provider whose configured access path has no write path (Super Productivity's "
|
||||
"`access: \"snapshot\"`) refuses entirely with exit 1, naming the `access: \"api\"` "
|
||||
"instance to use instead.",
|
||||
"**Never exits 42.** A `--project` matching no tracker project, or `--waiting` against "
|
||||
"a provider that cannot represent it right now (Super Productivity: the `waiting` tag "
|
||||
"does not exist yet, and its API cannot create tags), are ordinary exit-1 refusals "
|
||||
"that create nothing.",
|
||||
"Not idempotent: every successful run creates another item.",
|
||||
),
|
||||
failures=(
|
||||
cli_contract.Failure(
|
||||
cause="No `.wikitool-tasks.json` - no tracker configured",
|
||||
reaction="Not transient - configure a tracker first",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="Neither or both of `--project`/`--inbox`, or a `--follow-up-at` without "
|
||||
"`--waiting` or not `YYYY-MM-DD`",
|
||||
reaction="Fix the argument and retry once",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="A `--project` name matching no tracker project",
|
||||
reaction="Create the tracker project first, or fix the name, then retry once",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="`--waiting` against a provider with no way to represent it right now (Super "
|
||||
"Productivity: the `waiting` tag does not exist)",
|
||||
reaction="Create the tag in the tracker first, then retry once",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="A read-only access path (Super Productivity's `access: \"snapshot\"`)",
|
||||
reaction="Point at the `access: \"api\"` instance the error names, then retry once",
|
||||
),
|
||||
),
|
||||
examples=(
|
||||
'tools/wikitool task new --title "Renew the TLS certificate" --project "Homelab migration"',
|
||||
'tools/wikitool task new --title "Quote from the electrician" --project "Homelab migration" '
|
||||
"--waiting --follow-up-at 2026-10-05",
|
||||
'tools/wikitool task new --title "Read the qmd README" --inbox '
|
||||
'--notes "from [[Source - qmd - GitHub Repository]]"',
|
||||
),
|
||||
never=(
|
||||
"Never fall back to `--inbox`, or to another project, when `--project` does not match.",
|
||||
),
|
||||
see_also=(
|
||||
"`wikitool task list` - a project's open items and their ids",
|
||||
"`wikitool task close` - marks an item done",
|
||||
"`wikitool new project` - a project page and its tracker project",
|
||||
"`docs/knowledge-and-commitment.md` - why commitments live in the tracker, not in `kb/`",
|
||||
),
|
||||
))
|
||||
def task_new_command(
|
||||
title: str = typer.Option(..., "--title", help="The item's title. Stored verbatim, never parsed."),
|
||||
@@ -192,19 +231,35 @@ def task_new_command(
|
||||
budget=cli_contract.Budget.COUNTED,
|
||||
network=cli_contract.Network.YES,
|
||||
),
|
||||
notes="Read-only; the id source `task close` and the review's own "
|
||||
"`waiting_overdue`/`someday_stale` findings need, without first running `wikitool review`. "
|
||||
"Works on either access mode a provider offers, unlike the write commands below. No "
|
||||
"`.wikitool-tasks.json` fails with the same \"no tracker configured\" message as "
|
||||
"`review`/`task new`; a `--project` matching no tracker project prints \"No open items\", "
|
||||
"since `TaskReader.open_items` does not distinguish \"empty\" from \"unknown\" "
|
||||
"(`chemenu.tasks.protocol.TaskReader.open_items`'s own docstring)",
|
||||
failures=(cli_contract.Failure(
|
||||
label="",
|
||||
cause="No `.wikitool-tasks.json`",
|
||||
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",
|
||||
),),
|
||||
notes=(
|
||||
"Lists one tracker project's open items: id, title, and whether each carries the "
|
||||
"WAITING status.",
|
||||
"The id source `task close` and `review`'s `waiting_overdue`/`someday_stale` findings "
|
||||
"need, without running `review` first.",
|
||||
"Works on every access path a provider offers, read-only ones included.",
|
||||
"A `--project` matching no tracker project prints \"No open items\": the tracker read "
|
||||
"does not distinguish an empty project from an unknown one.",
|
||||
"Read-only.",
|
||||
),
|
||||
failures=(
|
||||
cli_contract.Failure(
|
||||
cause="A `--project` matching no tracker project - prints \"No open items\", not an "
|
||||
"error",
|
||||
reaction="",
|
||||
code=0,
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="No `.wikitool-tasks.json` - no tracker configured",
|
||||
reaction="Not transient - configure a tracker first, then retry once",
|
||||
),
|
||||
),
|
||||
examples=(
|
||||
'tools/wikitool task list --project "Homelab migration"',
|
||||
),
|
||||
see_also=(
|
||||
"`wikitool task close` - closes an item by the id listed here",
|
||||
"`wikitool review` - the findings that name items",
|
||||
),
|
||||
))
|
||||
def task_list_command(
|
||||
project: str = typer.Option(
|
||||
@@ -251,23 +306,41 @@ def task_list_command(
|
||||
budget=cli_contract.Budget.COUNTED,
|
||||
network=cli_contract.Network.YES,
|
||||
),
|
||||
notes="`<item-id>` is the provider's own id, from `task list` or a `review` finding, never "
|
||||
"a title - the tracker-side identity is opaque, unlike the project name that is `kb/`'s and "
|
||||
"the tracker's only shared coupling. The only closing write this stack makes: no \"move a "
|
||||
"reminder\", no \"remove an item\". No `.wikitool-tasks.json` fails with the same \"no "
|
||||
"tracker configured\" message as `task new`. A provider whose configured access path has no "
|
||||
"write path (Super Productivity's `access: \"snapshot\"`) refuses **entirely**, exit **1**, "
|
||||
"naming the `access: \"api\"` instance to use instead - same posture as `task new`. Never "
|
||||
"exits 42, same reasoning as `task new`: every provider offering a write path has a real "
|
||||
"per-item write call",
|
||||
failures=(cli_contract.Failure(
|
||||
label="",
|
||||
cause="No `.wikitool-tasks.json`, an `--id` matching no tracker item right now, or a "
|
||||
"read-only access path (Super Productivity's `access: \"snapshot\"`)",
|
||||
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 "
|
||||
"reasoning as `task new`",
|
||||
),),
|
||||
notes=(
|
||||
"Marks one tracker item done. It never deletes or moves an item - the only closing "
|
||||
"write this stack makes.",
|
||||
"`--id` is the provider's own item id, from `task list` or a `review` finding - never a "
|
||||
"title.",
|
||||
"No `.wikitool-tasks.json` fails with a \"no tracker configured\" message.",
|
||||
"A provider whose configured access path has no write path (Super Productivity's "
|
||||
"`access: \"snapshot\"`) refuses entirely with exit 1, naming the `access: \"api\"` "
|
||||
"instance to use instead.",
|
||||
"**Never exits 42.**",
|
||||
),
|
||||
failures=(
|
||||
cli_contract.Failure(
|
||||
cause="No `.wikitool-tasks.json` - no tracker configured",
|
||||
reaction="Not transient - configure a tracker first",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="An `--id` matching no tracker item right now",
|
||||
reaction="Get a current id from `task list` or `review`, then retry once",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="A read-only access path (Super Productivity's `access: \"snapshot\"`)",
|
||||
reaction="Point at the `access: \"api\"` instance the error names, then retry once",
|
||||
),
|
||||
),
|
||||
examples=(
|
||||
"tools/wikitool task close --id <item-id>",
|
||||
),
|
||||
never=(
|
||||
"Never pass a title as `--id`.",
|
||||
),
|
||||
see_also=(
|
||||
"`wikitool task list` - where the id comes from",
|
||||
"`wikitool review` - findings that carry item ids",
|
||||
),
|
||||
))
|
||||
def task_close_command(
|
||||
item_id: str = typer.Option(
|
||||
|
||||
@@ -197,26 +197,61 @@ def _apply_remove(frontmatter: Dict[str, Any], field: str, value: Any) -> Option
|
||||
atomic="Yes - single file write, and every refusal happens before it",
|
||||
budget=cli_contract.Budget.COUNTED,
|
||||
),
|
||||
notes="Update a page's own frontmatter: bump `modified:` and optionally rewrite any field "
|
||||
"its type declares. `--summary`/`--provenance` are shorthands; `--set` reaches every other "
|
||||
"field and **replaces** its value, while `--add`/`--remove` change single elements of an "
|
||||
"array field (removing an absent element succeeds and says so). Repeating `--set` for one "
|
||||
"array field appends *within the call*, and `\\,` is a literal comma - same rules as "
|
||||
"`new --set`. Refused with the command that owns them instead: `type:` (page-lifecycle), "
|
||||
"and the page-ref arrays `related:`/`sources:`/`entities:`/`concepts:` (`xref`). Everything "
|
||||
"else the schema declares is settable, and an unknown field lists what the page actually "
|
||||
"has. Schema-validates the fields it writes, and `raw_files:` entries must exist on disk. A "
|
||||
"source declares `date:` instead of `modified:`, and that is the *publication* date of the "
|
||||
"raw material - it is never bumped to today, and changes only when `--date` names a value "
|
||||
"explicitly.",
|
||||
failures=(cli_contract.Failure(
|
||||
label="",
|
||||
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; "
|
||||
"`--add`/`--remove` on a non-array field; a `raw_files:` path that doesn't exist",
|
||||
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",
|
||||
),),
|
||||
notes=(
|
||||
"Bumps `modified:` to today and optionally rewrites any other field the page's type "
|
||||
"declares.",
|
||||
"`--summary`/`--provenance` are shorthands; `--set` reaches every other field and "
|
||||
"**replaces** its value, while `--add`/`--remove` change single elements of an array "
|
||||
"field (removing an absent element succeeds and says so).",
|
||||
"Repeating `--set` for one array field appends *within the call*, and `\\,` is a "
|
||||
"literal comma.",
|
||||
"Refused, naming the command that owns them instead: `type:` (the page-lifecycle "
|
||||
"procedure) and the page-ref arrays `related:`/`sources:`/`entities:`/`concepts:` "
|
||||
"(`xref`). Everything else the schema declares is settable, and an unknown field is "
|
||||
"refused with the list of fields the page actually has.",
|
||||
"Schema-validates the fields it writes; `raw_files:` entries must exist on disk.",
|
||||
"A source page declares `date:` instead of `modified:`: the *publication* date of the "
|
||||
"raw material. It is never bumped to today and changes only when `--date` names a value "
|
||||
"explicitly.",
|
||||
"`--no-date` changes only the given fields and leaves the date alone; `--dry-run` "
|
||||
"previews the new frontmatter without writing.",
|
||||
"Safe to re-run as-is: `--set` and `--add` are idempotent, and `--remove` of an "
|
||||
"already-absent element succeeds while reporting it.",
|
||||
),
|
||||
failures=(
|
||||
cli_contract.Failure(
|
||||
cause="Page not found",
|
||||
reaction="Fix the title and retry once",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="An invalid value for a field it writes, or a `raw_files:` path that doesn't "
|
||||
"exist",
|
||||
reaction="Fix the argument and retry once",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="A field owned by another command (`type:`, a page-ref array) or absent from "
|
||||
"the type's schema",
|
||||
reaction="Use the command the error names, or a field from the list it prints",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="`--add`/`--remove` on a non-array field",
|
||||
reaction="Use `--set` for a scalar field, then retry once",
|
||||
),
|
||||
),
|
||||
examples=(
|
||||
'tools/wikitool touch --page "Docker" --summary "Container runtime and image format"',
|
||||
'tools/wikitool touch --page "Docker" --add tags=containers --no-date',
|
||||
'tools/wikitool touch --page "Source - Docker Cheatsheet" --date 2026-09-01 --dry-run',
|
||||
),
|
||||
never=(
|
||||
"Never edit `type:` or a page-ref array (`related:`/`sources:`/`entities:`/"
|
||||
"`concepts:`) by hand - `type:` changes go through the page-lifecycle procedure, "
|
||||
"page-ref arrays through `xref`.",
|
||||
),
|
||||
see_also=(
|
||||
"`wikitool xref add` / `wikitool xref remove` - the page-ref arrays",
|
||||
"`instructions/page-lifecycle.md` - changing a page's `type:`",
|
||||
),
|
||||
))
|
||||
def touch_command(
|
||||
page_title: str = typer.Option(..., "--page", help="Exact page title, e.g. 'Docker Cheatsheet'"),
|
||||
|
||||
Reference in new issue
Block a user