@vibecheckai/cli 3.2.6 → 3.3.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.
Files changed (84) hide show
  1. package/bin/registry.js +192 -5
  2. package/bin/runners/lib/agent-firewall/change-packet/builder.js +280 -6
  3. package/bin/runners/lib/agent-firewall/critic/index.js +151 -0
  4. package/bin/runners/lib/agent-firewall/critic/judge.js +432 -0
  5. package/bin/runners/lib/agent-firewall/critic/prompts.js +305 -0
  6. package/bin/runners/lib/agent-firewall/lawbook/distributor.js +465 -0
  7. package/bin/runners/lib/agent-firewall/lawbook/evaluator.js +604 -0
  8. package/bin/runners/lib/agent-firewall/lawbook/index.js +304 -0
  9. package/bin/runners/lib/agent-firewall/lawbook/registry.js +514 -0
  10. package/bin/runners/lib/agent-firewall/lawbook/schema.js +420 -0
  11. package/bin/runners/lib/agent-firewall/logger.js +141 -0
  12. package/bin/runners/lib/agent-firewall/policy/loader.js +312 -4
  13. package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +113 -1
  14. package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +133 -6
  15. package/bin/runners/lib/agent-firewall/proposal/extractor.js +394 -0
  16. package/bin/runners/lib/agent-firewall/proposal/index.js +212 -0
  17. package/bin/runners/lib/agent-firewall/proposal/schema.js +251 -0
  18. package/bin/runners/lib/agent-firewall/proposal/validator.js +386 -0
  19. package/bin/runners/lib/agent-firewall/reality/index.js +332 -0
  20. package/bin/runners/lib/agent-firewall/reality/state.js +625 -0
  21. package/bin/runners/lib/agent-firewall/reality/watcher.js +322 -0
  22. package/bin/runners/lib/agent-firewall/risk/index.js +173 -0
  23. package/bin/runners/lib/agent-firewall/risk/scorer.js +328 -0
  24. package/bin/runners/lib/agent-firewall/risk/thresholds.js +321 -0
  25. package/bin/runners/lib/agent-firewall/risk/vectors.js +421 -0
  26. package/bin/runners/lib/agent-firewall/simulator/diff-simulator.js +472 -0
  27. package/bin/runners/lib/agent-firewall/simulator/import-resolver.js +346 -0
  28. package/bin/runners/lib/agent-firewall/simulator/index.js +181 -0
  29. package/bin/runners/lib/agent-firewall/simulator/route-validator.js +380 -0
  30. package/bin/runners/lib/agent-firewall/time-machine/incident-correlator.js +661 -0
  31. package/bin/runners/lib/agent-firewall/time-machine/index.js +267 -0
  32. package/bin/runners/lib/agent-firewall/time-machine/replay-engine.js +436 -0
  33. package/bin/runners/lib/agent-firewall/time-machine/state-reconstructor.js +490 -0
  34. package/bin/runners/lib/agent-firewall/time-machine/timeline-builder.js +530 -0
  35. package/bin/runners/lib/analyzers.js +81 -18
  36. package/bin/runners/lib/authority-badge.js +425 -0
  37. package/bin/runners/lib/cli-output.js +7 -1
  38. package/bin/runners/lib/error-handler.js +16 -9
  39. package/bin/runners/lib/exit-codes.js +275 -0
  40. package/bin/runners/lib/global-flags.js +37 -0
  41. package/bin/runners/lib/help-formatter.js +413 -0
  42. package/bin/runners/lib/logger.js +38 -0
  43. package/bin/runners/lib/unified-cli-output.js +604 -0
  44. package/bin/runners/lib/upsell.js +148 -0
  45. package/bin/runners/runApprove.js +1200 -0
  46. package/bin/runners/runAuth.js +324 -95
  47. package/bin/runners/runCheckpoint.js +39 -21
  48. package/bin/runners/runClassify.js +859 -0
  49. package/bin/runners/runContext.js +136 -24
  50. package/bin/runners/runDoctor.js +108 -68
  51. package/bin/runners/runFix.js +6 -5
  52. package/bin/runners/runGuard.js +212 -118
  53. package/bin/runners/runInit.js +3 -2
  54. package/bin/runners/runMcp.js +130 -52
  55. package/bin/runners/runPolish.js +43 -20
  56. package/bin/runners/runProve.js +1 -2
  57. package/bin/runners/runReport.js +3 -2
  58. package/bin/runners/runScan.js +63 -44
  59. package/bin/runners/runShip.js +3 -4
  60. package/bin/runners/runValidate.js +19 -2
  61. package/bin/runners/runWatch.js +104 -53
  62. package/bin/vibecheck.js +106 -19
  63. package/mcp-server/HARDENING_SUMMARY.md +299 -0
  64. package/mcp-server/agent-firewall-interceptor.js +367 -31
  65. package/mcp-server/authority-tools.js +569 -0
  66. package/mcp-server/conductor/conflict-resolver.js +588 -0
  67. package/mcp-server/conductor/execution-planner.js +544 -0
  68. package/mcp-server/conductor/index.js +377 -0
  69. package/mcp-server/conductor/lock-manager.js +615 -0
  70. package/mcp-server/conductor/request-queue.js +550 -0
  71. package/mcp-server/conductor/session-manager.js +500 -0
  72. package/mcp-server/conductor/tools.js +510 -0
  73. package/mcp-server/index.js +1149 -243
  74. package/mcp-server/lib/{api-client.js → api-client.cjs} +40 -4
  75. package/mcp-server/lib/logger.cjs +30 -0
  76. package/mcp-server/logger.js +173 -0
  77. package/mcp-server/package.json +2 -2
  78. package/mcp-server/premium-tools.js +2 -2
  79. package/mcp-server/tier-auth.js +245 -35
  80. package/mcp-server/truth-firewall-tools.js +145 -15
  81. package/mcp-server/vibecheck-tools.js +2 -2
  82. package/package.json +2 -3
  83. package/mcp-server/index.old.js +0 -4137
  84. package/mcp-server/package-lock.json +0 -165
