{
  "schema_version": 1,
  "repository_base": "https://github.com/structbuild/polymarket-rtds-vs-struct-benchmark",
  "struct_upstream_base_commit": "be622c1edafb977e76c1126c797b2b01f926f9f9",
  "extended_harness_commit": "4df16e1c2c6a8b51728842b8031d027382910dd0",
  "note": "Exact credential-free source snapshot used for the final benchmark. Restore each files entry to its named path.",
  "files": {
    "index.ts": "import { execFile } from \"node:child_process\";\nimport { createHash } from \"node:crypto\";\nimport { lookup } from \"node:dns/promises\";\nimport { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { hostname, platform, arch } from \"node:os\";\nimport { resolve } from \"node:path\";\nimport { promisify } from \"node:util\";\n\nimport WebSocketImpl, { type RawData } from \"ws\";\nimport { StructWebSocket } from \"@structbuild/sdk\";\n\nimport {\n  analyze,\n  latencySamplesCsv,\n  renderReport,\n  type ChainFill,\n  type EventPath,\n  type NormalizedEvent,\n  type Source,\n} from \"./analysis.js\";\n\nif (typeof (globalThis as { WebSocket?: unknown }).WebSocket === \"undefined\") {\n  (globalThis as { WebSocket?: unknown }).WebSocket = WebSocketImpl;\n}\n\nconst execFileAsync = promisify(execFile);\nconst DEFAULT_STRUCT_WS_BASE = \"https://api.struct.to\";\nconst DEFAULT_POLYNODE_WS_URL = \"wss://ws.polynode.dev/ws\";\nconst DEFAULT_RTDS_URL = \"wss://ws-live-data.polymarket.com\";\nconst RTDS_PING_INTERVAL_MS = 5_000;\n\nconst V1_EXCHANGES = [\n  \"0x4bfb41d5b3570defd03c39a9a4d8de6bd8b8982e\",\n  \"0xc5d563a36ae78145c45a50134d48a1215220f80a\",\n];\nconst V2_EXCHANGES = [\n  \"0xe111180000d2663c0091e4f400237545b87b996b\",\n  \"0xe2222d279d744050d28e00520010520000310f59\",\n  \"0xe2222d002000ba0053cef3375333610f64600036\",\n];\nconst V1_ORDER_FILLED_TOPIC =\n  \"0xd0a08e8c493f9c94f29311604c9de1b4e8c8d4c06bd0c789af57f2d65bfec0f6\";\nconst V2_ORDER_FILLED_TOPIC =\n  \"0xd543adfd945773f1a62f74f0ee55a5e3b9b1a28262980ba90b1a89f2ea84d8ee\";\n\ntype StructStatus = \"pending\" | \"confirmed\" | \"all\";\ntype RtdsHeartbeat = \"struct-uppercase\" | \"polymarket-lowercase\";\n\ntype Args = {\n  durationSec: number;\n  warmupSec: number;\n  tailSec: number;\n  structStatus: StructStatus;\n  structApiKey: string;\n  polynodeApiKey: string;\n  polygonRpcUrl: string;\n  structWsBase: string;\n  polynodeWsUrl: string;\n  rtdsUrl: string;\n  rtdsHeartbeat: RtdsHeartbeat;\n  rtdsSilenceReconnectSec: number;\n  tradeTypes?: string[];\n  out?: string;\n};\n\ntype Stamp = { monoNs: string; wallMs: number };\n\ntype RawFrame = {\n  sequence: number;\n  source: Source;\n  arrival_mono_ns: string;\n  arrival_wall_ms: number;\n  parsed: unknown;\n};\n\ntype LifecycleEvent = {\n  source: Source | \"rpc\" | \"benchmark\";\n  event: string;\n  wall_ms: number;\n  detail?: unknown;\n};\n\ntype RpcAudit = {\n  method: string;\n  params: unknown[];\n  started_at: string;\n  duration_ms: number;\n  http_status?: number;\n  result_count?: number;\n  error?: string;\n};\n\ntype JsonRpcLog = {\n  address?: string;\n  blockHash?: string;\n  blockNumber?: string;\n  transactionHash?: string;\n  logIndex?: string;\n  topics?: string[];\n  removed?: boolean;\n};\n\ntype JsonRpcReceipt = {\n  transactionHash?: string;\n  logs?: JsonRpcLog[];\n};\n\ntype Closable = { close: () => void };\n\nfunction printHelp(): void {\n  console.log(`struct-rtds-polynode-benchmark\n\nOne-process latency race plus independent Polygon OrderFilled completeness audit.\n\nUsage:\n  STRUCT_API_KEY=... POLYNODE_API_KEY=... POLYGON_RPC_URL=... npm run bench -- [options]\n\nOptions:\n  --duration-sec <n>   Core collection duration (default 120)\n  --warmup-sec <n>     Socket warm-up before the future-block window (default 12)\n  --tail-sec <n>       Confirmation tail after the window closes (default 20)\n  --struct-status <s>  pending | confirmed | all (default all)\n  --trade-types <csv>  Optional Struct filter (default no filter, per docs)\n  --struct-ws <url>    Struct base URL (default ${DEFAULT_STRUCT_WS_BASE})\n  --polynode-ws <url>  PolyNode WS URL (default ${DEFAULT_POLYNODE_WS_URL})\n  --rtds-url <url>     RTDS URL (default ${DEFAULT_RTDS_URL})\n  --rtds-heartbeat <m> struct-uppercase | polymarket-lowercase (default struct-uppercase)\n  --rtds-silence-reconnect-sec <n> Reconnect after n seconds without activity (default 0/off)\n  --out <dir>          Artifact directory (default results/<UTC timestamp>)\n  --help               Show this message\n\nCredentials are read only from environment variables and are never written to artifacts.\n`);\n}\n\nfunction finiteSeconds(value: string, label: string): number {\n  const parsed = Number(value);\n  if (!Number.isFinite(parsed) || parsed < 0) throw new Error(`${label} must be a non-negative number`);\n  return parsed;\n}\n\nfunction parseArgs(): Args {\n  const args: Args = {\n    durationSec: finiteSeconds(process.env.DURATION_SEC ?? \"120\", \"DURATION_SEC\"),\n    warmupSec: finiteSeconds(process.env.WARMUP_SEC ?? \"12\", \"WARMUP_SEC\"),\n    tailSec: finiteSeconds(process.env.TAIL_SEC ?? \"20\", \"TAIL_SEC\"),\n    structStatus: (process.env.STRUCT_STATUS as StructStatus) ?? \"all\",\n    structApiKey: process.env.STRUCT_API_KEY ?? \"\",\n    polynodeApiKey: process.env.POLYNODE_API_KEY ?? \"\",\n    polygonRpcUrl: process.env.POLYGON_RPC_URL ?? \"\",\n    structWsBase: process.env.STRUCT_WS_BASE ?? DEFAULT_STRUCT_WS_BASE,\n    polynodeWsUrl: process.env.POLYNODE_WS_URL ?? DEFAULT_POLYNODE_WS_URL,\n    rtdsUrl: process.env.RTDS_URL ?? DEFAULT_RTDS_URL,\n    rtdsHeartbeat: (process.env.RTDS_HEARTBEAT as RtdsHeartbeat) ?? \"struct-uppercase\",\n    rtdsSilenceReconnectSec: finiteSeconds(\n      process.env.RTDS_SILENCE_RECONNECT_SEC ?? \"0\",\n      \"RTDS_SILENCE_RECONNECT_SEC\",\n    ),\n    out: process.env.OUT_DIR,\n  };\n\n  const argv = process.argv.slice(2);\n  for (let index = 0; index < argv.length; index++) {\n    const current = argv[index];\n    const next = argv[index + 1];\n    if (current === \"--help\") {\n      printHelp();\n      process.exit(0);\n    } else if (current === \"--duration-sec\" && next) {\n      args.durationSec = finiteSeconds(next, current);\n      index++;\n    } else if (current === \"--warmup-sec\" && next) {\n      args.warmupSec = finiteSeconds(next, current);\n      index++;\n    } else if (current === \"--tail-sec\" && next) {\n      args.tailSec = finiteSeconds(next, current);\n      index++;\n    } else if (current === \"--struct-status\" && next) {\n      args.structStatus = next as StructStatus;\n      index++;\n    } else if (current === \"--trade-types\" && next) {\n      args.tradeTypes = next.split(\",\").map((value) => value.trim()).filter(Boolean);\n      index++;\n    } else if (current === \"--struct-ws\" && next) {\n      args.structWsBase = next;\n      index++;\n    } else if (current === \"--polynode-ws\" && next) {\n      args.polynodeWsUrl = next;\n      index++;\n    } else if (current === \"--rtds-url\" && next) {\n      args.rtdsUrl = next;\n      index++;\n    } else if (current === \"--rtds-heartbeat\" && next) {\n      args.rtdsHeartbeat = next as RtdsHeartbeat;\n      index++;\n    } else if (current === \"--rtds-silence-reconnect-sec\" && next) {\n      args.rtdsSilenceReconnectSec = finiteSeconds(next, current);\n      index++;\n    } else if (current === \"--out\" && next) {\n      args.out = next;\n      index++;\n    } else {\n      throw new Error(`Unknown or incomplete option: ${current}`);\n    }\n  }\n\n  if (![\"pending\", \"confirmed\", \"all\"].includes(args.structStatus)) {\n    throw new Error(\"--struct-status must be pending, confirmed, or all\");\n  }\n  if (![\"struct-uppercase\", \"polymarket-lowercase\"].includes(args.rtdsHeartbeat)) {\n    throw new Error(\"--rtds-heartbeat must be struct-uppercase or polymarket-lowercase\");\n  }\n  if (!args.structApiKey) throw new Error(\"Missing STRUCT_API_KEY\");\n  if (!args.polynodeApiKey) throw new Error(\"Missing POLYNODE_API_KEY\");\n  if (!args.polygonRpcUrl) throw new Error(\"Missing POLYGON_RPC_URL\");\n  if (args.durationSec < 10) throw new Error(\"--duration-sec must be at least 10 seconds\");\n  return args;\n}\n\nfunction now(): Stamp {\n  return { monoNs: process.hrtime.bigint().toString(), wallMs: Date.now() };\n}\n\nfunction sleep(ms: number): Promise<void> {\n  return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));\n}\n\nfunction normalizeHash(value: unknown): string | undefined {\n  if (typeof value !== \"string\") return undefined;\n  const text = value.trim().toLowerCase();\n  if (!text) return undefined;\n  return text.startsWith(\"0x\") ? text : `0x${text}`;\n}\n\nfunction numeric(value: unknown): number | undefined {\n  if (typeof value === \"number\" && Number.isFinite(value)) return value;\n  if (typeof value !== \"string\" || value.trim() === \"\") return undefined;\n  const parsed = value.startsWith(\"0x\") ? Number.parseInt(value, 16) : Number(value);\n  return Number.isFinite(parsed) ? parsed : undefined;\n}\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n  return value != null && typeof value === \"object\" && !Array.isArray(value);\n}\n\nfunction errorText(error: unknown): string {\n  return error instanceof Error ? error.message : String(error);\n}\n\nfunction safeUrl(value: string): string {\n  const url = new URL(value);\n  url.username = \"\";\n  url.password = \"\";\n  url.search = \"\";\n  url.hash = \"\";\n  if (url.pathname.includes(\"/v2/\")) url.pathname = `${url.pathname.slice(0, url.pathname.indexOf(\"/v2/\") + 4)}<redacted>`;\n  return url.toString();\n}\n\nfunction fingerprint(secret: string): string {\n  return `sha256:${createHash(\"sha256\").update(secret).digest(\"hex\").slice(0, 16)}`;\n}\n\nfunction hexBlock(block: number): string {\n  return `0x${block.toString(16)}`;\n}\n\nclass Recorder {\n  readonly events: NormalizedEvent[] = [];\n  readonly rawFrames: RawFrame[] = [];\n  readonly lifecycle: LifecycleEvent[] = [];\n  private sequence = 0;\n\n  frame(source: Source, parsed: unknown, stamp = now()): void {\n    this.rawFrames.push({\n      sequence: ++this.sequence,\n      source,\n      arrival_mono_ns: stamp.monoNs,\n      arrival_wall_ms: stamp.wallMs,\n      parsed,\n    });\n  }\n\n  event(\n    source: Source,\n    path: EventPath,\n    eventType: string,\n    fields: Partial<Omit<NormalizedEvent, \"sequence\" | \"source\" | \"path\" | \"event_type\" | \"arrival_mono_ns\" | \"arrival_wall_ms\">>,\n    stamp = now(),\n  ): void {\n    this.events.push({\n      sequence: ++this.sequence,\n      source,\n      path,\n      event_type: eventType,\n      arrival_mono_ns: stamp.monoNs,\n      arrival_wall_ms: stamp.wallMs,\n      ...fields,\n    });\n  }\n\n  life(source: LifecycleEvent[\"source\"], event: string, detail?: unknown): void {\n    this.lifecycle.push({ source, event, wall_ms: Date.now(), ...(detail === undefined ? {} : { detail }) });\n  }\n}\n\nclass RpcClient {\n  readonly audit: RpcAudit[] = [];\n  private id = 0;\n\n  constructor(private readonly url: string, private readonly recorder: Recorder) {}\n\n  async call<T>(method: string, params: unknown[]): Promise<T> {\n    let lastError: unknown;\n    for (let attempt = 1; attempt <= 3; attempt++) {\n      const started = performance.now();\n      const audit: RpcAudit = {\n        method,\n        params,\n        started_at: new Date().toISOString(),\n        duration_ms: 0,\n      };\n      try {\n        const response = await fetch(this.url, {\n          method: \"POST\",\n          headers: { \"content-type\": \"application/json\" },\n          body: JSON.stringify({ jsonrpc: \"2.0\", id: ++this.id, method, params }),\n          signal: AbortSignal.timeout(20_000),\n        });\n        audit.http_status = response.status;\n        const body = (await response.json()) as { result?: T; error?: { message?: string } };\n        audit.duration_ms = performance.now() - started;\n        if (!response.ok || body.error || body.result === undefined) {\n          throw new Error(body.error?.message ?? `HTTP ${response.status}`);\n        }\n        if (Array.isArray(body.result)) audit.result_count = body.result.length;\n        this.audit.push(audit);\n        return body.result;\n      } catch (error) {\n        audit.duration_ms = performance.now() - started;\n        audit.error = errorText(error);\n        this.audit.push(audit);\n        lastError = error;\n        this.recorder.life(\"rpc\", \"retry\", { method, attempt, error: audit.error });\n        if (attempt < 3) await sleep(250 * attempt);\n      }\n    }\n    throw new Error(`RPC ${method} failed after retries: ${errorText(lastError)}`);\n  }\n\n  async blockNumber(): Promise<number> {\n    const value = await this.call<string>(\"eth_blockNumber\", []);\n    const parsed = numeric(value);\n    if (parsed == null) throw new Error(`Invalid eth_blockNumber response: ${String(value)}`);\n    return parsed;\n  }\n\n  async batch<T>(method: string, parameterSets: unknown[][], batchSize = 100): Promise<T[]> {\n    const allResults: T[] = [];\n    for (let offset = 0; offset < parameterSets.length; offset += batchSize) {\n      const chunk = parameterSets.slice(offset, offset + batchSize);\n      let lastError: unknown;\n      let completed = false;\n      for (let attempt = 1; attempt <= 3 && !completed; attempt++) {\n        const started = performance.now();\n        const requests = chunk.map((params) => ({ jsonrpc: \"2.0\", id: ++this.id, method, params }));\n        const audit: RpcAudit = {\n          method: `${method}[batch]`,\n          params: [{ offset, batch_size: chunk.length }],\n          started_at: new Date().toISOString(),\n          duration_ms: 0,\n        };\n        try {\n          const response = await fetch(this.url, {\n            method: \"POST\",\n            headers: { \"content-type\": \"application/json\" },\n            body: JSON.stringify(requests),\n            signal: AbortSignal.timeout(30_000),\n          });\n          audit.http_status = response.status;\n          const body = (await response.json()) as Array<{\n            id: number;\n            result?: T;\n            error?: { message?: string };\n          }>;\n          audit.duration_ms = performance.now() - started;\n          if (!response.ok || !Array.isArray(body)) throw new Error(`HTTP ${response.status} or invalid batch response`);\n          const byId = new Map(body.map((entry) => [entry.id, entry]));\n          const ordered = requests.map((request) => {\n            const entry = byId.get(request.id);\n            if (!entry || entry.error || entry.result === undefined) {\n              throw new Error(entry?.error?.message ?? `Missing batch response id ${request.id}`);\n            }\n            return entry.result;\n          });\n          audit.result_count = ordered.length;\n          this.audit.push(audit);\n          allResults.push(...ordered);\n          completed = true;\n        } catch (error) {\n          audit.duration_ms = performance.now() - started;\n          audit.error = errorText(error);\n          this.audit.push(audit);\n          lastError = error;\n          this.recorder.life(\"rpc\", \"batch_retry\", { method, offset, attempt, error: audit.error });\n          if (attempt < 3) await sleep(250 * attempt);\n        }\n      }\n      if (!completed) throw new Error(`RPC batch ${method} failed after retries: ${errorText(lastError)}`);\n    }\n    return allResults;\n  }\n}\n\nfunction connectRtds(args: Args, recorder: Recorder): Closable & { ready: Promise<unknown> } {\n  let socket: WebSocketImpl | undefined;\n  let heartbeatTimer: ReturnType<typeof setTimeout> | undefined;\n  let watchdogTimer: ReturnType<typeof setInterval> | undefined;\n  let reconnectTimer: ReturnType<typeof setTimeout> | undefined;\n  let settled = false;\n  let stopped = false;\n  let attempt = 0;\n  let lastActivityWallMs = 0;\n  let connectedWallMs = 0;\n  let resolveReady!: (value: unknown) => void;\n  let rejectReady!: (reason: unknown) => void;\n  const request = {\n    action: \"subscribe\",\n    subscriptions: [\n      { topic: \"activity\", type: \"trades\" },\n      { topic: \"activity\", type: \"orders_matched\" },\n    ],\n  };\n  const ready = new Promise<unknown>((resolvePromise, rejectPromise) => {\n    resolveReady = resolvePromise;\n    rejectReady = rejectPromise;\n  });\n  const initialTimeout = setTimeout(() => {\n    if (!settled) {\n      settled = true;\n      rejectReady(new Error(\"RTDS connection timed out\"));\n    }\n  }, 12_000);\n\n  const clearSocketTimers = () => {\n    if (heartbeatTimer) clearTimeout(heartbeatTimer);\n    if (watchdogTimer) clearInterval(watchdogTimer);\n    heartbeatTimer = undefined;\n    watchdogTimer = undefined;\n  };\n\n  const scheduleReconnect = () => {\n    if (stopped || reconnectTimer) return;\n    reconnectTimer = setTimeout(() => {\n      reconnectTimer = undefined;\n      openSocket();\n    }, 250);\n  };\n\n  const openSocket = () => {\n    if (stopped) return;\n    attempt++;\n    const current = new WebSocketImpl(args.rtdsUrl);\n    socket = current;\n    const sendOfficialHeartbeat = () => {\n      if (current.readyState === WebSocketImpl.OPEN) current.send(\"ping\");\n    };\n\n    current.on(\"pong\", () => {\n      if (args.rtdsHeartbeat !== \"polymarket-lowercase\" || current !== socket) return;\n      if (heartbeatTimer) clearTimeout(heartbeatTimer);\n      heartbeatTimer = setTimeout(sendOfficialHeartbeat, RTDS_PING_INTERVAL_MS);\n    });\n    current.on(\"open\", () => {\n      if (current !== socket) return;\n      connectedWallMs = Date.now();\n      lastActivityWallMs = connectedWallMs;\n      recorder.life(\"rtds\", attempt === 1 ? \"connected\" : \"reconnected\", { attempt });\n      current.send(JSON.stringify(request));\n      recorder.life(\"rtds\", \"subscription_sent\", { attempt, ...request });\n      if (args.rtdsHeartbeat === \"struct-uppercase\") {\n        heartbeatTimer = setInterval(() => {\n          if (current.readyState === WebSocketImpl.OPEN) current.send(\"PING\");\n        }, RTDS_PING_INTERVAL_MS);\n      } else {\n        sendOfficialHeartbeat();\n      }\n      if (args.rtdsSilenceReconnectSec > 0) {\n        watchdogTimer = setInterval(() => {\n          if (current !== socket || current.readyState !== WebSocketImpl.OPEN) return;\n          const quietMs = Date.now() - lastActivityWallMs;\n          if (quietMs < args.rtdsSilenceReconnectSec * 1_000) return;\n          recorder.life(\"rtds\", \"silence_reconnect\", {\n            attempt,\n            quiet_ms: quietMs,\n            connected_ms: Date.now() - connectedWallMs,\n          });\n          clearSocketTimers();\n          current.terminate();\n        }, 1_000);\n      }\n      if (!settled) {\n        settled = true;\n        clearTimeout(initialTimeout);\n        resolveReady({\n          sent: request,\n          protocol_has_no_required_ack: true,\n          heartbeat: args.rtdsHeartbeat,\n          silence_reconnect_sec: args.rtdsSilenceReconnectSec,\n        });\n      }\n    });\n    current.on(\"message\", (raw: RawData) => {\n      if (current !== socket) return;\n      const stamp = now();\n      const text = raw.toString();\n      if (text === \"PONG\") return;\n      let parsed: unknown;\n      try {\n        parsed = JSON.parse(text);\n      } catch {\n        recorder.frame(\"rtds\", text, stamp);\n        return;\n      }\n      recorder.frame(\"rtds\", parsed, stamp);\n      if (!isObject(parsed) || parsed.topic !== \"activity\") return;\n      if (parsed.type !== \"trades\" && parsed.type !== \"orders_matched\") return;\n      lastActivityWallMs = stamp.wallMs;\n      const payloads = Array.isArray(parsed.payload) ? parsed.payload : [parsed.payload];\n      for (const rawPayload of payloads) {\n        if (!isObject(rawPayload)) continue;\n        recorder.event(\n          \"rtds\",\n          \"activity\",\n          String(parsed.type),\n          {\n            tx_hash: normalizeHash(rawPayload.transactionHash ?? rawPayload.transaction_hash ?? rawPayload.hash),\n            trade_type: String(parsed.type),\n          },\n          stamp,\n        );\n      }\n    });\n    current.on(\"error\", (error) => {\n      recorder.life(\"rtds\", \"error\", { attempt, message: error.message });\n      if (!settled) {\n        settled = true;\n        clearTimeout(initialTimeout);\n        rejectReady(error);\n      }\n    });\n    current.on(\"close\", (code, reason) => {\n      recorder.life(\"rtds\", \"disconnected\", { attempt, code, reason: reason.toString(), stopped });\n      if (current === socket) {\n        clearSocketTimers();\n        scheduleReconnect();\n      }\n    });\n  };\n\n  openSocket();\n\n  return {\n    ready,\n    close: () => {\n      stopped = true;\n      clearTimeout(initialTimeout);\n      clearSocketTimers();\n      if (reconnectTimer) clearTimeout(reconnectTimer);\n      try { socket?.close(); } catch { /* noop */ }\n    },\n  };\n}\n\nasync function connectStruct(args: Args, recorder: Recorder): Promise<Closable & { ack: unknown }> {\n  const socket = new StructWebSocket({ apiKey: args.structApiKey, baseUrl: args.structWsBase });\n  socket.on(\"connected\", () => recorder.life(\"struct\", \"connected\"));\n  socket.on(\"disconnected\", (detail) => recorder.life(\"struct\", \"disconnected\", detail));\n  socket.on(\"reconnecting\", (detail) => recorder.life(\"struct\", \"reconnecting\", detail));\n  socket.on(\"reconnect_failed\", (error) => recorder.life(\"struct\", \"reconnect_failed\", { message: errorText(error) }));\n  socket.on(\"auth_failed\", (error) => recorder.life(\"struct\", \"auth_failed\", { message: errorText(error) }));\n  socket.on(\"warning\", (warning) => recorder.life(\"struct\", \"warning\", { message: errorText(warning) }));\n  socket.on(\"error\", (error) => recorder.life(\"struct\", \"error\", { message: errorText(error) }));\n  socket.on(\"trade_stream_update\", (value) => {\n    const stamp = now();\n    const event = value as unknown as Record<string, unknown>;\n    recorder.frame(\"struct\", event, stamp);\n    const isPending = event.received_at != null && event.block == null;\n    recorder.event(\n      \"struct\",\n      isPending ? \"pending\" : \"confirmed\",\n      \"trade_stream_update\",\n      {\n        tx_hash: normalizeHash(event.hash),\n        log_index: numeric(event.log_index),\n        block_number: numeric(event.block),\n        order_hash: normalizeHash(event.order_hash),\n        trade_type: typeof event.trade_type === \"string\" ? event.trade_type : undefined,\n        status: isPending ? \"pending\" : \"confirmed\",\n      },\n      stamp,\n    );\n  });\n\n  await socket.connect();\n  const filters: Record<string, unknown> = { status: args.structStatus, subscribe_all: true };\n  if (args.tradeTypes?.length) filters.trade_types = args.tradeTypes;\n  const ack = await socket.subscribe(\"polymarket_trades\", filters as never);\n  recorder.life(\"struct\", \"subscribed\", { requested: filters, acknowledged: ack });\n  return { ack, close: () => socket.disconnect() };\n}\n\nfunction connectPolynode(args: Args, recorder: Recorder): Closable & { ready: Promise<unknown> } {\n  const url = new URL(args.polynodeWsUrl);\n  url.searchParams.set(\"key\", args.polynodeApiKey);\n  const socket = new WebSocketImpl(url.toString());\n  let resolved = false;\n  let resolveReady: (value: unknown) => void;\n  let rejectReady: (reason: unknown) => void;\n  const ready = new Promise<unknown>((resolvePromise, rejectPromise) => {\n    resolveReady = resolvePromise;\n    rejectReady = rejectPromise;\n  });\n  const timeout = setTimeout(() => {\n    if (!resolved) {\n      resolved = true;\n      rejectReady(new Error(\"PolyNode subscription acknowledgment timed out\"));\n    }\n  }, 12_000);\n\n  socket.on(\"open\", () => {\n    recorder.life(\"polynode\", \"connected\");\n    const request = { action: \"subscribe\", type: \"trades\" };\n    socket.send(JSON.stringify(request));\n    recorder.life(\"polynode\", \"subscription_sent\", request);\n  });\n  socket.on(\"message\", (raw: RawData) => {\n    const stamp = now();\n    let parsed: unknown;\n    try {\n      parsed = JSON.parse(raw.toString());\n    } catch {\n      recorder.frame(\"polynode\", raw.toString(), stamp);\n      return;\n    }\n    recorder.frame(\"polynode\", parsed, stamp);\n    if (!isObject(parsed)) return;\n    if (parsed.type === \"subscribed\" && parsed.subscription_type === \"trades\" && !resolved) {\n      resolved = true;\n      clearTimeout(timeout);\n      recorder.life(\"polynode\", \"subscribed\", parsed);\n      resolveReady(parsed);\n    }\n    const candidates = Array.isArray(parsed.events) ? parsed.events : [parsed];\n    for (const candidate of candidates) {\n      if (!isObject(candidate)) continue;\n      const data = isObject(candidate.data) ? candidate.data : candidate;\n      const eventType = typeof data.event_type === \"string\" ? data.event_type : undefined;\n      if (!eventType) continue;\n      const fields = {\n        tx_hash: normalizeHash(data.tx_hash),\n        log_index: numeric(data.log_index),\n        block_number: numeric(data.block_number),\n        order_hash: normalizeHash(data.order_hash),\n        status: typeof data.status === \"string\" ? data.status.toLowerCase() : undefined,\n      };\n      if (eventType === \"settlement\") {\n        recorder.event(\n          \"polynode\",\n          fields.status === \"pending\" ? \"pending\" : \"settlement_other\",\n          eventType,\n          fields,\n          stamp,\n        );\n      } else if (eventType === \"status_update\") {\n        recorder.event(\"polynode\", \"confirmed\", eventType, fields, stamp);\n      } else if (eventType === \"trade\") {\n        recorder.event(\"polynode\", \"confirmed_fill\", eventType, fields, stamp);\n      } else {\n        recorder.event(\"polynode\", \"other\", eventType, fields, stamp);\n      }\n    }\n  });\n  socket.on(\"error\", (error) => {\n    recorder.life(\"polynode\", \"error\", { message: error.message });\n    if (!resolved) {\n      resolved = true;\n      clearTimeout(timeout);\n      rejectReady(error);\n    }\n  });\n  socket.on(\"close\", (code, reason) => recorder.life(\"polynode\", \"disconnected\", { code, reason: reason.toString() }));\n\n  return { ready, close: () => { try { socket.close(); } catch { /* noop */ } } };\n}\n\nasync function fetchOrderFilledLogs(rpc: RpcClient, fromBlock: number, toBlock: number): Promise<ChainFill[]> {\n  const queries: Array<{ version: \"v1\" | \"v2\"; addresses: string[]; topic: string }> = [\n    { version: \"v1\", addresses: V1_EXCHANGES, topic: V1_ORDER_FILLED_TOPIC },\n    { version: \"v2\", addresses: V2_EXCHANGES, topic: V2_ORDER_FILLED_TOPIC },\n  ];\n  const fills: ChainFill[] = [];\n  for (const query of queries) {\n    const logs = await rpc.call<JsonRpcLog[]>(\"eth_getLogs\", [{\n      fromBlock: hexBlock(fromBlock),\n      toBlock: hexBlock(toBlock),\n      address: query.addresses,\n      topics: [query.topic],\n    }]);\n    for (const log of logs) {\n      if (log.removed) continue;\n      const txHash = normalizeHash(log.transactionHash);\n      const logIndex = numeric(log.logIndex);\n      const blockNumber = numeric(log.blockNumber);\n      const address = normalizeHash(log.address);\n      if (!txHash || logIndex == null || blockNumber == null || !address) continue;\n      fills.push({\n        tx_hash: txHash,\n        log_index: logIndex,\n        receipt_log_index: -1,\n        block_number: blockNumber,\n        block_hash: normalizeHash(log.blockHash) ?? \"\",\n        address,\n        order_hash: normalizeHash(log.topics?.[1]),\n        version: query.version,\n      });\n    }\n  }\n  const unique = new Map<string, ChainFill>();\n  for (const fill of fills) unique.set(`${fill.tx_hash}:${fill.log_index}`, fill);\n  const ordered = [...unique.values()].sort((a, b) => a.block_number - b.block_number || a.log_index - b.log_index);\n\n  const txHashes = [...new Set(ordered.map((fill) => fill.tx_hash))];\n  const receipts = await rpc.batch<JsonRpcReceipt>(\n    \"eth_getTransactionReceipt\",\n    txHashes.map((hash) => [hash]),\n  );\n  const receiptsByHash = new Map<string, JsonRpcReceipt>();\n  for (const receipt of receipts) {\n    const hash = normalizeHash(receipt.transactionHash);\n    if (hash) receiptsByHash.set(hash, receipt);\n  }\n  for (const fill of ordered) {\n    const receipt = receiptsByHash.get(fill.tx_hash);\n    const receiptIndex = receipt?.logs?.findIndex(\n      (log) =>\n        numeric(log.logIndex) === fill.log_index &&\n        normalizeHash(log.address) === fill.address &&\n        normalizeHash(log.topics?.[0]) === (fill.version === \"v1\" ? V1_ORDER_FILLED_TOPIC : V2_ORDER_FILLED_TOPIC),\n    );\n    if (receiptIndex == null || receiptIndex < 0) {\n      throw new Error(`Could not resolve receipt-local log index for ${fill.tx_hash}:${fill.log_index}`);\n    }\n    fill.receipt_log_index = receiptIndex;\n  }\n  return ordered;\n}\n\nfunction canonicalizeStructFillIndexes(events: NormalizedEvent[], fills: ChainFill[]): Record<string, number> {\n  const byReceiptIndex = new Map(\n    fills.map((fill) => [`${fill.tx_hash}:${fill.receipt_log_index}`, fill]),\n  );\n  const byOrderHash = new Map<string, ChainFill[]>();\n  for (const fill of fills) {\n    if (!fill.order_hash) continue;\n    const key = `${fill.tx_hash}:${fill.order_hash}`;\n    const values = byOrderHash.get(key) ?? [];\n    values.push(fill);\n    byOrderHash.set(key, values);\n  }\n\n  const diagnostics = {\n    eligible_struct_confirmed_records: 0,\n    eligible_struct_orderfilled_events: 0,\n    eligible_struct_ordersmatched_events: 0,\n    mapped_by_receipt_log_index: 0,\n    mapped_orderfilled_by_unique_order_hash_fallback: 0,\n    mapped_ordersmatched_to_orderfilled_by_unique_order_hash: 0,\n    unmatched: 0,\n    index_order_hash_mismatches: 0,\n  };\n  const groundTruthTx = new Set(fills.map((fill) => fill.tx_hash));\n  for (const event of events) {\n    if (\n      event.source !== \"struct\" ||\n      event.path !== \"confirmed\" ||\n      (event.trade_type !== \"OrderFilled\" && event.trade_type !== \"OrdersMatched\") ||\n      !event.tx_hash ||\n      !groundTruthTx.has(event.tx_hash)\n    ) continue;\n    diagnostics.eligible_struct_confirmed_records++;\n    if (event.trade_type === \"OrdersMatched\") {\n      diagnostics.eligible_struct_ordersmatched_events++;\n      const orderMatches = event.order_hash ? byOrderHash.get(`${event.tx_hash}:${event.order_hash}`) : undefined;\n      if (orderMatches?.length === 1) {\n        event.canonical_log_index = orderMatches[0].log_index;\n        diagnostics.mapped_ordersmatched_to_orderfilled_by_unique_order_hash++;\n      } else {\n        diagnostics.unmatched++;\n      }\n      continue;\n    }\n\n    diagnostics.eligible_struct_orderfilled_events++;\n    const indexed = event.log_index == null ? undefined : byReceiptIndex.get(`${event.tx_hash}:${event.log_index}`);\n    const indexedMatchesOrder =\n      indexed != null &&\n      (event.order_hash == null || indexed.order_hash == null || event.order_hash === indexed.order_hash);\n    if (indexedMatchesOrder) {\n      event.canonical_log_index = indexed.log_index;\n      diagnostics.mapped_by_receipt_log_index++;\n      continue;\n    }\n    if (indexed) diagnostics.index_order_hash_mismatches++;\n    const orderMatches = event.order_hash ? byOrderHash.get(`${event.tx_hash}:${event.order_hash}`) : undefined;\n    if (orderMatches?.length === 1) {\n      event.canonical_log_index = orderMatches[0].log_index;\n      diagnostics.mapped_orderfilled_by_unique_order_hash_fallback++;\n    } else {\n      diagnostics.unmatched++;\n    }\n  }\n  return diagnostics;\n}\n\nasync function dnsManifest(urls: Record<string, string>): Promise<Record<string, unknown>> {\n  const result: Record<string, unknown> = {};\n  for (const [name, value] of Object.entries(urls)) {\n    const host = new URL(value).hostname;\n    try {\n      result[name] = { host, addresses: await lookup(host, { all: true }) };\n    } catch (error) {\n      result[name] = { host, error: errorText(error) };\n    }\n  }\n  return result;\n}\n\nasync function fileHash(path: string): Promise<string> {\n  return createHash(\"sha256\").update(await readFile(path)).digest(\"hex\");\n}\n\nasync function metadata(): Promise<{ upstreamCommit: string | null; sdkVersion: string | null }> {\n  let upstreamCommit: string | null = null;\n  let sdkVersion: string | null = null;\n  try {\n    upstreamCommit = (await execFileAsync(\"git\", [\"rev-parse\", \"HEAD\"])).stdout.trim();\n  } catch { /* recorded as null */ }\n  try {\n    const packageJson = JSON.parse(await readFile(\"node_modules/@structbuild/sdk/package.json\", \"utf8\")) as { version?: string };\n    sdkVersion = packageJson.version ?? null;\n  } catch { /* recorded as null */ }\n  return { upstreamCommit, sdkVersion };\n}\n\nasync function writeArtifacts(\n  outputDir: string,\n  recorder: Recorder,\n  rpc: RpcClient,\n  chainFills: ChainFill[],\n  manifest: Record<string, unknown>,\n): Promise<void> {\n  const blockWindow = manifest.block_window as {\n    from_block: number;\n    to_block: number;\n    from_block_hash?: string;\n    to_block_hash?: string;\n  };\n  const { analysis, latencySamples } = analyze(recorder.events, chainFills, blockWindow);\n  await mkdir(outputDir, { recursive: true });\n  const ndjson = (values: unknown[]) => values.map((value) => JSON.stringify(value)).join(\"\\n\") + \"\\n\";\n  await Promise.all([\n    writeFile(resolve(outputDir, \"manifest.json\"), JSON.stringify(manifest, null, 2) + \"\\n\"),\n    writeFile(resolve(outputDir, \"events.ndjson\"), ndjson(recorder.events)),\n    writeFile(resolve(outputDir, \"raw_frames.ndjson\"), ndjson(recorder.rawFrames)),\n    writeFile(resolve(outputDir, \"rpc_logs.json\"), JSON.stringify({ calls: rpc.audit, order_filled_logs: chainFills }, null, 2) + \"\\n\"),\n    writeFile(resolve(outputDir, \"latency_samples.csv\"), latencySamplesCsv(latencySamples)),\n    writeFile(resolve(outputDir, \"missing.json\"), JSON.stringify(analysis.missing, null, 2) + \"\\n\"),\n    writeFile(resolve(outputDir, \"summary.json\"), JSON.stringify(analysis, null, 2) + \"\\n\"),\n    writeFile(resolve(outputDir, \"REPORT.md\"), renderReport(analysis, manifest)),\n  ]);\n\n  console.log(\"\\nCompleted independent on-chain audit:\");\n  console.log(`  blocks: ${blockWindow.from_block}-${blockWindow.to_block}`);\n  console.log(`  ground truth: ${analysis.chain_ground_truth.transactions} tx / ${analysis.chain_ground_truth.fills} fills`);\n  console.log(`  Struct: ${analysis.observations.struct_pending_events} pending / ${analysis.observations.struct_confirmed_events} confirmed events`);\n  console.log(`  PolyNode: ${analysis.observations.polynode_pending_events} pending / ${analysis.observations.polynode_status_update_events} status updates / ${analysis.observations.polynode_trade_events} fills`);\n  console.log(`  RTDS: ${analysis.observations.rtds_activity_events} activity events`);\n  console.log(`  artifacts: ${outputDir}`);\n}\n\nasync function main(): Promise<void> {\n  const args = parseArgs();\n  const recorder = new Recorder();\n  const rpc = new RpcClient(args.polygonRpcUrl, recorder);\n  const startedAt = new Date();\n  const stamp = startedAt.toISOString().replaceAll(\":\", \"-\").replaceAll(\".\", \"-\");\n  const outputDir = resolve(args.out ?? `results/${stamp}`);\n  recorder.life(\"benchmark\", \"started\", { output_dir: outputDir });\n\n  console.log(`Connecting Struct, PolyNode, and RTDS from one process...`);\n  const rtds = connectRtds(args, recorder);\n  const polynode = connectPolynode(args, recorder);\n  let struct: (Closable & { ack: unknown }) | undefined;\n  try {\n    const [structConnection, polynodeAck, rtdsAck] = await Promise.all([\n      connectStruct(args, recorder),\n      polynode.ready,\n      rtds.ready,\n    ]);\n    struct = structConnection;\n    recorder.life(\"benchmark\", \"all_subscribed\");\n    console.log(`All subscriptions active. Warming up for ${args.warmupSec}s...`);\n    await sleep(args.warmupSec * 1_000);\n\n    const startTip = await rpc.blockNumber();\n    const fromBlock = startTip + 2;\n    const coreStarted = now();\n    recorder.life(\"benchmark\", \"ground_truth_window_armed\", { start_tip: startTip, from_block: fromBlock });\n    console.log(`Ground-truth window armed at future block ${fromBlock}; collecting for ${args.durationSec}s...`);\n    await sleep(args.durationSec * 1_000);\n\n    const endTip = await rpc.blockNumber();\n    const toBlock = endTip - 2;\n    if (toBlock < fromBlock) throw new Error(`Polygon advanced too few blocks (${fromBlock}-${toBlock})`);\n    const coreEnded = now();\n    recorder.life(\"benchmark\", \"ground_truth_window_closed\", { end_tip: endTip, to_block: toBlock });\n    console.log(`Window closed at block ${toBlock}. Keeping sockets open for a ${args.tailSec}s confirmation tail...`);\n    await sleep(args.tailSec * 1_000);\n\n    struct.close();\n    polynode.close();\n    rtds.close();\n    recorder.life(\"benchmark\", \"sockets_closed\");\n\n    const [fromAnchor, toAnchor, chainFills, dns, meta] = await Promise.all([\n      rpc.call<Record<string, unknown>>(\"eth_getBlockByNumber\", [hexBlock(fromBlock), false]),\n      rpc.call<Record<string, unknown>>(\"eth_getBlockByNumber\", [hexBlock(toBlock), false]),\n      fetchOrderFilledLogs(rpc, fromBlock, toBlock),\n      dnsManifest({ struct: args.structWsBase, polynode: args.polynodeWsUrl, rtds: args.rtdsUrl, polygon_rpc: args.polygonRpcUrl }),\n      metadata(),\n    ]);\n    const structFillIndexReconciliation = canonicalizeStructFillIndexes(recorder.events, chainFills);\n    const indexPath = new URL(import.meta.url).pathname;\n    const analysisPath = new URL(\"./analysis.ts\", import.meta.url).pathname;\n    const manifest: Record<string, unknown> = {\n      schema_version: 1,\n      generated_at: new Date().toISOString(),\n      benchmark_started_at: startedAt.toISOString(),\n      provenance: {\n        upstream_repository: \"https://github.com/structbuild/polymarket-rtds-vs-struct-benchmark\",\n        upstream_commit: meta.upstreamCommit,\n        harness_note: \"Extended from Struct's published harness with PolyNode and independent Polygon log truth.\",\n        index_ts_sha256: await fileHash(indexPath),\n        analysis_ts_sha256: await fileHash(analysisPath),\n      },\n      runtime: {\n        node: process.version,\n        struct_sdk: meta.sdkVersion,\n        platform: platform(),\n        arch: arch(),\n        hostname: hostname(),\n        timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,\n      },\n      clock: {\n        arrival_clock: \"process.hrtime.bigint()\",\n        resolution: \"nanoseconds represented as decimal strings\",\n        wall_clock: \"Date.now() retained only for audit; latency uses monotonic clock\",\n        single_process: true,\n      },\n      endpoints: {\n        struct: safeUrl(args.structWsBase),\n        polynode: safeUrl(args.polynodeWsUrl),\n        rtds: safeUrl(args.rtdsUrl),\n        polygon_rpc: safeUrl(args.polygonRpcUrl),\n      },\n      credential_fingerprints: {\n        struct: fingerprint(args.structApiKey),\n        polynode: fingerprint(args.polynodeApiKey),\n        polygon_rpc: fingerprint(args.polygonRpcUrl),\n      },\n      config: {\n        duration_sec: args.durationSec,\n        warmup_sec: args.warmupSec,\n        confirmation_tail_sec: args.tailSec,\n        boundary_guard_blocks: 2,\n        struct_status: args.structStatus,\n        struct_trade_types: args.tradeTypes ?? \"unfiltered\",\n        rtds_heartbeat: args.rtdsHeartbeat,\n        rtds_silence_reconnect_sec: args.rtdsSilenceReconnectSec,\n      },\n      subscriptions: {\n        struct: {\n          room: \"polymarket_trades\",\n          requested: { status: args.structStatus, subscribe_all: true, ...(args.tradeTypes?.length ? { trade_types: args.tradeTypes } : {}) },\n          acknowledged: struct.ack,\n        },\n        polynode: { requested: { action: \"subscribe\", type: \"trades\" }, acknowledged: polynodeAck },\n        rtds: rtdsAck,\n      },\n      block_window: {\n        from_block: fromBlock,\n        to_block: toBlock,\n        from_block_hash: normalizeHash(fromAnchor.hash),\n        to_block_hash: normalizeHash(toAnchor.hash),\n        start_tip: startTip,\n        end_tip: endTip,\n      },\n      collection_timeline: {\n        core_started_mono_ns: coreStarted.monoNs,\n        core_started_wall_ms: coreStarted.wallMs,\n        core_ended_mono_ns: coreEnded.monoNs,\n        core_ended_wall_ms: coreEnded.wallMs,\n      },\n      polygon_scope: {\n        v1_exchanges: V1_EXCHANGES,\n        v1_order_filled_topic: V1_ORDER_FILLED_TOPIC,\n        v2_exchanges: V2_EXCHANGES,\n        v2_order_filled_topic: V2_ORDER_FILLED_TOPIC,\n      },\n      struct_fill_index_reconciliation: structFillIndexReconciliation,\n      dns,\n      lifecycle: recorder.lifecycle,\n      rpc_audit: rpc.audit,\n    };\n    await writeArtifacts(outputDir, recorder, rpc, chainFills, manifest);\n  } finally {\n    try { struct?.close(); } catch { /* noop */ }\n    try { polynode.close(); } catch { /* noop */ }\n    try { rtds.close(); } catch { /* noop */ }\n  }\n}\n\nmain().catch((error) => {\n  console.error(errorText(error));\n  process.exitCode = 1;\n});\n",
    "analysis.ts": "export type Source = \"struct\" | \"polynode\" | \"rtds\";\n\nexport type EventPath =\n  | \"pending\"\n  | \"confirmed\"\n  | \"confirmed_fill\"\n  | \"activity\"\n  | \"settlement_other\"\n  | \"control\"\n  | \"other\";\n\nexport type NormalizedEvent = {\n  sequence: number;\n  source: Source;\n  path: EventPath;\n  event_type: string;\n  arrival_mono_ns: string;\n  arrival_wall_ms: number;\n  tx_hash?: string;\n  log_index?: number;\n  canonical_log_index?: number;\n  block_number?: number;\n  order_hash?: string;\n  trade_type?: string;\n  status?: string;\n};\n\nexport type ChainFill = {\n  tx_hash: string;\n  log_index: number;\n  receipt_log_index: number;\n  block_number: number;\n  block_hash: string;\n  address: string;\n  order_hash?: string;\n  version: \"v1\" | \"v2\";\n};\n\nexport type BlockWindow = {\n  from_block: number;\n  to_block: number;\n  from_block_hash?: string;\n  to_block_hash?: string;\n};\n\nexport type LatencyStats = {\n  samples: number;\n  left_first: number;\n  right_first: number;\n  ties: number;\n  left_first_pct: number;\n  min_lead_ms: number;\n  p25_lead_ms: number;\n  median_lead_ms: number;\n  mean_lead_ms: number;\n  p75_lead_ms: number;\n  p95_lead_ms: number;\n  max_lead_ms: number;\n};\n\nexport type LatencySample = {\n  comparison: string;\n  join_key: string;\n  tx_hash: string;\n  log_index?: number;\n  left: string;\n  right: string;\n  left_arrival_mono_ns: string;\n  right_arrival_mono_ns: string;\n  left_arrival_wall_ms: number;\n  right_arrival_wall_ms: number;\n  left_lead_ms: number;\n};\n\ntype PathCoverage = {\n  unit: \"transaction\" | \"fill\";\n  ground_truth: number;\n  seen: number;\n  missing: number;\n  coverage_pct: number | null;\n  observed_events_total: number;\n};\n\nexport type Analysis = {\n  semantics: {\n    latency_sign: string;\n    pending: string;\n    confirmed: string;\n    fill_audit: string;\n  };\n  chain_ground_truth: {\n    window: BlockWindow;\n    transactions: number;\n    fills: number;\n    multi_fill_transactions: number;\n    fills_by_version: Record<string, number>;\n    fills_by_contract: Record<string, number>;\n  };\n  coverage: Record<string, PathCoverage>;\n  latency: Record<string, LatencyStats | null>;\n  missing: {\n    transactions: Record<string, Array<{ tx_hash: string; block_number: number; fill_count: number }>>;\n    fills: Record<string, ChainFill[]>;\n  };\n  observations: {\n    total_events: number;\n    events_by_source_path: Record<string, number>;\n    struct_pending_events: number;\n    struct_confirmed_events: number;\n    polynode_pending_events: number;\n    polynode_status_update_events: number;\n    polynode_trade_events: number;\n    rtds_activity_events: number;\n  };\n};\n\ntype FirstArrival = Pick<\n  NormalizedEvent,\n  \"arrival_mono_ns\" | \"arrival_wall_ms\" | \"tx_hash\" | \"log_index\"\n>;\n\ntype PathSpec = {\n  unit: \"transaction\" | \"fill\";\n  match: (event: NormalizedEvent) => boolean;\n};\n\nconst PATHS: Record<string, PathSpec> = {\n  polynode_pending: {\n    unit: \"transaction\",\n    match: (event) => event.source === \"polynode\" && event.path === \"pending\",\n  },\n  polynode_status_update: {\n    unit: \"transaction\",\n    match: (event) => event.source === \"polynode\" && event.path === \"confirmed\",\n  },\n  polynode_trade_tx: {\n    unit: \"transaction\",\n    match: (event) => event.source === \"polynode\" && event.path === \"confirmed_fill\",\n  },\n  polynode_trade_fill: {\n    unit: \"fill\",\n    match: (event) => event.source === \"polynode\" && event.path === \"confirmed_fill\",\n  },\n  struct_pending: {\n    unit: \"transaction\",\n    match: (event) => event.source === \"struct\" && event.path === \"pending\",\n  },\n  struct_confirmed_tx: {\n    unit: \"transaction\",\n    match: (event) => event.source === \"struct\" && event.path === \"confirmed\",\n  },\n  struct_confirmed_fill: {\n    unit: \"fill\",\n    match: (event) =>\n      event.source === \"struct\" &&\n      event.path === \"confirmed\" &&\n      event.trade_type === \"OrderFilled\" &&\n      event.canonical_log_index != null,\n  },\n  struct_confirmed_record_fill: {\n    unit: \"fill\",\n    match: (event) =>\n      event.source === \"struct\" &&\n      event.path === \"confirmed\" &&\n      (event.trade_type === \"OrderFilled\" || event.trade_type === \"OrdersMatched\") &&\n      event.canonical_log_index != null,\n  },\n  rtds_activity: {\n    unit: \"transaction\",\n    match: (event) => event.source === \"rtds\" && event.path === \"activity\",\n  },\n};\n\nfunction quantile(sorted: number[], q: number): number {\n  if (sorted.length === 0) return Number.NaN;\n  const position = (sorted.length - 1) * q;\n  const base = Math.floor(position);\n  const fraction = position - base;\n  const lower = sorted[base];\n  const upper = sorted[base + 1] ?? lower;\n  return lower + fraction * (upper - lower);\n}\n\nfunction percentage(numerator: number, denominator: number): number | null {\n  return denominator === 0 ? null : (numerator / denominator) * 100;\n}\n\nfunction fillKey(txHash: string, logIndex: number): string {\n  return `${txHash}:${logIndex}`;\n}\n\nfunction earlier(candidate: FirstArrival, current: FirstArrival | undefined): boolean {\n  if (!current) return true;\n  return BigInt(candidate.arrival_mono_ns) < BigInt(current.arrival_mono_ns);\n}\n\nfunction firstByTransaction(events: NormalizedEvent[], match: PathSpec[\"match\"]): Map<string, FirstArrival> {\n  const result = new Map<string, FirstArrival>();\n  for (const event of events) {\n    if (!match(event) || !event.tx_hash) continue;\n    if (earlier(event, result.get(event.tx_hash))) result.set(event.tx_hash, event);\n  }\n  return result;\n}\n\nfunction firstByFill(events: NormalizedEvent[], match: PathSpec[\"match\"]): Map<string, FirstArrival> {\n  const result = new Map<string, FirstArrival>();\n  for (const event of events) {\n    if (!match(event) || !event.tx_hash) continue;\n    const canonicalIndex = event.source === \"struct\" ? event.canonical_log_index : event.log_index;\n    if (canonicalIndex == null) continue;\n    const key = fillKey(event.tx_hash, canonicalIndex);\n    if (earlier(event, result.get(key))) result.set(key, event);\n  }\n  return result;\n}\n\nfunction latencyStats(samples: LatencySample[]): LatencyStats | null {\n  if (samples.length === 0) return null;\n  const leads = samples.map((sample) => sample.left_lead_ms).sort((a, b) => a - b);\n  const leftFirst = leads.filter((lead) => lead > 0).length;\n  const rightFirst = leads.filter((lead) => lead < 0).length;\n  const ties = leads.length - leftFirst - rightFirst;\n  return {\n    samples: leads.length,\n    left_first: leftFirst,\n    right_first: rightFirst,\n    ties,\n    left_first_pct: (leftFirst / leads.length) * 100,\n    min_lead_ms: leads[0],\n    p25_lead_ms: quantile(leads, 0.25),\n    median_lead_ms: quantile(leads, 0.5),\n    mean_lead_ms: leads.reduce((sum, value) => sum + value, 0) / leads.length,\n    p75_lead_ms: quantile(leads, 0.75),\n    p95_lead_ms: quantile(leads, 0.95),\n    max_lead_ms: leads[leads.length - 1],\n  };\n}\n\nfunction compare(\n  comparison: string,\n  leftName: string,\n  rightName: string,\n  left: Map<string, FirstArrival>,\n  right: Map<string, FirstArrival>,\n  validKeys: Set<string>,\n): { stats: LatencyStats | null; samples: LatencySample[] } {\n  const samples: LatencySample[] = [];\n  for (const key of validKeys) {\n    const leftArrival = left.get(key);\n    const rightArrival = right.get(key);\n    if (!leftArrival || !rightArrival) continue;\n    const leftNs = BigInt(leftArrival.arrival_mono_ns);\n    const rightNs = BigInt(rightArrival.arrival_mono_ns);\n    const txHash = key.includes(\":\") ? key.slice(0, key.lastIndexOf(\":\")) : key;\n    const indexText = key.includes(\":\") ? key.slice(key.lastIndexOf(\":\") + 1) : undefined;\n    samples.push({\n      comparison,\n      join_key: key,\n      tx_hash: txHash,\n      log_index: indexText == null ? undefined : Number(indexText),\n      left: leftName,\n      right: rightName,\n      left_arrival_mono_ns: leftArrival.arrival_mono_ns,\n      right_arrival_mono_ns: rightArrival.arrival_mono_ns,\n      left_arrival_wall_ms: leftArrival.arrival_wall_ms,\n      right_arrival_wall_ms: rightArrival.arrival_wall_ms,\n      left_lead_ms: Number(rightNs - leftNs) / 1_000_000,\n    });\n  }\n  return { stats: latencyStats(samples), samples };\n}\n\nfunction increment(record: Record<string, number>, key: string): void {\n  record[key] = (record[key] ?? 0) + 1;\n}\n\nexport function analyze(\n  events: NormalizedEvent[],\n  chainFills: ChainFill[],\n  blockWindow: BlockWindow,\n): { analysis: Analysis; latencySamples: LatencySample[] } {\n  const chainFillByKey = new Map(chainFills.map((fill) => [fillKey(fill.tx_hash, fill.log_index), fill]));\n  const chainFillKeys = new Set(chainFillByKey.keys());\n  const txDetails = new Map<string, { block_number: number; fill_count: number }>();\n  for (const fill of chainFills) {\n    const current = txDetails.get(fill.tx_hash);\n    if (current) current.fill_count++;\n    else txDetails.set(fill.tx_hash, { block_number: fill.block_number, fill_count: 1 });\n  }\n  const chainTxKeys = new Set(txDetails.keys());\n\n  const txMaps: Record<string, Map<string, FirstArrival>> = {};\n  const fillMaps: Record<string, Map<string, FirstArrival>> = {};\n  for (const [name, spec] of Object.entries(PATHS)) {\n    if (spec.unit === \"transaction\") txMaps[name] = firstByTransaction(events, spec.match);\n    else fillMaps[name] = firstByFill(events, spec.match);\n  }\n\n  const coverage: Record<string, PathCoverage> = {};\n  const missingTransactions: Analysis[\"missing\"][\"transactions\"] = {};\n  const missingFills: Analysis[\"missing\"][\"fills\"] = {};\n  for (const [name, spec] of Object.entries(PATHS)) {\n    const observedEvents = events.filter(spec.match).length;\n    if (spec.unit === \"transaction\") {\n      const map = txMaps[name];\n      const missing = [...chainTxKeys]\n        .filter((key) => !map.has(key))\n        .map((tx_hash) => ({ tx_hash, ...txDetails.get(tx_hash)! }))\n        .sort((a, b) => a.block_number - b.block_number || a.tx_hash.localeCompare(b.tx_hash));\n      const seen = chainTxKeys.size - missing.length;\n      coverage[name] = {\n        unit: \"transaction\",\n        ground_truth: chainTxKeys.size,\n        seen,\n        missing: missing.length,\n        coverage_pct: percentage(seen, chainTxKeys.size),\n        observed_events_total: observedEvents,\n      };\n      missingTransactions[name] = missing;\n    } else {\n      const map = fillMaps[name];\n      const missing = [...chainFillKeys]\n        .filter((key) => !map.has(key))\n        .map((key) => chainFillByKey.get(key)!)\n        .sort((a, b) => a.block_number - b.block_number || a.log_index - b.log_index);\n      const seen = chainFillKeys.size - missing.length;\n      coverage[name] = {\n        unit: \"fill\",\n        ground_truth: chainFillKeys.size,\n        seen,\n        missing: missing.length,\n        coverage_pct: percentage(seen, chainFillKeys.size),\n        observed_events_total: observedEvents,\n      };\n      missingFills[name] = missing;\n    }\n  }\n\n  const comparisons: Array<{\n    name: string;\n    leftName: string;\n    rightName: string;\n    left: Map<string, FirstArrival>;\n    right: Map<string, FirstArrival>;\n    keys: Set<string>;\n  }> = [\n    {\n      name: \"polynode_pending_vs_polynode_status_update\",\n      leftName: \"polynode_pending\",\n      rightName: \"polynode_status_update\",\n      left: txMaps.polynode_pending,\n      right: txMaps.polynode_status_update,\n      keys: chainTxKeys,\n    },\n    {\n      name: \"struct_pending_vs_struct_confirmed\",\n      leftName: \"struct_pending\",\n      rightName: \"struct_confirmed\",\n      left: txMaps.struct_pending,\n      right: txMaps.struct_confirmed_tx,\n      keys: chainTxKeys,\n    },\n    {\n      name: \"polynode_pending_vs_struct_pending\",\n      leftName: \"polynode_pending\",\n      rightName: \"struct_pending\",\n      left: txMaps.polynode_pending,\n      right: txMaps.struct_pending,\n      keys: chainTxKeys,\n    },\n    {\n      name: \"polynode_status_update_vs_struct_confirmed\",\n      leftName: \"polynode_status_update\",\n      rightName: \"struct_confirmed\",\n      left: txMaps.polynode_status_update,\n      right: txMaps.struct_confirmed_tx,\n      keys: chainTxKeys,\n    },\n    {\n      name: \"polynode_pending_vs_rtds\",\n      leftName: \"polynode_pending\",\n      rightName: \"rtds_activity\",\n      left: txMaps.polynode_pending,\n      right: txMaps.rtds_activity,\n      keys: chainTxKeys,\n    },\n    {\n      name: \"polynode_status_update_vs_rtds\",\n      leftName: \"polynode_status_update\",\n      rightName: \"rtds_activity\",\n      left: txMaps.polynode_status_update,\n      right: txMaps.rtds_activity,\n      keys: chainTxKeys,\n    },\n    {\n      name: \"struct_pending_vs_rtds\",\n      leftName: \"struct_pending\",\n      rightName: \"rtds_activity\",\n      left: txMaps.struct_pending,\n      right: txMaps.rtds_activity,\n      keys: chainTxKeys,\n    },\n    {\n      name: \"struct_confirmed_vs_rtds\",\n      leftName: \"struct_confirmed\",\n      rightName: \"rtds_activity\",\n      left: txMaps.struct_confirmed_tx,\n      right: txMaps.rtds_activity,\n      keys: chainTxKeys,\n    },\n    {\n      name: \"polynode_trade_vs_struct_confirmed_record\",\n      leftName: \"polynode_trade\",\n      rightName: \"struct_confirmed_record\",\n      left: fillMaps.polynode_trade_fill,\n      right: fillMaps.struct_confirmed_record_fill,\n      keys: chainFillKeys,\n    },\n    {\n      name: \"polynode_trade_vs_struct_orderfilled_only\",\n      leftName: \"polynode_trade\",\n      rightName: \"struct_orderfilled\",\n      left: fillMaps.polynode_trade_fill,\n      right: fillMaps.struct_confirmed_fill,\n      keys: chainFillKeys,\n    },\n  ];\n\n  const latency: Record<string, LatencyStats | null> = {};\n  const latencySamples: LatencySample[] = [];\n  for (const spec of comparisons) {\n    const result = compare(spec.name, spec.leftName, spec.rightName, spec.left, spec.right, spec.keys);\n    latency[spec.name] = result.stats;\n    latencySamples.push(...result.samples);\n  }\n\n  const fillsByVersion: Record<string, number> = {};\n  const fillsByContract: Record<string, number> = {};\n  for (const fill of chainFills) {\n    increment(fillsByVersion, fill.version);\n    increment(fillsByContract, fill.address);\n  }\n  const eventsBySourcePath: Record<string, number> = {};\n  for (const event of events) increment(eventsBySourcePath, `${event.source}:${event.path}`);\n\n  const analysis: Analysis = {\n    semantics: {\n      latency_sign: \"Each lead is right_arrival - left_arrival. Positive values mean the named left-hand path arrived first.\",\n      pending: \"Struct pending is its documented received_at-without-block event; PolyNode pending is settlement with status=pending.\",\n      confirmed: \"Struct confirmed is a trade event carrying block; PolyNode confirmed is status_update, as requested.\",\n      fill_audit: \"Canonical confirmed-fill coverage is keyed by (transaction hash, block-global log index) against Polygon OrderFilled logs. Struct OrdersMatched records are also mapped to the corresponding taker OrderFilled by unique order hash and reported separately from strict event-type parity.\",\n    },\n    chain_ground_truth: {\n      window: blockWindow,\n      transactions: chainTxKeys.size,\n      fills: chainFillKeys.size,\n      multi_fill_transactions: [...txDetails.values()].filter((detail) => detail.fill_count > 1).length,\n      fills_by_version: fillsByVersion,\n      fills_by_contract: fillsByContract,\n    },\n    coverage,\n    latency,\n    missing: { transactions: missingTransactions, fills: missingFills },\n    observations: {\n      total_events: events.length,\n      events_by_source_path: eventsBySourcePath,\n      struct_pending_events: events.filter(PATHS.struct_pending.match).length,\n      struct_confirmed_events: events.filter(PATHS.struct_confirmed_tx.match).length,\n      polynode_pending_events: events.filter(PATHS.polynode_pending.match).length,\n      polynode_status_update_events: events.filter(PATHS.polynode_status_update.match).length,\n      polynode_trade_events: events.filter(PATHS.polynode_trade_fill.match).length,\n      rtds_activity_events: events.filter(PATHS.rtds_activity.match).length,\n    },\n  };\n\n  return { analysis, latencySamples };\n}\n\nfunction fmtPct(value: number | null): string {\n  return value == null ? \"n/a\" : `${value.toFixed(2)}%`;\n}\n\nfunction fmtMs(value: number): string {\n  return `${value >= 0 ? \"+\" : \"\"}${value.toFixed(2)} ms`;\n}\n\nfunction coverageRow(label: string, value: PathCoverage): string {\n  return `| ${label} | ${value.unit} | ${value.seen}/${value.ground_truth} | ${fmtPct(value.coverage_pct)} | ${value.missing} |`;\n}\n\nfunction latencyRow(label: string, stats: LatencyStats | null): string {\n  if (!stats) return `| ${label} | 0 | n/a | n/a | n/a |`;\n  return `| ${label} | ${stats.samples} | ${fmtMs(stats.median_lead_ms)} | ${fmtMs(stats.p95_lead_ms)} | ${stats.left_first_pct.toFixed(2)}% |`;\n}\n\nexport function renderReport(\n  analysis: Analysis,\n  manifest: Record<string, unknown>,\n): string {\n  const coverage = analysis.coverage;\n  const latency = analysis.latency;\n  const structPendingBroken = analysis.observations.struct_pending_events === 0;\n  const generatedAt = String(manifest.generated_at ?? new Date().toISOString());\n  const window = analysis.chain_ground_truth.window;\n\n  const findings: string[] = [];\n  if (structPendingBroken) {\n    findings.push(\n      \"- Struct emitted zero pending events under its documented `status: \\\"all\\\"` subscription. Pending latency and pending coverage are therefore unavailable, not zero-latency wins or losses.\",\n    );\n  }\n  const confirmedVsRtds = latency.struct_confirmed_vs_rtds;\n  if (confirmedVsRtds) {\n    findings.push(\n      `- Struct confirmed vs RTDS had ${confirmedVsRtds.samples} on-chain-overlap samples; Struct's median lead was ${fmtMs(confirmedVsRtds.median_lead_ms)} (positive means Struct first).`,\n    );\n  }\n  const pnVsStruct = latency.polynode_status_update_vs_struct_confirmed;\n  if (pnVsStruct) {\n    findings.push(\n      `- PolyNode status_update vs Struct confirmed had ${pnVsStruct.samples} samples; PolyNode's median lead was ${fmtMs(pnVsStruct.median_lead_ms)} and arrived first in ${pnVsStruct.left_first_pct.toFixed(2)}%.`,\n    );\n  }\n  findings.push(\n    `- Independent Polygon logs contained ${analysis.chain_ground_truth.fills} OrderFilled fills across ${analysis.chain_ground_truth.transactions} transactions; ${analysis.chain_ground_truth.multi_fill_transactions} transactions contained multiple fills.`,\n  );\n  const structRecords = coverage.struct_confirmed_record_fill;\n  findings.push(\n    `- Struct represented ${structRecords.seen}/${structRecords.ground_truth} on-chain fills when both of its documented record types are counted; strict OrderFilled-type parity was ${coverage.struct_confirmed_fill.seen}/${coverage.struct_confirmed_fill.ground_truth}.`,\n  );\n\n  return `# PolyNode vs Struct vs Polymarket RTDS smoke test\n\nGenerated: ${generatedAt}\n\n## Result\n\n${findings.join(\"\\n\")}\n\nThis is a same-vantage-point smoke test, not a universal latency claim. The raw observations and exact missing identifiers are retained so every conclusion can be independently checked.\n\n## On-chain completeness\n\nThe denominator is Polygon \\`OrderFilled\\` logs in the fully observed block interval **${window.from_block} through ${window.to_block}**, inclusive. Transaction coverage answers \"did the feed surface this transaction at least once?\" Fill coverage is stricter and keys every execution by \\`(transaction_hash, log_index)\\`.\n\n| Path | Unit | Seen | Coverage | Missing |\n|---|---:|---:|---:|---:|\n${coverageRow(\"PolyNode pending settlement\", coverage.polynode_pending)}\n${coverageRow(\"PolyNode status_update\", coverage.polynode_status_update)}\n${coverageRow(\"PolyNode confirmed trade (tx)\", coverage.polynode_trade_tx)}\n${coverageRow(\"PolyNode confirmed trade (fill)\", coverage.polynode_trade_fill)}\n${coverageRow(\"Struct pending\", coverage.struct_pending)}\n${coverageRow(\"Struct confirmed (tx)\", coverage.struct_confirmed_tx)}\n${coverageRow(\"Struct confirmed OrderFilled only (strict type parity)\", coverage.struct_confirmed_fill)}\n${coverageRow(\"Struct confirmed record (OrderFilled + mapped OrdersMatched)\", coverage.struct_confirmed_record_fill)}\n${coverageRow(\"RTDS activity\", coverage.rtds_activity)}\n\n## Latency\n\nAll arrivals were stamped inside one Node.js process using \\`process.hrtime.bigint()\\`. In every row, the number is **right arrival minus left arrival**; positive means the left-hand path arrived first. Only hashes/fills independently present in the on-chain denominator are eligible.\n\n| Comparison (left vs right) | Samples | Median left lead | P95 left lead | Left first |\n|---|---:|---:|---:|---:|\n${latencyRow(\"PolyNode pending vs PolyNode status_update\", latency.polynode_pending_vs_polynode_status_update)}\n${latencyRow(\"Struct pending vs Struct confirmed\", latency.struct_pending_vs_struct_confirmed)}\n${latencyRow(\"PolyNode pending vs Struct pending\", latency.polynode_pending_vs_struct_pending)}\n${latencyRow(\"PolyNode status_update vs Struct confirmed\", latency.polynode_status_update_vs_struct_confirmed)}\n${latencyRow(\"PolyNode pending vs RTDS\", latency.polynode_pending_vs_rtds)}\n${latencyRow(\"PolyNode status_update vs RTDS\", latency.polynode_status_update_vs_rtds)}\n${latencyRow(\"Struct pending vs RTDS\", latency.struct_pending_vs_rtds)}\n${latencyRow(\"Struct confirmed vs RTDS\", latency.struct_confirmed_vs_rtds)}\n${latencyRow(\"PolyNode trade vs Struct confirmed record\", latency.polynode_trade_vs_struct_confirmed_record)}\n${latencyRow(\"PolyNode trade vs Struct OrderFilled only\", latency.polynode_trade_vs_struct_orderfilled_only)}\n\n## Subscription semantics\n\n- Struct: room \\`polymarket_trades\\`, filters \\`{ \"status\": \"all\", \"subscribe_all\": true }\\`. Its published classification is preserved exactly: \\`received_at\\` present and \\`block\\` absent means pending; otherwise confirmed.\n- PolyNode pending: \\`settlement\\` events whose \\`status\\` is exactly \\`pending\\`.\n- PolyNode requested confirmed equivalent: \\`status_update\\`.\n- PolyNode exact confirmed-fill audit: \\`trade\\`, keyed by receipt \\`log_index\\`.\n- Polymarket RTDS: \\`activity/trades\\` and \\`activity/orders_matched\\`, matching Struct's published harness.\n\n## Boundary controls\n\nEvery socket was connected and acknowledged before warm-up. The denominator begins two future Polygon blocks after the warm-up tip, avoiding a partially observed first block. It ends two blocks behind the collection-end tip, and sockets remain open for a confirmation tail afterward. API keys are excluded from all artifacts; the manifest contains only one-way fingerprints.\n\n## Reproduction and artifacts\n\n- \\`manifest.json\\`: configuration, subscription acknowledgments, versions, key fingerprints, DNS, connection lifecycle, and anchored block window.\n- \\`events.ndjson\\`: normalized timestamped observations.\n- \\`raw_frames.ndjson\\`: parsed source frames/events for audit, with no credentials.\n- \\`rpc_logs.json\\`: independent Polygon logs.\n- \\`latency_samples.csv\\`: every matched latency sample, not only aggregates.\n- \\`missing.json\\`: exact missing transaction hashes and fill identifiers.\n- \\`summary.json\\`: machine-readable aggregate results.\n\nRun \\`npm test\\` before collection and invoke the harness with credentials supplied only through environment variables. See \\`README.md\\` for the exact command.\n`;\n}\n\nfunction csvCell(value: string | number | undefined): string {\n  if (value == null) return \"\";\n  const text = String(value);\n  return /[\",\\n]/.test(text) ? `\"${text.replaceAll('\"', '\"\"')}\"` : text;\n}\n\nexport function latencySamplesCsv(samples: LatencySample[]): string {\n  const columns: Array<keyof LatencySample> = [\n    \"comparison\",\n    \"join_key\",\n    \"tx_hash\",\n    \"log_index\",\n    \"left\",\n    \"right\",\n    \"left_arrival_mono_ns\",\n    \"right_arrival_mono_ns\",\n    \"left_arrival_wall_ms\",\n    \"right_arrival_wall_ms\",\n    \"left_lead_ms\",\n  ];\n  return [\n    columns.join(\",\"),\n    ...samples.map((sample) => columns.map((column) => csvCell(sample[column])).join(\",\")),\n  ].join(\"\\n\") + \"\\n\";\n}\n",
    "analysis.test.ts": "import assert from \"node:assert/strict\";\nimport test from \"node:test\";\n\nimport { analyze, type ChainFill, type NormalizedEvent } from \"./analysis.js\";\n\nfunction event(\n  sequence: number,\n  source: NormalizedEvent[\"source\"],\n  path: NormalizedEvent[\"path\"],\n  txHash: string,\n  logIndex?: number,\n  tradeType?: string,\n): NormalizedEvent {\n  return {\n    sequence,\n    source,\n    path,\n    event_type: path,\n    arrival_mono_ns: String(sequence * 1_000_000),\n    arrival_wall_ms: sequence,\n    tx_hash: txHash,\n    log_index: logIndex,\n    canonical_log_index: source === \"struct\" ? logIndex : undefined,\n    trade_type: tradeType,\n  };\n}\n\nconst tx = `0x${\"a\".repeat(64)}`;\nconst fills: ChainFill[] = [\n  {\n    tx_hash: tx,\n    log_index: 7,\n    receipt_log_index: 1,\n    block_number: 100,\n    block_hash: `0x${\"b\".repeat(64)}`,\n    address: `0x${\"1\".repeat(40)}`,\n    version: \"v1\",\n  },\n  {\n    tx_hash: tx,\n    log_index: 9,\n    receipt_log_index: 3,\n    block_number: 100,\n    block_hash: `0x${\"b\".repeat(64)}`,\n    address: `0x${\"1\".repeat(40)}`,\n    version: \"v1\",\n  },\n];\n\ntest(\"transaction coverage cannot conceal a missing fill\", () => {\n  const events: NormalizedEvent[] = [event(1, \"struct\", \"confirmed\", tx, 7, \"OrderFilled\")];\n  const { analysis } = analyze(events, fills, { from_block: 100, to_block: 100 });\n\n  assert.equal(analysis.coverage.struct_confirmed_tx.seen, 1);\n  assert.equal(analysis.coverage.struct_confirmed_tx.coverage_pct, 100);\n  assert.equal(analysis.coverage.struct_confirmed_fill.seen, 1);\n  assert.equal(analysis.coverage.struct_confirmed_fill.coverage_pct, 50);\n  assert.equal(analysis.missing.fills.struct_confirmed_fill[0].log_index, 9);\n});\n\ntest(\"positive lead means the named left-hand source arrived first\", () => {\n  const events: NormalizedEvent[] = [\n    event(1, \"polynode\", \"confirmed\", tx),\n    event(4, \"struct\", \"confirmed\", tx, 7, \"OrderFilled\"),\n  ];\n  const { analysis, latencySamples } = analyze(events, fills.slice(0, 1), {\n    from_block: 100,\n    to_block: 100,\n  });\n\n  const stats = analysis.latency.polynode_status_update_vs_struct_confirmed;\n  assert.ok(stats);\n  assert.equal(stats.samples, 1);\n  assert.equal(stats.median_lead_ms, 3);\n  assert.equal(stats.left_first_pct, 100);\n  assert.equal(\n    latencySamples.find((sample) => sample.comparison === \"polynode_status_update_vs_struct_confirmed\")?.left_lead_ms,\n    3,\n  );\n});\n\ntest(\"zero Struct pending events produces no fabricated latency statistic\", () => {\n  const events: NormalizedEvent[] = [event(1, \"polynode\", \"pending\", tx)];\n  const { analysis } = analyze(events, fills.slice(0, 1), { from_block: 100, to_block: 100 });\n\n  assert.equal(analysis.observations.struct_pending_events, 0);\n  assert.equal(analysis.coverage.struct_pending.seen, 0);\n  assert.equal(analysis.latency.polynode_pending_vs_struct_pending, null);\n});\n",
    "aggregate.ts": "import { readdir, readFile, writeFile } from \"node:fs/promises\";\nimport { basename, resolve } from \"node:path\";\n\ntype Coverage = {\n  unit: \"transaction\" | \"fill\";\n  ground_truth: number;\n  seen: number;\n  missing: number;\n  coverage_pct: number | null;\n  observed_events_total: number;\n};\n\ntype Latency = {\n  samples: number;\n  left_first: number;\n  right_first: number;\n  ties: number;\n  left_first_pct: number;\n  min_lead_ms: number;\n  p25_lead_ms: number;\n  median_lead_ms: number;\n  mean_lead_ms: number;\n  p75_lead_ms: number;\n  p95_lead_ms: number;\n  max_lead_ms: number;\n};\n\ntype Summary = {\n  chain_ground_truth: { transactions: number; fills: number };\n  coverage: Record<string, Coverage>;\n  latency: Record<string, Latency | null>;\n  observations: Record<string, number | Record<string, number>>;\n};\n\ntype Manifest = {\n  provenance: { index_ts_sha256: string; analysis_ts_sha256: string };\n  config: { duration_sec: number; struct_status: string };\n  block_window: { from_block: number; to_block: number };\n  lifecycle: Array<{ event: string }>;\n  rpc_audit: Array<{ error?: string }>;\n  struct_fill_index_reconciliation: { unmatched: number; index_order_hash_mismatches: number };\n};\n\ntype Run = {\n  name: string;\n  summary: Summary;\n  manifest: Manifest;\n  samples: Map<string, number[]>;\n};\n\nfunction quantile(sorted: number[], q: number): number {\n  const position = (sorted.length - 1) * q;\n  const base = Math.floor(position);\n  const fraction = position - base;\n  return sorted[base] + fraction * ((sorted[base + 1] ?? sorted[base]) - sorted[base]);\n}\n\nfunction stats(values: number[]): Latency | null {\n  if (!values.length) return null;\n  const sorted = [...values].sort((a, b) => a - b);\n  const left = sorted.filter((value) => value > 0).length;\n  const right = sorted.filter((value) => value < 0).length;\n  return {\n    samples: sorted.length,\n    left_first: left,\n    right_first: right,\n    ties: sorted.length - left - right,\n    left_first_pct: (left / sorted.length) * 100,\n    min_lead_ms: sorted[0],\n    p25_lead_ms: quantile(sorted, 0.25),\n    median_lead_ms: quantile(sorted, 0.5),\n    mean_lead_ms: sorted.reduce((sum, value) => sum + value, 0) / sorted.length,\n    p75_lead_ms: quantile(sorted, 0.75),\n    p95_lead_ms: quantile(sorted, 0.95),\n    max_lead_ms: sorted[sorted.length - 1],\n  };\n}\n\nfunction parseSamples(csv: string): Map<string, number[]> {\n  const result = new Map<string, number[]>();\n  for (const line of csv.trim().split(\"\\n\").slice(1)) {\n    const columns = line.split(\",\");\n    const comparison = columns[0];\n    const lead = Number(columns[10]);\n    if (!comparison || !Number.isFinite(lead)) continue;\n    const values = result.get(comparison) ?? [];\n    values.push(lead);\n    result.set(comparison, values);\n  }\n  return result;\n}\n\nasync function loadRun(directory: string): Promise<Run> {\n  const [summary, manifest, csv] = await Promise.all([\n    readFile(resolve(directory, \"summary.json\"), \"utf8\"),\n    readFile(resolve(directory, \"manifest.json\"), \"utf8\"),\n    readFile(resolve(directory, \"latency_samples.csv\"), \"utf8\"),\n  ]);\n  return {\n    name: basename(directory),\n    summary: JSON.parse(summary) as Summary,\n    manifest: JSON.parse(manifest) as Manifest,\n    samples: parseSamples(csv),\n  };\n}\n\nfunction percentage(numerator: number, denominator: number): number | null {\n  return denominator ? (numerator / denominator) * 100 : null;\n}\n\nfunction fmtPercent(value: number | null): string {\n  return value == null ? \"n/a\" : `${value.toFixed(2)}%`;\n}\n\nfunction fmtSeconds(milliseconds: number): string {\n  return `${milliseconds >= 0 ? \"+\" : \"\"}${(milliseconds / 1_000).toFixed(3)} s`;\n}\n\nfunction latencyRow(label: string, value: Latency | null): string {\n  if (!value) return `| ${label} | 0 | n/a | n/a | n/a |`;\n  return `| ${label} | ${value.samples.toLocaleString()} | ${fmtSeconds(value.median_lead_ms)} | ${fmtSeconds(value.p95_lead_ms)} | ${value.left_first_pct.toFixed(2)}% |`;\n}\n\nasync function main(): Promise<void> {\n  const root = resolve(process.argv[2] ?? \"results/full-suite-2026-08-01\");\n  const names = (await readdir(root)).filter((name) => name.startsWith(\"run-\")).sort();\n  if (!names.length) throw new Error(`No run directories found under ${root}`);\n  const runs = await Promise.all(names.map((name) => loadRun(resolve(root, name))));\n  runs.sort((a, b) => a.manifest.block_window.from_block - b.manifest.block_window.from_block);\n\n  const issues: string[] = [];\n  const sourceHashes = new Set(runs.map((run) =>\n    `${run.manifest.provenance.index_ts_sha256}:${run.manifest.provenance.analysis_ts_sha256}`,\n  ));\n  if (sourceHashes.size !== 1) issues.push(\"Harness hashes differ across runs\");\n  for (let index = 1; index < runs.length; index++) {\n    if (runs[index].manifest.block_window.from_block <= runs[index - 1].manifest.block_window.to_block) {\n      issues.push(`${runs[index - 1].name} overlaps ${runs[index].name}`);\n    }\n  }\n  for (const run of runs) {\n    if (run.manifest.config.struct_status !== \"all\") issues.push(`${run.name}: Struct status was not all`);\n    if (run.manifest.lifecycle.some(({ event }) => /error|failed|reconnect/i.test(event))) {\n      issues.push(`${run.name}: socket lifecycle error/reconnect`);\n    }\n    if (run.manifest.rpc_audit.some(({ error }) => error)) issues.push(`${run.name}: RPC error`);\n    if (run.manifest.struct_fill_index_reconciliation.unmatched) issues.push(`${run.name}: unmatched Struct fill`);\n    if (run.manifest.struct_fill_index_reconciliation.index_order_hash_mismatches) {\n      issues.push(`${run.name}: Struct index/order-hash mismatch`);\n    }\n  }\n\n  const coverage: Record<string, Coverage> = {};\n  for (const name of Object.keys(runs[0].summary.coverage)) {\n    const values = runs.map((run) => run.summary.coverage[name]);\n    const groundTruth = values.reduce((sum, value) => sum + value.ground_truth, 0);\n    const seen = values.reduce((sum, value) => sum + value.seen, 0);\n    coverage[name] = {\n      unit: values[0].unit,\n      ground_truth: groundTruth,\n      seen,\n      missing: groundTruth - seen,\n      coverage_pct: percentage(seen, groundTruth),\n      observed_events_total: values.reduce((sum, value) => sum + value.observed_events_total, 0),\n    };\n  }\n\n  const pooled = new Map<string, number[]>();\n  for (const run of runs) {\n    for (const [comparison, values] of run.samples) {\n      const current = pooled.get(comparison) ?? [];\n      current.push(...values);\n      pooled.set(comparison, current);\n    }\n  }\n  const latency = Object.fromEntries([...pooled].map(([name, values]) => [name, stats(values)])) as Record<string, Latency | null>;\n  for (const name of Object.keys(runs[0].summary.latency)) latency[name] ??= null;\n\n  const transactions = runs.reduce((sum, run) => sum + run.summary.chain_ground_truth.transactions, 0);\n  const fills = runs.reduce((sum, run) => sum + run.summary.chain_ground_truth.fills, 0);\n  const suiteRuns = runs.map((run) => ({\n    name: run.name,\n    duration_sec: run.manifest.config.duration_sec,\n    ...run.manifest.block_window,\n    transactions: run.summary.chain_ground_truth.transactions,\n    fills: run.summary.chain_ground_truth.fills,\n    struct_pending_events: Number(run.summary.observations.struct_pending_events),\n    confirmed_fill_latency: run.summary.latency.polynode_trade_vs_struct_confirmed_record,\n    pending_vs_rtds_latency: run.summary.latency.polynode_pending_vs_rtds,\n    rtds_coverage: run.summary.coverage.rtds_activity.coverage_pct,\n  }));\n  const suite = {\n    generated_at: new Date().toISOString(),\n    runs: suiteRuns,\n    run_count: runs.length,\n    scheduled_duration_sec: runs.reduce((sum, run) => sum + run.manifest.config.duration_sec, 0),\n    chain_ground_truth: { transactions, fills },\n    coverage,\n    latency,\n    integrity: { ok: issues.length === 0, issues, source_hashes: [...sourceHashes] },\n  };\n\n  const runRows = suiteRuns.map((run) => {\n    const result = run.confirmed_fill_latency;\n    return `| ${run.name} | ${run.duration_sec}s | ${run.from_block}-${run.to_block} | ${run.transactions.toLocaleString()} | ${run.fills.toLocaleString()} | ${run.struct_pending_events} | ${result ? fmtSeconds(result.median_lead_ms) : \"n/a\"} | ${result ? `${result.left_first_pct.toFixed(2)}%` : \"n/a\"} | ${fmtPercent(run.rtds_coverage)} |`;\n  }).join(\"\\n\");\n\n  const report = `# Exact Struct-methodology benchmark suite\n\nGenerated: ${suite.generated_at}\n\nFive non-overlapping windows reproduce Struct's subscription and heartbeat implementation, while adding PolyNode and independent Polygon/Alchemy receipts. Positive latency means the left-hand path arrived first.\n\n## Totals\n\n- Core collection: ${suite.scheduled_duration_sec} seconds across ${suite.run_count} windows.\n- Chain truth: ${transactions.toLocaleString()} transactions / ${fills.toLocaleString()} OrderFilled logs.\n- Struct pending events: ${coverage.struct_pending.observed_events_total.toLocaleString()}.\n- Integrity checks: ${suite.integrity.ok ? \"passed\" : `failed: ${issues.join(\"; \")}`}.\n\n## Per-window results\n\n| Run | Duration | Blocks | Transactions | Fills | Struct pending | Our confirmed-fill median lead | Our fills first | RTDS tx coverage |\n|---|---:|---:|---:|---:|---:|---:|---:|---:|\n${runRows}\n\n## Pooled latency\n\n| Left vs right | Samples | Median left lead | P95 left lead | Left first |\n|---|---:|---:|---:|---:|\n${latencyRow(\"PolyNode pending vs RTDS activity\", latency.polynode_pending_vs_rtds)}\n${latencyRow(\"PolyNode status_update vs RTDS activity\", latency.polynode_status_update_vs_rtds)}\n${latencyRow(\"PolyNode status_update vs Struct confirmed\", latency.polynode_status_update_vs_struct_confirmed)}\n${latencyRow(\"PolyNode confirmed fill vs Struct confirmed record\", latency.polynode_trade_vs_struct_confirmed_record)}\n${latencyRow(\"Struct confirmed vs RTDS activity\", latency.struct_confirmed_vs_rtds)}\n${latencyRow(\"PolyNode pending vs Struct pending\", latency.polynode_pending_vs_struct_pending)}\n\n## Confirmed completeness\n\n| Path | Seen | Coverage | Missing |\n|---|---:|---:|---:|\n| PolyNode status_update transactions | ${coverage.polynode_status_update.seen.toLocaleString()}/${coverage.polynode_status_update.ground_truth.toLocaleString()} | ${fmtPercent(coverage.polynode_status_update.coverage_pct)} | ${coverage.polynode_status_update.missing} |\n| PolyNode confirmed fills | ${coverage.polynode_trade_fill.seen.toLocaleString()}/${coverage.polynode_trade_fill.ground_truth.toLocaleString()} | ${fmtPercent(coverage.polynode_trade_fill.coverage_pct)} | ${coverage.polynode_trade_fill.missing} |\n| Struct confirmed transactions | ${coverage.struct_confirmed_tx.seen.toLocaleString()}/${coverage.struct_confirmed_tx.ground_truth.toLocaleString()} | ${fmtPercent(coverage.struct_confirmed_tx.coverage_pct)} | ${coverage.struct_confirmed_tx.missing} |\n| Struct confirmed records mapped to fills | ${coverage.struct_confirmed_record_fill.seen.toLocaleString()}/${coverage.struct_confirmed_record_fill.ground_truth.toLocaleString()} | ${fmtPercent(coverage.struct_confirmed_record_fill.coverage_pct)} | ${coverage.struct_confirmed_record_fill.missing} |\n| RTDS activity transactions | ${coverage.rtds_activity.seen.toLocaleString()}/${coverage.rtds_activity.ground_truth.toLocaleString()} | ${fmtPercent(coverage.rtds_activity.coverage_pct)} | ${coverage.rtds_activity.missing} |\n\nThe RTDS results in this suite use Struct's literal uppercase \\`PING\\`. Polymarket's official client uses lowercase \\`ping\\`; an official-heartbeat control must be kept separate from these exact-reproduction results.\n`;\n\n  await Promise.all([\n    writeFile(resolve(root, \"suite-summary.json\"), JSON.stringify(suite, null, 2) + \"\\n\"),\n    writeFile(resolve(root, \"SUITE_REPORT.md\"), report),\n  ]);\n  console.log(`Aggregated ${runs.length} runs into ${root}`);\n  if (issues.length) process.exitCode = 2;\n}\n\nmain().catch((error) => {\n  console.error(error instanceof Error ? error.message : error);\n  process.exitCode = 1;\n});\n",
    "package.json": "{\n  \"name\": \"polymarket-rtds-vs-struct-benchmark\",\n  \"version\": \"1.0.0\",\n  \"description\": \"Race Polymarket's public RTDS feed against the Struct Trades WebSocket and measure the latency, coverage, and mempool reliability difference.\",\n  \"type\": \"module\",\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"start\": \"tsx index.ts\",\n    \"bench\": \"tsx index.ts\",\n    \"aggregate\": \"tsx aggregate.ts\",\n    \"check\": \"tsc --noEmit\",\n    \"test\": \"tsx --test analysis.test.ts\"\n  },\n  \"dependencies\": {\n    \"@structbuild/sdk\": \"^0.6.7\",\n    \"ws\": \"^8.18.0\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^22.10.0\",\n    \"@types/ws\": \"^8.5.13\",\n    \"tsx\": \"^4.19.0\",\n    \"typescript\": \"^5.7.0\"\n  },\n  \"engines\": {\n    \"node\": \">=20\"\n  }\n}\n",
    "package-lock.json": "{\n  \"name\": \"polymarket-latency-benchmark\",\n  \"version\": \"1.0.0\",\n  \"lockfileVersion\": 3,\n  \"requires\": true,\n  \"packages\": {\n    \"\": {\n      \"name\": \"polymarket-latency-benchmark\",\n      \"version\": \"1.0.0\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@structbuild/sdk\": \"^0.6.7\",\n        \"ws\": \"^8.18.0\"\n      },\n      \"devDependencies\": {\n        \"@types/node\": \"^22.10.0\",\n        \"@types/ws\": \"^8.5.13\",\n        \"tsx\": \"^4.19.0\",\n        \"typescript\": \"^5.7.0\"\n      },\n      \"engines\": {\n        \"node\": \">=20\"\n      }\n    },\n    \"node_modules/@esbuild/aix-ppc64\": {\n      \"version\": \"0.28.1\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz\",\n      \"integrity\": \"sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==\",\n      \"cpu\": [\n        \"ppc64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"aix\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/android-arm\": {\n      \"version\": \"0.28.1\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz\",\n      \"integrity\": \"sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==\",\n      \"cpu\": [\n        \"arm\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"android\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/android-arm64\": {\n      \"version\": \"0.28.1\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz\",\n      \"integrity\": \"sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"android\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/android-x64\": {\n      \"version\": \"0.28.1\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz\",\n      \"integrity\": \"sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"android\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/darwin-arm64\": {\n      \"version\": \"0.28.1\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz\",\n      \"integrity\": \"sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"darwin\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/darwin-x64\": {\n      \"version\": \"0.28.1\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz\",\n      \"integrity\": \"sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"darwin\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/freebsd-arm64\": {\n      \"version\": \"0.28.1\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz\",\n      \"integrity\": \"sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"freebsd\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/freebsd-x64\": {\n      \"version\": \"0.28.1\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz\",\n      \"integrity\": \"sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"freebsd\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/linux-arm\": {\n      \"version\": \"0.28.1\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz\",\n      \"integrity\": \"sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==\",\n      \"cpu\": [\n        \"arm\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/linux-arm64\": {\n      \"version\": \"0.28.1\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz\",\n      \"integrity\": \"sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/linux-ia32\": {\n      \"version\": \"0.28.1\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz\",\n      \"integrity\": \"sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==\",\n      \"cpu\": [\n        \"ia32\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/linux-loong64\": {\n      \"version\": \"0.28.1\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz\",\n      \"integrity\": \"sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==\",\n      \"cpu\": [\n        \"loong64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/linux-mips64el\": {\n      \"version\": \"0.28.1\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz\",\n      \"integrity\": \"sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==\",\n      \"cpu\": [\n        \"mips64el\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/linux-ppc64\": {\n      \"version\": \"0.28.1\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz\",\n      \"integrity\": \"sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==\",\n      \"cpu\": [\n        \"ppc64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/linux-riscv64\": {\n      \"version\": \"0.28.1\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz\",\n      \"integrity\": \"sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==\",\n      \"cpu\": [\n        \"riscv64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/linux-s390x\": {\n      \"version\": \"0.28.1\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz\",\n      \"integrity\": \"sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==\",\n      \"cpu\": [\n        \"s390x\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/linux-x64\": {\n      \"version\": \"0.28.1\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz\",\n      \"integrity\": \"sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/netbsd-arm64\": {\n      \"version\": \"0.28.1\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz\",\n      \"integrity\": \"sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"netbsd\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/netbsd-x64\": {\n      \"version\": \"0.28.1\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz\",\n      \"integrity\": \"sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"netbsd\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/openbsd-arm64\": {\n      \"version\": \"0.28.1\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz\",\n      \"integrity\": \"sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"openbsd\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/openbsd-x64\": {\n      \"version\": \"0.28.1\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz\",\n      \"integrity\": \"sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"openbsd\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/openharmony-arm64\": {\n      \"version\": \"0.28.1\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz\",\n      \"integrity\": \"sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"openharmony\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/sunos-x64\": {\n      \"version\": \"0.28.1\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz\",\n      \"integrity\": \"sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"sunos\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/win32-arm64\": {\n      \"version\": \"0.28.1\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz\",\n      \"integrity\": \"sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"win32\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/win32-ia32\": {\n      \"version\": \"0.28.1\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz\",\n      \"integrity\": \"sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==\",\n      \"cpu\": [\n        \"ia32\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"win32\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@esbuild/win32-x64\": {\n      \"version\": \"0.28.1\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz\",\n      \"integrity\": \"sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"win32\"\n      ],\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@structbuild/sdk\": {\n      \"version\": \"0.6.7\",\n      \"resolved\": \"https://registry.npmjs.org/@structbuild/sdk/-/sdk-0.6.7.tgz\",\n      \"integrity\": \"sha512-+jBZkWZrarTm++UM2KSO1ZqYpSwvKJo8ktusUZgMP1esims0iw2XWE0MfSFQV2ay6Epi2OkHS2+VuM8BX9xB9A==\"\n    },\n    \"node_modules/@types/node\": {\n      \"version\": \"22.20.1\",\n      \"resolved\": \"https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz\",\n      \"integrity\": \"sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"undici-types\": \"~6.21.0\"\n      }\n    },\n    \"node_modules/@types/ws\": {\n      \"version\": \"8.18.1\",\n      \"resolved\": \"https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz\",\n      \"integrity\": \"sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/node\": \"*\"\n      }\n    },\n    \"node_modules/esbuild\": {\n      \"version\": \"0.28.1\",\n      \"resolved\": \"https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz\",\n      \"integrity\": \"sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==\",\n      \"dev\": true,\n      \"hasInstallScript\": true,\n      \"license\": \"MIT\",\n      \"bin\": {\n        \"esbuild\": \"bin/esbuild\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"optionalDependencies\": {\n        \"@esbuild/aix-ppc64\": \"0.28.1\",\n        \"@esbuild/android-arm\": \"0.28.1\",\n        \"@esbuild/android-arm64\": \"0.28.1\",\n        \"@esbuild/android-x64\": \"0.28.1\",\n        \"@esbuild/darwin-arm64\": \"0.28.1\",\n        \"@esbuild/darwin-x64\": \"0.28.1\",\n        \"@esbuild/freebsd-arm64\": \"0.28.1\",\n        \"@esbuild/freebsd-x64\": \"0.28.1\",\n        \"@esbuild/linux-arm\": \"0.28.1\",\n        \"@esbuild/linux-arm64\": \"0.28.1\",\n        \"@esbuild/linux-ia32\": \"0.28.1\",\n        \"@esbuild/linux-loong64\": \"0.28.1\",\n        \"@esbuild/linux-mips64el\": \"0.28.1\",\n        \"@esbuild/linux-ppc64\": \"0.28.1\",\n        \"@esbuild/linux-riscv64\": \"0.28.1\",\n        \"@esbuild/linux-s390x\": \"0.28.1\",\n        \"@esbuild/linux-x64\": \"0.28.1\",\n        \"@esbuild/netbsd-arm64\": \"0.28.1\",\n        \"@esbuild/netbsd-x64\": \"0.28.1\",\n        \"@esbuild/openbsd-arm64\": \"0.28.1\",\n        \"@esbuild/openbsd-x64\": \"0.28.1\",\n        \"@esbuild/openharmony-arm64\": \"0.28.1\",\n        \"@esbuild/sunos-x64\": \"0.28.1\",\n        \"@esbuild/win32-arm64\": \"0.28.1\",\n        \"@esbuild/win32-ia32\": \"0.28.1\",\n        \"@esbuild/win32-x64\": \"0.28.1\"\n      }\n    },\n    \"node_modules/fsevents\": {\n      \"version\": \"2.3.3\",\n      \"resolved\": \"https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz\",\n      \"integrity\": \"sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==\",\n      \"dev\": true,\n      \"hasInstallScript\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"darwin\"\n      ],\n      \"engines\": {\n        \"node\": \"^8.16.0 || ^10.6.0 || >=11.0.0\"\n      }\n    },\n    \"node_modules/tsx\": {\n      \"version\": \"4.23.0\",\n      \"resolved\": \"https://registry.npmjs.org/tsx/-/tsx-4.23.0.tgz\",\n      \"integrity\": \"sha512-eUdUIaCr963q2h5u3+QwvYp0+eqPvn+egeqZUm0hwERCqqx1E3kK5ehbGCvqSE5MQAULr67ww0cA3jKc3YkM1w==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"esbuild\": \"~0.28.0\"\n      },\n      \"bin\": {\n        \"tsx\": \"dist/cli.mjs\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      },\n      \"optionalDependencies\": {\n        \"fsevents\": \"~2.3.3\"\n      }\n    },\n    \"node_modules/typescript\": {\n      \"version\": \"5.9.3\",\n      \"resolved\": \"https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz\",\n      \"integrity\": \"sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"bin\": {\n        \"tsc\": \"bin/tsc\",\n        \"tsserver\": \"bin/tsserver\"\n      },\n      \"engines\": {\n        \"node\": \">=14.17\"\n      }\n    },\n    \"node_modules/undici-types\": {\n      \"version\": \"6.21.0\",\n      \"resolved\": \"https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz\",\n      \"integrity\": \"sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/ws\": {\n      \"version\": \"8.21.0\",\n      \"resolved\": \"https://registry.npmjs.org/ws/-/ws-8.21.0.tgz\",\n      \"integrity\": \"sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=10.0.0\"\n      },\n      \"peerDependencies\": {\n        \"bufferutil\": \"^4.0.1\",\n        \"utf-8-validate\": \">=5.0.2\"\n      },\n      \"peerDependenciesMeta\": {\n        \"bufferutil\": {\n          \"optional\": true\n        },\n        \"utf-8-validate\": {\n          \"optional\": true\n        }\n      }\n    }\n  }\n}\n",
    "tsconfig.json": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"lib\": [\"ES2023\", \"DOM\"],\n    \"types\": [\"node\"],\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true,\n    \"resolveJsonModule\": true,\n    \"noEmit\": true\n  },\n  \"include\": [\"index.ts\", \"analysis.ts\", \"analysis.test.ts\", \"aggregate.ts\"]\n}\n",
    "README.md": "# Struct vs PolyNode vs Polymarket RTDS benchmark\n\nThis is a reproducible, one-process smoke test of three live Polymarket trade streams:\n\n- Struct `polymarket_trades`\n- PolyNode `settlement`, `status_update`, and `trade`\n- Polymarket RTDS `activity/trades` and `activity/orders_matched`\n\nIt is extended directly from Struct's published\n[`polymarket-rtds-vs-struct-benchmark`](https://github.com/structbuild/polymarket-rtds-vs-struct-benchmark).\nThe Struct and RTDS subscriptions preserve that harness's documented behavior. The extension adds PolyNode, raw artifacts, strict boundary controls, and independent Polygon `OrderFilled` logs so RTDS is not treated as unquestioned ground truth.\n\n## Exact comparison\n\n| Question | PolyNode path | Struct path | Join unit |\n|---|---|---|---|\n| Pending speed and coverage | `settlement`, only `status=pending` | `received_at` present and `block` absent | transaction hash |\n| Requested confirmed comparison | `status_update` | event carrying `block` | transaction hash |\n| Exact confirmed fill audit | `trade` | confirmed `OrderFilled` | transaction hash + canonical block-global log index |\n| Website feed comparison | each transaction path | each transaction path | RTDS transaction hash |\n\nThe Struct request is exactly:\n\n```json\n{\n  \"room\": \"polymarket_trades\",\n  \"filters\": { \"status\": \"all\", \"subscribe_all\": true }\n}\n```\n\nRTDS receives the same two activity subscriptions used by Struct's public harness. PolyNode uses its `trades` preset, which delivers pending settlements, confirmed flat fills, and lifecycle status updates over one socket.\n\n## Why the ground truth is on-chain\n\nRTDS is another feed, not an independent completeness denominator. For every run the harness queries Polygon for `OrderFilled` logs from the known current and legacy CLOB exchanges. It reports both:\n\n- transaction coverage: whether a path emitted the transaction at least once;\n- fill coverage: whether every `(transaction_hash, log_index)` appeared.\n\nThe distinction matters because one transaction can contain several fills. A feed can show 100% transaction coverage while silently omitting one or more fills. Struct reports a receipt-local `log_index`, while Polygon `eth_getLogs` and PolyNode report a block-global index. The harness fetches each eligible receipt and translates Struct's value to the canonical block-global index; it cross-checks the indexed `order_hash` and records reconciliation diagnostics in the manifest.\n\nThe first eligible block is two blocks ahead of the Polygon tip observed after every socket is active and warmed up. The last eligible block is two blocks behind the tip at the end of core collection. Sockets then stay open for a confirmation tail. This prevents partial first/last blocks from being misclassified as misses.\n\n## Run\n\nNode 20 or later is required.\n\n```bash\nnpm ci\nnpm test\nnpm run check\n\nSTRUCT_API_KEY='...' \\\nPOLYNODE_API_KEY='...' \\\nPOLYGON_RPC_URL='https://polygon-mainnet.example/v2/...' \\\nnpm run bench -- \\\n  --duration-sec 180 \\\n  --warmup-sec 12 \\\n  --tail-sec 20\n```\n\nDo not put real keys in `.env.example`, command history, reports, or issue text. The harness reads credentials from environment variables and records only short SHA-256 fingerprints.\n\nUseful options:\n\n```text\n--duration-sec <n>   Core collection duration (default 120)\n--warmup-sec <n>     Warm-up before future-block window (default 12)\n--tail-sec <n>       Post-window confirmation tail (default 20)\n--struct-status <s>  pending | confirmed | all (default all)\n--trade-types <csv>  Optional Struct filter; omitted by default\n--rtds-heartbeat <m> Struct's uppercase PING or Polymarket's lowercase ping\n--rtds-silence-reconnect-sec <n> Reconnect/resubscribe after n silent seconds\n--out <dir>          Explicit artifact directory\n```\n\n`status=all` is the full test because Struct documents it as delivering both pending and confirmed events. A focused diagnostic can also be run with `--struct-status pending`; it should not be mixed into latency aggregates from a separate process.\n\nThe default `--rtds-heartbeat struct-uppercase` preserves Struct's published harness exactly (`\"PING\"` every five seconds). Polymarket's official client uses lowercase `\"ping\"`, sends the first immediately, and schedules the next after a WebSocket pong. Use `--rtds-heartbeat polymarket-lowercase` for that isolated control and never pool its coverage with the exact-Struct-methodology runs without labeling the difference.\n\nThe default silence timeout is disabled to preserve the published harness. For long production-style controls, `--rtds-silence-reconnect-sec 10` terminates and resubscribes after ten seconds without global activity. Every detection, disconnect, reconnection, and subscription is recorded in the lifecycle manifest; recovered runs must report those interruptions rather than describing them as uninterrupted.\n\n## Latency convention\n\nEvery callback is stamped immediately with `process.hrtime.bigint()` in the same process. There is no comparison of provider-supplied timestamps and no cross-machine clock synchronization dependency.\n\nEach reported lead is:\n\n```text\nright-hand arrival - left-hand arrival\n```\n\nA positive number means the named left-hand feed arrived first. A missing lane produces `n/a`; the harness never converts missing samples into synthetic zeroes.\n\n## Artifacts\n\nEach run creates a self-contained directory under `results/` unless `--out` is supplied:\n\n- `REPORT.md` — readable methodology and results\n- `manifest.json` — versions, hashes, exact requests/acks, DNS, lifecycle, block anchors\n- `events.ndjson` — normalized arrival observations\n- `raw_frames.ndjson` — parsed source frames/events without credentials\n- `rpc_logs.json` — JSON-RPC audit plus independent on-chain fills\n- `latency_samples.csv` — every latency pair behind the aggregates\n- `missing.json` — exact missing transaction and fill identifiers\n- `summary.json` — machine-readable aggregate results\n\nThe test suite includes a multi-fill fixture proving that transaction-level matching cannot hide a missing fill.\n\n## Interpretation limits\n\nThis is a network-vantage-specific smoke test. A defensible public claim should use multiple time windows, retain all artifacts, publish failures as well as successes, and avoid generalizing beyond the measured date, region, endpoints, subscription semantics, and chain scope. Pending data cannot be proven complete from confirmed logs alone; on-chain logs can prove confirmed completeness and can determine whether pending sightings later confirmed.\n\n## Upstream provenance\n\nThe repository began at Struct's public commit recorded in each run manifest. Local source hashes make the exact extension auditable. Struct's original MIT license remains in `LICENSE`.\n",
    "LICENSE": "MIT License\n\nCopyright (c) 2026 Struct\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  }
}
