From 3dae1b011777e5323e05630e51e468f93745079d Mon Sep 17 00:00:00 2001 From: ykcheungab Date: Wed, 30 Sep 2026 16:12:30 +0800 Subject: [PATCH] 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. --- tools/lint_gate.py | 72 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 65 insertions(+), 7 deletions(-) diff --git a/tools/lint_gate.py b/tools/lint_gate.py index e1109b4..c40c8c4 100644 --- a/tools/lint_gate.py +++ b/tools/lint_gate.py @@ -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) - ruff = find_ruff() + # 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__":