Wire AXIS controls through operation gates

This commit is contained in:
2026-08-23 09:30:44 +08:00
parent a1edd8defe
commit a7fcec9d33
11 changed files with 367 additions and 17 deletions

View File

@@ -0,0 +1,46 @@
import { AXIS_COMMANDS, axisGate } from "./axis-contract.mjs";
export const COMMAND_ACTIONS = Object.freeze(Object.fromEntries(Object.entries(AXIS_COMMANDS).map(([action, command]) => [command, action])));
export class AxisGateError extends Error {
constructor(result) {
super(result.reason);
this.name = "AxisGateError";
this.code = result.reason;
this.gate = result;
}
}
export class AxisOperationController {
constructor(client) {
if (!client || typeof client.send !== "function") throw new Error("axis_operation_client_required");
this.client = client;
this.counter = 0;
this.trace = [];
}
nextOperationId(kind) {
this.counter += 1;
return `axis-${String(kind).toLowerCase().replace(/[^a-z0-9]+/g, "-")}-${String(this.counter).padStart(6, "0")}`;
}
async execute(kind, payload = {}, { state = {}, action = COMMAND_ACTIONS[kind] ?? null, gate = true } = {}) {
const operationId = this.nextOperationId(kind);
const gateResult = action ? axisGate(action, state) : { action: null, enabled: true, reason: "axis_unrestricted_internal_command" };
if (gate && !gateResult.enabled) {
this.trace.push(Object.freeze({ operation_id: operationId, kind, action, phase: "blocked", reason: gateResult.reason }));
throw new AxisGateError(gateResult);
}
this.trace.push(Object.freeze({ operation_id: operationId, kind, action, phase: "pending", reason: gateResult.reason }));
try {
const response = await this.client.send(kind, payload, operationId);
this.trace.push(Object.freeze({ operation_id: operationId, kind, action, phase: "result", response_kind: response?.kind ?? null }));
return Object.freeze({ operationId, response, gate: gateResult });
} catch (error) {
this.trace.push(Object.freeze({ operation_id: operationId, kind, action, phase: "error", reason: error?.message ?? String(error) }));
throw error;
}
}
exportTrace() { return structuredClone(this.trace); }
}