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:
parent
f1f27a4343
commit
c0f10618da
4 changed files with 785 additions and 0 deletions
144
scripts/okf-consume/cut.mjs
Normal file
144
scripts/okf-consume/cut.mjs
Normal 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));
|
||||
Loading…
Add table
Add a link
Reference in a new issue