@@ -0,0 +1,530 @@
1
+ /**
2
+ * Time Machine Timeline Builder
3
+ *
4
+ * Builds event timelines from multiple sources.
5
+ * Correlates agent actions with outcomes and incidents.
6
+ *
7
+ * Codename: Time Machine
8
+ */
9
+
10
+ "use strict";
11
+
12
+ const fs = require("fs");
13
+ const path = require("path");
14
+ const { timeMachineLogger: log, getErrorMessage } = require("../logger.js");
15
+
16
+ /**
17
+ * @typedef {Object} TimelineEvent
18
+ * @property {string} id - Event ID
19
+ * @property {Date} timestamp - When event occurred
20
+ * @property {string} type - Event type
21
+ * @property {string} source - Event source (firewall, conductor, git, etc.)
22
+ * @property {string} summary - Human-readable summary
23
+ * @property {Object} details - Full event details
24
+ * @property {string[]} relatedEvents - IDs of related events
25
+ * @property {Object} [causalLink] - Link to caused/causing events
26
+ */
27
+
28
+ /**
29
+ * @typedef {Object} ForensicTimeline
30
+ * @property {string} timelineId - Timeline ID
31
+ * @property {Object} incident - Incident info if applicable
32
+ * @property {TimelineEvent[]} events - All events in timeline
33
+ * @property {Object[]} causalChain - Causal relationships
34
+ * @property {TimelineEvent} [rootCause] - Identified root cause
35
+ * @property {Date} generatedAt - When timeline was generated
36
+ */
37
+
38
+ /**
39
+ * Event types for timeline
40
+ */
41
+ const EVENT_TYPES = {
42
+ PROPOSAL_SUBMITTED: "proposal_submitted",
43
+ PROPOSAL_ALLOWED: "proposal_allowed",
44
+ PROPOSAL_BLOCKED: "proposal_blocked",
45
+ PROPOSAL_WARNED: "proposal_warned",
46
+ OVERRIDE_USED: "override_used",
47
+ FILE_CHANGED: "file_changed",
48
+ SESSION_STARTED: "session_started",
49
+ SESSION_ENDED: "session_ended",
50
+ LOCK_ACQUIRED: "lock_acquired",
51
+ LOCK_RELEASED: "lock_released",
52
+ CONFLICT_DETECTED: "conflict_detected",
53
+ INCIDENT_REPORTED: "incident_reported",
54
+ BUILD_FAILED: "build_failed",
55
+ TEST_FAILED: "test_failed",
56
+ };
57
+
58
+ /**
59
+ * Timeline Builder class
60
+ */
61
+ class TimelineBuilder {
62
+ constructor(options = {}) {
63
+ this.projectRoot = options.projectRoot || process.cwd();
64
+ this.auditDir = path.join(this.projectRoot, ".vibecheck", "audit");
65
+ this.packetsDir = path.join(this.projectRoot, ".vibecheck", "packets");
66
+ this.incidentsDir = path.join(this.projectRoot, ".vibecheck", "incidents");
67
+ }
68
+
69
+ /**
70
+ * Build a timeline for a time range
71
+ * @param {Object} options - Build options
72
+ * @returns {ForensicTimeline} Built timeline
73
+ */
74
+ async buildTimeline(options = {}) {
75
+ const {
76
+ startTime,
77
+ endTime = new Date(),
78
+ file = null,
79
+ agentId = null,
80
+ includeGit = true,
81
+ includeIncidents = true,
82
+ } = options;
83
+
84
+ const timelineId = `timeline_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
85
+ const events = [];
86
+
87
+ // Load firewall events
88
+ const firewallEvents = await this.loadFirewallEvents(startTime, endTime, file, agentId);
89
+ events.push(...firewallEvents);
90
+
91
+ // Load conductor events
92
+ const conductorEvents = await this.loadConductorEvents(startTime, endTime, agentId);
93
+ events.push(...conductorEvents);
94
+
95
+ // Load git events if requested
96
+ if (includeGit) {
97
+ const gitEvents = await this.loadGitEvents(startTime, endTime, file);
98
+ events.push(...gitEvents);
99
+ }
100
+
101
+ // Load incident events if requested
102
+ let incidentInfo = null;
103
+ if (includeIncidents) {
104
+ const incidentEvents = await this.loadIncidentEvents(startTime, endTime, file);
105
+ events.push(...incidentEvents);
106
+
107
+ // Find the main incident if any
108
+ incidentInfo = incidentEvents.find(e => e.type === EVENT_TYPES.INCIDENT_REPORTED)?.details;
109
+ }
110
+
111
+ // Sort by timestamp
112
+ events.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
113
+
114
+ // Build causal relationships
115
+ const { causalChain, rootCause } = this.buildCausalChain(events);
116
+
117
+ // Link related events
118
+ this.linkRelatedEvents(events);
119
+
120
+ return {
121
+ timelineId,
122
+ incident: incidentInfo,
123
+ events,
124
+ causalChain,
125
+ rootCause,
126
+ generatedAt: new Date(),
127
+ options,
128
+ };
129
+ }
130
+
131
+ /**
132
+ * Load firewall events from audit log
133
+ * @param {Date} startTime - Start time
134
+ * @param {Date} endTime - End time
135
+ * @param {string} file - File filter
136
+ * @param {string} agentId - Agent filter
137
+ * @returns {TimelineEvent[]} Firewall events
138
+ */
139
+ async loadFirewallEvents(startTime, endTime, file, agentId) {
140
+ const events = [];
141
+ const auditFile = path.join(this.auditDir, "firewall-events.jsonl");
142
+
143
+ if (!fs.existsSync(auditFile)) {
144
+ return events;
145
+ }
146
+
147
+ try {
148
+ const content = fs.readFileSync(auditFile, "utf-8");
149
+ const lines = content.trim().split("\n").filter(l => l);
150
+
151
+ for (const line of lines) {
152
+ try {
153
+ const raw = JSON.parse(line);
154
+ const eventTime = new Date(raw.timestamp);
155
+
156
+ // Apply filters
157
+ if (startTime && eventTime < new Date(startTime)) continue;
158
+ if (endTime && eventTime > new Date(endTime)) continue;
159
+ if (file && raw.file !== file && !raw.file?.includes(file)) continue;
160
+ if (agentId && raw.agentId !== agentId) continue;
161
+
162
+ events.push({
163
+ id: raw.id || `evt_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
164
+ timestamp: eventTime,
165
+ type: this.mapVerdictToEventType(raw.verdict, raw.action),
166
+ source: "firewall",
167
+ summary: this.buildFirewallSummary(raw),
168
+ details: raw,
169
+ relatedEvents: [],
170
+ });
171
+ } catch {
172
+ // Skip invalid lines
173
+ }
174
+ }
175
+ } catch (error) {
176
+ log.warn(`Failed to load firewall events: ${getErrorMessage(error)}`);
177
+ }
178
+
179
+ return events;
180
+ }
181
+
182
+ /**
183
+ * Load conductor events
184
+ * @param {Date} startTime - Start time
185
+ * @param {Date} endTime - End time
186
+ * @param {string} agentId - Agent filter
187
+ * @returns {TimelineEvent[]} Conductor events
188
+ */
189
+ async loadConductorEvents(startTime, endTime, agentId) {
190
+ const events = [];
191
+ const conductorFile = path.join(this.auditDir, "conductor-events.jsonl");
192
+
193
+ if (!fs.existsSync(conductorFile)) {
194
+ return events;
195
+ }
196
+
197
+ try {
198
+ const content = fs.readFileSync(conductorFile, "utf-8");
199
+ const lines = content.trim().split("\n").filter(l => l);
200
+
201
+ for (const line of lines) {
202
+ try {
203
+ const raw = JSON.parse(line);
204
+ const eventTime = new Date(raw.timestamp);
205
+
206
+ if (startTime && eventTime < new Date(startTime)) continue;
207
+ if (endTime && eventTime > new Date(endTime)) continue;
208
+ if (agentId && raw.agentId !== agentId) continue;
209
+
210
+ events.push({
211
+ id: raw.id || `evt_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
212
+ timestamp: eventTime,
213
+ type: raw.type || raw.action,
214
+ source: "conductor",
215
+ summary: this.buildConductorSummary(raw),
216
+ details: raw,
217
+ relatedEvents: [],
218
+ });
219
+ } catch {
220
+ // Skip invalid lines
221
+ }
222
+ }
223
+ } catch (error) {
224
+ log.warn(`Failed to load conductor events: ${getErrorMessage(error)}`);
225
+ }
226
+
227
+ return events;
228
+ }
229
+
230
+ /**
231
+ * Load git events (commits, merges)
232
+ * @param {Date} startTime - Start time
233
+ * @param {Date} endTime - End time
234
+ * @param {string} file - File filter
235
+ * @returns {TimelineEvent[]} Git events
236
+ */
237
+ async loadGitEvents(startTime, endTime, file) {
238
+ const events = [];
239
+
240
+ // This would typically call git log
241
+ // For now, return empty - would be implemented with git integration
242
+
243
+ return events;
244
+ }
245
+
246
+ /**
247
+ * Load incident events
248
+ * @param {Date} startTime - Start time
249
+ * @param {Date} endTime - End time
250
+ * @param {string} file - File filter
251
+ * @returns {TimelineEvent[]} Incident events
252
+ */
253
+ async loadIncidentEvents(startTime, endTime, file) {
254
+ const events = [];
255
+ const incidentsFile = path.join(this.incidentsDir, "incidents.jsonl");
256
+
257
+ if (!fs.existsSync(incidentsFile)) {
258
+ return events;
259
+ }
260
+
261
+ try {
262
+ const content = fs.readFileSync(incidentsFile, "utf-8");
263
+ const lines = content.trim().split("\n").filter(l => l);
264
+
265
+ for (const line of lines) {
266
+ try {
267
+ const raw = JSON.parse(line);
268
+ const eventTime = new Date(raw.timestamp || raw.reportedAt);
269
+
270
+ if (startTime && eventTime < new Date(startTime)) continue;
271
+ if (endTime && eventTime > new Date(endTime)) continue;
272
+ if (file && !raw.affectedFiles?.includes(file)) continue;
273
+
274
+ events.push({
275
+ id: raw.id || raw.incidentId,
276
+ timestamp: eventTime,
277
+ type: EVENT_TYPES.INCIDENT_REPORTED,
278
+ source: "incident",
279
+ summary: `Incident: ${raw.title || raw.description || "Unknown"}`,
280
+ details: raw,
281
+ relatedEvents: [],
282
+ });
283
+ } catch {
284
+ // Skip invalid lines
285
+ }
286
+ }
287
+ } catch (error) {
288
+ log.warn(`Failed to load incident events: ${getErrorMessage(error)}`);
289
+ }
290
+
291
+ return events;
292
+ }
293
+
294
+ /**
295
+ * Map verdict to event type
296
+ * @param {string} verdict - Verdict
297
+ * @param {string} action - Action
298
+ * @returns {string} Event type
299
+ */
300
+ mapVerdictToEventType(verdict, action) {
301
+ if (action === "override") return EVENT_TYPES.OVERRIDE_USED;
302
+
303
+ switch (verdict) {
304
+ case "ALLOW":
305
+ return EVENT_TYPES.PROPOSAL_ALLOWED;
306
+ case "BLOCK":
307
+ return EVENT_TYPES.PROPOSAL_BLOCKED;
308
+ case "WARN":
309
+ return EVENT_TYPES.PROPOSAL_WARNED;
310
+ default:
311
+ return EVENT_TYPES.PROPOSAL_SUBMITTED;
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Build summary for firewall event
317
+ * @param {Object} raw - Raw event data
318
+ * @returns {string} Summary
319
+ */
320
+ buildFirewallSummary(raw) {
321
+ const parts = [];
322
+
323
+ if (raw.verdict) {
324
+ parts.push(`[${raw.verdict}]`);
325
+ }
326
+
327
+ if (raw.agentId) {
328
+ parts.push(`Agent: ${raw.agentId}`);
329
+ }
330
+
331
+ if (raw.file) {
332
+ parts.push(`File: ${path.basename(raw.file)}`);
333
+ }
334
+
335
+ if (raw.intent) {
336
+ parts.push(raw.intent.slice(0, 50) + (raw.intent.length > 50 ? "..." : ""));
337
+ }
338
+
339
+ return parts.join(" | ") || "Firewall event";
340
+ }
341
+
342
+ /**
343
+ * Build summary for conductor event
344
+ * @param {Object} raw - Raw event data
345
+ * @returns {string} Summary
346
+ */
347
+ buildConductorSummary(raw) {
348
+ const parts = [];
349
+
350
+ if (raw.type || raw.action) {
351
+ parts.push(`[${raw.type || raw.action}]`);
352
+ }
353
+
354
+ if (raw.agentId) {
355
+ parts.push(`Agent: ${raw.agentId}`);
356
+ }
357
+
358
+ if (raw.sessionId) {
359
+ parts.push(`Session: ${raw.sessionId.slice(0, 12)}...`);
360
+ }
361
+
362
+ return parts.join(" | ") || "Conductor event";
363
+ }
364
+
365
+ /**
366
+ * Build causal chain from events
367
+ * @param {TimelineEvent[]} events - Events to analyze
368
+ * @returns {Object} Causal chain and root cause
369
+ */
370
+ buildCausalChain(events) {
371
+ const causalChain = [];
372
+ let rootCause = null;
373
+
374
+ // Find incident events
375
+ const incidents = events.filter(e => e.type === EVENT_TYPES.INCIDENT_REPORTED);
376
+
377
+ if (incidents.length === 0) {
378
+ return { causalChain, rootCause };
379
+ }
380
+
381
+ // For each incident, trace back to find potential causes
382
+ for (const incident of incidents) {
383
+ const affectedFiles = incident.details.affectedFiles || [];
384
+ const incidentTime = new Date(incident.timestamp);
385
+
386
+ // Find events that affected the same files before the incident
387
+ const potentialCauses = events.filter(e => {
388
+ const eventTime = new Date(e.timestamp);
389
+ if (eventTime >= incidentTime) return false;
390
+
391
+ const eventFile = e.details?.file;
392
+ if (!eventFile) return false;
393
+
394
+ return affectedFiles.some(f => f.includes(eventFile) || eventFile.includes(f));
395
+ });
396
+
397
+ // Look for overrides or blocked proposals that were overridden
398
+ for (const cause of potentialCauses) {
399
+ if (cause.type === EVENT_TYPES.OVERRIDE_USED ||
400
+ (cause.type === EVENT_TYPES.PROPOSAL_ALLOWED && cause.details?.overrideUsed)) {
401
+ causalChain.push({
402
+ cause: cause.id,
403
+ effect: incident.id,
404
+ relationship: "override_led_to_incident",
405
+ confidence: 0.8,
406
+ });
407
+
408
+ if (!rootCause) {
409
+ rootCause = cause;
410
+ }
411
+ }
412
+ }
413
+
414
+ // Also look for the first suspicious change
415
+ const firstSuspicious = potentialCauses.find(e =>
416
+ e.type === EVENT_TYPES.PROPOSAL_ALLOWED &&
417
+ (e.details?.riskScore || 0) >= 50
418
+ );
419
+
420
+ if (firstSuspicious && !rootCause) {
421
+ rootCause = firstSuspicious;
422
+ causalChain.push({
423
+ cause: firstSuspicious.id,
424
+ effect: incident.id,
425
+ relationship: "high_risk_change_led_to_incident",
426
+ confidence: 0.6,
427
+ });
428
+ }
429
+ }
430
+
431
+ return { causalChain, rootCause };
432
+ }
433
+
434
+ /**
435
+ * Link related events together
436
+ * @param {TimelineEvent[]} events - Events to link
437
+ */
438
+ linkRelatedEvents(events) {
439
+ // Group events by file
440
+ const byFile = new Map();
441
+
442
+ for (const event of events) {
443
+ const file = event.details?.file;
444
+ if (file) {
445
+ if (!byFile.has(file)) {
446
+ byFile.set(file, []);
447
+ }
448
+ byFile.get(file).push(event);
449
+ }
450
+ }
451
+
452
+ // Link events that share the same file
453
+ for (const [, fileEvents] of byFile) {
454
+ for (let i = 0; i < fileEvents.length; i++) {
455
+ for (let j = i + 1; j < fileEvents.length; j++) {
456
+ fileEvents[i].relatedEvents.push(fileEvents[j].id);
457
+ fileEvents[j].relatedEvents.push(fileEvents[i].id);
458
+ }
459
+ }
460
+ }
461
+
462
+ // Group events by session
463
+ const bySession = new Map();
464
+
465
+ for (const event of events) {
466
+ const sessionId = event.details?.sessionId;
467
+ if (sessionId) {
468
+ if (!bySession.has(sessionId)) {
469
+ bySession.set(sessionId, []);
470
+ }
471
+ bySession.get(sessionId).push(event);
472
+ }
473
+ }
474
+
475
+ // Link events in same session
476
+ for (const [, sessionEvents] of bySession) {
477
+ for (let i = 0; i < sessionEvents.length; i++) {
478
+ for (let j = i + 1; j < sessionEvents.length; j++) {
479
+ if (!sessionEvents[i].relatedEvents.includes(sessionEvents[j].id)) {
480
+ sessionEvents[i].relatedEvents.push(sessionEvents[j].id);
481
+ }
482
+ if (!sessionEvents[j].relatedEvents.includes(sessionEvents[i].id)) {
483
+ sessionEvents[j].relatedEvents.push(sessionEvents[i].id);
484
+ }
485
+ }
486
+ }
487
+ }
488
+ }
489
+
490
+ /**
491
+ * Generate a timeline report
492
+ * @param {ForensicTimeline} timeline - Timeline to report on
493
+ * @returns {Object} Report
494
+ */
495
+ generateReport(timeline) {
496
+ const eventCounts = {};
497
+ const sourceCounts = {};
498
+
499
+ for (const event of timeline.events) {
500
+ eventCounts[event.type] = (eventCounts[event.type] || 0) + 1;
501
+ sourceCounts[event.source] = (sourceCounts[event.source] || 0) + 1;
502
+ }
503
+
504
+ return {
505
+ timelineId: timeline.timelineId,
506
+ generatedAt: timeline.generatedAt,
507
+ totalEvents: timeline.events.length,
508
+ eventCounts,
509
+ sourceCounts,
510
+ hasIncident: !!timeline.incident,
511
+ rootCauseIdentified: !!timeline.rootCause,
512
+ causalChainLength: timeline.causalChain.length,
513
+ timeRange: {
514
+ start: timeline.events[0]?.timestamp,
515
+ end: timeline.events[timeline.events.length - 1]?.timestamp,
516
+ },
517
+ };
518
+ }
519
+ }
520
+
521
+ /**
522
+ * Create a timeline builder instance
523
+ * @param {Object} options - Options
524
+ * @returns {TimelineBuilder} Timeline builder
525
+ */
526
+ function createTimelineBuilder(options = {}) {
527
+ return new TimelineBuilder(options);
528
+ }
529
+
530
+ module.exports = { TimelineBuilder, createTimelineBuilder, EVENT_TYPES };
@@ -177,19 +177,69 @@ function pathLooksLikeAsset(p) {
177
177
  }
178
178
 
179
179
  function isInternalUtilityRoute(p) {
180
- return !!rxTest(/^\/(health|metrics|ready|live|version|debug|internal|security|websocket|ws|admin|dashboard|_|\.)/i, p);
180
+ // Common internal/utility routes that should NOT be flagged as missing
181
+ // These are typically framework-specific, monitoring, or debugging endpoints
182
+ const internalPatterns = [
183
+ // Health/monitoring
184
+ /^\/(health|healthz|healthcheck|ready|readyz|live|livez|liveness|readiness|metrics|status|ping|version)/i,
185
+ // Internal/debug
186
+ /^\/(debug|internal|_internal|__internal|security|\.well-known)/i,
187
+ // WebSockets
188
+ /^\/(websocket|ws|socket\.io|sockjs)/i,
189
+ // Admin/dashboard
190
+ /^\/(admin|dashboard|_admin|__admin)/i,
191
+ // Next.js internals
192
+ /^\/_next\//i,
193
+ // Vite/dev tools
194
+ /^\/@vite|^\/@fs|^\/__vite/i,
195
+ // GraphQL
196
+ /^\/(graphql|graphiql|playground)/i,
197
+ // Swagger/API docs
198
+ /^\/(swagger|api-docs|openapi|docs|redoc)/i,
199
+ // Auth callbacks
200
+ /^\/(auth|oauth|callback|login|logout|signin|signout)\/?(callback|redirect)?$/i,
201
+ // Common framework routes
202
+ /^\/(favicon\.ico|robots\.txt|sitemap\.xml|manifest\.json)$/i,
203
+ // Vercel/serverless
204
+ /^\/api\/_/i,
205
+ // Hidden paths
206
+ /^\/[._]/i,
207
+ ];
208
+ return internalPatterns.some(rx => rxTest(rx, p));
181
209
  }
182
210
 
183
211
  function looksInventedRoute(p) {
184
- // Stuff AI loves to hallucinate - but NOT legitimate test endpoints like /test-email
185
- // Only flag truly fake/placeholder routes, not common test/debug endpoints
186
- if (rxTest(/^\/(fake|dummy|foo|bar|baz|xxx|yyy|placeholder|asdf|qwerty|lorem|ipsum)\b/i, p)) return true;
187
- // Random hashes in path
188
- if (rxTest(/\/[a-f0-9]{32,}\b/i, p)) return true;
189
- // Obvious "ai generated" patterns
190
- if (rxTest(/^\/(generated|auto[-_]?gen)\b/i, p)) return true;
191
- // Obvious placeholder test data patterns (not legitimate /test-* endpoints)
192
- if (rxTest(/\/(test123|abc123|demo123|sample123)\b/i, p)) return true;
212
+ // Stuff AI loves to hallucinate - but be VERY precise to avoid false positives
213
+ // Only flag patterns that are CLEARLY fake/placeholder
214
+
215
+ // Require routes to start with these patterns (not just contain them)
216
+ // This avoids flagging legitimate routes like /users/foo-bar-123
217
+ const clearlyFakeStarts = [
218
+ /^\/(fake|dummy|placeholder|asdf|qwerty|lorem|ipsum)\b/i,
219
+ /^\/(foo|bar|baz|xxx|yyy)$/i, // Only if it's the ENTIRE route segment
220
+ ];
221
+
222
+ for (const rx of clearlyFakeStarts) {
223
+ if (rxTest(rx, p)) return true;
224
+ }
225
+
226
+ // Obvious "ai generated" patterns - must be at route start
227
+ if (rxTest(/^\/(generated|auto[-_]?gen|ai[-_]?gen)\b/i, p)) return true;
228
+
229
+ // Obvious placeholder test data patterns (test123, abc123, demo123)
230
+ // Only if the ENTIRE segment is clearly placeholder
231
+ if (rxTest(/\/(test123|abc123|demo123|sample123|example123)$/i, p)) return true;
232
+
233
+ // Very long hex strings in route (32+ chars) that aren't IDs
234
+ // Skip if it looks like a valid UUID pattern or session token
235
+ const segments = p.split('/').filter(Boolean);
236
+ for (const seg of segments) {
237
+ // Long hex that's NOT a UUID format and NOT a reasonable ID length
238
+ if (/^[a-f0-9]{40,}$/i.test(seg) && !/^[a-f0-9]{8}-/.test(seg)) {
239
+ return true;
240
+ }
241
+ }
242
+
193
243
  return false;
194
244
  }
195
245
 
@@ -445,7 +495,9 @@ function findMissingRoutes(truthpack) {
445
495
  .sort((a, b) => b.score - a.score)
446
496
  .slice(0, 3);
447
497
 
448
- return scored.filter((x) => x.score >= 0.35).map((x) => ({
498
+ // Raise threshold to 0.50 to only show genuinely similar routes
499
+ // This reduces "did you mean" noise for unrelated routes
500
+ return scored.filter((x) => x.score >= 0.50).map((x) => ({
449
501
  method: x.r._method,
450
502
  path: x.r._pathNorm,
451
503
  score: Number(x.score.toFixed(2)),
@@ -553,28 +605,39 @@ function findMissingRoutes(truthpack) {
553
605
 
554
606
  const invented = looksInventedRoute(pNorm);
555
607
  const internal = isInternalUtilityRoute(pNorm);
608
+
609
+ // Skip internal utility routes entirely - they're almost never real issues
610
+ if (internal) continue;
556
611
 
557
612
  // Similarity suggestions
558
613
  const suggestions = closestSuggestions(method, pNorm);
559
614
 
560
- // Confidence + severity gating
615
+ // Confidence + severity gating - CONSERVATIVE by default to reduce noise
561
616
  let confidence = "low";
562
617
  let severity = "WARN";
563
618
 
564
- if (invented && !internal) {
619
+ if (invented) {
620
+ // Only BLOCK truly invented routes - and require high confidence
565
621
  severity = "BLOCK";
566
622
  confidence = "high";
567
- } else if (routeMapQuality === "strong" && !isLikelyMonorepo && !internal) {
568
- // Only escalate if route map quality is strong and it doesn't look like a monorepo
623
+ } else if (routeMapQuality === "strong" && !isLikelyMonorepo) {
624
+ // Even with strong route map, be conservative
569
625
  const best = suggestions[0]?.score ?? 0;
570
- // If there's no close suggestion, it's more likely actually missing
571
- if (best < 0.40) {
572
- severity = "WARN"; // keep WARN by default; you can flip to BLOCK if you want
626
+ if (best < 0.30) {
627
+ // No close matches - more likely to be a real issue, but still WARN
573
628
  confidence = "med";
629
+ severity = "WARN";
630
+ } else if (best >= 0.70) {
631
+ // Very close match exists - probably a typo, keep as low-priority WARN
632
+ confidence = "low";
633
+ severity = "WARN";
574
634
  } else {
635
+ // Moderate similarity - unclear
575
636
  confidence = "low";
637
+ severity = "WARN";
576
638
  }
577
639
  } else {
640
+ // Weak route map or monorepo - don't trust findings, always WARN with low confidence
578
641
  confidence = "low";
579
642
  severity = "WARN";
580
643
  }