// 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 }); } });