Nick's directive: linting must apply DYNAMICALLY on the right language — Go/Rust/anything gets the correct linter — plus fuzzy tests and useful bug-catchers, wired into the Gitea pipeline. THE GAPS (confirmed by inspection): Rust had ZERO linting (crates built, never clippy); Go had vet only (gofmt drifted across 41 files); no fuzzing anywhere; no property-based tests. NEW: - scripts/polyglot_lint.py: detects changed files vs the push range, groups by language (extension + path prefixes), runs ruff / go vet + gofmt -l / cargo clippy + cargo fmt --check per language; unknown extensions skipped with a note; exit non-zero on any finding - CI: gofmt gate (formatting is a gate), go test -race sweep, go fuzz smoke (15s per target), and a NEW rust-surface job: rustfmt --check + clippy -- -D warnings + cargo test (currently trivial — the crates carry no #[test] yet; this lights up the moment the first lands) - GO FUZZ TARGETS on the byte-parity surfaces: canonicalJSON (round-trip), Norm (deterministic + charset), Stem (no-grow/no-empty — idempotence deliberately NOT asserted: the oracle folds retroactively → retroactive → retroactiv, verified), FtsQueryConjunctive, Overlap (symmetry + [0,1]) - PYTHON property invariants (hypothesis, pinned 6.168.5 in dev extras + requirements-ci.txt): the same mathematical properties, mirrored FOUND BY THE FUZZER ON FIRST LIGHT: - canonicalJSON passed control bytes (<0x20) through RAW — Python json.dumps escapes them \uXXXX even with ensure_ascii=False. The Go digest DIVERGED from the oracle on adversarial payloads. FIXED (jsonEsc now emits \u00XX); the approval-digest surface is byte-stable again - the FTS builder never escapes embedded quotes (oracle-verified: Python does the same) — pinned as oracle truth; FTS5 hardening filed separately Also: gofmt -w across the tree (the 41-file drift converged; every module builds), clippy --fix + cargo fmt across all 80 crates (warnings ~110 → 59 mechanical-fixed, remainder are stylistic and now GATED so debt cannot grow). THE PIPE-MASK hit my own verification twice more this session — the gate script itself exits 1 correctly (verified bare).
182 lines
5.6 KiB
Python
182 lines
5.6 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 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 = []
|
|
crates = sorted({str(Path(f).relative_to(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())
|