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:
Kjell Tore Guttormsen 2026-06-20 20:32:51 +02:00
commit 4ac18a76c8
5 changed files with 603 additions and 2 deletions

View file

@ -158,3 +158,29 @@ export function listActions(ledger, status = null) {
const all = Object.values(ledger.actions ?? {});
return status ? all.filter((a) => a.status === status) : all;
}
/**
* Transition a recorded action to a new status. Pure returns a new ledger,
* does not mutate. Every other field (targets, guardrail, note) is preserved.
* This is the apply-path's one write primitive (Sesjon 18): after a successful
* skills/ mutation it flips the operator-approved entry approved -> applied,
* keeping the entry as an audit record (policy A dedup still holds).
* @param {object} ledger
* @param {string} key from actionKey()
* @param {string} status new status (e.g. 'approved', 'applied')
* @param {string|null} [decided_at] when given, advances the entry date + updated_at
* @returns {object} new ledger
* @throws if no action exists under `key`
*/
export function setActionStatus(ledger, key, status, decided_at = null) {
const existing = ledger.actions?.[key];
if (!existing) throw new Error(`setActionStatus: no action recorded under key "${key}"`);
return {
...ledger,
updated_at: decided_at ?? ledger.updated_at,
actions: {
...ledger.actions,
[key]: { ...existing, status, decided_at: decided_at ?? existing.decided_at },
},
};
}