/** * POC-side pattern enrichers. * * Lift insights from existing engine/templater data into the POC report * without changing how the data is sourced. Each enricher takes the * already-enriched pattern list (cost + severity + action computed in * `poc-report-renderer.ts:enrichPatterns`) and decorates it in-place * with additional fields the summary/full views can render. * * Honest gaps: * - `first_seen` from engine history is only available when the * caller has an engine env configured AND the pattern has a * `tenxHash`. The POC's primary path (paste SIEM creds, no engine) * gets `null`. The badge degrades to `(unknown)` rather than * showing a wrong number. * - Trajectory (constant vs bursty within the window) needs per-event * timestamps. `ExtractedPattern` discards them after templating, so * we skip this for POC and surface a planned-gap comment. Adding it * means threading event timestamps through the templater output. * - Redundancy detection works from counts alone — no timestamps * needed — so it's implemented in full here. */ import type { IncidentCluster } from './detectors/incident-cluster.js'; /** Subset of EnrichedPattern that this module reads. Avoids a circular import. */ export interface EnrichableForPoc { identity: string; service?: string; severity?: string; template: string; symbolMessage?: string; count: number; bytes: number; costPerWindow: number; costPerWeek: number; variables: Record; /** * Per-slot TRUE distinct counts from the templater. When present, * this is the source of truth for cardinality — `variables[k].length` * is the sample size (capped at 20), not the distinct count. */ slotDistinctCounts?: Record; /** * The renderer's lossless lever for this pattern. We never emit * mute/drop/sample as an auto-recommendation. `'mute'`/`'sample'` remain * in the union only for back-compat with older callers + the envelope * fixtures; the PoC renderer produces only compact/offload/tier_down/keep. */ recommendedAction: 'compact' | 'offload' | 'tier_down' | 'keep' | 'mute' | 'sample'; sampleRate: number; reasoning: string; /** Per-event timestamps from the SIEM pull, used for first-seen + growth signals. */ firstSeenMs?: number; lastSeenMs?: number; eventsByHour?: Record; } /** * Pattern emergence shape, derived from `firstSeenMs / lastSeenMs / * eventsByHour` against the pulled window boundaries. */ export interface PatternEmergence { /** ms since the pattern's first occurrence in the pulled window. */ ageInWindowMs: number; /** ms span from first to last occurrence in the pulled window. */ durationMs: number; /** * Category derived from emergence + duration: * - `new` — appeared within the last 24h of the window * - `growing` — events/hr in last 24h ≥ 2x the window's average events/hr * - `stable` — fired throughout the window, no recent surge * - `recent_burst` — entire pattern fits in <40% of the window * - `unknown` — no timestamps available */ category: 'new' | 'growing' | 'stable' | 'recent_burst' | 'unknown'; /** Ratio of events/hr in last 24h vs the window's average events/hr. */ accelerationRatio: number; } /** Slot with the highest distinct-value count for a pattern. */ export interface TopSlot { slot: string; distinctCount: number; /** Fraction of events showing distinct slot values; high values = unbounded variable. */ distinctOverCount: number; } /** Single redundancy pair: two patterns whose counts move together. */ export interface RedundancyPair { identityA: string; identityB: string; /** count(A) / count(B) — closeness to 1 indicates 1:1 firing. */ ratio: number; /** Minimum absolute count in the pair (filters low-confidence pairs). */ minCount: number; } /** Decoration applied to each pattern by these enrichers. */ export interface PocEnrichment { /** Cluster id (0-based index into the returned clusters array), or null. */ incidentClusterId: number | null; /** Top variable slot by distinct value count, or null when no slots. */ topSlot: TopSlot | null; /** Identities this pattern fires 1:1 with (within the sample). */ redundantWith: string[]; /** Pattern's first-seen age in seconds when engine history is available. */ firstSeenAgeSeconds: number | null; /** * Action category after dependency-check fold-in. Carries the renderer's * lossless lever (compact / offload / tier_down / keep) plus one * refinement: `blocked` (dep-check found refs, do not auto-act). * `mute`/`sample` remain for back-compat with older callers. * * There is deliberately no `fix`. See `refineAction` for why. */ refinedAction: 'compact' | 'offload' | 'tier_down' | 'keep' | 'blocked' | 'mute' | 'sample'; /** Number of dependencies (monitors/dashboards/saved-searches) found, when checked. */ dependencyCount: number | null; /** Source of the dep-check result (or `null` when not run for this pattern). */ dependencyChecked: boolean; /** * Emergence shape computed from per-event timestamps within the pulled * window — `new` / `growing` / `stable` / `recent_burst` / `unknown`. * Together with `accelerationRatio` this is the longitudinal signal * that an unaided agent can't compute from a small sample. */ emergence: PatternEmergence | null; } export declare function computeTopSlot(variables: Record, count: number, slotDistinctCounts?: Record): TopSlot | null; /** * Detect redundancy pairs: patterns whose event counts are close enough * to be the same event logged twice (request-received + transaction- * complete, http-in + http-out, etc.). Pair-wise check across the top N * patterns to keep cost O(N^2) bounded. * * Heuristic: * - Both patterns must have count >= `minCount` (default 50). Low-count * pairs are noise from sample variance. * - Ratio = max(a,b) / min(a,b) must be within [1, `maxRatio`] (default * 1.15). Tighter than 0.85 < count_a/count_b < 1.15 gives a 15% * tolerance window. * - Patterns must share a service (different services with matching * counts are coincidence, not redundancy). * * Returns sorted-by-count pairs (highest first). One pattern may appear * in multiple pairs; downstream renderer decides whether to dedup. */ export declare function detectRedundancyPairs(patterns: EnrichableForPoc[], opts?: { minCount?: number; maxRatio?: number; }): RedundancyPair[]; /** * Compute the pattern's emergence shape inside the pulled window. The * window edges come from the caller — the pull layer knows when it * started and ended; the per-event timestamps tell us where this * pattern fired relative to those edges. Returns `null` when no * timestamps are available (paste-Lambda fallback, older SIEM * connectors, CloudWatch events without `timestamp` field, etc.). * * Categories: * - `new` — first_seen within the last 24h of the window * (pattern showed up recently — incident signal) * - `growing` — last-24h rate >= 2x the window's average rate * (pattern is accelerating) * - `stable` — fired throughout the window, no recent surge * (head-of-tail noise, safe sample/mute candidate) * - `recent_burst` — entire activity fits in <40% of the window * (transient, not steady-state — check correlation) * - `unknown` — no timestamps in the pull */ export declare function computeEmergence(p: { firstSeenMs?: number; lastSeenMs?: number; eventsByHour?: Record; count: number; }, windowStartMs: number, windowEndMs: number): PatternEmergence; /** * Refine the recommended action by folding in dependency-check * results + severity. The existing renderer logic returns mute / sample * / keep purely from cost-tier. This refiner adds: * * - `blocked`: dependency_check found refs (monitors / dashboards / * saved searches). Even if the original recommendation was mute, * don't auto-act. * * `keep` / `sample` / `mute` are passed through when no refinement applies. * * REMOVED: `fix`. It fired on ERROR-class patterns whose text matched * dial/timeout/no-such-host/refused and rendered as **FIX** in the Action * column, meaning "this is a broken dependency, open a ticket". * * Three reasons it is gone. It was not an action: every other value in that * column is something the receiver config performs, while `fix` is something * a human does in a codebase we do not touch, cannot verify, and cannot * price. It had no enforcement consequence: `poc-envelope-v2` already mapped * it to `pass`, byte-identical to `keep`, so it changed a label and nothing * else. And it turned a cost report into a code review, which is not the job * — the report exists to stabilise and enforce a budget, not to act as * someone's SRE. * * The same fact still reaches the reader, in the only framing we own: an * error-class pattern is protected, and protected spend is what sets the * ceiling on achievable savings. That is the "Why not more" line. */ export declare function refineAction(p: EnrichableForPoc, dependencyCount: number | null): PocEnrichment['refinedAction']; /** * Apply all enrichers in one pass. Returns a parallel array (same * order, same length) of decorations. The renderer joins them by index. * * `dependencyByIdentity` is the map produced by the optional pre-warm * pass in the POC submit pipeline. Missing entries fall back to * `null` (unchecked) without inflating false confidence. */ export declare function enrichForPoc(patterns: EnrichableForPoc[], opts?: { /** Limit incident-clustering to the top N patterns (cost-ranked). */ incidentTopN?: number; /** Per-identity dependency counts, when pre-computed. */ dependencyByIdentity?: Map; /** Per-identity first-seen ages from engine, when pre-computed. */ firstSeenByIdentity?: Map; /** * Window boundaries of the SIEM pull, in epoch ms. When set, each * pattern's `emergence` field is computed against these bounds. When * omitted, emergence falls back to `unknown`. */ windowStartMs?: number; windowEndMs?: number; }): { enrichments: PocEnrichment[]; clusters: IncidentCluster[]; redundancyPairs: RedundancyPair[]; };