import crypto from "node:crypto"; export const COMMAND_BUDGET = Object.freeze({ maxCommands: 48, maxReadCommands: 16, }); /** Return the explicit source/RNA record IDs selected for one module batch. */ export function selectedRecordIds(module = {}) { const values = module.selectedRecordIds ?? module.coverageBindings ?? module.sourceUnits ?? []; return [...new Set((Array.isArray(values) ? values : []).filter((value) => typeof value === "string" && value.length > 0))]; } /** A batch identity is stable across retries and changes when selection changes. */ export function batchIdFor(task, module = {}) { const ids = selectedRecordIds(module); const moduleId = module.moduleId ?? "UNKNOWN_MODULE"; const digest = crypto.createHash("sha256").update(JSON.stringify(ids)).digest("hex").slice(0, 16); return module.batchId ?? `${task}:${moduleId}:${digest}`; } export function batchContract(task, module = {}) { const selected = selectedRecordIds(module); return { batchId: batchIdFor(task, module), selectedRecordIds: selected, selectedCount: selected.length, }; } export function completedBatch(manifest, task, module = {}) { const expected = batchContract(task, module); return manifest?.status === "done" && manifest.batchId === expected.batchId && JSON.stringify(manifest.selectedRecordIds ?? []) === JSON.stringify(expected.selectedRecordIds); } export function commandBudget(commands = []) { const values = Array.isArray(commands) ? commands.filter((value) => typeof value === "string") : []; const readCommands = values.filter((value) => /\b(?:cat|head|tail|sed|rg|find|ls|stat)\b/u.test(value)); return { maxCommands: COMMAND_BUDGET.maxCommands, maxReadCommands: COMMAND_BUDGET.maxReadCommands, commandCount: values.length, readCommandCount: readCommands.length, withinBudget: values.length <= COMMAND_BUDGET.maxCommands && readCommands.length <= COMMAND_BUDGET.maxReadCommands, }; } export function adaptiveBatchSize({ currentSize, history = [], minSize = 1, maxSize = currentSize, remaining = Number.MAX_SAFE_INTEGER }) { const size = Math.max(minSize, Math.min(maxSize, currentSize)); const recent = history.filter((item) => Number.isFinite(item.durationMs) && Number.isFinite(item.commands)).slice(-3); let next = size; if (recent.some((item) => item.timeout === true || item.durationMs > 900_000 || item.commands > COMMAND_BUDGET.maxCommands)) { next = Math.floor(size / 2); } else if (recent.length > 0 && recent.every((item) => item.durationMs < 300_000 && item.commands < 24 && item.focusedGateFailure !== true)) { next = size + 4; } return Math.max(minSize, Math.min(maxSize, remaining, next)); }