fix(review): group a triplet's findings title first, so arrival order never changes the rows
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
7175783ce5
commit
a5fcb5e8b4
6 changed files with 339 additions and 37 deletions
|
|
@ -123,7 +123,8 @@ const VERDICT_SCHEMA = {
|
|||
// first (for a catalogue key it always ties inside one triplet, since the
|
||||
// rule_key is shared), then a reviewer finding (no `engine`) over an engine
|
||||
// finding (decision 13, operator 12.10), then the higher asserted severity;
|
||||
// the workflow has no conformance rule, so otherwise the first-seen one stays.
|
||||
// the workflow has no conformance rule, so otherwise the first in the group's
|
||||
// order stays (the order the two passes below place the members in).
|
||||
// The survivor carries the highest severity any finding in its group asserted,
|
||||
// so an engine's mark never lowers what a reviewer agent asserted.
|
||||
// As in coordinator-contract Pass 1, one source never duplicates itself: its
|
||||
|
|
@ -131,25 +132,64 @@ const VERDICT_SCHEMA = {
|
|||
// (voyage-lekerepo trial 5, finding 7). `sourceOf` names the reviewer or engine.
|
||||
const sameTitle = (a, b) => a.title === undefined || b.title === undefined ||
|
||||
String(a.title).trim().toLowerCase() === String(b.title).trim().toLowerCase();
|
||||
// As there too, a triplet's findings are grouped in two passes, so the order
|
||||
// they arrive in never changes which row a finding joins: (1) every titled
|
||||
// finding joins the class of its normalized title, over all of the triplet's
|
||||
// findings; (2) the classes, in alphabetical order of the normalized title,
|
||||
// each join the first earlier group whose every member fits every finding of
|
||||
// the class (another source, or the same title), else they form a group of
|
||||
// their own; untitled findings come last, in canonical order (source, detail,
|
||||
// the whole finding), each joining the first group it fits. Members keep the
|
||||
// order the passes place them in (the classes alphabetically, each class's
|
||||
// findings in canonical order, the untitled findings last), so a full tie on
|
||||
// the survivor never follows arrival order either; a triplet's groups keep the
|
||||
// order of their earliest input index. With `perSource: false` everything on a
|
||||
// triplet is one group, as before.
|
||||
const sourceOf = new Map();
|
||||
const cmpStr = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
||||
function groupTriplet(entries) {
|
||||
const srcOf = (f) => sourceOf.get(f) ?? null;
|
||||
const fits = (m, f) => srcOf(m) !== srcOf(f) || sameTitle(m, f);
|
||||
const canon = (x, y) => cmpStr(String(srcOf(x.f) ?? ''), String(srcOf(y.f) ?? ''))
|
||||
|| cmpStr(String(x.f.detail ?? ''), String(y.f.detail ?? ''))
|
||||
|| cmpStr(JSON.stringify(x.f), JSON.stringify(y.f));
|
||||
const classes = new Map();
|
||||
const untitled = [];
|
||||
for (const e of entries) {
|
||||
if (e.f.title === undefined) {
|
||||
untitled.push(e);
|
||||
continue;
|
||||
}
|
||||
const t = String(e.f.title).trim().toLowerCase();
|
||||
if (!classes.has(t)) classes.set(t, []);
|
||||
classes.get(t).push(e);
|
||||
}
|
||||
const groups = [];
|
||||
const place = (members) => {
|
||||
let g = groups.find((grp) => grp.every((m) => members.every((e) => fits(m.f, e.f))));
|
||||
if (!g) {
|
||||
g = [];
|
||||
groups.push(g);
|
||||
}
|
||||
g.push(...members);
|
||||
};
|
||||
for (const t of [...classes.keys()].sort(cmpStr)) place(classes.get(t).sort(canon));
|
||||
for (const e of untitled.sort(canon)) place([e]);
|
||||
return groups
|
||||
.map((g) => ({ first: Math.min(...g.map((e) => e.i)), members: g.map((e) => e.f) }))
|
||||
.sort((a, b) => a.first - b.first)
|
||||
.map((g) => g.members);
|
||||
}
|
||||
function dedupByTriplet(findings, { perSource = true } = {}) {
|
||||
const byTriplet = new Map();
|
||||
for (const f of findings) {
|
||||
findings.forEach((f, i) => {
|
||||
const key = tripletOf(f);
|
||||
if (!byTriplet.has(key)) byTriplet.set(key, []);
|
||||
const subs = byTriplet.get(key);
|
||||
const src = sourceOf.get(f) ?? null;
|
||||
const fits = (g) => g.every((m) => (sourceOf.get(m) ?? null) !== src || sameTitle(m, f));
|
||||
let sub = !perSource ? subs[0]
|
||||
: subs.find((g) => fits(g) && g.some((m) => m.title !== undefined && f.title !== undefined && sameTitle(m, f)))
|
||||
?? subs.find(fits);
|
||||
if (!sub) {
|
||||
sub = [];
|
||||
subs.push(sub);
|
||||
}
|
||||
sub.push(f);
|
||||
}
|
||||
const groups = [...byTriplet.values()].flat();
|
||||
byTriplet.get(key).push({ f, i });
|
||||
});
|
||||
const groups = [...byTriplet.values()].flatMap((entries) => (perSource
|
||||
? groupTriplet(entries)
|
||||
: [entries.map((e) => e.f)]));
|
||||
const tier = (f) => sevRank(tierOf(f.rule_key) ?? f.severity);
|
||||
const out = [];
|
||||
for (const group of groups) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue