tools: command records, Finding and checking group - one line per cause, examples, prohibitions (#142)
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:
1 parent
a1f3c47e62
commit
0b2d93a4a4
6 files changed
+260
-104
No files matched your search
@@ -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"),
|
||||
|
||||
@@ -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."),
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in new issue
Block a user