{"version":3,"file":"index.cjs","names":[],"sources":["../../../src/observability/metrics/index.ts"],"sourcesContent":["import type { IamEngineTypes } from '../../core/engine/engine.types'\n\n/** IamMetrics observability types. Type-only namespace - zero bundle cost. */\nexport namespace IamMetrics {\n  /**\n   * Aggregates the engine's `onMetrics` events in-process.\n   *\n   * Wire `.record` into `hooks.onMetrics` and read `.snapshot()` on demand (a\n   * `/metrics` route, a Prom scrape, an OTel exporter). Holds a rolling sample\n   * of recent durations to compute p50 / p95 / p99 without pulling in a\n   * histogram library. Sample size defaults to `1000`; beyond that the oldest\n   * sample is evicted via a fixed-size ring buffer.\n   */\n  export interface IAggregator {\n    /**\n     * Records a single metrics event. Bind directly to {@link IamEngineTypes.IHooks.onMetrics}.\n     *\n     * @param event - Receives the metrics event emitted by the engine after every check.\n     */\n    record(event: IamEngineTypes.IMetricsEvent): void\n    /**\n     * Computes the current rolling snapshot. Callers can poll at any interval.\n     *\n     * @returns Immutable {@link ISnapshot} summarising the rolling window.\n     */\n    snapshot(): ISnapshot\n    /** Resets counters and clears the rolling sample buffer. */\n    reset(): void\n  }\n\n  /** Immutable snapshot of aggregated metrics over the rolling window. */\n  export interface ISnapshot {\n    /** Total events recorded since the last reset. */\n    readonly total: number\n    /** Number of allow verdicts. */\n    readonly allow: number\n    /** Number of deny verdicts. */\n    readonly deny: number\n    /**\n     * Number of allow verdicts that were attributable solely to the engine's\n     * `defaultEffect: 'allow'` fallback (no applicable policy fired). Subset\n     * of {@link allow}. Chart this to detect silent policy-set breakage.\n     */\n    readonly failOpen: number\n    /** p50 latency in milliseconds over the rolling window. */\n    readonly p50: number\n    /** p95 latency in milliseconds over the rolling window. */\n    readonly p95: number\n    /** p99 latency in milliseconds over the rolling window. */\n    readonly p99: number\n    /** Maximum latency in the rolling window. */\n    readonly max: number\n    /** Window size (capped by the configured `sampleSize`). */\n    readonly samples: number\n  }\n\n  /** Configures {@link iamCreateMetricsAggregator}. */\n  export interface IConfig {\n    /**\n     * Maximum number of duration samples kept in the rolling window. Higher\n     * values give more accurate tail percentiles at the cost of memory.\n     * Defaults to `1000`.\n     */\n    sampleSize?: number\n  }\n}\n\n/**\n * Creates an in-process metrics aggregator with a fixed-size ring buffer for\n * latency percentiles and running counters for allow/deny verdicts.\n *\n * Bind `.record` to {@link IamEngineTypes.IHooks.onMetrics} and read `.snapshot()`\n * on demand.\n *\n * @param config - Optionally overrides the sample size; see {@link IamMetrics.IConfig}.\n * @returns A new {@link IamMetrics.IAggregator} instance with zeroed counters.\n * @example\n * ```ts\n * const metrics = iamCreateMetricsAggregator({ sampleSize: 2000 })\n * const engine = new IamEngine({ adapter, hooks: { onMetrics: metrics.record } })\n * app.get('/metrics', (_, res) => res.json(metrics.snapshot()))\n * ```\n */\nexport function iamCreateMetricsAggregator(config: IamMetrics.IConfig = {}): IamMetrics.IAggregator {\n  const cap = config.sampleSize ?? 1000\n  const buf = new Float64Array(cap)\n  let head = 0\n  let count = 0\n  let total = 0\n  let allow = 0\n  let deny = 0\n  let failOpen = 0\n\n  return {\n    record(event) {\n      total++\n      if (event.allowed) allow++\n      else deny++\n      if (event.failOpen) failOpen++\n      buf[head] = event.durationMs\n      head = (head + 1) % cap\n      if (count < cap) count++\n    },\n    snapshot() {\n      if (count === 0) {\n        return { total, allow, deny, failOpen, p50: 0, p95: 0, p99: 0, max: 0, samples: 0 }\n      }\n      // Copy the live region of the ring buffer + sort to compute percentiles.\n      // O(n log n) per snapshot; fine at the cap (<= 1000) and avoids a\n      // streaming-quantile dependency.\n      const sorted = Array.from(buf.subarray(0, count))\n      sorted.sort((a, b) => a - b)\n      return {\n        total,\n        allow,\n        deny,\n        failOpen,\n        p50: percentile(sorted, 0.5),\n        p95: percentile(sorted, 0.95),\n        p99: percentile(sorted, 0.99),\n        max: sorted[sorted.length - 1] ?? 0,\n        samples: count,\n      }\n    },\n    reset() {\n      head = 0\n      count = 0\n      total = 0\n      allow = 0\n      deny = 0\n      failOpen = 0\n    },\n  }\n}\n\nfunction percentile(sorted: number[], q: number): number {\n  if (sorted.length === 0) return 0\n  const i = Math.max(0, Math.min(sorted.length - 1, Math.ceil(q * sorted.length) - 1))\n  return sorted[i] ?? 0\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAmFA,SAAgB,2BAA2B,SAA6B,CAAC,GAA2B;CAClG,MAAM,MAAM,OAAO,cAAc;CACjC,MAAM,MAAM,IAAI,aAAa,GAAG;CAChC,IAAI,OAAO;CACX,IAAI,QAAQ;CACZ,IAAI,QAAQ;CACZ,IAAI,QAAQ;CACZ,IAAI,OAAO;CACX,IAAI,WAAW;CAEf,OAAO;EACL,OAAO,OAAO;GACZ;GACA,IAAI,MAAM,SAAS;QACd;GACL,IAAI,MAAM,UAAU;GACpB,IAAI,QAAQ,MAAM;GAClB,QAAQ,OAAO,KAAK;GACpB,IAAI,QAAQ,KAAK;EACnB;EACA,WAAW;GACT,IAAI,UAAU,GACZ,OAAO;IAAE;IAAO;IAAO;IAAM;IAAU,KAAK;IAAG,KAAK;IAAG,KAAK;IAAG,KAAK;IAAG,SAAS;GAAE;GAKpF,MAAM,SAAS,MAAM,KAAK,IAAI,SAAS,GAAG,KAAK,CAAC;GAChD,OAAO,MAAM,GAAG,MAAM,IAAI,CAAC;GAC3B,OAAO;IACL;IACA;IACA;IACA;IACA,KAAK,WAAW,QAAQ,EAAG;IAC3B,KAAK,WAAW,QAAQ,GAAI;IAC5B,KAAK,WAAW,QAAQ,GAAI;IAC5B,KAAK,OAAO,OAAO,SAAS,MAAM;IAClC,SAAS;GACX;EACF;EACA,QAAQ;GACN,OAAO;GACP,QAAQ;GACR,QAAQ;GACR,QAAQ;GACR,OAAO;GACP,WAAW;EACb;CACF;AACF;AAEA,SAAS,WAAW,QAAkB,GAAmB;CACvD,IAAI,OAAO,WAAW,GAAG,OAAO;CAEhC,OAAO,OADG,KAAK,IAAI,GAAG,KAAK,IAAI,OAAO,SAAS,GAAG,KAAK,KAAK,IAAI,OAAO,MAAM,IAAI,CAAC,CACpE,MAAM;AACtB"}