@a-company/paradigm 3.0.2 → 3.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.
@@ -1,17 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  SentinelStorage
4
- } from "./chunk-S65LENNL.js";
5
- import "./chunk-MO4EEYFW.js";
6
-
7
- // src/commands/triage/index.ts
8
- import chalk2 from "chalk";
9
- import ora from "ora";
4
+ } from "./chunk-VZ7CXFRZ.js";
10
5
 
11
6
  // ../sentinel/dist/index.js
12
7
  import * as path from "path";
13
8
  import * as fs from "fs";
14
9
  import { fileURLToPath } from "url";
10
+ import * as fs2 from "fs";
11
+ import * as path2 from "path";
12
+ import * as fs3 from "fs";
13
+ import * as path3 from "path";
14
+ import * as fs4 from "fs";
15
+ import * as path4 from "path";
15
16
  import * as fs5 from "fs";
16
17
  var DEFAULT_CONFIG = {
17
18
  minScore: 30,
@@ -19,8 +20,8 @@ var DEFAULT_CONFIG = {
19
20
  boostConfidence: true
20
21
  };
21
22
  var PatternMatcher = class {
22
- constructor(storage2) {
23
- this.storage = storage2;
23
+ constructor(storage) {
24
+ this.storage = storage;
24
25
  }
25
26
  /**
26
27
  * Match an incident against all patterns and return ranked results
@@ -253,6 +254,831 @@ function loadAllSeedPatterns() {
253
254
  patterns: [...universal.patterns, ...paradigm.patterns]
254
255
  };
255
256
  }
257
+ function ensurePrefix(id, prefix) {
258
+ return id.startsWith(prefix) ? id : `${prefix}${id}`;
259
+ }
260
+ var FlowTracker = class {
261
+ flowId;
262
+ sentinel;
263
+ actual = [];
264
+ expected = [];
265
+ completed = false;
266
+ constructor(flowId, sentinel) {
267
+ this.flowId = ensurePrefix(flowId, "$");
268
+ this.sentinel = sentinel;
269
+ }
270
+ /** Declare which signals/gates are expected in this flow */
271
+ expect(...symbols) {
272
+ this.expected.push(...symbols);
273
+ return this;
274
+ }
275
+ /** Record a generic step in the flow */
276
+ step(symbol) {
277
+ this.actual.push(symbol);
278
+ return this;
279
+ }
280
+ /** Record a gate check result */
281
+ gate(id, passed) {
282
+ const gateId = ensurePrefix(id, "^");
283
+ this.actual.push(gateId);
284
+ if (!passed) {
285
+ this.fail(new Error(`Gate ${gateId} failed`));
286
+ }
287
+ return this;
288
+ }
289
+ /** Record a signal emission */
290
+ signal(id, _data) {
291
+ this.actual.push(ensurePrefix(id, "!"));
292
+ return this;
293
+ }
294
+ /** Mark the flow as successfully completed */
295
+ complete() {
296
+ this.completed = true;
297
+ }
298
+ /** Capture an error with full flow position context */
299
+ fail(error) {
300
+ if (this.completed) return;
301
+ this.completed = true;
302
+ const missing = this.expected.filter((s) => !this.actual.includes(s));
303
+ const failedAt = this.actual.length > 0 ? this.actual[this.actual.length - 1] : void 0;
304
+ const flowPosition = {
305
+ flowId: this.flowId,
306
+ expected: this.expected,
307
+ actual: this.actual,
308
+ missing,
309
+ failedAt
310
+ };
311
+ this.sentinel.capture(error, { flow: this.flowId }, flowPosition);
312
+ }
313
+ };
314
+ var Sentinel = class {
315
+ storage;
316
+ matcher;
317
+ config;
318
+ ready = false;
319
+ readyPromise = null;
320
+ seeded = false;
321
+ constructor(config) {
322
+ this.config = config;
323
+ this.storage = new SentinelStorage(config.dbPath);
324
+ this.matcher = new PatternMatcher(this.storage);
325
+ }
326
+ /** Explicitly initialize storage. Optional — auto-called on first capture. */
327
+ async init() {
328
+ if (this.ready) return;
329
+ if (this.readyPromise) return this.readyPromise;
330
+ this.readyPromise = this.doInit();
331
+ return this.readyPromise;
332
+ }
333
+ async doInit() {
334
+ await this.storage.ensureReady();
335
+ if (!this.seeded) {
336
+ try {
337
+ const { patterns } = loadAllSeedPatterns();
338
+ for (const pattern of patterns) {
339
+ try {
340
+ this.storage.addPattern(pattern);
341
+ } catch {
342
+ }
343
+ }
344
+ } catch {
345
+ }
346
+ this.seeded = true;
347
+ }
348
+ this.ready = true;
349
+ }
350
+ ensureReady() {
351
+ if (!this.ready) {
352
+ if (!this.readyPromise) {
353
+ this.readyPromise = this.doInit();
354
+ }
355
+ }
356
+ }
357
+ // ── Symbol Context ──────────────────────────────────────────────
358
+ /**
359
+ * Create a component context for scoped error capture.
360
+ *
361
+ * @param id - Component symbol (e.g. '#checkout' or 'checkout')
362
+ * @returns ComponentContext with capture() and wrap() methods
363
+ */
364
+ component(id) {
365
+ const componentId = ensurePrefix(id, "#");
366
+ const self = this;
367
+ return {
368
+ id: componentId,
369
+ capture(error, extra) {
370
+ return self.capture(error, { component: componentId, ...extra });
371
+ },
372
+ wrap(fn) {
373
+ const wrapped = ((...args) => {
374
+ try {
375
+ const result = fn(...args);
376
+ if (result && typeof result.catch === "function") {
377
+ return result.catch((err) => {
378
+ self.capture(err, { component: componentId });
379
+ throw err;
380
+ });
381
+ }
382
+ return result;
383
+ } catch (err) {
384
+ if (err instanceof Error) {
385
+ self.capture(err, { component: componentId });
386
+ }
387
+ throw err;
388
+ }
389
+ });
390
+ return wrapped;
391
+ }
392
+ };
393
+ }
394
+ /**
395
+ * Record a gate check result.
396
+ * If the gate fails, auto-captures an incident.
397
+ *
398
+ * @param id - Gate symbol (e.g. '^authenticated' or 'authenticated')
399
+ * @param passed - Whether the gate passed
400
+ */
401
+ gate(id, passed) {
402
+ if (!passed) {
403
+ const gateId = ensurePrefix(id, "^");
404
+ this.capture(new Error(`Gate ${gateId} failed`), { gate: gateId });
405
+ }
406
+ }
407
+ /**
408
+ * Record a signal emission. Primarily for flow tracking context.
409
+ *
410
+ * @param id - Signal symbol (e.g. '!payment-authorized' or 'payment-authorized')
411
+ */
412
+ signal(id, _data) {
413
+ void ensurePrefix(id, "!");
414
+ }
415
+ // ── Flow Tracking ───────────────────────────────────────────────
416
+ /**
417
+ * Create a flow tracker for monitoring multi-step operations.
418
+ *
419
+ * @param id - Flow symbol (e.g. '$checkout-flow' or 'checkout-flow')
420
+ * @returns FlowTracker instance
421
+ */
422
+ flow(id) {
423
+ return new FlowTracker(id, this);
424
+ }
425
+ // ── Error Capture ───────────────────────────────────────────────
426
+ /**
427
+ * Capture an error with symbolic context.
428
+ *
429
+ * @param error - The error to capture
430
+ * @param context - Symbolic context (component, gate, flow, signal)
431
+ * @param flowPosition - Optional flow position data
432
+ * @returns Incident ID (e.g. 'INC-001')
433
+ */
434
+ capture(error, context, flowPosition) {
435
+ this.ensureReady();
436
+ const input = {
437
+ error: {
438
+ message: error.message,
439
+ stack: error.stack,
440
+ type: error.constructor.name !== "Error" ? error.constructor.name : void 0
441
+ },
442
+ symbols: context || {},
443
+ environment: this.config.environment || "development",
444
+ service: this.config.service,
445
+ version: this.config.version,
446
+ flowPosition
447
+ };
448
+ const incidentId = this.storage.recordIncident(input);
449
+ const incident = this.storage.getIncident(incidentId);
450
+ if (incident && this.config.onCapture) {
451
+ this.config.onCapture(incident);
452
+ }
453
+ return incidentId;
454
+ }
455
+ /**
456
+ * Get pattern matches for a captured incident.
457
+ *
458
+ * @param incidentId - The incident ID to match
459
+ * @returns Array of pattern matches sorted by confidence
460
+ */
461
+ match(incidentId) {
462
+ const incident = this.storage.getIncident(incidentId);
463
+ if (!incident) return [];
464
+ return this.matcher.match(incident);
465
+ }
466
+ // ── Framework Integration ───────────────────────────────────────
467
+ /**
468
+ * Create Express error-handling middleware.
469
+ *
470
+ * Usage:
471
+ * app.use(sentinel.express());
472
+ */
473
+ express() {
474
+ const self = this;
475
+ return (err, req, res, next) => {
476
+ const context = {};
477
+ const routeParts = (req.path || req.url || "").split("/").filter(Boolean);
478
+ if (routeParts.length >= 2) {
479
+ context.component = `#${routeParts[1]}`;
480
+ }
481
+ const incidentId = self.capture(err, context);
482
+ if (res.setHeader) {
483
+ res.setHeader("X-Sentinel-Incident", incidentId);
484
+ }
485
+ next(err);
486
+ };
487
+ }
488
+ // ── Lifecycle ───────────────────────────────────────────────────
489
+ /** Close the database connection. Call when shutting down. */
490
+ close() {
491
+ this.storage.close();
492
+ this.ready = false;
493
+ this.readyPromise = null;
494
+ }
495
+ /** Get the underlying storage instance (for advanced usage). */
496
+ getStorage() {
497
+ return this.storage;
498
+ }
499
+ /** Get the underlying pattern matcher (for advanced usage). */
500
+ getMatcher() {
501
+ return this.matcher;
502
+ }
503
+ };
504
+ var CONFIG_FILES = [".sentinel.yaml", ".sentinel.yml"];
505
+ function loadConfig(projectDir) {
506
+ for (const filename of CONFIG_FILES) {
507
+ const filePath = path2.join(projectDir, filename);
508
+ if (fs2.existsSync(filePath)) {
509
+ const content = fs2.readFileSync(filePath, "utf-8");
510
+ return parseSimpleYaml(content);
511
+ }
512
+ }
513
+ return null;
514
+ }
515
+ function writeConfig(projectDir, config) {
516
+ const filePath = path2.join(projectDir, ".sentinel.yaml");
517
+ const content = serializeSimpleYaml(config);
518
+ fs2.writeFileSync(filePath, content, "utf-8");
519
+ }
520
+ function parseSimpleYaml(content) {
521
+ const config = { version: "1.0", project: "" };
522
+ const lines = content.split("\n");
523
+ let currentSection = null;
524
+ let currentSubSection = null;
525
+ for (const line of lines) {
526
+ const trimmed = line.trimEnd();
527
+ if (!trimmed || trimmed.startsWith("#")) continue;
528
+ const topMatch = trimmed.match(/^(\w+):\s*(.+)$/);
529
+ if (topMatch) {
530
+ const [, key, value] = topMatch;
531
+ if (key === "version") config.version = value.replace(/['"]/g, "");
532
+ else if (key === "project") config.project = value.replace(/['"]/g, "");
533
+ else if (key === "environment") config.environment = value.replace(/['"]/g, "");
534
+ currentSection = null;
535
+ currentSubSection = null;
536
+ continue;
537
+ }
538
+ const sectionMatch = trimmed.match(/^(\w+):$/);
539
+ if (sectionMatch) {
540
+ currentSection = sectionMatch[1];
541
+ currentSubSection = null;
542
+ if (currentSection === "symbols" && !config.symbols) {
543
+ config.symbols = {};
544
+ }
545
+ if (currentSection === "routes" && !config.routes) {
546
+ config.routes = {};
547
+ }
548
+ if (currentSection === "scrub" && !config.scrub) {
549
+ config.scrub = {};
550
+ }
551
+ continue;
552
+ }
553
+ const subMatch = trimmed.match(/^\s{2}(\w+):$/);
554
+ if (subMatch && currentSection) {
555
+ currentSubSection = subMatch[1];
556
+ if (currentSection === "symbols" && config.symbols) {
557
+ config.symbols[currentSubSection] = [];
558
+ }
559
+ if (currentSection === "scrub" && config.scrub) {
560
+ config.scrub[currentSubSection] = [];
561
+ }
562
+ continue;
563
+ }
564
+ const listMatch = trimmed.match(/^\s+-\s+(.+)$/);
565
+ if (listMatch && currentSection && currentSubSection) {
566
+ const value = listMatch[1].replace(/['"]/g, "");
567
+ if (currentSection === "symbols" && config.symbols) {
568
+ const arr = config.symbols[currentSubSection];
569
+ if (Array.isArray(arr)) arr.push(value);
570
+ }
571
+ if (currentSection === "scrub" && config.scrub) {
572
+ const arr = config.scrub[currentSubSection];
573
+ if (Array.isArray(arr)) arr.push(value);
574
+ }
575
+ continue;
576
+ }
577
+ const routeMatch = trimmed.match(/^\s+(['"]?\/[^'"]+['"]?):\s+['"]?([^'"]+)['"]?$/);
578
+ if (routeMatch && currentSection === "routes" && config.routes) {
579
+ const route = routeMatch[1].replace(/['"]/g, "");
580
+ config.routes[route] = routeMatch[2];
581
+ continue;
582
+ }
583
+ }
584
+ return config;
585
+ }
586
+ function serializeSimpleYaml(config) {
587
+ const lines = [];
588
+ lines.push(`# Sentinel Configuration`);
589
+ lines.push(`# Auto-generated \u2014 edit freely`);
590
+ lines.push("");
591
+ lines.push(`version: "${config.version}"`);
592
+ lines.push(`project: "${config.project}"`);
593
+ if (config.environment) {
594
+ lines.push(`environment: "${config.environment}"`);
595
+ }
596
+ if (config.symbols) {
597
+ lines.push("");
598
+ lines.push("symbols:");
599
+ for (const [key, values] of Object.entries(config.symbols)) {
600
+ if (values && values.length > 0) {
601
+ lines.push(` ${key}:`);
602
+ for (const v of values) {
603
+ lines.push(` - ${v}`);
604
+ }
605
+ }
606
+ }
607
+ }
608
+ if (config.routes && Object.keys(config.routes).length > 0) {
609
+ lines.push("");
610
+ lines.push("routes:");
611
+ for (const [route, symbol] of Object.entries(config.routes)) {
612
+ lines.push(` "${route}": ${symbol}`);
613
+ }
614
+ }
615
+ if (config.scrub) {
616
+ lines.push("");
617
+ lines.push("scrub:");
618
+ if (config.scrub.headers?.length) {
619
+ lines.push(" headers:");
620
+ for (const h of config.scrub.headers) {
621
+ lines.push(` - ${h}`);
622
+ }
623
+ }
624
+ if (config.scrub.fields?.length) {
625
+ lines.push(" fields:");
626
+ for (const f of config.scrub.fields) {
627
+ lines.push(` - ${f}`);
628
+ }
629
+ }
630
+ }
631
+ lines.push("");
632
+ return lines.join("\n");
633
+ }
634
+ var DIR_PATTERNS = [
635
+ { dirs: ["services", "src/services"], prefix: "#", type: "components" },
636
+ { dirs: ["routes", "src/routes", "api", "src/api"], prefix: "#", type: "components" },
637
+ { dirs: ["handlers", "src/handlers"], prefix: "#", type: "components" },
638
+ { dirs: ["controllers", "src/controllers"], prefix: "#", type: "components" },
639
+ { dirs: ["components", "src/components"], prefix: "#", type: "components" },
640
+ { dirs: ["lib", "src/lib"], prefix: "#", type: "components" },
641
+ { dirs: ["middleware", "src/middleware"], prefix: "^", type: "gates" },
642
+ { dirs: ["guards", "src/guards"], prefix: "^", type: "gates" },
643
+ { dirs: ["auth", "src/auth"], prefix: "^", type: "gates" },
644
+ { dirs: ["events", "src/events"], prefix: "!", type: "signals" },
645
+ { dirs: ["listeners", "src/listeners"], prefix: "!", type: "signals" },
646
+ { dirs: ["flows", "src/flows"], prefix: "$", type: "flows" },
647
+ { dirs: ["workflows", "src/workflows"], prefix: "$", type: "flows" },
648
+ { dirs: ["pipelines", "src/pipelines"], prefix: "$", type: "flows" }
649
+ ];
650
+ var CODE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".js", ".tsx", ".jsx", ".mjs", ".mts"]);
651
+ function detectSymbols(projectDir) {
652
+ const result = {
653
+ components: [],
654
+ gates: [],
655
+ flows: [],
656
+ signals: [],
657
+ routes: {}
658
+ };
659
+ const purposeSymbols = readPurposeFiles(projectDir);
660
+ if (purposeSymbols) {
661
+ result.components.push(...purposeSymbols.components);
662
+ result.gates.push(...purposeSymbols.gates);
663
+ result.flows.push(...purposeSymbols.flows);
664
+ result.signals.push(...purposeSymbols.signals);
665
+ }
666
+ for (const pattern of DIR_PATTERNS) {
667
+ for (const dir of pattern.dirs) {
668
+ const fullPath = path3.join(projectDir, dir);
669
+ if (!fs3.existsSync(fullPath)) continue;
670
+ const files = safeReaddir(fullPath);
671
+ for (const file of files) {
672
+ const ext = path3.extname(file);
673
+ if (!CODE_EXTENSIONS.has(ext)) continue;
674
+ const name = path3.basename(file, ext);
675
+ if (name === "index" || name.endsWith(".test") || name.endsWith(".spec")) continue;
676
+ const symbol = `${pattern.prefix}${toKebabCase(name)}`;
677
+ if (!result[pattern.type].includes(symbol)) {
678
+ result[pattern.type].push(symbol);
679
+ }
680
+ }
681
+ }
682
+ }
683
+ scanRoutes(projectDir, result);
684
+ return result;
685
+ }
686
+ function generateConfig(projectDir) {
687
+ const detected = detectSymbols(projectDir);
688
+ return {
689
+ version: "1.0",
690
+ project: path3.basename(projectDir),
691
+ symbols: {
692
+ components: detected.components.length > 0 ? detected.components : void 0,
693
+ gates: detected.gates.length > 0 ? detected.gates : void 0,
694
+ flows: detected.flows.length > 0 ? detected.flows : void 0,
695
+ signals: detected.signals.length > 0 ? detected.signals : void 0
696
+ },
697
+ routes: Object.keys(detected.routes).length > 0 ? detected.routes : void 0
698
+ };
699
+ }
700
+ function readPurposeFiles(projectDir) {
701
+ const paradigmDir = path3.join(projectDir, ".paradigm");
702
+ if (!fs3.existsSync(paradigmDir)) return null;
703
+ const result = {
704
+ components: [],
705
+ gates: [],
706
+ flows: [],
707
+ signals: [],
708
+ routes: {}
709
+ };
710
+ const purposeFiles = findFiles(projectDir, ".purpose");
711
+ for (const file of purposeFiles) {
712
+ try {
713
+ const content = fs3.readFileSync(file, "utf-8");
714
+ extractPurposeSymbols(content, result);
715
+ } catch {
716
+ }
717
+ }
718
+ const hasAny = result.components.length > 0 || result.gates.length > 0 || result.flows.length > 0 || result.signals.length > 0;
719
+ return hasAny ? result : null;
720
+ }
721
+ function extractPurposeSymbols(content, result) {
722
+ const lines = content.split("\n");
723
+ let currentSection = "";
724
+ for (const line of lines) {
725
+ const trimmed = line.trim();
726
+ if (trimmed === "components:" || trimmed === "features:") {
727
+ currentSection = "components";
728
+ continue;
729
+ }
730
+ if (trimmed === "gates:") {
731
+ currentSection = "gates";
732
+ continue;
733
+ }
734
+ if (trimmed === "flows:") {
735
+ currentSection = "flows";
736
+ continue;
737
+ }
738
+ if (trimmed === "signals:") {
739
+ currentSection = "signals";
740
+ continue;
741
+ }
742
+ if (currentSection && /^\s{2}\S/.test(line)) {
743
+ const idMatch = trimmed.match(/^([a-zA-Z][\w-]*):$/);
744
+ if (idMatch) {
745
+ const prefixes = {
746
+ components: "#",
747
+ gates: "^",
748
+ flows: "$",
749
+ signals: "!"
750
+ };
751
+ const prefix = prefixes[currentSection] || "#";
752
+ const symbol = `${prefix}${idMatch[1]}`;
753
+ if (!result[currentSection]?.includes(symbol)) {
754
+ result[currentSection]?.push(symbol);
755
+ }
756
+ }
757
+ }
758
+ if (trimmed && !line.startsWith(" ") && !trimmed.endsWith(":")) {
759
+ currentSection = "";
760
+ }
761
+ }
762
+ }
763
+ function scanRoutes(projectDir, result) {
764
+ const routeDirs = ["routes", "src/routes", "api", "src/api"];
765
+ for (const dir of routeDirs) {
766
+ const fullPath = path3.join(projectDir, dir);
767
+ if (!fs3.existsSync(fullPath)) continue;
768
+ const files = safeReaddir(fullPath);
769
+ for (const file of files) {
770
+ const ext = path3.extname(file);
771
+ if (!CODE_EXTENSIONS.has(ext)) continue;
772
+ const name = path3.basename(file, ext);
773
+ if (name === "index") continue;
774
+ const routePrefix = `/api/${toKebabCase(name)}`;
775
+ const component = `#${toKebabCase(name)}`;
776
+ result.routes[routePrefix] = component;
777
+ }
778
+ }
779
+ }
780
+ function toKebabCase(str) {
781
+ return str.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[_\s]+/g, "-").replace(/\..*$/, "").toLowerCase();
782
+ }
783
+ function safeReaddir(dir) {
784
+ try {
785
+ return fs3.readdirSync(dir).filter((f) => {
786
+ const fullPath = path3.join(dir, f);
787
+ try {
788
+ return fs3.statSync(fullPath).isFile();
789
+ } catch {
790
+ return false;
791
+ }
792
+ });
793
+ } catch {
794
+ return [];
795
+ }
796
+ }
797
+ function findFiles(dir, filename, maxDepth = 4, depth = 0) {
798
+ if (depth > maxDepth) return [];
799
+ const results = [];
800
+ const skipDirs = /* @__PURE__ */ new Set(["node_modules", "dist", ".git", "coverage", ".next", ".nuxt"]);
801
+ try {
802
+ const entries = fs3.readdirSync(dir, { withFileTypes: true });
803
+ for (const entry of entries) {
804
+ if (entry.isFile() && entry.name === filename) {
805
+ results.push(path3.join(dir, entry.name));
806
+ } else if (entry.isDirectory() && !skipDirs.has(entry.name)) {
807
+ results.push(...findFiles(path3.join(dir, entry.name), filename, maxDepth, depth + 1));
808
+ }
809
+ }
810
+ } catch {
811
+ }
812
+ return results;
813
+ }
814
+ var SIMILARITY_THRESHOLD = 0.6;
815
+ var IncidentGrouper = class {
816
+ constructor(storage) {
817
+ this.storage = storage;
818
+ }
819
+ /**
820
+ * Try to find or create a group for an incident
821
+ * Returns the group ID if grouped, null if no suitable group
822
+ */
823
+ group(incident) {
824
+ const groups = this.storage.getGroups({ limit: 100 });
825
+ for (const group of groups) {
826
+ if (this.shouldJoinGroup(incident, group)) {
827
+ this.storage.addToGroup(group.id, incident.id);
828
+ return group.id;
829
+ }
830
+ }
831
+ const similar = this.findSimilar(incident, 10);
832
+ if (similar.length >= 1) {
833
+ const commonSymbols = this.extractCommonSymbols([incident, ...similar]);
834
+ const commonErrorPatterns = this.extractCommonErrorPatterns([
835
+ incident,
836
+ ...similar
837
+ ]);
838
+ const groupId = this.storage.createGroup({
839
+ incidents: [incident.id, ...similar.map((i) => i.id)],
840
+ commonSymbols,
841
+ commonErrorPatterns,
842
+ firstSeen: this.getEarliestTimestamp([incident, ...similar]),
843
+ lastSeen: incident.timestamp,
844
+ environments: this.getUniqueEnvironments([incident, ...similar])
845
+ });
846
+ return groupId;
847
+ }
848
+ return null;
849
+ }
850
+ /**
851
+ * Find incidents similar to the given one
852
+ */
853
+ findSimilar(incident, limit = 10) {
854
+ const candidates = this.storage.getRecentIncidents({
855
+ limit: 500,
856
+ status: "all"
857
+ });
858
+ const similar = [];
859
+ for (const candidate of candidates) {
860
+ if (candidate.id === incident.id) {
861
+ continue;
862
+ }
863
+ const score = this.calculateSimilarity(incident, candidate);
864
+ if (score >= SIMILARITY_THRESHOLD) {
865
+ similar.push({ incident: candidate, score });
866
+ }
867
+ }
868
+ return similar.sort((a, b) => b.score - a.score).slice(0, limit).map((s) => s.incident);
869
+ }
870
+ /**
871
+ * Analyze ungrouped incidents and create groups automatically
872
+ */
873
+ analyzeAndGroup(options = {}) {
874
+ const minSize = options.minSize || 3;
875
+ const ungrouped = this.storage.getRecentIncidents({
876
+ limit: 1e3
877
+ }).filter((i) => !i.groupId);
878
+ const newGroups = [];
879
+ const processed = /* @__PURE__ */ new Set();
880
+ for (const incident of ungrouped) {
881
+ if (processed.has(incident.id)) {
882
+ continue;
883
+ }
884
+ const similar = ungrouped.filter(
885
+ (other) => other.id !== incident.id && !processed.has(other.id) && this.calculateSimilarity(incident, other) >= SIMILARITY_THRESHOLD
886
+ );
887
+ if (similar.length + 1 >= minSize) {
888
+ const members = [incident, ...similar];
889
+ const commonSymbols = this.extractCommonSymbols(members);
890
+ const commonErrorPatterns = this.extractCommonErrorPatterns(members);
891
+ const groupId = this.storage.createGroup({
892
+ incidents: members.map((m) => m.id),
893
+ commonSymbols,
894
+ commonErrorPatterns,
895
+ firstSeen: this.getEarliestTimestamp(members),
896
+ lastSeen: this.getLatestTimestamp(members),
897
+ environments: this.getUniqueEnvironments(members)
898
+ });
899
+ for (const m of members) {
900
+ processed.add(m.id);
901
+ }
902
+ const group = this.storage.getGroup(groupId);
903
+ if (group) {
904
+ newGroups.push(group);
905
+ }
906
+ }
907
+ }
908
+ return newGroups;
909
+ }
910
+ /**
911
+ * Calculate similarity between two incidents (0-1)
912
+ */
913
+ calculateSimilarity(a, b) {
914
+ let score = 0;
915
+ let maxScore = 0;
916
+ const symbolWeight = 0.6;
917
+ const symbolTypes = [
918
+ "feature",
919
+ "component",
920
+ "flow",
921
+ "gate",
922
+ "signal",
923
+ "state",
924
+ "integration"
925
+ ];
926
+ for (const type of symbolTypes) {
927
+ const aValue = a.symbols[type];
928
+ const bValue = b.symbols[type];
929
+ if (aValue || bValue) {
930
+ maxScore += symbolWeight / symbolTypes.length;
931
+ if (aValue === bValue) {
932
+ score += symbolWeight / symbolTypes.length;
933
+ }
934
+ }
935
+ }
936
+ const errorWeight = 0.3;
937
+ const errorSimilarity = this.stringSimilarity(
938
+ a.error.message,
939
+ b.error.message
940
+ );
941
+ score += errorWeight * errorSimilarity;
942
+ maxScore += errorWeight;
943
+ const envWeight = 0.1;
944
+ if (a.environment === b.environment) {
945
+ score += envWeight;
946
+ }
947
+ maxScore += envWeight;
948
+ return maxScore > 0 ? score / maxScore : 0;
949
+ }
950
+ /**
951
+ * Calculate string similarity using Levenshtein distance
952
+ */
953
+ stringSimilarity(a, b) {
954
+ const maxLen = Math.max(a.length, b.length);
955
+ if (maxLen === 0) return 1;
956
+ const distance = this.levenshteinDistance(
957
+ a.toLowerCase(),
958
+ b.toLowerCase()
959
+ );
960
+ return 1 - distance / maxLen;
961
+ }
962
+ /**
963
+ * Levenshtein distance for string comparison
964
+ */
965
+ levenshteinDistance(a, b) {
966
+ if (a.length === 0) return b.length;
967
+ if (b.length === 0) return a.length;
968
+ const matrix = [];
969
+ for (let i = 0; i <= b.length; i++) {
970
+ matrix[i] = [i];
971
+ }
972
+ for (let j = 0; j <= a.length; j++) {
973
+ matrix[0][j] = j;
974
+ }
975
+ for (let i = 1; i <= b.length; i++) {
976
+ for (let j = 1; j <= a.length; j++) {
977
+ const cost = a[j - 1] === b[i - 1] ? 0 : 1;
978
+ matrix[i][j] = Math.min(
979
+ matrix[i - 1][j] + 1,
980
+ matrix[i][j - 1] + 1,
981
+ matrix[i - 1][j - 1] + cost
982
+ );
983
+ }
984
+ }
985
+ return matrix[b.length][a.length];
986
+ }
987
+ /**
988
+ * Check if incident should join existing group
989
+ */
990
+ shouldJoinGroup(incident, group) {
991
+ let matchCount = 0;
992
+ let totalCommon = 0;
993
+ for (const [key, value] of Object.entries(group.commonSymbols)) {
994
+ if (value) {
995
+ totalCommon++;
996
+ const incidentValue = incident.symbols[key];
997
+ if (incidentValue === value) {
998
+ matchCount++;
999
+ }
1000
+ }
1001
+ }
1002
+ if (totalCommon === 0) {
1003
+ return false;
1004
+ }
1005
+ const symbolMatch = matchCount / totalCommon;
1006
+ const errorLower = incident.error.message.toLowerCase();
1007
+ const errorMatch = group.commonErrorPatterns.some(
1008
+ (pattern) => errorLower.includes(pattern.toLowerCase())
1009
+ );
1010
+ return symbolMatch >= 0.5 || errorMatch;
1011
+ }
1012
+ /**
1013
+ * Extract symbols common to all incidents
1014
+ */
1015
+ extractCommonSymbols(incidents) {
1016
+ if (incidents.length === 0) return {};
1017
+ const first = incidents[0].symbols;
1018
+ const common = {};
1019
+ for (const [key, value] of Object.entries(first)) {
1020
+ if (!value) continue;
1021
+ const allMatch = incidents.every(
1022
+ (i) => i.symbols[key] === value
1023
+ );
1024
+ if (allMatch) {
1025
+ common[key] = value;
1026
+ }
1027
+ }
1028
+ return common;
1029
+ }
1030
+ /**
1031
+ * Extract common error patterns from incidents
1032
+ */
1033
+ extractCommonErrorPatterns(incidents) {
1034
+ if (incidents.length === 0) return [];
1035
+ const wordCounts = /* @__PURE__ */ new Map();
1036
+ const stopWords = /* @__PURE__ */ new Set([
1037
+ "the",
1038
+ "a",
1039
+ "an",
1040
+ "is",
1041
+ "are",
1042
+ "was",
1043
+ "were",
1044
+ "in",
1045
+ "on",
1046
+ "at",
1047
+ "to",
1048
+ "for",
1049
+ "of",
1050
+ "with",
1051
+ "error",
1052
+ "failed",
1053
+ "cannot"
1054
+ ]);
1055
+ for (const incident of incidents) {
1056
+ const words = incident.error.message.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !stopWords.has(w));
1057
+ const uniqueWords = new Set(words);
1058
+ for (const word of uniqueWords) {
1059
+ wordCounts.set(word, (wordCounts.get(word) || 0) + 1);
1060
+ }
1061
+ }
1062
+ const threshold = Math.ceil(incidents.length * 0.6);
1063
+ const commonPatterns = Array.from(wordCounts.entries()).filter(([, count]) => count >= threshold).map(([word]) => word).slice(0, 5);
1064
+ return commonPatterns;
1065
+ }
1066
+ getEarliestTimestamp(incidents) {
1067
+ return incidents.reduce(
1068
+ (earliest, i) => i.timestamp < earliest ? i.timestamp : earliest,
1069
+ incidents[0].timestamp
1070
+ );
1071
+ }
1072
+ getLatestTimestamp(incidents) {
1073
+ return incidents.reduce(
1074
+ (latest, i) => i.timestamp > latest ? i.timestamp : latest,
1075
+ incidents[0].timestamp
1076
+ );
1077
+ }
1078
+ getUniqueEnvironments(incidents) {
1079
+ return [...new Set(incidents.map((i) => i.environment))];
1080
+ }
1081
+ };
256
1082
  var TimelineBuilder = class {
257
1083
  /**
258
1084
  * Build a timeline from an incident with flow position
@@ -426,8 +1252,8 @@ var TimelineBuilder = class {
426
1252
  }
427
1253
  };
428
1254
  var StatsCalculator = class {
429
- constructor(storage2) {
430
- this.storage = storage2;
1255
+ constructor(storage) {
1256
+ this.storage = storage;
431
1257
  }
432
1258
  /**
433
1259
  * Get comprehensive statistics for a time period
@@ -653,9 +1479,190 @@ var StatsCalculator = class {
653
1479
  return symbols;
654
1480
  }
655
1481
  };
1482
+ var ContextEnricher = class {
1483
+ constructor(projectRoot = process.cwd()) {
1484
+ this.projectRoot = projectRoot;
1485
+ }
1486
+ symbolCache = /* @__PURE__ */ new Map();
1487
+ purposeCache = /* @__PURE__ */ new Map();
1488
+ /**
1489
+ * Enrich an incident with symbol context
1490
+ */
1491
+ enrich(incident) {
1492
+ const symbolEnrichments = {};
1493
+ for (const [, value] of Object.entries(incident.symbols)) {
1494
+ if (value) {
1495
+ const enrichment = this.getSymbolContext(value);
1496
+ if (enrichment && Object.keys(enrichment).length > 0) {
1497
+ symbolEnrichments[value] = enrichment;
1498
+ }
1499
+ }
1500
+ }
1501
+ let flowDescription;
1502
+ if (incident.symbols.flow) {
1503
+ const flowContext = this.getSymbolContext(incident.symbols.flow);
1504
+ flowDescription = flowContext?.description;
1505
+ }
1506
+ return {
1507
+ ...incident,
1508
+ enriched: {
1509
+ symbols: symbolEnrichments,
1510
+ flowDescription
1511
+ }
1512
+ };
1513
+ }
1514
+ /**
1515
+ * Get symbol metadata from index or .purpose files
1516
+ */
1517
+ getSymbolContext(symbol) {
1518
+ const cached = this.symbolCache.get(symbol);
1519
+ if (cached) {
1520
+ return cached;
1521
+ }
1522
+ const enrichment = {};
1523
+ const indexEntry = this.findInSymbolIndex(symbol);
1524
+ if (indexEntry) {
1525
+ enrichment.description = indexEntry.description;
1526
+ enrichment.definedIn = indexEntry.file;
1527
+ enrichment.references = indexEntry.references;
1528
+ enrichment.referencedBy = indexEntry.referencedBy;
1529
+ }
1530
+ const purposeEntry = this.findInPurposeFiles(symbol);
1531
+ if (purposeEntry) {
1532
+ if (!enrichment.description && purposeEntry.description) {
1533
+ enrichment.description = purposeEntry.description;
1534
+ }
1535
+ if (purposeEntry.references) {
1536
+ enrichment.references = [
1537
+ .../* @__PURE__ */ new Set([...enrichment.references || [], ...purposeEntry.references])
1538
+ ];
1539
+ }
1540
+ if (purposeEntry.referencedBy) {
1541
+ enrichment.referencedBy = [
1542
+ .../* @__PURE__ */ new Set([...enrichment.referencedBy || [], ...purposeEntry.referencedBy])
1543
+ ];
1544
+ }
1545
+ }
1546
+ this.symbolCache.set(symbol, enrichment);
1547
+ return enrichment;
1548
+ }
1549
+ /**
1550
+ * Find symbol in premise index
1551
+ */
1552
+ findInSymbolIndex(symbol) {
1553
+ const indexPath = path4.join(this.projectRoot, ".paradigm", "index.json");
1554
+ if (!fs4.existsSync(indexPath)) {
1555
+ return null;
1556
+ }
1557
+ try {
1558
+ const indexContent = fs4.readFileSync(indexPath, "utf-8");
1559
+ const index = JSON.parse(indexContent);
1560
+ if (index.symbols && Array.isArray(index.symbols)) {
1561
+ return index.symbols.find(
1562
+ (s) => s.id === symbol
1563
+ ) || null;
1564
+ }
1565
+ return null;
1566
+ } catch {
1567
+ return null;
1568
+ }
1569
+ }
1570
+ /**
1571
+ * Find symbol in .purpose files
1572
+ */
1573
+ findInPurposeFiles(symbol) {
1574
+ const searchPaths = this.getSearchPathsForSymbol(symbol);
1575
+ for (const searchPath of searchPaths) {
1576
+ const fullPath = path4.join(this.projectRoot, searchPath);
1577
+ if (!fs4.existsSync(fullPath)) {
1578
+ continue;
1579
+ }
1580
+ const cached = this.purposeCache.get(fullPath);
1581
+ if (cached) {
1582
+ if (cached.symbol === symbol) {
1583
+ return cached;
1584
+ }
1585
+ continue;
1586
+ }
1587
+ try {
1588
+ const content = fs4.readFileSync(fullPath, "utf-8");
1589
+ const purpose = this.parsePurposeFile(content);
1590
+ this.purposeCache.set(fullPath, purpose);
1591
+ if (purpose.symbol === symbol) {
1592
+ return purpose;
1593
+ }
1594
+ } catch {
1595
+ continue;
1596
+ }
1597
+ }
1598
+ return null;
1599
+ }
1600
+ /**
1601
+ * Get potential file paths for a symbol
1602
+ */
1603
+ getSearchPathsForSymbol(symbol) {
1604
+ const paths = [];
1605
+ const cleanSymbol = symbol.replace(/^[@#$%^!&~?]/, "");
1606
+ const prefixDirs = {
1607
+ "@": ["features", "src/features"],
1608
+ "#": ["components", "src/components"],
1609
+ "$": ["flows", "src/flows"],
1610
+ "^": ["middleware", "gates", "src/middleware"],
1611
+ "!": ["signals", "events", "src/signals"],
1612
+ "%": ["state", "store", "src/state"],
1613
+ "&": ["integrations", "services", "src/integrations"]
1614
+ };
1615
+ const prefix = symbol[0];
1616
+ const dirs = prefixDirs[prefix] || [];
1617
+ for (const dir of dirs) {
1618
+ paths.push(path4.join(dir, cleanSymbol, ".purpose"));
1619
+ paths.push(path4.join(dir, `${cleanSymbol}.purpose`));
1620
+ }
1621
+ paths.push(path4.join(".paradigm", "purposes", `${cleanSymbol}.yaml`));
1622
+ paths.push(path4.join(".paradigm", "purposes", `${cleanSymbol}.json`));
1623
+ return paths;
1624
+ }
1625
+ /**
1626
+ * Parse a .purpose file
1627
+ */
1628
+ parsePurposeFile(content) {
1629
+ const result = {};
1630
+ const lines = content.split("\n");
1631
+ for (const line of lines) {
1632
+ const trimmed = line.trim();
1633
+ if (trimmed.startsWith("symbol:")) {
1634
+ result.symbol = trimmed.substring(7).trim();
1635
+ } else if (trimmed.startsWith("description:")) {
1636
+ result.description = trimmed.substring(12).trim();
1637
+ } else if (trimmed.startsWith("purpose:")) {
1638
+ result.description = trimmed.substring(8).trim();
1639
+ }
1640
+ }
1641
+ if (!result.description) {
1642
+ const firstLine = lines.find((l) => l.trim() && !l.startsWith("#"));
1643
+ if (firstLine) {
1644
+ result.description = firstLine.trim();
1645
+ }
1646
+ }
1647
+ return result;
1648
+ }
1649
+ /**
1650
+ * Clear caches
1651
+ */
1652
+ clearCache() {
1653
+ this.symbolCache.clear();
1654
+ this.purposeCache.clear();
1655
+ }
1656
+ /**
1657
+ * Batch enrich multiple incidents
1658
+ */
1659
+ enrichBatch(incidents) {
1660
+ return incidents.map((i) => this.enrich(i));
1661
+ }
1662
+ };
656
1663
  var PatternSuggester = class {
657
- constructor(storage2) {
658
- this.storage = storage2;
1664
+ constructor(storage) {
1665
+ this.storage = storage;
659
1666
  }
660
1667
  /**
661
1668
  * Suggest a pattern from a resolved incident
@@ -1224,656 +2231,21 @@ var PatternImporter = class {
1224
2231
  }
1225
2232
  };
1226
2233
 
1227
- // src/commands/triage/utils/format.ts
1228
- import chalk from "chalk";
1229
- function formatHeader() {
1230
- return `
1231
- ${chalk.cyan("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557")}
1232
- ${chalk.cyan("\u2551")} ${chalk.bold.white("PARADIGM SENTINEL TRIAGE")} ${chalk.cyan("\u2551")}
1233
- ${chalk.cyan("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D")}
1234
- `;
1235
- }
1236
- function formatSummaryBar(stats) {
1237
- return `${chalk.cyan("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557")}
1238
- ${chalk.cyan("\u2551")} Open: ${chalk.yellow(String(stats.open).padEnd(4))} \u2502 Investigating: ${chalk.blue(String(stats.investigating).padEnd(3))} \u2502 Resolved: ${chalk.green(String(stats.resolved).padEnd(4))} \u2502 Today: ${chalk.magenta(`+${stats.today}`)} ${chalk.cyan("\u2551")}
1239
- ${chalk.cyan("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D")}
1240
- `;
1241
- }
1242
- function formatIncident(incident, matches) {
1243
- const lines = [];
1244
- const statusColor = getStatusColor(incident.status);
1245
- lines.push(
1246
- chalk.gray("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510")
1247
- );
1248
- lines.push(
1249
- chalk.gray("\u2502 ") + chalk.bold(`[${incident.id}] `) + statusColor(incident.status.toUpperCase().padEnd(12)) + chalk.gray(incident.timestamp.substring(0, 19).replace("T", " ").padStart(19)) + chalk.gray(" \u2502")
1250
- );
1251
- lines.push(
1252
- chalk.gray("\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524")
1253
- );
1254
- lines.push(chalk.gray("\u2502 ") + chalk.red("Error: ") + truncate(incident.error.message, 55) + chalk.gray(" \u2502"));
1255
- lines.push(chalk.gray("\u2502") + " ".repeat(65) + chalk.gray("\u2502"));
1256
- lines.push(chalk.gray("\u2502 ") + chalk.cyan("Symbolic Context:") + " ".repeat(47) + chalk.gray("\u2502"));
1257
- const symbols = formatSymbols(incident.symbols);
1258
- for (const sym of symbols) {
1259
- lines.push(chalk.gray("\u2502 ") + sym.padEnd(61) + chalk.gray(" \u2502"));
1260
- }
1261
- lines.push(chalk.gray("\u2502") + " ".repeat(65) + chalk.gray("\u2502"));
1262
- const envLine = `Environment: ${chalk.yellow(incident.environment)} \u2502 Service: ${chalk.yellow(incident.service || "N/A")} \u2502 v${incident.version || "N/A"}`;
1263
- lines.push(chalk.gray("\u2502 ") + envLine.substring(0, 63).padEnd(63) + chalk.gray(" \u2502"));
1264
- if (matches && matches.length > 0) {
1265
- lines.push(chalk.gray("\u2502") + " ".repeat(65) + chalk.gray("\u2502"));
1266
- lines.push(
1267
- chalk.gray("\u2502 \u250C\u2500 ") + chalk.cyan("Matched Patterns") + chalk.gray(" \u2500".repeat(22) + "\u2510 \u2502")
1268
- );
1269
- for (let i = 0; i < Math.min(matches.length, 3); i++) {
1270
- const match = matches[i];
1271
- const icon = i === 0 ? chalk.yellow("\u2605") : chalk.gray("\u25CB");
1272
- const conf = `${match.confidence}% confidence`;
1273
- lines.push(
1274
- chalk.gray("\u2502 \u2502 ") + icon + " " + chalk.bold(truncate(match.pattern.id, 30).padEnd(30)) + chalk.gray(conf.padStart(15)) + chalk.gray(" \u2502 \u2502")
1275
- );
1276
- lines.push(
1277
- chalk.gray("\u2502 \u2502 ") + chalk.italic(truncate(match.pattern.description, 45).padEnd(45)) + chalk.gray(" \u2502 \u2502")
1278
- );
1279
- lines.push(
1280
- chalk.gray("\u2502 \u2502 Strategy: ") + chalk.cyan(match.pattern.resolution.strategy.padEnd(40)) + chalk.gray(" \u2502 \u2502")
1281
- );
1282
- if (i < Math.min(matches.length, 3) - 1) {
1283
- lines.push(chalk.gray("\u2502 \u2502") + " ".repeat(59) + chalk.gray("\u2502 \u2502"));
1284
- }
1285
- }
1286
- lines.push(
1287
- chalk.gray("\u2502 \u2514") + "\u2500".repeat(59) + chalk.gray("\u2518 \u2502")
1288
- );
1289
- }
1290
- lines.push(chalk.gray("\u2502") + " ".repeat(65) + chalk.gray("\u2502"));
1291
- lines.push(chalk.gray("\u2502 ") + chalk.dim("Actions:") + " ".repeat(56) + chalk.gray("\u2502"));
1292
- lines.push(
1293
- chalk.gray("\u2502 ") + chalk.dim(`paradigm triage resolve ${incident.id}`) + " ".repeat(35 - incident.id.length) + chalk.gray("\u2502")
1294
- );
1295
- lines.push(
1296
- chalk.gray("\u2502 ") + chalk.dim(`paradigm triage show ${incident.id} --timeline`) + " ".repeat(28 - incident.id.length) + chalk.gray("\u2502")
1297
- );
1298
- lines.push(
1299
- chalk.gray("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518")
1300
- );
1301
- return lines.join("\n");
1302
- }
1303
- function formatIncidentCompact(incident) {
1304
- const statusColor = getStatusColor(incident.status);
1305
- const status = statusColor(incident.status.substring(0, 4).toUpperCase().padEnd(4));
1306
- const timestamp = incident.timestamp.substring(5, 16).replace("T", " ");
1307
- const error = truncate(incident.error.message, 40);
1308
- const symbols = Object.values(incident.symbols).filter(Boolean).join(" ");
1309
- return `${chalk.bold(incident.id)} ${status} ${chalk.gray(timestamp)} ${error}
1310
- ${chalk.cyan(truncate(symbols, 60))}`;
1311
- }
1312
- function formatPattern(pattern) {
1313
- const lines = [];
1314
- lines.push(chalk.bold.cyan(`Pattern: ${pattern.id}`));
1315
- lines.push(chalk.white(` Name: ${pattern.name}`));
1316
- lines.push(chalk.gray(` Description: ${pattern.description}`));
1317
- lines.push("");
1318
- lines.push(chalk.yellow(" Matching Criteria:"));
1319
- if (pattern.pattern.symbols) {
1320
- for (const [key, value] of Object.entries(pattern.pattern.symbols)) {
1321
- if (value) {
1322
- const v = Array.isArray(value) ? value.join(", ") : value;
1323
- lines.push(` ${chalk.cyan(key)}: ${v}`);
1324
- }
1325
- }
1326
- }
1327
- if (pattern.pattern.errorContains) {
1328
- lines.push(
1329
- ` ${chalk.cyan("errorContains")}: ${pattern.pattern.errorContains.join(", ")}`
1330
- );
1331
- }
1332
- if (pattern.pattern.missingSignals) {
1333
- lines.push(
1334
- ` ${chalk.cyan("missingSignals")}: ${pattern.pattern.missingSignals.join(", ")}`
1335
- );
1336
- }
1337
- lines.push("");
1338
- lines.push(chalk.green(" Resolution:"));
1339
- lines.push(` ${chalk.white(pattern.resolution.description)}`);
1340
- lines.push(
1341
- ` Strategy: ${chalk.cyan(pattern.resolution.strategy)} Priority: ${getPriorityColor(pattern.resolution.priority)(pattern.resolution.priority)}`
1342
- );
1343
- if (pattern.resolution.codeHint) {
1344
- lines.push(` ${chalk.dim("Hint: " + pattern.resolution.codeHint)}`);
1345
- }
1346
- lines.push("");
1347
- lines.push(chalk.blue(" Confidence:"));
1348
- lines.push(
1349
- ` Score: ${getConfidenceColor(pattern.confidence.score)(pattern.confidence.score + "%")} Matched: ${pattern.confidence.timesMatched} Resolved: ${pattern.confidence.timesResolved} Recurred: ${pattern.confidence.timesRecurred}`
1350
- );
1351
- lines.push("");
1352
- lines.push(
1353
- chalk.gray(
1354
- ` Source: ${pattern.source} Tags: ${pattern.tags.join(", ") || "none"}`
1355
- )
1356
- );
1357
- return lines.join("\n");
1358
- }
1359
- function formatPatternCompact(pattern) {
1360
- const confidence = getConfidenceColor(pattern.confidence.score)(
1361
- `${pattern.confidence.score}%`
1362
- );
1363
- const tags = pattern.tags.slice(0, 3).join(", ");
1364
- return `${chalk.bold(pattern.id.padEnd(30))} ${confidence.padStart(8)} ${chalk.gray(pattern.source.padEnd(10))} ${chalk.dim(tags)}
1365
- ${chalk.white(truncate(pattern.name, 60))}`;
1366
- }
1367
- function formatSymbols(symbols) {
1368
- const result = [];
1369
- for (const [key, value] of Object.entries(symbols)) {
1370
- if (value) {
1371
- const color = getSymbolColor(key);
1372
- result.push(`${color(value.padEnd(20))} ${chalk.dim(key)}`);
1373
- }
1374
- }
1375
- return result;
1376
- }
1377
- function getStatusColor(status) {
1378
- switch (status) {
1379
- case "open":
1380
- return chalk.red;
1381
- case "investigating":
1382
- return chalk.yellow;
1383
- case "resolved":
1384
- return chalk.green;
1385
- case "wont-fix":
1386
- return chalk.gray;
1387
- default:
1388
- return chalk.white;
1389
- }
1390
- }
1391
- function getSymbolColor(type) {
1392
- switch (type) {
1393
- case "feature":
1394
- return chalk.magenta;
1395
- case "component":
1396
- return chalk.blue;
1397
- case "flow":
1398
- return chalk.cyan;
1399
- case "gate":
1400
- return chalk.yellow;
1401
- case "signal":
1402
- return chalk.green;
1403
- case "state":
1404
- return chalk.red;
1405
- case "integration":
1406
- return chalk.white;
1407
- default:
1408
- return chalk.gray;
1409
- }
1410
- }
1411
- function getPriorityColor(priority) {
1412
- switch (priority) {
1413
- case "critical":
1414
- return chalk.red.bold;
1415
- case "high":
1416
- return chalk.red;
1417
- case "medium":
1418
- return chalk.yellow;
1419
- case "low":
1420
- return chalk.gray;
1421
- default:
1422
- return chalk.white;
1423
- }
1424
- }
1425
- function getConfidenceColor(score) {
1426
- if (score >= 80) return chalk.green;
1427
- if (score >= 60) return chalk.yellow;
1428
- if (score >= 40) return chalk.red;
1429
- return chalk.gray;
1430
- }
1431
- function truncate(str, maxLen) {
1432
- if (str.length <= maxLen) return str;
1433
- return str.substring(0, maxLen - 3) + "...";
1434
- }
1435
-
1436
- // src/commands/triage/index.ts
1437
- import * as fs2 from "fs";
1438
- var storage = null;
1439
- function getStorage() {
1440
- if (!storage) {
1441
- storage = new SentinelStorage();
1442
- }
1443
- return storage;
1444
- }
1445
- async function triageListCommand(options) {
1446
- const store = getStorage();
1447
- const matcher = new PatternMatcher(store);
1448
- const limit = parseInt(options.limit || "10", 10);
1449
- const status = options.status;
1450
- const incidents = store.getRecentIncidents({
1451
- limit,
1452
- status: status || "all",
1453
- symbol: options.symbol,
1454
- environment: options.env,
1455
- search: options.search,
1456
- dateFrom: options.from,
1457
- dateTo: options.to
1458
- });
1459
- if (options.json) {
1460
- const result = incidents.map((i) => ({
1461
- incident: i,
1462
- matches: matcher.match(i, { maxResults: 3 })
1463
- }));
1464
- console.log(JSON.stringify(result, null, 2));
1465
- return;
1466
- }
1467
- const stats = new StatsCalculator(store).getStats(7);
1468
- const todayStart = /* @__PURE__ */ new Date();
1469
- todayStart.setHours(0, 0, 0, 0);
1470
- const todayCount = store.getIncidentCount({
1471
- dateFrom: todayStart.toISOString()
1472
- });
1473
- console.log(formatHeader());
1474
- console.log(
1475
- formatSummaryBar({
1476
- open: stats.incidents.open,
1477
- investigating: stats.incidents.total - stats.incidents.open - stats.incidents.resolved,
1478
- resolved: stats.incidents.resolved,
1479
- today: todayCount
1480
- })
1481
- );
1482
- console.log("");
1483
- if (incidents.length === 0) {
1484
- console.log(chalk2.gray("No incidents found."));
1485
- return;
1486
- }
1487
- for (const incident of incidents) {
1488
- const matches = matcher.match(incident, { maxResults: 3 });
1489
- console.log(formatIncident(incident, matches));
1490
- console.log("");
1491
- }
1492
- }
1493
- async function triageShowCommand(incidentId, options) {
1494
- const store = getStorage();
1495
- const matcher = new PatternMatcher(store);
1496
- const incident = store.getIncident(incidentId);
1497
- if (!incident) {
1498
- console.log(chalk2.red(`Incident ${incidentId} not found.`));
1499
- return;
1500
- }
1501
- const matches = matcher.match(incident, { maxResults: 5 });
1502
- if (options.json) {
1503
- const result = { incident, matches };
1504
- if (options.timeline && incident.flowPosition) {
1505
- const timeline = new TimelineBuilder().build(incident);
1506
- result.timeline = timeline ? new TimelineBuilder().renderStructured(timeline) : null;
1507
- }
1508
- console.log(JSON.stringify(result, null, 2));
1509
- return;
1510
- }
1511
- console.log(formatIncident(incident, matches));
1512
- if (options.timeline && incident.flowPosition) {
1513
- const timeline = new TimelineBuilder().build(incident);
1514
- if (timeline) {
1515
- console.log("");
1516
- console.log(chalk2.cyan.bold("Flow Timeline"));
1517
- console.log(chalk2.gray("\u2500".repeat(50)));
1518
- console.log(new TimelineBuilder().renderAscii(timeline));
1519
- }
1520
- }
1521
- if (incident.notes.length > 0) {
1522
- console.log("");
1523
- console.log(chalk2.cyan.bold("Notes"));
1524
- console.log(chalk2.gray("\u2500".repeat(50)));
1525
- for (const note of incident.notes) {
1526
- console.log(
1527
- chalk2.gray(note.timestamp.substring(0, 16)) + " " + (note.author ? chalk2.yellow(note.author + ": ") : "") + note.content
1528
- );
1529
- }
1530
- }
1531
- if (incident.relatedIncidents.length > 0) {
1532
- console.log("");
1533
- console.log(chalk2.cyan.bold("Related Incidents"));
1534
- console.log(chalk2.gray("\u2500".repeat(50)));
1535
- console.log(" " + incident.relatedIncidents.join(", "));
1536
- }
1537
- }
1538
- async function triageResolveCommand(incidentId, options) {
1539
- const store = getStorage();
1540
- const incident = store.getIncident(incidentId);
1541
- if (!incident) {
1542
- console.log(chalk2.red(`Incident ${incidentId} not found.`));
1543
- return;
1544
- }
1545
- if (incident.status === "resolved" || incident.status === "wont-fix") {
1546
- console.log(chalk2.yellow(`Incident ${incidentId} is already ${incident.status}.`));
1547
- return;
1548
- }
1549
- if (options.wontFix) {
1550
- store.updateIncident(incidentId, {
1551
- status: "wont-fix",
1552
- resolvedAt: (/* @__PURE__ */ new Date()).toISOString(),
1553
- resolvedBy: "manual",
1554
- resolution: {
1555
- notes: options.notes
1556
- }
1557
- });
1558
- console.log(chalk2.gray(`Incident ${incidentId} marked as won't fix.`));
1559
- return;
1560
- }
1561
- store.recordResolution({
1562
- incidentId,
1563
- patternId: options.pattern,
1564
- commitHash: options.commit,
1565
- prUrl: options.pr,
1566
- notes: options.notes
1567
- });
1568
- console.log(chalk2.green(`Incident ${incidentId} resolved.`));
1569
- if (options.pattern) {
1570
- console.log(chalk2.gray(` Pattern: ${options.pattern}`));
1571
- }
1572
- if (options.commit) {
1573
- console.log(chalk2.gray(` Commit: ${options.commit}`));
1574
- }
1575
- if (options.pr) {
1576
- console.log(chalk2.gray(` PR: ${options.pr}`));
1577
- }
1578
- }
1579
- async function triageNoteCommand(incidentId, note) {
1580
- const store = getStorage();
1581
- const incident = store.getIncident(incidentId);
1582
- if (!incident) {
1583
- console.log(chalk2.red(`Incident ${incidentId} not found.`));
1584
- return;
1585
- }
1586
- store.addIncidentNote(incidentId, {
1587
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1588
- content: note
1589
- });
1590
- console.log(chalk2.green(`Note added to ${incidentId}.`));
1591
- }
1592
- async function triageLinkCommand(incidentId1, incidentId2) {
1593
- const store = getStorage();
1594
- const inc1 = store.getIncident(incidentId1);
1595
- const inc2 = store.getIncident(incidentId2);
1596
- if (!inc1) {
1597
- console.log(chalk2.red(`Incident ${incidentId1} not found.`));
1598
- return;
1599
- }
1600
- if (!inc2) {
1601
- console.log(chalk2.red(`Incident ${incidentId2} not found.`));
1602
- return;
1603
- }
1604
- store.linkIncidents(incidentId1, incidentId2);
1605
- console.log(chalk2.green(`Linked ${incidentId1} and ${incidentId2}.`));
1606
- }
1607
- async function triagePatternsListCommand(options) {
1608
- const store = getStorage();
1609
- const patterns = store.getAllPatterns({
1610
- source: options.source,
1611
- minConfidence: options.minConfidence ? parseInt(options.minConfidence, 10) : void 0,
1612
- includePrivate: true
1613
- });
1614
- if (options.json) {
1615
- console.log(JSON.stringify(patterns, null, 2));
1616
- return;
1617
- }
1618
- console.log(chalk2.cyan.bold("\nFailure Patterns"));
1619
- console.log(chalk2.gray("\u2500".repeat(70)));
1620
- if (patterns.length === 0) {
1621
- console.log(chalk2.gray("No patterns found. Run `paradigm triage patterns seed` to load defaults."));
1622
- return;
1623
- }
1624
- for (const pattern of patterns) {
1625
- console.log(formatPatternCompact(pattern));
1626
- console.log("");
1627
- }
1628
- }
1629
- async function triagePatternsShowCommand(patternId, options) {
1630
- const store = getStorage();
1631
- const pattern = store.getPattern(patternId);
1632
- if (!pattern) {
1633
- console.log(chalk2.red(`Pattern ${patternId} not found.`));
1634
- return;
1635
- }
1636
- if (options.json) {
1637
- console.log(JSON.stringify(pattern, null, 2));
1638
- return;
1639
- }
1640
- console.log(formatPattern(pattern));
1641
- }
1642
- async function triagePatternsAddCommand(options) {
1643
- const store = getStorage();
1644
- if (options.fromIncident) {
1645
- const incident = store.getIncident(options.fromIncident);
1646
- if (!incident) {
1647
- console.log(chalk2.red(`Incident ${options.fromIncident} not found.`));
1648
- return;
1649
- }
1650
- const suggester = new PatternSuggester(store);
1651
- const suggested = suggester.suggestFromIncident(incident);
1652
- console.log(chalk2.cyan("\nSuggested Pattern:"));
1653
- console.log(JSON.stringify(suggested, null, 2));
1654
- console.log(
1655
- chalk2.gray(
1656
- '\nEdit and add with: paradigm triage patterns add --id <id> --name "..." ...'
1657
- )
1658
- );
1659
- return;
1660
- }
1661
- const symbols = {};
1662
- if (options.symbols) {
1663
- const pairs = options.symbols.split(",");
1664
- for (const pair of pairs) {
1665
- const [key, value] = pair.split(":").map((s) => s.trim());
1666
- if (key && value) {
1667
- symbols[key] = value;
1668
- }
1669
- }
1670
- }
1671
- const input = {
1672
- id: options.id,
1673
- name: options.name,
1674
- description: options.description || "",
1675
- pattern: {
1676
- symbols,
1677
- errorContains: options.errorContains?.split(",").map((s) => s.trim()),
1678
- missingSignals: options.missingSignals?.split(",").map((s) => s.trim())
1679
- },
1680
- resolution: {
1681
- description: "Resolution TBD",
1682
- strategy: options.strategy || "fix-code",
1683
- priority: options.priority || "medium",
1684
- codeHint: options.codeHint
1685
- },
1686
- source: "manual",
1687
- private: false,
1688
- tags: options.tags?.split(",").map((s) => s.trim()) || []
1689
- };
1690
- store.addPattern(input);
1691
- console.log(chalk2.green(`Pattern ${options.id} created.`));
1692
- }
1693
- async function triagePatternsDeleteCommand(patternId) {
1694
- const store = getStorage();
1695
- const pattern = store.getPattern(patternId);
1696
- if (!pattern) {
1697
- console.log(chalk2.red(`Pattern ${patternId} not found.`));
1698
- return;
1699
- }
1700
- store.deletePattern(patternId);
1701
- console.log(chalk2.green(`Pattern ${patternId} deleted.`));
1702
- }
1703
- async function triagePatternsTestCommand(patternId, options) {
1704
- const store = getStorage();
1705
- const matcher = new PatternMatcher(store);
1706
- const pattern = store.getPattern(patternId);
1707
- if (!pattern) {
1708
- console.log(chalk2.red(`Pattern ${patternId} not found.`));
1709
- return;
1710
- }
1711
- const limit = parseInt(options.limit || "100", 10);
1712
- const result = matcher.testPattern(pattern, limit);
1713
- if (options.json) {
1714
- console.log(JSON.stringify(result, null, 2));
1715
- return;
1716
- }
1717
- console.log(chalk2.cyan.bold(`
1718
- Pattern Test: ${patternId}`));
1719
- console.log(chalk2.gray("\u2500".repeat(50)));
1720
- console.log(`Would match: ${chalk2.yellow(result.matchCount)} incidents`);
1721
- console.log(`Average score: ${chalk2.yellow(result.avgScore + "%")}`);
1722
- if (result.wouldMatch.length > 0) {
1723
- console.log("");
1724
- console.log(chalk2.cyan("Sample matches:"));
1725
- for (const incident of result.wouldMatch.slice(0, 5)) {
1726
- console.log(formatIncidentCompact(incident));
1727
- }
1728
- }
1729
- }
1730
- async function triagePatternsSeedCommand() {
1731
- const store = getStorage();
1732
- const spinner = ora("Loading seed patterns...").start();
1733
- try {
1734
- const seedData = loadAllSeedPatterns();
1735
- const result = store.importPatterns(seedData, { overwrite: false });
1736
- spinner.succeed(
1737
- `Loaded ${result.imported} patterns (${result.skipped} skipped).`
1738
- );
1739
- } catch (error) {
1740
- spinner.fail(`Failed to load seed patterns: ${error}`);
1741
- }
1742
- }
1743
- async function triageExportCommand(type, options) {
1744
- const store = getStorage();
1745
- let data;
1746
- let defaultFilename;
1747
- if (type === "patterns") {
1748
- data = store.exportPatterns({
1749
- includePrivate: options.includePrivate
1750
- });
1751
- defaultFilename = "sentinel-patterns.json";
1752
- } else {
1753
- data = store.exportBackup();
1754
- defaultFilename = "sentinel-backup.json";
1755
- }
1756
- const outputPath = options.output || defaultFilename;
1757
- fs2.writeFileSync(outputPath, JSON.stringify(data, null, 2));
1758
- console.log(chalk2.green(`Exported to ${outputPath}`));
1759
- }
1760
- async function triageImportCommand(filePath, options) {
1761
- const store = getStorage();
1762
- const importer = new PatternImporter();
1763
- const spinner = ora(`Importing from ${filePath}...`).start();
1764
- try {
1765
- const data = importer.loadFromFile(filePath);
1766
- const result = store.importPatterns(data, {
1767
- overwrite: options.overwrite
1768
- });
1769
- spinner.succeed(
1770
- `Imported ${result.imported} patterns (${result.skipped} skipped).`
1771
- );
1772
- } catch (error) {
1773
- spinner.fail(`Import failed: ${error}`);
1774
- }
1775
- }
1776
- async function triageRestoreCommand(filePath) {
1777
- const store = getStorage();
1778
- const spinner = ora(`Restoring from ${filePath}...`).start();
1779
- try {
1780
- const content = fs2.readFileSync(filePath, "utf-8");
1781
- const data = JSON.parse(content);
1782
- store.importBackup(data);
1783
- spinner.succeed("Backup restored.");
1784
- } catch (error) {
1785
- spinner.fail(`Restore failed: ${error}`);
1786
- }
1787
- }
1788
- async function triageStatsCommand(options) {
1789
- const store = getStorage();
1790
- const calculator = new StatsCalculator(store);
1791
- let periodDays = 7;
1792
- if (options.period) {
1793
- const match = options.period.match(/^(\d+)d$/);
1794
- if (match) {
1795
- periodDays = parseInt(match[1], 10);
1796
- }
1797
- }
1798
- if (options.symbol) {
1799
- const health = calculator.getSymbolHealth(options.symbol);
1800
- if (options.json) {
1801
- console.log(JSON.stringify(health, null, 2));
1802
- } else {
1803
- console.log(chalk2.cyan.bold(`
1804
- Symbol Health: ${options.symbol}`));
1805
- console.log(chalk2.gray("\u2500".repeat(50)));
1806
- console.log(` Incidents: ${health.incidentCount}`);
1807
- console.log(` Avg Time to Resolve: ${health.avgTimeToResolve}m`);
1808
- console.log("");
1809
- console.log(chalk2.cyan(" Top Patterns:"));
1810
- for (const { patternId, count } of health.topPatterns) {
1811
- console.log(` ${patternId}: ${count} times`);
1812
- }
1813
- }
1814
- return;
1815
- }
1816
- const stats = calculator.getStats(periodDays);
1817
- if (options.json) {
1818
- console.log(JSON.stringify(stats, null, 2));
1819
- return;
1820
- }
1821
- console.log(calculator.generateDashboard(periodDays));
1822
- }
1823
- async function triageRecordCommand(options) {
1824
- const store = getStorage();
1825
- const matcher = new PatternMatcher(store);
1826
- const incidentId = store.recordIncident({
1827
- error: {
1828
- message: options.error,
1829
- stack: options.stack
1830
- },
1831
- symbols: {
1832
- feature: options.feature,
1833
- component: options.component,
1834
- flow: options.flow,
1835
- gate: options.gate,
1836
- signal: options.signal,
1837
- state: options.state,
1838
- integration: options.integration
1839
- },
1840
- environment: options.env,
1841
- service: options.service,
1842
- version: options.version
1843
- });
1844
- const incident = store.getIncident(incidentId);
1845
- const matches = incident ? matcher.match(incident, { maxResults: 3 }) : [];
1846
- if (options.json) {
1847
- console.log(JSON.stringify({ incidentId, matches }, null, 2));
1848
- return;
1849
- }
1850
- console.log(chalk2.green(`Recorded incident ${incidentId}`));
1851
- if (matches.length > 0) {
1852
- console.log("");
1853
- console.log(chalk2.cyan("Matched patterns:"));
1854
- for (const match of matches) {
1855
- console.log(
1856
- ` ${chalk2.yellow("\u2605")} ${match.pattern.id} (${match.confidence}% confidence)`
1857
- );
1858
- console.log(chalk2.gray(` ${match.pattern.resolution.description}`));
1859
- }
1860
- }
1861
- }
1862
2234
  export {
1863
- triageExportCommand,
1864
- triageImportCommand,
1865
- triageLinkCommand,
1866
- triageListCommand,
1867
- triageNoteCommand,
1868
- triagePatternsAddCommand,
1869
- triagePatternsDeleteCommand,
1870
- triagePatternsListCommand,
1871
- triagePatternsSeedCommand,
1872
- triagePatternsShowCommand,
1873
- triagePatternsTestCommand,
1874
- triageRecordCommand,
1875
- triageResolveCommand,
1876
- triageRestoreCommand,
1877
- triageShowCommand,
1878
- triageStatsCommand
2235
+ PatternMatcher,
2236
+ loadUniversalPatterns,
2237
+ loadParadigmPatterns,
2238
+ loadAllSeedPatterns,
2239
+ FlowTracker,
2240
+ Sentinel,
2241
+ loadConfig,
2242
+ writeConfig,
2243
+ detectSymbols,
2244
+ generateConfig,
2245
+ IncidentGrouper,
2246
+ TimelineBuilder,
2247
+ StatsCalculator,
2248
+ ContextEnricher,
2249
+ PatternSuggester,
2250
+ PatternImporter
1879
2251
  };