logpare 0.0.4 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -105,15 +105,18 @@ function extractDurations(line) {
105
105
  return durations.slice(0, 5);
106
106
  }
107
107
  var DEFAULT_PATTERNS = {
108
- // Timestamps (most specific - must run before port to avoid fragmentation)
108
+ // Timestamps (must run before port to avoid fragmentation)
109
109
  isoTimestamp: /\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:[.,]\d+)?(?:Z|[+-]\d{2}:?\d{2})?/g,
110
+ // UUID must run before unixTimestamp to prevent partial matching of UUID segments
111
+ uuid: /\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b/g,
110
112
  unixTimestamp: /\b\d{10,13}\b/g,
111
113
  // Network addresses
112
114
  ipv4: /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g,
113
- ipv6: /\b[0-9a-fA-F:]{7,39}\b/g,
115
+ // IPv6: matches full, compressed (::1, ::), and partial forms
116
+ // Order matters: longer matches must come before shorter ones in alternation
117
+ ipv6: /(?:(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?::[0-9a-fA-F]{1,4}){1,6}|:(?::[0-9a-fA-F]{1,4}){1,7}|(?:[0-9a-fA-F]{1,4}:){1,7}:|::)/g,
114
118
  port: /:\d{2,5}\b/g,
115
119
  // Identifiers
116
- uuid: /\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b/g,
117
120
  hexId: /\b0x[0-9a-fA-F]+\b/g,
118
121
  blockId: /\bblk_-?\d+\b/g,
119
122
  // Paths and URLs
@@ -412,6 +415,20 @@ var LogCluster = class {
412
415
  };
413
416
 
414
417
  // src/output/formatter.ts
418
+ function sortObjectKeys(value) {
419
+ if (value === null || typeof value !== "object") {
420
+ return value;
421
+ }
422
+ if (Array.isArray(value)) {
423
+ return value.map(sortObjectKeys);
424
+ }
425
+ const sorted = {};
426
+ const keys = Object.keys(value).sort();
427
+ for (const key of keys) {
428
+ sorted[key] = sortObjectKeys(value[key]);
429
+ }
430
+ return sorted;
431
+ }
415
432
  function formatSummary(templates, stats) {
416
433
  const lines = [];
417
434
  lines.push("=== Log Compression Summary ===");
@@ -518,6 +535,33 @@ function formatJson(templates, stats) {
518
535
  };
519
536
  return JSON.stringify(output, null, 2);
520
537
  }
538
+ function formatJsonStable(templates, stats) {
539
+ const output = {
540
+ stats: {
541
+ compressionRatio: Math.round(stats.compressionRatio * 1e3) / 1e3,
542
+ estimatedTokenReduction: Math.round(stats.estimatedTokenReduction * 1e3) / 1e3,
543
+ inputLines: stats.inputLines,
544
+ uniqueTemplates: stats.uniqueTemplates
545
+ },
546
+ templates: templates.map((t) => ({
547
+ correlationIdSamples: t.correlationIdSamples,
548
+ durationSamples: t.durationSamples,
549
+ firstSeen: t.firstSeen,
550
+ fullUrlSamples: t.fullUrlSamples,
551
+ id: t.id,
552
+ isStackFrame: t.isStackFrame,
553
+ lastSeen: t.lastSeen,
554
+ occurrences: t.occurrences,
555
+ pattern: t.pattern,
556
+ samples: t.sampleVariables,
557
+ severity: t.severity,
558
+ statusCodeSamples: t.statusCodeSamples,
559
+ urlSamples: t.urlSamples
560
+ })),
561
+ version: "1.1"
562
+ };
563
+ return JSON.stringify(sortObjectKeys(output));
564
+ }
521
565
 
522
566
  // src/drain/drain.ts
