stack: wikitool review - der Wochenrueckblick als Join zur Lesezeit (#125)
Files changed: - CHANGES.md - VERSION - tools/CONTRACT.md - tools/chemenu/cli.py - tools/chemenu/commands/review_cmd.py - tools/chemenu/commands/run_budget.py - tools/chemenu/review.py - tools/chemenu/tests/test_review.py
This commit is contained in:
1 parent
1875449b31
commit
80b57e0d01
8 files changed
+751
-5
No files matched your search
@@ -0,0 +1,88 @@
|
||||
"""`wikitool review` - the terminal adapter over `chemenu.review` (Gitea #125).
|
||||
|
||||
The checks, the join and the partial-report rule live in `chemenu.review`,
|
||||
which imports no CLI machinery. This module owns only what a terminal needs:
|
||||
the `--json` flag, the two render forms, and the exit code.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
import typer
|
||||
|
||||
from chemenu import config
|
||||
from chemenu.commands._util import fail
|
||||
from chemenu.errors import ValidationError
|
||||
from chemenu.review import ALL_CHECKS, ReviewReport, run_review
|
||||
|
||||
__all__ = ["render_report", "report_to_dict", "review_command"]
|
||||
|
||||
|
||||
def render_report(report: ReviewReport) -> str:
|
||||
"""The `--json`-free rendering. One line per finding, `[check] project:
|
||||
message`, so a hit can be told apart from the summary line without a
|
||||
schema - the same shape `search`'s table takes for the same reason."""
|
||||
lines: list[str] = []
|
||||
if report.checks_skipped:
|
||||
lines.append("INCOMPLETE - the following check(s) did not run:")
|
||||
for check, reason in report.checks_skipped:
|
||||
lines.append(f" - {check}: {reason}")
|
||||
lines.append(
|
||||
f"Partial result: {report.kb_project_count} kb/ project page(s) found; no "
|
||||
"tracker cross-check for the check(s) above."
|
||||
)
|
||||
lines.append("")
|
||||
|
||||
if not report.findings:
|
||||
lines.append("No findings.")
|
||||
else:
|
||||
for finding in report.findings:
|
||||
lines.append(f"[{finding.check}] {finding.project}: {finding.message}")
|
||||
|
||||
lines.append("")
|
||||
lines.append(f"{len(report.findings)} finding(s), {len(report.checks_run)}/{len(ALL_CHECKS)} check(s) ran.")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def report_to_dict(report: ReviewReport) -> dict:
|
||||
"""The `--json` form. Carries the same three things the text form does -
|
||||
findings, which checks ran, which were skipped and why - so a caller never
|
||||
has to parse prose to tell a partial report from a complete one."""
|
||||
return {
|
||||
"findings": [
|
||||
{"check": finding.check, "project": finding.project, "message": finding.message}
|
||||
for finding in report.findings
|
||||
],
|
||||
"checks_run": list(report.checks_run),
|
||||
"checks_skipped": [
|
||||
{"check": check, "reason": reason} for check, reason in report.checks_skipped
|
||||
],
|
||||
"kb_project_count": report.kb_project_count,
|
||||
"complete": report.complete,
|
||||
}
|
||||
|
||||
|
||||
def review_command(
|
||||
json_out: bool = typer.Option(False, "--json", help="Print the findings as JSON."),
|
||||
):
|
||||
"""Run the weekly GTD review: join the task tracker against kb/gtd/ pages
|
||||
over the project name and report the five staleness/mismatch checks
|
||||
(#119 D10/D26). Read-only - stores nothing, not even a reports/ file
|
||||
(#119 D3), and is exempt from the Iteration Budget Gate like `search`."""
|
||||
try:
|
||||
report = run_review(config.ROOT)
|
||||
except ValidationError as exc:
|
||||
fail(str(exc))
|
||||
return
|
||||
|
||||
if json_out:
|
||||
typer.echo(json.dumps(report_to_dict(report), indent=2))
|
||||
else:
|
||||
typer.echo(render_report(report))
|
||||
|
||||
if not report.complete:
|
||||
# Printed above already - this is deliberately not fail(), which
|
||||
# would swallow the report just rendered behind a single ERROR line.
|
||||
# See chemenu.review.ReviewReport.complete: an incomplete report must
|
||||
# never exit 0 the way a quiet week does.
|
||||
raise typer.Exit(code=1)
|
||||
@@ -115,9 +115,11 @@ SKIP_COMMAND_PATHS = {
|
||||
# reading, not iterating: the budget exists to stop an agent looping over the
|
||||
# wiki's *state*, and charging for a search would penalise the one habit that
|
||||
# lowers cost - looking before reading. `doctor` is here for the same reason:
|
||||
# it only reads and reports, never mutates anything. Every command that
|
||||
# mutates anything stays counted.
|
||||
SKIP_COMMANDS = {"search", "doctor"}
|
||||
# it only reads and reports, never mutates anything. `review` (#125) joins the
|
||||
# task tracker against kb/gtd/ pages and stores nothing either (#119 D3) - the
|
||||
# same read-only argument as `search`, just over a different pair of sources.
|
||||
# Every command that mutates anything stays counted.
|
||||
SKIP_COMMANDS = {"search", "doctor", "review"}
|
||||
|
||||
|
||||
def is_exempt(command: str, args: list[str]) -> bool:
|
||||
|
||||
Reference in new issue
Block a user