feat(ms-ai-architect): Sesjon 19 — B3 merge-apply (cross-skill + taksonomi-persist) [skip-docs]
Merge-apply = den tyngste destruktive op-en: eksekver en operatør-godkjent merge_skills-entry → faktisk cross-skill skills/-mutasjon. Tre mekanismer som sanitize/retire ikke trengte: kuratert relokasjon (ikke arkiv), taksonomi-persistering, retire-av-absorbert. - taxonomy.mjs (ny persist): saveTaxonomy (atomisk .tmp+rename, speiler saveDecisions) + ren applyCategoryReassignments (repointer KUN der eier === from; absent/allerede-flyttet = no-op). Lag-0 er normalt read-only; ENESTE writer = gated merge-apply. - applyApprovedAction merge-gren (skill-ops.mjs): re-plan fersk → revalidér (kollisjon → guardrail.ok=false → abort) → flytt absorbed-refs → absorber (moveFile; flyttingen ER bevaringen set-equality beviser) → persister taksonomi-repoint fra FERSK plan (drift-sikker) → arkivér absorbed-SKILL.md FØR rmdir → flipp ledger approved→applied. Description-forsoning forblir MANUELL (planner-kontrakt: apply rører aldri absorber-SKILL.md). archiveMove→moveFile (generisk: arkiv + kuratert flytt). - CLI apply-skill-op.mjs merge <absorber> <absorbed> — lookup via actionKey (merge_skills:<sortert par>); default PREVIEW, --apply = dobbel-gate. TDD, tmpdir-fixtures: 0 ekte skills/-mutasjon. Ny test-skill-ops-merge-apply (7) + 2 taksonomi-tester; fjernet foreldet merge-throws fra S18-fila. Tester: kb-eval 78→84, kb-update 137→139; validate 239 · kb-integrity 192/192 uendret. eval --json deterministisk uendret (K10 eng+infra FAIL, øvrige PASS). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4ac18a76c8
commit
de3a9a483b
6 changed files with 434 additions and 55 deletions
|
|
@ -7,12 +7,17 @@
|
|||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import {
|
||||
loadTaxonomy,
|
||||
saveTaxonomy,
|
||||
getSitemapPrefixes,
|
||||
getCategorySkill,
|
||||
makeClassifier,
|
||||
makePriorityFn,
|
||||
applyCategoryReassignments,
|
||||
} from '../../scripts/kb-update/lib/taxonomy.mjs';
|
||||
|
||||
const tax = loadTaxonomy();
|
||||
|
|
@ -123,3 +128,40 @@ test('getFilePriority — order: cost (critical) outranks governance (high)', ()
|
|||
// contains both 'cost' and 'governance' — critical rule is evaluated first
|
||||
assert.equal(prio('skills/x/references/cost-governance-tradeoffs.md'), 'critical');
|
||||
});
|
||||
|
||||
// --- (e) taxonomy persistence (Sesjon 19, B3 merge-apply) ---
|
||||
// merge-apply repoints every category owned by the absorbed skill to the
|
||||
// absorber and must PERSIST that to domain-taxonomy.json. Two new primitives:
|
||||
// a pure reassign helper + an atomic save mirroring saveDecisions.
|
||||
|
||||
test('applyCategoryReassignments — repoints only matching from->to; pure (no mutation)', () => {
|
||||
const before = JSON.parse(JSON.stringify(tax.category_skill));
|
||||
const next = applyCategoryReassignments(tax, [
|
||||
{ category: 'rag-architecture', from: 'ms-ai-engineering', to: 'ms-ai-advisor' }, // matches → repointed
|
||||
{ category: 'responsible-ai', from: 'ms-ai-NONMATCH', to: 'ms-ai-advisor' }, // current != from → no-op
|
||||
{ category: 'does-not-exist', from: 'whatever', to: 'ms-ai-advisor' }, // absent category → no-op
|
||||
]);
|
||||
assert.equal(next.category_skill['rag-architecture'], 'ms-ai-advisor', 'matching entry repointed');
|
||||
assert.equal(next.category_skill['responsible-ai'], 'ms-ai-governance', 'non-matching from is a no-op');
|
||||
assert.equal(next.category_skill['does-not-exist'], undefined, 'absent category stays absent');
|
||||
// purity: original object + its nested map are untouched, a fresh object is returned
|
||||
assert.deepEqual(tax.category_skill, before);
|
||||
assert.notEqual(next, tax);
|
||||
assert.notEqual(next.category_skill, tax.category_skill);
|
||||
});
|
||||
|
||||
test('saveTaxonomy + loadTaxonomy — atomic round-trip preserves content', () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'tax-save-'));
|
||||
try {
|
||||
const next = applyCategoryReassignments(tax, [
|
||||
{ category: 'rag-architecture', from: 'ms-ai-engineering', to: 'ms-ai-advisor' },
|
||||
]);
|
||||
saveTaxonomy(next, dir);
|
||||
const reloaded = loadTaxonomy(dir);
|
||||
assert.equal(getCategorySkill(reloaded, 'rag-architecture'), 'ms-ai-advisor', 'reassignment persisted');
|
||||
assert.equal(getCategorySkill(reloaded, 'responsible-ai'), 'ms-ai-governance', 'untouched entries survive');
|
||||
assert.equal(reloaded.version, tax.version, 'non-category_skill fields survive the round-trip');
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue