Files
rune/scripts/polyglot_lint.py
T
Nick 570e7d10e2
Auto-version and Release / release (push) Failing after 9s
CI Pipeline / lint (push) Failing after 28s
CI Pipeline / go-surface (push) Failing after 17s
CI Pipeline / rust-surface (push) Successful in 4m7s
CI Pipeline / best-practices (push) Successful in 7s
CI Pipeline / canary (push) Failing after 1m3s
CI Pipeline / documentation (push) Successful in 7s
CI Pipeline / gitea-offline (push) Successful in 17s
CI Pipeline / type-check (push) Failing after 34s
CI Pipeline / unit-tests (push) Failing after 8m16s
deploy / deploy (push) Failing after 15m11s
CI Pipeline / required-checks (push) Failing after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Failing after 11s
fix(lint): polyglot gate path normalization + go/rust formatting debt — fault-watch fix (contract BUILDER-2026-10-08T15:15:16Z-6a78f4)
- ROOT CAUSE: lint_rust called Path(f).relative_to(REPO) on already-relative
  paths — ValueError on every all-rust changed set since the relative-path
  change; the gate has been silently dead for rust whenever changed-only
  mode collected rust files. Normalized via os.path.isabs.
- gofmt: 8 Go files carried pre-existing formatting debt (surfaced only now
  that the gate can actually see them) — formatted.
- rustfmt: branch_battery.rs formatted.
- Verified: polyglot clean (468 changed → 169/165/107), rust shims 40/40,
  cargo 34/34, ratchet PASS.
2026-10-08 16:16:19 +01:00

186 lines
5.9 KiB
Python

#!/usr/bin/env python3
"""polyglot_lint.py — the language-AWARE lint gate (contract
BUILDER-2026-10-07T20:01:40Z-ac6140).
Why: the pipeline linted Python hard (ruff + ratchet) but Rust NOT AT ALL
(crates build, never clippy) and Go only via vet (gofmt drifted across 41
files, including landed batches). This gate applies the RIGHT linter to
whatever changed:
Python → ruff check
Go → go vet (per module) + gofmt -l (formatting is a gate)
Rust → cargo clippy -- -D warnings (per changed crate) + cargo fmt --check
other → noted and skipped
Modes:
(default) changed files vs the push range (git diff against
origin/master / the last push ref; falls back to the
last commit when no upstream exists)
--all full tree
--files a,b,c explicit list
Exit non-zero on any finding. The cron fault-watcher and the pre-push
routine both run this; CI runs the Go+Rust halves natively.
Usage:
python3 scripts/polyglot_lint.py # changed-only
python3 scripts/polyglot_lint.py --all # everything
"""
import argparse
import os
import subprocess
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
LANG_BY_EXT = {
".py": "python",
".go": "go",
".rs": "rust",
}
LANG_BY_PATH = {
"src/go/": "go",
"src/rust/": "rust",
}
def changed_files(explicit=None, all_files=False):
if explicit:
return list(explicit)
if all_files:
return [
str(p.relative_to(REPO))
for p in REPO.rglob("*")
if p.is_file() and not str(p).startswith((".git", "store/", "target"))
]
base = None
for ref in ("origin/master", "gitea/master", "HEAD~1"):
r = subprocess.run(["git", "rev-parse", "--verify", ref],
capture_output=True, text=True, cwd=REPO)
if r.returncode == 0:
base = ref
break
if base is None:
return []
r = subprocess.run(["git", "diff", "--name-only", f"{base}...HEAD"],
capture_output=True, text=True, cwd=REPO)
return [f for f in r.stdout.splitlines() if f.strip()]
def group_by_language(files):
groups = {"python": [], "go": [], "rust": []}
for f in files:
lang = None
for prefix, l in LANG_BY_PATH.items():
if f.startswith(prefix):
lang = l
break
if lang is None:
lang = LANG_BY_EXT.get(Path(f).suffix)
if lang:
groups[lang].append(f)
return groups
def run(cmd, cwd=None):
r = subprocess.run(cmd, cwd=cwd or REPO, capture_output=True, text=True)
return r.returncode, r.stdout, r.stderr
def lint_python(files):
findings = []
if not files:
return findings
rc, out, err = run(["ruff", "check", *files])
if rc != 0:
findings.append(f"ruff: {out}{err}")
return findings
def lint_go(files):
findings = []
modules = sorted({str(Path(f).parent) for f in files})
# collapse to module roots (dirs containing go.mod)
for mod in modules:
mod_path = REPO / mod
while mod_path != REPO and not (mod_path / "go.mod").exists():
mod_path = mod_path.parent
if mod_path == REPO:
continue
rel = mod_path.relative_to(REPO)
rc, out, err = run(["go", "vet", "./..."], cwd=mod_path)
if rc != 0:
findings.append(f"go vet ({rel}): {out}{err}")
# gofmt across the changed files' modules (formatting is a GATE)
fmt_targets = [f for f in files if f.endswith(".go")]
if fmt_targets:
rc, out, _ = run(["gofmt", "-l", *fmt_targets])
if rc != 0:
findings.append(f"gofmt: failed to run")
elif out.strip():
findings.append(
"gofmt unformatted (run `gofmt -w` on these):\n "
+ "\n ".join(out.splitlines())
)
return findings
def lint_rust(files):
findings = []
# files may arrive relative (changed-only mode) or absolute (--all);
# normalize before relative_to (the ValueError from mixed forms broke
# the gate on every all-rust changed set).
crates = sorted({str((f if os.path.isabs(f) else REPO / f)).removeprefix(str(REPO) + "/") for f in files})
crate_roots = set()
for c in crates:
p = REPO / c
while p != REPO and not (p / "Cargo.toml").exists():
p = p.parent
if p != REPO:
crate_roots.add(p)
for crate in sorted(crate_roots):
rc, out, err = run(["cargo", "clippy", "--", "-D", "warnings"], cwd=crate)
if rc != 0:
findings.append(f"clippy ({crate.name}):\n{out}{err}")
rc, out, err = run(["cargo", "fmt", "--check"], cwd=crate)
if rc != 0:
findings.append(f"rustfmt ({crate.name}):\n{out}{err}")
return findings
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--all", action="store_true", help="lint the full tree")
ap.add_argument("--files", help="comma-separated explicit file list")
args = ap.parse_args()
explicit = args.files.split(",") if args.files else None
files = changed_files(explicit, args.all)
if not files:
print("polyglot lint: no changed files detected — nothing to do")
return 0
groups = group_by_language(files)
print(f"polyglot lint: {len(files)} changed → "
f"python={len(groups['python'])} go={len(groups['go'])} "
f"rust={len(groups['rust'])}")
all_findings = []
all_findings += lint_python(groups["python"])
all_findings += lint_go(groups["go"])
all_findings += lint_rust(groups["rust"])
if all_findings:
print(f"POLYGLOT LINT: {len(all_findings)} finding(s):")
for f in all_findings:
print(f" - {f}")
return 1
print("polyglot lint: clean")
return 0
if __name__ == "__main__":
sys.exit(main())