ms-ai-architect/tests/kb-eval/test-k10-sibling-overlap.test.mjs
Kjell Tore Guttormsen ba597eb988 feat(ms-ai-architect): Sesjon 15 — B2 K10 søsken-scope-ikke-overlapp
- 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
2026-06-20 11:59:59 +02:00

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