Webhook-driven central review service for Granthi forges: HMAC-verified push/pull_request webhooks, per-repo serialization with global LLM concurrency 1, shre-router powered rubric review (8 axes, strict JSON, tolerant extractor + retry), commit statuses + PR scorecard comments, sqlite attribution ledger with Co-Authored-By trailer parsing, per-repo HTML history dashboard, nightly per-agent digests, fail-open-with- visibility when the router is unavailable. Posture: BLOCK on confirmed critical/high correctness+security findings; admin merge is the human override. Co-Authored-By: Claude Fable 5 <[email protected]>
74 lines
3.0 KiB
JavaScript
74 lines
3.0 KiB
JavaScript
// Verdict derivation. Posture: BLOCK on confirmed critical/high findings in
|
|
// correctness/security; everything else is warn-or-pass. Human override =
|
|
// admin merge (branch protection does not apply to admins).
|
|
|
|
const BLOCKING_SEVERITIES = new Set(['critical', 'high']);
|
|
const BLOCKING_AXES = new Set(['correctness', 'security']);
|
|
|
|
export function isBlockingFinding(f) {
|
|
if (!f || typeof f !== 'object') return false;
|
|
const sev = String(f.severity || '').toLowerCase();
|
|
const conf = String(f.confidence || '').toLowerCase();
|
|
if (!BLOCKING_SEVERITIES.has(sev)) return false;
|
|
if (conf !== 'confirmed') return false;
|
|
// If the model tagged an axis, only correctness/security block.
|
|
// Untagged critical+confirmed findings block too (conservative).
|
|
const axis = f.axis ? String(f.axis).toLowerCase() : null;
|
|
return axis === null || BLOCKING_AXES.has(axis);
|
|
}
|
|
|
|
export function deriveVerdict(review) {
|
|
const findings = Array.isArray(review?.findings) ? review.findings : [];
|
|
if (findings.some(isBlockingFinding)) return 'fail';
|
|
const hasWarn = findings.some(f => {
|
|
const sev = String(f?.severity || '').toLowerCase();
|
|
return sev === 'critical' || sev === 'high' || sev === 'medium';
|
|
});
|
|
if (hasWarn) return 'warn';
|
|
// trust the model's verdict if it said warn without matching findings
|
|
if (review?.verdict === 'warn') return 'warn';
|
|
return 'pass';
|
|
}
|
|
|
|
export function normalizeReview(raw) {
|
|
if (!raw || typeof raw !== 'object') return null;
|
|
const axesNames = ['correctness', 'security', 'tests_coverage', 'design_simplicity',
|
|
'performance', 'style', 'breaking_changes', 'docs'];
|
|
const axes = {};
|
|
for (const name of axesNames) {
|
|
const a = raw.axes?.[name];
|
|
axes[name] = {
|
|
score: clampScore(a?.score),
|
|
rationale: typeof a?.rationale === 'string' ? a.rationale.slice(0, 500) : ''
|
|
};
|
|
}
|
|
const findings = (Array.isArray(raw.findings) ? raw.findings : []).map(f => ({
|
|
title: String(f?.title ?? 'untitled').slice(0, 200),
|
|
file: String(f?.file ?? '').slice(0, 300),
|
|
line: Number.isFinite(Number(f?.line)) ? Number(f.line) : null,
|
|
severity: pick(String(f?.severity || '').toLowerCase(), ['critical', 'high', 'medium', 'low'], 'low'),
|
|
confidence: pick(String(f?.confidence || '').toLowerCase(), ['confirmed', 'plausible'], 'plausible'),
|
|
axis: f?.axis ? String(f.axis).toLowerCase() : null,
|
|
detail: String(f?.detail ?? '').slice(0, 2000)
|
|
}));
|
|
const review = {
|
|
axes,
|
|
overall: clampScore(raw.overall),
|
|
grade: pick(String(raw.grade || '').toUpperCase(), ['A', 'B', 'C', 'D', 'F'], 'C'),
|
|
findings,
|
|
verdict: pick(String(raw.verdict || '').toLowerCase(), ['pass', 'warn', 'fail'], 'pass')
|
|
};
|
|
review.verdict = deriveVerdict(review); // service-side derivation is authoritative
|
|
return review;
|
|
}
|
|
|
|
function clampScore(v) {
|
|
const n = Number(v);
|
|
if (!Number.isFinite(n)) return 0;
|
|
return Math.max(0, Math.min(100, Math.round(n)));
|
|
}
|
|
|
|
function pick(v, allowed, fallback) {
|
|
return allowed.includes(v) ? v : fallback;
|
|
}
|