/** Read-only evidence projection. Removed/refreshed auras end the observed window. */ function summarizeActorDisarmEvidence(events, auraSamples, playerId, partyIds = []) { const actorIds = new Set([playerId, ...partyIds]); const windows = [], open = new Map(); const keyFor = (aura) => JSON.stringify([aura.instanceId, aura.sourceId, aura.targetId, aura.appliedAt]); const samples = [...auraSamples].sort((a, b) => a.observedAt - b.observedAt); for (const sample of samples) { const auras = (sample.auras ?? []).filter((aura) => aura.definitionId === 'enemy-source-spell-6713' && actorIds.has(aura.targetId) && Number.isFinite(aura.appliedAt)); const keys = new Set(auras.map(keyFor)); for (const [key, window] of open) { if (keys.has(key)) continue; const replacement = auras.find((aura) => aura.instanceId === window.instanceId && aura.targetId === window.targetId); const removalAt = Number.isFinite(replacement?.appliedAt) ? replacement.appliedAt : sample.observedAt; window.observedEndAt = Math.min(window.expiresAt ?? Number.POSITIVE_INFINITY, removalAt); window.endEvidence = replacement ? 'refresh' : 'committed-removal'; open.delete(key); } for (const aura of auras) { const key = keyFor(aura); if (open.has(key)) continue; const window = { ...aura, observedEndAt: aura.expiresAt ?? null, endEvidence: 'source-expiry' }; windows.push(window); open.set(key, window); } } const actorHandEvents = events.filter((event) => event.kind === 'damage' && actorIds.has(event.sourceActorId) && event.attackHand != null); const disarmWindows = windows.map((window) => ({ ...window, handEvents: actorHandEvents.filter((event) => event.sourceActorId === window.targetId && event.occurredAt >= window.appliedAt && window.observedEndAt !== null && event.occurredAt < window.observedEndAt), })); return { actorHandEvents, disarmWindows, coverage: 'Hand-tagged player/party damage results matched to committed source Disarm windows; removal/refresh ends a window. No event proves an unsampled cast gate, damage formula, or missing hand.' }; } module.exports = { summarizeActorDisarmEvidence };