feat(ms-ai-architect): OKF-konsum-cutter mot konsum-kontraktens §4, målt [skip-docs]

Deterministisk halvdel av konsum-skillen (ordre 20260826T123330Z-1842820111):
tokenisering, feltvekting, mekanisk supersesjonsregel og budsjett-kutt med
[unread]-kvittering mot hele kandidatnevneren. Eval-driver mot bake-off-korpuset.

Målt, budsjett 12 000 tegn, nevner 40 gullspørsmål:
  B1 kun indekslabel        6/40   feller 4/20
  B2 konform strategi      12/40   feller 0/20
  B3 kontroll uten OKF     12/40   feller 1/20
  B2+/B3+ (POST-HOC)       13/40 / 14/40

B2 == B3 er R3, den ene forhåndsregistrerte sammenligningen: med denne rangereren
gir OKF-metadataen ingen målbar gevinst på primærmetrikken. Differansen mot
bake-offens A3 (40/40, forfatter-informert velger = øvre grense) tilskrives per
låst regel R5 RANGEREREN, ikke OKF som teknologi.

Instrumentvalidering reproduserer eksakt: A1=31343, A2a=2884, hele bundlen=45580.
Suite 1097/1097 (1070 sporet + 27 nye). Negativ kontroll kjørt: tre mutasjoner i
bundle-cut.mjs, alle drept (1/1/3 feil), treet restaurert byte-identisk.

Konformitet mot §4: 8 av 10 oppfylt, punkt 3 delvis (falsifisering ikke skrevet
før kjøring), punkt 5 ikke oppfylt (markeringssett ikke deklarert).

[skip-docs]: ren intern måle- og evalueringsmekanikk. Ingen ny kommando, agent
eller skill; ingen brukervendt flate endret, så README/CLAUDE.md er uberørt.
This commit is contained in:
Kjell Tore Guttormsen 2026-08-27 12:13:03 +02:00
commit c0f10618da
4 changed files with 785 additions and 0 deletions

144
scripts/okf-consume/cut.mjs Normal file
View file

