tools: delegate documentation checks to the workspace doc gate

`tools/lint_gate.py` now also runs `D:\orisys_ws\scripts\doc_lint.py`, the
workspace's single documentation gate, alongside Ruff:

- changed Markdown by default, the whole corpus under --all
- `--files` names Python files explicitly, so it skips the doc gate
- `--no-docs` opts out
- the docs verdict is reported even when Ruff is missing, so a missing
  linter cannot hide it (the exit code still signals the environment problem)

The doc rules live in exactly one place on purpose: this file is duplicated
across three repositories and had already drifted, so adding a fourth copy of
lint logic would repeat the mistake. Failing checks are the documentation
budget, inventory membership, a declared kind, unresolved mojibake, paragraph
walls, and duplicate explanations across documents. Emphasis density is
advisory only - measured as a continuum from 12% to 48% across the corpus, so
no threshold separates signal from noise.

Pre-existing oversized documents are recorded as budget-exempt with a written
reason in D:\orisys_ws\docs\inventory.json, and pre-existing findings are
baselined there, so this gate is red only for new regression.

With Ruff unavailable in the current environment the doc half still reports,
which is how it was verified.
This commit is contained in:
ykcheungab
2026-09-30 16:12:30 +08:00
parent 73b1fa7fa4
commit cde832eaca
+64 -6
View File
@@ -122,15 +122,60 @@ def describe(finding: dict) -> str:
)
def run_doc_gate(force_all: bool) -> dict:
"""Run the workspace documentation gate and return its JSON payload.
Delegates to the single authoritative script in the workspace root. Deliberately not a
fourth copy of any lint logic in this repo: this file is already duplicated across three
repositories and has drifted, so the doc rules live in exactly one place.
"""
script = REPO.parent / "scripts" / "doc_lint.py"
if not script.is_file():
return {"status": "unavailable", "findings": [], "baselined": []}
cmd = [sys.executable, str(script), "--json"]
if force_all:
cmd.append("--all")
result = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8", check=False)
if not result.stdout.strip():
print(f"lint_gate: doc gate produced no output: {result.stderr.strip()[:200]}", file=sys.stderr)
return {"status": "error", "findings": [], "baselined": []}
try:
return json.loads(result.stdout)
except json.JSONDecodeError:
print("lint_gate: could not parse doc gate output", file=sys.stderr)
return {"status": "error", "findings": [], "baselined": []}
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("--base", default=None, help="compare against this ref (e.g. origin/main)")
parser.add_argument("--files", nargs="*", default=None, help="explicit files to check")
parser.add_argument("--all", action="store_true", help="run the gate rules over the whole repo")
parser.add_argument("--json", action="store_true", help="machine-readable result on stdout")
parser.add_argument("--no-docs", action="store_true",
help="skip the workspace documentation gate")
args = parser.parse_args(argv)
# Documentation: changed Markdown by default, the whole corpus under --all. `--files` names
# Python files explicitly, so it disables the doc gate rather than misreading them as docs.
docs: dict = {"status": "skipped", "findings": [], "baselined": []}
if not args.no_docs and args.files is None:
docs = run_doc_gate(force_all=args.all)
# Report the documentation verdict even when Ruff is unavailable: a missing linter must not
# hide the docs result, and the exit code still signals the environment problem.
try:
ruff = find_ruff()
except SystemExit as exc:
doc_findings = docs.get("findings") or []
if doc_findings:
print(f"docs: {len(doc_findings)} finding(s) (see docs/AUTHORING.md):", file=sys.stderr)
for finding in doc_findings:
print(f" {finding['file']}: {finding['check']}: {finding['message']}",
file=sys.stderr)
else:
print(f"docs: clean ({docs.get('status')})", file=sys.stderr)
return int(exc.code or 2)
if args.all:
targets = [str(REPO)]
@@ -139,8 +184,9 @@ def main(argv: list[str] | None = None) -> int:
if not targets:
print("lint_gate: no changed Python files; nothing to check.")
if args.json:
print(json.dumps({"findings": [], "checked": [], "status": "clean"}))
return 0
print(json.dumps({"findings": [], "checked": [], "docs": docs,
"status": "findings" if docs.get("findings") else "clean"}))
return 1 if docs.get("findings") else 0
findings, version = run_ruff(ruff, ["--select", GATE_SELECT, *targets])
report_only, _ = run_ruff(ruff, ["--select", REPORT_ONLY, *targets])
@@ -164,12 +210,13 @@ def main(argv: list[str] | None = None) -> int:
for f in findings
],
"unused_suppressions": [describe(f) for f in report_only],
"status": "findings" if findings else "clean",
"docs": docs,
"status": "findings" if (findings or docs.get("findings")) else "clean",
},
indent=2,
)
)
return 1 if findings else 0
return 1 if (findings or docs.get("findings")) else 0
print(f"lint_gate: {version}")
print(f" select {GATE_SELECT} (BLE001 is allowed by policy; see ruff.toml)")
@@ -185,8 +232,19 @@ def main(argv: list[str] | None = None) -> int:
print(f"\n{len(report_only)} unused suppression(s) to triage by hand (never auto-removed):")
for finding in report_only:
print(f" {describe(finding)}")
print(f"\nstatus: {'FINDINGS' if findings else 'clean'}")
return 1 if findings else 0
doc_findings = docs.get("findings") or []
if docs.get("status") == "unavailable":
print("\ndocs: workspace doc gate not found; skipped")
elif doc_findings:
print(f"\ndocs: {len(doc_findings)} finding(s) (see docs/AUTHORING.md):")
for finding in doc_findings:
print(f" {finding['file']}: {finding['check']}: {finding['message']}")
else:
print(f"\ndocs: clean ({docs.get('status')})")
print(f"\nstatus: {'FINDINGS' if (findings or doc_findings) else 'clean'}")
return 1 if (findings or doc_findings) else 0
if __name__ == "__main__":