import { fnv1a64 } from "../worker/replay.mjs"; export const CONFIGURATION_BATCH_SCHEMA = "configuration-startup-batch.v1"; const MAX_ENTRIES = 512; const TERMINAL = new Set(["ready", "blocked"]); const clone = (value) => structuredClone(value); const text = (value) => typeof value === "string" && value.length > 0; function diagnostic(row) { const blockedCode = row.startup_preflight === "blocked_unindexed_resource" || row.resource_status === "unindexed" ? "resource_unindexed" : row.startup_preflight === "blocked_unregistered_kinematics" ? "kinematics_unregistered" : row.unresolved_resources?.length ? "resource_unresolved" : "resource_missing"; return { status: "blocked", code: blockedCode, config_id: row.config_id, entrypoint: row.path, module: row.module ?? null, startup_preflight: row.startup_preflight, resource_status: row.resource_status, missing_resources: [...(row.missing_resources ?? [])].sort(), unresolved_resources: [...(row.unresolved_resources ?? [])].sort(), }; } function normalizeRow(row, index) { if (!row || typeof row !== "object" || Array.isArray(row)) throw new Error("configuration_batch_row"); if (!text(row.config_id) || !text(row.path) || !text(row.startup_preflight) || !text(row.resource_status)) throw new Error("configuration_batch_fields"); const terminal = row.startup_preflight === "static_ready" ? "ready" : "blocked"; if (!TERMINAL.has(terminal) || row.status !== terminal) throw new Error("configuration_batch_status"); if (!Array.isArray(row.missing_resources) || !Array.isArray(row.unresolved_resources)) throw new Error("configuration_batch_diagnostics"); return { config_id: row.config_id, path: row.path, module: row.module ?? null, family: row.family ?? null, source_id: row.source_id ?? row.config_id, line: Number.isSafeInteger(row.line) ? row.line : null, status: terminal, startup_preflight: row.startup_preflight, resource_status: row.resource_status, resource_rows: Number.isSafeInteger(row.resource_rows) ? row.resource_rows : 0, missing_resources: [...row.missing_resources].map(String).sort(), unresolved_resources: [...row.unresolved_resources].map(String).sort(), ...(terminal === "blocked" ? { diagnostic: diagnostic({ ...row, status: terminal }) } : {}), }; } export class ConfigurationBatchRuntime { constructor({ context_id = null } = {}) { this.contextId = context_id; this.reset(); } reset() { this.state = { schema: CONFIGURATION_BATCH_SCHEMA, context_id: this.contextId, entrypoints: [], counts: { total: 0, ready: 0, blocked: 0, startup_preflight: {}, resource_status: {} }, diagnostics: [], last_error: null }; return this.export(); } export() { const unsigned = clone(this.state); return { ...unsigned, state_hash: fnv1a64(unsigned) }; } hash() { return this.export().state_hash; } fail(code, detail = null) { this.state.last_error = code; return { status: "blocked", code, ...(detail ? { detail } : {}), state: this.export() }; } load(entries, { context_id = this.contextId } = {}) { if (!Array.isArray(entries) || entries.length === 0 || entries.length > MAX_ENTRIES) return this.fail("configuration_batch_capacity"); if (context_id !== null && !text(context_id)) return this.fail("configuration_batch_context"); const before = clone(this.state); try { const normalized = entries.map(normalizeRow); const ids = new Set(); for (const row of normalized) { if (ids.has(row.config_id)) throw new Error("configuration_batch_duplicate"); ids.add(row.config_id); } normalized.sort((left, right) => left.config_id.localeCompare(right.config_id)); const counts = { total: normalized.length, ready: normalized.filter((row) => row.status === "ready").length, blocked: normalized.filter((row) => row.status === "blocked").length, startup_preflight: {}, resource_status: {} }; for (const row of normalized) { counts.startup_preflight[row.startup_preflight] = (counts.startup_preflight[row.startup_preflight] ?? 0) + 1; counts.resource_status[row.resource_status] = (counts.resource_status[row.resource_status] ?? 0) + 1; } this.contextId = context_id; this.state = { schema: CONFIGURATION_BATCH_SCHEMA, context_id: this.contextId, entrypoints: normalized, counts, diagnostics: normalized.filter((row) => row.status === "blocked").map((row) => row.diagnostic), last_error: null }; return { status: "verified", command: "CONFIGURATION_BATCH", state: this.export() }; } catch (error) { this.state = before; return this.fail(error.message); } } restore(value) { if (!value || value.schema !== CONFIGURATION_BATCH_SCHEMA || !Array.isArray(value.entrypoints)) throw new Error("configuration_batch_restore_schema"); const unsigned = clone(value); delete unsigned.state_hash; if (value.state_hash !== fnv1a64(unsigned)) throw new Error("configuration_batch_restore_hash"); if (unsigned.entrypoints.length > MAX_ENTRIES) throw new Error("configuration_batch_restore_capacity"); this.state = unsigned; this.contextId = unsigned.context_id ?? this.contextId; return this.export(); } } export function classifyConfigurationBatch(entries, options = {}) { return new ConfigurationBatchRuntime(options).load(entries, options); } export { MAX_ENTRIES as CONFIGURATION_BATCH_CAPACITY };