Files
LWLT-AIBOT/chrome-extension/ltjt-order-assistant/operation-timing.js

336 lines
12 KiB
JavaScript

(function initOperationTiming(root, factory) {
const api = factory();
if (typeof module === 'object' && module.exports) module.exports = api;
root.LTJTOperationTiming = api;
})(typeof self !== 'undefined' ? self : globalThis, function createOperationTiming() {
'use strict';
const SCHEMA_VERSION = 'ltjt-operation-timing-v1';
const MAX_STAGES = 64;
const MAX_DETAILS_PER_STAGE = 48;
const MAX_COUNTERS = 24;
const MAX_DURATION_MS = 7 * 24 * 60 * 60 * 1000;
const CODE_PATTERN = /^[a-z][a-z0-9_.:-]{0,79}$/i;
const TERMINAL_STATUSES = new Set([
'completed',
'dry_run',
'blocked',
'failed',
'cancelled',
'saved_unverified',
'execution_uncertain',
'reconciliation_pending'
]);
function safeCode(value, fallback = '') {
const normalized = String(value || '').trim();
return CODE_PATTERN.test(normalized) ? normalized : fallback;
}
function safeDuration(value) {
const number = Number(value);
if (!Number.isFinite(number) || number <= 0) return 0;
return Math.min(MAX_DURATION_MS, Math.round(number));
}
function safeTimestamp(value, fallback) {
const number = Number(value);
return Number.isFinite(number) && number > 0 ? number : fallback;
}
function sanitizeCounters(value) {
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
const output = {};
for (const [key, entry] of Object.entries(value).slice(0, MAX_COUNTERS)) {
const safeKey = safeCode(key);
const number = Number(entry);
if (!safeKey || !Number.isFinite(number) || number < 0) continue;
output[safeKey] = Math.min(Number.MAX_SAFE_INTEGER, Math.round(number));
}
return Object.keys(output).length ? output : undefined;
}
function sanitizeDetail(value) {
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
const step = safeCode(value.step);
if (!step) return null;
const counters = sanitizeCounters(value.counters);
return {
step,
status: safeCode(value.status, 'completed'),
duration_ms: safeDuration(value.duration_ms),
...(counters ? { counters } : {})
};
}
function mergeDetails(existing = [], incoming = []) {
const output = [];
let truncated = 0;
const add = (candidate) => {
const detail = sanitizeDetail(candidate);
if (!detail) return;
const matched = output.find((item) => item.step === detail.step);
if (matched) {
matched.duration_ms = safeDuration(matched.duration_ms + detail.duration_ms);
if (matched.status !== detail.status) matched.status = 'mixed';
if (detail.counters) {
matched.counters = matched.counters || {};
for (const [key, value] of Object.entries(detail.counters)) {
matched.counters[key] = Math.min(
Number.MAX_SAFE_INTEGER,
Math.round(Number(matched.counters[key] || 0) + Number(value || 0))
);
}
}
return;
}
if (output.length >= MAX_DETAILS_PER_STAGE) {
truncated += 1;
return;
}
output.push(detail);
};
for (const detail of Array.isArray(existing) ? existing : []) add(detail);
for (const detail of Array.isArray(incoming) ? incoming : []) add(detail);
return { details: output, truncated };
}
function sanitizeStage(value) {
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
const stage = safeCode(value.stage);
if (!stage) return null;
const merged = mergeDetails([], value.details);
const truncatedDetailCount = Math.max(
0,
Math.round(Number(value.truncated_detail_count || 0)) + merged.truncated
);
return {
stage,
status: safeCode(value.status, 'unknown'),
duration_ms: safeDuration(value.duration_ms),
...(merged.details.length ? { details: merged.details } : {}),
...(truncatedDetailCount ? { truncated_detail_count: truncatedDetailCount } : {})
};
}
function sanitizeSnapshot(value) {
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
if (value.schema_version !== SCHEMA_VERSION) return null;
const stages = (Array.isArray(value.stages) ? value.stages : [])
.map(sanitizeStage)
.filter(Boolean)
.slice(0, MAX_STAGES);
const truncatedStageCount = Math.max(0, Math.round(Number(value.truncated_stage_count || 0)));
return {
schema_version: SCHEMA_VERSION,
status: safeCode(value.status, 'unknown'),
total_ms: safeDuration(value.total_ms),
stage_count: Math.max(stages.length, Math.round(Number(value.stage_count || stages.length))),
stages,
...(truncatedStageCount ? { truncated_stage_count: truncatedStageCount } : {})
};
}
function createState({ now_ms, started_at_ms, stage = 'auto_executor', status = 'accepted' } = {}) {
const now = safeTimestamp(now_ms, Date.now());
const startedAt = Math.min(now, safeTimestamp(started_at_ms, now));
return {
schema_version: SCHEMA_VERSION,
base_total_ms: 0,
started_at_ms: startedAt,
active_stage: safeCode(stage, 'auto_executor'),
active_stage_started_at_ms: startedAt,
active_status: safeCode(status, 'accepted'),
active_details: [],
active_truncated_detail_count: 0,
stages: [],
truncated_stage_count: 0,
final_status: ''
};
}
function stateFromSnapshot(snapshotValue, update, now) {
const snapshot = sanitizeSnapshot(snapshotValue);
if (!snapshot) return null;
const updatedAt = safeTimestamp(update.updated_at_ms, now);
const gap = Math.max(0, now - updatedAt);
const terminal = TERMINAL_STATUSES.has(snapshot.status);
if (terminal) {
return {
schema_version: SCHEMA_VERSION,
base_total_ms: snapshot.total_ms,
started_at_ms: now,
active_stage: '',
active_stage_started_at_ms: now,
active_status: '',
active_details: [],
active_truncated_detail_count: 0,
stages: snapshot.stages,
truncated_stage_count: snapshot.truncated_stage_count || 0,
final_status: snapshot.status
};
}
const active = snapshot.stages[snapshot.stages.length - 1] || null;
const activeDuration = safeDuration(active?.duration_ms) + gap;
return {
schema_version: SCHEMA_VERSION,
base_total_ms: 0,
started_at_ms: Math.max(0, now - snapshot.total_ms - gap),
active_stage: safeCode(active?.stage, safeCode(update.stage, 'auto_executor')),
active_stage_started_at_ms: Math.max(0, now - activeDuration),
active_status: safeCode(active?.status, safeCode(update.status, 'running')),
active_details: Array.isArray(active?.details) ? active.details : [],
active_truncated_detail_count: Math.max(0, Math.round(Number(active?.truncated_detail_count || 0))),
stages: active ? snapshot.stages.slice(0, -1) : snapshot.stages,
truncated_stage_count: snapshot.truncated_stage_count || 0,
final_status: ''
};
}
function sanitizeState(value, update, now) {
if (!value || typeof value !== 'object' || value.schema_version !== SCHEMA_VERSION) {
return stateFromSnapshot(update.existing_timing, update, now)
|| createState({
now_ms: now,
started_at_ms: update.started_at_ms,
stage: update.stage,
status: update.status
});
}
const stages = (Array.isArray(value.stages) ? value.stages : [])
.map(sanitizeStage)
.filter(Boolean)
.slice(-MAX_STAGES);
const merged = mergeDetails([], value.active_details);
return {
schema_version: SCHEMA_VERSION,
base_total_ms: safeDuration(value.base_total_ms),
started_at_ms: Math.min(now, safeTimestamp(value.started_at_ms, now)),
active_stage: safeCode(value.active_stage),
active_stage_started_at_ms: Math.min(now, safeTimestamp(value.active_stage_started_at_ms, now)),
active_status: safeCode(value.active_status, 'running'),
active_details: merged.details,
active_truncated_detail_count: Math.max(
0,
Math.round(Number(value.active_truncated_detail_count || 0)) + merged.truncated
),
stages,
truncated_stage_count: Math.max(0, Math.round(Number(value.truncated_stage_count || 0))),
final_status: safeCode(value.final_status)
};
}
function appendClosedStage(state, stage) {
// Reserve one public slot for the currently active stage so the newest
// work is never hidden by the bounded history.
if (state.stages.length >= MAX_STAGES - 1) {
state.stages.shift();
state.truncated_stage_count += 1;
}
state.stages.push(stage);
}
function closeActiveStage(state, now, status) {
if (!state.active_stage) return;
appendClosedStage(state, {
stage: state.active_stage,
status: safeCode(status, state.active_status || 'completed'),
duration_ms: safeDuration(now - state.active_stage_started_at_ms),
...(state.active_details.length ? { details: state.active_details } : {}),
...(state.active_truncated_detail_count
? { truncated_detail_count: state.active_truncated_detail_count }
: {})
});
state.active_stage = '';
state.active_stage_started_at_ms = now;
state.active_status = '';
state.active_details = [];
state.active_truncated_detail_count = 0;
}
function openStage(state, stage, status, now) {
state.active_stage = safeCode(stage, 'unknown_stage');
state.active_stage_started_at_ms = now;
state.active_status = safeCode(status, 'running');
state.active_details = [];
state.active_truncated_detail_count = 0;
state.final_status = '';
}
function snapshot(stateValue, nowValue = Date.now()) {
const now = safeTimestamp(nowValue, Date.now());
const state = sanitizeState(stateValue, {}, now);
const stages = state.stages.map((stage) => ({ ...stage }));
if (state.active_stage) {
stages.push({
stage: state.active_stage,
status: state.active_status || 'running',
duration_ms: safeDuration(now - state.active_stage_started_at_ms),
...(state.active_details.length ? { details: state.active_details } : {}),
...(state.active_truncated_detail_count
? { truncated_detail_count: state.active_truncated_detail_count }
: {})
});
}
const stageCount = stages.length + state.truncated_stage_count;
return sanitizeSnapshot({
schema_version: SCHEMA_VERSION,
status: state.active_stage ? (state.active_status || 'running') : (state.final_status || 'unknown'),
total_ms: state.base_total_ms + Math.max(0, now - state.started_at_ms),
stage_count: stageCount,
stages,
truncated_stage_count: state.truncated_stage_count
});
}
function advance(stateValue, update = {}) {
const now = safeTimestamp(update.now_ms, Date.now());
const state = sanitizeState(stateValue, update, now);
const nextStage = safeCode(update.stage, state.active_stage || 'unknown_stage');
const nextStatus = safeCode(update.status, state.active_status || 'running');
const incoming = mergeDetails([], update.details);
if (state.active_stage) {
const merged = mergeDetails(state.active_details, incoming.details);
state.active_details = merged.details;
state.active_truncated_detail_count += incoming.truncated + merged.truncated;
if (state.active_stage !== nextStage) {
closeActiveStage(state, now, 'completed');
openStage(state, nextStage, nextStatus, now);
}
} else {
openStage(state, nextStage, nextStatus, now);
const merged = mergeDetails([], incoming.details);
state.active_details = merged.details;
state.active_truncated_detail_count += incoming.truncated + merged.truncated;
}
state.active_status = nextStatus;
if (TERMINAL_STATUSES.has(nextStatus)) {
closeActiveStage(state, now, nextStatus);
state.final_status = nextStatus;
}
return {
state,
snapshot: snapshot(state, now),
terminal: TERMINAL_STATUSES.has(nextStatus)
};
}
return {
SCHEMA_VERSION,
MAX_STAGES,
MAX_DETAILS_PER_STAGE,
TERMINAL_STATUSES,
safeCode,
sanitizeCounters,
sanitizeDetail,
mergeDetails,
sanitizeSnapshot,
createState,
snapshot,
advance
};
});