234 lines
9.2 KiB
JavaScript
234 lines
9.2 KiB
JavaScript
// lib/review/coordinator-contract.mjs
|
|
// SKAL-1·4a — deterministic reference implementation of the review-coordinator
|
|
// 4-pass contract (agents/review-coordinator.md §"Your 4-pass process").
|
|
//
|
|
// This is a DETERMINISTIC SUBSET, not a full mirror of the LLM coordinator.
|
|
// It implements the pure, hermetic passes and DELIBERATELY EXCLUDES the parts
|
|
// that need a live filesystem or LLM judgement (which belong to the 4c
|
|
// LLM-in-the-loop eval, not this all-agree foundation tier):
|
|
// - Pass 2 "Accuracy" file-existence / line-plausibility glob (fs I/O).
|
|
// - Pass 2 "Actionability" imperative-verb heuristic — the real coordinator
|
|
// uses LLM judgement here; a verb-list approximation would DIVERGE from the
|
|
// contract being mirrored, so only the deterministic "recommended_action is
|
|
// present-and-non-empty when supplied" half is kept.
|
|
// - The doc's 4-tuple `(file,line,rule_key,title)` id recompute — the shipped
|
|
// `computeFindingId` is 3-arg `(file,line,rule_key)`; this module follows
|
|
// the shipped code and flags the doc divergence (the 4-tuple id is not
|
|
// producible by the current helper).
|
|
//
|
|
// What IS implemented, purely: Pass 1 (triplet dedup → highest-severity-wins
|
|
// survivor + conformance tiebreak + detail concat + raised_by provenance),
|
|
// Pass 2 succinctness + actionability-presence, Pass 3 reasonableness
|
|
// (citation / unknown-rule_key drop, severity-mismatch correction), Pass 4
|
|
// verdict thresholds. No LLM, no network, no time, no randomness.
|
|
//
|
|
// Reuses: SEVERITY_VALUES / RULE_KEYS / getRule (rule-catalogue.mjs),
|
|
// computeFindingId (finding-id.mjs, triplet), validateFindings
|
|
// (findings-schema.mjs). Triplet key format mirrors
|
|
// scripts/bakeoff-armA-merge.mjs:33; raised_by provenance mirrors
|
|
// lib/review/plan-review-dedup.mjs.
|
|
|
|
import { SEVERITY_VALUES, RULE_KEYS, getRule } from './rule-catalogue.mjs';
|
|
import { computeFindingId } from '../parsers/finding-id.mjs';
|
|
import { validateFindings } from './findings-schema.mjs';
|
|
|
|
export const JUDGE_TITLE_MAX = 100;
|
|
export const JUDGE_DETAIL_MAX = 800;
|
|
|
|
/**
|
|
* Catalogue-tier rank of a severity: lower number = higher severity.
|
|
* BLOCKER=0 … SUGGESTION=3; an unknown severity ranks last.
|
|
* @param {string} severity
|
|
* @returns {number}
|
|
*/
|
|
export function severityRank(severity) {
|
|
const i = SEVERITY_VALUES.indexOf(severity);
|
|
return i === -1 ? SEVERITY_VALUES.length : i;
|
|
}
|
|
|
|
function isConformance(reviewer) {
|
|
return typeof reviewer === 'string' && reviewer.toLowerCase().includes('conformance');
|
|
}
|
|
|
|
function tripletKey(f) {
|
|
return `${f.file} ${f.line} ${f.rule_key}`;
|
|
}
|
|
|
|
/**
|
|
* Validate each reviewer payload and collect findings from the VALID ones,
|
|
* tagging each finding with its source reviewer (mirrors mergeArmA — invalid
|
|
* payloads are skipped, not crashed-on).
|
|
* @param {Array<{reviewer?: string, findings: object[]}>} reviewerPayloads
|
|
* @returns {{ findings: object[], skipped: Array<{reviewer: string|null, error_codes: string[]}> }}
|
|
*/
|
|
export function ingest(reviewerPayloads) {
|
|
const findings = [];
|
|
const skipped = [];
|
|
for (const payload of reviewerPayloads) {
|
|
const r = validateFindings(payload);
|
|
if (!r.valid) {
|
|
skipped.push({ reviewer: payload?.reviewer ?? null, error_codes: r.errors.map((e) => e.code) });
|
|
continue;
|
|
}
|
|
for (const f of payload.findings) {
|
|
findings.push({ ...f, reviewer: f.reviewer ?? payload.reviewer ?? f.owner_reviewer ?? null });
|
|
}
|
|
}
|
|
return { findings, skipped };
|
|
}
|
|
|
|
/**
|
|
* Pass 1 — dedup by (file, line, rule_key) triplet. Survivor = highest
|
|
* catalogue severity; severity tie → prefer the conformance reviewer; carries
|
|
* raised_by provenance, concatenates other reviewers' attribution into detail,
|
|
* and recomputes the id over the triplet.
|
|
* @param {object[]} findings
|
|
* @returns {object[]}
|
|
*/
|
|
export function dedupByTriplet(findings) {
|
|
const groups = new Map();
|
|
for (const f of findings) {
|
|
const key = tripletKey(f);
|
|
if (!groups.has(key)) groups.set(key, []);
|
|
groups.get(key).push(f);
|
|
}
|
|
const out = [];
|
|
for (const group of groups.values()) {
|
|
let survivor = group[0];
|
|
for (const f of group.slice(1)) {
|
|
const higher = severityRank(f.severity) < severityRank(survivor.severity);
|
|
const tieToConformance =
|
|
severityRank(f.severity) === severityRank(survivor.severity) &&
|
|
isConformance(f.reviewer) && !isConformance(survivor.reviewer);
|
|
if (higher || tieToConformance) survivor = f;
|
|
}
|
|
const raised_by = [...new Set(group.map((f) => f.reviewer).filter(Boolean))];
|
|
const others = group.filter((f) => f !== survivor);
|
|
let detail = survivor.detail;
|
|
if (others.length > 0) {
|
|
detail = survivor.detail ?? '';
|
|
for (const o of others) {
|
|
detail += `\nAlso flagged by ${o.reviewer ?? 'unknown'}: ${o.title ?? o.rule_key}.`;
|
|
}
|
|
}
|
|
const id = computeFindingId(survivor.file, survivor.line, survivor.rule_key);
|
|
out.push({ ...survivor, id, ...(detail !== undefined ? { detail } : {}), raised_by });
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* Pass 2 — HubSpot Judge (deterministic subset): drop on succinctness
|
|
* (title > 100 or detail > 800 chars) and actionability (recommended_action,
|
|
* when present, must be a non-empty string). The imperative-verb test is
|
|
* excluded (LLM judgement).
|
|
* @param {object[]} findings
|
|
* @returns {{ kept: object[], dropped: object[] }}
|
|
*/
|
|
export function judgeFilter(findings) {
|
|
const kept = [];
|
|
const dropped = [];
|
|
for (const f of findings) {
|
|
const titleLen = (f.title ?? '').length;
|
|
const detailLen = (f.detail ?? '').length;
|
|
let reason = null;
|
|
if (titleLen > JUDGE_TITLE_MAX) reason = 'succinctness:title';
|
|
else if (detailLen > JUDGE_DETAIL_MAX) reason = 'succinctness:detail';
|
|
else if ('recommended_action' in f &&
|
|
(typeof f.recommended_action !== 'string' || f.recommended_action.trim().length === 0)) {
|
|
reason = 'actionability:empty';
|
|
}
|
|
if (reason) dropped.push({ ...f, suppressed_reason: reason });
|
|
else kept.push(f);
|
|
}
|
|
return { kept, dropped };
|
|
}
|
|
|
|
/**
|
|
* Pass 3 — Cloudflare reasonableness (deterministic subset): drop findings
|
|
* with no citation (empty file / line < 0) or an unknown rule_key; CORRECT a
|
|
* severity that does not match the catalogue tier (a correction, not a drop).
|
|
* The fs file-existence glob is excluded (I/O).
|
|
* @param {object[]} findings
|
|
* @returns {{ kept: object[], dropped: object[] }}
|
|
*/
|
|
export function reasonablenessFilter(findings) {
|
|
const kept = [];
|
|
const dropped = [];
|
|
for (const f of findings) {
|
|
if (typeof f.file !== 'string' || f.file.length === 0 ||
|
|
(typeof f.line === 'number' && f.line < 0)) {
|
|
dropped.push({ ...f, suppressed_reason: 'no-citation' });
|
|
continue;
|
|
}
|
|
if (!RULE_KEYS.has(f.rule_key)) {
|
|
dropped.push({ ...f, suppressed_reason: 'unknown-rule_key' });
|
|
continue;
|
|
}
|
|
const rule = getRule(f.rule_key);
|
|
if (rule && f.severity !== rule.severity) {
|
|
kept.push({ ...f, severity: rule.severity, original_severity: f.severity });
|
|
} else {
|
|
kept.push(f);
|
|
}
|
|
}
|
|
return { kept, dropped };
|
|
}
|
|
|
|
/**
|
|
* Pass 4 — compute the verdict from severity counts (after dedup + filtering).
|
|
* BLOCKER ≥ 1 → BLOCK; else MAJOR ≥ 1 → WARN; else ALLOW.
|
|
* @param {object[]} findings
|
|
* @returns {{ verdict: 'BLOCK'|'WARN'|'ALLOW', counts: Record<string, number> }}
|
|
*/
|
|
export function computeVerdict(findings) {
|
|
const counts = { BLOCKER: 0, MAJOR: 0, MINOR: 0, SUGGESTION: 0 };
|
|
for (const f of findings) {
|
|
if (counts[f.severity] !== undefined) counts[f.severity] += 1;
|
|
}
|
|
let verdict;
|
|
if (counts.BLOCKER >= 1) verdict = 'BLOCK';
|
|
else if (counts.MAJOR >= 1) verdict = 'WARN';
|
|
else verdict = 'ALLOW';
|
|
return { verdict, counts };
|
|
}
|
|
|
|
/**
|
|
* Run the full deterministic contract: ingest → Pass 1 → Pass 2 → Pass 3 → Pass 4.
|
|
* @param {Array<{reviewer?: string, findings: object[]}>} reviewerPayloads
|
|
* @returns {{ verdict: string, counts: Record<string, number>, findings: object[], suppressed: object[], skipped: object[] }}
|
|
*/
|
|
export function runContract(reviewerPayloads) {
|
|
const { findings: ingested, skipped } = ingest(reviewerPayloads);
|
|
const deduped = dedupByTriplet(ingested);
|
|
const judged = judgeFilter(deduped);
|
|
const reasoned = reasonablenessFilter(judged.kept);
|
|
const { verdict, counts } = computeVerdict(reasoned.kept);
|
|
return {
|
|
verdict,
|
|
counts,
|
|
findings: reasoned.kept,
|
|
suppressed: [...judged.dropped, ...reasoned.dropped],
|
|
skipped,
|
|
};
|
|
}
|
|
|
|
// ---- CLI shim ----------------------------------------------------------------
|
|
|
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
const args = process.argv.slice(2);
|
|
const filePath = args.find((a) => !a.startsWith('--'));
|
|
if (!filePath) {
|
|
process.stderr.write('Usage: coordinator-contract.mjs [--json] <reviewer-payloads.json>\n');
|
|
process.exit(2);
|
|
}
|
|
const { readFileSync } = await import('node:fs');
|
|
const payloads = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
const result = runContract(Array.isArray(payloads) ? payloads : [payloads]);
|
|
if (args.includes('--json')) {
|
|
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
} else {
|
|
process.stdout.write(`coordinator-contract: ${result.verdict} (${result.findings.length} findings, ${result.suppressed.length} suppressed)\n`);
|
|
}
|
|
process.exit(0);
|
|
}
|