/** * Slot-aggregation (Option B from the plan). * * Problem: the engine assigns events a `templateHash` from the * field-set structure (which fields exist, in what positions, with * what types). Two events that look logically "the same" but differ * structurally (one has `tags: []`, the other has `tags: ["x","y"]`) * get different templateHashes. The 10x engine normalizes some of * this at the Reporter tier and they can share a `symbolMessage`. * * Naive use of `computeConcentration()` from * `src/lib/variable-concentration.ts` groups by templateHash. For * a logical pattern split 50/50 across two templateHashes, the * concentration detectors emit two findings, each over half the * data. That misses the merge opportunity and confuses users. * * Option B (user-confirmed): merge findings across templateHashes * that share a `symbolMessage`, **only when slots align by * `precedingToken`**. The precedingToken is the structured key text * immediately before the slot in the template (e.g. `userId=`, * `"customer":"`). Two slots from different templates align when * their precedingTokens match exactly. * * Position-based alignment is NOT used: templates with different * field counts have different slot positions, so position is * unreliable across symbol-message peers. precedingToken is the * only safe matcher when slot positions may not match. * * Slots without a precedingToken (or with mismatched precedingTokens * across the group) stay per-templateHash with * `aggregationStatus: 'per_template_hash_only'` and a * `aggregationReason`. Honest signaling beats silent mis-merging. * * When the input has no templates (e.g., the engine didn't emit * Template records, or only aggregated ExtractedPattern data is * available), it falls back to slot-NAME alignment: ExtractedPattern stores * variables under the engine's inferred slot name (e.g. `userId`, * `tenant`, or positional `slot_N`). Positional names (`slot_N`) are * never aligned across templates because position is unreliable. * Semantic names that match across templates DO align, with * `aggregationStatus: 'merged'`. This is the path the detectors take * in env mode (where raw EncodedEvents+Templates are not always present). */ import type { ExtractedPattern } from '../pattern-extraction.js'; import type { Template } from '../cli-output-parser.js'; export interface AggregatedSlot { /** Source slot name (engine inferred). For merged slots across multiple * templateHashes, this is the common name. */ slotName: string; /** Preceding token if aligned by it; undefined if aligned by slotName. */ precedingToken?: string; /** Number of distinct values across the union of contributing events. */ distinctCount: number; /** Most frequent value across the union. */ dominantValue: string; /** Fraction of union events carrying the dominant value. */ dominantPct: number; /** Top-N values with counts and percentages. */ topValues: Array<{ value: string; count: number; pct: number; }>; /** TemplateHashes contributing to this slot's aggregated result. */ templateHashesContributing: string[]; /** Sum of `count` across all contributing patterns. */ totalEventsIncluded: number; /** 'merged' = consolidated across templateHashes via slot alignment. * 'per_template_hash_only' = could not safely align; reported as a * single-template slot. */ aggregationStatus: 'merged' | 'per_template_hash_only'; /** Reason status is per_template_hash_only, when applicable. */ aggregationReason?: string; } export interface AggregatedPattern { /** symbolMessage shared by all member patterns. */ symbolMessage: string; /** All member templateHashes. */ templateHashes: string[]; /** Sum of `count` across all members. */ totalEvents: number; /** Sum of `bytes` across all members. */ totalBytes: number; /** Sum of `encodedBytes` across all members (0 when engine didn't emit). */ totalEncodedBytes: number; /** Aggregated slot results, sorted by distinctCount descending. */ slots: AggregatedSlot[]; } export interface AggregationOptions { /** Optional templates map (templateHash → Template). When present, alignment uses precedingToken; otherwise falls back to slotName matching. */ templates?: Map; /** Drop slots with totalEventsIncluded below this. Default 10. */ minEvents?: number; /** Top-N values per slot. Default 3. */ topN?: number; } /** * Aggregate ExtractedPatterns by symbolMessage with slot-level merging. * * Patterns without a symbolMessage are reported as singleton aggregations * (one member, no cross-template merge possible). */ export declare function aggregateSlotsBySymbolMessage(patterns: ExtractedPattern[], opts?: AggregationOptions): AggregatedPattern[];