{"files": {"main.ts": "import {run} from './core.ts';\nimport {POLICY} from './policy.ts';\nexport function evaluate(payload: any): any { return run(payload, POLICY); }\n", "core.ts": "import {addDecimal, formatDecimal, parseDecimal} from './helpers.ts';\nexport function run(payload: any, policy: any): any {\n  const now = payload.now;\n  const events = Array.isArray(payload.events) ? payload.events : [];\n  const maxAge = policy.max_age;\n  const inclusiveAge = !!policy.inclusive_age;\n  const filtered: any[] = [];\n  for (let i = 0; i < events.length; i++) {\n    const ev = events[i];\n    const age = now - ev.ts;\n    if (age > maxAge || (!inclusiveAge && age === maxAge)) continue;\n    filtered.push({ev: ev, index: i});\n  }\n  const duplicates: string[] = [];\n  const seen = new Set<string>();\n  const deduped: any[] = [];\n  for (const item of filtered) {\n    const ev = item.ev;\n    const key = policy.scope_key === 'tenant' ? ev.tenant + '\\u0000' + ev.id : String(ev.id);\n    if (seen.has(key)) {\n      if (policy.audit_duplicate) duplicates.push(ev.id);\n      continue;\n    }\n    seen.add(key);\n    deduped.push(item);\n  }\n  const groups = new Map<string, any>();\n  for (const item of deduped) {\n    const ev = item.ev;\n    const key = ev.tenant + '\\u0000' + ev.entity;\n    let group = groups.get(key);\n    if (!group) {\n      group = {tenant: ev.tenant, entity: ev.entity, earliest: item.index, items: []};\n      groups.set(key, group);\n    }\n    if (item.index < group.earliest) group.earliest = item.index;\n    group.items.push(item);\n  }\n  const useSeq = policy.latest_by === 'seq' || policy.latest_by === 'sequence';\n  const sortValue = (ev: any): number => useSeq ? ev.seq : ev.ts;\n  const entities: any[] = [];\n  groups.forEach((group) => {\n    const byTie = new Map<number, any>();\n    for (const item of group.items) {\n      const k = sortValue(item.ev);\n      if (!byTie.has(k) || policy.tie_policy === 'last') byTie.set(k, item);\n    }\n    const retained: any[] = [];\n    byTie.forEach((item) => retained.push(item));\n    retained.sort((a, b) => {\n      const av = sortValue(a.ev);\n      const bv = sortValue(b.ev);\n      if (av < bv) return -1;\n      if (av > bv) return 1;\n      return a.index - b.index;\n    });\n    let seq = 0;\n    let value = {mantissa: BigInt(0), scale: 0};\n    let deleted = false;\n    for (const item of retained) {\n      const ev = item.ev;\n      if (policy.gap_policy === 'ignore' && ev.seq !== seq + 1) continue;\n      seq = ev.seq;\n      if (ev.kind === 'delete') {\n        deleted = true;\n      } else if (ev.kind === 'set') {\n        if (!deleted || policy.allow_resurrection) {\n          deleted = false;\n          const amount = parseDecimal(ev.amount);\n          value = policy.amount_mode === 'delta' ? addDecimal(value, amount) : amount;\n        }\n      }\n    }\n    if (policy.delete_wins) {\n      for (const item of retained) {\n        if (item.ev.kind === 'delete') { deleted = true; break; }\n      }\n    }\n    entities.push({tenant: group.tenant, entity: group.entity, value: formatDecimal(value, policy.precision), deleted: deleted, earliest: group.earliest});\n  });\n  if (policy.order === 'key') {\n    entities.sort((a, b) => {\n      if (a.tenant < b.tenant) return -1;\n      if (a.tenant > b.tenant) return 1;\n      if (a.entity < b.entity) return -1;\n      if (a.entity > b.entity) return 1;\n      return 0;\n    });\n  } else {\n    entities.sort((a, b) => a.earliest - b.earliest);\n  }\n  const outEntities = entities.map((e) => ({tenant: e.tenant, entity: e.entity, value: e.value, deleted: e.deleted}));\n  return {entities: outEntities, duplicates: duplicates};\n}\n", "policy.ts": "export const POLICY: any = {\"allow_resurrection\": false, \"amount_mode\": \"delta\", \"audit_duplicate\": false, \"delete_wins\": false, \"gap_policy\": \"ignore\", \"inclusive_age\": false, \"latest_by\": \"timestamp\", \"max_age\": 120, \"order\": \"input\", \"precision\": 2, \"scope_key\": \"global\", \"tie_policy\": \"last\"};\n", "helpers.ts": "export interface DecimalValue { mantissa: bigint; scale: number; }\nexport function pow10(n: number): bigint { let r = BigInt(1); for (let i = 0; i < n; i++) r = r * BigInt(10); return r; }\nexport function parseDecimal(input: any): DecimalValue { const s = String(input).trim(); let sign = BigInt(1); let body = s; if (body.startsWith('-')) { sign = BigInt(-1); body = body.slice(1); } else if (body.startsWith('+')) { body = body.slice(1); } const dot = body.indexOf('.'); let intPart = dot === -1 ? body : body.slice(0, dot); let fracPart = dot === -1 ? '' : body.slice(dot + 1); if (intPart === '') intPart = '0'; if (!/^[0-9]+$/.test(intPart)) throw new Error('invalid decimal'); if (fracPart !== '' && !/^[0-9]+$/.test(fracPart)) throw new Error('invalid decimal'); const digits = intPart + fracPart; const mantissa = sign * BigInt(digits === '' ? '0' : digits);\n return {mantissa: mantissa, scale: fracPart.length}; }\nexport function addDecimal(a: DecimalValue, b: DecimalValue): DecimalValue { const scale = Math.max(a.scale, b.scale); const am = a.mantissa * pow10(scale - a.scale); const bm = b.mantissa * pow10(scale - b.scale); return {mantissa: am + bm, scale: scale}; }\nexport function formatDecimal(value: DecimalValue, precision: number): string { let mantissa = value.mantissa; let scale = value.scale; const negative = mantissa < BigInt(0); let abs = negative ? -mantissa : mantissa; if (scale > precision) { const divisor = pow10(scale - precision); const quotient = abs / divisor; const remainder = abs % divisor; abs = remainder * BigInt(2) >= divisor ? quotient + BigInt(1) : quotient; scale = precision; } else if (scale < precision) { abs = abs * pow10(precision - scale); scale = precision; } let digits = abs.toString(); if (precision === 0) return (negative && abs !== BigInt(0) ? '-' : '') + digits; digits = digits.padStart(precision + 1, '0'); const intPart = digits.slice(0, digits.length - precision); const fracPart = digits.slice(digits.length - precision); return (negative && abs !== BigInt(0) ? '-' : '') + intPart + '.' + fracPart; }\n"}}