local-review.yml (push) Invalid workflow file
Auto-version and Release / release (push) Failing after 9s
CI Pipeline / lint (push) Failing after 29s
CI Pipeline / go-surface (push) Failing after 18s
CI Pipeline / rust-surface (push) Successful in 4m15s
CI Pipeline / unit-tests (push) Successful in 5m29s
CI Pipeline / best-practices (push) Successful in 7s
CI Pipeline / documentation (push) Successful in 8s
CI Pipeline / canary (push) Successful in 1m6s
CI Pipeline / type-check (push) Failing after 35s
CI Pipeline / gitea-offline (push) Successful in 18s
deploy / deploy (push) Failing after 15m11s
CI Pipeline / required-checks (push) Failing after 1s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Successful in 10s
- #129: dimensional review rubric folded into the dispatch template (correctness/design/test-quality/observability/security, forced-choice verdicts; missing dimension = incomplete review) - #130: calibration harness (scripts/reviewer_calibration.py) — 5 known defects from the ledger + 1 clean control, 6/6 caught (100%, CALIBRATED). The pagination pattern needed widening for dotted receivers (self._rows[:50]) — the harness caught its own blind spot. - #131: local-model first-pass reviewer — Gitea workflow (local-review.yml) + agent script (local_firstpass_review.py, Ollama + review-lessons + #129 rubric). Fail-open by design: Ollama unreachable → skip with a note; findings are advisory PR comments, never gates.
78 lines
2.7 KiB
Python
78 lines
2.7 KiB
Python
#!/usr/bin/env python3
|
|
"""local_firstpass_review.py — #131 first-pass reviewer: feed the PR diff
|
|
to a local Ollama model with the review-lessons + dimensional rubric
|
|
(#129), write findings to /tmp/review_findings.md. Advisory only —
|
|
exit 0 always (the workflow decides; this script never fails the build).
|
|
|
|
Usage: python3 scripts/local_firstpass_review.py --diff D --stat S
|
|
[--model qwen2.5-coder:7b] [--host http://192.168.1.50:11434]
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
import urllib.request
|
|
from pathlib import Path
|
|
|
|
REPO = Path(__file__).resolve().parent.parent
|
|
|
|
|
|
def lessons_prompt():
|
|
"""The review-lessons tag index + dimensional rubric (#129)."""
|
|
try:
|
|
lessons = (REPO / "docs" / "review-lessons.yaml").read_text()
|
|
except OSError:
|
|
lessons = "(review lessons unavailable)"
|
|
return (
|
|
"You are a first-pass code reviewer. Advisory only — list findings, "
|
|
"do not approve or reject.\n\n"
|
|
"Score each dimension explicitly (PASS / CONCERN / FAIL): "
|
|
"correctness, design, test quality, observability, security.\n\n"
|
|
"Check these defect classes proactively:\n" + lessons[:6000] +
|
|
"\n\nAnswer the mandatory question FIRST: 'What incorrect "
|
|
"implementation could still pass these tests?'"
|
|
)
|
|
|
|
|
|
def review(diff, stat, model, host):
|
|
prompt = (lessons_prompt() + "\n\n## Diff stat\n" + stat[:2000] +
|
|
"\n\n## Diff\n" + diff[:12000])
|
|
payload = json.dumps({
|
|
"model": model,
|
|
"prompt": prompt,
|
|
"stream": False,
|
|
"options": {"num_ctx": 8192},
|
|
}).encode()
|
|
req = urllib.request.Request(
|
|
f"{host.rstrip('/')}/api/generate", data=payload,
|
|
headers={"Content-Type": "application/json"}, method="POST")
|
|
with urllib.request.urlopen(req, timeout=600) as r:
|
|
data = json.loads(r.read())
|
|
return data.get("response", "").strip()
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--diff", required=True)
|
|
ap.add_argument("--stat", required=True)
|
|
ap.add_argument("--model", default="qwen2.5-coder:7b")
|
|
ap.add_argument("--host", default="http://192.168.1.50:11434")
|
|
ap.add_argument("--out", default="/tmp/review_findings.md")
|
|
args = ap.parse_args()
|
|
|
|
diff = Path(args.diff).read_text()
|
|
stat = Path(args.stat).read_text()
|
|
try:
|
|
findings = review(diff, stat, args.model, args.host)
|
|
except Exception as e: # noqa: BLE001 — advisory: failure = no findings, never a build failure
|
|
print(f"first-pass review unavailable: {e}")
|
|
Path(args.out).write_text("")
|
|
return 0
|
|
Path(args.out).write_text(findings + "\n" if findings else "")
|
|
print(f"findings written ({len(findings)} chars)")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|