§4 Port 3 verifiseringskrav: «CT5 (sourcedness) ERSTATTER K8s rolle». K8 var LLM-judge (sample 5 ref-filer, ikke-deterministisk); CT5 er samme signal gjort deterministisk + hel-korpus fra Port 1-kontrakten. Per ref-kb-direction-note §45 MÅ de ikke leve parallelt (dobbelttelling + divergerende dashbord) — så K8 er fjernet, ikke duplisert. eval.mjs: checkCT5(refFiles) — blant filer som deklarerer type:reference, andel med **Source:** (template/methodology/regulatory ekskludert fra nevneren). Wiret som deterministic.CT5_sourcedness. parseTypeHeader/parseSourceHeader importert fra kb-update/lib/kb-headers (tillatt retning). skill-score.mjs: K8-kriteriet (source:judge) → CT5 (source:det, vekt 1). available:false når referenceFiles=0 → droppet fra vektet snitt → tanker IKKE scoren på umigrert korpus (alarm-tretthet unngått, direction-note §45). judge-prompt.md: K8 fjernet fra rubrikk/instruksjon/JSON (judgen gjør nå K1/K4/K7/K9). Ekte kjøring: alle 5 skills CT5 dormant (referenceFiles:0, umigrert), scorer uendret (91-96, 0 under mål). CT5 aktiveres deterministisk når Spor 1 migrerer. TDD (Iron Law): 5 checkCT5 + 3 CT5-integrasjon; død K8-fixture-data ryddet. Suite 620/620. Plugin-validering 239/0/0.
102 lines
5 KiB
JavaScript
102 lines
5 KiB
JavaScript
// tests/kb-eval/test-apply-score-cache.test.mjs
|
|
// Spor D STEG 1 (S42): apply-skill-op.mjs refreshes the WHOLE-CORPUS score-cache
|
|
// after a successful mutation, so the SessionStart surfacing (STEG C) reflects the
|
|
// new disk immediately instead of going stale until the next manual
|
|
// `score-skill.mjs --write`.
|
|
//
|
|
// refreshScoreCache(report, {cachePath, generatedAt}) is the pure-scoring/impure-write
|
|
// seam: scoreReport + buildScoreCache (pure) then a single writeFileSync. The cache is
|
|
// WHOLE-CORPUS (independent of WHICH skill changed) because retire drops a skill and
|
|
// create/merge/sanitize shift sibling K10 — every applied mutation must regenerate it.
|
|
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFileSync, rmSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { refreshScoreCache, SCORE_CACHE_FILE } from '../../scripts/kb-eval/apply-skill-op.mjs';
|
|
import { scoreReport, buildScoreCache } from '../../scripts/kb-eval/lib/skill-score.mjs';
|
|
|
|
// A synthetic skill whose deterministic + judge criteria all pass; K10 is
|
|
// parametrizable so one helper drives both the OK and the below-target cases.
|
|
function skill(name, { k10Pass = true, k10Combined = 1, worst = 'other' } = {}) {
|
|
return {
|
|
name,
|
|
deterministic: {
|
|
K2_descriptionFormat: { pass: true },
|
|
K3_bodyLength: { bodyLines: 100, pass: true },
|
|
K5_progressiveDisclosure: { namedRatio: 0.5, pass: true },
|
|
K6_routingTable: { pass: true },
|
|
refCountConsistency: { consistent: true },
|
|
K10_siblingScopeOverlap: { maxCombined: k10Combined, pass: k10Pass, threshold: 7, worstSibling: worst },
|
|
N1_nameValidity: { pass: true },
|
|
N2_descriptionLength: { pass: true },
|
|
N3_refNestingDepth: { pass: true },
|
|
N4_refToc: { ratio: 1, pass: true, withToc: 1, largeFiles: 1 },
|
|
N5_forwardSlashPaths: { pass: true },
|
|
},
|
|
judge: {
|
|
K1_triggerPrecision: { precision: 1, pass: true },
|
|
K4_noDuplication: { score: 5, pass: true },
|
|
K9_noTimeSensitive: { pass: true, findings: [] },
|
|
K7_imperativeStyle: { ratio: 1, pass: true },
|
|
},
|
|
};
|
|
}
|
|
|
|
test('SCORE_CACHE_FILE points at the surfacing cache (scripts/kb-eval/data/skill-score-report.json)', () => {
|
|
assert.ok(
|
|
SCORE_CACHE_FILE.endsWith(join('scripts', 'kb-eval', 'data', 'skill-score-report.json')),
|
|
`cache must be the same file STEG C surfacing reads; got: ${SCORE_CACHE_FILE}`,
|
|
);
|
|
});
|
|
|
|
test('refreshScoreCache — persists exactly buildScoreCache(scoreReport(report)) and returns it', () => {
|
|
const out = join(tmpdir(), `apply-score-cache-${process.pid}.json`);
|
|
try {
|
|
const report = { skills: [skill('alpha'), skill('beta', { k10Pass: false, k10Combined: 9, worst: 'alpha' })] };
|
|
const returned = refreshScoreCache(report, { cachePath: out, generatedAt: '2026-06-23' });
|
|
|
|
const onDisk = JSON.parse(readFileSync(out, 'utf8'));
|
|
const expected = buildScoreCache(scoreReport(report, { target: 90 }), { generatedAt: '2026-06-23' });
|
|
assert.deepEqual(onDisk, expected, 'persisted cache must equal buildScoreCache(scoreReport(report))');
|
|
assert.deepEqual(returned, expected, 'return value mirrors what was written');
|
|
|
|
// whole-corpus + compact + below[] as names
|
|
assert.equal(onDisk.scored.length, 2);
|
|
assert.ok(!('criteria' in onDisk.scored[0]), 'cache stays compact (no verbose criteria[])');
|
|
assert.deepEqual(onDisk.below, ['beta'], 'the K10-failing skill surfaces as below-target');
|
|
assert.equal(onDisk.generatedAt, '2026-06-23');
|
|
} finally {
|
|
rmSync(out, { force: true });
|
|
}
|
|
});
|
|
|
|
test('refreshScoreCache — OVERWRITES (does not merge): a retire shrinks the cache', () => {
|
|
const out = join(tmpdir(), `apply-score-cache-retire-${process.pid}.json`);
|
|
try {
|
|
// Corpus BEFORE: 3 skills.
|
|
refreshScoreCache({ skills: [skill('alpha'), skill('beta'), skill('gamma')] }, { cachePath: out, generatedAt: '2026-06-23' });
|
|
assert.equal(JSON.parse(readFileSync(out, 'utf8')).scored.length, 3);
|
|
|
|
// A retire drops 'gamma' -> refresh with the smaller corpus. The cache is
|
|
// overwritten whole-corpus, so the retired skill must not linger and re-surface.
|
|
refreshScoreCache({ skills: [skill('alpha'), skill('beta')] }, { cachePath: out, generatedAt: '2026-06-23' });
|
|
const after = JSON.parse(readFileSync(out, 'utf8'));
|
|
assert.equal(after.scored.length, 2, 'cache overwritten, not merged');
|
|
assert.ok(!after.scored.some((s) => s.name === 'gamma'), 'retired skill dropped from the surfacing cache');
|
|
} finally {
|
|
rmSync(out, { force: true });
|
|
}
|
|
});
|
|
|
|
test('refreshScoreCache — generatedAt defaults to an ISO date (today) when not injected', () => {
|
|
const out = join(tmpdir(), `apply-score-cache-date-${process.pid}.json`);
|
|
try {
|
|
refreshScoreCache({ skills: [skill('alpha')] }, { cachePath: out });
|
|
const onDisk = JSON.parse(readFileSync(out, 'utf8'));
|
|
assert.match(onDisk.generatedAt, /^\d{4}-\d{2}-\d{2}$/, 'impure layer stamps today as YYYY-MM-DD');
|
|
} finally {
|
|
rmSync(out, { force: true });
|
|
}
|
|
});
|