brakit 0.9.2 → 0.10.1

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.
@@ -15,7 +15,7 @@ var __export = (target, all) => {
15
15
  };
16
16
 
17
17
  // src/constants/config.ts
18
- var MAX_REQUEST_ENTRIES, DEFAULT_MAX_BODY_CAPTURE, DEFAULT_API_LIMIT, MAX_TELEMETRY_ENTRIES, MAX_TAB_NAME_LENGTH, MAX_INGEST_BYTES, TERMINAL_TRUNCATE_LENGTH, SENSITIVE_MASK_MIN_LENGTH, SENSITIVE_MASK_VISIBLE_CHARS, MAX_JSON_BODY_BYTES, ANALYSIS_DEBOUNCE_MS, ISSUE_ID_HASH_LENGTH, ISSUES_DATA_VERSION, SENSITIVE_MASK_PLACEHOLDER, PROJECT_HASH_LENGTH, SECRET_SCAN_ARRAY_LIMIT, PII_SCAN_ARRAY_LIMIT, MIN_SECRET_VALUE_LENGTH, FULL_RECORD_MIN_FIELDS, LIST_PII_MIN_ITEMS, MAX_API_LIMIT, MAX_OBJECT_SCAN_DEPTH, MAX_UNIQUE_ENDPOINTS, MAX_ACCUMULATOR_ENTRIES, ISSUE_PRUNE_TTL_MS, FLOW_GAP_MS, SLOW_REQUEST_THRESHOLD_MS, MIN_POLLING_SEQUENCE, ENDPOINT_TRUNCATE_LENGTH, N1_QUERY_THRESHOLD, ERROR_RATE_THRESHOLD_PCT, MIN_REQUESTS_FOR_INSIGHT, HIGH_QUERY_COUNT_PER_REQ, CROSS_ENDPOINT_MIN_ENDPOINTS, CROSS_ENDPOINT_PCT, CROSS_ENDPOINT_MIN_OCCURRENCES, REDUNDANT_QUERY_MIN_COUNT, LARGE_RESPONSE_BYTES, HIGH_ROW_COUNT, OVERFETCH_MIN_REQUESTS, OVERFETCH_MIN_FIELDS, OVERFETCH_MIN_INTERNAL_IDS, OVERFETCH_NULL_RATIO, REGRESSION_PCT_THRESHOLD, REGRESSION_MIN_INCREASE_MS, REGRESSION_MIN_REQUESTS, QUERY_COUNT_REGRESSION_RATIO, OVERFETCH_MANY_FIELDS, OVERFETCH_UNWRAP_MIN_SIZE, MAX_DUPLICATE_INSIGHTS, INSIGHT_WINDOW_PER_ENDPOINT, CLEAN_HITS_FOR_RESOLUTION, STALE_ISSUE_TTL_MS, STRICT_MODE_MAX_GAP_MS, BASELINE_MIN_SESSIONS, BASELINE_MIN_REQUESTS_PER_SESSION, BASELINE_PENDING_POINTS_MIN, METRICS_DIR, METRICS_FILE, PORT_FILE, ISSUES_FILE, METRICS_FLUSH_INTERVAL_MS, METRICS_MAX_SESSIONS, METRICS_MAX_DATA_POINTS, ISSUES_FLUSH_INTERVAL_MS, SSE_HEARTBEAT_INTERVAL_MS, NOISE_HOSTS, NOISE_PATH_PATTERNS, VALID_ISSUE_STATES, VALID_ISSUE_CATEGORIES, VALID_AI_FIX_STATUSES, TELEMETRY_EVENT_SETUP_COMPLETED, TELEMETRY_EVENT_FIRST_REQUEST, TELEMETRY_EVENT_DASHBOARD_VIEWED, TELEMETRY_EVENT_SESSION, EXIT_REASON_CLEAN, EXIT_REASON_SIGINT, EXIT_REASON_SIGTERM, DETAIL_PREVIEW_LENGTH, KNOWN_DEPENDENCY_NAMES;
18
+ var MAX_REQUEST_ENTRIES, DEFAULT_MAX_BODY_CAPTURE, DEFAULT_API_LIMIT, MAX_TELEMETRY_ENTRIES, MAX_TAB_NAME_LENGTH, MAX_INGEST_BYTES, TERMINAL_TRUNCATE_LENGTH, SENSITIVE_MASK_MIN_LENGTH, SENSITIVE_MASK_VISIBLE_CHARS, MAX_JSON_BODY_BYTES, ANALYSIS_DEBOUNCE_MS, ISSUE_ID_HASH_LENGTH, ISSUES_DATA_VERSION, SENSITIVE_MASK_PLACEHOLDER, PROJECT_HASH_LENGTH, SECRET_SCAN_ARRAY_LIMIT, PII_SCAN_ARRAY_LIMIT, MIN_SECRET_VALUE_LENGTH, FULL_RECORD_MIN_FIELDS, LIST_PII_MIN_ITEMS, MAX_API_LIMIT, MAX_OBJECT_SCAN_DEPTH, MAX_UNIQUE_ENDPOINTS, MAX_ACCUMULATOR_ENTRIES, ISSUE_PRUNE_TTL_MS, FLOW_GAP_MS, SLOW_REQUEST_THRESHOLD_MS, MIN_POLLING_SEQUENCE, ENDPOINT_TRUNCATE_LENGTH, N1_QUERY_THRESHOLD, ERROR_RATE_THRESHOLD_PCT, MIN_REQUESTS_FOR_INSIGHT, HIGH_QUERY_COUNT_PER_REQ, CROSS_ENDPOINT_MIN_ENDPOINTS, CROSS_ENDPOINT_PCT, CROSS_ENDPOINT_MIN_OCCURRENCES, REDUNDANT_QUERY_MIN_COUNT, LARGE_RESPONSE_BYTES, HIGH_ROW_COUNT, OVERFETCH_MIN_REQUESTS, OVERFETCH_MIN_FIELDS, OVERFETCH_MIN_INTERNAL_IDS, OVERFETCH_NULL_RATIO, REGRESSION_PCT_THRESHOLD, REGRESSION_MIN_INCREASE_MS, REGRESSION_MIN_REQUESTS, QUERY_COUNT_REGRESSION_RATIO, OVERFETCH_MANY_FIELDS, OVERFETCH_UNWRAP_MIN_SIZE, MAX_DUPLICATE_INSIGHTS, INSIGHT_WINDOW_PER_ENDPOINT, CLEAN_HITS_FOR_RESOLUTION, STALE_ISSUE_TTL_MS, STRICT_MODE_MAX_GAP_MS, BASELINE_MIN_SESSIONS, BASELINE_MIN_REQUESTS_PER_SESSION, BASELINE_PENDING_POINTS_MIN, METRICS_DIR, METRICS_FILE, PORT_FILE, ISSUES_FILE, METRICS_FLUSH_INTERVAL_MS, METRICS_MAX_SESSIONS, METRICS_MAX_DATA_POINTS, ISSUES_FLUSH_INTERVAL_MS, SSE_HEARTBEAT_INTERVAL_MS, NOISE_HOSTS, NOISE_PATH_PATTERNS, VALID_ISSUE_STATES, VALID_ISSUE_CATEGORIES, VALID_AI_FIX_STATUSES, TELEMETRY_EVENT_SETUP_COMPLETED, TELEMETRY_EVENT_FIRST_REQUEST, TELEMETRY_EVENT_DASHBOARD_VIEWED, TELEMETRY_EVENT_SESSION, TELEMETRY_EVENT_GRAPH_FEATURE, EXIT_REASON_CLEAN, EXIT_REASON_SIGINT, EXIT_REASON_SIGTERM, DETAIL_PREVIEW_LENGTH, KNOWN_DEPENDENCY_NAMES;
19
19
  var init_config = __esm({
20
20
  "src/constants/config.ts"() {
21
21
  "use strict";
@@ -94,6 +94,7 @@ var init_config = __esm({
94
94
  TELEMETRY_EVENT_FIRST_REQUEST = "first_request";
95
95
  TELEMETRY_EVENT_DASHBOARD_VIEWED = "dashboard_viewed";
96
96
  TELEMETRY_EVENT_SESSION = "session";
97
+ TELEMETRY_EVENT_GRAPH_FEATURE = "graph_feature";
97
98
  EXIT_REASON_CLEAN = "clean";
98
99
  EXIT_REASON_SIGINT = "sigint";
99
100
  EXIT_REASON_SIGTERM = "sigterm";
@@ -104,11 +105,14 @@ var init_config = __esm({
104
105
  "nuxt",
105
106
  "vite",
106
107
  "astro",
108
+ "@nestjs/core",
109
+ "@adonisjs/core",
110
+ "sails",
107
111
  "express",
108
112
  "fastify",
109
113
  "hono",
110
114
  "koa",
111
- "nest",
115
+ "@hapi/hapi",
112
116
  "prisma",
113
117
  "drizzle-orm",
114
118
  "typeorm",
@@ -118,7 +122,7 @@ var init_config = __esm({
118
122
  });
119
123
 
120
124
  // src/constants/labels.ts
121
- var DASHBOARD_PREFIX, DASHBOARD_API_REQUESTS, DASHBOARD_API_EVENTS, DASHBOARD_API_FLOWS, DASHBOARD_API_CLEAR, DASHBOARD_API_LOGS, DASHBOARD_API_FETCHES, DASHBOARD_API_ERRORS, DASHBOARD_API_QUERIES, DASHBOARD_API_INGEST, DASHBOARD_API_METRICS, DASHBOARD_API_ACTIVITY, DASHBOARD_API_METRICS_LIVE, DASHBOARD_API_INSIGHTS, DASHBOARD_API_SECURITY, DASHBOARD_API_TAB, DASHBOARD_API_FINDINGS, DASHBOARD_API_FINDINGS_REPORT, VALID_TABS_TUPLE, VALID_TABS, BRAKIT_REQUEST_ID_HEADER, BRAKIT_FETCH_ID_HEADER, SENSITIVE_HEADER_NAMES, HTTP_OK, HTTP_NO_CONTENT, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_METHOD_NOT_ALLOWED, HTTP_PAYLOAD_TOO_LARGE, HTTP_INTERNAL_ERROR, SECURITY_HEADERS, CONTENT_ENCODING_GZIP, CONTENT_ENCODING_BR, CONTENT_ENCODING_DEFLATE, SEVERITY_ICON, SSE_EVENT_FETCH, SSE_EVENT_LOG, SSE_EVENT_ERROR, SSE_EVENT_QUERY, SSE_EVENT_ISSUES, SDK_EVENT_REQUEST, SDK_EVENT_DB_QUERY, SDK_EVENT_FETCH, SDK_EVENT_LOG, SDK_EVENT_ERROR, SDK_EVENT_AUTH_CHECK, POSTHOG_HOST, POSTHOG_CAPTURE_PATH, POSTHOG_REQUEST_TIMEOUT_MS, SPEED_BUCKET_THRESHOLDS, TIMELINE_FETCH, TIMELINE_LOG, TIMELINE_ERROR, TIMELINE_QUERY;
125
+ var DASHBOARD_PREFIX, DASHBOARD_API_REQUESTS, DASHBOARD_API_EVENTS, DASHBOARD_API_FLOWS, DASHBOARD_API_CLEAR, DASHBOARD_API_LOGS, DASHBOARD_API_FETCHES, DASHBOARD_API_ERRORS, DASHBOARD_API_QUERIES, DASHBOARD_API_INGEST, DASHBOARD_API_METRICS, DASHBOARD_API_ACTIVITY, DASHBOARD_API_METRICS_LIVE, DASHBOARD_API_INSIGHTS, DASHBOARD_API_SECURITY, DASHBOARD_API_TAB, DASHBOARD_API_FINDINGS, DASHBOARD_API_FINDINGS_REPORT, DASHBOARD_API_GRAPH, VALID_TABS_TUPLE, VALID_TABS, BRAKIT_REQUEST_ID_HEADER, BRAKIT_FETCH_ID_HEADER, SENSITIVE_HEADER_NAMES, HTTP_OK, HTTP_NO_CONTENT, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_METHOD_NOT_ALLOWED, HTTP_PAYLOAD_TOO_LARGE, HTTP_INTERNAL_ERROR, SECURITY_HEADERS, CONTENT_ENCODING_GZIP, CONTENT_ENCODING_BR, CONTENT_ENCODING_DEFLATE, SEVERITY_ICON, SSE_EVENT_FETCH, SSE_EVENT_LOG, SSE_EVENT_ERROR, SSE_EVENT_QUERY, SSE_EVENT_ISSUES, SDK_EVENT_REQUEST, SDK_EVENT_DB_QUERY, SDK_EVENT_FETCH, SDK_EVENT_LOG, SDK_EVENT_ERROR, SDK_EVENT_AUTH_CHECK, POSTHOG_HOST, POSTHOG_CAPTURE_PATH, POSTHOG_REQUEST_TIMEOUT_MS, SPEED_BUCKET_THRESHOLDS, TIMELINE_FETCH, TIMELINE_LOG, TIMELINE_ERROR, TIMELINE_QUERY;
122
126
  var init_labels = __esm({
123
127
  "src/constants/labels.ts"() {
124
128
  "use strict";
@@ -140,16 +144,14 @@ var init_labels = __esm({
140
144
  DASHBOARD_API_TAB = `${DASHBOARD_PREFIX}/api/tab`;
141
145
  DASHBOARD_API_FINDINGS = `${DASHBOARD_PREFIX}/api/findings`;
142
146
  DASHBOARD_API_FINDINGS_REPORT = `${DASHBOARD_PREFIX}/api/findings/report`;
147
+ DASHBOARD_API_GRAPH = `${DASHBOARD_PREFIX}/api/graph`;
143
148
  VALID_TABS_TUPLE = [
144
149
  "overview",
145
150
  "actions",
146
- "requests",
147
- "fetches",
148
- "queries",
149
- "errors",
150
- "logs",
151
+ "insights",
151
152
  "performance",
152
- "security"
153
+ "graph",
154
+ "explorer"
153
155
  ];
154
156
  VALID_TABS = new Set(VALID_TABS_TUPLE);
155
157
  BRAKIT_REQUEST_ID_HEADER = "x-brakit-request-id";
@@ -173,7 +175,7 @@ var init_labels = __esm({
173
175
  "x-content-type-options": "nosniff",
174
176
  "x-frame-options": "DENY",
175
177
  "referrer-policy": "no-referrer",
176
- "content-security-policy": "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'; img-src data:"
178
+ "content-security-policy": "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'; img-src data: blob:"
177
179
  };
178
180
  CONTENT_ENCODING_GZIP = "gzip";
179
181
  CONTENT_ENCODING_BR = "br";
@@ -336,6 +338,214 @@ var init_fetch = __esm({
336
338
  }
337
339
  });
338
340
 
341
+ // src/utils/log.ts
342
+ function brakitWarn(message) {
343
+ process.stderr.write(`${PREFIX} ${message}
344
+ `);
345
+ }
346
+ function brakitDebug(message) {
347
+ if (process.env.DEBUG_BRAKIT) {
348
+ process.stderr.write(`${PREFIX}:debug ${message}
349
+ `);
350
+ }
351
+ }
352
+ var PREFIX;
353
+ var init_log = __esm({
354
+ "src/utils/log.ts"() {
355
+ "use strict";
356
+ PREFIX = "[brakit]";
357
+ }
358
+ });
359
+
360
+ // src/utils/type-guards.ts
361
+ function isString(val) {
362
+ return typeof val === "string";
363
+ }
364
+ function isNumber(val) {
365
+ return typeof val === "number" && !isNaN(val);
366
+ }
367
+ function isBoolean(val) {
368
+ return typeof val === "boolean";
369
+ }
370
+ function isThenable(value) {
371
+ return value != null && typeof value.then === "function";
372
+ }
373
+ function getErrorMessage(err) {
374
+ if (err instanceof Error) return err.message;
375
+ if (typeof err === "string") return err;
376
+ return String(err);
377
+ }
378
+ function isValidIssueState(val) {
379
+ return typeof val === "string" && VALID_ISSUE_STATES.has(val);
380
+ }
381
+ function isValidIssueCategory(val) {
382
+ return typeof val === "string" && VALID_ISSUE_CATEGORIES.has(val);
383
+ }
384
+ function isValidAiFixStatus(val) {
385
+ return typeof val === "string" && VALID_AI_FIX_STATUSES.has(val);
386
+ }
387
+ function validateIssuesData(parsed) {
388
+ if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
389
+ const obj = parsed;
390
+ if (obj.version === ISSUES_DATA_VERSION && Array.isArray(obj.issues)) {
391
+ return parsed;
392
+ }
393
+ return null;
394
+ }
395
+ function validateMetricsData(parsed) {
396
+ if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
397
+ const obj = parsed;
398
+ if (obj.version === 1 && Array.isArray(obj.endpoints)) {
399
+ return parsed;
400
+ }
401
+ return null;
402
+ }
403
+ var init_type_guards = __esm({
404
+ "src/utils/type-guards.ts"() {
405
+ "use strict";
406
+ init_config();
407
+ }
408
+ });
409
+
410
+ // src/runtime/capture.ts
411
+ import { gunzip, brotliDecompress, inflate } from "zlib";
412
+ function outgoingToIncoming(headers2) {
413
+ const result = {};
414
+ for (const [key, value] of Object.entries(headers2)) {
415
+ if (value === void 0) continue;
416
+ if (Array.isArray(value)) {
417
+ result[key] = value.map(String);
418
+ } else {
419
+ result[key] = String(value);
420
+ }
421
+ }
422
+ return result;
423
+ }
424
+ function getDecompressor(encoding) {
425
+ if (encoding === CONTENT_ENCODING_GZIP) return gunzip;
426
+ if (encoding === CONTENT_ENCODING_BR) return brotliDecompress;
427
+ if (encoding === CONTENT_ENCODING_DEFLATE) return inflate;
428
+ return null;
429
+ }
430
+ function decompressAsync(body, encoding) {
431
+ const decompressor = getDecompressor(encoding);
432
+ if (!decompressor) return Promise.resolve(body);
433
+ return new Promise((resolve6) => {
434
+ decompressor(body, (err, result) => {
435
+ resolve6(err ? body : result);
436
+ });
437
+ });
438
+ }
439
+ function toBuffer(chunk) {
440
+ if (Buffer.isBuffer(chunk)) return chunk;
441
+ if (chunk instanceof Uint8Array) return Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
442
+ if (typeof chunk === "string") return Buffer.from(chunk);
443
+ return null;
444
+ }
445
+ function drainPendingCaptures() {
446
+ if (pendingCaptures === 0) return Promise.resolve();
447
+ return new Promise((resolve6) => {
448
+ drainResolvers.push(resolve6);
449
+ });
450
+ }
451
+ function onCaptureSettled() {
452
+ pendingCaptures--;
453
+ if (pendingCaptures === 0 && drainResolvers.length > 0) {
454
+ const resolvers = drainResolvers;
455
+ drainResolvers = [];
456
+ for (const resolve6 of resolvers) resolve6();
457
+ }
458
+ }
459
+ function captureInProcess(req, res, requestId, requestStore, isChild = false) {
460
+ const startTime = performance.now();
461
+ const method = req.method ?? "GET";
462
+ const resChunks = [];
463
+ let resSize = 0;
464
+ const originalWrite = res.write;
465
+ const originalEnd = res.end;
466
+ let truncated = false;
467
+ res.write = function(...args) {
468
+ try {
469
+ const chunk = args[0];
470
+ if (chunk != null && typeof chunk !== "function") {
471
+ if (resSize < DEFAULT_MAX_BODY_CAPTURE) {
472
+ const buf = toBuffer(chunk);
473
+ if (buf) {
474
+ resChunks.push(buf);
475
+ resSize += buf.length;
476
+ }
477
+ } else {
478
+ truncated = true;
479
+ }
480
+ }
481
+ } catch (e) {
482
+ brakitDebug(`capture write: ${getErrorMessage(e)}`);
483
+ }
484
+ return originalWrite.apply(this, args);
485
+ };
486
+ res.end = function(...args) {
487
+ try {
488
+ const chunk = typeof args[0] !== "function" ? args[0] : void 0;
489
+ if (chunk != null && resSize < DEFAULT_MAX_BODY_CAPTURE) {
490
+ const buf = toBuffer(chunk);
491
+ if (buf) {
492
+ resChunks.push(buf);
493
+ }
494
+ }
495
+ } catch (e) {
496
+ brakitDebug(`capture end: ${getErrorMessage(e)}`);
497
+ }
498
+ const result = originalEnd.apply(this, args);
499
+ const endTime = performance.now();
500
+ const encoding = String(res.getHeader("content-encoding") ?? "").toLowerCase();
501
+ const statusCode = res.statusCode;
502
+ const responseHeaders = outgoingToIncoming(res.getHeaders());
503
+ const responseContentType = String(res.getHeader("content-type") ?? "");
504
+ const capturedChunks = resChunks.slice();
505
+ if (!isChild) {
506
+ pendingCaptures++;
507
+ void (async () => {
508
+ try {
509
+ let body = capturedChunks.length > 0 ? Buffer.concat(capturedChunks) : null;
510
+ if (body && encoding && !truncated) {
511
+ body = await decompressAsync(body, encoding);
512
+ }
513
+ requestStore.capture({
514
+ requestId,
515
+ method,
516
+ url: req.url ?? "/",
517
+ requestHeaders: req.headers,
518
+ requestBody: null,
519
+ statusCode,
520
+ responseHeaders,
521
+ responseBody: body,
522
+ responseContentType,
523
+ startTime,
524
+ endTime,
525
+ config: { maxBodyCapture: DEFAULT_MAX_BODY_CAPTURE }
526
+ });
527
+ } catch (e) {
528
+ brakitDebug(`capture store: ${getErrorMessage(e)}`);
529
+ } finally {
530
+ onCaptureSettled();
531
+ }
532
+ })();
533
+ }
534
+ return result;
535
+ };
536
+ }
537
+ var pendingCaptures, drainResolvers;
538
+ var init_capture = __esm({
539
+ "src/runtime/capture.ts"() {
540
+ "use strict";
541
+ init_constants();
542
+ init_log();
543
+ init_type_guards();
544
+ pendingCaptures = 0;
545
+ drainResolvers = [];
546
+ }
547
+ });
548
+
339
549
  // src/instrument/hooks/console.ts
340
550
  import { format } from "util";
341
551
  function setupConsoleHook(emit) {
@@ -486,7 +696,15 @@ var init_adapter_registry = __esm({
486
696
  function normalizeSQL(sql) {
487
697
  if (!sql) return { op: "OTHER", table: "" };
488
698
  const trimmed = sql.trim();
489
- const keyword = trimmed.split(/\s+/, 1)[0].toUpperCase();
699
+ let spaceIdx = -1;
700
+ for (let i = 0; i < trimmed.length; i++) {
701
+ const c = trimmed[i];
702
+ if (c === " " || c === " " || c === "\n" || c === "\r") {
703
+ spaceIdx = i;
704
+ break;
705
+ }
706
+ }
707
+ const keyword = (spaceIdx === -1 ? trimmed : trimmed.slice(0, spaceIdx)).toUpperCase();
490
708
  const op = VALID_OPS.has(keyword) ? keyword : "OTHER";
491
709
  const table = trimmed.match(TABLE_RE)?.[1] ?? "";
492
710
  return { op, table };
@@ -528,75 +746,6 @@ var init_normalize = __esm({
528
746
  }
529
747
  });
530
748
 
531
- // src/utils/type-guards.ts
532
- function isString(val) {
533
- return typeof val === "string";
534
- }
535
- function isNumber(val) {
536
- return typeof val === "number" && !isNaN(val);
537
- }
538
- function isBoolean(val) {
539
- return typeof val === "boolean";
540
- }
541
- function isThenable(value) {
542
- return value != null && typeof value.then === "function";
543
- }
544
- function getErrorMessage(err) {
545
- if (err instanceof Error) return err.message;
546
- if (typeof err === "string") return err;
547
- return String(err);
548
- }
549
- function isValidIssueState(val) {
550
- return typeof val === "string" && VALID_ISSUE_STATES.has(val);
551
- }
552
- function isValidIssueCategory(val) {
553
- return typeof val === "string" && VALID_ISSUE_CATEGORIES.has(val);
554
- }
555
- function isValidAiFixStatus(val) {
556
- return typeof val === "string" && VALID_AI_FIX_STATUSES.has(val);
557
- }
558
- function validateIssuesData(parsed) {
559
- if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
560
- const obj = parsed;
561
- if (obj.version === ISSUES_DATA_VERSION && Array.isArray(obj.issues)) {
562
- return parsed;
563
- }
564
- return null;
565
- }
566
- function validateMetricsData(parsed) {
567
- if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
568
- const obj = parsed;
569
- if (obj.version === 1 && Array.isArray(obj.endpoints)) {
570
- return parsed;
571
- }
572
- return null;
573
- }
574
- var init_type_guards = __esm({
575
- "src/utils/type-guards.ts"() {
576
- "use strict";
577
- init_config();
578
- }
579
- });
580
-
581
- // src/utils/log.ts
582
- function brakitWarn(message) {
583
- process.stderr.write(`${PREFIX} ${message}
584
- `);
585
- }
586
- function brakitDebug(message) {
587
- if (process.env.DEBUG_BRAKIT) {
588
- process.stderr.write(`${PREFIX}:debug ${message}
589
- `);
590
- }
591
- }
592
- var PREFIX;
593
- var init_log = __esm({
594
- "src/utils/log.ts"() {
595
- "use strict";
596
- PREFIX = "[brakit]";
597
- }
598
- });
599
-
600
749
  // src/instrument/adapters/shared.ts
601
750
  import { createRequire } from "module";
602
751
  function tryRequire(id) {
@@ -862,11 +1011,54 @@ var init_adapters = __esm({
862
1011
  init_mysql2();
863
1012
  init_prisma();
864
1013
  }
865
- });
866
-
867
- // src/utils/endpoint.ts
1014
+ });
1015
+
1016
+ // src/utils/endpoint.ts
1017
+ function isUUID(s) {
1018
+ if (s.length !== UUID_LEN) return false;
1019
+ for (let i = 0; i < s.length; i++) {
1020
+ const c = s[i];
1021
+ if (i === 8 || i === 13 || i === 18 || i === 23) {
1022
+ if (c !== "-") return false;
1023
+ } else {
1024
+ if (!isHexChar(c)) return false;
1025
+ }
1026
+ }
1027
+ return true;
1028
+ }
1029
+ function isHexChar(c) {
1030
+ const code = c.charCodeAt(0);
1031
+ return code >= 48 && code <= 57 || code >= 65 && code <= 70 || code >= 97 && code <= 102;
1032
+ }
1033
+ function isNumericId(s) {
1034
+ if (s.length === 0) return false;
1035
+ for (let i = 0; i < s.length; i++) {
1036
+ const code = s.charCodeAt(i);
1037
+ if (code < 48 || code > 57) return false;
1038
+ }
1039
+ return true;
1040
+ }
1041
+ function isHexHash(s) {
1042
+ if (s.length < MIN_HEX_LEN) return false;
1043
+ for (let i = 0; i < s.length; i++) {
1044
+ if (!isHexChar(s[i])) return false;
1045
+ }
1046
+ return true;
1047
+ }
1048
+ function isAlphanumericToken(s) {
1049
+ if (s.length < MIN_TOKEN_LEN) return false;
1050
+ let hasLetter = false;
1051
+ let hasDigit = false;
1052
+ for (let i = 0; i < s.length; i++) {
1053
+ const code = s.charCodeAt(i);
1054
+ if (code >= 65 && code <= 90 || code >= 97 && code <= 122) hasLetter = true;
1055
+ else if (code >= 48 && code <= 57) hasDigit = true;
1056
+ else if (code !== 95 && code !== 45) return false;
1057
+ }
1058
+ return hasLetter && hasDigit;
1059
+ }
868
1060
  function isDynamicSegment(segment) {
869
- return UUID_RE.test(segment) || NUMERIC_ID_RE.test(segment) || HEX_HASH_RE.test(segment) || ALPHA_TOKEN_RE.test(segment);
1061
+ return isUUID(segment) || isNumericId(segment) || isHexHash(segment) || isAlphanumericToken(segment);
870
1062
  }
871
1063
  function normalizePath(path) {
872
1064
  const qIdx = path.indexOf("?");
@@ -877,22 +1069,24 @@ function getEndpointKey(method, path) {
877
1069
  return `${method} ${normalizePath(path)}`;
878
1070
  }
879
1071
  function extractEndpointFromDesc(desc) {
880
- return desc.match(ENDPOINT_PREFIX_RE)?.[1] ?? null;
1072
+ const spaceIdx = desc.indexOf(" ");
1073
+ if (spaceIdx <= 0) return null;
1074
+ const secondSpace = desc.indexOf(" ", spaceIdx + 1);
1075
+ if (secondSpace === -1) return desc;
1076
+ return desc.slice(0, secondSpace);
881
1077
  }
882
1078
  function stripQueryString(path) {
883
1079
  const i = path.indexOf("?");
884
1080
  return i === -1 ? path : path.slice(0, i);
885
1081
  }
886
- var UUID_RE, NUMERIC_ID_RE, HEX_HASH_RE, ALPHA_TOKEN_RE, DYNAMIC_SEGMENT_PLACEHOLDER, ENDPOINT_PREFIX_RE;
1082
+ var UUID_LEN, MIN_HEX_LEN, MIN_TOKEN_LEN, DYNAMIC_SEGMENT_PLACEHOLDER;
887
1083
  var init_endpoint = __esm({
888
1084
  "src/utils/endpoint.ts"() {
889
1085
  "use strict";
890
- UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
891
- NUMERIC_ID_RE = /^\d+$/;
892
- HEX_HASH_RE = /^[0-9a-f]{12,}$/i;
893
- ALPHA_TOKEN_RE = /^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z0-9_-]{8,}$/;
1086
+ UUID_LEN = 36;
1087
+ MIN_HEX_LEN = 12;
1088
+ MIN_TOKEN_LEN = 8;
894
1089
  DYNAMIC_SEGMENT_PLACEHOLDER = ":id";
895
- ENDPOINT_PREFIX_RE = /^(\S+\s+\S+)/;
896
1090
  }
897
1091
  });
898
1092
 
@@ -913,6 +1107,10 @@ var init_http_status = __esm({
913
1107
  });
914
1108
 
915
1109
  // src/analysis/categorize.ts
1110
+ function isAuthPath(path) {
1111
+ const lower = path.toLowerCase();
1112
+ return lower.startsWith("/api/auth") || lower.startsWith("/clerk") || lower.startsWith("/api/clerk");
1113
+ }
916
1114
  function detectCategory(req) {
917
1115
  const { method, url, statusCode, responseHeaders } = req;
918
1116
  if (req.isStatic) return "static";
@@ -921,7 +1119,7 @@ function detectCategory(req) {
921
1119
  return "auth-handshake";
922
1120
  }
923
1121
  const effectivePath = getEffectivePath(req);
924
- if (/^\/api\/auth/i.test(effectivePath) || /^\/(api\/)?clerk/i.test(effectivePath)) {
1122
+ if (isAuthPath(effectivePath)) {
925
1123
  return "auth-check";
926
1124
  }
927
1125
  if (method === "POST" && !effectivePath.startsWith("/api/")) {
@@ -945,6 +1143,13 @@ function detectCategory(req) {
945
1143
  }
946
1144
  return "unknown";
947
1145
  }
1146
+ function hasAuthCredentials(req) {
1147
+ if (req.headers["authorization"]) return true;
1148
+ const cookie = (req.headers["cookie"] || "").toLowerCase();
1149
+ if (cookie && AUTH_COOKIE_NAMES.some((name) => cookie.includes(name))) return true;
1150
+ if (req.statusCode === 401) return true;
1151
+ return false;
1152
+ }
948
1153
  function getEffectivePath(req) {
949
1154
  const rewrite = req.responseHeaders["x-middleware-rewrite"];
950
1155
  if (!rewrite) return req.path;
@@ -955,9 +1160,21 @@ function getEffectivePath(req) {
955
1160
  return rewrite.startsWith("/") ? rewrite : req.path;
956
1161
  }
957
1162
  }
1163
+ var AUTH_COOKIE_NAMES;
958
1164
  var init_categorize = __esm({
959
1165
  "src/analysis/categorize.ts"() {
960
1166
  "use strict";
1167
+ AUTH_COOKIE_NAMES = [
1168
+ "__session=",
1169
+ "__clerk",
1170
+ "__host-next-auth",
1171
+ "next-auth.session-token=",
1172
+ "auth_token=",
1173
+ "session_id=",
1174
+ "access_token=",
1175
+ "_session=",
1176
+ "appsession="
1177
+ ];
961
1178
  }
962
1179
  });
963
1180
 
@@ -1016,8 +1233,11 @@ function generateHumanLabel(req, category) {
1016
1233
  return failed ? `${req.method} ${req.path} failed` : `${req.method} ${req.path}`;
1017
1234
  }
1018
1235
  }
1236
+ function stripApiPrefix(s) {
1237
+ return s.startsWith("/api/") ? s.slice(5) : s;
1238
+ }
1019
1239
  function prettifyEndpoint(name) {
1020
- const cleaned = name.replace(/^\/api\//, "").replace(/\//g, " ").replace(/\.\.\./g, "").trim();
1240
+ const cleaned = stripApiPrefix(name).split("/").join(" ").split("...").join("").trim();
1021
1241
  if (!cleaned) return "data";
1022
1242
  return cleaned.split(" ").map((word) => {
1023
1243
  if (word.endsWith("ses") || word.endsWith("us") || word.endsWith("ss"))
@@ -1029,22 +1249,8 @@ function prettifyEndpoint(name) {
1029
1249
  }
1030
1250
  function deriveActionVerb(method, endpointName) {
1031
1251
  const lower = endpointName.toLowerCase();
1032
- const VERB_PATTERNS = [
1033
- [/enhance/, "Enhanced"],
1034
- [/generate/, "Generated"],
1035
- [/create/, "Created"],
1036
- [/update/, "Updated"],
1037
- [/delete|remove/, "Deleted"],
1038
- [/send/, "Sent"],
1039
- [/upload/, "Uploaded"],
1040
- [/save/, "Saved"],
1041
- [/submit/, "Submitted"],
1042
- [/login|signin/, "Logged in"],
1043
- [/logout|signout/, "Logged out"],
1044
- [/register|signup/, "Registered"]
1045
- ];
1046
- for (const [pattern, verb] of VERB_PATTERNS) {
1047
- if (pattern.test(lower)) return verb;
1252
+ for (const [keyword, verb] of VERB_MAP) {
1253
+ if (lower.includes(keyword)) return verb;
1048
1254
  }
1049
1255
  switch (method) {
1050
1256
  case "POST":
@@ -1059,24 +1265,45 @@ function deriveActionVerb(method, endpointName) {
1059
1265
  }
1060
1266
  }
1061
1267
  function getEndpointName(path) {
1062
- const parts = path.replace(/^\/api\//, "").split("/");
1268
+ const parts = stripApiPrefix(path).split("/");
1063
1269
  if (parts.length <= 2) return parts.join("/");
1064
1270
  return parts.map((p) => p.length > ENDPOINT_TRUNCATE_LENGTH ? "..." : p).join("/");
1065
1271
  }
1066
1272
  function prettifyPageName(path) {
1067
- const clean = path.replace(/^\//, "").replace(/\/$/, "");
1273
+ let clean = path;
1274
+ if (clean.startsWith("/")) clean = clean.slice(1);
1275
+ if (clean.endsWith("/")) clean = clean.slice(0, -1);
1068
1276
  if (!clean) return "Home";
1069
- return clean.split("/").map((s) => capitalize(s.replace(/[-_]/g, " "))).join(" ");
1277
+ return clean.split("/").map((s) => capitalize(s.split("-").join(" ").split("_").join(" "))).join(" ");
1070
1278
  }
1071
1279
  function capitalize(s) {
1072
1280
  return s.charAt(0).toUpperCase() + s.slice(1);
1073
1281
  }
1282
+ var VERB_MAP;
1074
1283
  var init_label = __esm({
1075
1284
  "src/analysis/label.ts"() {
1076
1285
  "use strict";
1077
1286
  init_constants();
1078
1287
  init_categorize();
1079
1288
  init_http_status();
1289
+ VERB_MAP = [
1290
+ ["enhance", "Enhanced"],
1291
+ ["generate", "Generated"],
1292
+ ["create", "Created"],
1293
+ ["update", "Updated"],
1294
+ ["delete", "Deleted"],
1295
+ ["remove", "Deleted"],
1296
+ ["send", "Sent"],
1297
+ ["upload", "Uploaded"],
1298
+ ["save", "Saved"],
1299
+ ["submit", "Submitted"],
1300
+ ["login", "Logged in"],
1301
+ ["signin", "Logged in"],
1302
+ ["logout", "Logged out"],
1303
+ ["signout", "Logged out"],
1304
+ ["register", "Registered"],
1305
+ ["signup", "Registered"]
1306
+ ];
1080
1307
  }
1081
1308
  });
1082
1309
 
@@ -1522,33 +1749,85 @@ var init_handlers = __esm({
1522
1749
 
1523
1750
  // src/utils/static-patterns.ts
1524
1751
  function isStaticPath(urlPath) {
1525
- return STATIC_PATTERNS.some((p) => p.test(urlPath));
1752
+ const dotIdx = urlPath.lastIndexOf(".");
1753
+ if (dotIdx !== -1) {
1754
+ const ext = urlPath.slice(dotIdx).toLowerCase();
1755
+ if (STATIC_EXTENSIONS.has(ext)) return true;
1756
+ }
1757
+ return STATIC_PREFIXES.some((p) => urlPath.startsWith(p));
1526
1758
  }
1527
1759
  function isHealthCheckPath(urlPath) {
1528
- return HEALTH_CHECK_PATTERNS.some((p) => p.test(urlPath));
1760
+ return HEALTH_CHECK_PATHS.has(urlPath.toLowerCase());
1529
1761
  }
1530
- var STATIC_PATTERNS, HEALTH_CHECK_PATTERNS;
1762
+ var STATIC_EXTENSIONS, STATIC_PREFIXES, HEALTH_CHECK_PATHS;
1531
1763
  var init_static_patterns = __esm({
1532
1764
  "src/utils/static-patterns.ts"() {
1533
1765
  "use strict";
1534
- STATIC_PATTERNS = [
1535
- /\.(?:js|css|map|ico|png|jpg|jpeg|gif|svg|webp|woff2?|ttf|eot)$/,
1536
- /^\/favicon/,
1537
- /^\/node_modules\//,
1538
- // Framework-specific static/internal paths
1539
- /^\/_next\//,
1540
- /^\/__nextjs/,
1541
- /^\/@vite\//,
1542
- /^\/__vite/
1543
- ];
1544
- HEALTH_CHECK_PATTERNS = [
1545
- /^\/health(z|check)?$/i,
1546
- /^\/ping$/i,
1547
- /^\/(ready|readiness|liveness)$/i,
1548
- /^\/status$/i,
1549
- /^\/__health$/i,
1550
- /^\/api\/health(z|check)?$/i
1766
+ STATIC_EXTENSIONS = /* @__PURE__ */ new Set([
1767
+ ".js",
1768
+ ".css",
1769
+ ".map",
1770
+ ".ico",
1771
+ ".png",
1772
+ ".jpg",
1773
+ ".jpeg",
1774
+ ".gif",
1775
+ ".svg",
1776
+ ".webp",
1777
+ ".woff",
1778
+ ".woff2",
1779
+ ".ttf",
1780
+ ".eot"
1781
+ ]);
1782
+ STATIC_PREFIXES = [
1783
+ "/favicon",
1784
+ "/node_modules/",
1785
+ // Next.js
1786
+ "/_next/",
1787
+ "/__nextjs",
1788
+ // Vite (also used by Nuxt, Astro in dev)
1789
+ "/@vite/",
1790
+ "/__vite",
1791
+ "/@fs/",
1792
+ "/@id/",
1793
+ // Remix
1794
+ "/__remix",
1795
+ // Nuxt
1796
+ "/_nuxt/",
1797
+ "/__nuxt",
1798
+ // Astro
1799
+ "/@astro",
1800
+ "/_astro/",
1801
+ // Django
1802
+ "/static/",
1803
+ "/media/",
1804
+ "/__debug__/",
1805
+ // Flask / Werkzeug
1806
+ "/_debugtoolbar/",
1807
+ // FastAPI / Starlette
1808
+ "/openapi.json",
1809
+ "/docs",
1810
+ "/redoc",
1811
+ // Rails
1812
+ "/assets/",
1813
+ "/packs/",
1814
+ // Browser probes
1815
+ "/.well-known/"
1551
1816
  ];
1817
+ HEALTH_CHECK_PATHS = /* @__PURE__ */ new Set([
1818
+ "/health",
1819
+ "/healthz",
1820
+ "/healthcheck",
1821
+ "/ping",
1822
+ "/ready",
1823
+ "/readiness",
1824
+ "/liveness",
1825
+ "/status",
1826
+ "/__health",
1827
+ "/api/health",
1828
+ "/api/healthz",
1829
+ "/api/healthcheck"
1830
+ ]);
1552
1831
  }
1553
1832
  });
1554
1833
 
@@ -1704,29 +1983,25 @@ function createIngestHandler(services) {
1704
1983
  const routeEvent = (event) => {
1705
1984
  switch (event.type) {
1706
1985
  case TIMELINE_FETCH:
1707
- services.fetchStore.add(event.data);
1986
+ bus.emit("telemetry:fetch", event.data);
1708
1987
  break;
1709
1988
  case TIMELINE_LOG:
1710
- services.logStore.add(event.data);
1989
+ bus.emit("telemetry:log", event.data);
1711
1990
  break;
1712
1991
  case TIMELINE_ERROR:
1713
- services.errorStore.add(event.data);
1992
+ bus.emit("telemetry:error", event.data);
1714
1993
  break;
1715
1994
  case TIMELINE_QUERY:
1716
- services.queryStore.add(event.data);
1995
+ bus.emit("telemetry:query", event.data);
1717
1996
  break;
1718
1997
  }
1719
1998
  };
1720
- const queryStore = services.queryStore;
1721
- const fetchStore = services.fetchStore;
1722
- const logStore = services.logStore;
1723
- const errorStore = services.errorStore;
1724
- const requestStore = services.requestStore;
1999
+ const { bus, requestStore } = services;
1725
2000
  const stores = {
1726
- addQuery: (data) => queryStore.add(data),
1727
- addFetch: (data) => fetchStore.add(data),
1728
- addLog: (data) => logStore.add(data),
1729
- addError: (data) => errorStore.add(data),
2001
+ addQuery: (data) => bus.emit("telemetry:query", data),
2002
+ addFetch: (data) => bus.emit("telemetry:fetch", data),
2003
+ addLog: (data) => bus.emit("telemetry:log", data),
2004
+ addError: (data) => bus.emit("telemetry:error", data),
1730
2005
  addRequest: (data) => requestStore.add(data)
1731
2006
  };
1732
2007
  return (req, res) => {
@@ -1977,6 +2252,42 @@ var init_issues = __esm({
1977
2252
  }
1978
2253
  });
1979
2254
 
2255
+ // src/dashboard/api/graph.ts
2256
+ function createGraphHandler(services) {
2257
+ return (req, res) => {
2258
+ if (!requireGet(req, res)) return;
2259
+ const url = parseRequestUrl(req);
2260
+ const rawCluster = url.searchParams.get("cluster") ?? void 0;
2261
+ const rawNode = url.searchParams.get("node") ?? void 0;
2262
+ const rawLevel = url.searchParams.get("level") ?? void 0;
2263
+ const rawGrouping = url.searchParams.get("grouping") ?? void 0;
2264
+ const cluster = rawCluster && rawCluster.length <= MAX_PARAM_LENGTH ? rawCluster : void 0;
2265
+ const node = rawNode && rawNode.length <= MAX_PARAM_LENGTH ? rawNode : void 0;
2266
+ const level = rawLevel && VALID_LEVELS.has(rawLevel) ? rawLevel : void 0;
2267
+ const grouping = rawGrouping && VALID_GROUPINGS.has(rawGrouping) ? rawGrouping : void 0;
2268
+ const { graphBuilder, metricsStore } = services;
2269
+ graphBuilder.enrichWithMetrics((endpointKey) => {
2270
+ const metrics = metricsStore.getEndpoint(endpointKey);
2271
+ if (!metrics || metrics.sessions.length === 0) return void 0;
2272
+ const latest = metrics.sessions[metrics.sessions.length - 1];
2273
+ return latest.p95DurationMs;
2274
+ });
2275
+ const data = graphBuilder.getApiResponse({ cluster, node, level, grouping });
2276
+ sendJson(req, res, HTTP_OK, data);
2277
+ };
2278
+ }
2279
+ var VALID_LEVELS, VALID_GROUPINGS, MAX_PARAM_LENGTH;
2280
+ var init_graph = __esm({
2281
+ "src/dashboard/api/graph.ts"() {
2282
+ "use strict";
2283
+ init_labels();
2284
+ init_shared2();
2285
+ VALID_LEVELS = /* @__PURE__ */ new Set(["endpoints", "clusters"]);
2286
+ VALID_GROUPINGS = /* @__PURE__ */ new Set(["path", "auth-boundary", "data-domain"]);
2287
+ MAX_PARAM_LENGTH = 200;
2288
+ }
2289
+ });
2290
+
1980
2291
  // src/dashboard/sse.ts
1981
2292
  function createSSEHandler(services) {
1982
2293
  const clients = /* @__PURE__ */ new Set();
@@ -2249,7 +2560,7 @@ var init_issue_store = __esm({
2249
2560
  existing.occurrences++;
2250
2561
  existing.issue = issue;
2251
2562
  existing.cleanHitsSinceLastSeen = 0;
2252
- if (existing.state === "resolved" || existing.state === "stale") {
2563
+ if (existing.aiStatus !== "wont_fix" && (existing.state === "resolved" || existing.state === "stale")) {
2253
2564
  existing.state = "regressed";
2254
2565
  existing.resolvedAt = null;
2255
2566
  }
@@ -2275,10 +2586,11 @@ var init_issue_store = __esm({
2275
2586
  return stateful;
2276
2587
  }
2277
2588
  /**
2278
- * Reconcile issues against the current analysis results using evidence-based resolution.
2279
- *
2280
- * @param currentIssueIds - IDs of issues detected in the current analysis cycle
2281
- * @param activeEndpoints - Endpoints that had requests in the current cycle
2589
+ * Evidence-based reconciliation: for each active issue whose endpoint had
2590
+ * traffic but the issue was NOT re-detected, increment cleanHitsSinceLastSeen.
2591
+ * After CLEAN_HITS_FOR_RESOLUTION consecutive clean cycles, auto-resolve.
2592
+ * Issues on endpoints with no recent traffic are marked stale after STALE_ISSUE_TTL_MS.
2593
+ * Resolved and stale issues are pruned after their respective TTLs expire.
2282
2594
  */
2283
2595
  reconcile(currentIssueIds, activeEndpoints) {
2284
2596
  const now = Date.now();
@@ -2428,11 +2740,22 @@ var init_project = __esm({
2428
2740
  "use strict";
2429
2741
  init_fs();
2430
2742
  FRAMEWORKS = [
2743
+ // Meta-frameworks first (they bundle Express/Vite internally)
2431
2744
  { name: "nextjs", dep: "next", devCmd: "next dev", bin: "next", defaultPort: 3e3, devArgs: ["dev", "--port"] },
2432
2745
  { name: "remix", dep: "@remix-run/dev", devCmd: "remix dev", bin: "remix", defaultPort: 3e3, devArgs: ["dev"] },
2433
2746
  { name: "nuxt", dep: "nuxt", devCmd: "nuxt dev", bin: "nuxt", defaultPort: 3e3, devArgs: ["dev", "--port"] },
2434
- { name: "vite", dep: "vite", devCmd: "vite", bin: "vite", defaultPort: 5173, devArgs: ["--port"] },
2435
- { name: "astro", dep: "astro", devCmd: "astro dev", bin: "astro", defaultPort: 4321, devArgs: ["dev", "--port"] }
2747
+ { name: "astro", dep: "astro", devCmd: "astro dev", bin: "astro", defaultPort: 4321, devArgs: ["dev", "--port"] },
2748
+ { name: "nestjs", dep: "@nestjs/core", devCmd: "nest start", bin: "nest", defaultPort: 3e3, devArgs: ["--watch"] },
2749
+ { name: "adonis", dep: "@adonisjs/core", devCmd: "node ace serve", bin: "ace", defaultPort: 3333, devArgs: ["serve", "--watch"] },
2750
+ { name: "sails", dep: "sails", devCmd: "sails lift", bin: "sails", defaultPort: 1337, devArgs: ["lift"] },
2751
+ // Server frameworks
2752
+ { name: "hono", dep: "hono", devCmd: "node", bin: "node", defaultPort: 3e3 },
2753
+ { name: "fastify", dep: "fastify", devCmd: "node", bin: "node", defaultPort: 3e3 },
2754
+ { name: "koa", dep: "koa", devCmd: "node", bin: "node", defaultPort: 3e3 },
2755
+ { name: "hapi", dep: "@hapi/hapi", devCmd: "node", bin: "node", defaultPort: 3e3 },
2756
+ { name: "express", dep: "express", devCmd: "node", bin: "node", defaultPort: 3e3 },
2757
+ // Bundlers (last — likely used alongside a framework above)
2758
+ { name: "vite", dep: "vite", devCmd: "vite", bin: "vite", defaultPort: 5173, devArgs: ["--port"] }
2436
2759
  ];
2437
2760
  }
2438
2761
  });
@@ -2478,26 +2801,128 @@ var init_response = __esm({
2478
2801
  });
2479
2802
 
2480
2803
  // src/analysis/rules/patterns.ts
2481
- var SECRET_KEYS, TOKEN_PARAMS, SAFE_PARAMS, STACK_TRACE_RE, DB_CONN_RE, SQL_FRAGMENT_RE, SECRET_VAL_RE, LOG_SECRET_RE, MASKED_RE, EMAIL_RE, INTERNAL_ID_KEYS, INTERNAL_ID_SUFFIX, SELF_SERVICE_PATH, SENSITIVE_FIELD_NAMES, SELECT_STAR_RE, SELECT_DOT_STAR_RE, RULE_HINTS;
2804
+ var SECRET_KEY_SET, SECRET_KEYS, TOKEN_PARAM_SET, TOKEN_PARAMS, SAFE_PARAM_SET, SAFE_PARAMS, INTERNAL_ID_KEY_SET, INTERNAL_ID_KEYS, INTERNAL_ID_SUFFIX, SENSITIVE_FIELD_SET, SENSITIVE_FIELD_NAMES, SELF_SERVICE_SEGMENTS, SELF_SERVICE_PATH, MASKED_LITERALS, MASKED_RE, DB_PROTOCOLS, DB_CONN_RE, SELECT_STAR_RE, SELECT_DOT_STAR_RE, STACK_TRACE_RE, SQL_FRAGMENT_RE, SECRET_VAL_RE, LOG_SECRET_RE, EMAIL_RE, RULE_HINTS;
2482
2805
  var init_patterns = __esm({
2483
2806
  "src/analysis/rules/patterns.ts"() {
2484
2807
  "use strict";
2485
- SECRET_KEYS = /^(password|passwd|secret|api_key|apiKey|api_secret|apiSecret|private_key|privateKey|client_secret|clientSecret)$/;
2486
- TOKEN_PARAMS = /^(token|api_key|apiKey|secret|password|access_token|session_id|sessionId)$/;
2487
- SAFE_PARAMS = /^(_rsc|__clerk_handshake|__clerk_db_jwt|callback|code|state|nonce|redirect_uri|utm_|fbclid|gclid)$/;
2808
+ SECRET_KEY_SET = /* @__PURE__ */ new Set([
2809
+ "password",
2810
+ "passwd",
2811
+ "secret",
2812
+ "api_key",
2813
+ "apiKey",
2814
+ "api_secret",
2815
+ "apiSecret",
2816
+ "private_key",
2817
+ "privateKey",
2818
+ "client_secret",
2819
+ "clientSecret"
2820
+ ]);
2821
+ SECRET_KEYS = { test: (s) => SECRET_KEY_SET.has(s) };
2822
+ TOKEN_PARAM_SET = /* @__PURE__ */ new Set([
2823
+ "token",
2824
+ "api_key",
2825
+ "apiKey",
2826
+ "secret",
2827
+ "password",
2828
+ "access_token",
2829
+ "session_id",
2830
+ "sessionId"
2831
+ ]);
2832
+ TOKEN_PARAMS = { test: (s) => TOKEN_PARAM_SET.has(s) };
2833
+ SAFE_PARAM_SET = /* @__PURE__ */ new Set([
2834
+ "_rsc",
2835
+ "__clerk_handshake",
2836
+ "__clerk_db_jwt",
2837
+ "callback",
2838
+ "code",
2839
+ "state",
2840
+ "nonce",
2841
+ "redirect_uri",
2842
+ "utm_",
2843
+ "fbclid",
2844
+ "gclid"
2845
+ ]);
2846
+ SAFE_PARAMS = { test: (s) => SAFE_PARAM_SET.has(s) };
2847
+ INTERNAL_ID_KEY_SET = /* @__PURE__ */ new Set([
2848
+ "id",
2849
+ "_id",
2850
+ "userId",
2851
+ "user_id",
2852
+ "createdBy",
2853
+ "updatedBy",
2854
+ "organizationId",
2855
+ "org_id",
2856
+ "tenantId",
2857
+ "tenant_id"
2858
+ ]);
2859
+ INTERNAL_ID_KEYS = { test: (s) => INTERNAL_ID_KEY_SET.has(s) };
2860
+ INTERNAL_ID_SUFFIX = {
2861
+ test: (s) => s.endsWith("Id") || s.endsWith("_id")
2862
+ };
2863
+ SENSITIVE_FIELD_SET = /* @__PURE__ */ new Set([
2864
+ "phone",
2865
+ "phonenumber",
2866
+ "phone_number",
2867
+ "ssn",
2868
+ "socialsecuritynumber",
2869
+ "social_security_number",
2870
+ "dateofbirth",
2871
+ "date_of_birth",
2872
+ "dob",
2873
+ "address",
2874
+ "streetaddress",
2875
+ "street_address",
2876
+ "creditcard",
2877
+ "credit_card",
2878
+ "cardnumber",
2879
+ "card_number",
2880
+ "bankaccount",
2881
+ "bank_account",
2882
+ "passport",
2883
+ "passportnumber",
2884
+ "passport_number",
2885
+ "nationalid",
2886
+ "national_id"
2887
+ ]);
2888
+ SENSITIVE_FIELD_NAMES = {
2889
+ test: (s) => SENSITIVE_FIELD_SET.has(s.toLowerCase())
2890
+ };
2891
+ SELF_SERVICE_SEGMENTS = /* @__PURE__ */ new Set(["me", "account", "profile", "settings", "self"]);
2892
+ SELF_SERVICE_PATH = {
2893
+ test: (path) => {
2894
+ const segments = path.toLowerCase().split(/[/?#]/);
2895
+ return segments.some((seg) => SELF_SERVICE_SEGMENTS.has(seg));
2896
+ }
2897
+ };
2898
+ MASKED_LITERALS = ["[REDACTED]", "[FILTERED]", "CHANGE_ME"];
2899
+ MASKED_RE = {
2900
+ test: (s) => {
2901
+ const upper = s.toUpperCase();
2902
+ if (MASKED_LITERALS.some((m) => upper.includes(m))) return true;
2903
+ if (s.length > 0 && s.split("").every((c) => c === "*")) return true;
2904
+ if (s.length >= 3 && s.split("").every((c) => c === "x" || c === "X")) return true;
2905
+ return false;
2906
+ }
2907
+ };
2908
+ DB_PROTOCOLS = ["postgres://", "mysql://", "mongodb://", "redis://"];
2909
+ DB_CONN_RE = {
2910
+ test: (s) => DB_PROTOCOLS.some((p) => s.includes(p))
2911
+ };
2912
+ SELECT_STAR_RE = {
2913
+ test: (s) => {
2914
+ const t = s.trimStart().toUpperCase();
2915
+ return t.startsWith("SELECT *") || t.startsWith("SELECT *");
2916
+ }
2917
+ };
2918
+ SELECT_DOT_STAR_RE = {
2919
+ test: (s) => s.toUpperCase().includes(".* FROM")
2920
+ };
2488
2921
  STACK_TRACE_RE = /at\s+.+\(.+:\d+:\d+\)|at\s+Module\._compile|at\s+Object\.<anonymous>|at\s+processTicksAndRejections|Traceback \(most recent call last\)|File ".+", line \d+/;
2489
- DB_CONN_RE = /(postgres|mysql|mongodb|redis):\/\//;
2490
2922
  SQL_FRAGMENT_RE = /\b(SELECT\s+[\w.*]+\s+FROM|INSERT\s+INTO|UPDATE\s+\w+\s+SET|DELETE\s+FROM)\b/i;
2491
2923
  SECRET_VAL_RE = /(api_key|apiKey|secret|token)\s*[:=]\s*["']?[A-Za-z0-9_\-.+/]{8,}/;
2492
2924
  LOG_SECRET_RE = /(password|secret|token|api_key|apiKey)\s*[:=]\s*["']?[A-Za-z0-9_\-.+/]{8,}/i;
2493
- MASKED_RE = /^\*+$|\[REDACTED\]|\[FILTERED\]|CHANGE_ME|^x{3,}$/i;
2494
2925
  EMAIL_RE = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/;
2495
- INTERNAL_ID_KEYS = /^(id|_id|userId|user_id|createdBy|updatedBy|organizationId|org_id|tenantId|tenant_id)$/;
2496
- INTERNAL_ID_SUFFIX = /Id$|_id$/;
2497
- SELF_SERVICE_PATH = /\/(?:me|account|profile|settings|self)(?=\/|\?|#|$)/i;
2498
- SENSITIVE_FIELD_NAMES = /^(phone|phoneNumber|phone_number|ssn|socialSecurityNumber|social_security_number|dateOfBirth|date_of_birth|dob|address|streetAddress|street_address|creditCard|credit_card|cardNumber|card_number|bankAccount|bank_account|passport|passportNumber|passport_number|nationalId|national_id)$/i;
2499
- SELECT_STAR_RE = /^SELECT\s+\*/i;
2500
- SELECT_DOT_STAR_RE = /\.\*\s+FROM/i;
2501
2926
  RULE_HINTS = {
2502
2927
  "exposed-secret": "Never include secret fields in API responses. Strip sensitive fields before returning.",
2503
2928
  "token-in-url": "Pass tokens in the Authorization header, not URL query parameters.",
@@ -3956,7 +4381,7 @@ var init_src = __esm({
3956
4381
  init_engine();
3957
4382
  init_insights2();
3958
4383
  init_insights();
3959
- VERSION = "0.9.2";
4384
+ VERSION = "0.10.1";
3960
4385
  }
3961
4386
  });
3962
4387
 
@@ -3976,7 +4401,10 @@ function getBaseStyles() {
3976
4401
  --red:#dc2626;
3977
4402
  --cyan:#0891b2;
3978
4403
  --green-bg:rgba(22,163,74,0.08);--green-bg-subtle:rgba(22,163,74,0.05);--green-border:rgba(22,163,74,0.2);--green-border-subtle:rgba(22,163,74,0.15);
3979
- --amber-bg:rgba(217,119,6,0.07);--red-bg:rgba(220,38,38,0.07);--blue-bg:rgba(37,99,235,0.08);--cyan-bg:rgba(8,145,178,0.07);
4404
+ --amber-bg:rgba(217,119,6,0.08);--amber-border:rgba(217,119,6,0.15);
4405
+ --red-bg:rgba(220,38,38,0.08);--red-border:rgba(220,38,38,0.2);
4406
+ --blue-bg:rgba(37,99,235,0.08);--cyan-bg:rgba(8,145,178,0.07);
4407
+ --accent-bg:rgba(99,102,241,0.08);
3980
4408
  --sidebar-width:232px;--header-height:52px;
3981
4409
  --radius:8px;--radius-sm:6px;
3982
4410
  --shadow-sm:0 1px 3px rgba(0,0,0,0.06),0 1px 2px rgba(0,0,0,0.03);
@@ -4033,6 +4461,7 @@ function getLayoutStyles() {
4033
4461
  .sidebar-logo .logo-version{font-weight:400;font-size:11px;color:var(--text-muted);margin-left:8px;letter-spacing:0}
4034
4462
  .sidebar-nav{padding:12px;flex:1}
4035
4463
  .sidebar-section{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);padding:16px 12px 8px}
4464
+ .sidebar-divider{height:1px;background:var(--border-subtle);margin:8px 12px}
4036
4465
  .sidebar-item{display:flex;align-items:center;gap:12px;padding:10px 12px;border-radius:var(--radius);color:var(--text-dim);font-size:14px;font-weight:500;cursor:pointer;transition:all .15s;border:none;background:transparent;width:100%;text-align:left;font-family:var(--sans)}
4037
4466
  .sidebar-item:hover{background:var(--bg-hover);color:var(--text)}
4038
4467
  .sidebar-item.active{background:var(--bg-active);color:var(--accent)}
@@ -4041,6 +4470,7 @@ function getLayoutStyles() {
4041
4470
  .sidebar-item:hover .item-icon{opacity:.8}
4042
4471
  .sidebar-item .item-label{flex:1}
4043
4472
  .sidebar-item .item-count{font-size:12px;font-family:var(--mono);color:var(--text-muted);background:var(--bg-muted);padding:2px 8px;border-radius:10px;min-width:24px;text-align:center}
4473
+ .sidebar-beta{font-size:9px;color:#6366f1;background:#eef2ff;border:1px solid #e0e7ff;border-radius:4px;padding:0 5px;margin-left:auto;line-height:16px}
4044
4474
  .sidebar-item.disabled{opacity:.35;cursor:default;pointer-events:none}
4045
4475
  .sidebar-item .coming-soon{font-size:10px;color:var(--text-muted);background:var(--bg-muted);padding:2px 8px;border-radius:10px;font-weight:600;letter-spacing:.3px}
4046
4476
  .sidebar-footer{padding:16px 24px;border-top:1px solid var(--border-subtle);font-size:12px;color:var(--text-muted);font-family:var(--mono)}
@@ -4065,7 +4495,8 @@ function getLayoutStyles() {
4065
4495
  /* Content */
4066
4496
  .main-content{flex:1;overflow-y:auto}
4067
4497
  bk-dashboard{display:contents}
4068
- bk-overview-view,bk-flows-view,bk-requests-view,bk-fetches-view,bk-queries-view,bk-errors-view,bk-logs-view,bk-security-view,bk-performance-view,bk-timeline-panel,bk-empty-state{display:block}
4498
+ bk-overview-view,bk-flows-view,bk-requests-view,bk-fetches-view,bk-queries-view,bk-errors-view,bk-logs-view,bk-security-view,bk-performance-view,bk-explorer-view,bk-insights-view,bk-timeline-panel,bk-empty-state{display:block}
4499
+ bk-graph-view{display:block}
4069
4500
  bk-method-badge,bk-status-pill,bk-duration-label,bk-copy-button{display:inline-flex;flex-shrink:0}
4070
4501
  bk-stat-card{display:inline-flex}
4071
4502
  bk-toast{display:block;position:fixed;top:0;left:0;right:0;z-index:100;pointer-events:none}
@@ -4442,7 +4873,7 @@ span.perf-breakdown-dot.perf-breakdown-app{background:var(--breakdown-app)}
4442
4873
  .perf-trend-faster{color:var(--green)}
4443
4874
  `;
4444
4875
  }
4445
- var init_graph = __esm({
4876
+ var init_graph2 = __esm({
4446
4877
  "src/dashboard/styles/graph.ts"() {
4447
4878
  "use strict";
4448
4879
  }
@@ -4451,53 +4882,25 @@ var init_graph = __esm({
4451
4882
  // src/dashboard/styles/overview.ts
4452
4883
  function getOverviewStyles() {
4453
4884
  return `
4454
- /* Overview */
4455
- .ov-container{padding:24px 28px}
4456
-
4457
- /* Summary banner */
4458
- .ov-summary{display:flex;gap:10px;margin-bottom:24px;flex-wrap:wrap}
4459
- .ov-stat{display:flex;flex-direction:column;gap:4px;flex:1;min-width:100px;padding:16px 18px;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);box-shadow:var(--shadow-sm);transition:box-shadow var(--transition, .15s ease)}
4460
- .ov-stat:hover{box-shadow:var(--shadow-md)}
4461
- .ov-stat-value{font-size:22px;font-weight:700;font-family:var(--mono);color:var(--text);line-height:1.2}
4462
- .ov-stat-label{font-size:10px;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);font-weight:600}
4463
-
4464
- /* Section header */
4465
- .ov-section-title{font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.8px;color:var(--text-muted);margin-bottom:12px;display:flex;align-items:center;gap:8px}
4466
- .ov-issue-count{font-size:11px;font-family:var(--mono);color:var(--text-dim);background:var(--bg-muted);border:1px solid var(--border);padding:1px 8px;border-radius:10px}
4467
-
4468
- /* Insight cards */
4469
- .ov-cards{display:flex;flex-direction:column;gap:8px}
4470
- .ov-card{display:flex;align-items:flex-start;gap:14px;padding:16px 20px;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);cursor:pointer;transition:all var(--transition, .15s ease);box-shadow:var(--shadow-sm)}
4471
- .ov-card:hover{background:var(--bg-hover);border-color:var(--border-light);box-shadow:var(--shadow-md);transform:translateY(-1px)}
4472
- .ov-card-icon{width:20px;height:20px;display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:10px;border-radius:50%;margin-top:2px}
4473
- .ov-card-icon.critical{background:rgba(220,38,38,.08);color:var(--red)}
4474
- .ov-card-icon.warning{background:rgba(217,119,6,.08);color:var(--amber)}
4475
- .ov-card-icon.info{background:rgba(37,99,235,.08);color:var(--blue)}
4476
- .ov-card-icon.resolved{background:var(--green-bg);color:var(--green)}
4477
- .ov-card-body{flex:1;min-width:0}
4478
- .ov-card-title{font-size:13px;font-weight:600;color:var(--text);margin-bottom:2px}
4479
- .ov-card-desc{font-size:12px;color:var(--text-dim);line-height:1.5}
4480
- .ov-card-detail{font-size:11px;font-family:var(--mono);color:var(--text-muted);margin-top:6px;padding:8px 10px;background:var(--bg-muted);border:1px solid var(--border-subtle);border-radius:var(--radius-sm);line-height:1.5}
4481
- .ov-card-desc strong{color:var(--text);font-family:var(--mono);font-weight:600}
4482
- .ov-card-arrow{color:var(--text-muted);font-size:12px;flex-shrink:0;margin-top:2px;font-family:var(--mono);transition:transform .15s}
4483
-
4484
- /* Expanded card */
4485
- .ov-card.expanded{border-color:var(--border-light);box-shadow:var(--shadow-md)}
4486
- .ov-card-expand{display:none;margin-top:10px;padding-top:10px;border-top:1px solid var(--border)}
4487
- .ov-card-hint{font-size:12px;color:var(--text-dim);line-height:1.5;margin-bottom:10px}
4488
- .ov-card-link{font-size:12px;font-weight:600;color:var(--blue);cursor:pointer;display:inline-block;padding:4px 0}
4489
- .ov-card-link:hover{text-decoration:underline}
4490
- .ov-detail-label{font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:4px}
4491
- .ov-detail-item{font-size:12px;color:var(--text);font-family:var(--mono);padding:2px 0}
4492
-
4493
- /* All-clear banner */
4494
- .ov-clear{display:flex;align-items:center;gap:12px;padding:16px 20px;background:var(--green-bg-subtle);border:1px solid var(--green-border);border-radius:var(--radius);color:var(--green);font-size:13px;font-weight:500}
4495
- .ov-clear-icon{font-size:16px}
4885
+ .ov-container{padding:28px}
4496
4886
 
4497
- /* Resolved section */
4498
- .ov-resolved-title{margin-top:24px}
4499
- .ov-card-resolved{opacity:.7;border-color:var(--green-border);cursor:default}
4500
- .ov-card-resolved:hover{opacity:1;box-shadow:var(--shadow-sm)}
4887
+ .ov-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px}
4888
+
4889
+ .ov-card-nav{display:flex;align-items:center;gap:16px;padding:20px 24px;background:var(--bg-card);border:1px solid var(--border);border-radius:12px;cursor:pointer;transition:all .18s ease;box-shadow:0 1px 3px rgba(0,0,0,.04)}
4890
+ .ov-card-nav:hover{border-color:var(--border-light);box-shadow:0 4px 16px rgba(0,0,0,.06);transform:translateY(-2px)}
4891
+ .ov-card-nav:active{transform:translateY(0)}
4892
+
4893
+ .ov-card-empty{opacity:.55}
4894
+ .ov-card-empty:hover{opacity:.8}
4895
+
4896
+ .ov-card-icon-lg{font-size:22px;width:40px;height:40px;display:flex;align-items:center;justify-content:center;flex-shrink:0;background:var(--bg-muted);border-radius:10px;color:var(--text-muted)}
4897
+
4898
+ .ov-card-content{flex:1;min-width:0}
4899
+ .ov-card-headline{font-size:16px;font-weight:700;color:var(--text);font-family:var(--mono);line-height:1.3}
4900
+ .ov-card-context{font-size:12px;color:var(--text-muted);margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
4901
+
4902
+ .ov-card-arrow{font-size:16px;color:var(--text-muted);opacity:0;transition:opacity .15s,transform .15s;flex-shrink:0}
4903
+ .ov-card-nav:hover .ov-card-arrow{opacity:1;transform:translateX(2px)}
4501
4904
  `;
4502
4905
  }
4503
4906
  var init_overview = __esm({
@@ -4779,9 +5182,205 @@ var init_timeline = __esm({
4779
5182
  }
4780
5183
  });
4781
5184
 
5185
+ // src/dashboard/styles/graph-view.ts
5186
+ function getGraphViewStyles() {
5187
+ return `
5188
+ .graph-wrapper{display:flex;flex-direction:column;height:calc(100vh - 120px);outline:none}
5189
+
5190
+ /* Toolbar \u2014 centered search, layers left, flow picker right */
5191
+ .graph-toolbar{display:flex;align-items:center;gap:10px;padding:8px 16px;border-bottom:1px solid var(--border)}
5192
+
5193
+ /* Layer toggles */
5194
+ .graph-layer-toggles{display:flex;gap:4px;flex-shrink:0}
5195
+ .graph-layer-btn{display:flex;align-items:center;gap:3px;font-size:10px;font-weight:500;padding:3px 8px;border:1px solid var(--border);border-radius:12px;background:var(--bg);color:var(--text-muted);cursor:pointer;transition:all .15s;white-space:nowrap}
5196
+ .graph-layer-btn:hover{border-color:var(--text-muted);color:var(--text)}
5197
+ .graph-layer-btn.active{background:var(--bg-card);font-weight:600}
5198
+
5199
+ /* Search \u2014 takes remaining space, centered */
5200
+ .graph-search{flex:1;position:relative;display:flex;align-items:center;max-width:360px;margin:0 auto}
5201
+ .graph-search-icon{position:absolute;left:10px;color:var(--text-muted);font-size:13px;pointer-events:none;opacity:0.5}
5202
+ .graph-search-input{width:100%;font-size:11px;padding:6px 28px 6px 28px;border:1px solid var(--border);border-radius:8px;background:var(--bg);color:var(--text);font-family:var(--mono);outline:none;transition:border-color .15s,box-shadow .15s}
5203
+ .graph-search-input:focus{border-color:#6366f1;box-shadow:0 0 0 3px rgba(99,102,241,.1)}
5204
+ .graph-search-input::placeholder{color:var(--text-muted);opacity:0.5}
5205
+ .graph-search-clear{position:absolute;right:8px;background:none;border:none;color:var(--text-muted);cursor:pointer;font-size:11px;padding:0 4px;line-height:1;border-radius:3px}
5206
+ .graph-search-clear:hover{color:var(--text);background:var(--bg-card)}
5207
+
5208
+ /* Flow picker */
5209
+ .graph-flow-picker{font-size:10px;padding:4px 8px;border:1px solid var(--border);border-radius:8px;background:var(--bg);color:var(--text);cursor:pointer;font-family:var(--mono);max-width:200px;flex-shrink:0}
5210
+
5211
+ /* Auth legend \u2014 inline in toolbar */
5212
+ .graph-auth-legend{display:flex;gap:8px;align-items:center;font-size:10px;color:var(--text-muted);flex-shrink:0}
5213
+ .graph-auth-legend-item{display:flex;align-items:center;gap:3px;white-space:nowrap}
5214
+
5215
+ /* Canvas */
5216
+ .graph-body{display:flex;flex:1;min-height:0}
5217
+ .graph-canvas{flex:1;overflow:hidden;padding:0;position:relative;min-height:0}
5218
+ .graph-svg{display:block}
5219
+ .graph-col-header{fill:#c4c4cc;font-size:9px;font-weight:600;font-family:'Inter',system-ui,sans-serif;letter-spacing:1.5px}
5220
+
5221
+ /* Floating controls \u2014 bottom-center pill */
5222
+ .graph-float{position:absolute;top:12px;right:12px;display:flex;align-items:center;gap:2px;background:var(--bg-card);border:1px solid var(--border);border-radius:10px;padding:4px 6px;box-shadow:0 2px 12px rgba(0,0,0,.08);z-index:10}
5223
+ .graph-float-btn{background:none;border:none;color:var(--text-muted);cursor:pointer;font-size:13px;padding:4px 8px;line-height:1;border-radius:6px;transition:all .12s;white-space:nowrap}
5224
+ .graph-float-btn:hover{background:var(--bg);color:var(--text)}
5225
+ .graph-float-btn-accent{font-size:11px;font-weight:600;color:#6366f1}
5226
+ .graph-float-btn-accent:hover{background:rgba(99,102,241,.08);color:#4f46e5}
5227
+ .graph-float-zoom{font-size:10px;color:var(--text-muted);font-family:var(--mono);min-width:36px;text-align:center;user-select:none}
5228
+ .graph-float-sep{width:1px;height:16px;background:var(--border);margin:0 2px;flex-shrink:0}
5229
+
5230
+ /* Empty & loading states */
5231
+ .graph-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:400px;color:var(--text-muted);text-align:center;padding:40px}
5232
+ .graph-empty-icon{font-size:40px;opacity:0.25;margin-bottom:12px}
5233
+ .graph-empty-title{font-size:15px;font-weight:600;color:var(--text);margin-bottom:6px}
5234
+ .graph-empty-desc{font-size:12px;max-width:320px;line-height:1.5}
5235
+ .graph-loading{display:flex;align-items:center;justify-content:center;min-height:400px;color:var(--text-muted);font-size:13px}
5236
+
5237
+ /* Detail panel */
5238
+ .graph-detail{width:320px;border-left:1px solid var(--border);overflow-y:auto;padding:16px;background:var(--bg-card);flex-shrink:0;max-height:calc(100vh - 160px)}
5239
+ .graph-detail-head{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:12px}
5240
+ .graph-detail-badge{font-size:11px;font-weight:500;text-transform:uppercase;letter-spacing:0.5px;margin-bottom:4px}
5241
+ .graph-detail-name{font-size:14px;font-weight:700;color:var(--text);word-break:break-all;font-family:var(--mono)}
5242
+ .graph-detail-close{background:none;border:none;color:var(--text-muted);cursor:pointer;font-size:16px;padding:0 4px;line-height:1}
5243
+ .graph-detail-close:hover{color:var(--text)}
5244
+
5245
+ .graph-detail-auth-badge{display:inline-block;font-size:10px;font-weight:500;color:#059669;background:#ecfdf5;border:1px solid #a7f3d0;border-radius:4px;padding:1px 6px;margin-top:4px}
5246
+ .graph-detail-mw-badge{display:inline-block;font-size:10px;font-weight:500;color:#6b7280;background:#f3f4f6;border:1px solid #d1d5db;border-radius:4px;padding:1px 6px;margin-top:4px;margin-left:4px}
5247
+
5248
+ /* Detail tabs */
5249
+ .graph-detail-tabs{display:flex;gap:2px;margin-bottom:12px;border-bottom:1px solid var(--border);padding-bottom:0}
5250
+ .graph-detail-tab{background:none;border:none;border-bottom:2px solid transparent;color:var(--text-muted);cursor:pointer;font-size:11px;font-weight:500;padding:6px 10px;transition:all .12s}
5251
+ .graph-detail-tab:hover{color:var(--text)}
5252
+ .graph-detail-tab.active{color:#6366f1;border-bottom-color:#6366f1}
5253
+
5254
+ /* Detail stats */
5255
+ .graph-detail-stats{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:12px}
5256
+ .graph-detail-stat{background:var(--bg);border-radius:var(--radius-sm);padding:10px 12px}
5257
+ .graph-detail-val{font-size:20px;font-weight:700;font-family:var(--mono);color:var(--text);line-height:1.2}
5258
+ .graph-detail-lbl{font-size:9px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.6px;margin-top:2px}
5259
+
5260
+ /* Detail sections */
5261
+ .graph-detail-sec{font-size:10px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.8px;margin:12px 0 8px;padding-top:10px;border-top:1px solid var(--border)}
5262
+ .graph-detail-conn{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--text);padding:6px 8px;background:var(--bg);border-radius:var(--radius-sm);margin-bottom:4px;font-family:var(--mono)}
5263
+ .graph-detail-edge-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}
5264
+ .graph-detail-edge-type{font-size:10px;font-weight:600;text-transform:uppercase;min-width:42px}
5265
+ .graph-detail-dim{color:var(--text-muted);font-size:10px;margin-left:auto;white-space:nowrap}
5266
+ .graph-detail-sql{font-size:10px;color:var(--text-muted);padding:8px 10px;background:var(--bg);border-radius:var(--radius-sm);font-family:var(--mono);word-break:break-all;line-height:1.5;margin:0 0 4px;white-space:pre-wrap;border:1px solid var(--border)}
5267
+
5268
+ /* Security findings in detail */
5269
+ .graph-detail-finding{padding:8px 10px;background:var(--bg);border-radius:var(--radius-sm);margin-bottom:6px;border:1px solid var(--border)}
5270
+ .graph-detail-finding-title{font-size:12px;font-weight:600;color:var(--text);margin-top:4px}
5271
+ .graph-detail-finding-meta{font-size:10px;color:var(--text-muted);margin-top:2px;font-family:var(--mono)}
5272
+ .graph-detail-severity{font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:0.5px;padding:1px 6px;border-radius:3px}
5273
+ .graph-detail-severity-critical{background:#fef2f2;color:#dc2626;border:1px solid #fecaca}
5274
+ .graph-detail-severity-warning{background:#fffbeb;color:#d97706;border:1px solid #fde68a}
5275
+ .graph-detail-severity-info{background:#eff6ff;color:#2563eb;border:1px solid #bfdbfe}
5276
+
5277
+ /* Issues in detail */
5278
+ .graph-detail-issue-summary{margin-bottom:12px}
5279
+ .graph-detail-hint{font-size:11px;color:var(--text-muted);line-height:1.5;margin:0}
5280
+ .graph-detail-empty{font-size:12px;color:var(--text-muted);padding:16px;text-align:center}
5281
+
5282
+ /* Pulse animation for critical security badges */
5283
+ @keyframes graph-pulse{0%,100%{opacity:1}50%{opacity:0.5}}
5284
+ .graph-pulse{animation:graph-pulse 2s ease-in-out infinite}
5285
+
5286
+ /* Flow edge animation */
5287
+ @keyframes graph-flow-dash{to{stroke-dashoffset:-24}}
5288
+ .graph-flow-edge{animation:graph-flow-dash 1s linear infinite}
5289
+ `;
5290
+ }
5291
+ var init_graph_view = __esm({
5292
+ "src/dashboard/styles/graph-view.ts"() {
5293
+ "use strict";
5294
+ }
5295
+ });
5296
+
5297
+ // src/dashboard/styles/explorer.ts
5298
+ function getExplorerStyles() {
5299
+ return `
5300
+ /* Explorer sub-tabs */
5301
+ .explorer-tabs{display:flex;gap:0;border-bottom:1px solid var(--border);padding:0 28px;background:var(--bg);position:sticky;top:0;z-index:2}
5302
+ .explorer-tab{padding:10px 16px;font-size:13px;font-weight:500;color:var(--text-muted);background:none;border:none;border-bottom:2px solid transparent;cursor:pointer;transition:all .15s;display:flex;align-items:center;gap:6px;font-family:var(--sans);white-space:nowrap}
5303
+ .explorer-tab:hover{color:var(--text)}
5304
+ .explorer-tab.active{color:var(--accent);border-bottom-color:var(--accent)}
5305
+ .explorer-tab-count{font-size:11px;font-family:var(--mono);color:var(--text-muted);background:var(--bg-muted);padding:1px 6px;border-radius:8px}
5306
+ .explorer-tab.active .explorer-tab-count{color:var(--accent);background:var(--accent-bg)}
5307
+ `;
5308
+ }
5309
+ var init_explorer = __esm({
5310
+ "src/dashboard/styles/explorer.ts"() {
5311
+ "use strict";
5312
+ }
5313
+ });
5314
+
5315
+ // src/dashboard/styles/insights.ts
5316
+ function getInsightsStyles() {
5317
+ return `
5318
+ /* Insights filter chips */
5319
+ .insights-filters{display:flex;gap:6px;padding:16px 28px;border-bottom:1px solid var(--border);background:var(--bg);position:sticky;top:0;z-index:2}
5320
+ .insights-chip{font-size:12px;font-weight:500;padding:5px 14px;border:1px solid var(--border);border-radius:20px;background:var(--bg);color:var(--text-muted);cursor:pointer;transition:all .15s;display:flex;align-items:center;gap:5px;font-family:var(--sans)}
5321
+ .insights-chip:hover{border-color:var(--text-muted);color:var(--text)}
5322
+ .insights-chip.active{background:var(--accent);color:white;border-color:var(--accent)}
5323
+ .insights-chip-count{font-size:10px;font-family:var(--mono);background:rgba(0,0,0,.08);padding:1px 5px;border-radius:8px}
5324
+ .insights-chip.active .insights-chip-count{background:rgba(255,255,255,.25)}
5325
+
5326
+ /* Insights card list */
5327
+ .insights-list{padding:16px 28px}
5328
+
5329
+ .insights-empty{display:flex;align-items:center;gap:10px;padding:24px;color:var(--green);font-size:14px;font-weight:500}
5330
+ .insights-empty-icon{font-size:18px}
5331
+
5332
+ .insights-card{display:flex;align-items:flex-start;gap:12px;padding:14px 18px;background:var(--bg-card);border:1px solid var(--border);border-radius:10px;cursor:pointer;transition:all .15s;margin-bottom:8px}
5333
+ .insights-card:hover{border-color:var(--border-light);box-shadow:0 2px 8px rgba(0,0,0,.04)}
5334
+ .insights-card.expanded{border-color:var(--border-light);box-shadow:0 2px 8px rgba(0,0,0,.04)}
5335
+ .insights-card.resolved{opacity:.55}
5336
+ .insights-card.resolved:hover{opacity:.8}
5337
+
5338
+ .insights-card-left{flex-shrink:0;padding-top:2px}
5339
+ .insights-sev{width:22px;height:22px;display:flex;align-items:center;justify-content:center;font-size:10px;border-radius:50%}
5340
+ .insights-sev.critical{background:var(--red-bg);color:var(--red)}
5341
+ .insights-sev.warning{background:var(--amber-bg);color:var(--amber)}
5342
+ .insights-sev.info{background:var(--blue-bg);color:var(--blue)}
5343
+
5344
+ .insights-card-body{flex:1;min-width:0}
5345
+ .insights-card-header{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:3px}
5346
+ .insights-card-title{font-size:13px;font-weight:600;color:var(--text)}
5347
+ .insights-card-title.resolved{text-decoration:line-through;color:var(--text-muted)}
5348
+ .insights-card-cat{font-size:9px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;color:var(--text-muted);background:var(--bg-muted);padding:1px 6px;border-radius:4px}
5349
+ .insights-card-count{font-size:11px;font-family:var(--mono);color:var(--text-muted)}
5350
+ .insights-card-desc{font-size:12px;color:var(--text-dim);line-height:1.5}
5351
+ .insights-card-detail{font-size:11px;font-family:var(--mono);color:var(--text-muted);margin-top:6px;padding:6px 10px;background:var(--bg-muted);border:1px solid var(--border-subtle);border-radius:6px;line-height:1.5}
5352
+ .insights-card-progress{font-size:11px;color:var(--text-muted);margin-top:4px;font-family:var(--mono)}
5353
+ .insights-card-hint{font-size:12px;color:var(--text-dim);line-height:1.6;margin-top:8px;padding-top:8px;border-top:1px solid var(--border)}
5354
+
5355
+ .insights-badge-regressed{font-size:9px;font-weight:700;color:var(--red);background:var(--red-bg);padding:1px 6px;border-radius:4px}
5356
+ .insights-badge-verifying{font-size:9px;font-weight:700;color:var(--amber);background:var(--amber-bg);padding:1px 6px;border-radius:4px}
5357
+ .insights-badge-resolved{font-size:9px;font-weight:700;color:var(--green);background:var(--green-bg);padding:1px 6px;border-radius:4px}
5358
+
5359
+ .insights-card-arrow{color:var(--text-muted);font-size:12px;flex-shrink:0;padding-top:2px;font-family:var(--mono);transition:transform .15s}
5360
+
5361
+ .insights-section{display:flex;align-items:center;gap:8px;padding:14px 0 8px;margin-top:4px;font-size:12px;font-weight:700;color:var(--text);text-transform:uppercase;letter-spacing:.5px;border-top:1px solid var(--border);user-select:none}
5362
+ .insights-section:first-child{border-top:none;margin-top:0}
5363
+ .insights-section-icon{font-size:11px;width:16px;text-align:center}
5364
+ .insights-section-count{font-size:11px;font-family:var(--mono);color:var(--text-muted);background:var(--bg-muted);padding:1px 7px;border-radius:8px;font-weight:500}
5365
+ .insights-section-regressed{color:var(--red)}
5366
+ .insights-section-regressed .insights-section-count{color:var(--red);background:var(--red-bg)}
5367
+ .insights-section-verifying{color:var(--amber)}
5368
+ .insights-section-verifying .insights-section-count{color:var(--amber);background:var(--amber-bg)}
5369
+ .insights-section-resolved{color:var(--green)}
5370
+ .insights-section-resolved .insights-section-count{color:var(--green);background:var(--green-bg)}
5371
+ .insights-section-dismissed{color:var(--text-muted);cursor:pointer}
5372
+ .insights-section-dismissed:hover{color:var(--text)}
5373
+ `;
5374
+ }
5375
+ var init_insights3 = __esm({
5376
+ "src/dashboard/styles/insights.ts"() {
5377
+ "use strict";
5378
+ }
5379
+ });
5380
+
4782
5381
  // src/dashboard/styles.ts
4783
5382
  function getStyles() {
4784
- return getBaseStyles() + getLayoutStyles() + getFlowStyles() + getRequestStyles() + getPerformanceStyles() + getOverviewStyles() + getSecurityStyles() + getTimelineStyles();
5383
+ return getBaseStyles() + getLayoutStyles() + getFlowStyles() + getRequestStyles() + getPerformanceStyles() + getOverviewStyles() + getSecurityStyles() + getTimelineStyles() + getGraphViewStyles() + getExplorerStyles() + getInsightsStyles();
4785
5384
  }
4786
5385
  var init_styles = __esm({
4787
5386
  "src/dashboard/styles.ts"() {
@@ -4790,10 +5389,13 @@ var init_styles = __esm({
4790
5389
  init_layout();
4791
5390
  init_flows();
4792
5391
  init_requests();
4793
- init_graph();
5392
+ init_graph2();
4794
5393
  init_overview();
4795
5394
  init_security2();
4796
5395
  init_timeline();
5396
+ init_graph_view();
5397
+ init_explorer();
5398
+ init_insights3();
4797
5399
  }
4798
5400
  });
4799
5401
 
@@ -4976,6 +5578,12 @@ function recordDashboardOpened() {
4976
5578
  request_count_at_open: session.requestCount
4977
5579
  });
4978
5580
  }
5581
+ function recordGraphFeature(feature, detail) {
5582
+ trackEvent(TELEMETRY_EVENT_GRAPH_FEATURE, {
5583
+ feature,
5584
+ ...detail ? { detail } : {}
5585
+ });
5586
+ }
4979
5587
  function recordSetupCompleted(info) {
4980
5588
  session.frameworkCandidates = info.frameworkCandidates;
4981
5589
  session.adaptersFailed = info.adaptersFailed;
@@ -5035,8 +5643,8 @@ function trackSession(services) {
5035
5643
  tabs_viewed: [...session.tabsViewed],
5036
5644
  dashboard_opened: session.dashboardOpened,
5037
5645
  explain_used: session.explainUsed,
5038
- session_duration_s: Math.round((now - session.startTime) / 1e3),
5039
- // Enhanced fields
5646
+ session_duration_s: Math.ceil((now - session.startTime) / 1e3),
5647
+ session_duration_ms: now - session.startTime,
5040
5648
  setup_succeeded: session.setupSucceeded,
5041
5649
  setup_duration_ms: session.setupDurationMs,
5042
5650
  framework_detection_candidates: session.frameworkCandidates,
@@ -5108,11 +5716,19 @@ function createDashboardHandler(services) {
5108
5716
  issueStore,
5109
5717
  services.bus
5110
5718
  );
5719
+ routes[DASHBOARD_API_GRAPH] = createGraphHandler(services);
5111
5720
  routes[DASHBOARD_API_TAB] = (req, res) => {
5112
- const raw = (req.url ?? "").split("tab=")[1];
5113
- if (raw) {
5114
- const tab = decodeURIComponent(raw).slice(0, MAX_TAB_NAME_LENGTH);
5115
- if (VALID_TABS.has(tab) && isTelemetryEnabled()) recordTabViewed(tab);
5721
+ if (isTelemetryEnabled()) {
5722
+ const url = new URL(req.url ?? "/", "http://localhost");
5723
+ const tab = url.searchParams.get("tab");
5724
+ if (tab && tab.length <= MAX_TAB_NAME_LENGTH && VALID_TABS.has(tab)) {
5725
+ recordTabViewed(tab);
5726
+ }
5727
+ const event = url.searchParams.get("event");
5728
+ if (event && event.length <= MAX_TAB_NAME_LENGTH) {
5729
+ const detail = url.searchParams.get("detail") ?? void 0;
5730
+ recordGraphFeature(event, detail?.slice(0, MAX_TAB_NAME_LENGTH));
5731
+ }
5116
5732
  }
5117
5733
  res.writeHead(HTTP_NO_CONTENT);
5118
5734
  res.end();
@@ -5141,6 +5757,7 @@ var init_router = __esm({
5141
5757
  init_labels();
5142
5758
  init_api();
5143
5759
  init_issues();
5760
+ init_graph();
5144
5761
  init_sse();
5145
5762
  init_page();
5146
5763
  init_telemetry();
@@ -5557,116 +6174,732 @@ var init_metrics_store = __esm({
5557
6174
  if (epMetrics.sessions.length > METRICS_MAX_SESSIONS) {
5558
6175
  epMetrics.sessions = epMetrics.sessions.slice(-METRICS_MAX_SESSIONS);
5559
6176
  }
5560
- acc.durations.length = 0;
5561
- acc.queryCounts.length = 0;
5562
- acc.errorCount = 0;
6177
+ acc.durations.length = 0;
6178
+ acc.queryCounts.length = 0;
6179
+ acc.errorCount = 0;
6180
+ }
6181
+ for (const [endpoint, points] of this.pendingPoints) {
6182
+ if (points.length === 0) continue;
6183
+ const epMetrics = this.getOrCreateEndpoint(endpoint);
6184
+ const existing = epMetrics.dataPoints ?? [];
6185
+ epMetrics.dataPoints = existing.concat(points).slice(-METRICS_MAX_DATA_POINTS);
6186
+ }
6187
+ this.pendingPoints.clear();
6188
+ this.baselineCache.clear();
6189
+ if (!this.dirty) return;
6190
+ if (sync) {
6191
+ this.persistence.saveSync(this.data);
6192
+ } else {
6193
+ this.persistence.save(this.data);
6194
+ }
6195
+ this.dirty = false;
6196
+ }
6197
+ getOrCreateEndpoint(endpoint) {
6198
+ let ep = this.endpointIndex.get(endpoint);
6199
+ if (!ep) {
6200
+ ep = { endpoint, sessions: [] };
6201
+ this.data.endpoints.push(ep);
6202
+ this.endpointIndex.set(endpoint, ep);
6203
+ }
6204
+ return ep;
6205
+ }
6206
+ };
6207
+ }
6208
+ });
6209
+
6210
+ // src/store/metrics/persistence.ts
6211
+ import { readFile as readFile4 } from "fs/promises";
6212
+ import { readFileSync as readFileSync5, existsSync as existsSync6, unlinkSync as unlinkSync2 } from "fs";
6213
+ import { resolve as resolve4 } from "path";
6214
+ var DEFAULT_METRICS, FileMetricsPersistence;
6215
+ var init_persistence = __esm({
6216
+ "src/store/metrics/persistence.ts"() {
6217
+ "use strict";
6218
+ init_constants();
6219
+ init_atomic_writer();
6220
+ init_fs();
6221
+ init_log();
6222
+ init_type_guards();
6223
+ DEFAULT_METRICS = { version: 1, endpoints: [] };
6224
+ FileMetricsPersistence = class {
6225
+ constructor(dataDir) {
6226
+ this.metricsPath = resolve4(dataDir, METRICS_FILE);
6227
+ this.writer = new AtomicWriter({
6228
+ dir: dataDir,
6229
+ filePath: this.metricsPath,
6230
+ label: "metrics"
6231
+ });
6232
+ }
6233
+ load() {
6234
+ try {
6235
+ if (existsSync6(this.metricsPath)) {
6236
+ return this.parseMetrics(readFileSync5(this.metricsPath, "utf-8"));
6237
+ }
6238
+ } catch (err) {
6239
+ brakitWarn(`failed to load ${this.metricsPath}: ${getErrorMessage(err)}`);
6240
+ }
6241
+ return { ...DEFAULT_METRICS };
6242
+ }
6243
+ async loadAsync() {
6244
+ try {
6245
+ if (await fileExists(this.metricsPath)) {
6246
+ return this.parseMetrics(await readFile4(this.metricsPath, "utf-8"));
6247
+ }
6248
+ } catch (err) {
6249
+ brakitWarn(`failed to load ${this.metricsPath}: ${getErrorMessage(err)}`);
6250
+ }
6251
+ return { ...DEFAULT_METRICS };
6252
+ }
6253
+ /** Parse and validate metrics JSON, returning default empty data on invalid input. */
6254
+ parseMetrics(raw) {
6255
+ return validateMetricsData(JSON.parse(raw)) ?? { ...DEFAULT_METRICS };
6256
+ }
6257
+ save(data) {
6258
+ this.writer.writeAsync(JSON.stringify(data));
6259
+ }
6260
+ saveSync(data) {
6261
+ this.writer.writeSync(JSON.stringify(data));
6262
+ }
6263
+ remove() {
6264
+ try {
6265
+ if (existsSync6(this.metricsPath)) {
6266
+ unlinkSync2(this.metricsPath);
6267
+ }
6268
+ } catch (err) {
6269
+ brakitDebug(`failed to remove metrics file: ${getErrorMessage(err)}`);
6270
+ }
6271
+ }
6272
+ };
6273
+ }
6274
+ });
6275
+
6276
+ // src/store/index.ts
6277
+ var init_store = __esm({
6278
+ "src/store/index.ts"() {
6279
+ "use strict";
6280
+ init_request_store();
6281
+ init_telemetry_store();
6282
+ init_metrics_store();
6283
+ init_persistence();
6284
+ }
6285
+ });
6286
+
6287
+ // src/graph/constants.ts
6288
+ var MAX_PATTERNS_PER_EDGE, CLUSTER_SPLIT_THRESHOLD, COMMON_PATH_PREFIXES, PENDING_BUFFER_MAX, PENDING_EVICTION_TARGET, PENDING_TTL_MS;
6289
+ var init_constants2 = __esm({
6290
+ "src/graph/constants.ts"() {
6291
+ "use strict";
6292
+ MAX_PATTERNS_PER_EDGE = 10;
6293
+ CLUSTER_SPLIT_THRESHOLD = 15;
6294
+ COMMON_PATH_PREFIXES = /* @__PURE__ */ new Set(["api", "v1", "v2", "v3", "v4"]);
6295
+ PENDING_BUFFER_MAX = 500;
6296
+ PENDING_EVICTION_TARGET = 200;
6297
+ PENDING_TTL_MS = 6e4;
6298
+ }
6299
+ });
6300
+
6301
+ // src/graph/graph-builder.ts
6302
+ function shouldSkipRequest(req) {
6303
+ if (req.isStatic || req.isHealthCheck) return true;
6304
+ if (isDashboardRequest(req.path)) return true;
6305
+ return false;
6306
+ }
6307
+ function extractHostname(url) {
6308
+ try {
6309
+ const parsed = new URL(url);
6310
+ const host = parsed.hostname;
6311
+ if (host === "localhost" || host === "127.0.0.1" || host === "::1")
6312
+ return null;
6313
+ return host;
6314
+ } catch {
6315
+ return null;
6316
+ }
6317
+ }
6318
+ function makeEdgeId(source, target) {
6319
+ return `${source} -> ${target}`;
6320
+ }
6321
+ function upsertNode(graph, id, type, label, now) {
6322
+ let node = graph.nodes.get(id);
6323
+ if (!node) {
6324
+ node = {
6325
+ id,
6326
+ type,
6327
+ label,
6328
+ stats: {
6329
+ requestCount: 0,
6330
+ avgLatencyMs: 0,
6331
+ errorRate: 0,
6332
+ avgQueryCount: 0,
6333
+ lastSeenAt: now,
6334
+ firstSeenAt: now
6335
+ }
6336
+ };
6337
+ graph.nodes.set(id, node);
6338
+ }
6339
+ node.stats.lastSeenAt = now;
6340
+ return node;
6341
+ }
6342
+ function upsertEdge(graph, source, target, type, now) {
6343
+ const id = makeEdgeId(source, target);
6344
+ let edge = graph.edges.get(id);
6345
+ if (!edge) {
6346
+ edge = {
6347
+ id,
6348
+ source,
6349
+ target,
6350
+ type,
6351
+ stats: {
6352
+ frequency: 0,
6353
+ avgLatencyMs: 0,
6354
+ lastSeenAt: now,
6355
+ firstSeenAt: now
6356
+ }
6357
+ };
6358
+ graph.edges.set(id, edge);
6359
+ }
6360
+ edge.stats.lastSeenAt = now;
6361
+ return edge;
6362
+ }
6363
+ function updateRollingAvg(current, newValue, count) {
6364
+ return Math.round(current + (newValue - current) / count);
6365
+ }
6366
+ var GraphBuilder;
6367
+ var init_graph_builder = __esm({
6368
+ "src/graph/graph-builder.ts"() {
6369
+ "use strict";
6370
+ init_endpoint();
6371
+ init_normalize();
6372
+ init_router();
6373
+ init_categorize();
6374
+ init_constants2();
6375
+ GraphBuilder = class {
6376
+ constructor(bus, requestStore) {
6377
+ this.bus = bus;
6378
+ this.requestStore = requestStore;
6379
+ this.graph = {
6380
+ nodes: /* @__PURE__ */ new Map(),
6381
+ edges: /* @__PURE__ */ new Map(),
6382
+ metadata: { totalObservations: 0, lastUpdatedAt: Date.now() }
6383
+ };
6384
+ /**
6385
+ * Buffered telemetry events waiting for their parent request to complete.
6386
+ * Key = parentRequestId, value = pending queries/fetches.
6387
+ */
6388
+ this.pending = /* @__PURE__ */ new Map();
6389
+ /** Accumulated request categories per endpoint node. */
6390
+ this.nodeCategories = /* @__PURE__ */ new Map();
6391
+ /** Latest analysis snapshot — refreshed on every analysis:updated event. */
6392
+ this.latestAnalysis = null;
6393
+ this.cleanupUnsubs = [];
6394
+ }
6395
+ start() {
6396
+ this.cleanupUnsubs.push(
6397
+ this.bus.on("request:completed", (req) => this.handleRequest(req)),
6398
+ this.bus.on("telemetry:query", (q) => this.handleQuery(q)),
6399
+ this.bus.on("telemetry:fetch", (f) => this.handleFetch(f)),
6400
+ this.bus.on(
6401
+ "analysis:updated",
6402
+ (update) => this.handleAnalysisUpdate(update)
6403
+ )
6404
+ );
6405
+ }
6406
+ stop() {
6407
+ for (const unsub of this.cleanupUnsubs) unsub();
6408
+ this.cleanupUnsubs.length = 0;
6409
+ }
6410
+ getGraph() {
6411
+ return this.graph;
6412
+ }
6413
+ /**
6414
+ * Enrich endpoint nodes with p95 from an external metrics source.
6415
+ * Called by the API handler which has access to MetricsStore.
6416
+ */
6417
+ enrichWithMetrics(getP95) {
6418
+ for (const node of this.graph.nodes.values()) {
6419
+ if (node.type !== "endpoint") continue;
6420
+ const key = node.label;
6421
+ const p95 = getP95(key);
6422
+ if (p95 !== void 0) {
6423
+ if (!node.annotations) node.annotations = {};
6424
+ node.annotations.p95Ms = Math.round(p95);
6425
+ }
6426
+ }
6427
+ }
6428
+ getApiResponse(options) {
6429
+ const grouping = options?.grouping ?? "path";
6430
+ const clusters = this.computeClusters(grouping);
6431
+ if (options?.level === "endpoints") {
6432
+ return this.getEndpointView();
6433
+ }
6434
+ if (options?.node) {
6435
+ return this.getNodeNeighborhood(options.node, clusters);
6436
+ }
6437
+ if (options?.cluster) {
6438
+ return this.getClusterExpanded(options.cluster, clusters);
6439
+ }
6440
+ return this.getClusterView(clusters);
6441
+ }
6442
+ clear() {
6443
+ this.graph.nodes.clear();
6444
+ this.graph.edges.clear();
6445
+ this.graph.metadata.totalObservations = 0;
6446
+ this.pending.clear();
6447
+ this.nodeCategories.clear();
6448
+ this.latestAnalysis = null;
6449
+ }
6450
+ handleAnalysisUpdate(update) {
6451
+ this.latestAnalysis = update;
6452
+ this.enrichNodesFromAnalysis();
6453
+ }
6454
+ enrichNodesFromAnalysis() {
6455
+ if (!this.latestAnalysis) return;
6456
+ const { findings, insights, issues } = this.latestAnalysis;
6457
+ for (const node of this.graph.nodes.values()) {
6458
+ if (node.type !== "endpoint") continue;
6459
+ if (node.annotations) {
6460
+ node.annotations.securityFindings = void 0;
6461
+ node.annotations.insights = void 0;
6462
+ node.annotations.openIssueCount = void 0;
6463
+ }
6464
+ }
6465
+ for (const f of findings) {
6466
+ if (!f.endpoint) continue;
6467
+ const nodeId = `endpoint:${f.endpoint}`;
6468
+ const node = this.graph.nodes.get(nodeId);
6469
+ if (!node) continue;
6470
+ if (!node.annotations) node.annotations = {};
6471
+ if (!node.annotations.securityFindings)
6472
+ node.annotations.securityFindings = [];
6473
+ const existing = node.annotations.securityFindings.find(
6474
+ (s) => s.rule === f.rule
6475
+ );
6476
+ if (existing) {
6477
+ existing.count = f.count;
6478
+ } else {
6479
+ node.annotations.securityFindings.push({
6480
+ rule: f.rule,
6481
+ severity: f.severity,
6482
+ title: f.title,
6483
+ count: f.count
6484
+ });
6485
+ }
6486
+ }
6487
+ const endpointNodesByLabel = /* @__PURE__ */ new Map();
6488
+ for (const node of this.graph.nodes.values()) {
6489
+ if (node.type === "endpoint") endpointNodesByLabel.set(node.label, node);
6490
+ }
6491
+ for (const insight of insights) {
6492
+ if (!insight.nav) continue;
6493
+ for (const [label, node] of endpointNodesByLabel) {
6494
+ if (insight.title.includes(label) || insight.desc.includes(label)) {
6495
+ if (!node.annotations) node.annotations = {};
6496
+ if (!node.annotations.insights) node.annotations.insights = [];
6497
+ if (!node.annotations.insights.some(
6498
+ (i) => i.type === insight.type && i.title === insight.title
6499
+ )) {
6500
+ node.annotations.insights.push({
6501
+ type: insight.type,
6502
+ severity: insight.severity,
6503
+ title: insight.title
6504
+ });
6505
+ }
6506
+ }
6507
+ }
6508
+ }
6509
+ for (const si of issues) {
6510
+ if (!si.issue.endpoint) continue;
6511
+ const nodeId = `endpoint:${si.issue.endpoint}`;
6512
+ const node = this.graph.nodes.get(nodeId);
6513
+ if (!node) continue;
6514
+ if (si.state === "open" || si.state === "regressed") {
6515
+ if (!node.annotations) node.annotations = {};
6516
+ node.annotations.openIssueCount = (node.annotations.openIssueCount ?? 0) + 1;
6517
+ }
6518
+ }
6519
+ for (const insight of insights) {
6520
+ if (insight.type !== "n1" && insight.type !== "redundant-query") continue;
6521
+ for (const edge of this.graph.edges.values()) {
6522
+ if (edge.type !== "reads" && edge.type !== "writes") continue;
6523
+ const sourceNode = this.graph.nodes.get(edge.source);
6524
+ if (sourceNode?.annotations?.insights?.some(
6525
+ (i) => i.type === insight.type
6526
+ )) {
6527
+ if (!edge.annotations) edge.annotations = {};
6528
+ edge.annotations.hasIssue = true;
6529
+ }
6530
+ }
6531
+ }
6532
+ }
6533
+ handleRequest(req) {
6534
+ if (shouldSkipRequest(req)) return;
6535
+ const now = Date.now();
6536
+ const endpointKey = getEndpointKey(req.method, req.path);
6537
+ const nodeId = `endpoint:${endpointKey}`;
6538
+ const node = upsertNode(this.graph, nodeId, "endpoint", endpointKey, now);
6539
+ node.stats.requestCount++;
6540
+ const category = detectCategory(req);
6541
+ let cats = this.nodeCategories.get(nodeId);
6542
+ if (!cats) {
6543
+ cats = /* @__PURE__ */ new Set();
6544
+ this.nodeCategories.set(nodeId, cats);
6545
+ }
6546
+ cats.add(category);
6547
+ if (!node.annotations) node.annotations = {};
6548
+ node.annotations.categories = [...cats];
6549
+ node.annotations.isMiddleware = cats.has("middleware");
6550
+ if (!node.annotations.hasAuth) {
6551
+ node.annotations.hasAuth = cats.has("auth-check") || cats.has("auth-handshake") || hasAuthCredentials(req);
6552
+ }
6553
+ node.stats.avgLatencyMs = updateRollingAvg(
6554
+ node.stats.avgLatencyMs,
6555
+ req.durationMs,
6556
+ node.stats.requestCount
6557
+ );
6558
+ const isError = req.statusCode >= 400 ? 1 : 0;
6559
+ node.stats.errorRate = node.stats.errorRate + (isError - node.stats.errorRate) / node.stats.requestCount;
6560
+ this.graph.metadata.totalObservations++;
6561
+ this.graph.metadata.lastUpdatedAt = now;
6562
+ const buffered = this.pending.get(req.id);
6563
+ if (buffered) {
6564
+ let queryCount = 0;
6565
+ for (const query of buffered.queries) {
6566
+ this.processQuery(query, nodeId, now);
6567
+ queryCount++;
6568
+ }
6569
+ for (const fetch of buffered.fetches) {
6570
+ this.processFetch(fetch, nodeId, now);
6571
+ }
6572
+ this.pending.delete(req.id);
6573
+ if (queryCount > 0) {
6574
+ node.stats.avgQueryCount = updateRollingAvg(
6575
+ node.stats.avgQueryCount,
6576
+ queryCount,
6577
+ node.stats.requestCount
6578
+ );
6579
+ }
5563
6580
  }
5564
- for (const [endpoint, points] of this.pendingPoints) {
5565
- if (points.length === 0) continue;
5566
- const epMetrics = this.getOrCreateEndpoint(endpoint);
5567
- const existing = epMetrics.dataPoints ?? [];
5568
- epMetrics.dataPoints = existing.concat(points).slice(-METRICS_MAX_DATA_POINTS);
6581
+ const now2 = Date.now();
6582
+ for (const [key, entry] of this.pending) {
6583
+ if (now2 - entry.createdAt > PENDING_TTL_MS) this.pending.delete(key);
5569
6584
  }
5570
- this.pendingPoints.clear();
5571
- this.baselineCache.clear();
5572
- if (!this.dirty) return;
5573
- if (sync) {
5574
- this.persistence.saveSync(this.data);
5575
- } else {
5576
- this.persistence.save(this.data);
6585
+ if (this.pending.size > PENDING_BUFFER_MAX) {
6586
+ const keys = [...this.pending.keys()];
6587
+ for (let i = 0; i < keys.length - PENDING_EVICTION_TARGET; i++) {
6588
+ this.pending.delete(keys[i]);
6589
+ }
5577
6590
  }
5578
- this.dirty = false;
5579
6591
  }
5580
- getOrCreateEndpoint(endpoint) {
5581
- let ep = this.endpointIndex.get(endpoint);
5582
- if (!ep) {
5583
- ep = { endpoint, sessions: [] };
5584
- this.data.endpoints.push(ep);
5585
- this.endpointIndex.set(endpoint, ep);
6592
+ handleQuery(query) {
6593
+ if (!query.parentRequestId || !query.table) return;
6594
+ let pending2 = this.pending.get(query.parentRequestId);
6595
+ if (!pending2) {
6596
+ pending2 = { queries: [], fetches: [], createdAt: Date.now() };
6597
+ this.pending.set(query.parentRequestId, pending2);
5586
6598
  }
5587
- return ep;
5588
- }
5589
- };
5590
- }
5591
- });
5592
-
5593
- // src/store/metrics/persistence.ts
5594
- import { readFile as readFile4 } from "fs/promises";
5595
- import { readFileSync as readFileSync5, existsSync as existsSync6, unlinkSync as unlinkSync2 } from "fs";
5596
- import { resolve as resolve4 } from "path";
5597
- var DEFAULT_METRICS, FileMetricsPersistence;
5598
- var init_persistence = __esm({
5599
- "src/store/metrics/persistence.ts"() {
5600
- "use strict";
5601
- init_constants();
5602
- init_atomic_writer();
5603
- init_fs();
5604
- init_log();
5605
- init_type_guards();
5606
- DEFAULT_METRICS = { version: 1, endpoints: [] };
5607
- FileMetricsPersistence = class {
5608
- constructor(dataDir) {
5609
- this.metricsPath = resolve4(dataDir, METRICS_FILE);
5610
- this.writer = new AtomicWriter({
5611
- dir: dataDir,
5612
- filePath: this.metricsPath,
5613
- label: "metrics"
5614
- });
6599
+ pending2.queries.push(query);
5615
6600
  }
5616
- load() {
5617
- try {
5618
- if (existsSync6(this.metricsPath)) {
5619
- return this.parseMetrics(readFileSync5(this.metricsPath, "utf-8"));
6601
+ handleFetch(fetch) {
6602
+ if (!fetch.parentRequestId) return;
6603
+ const hostname = extractHostname(fetch.url);
6604
+ if (!hostname) return;
6605
+ let pending2 = this.pending.get(fetch.parentRequestId);
6606
+ if (!pending2) {
6607
+ pending2 = { queries: [], fetches: [], createdAt: Date.now() };
6608
+ this.pending.set(fetch.parentRequestId, pending2);
6609
+ }
6610
+ pending2.fetches.push(fetch);
6611
+ }
6612
+ // ── Process buffered events (called after request completes) ──
6613
+ processQuery(query, endpointNodeId, now) {
6614
+ if (!query.table) return;
6615
+ const tableNodeId = `table:${query.table}`;
6616
+ upsertNode(this.graph, tableNodeId, "table", query.table, now);
6617
+ const edgeType = query.normalizedOp === "SELECT" ? "reads" : "writes";
6618
+ const edge = upsertEdge(
6619
+ this.graph,
6620
+ endpointNodeId,
6621
+ tableNodeId,
6622
+ edgeType,
6623
+ now
6624
+ );
6625
+ edge.stats.frequency++;
6626
+ edge.stats.avgLatencyMs = updateRollingAvg(
6627
+ edge.stats.avgLatencyMs,
6628
+ query.durationMs,
6629
+ edge.stats.frequency
6630
+ );
6631
+ if (query.sql) {
6632
+ const normalized = normalizeQueryParams(query.sql);
6633
+ if (normalized) {
6634
+ if (!edge.patterns) edge.patterns = [];
6635
+ if (!edge.patterns.includes(normalized) && edge.patterns.length < MAX_PATTERNS_PER_EDGE) {
6636
+ edge.patterns.push(normalized);
6637
+ }
5620
6638
  }
5621
- } catch (err) {
5622
- brakitWarn(`failed to load ${this.metricsPath}: ${getErrorMessage(err)}`);
5623
6639
  }
5624
- return { ...DEFAULT_METRICS };
5625
6640
  }
5626
- async loadAsync() {
5627
- try {
5628
- if (await fileExists(this.metricsPath)) {
5629
- return this.parseMetrics(await readFile4(this.metricsPath, "utf-8"));
6641
+ processFetch(fetch, endpointNodeId, now) {
6642
+ const hostname = extractHostname(fetch.url);
6643
+ if (!hostname) return;
6644
+ const externalNodeId = `external:${hostname}`;
6645
+ upsertNode(this.graph, externalNodeId, "external", hostname, now);
6646
+ const edge = upsertEdge(
6647
+ this.graph,
6648
+ endpointNodeId,
6649
+ externalNodeId,
6650
+ "fetches",
6651
+ now
6652
+ );
6653
+ edge.stats.frequency++;
6654
+ edge.stats.avgLatencyMs = updateRollingAvg(
6655
+ edge.stats.avgLatencyMs,
6656
+ fetch.durationMs,
6657
+ edge.stats.frequency
6658
+ );
6659
+ }
6660
+ // ── Clustering ──
6661
+ computeClusters(strategy = "path") {
6662
+ const endpointNodes = [...this.graph.nodes.values()].filter(
6663
+ (n) => n.type === "endpoint"
6664
+ );
6665
+ if (strategy === "auth-boundary") {
6666
+ return this.clusterByAuthBoundary(endpointNodes);
6667
+ }
6668
+ if (strategy === "data-domain") {
6669
+ return this.clusterByDataDomain(endpointNodes);
6670
+ }
6671
+ return this.clusterByPath(endpointNodes);
6672
+ }
6673
+ clusterByPath(endpointNodes) {
6674
+ const groups = /* @__PURE__ */ new Map();
6675
+ for (const node of endpointNodes) {
6676
+ const parts = node.label.split(" ");
6677
+ const path = parts[1] || parts[0];
6678
+ const segments = path.split("/").filter(Boolean);
6679
+ let i = 0;
6680
+ while (i < segments.length && COMMON_PATH_PREFIXES.has(segments[i])) {
6681
+ i++;
5630
6682
  }
5631
- } catch (err) {
5632
- brakitWarn(`failed to load ${this.metricsPath}: ${getErrorMessage(err)}`);
6683
+ const groupKey = segments[i] || segments[0] || "root";
6684
+ if (!groups.has(groupKey)) groups.set(groupKey, []);
6685
+ groups.get(groupKey).push(node.id);
5633
6686
  }
5634
- return { ...DEFAULT_METRICS };
6687
+ const clusters = /* @__PURE__ */ new Map();
6688
+ for (const [key, children] of groups) {
6689
+ if (children.length > CLUSTER_SPLIT_THRESHOLD) {
6690
+ const subGroups = /* @__PURE__ */ new Map();
6691
+ for (const childId of children) {
6692
+ const node = this.graph.nodes.get(childId);
6693
+ const parts = node.label.split(" ");
6694
+ const path = parts[1] || parts[0];
6695
+ const segments = path.split("/").filter(Boolean);
6696
+ let idx = segments.indexOf(key);
6697
+ if (idx === -1) idx = 0;
6698
+ const subKey = `${key}/${segments[idx + 1] || "root"}`;
6699
+ if (!subGroups.has(subKey)) subGroups.set(subKey, []);
6700
+ subGroups.get(subKey).push(childId);
6701
+ }
6702
+ for (const [subKey, subChildren] of subGroups) {
6703
+ clusters.set(subKey, this.buildCluster(subKey, subChildren));
6704
+ }
6705
+ } else {
6706
+ clusters.set(key, this.buildCluster(key, children));
6707
+ }
6708
+ }
6709
+ return clusters;
6710
+ }
6711
+ clusterByAuthBoundary(endpointNodes) {
6712
+ const authed = [];
6713
+ const unauthed = [];
6714
+ for (const node of endpointNodes) {
6715
+ if (node.annotations?.hasAuth) {
6716
+ authed.push(node.id);
6717
+ } else {
6718
+ unauthed.push(node.id);
6719
+ }
6720
+ }
6721
+ const clusters = /* @__PURE__ */ new Map();
6722
+ if (authed.length > 0)
6723
+ clusters.set("authenticated", this.buildCluster("authenticated", authed));
6724
+ if (unauthed.length > 0)
6725
+ clusters.set(
6726
+ "unauthenticated",
6727
+ this.buildCluster("unauthenticated", unauthed)
6728
+ );
6729
+ return clusters;
6730
+ }
6731
+ clusterByDataDomain(endpointNodes) {
6732
+ const endpointTables = /* @__PURE__ */ new Map();
6733
+ for (const edge of this.graph.edges.values()) {
6734
+ if (edge.type !== "reads" && edge.type !== "writes") continue;
6735
+ const tableLabel = this.graph.nodes.get(edge.target)?.label;
6736
+ if (!tableLabel) continue;
6737
+ let tables = endpointTables.get(edge.source);
6738
+ if (!tables) {
6739
+ tables = /* @__PURE__ */ new Set();
6740
+ endpointTables.set(edge.source, tables);
6741
+ }
6742
+ tables.add(tableLabel);
6743
+ }
6744
+ const groups = /* @__PURE__ */ new Map();
6745
+ for (const node of endpointNodes) {
6746
+ const tables = endpointTables.get(node.id);
6747
+ const groupKey = tables && tables.size > 0 ? [...tables].sort().join("+") : "no-db";
6748
+ if (!groups.has(groupKey)) groups.set(groupKey, []);
6749
+ groups.get(groupKey).push(node.id);
6750
+ }
6751
+ const clusters = /* @__PURE__ */ new Map();
6752
+ for (const [key, children] of groups) {
6753
+ clusters.set(key, this.buildCluster(key, children));
6754
+ }
6755
+ return clusters;
6756
+ }
6757
+ buildCluster(key, children) {
6758
+ let totalLatency = 0;
6759
+ let totalRequests = 0;
6760
+ let totalErrors = 0;
6761
+ let totalQueries = 0;
6762
+ let firstSeen = Infinity;
6763
+ let lastSeen = 0;
6764
+ for (const childId of children) {
6765
+ const node = this.graph.nodes.get(childId);
6766
+ if (!node) continue;
6767
+ totalRequests += node.stats.requestCount;
6768
+ totalLatency += node.stats.avgLatencyMs * node.stats.requestCount;
6769
+ totalErrors += node.stats.errorRate * node.stats.requestCount;
6770
+ totalQueries += node.stats.avgQueryCount * node.stats.requestCount;
6771
+ firstSeen = Math.min(firstSeen, node.stats.firstSeenAt);
6772
+ lastSeen = Math.max(lastSeen, node.stats.lastSeenAt);
6773
+ }
6774
+ return {
6775
+ id: `cluster:${key}`,
6776
+ label: key,
6777
+ children,
6778
+ stats: {
6779
+ requestCount: totalRequests,
6780
+ avgLatencyMs: totalRequests > 0 ? Math.round(totalLatency / totalRequests) : 0,
6781
+ errorRate: totalRequests > 0 ? totalErrors / totalRequests : 0,
6782
+ avgQueryCount: totalRequests > 0 ? Math.round(totalQueries / totalRequests * 10) / 10 : 0,
6783
+ lastSeenAt: lastSeen || Date.now(),
6784
+ firstSeenAt: firstSeen === Infinity ? Date.now() : firstSeen
6785
+ }
6786
+ };
5635
6787
  }
5636
- /** Parse and validate metrics JSON, returning default empty data on invalid input. */
5637
- parseMetrics(raw) {
5638
- return validateMetricsData(JSON.parse(raw)) ?? { ...DEFAULT_METRICS };
6788
+ // ── API response builders ──
6789
+ getEndpointView() {
6790
+ const allNodes = [...this.graph.nodes.values()];
6791
+ const allEdges = [...this.graph.edges.values()];
6792
+ return {
6793
+ nodes: allNodes,
6794
+ edges: allEdges,
6795
+ clusters: [],
6796
+ metadata: this.graph.metadata
6797
+ };
5639
6798
  }
5640
- save(data) {
5641
- this.writer.writeAsync(JSON.stringify(data));
6799
+ getClusterView(clusters) {
6800
+ const clusterArr = [...clusters.values()];
6801
+ const otherNodes = [...this.graph.nodes.values()].filter(
6802
+ (n) => n.type !== "endpoint"
6803
+ );
6804
+ const endpointToCluster = /* @__PURE__ */ new Map();
6805
+ for (const c of clusterArr) {
6806
+ for (const childId of c.children) {
6807
+ endpointToCluster.set(childId, c.id);
6808
+ }
6809
+ }
6810
+ const edgeAgg = /* @__PURE__ */ new Map();
6811
+ for (const edge of this.graph.edges.values()) {
6812
+ const sourceCluster = endpointToCluster.get(edge.source) ?? edge.source;
6813
+ const target = edge.target;
6814
+ const aggId = makeEdgeId(sourceCluster, target);
6815
+ let agg = edgeAgg.get(aggId);
6816
+ if (!agg) {
6817
+ agg = {
6818
+ id: aggId,
6819
+ source: sourceCluster,
6820
+ target,
6821
+ type: edge.type,
6822
+ stats: { ...edge.stats },
6823
+ patterns: edge.patterns ? [...edge.patterns] : void 0
6824
+ };
6825
+ edgeAgg.set(aggId, agg);
6826
+ } else {
6827
+ agg.stats.frequency += edge.stats.frequency;
6828
+ agg.stats.avgLatencyMs = Math.round(
6829
+ (agg.stats.avgLatencyMs + edge.stats.avgLatencyMs) / 2
6830
+ );
6831
+ agg.stats.lastSeenAt = Math.max(
6832
+ agg.stats.lastSeenAt,
6833
+ edge.stats.lastSeenAt
6834
+ );
6835
+ agg.stats.firstSeenAt = Math.min(
6836
+ agg.stats.firstSeenAt,
6837
+ edge.stats.firstSeenAt
6838
+ );
6839
+ }
6840
+ }
6841
+ return {
6842
+ nodes: otherNodes,
6843
+ edges: [...edgeAgg.values()],
6844
+ clusters: clusterArr,
6845
+ metadata: this.graph.metadata
6846
+ };
5642
6847
  }
5643
- saveSync(data) {
5644
- this.writer.writeSync(JSON.stringify(data));
6848
+ getClusterExpanded(clusterId, clusters) {
6849
+ const clusterKey = clusterId.startsWith("cluster:") ? clusterId.slice(8) : clusterId;
6850
+ const cluster = clusters.get(clusterKey);
6851
+ if (!cluster) return this.getClusterView(clusters);
6852
+ const childNodes = [];
6853
+ const connectedNodeIds = /* @__PURE__ */ new Set();
6854
+ const relevantEdges = [];
6855
+ for (const childId of cluster.children) {
6856
+ const node = this.graph.nodes.get(childId);
6857
+ if (node) childNodes.push(node);
6858
+ for (const edge of this.graph.edges.values()) {
6859
+ if (edge.source === childId || edge.target === childId) {
6860
+ relevantEdges.push(edge);
6861
+ if (edge.source !== childId) connectedNodeIds.add(edge.source);
6862
+ if (edge.target !== childId) connectedNodeIds.add(edge.target);
6863
+ }
6864
+ }
6865
+ }
6866
+ for (const nodeId of connectedNodeIds) {
6867
+ if (!cluster.children.includes(nodeId)) {
6868
+ const node = this.graph.nodes.get(nodeId);
6869
+ if (node) childNodes.push(node);
6870
+ }
6871
+ }
6872
+ return {
6873
+ nodes: childNodes,
6874
+ edges: relevantEdges,
6875
+ clusters: [cluster],
6876
+ metadata: this.graph.metadata
6877
+ };
5645
6878
  }
5646
- remove() {
5647
- try {
5648
- if (existsSync6(this.metricsPath)) {
5649
- unlinkSync2(this.metricsPath);
6879
+ getNodeNeighborhood(nodeId, clusters) {
6880
+ const centerNode = this.graph.nodes.get(nodeId);
6881
+ if (!centerNode) return this.getClusterView(clusters);
6882
+ const nodes = [centerNode];
6883
+ const edges = [];
6884
+ for (const edge of this.graph.edges.values()) {
6885
+ if (edge.source === nodeId || edge.target === nodeId) {
6886
+ edges.push(edge);
6887
+ const otherId = edge.source === nodeId ? edge.target : edge.source;
6888
+ const otherNode = this.graph.nodes.get(otherId);
6889
+ if (otherNode) nodes.push(otherNode);
5650
6890
  }
5651
- } catch (err) {
5652
- brakitDebug(`failed to remove metrics file: ${getErrorMessage(err)}`);
5653
6891
  }
6892
+ return {
6893
+ nodes,
6894
+ edges,
6895
+ clusters: [],
6896
+ metadata: this.graph.metadata
6897
+ };
5654
6898
  }
5655
6899
  };
5656
6900
  }
5657
6901
  });
5658
6902
 
5659
- // src/store/index.ts
5660
- var init_store = __esm({
5661
- "src/store/index.ts"() {
5662
- "use strict";
5663
- init_request_store();
5664
- init_telemetry_store();
5665
- init_metrics_store();
5666
- init_persistence();
5667
- }
5668
- });
5669
-
5670
6903
  // src/output/terminal.ts
5671
6904
  import pc from "picocolors";
5672
6905
  function print(line) {
@@ -5704,6 +6937,7 @@ function startTerminalInsights(services, proxyPort) {
5704
6937
  const resolvedLines = [];
5705
6938
  const regressedLines = [];
5706
6939
  for (const si of issues) {
6940
+ if (si.aiStatus === "wont_fix") continue;
5707
6941
  if (si.state === "resolved") {
5708
6942
  if (resolvedKeys.has(si.issueId)) continue;
5709
6943
  resolvedKeys.add(si.issueId);
@@ -5848,125 +7082,6 @@ var init_guard = __esm({
5848
7082
  }
5849
7083
  });
5850
7084
 
5851
- // src/runtime/capture.ts
5852
- import { gunzip, brotliDecompress, inflate } from "zlib";
5853
- function outgoingToIncoming(headers2) {
5854
- const result = {};
5855
- for (const [key, value] of Object.entries(headers2)) {
5856
- if (value === void 0) continue;
5857
- if (Array.isArray(value)) {
5858
- result[key] = value.map(String);
5859
- } else {
5860
- result[key] = String(value);
5861
- }
5862
- }
5863
- return result;
5864
- }
5865
- function getDecompressor(encoding) {
5866
- if (encoding === CONTENT_ENCODING_GZIP) return gunzip;
5867
- if (encoding === CONTENT_ENCODING_BR) return brotliDecompress;
5868
- if (encoding === CONTENT_ENCODING_DEFLATE) return inflate;
5869
- return null;
5870
- }
5871
- function decompressAsync(body, encoding) {
5872
- const decompressor = getDecompressor(encoding);
5873
- if (!decompressor) return Promise.resolve(body);
5874
- return new Promise((resolve6) => {
5875
- decompressor(body, (err, result) => {
5876
- resolve6(err ? body : result);
5877
- });
5878
- });
5879
- }
5880
- function toBuffer(chunk) {
5881
- if (Buffer.isBuffer(chunk)) return chunk;
5882
- if (chunk instanceof Uint8Array) return Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
5883
- if (typeof chunk === "string") return Buffer.from(chunk);
5884
- return null;
5885
- }
5886
- function captureInProcess(req, res, requestId, requestStore, isChild = false) {
5887
- const startTime = performance.now();
5888
- const method = req.method ?? "GET";
5889
- const resChunks = [];
5890
- let resSize = 0;
5891
- const originalWrite = res.write;
5892
- const originalEnd = res.end;
5893
- let truncated = false;
5894
- res.write = function(...args) {
5895
- try {
5896
- const chunk = args[0];
5897
- if (chunk != null && typeof chunk !== "function") {
5898
- if (resSize < DEFAULT_MAX_BODY_CAPTURE) {
5899
- const buf = toBuffer(chunk);
5900
- if (buf) {
5901
- resChunks.push(buf);
5902
- resSize += buf.length;
5903
- }
5904
- } else {
5905
- truncated = true;
5906
- }
5907
- }
5908
- } catch (e) {
5909
- brakitDebug(`capture write: ${getErrorMessage(e)}`);
5910
- }
5911
- return originalWrite.apply(this, args);
5912
- };
5913
- res.end = function(...args) {
5914
- try {
5915
- const chunk = typeof args[0] !== "function" ? args[0] : void 0;
5916
- if (chunk != null && resSize < DEFAULT_MAX_BODY_CAPTURE) {
5917
- const buf = toBuffer(chunk);
5918
- if (buf) {
5919
- resChunks.push(buf);
5920
- }
5921
- }
5922
- } catch (e) {
5923
- brakitDebug(`capture end: ${getErrorMessage(e)}`);
5924
- }
5925
- const result = originalEnd.apply(this, args);
5926
- const endTime = performance.now();
5927
- const encoding = String(res.getHeader("content-encoding") ?? "").toLowerCase();
5928
- const statusCode = res.statusCode;
5929
- const responseHeaders = outgoingToIncoming(res.getHeaders());
5930
- const responseContentType = String(res.getHeader("content-type") ?? "");
5931
- const capturedChunks = resChunks.slice();
5932
- if (!isChild) {
5933
- void (async () => {
5934
- try {
5935
- let body = capturedChunks.length > 0 ? Buffer.concat(capturedChunks) : null;
5936
- if (body && encoding && !truncated) {
5937
- body = await decompressAsync(body, encoding);
5938
- }
5939
- requestStore.capture({
5940
- requestId,
5941
- method,
5942
- url: req.url ?? "/",
5943
- requestHeaders: req.headers,
5944
- requestBody: null,
5945
- statusCode,
5946
- responseHeaders,
5947
- responseBody: body,
5948
- responseContentType,
5949
- startTime,
5950
- endTime,
5951
- config: { maxBodyCapture: DEFAULT_MAX_BODY_CAPTURE }
5952
- });
5953
- } catch (e) {
5954
- brakitDebug(`capture store: ${getErrorMessage(e)}`);
5955
- }
5956
- })();
5957
- }
5958
- return result;
5959
- };
5960
- }
5961
- var init_capture = __esm({
5962
- "src/runtime/capture.ts"() {
5963
- "use strict";
5964
- init_constants();
5965
- init_log();
5966
- init_type_guards();
5967
- }
5968
- });
5969
-
5970
7085
  // src/runtime/interceptor.ts
5971
7086
  import http from "http";
5972
7087
  import { randomUUID as randomUUID8 } from "crypto";
@@ -6152,7 +7267,8 @@ function registerLifecycle(allServices, stores, services, cwd) {
6152
7267
  process.on("SIGTERM", () => {
6153
7268
  recordExitReason(EXIT_REASON_SIGTERM);
6154
7269
  });
6155
- process.on("beforeExit", () => {
7270
+ process.on("beforeExit", async () => {
7271
+ await drainPendingCaptures();
6156
7272
  recordExitReason(EXIT_REASON_CLEAN);
6157
7273
  sendTelemetry();
6158
7274
  });
@@ -6182,6 +7298,9 @@ async function doSetup() {
6182
7298
  hooks_installed: ["fetch", "console", "error"],
6183
7299
  setup_duration_ms: setupDurationMs
6184
7300
  });
7301
+ const graphBuilder = new GraphBuilder(bus, stores.requestStore);
7302
+ graphBuilder.start();
7303
+ services.graphBuilder = graphBuilder;
6185
7304
  const dataDir = getProjectDataDir(cwd);
6186
7305
  const analysisServices = startAnalysis(bus, stores, dataDir, services);
6187
7306
  const config = {
@@ -6242,6 +7361,7 @@ var init_setup = __esm({
6242
7361
  "src/runtime/setup.ts"() {
6243
7362
  "use strict";
6244
7363
  init_fetch();
7364
+ init_capture();
6245
7365
  init_console();
6246
7366
  init_errors();
6247
7367
  init_adapters();
@@ -6252,6 +7372,7 @@ var init_setup = __esm({
6252
7372
  init_store();
6253
7373
  init_issue_store();
6254
7374
  init_engine();
7375
+ init_graph_builder();
6255
7376
  init_terminal();
6256
7377
  init_src();
6257
7378
  init_constants();