523
567
  var DEFAULTS = {
@@ -582,6 +626,14 @@ var Drain = class {
582
626
  addLogLines(lines) {
583
627
  const total = lines.length;
584
628
  const reportInterval = Math.max(1, Math.floor(total / 100));
629
+ if (this.onProgress && total > 0) {
630
+ this.onProgress({
631
+ processedLines: 0,
632
+ totalLines: total,
633
+ currentPhase: "parsing",
634
+ percentComplete: 0
635
+ });
636
+ }
585
637
  for (let i = 0; i < total; i++) {
586
638
  this.addLogLine(lines[i]);
587
639
  if (this.onProgress && i % reportInterval === 0) {
@@ -604,6 +656,10 @@ var Drain = class {
604
656
  }
605
657
  /**
606
658
  * Search the parse tree for a matching cluster.
659
+ *
660
+ * Uses XDrain-inspired token-position fallback: if the first token
661
+ * looks like a variable (timestamp, PID, etc.), tries using the
662
+ * second token as the navigation key for better matching.
607
663
  */
608
664
  treeSearch(tokens) {
609
665
  const tokenCount = tokens.length;
@@ -612,19 +668,36 @@ var Drain = class {
612
668
  if (lengthNode === void 0) {
613
669
  return null;
614
670
  }
671
+ const primaryResult = this.treeSearchFromToken(lengthNode, tokens, 0);
672
+ if (primaryResult !== null) {
673
+ return primaryResult;
674
+ }
615
675
  const firstToken = tokens[0];
616
- if (firstToken === void 0) {
676
+ if (tokens.length > 1 && firstToken !== void 0 && this.looksLikeVariable(firstToken)) {
677
+ return this.treeSearchFromToken(lengthNode, tokens, 1);
678
+ }
679
+ return null;
680
+ }
681
+ /**
682
+ * Search from a specific token position in the tree.
683
+ * @param lengthNode - The node at level 1 (token count)
684
+ * @param tokens - All tokens in the log line
685
+ * @param startIndex - Which token to use as "first token" for navigation
686
+ */
687
+ treeSearchFromToken(lengthNode, tokens, startIndex) {
688
+ const navToken = tokens[startIndex];
689
+ if (navToken === void 0) {
617
690
  return null;
618
691
  }
619
- let currentNode = lengthNode.getChild(firstToken);
620
- if (currentNode === void 0) {
692
+ let currentNode = lengthNode.getChild(navToken);
693
+ if (currentNode === void 0 && startIndex === 0) {
621
694
  currentNode = lengthNode.getChild(WILDCARD_KEY);
622
695
  }
623
696
  if (currentNode === void 0) {
624
697
  return null;
625
698
  }
626
699
  let searchNode = currentNode;
627
- for (let i = 1; i < Math.min(tokens.length, this.depth); i++) {
700
+ for (let i = startIndex + 1; i < Math.min(tokens.length, this.depth); i++) {
628
701
  const token = tokens[i];
629
702
  if (token === void 0) {
630
703
  break;
@@ -752,6 +825,9 @@ var Drain = class {
752
825
  case "json":
753
826
  formatted = formatJson(limitedTemplates, stats);
754
827
  break;
828
+ case "json-stable":
829
+ formatted = formatJsonStable(limitedTemplates, stats);
830
+ break;
755
831
  case "summary":
756
832
  default:
757
833
  formatted = formatSummary(limitedTemplates, stats);
@@ -829,6 +905,10 @@ export {
829
905
  detectSeverity,
830
906
  isStackFrame,
831
907
  extractUrls,
908
+ extractFullUrls,
909
+ extractStatusCodes,
910
+ extractCorrelationIds,
911
+ extractDurations,
832
912
  DEFAULT_PATTERNS,
833
913
  WILDCARD,
834
914
  defineStrategy,
@@ -837,4 +917,4 @@ export {
837
917
  compress,
838
918
  compressText
839
919
  };
840
- //# sourceMappingURL=chunk-VVVVUJFY.js.map
920
+ //# sourceMappingURL=chunk-SAYWGRX4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/preprocessing/patterns.ts","../src/preprocessing/default.ts","../src/drain/node.ts","../src/drain/cluster.ts","../src/output/formatter.ts","../src/drain/drain.ts","../src/api.ts"],"sourcesContent":["import type { Severity } from '../types.js';\n\n/**\n * Patterns for detecting log severity levels.\n */\nexport const SEVERITY_PATTERNS = {\n error: /\\b(Error|ERROR|error|Uncaught|UNCAUGHT|Failed|FAILED|Exception|EXCEPTION|FATAL|fatal|TypeError|ReferenceError|SyntaxError|RangeError)\\b/,\n warning: /\\b(Warning|WARNING|warn|WARN|\\[Violation\\]|Violation|DEPRECATED|deprecated|Deprecation)\\b/,\n} as const;\n\n/**\n * Patterns for detecting stack trace frames.\n */\nexport const STACK_FRAME_PATTERNS = [\n /^\\s*at\\s+/, // \" at Function.x\" (V8/Node)\n /^\\s*@\\s*\\S+:\\d+/, // \"@ file.js:123\" (Firefox)\n /^\\s*\\w+@\\S+:\\d+/, // \"fn@file.js:123\" (Firefox named)\n /^\\s*\\(anonymous\\)\\s*@/, // \"(anonymous) @ file.js:123\" (Chrome DevTools)\n /^\\s*[A-Za-z_$][\\w$]*\\s+@\\s+\\S+:\\d+/, // \"functionName @ file.js:123\"\n] as const;\n\n/**\n * Detect the severity level of a log line.\n * Returns 'error', 'warning', or 'info'.\n */\nexport function detectSeverity(line: string): Severity {\n if (SEVERITY_PATTERNS.error.test(line)) {\n return 'error';\n }\n if (SEVERITY_PATTERNS.warning.test(line)) {\n return 'warning';\n }\n return 'info';\n}\n\n/**\n * Detect if a line is a stack trace frame.\n */\nexport function isStackFrame(line: string): boolean {\n return STACK_FRAME_PATTERNS.some(pattern => pattern.test(line));\n}\n\n/**\n * Extract URLs from a line (before masking).\n * Returns hostnames only for brevity in urlSamples.\n */\nexport function extractUrls(line: string): string[] {\n const urlPattern = /https?:\\/\\/[^\\s\"'<>]+/g;\n const matches = line.match(urlPattern);\n if (!matches) return [];\n\n // Extract just the host from each URL for brevity\n return matches.map(url => {\n try {\n const parsed = new URL(url);\n return parsed.hostname;\n } catch {\n return url;\n }\n }).filter((v, i, a) => a.indexOf(v) === i); // dedupe\n}\n\n/**\n * Extract full URLs from a line (before masking).\n * Returns complete URLs with paths for better diagnostics.\n */\nexport function extractFullUrls(line: string): string[] {\n const urlPattern = /https?:\\/\\/[^\\s\"'<>]+/g;\n const matches = line.match(urlPattern);\n if (!matches) return [];\n\n // Return full URLs, deduplicated\n return [...new Set(matches)];\n}\n\n/**\n * Extract HTTP status codes from a line.\n * Matches common patterns like \"status 404\", \"HTTP 500\", \"status: 403\".\n */\nexport function extractStatusCodes(line: string): number[] {\n // Match status codes in context to avoid false positives\n const patterns = [\n /\\bstatus[:\\s]+(\\d{3})\\b/gi, // \"status 404\", \"status: 500\"\n /\\bHTTP[\\/\\s]\\d\\.\\d\\s+(\\d{3})\\b/gi, // \"HTTP/1.1 404\", \"HTTP 1.1 500\"\n /\\bcode[:\\s]+(\\d{3})\\b/gi, // \"code: 403\", \"code 500\"\n /\\b(\\d{3})\\s+(?:OK|Not Found|Bad Request|Unauthorized|Forbidden|Internal Server Error|Service Unavailable)\\b/gi,\n ];\n\n const codes: number[] = [];\n\n for (const pattern of patterns) {\n const regex = new RegExp(pattern.source, pattern.flags);\n let match;\n while ((match = regex.exec(line)) !== null) {\n const codeStr = match[1];\n if (codeStr) {\n const code = parseInt(codeStr, 10);\n // Only include valid HTTP status codes (100-599)\n if (code >= 100 && code <= 599 && !codes.includes(code)) {\n codes.push(code);\n }\n }\n }\n }\n\n return codes;\n}\n\n/**\n * Extract correlation/trace IDs from a line.\n * Matches common patterns like trace-id, request-id, correlation-id, and UUIDs.\n */\nexport function extractCorrelationIds(line: string): string[] {\n const patterns = [\n // Named correlation IDs: trace-id=xxx, request_id: xxx, x-request-id=xxx\n /\\b(?:trace[-_]?id|request[-_]?id|correlation[-_]?id|x-request-id)[=:\\s]+[\"']?([a-zA-Z0-9-_]+)[\"']?/gi,\n // Standalone UUIDs (common correlation ID format)\n /\\b([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})\\b/gi,\n ];\n\n const ids: string[] = [];\n\n for (const pattern of patterns) {\n const regex = new RegExp(pattern.source, pattern.flags);\n let match;\n while ((match = regex.exec(line)) !== null) {\n const id = match[1] || match[0];\n if (id && !ids.includes(id)) {\n ids.push(id);\n }\n }\n }\n\n // Limit to 3 IDs per line\n return ids.slice(0, 3);\n}\n\n/**\n * Extract duration/timing values from a line.\n * Matches common duration patterns like \"80ms\", \"1.5s\", \"250µs\", \"2sec\", etc.\n * Called before preprocessing masks these values.\n */\nexport function extractDurations(line: string): string[] {\n // Pattern matches:\n // - Integer or decimal numbers\n // - Followed by duration units (case-insensitive)\n // - Common units: ms, s, sec, second(s), millisecond(s), µs, us, μs, ns, min, hour(s), hr\n const durationPattern = /\\b(\\d+(?:\\.\\d+)?)\\s*(ms|milliseconds?|s|sec(?:onds?)?|µs|μs|us|microseconds?|ns|nanoseconds?|min(?:utes?)?|h(?:ours?)?|hr)\\b/gi;\n\n const durations: string[] = [];\n let match;\n\n while ((match = durationPattern.exec(line)) !== null) {\n // Reconstruct the full duration string (number + unit, no whitespace)\n const duration = `${match[1]}${match[2]}`;\n if (!durations.includes(duration)) {\n durations.push(duration);\n }\n }\n\n // Limit to 5 durations per line to prevent memory issues\n return durations.slice(0, 5);\n}\n\n/**\n * Built-in regex patterns for common variable types.\n * These are applied in order during preprocessing to mask variables.\n * Order matters: more specific patterns (like timestamps) must run before\n * patterns that could match substrings (like port numbers).\n */\nexport const DEFAULT_PATTERNS: Record<string, RegExp> = {\n // Timestamps (must run before port to avoid fragmentation)\n isoTimestamp: /\\d{4}-\\d{2}-\\d{2}[T ]\\d{2}:\\d{2}:\\d{2}(?:[.,]\\d+)?(?:Z|[+-]\\d{2}:?\\d{2})?/g,\n\n // UUID must run before unixTimestamp to prevent partial matching of UUID segments\n uuid: /\\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\\b/g,\n\n unixTimestamp: /\\b\\d{10,13}\\b/g,\n\n // Network addresses\n ipv4: /\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b/g,\n // IPv6: matches full, compressed (::1, ::), and partial forms\n // Order matters: longer matches must come before shorter ones in alternation\n ipv6: /(?:(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?::[0-9a-fA-F]{1,4}){1,6}|:(?::[0-9a-fA-F]{1,4}){1,7}|(?:[0-9a-fA-F]{1,4}:){1,7}:|::)/g,\n port: /:\\d{2,5}\\b/g,\n\n // Identifiers\n hexId: /\\b0x[0-9a-fA-F]+\\b/g,\n blockId: /\\bblk_-?\\d+\\b/g,\n\n // Paths and URLs\n // Note: Don't match version-like paths (e.g., /2.7, /v1.0)\n // Require at least 2 path segments or a segment with letters\n filePath: /(?:\\/[a-zA-Z][\\w.-]*)+/g,\n url: /https?:\\/\\/[^\\s]+/g,\n\n // Long numeric IDs only (6+ digits) - preserves status codes, line numbers\n // Examples masked: request IDs (12345678), order numbers (1234567890)\n // Examples preserved: HTTP 404, line:123, /v2.7\n numericId: /\\b\\d{6,}\\b/g,\n\n // Numbers with optional duration/size suffixes\n // Matches: 1500, 250ms, 1.5s, 100KB, etc.\n numbers: /\\b\\d+(?:\\.\\d+)?(?:ms|s|µs|us|ns|min|h|hr|sec|[KkMmGgTt][Bb]?)?\\b/g,\n};\n\n/**\n * Placeholder used when masking variables.\n */\nexport const WILDCARD = '<*>';\n\n/**\n * Apply a set of patterns to mask variables in a line.\n * Patterns are applied in the order provided.\n */\nexport function applyPatterns(\n line: string,\n patterns: Record<string, RegExp>,\n wildcard: string = WILDCARD\n): string {\n let result = line;\n\n for (const pattern of Object.values(patterns)) {\n // Create a new RegExp instance to reset lastIndex for global patterns\n const regex = new RegExp(pattern.source, pattern.flags);\n result = result.replace(regex, wildcard);\n }\n\n return result;\n}\n","import type { ParsingStrategy } from '../types.js';\nimport { DEFAULT_PATTERNS, applyPatterns, WILDCARD } from './patterns.js';\n\n/**\n * Default parsing strategy for log preprocessing.\n */\nexport const defaultStrategy: ParsingStrategy = {\n /**\n * Preprocess a log line by masking common variable patterns.\n */\n preprocess(line: string): string {\n return applyPatterns(line, DEFAULT_PATTERNS, WILDCARD);\n },\n\n /**\n * Tokenize a line by splitting on whitespace.\n */\n tokenize(line: string): string[] {\n return line.split(/\\s+/).filter((token) => token.length > 0);\n },\n\n /**\n * Get similarity threshold for a given tree depth.\n * Uses a constant threshold of 0.4.\n */\n getSimThreshold(_depth: number): number {\n return 0.4;\n },\n};\n\n/**\n * Create a custom parsing strategy by extending the default.\n */\nexport function defineStrategy(\n overrides: Partial<ParsingStrategy> & {\n patterns?: Record<string, RegExp>;\n }\n): ParsingStrategy {\n const { patterns, ...strategyOverrides } = overrides;\n\n return {\n preprocess: strategyOverrides.preprocess ?? ((line: string) => {\n const mergedPatterns = patterns\n ? { ...DEFAULT_PATTERNS, ...patterns }\n : DEFAULT_PATTERNS;\n return applyPatterns(line, mergedPatterns, WILDCARD);\n }),\n\n tokenize: strategyOverrides.tokenize ?? defaultStrategy.tokenize,\n\n getSimThreshold: strategyOverrides.getSimThreshold ?? defaultStrategy.getSimThreshold,\n };\n}\n","import type { LogCluster } from './cluster.js';\n\n/**\n * A node in the Drain parse tree.\n *\n * V8 Optimization: Uses Map<string, DrainNode> instead of plain objects\n * for dynamic children. This prevents V8 \"dictionary mode\" which would\n * cause 10-100x slower property access.\n *\n * V8 Optimization: All properties are initialized in the constructor\n * to ensure monomorphic object shapes.\n */\nexport class DrainNode {\n /** Depth of this node in the tree (0 = root) */\n readonly depth: number;\n\n /**\n * Child nodes keyed by token value.\n * Using Map instead of Object for V8 optimization.\n */\n readonly children: Map<string, DrainNode>;\n\n /**\n * Clusters (templates) stored at this node.\n * Only leaf nodes contain clusters.\n */\n readonly clusters: LogCluster[];\n\n constructor(depth: number) {\n this.depth = depth;\n this.children = new Map();\n this.clusters = [];\n }\n\n /**\n * Get or create a child node for the given key.\n */\n getOrCreateChild(key: string): DrainNode {\n let child = this.children.get(key);\n if (child === undefined) {\n child = new DrainNode(this.depth + 1);\n this.children.set(key, child);\n }\n return child;\n }\n\n /**\n * Check if this node has a child for the given key.\n */\n hasChild(key: string): boolean {\n return this.children.has(key);\n }\n\n /**\n * Get a child node by key, or undefined if not found.\n */\n getChild(key: string): DrainNode | undefined {\n return this.children.get(key);\n }\n\n /**\n * Add a cluster to this node.\n */\n addCluster(cluster: LogCluster): void {\n this.clusters.push(cluster);\n }\n\n /**\n * Get the number of children.\n */\n get childCount(): number {\n return this.children.size;\n }\n\n /**\n * Get the number of clusters.\n */\n get clusterCount(): number {\n return this.clusters.length;\n }\n}\n","import {\n WILDCARD,\n detectSeverity,\n isStackFrame,\n extractUrls,\n extractFullUrls,\n extractStatusCodes,\n extractCorrelationIds,\n extractDurations,\n} from '../preprocessing/patterns.js';\nimport type { Severity } from '../types.js';\n\n/**\n * Represents a log cluster (template) discovered by the Drain algorithm.\n *\n * V8 Optimization: All properties are initialized in the constructor\n * to ensure monomorphic object shapes for optimal property access.\n */\nexport class LogCluster {\n /** Unique identifier for this cluster */\n readonly id: string;\n\n /** Template tokens (with wildcards for variable positions) */\n readonly tokens: string[];\n\n /** Number of log lines matching this template */\n count: number;\n\n /** Sample variable values from first N matches */\n readonly sampleVariables: string[][];\n\n /** Line index of first occurrence */\n firstSeen: number;\n\n /** Line index of most recent occurrence */\n lastSeen: number;\n\n /** Detected severity level */\n readonly severity: Severity;\n\n /** Sample URLs extracted from matching lines (hostnames only) */\n readonly urlSamples: string[];\n\n /** Full URLs extracted from matching lines (complete paths) */\n readonly fullUrlSamples: string[];\n\n /** HTTP status codes extracted from matching lines */\n readonly statusCodeSamples: number[];\n\n /** Correlation/trace IDs extracted from matching lines */\n readonly correlationIdSamples: string[];\n\n /** Duration/timing values extracted from matching lines */\n readonly durationSamples: string[];\n\n /** Whether this template represents a stack trace frame */\n readonly isStackFrame: boolean;\n\n /** Maximum number of sample variables to store */\n private readonly maxSamples: number;\n\n /** Maximum number of URL samples to store */\n private readonly maxUrlSamples: number = 5;\n\n /** Maximum number of status code samples to store */\n private readonly maxStatusCodeSamples: number = 5;\n\n /** Maximum number of correlation ID samples to store */\n private readonly maxCorrelationIdSamples: number = 3;\n\n /** Maximum number of duration samples to store */\n private readonly maxDurationSamples: number = 5;\n\n constructor(\n id: string,\n tokens: string[],\n lineIndex: number,\n maxSamples: number = 3,\n originalLine: string = ''\n ) {\n this.id = id;\n this.tokens = tokens.slice(); // Defensive copy\n this.count = 1;\n this.sampleVariables = [];\n this.firstSeen = lineIndex;\n this.lastSeen = lineIndex;\n this.maxSamples = maxSamples;\n\n // Detect severity, stack frame from original line\n this.severity = detectSeverity(originalLine);\n this.isStackFrame = isStackFrame(originalLine);\n\n // Initialize sample arrays\n this.urlSamples = [];\n this.fullUrlSamples = [];\n this.statusCodeSamples = [];\n this.correlationIdSamples = [];\n this.durationSamples = [];\n\n // Extract and store samples from original line\n const urls = extractUrls(originalLine);\n if (urls.length > 0) {\n this.urlSamples.push(...urls.slice(0, this.maxUrlSamples));\n }\n\n const fullUrls = extractFullUrls(originalLine);\n if (fullUrls.length > 0) {\n this.fullUrlSamples.push(...fullUrls.slice(0, this.maxUrlSamples));\n }\n\n const statusCodes = extractStatusCodes(originalLine);\n if (statusCodes.length > 0) {\n this.statusCodeSamples.push(...statusCodes.slice(0, this.maxStatusCodeSamples));\n }\n\n const correlationIds = extractCorrelationIds(originalLine);\n if (correlationIds.length > 0) {\n this.correlationIdSamples.push(...correlationIds.slice(0, this.maxCorrelationIdSamples));\n }\n\n const durations = extractDurations(originalLine);\n if (durations.length > 0) {\n this.durationSamples.push(...durations.slice(0, this.maxDurationSamples));\n }\n }\n\n /**\n * Update the cluster with a new matching log line.\n * Returns the variables extracted from this match.\n */\n update(tokens: string[], lineIndex: number, originalLine: string = ''): string[] {\n this.count++;\n this.lastSeen = lineIndex;\n\n // Extract variables (positions where template has wildcard)\n const variables: string[] = [];\n for (let i = 0; i < this.tokens.length && i < tokens.length; i++) {\n if (this.tokens[i] === WILDCARD) {\n variables.push(tokens[i] ?? '');\n }\n }\n\n // Store sample if we haven't reached the limit\n if (this.sampleVariables.length < this.maxSamples) {\n this.sampleVariables.push(variables);\n }\n\n // Extract and store samples from original line if present\n if (originalLine) {\n // URLs (hostnames)\n if (this.urlSamples.length < this.maxUrlSamples) {\n const urls = extractUrls(originalLine);\n for (const url of urls) {\n if (!this.urlSamples.includes(url) && this.urlSamples.length < this.maxUrlSamples) {\n this.urlSamples.push(url);\n }\n }\n }\n\n // Full URLs\n if (this.fullUrlSamples.length < this.maxUrlSamples) {\n const fullUrls = extractFullUrls(originalLine);\n for (const url of fullUrls) {\n if (!this.fullUrlSamples.includes(url) && this.fullUrlSamples.length < this.maxUrlSamples) {\n this.fullUrlSamples.push(url);\n }\n }\n }\n\n // Status codes\n if (this.statusCodeSamples.length < this.maxStatusCodeSamples) {\n const statusCodes = extractStatusCodes(originalLine);\n for (const code of statusCodes) {\n if (!this.statusCodeSamples.includes(code) && this.statusCodeSamples.length < this.maxStatusCodeSamples) {\n this.statusCodeSamples.push(code);\n }\n }\n }\n\n // Correlation IDs\n if (this.correlationIdSamples.length < this.maxCorrelationIdSamples) {\n const correlationIds = extractCorrelationIds(originalLine);\n for (const id of correlationIds) {\n if (!this.correlationIdSamples.includes(id) && this.correlationIdSamples.length < this.maxCorrelationIdSamples) {\n this.correlationIdSamples.push(id);\n }\n }\n }\n\n // Durations\n if (this.durationSamples.length < this.maxDurationSamples) {\n const durations = extractDurations(originalLine);\n for (const duration of durations) {\n if (!this.durationSamples.includes(duration) && this.durationSamples.length < this.maxDurationSamples) {\n this.durationSamples.push(duration);\n }\n }\n }\n }\n\n return variables;\n }\n\n /**\n * Get the template pattern as a string.\n */\n getPattern(): string {\n return this.tokens.join(' ');\n }\n\n /**\n * Compute similarity between this cluster's template and a set of tokens.\n * Returns a value between 0.0 and 1.0.\n */\n computeSimilarity(tokens: string[]): number {\n // Guard against division by zero\n if (this.tokens.length === 0) {\n return 0;\n }\n\n if (tokens.length !== this.tokens.length) {\n return 0;\n }\n\n let matchCount = 0;\n for (let i = 0; i < this.tokens.length; i++) {\n const templateToken = this.tokens[i];\n const inputToken = tokens[i];\n\n // Wildcards always match\n if (templateToken === WILDCARD) {\n matchCount++;\n } else if (templateToken === inputToken) {\n matchCount++;\n }\n }\n\n return matchCount / this.tokens.length;\n }\n\n /**\n * Merge tokens into the template, converting differing positions to wildcards.\n * Mutates the template tokens in place.\n */\n mergeTokens(tokens: string[]): void {\n for (let i = 0; i < this.tokens.length && i < tokens.length; i++) {\n if (this.tokens[i] !== WILDCARD && this.tokens[i] !== tokens[i]) {\n (this.tokens as string[])[i] = WILDCARD;\n }\n }\n }\n}\n","import type { Template, CompressionResult } from '../types.js';\n\n/**\n * Recursively sort object keys for deterministic JSON output.\n * This enables LLM KV-cache hits when the same data is serialized multiple times.\n */\nfunction sortObjectKeys(value: unknown): unknown {\n if (value === null || typeof value !== 'object') {\n return value;\n }\n\n if (Array.isArray(value)) {\n return value.map(sortObjectKeys);\n }\n\n const sorted: Record<string, unknown> = {};\n const keys = Object.keys(value as Record<string, unknown>).sort();\n for (const key of keys) {\n sorted[key] = sortObjectKeys((value as Record<string, unknown>)[key]);\n }\n return sorted;\n}\n\n/**\n * Format templates as a compact summary.\n */\nexport function formatSummary(\n templates: Template[],\n stats: CompressionResult['stats']\n): string {\n const lines: string[] = [];\n\n // Header\n lines.push('=== Log Compression Summary ===');\n lines.push(\n `Input: ${stats.inputLines.toLocaleString()} lines → ${stats.uniqueTemplates} templates ` +\n `(${(stats.compressionRatio * 100).toFixed(1)}% reduction)`\n );\n lines.push('');\n\n if (templates.length === 0) {\n lines.push('No templates discovered.');\n return lines.join('\\n');\n }\n\n // Top templates by frequency\n lines.push('Top templates by frequency:');\n\n const topTemplates = templates.slice(0, 20);\n topTemplates.forEach((template, index) => {\n const count = template.occurrences.toLocaleString();\n lines.push(`${index + 1}. [${count}x] ${template.pattern}`);\n });\n\n if (templates.length > 20) {\n lines.push(`... and ${templates.length - 20} more templates`);\n }\n\n // Rare events section\n const rareTemplates = templates.filter((t) => t.occurrences <= 5);\n if (rareTemplates.length > 0) {\n lines.push('');\n lines.push(`Rare events (≤5 occurrences): ${rareTemplates.length} templates`);\n\n const shownRare = rareTemplates.slice(0, 5);\n for (const template of shownRare) {\n lines.push(`- [${template.occurrences}x] ${template.pattern}`);\n }\n\n if (rareTemplates.length > 5) {\n lines.push(`... and ${rareTemplates.length - 5} more rare templates`);\n }\n }\n\n return lines.join('\\n');\n}\n\n/**\n * Format templates with full details including sample variables.\n */\nexport function formatDetailed(\n templates: Template[],\n stats: CompressionResult['stats']\n): string {\n const lines: string[] = [];\n\n // Header\n lines.push('=== Log Compression Details ===');\n lines.push(\n `Input: ${stats.inputLines.toLocaleString()} lines → ${stats.uniqueTemplates} templates ` +\n `(${(stats.compressionRatio * 100).toFixed(1)}% reduction)`\n );\n lines.push(`Estimated token reduction: ${(stats.estimatedTokenReduction * 100).toFixed(1)}%`);\n lines.push('');\n\n if (templates.length === 0) {\n lines.push('No templates discovered.');\n return lines.join('\\n');\n }\n\n // Each template with samples\n for (const template of templates) {\n lines.push(`=== Template ${template.id} (${template.occurrences.toLocaleString()} occurrences) ===`);\n lines.push(`Pattern: ${template.pattern}`);\n lines.push(`Severity: ${template.severity}${template.isStackFrame ? ' (stack frame)' : ''}`);\n lines.push(`First seen: line ${template.firstSeen + 1}`);\n lines.push(`Last seen: line ${template.lastSeen + 1}`);\n\n if (template.fullUrlSamples.length > 0) {\n lines.push('URLs:');\n for (const url of template.fullUrlSamples) {\n lines.push(` - ${url}`);\n }\n }\n\n if (template.statusCodeSamples.length > 0) {\n lines.push(`Status codes: ${template.statusCodeSamples.join(', ')}`);\n }\n\n if (template.correlationIdSamples.length > 0) {\n lines.push(`Correlation IDs: ${template.correlationIdSamples.join(', ')}`);\n }\n\n if (template.durationSamples.length > 0) {\n lines.push(`Durations: ${template.durationSamples.join(', ')}`);\n }\n\n if (template.sampleVariables.length > 0) {\n lines.push('Sample variables:');\n for (const vars of template.sampleVariables) {\n if (vars.length > 0) {\n lines.push(` - ${vars.join(', ')}`);\n }\n }\n }\n\n lines.push('');\n }\n\n return lines.join('\\n');\n}\n\n/**\n * Format templates as JSON.\n */\nexport function formatJson(\n templates: Template[],\n stats: CompressionResult['stats']\n): string {\n const output = {\n version: '1.1',\n stats: {\n inputLines: stats.inputLines,\n uniqueTemplates: stats.uniqueTemplates,\n compressionRatio: Math.round(stats.compressionRatio * 1000) / 1000,\n estimatedTokenReduction: Math.round(stats.estimatedTokenReduction * 1000) / 1000,\n },\n templates: templates.map((t) => ({\n id: t.id,\n pattern: t.pattern,\n occurrences: t.occurrences,\n severity: t.severity,\n isStackFrame: t.isStackFrame,\n samples: t.sampleVariables,\n urlSamples: t.urlSamples,\n fullUrlSamples: t.fullUrlSamples,\n statusCodeSamples: t.statusCodeSamples,\n correlationIdSamples: t.correlationIdSamples,\n durationSamples: t.durationSamples,\n firstSeen: t.firstSeen,\n lastSeen: t.lastSeen,\n })),\n };\n\n return JSON.stringify(output, null, 2);\n}\n\n/**\n * Format templates as cache-optimized JSON.\n *\n * Key differences from standard JSON:\n * - Sorted keys at all levels for deterministic output\n * - Compact format (no whitespace) to minimize tokens\n * - Enables LLM prompt caching (KV-cache hits) when sending\n * the same compressed logs multiple times\n */\nexport function formatJsonStable(\n templates: Template[],\n stats: CompressionResult['stats']\n): string {\n const output = {\n stats: {\n compressionRatio: Math.round(stats.compressionRatio * 1000) / 1000,\n estimatedTokenReduction: Math.round(stats.estimatedTokenReduction * 1000) / 1000,\n inputLines: stats.inputLines,\n uniqueTemplates: stats.uniqueTemplates,\n },\n templates: templates.map((t) => ({\n correlationIdSamples: t.correlationIdSamples,\n durationSamples: t.durationSamples,\n firstSeen: t.firstSeen,\n fullUrlSamples: t.fullUrlSamples,\n id: t.id,\n isStackFrame: t.isStackFrame,\n lastSeen: t.lastSeen,\n occurrences: t.occurrences,\n pattern: t.pattern,\n samples: t.sampleVariables,\n severity: t.severity,\n statusCodeSamples: t.statusCodeSamples,\n urlSamples: t.urlSamples,\n })),\n version: '1.1',\n };\n\n // Compact output with sorted keys for cache optimization\n return JSON.stringify(sortObjectKeys(output));\n}\n","import type { DrainOptions, ParsingStrategy, Template, OutputFormat, CompressionResult, ProgressCallback } from '../types.js';\nimport { defaultStrategy } from '../preprocessing/default.js';\nimport { WILDCARD } from '../preprocessing/patterns.js';\nimport { DrainNode } from './node.js';\nimport { LogCluster } from './cluster.js';\nimport { formatSummary, formatDetailed, formatJson, formatJsonStable } from '../output/formatter.js';\n\n/**\n * Default configuration values for Drain algorithm.\n */\nconst DEFAULTS = {\n depth: 4,\n simThreshold: 0.4,\n maxChildren: 100,\n maxClusters: 1000,\n maxSamples: 3,\n} as const;\n\n/**\n * Special key used for the wildcard child in the parse tree.\n */\nconst WILDCARD_KEY = '<WILDCARD>';\n\n/**\n * Drain algorithm implementation for log template mining.\n *\n * The algorithm constructs a fixed-depth parse tree to efficiently\n * cluster log messages by their template structure.\n *\n * Tree Structure:\n * - Level 0 (root): Entry point\n * - Level 1: Token count (length of log message)\n * - Level 2: First token of the message\n * - Levels 3+: Subsequent tokens up to configured depth\n * - Leaf: LogCluster containing the template\n */\nexport class Drain {\n private readonly root: DrainNode;\n private readonly clusters: LogCluster[];\n private readonly strategy: ParsingStrategy;\n private readonly depth: number;\n private readonly maxChildren: number;\n private readonly maxClusters: number;\n private readonly maxSamples: number;\n private readonly onProgress: ProgressCallback | undefined;\n private lineCount: number;\n private nextClusterId: number;\n\n constructor(options: DrainOptions = {}) {\n this.root = new DrainNode(0);\n this.clusters = [];\n this.strategy = options.preprocessing ?? defaultStrategy;\n this.depth = options.depth ?? DEFAULTS.depth;\n this.maxChildren = options.maxChildren ?? DEFAULTS.maxChildren;\n this.maxClusters = options.maxClusters ?? DEFAULTS.maxClusters;\n this.maxSamples = options.maxSamples ?? DEFAULTS.maxSamples;\n this.onProgress = options.onProgress;\n this.lineCount = 0;\n this.nextClusterId = 1;\n }\n\n /**\n * Process a single log line.\n */\n addLogLine(line: string): LogCluster | null {\n const lineIndex = this.lineCount++;\n\n // Skip empty lines\n const trimmed = line.trim();\n if (trimmed.length === 0) {\n return null;\n }\n\n // Preprocess and tokenize\n const preprocessed = this.strategy.preprocess(trimmed);\n const tokens = this.strategy.tokenize(preprocessed);\n\n if (tokens.length === 0) {\n return null;\n }\n\n // Search for matching cluster\n const matchedCluster = this.treeSearch(tokens);\n\n if (matchedCluster !== null) {\n // Update existing cluster, passing original line for URL extraction\n matchedCluster.update(tokens, lineIndex, trimmed);\n matchedCluster.mergeTokens(tokens);\n return matchedCluster;\n }\n\n // Create new cluster if under limit\n if (this.clusters.length >= this.maxClusters) {\n return null;\n }\n\n return this.createCluster(tokens, lineIndex, trimmed);\n }\n\n /**\n * Process multiple log lines with optional progress reporting.\n */\n addLogLines(lines: string[]): void {\n const total = lines.length;\n\n // Calculate report interval (emit at most 100 progress events)\n const reportInterval = Math.max(1, Math.floor(total / 100));\n\n // Emit initial parsing phase\n if (this.onProgress && total > 0) {\n this.onProgress({\n processedLines: 0,\n totalLines: total,\n currentPhase: 'parsing',\n percentComplete: 0,\n });\n }\n\n for (let i = 0; i < total; i++) {\n this.addLogLine(lines[i] as string);\n\n // Emit progress at intervals\n if (this.onProgress && i % reportInterval === 0) {\n this.onProgress({\n processedLines: i + 1,\n totalLines: total,\n currentPhase: 'clustering',\n percentComplete: Math.round(((i + 1) / total) * 100),\n });\n }\n }\n\n // Emit final progress event\n if (this.onProgress && total > 0) {\n this.onProgress({\n processedLines: total,\n totalLines: total,\n currentPhase: 'finalizing',\n percentComplete: 100,\n });\n }\n }\n\n /**\n * Search the parse tree for a matching cluster.\n *\n * Uses XDrain-inspired token-position fallback: if the first token\n * looks like a variable (timestamp, PID, etc.), tries using the\n * second token as the navigation key for better matching.\n */\n private treeSearch(tokens: string[]): LogCluster | null {\n const tokenCount = tokens.length;\n const tokenCountKey = String(tokenCount);\n\n // Level 1: Navigate by token count\n const lengthNode = this.root.getChild(tokenCountKey);\n if (lengthNode === undefined) {\n return null;\n }\n\n // Try primary search with first token\n const primaryResult = this.treeSearchFromToken(lengthNode, tokens, 0);\n if (primaryResult !== null) {\n return primaryResult;\n }\n\n // XDrain-inspired fallback: if first token looks like a variable,\n // try searching with second token as the navigation key\n const firstToken = tokens[0];\n if (\n tokens.length > 1 &&\n firstToken !== undefined &&\n this.looksLikeVariable(firstToken)\n ) {\n return this.treeSearchFromToken(lengthNode, tokens, 1);\n }\n\n return null;\n }\n\n /**\n * Search from a specific token position in the tree.\n * @param lengthNode - The node at level 1 (token count)\n * @param tokens - All tokens in the log line\n * @param startIndex - Which token to use as \"first token\" for navigation\n */\n private treeSearchFromToken(\n lengthNode: DrainNode,\n tokens: string[],\n startIndex: number\n ): LogCluster | null {\n // Level 2: Navigate by token at startIndex\n const navToken = tokens[startIndex];\n if (navToken === undefined) {\n return null;\n }\n\n let currentNode = lengthNode.getChild(navToken);\n\n // Only use wildcard fallback for primary search (startIndex=0).\n // For alternate token searches, wildcard path is already covered by\n // primary search and would have misaligned traversal indices.\n if (currentNode === undefined && startIndex === 0) {\n currentNode = lengthNode.getChild(WILDCARD_KEY);\n }\n\n if (currentNode === undefined) {\n return null;\n }\n\n // Levels 3+: Navigate by subsequent tokens\n // Use this.depth as upper bound (not this.depth + startIndex) to match\n // the tree structure created during insertion\n let searchNode: DrainNode = currentNode;\n for (let i = startIndex + 1; i < Math.min(tokens.length, this.depth); i++) {\n const token = tokens[i];\n if (token === undefined) {\n break;\n }\n\n let nextNode = searchNode.getChild(token);\n\n // Try wildcard child if exact match not found\n if (nextNode === undefined) {\n nextNode = searchNode.getChild(WILDCARD_KEY);\n }\n\n if (nextNode === undefined) {\n break;\n }\n\n searchNode = nextNode;\n }\n\n // Search clusters at this node for best match\n return this.findBestMatch(searchNode, tokens);\n }\n\n /**\n * Find the best matching cluster at a node.\n */\n private findBestMatch(node: DrainNode, tokens: string[]): LogCluster | null {\n let bestCluster: LogCluster | null = null;\n let bestSimilarity = 0;\n\n const threshold = this.strategy.getSimThreshold(node.depth);\n\n for (const cluster of node.clusters) {\n const similarity = cluster.computeSimilarity(tokens);\n\n if (similarity >= threshold && similarity > bestSimilarity) {\n bestSimilarity = similarity;\n bestCluster = cluster;\n }\n }\n\n return bestCluster;\n }\n\n /**\n * Create a new cluster and add it to the tree.\n */\n private createCluster(tokens: string[], lineIndex: number, originalLine: string = ''): LogCluster {\n const clusterId = `t${String(this.nextClusterId++).padStart(3, '0')}`;\n const cluster = new LogCluster(clusterId, tokens, lineIndex, this.maxSamples, originalLine);\n\n // Navigate/create path in tree\n const tokenCount = tokens.length;\n const tokenCountKey = String(tokenCount);\n\n // Level 1: Token count\n const lengthNode = this.root.getOrCreateChild(tokenCountKey);\n\n // Level 2: First token\n const firstToken = tokens[0];\n if (firstToken === undefined) {\n this.clusters.push(cluster);\n return cluster;\n }\n\n // Decide whether to use actual token or wildcard key\n const firstKey = this.shouldUseWildcard(lengthNode, firstToken)\n ? WILDCARD_KEY\n : firstToken;\n\n let currentNode = lengthNode.getOrCreateChild(firstKey);\n\n // Levels 3+: Subsequent tokens\n for (let i = 1; i < Math.min(tokens.length, this.depth); i++) {\n const token = tokens[i];\n if (token === undefined) {\n break;\n }\n\n const key = this.shouldUseWildcard(currentNode, token)\n ? WILDCARD_KEY\n : token;\n\n currentNode = currentNode.getOrCreateChild(key);\n }\n\n // Add cluster to leaf node\n currentNode.addCluster(cluster);\n this.clusters.push(cluster);\n\n return cluster;\n }\n\n /**\n * Determine if we should use a wildcard key for a token.\n * Uses maxChildren limit to prevent tree explosion.\n */\n private shouldUseWildcard(node: DrainNode, token: string): boolean {\n // If token already exists as child, use it\n if (node.hasChild(token)) {\n return false;\n }\n\n // If we have wildcard child and are at capacity, use wildcard\n if (node.hasChild(WILDCARD_KEY) && node.childCount >= this.maxChildren) {\n return true;\n }\n\n // If token looks like a variable (starts with digit, etc.), use wildcard\n if (this.looksLikeVariable(token)) {\n return true;\n }\n\n // Otherwise use the actual token\n return false;\n }\n\n /**\n * Heuristic to detect if a token looks like a variable value.\n */\n private looksLikeVariable(token: string): boolean {\n // Already a wildcard\n if (token === WILDCARD) {\n return true;\n }\n\n // Starts with a digit\n const firstChar = token.charAt(0);\n if (firstChar >= '0' && firstChar <= '9') {\n return true;\n }\n\n // Contains only hex characters (likely an ID)\n if (/^[0-9a-fA-F]+$/.test(token) && token.length > 8) {\n return true;\n }\n\n return false;\n }\n\n /**\n * Get all discovered templates.\n */\n getTemplates(): Template[] {\n return this.clusters.map((cluster) => ({\n id: cluster.id,\n pattern: cluster.getPattern(),\n occurrences: cluster.count,\n sampleVariables: cluster.sampleVariables,\n firstSeen: cluster.firstSeen,\n lastSeen: cluster.lastSeen,\n severity: cluster.severity,\n urlSamples: cluster.urlSamples,\n fullUrlSamples: cluster.fullUrlSamples,\n statusCodeSamples: cluster.statusCodeSamples,\n correlationIdSamples: cluster.correlationIdSamples,\n durationSamples: cluster.durationSamples,\n isStackFrame: cluster.isStackFrame,\n }));\n }\n\n /**\n * Get compression result with formatted output.\n */\n getResult(format: OutputFormat = 'summary', maxTemplates: number = 50): CompressionResult {\n const templates = this.getTemplates();\n\n // Sort by occurrences (descending)\n templates.sort((a, b) => b.occurrences - a.occurrences);\n\n // Limit templates in output\n const limitedTemplates = templates.slice(0, maxTemplates);\n\n // Calculate stats\n const stats = this.calculateStats(templates);\n\n // Format output\n let formatted: string;\n switch (format) {\n case 'detailed':\n formatted = formatDetailed(limitedTemplates, stats);\n break;\n case 'json':\n formatted = formatJson(limitedTemplates, stats);\n break;\n case 'json-stable':\n formatted = formatJsonStable(limitedTemplates, stats);\n break;\n case 'summary':\n default:\n formatted = formatSummary(limitedTemplates, stats);\n break;\n }\n\n return {\n templates: limitedTemplates,\n stats,\n formatted,\n };\n }\n\n /**\n * Calculate compression statistics.\n */\n private calculateStats(templates: Template[]): CompressionResult['stats'] {\n const inputLines = this.lineCount;\n const uniqueTemplates = templates.length;\n\n // Compression ratio: 1 - (templates / lines)\n const compressionRatio = inputLines > 0\n ? 1 - (uniqueTemplates / inputLines)\n : 0;\n\n // Estimate token reduction using character count proxy\n // Each template is shown once instead of repeated N times\n let originalChars = 0;\n let compressedChars = 0;\n\n for (const template of templates) {\n const patternLength = template.pattern.length;\n // Original: pattern repeated for each occurrence\n originalChars += patternLength * template.occurrences;\n // Compressed: pattern shown once + count indicator\n compressedChars += patternLength + 20; // ~20 chars for \"[Nx] \" prefix\n }\n\n const estimatedTokenReduction = originalChars > 0\n ? 1 - (compressedChars / originalChars)\n : 0;\n\n return {\n inputLines,\n uniqueTemplates,\n compressionRatio: Math.max(0, Math.min(1, compressionRatio)),\n estimatedTokenReduction: Math.max(0, Math.min(1, estimatedTokenReduction)),\n };\n }\n\n /**\n * Get the number of lines processed.\n */\n get totalLines(): number {\n return this.lineCount;\n }\n\n /**\n * Get the number of clusters (templates) discovered.\n */\n get totalClusters(): number {\n return this.clusters.length;\n }\n}\n\n/**\n * Create a new Drain instance with the given options.\n */\nexport function createDrain(options?: DrainOptions): Drain {\n return new Drain(options);\n}\n","import type { CompressOptions, CompressionResult } from './types.js';\nimport { createDrain } from './drain/drain.js';\n\n/**\n * Compress log lines by extracting templates.\n *\n * This is the main entry point for simple use cases.\n * For more control, use `createDrain()` directly.\n *\n * @param lines - Array of log lines to compress\n * @param options - Compression options\n * @returns Compression result with templates and statistics\n *\n * @example\n * ```typescript\n * import { compress } from 'logpare';\n *\n * const logs = [\n * 'Connection from 192.168.1.1 established',\n * 'Connection from 192.168.1.2 established',\n * 'Connection from 10.0.0.1 established',\n * ];\n *\n * const result = compress(logs);\n * console.log(result.formatted);\n * // Output: [3x] Connection from <*> established\n * ```\n */\nexport function compress(\n lines: string[],\n options: CompressOptions = {}\n): CompressionResult {\n const { format = 'summary', maxTemplates = 50, drain: drainOptions } = options;\n\n const startTime = performance.now();\n\n const drain = createDrain(drainOptions);\n drain.addLogLines(lines);\n\n const result = drain.getResult(format, maxTemplates);\n const processingTimeMs = Math.round(performance.now() - startTime);\n\n // Add processing time to stats\n return {\n ...result,\n stats: {\n ...result.stats,\n processingTimeMs,\n },\n };\n}\n\n/**\n * Compress a single string containing multiple log lines.\n *\n * @param text - Raw log text (lines separated by newlines)\n * @param options - Compression options\n * @returns Compression result with templates and statistics\n */\nexport function compressText(\n text: string,\n options: CompressOptions = {}\n): CompressionResult {\n const lines = text.split(/\\r?\\n/);\n return compress(lines, options);\n}\n"],"mappings":";AAKO,IAAM,oBAAoB;AAAA,EAC/B,OAAO;AAAA,EACP,SAAS;AACX;AAKO,IAAM,uBAAuB;AAAA,EAClC;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AACF;AAMO,SAAS,eAAe,MAAwB;AACrD,MAAI,kBAAkB,MAAM,KAAK,IAAI,GAAG;AACtC,WAAO;AAAA,EACT;AACA,MAAI,kBAAkB,QAAQ,KAAK,IAAI,GAAG;AACxC,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAKO,SAAS,aAAa,MAAuB;AAClD,SAAO,qBAAqB,KAAK,aAAW,QAAQ,KAAK,IAAI,CAAC;AAChE;AAMO,SAAS,YAAY,MAAwB;AAClD,QAAM,aAAa;AACnB,QAAM,UAAU,KAAK,MAAM,UAAU;AACrC,MAAI,CAAC,QAAS,QAAO,CAAC;AAGtB,SAAO,QAAQ,IAAI,SAAO;AACxB,QAAI;AACF,YAAM,SAAS,IAAI,IAAI,GAAG;AAC1B,aAAO,OAAO;AAAA,IAChB,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF,CAAC,EAAE,OAAO,CAAC,GAAG,GAAG,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC;AAC3C;AAMO,SAAS,gBAAgB,MAAwB;AACtD,QAAM,aAAa;AACnB,QAAM,UAAU,KAAK,MAAM,UAAU;AACrC,MAAI,CAAC,QAAS,QAAO,CAAC;AAGtB,SAAO,CAAC,GAAG,IAAI,IAAI,OAAO,CAAC;AAC7B;AAMO,SAAS,mBAAmB,MAAwB;AAEzD,QAAM,WAAW;AAAA,IACf;AAAA;AAAA,IACA;AAAA;AAAA,IACA;AAAA;AAAA,IACA;AAAA,EACF;AAEA,QAAM,QAAkB,CAAC;AAEzB,aAAW,WAAW,UAAU;AAC9B,UAAM,QAAQ,IAAI,OAAO,QAAQ,QAAQ,QAAQ,KAAK;AACtD,QAAI;AACJ,YAAQ,QAAQ,MAAM,KAAK,IAAI,OAAO,MAAM;AAC1C,YAAM,UAAU,MAAM,CAAC;AACvB,UAAI,SAAS;AACX,cAAM,OAAO,SAAS,SAAS,EAAE;AAEjC,YAAI,QAAQ,OAAO,QAAQ,OAAO,CAAC,MAAM,SAAS,IAAI,GAAG;AACvD,gBAAM,KAAK,IAAI;AAAA,QACjB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAMO,SAAS,sBAAsB,MAAwB;AAC5D,QAAM,WAAW;AAAA;AAAA,IAEf;AAAA;AAAA,IAEA;AAAA,EACF;AAEA,QAAM,MAAgB,CAAC;AAEvB,aAAW,WAAW,UAAU;AAC9B,UAAM,QAAQ,IAAI,OAAO,QAAQ,QAAQ,QAAQ,KAAK;AACtD,QAAI;AACJ,YAAQ,QAAQ,MAAM,KAAK,IAAI,OAAO,MAAM;AAC1C,YAAM,KAAK,MAAM,CAAC,KAAK,MAAM,CAAC;AAC9B,UAAI,MAAM,CAAC,IAAI,SAAS,EAAE,GAAG;AAC3B,YAAI,KAAK,EAAE;AAAA,MACb;AAAA,IACF;AAAA,EACF;AAGA,SAAO,IAAI,MAAM,GAAG,CAAC;AACvB;AAOO,SAAS,iBAAiB,MAAwB;AAKvD,QAAM,kBAAkB;AAExB,QAAM,YAAsB,CAAC;AAC7B,MAAI;AAEJ,UAAQ,QAAQ,gBAAgB,KAAK,IAAI,OAAO,MAAM;AAEpD,UAAM,WAAW,GAAG,MAAM,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC;AACvC,QAAI,CAAC,UAAU,SAAS,QAAQ,GAAG;AACjC,gBAAU,KAAK,QAAQ;AAAA,IACzB;AAAA,EACF;AAGA,SAAO,UAAU,MAAM,GAAG,CAAC;AAC7B;AAQO,IAAM,mBAA2C;AAAA;AAAA,EAEtD,cAAc;AAAA;AAAA,EAGd,MAAM;AAAA,EAEN,eAAe;AAAA;AAAA,EAGf,MAAM;AAAA;AAAA;AAAA,EAGN,MAAM;AAAA,EACN,MAAM;AAAA;AAAA,EAGN,OAAO;AAAA,EACP,SAAS;AAAA;AAAA;AAAA;AAAA,EAKT,UAAU;AAAA,EACV,KAAK;AAAA;AAAA;AAAA;AAAA,EAKL,WAAW;AAAA;AAAA;AAAA,EAIX,SAAS;AACX;AAKO,IAAM,WAAW;AAMjB,SAAS,cACd,MACA,UACA,WAAmB,UACX;AACR,MAAI,SAAS;AAEb,aAAW,WAAW,OAAO,OAAO,QAAQ,GAAG;AAE7C,UAAM,QAAQ,IAAI,OAAO,QAAQ,QAAQ,QAAQ,KAAK;AACtD,aAAS,OAAO,QAAQ,OAAO,QAAQ;AAAA,EACzC;AAEA,SAAO;AACT;;;AC/NO,IAAM,kBAAmC;AAAA;AAAA;AAAA;AAAA,EAI9C,WAAW,MAAsB;AAC/B,WAAO,cAAc,MAAM,kBAAkB,QAAQ;AAAA,EACvD;AAAA;AAAA;AAAA;AAAA,EAKA,SAAS,MAAwB;AAC/B,WAAO,KAAK,MAAM,KAAK,EAAE,OAAO,CAAC,UAAU,MAAM,SAAS,CAAC;AAAA,EAC7D;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,gBAAgB,QAAwB;AACtC,WAAO;AAAA,EACT;AACF;AAKO,SAAS,eACd,WAGiB;AACjB,QAAM,EAAE,UAAU,GAAG,kBAAkB,IAAI;AAE3C,SAAO;AAAA,IACL,YAAY,kBAAkB,eAAe,CAAC,SAAiB;AAC7D,YAAM,iBAAiB,WACnB,EAAE,GAAG,kBAAkB,GAAG,SAAS,IACnC;AACJ,aAAO,cAAc,MAAM,gBAAgB,QAAQ;AAAA,IACrD;AAAA,IAEA,UAAU,kBAAkB,YAAY,gBAAgB;AAAA,IAExD,iBAAiB,kBAAkB,mBAAmB,gBAAgB;AAAA,EACxE;AACF;;;ACxCO,IAAM,YAAN,MAAM,WAAU;AAAA;AAAA,EAEZ;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA;AAAA,EAET,YAAY,OAAe;AACzB,SAAK,QAAQ;AACb,SAAK,WAAW,oBAAI,IAAI;AACxB,SAAK,WAAW,CAAC;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA,EAKA,iBAAiB,KAAwB;AACvC,QAAI,QAAQ,KAAK,SAAS,IAAI,GAAG;AACjC,QAAI,UAAU,QAAW;AACvB,cAAQ,IAAI,WAAU,KAAK,QAAQ,CAAC;AACpC,WAAK,SAAS,IAAI,KAAK,KAAK;AAAA,IAC9B;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,SAAS,KAAsB;AAC7B,WAAO,KAAK,SAAS,IAAI,GAAG;AAAA,EAC9B;AAAA;AAAA;AAAA;AAAA,EAKA,SAAS,KAAoC;AAC3C,WAAO,KAAK,SAAS,IAAI,GAAG;AAAA,EAC9B;AAAA;AAAA;AAAA;AAAA,EAKA,WAAW,SAA2B;AACpC,SAAK,SAAS,KAAK,OAAO;AAAA,EAC5B;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,aAAqB;AACvB,WAAO,KAAK,SAAS;AAAA,EACvB;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,eAAuB;AACzB,WAAO,KAAK,SAAS;AAAA,EACvB;AACF;;;AC9DO,IAAM,aAAN,MAAiB;AAAA;AAAA,EAEb;AAAA;AAAA,EAGA;AAAA;AAAA,EAGT;AAAA;AAAA,EAGS;AAAA;AAAA,EAGT;AAAA;AAAA,EAGA;AAAA;AAAA,EAGS;AAAA;AAAA,EAGA;AAAA;AAAA,EAGA;AAAA;AAAA,EAGA;AAAA;AAAA,EAGA;AAAA;AAAA,EAGA;AAAA;AAAA,EAGA;AAAA;AAAA,EAGQ;AAAA;AAAA,EAGA,gBAAwB;AAAA;AAAA,EAGxB,uBAA+B;AAAA;AAAA,EAG/B,0BAAkC;AAAA;AAAA,EAGlC,qBAA6B;AAAA,EAE9C,YACE,IACA,QACA,WACA,aAAqB,GACrB,eAAuB,IACvB;AACA,SAAK,KAAK;AACV,SAAK,SAAS,OAAO,MAAM;AAC3B,SAAK,QAAQ;AACb,SAAK,kBAAkB,CAAC;AACxB,SAAK,YAAY;AACjB,SAAK,WAAW;AAChB,SAAK,aAAa;AAGlB,SAAK,WAAW,eAAe,YAAY;AAC3C,SAAK,eAAe,aAAa,YAAY;AAG7C,SAAK,aAAa,CAAC;AACnB,SAAK,iBAAiB,CAAC;AACvB,SAAK,oBAAoB,CAAC;AAC1B,SAAK,uBAAuB,CAAC;AAC7B,SAAK,kBAAkB,CAAC;AAGxB,UAAM,OAAO,YAAY,YAAY;AACrC,QAAI,KAAK,SAAS,GAAG;AACnB,WAAK,WAAW,KAAK,GAAG,KAAK,MAAM,GAAG,KAAK,aAAa,CAAC;AAAA,IAC3D;AAEA,UAAM,WAAW,gBAAgB,YAAY;AAC7C,QAAI,SAAS,SAAS,GAAG;AACvB,WAAK,eAAe,KAAK,GAAG,SAAS,MAAM,GAAG,KAAK,aAAa,CAAC;AAAA,IACnE;AAEA,UAAM,cAAc,mBAAmB,YAAY;AACnD,QAAI,YAAY,SAAS,GAAG;AAC1B,WAAK,kBAAkB,KAAK,GAAG,YAAY,MAAM,GAAG,KAAK,oBAAoB,CAAC;AAAA,IAChF;AAEA,UAAM,iBAAiB,sBAAsB,YAAY;AACzD,QAAI,eAAe,SAAS,GAAG;AAC7B,WAAK,qBAAqB,KAAK,GAAG,eAAe,MAAM,GAAG,KAAK,uBAAuB,CAAC;AAAA,IACzF;AAEA,UAAM,YAAY,iBAAiB,YAAY;AAC/C,QAAI,UAAU,SAAS,GAAG;AACxB,WAAK,gBAAgB,KAAK,GAAG,UAAU,MAAM,GAAG,KAAK,kBAAkB,CAAC;AAAA,IAC1E;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,OAAO,QAAkB,WAAmB,eAAuB,IAAc;AAC/E,SAAK;AACL,SAAK,WAAW;AAGhB,UAAM,YAAsB,CAAC;AAC7B,aAAS,IAAI,GAAG,IAAI,KAAK,OAAO,UAAU,IAAI,OAAO,QAAQ,KAAK;AAChE,UAAI,KAAK,OAAO,CAAC,MAAM,UAAU;AAC/B,kBAAU,KAAK,OAAO,CAAC,KAAK,EAAE;AAAA,MAChC;AAAA,IACF;AAGA,QAAI,KAAK,gBAAgB,SAAS,KAAK,YAAY;AACjD,WAAK,gBAAgB,KAAK,SAAS;AAAA,IACrC;AAGA,QAAI,cAAc;AAEhB,UAAI,KAAK,WAAW,SAAS,KAAK,eAAe;AAC/C,cAAM,OAAO,YAAY,YAAY;AACrC,mBAAW,OAAO,MAAM;AACtB,cAAI,CAAC,KAAK,WAAW,SAAS,GAAG,KAAK,KAAK,WAAW,SAAS,KAAK,eAAe;AACjF,iBAAK,WAAW,KAAK,GAAG;AAAA,UAC1B;AAAA,QACF;AAAA,MACF;AAGA,UAAI,KAAK,eAAe,SAAS,KAAK,eAAe;AACnD,cAAM,WAAW,gBAAgB,YAAY;AAC7C,mBAAW,OAAO,UAAU;AAC1B,cAAI,CAAC,KAAK,eAAe,SAAS,GAAG,KAAK,KAAK,eAAe,SAAS,KAAK,eAAe;AACzF,iBAAK,eAAe,KAAK,GAAG;AAAA,UAC9B;AAAA,QACF;AAAA,MACF;AAGA,UAAI,KAAK,kBAAkB,SAAS,KAAK,sBAAsB;AAC7D,cAAM,cAAc,mBAAmB,YAAY;AACnD,mBAAW,QAAQ,aAAa;AAC9B,cAAI,CAAC,KAAK,kBAAkB,SAAS,IAAI,KAAK,KAAK,kBAAkB,SAAS,KAAK,sBAAsB;AACvG,iBAAK,kBAAkB,KAAK,IAAI;AAAA,UAClC;AAAA,QACF;AAAA,MACF;AAGA,UAAI,KAAK,qBAAqB,SAAS,KAAK,yBAAyB;AACnE,cAAM,iBAAiB,sBAAsB,YAAY;AACzD,mBAAW,MAAM,gBAAgB;AAC/B,cAAI,CAAC,KAAK,qBAAqB,SAAS,EAAE,KAAK,KAAK,qBAAqB,SAAS,KAAK,yBAAyB;AAC9G,iBAAK,qBAAqB,KAAK,EAAE;AAAA,UACnC;AAAA,QACF;AAAA,MACF;AAGA,UAAI,KAAK,gBAAgB,SAAS,KAAK,oBAAoB;AACzD,cAAM,YAAY,iBAAiB,YAAY;AAC/C,mBAAW,YAAY,WAAW;AAChC,cAAI,CAAC,KAAK,gBAAgB,SAAS,QAAQ,KAAK,KAAK,gBAAgB,SAAS,KAAK,oBAAoB;AACrG,iBAAK,gBAAgB,KAAK,QAAQ;AAAA,UACpC;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,aAAqB;AACnB,WAAO,KAAK,OAAO,KAAK,GAAG;AAAA,EAC7B;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,kBAAkB,QAA0B;AAE1C,QAAI,KAAK,OAAO,WAAW,GAAG;AAC5B,aAAO;AAAA,IACT;AAEA,QAAI,OAAO,WAAW,KAAK,OAAO,QAAQ;AACxC,aAAO;AAAA,IACT;AAEA,QAAI,aAAa;AACjB,aAAS,IAAI,GAAG,IAAI,KAAK,OAAO,QAAQ,KAAK;AAC3C,YAAM,gBAAgB,KAAK,OAAO,CAAC;AACnC,YAAM,aAAa,OAAO,CAAC;AAG3B,UAAI,kBAAkB,UAAU;AAC9B;AAAA,MACF,WAAW,kBAAkB,YAAY;AACvC;AAAA,MACF;AAAA,IACF;AAEA,WAAO,aAAa,KAAK,OAAO;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,YAAY,QAAwB;AAClC,aAAS,IAAI,GAAG,IAAI,KAAK,OAAO,UAAU,IAAI,OAAO,QAAQ,KAAK;AAChE,UAAI,KAAK,OAAO,CAAC,MAAM,YAAY,KAAK,OAAO,CAAC,MAAM,OAAO,CAAC,GAAG;AAC/D,QAAC,KAAK,OAAoB,CAAC,IAAI;AAAA,MACjC;AAAA,IACF;AAAA,EACF;AACF;;;ACrPA,SAAS,eAAe,OAAyB;AAC/C,MAAI,UAAU,QAAQ,OAAO,UAAU,UAAU;AAC/C,WAAO;AAAA,EACT;AAEA,MAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,WAAO,MAAM,IAAI,cAAc;AAAA,EACjC;AAEA,QAAM,SAAkC,CAAC;AACzC,QAAM,OAAO,OAAO,KAAK,KAAgC,EAAE,KAAK;AAChE,aAAW,OAAO,MAAM;AACtB,WAAO,GAAG,IAAI,eAAgB,MAAkC,GAAG,CAAC;AAAA,EACtE;AACA,SAAO;AACT;AAKO,SAAS,cACd,WACA,OACQ;AACR,QAAM,QAAkB,CAAC;AAGzB,QAAM,KAAK,iCAAiC;AAC5C,QAAM;AAAA,IACJ,UAAU,MAAM,WAAW,eAAe,CAAC,iBAAY,MAAM,eAAe,gBACvE,MAAM,mBAAmB,KAAK,QAAQ,CAAC,CAAC;AAAA,EAC/C;AACA,QAAM,KAAK,EAAE;AAEb,MAAI,UAAU,WAAW,GAAG;AAC1B,UAAM,KAAK,0BAA0B;AACrC,WAAO,MAAM,KAAK,IAAI;AAAA,EACxB;AAGA,QAAM,KAAK,6BAA6B;AAExC,QAAM,eAAe,UAAU,MAAM,GAAG,EAAE;AAC1C,eAAa,QAAQ,CAAC,UAAU,UAAU;AACxC,UAAM,QAAQ,SAAS,YAAY,eAAe;AAClD,UAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,KAAK,MAAM,SAAS,OAAO,EAAE;AAAA,EAC5D,CAAC;AAED,MAAI,UAAU,SAAS,IAAI;AACzB,UAAM,KAAK,WAAW,UAAU,SAAS,EAAE,iBAAiB;AAAA,EAC9D;AAGA,QAAM,gBAAgB,UAAU,OAAO,CAAC,MAAM,EAAE,eAAe,CAAC;AAChE,MAAI,cAAc,SAAS,GAAG;AAC5B,UAAM,KAAK,EAAE;AACb,UAAM,KAAK,sCAAiC,cAAc,MAAM,YAAY;AAE5E,UAAM,YAAY,cAAc,MAAM,GAAG,CAAC;AAC1C,eAAW,YAAY,WAAW;AAChC,YAAM,KAAK,MAAM,SAAS,WAAW,MAAM,SAAS,OAAO,EAAE;AAAA,IAC/D;AAEA,QAAI,cAAc,SAAS,GAAG;AAC5B,YAAM,KAAK,WAAW,cAAc,SAAS,CAAC,sBAAsB;AAAA,IACtE;AAAA,EACF;AAEA,SAAO,MAAM,KAAK,IAAI;AACxB;AAKO,SAAS,eACd,WACA,OACQ;AACR,QAAM,QAAkB,CAAC;AAGzB,QAAM,KAAK,iCAAiC;AAC5C,QAAM;AAAA,IACJ,UAAU,MAAM,WAAW,eAAe,CAAC,iBAAY,MAAM,eAAe,gBACvE,MAAM,mBAAmB,KAAK,QAAQ,CAAC,CAAC;AAAA,EAC/C;AACA,QAAM,KAAK,+BAA+B,MAAM,0BAA0B,KAAK,QAAQ,CAAC,CAAC,GAAG;AAC5F,QAAM,KAAK,EAAE;AAEb,MAAI,UAAU,WAAW,GAAG;AAC1B,UAAM,KAAK,0BAA0B;AACrC,WAAO,MAAM,KAAK,IAAI;AAAA,EACxB;AAGA,aAAW,YAAY,WAAW;AAChC,UAAM,KAAK,gBAAgB,SAAS,EAAE,KAAK,SAAS,YAAY,eAAe,CAAC,mBAAmB;AACnG,UAAM,KAAK,YAAY,SAAS,OAAO,EAAE;AACzC,UAAM,KAAK,aAAa,SAAS,QAAQ,GAAG,SAAS,eAAe,mBAAmB,EAAE,EAAE;AAC3F,UAAM,KAAK,oBAAoB,SAAS,YAAY,CAAC,EAAE;AACvD,UAAM,KAAK,mBAAmB,SAAS,WAAW,CAAC,EAAE;AAErD,QAAI,SAAS,eAAe,SAAS,GAAG;AACtC,YAAM,KAAK,OAAO;AAClB,iBAAW,OAAO,SAAS,gBAAgB;AACzC,cAAM,KAAK,OAAO,GAAG,EAAE;AAAA,MACzB;AAAA,IACF;AAEA,QAAI,SAAS,kBAAkB,SAAS,GAAG;AACzC,YAAM,KAAK,iBAAiB,SAAS,kBAAkB,KAAK,IAAI,CAAC,EAAE;AAAA,IACrE;AAEA,QAAI,SAAS,qBAAqB,SAAS,GAAG;AAC5C,YAAM,KAAK,oBAAoB,SAAS,qBAAqB,KAAK,IAAI,CAAC,EAAE;AAAA,IAC3E;AAEA,QAAI,SAAS,gBAAgB,SAAS,GAAG;AACvC,YAAM,KAAK,cAAc,SAAS,gBAAgB,KAAK,IAAI,CAAC,EAAE;AAAA,IAChE;AAEA,QAAI,SAAS,gBAAgB,SAAS,GAAG;AACvC,YAAM,KAAK,mBAAmB;AAC9B,iBAAW,QAAQ,SAAS,iBAAiB;AAC3C,YAAI,KAAK,SAAS,GAAG;AACnB,gBAAM,KAAK,OAAO,KAAK,KAAK,IAAI,CAAC,EAAE;AAAA,QACrC;AAAA,MACF;AAAA,IACF;AAEA,UAAM,KAAK,EAAE;AAAA,EACf;AAEA,SAAO,MAAM,KAAK,IAAI;AACxB;AAKO,SAAS,WACd,WACA,OACQ;AACR,QAAM,SAAS;AAAA,IACb,SAAS;AAAA,IACT,OAAO;AAAA,MACL,YAAY,MAAM;AAAA,MAClB,iBAAiB,MAAM;AAAA,MACvB,kBAAkB,KAAK,MAAM,MAAM,mBAAmB,GAAI,IAAI;AAAA,MAC9D,yBAAyB,KAAK,MAAM,MAAM,0BAA0B,GAAI,IAAI;AAAA,IAC9E;AAAA,IACA,WAAW,UAAU,IAAI,CAAC,OAAO;AAAA,MAC/B,IAAI,EAAE;AAAA,MACN,SAAS,EAAE;AAAA,MACX,aAAa,EAAE;AAAA,MACf,UAAU,EAAE;AAAA,MACZ,cAAc,EAAE;AAAA,MAChB,SAAS,EAAE;AAAA,MACX,YAAY,EAAE;AAAA,MACd,gBAAgB,EAAE;AAAA,MAClB,mBAAmB,EAAE;AAAA,MACrB,sBAAsB,EAAE;AAAA,MACxB,iBAAiB,EAAE;AAAA,MACnB,WAAW,EAAE;AAAA,MACb,UAAU,EAAE;AAAA,IACd,EAAE;AAAA,EACJ;AAEA,SAAO,KAAK,UAAU,QAAQ,MAAM,CAAC;AACvC;AAWO,SAAS,iBACd,WACA,OACQ;AACR,QAAM,SAAS;AAAA,IACb,OAAO;AAAA,MACL,kBAAkB,KAAK,MAAM,MAAM,mBAAmB,GAAI,IAAI;AAAA,MAC9D,yBAAyB,KAAK,MAAM,MAAM,0BAA0B,GAAI,IAAI;AAAA,MAC5E,YAAY,MAAM;AAAA,MAClB,iBAAiB,MAAM;AAAA,IACzB;AAAA,IACA,WAAW,UAAU,IAAI,CAAC,OAAO;AAAA,MAC/B,sBAAsB,EAAE;AAAA,MACxB,iBAAiB,EAAE;AAAA,MACnB,WAAW,EAAE;AAAA,MACb,gBAAgB,EAAE;AAAA,MAClB,IAAI,EAAE;AAAA,MACN,cAAc,EAAE;AAAA,MAChB,UAAU,EAAE;AAAA,MACZ,aAAa,EAAE;AAAA,MACf,SAAS,EAAE;AAAA,MACX,SAAS,EAAE;AAAA,MACX,UAAU,EAAE;AAAA,MACZ,mBAAmB,EAAE;AAAA,MACrB,YAAY,EAAE;AAAA,IAChB,EAAE;AAAA,IACF,SAAS;AAAA,EACX;AAGA,SAAO,KAAK,UAAU,eAAe,MAAM,CAAC;AAC9C;;;AC/MA,IAAM,WAAW;AAAA,EACf,OAAO;AAAA,EACP,cAAc;AAAA,EACd,aAAa;AAAA,EACb,aAAa;AAAA,EACb,YAAY;AACd;AAKA,IAAM,eAAe;AAed,IAAM,QAAN,MAAY;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACT;AAAA,EACA;AAAA,EAER,YAAY,UAAwB,CAAC,GAAG;AACtC,SAAK,OAAO,IAAI,UAAU,CAAC;AAC3B,SAAK,WAAW,CAAC;AACjB,SAAK,WAAW,QAAQ,iBAAiB;AACzC,SAAK,QAAQ,QAAQ,SAAS,SAAS;AACvC,SAAK,cAAc,QAAQ,eAAe,SAAS;AACnD,SAAK,cAAc,QAAQ,eAAe,SAAS;AACnD,SAAK,aAAa,QAAQ,cAAc,SAAS;AACjD,SAAK,aAAa,QAAQ;AAC1B,SAAK,YAAY;AACjB,SAAK,gBAAgB;AAAA,EACvB;AAAA;AAAA;AAAA;AAAA,EAKA,WAAW,MAAiC;AAC1C,UAAM,YAAY,KAAK;AAGvB,UAAM,UAAU,KAAK,KAAK;AAC1B,QAAI,QAAQ,WAAW,GAAG;AACxB,aAAO;AAAA,IACT;AAGA,UAAM,eAAe,KAAK,SAAS,WAAW,OAAO;AACrD,UAAM,SAAS,KAAK,SAAS,SAAS,YAAY;AAElD,QAAI,OAAO,WAAW,GAAG;AACvB,aAAO;AAAA,IACT;AAGA,UAAM,iBAAiB,KAAK,WAAW,MAAM;AAE7C,QAAI,mBAAmB,MAAM;AAE3B,qBAAe,OAAO,QAAQ,WAAW,OAAO;AAChD,qBAAe,YAAY,MAAM;AACjC,aAAO;AAAA,IACT;AAGA,QAAI,KAAK,SAAS,UAAU,KAAK,aAAa;AAC5C,aAAO;AAAA,IACT;AAEA,WAAO,KAAK,cAAc,QAAQ,WAAW,OAAO;AAAA,EACtD;AAAA;AAAA;AAAA;AAAA,EAKA,YAAY,OAAuB;AACjC,UAAM,QAAQ,MAAM;AAGpB,UAAM,iBAAiB,KAAK,IAAI,GAAG,KAAK,MAAM,QAAQ,GAAG,CAAC;AAG1D,QAAI,KAAK,cAAc,QAAQ,GAAG;AAChC,WAAK,WAAW;AAAA,QACd,gBAAgB;AAAA,QAChB,YAAY;AAAA,QACZ,cAAc;AAAA,QACd,iBAAiB;AAAA,MACnB,CAAC;AAAA,IACH;AAEA,aAAS,IAAI,GAAG,IAAI,OAAO,KAAK;AAC9B,WAAK,WAAW,MAAM,CAAC,CAAW;AAGlC,UAAI,KAAK,cAAc,IAAI,mBAAmB,GAAG;AAC/C,aAAK,WAAW;AAAA,UACd,gBAAgB,IAAI;AAAA,UACpB,YAAY;AAAA,UACZ,cAAc;AAAA,UACd,iBAAiB,KAAK,OAAQ,IAAI,KAAK,QAAS,GAAG;AAAA,QACrD,CAAC;AAAA,MACH;AAAA,IACF;AAGA,QAAI,KAAK,cAAc,QAAQ,GAAG;AAChC,WAAK,WAAW;AAAA,QACd,gBAAgB;AAAA,QAChB,YAAY;AAAA,QACZ,cAAc;AAAA,QACd,iBAAiB;AAAA,MACnB,CAAC;AAAA,IACH;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASQ,WAAW,QAAqC;AACtD,UAAM,aAAa,OAAO;AAC1B,UAAM,gBAAgB,OAAO,UAAU;AAGvC,UAAM,aAAa,KAAK,KAAK,SAAS,aAAa;AACnD,QAAI,eAAe,QAAW;AAC5B,aAAO;AAAA,IACT;AAGA,UAAM,gBAAgB,KAAK,oBAAoB,YAAY,QAAQ,CAAC;AACpE,QAAI,kBAAkB,MAAM;AAC1B,aAAO;AAAA,IACT;AAIA,UAAM,aAAa,OAAO,CAAC;AAC3B,QACE,OAAO,SAAS,KAChB,eAAe,UACf,KAAK,kBAAkB,UAAU,GACjC;AACA,aAAO,KAAK,oBAAoB,YAAY,QAAQ,CAAC;AAAA,IACvD;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQQ,oBACN,YACA,QACA,YACmB;AAEnB,UAAM,WAAW,OAAO,UAAU;AAClC,QAAI,aAAa,QAAW;AAC1B,aAAO;AAAA,IACT;AAEA,QAAI,cAAc,WAAW,SAAS,QAAQ;AAK9C,QAAI,gBAAgB,UAAa,eAAe,GAAG;AACjD,oBAAc,WAAW,SAAS,YAAY;AAAA,IAChD;AAEA,QAAI,gBAAgB,QAAW;AAC7B,aAAO;AAAA,IACT;AAKA,QAAI,aAAwB;AAC5B,aAAS,IAAI,aAAa,GAAG,IAAI,KAAK,IAAI,OAAO,QAAQ,KAAK,KAAK,GAAG,KAAK;AACzE,YAAM,QAAQ,OAAO,CAAC;AACtB,UAAI,UAAU,QAAW;AACvB;AAAA,MACF;AAEA,UAAI,WAAW,WAAW,SAAS,KAAK;AAGxC,UAAI,aAAa,QAAW;AAC1B,mBAAW,WAAW,SAAS,YAAY;AAAA,MAC7C;AAEA,UAAI,aAAa,QAAW;AAC1B;AAAA,MACF;AAEA,mBAAa;AAAA,IACf;AAGA,WAAO,KAAK,cAAc,YAAY,MAAM;AAAA,EAC9C;AAAA;AAAA;AAAA;AAAA,EAKQ,cAAc,MAAiB,QAAqC;AAC1E,QAAI,cAAiC;AACrC,QAAI,iBAAiB;AAErB,UAAM,YAAY,KAAK,SAAS,gBAAgB,KAAK,KAAK;AAE1D,eAAW,WAAW,KAAK,UAAU;AACnC,YAAM,aAAa,QAAQ,kBAAkB,MAAM;AAEnD,UAAI,cAAc,aAAa,aAAa,gBAAgB;AAC1D,yBAAiB;AACjB,sBAAc;AAAA,MAChB;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKQ,cAAc,QAAkB,WAAmB,eAAuB,IAAgB;AAChG,UAAM,YAAY,IAAI,OAAO,KAAK,eAAe,EAAE,SAAS,GAAG,GAAG,CAAC;AACnE,UAAM,UAAU,IAAI,WAAW,WAAW,QAAQ,WAAW,KAAK,YAAY,YAAY;AAG1F,UAAM,aAAa,OAAO;AAC1B,UAAM,gBAAgB,OAAO,UAAU;AAGvC,UAAM,aAAa,KAAK,KAAK,iBAAiB,aAAa;AAG3D,UAAM,aAAa,OAAO,CAAC;AAC3B,QAAI,eAAe,QAAW;AAC5B,WAAK,SAAS,KAAK,OAAO;AAC1B,aAAO;AAAA,IACT;AAGA,UAAM,WAAW,KAAK,kBAAkB,YAAY,UAAU,IAC1D,eACA;AAEJ,QAAI,cAAc,WAAW,iBAAiB,QAAQ;AAGtD,aAAS,IAAI,GAAG,IAAI,KAAK,IAAI,OAAO,QAAQ,KAAK,KAAK,GAAG,KAAK;AAC5D,YAAM,QAAQ,OAAO,CAAC;AACtB,UAAI,UAAU,QAAW;AACvB;AAAA,MACF;AAEA,YAAM,MAAM,KAAK,kBAAkB,aAAa,KAAK,IACjD,eACA;AAEJ,oBAAc,YAAY,iBAAiB,GAAG;AAAA,IAChD;AAGA,gBAAY,WAAW,OAAO;AAC9B,SAAK,SAAS,KAAK,OAAO;AAE1B,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,kBAAkB,MAAiB,OAAwB;AAEjE,QAAI,KAAK,SAAS,KAAK,GAAG;AACxB,aAAO;AAAA,IACT;AAGA,QAAI,KAAK,SAAS,YAAY,KAAK,KAAK,cAAc,KAAK,aAAa;AACtE,aAAO;AAAA,IACT;AAGA,QAAI,KAAK,kBAAkB,KAAK,GAAG;AACjC,aAAO;AAAA,IACT;AAGA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKQ,kBAAkB,OAAwB;AAEhD,QAAI,UAAU,UAAU;AACtB,aAAO;AAAA,IACT;AAGA,UAAM,YAAY,MAAM,OAAO,CAAC;AAChC,QAAI,aAAa,OAAO,aAAa,KAAK;AACxC,aAAO;AAAA,IACT;AAGA,QAAI,iBAAiB,KAAK,KAAK,KAAK,MAAM,SAAS,GAAG;AACpD,aAAO;AAAA,IACT;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,eAA2B;AACzB,WAAO,KAAK,SAAS,IAAI,CAAC,aAAa;AAAA,MACrC,IAAI,QAAQ;AAAA,MACZ,SAAS,QAAQ,WAAW;AAAA,MAC5B,aAAa,QAAQ;AAAA,MACrB,iBAAiB,QAAQ;AAAA,MACzB,WAAW,QAAQ;AAAA,MACnB,UAAU,QAAQ;AAAA,MAClB,UAAU,QAAQ;AAAA,MAClB,YAAY,QAAQ;AAAA,MACpB,gBAAgB,QAAQ;AAAA,MACxB,mBAAmB,QAAQ;AAAA,MAC3B,sBAAsB,QAAQ;AAAA,MAC9B,iBAAiB,QAAQ;AAAA,MACzB,cAAc,QAAQ;AAAA,IACxB,EAAE;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA,EAKA,UAAU,SAAuB,WAAW,eAAuB,IAAuB;AACxF,UAAM,YAAY,KAAK,aAAa;AAGpC,cAAU,KAAK,CAAC,GAAG,MAAM,EAAE,cAAc,EAAE,WAAW;AAGtD,UAAM,mBAAmB,UAAU,MAAM,GAAG,YAAY;AAGxD,UAAM,QAAQ,KAAK,eAAe,SAAS;AAG3C,QAAI;AACJ,YAAQ,QAAQ;AAAA,MACd,KAAK;AACH,oBAAY,eAAe,kBAAkB,KAAK;AAClD;AAAA,MACF,KAAK;AACH,oBAAY,WAAW,kBAAkB,KAAK;AAC9C;AAAA,MACF,KAAK;AACH,oBAAY,iBAAiB,kBAAkB,KAAK;AACpD;AAAA,MACF,KAAK;AAAA,MACL;AACE,oBAAY,cAAc,kBAAkB,KAAK;AACjD;AAAA,IACJ;AAEA,WAAO;AAAA,MACL,WAAW;AAAA,MACX;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKQ,eAAe,WAAmD;AACxE,UAAM,aAAa,KAAK;AACxB,UAAM,kBAAkB,UAAU;AAGlC,UAAM,mBAAmB,aAAa,IAClC,IAAK,kBAAkB,aACvB;AAIJ,QAAI,gBAAgB;AACpB,QAAI,kBAAkB;AAEtB,eAAW,YAAY,WAAW;AAChC,YAAM,gBAAgB,SAAS,QAAQ;AAEvC,uBAAiB,gBAAgB,SAAS;AAE1C,yBAAmB,gBAAgB;AAAA,IACrC;AAEA,UAAM,0BAA0B,gBAAgB,IAC5C,IAAK,kBAAkB,gBACvB;AAEJ,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA,kBAAkB,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,gBAAgB,CAAC;AAAA,MAC3D,yBAAyB,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,uBAAuB,CAAC;AAAA,IAC3E;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,aAAqB;AACvB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,gBAAwB;AAC1B,WAAO,KAAK,SAAS;AAAA,EACvB;AACF;AAKO,SAAS,YAAY,SAA+B;AACzD,SAAO,IAAI,MAAM,OAAO;AAC1B;;;AC7bO,SAAS,SACd,OACA,UAA2B,CAAC,GACT;AACnB,QAAM,EAAE,SAAS,WAAW,eAAe,IAAI,OAAO,aAAa,IAAI;AAEvE,QAAM,YAAY,YAAY,IAAI;AAElC,QAAM,QAAQ,YAAY,YAAY;AACtC,QAAM,YAAY,KAAK;AAEvB,QAAM,SAAS,MAAM,UAAU,QAAQ,YAAY;AACnD,QAAM,mBAAmB,KAAK,MAAM,YAAY,IAAI,IAAI,SAAS;AAGjE,SAAO;AAAA,IACL,GAAG;AAAA,IACH,OAAO;AAAA,MACL,GAAG,OAAO;AAAA,MACV;AAAA,IACF;AAAA,EACF;AACF;AASO,SAAS,aACd,MACA,UAA2B,CAAC,GACT;AACnB,QAAM,QAAQ,KAAK,MAAM,OAAO;AAChC,SAAO,SAAS,OAAO,OAAO;AAChC;","names":[]}
package/dist/cli.cjs CHANGED
@@ -4,8 +4,6 @@
4
4
  // src/cli.ts
5
5
  var import_node_util = require("util");
6
6
  var import_node_fs = require("fs");
7
- var import_node_url = require("url");
8
- var import_node_path = require("path");
9
7
 
10
8
  // src/preprocessing/patterns.ts
11
9
  var SEVERITY_PATTERNS = {
@@ -114,15 +112,18 @@ function extractDurations(line) {
114
112
  return durations.slice(0, 5);
115
113
  }
116
114
  var DEFAULT_PATTERNS = {
117
- // Timestamps (most specific - must run before port to avoid fragmentation)
115
+ // Timestamps (must run before port to avoid fragmentation)
118
116
  isoTimestamp: /\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:[.,]\d+)?(?:Z|[+-]\d{2}:?\d{2})?/g,
117
+ // UUID must run before unixTimestamp to prevent partial matching of UUID segments
118
+ uuid: /\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b/g,
119
119
  unixTimestamp: /\b\d{10,13}\b/g,
120
120
  // Network addresses
121
121
  ipv4: /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g,
122
- ipv6: /\b[0-9a-fA-F:]{7,39}\b/g,
122
+ // IPv6: matches full, compressed (::1, ::), and partial forms
123
+ // Order matters: longer matches must come before shorter ones in alternation
124
+ ipv6: /(?:(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?::[0-9a-fA-F]{1,4}){1,6}|:(?::[0-9a-fA-F]{1,4}){1,7}|(?:[0-9a-fA-F]{1,4}:){1,7}:|::)/g,
123
125
  port: /:\d{2,5}\b/g,
124
126
  // Identifiers
125
- uuid: /\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b/g,
126
127
  hexId: /\b0x[0-9a-fA-F]+\b/g,
127
128
  blockId: /\bblk_-?\d+\b/g,
128
129
  // Paths and URLs
@@ -410,6 +411,20 @@ var LogCluster = class {
410
411
  };
411
412
 
412
413
  // src/output/formatter.ts
414
+ function sortObjectKeys(value) {
415
+ if (value === null || typeof value !== "object") {
416
+ return value;
417
+ }
418
+ if (Array.isArray(value)) {
419
+ return value.map(sortObjectKeys);
420
+ }
421
+ const sorted = {};
422
+ const keys = Object.keys(value).sort();
423
+ for (const key of keys) {
424
+ sorted[key] = sortObjectKeys(value[key]);
425
+ }
426
+ return sorted;
427
+ }
413
428
  function formatSummary(templates, stats) {
414
429
  const lines = [];
415
430
  lines.push("=== Log Compression Summary ===");
@@ -516,6 +531,33 @@ function formatJson(templates, stats) {
516
531
  };
517
532
  return JSON.stringify(output, null, 2);
518
533
  }
534
+ function formatJsonStable(templates, stats) {
535
+ const output = {
536
+ stats: {
537
+ compressionRatio: Math.round(stats.compressionRatio * 1e3) / 1e3,
538
+ estimatedTokenReduction: Math.round(stats.estimatedTokenReduction * 1e3) / 1e3,
539
+ inputLines: stats.inputLines,
540
+ uniqueTemplates: stats.uniqueTemplates
541
+ },
542
+ templates: templates.map((t) => ({
543
+ correlationIdSamples: t.correlationIdSamples,
544
+ durationSamples: t.durationSamples,
545
+ firstSeen: t.firstSeen,
546
+ fullUrlSamples: t.fullUrlSamples,
547
+ id: t.id,
548
+ isStackFrame: t.isStackFrame,
549
+ lastSeen: t.lastSeen,
550
+ occurrences: t.occurrences,
551
+ pattern: t.pattern,
552
+ samples: t.sampleVariables,
553
+ severity: t.severity,
554
+ statusCodeSamples: t.statusCodeSamples,
555
+ urlSamples: t.urlSamples
556
+ })),
557
+ version: "1.1"
558
+ };
559
+ return JSON.stringify(sortObjectKeys(output));
560
+ }
519
561
 
520
562
  // src/drain/drain.ts
521
563
  var DEFAULTS = {
@@ -580,6 +622,14 @@ var Drain = class {
580
622
  addLogLines(lines) {
581
623
  const total = lines.length;
582
624
  const reportInterval = Math.max(1, Math.floor(total / 100));
625
+ if (this.onProgress && total > 0) {
626
+ this.onProgress({
627
+ processedLines: 0,
628
+ totalLines: total,
629
+ currentPhase: "parsing",
630
+ percentComplete: 0
631
+ });
632
+ }
583
633
  for (let i = 0; i < total; i++) {
584
634
  this.addLogLine(lines[i]);
585
635
  if (this.onProgress && i % reportInterval === 0) {
@@ -602,6 +652,10 @@ var Drain = class {
602
652
  }
603
653
  /**
604
654
  * Search the parse tree for a matching cluster.
655
+ *
656
+ * Uses XDrain-inspired token-position fallback: if the first token
657
+ * looks like a variable (timestamp, PID, etc.), tries using the
658
+ * second token as the navigation key for better matching.
605
659
  */
606
660
  treeSearch(tokens) {
607
661
  const tokenCount = tokens.length;
@@ -610,19 +664,36 @@ var Drain = class {
610
664
  if (lengthNode === void 0) {
611
665
  return null;
612
666
  }
667
+ const primaryResult = this.treeSearchFromToken(lengthNode, tokens, 0);
668
+ if (primaryResult !== null) {
669
+ return primaryResult;
670
+ }
613
671
  const firstToken = tokens[0];
614
- if (firstToken === void 0) {
672
+ if (tokens.length > 1 && firstToken !== void 0 && this.looksLikeVariable(firstToken)) {
673
+ return this.treeSearchFromToken(lengthNode, tokens, 1);
674
+ }
675
+ return null;
676
+ }
677
+ /**
678
+ * Search from a specific token position in the tree.
679
+ * @param lengthNode - The node at level 1 (token count)
680
+ * @param tokens - All tokens in the log line
681
+ * @param startIndex - Which token to use as "first token" for navigation
682
+ */
683
+ treeSearchFromToken(lengthNode, tokens, startIndex) {
684
+ const navToken = tokens[startIndex];
685
+ if (navToken === void 0) {
615
686
  return null;
616
687
  }
617
- let currentNode = lengthNode.getChild(firstToken);
618
- if (currentNode === void 0) {
688
+ let currentNode = lengthNode.getChild(navToken);
689
+ if (currentNode === void 0 && startIndex === 0) {
619
690
  currentNode = lengthNode.getChild(WILDCARD_KEY);
620
691
  }
621
692
  if (currentNode === void 0) {
622
693
  return null;
623
694
  }
624
695
  let searchNode = currentNode;
625
- for (let i = 1; i < Math.min(tokens.length, this.depth); i++) {
696
+ for (let i = startIndex + 1; i < Math.min(tokens.length, this.depth); i++) {
626
697
  const token = tokens[i];
627
698
  if (token === void 0) {
628
699
  break;
@@ -750,6 +821,9 @@ var Drain = class {
750
821
  case "json":
751
822
  formatted = formatJson(limitedTemplates, stats);
752
823
  break;
824
+ case "json-stable":
825
+ formatted = formatJsonStable(limitedTemplates, stats);
826
+ break;
753
827
  case "summary":
754
828
  default:
755
829
  formatted = formatSummary(limitedTemplates, stats);
@@ -822,10 +896,7 @@ function compressText(text, options = {}) {
822
896
  }
823
897
 
824
898
  // src/cli.ts
825
- var import_meta = {};
826
- var __dirname = (0, import_node_path.dirname)((0, import_node_url.fileURLToPath)(import_meta.url));
827
- var pkg = JSON.parse((0, import_node_fs.readFileSync)((0, import_node_path.join)(__dirname, "..", "package.json"), "utf-8"));
828
- var VERSION = pkg.version;
899
+ var VERSION = true ? "0.1.0" : "0.0.0";
829
900
  var HELP = `
830
901
  logpare - Semantic log compression for LLM context windows
831
902
 
@@ -834,7 +905,7 @@ USAGE:
834
905
  cat logs.txt | logpare [options]
835
906
 
836
907
  OPTIONS:
837
- -f, --format <fmt> Output format: summary, detailed, json (default: summary)
908
+ -f, --format <fmt> Output format: summary, detailed, json, json-stable (default: summary)
838
909
  -o, --output <file> Write output to file instead of stdout
839
910
  -d, --depth <n> Parse tree depth (default: 4)
840
911
  -t, --threshold <n> Similarity threshold 0.0-1.0 (default: 0.4)
@@ -873,9 +944,9 @@ function parseCliArgs() {
873
944
  allowPositionals: true
874
945
  });
875
946
  const format = values.format;
876
- if (!["summary", "detailed", "json"].includes(format)) {
947
+ if (!["summary", "detailed", "json", "json-stable"].includes(format)) {
877
948
  console.error(
878
- `Error: Invalid format "${format}". Use: summary, detailed, json`
949
+ `Error: Invalid format "${format}". Use: summary, detailed, json, json-stable`
879
950
  );
880
951
  process.exit(1);
881
952
  }
@@ -974,11 +1045,7 @@ function main() {
974
1045
  }
975
1046
  };
976
1047
  const result = compressText(input, options);
977
- const output = args.format === "json" ? JSON.stringify(
978
- { templates: result.templates, stats: result.stats },
979
- null,
980
- 2
981
- ) : result.formatted;
1048
+ const output = result.formatted;
982
1049
  if (args.output) {
983
1050
  (0, import_node_fs.writeFileSync)(args.output, output, "utf-8");
984
1051
  console.error(`Output written to ${args.output}`);