Files
chemenu/tools/chemenu/commands/lint.py
T
torben fd0f60b2e7
CI / verify (push) Successful in 1m15s
Release / release (push) Successful in 37s
tools: command records, Git group - NOTES as bullets, one exit line per cause, examples and prohibitions (#142)
Files changed:
- CHANGES.md
- VERSION
- tools/CONTRACT.md
- tools/README.md
- tools/chemenu/cli_contract.py
- tools/chemenu/commands/cite_cmd.py
- tools/chemenu/commands/dist_cmd.py
- tools/chemenu/commands/docs_verify.py
- tools/chemenu/commands/doctor.py
- tools/chemenu/commands/eval_cmd.py
- tools/chemenu/commands/git_publish.py
- tools/chemenu/commands/index_build.py
- tools/chemenu/commands/instructions_cmd.py
- tools/chemenu/commands/links_cmd.py
- tools/chemenu/commands/lint.py
- tools/chemenu/commands/log_append.py
- tools/chemenu/commands/migrate_cmd.py
- tools/chemenu/commands/new_page.py
- tools/chemenu/commands/page_ops.py
- tools/chemenu/commands/provenance_cmd.py
- tools/chemenu/commands/raw_cmd.py
- tools/chemenu/commands/review_cmd.py
- tools/chemenu/commands/run_budget.py
- tools/chemenu/commands/search.py
- tools/chemenu/commands/task_cmd.py
- tools/chemenu/commands/touch.py
- tools/chemenu/commands/types_cmd.py
- tools/chemenu/commands/upload_cmd.py
- tools/chemenu/commands/upstream_cmd.py
- tools/chemenu/commands/version_cmd.py
- tools/chemenu/commands/work_cmd.py
- tools/chemenu/commands/xref.py
- tools/chemenu/tests/test_cli_contract.py
- tools/chemenu/tests/test_docs_verify.py
2026-09-26 08:25:41 +02:00

128 lines
5.7 KiB
Python

"""`wikitool lint` - the terminal adapter over `chemenu.lint_core`.
The checks, the report and the hard-error rule live in `chemenu/lint_core.py`,
which imports no CLI machinery. This module owns only what a terminal needs:
the flags, where the report file lands, and the exit code.
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Optional
import typer
from chemenu import cli_contract, config
from chemenu.commands._util import rel_path, success
from chemenu.lint_core import (
HARD_ERROR_KEYS,
MIGRATION_GATED_KEYS,
MOST_LINKED_COUNT,
QUOTE_LIMIT,
count_quote_blocks,
default_report_path,
hard_error_keys,
has_hard_errors,
render_markdown,
render_summary,
run_lint,
)
# Re-exported: `from chemenu.commands.lint import run_lint` still resolves, and
# so does every other name the tests and sibling commands already import.
__all__ = [
"HARD_ERROR_KEYS",
"MIGRATION_GATED_KEYS",
"MOST_LINKED_COUNT",
"QUOTE_LIMIT",
"count_quote_blocks",
"default_report_path",
"hard_error_keys",
"has_hard_errors",
"render_markdown",
"render_summary",
"run_lint",
"lint_command",
]
@cli_contract.record(cli_contract.CommandRecord(
path="lint",
summary="Run structural lint checks against kb/.",
synopsis=(cli_contract.Variant(
usage="lint [--json] [--markdown out.md] [--full] [--fail-on-error]",
),),
properties=cli_contract.Properties(
effect=cli_contract.Effect.WRITE,
idempotent=cli_contract.Idempotent.YES,
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",
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\"",
),),
))
def lint_command(
json_out: bool = typer.Option(False, "--json", help="Print the raw findings as JSON and write no report"),
markdown_out: Optional[Path] = typer.Option(None, "--markdown", help="Write the markdown report here instead of the default reports/Lint Report <date>.md"),
full: bool = typer.Option(False, "--full", help="Print the whole report instead of only the sections with findings"),
fail_on_error: bool = typer.Option(False, "--fail-on-error", help="Exit non-zero if hard errors were found"),
):
"""Run structural lint checks against kb/.
\f
Unless `--json` is given, the full report is always written to a file and
its path is printed. That path is the point: a lint report is long, and an
agent that only saw it on stdout had no way back to the part it scrolled
past except by running lint again - two budget slots for one look at the
corpus.
"""
report = run_lint(config.KB_DIR)
if json_out:
typer.echo(json.dumps(report, indent=2))
if fail_on_error and has_hard_errors(report):
raise typer.Exit(code=1)
return
typer.echo(render_markdown(report) if full else render_summary(report))
target = markdown_out or default_report_path(report)
frontmatter = (
"---\n"
"type: types/lint-report.md\n"
f"created: {report['generated']}\n"
f"summary: Structural lint report - {report['page_count']} pages scanned\n"
"---\n\n"
)
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(frontmatter + render_markdown(report) + "\n", encoding="utf-8")
success(f"Full report written to {rel_path(target)}")
if fail_on_error and has_hard_errors(report):
raise typer.Exit(code=1)