@ -0,0 +1,144 @@
#!/usr/bin/env node
// cut.mjs — entry point for the DETERMINISTIC half of the OKF bundle
// consumption skill (contract C4: the script cuts, the agent judges).
//
// Usage:
// node scripts/okf-consume/cut.mjs --bundle <dir> --query "<question>" [--k N]
// [--budget N] [--mode index|headscan] [--json]
//
// The OKF envelope of each bundle document is parsed by llm-ingestion-okf
// itself (materialize.parse_frontmatter), invoked as a subprocess. Nothing in
// this repo re-implements it. Set OKF_PYTHON to point at that package's venv
// interpreter; without it the envelope is reported as unavailable rather than
// guessed at.
import { readFileSync, readdirSync, existsSync } from 'node:fs';
import { join } from 'node:path';
import { execFileSync } from 'node:child_process';
import {
tokenize, parseIndex, headScan, scoreCandidate, cutToBudget,
AGENT_BUDGET, HEAD_SCAN_LINES,
} from './lib/bundle-cut.mjs';
const OKF_PYTHON = process.env.OKF_PYTHON
|| '/Users/ktg/repos/llm-ingestion-okf/.venv/bin/python';
/**
* Parse the OKF envelope of every document in the bundle, using the producing
* package. Returns a map target -> envelope, or null when the interpreter is
* unavailable never a hand-rolled fallback, because a fallback would be this
* repo owning a format it does not own.
*/
export function readEnvelopes(bundleDir, python = OKF_PYTHON) {
if (!existsSync(python)) return null;
const src = `
import json, sys
from pathlib import Path
from llm_ingestion_okf.materialize import parse_frontmatter
d = Path(sys.argv[1])
out = {}
for p in sorted(d.iterdir()):
if p.name == 'index.md' or p.suffix != '.md':
continue
try:
out[p.name] = parse_frontmatter(p)
except Exception as e:
out[p.name] = {'_error': type(e).__name__}
json.dump(out, sys.stdout)
`;
try {
return JSON.parse(execFileSync(python, ['-c', src, bundleDir], { encoding: 'utf8' }));
} catch {
return null;
}
}
/**
* Build the candidate set. In 'index' mode the ranker sees only what the index
* carries (labels); in 'headscan' mode it also sees the preserved frontmatter
* and H2 headings inside the first HEAD_SCAN_LINES body lines.
*
* assemblyChars counts what the SCRIPT read to get here. It is reported
* separately from what reaches the agent, because those are different costs and
* conflating them is how a cut looks cheaper than it is.
*/
export function buildCandidates({ bundleDir, mode = 'headscan', entries, readFile = readFileSync }) {
let assemblyChars = 0;
const candidates = [];
for (const { label, target } of entries) {
const path = join(bundleDir, target);
let text = '';
try { text = readFile(path, 'utf8'); } catch { continue; }
const cand = { label, target, body: text, h2: [] };
if (mode === 'headscan') {
const head = text.split('\n').slice(0, HEAD_SCAN_LINES + 8).join('\n');
assemblyChars += head.length;
Object.assign(cand, headScan(text));
}
candidates.push(cand);
}
return { candidates, assemblyChars };
}
export function runCut({ bundleDir, query, mode = 'headscan', k = Infinity, budget = AGENT_BUDGET }) {
const indexPath = join(bundleDir, 'index.md');
const indexMd = readFileSync(indexPath, 'utf8');
const entries = parseIndex(indexMd);
const { candidates, assemblyChars: scanChars } = buildCandidates({ bundleDir, mode, entries });
const queryTokens = tokenize(query);
for (const c of candidates) c.score = scoreCandidate(queryTokens, c);
const result = cutToBudget({
candidates, budget, k, applySupersession: mode === 'headscan',
});
const assemblyChars = indexMd.length + scanChars
+ result.delivered.reduce((n, d) => n + d.body.length, 0);
return { ...result, mode, queryTokens, indexChars: indexMd.length, scanChars, assemblyChars };
}
function main(argv) {
const arg = (name, fallback) => {
const i = argv.indexOf(`--${name}`);
return i === -1 ? fallback : argv[i + 1];
};
const bundleDir = arg('bundle');
const query = arg('query');
if (!bundleDir || !query) {
console.error('usage: cut.mjs --bundle <dir> --query "<question>" [--k N] [--budget N] [--mode index|headscan] [--json]');
process.exit(2);
}
const r = runCut({
bundleDir,
query,
mode: arg('mode', 'headscan'),
k: arg('k') ? Number(arg('k')) : Infinity,
budget: Number(arg('budget', AGENT_BUDGET)),
});
if (argv.includes('--json')) {
console.log(JSON.stringify({
...r, delivered: r.delivered.map((d) => ({ label: d.label, target: d.target, score: d.score })),
}, null, 2));
return;
}
// The receipt comes FIRST, so a reader cannot consume the cut without seeing
// what was held back.
console.log(`## Kutt-kvittering (OKF-bundle, modus: ${r.mode})`);
console.log(`- Levert: ${r.delivered.length} av ${r.denominator} dokumenter`);
console.log(`- \`[unread]\`: ${r.unread} av ${r.denominator} dokumenter i bundlen ble IKKE lest`);
if (r.superseded) console.log(`- Forkastet som \`status: erstattet\`: ${r.superseded}`);
if (r.statusUnknown) console.log(`- Uten \`status\`-felt (byggingen registrerte ingen status — ikke det samme som gjeldende): ${r.statusUnknown}`);
console.log(`- Agent-kontekst: ${r.agentChars} tegn av budsjett ${arg('budget', AGENT_BUDGET)}`);
console.log(`- Assemblering (hva skriptet leste): ${r.assemblyChars} tegn`);
console.log('');
for (const d of r.delivered) {
console.log(`### ${d.label} (score ${d.score})`);
console.log(d.body);
}
}
if (import.meta.url === `file://${process.argv[1]}`) main(process.argv.slice(2));