Implement AXIS parity UI and execution baseline
This commit is contained in:
265
web/workspace/configuration-startup.mjs
Normal file
265
web/workspace/configuration-startup.mjs
Normal file
@@ -0,0 +1,265 @@
|
||||
import { fnv1a64 } from "../worker/replay.mjs";
|
||||
import { WorkspaceRuntime, normalizeWorkspacePath } from "./runtime.mjs";
|
||||
import { parseIni } from "./ini-parser.mjs";
|
||||
import { resolveResources } from "./resource-resolver.mjs";
|
||||
import { preflightHalConfiguration } from "./hal-configuration.mjs";
|
||||
|
||||
export const CONFIGURATION_STARTUP_SCHEMA = "configuration-startup-runtime.v1";
|
||||
export const STARTUP_STATES = Object.freeze([
|
||||
"NEW", "MOUNTED", "PARSED", "RESOLVED", "CORE_CREATED", "HAL_READY", "TASK_READY", "BLOCKED",
|
||||
]);
|
||||
|
||||
export const STARTUP_ERRORS = Object.freeze({
|
||||
invalid_input: "startup_input_invalid",
|
||||
workspace: "startup_workspace_blocked",
|
||||
ini: "startup_ini_blocked",
|
||||
resources: "startup_resources_blocked",
|
||||
core: "startup_core_blocked",
|
||||
hal: "startup_hal_blocked",
|
||||
task: "startup_task_blocked",
|
||||
transition: "startup_transition_invalid",
|
||||
capability: "startup_capability_unsupported",
|
||||
});
|
||||
|
||||
const ALLOWED = Object.freeze({
|
||||
NEW: ["MOUNTED", "BLOCKED"],
|
||||
MOUNTED: ["PARSED", "BLOCKED"],
|
||||
PARSED: ["RESOLVED", "BLOCKED"],
|
||||
RESOLVED: ["CORE_CREATED", "BLOCKED"],
|
||||
CORE_CREATED: ["HAL_READY", "BLOCKED"],
|
||||
HAL_READY: ["TASK_READY", "BLOCKED"],
|
||||
TASK_READY: [],
|
||||
BLOCKED: [],
|
||||
});
|
||||
|
||||
function clone(value) { return structuredClone(value); }
|
||||
function text(value) { return typeof value === "string" && value.length > 0 ? value : null; }
|
||||
|
||||
function coreCall(core, name, ...args) {
|
||||
if (!core) return 0;
|
||||
if (args.some((value) => typeof value === "string") && typeof core.ccall === "function") {
|
||||
return core.ccall(name, "number", args.map((value) => typeof value === "string" ? "string" : "number"), args);
|
||||
}
|
||||
const direct = core[`_${name}`] ?? core[name];
|
||||
if (typeof direct === "function") return direct(...args);
|
||||
if (typeof core.ccall === "function") {
|
||||
const types = args.map((value) => typeof value === "string" ? "string" : "number");
|
||||
return core.ccall(name, "number", types, args);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function diagnostic(code, source = null, line = 0, extra = {}) {
|
||||
return { status: "blocked", code, source, line: Number.isSafeInteger(line) ? line : 0, ...extra };
|
||||
}
|
||||
|
||||
function asWorkspace(value) {
|
||||
if (value && typeof value.read === "function" && typeof value.list === "function") return value;
|
||||
if (value instanceof WorkspaceRuntime) return value;
|
||||
if (value && typeof value === "object" && value.files && typeof value.files === "object") return new WorkspaceRuntime(value);
|
||||
if (value && typeof value === "object") return new WorkspaceRuntime({ files: value });
|
||||
throw new Error(STARTUP_ERRORS.workspace);
|
||||
}
|
||||
|
||||
function halManifest(manifest = []) {
|
||||
return Array.isArray(manifest) ? manifest : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Deterministic configuration startup pipeline. A pipeline instance is single-use;
|
||||
* all externally visible data is returned by report() and can be hashed/replayed.
|
||||
*/
|
||||
export class ConfigurationStartupStateMachine {
|
||||
constructor({ workspace = null, files = null, entrypoint = null, variables = {}, maxDepth = 32, core = null, task = null, halManifest: manifest = [], coreCreated = false } = {}) {
|
||||
this.state = "NEW";
|
||||
this.workspace = workspace;
|
||||
this.files = files;
|
||||
this.entrypoint = entrypoint;
|
||||
this.variables = { ...variables };
|
||||
this.maxDepth = maxDepth;
|
||||
this.core = core;
|
||||
this.task = task;
|
||||
this.manifest = manifest;
|
||||
this.coreCreated = coreCreated === true;
|
||||
this.ini = null;
|
||||
this.resources = null;
|
||||
this.hal = null;
|
||||
this.diagnostics = [];
|
||||
this.transitions = [{ from: null, to: "NEW" }];
|
||||
this.coreResult = null;
|
||||
this.stateHash = null;
|
||||
}
|
||||
|
||||
transition(next) {
|
||||
if (!STARTUP_STATES.includes(next) || !ALLOWED[this.state]?.includes(next)) {
|
||||
throw new Error(`${STARTUP_ERRORS.transition}:${this.state}->${next}`);
|
||||
}
|
||||
const from = this.state;
|
||||
this.state = next;
|
||||
this.transitions.push({ from, to: next });
|
||||
}
|
||||
|
||||
block(code, source = null, line = 0, extra = {}) {
|
||||
const row = diagnostic(code, source, line, extra);
|
||||
this.diagnostics.push(row);
|
||||
if (this.state !== "BLOCKED") {
|
||||
const from = this.state;
|
||||
this.state = "BLOCKED";
|
||||
this.transitions.push({ from, to: "BLOCKED", diagnostic: clone(row) });
|
||||
}
|
||||
this.stateHash = this.hash();
|
||||
return this.report();
|
||||
}
|
||||
|
||||
mount() {
|
||||
if (this.state !== "NEW") return this.report();
|
||||
try {
|
||||
this.workspace = asWorkspace(this.workspace ?? this.files);
|
||||
if (!text(this.entrypoint)) throw Object.assign(new Error(STARTUP_ERRORS.invalid_input), { code: STARTUP_ERRORS.invalid_input });
|
||||
this.entrypoint = normalizeWorkspacePath(this.entrypoint);
|
||||
if (!this.entrypoint) throw Object.assign(new Error(STARTUP_ERRORS.invalid_input), { code: STARTUP_ERRORS.invalid_input });
|
||||
this.workspace.stat(`/workspace/${this.entrypoint}`);
|
||||
this.transition("MOUNTED");
|
||||
} catch (cause) {
|
||||
return this.block(cause.code ?? cause.message ?? STARTUP_ERRORS.workspace, this.entrypoint, cause.line ?? 0, { cause: String(cause?.message ?? cause) });
|
||||
}
|
||||
return this.report();
|
||||
}
|
||||
|
||||
parse() {
|
||||
if (this.state !== "MOUNTED") return this.report();
|
||||
try {
|
||||
this.ini = parseIni({ workspace: this.workspace, entrypoint: this.entrypoint, variables: this.variables, maxDepth: this.maxDepth });
|
||||
this.transition("PARSED");
|
||||
} catch (cause) {
|
||||
return this.block(cause.code ?? cause.message ?? STARTUP_ERRORS.ini, cause.source ?? this.entrypoint, cause.line ?? 0, {
|
||||
cause: String(cause?.message ?? cause),
|
||||
...(cause.variable ? { variable: cause.variable } : {}),
|
||||
});
|
||||
}
|
||||
return this.report();
|
||||
}
|
||||
|
||||
resolve() {
|
||||
if (this.state !== "PARSED") return this.report();
|
||||
try {
|
||||
this.resources = resolveResources({ workspace: this.workspace, entrypoint: this.entrypoint, ini: this.ini });
|
||||
} catch (cause) {
|
||||
return this.block(cause.code ?? cause.message ?? STARTUP_ERRORS.resources, this.entrypoint, cause.line ?? 0, { cause: String(cause?.message ?? cause) });
|
||||
}
|
||||
const blocked = this.resources.resources.filter((row) => row.status !== "resolved");
|
||||
if (blocked.length) {
|
||||
this.diagnostics.push(...blocked.map((row) => ({ ...row, status: "blocked" })));
|
||||
return this.block(STARTUP_ERRORS.resources, blocked[0].source ?? this.entrypoint, blocked[0].line ?? 0, { resources: clone(blocked) });
|
||||
}
|
||||
this.transition("RESOLVED");
|
||||
return this.report();
|
||||
}
|
||||
|
||||
createCore() {
|
||||
if (this.state !== "RESOLVED") return this.report();
|
||||
try {
|
||||
const result = this.coreCreated ? 0 : coreCall(this.core, "sim_create", 0, 0);
|
||||
this.coreResult = Number.isFinite(result) ? result : 0;
|
||||
if (result !== 0) return this.block(STARTUP_ERRORS.core, this.entrypoint, 0, { result });
|
||||
const loaded = coreCall(this.core, "sim_load_workspace", "/workspace");
|
||||
if (loaded !== 0) return this.block(STARTUP_ERRORS.core, this.entrypoint, 0, { result: loaded, operation: "sim_load_workspace" });
|
||||
this.transition("CORE_CREATED");
|
||||
} catch (cause) {
|
||||
return this.block(STARTUP_ERRORS.core, this.entrypoint, 0, { cause: String(cause?.message ?? cause) });
|
||||
}
|
||||
return this.report();
|
||||
}
|
||||
|
||||
prepareHal() {
|
||||
if (this.state !== "CORE_CREATED") return this.report();
|
||||
const records = [];
|
||||
for (const resource of this.resources.resources.filter((row) => row.status === "resolved" && row.type === "hal")) {
|
||||
let lines;
|
||||
try {
|
||||
const rawLines = this.workspace.read(`/workspace/${resource.path}`).split(/\r?\n/);
|
||||
lines = [];
|
||||
for (const rawLine of rawLines) {
|
||||
if (lines.length > 0 && /\\\s*$/.test(lines.at(-1))) lines[lines.length - 1] = `${lines.at(-1).replace(/\\\s*$/, "")} ${rawLine.trim()}`;
|
||||
else lines.push(rawLine);
|
||||
}
|
||||
} catch (cause) {
|
||||
return this.block(STARTUP_ERRORS.hal, resource.path, resource.line ?? 0, { cause: String(cause?.message ?? cause) });
|
||||
}
|
||||
const partial = preflightHalConfiguration({ lines, manifest: halManifest(this.manifest), variables: this.ini?.sections ?? {} });
|
||||
records.push(...partial.records.map((record) => ({ ...record, source: resource.path, resource_id: resource.resource_id })));
|
||||
}
|
||||
const blockedRecords = records.filter((record) => record.status === "blocked");
|
||||
this.hal = {
|
||||
schema: "hal-configuration-preflight.v1",
|
||||
status: blockedRecords.length ? "blocked" : "ready",
|
||||
records,
|
||||
record_count: records.length,
|
||||
blocked_count: blockedRecords.length,
|
||||
};
|
||||
if (this.hal.status !== "ready") {
|
||||
const first = this.hal.records.find((row) => row.status === "blocked");
|
||||
return this.block(STARTUP_ERRORS.hal, first?.source ?? this.entrypoint, first?.line ?? 0, { hal: clone(this.hal) });
|
||||
}
|
||||
this.transition("HAL_READY");
|
||||
return this.report();
|
||||
}
|
||||
|
||||
prepareTask() {
|
||||
if (this.state !== "HAL_READY") return this.report();
|
||||
try {
|
||||
if (this.task && typeof this.task.transition === "function") {
|
||||
const current = this.task.state?.state ?? this.task.state;
|
||||
if (current === "NEW" && typeof this.task.transition === "function") this.task.transition("READY");
|
||||
}
|
||||
this.transition("TASK_READY");
|
||||
} catch (cause) {
|
||||
return this.block(STARTUP_ERRORS.task, this.entrypoint, 0, { cause: String(cause?.message ?? cause) });
|
||||
}
|
||||
return this.report();
|
||||
}
|
||||
|
||||
run() {
|
||||
for (const step of ["mount", "parse", "resolve", "createCore", "prepareHal", "prepareTask"]) {
|
||||
if (this.state === "BLOCKED" || this.state === "TASK_READY") break;
|
||||
this[step]();
|
||||
}
|
||||
this.stateHash = this.hash();
|
||||
return this.report();
|
||||
}
|
||||
|
||||
hash() {
|
||||
const unsigned = this.unsignedReport();
|
||||
return fnv1a64(unsigned);
|
||||
}
|
||||
|
||||
unsignedReport() {
|
||||
return {
|
||||
schema: CONFIGURATION_STARTUP_SCHEMA,
|
||||
entrypoint: this.entrypoint,
|
||||
state: this.state,
|
||||
transitions: this.transitions,
|
||||
diagnostics: this.diagnostics,
|
||||
ini: this.ini ? { state_hash: this.ini.state_hash, graph: this.ini.graph, includes: this.ini.includes } : null,
|
||||
resources: this.resources ? { state_hash: this.resources.state_hash, resolved_count: this.resources.resolved_count, blocked_count: this.resources.blocked_count, resources: this.resources.resources } : null,
|
||||
hal: this.hal ? { schema: this.hal.schema, status: this.hal.status, record_count: this.hal.record_count, blocked_count: this.hal.blocked_count, records: this.hal.records } : null,
|
||||
core: this.coreResult === null ? null : { result: this.coreResult },
|
||||
};
|
||||
}
|
||||
|
||||
report() {
|
||||
const unsigned = this.unsignedReport();
|
||||
return { ...clone(unsigned), status: this.state === "TASK_READY" ? "ready" : "blocked", state_hash: fnv1a64(unsigned) };
|
||||
}
|
||||
}
|
||||
|
||||
export function startConfiguration(options = {}) {
|
||||
return new ConfigurationStartupStateMachine(options).run();
|
||||
}
|
||||
|
||||
export function createConfigurationStartup(options = {}) {
|
||||
return new ConfigurationStartupStateMachine(options);
|
||||
}
|
||||
|
||||
export const ConfigurationStartup = ConfigurationStartupStateMachine;
|
||||
export const STARTUP_SCHEMA = CONFIGURATION_STARTUP_SCHEMA;
|
||||
Reference in New Issue
Block a user