ms-ai-architect/scripts/kb-eval/lib/g7-queue.mjs
Kjell Tore Guttormsen 8ea54ec00e feat(ms-ai-architect): G7-ankere kan baere sin egen fil — klasse-entryen for de 42 er skrevet, og #17 og #14 er begge oppfylt
Operator ratifiserte utvidelsen 2026-08-11. Beslutning #17 (én klasse-entry)
og grunnen bak #14 (ingen lokator usynlig for gaten) var begge ekte og
kolliderte i skjemaet. De kolliderer ikke lenger.

SKJEMAET: et anker er enten en ordrett STRENG, sjekket mot entry.file som foer,
eller et { file, text }-PAR sjekket mot SIN EGEN fil. Bakoverkompatibelt — alle
47 eksisterende entries er uroert. Et misformet par er en schema-feil, ikke et
anker som stille matcher ingenting.

TDD, og testene maatte skjerpes foer de var ekte: fem tester skrevet og kjoert
FOERST. To av dem PASSERTE mot gammel kode — av feil grunn: den gamle koden
stringify-er ankerobjektet inn i drift-meldingen, saa /other\.md/ traff
tilfeldig. En test som ikke kan feile beviser ingenting. Begge fikk en
diskriminator (meldingen skal IKKE navne entry.file) og feilet deretter.
5 feilende -> implementasjon -> 20/20 i fila, 1052/1052 i suiten.

idx-26an: 42 { file, text }-ankere, ett per medlem, hvert re-verifisert ordrett
OG unikt i sin egen fil med split-telling foer skriving.

MEKANISMEN ER BEVIST, IKKE ANTATT: kjoert mot den ekte validatoren med ett
medlems anker fjernet i en lesestubb — ok=false, ett anchor_drift, riktig
entry-id, og meldingen navner agent-evaluation-testing-frameworks.md. Det er
nettopp garantien #14 fantes for, naa baaret av én entry i stedet for 42.

MAALT HASARD BOKFOERT I ENTRYEN: to av de 42 ankertekstene er strenge
prefikser av andre medlemmers ankere («**Total MCP calls:** 6» i
ai-services-cost-optimization.md, «**MCP Calls:** 3» i
reserved-capacity-planning.md). Hver er unik i SIN fil, saa koen er trygg — en
kryss-fil search-and-replace er det ikke. Slett per fil, og rest-soek etter
hver edit.

Koen: 47 -> 48 entries (28 open, 20 resolved). Bokfoeringen av #17 er dermed
komplett: 1 klasse-entry + 16 individuelle = 17. Ingen korpusfil roert.
2026-08-11 22:23:13 +02:00

132 lines
5.1 KiB
JavaScript

/**
* G7 review queue — the named queue into the human review phase.
*
* G7 is the gap for corrections that are RIGHT but larger than the O2 envelope
* (one locator + deletion-only). Measured in R11 §9.6: of the four subtractions
* applied in 957ebef, two left a residue. Residues are the normal by-product of
* a delete-only envelope, not an exception — and two of the five members are
* replacements rather than multi-locator cases, which a deletion-oriented O4
* class would not have fixed. Hence a queue (form b), ratified 2026-08-03.
*
* This module validates the queue. It deliberately does NOT apply anything:
* every entry here is by definition outside the machine-appliable envelope.
*
* Anchors are verbatim strings, never line numbers — line numbers drift, and
* §9.4 measured `line` ≠ `real_line` in 9 of 17 records. An open entry whose
* anchor no longer occurs is reported as drift rather than quietly passing,
* because the whole purpose of the queue is that a defect cannot fall out of
* the programme unnoticed.
*/
export const ENTRY_CLASSES = new Set(['multi-locator', 'replacement']);
export const ENTRY_STATES = new Set(['open', 'resolved']);
const REQUIRED = ['id', 'file', 'class', 'status', 'raised', 'summary', 'evidence', 'anchors'];
const isAnchor = (a) =>
typeof a === 'string' || (a !== null && typeof a === 'object' && typeof a.file === 'string' && typeof a.text === 'string');
/**
* @param {unknown} entries queue entries
* @param {(path: string) => string} readFile
* @returns {{ ok: boolean, findings: Array<{id?: string, kind: string, message: string}> }}
*/
export function validateQueue(entries, readFile) {
const findings = [];
if (!Array.isArray(entries)) {
return { ok: false, findings: [{ kind: 'schema', message: 'queue must be an array of entries' }] };
}
const seen = new Set();
for (const entry of entries) {
const id = typeof entry?.id === 'string' ? entry.id : '<no id>';
const missing = REQUIRED.filter((f) => entry?.[f] === undefined);
if (missing.length > 0) {
findings.push({ id, kind: 'schema', message: `missing required field(s): ${missing.join(', ')}` });
continue;
}
if (seen.has(entry.id)) {
findings.push({ id, kind: 'schema', message: `duplicate id: ${entry.id}` });
continue;
}
seen.add(entry.id);
if (!ENTRY_CLASSES.has(entry.class)) {
findings.push({ id, kind: 'schema', message: `unknown class: ${entry.class}` });
continue;
}
if (!ENTRY_STATES.has(entry.status)) {
findings.push({ id, kind: 'schema', message: `unknown status: ${entry.status}` });
continue;
}
// A resolved entry has to say what closed it. Without that, "resolved" is
// indistinguishable from "quietly dropped" — the exact failure G7 exists to
// prevent. Resolved entries are exempt from anchor checking, since a real
// fix is expected to have changed the text the anchor pointed at.
if (entry.status === 'resolved') {
if (typeof entry.resolution !== 'string' || entry.resolution.trim() === '') {
findings.push({ id, kind: 'schema', message: 'a resolved entry must record a resolution' });
}
continue;
}
if (!Array.isArray(entry.anchors) || entry.anchors.length === 0) {
findings.push({ id, kind: 'schema', message: 'an open entry must carry at least one anchor' });
continue;
}
// An anchor is either a verbatim string, checked against entry.file, or a
// { file, text } pair checked against ITS OWN file. The pair form exists for
// class-wide entries — one record for a defect that lives in many files.
// Measured 2026-08-11: booking such a class as a single-file entry hid 41 of
// 42 locators from this check, which is the 795d494 failure, a locator alive
// only in prose while both gates go green. A malformed pair is a schema fault
// rather than an anchor that quietly matches nothing.
const malformed = entry.anchors.filter((a) => !isAnchor(a));
if (malformed.length > 0) {
findings.push({
id,
kind: 'schema',
message: `anchor must be a verbatim string or a { file, text } pair: ${malformed
.map((m) => JSON.stringify(m))
.join(', ')}`,
});
continue;
}
const byFile = new Map();
for (const a of entry.anchors) {
const target = typeof a === 'string' ? entry.file : a.file;
const text = typeof a === 'string' ? a : a.text;
if (!byFile.has(target)) byFile.set(target, []);
byFile.get(target).push(text);
}
for (const [target, texts] of byFile) {
let text;
try {
text = readFile(target);
} catch {
findings.push({ id, kind: 'file_unreadable', message: `cannot read ${target}` });
continue;
}
const gone = texts.filter((t) => !text.includes(t));
if (gone.length > 0) {
findings.push({
id,
kind: 'anchor_drift',
message: `anchor no longer occurs in ${target}: ${gone.map((g) => JSON.stringify(g)).join(', ')}`,
});
}
}
}
return { ok: findings.length === 0, findings };
}