perf(web): add idempotent batch verification fast paths
This commit is contained in:
60
tools/web/wbv2-batch-contract.mjs
Normal file
60
tools/web/wbv2-batch-contract.mjs
Normal file
@@ -0,0 +1,60 @@
|
||||
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));
|
||||
}
|
||||
Reference in New Issue
Block a user