feat(ms-ai-architect): Sesjon 16 — B3 ledger action-lag + merge_skills dry-run-planner
Spor B / B3 start. Operatør-gated dry-run for skill-livssyklus, merge først.
- decisions-io.mjs: additivt skill-nøklet `actions`-lag ved siden av urørt
URL-nøklet `decisions` (14 URL-tester urørt = regresjonsbevis). version=1,
loadDecisions backfiller manglende actions-nøkkel. Nye rene fn: actionKey
(merge symmetrisk på paret), isActionDecided, recordAction (no-mutate),
listActions. Policy A gjelder også actions.
- scripts/kb-eval/lib/skill-ops.mjs: ren planMergeSkills → {entry,diff,guardrail},
leser ingen disk, anvender INGENTING. Guardrail = ingen kuratert verdi tapt
(count-invariant + set-equality + kollisjons-deteksjon; ok=false ved klobring).
Impur skall loadSkillRefs/runMergePlan + CLI plan-skill-op.mjs (gated --write).
- Ekte eng↔infra dry-run: guardrail OK (34+153=187, 0 kollisjoner), 153 fil-flytt
+ 7 taksonomi-reassign + retire eng; 0 skriving til skills/, ledger uendret
(retning er operatør/judge-valg, ikke pushet gjennom gaten).
- TDD: test-decisions-actions (10) + test-skill-ops-merge (10, inkl. byte-for-byte
skills/-snapshot som no-write-bevis). kb-eval 40→50, kb-update 122→132.
Regresjon grønn: validate 239, kb-integrity 192/192.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ba597eb988
commit
f03a0e08b2
5 changed files with 672 additions and 4 deletions
|
|
@ -17,21 +17,32 @@ const DEFAULT_DATA_DIR = join(__dirname, '..', 'data');
|
|||
|
||||
/**
|
||||
* Empty ledger scaffold.
|
||||
* @returns {{version: number, updated_at: string|null, decisions: object}}
|
||||
*
|
||||
* Two parallel collections, both written ONLY through the operator gate:
|
||||
* - decisions — URL-keyed (Spor A): which Microsoft Learn page belongs where.
|
||||
* - actions — skill-keyed (Spor B / B3): skill-lifecycle ops (merge_skills,
|
||||
* sanitize_skill, retire_skill, create_skill). Additive: version
|
||||
* stays 1, and a pre-action ledger on disk normalizes cleanly
|
||||
* (loadDecisions backfills the missing actions key).
|
||||
* @returns {{version: number, updated_at: string|null, decisions: object, actions: object}}
|
||||
*/
|
||||
export function createLedger() {
|
||||
return { version: 1, updated_at: null, decisions: {} };
|
||||
return { version: 1, updated_at: null, decisions: {}, actions: {} };
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the decision ledger from disk.
|
||||
* Load the decision ledger from disk. Backward-compatible: a ledger written
|
||||
* before the action layer existed (no `actions` key) is normalized to `{}` so
|
||||
* callers never have to guard against undefined.
|
||||
* @param {string} [dataDir] — defaults to ../data/ relative to lib/
|
||||
* @returns {object} parsed ledger or empty scaffold
|
||||
*/
|
||||
export function loadDecisions(dataDir = DEFAULT_DATA_DIR) {
|
||||
const path = join(dataDir, 'decisions.json');
|
||||
if (!existsSync(path)) return createLedger();
|
||||
return JSON.parse(readFileSync(path, 'utf8'));
|
||||
const led = JSON.parse(readFileSync(path, 'utf8'));
|
||||
if (!led.actions) led.actions = {};
|
||||
return led;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -84,3 +95,66 @@ export function recordDecision(ledger, url, decision) {
|
|||
export function filterUndecided(ledger, candidates) {
|
||||
return candidates.filter((c) => !isDecided(ledger, c.url));
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Skill-level action layer (Spor B / B3) — additive, mirrors the URL helpers.
|
||||
// Detection/transformation produce a PENDING action entry; only the gate writes
|
||||
// it. recordAction never mutates skills/ — it records a proposal in the ledger.
|
||||
// ===========================================================================
|
||||
|
||||
/**
|
||||
* Deterministic dedup key for a skill-lifecycle action. merge_skills is keyed
|
||||
* symmetrically on the pair (merging A<-B and B<-A dedup to the same key);
|
||||
* single-skill ops (sanitize/retire/create) are keyed by their one target.
|
||||
* @param {{operation_type: string, targets?: object}} entry
|
||||
* @returns {string}
|
||||
*/
|
||||
export function actionKey(entry) {
|
||||
const op = entry.operation_type;
|
||||
const t = entry.targets ?? {};
|
||||
if (op === 'merge_skills') {
|
||||
const pair = [t.absorber, t.absorbed].filter(Boolean).sort();
|
||||
return `merge_skills:${pair.join('+')}`;
|
||||
}
|
||||
const single = t.skill ?? t.name ?? t.absorber ?? '?';
|
||||
return `${op}:${single}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Has the operator already decided on this action? Policy A: any entry
|
||||
* (pending | approved | rejected) counts — mirrors isDecided for URLs.
|
||||
* @param {object} ledger
|
||||
* @param {string} key — from actionKey()
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isActionDecided(ledger, key) {
|
||||
return Boolean(ledger.actions && ledger.actions[key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a skill-lifecycle action. Pure — returns a new ledger, does not mutate.
|
||||
* Keyed by actionKey(entry). Leaves the URL-keyed decisions map untouched.
|
||||
* @param {object} ledger
|
||||
* @param {{operation_type: string, status: string, decided_at?: string|null,
|
||||
* targets?: object, guardrail?: object, note?: string}} entry
|
||||
* @returns {object} new ledger
|
||||
*/
|
||||
export function recordAction(ledger, entry) {
|
||||
const key = actionKey(entry);
|
||||
return {
|
||||
...ledger,
|
||||
updated_at: entry.decided_at ?? ledger.updated_at,
|
||||
actions: { ...(ledger.actions ?? {}), [key]: { ...entry } },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* List recorded actions, optionally filtered by status.
|
||||
* @param {object} ledger
|
||||
* @param {string|null} [status] — when given, keep only entries with this status
|
||||
* @returns {Array<object>}
|
||||
*/
|
||||
export function listActions(ledger, status = null) {
|
||||
const all = Object.values(ledger.actions ?? {});
|
||||
return status ? all.filter((a) => a.status === status) : all;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue