@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.
- package/bin/registry.js +192 -5
- package/bin/runners/lib/agent-firewall/change-packet/builder.js +280 -6
- package/bin/runners/lib/agent-firewall/critic/index.js +151 -0
- package/bin/runners/lib/agent-firewall/critic/judge.js +432 -0
- package/bin/runners/lib/agent-firewall/critic/prompts.js +305 -0
- package/bin/runners/lib/agent-firewall/lawbook/distributor.js +465 -0
- package/bin/runners/lib/agent-firewall/lawbook/evaluator.js +604 -0
- package/bin/runners/lib/agent-firewall/lawbook/index.js +304 -0
- package/bin/runners/lib/agent-firewall/lawbook/registry.js +514 -0
- package/bin/runners/lib/agent-firewall/lawbook/schema.js +420 -0
- package/bin/runners/lib/agent-firewall/logger.js +141 -0
- package/bin/runners/lib/agent-firewall/policy/loader.js +312 -4
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +113 -1
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +133 -6
- package/bin/runners/lib/agent-firewall/proposal/extractor.js +394 -0
- package/bin/runners/lib/agent-firewall/proposal/index.js +212 -0
- package/bin/runners/lib/agent-firewall/proposal/schema.js +251 -0
- package/bin/runners/lib/agent-firewall/proposal/validator.js +386 -0
- package/bin/runners/lib/agent-firewall/reality/index.js +332 -0
- package/bin/runners/lib/agent-firewall/reality/state.js +625 -0
- package/bin/runners/lib/agent-firewall/reality/watcher.js +322 -0
- package/bin/runners/lib/agent-firewall/risk/index.js +173 -0
- package/bin/runners/lib/agent-firewall/risk/scorer.js +328 -0
- package/bin/runners/lib/agent-firewall/risk/thresholds.js +321 -0
- package/bin/runners/lib/agent-firewall/risk/vectors.js +421 -0
- package/bin/runners/lib/agent-firewall/simulator/diff-simulator.js +472 -0
- package/bin/runners/lib/agent-firewall/simulator/import-resolver.js +346 -0
- package/bin/runners/lib/agent-firewall/simulator/index.js +181 -0
- package/bin/runners/lib/agent-firewall/simulator/route-validator.js +380 -0
- package/bin/runners/lib/agent-firewall/time-machine/incident-correlator.js +661 -0
- package/bin/runners/lib/agent-firewall/time-machine/index.js +267 -0
- package/bin/runners/lib/agent-firewall/time-machine/replay-engine.js +436 -0
- package/bin/runners/lib/agent-firewall/time-machine/state-reconstructor.js +490 -0
- package/bin/runners/lib/agent-firewall/time-machine/timeline-builder.js +530 -0
- package/bin/runners/lib/analyzers.js +81 -18
- package/bin/runners/lib/authority-badge.js +425 -0
- package/bin/runners/lib/cli-output.js +7 -1
- package/bin/runners/lib/error-handler.js +16 -9
- package/bin/runners/lib/exit-codes.js +275 -0
- package/bin/runners/lib/global-flags.js +37 -0
- package/bin/runners/lib/help-formatter.js +413 -0
- package/bin/runners/lib/logger.js +38 -0
- package/bin/runners/lib/unified-cli-output.js +604 -0
- package/bin/runners/lib/upsell.js +148 -0
- package/bin/runners/runApprove.js +1200 -0
- package/bin/runners/runAuth.js +324 -95
- package/bin/runners/runCheckpoint.js +39 -21
- package/bin/runners/runClassify.js +859 -0
- package/bin/runners/runContext.js +136 -24
- package/bin/runners/runDoctor.js +108 -68
- package/bin/runners/runFix.js +6 -5
- package/bin/runners/runGuard.js +212 -118
- package/bin/runners/runInit.js +3 -2
- package/bin/runners/runMcp.js +130 -52
- package/bin/runners/runPolish.js +43 -20
- package/bin/runners/runProve.js +1 -2
- package/bin/runners/runReport.js +3 -2
- package/bin/runners/runScan.js +63 -44
- package/bin/runners/runShip.js +3 -4
- package/bin/runners/runValidate.js +19 -2
- package/bin/runners/runWatch.js +104 -53
- package/bin/vibecheck.js +106 -19
- package/mcp-server/HARDENING_SUMMARY.md +299 -0
- package/mcp-server/agent-firewall-interceptor.js +367 -31
- package/mcp-server/authority-tools.js +569 -0
- package/mcp-server/conductor/conflict-resolver.js +588 -0
- package/mcp-server/conductor/execution-planner.js +544 -0
- package/mcp-server/conductor/index.js +377 -0
- package/mcp-server/conductor/lock-manager.js +615 -0
- package/mcp-server/conductor/request-queue.js +550 -0
- package/mcp-server/conductor/session-manager.js +500 -0
- package/mcp-server/conductor/tools.js +510 -0
- package/mcp-server/index.js +1149 -243
- package/mcp-server/lib/{api-client.js → api-client.cjs} +40 -4
- package/mcp-server/lib/logger.cjs +30 -0
- package/mcp-server/logger.js +173 -0
- package/mcp-server/package.json +2 -2
- package/mcp-server/premium-tools.js +2 -2
- package/mcp-server/tier-auth.js +245 -35
- package/mcp-server/truth-firewall-tools.js +145 -15
- package/mcp-server/vibecheck-tools.js +2 -2
- package/package.json +2 -3
- package/mcp-server/index.old.js +0 -4137
- 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
|
-
|
|
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
|
|
185
|
-
// Only flag
|
|
186
|
-
|
|
187
|
-
//
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
568
|
-
//
|
|
623
|
+
} else if (routeMapQuality === "strong" && !isLikelyMonorepo) {
|
|
624
|
+
// Even with strong route map, be conservative
|
|
569
625
|
const best = suggestions[0]?.score ?? 0;
|
|
570
|
-
|
|
571
|
-
|
|
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
|
}
|