/** * DevLoop · unitRegistry * * Unit 状态机 (M-2 + P-1) 的唯一真源。所有 markCoding/markTesting/pass/fail/ * block/reset 都走 FSM 校验,非法转换抛 InvalidTransitionError。 * * 持久化:通过 docStore 读写 units.json(不直接 fs IO)。 * 事件:每次合法状态转换发 SSE `unit.state`(若 sseBroker 注入)。 * * 规格:docs/prd/newfunction/_spec/sdd-control.md · C-10 * docs/prd/newfunction/_spec/sdd-process.md · P-1 * docs/prd/newfunction/_spec/sdd-model.md · M-2 */ const docStore = require('./doc-store'); const pathResolver = require('./path-resolver'); class InvalidTransitionError extends Error { constructor(from, to, unitId) { super(`Invalid state transition for ${unitId}: ${from} → ${to}`); this.name = 'InvalidTransitionError'; this.from = from; this.to = to; this.unitId = unitId; } } // FSM 合法转换表(来自 sdd-process P-1) // from → allowed [to] 集合 const TRANSITIONS = { pending: ['coding'], coding: ['testing', 'failed'], testing: ['passed', 'failed'], failed: ['coding', 'blocked'], passed: ['pending'], // L2 reset blocked: ['pending'], // 人类干预后 }; function canTransition(from, to) { if (!TRANSITIONS[from]) return false; return TRANSITIONS[from].includes(to); } // sseBroker 是可选依赖。LoopController 启动时 attach 进来。 let sseBroker = null; function attachSseBroker(broker) { sseBroker = broker; } // ──────────────────────────────────────────────────────────────── // 读 // ──────────────────────────────────────────────────────────────── function _loadUnits(pid) { const u = docStore.readUnits(pid); if (!u) throw new Error(`units.json not found for ${pid}`); return u; } function list(pid) { return _loadUnits(pid).units; } function get(pid, unitId) { const u = list(pid).find(x => x.id === unitId); if (!u) throw new Error(`unit ${unitId} not found in project ${pid}`); return u; } function pending(pid) { return list(pid).filter(u => u.status === 'pending'); } function passed(pid) { return list(pid).filter(u => u.status === 'passed'); } function blocked(pid) { return list(pid).filter(u => u.status === 'blocked'); } function failed(pid) { return list(pid).filter(u => u.status === 'failed'); } function anyBlocked(pid) { return blocked(pid).length > 0; } /** * L2 闸门:全部 unit status=passed 且 contract_hash 与当前 SDD 一致。 * 这里只检查 status;contract_hash 一致性由 LoopController 调用前先 verifyContractHash * (这是更深的检查,需要读每个 unit 的 contract.json,太重不放这里)。 */ function l2GateOpen(pid) { const units = list(pid); if (units.length === 0) return false; return units.every(u => u.status === 'passed'); } // ──────────────────────────────────────────────────────────────── // 写(FSM 转换) // ──────────────────────────────────────────────────────────────── function _writeBack(pid, unitsJson) { docStore.writeUnits(pid, unitsJson, { skipValidation: false }); } function _transition(pid, unitId, to, extra = {}) { const unitsJson = _loadUnits(pid); const unit = unitsJson.units.find(u => u.id === unitId); if (!unit) throw new Error(`unit ${unitId} not found in project ${pid}`); const from = unit.status; if (!canTransition(from, to)) { throw new InvalidTransitionError(from, to, unitId); } unit.status = to; Object.assign(unit, extra); _writeBack(pid, unitsJson); if (sseBroker) { sseBroker.emit(pid, 'unit.state', { unit_id: unitId, from, to, retries: unit.retries, reason: extra.reason || null, }); } return unit; } function markCoding(pid, unitId) { return _transition(pid, unitId, 'coding'); } function markTesting(pid, unitId) { return _transition(pid, unitId, 'testing'); } function pass(pid, unitId, contractHash) { return _transition(pid, unitId, 'passed', { contract_hash: contractHash }); } function fail(pid, unitId, reason) { // failed 时累加 retries const unitsJson = _loadUnits(pid); const unit = unitsJson.units.find(u => u.id === unitId); if (!unit) throw new Error(`unit ${unitId} not found`); if (!canTransition(unit.status, 'failed')) { throw new InvalidTransitionError(unit.status, 'failed', unitId); } const from = unit.status; unit.status = 'failed'; unit.retries = (unit.retries || 0) + 1; unit.last_failure_reason = reason; _writeBack(pid, unitsJson); if (sseBroker) { sseBroker.emit(pid, 'unit.state', { unit_id: unitId, from, to: 'failed', retries: unit.retries, reason, }); } return unit; } function block(pid, unitId, reason) { return _transition(pid, unitId, 'blocked', { block_reason: reason }); } /** * 重置一批 Unit 到 pending(L2 修复后或 L3 增量后)。 * 允许 passed → pending 和 blocked → pending;其他状态保留(warning log)。 */ function reset(pid, unitIds) { const unitsJson = _loadUnits(pid); const reset_units = []; const skipped = []; for (const id of unitIds) { const u = unitsJson.units.find(x => x.id === id); if (!u) { skipped.push({ id, reason: 'not_found' }); continue; } const from = u.status; if (!canTransition(from, 'pending')) { skipped.push({ id, reason: `cannot_reset_from_${from}` }); continue; } u.status = 'pending'; u.retries = 0; // 重置 retry counter reset_units.push({ id, from }); } _writeBack(pid, unitsJson); if (sseBroker && reset_units.length) { for (const r of reset_units) { sseBroker.emit(pid, 'unit.state', { unit_id: r.id, from: r.from, to: 'pending', retries: 0, reason: 'reset', }); } } return { reset: reset_units, skipped }; } // ──────────────────────────────────────────────────────────────── // L3 增量 diff // ──────────────────────────────────────────────────────────────── /** * 对比新旧 units.json,返回 added/changed/removed 三类 unit id。 * - added : 新 list 有,旧 list 无 * - changed: 双方都有但 contract_hash 不同 * - removed: 旧 list 有,新 list 无 */ function diffUnits(oldUnitsList, newUnitsList) { const oldMap = new Map((oldUnitsList || []).map(u => [u.id, u])); const newMap = new Map((newUnitsList || []).map(u => [u.id, u])); const added = []; const changed = []; const removed = []; for (const [id, n] of newMap) { if (!oldMap.has(id)) { added.push(id); } else if (oldMap.get(id).contract_hash !== n.contract_hash) { changed.push(id); } } for (const [id] of oldMap) { if (!newMap.has(id)) removed.push(id); } return { added, changed, removed }; } // ──────────────────────────────────────────────────────────────── // 初始化 units.json 骨架 // ──────────────────────────────────────────────────────────────── /** * Architect 第一次生成 SDD 时,构造 units.json 骨架。 * 各 unit 自带默认 retry_budget=5、status=pending、retries=0。 */ function buildUnitsJson({ project_id, prd_rev, sdd_rev, units, l2_budget = 3 }) { return { project_id, prd_rev: String(prd_rev), sdd_rev: String(sdd_rev), l2_rounds: 0, l2_budget, units: units.map(u => ({ id: u.id, type: u.type, status: 'pending', depends_on: u.depends_on || [], retries: 0, retry_budget: u.retry_budget || 5, contract_hash: u.contract_hash, ...(u.last_run ? { last_run: u.last_run } : {}), })), }; } module.exports = { // 读 list, get, pending, passed, blocked, failed, anyBlocked, l2GateOpen, // 写 markCoding, markTesting, pass, fail, block, reset, // L3 diffUnits, // 工具 buildUnitsJson, canTransition, TRANSITIONS, // 错误 InvalidTransitionError, // 依赖注入 attachSseBroker, };