feat(ms-ai-architect): Sesjon 18 — B3 apply-path (sanitize+retire), merge→S19 [skip-docs]
Apply-path = den destruktive frontieren: eksekver en operatør-godkjent
(status:approved) ledger-entry → faktisk skills/-mutasjon. Staget per
operatør-beslutning til single-skill ops (sanitize+retire); merge-apply
isoleres til S19 (krever taksonomi-persistering + cross-skill flytt).
- revalidateApply (ren, skill-ops.mjs): idempotent revalidering mot fersk
re-plan — nekter ved status≠approved, op-mismatch, fersk guardrail≠ok,
eller drift (isDeepStrictEqual fresh vs approved guardrail-snapshot).
- applyApprovedAction (impur): arkiver-så-slett (rename skills/…→archive/…
atomisk; retire rmdir'er tom katalog sist) + flipp ledger approved→applied.
apply:false = preview (0 mutasjon). merge_skills → throw (S19).
- setActionStatus (ren, decisions-io.mjs): ledger status-transisjon, bevarer
targets+guardrail, audit-record beholdes.
- CLI apply-skill-op.mjs {list|sanitize|retire} — default PREVIEW, --apply =
dobbel-gate utover ledger-approved.
TDD, tmpdir-fixtures: 0 ekte skills/-mutasjon. Tester: kb-eval 66→78,
kb-update 132→137; validate 239 · kb-integrity 192/192 uendret.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f7b622b318
commit
4ac18a76c8
5 changed files with 603 additions and 2 deletions
|
|
@ -21,6 +21,7 @@ import {
|
|||
isActionDecided,
|
||||
recordAction,
|
||||
listActions,
|
||||
setActionStatus,
|
||||
} from '../../scripts/kb-update/lib/decisions-io.mjs';
|
||||
|
||||
function withTmp(fn) {
|
||||
|
|
@ -154,3 +155,61 @@ test('saveDecisions + loadDecisions — actions round-trip through disk', () =>
|
|||
assert.deepEqual(back, led);
|
||||
});
|
||||
});
|
||||
|
||||
// --- setActionStatus — pure status transition (Sesjon 18: gates the apply-path) ---
|
||||
// The apply-path flips an operator-approved entry approved -> applied AFTER a
|
||||
// successful skills/ mutation. setActionStatus is the one pure primitive that
|
||||
// records that transition; it preserves every other field (targets, guardrail).
|
||||
|
||||
test('setActionStatus — transitions status, preserves targets+guardrail, is pure', () => {
|
||||
const led = recordAction(createLedger(), { ...MERGE, status: 'approved' });
|
||||
const key = actionKey(MERGE);
|
||||
const next = setActionStatus(led, key, 'applied', '2026-06-21');
|
||||
|
||||
assert.equal(led.actions[key].status, 'approved', 'input ledger untouched (pure)');
|
||||
assert.notEqual(next, led);
|
||||
assert.equal(next.actions[key].status, 'applied');
|
||||
assert.deepEqual(next.actions[key].targets, MERGE.targets, 'targets preserved');
|
||||
assert.deepEqual(next.actions[key].guardrail, MERGE.guardrail, 'guardrail preserved');
|
||||
});
|
||||
|
||||
test('setActionStatus — advances updated_at to the transition date', () => {
|
||||
const led = recordAction(createLedger(), { ...MERGE, status: 'approved' });
|
||||
const next = setActionStatus(led, actionKey(MERGE), 'applied', '2026-06-21');
|
||||
assert.equal(next.actions[actionKey(MERGE)].decided_at, '2026-06-21', 'entry date advances');
|
||||
assert.equal(next.updated_at, '2026-06-21');
|
||||
});
|
||||
|
||||
test('setActionStatus — without a date, keeps the entry decided_at and ledger updated_at', () => {
|
||||
let led = recordAction(createLedger(), { ...MERGE, status: 'approved' }); // decided_at 2026-06-20
|
||||
const next = setActionStatus(led, actionKey(MERGE), 'applied');
|
||||
assert.equal(next.actions[actionKey(MERGE)].decided_at, '2026-06-20', 'no date -> keep original');
|
||||
});
|
||||
|
||||
test('setActionStatus — throws on an unknown action key (cannot transition a non-entry)', () => {
|
||||
const led = recordAction(createLedger(), { ...MERGE, status: 'approved' });
|
||||
assert.throws(() => setActionStatus(led, 'sanitize_skill:ghost', 'applied'), /no action|ghost/i);
|
||||
});
|
||||
|
||||
test('setActionStatus — leaves the URL-keyed decisions map and sibling actions untouched', () => {
|
||||
let led = recordDecision(createLedger(), 'https://learn.microsoft.com/x', {
|
||||
status: 'approved',
|
||||
decided_at: '2026-06-19',
|
||||
});
|
||||
led = recordAction(led, { ...MERGE, status: 'approved' });
|
||||
led = recordAction(led, {
|
||||
operation_type: 'sanitize_skill',
|
||||
status: 'approved',
|
||||
decided_at: '2026-06-20',
|
||||
targets: { skill: 'ms-ai-governance' },
|
||||
guardrail: { ok: true },
|
||||
});
|
||||
const next = setActionStatus(led, actionKey(MERGE), 'applied', '2026-06-21');
|
||||
|
||||
assert.equal(isDecided(next, 'https://learn.microsoft.com/x'), true, 'URL decision survives');
|
||||
assert.equal(
|
||||
next.actions['sanitize_skill:ms-ai-governance'].status,
|
||||
'approved',
|
||||
'sibling action untouched',
|
||||
);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue