§10-målingen er gjennomført mot live ledger (243 records). Ingen KB-fil er redigert og ingen ledger-record er skrevet — §8s single-writer-state er urørt. Instrumentet ER O1-driveren med writes av (scripts/kb-eval/lib/fix-op.mjs, 30 tester). Måling #1 og #3 kommer dermed ut av mekanismen som senere skal ta på korpuset, ikke ut av en proxy-heuristikk. HOVEDFUNN — §4 som skrevet er utilstrekkelig, målt: Kjørt eksakt som spesifisert slapp den gjennom 6 swaps på piloten, hvorav 4 er GALE editer (presisjon 2/6): - 30-dagers → 24-dagers (enhets-kryssing: kilden sier 24 HOURS) - 3000 req/sek → 50 (metrikk-kryssing: query-throttle vs indexing-rate) - Microsoft Agent 365 → 7 (identifikator lemlestet, «7» høstet fra «E7») - text-embedding-ada-002 → ada-2 (identifikator lemlestet) §4 binder proveniensen til verdien og formen på editen — ingenting om at de to tokenene betegner SAMME STØRRELSE. Påstanden om at invarianten er «deliberately stronger than human review at scale» holder ikke. TILLEGG: contextCorresponds() krever samme label eller samme enhet på begge sider. Bevisst leksikalsk, UTEN oversettelsestabell — «dokumenter» læres ikke å være «documents», fordi en synonymtabell innfører en ny faktakilde og er en operatørbeslutning. Konsekvensen er målt: swap er provbar praktisk talt bare der konteksten er språknøytral (URL, kodeeksempel, parameternøkkel). TALLENE: - Pilot (≥7): 24 filer / 202 flagg → O1 = 2 (1,0 %), O3 = 200 (99,0 %) - Hele korpuset: 218 filer / 776 flagg → 15 sluppet gjennom, 9 korrekte - Kun iso_date (api-version-bump) overlever hånd-verifisering: 9/9. number/version lemlester identifikatorer (AI-900 → AI-901, gpt-4o → gpt-5.1o ×2, Java-agent 3.7.5 → 3.4.0 = nedgradering) og skal IKKE påføres. - Kun 7 av 200 aborter (3,5 %) er en fiksbar engineering-gap. Mer locator- arbeid kan ikke flytte O1-tallet vesentlig. Måling #2 (R8 → O2) er IKKE besvart og kan ikke besvares maskinelt: R8 gir null O1, og hvilke av de 46 enumerasjonene som subtraherer rent avhenger av dommerens PROSA-reason. Måling #4 (review-throughput) er ikke målt — det krever menneskelige review-økter som ikke har skjedd. Begge står som ikke-målt, ikke som antatt. VIDERE FUNN: subtraksjon kan etterlate en misvisende rest (§5 sier den «cannot introduce a new error» — sant om setningen, usant om leserens slutning), og kan ødelegge sann informasjon (prebuilt-check → finnes, heter prebuilt-check.us). `disposition` er `outdated` på 202/202 og bærer null informasjon, i strid med flagg-formatspesifikasjonen. `claim` matcher fillinjen ordrett i 0 av 202. Full oppskrift og åpne operatørbeslutninger: docs/r11-pilot-results.md
297 lines
13 KiB
JavaScript
297 lines
13 KiB
JavaScript
// fix-op.mjs — R11 fix-operation classifier over judge-pass flags.
|
||
//
|
||
// Contract: docs/r11-tiered-fix-design.md §3 (the O1/O2/O3 partition is by
|
||
// OPERATION, not by rule code) and §4 (the O1 invariant).
|
||
//
|
||
// This module IS the O1 driver with writes disabled. It attempts the value swap
|
||
// and checks §4's three conditions; an item it cannot prove is O3 with a typed
|
||
// abort code. That is deliberate: a proxy heuristic would have measured
|
||
// something other than the mechanism that will later touch a public corpus.
|
||
//
|
||
// Two properties the callers depend on:
|
||
// - PURE. No fs, no network, no mutation of the input flag. The caller reads
|
||
// the file and passes its text.
|
||
// - FAILS CLOSED. Every path returns O1-with-proof or O3-with-a-known-code.
|
||
// A misrouted O3 costs one human review; a misrouted O1 ships a wrong edit
|
||
// to a publicly distributed file.
|
||
//
|
||
// What this module deliberately does NOT do: decide O2. Subtraction candidacy
|
||
// turns on which sub-assertion the judge's prose `reason` names as failing, and
|
||
// no regex reads prose. O2 requires operator ratification (§5) before it exists
|
||
// as a class at all; until then every non-O1 item is O3 by design.
|
||
|
||
/**
|
||
* Abort codes. The taxonomy is part of the contract, not diagnostics: the pilot's
|
||
* measurement #3 (§10) is the DISTRIBUTION of these, because "abort rate 85 %"
|
||
* is not actionable while "60 % LOCATOR_MISS" is an engineering gap and "60 %
|
||
* NOT_VERBATIM" is intrinsic to the corpus.
|
||
*/
|
||
export const ABORT_CODES = {
|
||
MULTI_PART_CLAIM: 'MULTI_PART_CLAIM', // enumeration / several assertions in one claim (§3, the R8 class)
|
||
NO_VALUE_TOKEN: 'NO_VALUE_TOKEN', // nothing swappable — the claim asserts prose
|
||
STATUS_SYNONYM: 'STATUS_SYNONYM', // GA/Preview class: file vocabulary != source vocabulary (operator question)
|
||
MULTI_VALUE_TOKEN: 'MULTI_VALUE_TOKEN', // several distinct values — which one is wrong is a judgement
|
||
LOCATOR_MISS: 'LOCATOR_MISS', // value not found in the block the flag points at
|
||
LOCATOR_AMBIGUOUS: 'LOCATOR_AMBIGUOUS', // value occurs more than once in that block
|
||
NOT_VERBATIM: 'NOT_VERBATIM', // no same-type replacement occurs verbatim in evidence_quote (§4.1)
|
||
MULTI_REPLACEMENT: 'MULTI_REPLACEMENT', // quote offers several candidate values
|
||
CONTEXT_MISMATCH: 'CONTEXT_MISMATCH', // §4 held but the tokens do not denote the same quantity (see below)
|
||
INVARIANT_FAIL: 'INVARIANT_FAIL', // swap constructed but §4 did not hold — must never happen silently
|
||
};
|
||
|
||
/** Verdict code for a proven swap. Kept out of ABORT_CODES so `op === 'O1' <=> code === 'PROVEN'`. */
|
||
export const PROVEN = 'PROVEN';
|
||
|
||
// Value types, most specific first. Matching is non-overlapping and priority
|
||
// ordered, so `2.3.0` is one version rather than two numbers, and `20 %` is a
|
||
// percent rather than the number 20. Types never cross in a swap: a percent may
|
||
// only be replaced by a percent.
|
||
const TOKEN_PATTERNS = [
|
||
['iso_date', /\d{4}-\d{2}-\d{2}/g],
|
||
['percent', /\d+(?:[.,]\d+)?\s?%/g],
|
||
['version', /v?\d+\.\d+\.\d+/g],
|
||
['number', /\d+(?:[.,]\d+)?/g],
|
||
];
|
||
|
||
// Lifecycle vocabulary. Present in a claim without any numeric token, this is the
|
||
// GA/Preview class: the corpus writes `**Preview**` / `**GA**` while the cited
|
||
// source writes "generally available". A swap would satisfy §4 literally while
|
||
// pasting English prose into a Norwegian table, so the class aborts and is put to
|
||
// the operator as a design question (a ratified synonym table, or permanent O3).
|
||
const STATUS_RE =
|
||
/\b(?:GA|generally available|allment tilgjengelig|public preview|private preview|preview|deprecated|utfaset|retired|avviklet)\b/i;
|
||
|
||
/**
|
||
* Extract swappable value tokens, non-overlapping and priority ordered.
|
||
* Status words are NOT value tokens — see STATUS_RE.
|
||
* @returns {Array<{type: string, value: string, index: number}>} in order of appearance
|
||
*/
|
||
export function extractValueTokens(text) {
|
||
if (!text) return [];
|
||
const taken = []; // [start, end) ranges already consumed by a higher-priority type
|
||
const out = [];
|
||
for (const [type, re] of TOKEN_PATTERNS) {
|
||
re.lastIndex = 0;
|
||
let m;
|
||
while ((m = re.exec(text)) !== null) {
|
||
const start = m.index;
|
||
const end = start + m[0].length;
|
||
if (taken.some(([s, e]) => start < e && end > s)) continue;
|
||
taken.push([start, end]);
|
||
out.push({ type, value: m[0], index: start });
|
||
}
|
||
}
|
||
return out.sort((a, b) => a.index - b.index);
|
||
}
|
||
|
||
/** True if the text carries lifecycle-status vocabulary. */
|
||
export function hasStatusWord(text) {
|
||
return STATUS_RE.test(text || '');
|
||
}
|
||
|
||
/**
|
||
* The contiguous non-blank block containing `line` (1-indexed).
|
||
*
|
||
* This is the search window, and it is structural rather than a magic ±N: claims
|
||
* are LLM-extracted restatements whose `line` often points at a table HEADER
|
||
* while the asserted value sits in a row below. A block is exactly that table,
|
||
* list, or paragraph. A blank line degenerates to itself.
|
||
*/
|
||
export function blockWindow(lines, line) {
|
||
if (line < 1 || line > lines.length) return { start: line, end: line };
|
||
if (lines[line - 1].trim() === '') return { start: line, end: line };
|
||
let start = line;
|
||
let end = line;
|
||
while (start > 1 && lines[start - 2].trim() !== '') start -= 1;
|
||
while (end < lines.length && lines[end].trim() !== '') end += 1;
|
||
return { start, end };
|
||
}
|
||
|
||
// ---------------------------------------------------------- context condition
|
||
//
|
||
// MEASURED, NOT ASSUMED: §4 alone admits wrong edits. On the pilot it proved six
|
||
// swaps of which four were false — "30-dagers" -> "24" from a quote saying 24
|
||
// HOURS (unit crossing), an indexing rate replaced by a query throttle (metric
|
||
// crossing), and two identifiers mutilated by digits harvested out of "E7" and a
|
||
// table cell ("Microsoft Agent 365" -> "Agent 7", "text-embedding-ada-002" ->
|
||
// "ada-2"). §4 constrains where the new value CAME FROM and what the edit LOOKS
|
||
// LIKE; it constrains nothing about whether the two tokens denote the same
|
||
// quantity.
|
||
//
|
||
// The condition below adds that, and it is deliberately lexical rather than
|
||
// semantic: the token must sit under the same label, or the same trailing unit,
|
||
// on both sides. No translation table — "dokumenter" is not taught to equal
|
||
// "documents", because a synonym/translation table introduces a new fact source
|
||
// and is an operator decision (§5-class), not an engineering one. The consequence
|
||
// is measured and reported: a swap is provable essentially only where the context
|
||
// is language-neutral (a URL, a code sample, a parameter key).
|
||
|
||
const WORD = /[A-Za-z0-9_.\-æøåÆØÅ]/;
|
||
|
||
/** Normalise a context run for comparison: lowercase, punctuation stripped. */
|
||
const normContext = (s) => s.toLowerCase().replace(/[^a-z0-9æøå]/g, '');
|
||
|
||
/** The word run immediately left of [index], skipping any separator run first. */
|
||
function leftContext(text, index) {
|
||
let i = index - 1;
|
||
// A separator run may be skipped; a word character adjacent to the token may
|
||
// NOT be — that adjacency is what makes "7" part of the identifier "E7".
|
||
if (i >= 0 && !WORD.test(text[i])) {
|
||
while (i >= 0 && !WORD.test(text[i])) i -= 1;
|
||
}
|
||
let end = i + 1;
|
||
while (i >= 0 && WORD.test(text[i])) i -= 1;
|
||
return normContext(text.slice(i + 1, end));
|
||
}
|
||
|
||
/** The word run immediately right of [index], skipping any separator run first. */
|
||
function rightContext(text, index) {
|
||
let i = index;
|
||
if (i < text.length && !WORD.test(text[i])) {
|
||
while (i < text.length && !WORD.test(text[i])) i += 1;
|
||
}
|
||
const start = i;
|
||
while (i < text.length && WORD.test(text[i])) i += 1;
|
||
return normContext(text.slice(start, i));
|
||
}
|
||
|
||
/**
|
||
* Do the two occurrences sit in corresponding context? True when a non-empty
|
||
* label matches on the left, or a non-empty unit matches on the right.
|
||
*/
|
||
export function contextCorresponds(fileLine, fileIndex, fileLen, quote, quoteIndex, quoteLen) {
|
||
const lf = leftContext(fileLine, fileIndex);
|
||
const lq = leftContext(quote, quoteIndex);
|
||
if (lf && lf === lq) return true;
|
||
const rf = rightContext(fileLine, fileIndex + fileLen);
|
||
const rq = rightContext(quote, quoteIndex + quoteLen);
|
||
return Boolean(rf) && rf === rq;
|
||
}
|
||
|
||
/** Distinct by type+value, preserving order. */
|
||
function distinct(tokens) {
|
||
const seen = new Set();
|
||
return tokens.filter((t) => {
|
||
const k = `${t.type} |