tools: command records, Finding and checking group - one line per cause, examples, prohibitions (#142)
CI / verify (push) Successful in 1m14s
Release / release (push) Successful in 37s

Files changed:
- CHANGES.md
- VERSION
- tools/CONTRACT.md
- tools/chemenu/commands/lint.py
- tools/chemenu/commands/review_cmd.py
- tools/chemenu/commands/search.py
This commit is contained in:
torben committed 2026-09-26 08:49:02 +02:00
1 parent a1f3c47e62
commit 0b2d93a4a4
6 files changed
+260 -104

No files matched your search

+38 -25
View File
@@ -58,34 +58,47 @@ __all__ = [
atomic="Writes one report file (single atomic write) unless `--json`",
budget=cli_contract.Budget.COUNTED,
),
notes="Structural + provenance checks: broken wikilinks, dangling frontmatter references, "
"orphan pages, index drift, schema gaps, duplicate titles, title mismatches, pages nested "
"more than one directory below their collection (hard - the generated catalog folds these "
"into their area silently rather than merely reading it), uncovered raw files, broken "
"`raw_files:` refs, raw files claimed by more than one source page, unmarked provenance, "
"citation/frontmatter drift, unbalanced generated-region markers, edges whose label is "
"missing or not authorised by the source collection's `outbound:` (both hard once "
"`kb_version` has reached the release that introduced labelled edges - advisory below it, "
"so a corpus mid-migration is not refused by the check measuring it), `see-also` edges "
"whose reverse direction already carries a specific label (advisory only - redundant rather "
"than wrong, and never migration-gated, since no version turns the redundancy into an "
"error), a collection past the catalog's per-area shard threshold that has no areas to "
"shard (advisory only - sharding is automatic but per *area*, so a collection nobody gave "
"areas keeps one table however large it grows; reported with the split its subtype field "
"would produce, and only when that split puts every resulting area at or under the "
"threshold, so a lopsided or small collection stays silent), source pages sitting in the "
"`unclassified` catalog slot (advisory only - `unclassified` is the visible fallback for a "
"genuinely unclear source, not a defect), quote-limit overages (>2 blockquoted lines/page, "
"advisory only). Prints only the sections that found something and always writes the full "
"report to `reports/Lint Report <date>.md` (or `--markdown`), naming the path - `--full` "
"prints everything, `--json` prints the findings and writes nothing",
notes=(
"Structural and provenance checks over `kb/`: broken wikilinks, dangling frontmatter "
"references, orphan pages, index drift, schema gaps, duplicate titles, title "
"mismatches, uncovered raw files, broken `raw_files:` refs, raw files claimed by more "
"than one source page, unmarked provenance, citation/frontmatter drift, and unbalanced "
"generated-region markers.",
"Pages nested more than one directory below their collection are a hard finding - the "
"generated catalog folds these into their area silently rather than merely reading it.",
"Edges whose label is missing or not authorised by the source collection's `outbound:` "
"are both hard once `kb_version` has reached the release that introduced labelled "
"edges, and advisory below it.",
"Advisory only: `see-also` edges whose reverse direction already carries a specific "
"label - never migration-gated.",
"Advisory only: a collection past the catalog's per-area shard threshold that has no "
"areas to shard, reported with the split its subtype field would produce, and only "
"when that split puts every resulting area at or under the threshold.",
"Advisory only: source pages sitting in the `unclassified` catalog slot.",
"Advisory only: quote-limit overages (>2 blockquoted lines/page).",
"Prints only the sections that found something and always writes the full report to "
"`reports/Lint Report <date>.md` (or `--markdown`), naming the path. `--full` prints "
"everything; `--json` prints the findings and writes nothing.",
"Exits 0 whatever it finds unless `--fail-on-error` is passed.",
),
failures=(cli_contract.Failure(
label="",
cause="Only with `--fail-on-error`: hard findings exist",
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 "
"tool is broken\"",
reaction="Act on the findings - exit 1 here means \"act on the findings\", not \"the "
"tool is broken\". Re-running is safe, but only to re-*measure* after a fix",
),),
examples=(
"tools/wikitool lint",
"tools/wikitool lint --json",
"tools/wikitool lint --fail-on-error",
),
never=(
"Never re-run just to re-read the findings - the printed path holds the full report.",
),
see_also=(
"`wiki-lint` skill - the procedure that runs this",
"`wikitool move --reconcile` - fixes Misplaced and Nested Pages",
"`wikitool log status` - whether a full lint is due",
),
))
def lint_command(
json_out: bool = typer.Option(False, "--json", help="Print the raw findings as JSON and write no report"),
+55 -37
View File
@@ -86,43 +86,61 @@ def report_to_dict(report: ReviewReport) -> dict:
budget=cli_contract.Budget.EXEMPT,
network=cli_contract.Network.YES,
),
notes="Joins the configured task-tracker provider (`chemenu.tasks`) against `kb/gtd/` "
"project pages over the case-normalized project name, at read time, storing nothing - not "
"even a `reports/` file. Five checks: **stalled** (a tracker project with zero open items "
"whose `kb/` page is `state: active` - `dormant`/`completed`/`abandoned` never fire, since "
"those states mean the initiative not having a next action is expected rather than a "
"problem), **waiting-overdue** (a `WAITING` item whose `follow_up_at` is older than "
"`thresholds.stalled_waiting_days`), **unpaged-project** (a tracker project with no "
"matching `kb/` page, older than `thresholds.unpaged_project_weeks`), **no-open-loop** (a "
"`kb/` page `state: active` with no matching tracker project, or one with zero open items - "
"the reverse direction of the unpaged-project join, so a rename on either side surfaces on "
"both), **someday-stale** (a someday/maybe item untouched for longer than "
"`thresholds.someday_stale_months`). A value a provider genuinely cannot supply - a "
"`WAITING` item with no `follow_up_at` at all, a tracker project with no determinable "
"creation date - is its own finding (`waiting_no_follow_up`/`project_age_unknown`) rather "
"than a silent skip of waiting-overdue/unpaged-project for that item or project. Thresholds "
"come from `.wikitool-tasks.json`, never from the schema. Text output is one "
"`[check] project: message` line per finding, preceded by a `Source:` line naming which "
"access path answered and, for `superproductivity`'s `access: \"snapshot\"`, the snapshot's "
"age; `--json` carries the same findings plus "
"`checks_run`/`checks_skipped`/`kb_project_count`/`complete`/`source` "
"(`{\"kind\": ..., \"detail\": ...}` or `null`). No `.wikitool-tasks.json` fails "
"immediately with a clear \"no tracker configured\" message; a provider that cannot be "
"reached mid-run degrades only the checks that needed the failing call, and the report is "
"never rendered as if it were complete - see its error-contract row. Read-only, and "
"**exempt from the Iteration Budget Gate**",
failures=(cli_contract.Failure(
label="",
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 "
"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",
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 "
"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=(
"Joins the configured task-tracker provider against `kb/gtd/` project pages over the "
"case-normalized project name, at read time, storing nothing - not even a `reports/` "
"file.",
"**stalled**: a tracker project with zero open items whose `kb/` page is `state: "
"active`; `dormant`/`completed`/`abandoned` never fire.",
"**waiting-overdue**: a `WAITING` item whose `follow_up_at` is older than "
"`thresholds.stalled_waiting_days`.",
"**unpaged-project**: a tracker project with no matching `kb/` page, older than "
"`thresholds.unpaged_project_weeks`.",
"**no-open-loop**: a `kb/` page `state: active` with no matching tracker project, or "
"one with zero open items - the reverse direction of unpaged-project, so a rename on "
"either side surfaces on both.",
"**someday-stale**: a someday/maybe item untouched for longer than "
"`thresholds.someday_stale_months`.",
"A value a provider genuinely cannot supply - a `WAITING` item with no `follow_up_at`, a "
"tracker project with no determinable creation date - is its own finding "
"(`waiting_no_follow_up`/`project_age_unknown`) rather than a silent skip.",
"Thresholds come from `.wikitool-tasks.json`, never from the schema.",
"Text output is one `[check] project: message` line per finding, preceded by a "
"`Source:` line naming which access path answered and, for `superproductivity`'s "
"`access: \"snapshot\"`, the snapshot's age. `--json` carries the same findings plus "
"`checks_run`/`checks_skipped`/`kb_project_count`/`complete`/`source` "
"(`{\"kind\": ..., \"detail\": ...}` or `null`).",
"A provider that cannot be reached mid-run degrades only the checks that needed the "
"failing call; the report is printed in full, then exit 1 follows - never rendered as "
"if it were complete.",
"Re-reads everything fresh on every call, so nothing is ever stale to re-fetch.",
"Read-only, and **exempt from the Iteration Budget Gate**.",
),
failures=(
cli_contract.Failure(
cause="No `.wikitool-tasks.json`, or a malformed one - a clear \"no tracker "
"configured\" message",
reaction="Not fixed by retrying unchanged - configure or repair "
"`.wikitool-tasks.json` first",
),
cli_contract.Failure(
cause="The provider was reachable at config-parse time but a read call failed "
"mid-run; the full report (findings plus which checks ran) was printed first",
reaction="Start the unreachable provider (e.g. the tracker app), then retry plainly",
),
),
examples=(
"tools/wikitool review",
"tools/wikitool review --json",
),
never=(
"Never present a report that exited 1 as complete.",
),
see_also=(
"`gtd-weekly-review` skill - turns the findings into decisions",
"`wikitool task list` / `wikitool task close` - act on an item",
"`wikitool new project` - a project page and its tracker project",
),
))
def review_command(
json_out: bool = typer.Option(False, "--json", help="Print the findings as JSON."),
+63 -32
View File
@@ -136,38 +136,69 @@ def render_table(result: SearchResult, show_matches: bool) -> str:
atomic="Read-only",
budget=cli_contract.Budget.EXEMPT,
),
notes="Find pages in `kb/` without reading the index. Text search runs through a pluggable "
"backend (`rg` today); `--field` predicates are evaluated on frontmatter - `f=v`, "
"`f~substring`, `'f>=v'`, `'f:*'` (present), `'!f'` (absent), repeatable and ANDed. With no "
"text this is a pure structured query. One hit per line, ` | `-separated as `score | "
"kind/subtype | title | path | summary`, so a hit can be judged without opening the page and "
"then opened without looking it up: **title and path are never truncated** (the title is "
"the identifier `touch`/`xref`/`cite` take), and the summary - the one lossy field, and the "
"only one that may contain the separator - goes last, so splitting on `\" | \"` with "
"`maxsplit=4` is unambiguous. Scope is pages: the backend walks `kb/` but drops anything "
"`kb_scan.iter_kb_pages` excludes (the kb-root meta files, every `COLLECTION.md`, every "
"generated `INDEX.md`), which is why a hand-run grep over `kb/` can add none of them but "
"those. `--limit` defaults to 50 (`0` for no limit) and **a truncated result says so** - "
"`50 of 182 result(s)` in the table, `total`/`truncated`/`limit` beside `count` in `--json`, "
"where `count` stays the number of results in the payload; the same default and the same "
"fields are what `api.search` and the MCP `search` tool carry, from one constant. A page "
"whose frontmatter does not parse can match no positive predicate, so it is **named** "
"rather than dropped: `--json` always carries an `unreadable` list of `{path, reason}` "
"(usually empty), and the table form writes the same lines to stderr. `--regex` is applied "
"by `rg` alone, whose engine is linear; the ranking boosts for title and summary are "
"literal-containment only, so a non-literal pattern is ranked by match count. `rg` is "
"killed after 30 s and reported as a failure. Read-only, and **exempt from the Iteration "
"Budget Gate**",
failures=(cli_contract.Failure(
label="",
cause="`rg` is not installed or did not finish within 30 s, a malformed `--field` "
"predicate, an unknown field name, or an unknown `--backend`",
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` "
"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=(
"Finds pages in `kb/` without reading the catalog. Text search runs through a "
"pluggable backend (`rg` today).",
"`--field` predicates are evaluated on frontmatter - `f=v`, `f~substring`, `'f>=v'`, "
"`'f:*'` (present), `'!f'` (absent) - repeatable and ANDed. With no text this is a pure "
"structured query.",
"One hit per line, ` | `-separated as `score | kind/subtype | title | path | summary`. "
"**Title and path are never truncated** - the title is the identifier "
"`touch`/`xref`/`cite` take. The summary, the one lossy field and the only one that may "
"contain the separator, goes last, so splitting on `\" | \"` with `maxsplit=4` is "
"unambiguous.",
"Scope is pages: the backend walks `kb/` but drops the kb-root meta files, every "
"`COLLECTION.md` and every generated `INDEX.md` - a hand-run grep over `kb/` can add "
"none of them but those.",
"`--limit` defaults to 50 (`0` for no limit), and **a truncated result says so**: "
"`50 of 182 result(s)` in the table, `total`/`truncated`/`limit` beside `count` in "
"`--json`, where `count` stays the number of results in the payload. `api.search` and "
"the MCP `search` tool carry the same default and the same fields.",
"A page whose frontmatter does not parse can match no positive predicate, so it is "
"**named** rather than dropped: `--json` always carries an `unreadable` list of "
"`{path, reason}` (usually empty), and the table form writes the same lines to stderr.",
"An unknown field name is reported with the list of fields that do exist - never "
"answered with an empty result, which would read as \"no such pages\".",
"`--regex` is applied by `rg` alone, whose engine is linear; the ranking boosts for "
"title and summary are literal-containment only, so a non-literal pattern is ranked by "
"match count.",
"`rg` is killed after 30 s and reported as a failure.",
"Read-only, and **exempt from the Iteration Budget Gate**.",
),
failures=(
cli_contract.Failure(
cause="`rg` is not installed",
reaction="Not transient - install `rg`, then retry",
),
cli_contract.Failure(
cause="`rg` did not finish within 30 s",
reaction="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",
),
cli_contract.Failure(
cause="A malformed `--field` predicate, or an unknown `--backend`",
reaction="Fix the argument and retry",
),
cli_contract.Failure(
cause="An unknown field name; the error lists the fields that exist",
reaction="Pick a field from that list and retry",
),
),
examples=(
'tools/wikitool search "act runner"',
"tools/wikitool search --field entity_type=system --field '!sources'",
'tools/wikitool search "docker" --collection entities --limit 10 --json',
),
never=(
"Never retry a timed-out query unchanged.",
"Never grep `kb/` yourself instead - it can add no page this misses, only the "
"generated files it excludes.",
),
see_also=(
"`wikitool links show` - the edges around a page once found",
"`wiki-query` skill - answering a question from the wiki",
),
))
def search_command(
text: str = typer.Argument(