voyage/scripts/bakeoff-armA-merge.mjs
Kjell Tore Guttormsen 05f6b67be9
fix(review): the workflow review ranks by tier and puts back every engine finding
Operator decision 23 (13.10); review findings ab1b5d9f and bc7f4e23 (workflow side).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:32:59 +02:00

105 lines
4.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// scripts/bakeoff-armA-merge.mjs
// NW2 bake-off — Arm A (prose path) merge step.
//
// Arm A is the CURRENT /trekreview Phase 5–6 substrate: main spawns the two
// reviewers FOREGROUND (Agent tool), each emits a trailing fenced ```json block
// of findings (the prose contract), and main hand-validates + dedups before
// spawning the coordinator. This script is that hand-validate + dedup, made
// reproducible:
//
// for each reviewer raw-output file:
// validateReviewerOutput() (NW1 — extract last json fence, parse, schema-check)
// collect findings from VALID reviewers
// triplet-dedup by (file,line,rule_key) [same key as Arm B's dedupByTriplet,
// first-wins here; Arm B ranks by tier
// and keeps the reviewer finding on a
// tie — triplet-only, no jaccard pass-2]
// → emit { merged, validation, raw_finding_count, deduped_count }
//
// The merged findings are then handed to the review-coordinator (spawned by main),
// mirroring Arm B's coordinatorPrompt input. JSON-robustness metric = the
// `validation` report (parse/schema failures + which codes).
//
// Usage:
// node scripts/bakeoff-armA-merge.mjs [--json] <reviewerA.txt> <reviewerB.txt> ...
// → stdout: merged findings JSON (or full report with --json). Exit 0 always
// (validation failures are reported in-band, not as a nonzero exit).
import { readFileSync, existsSync } from 'node:fs';
import { validateReviewerOutput } from '../lib/review/findings-schema.mjs';
import { isMain } from '../lib/util/is-main.mjs';
// Triplet dedup — the same (file,line,rule_key) key as
// scripts/trekreview-armB.workflow.mjs `dedupByTriplet`, but not the same
// policy: Arm A keeps the first-seen finding, while Arm B ranks by catalogue
// tier and keeps the reviewer finding on a tie (review bc7f4e23). Triplet-only;
// NO jaccard pass-2.
export function dedupByTriplet(findings) {
const seen = new Map();
for (const f of findings) {
const key = `${f.file} ${f.line} ${f.rule_key}`;
if (!seen.has(key)) seen.set(key, f);
}
return [...seen.values()];
}
/**
* Merge an array of reviewer raw-output texts into the coordinator's input.
* @param {Array<{label?: string, text: string}>} reviewerOutputs
* @returns {{ merged, validation, raw_finding_count, deduped_count }}
*/
export function mergeArmA(reviewerOutputs) {
const validation = [];
const allFindings = [];
for (const { label, text } of reviewerOutputs) {
const r = validateReviewerOutput(text);
validation.push({
label: label || null,
valid: r.valid,
finding_count: Array.isArray(r.parsed?.findings) ? r.parsed.findings.length : 0,
error_codes: (r.errors || []).map((e) => e.code),
warning_codes: (r.warnings || []).map((w) => w.code),
});
// Mirror Phase 5: only VALID reviewer output flows to the coordinator.
if (r.valid && Array.isArray(r.parsed?.findings)) {
allFindings.push(...r.parsed.findings);
}
}
const merged = dedupByTriplet(allFindings);
return {
merged,
validation,
raw_finding_count: allFindings.length,
deduped_count: merged.length,
};
}
// ---- CLI shim ----------------------------------------------------------------
if (isMain(import.meta.url)) {
const argv = process.argv.slice(2);
const asJson = argv.includes('--json');
const files = argv.filter((a) => !a.startsWith('--'));
if (files.length === 0) {
process.stderr.write('Usage: bakeoff-armA-merge.mjs [--json] <reviewer-output> ...\n');
process.exit(2);
}
const outputs = files.map((f) => {
if (!existsSync(f)) {
process.stderr.write(`bakeoff-armA-merge: file not found: ${f}\n`);
process.exit(2);
}
return { label: f, text: readFileSync(f, 'utf-8') };
});
const result = mergeArmA(outputs);
if (asJson) {
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
} else {
process.stdout.write(JSON.stringify(result.merged, null, 2) + '\n');
process.stderr.write(
`arm-A merge: ${result.raw_finding_count} raw → ${result.deduped_count} deduped; ` +
`validation ${result.validation.map((v) => `${v.label}:${v.valid ? 'OK' : v.error_codes.join('/')}`).join(' ')}\n`,
);
}
process.exit(0);
}