- Refaktor: overlap-kjerne flyttet til scripts/kb-eval/lib/sibling-overlap.mjs (bryter sirkulær import eval.mjs<->detect); detect re-eksporterer → B1-tester urørt - K10 = søsken-scope-ikke-overlapp, deterministisk cross-skill: perSkillSiblingOverlap + attachSiblingOverlap i eval.mjs. combined = boundaryTension + df-vektet leksikalsk; per-skill verdikt = verste søskenpar; terskel 7.0 (naturlig gap 7.42→6.67) - Empirisk (alle 5): eng+infra FAIL (7.42 mot hverandre), advisor/gov/sec PASS → eng↔infra-signal mater B3 merge/saner (ikke blokkering) - Gated baseline-regen via --write (descriptions urørt → judge K1/K4/K7/K8/K9 merget uendret, ikke fabrikkert); rubric K1-K10 - TDD: +9 tester (tests/kb-eval/test-k10-sibling-overlap.test.mjs), kb-eval 31→40 - 0 skriving til skills/. Suiter: validate 239 · kb-update 122 · kb-integrity 192/192
126 lines
6 KiB
JavaScript
126 lines
6 KiB
JavaScript
// tests/kb-eval/test-k10-sibling-overlap.test.mjs
|
|
// Unit tests for K10 — sibling-scope-non-overlap (Sesjon 15 / B2).
|
|
// Pure core: perSkillSiblingOverlap (lib/sibling-overlap.mjs) + the eval.mjs
|
|
// integration attachSiblingOverlap. Deterministic — no LLM judge.
|
|
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFileSync } from 'node:fs';
|
|
import { dirname, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
import {
|
|
perSkillSiblingOverlap,
|
|
computeOverlapFromInputs,
|
|
K10_OVERLAP_THRESHOLD,
|
|
} from '../../scripts/kb-eval/lib/sibling-overlap.mjs';
|
|
import { attachSiblingOverlap, splitFrontmatter, extractDescription } from '../../scripts/kb-eval/eval.mjs';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const PROMPTS_PATH = join(__dirname, '..', '..', 'scripts', 'kb-eval', 'data', 'k1-trigger-prompts.json');
|
|
const promptSet = JSON.parse(readFileSync(PROMPTS_PATH, 'utf8'));
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// perSkillSiblingOverlap — pure mapping pairs -> per-skill worst sibling
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test('perSkillSiblingOverlap — each skill takes its worst (max combined) pair', () => {
|
|
const pairs = [
|
|
{ pair: ['a', 'b'], combined: 7.5 },
|
|
{ pair: ['a', 'c'], combined: 3.0 },
|
|
{ pair: ['b', 'c'], combined: 6.0 },
|
|
];
|
|
const r = perSkillSiblingOverlap(pairs, { threshold: 7.0 });
|
|
assert.equal(r.a.maxCombined, 7.5);
|
|
assert.equal(r.a.worstSibling, 'b');
|
|
assert.equal(r.b.maxCombined, 7.5);
|
|
assert.equal(r.b.worstSibling, 'a');
|
|
assert.equal(r.c.maxCombined, 6.0);
|
|
assert.equal(r.c.worstSibling, 'b');
|
|
});
|
|
|
|
test('perSkillSiblingOverlap — pass iff maxCombined strictly below threshold', () => {
|
|
const pairs = [
|
|
{ pair: ['a', 'b'], combined: 7.5 },
|
|
{ pair: ['a', 'c'], combined: 3.0 },
|
|
{ pair: ['b', 'c'], combined: 6.0 },
|
|
];
|
|
const r = perSkillSiblingOverlap(pairs, { threshold: 7.0 });
|
|
assert.equal(r.a.pass, false); // 7.5 >= 7.0
|
|
assert.equal(r.b.pass, false); // 7.5 >= 7.0
|
|
assert.equal(r.c.pass, true); // 6.0 < 7.0
|
|
});
|
|
|
|
test('perSkillSiblingOverlap — combined exactly at threshold FAILS (strict <)', () => {
|
|
const r = perSkillSiblingOverlap([{ pair: ['x', 'y'], combined: 7.0 }], { threshold: 7.0 });
|
|
assert.equal(r.x.pass, false);
|
|
assert.equal(r.y.pass, false);
|
|
});
|
|
|
|
test('perSkillSiblingOverlap — threshold is configurable; records it on each verdict', () => {
|
|
const pairs = [{ pair: ['a', 'b'], combined: 7.5 }];
|
|
const lax = perSkillSiblingOverlap(pairs, { threshold: 8.0 });
|
|
assert.equal(lax.a.pass, true); // 7.5 < 8.0
|
|
assert.equal(lax.a.threshold, 8.0);
|
|
});
|
|
|
|
test('perSkillSiblingOverlap — default threshold is the rubric K10 constant (7.0)', () => {
|
|
assert.equal(K10_OVERLAP_THRESHOLD, 7.0);
|
|
const r = perSkillSiblingOverlap([{ pair: ['a', 'b'], combined: 7.5 }]);
|
|
assert.equal(r.a.threshold, 7.0);
|
|
assert.equal(r.a.pass, false);
|
|
});
|
|
|
|
test('perSkillSiblingOverlap — empty pairs yields empty verdict map', () => {
|
|
assert.deepEqual(perSkillSiblingOverlap([]), {});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Real-data path — eng + infra FAIL at 7.0; the other three PASS
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test('K10 on the real five — eng+infra FAIL (Azure-deployment boundary), rest PASS', () => {
|
|
// Build descriptions straight from the curated belongs_to graph + lexical
|
|
// surfaces via the same core the detector uses, so this asserts on live tokens.
|
|
const skills = ['ms-ai-advisor', 'ms-ai-engineering', 'ms-ai-governance', 'ms-ai-infrastructure', 'ms-ai-security'];
|
|
const descriptionsBySkill = {};
|
|
for (const s of skills) {
|
|
const md = join(__dirname, '..', '..', 'skills', s, 'SKILL.md');
|
|
descriptionsBySkill[s] = extractDescription(splitFrontmatter(readFileSync(md, 'utf8')).frontmatter);
|
|
}
|
|
const overlap = computeOverlapFromInputs(descriptionsBySkill, promptSet);
|
|
const k10 = perSkillSiblingOverlap(overlap.pairs, { threshold: 7.0 });
|
|
assert.equal(k10['ms-ai-engineering'].pass, false);
|
|
assert.equal(k10['ms-ai-infrastructure'].pass, false);
|
|
assert.equal(k10['ms-ai-engineering'].worstSibling, 'ms-ai-infrastructure');
|
|
assert.equal(k10['ms-ai-infrastructure'].worstSibling, 'ms-ai-engineering');
|
|
assert.equal(k10['ms-ai-advisor'].pass, true);
|
|
assert.equal(k10['ms-ai-governance'].pass, true);
|
|
assert.equal(k10['ms-ai-security'].pass, true);
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// attachSiblingOverlap — eval.mjs integration (injects K10 into deterministic)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test('attachSiblingOverlap — injects K10_siblingScopeOverlap into each skill deterministic block', () => {
|
|
const skills = [
|
|
{ name: 'a', deterministic: {}, judgeInputs: { description: 'Build RAG pipelines. Triggers on: "rag", "pipeline".' } },
|
|
{ name: 'b', deterministic: {}, judgeInputs: { description: 'Operate clusters. Triggers on: "operate", "cluster".' } },
|
|
];
|
|
const out = attachSiblingOverlap(skills, promptSet, { threshold: 7.0 });
|
|
assert.ok(out[0].deterministic.K10_siblingScopeOverlap, 'K10 present on skill a');
|
|
assert.ok(out[1].deterministic.K10_siblingScopeOverlap, 'K10 present on skill b');
|
|
assert.equal(typeof out[0].deterministic.K10_siblingScopeOverlap.maxCombined, 'number');
|
|
assert.equal(typeof out[0].deterministic.K10_siblingScopeOverlap.pass, 'boolean');
|
|
assert.equal(out[0].deterministic.K10_siblingScopeOverlap.threshold, 7.0);
|
|
});
|
|
|
|
test('attachSiblingOverlap — uses K10 default threshold when none given', () => {
|
|
const skills = [
|
|
{ name: 'a', deterministic: {}, judgeInputs: { description: 'x "p" "q" "r".' } },
|
|
{ name: 'b', deterministic: {}, judgeInputs: { description: 'y "s" "t" "u".' } },
|
|
];
|
|
const out = attachSiblingOverlap(skills, promptSet);
|
|
assert.equal(out[0].deterministic.K10_siblingScopeOverlap.threshold, K10_OVERLAP_THRESHOLD);
|
|
});
|