import { MAX_OPS, type HiveApplyResult, type HiveCatalog, type HiveFrame, type HiveOp, type HivePart, type HiveState, } from "./types"; import { validateHiveState } from "./validate"; // Port of ApiCore-Backend/src/hive/hive-ops.ts: same semantics, same reason // codes. Checked against the shared fixtures in ApiCore-Backend/test/fixtures. interface Location { part: HivePart; list: HivePart[]; index: number; isInsert: boolean; } /** Result of one op on the working copy: a reason string, or null on success. */ type OpOutcome = string | null; function locate(state: HiveState, id: unknown): Location | null { for (let index = 0; index < state.stack.length; index++) { const part = state.stack[index]; if (part.id === id) return { part, list: state.stack, index, isInsert: false }; const inserts = part.inserts ?? []; for (let i = 0; i < inserts.length; i++) { if (inserts[i].id === id) return { part: inserts[i], list: inserts, index: i, isInsert: true }; } } return null; } function missing(id: unknown): string { return `MISSING_ID: no part with id ${String(id)}`; } function clamp(value: unknown, max: number): number { if (typeof value !== "number" || Number.isNaN(value)) return max; return Math.min(Math.max(Math.trunc(value), 0), max); } /** Catalog membership and active check for a part to be added (and its inserts). */ function checkAddable(part: HivePart, catalog: HiveCatalog): OpOutcome { if (typeof part !== "object" || part === null) return "MISSING_ID: part is not an object"; const type = catalog.partTypes.get(part.partTypeId); if (!type) return `FOREIGN_PART_TYPE: ${String(part.partTypeId)} is not in this system`; if (!type.active) return `INACTIVE_PART_TYPE: ${type.id} is inactive`; for (const insert of part.inserts ?? []) { const nested = checkAddable(insert, catalog); if (nested) return nested; } return null; } /** Deep copy of an incoming part without empty `inserts` arrays. */ function normalisedCopy(part: HivePart): HivePart { const copy = structuredClone(part); if (Array.isArray(copy.inserts)) { if (copy.inserts.length === 0) delete copy.inserts; else copy.inserts = copy.inserts.map(normalisedCopy); } return copy; } type OpOf = Extract; function applyAddPart(state: HiveState, op: OpOf<"addPart">, catalog: HiveCatalog): OpOutcome { const problem = checkAddable(op.part, catalog); if (problem) return problem; state.stack.splice(clamp(op.at, state.stack.length), 0, normalisedCopy(op.part)); return null; } function applyAddInsert(state: HiveState, op: OpOf<"addInsert">, catalog: HiveCatalog): OpOutcome { const host = locate(state, op.hostId); if (!host || host.isInsert) return missing(op.hostId); const problem = checkAddable(op.part, catalog); if (problem) return problem; host.part.inserts = [...(host.part.inserts ?? []), normalisedCopy(op.part)]; return null; } function applyRemovePart(state: HiveState, op: OpOf<"removePart">): OpOutcome { const found = locate(state, op.id); if (!found) return missing(op.id); found.list.splice(found.index, 1); if (found.isInsert) { const host = state.stack.find((p) => p.inserts === found.list); if (host && found.list.length === 0) delete host.inserts; } return null; } function applyMovePart(state: HiveState, op: OpOf<"movePart">): OpOutcome { const found = locate(state, op.id); if (!found) return missing(op.id); if (found.isInsert) return `RULE_INSERT_HOST: ${op.id} is an insert and cannot be moved`; const [moved] = state.stack.splice(found.index, 1); state.stack.splice(clamp(op.to, state.stack.length), 0, moved); return null; } function applySetState(state: HiveState, op: OpOf<"setState">): OpOutcome { const found = locate(state, op.id); if (!found) return missing(op.id); found.part.state = { ...found.part.state, [op.key]: op.value }; return null; } function locateBox(state: HiveState, boxId: unknown): Location | string { const found = locate(state, boxId); if (!found) return missing(boxId); if (found.isInsert || !Array.isArray(found.part.frames)) { return `FRAMES_LENGTH: ${String(boxId)} is not a box with frames`; } return found; } function applySetFrame(state: HiveState, op: OpOf<"setFrame">): OpOutcome { const box = locateBox(state, op.boxId); if (typeof box === "string") return box; const frames = box.part.frames as HiveFrame[]; if (!Number.isInteger(op.slot) || op.slot < 1 || op.slot > frames.length) { return `FRAMES_LENGTH: slot ${String(op.slot)} is outside 1..${frames.length}`; } // name: undefined keeps the current name, null removes it. const current = frames[op.slot - 1]; const name = op.name === undefined ? current.name : (op.name ?? undefined); frames[op.slot - 1] = name === undefined ? { slot: op.slot, type: op.type } : { slot: op.slot, type: op.type, name }; return null; } function applySetFrames(state: HiveState, op: OpOf<"setFrames">): OpOutcome { const box = locateBox(state, op.boxId); if (typeof box === "string") return box; box.part.frames = structuredClone(op.frames); return null; } function applySetLabel(state: HiveState, op: OpOf<"setLabel">): OpOutcome { const found = locate(state, op.id); if (!found) return missing(op.id); if (op.label === null) delete found.part.label; else found.part.label = op.label; return null; } function applyOne(state: HiveState, op: HiveOp, catalog: HiveCatalog): OpOutcome { switch (op?.op) { case "addPart": return applyAddPart(state, op, catalog); case "addInsert": return applyAddInsert(state, op, catalog); case "removePart": return applyRemovePart(state, op); case "movePart": return applyMovePart(state, op); case "setState": return applySetState(state, op); case "setFrame": return applySetFrame(state, op); case "setFrames": return applySetFrames(state, op); case "setLabel": return applySetLabel(state, op); default: return "UNKNOWN_OP: unsupported operation"; } } /** * Applies ops in order and validates the whole state after each one. * Never mutates its inputs: it works on a private deep copy. */ export function applyHiveOps(state: HiveState, ops: HiveOp[], catalog: HiveCatalog): HiveApplyResult { if (ops.length > MAX_OPS) { return { ok: false, opIndex: MAX_OPS, reason: `LIMIT_OPS: at most ${MAX_OPS} operations per request`, }; } const working = structuredClone(state); for (let opIndex = 0; opIndex < ops.length; opIndex++) { const failure = applyOne(working, ops[opIndex], catalog) ?? validateHiveState(working, catalog)[0] ?? null; if (failure) return { ok: false, opIndex, reason: failure }; } return { ok: true, state: working }; }