brakit 0.8.5 → 0.8.6

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.
@@ -47,7 +47,7 @@ var init_routes = __esm({
47
47
  });
48
48
 
49
49
  // src/constants/limits.ts
50
- 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, FINDING_ID_HASH_LENGTH, FINDINGS_DATA_VERSION, SENSITIVE_MASK_PLACEHOLDER;
50
+ 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;
51
51
  var init_limits = __esm({
52
52
  "src/constants/limits.ts"() {
53
53
  "use strict";
@@ -62,14 +62,25 @@ var init_limits = __esm({
62
62
  SENSITIVE_MASK_VISIBLE_CHARS = 4;
63
63
  MAX_JSON_BODY_BYTES = 65536;
64
64
  ANALYSIS_DEBOUNCE_MS = 300;
65
- FINDING_ID_HASH_LENGTH = 16;
66
- FINDINGS_DATA_VERSION = 1;
65
+ ISSUE_ID_HASH_LENGTH = 16;
66
+ ISSUES_DATA_VERSION = 2;
67
67
  SENSITIVE_MASK_PLACEHOLDER = "****";
68
+ PROJECT_HASH_LENGTH = 8;
69
+ SECRET_SCAN_ARRAY_LIMIT = 5;
70
+ PII_SCAN_ARRAY_LIMIT = 10;
71
+ MIN_SECRET_VALUE_LENGTH = 8;
72
+ FULL_RECORD_MIN_FIELDS = 5;
73
+ LIST_PII_MIN_ITEMS = 2;
74
+ MAX_API_LIMIT = 500;
75
+ MAX_OBJECT_SCAN_DEPTH = 5;
76
+ MAX_UNIQUE_ENDPOINTS = 500;
77
+ MAX_ACCUMULATOR_ENTRIES = 1e3;
78
+ ISSUE_PRUNE_TTL_MS = 10 * 60 * 1e3;
68
79
  }
69
80
  });
70
81
 
71
82
  // src/constants/thresholds.ts
72
- var FLOW_GAP_MS, SLOW_REQUEST_THRESHOLD_MS, MIN_POLLING_SEQUENCE, ENDPOINT_TRUNCATE_LENGTH, N1_QUERY_THRESHOLD, ERROR_RATE_THRESHOLD_PCT, SLOW_ENDPOINT_THRESHOLD_MS, 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, RESOLVE_AFTER_ABSENCES, RESOLVED_INSIGHT_TTL_MS;
83
+ var FLOW_GAP_MS, SLOW_REQUEST_THRESHOLD_MS, MIN_POLLING_SEQUENCE, ENDPOINT_TRUNCATE_LENGTH, N1_QUERY_THRESHOLD, ERROR_RATE_THRESHOLD_PCT, SLOW_ENDPOINT_THRESHOLD_MS, 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;
73
84
  var init_thresholds = __esm({
74
85
  "src/constants/thresholds.ts"() {
75
86
  "use strict";
@@ -99,9 +110,9 @@ var init_thresholds = __esm({
99
110
  OVERFETCH_MANY_FIELDS = 12;
100
111
  OVERFETCH_UNWRAP_MIN_SIZE = 3;
101
112
  MAX_DUPLICATE_INSIGHTS = 3;
102
- INSIGHT_WINDOW_PER_ENDPOINT = 2;
103
- RESOLVE_AFTER_ABSENCES = 3;
104
- RESOLVED_INSIGHT_TTL_MS = 18e5;
113
+ INSIGHT_WINDOW_PER_ENDPOINT = 20;
114
+ CLEAN_HITS_FOR_RESOLUTION = 5;
115
+ STALE_ISSUE_TTL_MS = 30 * 60 * 1e3;
105
116
  }
106
117
  });
107
118
 
@@ -117,18 +128,18 @@ var init_transport = __esm({
117
128
  });
118
129
 
119
130
  // src/constants/metrics.ts
120
- var METRICS_DIR, METRICS_FILE, PORT_FILE, FINDINGS_FILE, METRICS_FLUSH_INTERVAL_MS, METRICS_MAX_SESSIONS, METRICS_MAX_DATA_POINTS, FINDINGS_FLUSH_INTERVAL_MS;
131
+ var METRICS_DIR, METRICS_FILE, PORT_FILE, ISSUES_FILE, METRICS_FLUSH_INTERVAL_MS, METRICS_MAX_SESSIONS, METRICS_MAX_DATA_POINTS, ISSUES_FLUSH_INTERVAL_MS;
121
132
  var init_metrics = __esm({
122
133
  "src/constants/metrics.ts"() {
123
134
  "use strict";
124
135
  METRICS_DIR = ".brakit";
125
- METRICS_FILE = ".brakit/metrics.json";
136
+ METRICS_FILE = "metrics.json";
126
137
  PORT_FILE = ".brakit/port";
127
- FINDINGS_FILE = ".brakit/findings.json";
138
+ ISSUES_FILE = "issues.json";
128
139
  METRICS_FLUSH_INTERVAL_MS = 3e4;
129
140
  METRICS_MAX_SESSIONS = 50;
130
141
  METRICS_MAX_DATA_POINTS = 200;
131
- FINDINGS_FLUSH_INTERVAL_MS = 1e4;
142
+ ISSUES_FLUSH_INTERVAL_MS = 1e4;
132
143
  }
133
144
  });
134
145
 
@@ -149,7 +160,7 @@ var init_headers = __esm({
149
160
  });
150
161
 
151
162
  // src/constants/network.ts
152
- var CLOUD_SIGNALS, MAX_HEALTH_ERRORS, RECOVERY_WINDOW_MS, LOCALHOST_IPS, LOCALHOST_HOSTNAMES, URL_PARSE_BASE;
163
+ var CLOUD_SIGNALS, MAX_HEALTH_ERRORS, RECOVERY_WINDOW_MS, LOCALHOST_IPS, LOCALHOST_HOSTNAMES, URL_PARSE_BASE, DIR_MODE_OWNER_ONLY, FILE_MODE_OWNER_ONLY;
153
164
  var init_network = __esm({
154
165
  "src/constants/network.ts"() {
155
166
  "use strict";
@@ -179,6 +190,8 @@ var init_network = __esm({
179
190
  LOCALHOST_IPS = /* @__PURE__ */ new Set(["127.0.0.1", "::1", "::ffff:127.0.0.1"]);
180
191
  LOCALHOST_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
181
192
  URL_PARSE_BASE = "http://localhost";
193
+ DIR_MODE_OWNER_ONLY = 448;
194
+ FILE_MODE_OWNER_ONLY = 384;
182
195
  }
183
196
  });
184
197
 
@@ -214,26 +227,35 @@ var init_severity = __esm({
214
227
  });
215
228
 
216
229
  // src/constants/telemetry.ts
217
- var POSTHOG_HOST, POSTHOG_CAPTURE_PATH, POSTHOG_REQUEST_TIMEOUT_MS;
230
+ var POSTHOG_HOST, POSTHOG_CAPTURE_PATH, POSTHOG_REQUEST_TIMEOUT_MS, SPEED_BUCKET_THRESHOLDS;
218
231
  var init_telemetry = __esm({
219
232
  "src/constants/telemetry.ts"() {
220
233
  "use strict";
221
234
  POSTHOG_HOST = "https://us.i.posthog.com";
222
235
  POSTHOG_CAPTURE_PATH = "/i/v0/e/";
223
236
  POSTHOG_REQUEST_TIMEOUT_MS = 3e3;
237
+ SPEED_BUCKET_THRESHOLDS = [200, 500, 1e3, 2e3, 5e3];
224
238
  }
225
239
  });
226
240
 
227
241
  // src/constants/lifecycle.ts
228
- var VALID_FINDING_STATES, VALID_AI_FIX_STATUSES;
242
+ var VALID_ISSUE_STATES, VALID_ISSUE_CATEGORIES, VALID_AI_FIX_STATUSES;
229
243
  var init_lifecycle = __esm({
230
244
  "src/constants/lifecycle.ts"() {
231
245
  "use strict";
232
- VALID_FINDING_STATES = /* @__PURE__ */ new Set(["open", "fixing", "resolved"]);
246
+ VALID_ISSUE_STATES = /* @__PURE__ */ new Set(["open", "fixing", "resolved", "stale", "regressed"]);
247
+ VALID_ISSUE_CATEGORIES = /* @__PURE__ */ new Set(["security", "performance", "reliability"]);
233
248
  VALID_AI_FIX_STATUSES = /* @__PURE__ */ new Set(["fixed", "wont_fix"]);
234
249
  }
235
250
  });
236
251
 
252
+ // src/constants/cli.ts
253
+ var init_cli = __esm({
254
+ "src/constants/cli.ts"() {
255
+ "use strict";
256
+ }
257
+ });
258
+
237
259
  // src/constants/index.ts
238
260
  var init_constants = __esm({
239
261
  "src/constants/index.ts"() {
@@ -250,6 +272,7 @@ var init_constants = __esm({
250
272
  init_severity();
251
273
  init_telemetry();
252
274
  init_lifecycle();
275
+ init_cli();
253
276
  }
254
277
  });
255
278
 
@@ -828,6 +851,22 @@ var init_http = __esm({
828
851
  }
829
852
  });
830
853
 
854
+ // src/utils/http-status.ts
855
+ function isErrorStatus(code) {
856
+ return code >= 400;
857
+ }
858
+ function isServerError(code) {
859
+ return code >= 500;
860
+ }
861
+ function isRedirect(code) {
862
+ return code >= 300 && code < 400;
863
+ }
864
+ var init_http_status = __esm({
865
+ "src/utils/http-status.ts"() {
866
+ "use strict";
867
+ }
868
+ });
869
+
831
870
  // src/analysis/categorize.ts
832
871
  function detectCategory(req) {
833
872
  const { method, url, statusCode, responseHeaders } = req;
@@ -896,7 +935,7 @@ function labelRequest(req) {
896
935
  function generateHumanLabel(req, category) {
897
936
  const effectivePath = getEffectivePath(req);
898
937
  const endpointName = getEndpointName(effectivePath);
899
- const failed = req.statusCode >= 400;
938
+ const failed = isErrorStatus(req.statusCode);
900
939
  switch (category) {
901
940
  case "auth-handshake":
902
941
  return "Auth handshake";
@@ -991,6 +1030,7 @@ var init_label = __esm({
991
1030
  "use strict";
992
1031
  init_constants();
993
1032
  init_categorize();
1033
+ init_http_status();
994
1034
  }
995
1035
  });
996
1036
 
@@ -1083,7 +1123,7 @@ function detectWarnings(requests) {
1083
1123
  for (const req of slowRequests) {
1084
1124
  warnings.push(`${req.label} took ${(req.durationMs / 1e3).toFixed(1)}s`);
1085
1125
  }
1086
- const errors = requests.filter((r) => r.statusCode >= 500);
1126
+ const errors = requests.filter((r) => isServerError(r.statusCode));
1087
1127
  for (const req of errors) {
1088
1128
  warnings.push(`${req.label} \u2014 server error (${req.statusCode})`);
1089
1129
  }
@@ -1095,6 +1135,7 @@ var init_transforms = __esm({
1095
1135
  init_constants();
1096
1136
  init_categorize();
1097
1137
  init_label();
1138
+ init_http_status();
1098
1139
  }
1099
1140
  });
1100
1141
 
@@ -1148,7 +1189,7 @@ function buildFlow(rawRequests) {
1148
1189
  requests,
1149
1190
  startTime,
1150
1191
  totalDurationMs: Math.round(endTime - startTime),
1151
- hasErrors: requests.some((r) => r.statusCode >= 400),
1192
+ hasErrors: requests.some((r) => isErrorStatus(r.statusCode)),
1152
1193
  warnings: detectWarnings(rawRequests),
1153
1194
  sourcePage,
1154
1195
  redundancyPct
@@ -1202,6 +1243,7 @@ var init_group = __esm({
1202
1243
  "src/analysis/group.ts"() {
1203
1244
  "use strict";
1204
1245
  init_constants();
1246
+ init_http_status();
1205
1247
  init_label();
1206
1248
  init_categorize();
1207
1249
  init_transforms();
@@ -1319,11 +1361,9 @@ function createRequestsHandler(registry) {
1319
1361
  const method = url.searchParams.get("method");
1320
1362
  const status = url.searchParams.get("status");
1321
1363
  const search = url.searchParams.get("search");
1322
- const limit = parseInt(
1323
- url.searchParams.get("limit") ?? String(DEFAULT_API_LIMIT),
1324
- 10
1325
- );
1326
- const offset = parseInt(url.searchParams.get("offset") ?? "0", 10);
1364
+ const rawLimit = parseInt(url.searchParams.get("limit") ?? String(DEFAULT_API_LIMIT), 10);
1365
+ const limit = Math.min(Math.max(rawLimit || DEFAULT_API_LIMIT, 1), MAX_API_LIMIT);
1366
+ const offset = Math.max(parseInt(url.searchParams.get("offset") ?? "0", 10) || 0, 0);
1327
1367
  let results = [...registry.get("request-store").getAll()].reverse();
1328
1368
  if (method) {
1329
1369
  results = results.filter((r) => r.method === method.toUpperCase());
@@ -1373,7 +1413,7 @@ function createClearHandler(registry) {
1373
1413
  registry.get("error-store").clear();
1374
1414
  registry.get("query-store").clear();
1375
1415
  registry.get("metrics-store").reset();
1376
- if (registry.has("finding-store")) registry.get("finding-store").clear();
1416
+ if (registry.has("issue-store")) registry.get("issue-store").clear();
1377
1417
  registry.get("event-bus").emit("store:cleared", void 0);
1378
1418
  sendJson(req, res, HTTP_OK, { cleared: true });
1379
1419
  };
@@ -1415,16 +1455,32 @@ function getErrorMessage(err) {
1415
1455
  if (typeof err === "string") return err;
1416
1456
  return String(err);
1417
1457
  }
1418
- function isValidFindingState(val) {
1419
- return typeof val === "string" && VALID_FINDING_STATES.has(val);
1458
+ function isValidIssueState(val) {
1459
+ return typeof val === "string" && VALID_ISSUE_STATES.has(val);
1460
+ }
1461
+ function isValidIssueCategory(val) {
1462
+ return typeof val === "string" && VALID_ISSUE_CATEGORIES.has(val);
1420
1463
  }
1421
1464
  function isValidAiFixStatus(val) {
1422
1465
  return typeof val === "string" && VALID_AI_FIX_STATUSES.has(val);
1423
1466
  }
1467
+ function validateIssuesData(parsed) {
1468
+ if (parsed != null && typeof parsed === "object" && !Array.isArray(parsed) && parsed.version === ISSUES_DATA_VERSION && Array.isArray(parsed.issues)) {
1469
+ return parsed;
1470
+ }
1471
+ return null;
1472
+ }
1473
+ function validateMetricsData(parsed) {
1474
+ if (parsed != null && typeof parsed === "object" && !Array.isArray(parsed) && parsed.version === 1 && Array.isArray(parsed.endpoints)) {
1475
+ return parsed;
1476
+ }
1477
+ return null;
1478
+ }
1424
1479
  var init_type_guards = __esm({
1425
1480
  "src/utils/type-guards.ts"() {
1426
1481
  "use strict";
1427
1482
  init_lifecycle();
1483
+ init_limits();
1428
1484
  }
1429
1485
  });
1430
1486
 
@@ -1762,46 +1818,42 @@ var init_api = __esm({
1762
1818
  }
1763
1819
  });
1764
1820
 
1765
- // src/dashboard/api/insights.ts
1766
- function createInsightsHandler(engine) {
1767
- return (req, res) => {
1768
- if (!requireGet(req, res)) return;
1769
- sendJson(req, res, HTTP_OK, { insights: engine.getStatefulInsights() });
1770
- };
1771
- }
1772
- function createSecurityHandler(engine) {
1821
+ // src/dashboard/api/issues.ts
1822
+ function createIssuesHandler(issueStore) {
1773
1823
  return (req, res) => {
1774
1824
  if (!requireGet(req, res)) return;
1775
- sendJson(req, res, HTTP_OK, { findings: engine.getStatefulFindings() });
1825
+ const url = parseRequestUrl(req);
1826
+ const stateParam = url.searchParams.get("state");
1827
+ const categoryParam = url.searchParams.get("category");
1828
+ let issues;
1829
+ if (stateParam && isValidIssueState(stateParam)) {
1830
+ issues = issueStore.getByState(stateParam);
1831
+ } else if (categoryParam && isValidIssueCategory(categoryParam)) {
1832
+ issues = issueStore.getByCategory(categoryParam);
1833
+ } else {
1834
+ issues = issueStore.getAll();
1835
+ }
1836
+ sendJson(req, res, HTTP_OK, { issues });
1776
1837
  };
1777
1838
  }
1778
- var init_insights = __esm({
1779
- "src/dashboard/api/insights.ts"() {
1780
- "use strict";
1781
- init_shared2();
1782
- init_http();
1783
- }
1784
- });
1785
-
1786
- // src/dashboard/api/findings.ts
1787
- function createFindingsHandler(findingStore) {
1839
+ function createFindingsHandler(issueStore) {
1788
1840
  return (req, res) => {
1789
1841
  if (!requireGet(req, res)) return;
1790
1842
  const url = parseRequestUrl(req);
1791
1843
  const stateParam = url.searchParams.get("state");
1792
- let findings;
1793
- if (stateParam && isValidFindingState(stateParam)) {
1794
- findings = findingStore.getByState(stateParam);
1844
+ let issues;
1845
+ if (stateParam && isValidIssueState(stateParam)) {
1846
+ issues = issueStore.getByState(stateParam);
1795
1847
  } else {
1796
- findings = findingStore.getAll();
1848
+ issues = issueStore.getAll();
1797
1849
  }
1798
1850
  sendJson(req, res, HTTP_OK, {
1799
- total: findings.length,
1800
- findings
1851
+ total: issues.length,
1852
+ findings: issues
1801
1853
  });
1802
1854
  };
1803
1855
  }
1804
- function createFindingsReportHandler(findingStore, eventBus, analysisEngine) {
1856
+ function createIssuesReportHandler(issueStore, eventBus) {
1805
1857
  return async (req, res) => {
1806
1858
  if (req.method !== "POST") {
1807
1859
  sendJson(req, res, HTTP_METHOD_NOT_ALLOWED, { error: "Method not allowed" });
@@ -1822,27 +1874,16 @@ function createFindingsReportHandler(findingStore, eventBus, analysisEngine) {
1822
1874
  sendJson(req, res, HTTP_BAD_REQUEST, { error: "notes is required" });
1823
1875
  return;
1824
1876
  }
1825
- const findingOk = findingStore.reportFix(findingId, status, notes);
1826
- if (findingOk) {
1827
- eventBus.emit("findings:changed", findingStore.getAll());
1828
- sendJson(req, res, HTTP_OK, { ok: true });
1829
- return;
1830
- }
1831
- if (analysisEngine?.reportInsightFix(findingId, status, notes)) {
1832
- eventBus.emit("analysis:updated", {
1833
- insights: analysisEngine.getInsights(),
1834
- findings: analysisEngine.getFindings(),
1835
- statefulFindings: analysisEngine.getStatefulFindings(),
1836
- statefulInsights: analysisEngine.getStatefulInsights()
1837
- });
1877
+ if (issueStore.reportFix(findingId, status, notes)) {
1878
+ eventBus.emit("issues:changed", issueStore.getAll());
1838
1879
  sendJson(req, res, HTTP_OK, { ok: true });
1839
1880
  return;
1840
1881
  }
1841
1882
  sendJson(req, res, HTTP_NOT_FOUND, { error: "Finding not found" });
1842
1883
  };
1843
1884
  }
1844
- var init_findings = __esm({
1845
- "src/dashboard/api/findings.ts"() {
1885
+ var init_issues = __esm({
1886
+ "src/dashboard/api/issues.ts"() {
1846
1887
  "use strict";
1847
1888
  init_shared2();
1848
1889
  init_type_guards();
@@ -1851,7 +1892,7 @@ var init_findings = __esm({
1851
1892
  });
1852
1893
 
1853
1894
  // src/constants/events.ts
1854
- var SSE_EVENT_FETCH, SSE_EVENT_LOG, SSE_EVENT_ERROR, SSE_EVENT_QUERY, SSE_EVENT_INSIGHTS, SSE_EVENT_SECURITY;
1895
+ var SSE_EVENT_FETCH, SSE_EVENT_LOG, SSE_EVENT_ERROR, SSE_EVENT_QUERY, SSE_EVENT_ISSUES;
1855
1896
  var init_events = __esm({
1856
1897
  "src/constants/events.ts"() {
1857
1898
  "use strict";
@@ -1859,8 +1900,7 @@ var init_events = __esm({
1859
1900
  SSE_EVENT_LOG = "log";
1860
1901
  SSE_EVENT_ERROR = "error_event";
1861
1902
  SSE_EVENT_QUERY = "query";
1862
- SSE_EVENT_INSIGHTS = "insights";
1863
- SSE_EVENT_SECURITY = "security";
1903
+ SSE_EVENT_ISSUES = "issues";
1864
1904
  }
1865
1905
  });
1866
1906
 
@@ -1893,12 +1933,11 @@ data: ${data}
1893
1933
  bus.on("telemetry:log", (e) => broadcast(SSE_EVENT_LOG, JSON.stringify(e)));
1894
1934
  bus.on("telemetry:error", (e) => broadcast(SSE_EVENT_ERROR, JSON.stringify(e)));
1895
1935
  bus.on("telemetry:query", (e) => broadcast(SSE_EVENT_QUERY, JSON.stringify(e)));
1896
- bus.on("analysis:updated", ({ statefulInsights, statefulFindings }) => {
1897
- broadcast(SSE_EVENT_INSIGHTS, JSON.stringify(statefulInsights));
1898
- broadcast(SSE_EVENT_SECURITY, JSON.stringify(statefulFindings));
1936
+ bus.on("analysis:updated", ({ issues }) => {
1937
+ broadcast(SSE_EVENT_ISSUES, JSON.stringify(issues));
1899
1938
  });
1900
- bus.on("findings:changed", (findings) => {
1901
- broadcast(SSE_EVENT_SECURITY, JSON.stringify(findings));
1939
+ bus.on("issues:changed", (issues) => {
1940
+ broadcast(SSE_EVENT_ISSUES, JSON.stringify(issues));
1902
1941
  });
1903
1942
  return (req, res) => {
1904
1943
  const headers2 = {
@@ -2531,7 +2570,14 @@ var init_styles = __esm({
2531
2570
  // src/utils/fs.ts
2532
2571
  import { access, readFile, writeFile } from "fs/promises";
2533
2572
  import { existsSync, readFileSync, writeFileSync } from "fs";
2534
- import { resolve } from "path";
2573
+ import { createHash } from "crypto";
2574
+ import { homedir } from "os";
2575
+ import { resolve, join } from "path";
2576
+ function getProjectDataDir(projectRoot) {
2577
+ const absolute = resolve(projectRoot);
2578
+ const hash = createHash("sha256").update(absolute).digest("hex").slice(0, PROJECT_HASH_LENGTH);
2579
+ return join(homedir(), ".brakit", "projects", hash);
2580
+ }
2535
2581
  async function fileExists(path) {
2536
2582
  try {
2537
2583
  await access(path);
@@ -2550,7 +2596,8 @@ function ensureGitignore(dir, entry) {
2550
2596
  } else {
2551
2597
  writeFileSync(gitignorePath, entry + "\n");
2552
2598
  }
2553
- } catch {
2599
+ } catch (err) {
2600
+ brakitDebug(`ensureGitignore failed: ${getErrorMessage(err)}`);
2554
2601
  }
2555
2602
  }
2556
2603
  async function ensureGitignoreAsync(dir, entry) {
@@ -2563,12 +2610,16 @@ async function ensureGitignoreAsync(dir, entry) {
2563
2610
  } else {
2564
2611
  await writeFile(gitignorePath, entry + "\n");
2565
2612
  }
2566
- } catch {
2613
+ } catch (err) {
2614
+ brakitDebug(`ensureGitignoreAsync failed: ${getErrorMessage(err)}`);
2567
2615
  }
2568
2616
  }
2569
2617
  var init_fs = __esm({
2570
2618
  "src/utils/fs.ts"() {
2571
2619
  "use strict";
2620
+ init_limits();
2621
+ init_log();
2622
+ init_type_guards();
2572
2623
  }
2573
2624
  });
2574
2625
 
@@ -2646,60 +2697,57 @@ var init_atomic_writer = __esm({
2646
2697
  }
2647
2698
  });
2648
2699
 
2649
- // src/store/finding-id.ts
2650
- import { createHash } from "crypto";
2651
- function computeFindingId(finding) {
2652
- const key = `${finding.rule}:${finding.endpoint}:${finding.desc}`;
2653
- return createHash("sha256").update(key).digest("hex").slice(0, FINDING_ID_HASH_LENGTH);
2654
- }
2655
- function computeInsightId(type, endpoint, desc) {
2656
- const key = `${type}:${endpoint}:${desc}`;
2657
- return createHash("sha256").update(key).digest("hex").slice(0, FINDING_ID_HASH_LENGTH);
2700
+ // src/utils/issue-id.ts
2701
+ import { createHash as createHash2 } from "crypto";
2702
+ function computeIssueId(issue) {
2703
+ const stableDesc = issue.desc.replace(/\d[\d,.]*\s*\w*/g, "#");
2704
+ const key = `${issue.rule}:${issue.endpoint ?? "global"}:${stableDesc}`;
2705
+ return createHash2("sha256").update(key).digest("hex").slice(0, ISSUE_ID_HASH_LENGTH);
2658
2706
  }
2659
- var init_finding_id = __esm({
2660
- "src/store/finding-id.ts"() {
2707
+ var init_issue_id = __esm({
2708
+ "src/utils/issue-id.ts"() {
2661
2709
  "use strict";
2662
2710
  init_limits();
2663
2711
  }
2664
2712
  });
2665
2713
 
2666
- // src/store/finding-store.ts
2714
+ // src/store/issue-store.ts
2667
2715
  import { readFile as readFile2 } from "fs/promises";
2668
- import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
2716
+ import { readFileSync as readFileSync2, existsSync as existsSync3, unlinkSync } from "fs";
2669
2717
  import { resolve as resolve2 } from "path";
2670
- var FindingStore;
2671
- var init_finding_store = __esm({
2672
- "src/store/finding-store.ts"() {
2718
+ var IssueStore;
2719
+ var init_issue_store = __esm({
2720
+ "src/store/issue-store.ts"() {
2673
2721
  "use strict";
2674
2722
  init_fs();
2675
- init_constants();
2723
+ init_metrics();
2724
+ init_limits();
2725
+ init_thresholds();
2676
2726
  init_limits();
2677
2727
  init_atomic_writer();
2678
2728
  init_log();
2679
- init_finding_id();
2680
- FindingStore = class {
2681
- constructor(rootDir) {
2682
- this.rootDir = rootDir;
2683
- const metricsDir = resolve2(rootDir, METRICS_DIR);
2684
- this.findingsPath = resolve2(rootDir, FINDINGS_FILE);
2729
+ init_type_guards();
2730
+ init_issue_id();
2731
+ IssueStore = class {
2732
+ constructor(dataDir) {
2733
+ this.dataDir = dataDir;
2734
+ this.issuesPath = resolve2(dataDir, ISSUES_FILE);
2685
2735
  this.writer = new AtomicWriter({
2686
- dir: metricsDir,
2687
- filePath: this.findingsPath,
2688
- gitignoreEntry: METRICS_DIR,
2689
- label: "findings"
2736
+ dir: dataDir,
2737
+ filePath: this.issuesPath,
2738
+ label: "issues"
2690
2739
  });
2691
2740
  }
2692
- findings = /* @__PURE__ */ new Map();
2741
+ issues = /* @__PURE__ */ new Map();
2693
2742
  flushTimer = null;
2694
2743
  dirty = false;
2695
2744
  writer;
2696
- findingsPath;
2745
+ issuesPath;
2697
2746
  start() {
2698
- this.loadAsync().catch(() => {
2699
- });
2747
+ this.loadAsync().catch((err) => brakitDebug(`IssueStore: async load failed: ${err}`));
2700
2748
  this.flushTimer = setInterval(
2701
2749
  () => this.flush(),
2702
- FINDINGS_FLUSH_INTERVAL_MS
2750
+ ISSUES_FLUSH_INTERVAL_MS
2703
2751
  );
2704
2752
  this.flushTimer.unref();
2705
2753
  }
@@ -2710,119 +2758,148 @@ var init_finding_store = __esm({
2710
2758
  }
2711
2759
  this.flushSync();
2712
2760
  }
2713
- upsert(finding, source) {
2714
- const id = computeFindingId(finding);
2715
- const existing = this.findings.get(id);
2761
+ upsert(issue, source) {
2762
+ const id = computeIssueId(issue);
2763
+ const existing = this.issues.get(id);
2716
2764
  const now = Date.now();
2717
2765
  if (existing) {
2718
2766
  existing.lastSeenAt = now;
2719
2767
  existing.occurrences++;
2720
- existing.finding = finding;
2721
- if (existing.state === "resolved") {
2722
- existing.state = "open";
2768
+ existing.issue = issue;
2769
+ existing.cleanHitsSinceLastSeen = 0;
2770
+ if (existing.state === "resolved" || existing.state === "stale") {
2771
+ existing.state = "regressed";
2723
2772
  existing.resolvedAt = null;
2724
2773
  }
2725
2774
  this.dirty = true;
2726
2775
  return existing;
2727
2776
  }
2728
2777
  const stateful = {
2729
- findingId: id,
2778
+ issueId: id,
2730
2779
  state: "open",
2731
2780
  source,
2732
- finding,
2781
+ category: issue.category,
2782
+ issue,
2733
2783
  firstSeenAt: now,
2734
2784
  lastSeenAt: now,
2735
2785
  resolvedAt: null,
2736
2786
  occurrences: 1,
2787
+ cleanHitsSinceLastSeen: 0,
2737
2788
  aiStatus: null,
2738
2789
  aiNotes: null
2739
2790
  };
2740
- this.findings.set(id, stateful);
2791
+ this.issues.set(id, stateful);
2741
2792
  this.dirty = true;
2742
2793
  return stateful;
2743
2794
  }
2744
- transition(findingId, state) {
2745
- const finding = this.findings.get(findingId);
2746
- if (!finding) return false;
2747
- finding.state = state;
2795
+ /**
2796
+ * Reconcile issues against the current analysis results using evidence-based resolution.
2797
+ *
2798
+ * @param currentIssueIds - IDs of issues detected in the current analysis cycle
2799
+ * @param activeEndpoints - Endpoints that had requests in the current cycle
2800
+ */
2801
+ reconcile(currentIssueIds, activeEndpoints) {
2802
+ const now = Date.now();
2803
+ for (const [, stateful] of this.issues) {
2804
+ const isActive = stateful.state === "open" || stateful.state === "fixing" || stateful.state === "regressed";
2805
+ if (!isActive) continue;
2806
+ if (currentIssueIds.has(stateful.issueId)) continue;
2807
+ const endpoint = stateful.issue.endpoint;
2808
+ if (endpoint && activeEndpoints.has(endpoint)) {
2809
+ stateful.cleanHitsSinceLastSeen++;
2810
+ if (stateful.cleanHitsSinceLastSeen >= CLEAN_HITS_FOR_RESOLUTION) {
2811
+ stateful.state = "resolved";
2812
+ stateful.resolvedAt = now;
2813
+ }
2814
+ this.dirty = true;
2815
+ } else if (now - stateful.lastSeenAt > STALE_ISSUE_TTL_MS) {
2816
+ stateful.state = "stale";
2817
+ this.dirty = true;
2818
+ }
2819
+ }
2820
+ for (const [id, stateful] of this.issues) {
2821
+ if (stateful.state === "resolved" && stateful.resolvedAt && now - stateful.resolvedAt > ISSUE_PRUNE_TTL_MS) {
2822
+ this.issues.delete(id);
2823
+ this.dirty = true;
2824
+ } else if (stateful.state === "stale" && now - stateful.lastSeenAt > STALE_ISSUE_TTL_MS + ISSUE_PRUNE_TTL_MS) {
2825
+ this.issues.delete(id);
2826
+ this.dirty = true;
2827
+ }
2828
+ }
2829
+ }
2830
+ transition(issueId, state) {
2831
+ const issue = this.issues.get(issueId);
2832
+ if (!issue) return false;
2833
+ issue.state = state;
2748
2834
  if (state === "resolved") {
2749
- finding.resolvedAt = Date.now();
2835
+ issue.resolvedAt = Date.now();
2750
2836
  }
2751
2837
  this.dirty = true;
2752
2838
  return true;
2753
2839
  }
2754
- reportFix(findingId, status, notes) {
2755
- const finding = this.findings.get(findingId);
2756
- if (!finding) return false;
2757
- finding.aiStatus = status;
2758
- finding.aiNotes = notes;
2840
+ reportFix(issueId, status, notes) {
2841
+ const issue = this.issues.get(issueId);
2842
+ if (!issue) return false;
2843
+ issue.aiStatus = status;
2844
+ issue.aiNotes = notes;
2759
2845
  if (status === "fixed") {
2760
- finding.state = "fixing";
2846
+ issue.state = "fixing";
2761
2847
  }
2762
2848
  this.dirty = true;
2763
2849
  return true;
2764
2850
  }
2765
- /**
2766
- * Reconcile passive findings against the current analysis results.
2767
- *
2768
- * Passive findings are detected by continuous scanning (not user-triggered).
2769
- * When a previously-seen finding is absent from the current results, it means
2770
- * the issue has been fixed — transition it to "resolved" automatically.
2771
- * Active findings (from MCP verify-fix) are not auto-resolved because they
2772
- * require explicit verification.
2773
- */
2774
- reconcilePassive(currentFindings) {
2775
- const currentIds = new Set(currentFindings.map(computeFindingId));
2776
- for (const [id, stateful] of this.findings) {
2777
- if (stateful.source === "passive" && (stateful.state === "open" || stateful.state === "fixing") && !currentIds.has(id)) {
2778
- stateful.state = "resolved";
2779
- stateful.resolvedAt = Date.now();
2780
- this.dirty = true;
2781
- }
2782
- }
2783
- }
2784
2851
  getAll() {
2785
- return [...this.findings.values()];
2852
+ return [...this.issues.values()];
2786
2853
  }
2787
2854
  getByState(state) {
2788
- return [...this.findings.values()].filter((f) => f.state === state);
2855
+ return [...this.issues.values()].filter((i) => i.state === state);
2856
+ }
2857
+ getByCategory(category) {
2858
+ return [...this.issues.values()].filter((i) => i.category === category);
2789
2859
  }
2790
- get(findingId) {
2791
- return this.findings.get(findingId);
2860
+ get(issueId) {
2861
+ return this.issues.get(issueId);
2792
2862
  }
2793
2863
  clear() {
2794
- this.findings.clear();
2795
- this.dirty = true;
2864
+ this.issues.clear();
2865
+ this.dirty = false;
2866
+ try {
2867
+ if (existsSync3(this.issuesPath)) {
2868
+ unlinkSync(this.issuesPath);
2869
+ }
2870
+ } catch {
2871
+ }
2872
+ }
2873
+ isDirty() {
2874
+ return this.dirty;
2796
2875
  }
2797
2876
  async loadAsync() {
2798
2877
  try {
2799
- if (await fileExists(this.findingsPath)) {
2800
- const raw = await readFile2(this.findingsPath, "utf-8");
2801
- const parsed = JSON.parse(raw);
2802
- if (parsed?.version === FINDINGS_DATA_VERSION && Array.isArray(parsed.findings)) {
2803
- for (const f of parsed.findings) {
2804
- this.findings.set(f.findingId, f);
2805
- }
2806
- }
2878
+ if (await fileExists(this.issuesPath)) {
2879
+ const raw = await readFile2(this.issuesPath, "utf-8");
2880
+ this.hydrate(raw);
2807
2881
  }
2808
2882
  } catch (err) {
2809
- brakitDebug(`FindingStore: could not load findings file, starting fresh: ${err}`);
2883
+ brakitDebug(`IssueStore: could not load issues file, starting fresh: ${err}`);
2810
2884
  }
2811
2885
  }
2812
2886
  /** Sync load for tests only — not used in production paths. */
2813
2887
  loadSync() {
2814
2888
  try {
2815
- if (existsSync3(this.findingsPath)) {
2816
- const raw = readFileSync2(this.findingsPath, "utf-8");
2817
- const parsed = JSON.parse(raw);
2818
- if (parsed?.version === FINDINGS_DATA_VERSION && Array.isArray(parsed.findings)) {
2819
- for (const f of parsed.findings) {
2820
- this.findings.set(f.findingId, f);
2821
- }
2822
- }
2889
+ if (existsSync3(this.issuesPath)) {
2890
+ const raw = readFileSync2(this.issuesPath, "utf-8");
2891
+ this.hydrate(raw);
2823
2892
  }
2824
2893
  } catch (err) {
2825
- brakitDebug(`FindingStore: could not load findings file, starting fresh: ${err}`);
2894
+ brakitDebug(`IssueStore: could not load issues file, starting fresh: ${err}`);
2895
+ }
2896
+ }
2897
+ /** Parse and populate issues from a raw JSON string. */
2898
+ hydrate(raw) {
2899
+ const validated = validateIssuesData(JSON.parse(raw));
2900
+ if (!validated) return;
2901
+ for (const issue of validated.issues) {
2902
+ this.issues.set(issue.issueId, issue);
2826
2903
  }
2827
2904
  }
2828
2905
  flush() {
@@ -2837,8 +2914,8 @@ var init_finding_store = __esm({
2837
2914
  }
2838
2915
  serialize() {
2839
2916
  const data = {
2840
- version: FINDINGS_DATA_VERSION,
2841
- findings: [...this.findings.values()]
2917
+ version: ISSUES_DATA_VERSION,
2918
+ issues: [...this.issues.values()]
2842
2919
  };
2843
2920
  return JSON.stringify(data);
2844
2921
  }
@@ -2849,7 +2926,7 @@ var init_finding_store = __esm({
2849
2926
  // src/detect/project.ts
2850
2927
  import { readFile as readFile3, readdir } from "fs/promises";
2851
2928
  import { existsSync as existsSync4 } from "fs";
2852
- import { join, relative } from "path";
2929
+ import { join as join2, relative } from "path";
2853
2930
  function detectFrameworkFromDeps(allDeps) {
2854
2931
  for (const f of FRAMEWORKS) {
2855
2932
  if (allDeps[f.dep]) return f.name;
@@ -2857,10 +2934,10 @@ function detectFrameworkFromDeps(allDeps) {
2857
2934
  return "unknown";
2858
2935
  }
2859
2936
  function detectPackageManagerSync(rootDir) {
2860
- if (existsSync4(join(rootDir, "bun.lockb")) || existsSync4(join(rootDir, "bun.lock"))) return "bun";
2861
- if (existsSync4(join(rootDir, "pnpm-lock.yaml"))) return "pnpm";
2862
- if (existsSync4(join(rootDir, "yarn.lock"))) return "yarn";
2863
- if (existsSync4(join(rootDir, "package-lock.json"))) return "npm";
2937
+ if (existsSync4(join2(rootDir, "bun.lockb")) || existsSync4(join2(rootDir, "bun.lock"))) return "bun";
2938
+ if (existsSync4(join2(rootDir, "pnpm-lock.yaml"))) return "pnpm";
2939
+ if (existsSync4(join2(rootDir, "yarn.lock"))) return "yarn";
2940
+ if (existsSync4(join2(rootDir, "package-lock.json"))) return "npm";
2864
2941
  return "unknown";
2865
2942
  }
2866
2943
  var FRAMEWORKS;
@@ -2878,6 +2955,44 @@ var init_project = __esm({
2878
2955
  }
2879
2956
  });
2880
2957
 
2958
+ // src/utils/response.ts
2959
+ function tryParseJson(body) {
2960
+ if (!body) return null;
2961
+ try {
2962
+ return JSON.parse(body);
2963
+ } catch {
2964
+ return null;
2965
+ }
2966
+ }
2967
+ function unwrapResponse(parsed) {
2968
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
2969
+ const obj = parsed;
2970
+ const keys = Object.keys(obj);
2971
+ if (keys.length > 3) return parsed;
2972
+ let best = null;
2973
+ let bestSize = 0;
2974
+ for (const key of keys) {
2975
+ const val = obj[key];
2976
+ if (Array.isArray(val) && val.length > bestSize) {
2977
+ best = val;
2978
+ bestSize = val.length;
2979
+ } else if (val && typeof val === "object" && !Array.isArray(val)) {
2980
+ const size = Object.keys(val).length;
2981
+ if (size > bestSize) {
2982
+ best = val;
2983
+ bestSize = size;
2984
+ }
2985
+ }
2986
+ }
2987
+ return best && bestSize >= OVERFETCH_UNWRAP_MIN_SIZE ? best : parsed;
2988
+ }
2989
+ var init_response = __esm({
2990
+ "src/utils/response.ts"() {
2991
+ "use strict";
2992
+ init_thresholds();
2993
+ }
2994
+ });
2995
+
2881
2996
  // src/analysis/rules/patterns.ts
2882
2997
  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, SELECT_STAR_RE, SELECT_DOT_STAR_RE, RULE_HINTS;
2883
2998
  var init_patterns = __esm({
@@ -2911,30 +3026,23 @@ var init_patterns = __esm({
2911
3026
  });
2912
3027
 
2913
3028
  // src/analysis/rules/exposed-secret.ts
2914
- function tryParseJson(body) {
2915
- if (!body) return null;
2916
- try {
2917
- return JSON.parse(body);
2918
- } catch {
2919
- return null;
2920
- }
2921
- }
2922
- function findSecretKeys(obj, prefix) {
3029
+ function findSecretKeys(obj, prefix, depth = 0) {
2923
3030
  const found = [];
3031
+ if (depth >= MAX_OBJECT_SCAN_DEPTH) return found;
2924
3032
  if (!obj || typeof obj !== "object") return found;
2925
3033
  if (Array.isArray(obj)) {
2926
- for (let i = 0; i < Math.min(obj.length, 5); i++) {
2927
- found.push(...findSecretKeys(obj[i], prefix));
3034
+ for (let i = 0; i < Math.min(obj.length, SECRET_SCAN_ARRAY_LIMIT); i++) {
3035
+ found.push(...findSecretKeys(obj[i], prefix, depth + 1));
2928
3036
  }
2929
3037
  return found;
2930
3038
  }
2931
3039
  for (const k of Object.keys(obj)) {
2932
3040
  const val = obj[k];
2933
- if (SECRET_KEYS.test(k) && typeof val === "string" && val.length >= 8 && !MASKED_RE.test(val)) {
3041
+ if (SECRET_KEYS.test(k) && typeof val === "string" && val.length >= MIN_SECRET_VALUE_LENGTH && !MASKED_RE.test(val)) {
2934
3042
  found.push(k);
2935
3043
  }
2936
3044
  if (typeof val === "object" && val !== null) {
2937
- found.push(...findSecretKeys(val, prefix + k + "."));
3045
+ found.push(...findSecretKeys(val, prefix + k + ".", depth + 1));
2938
3046
  }
2939
3047
  }
2940
3048
  return found;
@@ -2944,6 +3052,8 @@ var init_exposed_secret = __esm({
2944
3052
  "src/analysis/rules/exposed-secret.ts"() {
2945
3053
  "use strict";
2946
3054
  init_patterns();
3055
+ init_limits();
3056
+ init_http_status();
2947
3057
  exposedSecretRule = {
2948
3058
  id: "exposed-secret",
2949
3059
  severity: "critical",
@@ -2953,8 +3063,8 @@ var init_exposed_secret = __esm({
2953
3063
  const findings = [];
2954
3064
  const seen = /* @__PURE__ */ new Map();
2955
3065
  for (const r of ctx.requests) {
2956
- if (r.statusCode >= 400) continue;
2957
- const parsed = tryParseJson(r.responseBody);
3066
+ if (isErrorStatus(r.statusCode)) continue;
3067
+ const parsed = ctx.parsedBodies.response.get(r.id);
2958
3068
  if (!parsed) continue;
2959
3069
  const keys = findSecretKeys(parsed, "");
2960
3070
  if (keys.length === 0) continue;
@@ -3130,7 +3240,7 @@ var init_error_info_leak = __esm({
3130
3240
 
3131
3241
  // src/analysis/rules/insecure-cookie.ts
3132
3242
  function isFrameworkResponse(r) {
3133
- if (r.statusCode >= 300 && r.statusCode < 400) return true;
3243
+ if (isRedirect(r.statusCode)) return true;
3134
3244
  if (r.path?.startsWith("/__")) return true;
3135
3245
  if (r.responseHeaders?.["x-middleware-rewrite"]) return true;
3136
3246
  return false;
@@ -3140,6 +3250,7 @@ var init_insecure_cookie = __esm({
3140
3250
  "src/analysis/rules/insecure-cookie.ts"() {
3141
3251
  "use strict";
3142
3252
  init_patterns();
3253
+ init_http_status();
3143
3254
  insecureCookieRule = {
3144
3255
  id: "insecure-cookie",
3145
3256
  severity: "warning",
@@ -3257,51 +3368,14 @@ var init_cors_credentials = __esm({
3257
3368
  }
3258
3369
  });
3259
3370
 
3260
- // src/utils/response.ts
3261
- function unwrapResponse(parsed) {
3262
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
3263
- const obj = parsed;
3264
- const keys = Object.keys(obj);
3265
- if (keys.length > 3) return parsed;
3266
- let best = null;
3267
- let bestSize = 0;
3268
- for (const key of keys) {
3269
- const val = obj[key];
3270
- if (Array.isArray(val) && val.length > bestSize) {
3271
- best = val;
3272
- bestSize = val.length;
3273
- } else if (val && typeof val === "object" && !Array.isArray(val)) {
3274
- const size = Object.keys(val).length;
3275
- if (size > bestSize) {
3276
- best = val;
3277
- bestSize = size;
3278
- }
3279
- }
3280
- }
3281
- return best && bestSize >= OVERFETCH_UNWRAP_MIN_SIZE ? best : parsed;
3282
- }
3283
- var init_response = __esm({
3284
- "src/utils/response.ts"() {
3285
- "use strict";
3286
- init_thresholds();
3287
- }
3288
- });
3289
-
3290
3371
  // src/analysis/rules/response-pii-leak.ts
3291
- function tryParseJson2(body) {
3292
- if (!body) return null;
3293
- try {
3294
- return JSON.parse(body);
3295
- } catch {
3296
- return null;
3297
- }
3298
- }
3299
- function findEmails(obj) {
3372
+ function findEmails(obj, depth = 0) {
3300
3373
  const emails = [];
3374
+ if (depth >= MAX_OBJECT_SCAN_DEPTH) return emails;
3301
3375
  if (!obj || typeof obj !== "object") return emails;
3302
3376
  if (Array.isArray(obj)) {
3303
- for (let i = 0; i < Math.min(obj.length, 10); i++) {
3304
- emails.push(...findEmails(obj[i]));
3377
+ for (let i = 0; i < Math.min(obj.length, PII_SCAN_ARRAY_LIMIT); i++) {
3378
+ emails.push(...findEmails(obj[i], depth + 1));
3305
3379
  }
3306
3380
  return emails;
3307
3381
  }
@@ -3309,7 +3383,7 @@ function findEmails(obj) {
3309
3383
  if (typeof v === "string" && EMAIL_RE.test(v)) {
3310
3384
  emails.push(v);
3311
3385
  } else if (typeof v === "object" && v !== null) {
3312
- emails.push(...findEmails(v));
3386
+ emails.push(...findEmails(v, depth + 1));
3313
3387
  }
3314
3388
  }
3315
3389
  return emails;
@@ -3352,7 +3426,7 @@ function detectFullRecordPII(target) {
3352
3426
  function detectListPII(target) {
3353
3427
  if (!Array.isArray(target) || target.length < LIST_PII_MIN_ITEMS) return null;
3354
3428
  let itemsWithEmail = 0;
3355
- for (let i = 0; i < Math.min(target.length, 10); i++) {
3429
+ for (let i = 0; i < Math.min(target.length, PII_SCAN_ARRAY_LIMIT); i++) {
3356
3430
  const item = target[i];
3357
3431
  if (item && typeof item === "object" && findEmails(item).length > 0) {
3358
3432
  itemsWithEmail++;
@@ -3369,15 +3443,15 @@ function detectPII(method, reqBody, resBody) {
3369
3443
  const target = unwrapResponse(resBody);
3370
3444
  return detectEchoPII(method, reqBody, target) ?? detectFullRecordPII(target) ?? detectListPII(target);
3371
3445
  }
3372
- var WRITE_METHODS, FULL_RECORD_MIN_FIELDS, LIST_PII_MIN_ITEMS, REASON_LABELS, responsePiiLeakRule;
3446
+ var WRITE_METHODS, REASON_LABELS, responsePiiLeakRule;
3373
3447
  var init_response_pii_leak = __esm({
3374
3448
  "src/analysis/rules/response-pii-leak.ts"() {
3375
3449
  "use strict";
3376
3450
  init_patterns();
3377
3451
  init_response();
3452
+ init_limits();
3453
+ init_http_status();
3378
3454
  WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH"]);
3379
- FULL_RECORD_MIN_FIELDS = 5;
3380
- LIST_PII_MIN_ITEMS = 2;
3381
3455
  REASON_LABELS = {
3382
3456
  echo: "echoes back PII from the request body",
3383
3457
  "full-record": "returns a full record with email and internal IDs",
@@ -3392,10 +3466,10 @@ var init_response_pii_leak = __esm({
3392
3466
  const findings = [];
3393
3467
  const seen = /* @__PURE__ */ new Map();
3394
3468
  for (const r of ctx.requests) {
3395
- if (r.statusCode >= 400) continue;
3396
- const resJson = tryParseJson2(r.responseBody);
3469
+ if (isErrorStatus(r.statusCode)) continue;
3470
+ const resJson = ctx.parsedBodies.response.get(r.id);
3397
3471
  if (!resJson) continue;
3398
- const reqJson = tryParseJson2(r.requestBody);
3472
+ const reqJson = ctx.parsedBodies.request.get(r.id) ?? null;
3399
3473
  const detection = detectPII(r.method, reqJson, resJson);
3400
3474
  if (!detection) continue;
3401
3475
  const ep = `${r.method} ${r.path}`;
@@ -3424,6 +3498,21 @@ var init_response_pii_leak = __esm({
3424
3498
  });
3425
3499
 
3426
3500
  // src/analysis/rules/scanner.ts
3501
+ function buildBodyCache(requests) {
3502
+ const response = /* @__PURE__ */ new Map();
3503
+ const request = /* @__PURE__ */ new Map();
3504
+ for (const r of requests) {
3505
+ if (r.responseBody) {
3506
+ const parsed = tryParseJson(r.responseBody);
3507
+ if (parsed != null) response.set(r.id, parsed);
3508
+ }
3509
+ if (r.requestBody) {
3510
+ const parsed = tryParseJson(r.requestBody);
3511
+ if (parsed != null) request.set(r.id, parsed);
3512
+ }
3513
+ }
3514
+ return { response, request };
3515
+ }
3427
3516
  function createDefaultScanner() {
3428
3517
  const scanner = new SecurityScanner();
3429
3518
  scanner.register(exposedSecretRule);
@@ -3440,6 +3529,7 @@ var SecurityScanner;
3440
3529
  var init_scanner = __esm({
3441
3530
  "src/analysis/rules/scanner.ts"() {
3442
3531
  "use strict";
3532
+ init_response();
3443
3533
  init_exposed_secret();
3444
3534
  init_token_in_url();
3445
3535
  init_stack_trace_leak();
@@ -3453,7 +3543,11 @@ var init_scanner = __esm({
3453
3543
  register(rule) {
3454
3544
  this.rules.push(rule);
3455
3545
  }
3456
- scan(ctx) {
3546
+ scan(input) {
3547
+ const ctx = {
3548
+ ...input,
3549
+ parsedBodies: buildBodyCache(input.requests)
3550
+ };
3457
3551
  const findings = [];
3458
3552
  for (const rule of this.rules) {
3459
3553
  try {
@@ -3526,16 +3620,22 @@ var init_collections = __esm({
3526
3620
  });
3527
3621
 
3528
3622
  // src/utils/endpoint.ts
3623
+ function normalizePath(path) {
3624
+ const qIdx = path.indexOf("?");
3625
+ const pathname = qIdx === -1 ? path : path.slice(0, qIdx);
3626
+ return pathname.split("/").map((seg) => seg && DYNAMIC_SEGMENT_RE.test(seg) ? ":id" : seg).join("/");
3627
+ }
3529
3628
  function getEndpointKey(method, path) {
3530
- return `${method} ${path}`;
3629
+ return `${method} ${normalizePath(path)}`;
3531
3630
  }
3532
3631
  function extractEndpointFromDesc(desc) {
3533
3632
  return desc.match(ENDPOINT_PREFIX_RE)?.[1] ?? null;
3534
3633
  }
3535
- var ENDPOINT_PREFIX_RE;
3634
+ var DYNAMIC_SEGMENT_RE, ENDPOINT_PREFIX_RE;
3536
3635
  var init_endpoint = __esm({
3537
3636
  "src/utils/endpoint.ts"() {
3538
3637
  "use strict";
3638
+ DYNAMIC_SEGMENT_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$|^\d+$|^[0-9a-f]{12,}$|^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z0-9_-]{8,}$/i;
3539
3639
  ENDPOINT_PREFIX_RE = /^(\S+\s+\S+)/;
3540
3640
  }
3541
3641
  });
@@ -3589,6 +3689,15 @@ function windowByEndpoint(requests) {
3589
3689
  }
3590
3690
  return windowed;
3591
3691
  }
3692
+ function extractActiveEndpoints(requests) {
3693
+ const endpoints = /* @__PURE__ */ new Set();
3694
+ for (const r of requests) {
3695
+ if (!r.isStatic && (!r.path || !r.path.startsWith(DASHBOARD_PREFIX))) {
3696
+ endpoints.add(getEndpointKey(r.method, r.path));
3697
+ }
3698
+ }
3699
+ return endpoints;
3700
+ }
3592
3701
  function prepareContext(ctx) {
3593
3702
  const nonStatic = ctx.requests.filter(
3594
3703
  (r) => !r.isStatic && (!r.path || !r.path.startsWith(DASHBOARD_PREFIX))
@@ -3606,7 +3715,7 @@ function prepareContext(ctx) {
3606
3715
  endpointGroups.set(ep, g);
3607
3716
  }
3608
3717
  g.total++;
3609
- if (r.statusCode >= 400) g.errors++;
3718
+ if (isErrorStatus(r.statusCode)) g.errors++;
3610
3719
  g.totalDuration += r.durationMs;
3611
3720
  g.totalSize += r.responseSize ?? 0;
3612
3721
  const reqQueries = queriesByReq.get(r.id) ?? [];
@@ -3643,6 +3752,7 @@ var init_prepare = __esm({
3643
3752
  init_collections();
3644
3753
  init_endpoint();
3645
3754
  init_constants();
3755
+ init_http_status();
3646
3756
  init_thresholds();
3647
3757
  init_query_helpers();
3648
3758
  }
@@ -4127,6 +4237,7 @@ var init_response_overfetch = __esm({
4127
4237
  "use strict";
4128
4238
  init_endpoint();
4129
4239
  init_response();
4240
+ init_http_status();
4130
4241
  init_patterns();
4131
4242
  init_constants();
4132
4243
  responseOverfetchRule = {
@@ -4135,7 +4246,7 @@ var init_response_overfetch = __esm({
4135
4246
  const insights = [];
4136
4247
  const seen = /* @__PURE__ */ new Set();
4137
4248
  for (const r of ctx.nonStatic) {
4138
- if (r.statusCode >= 400 || !r.responseBody) continue;
4249
+ if (isErrorStatus(r.statusCode) || !r.responseBody) continue;
4139
4250
  const ep = getEndpointKey(r.method, r.path);
4140
4251
  if (seen.has(ep)) continue;
4141
4252
  let parsed;
@@ -4323,7 +4434,7 @@ function createDefaultInsightRunner() {
4323
4434
  function computeInsights(ctx) {
4324
4435
  return createDefaultInsightRunner().run(ctx);
4325
4436
  }
4326
- var init_insights2 = __esm({
4437
+ var init_insights = __esm({
4327
4438
  "src/analysis/insights/index.ts"() {
4328
4439
  "use strict";
4329
4440
  init_runner();
@@ -4333,95 +4444,48 @@ var init_insights2 = __esm({
4333
4444
  });
4334
4445
 
4335
4446
  // src/analysis/insights.ts
4336
- var init_insights3 = __esm({
4447
+ var init_insights2 = __esm({
4337
4448
  "src/analysis/insights.ts"() {
4338
4449
  "use strict";
4339
- init_insights2();
4450
+ init_insights();
4340
4451
  }
4341
4452
  });
4342
4453
 
4343
- // src/analysis/insight-tracker.ts
4344
- function computeInsightKey(insight) {
4345
- const identifier = extractEndpointFromDesc(insight.desc) ?? insight.title;
4346
- return `${insight.type}:${identifier}`;
4454
+ // src/analysis/issue-mappers.ts
4455
+ function categorizeInsight(type) {
4456
+ if (type === "security") return "security";
4457
+ if (type === "error" || type === "error-hotspot") return "reliability";
4458
+ return "performance";
4347
4459
  }
4348
- function enrichedIdFromInsight(insight) {
4349
- return computeInsightId(insight.type, insight.nav ?? "global", insight.desc);
4460
+ function insightToIssue(insight) {
4461
+ return {
4462
+ category: categorizeInsight(insight.type),
4463
+ rule: insight.type,
4464
+ severity: insight.severity,
4465
+ title: insight.title,
4466
+ desc: insight.desc,
4467
+ hint: insight.hint,
4468
+ detail: insight.detail,
4469
+ endpoint: extractEndpointFromDesc(insight.desc) ?? void 0,
4470
+ nav: insight.nav
4471
+ };
4350
4472
  }
4351
- var InsightTracker;
4352
- var init_insight_tracker = __esm({
4353
- "src/analysis/insight-tracker.ts"() {
4473
+ function securityFindingToIssue(finding) {
4474
+ return {
4475
+ category: "security",
4476
+ rule: finding.rule,
4477
+ severity: finding.severity,
4478
+ title: finding.title,
4479
+ desc: finding.desc,
4480
+ hint: finding.hint,
4481
+ endpoint: finding.endpoint,
4482
+ nav: "security"
4483
+ };
4484
+ }
4485
+ var init_issue_mappers = __esm({
4486
+ "src/analysis/issue-mappers.ts"() {
4354
4487
  "use strict";
4355
4488
  init_endpoint();
4356
- init_finding_id();
4357
- init_thresholds();
4358
- InsightTracker = class {
4359
- tracked = /* @__PURE__ */ new Map();
4360
- enrichedIndex = /* @__PURE__ */ new Map();
4361
- reconcile(current) {
4362
- const currentKeys = /* @__PURE__ */ new Set();
4363
- const now = Date.now();
4364
- for (const insight of current) {
4365
- const key = computeInsightKey(insight);
4366
- currentKeys.add(key);
4367
- const existing = this.tracked.get(key);
4368
- this.enrichedIndex.set(enrichedIdFromInsight(insight), key);
4369
- if (existing) {
4370
- existing.insight = insight;
4371
- existing.lastSeenAt = now;
4372
- existing.consecutiveAbsences = 0;
4373
- if (existing.state === "resolved") {
4374
- existing.state = "open";
4375
- existing.resolvedAt = null;
4376
- }
4377
- } else {
4378
- this.tracked.set(key, {
4379
- key,
4380
- state: "open",
4381
- insight,
4382
- firstSeenAt: now,
4383
- lastSeenAt: now,
4384
- resolvedAt: null,
4385
- consecutiveAbsences: 0,
4386
- aiStatus: null,
4387
- aiNotes: null
4388
- });
4389
- }
4390
- }
4391
- for (const [, stateful] of this.tracked) {
4392
- if ((stateful.state === "open" || stateful.state === "fixing") && !currentKeys.has(stateful.key)) {
4393
- stateful.consecutiveAbsences++;
4394
- if (stateful.consecutiveAbsences >= RESOLVE_AFTER_ABSENCES) {
4395
- stateful.state = "resolved";
4396
- stateful.resolvedAt = now;
4397
- }
4398
- } else if (stateful.state === "resolved" && stateful.resolvedAt !== null && now - stateful.resolvedAt > RESOLVED_INSIGHT_TTL_MS) {
4399
- this.tracked.delete(stateful.key);
4400
- this.enrichedIndex.delete(enrichedIdFromInsight(stateful.insight));
4401
- }
4402
- }
4403
- return [...this.tracked.values()];
4404
- }
4405
- reportFix(enrichedId, status, notes) {
4406
- const key = this.enrichedIndex.get(enrichedId);
4407
- if (!key) return false;
4408
- const stateful = this.tracked.get(key);
4409
- if (!stateful) return false;
4410
- stateful.aiStatus = status;
4411
- stateful.aiNotes = notes;
4412
- if (status === "fixed") {
4413
- stateful.state = "fixing";
4414
- }
4415
- return true;
4416
- }
4417
- getAll() {
4418
- return [...this.tracked.values()];
4419
- }
4420
- clear() {
4421
- this.tracked.clear();
4422
- this.enrichedIndex.clear();
4423
- }
4424
- };
4425
4489
  }
4426
4490
  });
4427
4491
 
@@ -4434,8 +4498,10 @@ var init_engine = __esm({
4434
4498
  init_disposable();
4435
4499
  init_group();
4436
4500
  init_rules();
4437
- init_insights3();
4438
- init_insight_tracker();
4501
+ init_insights2();
4502
+ init_issue_mappers();
4503
+ init_issue_id();
4504
+ init_prepare();
4439
4505
  AnalysisEngine = class {
4440
4506
  constructor(registry, debounceMs = ANALYSIS_DEBOUNCE_MS) {
4441
4507
  this.registry = registry;
@@ -4443,10 +4509,8 @@ var init_engine = __esm({
4443
4509
  this.scanner = createDefaultScanner();
4444
4510
  }
4445
4511
  scanner;
4446
- insightTracker = new InsightTracker();
4447
4512
  cachedInsights = [];
4448
4513
  cachedFindings = [];
4449
- cachedStatefulInsights = [];
4450
4514
  debounceTimer = null;
4451
4515
  subs = new SubscriptionBag();
4452
4516
  start() {
@@ -4469,15 +4533,6 @@ var init_engine = __esm({
4469
4533
  getFindings() {
4470
4534
  return this.cachedFindings;
4471
4535
  }
4472
- getStatefulFindings() {
4473
- return this.registry.has("finding-store") ? this.registry.get("finding-store").getAll() : [];
4474
- }
4475
- getStatefulInsights() {
4476
- return this.insightTracker.getAll();
4477
- }
4478
- reportInsightFix(enrichedId, status, notes) {
4479
- return this.insightTracker.reportFix(enrichedId, status, notes);
4480
- }
4481
4536
  scheduleRecompute() {
4482
4537
  if (this.debounceTimer) return;
4483
4538
  this.debounceTimer = setTimeout(() => {
@@ -4486,20 +4541,14 @@ var init_engine = __esm({
4486
4541
  }, this.debounceMs);
4487
4542
  }
4488
4543
  recompute() {
4489
- const requests = this.registry.get("request-store").getAll();
4544
+ const allRequests = this.registry.get("request-store").getAll();
4490
4545
  const queries = this.registry.get("query-store").getAll();
4491
4546
  const errors = this.registry.get("error-store").getAll();
4492
4547
  const logs = this.registry.get("log-store").getAll();
4493
4548
  const fetches = this.registry.get("fetch-store").getAll();
4549
+ const requests = windowByEndpoint(allRequests);
4494
4550
  const flows = groupRequestsIntoFlows(requests);
4495
4551
  this.cachedFindings = this.scanner.scan({ requests, logs });
4496
- if (this.registry.has("finding-store")) {
4497
- const findingStore = this.registry.get("finding-store");
4498
- for (const finding of this.cachedFindings) {
4499
- findingStore.upsert(finding, "passive");
4500
- }
4501
- findingStore.reconcilePassive(this.cachedFindings);
4502
- }
4503
4552
  this.cachedInsights = computeInsights({
4504
4553
  requests,
4505
4554
  queries,
@@ -4509,14 +4558,30 @@ var init_engine = __esm({
4509
4558
  previousMetrics: this.registry.get("metrics-store").getAll(),
4510
4559
  securityFindings: this.cachedFindings
4511
4560
  });
4512
- this.cachedStatefulInsights = this.insightTracker.reconcile(this.cachedInsights);
4513
- const update = {
4514
- insights: this.cachedInsights,
4515
- findings: this.cachedFindings,
4516
- statefulFindings: this.getStatefulFindings(),
4517
- statefulInsights: this.cachedStatefulInsights
4518
- };
4519
- this.registry.get("event-bus").emit("analysis:updated", update);
4561
+ if (this.registry.has("issue-store")) {
4562
+ const issueStore = this.registry.get("issue-store");
4563
+ for (const finding of this.cachedFindings) {
4564
+ issueStore.upsert(securityFindingToIssue(finding), "passive");
4565
+ }
4566
+ for (const insight of this.cachedInsights) {
4567
+ issueStore.upsert(insightToIssue(insight), "passive");
4568
+ }
4569
+ const currentIssueIds = /* @__PURE__ */ new Set();
4570
+ for (const finding of this.cachedFindings) {
4571
+ currentIssueIds.add(computeIssueId(securityFindingToIssue(finding)));
4572
+ }
4573
+ for (const insight of this.cachedInsights) {
4574
+ currentIssueIds.add(computeIssueId(insightToIssue(insight)));
4575
+ }
4576
+ const activeEndpoints = extractActiveEndpoints(allRequests);
4577
+ issueStore.reconcile(currentIssueIds, activeEndpoints);
4578
+ const update = {
4579
+ insights: this.cachedInsights,
4580
+ findings: this.cachedFindings,
4581
+ issues: issueStore.getAll()
4582
+ };
4583
+ this.registry.get("event-bus").emit("analysis:updated", update);
4584
+ }
4520
4585
  }
4521
4586
  };
4522
4587
  }
@@ -4527,14 +4592,14 @@ var VERSION;
4527
4592
  var init_src = __esm({
4528
4593
  "src/index.ts"() {
4529
4594
  "use strict";
4530
- init_finding_store();
4595
+ init_issue_store();
4531
4596
  init_project();
4532
4597
  init_adapter_registry();
4533
4598
  init_rules();
4534
4599
  init_engine();
4535
- init_insights3();
4536
4600
  init_insights2();
4537
- VERSION = "0.8.5";
4601
+ init_insights();
4602
+ VERSION = "0.8.6";
4538
4603
  }
4539
4604
  });
4540
4605
 
@@ -5148,7 +5213,7 @@ function getFlowInsights() {
5148
5213
  }
5149
5214
  `;
5150
5215
  }
5151
- var init_insights4 = __esm({
5216
+ var init_insights3 = __esm({
5152
5217
  "src/dashboard/client/views/flows/insights.ts"() {
5153
5218
  "use strict";
5154
5219
  init_constants();
@@ -5340,7 +5405,7 @@ function getFlowsView() {
5340
5405
  var init_flows2 = __esm({
5341
5406
  "src/dashboard/client/views/flows.ts"() {
5342
5407
  "use strict";
5343
- init_insights4();
5408
+ init_insights3();
5344
5409
  init_detail();
5345
5410
  }
5346
5411
  });
@@ -6480,8 +6545,8 @@ function getOverviewRender() {
6480
6545
  '<div class="ov-stat"><span class="ov-stat-value">' + state.fetches.length + '</span><span class="ov-stat-label">Fetches</span></div>';
6481
6546
  container.appendChild(summary);
6482
6547
 
6483
- var all = state.insights || [];
6484
- var open = all.filter(function(si) { return si.state === 'open' || si.state === 'fixing'; });
6548
+ var all = state.issues || [];
6549
+ var open = all.filter(function(si) { return si.state === 'open' || si.state === 'fixing' || si.state === 'regressed'; });
6485
6550
  var resolved = all.filter(function(si) { return si.state === 'resolved'; });
6486
6551
 
6487
6552
  if (open.length === 0 && resolved.length === 0) {
@@ -6513,31 +6578,35 @@ function getOverviewRender() {
6513
6578
 
6514
6579
  for (var i = 0; i < open.length; i++) {
6515
6580
  (function(si) {
6516
- var insight = si.insight;
6581
+ var issue = si.issue;
6517
6582
  var card = document.createElement('div');
6518
6583
  card.className = 'ov-card';
6519
6584
 
6520
- var sevCfg = SEV[insight.severity];
6585
+ var sevCfg = SEV[issue.severity];
6521
6586
  var iconCls = sevCfg.cls;
6522
6587
  var iconChar = sevCfg.icon;
6523
6588
 
6524
6589
  var expandHtml = '';
6525
- if (insight.detail) expandHtml += insight.detail;
6526
- if (insight.hint) expandHtml += '<div class="ov-card-hint">' + escHtml(insight.hint) + '</div>';
6527
- expandHtml += '<span class="ov-card-link" data-nav="' + insight.nav + '">View in ' + (NAV_LABELS[insight.nav] || insight.nav) + ' \\u2192</span>';
6590
+ if (issue.detail) expandHtml += issue.detail;
6591
+ if (issue.hint) expandHtml += '<div class="ov-card-hint">' + escHtml(issue.hint) + '</div>';
6592
+ if (issue.nav) expandHtml += '<span class="ov-card-link" data-nav="' + issue.nav + '">View in ' + (NAV_LABELS[issue.nav] || issue.nav) + ' \\u2192</span>';
6528
6593
 
6529
6594
  var aiBadge = '';
6530
6595
  if (si.state === 'fixing' && si.aiStatus === 'fixed') {
6531
6596
  aiBadge = '<span class="sec-ai-badge sec-ai-fixing">AI fixed \\u2014 awaiting verification</span>';
6532
6597
  } else if (si.aiStatus === 'wont_fix') {
6533
6598
  aiBadge = '<span class="sec-ai-badge sec-ai-wontfix">AI: won\\u2019t fix</span>';
6599
+ } else if (si.state === 'regressed') {
6600
+ aiBadge = '<span class="sec-ai-badge sec-ai-fixing" style="background:var(--red)">regressed</span>';
6534
6601
  }
6535
6602
 
6603
+ var occBadge = si.occurrences > 1 ? ' <span class="sec-item-count">' + si.occurrences + 'x</span>' : '';
6604
+
6536
6605
  card.innerHTML =
6537
6606
  '<span class="ov-card-icon ' + iconCls + '">' + iconChar + '</span>' +
6538
6607
  '<div class="ov-card-body">' +
6539
- '<div class="ov-card-title">' + escHtml(insight.title) + aiBadge + '</div>' +
6540
- '<div class="ov-card-desc">' + insight.desc + '</div>' +
6608
+ '<div class="ov-card-title">' + escHtml(issue.title) + occBadge + aiBadge + '</div>' +
6609
+ '<div class="ov-card-desc">' + issue.desc + '</div>' +
6541
6610
  '<div class="ov-card-expand">' + expandHtml + '</div>' +
6542
6611
  '</div>' +
6543
6612
  '<span class="ov-card-arrow">\\u2192</span>';
@@ -6583,14 +6652,14 @@ function getOverviewRender() {
6583
6652
  resolvedCards.className = 'ov-cards';
6584
6653
 
6585
6654
  for (var ri = 0; ri < resolved.length; ri++) {
6586
- var rInsight = resolved[ri].insight;
6655
+ var rIssue = resolved[ri].issue;
6587
6656
  var rCard = document.createElement('div');
6588
6657
  rCard.className = 'ov-card ov-card-resolved';
6589
6658
  rCard.innerHTML =
6590
6659
  '<span class="ov-card-icon resolved">\\u2713</span>' +
6591
6660
  '<div class="ov-card-body">' +
6592
- '<div class="ov-card-title" style="text-decoration:line-through;color:var(--text-muted)">' + escHtml(rInsight.title) + '</div>' +
6593
- '<div class="ov-card-desc">' + rInsight.desc + '</div>' +
6661
+ '<div class="ov-card-title" style="text-decoration:line-through;color:var(--text-muted)">' + escHtml(rIssue.title) + '</div>' +
6662
+ '<div class="ov-card-desc">' + rIssue.desc + '</div>' +
6594
6663
  '</div>';
6595
6664
  resolvedCards.appendChild(rCard);
6596
6665
  }
@@ -6628,30 +6697,12 @@ function getSecurityView() {
6628
6697
  container.innerHTML = '';
6629
6698
  var SEV = ${SEVERITY_MAP};
6630
6699
 
6631
- var all = (state.findings || []).slice();
6632
- var insightsList = state.insights || [];
6633
- for (var ix = 0; ix < insightsList.length; ix++) {
6634
- var si = insightsList[ix];
6635
- all.push({
6636
- findingId: si.key,
6637
- state: si.state,
6638
- aiStatus: si.aiStatus,
6639
- aiNotes: si.aiNotes,
6640
- finding: {
6641
- severity: si.insight.severity,
6642
- rule: 'insight-' + si.insight.type,
6643
- title: si.insight.title,
6644
- desc: si.insight.desc,
6645
- hint: si.insight.hint,
6646
- endpoint: si.insight.nav || 'global',
6647
- count: 1
6648
- }
6649
- });
6650
- }
6651
- var open = all.filter(function(f) { return f.state === 'open' || f.state === 'fixing'; });
6700
+ var all = (state.issues || []).slice();
6701
+ var open = all.filter(function(f) { return f.state === 'open' || f.state === 'fixing' || f.state === 'regressed'; });
6652
6702
  var resolved = all.filter(function(f) { return f.state === 'resolved'; });
6703
+ var stale = all.filter(function(f) { return f.state === 'stale'; });
6653
6704
 
6654
- if (open.length === 0 && resolved.length === 0) {
6705
+ if (open.length === 0 && resolved.length === 0 && stale.length === 0) {
6655
6706
  var hasData = state.requests.length > 0 || state.logs.length > 0 || state.queries.length > 0;
6656
6707
  if (!hasData) {
6657
6708
  container.innerHTML = '<div class="empty"><span class="empty-title">Waiting for requests...</span><span class="empty-sub">Start using your app to see security findings here</span></div>';
@@ -6663,7 +6714,7 @@ function getSecurityView() {
6663
6714
 
6664
6715
  var critCount = 0, warnCount = 0, infoCount = 0;
6665
6716
  for (var ci = 0; ci < open.length; ci++) {
6666
- var sev = open[ci].finding.severity;
6717
+ var sev = open[ci].issue.severity;
6667
6718
  if (sev === 'critical') critCount++;
6668
6719
  else if (sev === 'info') infoCount++;
6669
6720
  else warnCount++;
@@ -6696,7 +6747,7 @@ function getSecurityView() {
6696
6747
  var groupOrder = [];
6697
6748
  for (var gi = 0; gi < open.length; gi++) {
6698
6749
  var sf = open[gi];
6699
- var f = sf.finding;
6750
+ var f = sf.issue;
6700
6751
  if (!groups[f.rule]) {
6701
6752
  groups[f.rule] = { rule: f.rule, title: f.title, severity: f.severity, hint: f.hint, items: [] };
6702
6753
  groupOrder.push(f.rule);
@@ -6739,7 +6790,7 @@ function getSecurityView() {
6739
6790
  list.className = 'sec-items';
6740
6791
  for (var ii = 0; ii < group.items.length; ii++) {
6741
6792
  var sf2 = group.items[ii];
6742
- var item = sf2.finding;
6793
+ var item = sf2.issue;
6743
6794
  var row = document.createElement('div');
6744
6795
  row.className = 'sec-item';
6745
6796
  var aiBadge = '';
@@ -6747,11 +6798,14 @@ function getSecurityView() {
6747
6798
  aiBadge = '<span class="sec-ai-badge sec-ai-fixing">AI fixed \\u2014 awaiting verification</span>';
6748
6799
  } else if (sf2.aiStatus === 'wont_fix') {
6749
6800
  aiBadge = '<span class="sec-ai-badge sec-ai-wontfix">AI: won\\u2019t fix</span>';
6801
+ } else if (sf2.state === 'regressed') {
6802
+ aiBadge = '<span class="sec-ai-badge sec-ai-fixing" style="background:var(--red)">regressed</span>';
6750
6803
  }
6751
6804
  var aiNotes = sf2.aiNotes ? '<div class="sec-ai-notes">' + escHtml(sf2.aiNotes) + '</div>' : '';
6805
+ var occBadge = sf2.occurrences > 1 ? '<span class="sec-item-count">' + sf2.occurrences + 'x</span>' : '';
6752
6806
  row.innerHTML =
6753
6807
  '<div class="sec-item-desc">' + escHtml(item.desc) + '</div>' +
6754
- (item.count > 1 ? '<span class="sec-item-count">' + item.count + 'x</span>' : '') +
6808
+ occBadge +
6755
6809
  aiBadge + aiNotes;
6756
6810
  list.appendChild(row);
6757
6811
  }
@@ -6772,20 +6826,44 @@ function getSecurityView() {
6772
6826
  resolvedItems.className = 'sec-items';
6773
6827
  for (var ri = 0; ri < resolved.length; ri++) {
6774
6828
  var rsf = resolved[ri];
6775
- var rf = rsf.finding;
6829
+ var rf = rsf.issue;
6776
6830
  var rRow = document.createElement('div');
6777
6831
  rRow.className = 'sec-item sec-item-resolved';
6778
6832
  var verifiedBadge = rsf.aiStatus === 'fixed' ? '<span class="sec-ai-badge sec-ai-verified">Verified fix</span>' : '';
6779
6833
  var rNotes = rsf.aiNotes ? '<div class="sec-ai-notes">' + escHtml(rsf.aiNotes) + '</div>' : '';
6780
6834
  rRow.innerHTML =
6781
6835
  '<span class="sec-resolved-item-icon">\\u2713</span>' +
6782
- '<div class="sec-item-desc">' + escHtml(rf.title) + ' \\u2014 ' + escHtml(rf.endpoint) + '</div>' +
6836
+ '<div class="sec-item-desc">' + escHtml(rf.title) + ' \\u2014 ' + escHtml(rf.endpoint || 'global') + '</div>' +
6783
6837
  verifiedBadge + rNotes;
6784
6838
  resolvedItems.appendChild(rRow);
6785
6839
  }
6786
6840
  resolvedGroup.appendChild(resolvedItems);
6787
6841
  container.appendChild(resolvedGroup);
6788
6842
  }
6843
+
6844
+ if (stale.length > 0) {
6845
+ var staleTitle = document.createElement('div');
6846
+ staleTitle.className = 'sec-resolved-title';
6847
+ staleTitle.innerHTML = '<span style="color:var(--text-muted)">\\u23F8</span> Stale <span class="sec-resolved-count">' + stale.length + '</span>';
6848
+ container.appendChild(staleTitle);
6849
+
6850
+ var staleGroup = document.createElement('div');
6851
+ staleGroup.className = 'sec-group sec-group-resolved';
6852
+ var staleItems = document.createElement('div');
6853
+ staleItems.className = 'sec-items';
6854
+ for (var sti = 0; sti < stale.length; sti++) {
6855
+ var ssf = stale[sti];
6856
+ var sf3 = ssf.issue;
6857
+ var sRow = document.createElement('div');
6858
+ sRow.className = 'sec-item sec-item-resolved';
6859
+ sRow.innerHTML =
6860
+ '<span style="color:var(--text-muted)">\\u23F8</span>' +
6861
+ '<div class="sec-item-desc" style="color:var(--text-muted)">' + escHtml(sf3.title) + ' \\u2014 endpoint inactive</div>';
6862
+ staleItems.appendChild(sRow);
6863
+ }
6864
+ staleGroup.appendChild(staleItems);
6865
+ container.appendChild(staleGroup);
6866
+ }
6789
6867
  }
6790
6868
  `;
6791
6869
  }
@@ -6823,13 +6901,7 @@ function getApp() {
6823
6901
  try {
6824
6902
  var res3 = await fetch('${DASHBOARD_API_INSIGHTS}');
6825
6903
  var data3 = await res3.json();
6826
- state.insights = data3.insights || [];
6827
- } catch(e) { console.warn('[brakit]', e); }
6828
-
6829
- try {
6830
- var res4 = await fetch('${DASHBOARD_API_SECURITY}');
6831
- var data4 = await res4.json();
6832
- state.findings = data4.findings || [];
6904
+ state.issues = data3.issues || [];
6833
6905
  } catch(e) { console.warn('[brakit]', e); }
6834
6906
 
6835
6907
  updateStats();
@@ -6868,19 +6940,13 @@ function getApp() {
6868
6940
  registerTelemetryListener('error_event', 'errors', prependErrorRow);
6869
6941
  registerTelemetryListener('query', 'queries', prependQueryRow);
6870
6942
 
6871
- events.addEventListener('insights', function(e) {
6872
- state.insights = JSON.parse(e.data);
6943
+ events.addEventListener('issues', function(e) {
6944
+ state.issues = JSON.parse(e.data);
6873
6945
  if (state.activeView === 'overview') renderOverview();
6874
6946
  if (state.activeView === 'security') renderSecurity();
6875
6947
  updateStats();
6876
6948
  });
6877
6949
 
6878
- events.addEventListener('security', function(e) {
6879
- state.findings = JSON.parse(e.data);
6880
- if (state.activeView === 'security') renderSecurity();
6881
- updateStats();
6882
- });
6883
-
6884
6950
  window.addEventListener('beforeunload', function() {
6885
6951
  events.close();
6886
6952
  clearTimeout(reloadTimer);
@@ -6959,9 +7025,9 @@ function getApp() {
6959
7025
  if (queryCount) queryCount.textContent = state.queries.length;
6960
7026
  var secCount = document.getElementById('sidebar-count-security');
6961
7027
  if (secCount) {
6962
- var numFindings = (state.findings || []).filter(function(f) { return f.state !== 'resolved'; }).length;
6963
- secCount.textContent = numFindings;
6964
- secCount.style.display = numFindings > 0 ? '' : 'none';
7028
+ var numIssues = (state.issues || []).filter(function(f) { return f.state !== 'resolved' && f.state !== 'stale'; }).length;
7029
+ secCount.textContent = numIssues;
7030
+ secCount.style.display = numIssues > 0 ? '' : 'none';
6965
7031
  }
6966
7032
  }
6967
7033
 
@@ -6979,7 +7045,7 @@ function getApp() {
6979
7045
  if (!confirm('This will clear all data including performance metrics history. Continue?')) return;
6980
7046
  await fetch('${DASHBOARD_API_CLEAR}', {method: 'POST'});
6981
7047
  state.flows = []; state.requests = []; state.fetches = []; state.errors = []; state.logs = []; state.queries = [];
6982
- state.insights = []; state.findings = [];
7048
+ state.issues = [];
6983
7049
  graphData = []; selectedEndpoint = ${ALL_ENDPOINTS_SELECTOR}; timelineCache = {};
6984
7050
  renderFlows(); renderRequests(); renderFetches(); renderErrors(); renderLogs(); renderQueries(); renderGraph(); renderOverview(); renderSecurity(); updateStats();
6985
7051
  showToast('Cleared');
@@ -7072,8 +7138,8 @@ var init_page = __esm({
7072
7138
  });
7073
7139
 
7074
7140
  // src/telemetry/config.ts
7075
- import { homedir } from "os";
7076
- import { join as join2 } from "path";
7141
+ import { homedir as homedir2 } from "os";
7142
+ import { join as join3 } from "path";
7077
7143
  import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
7078
7144
  import { randomUUID as randomUUID4 } from "crypto";
7079
7145
  function readConfig() {
@@ -7087,9 +7153,9 @@ function readConfig() {
7087
7153
  function writeConfig(config) {
7088
7154
  try {
7089
7155
  if (!existsSync5(CONFIG_DIR))
7090
- mkdirSync3(CONFIG_DIR, { recursive: true, mode: 448 });
7156
+ mkdirSync3(CONFIG_DIR, { recursive: true, mode: DIR_MODE_OWNER_ONLY });
7091
7157
  writeFileSync3(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", {
7092
- mode: 384
7158
+ mode: FILE_MODE_OWNER_ONLY
7093
7159
  });
7094
7160
  } catch {
7095
7161
  }
@@ -7117,8 +7183,9 @@ var CONFIG_DIR, CONFIG_PATH, cachedEnabled;
7117
7183
  var init_config = __esm({
7118
7184
  "src/telemetry/config.ts"() {
7119
7185
  "use strict";
7120
- CONFIG_DIR = join2(homedir(), ".brakit");
7121
- CONFIG_PATH = join2(CONFIG_DIR, "config.json");
7186
+ init_network();
7187
+ CONFIG_DIR = join3(homedir2(), ".brakit");
7188
+ CONFIG_PATH = join3(CONFIG_DIR, "config.json");
7122
7189
  cachedEnabled = null;
7123
7190
  }
7124
7191
  });
@@ -7150,12 +7217,12 @@ function recordDashboardOpened() {
7150
7217
  }
7151
7218
  function speedBucket(ms) {
7152
7219
  if (ms === 0) return "none";
7153
- if (ms < 200) return "<200ms";
7154
- if (ms < 500) return "200-500ms";
7155
- if (ms < 1e3) return "500-1000ms";
7156
- if (ms < 2e3) return "1000-2000ms";
7157
- if (ms < 5e3) return "2000-5000ms";
7158
- return ">5000ms";
7220
+ const t = SPEED_BUCKET_THRESHOLDS;
7221
+ if (ms < t[0]) return `<${t[0]}ms`;
7222
+ for (let i = 1; i < t.length; i++) {
7223
+ if (ms < t[i]) return `${t[i - 1]}-${t[i]}ms`;
7224
+ }
7225
+ return `>${t[t.length - 1]}ms`;
7159
7226
  }
7160
7227
  function trackSession(registry) {
7161
7228
  if (!isTelemetryEnabled()) return;
@@ -7255,7 +7322,6 @@ function isDashboardRequest(url) {
7255
7322
  }
7256
7323
  function createDashboardHandler(registry) {
7257
7324
  const metricsStore = registry.get("metrics-store");
7258
- const analysisEngine = registry.has("analysis-engine") ? registry.get("analysis-engine") : void 0;
7259
7325
  const routes = {
7260
7326
  [DASHBOARD_API_REQUESTS]: createRequestsHandler(registry),
7261
7327
  [DASHBOARD_API_EVENTS]: createSSEHandler(registry),
@@ -7270,17 +7336,14 @@ function createDashboardHandler(registry) {
7270
7336
  [DASHBOARD_API_INGEST]: createIngestHandler(registry),
7271
7337
  [DASHBOARD_API_ACTIVITY]: createActivityHandler(registry)
7272
7338
  };
7273
- if (analysisEngine) {
7274
- routes[DASHBOARD_API_INSIGHTS] = createInsightsHandler(analysisEngine);
7275
- routes[DASHBOARD_API_SECURITY] = createSecurityHandler(analysisEngine);
7276
- }
7277
- if (registry.has("finding-store")) {
7278
- const findingStore = registry.get("finding-store");
7279
- routes[DASHBOARD_API_FINDINGS] = createFindingsHandler(findingStore);
7280
- routes[DASHBOARD_API_FINDINGS_REPORT] = createFindingsReportHandler(
7281
- findingStore,
7282
- registry.get("event-bus"),
7283
- analysisEngine
7339
+ if (registry.has("issue-store")) {
7340
+ const issueStore = registry.get("issue-store");
7341
+ routes[DASHBOARD_API_INSIGHTS] = createIssuesHandler(issueStore);
7342
+ routes[DASHBOARD_API_SECURITY] = createIssuesHandler(issueStore);
7343
+ routes[DASHBOARD_API_FINDINGS] = createFindingsHandler(issueStore);
7344
+ routes[DASHBOARD_API_FINDINGS_REPORT] = createIssuesReportHandler(
7345
+ issueStore,
7346
+ registry.get("event-bus")
7284
7347
  );
7285
7348
  }
7286
7349
  routes[DASHBOARD_API_TAB] = (req, res) => {
@@ -7314,8 +7377,7 @@ var init_router = __esm({
7314
7377
  init_constants();
7315
7378
  init_http();
7316
7379
  init_api();
7317
- init_insights();
7318
- init_findings();
7380
+ init_issues();
7319
7381
  init_sse();
7320
7382
  init_page();
7321
7383
  init_telemetry2();
@@ -7603,6 +7665,7 @@ var init_metrics_store = __esm({
7603
7665
  "use strict";
7604
7666
  init_constants();
7605
7667
  init_math();
7668
+ init_http_status();
7606
7669
  init_endpoint();
7607
7670
  MetricsStore = class {
7608
7671
  constructor(persistence) {
@@ -7614,6 +7677,7 @@ var init_metrics_store = __esm({
7614
7677
  sessionId = randomUUID6();
7615
7678
  sessionStart = Date.now();
7616
7679
  flushTimer = null;
7680
+ dirty = false;
7617
7681
  accumulators = /* @__PURE__ */ new Map();
7618
7682
  pendingPoints = /* @__PURE__ */ new Map();
7619
7683
  start() {
@@ -7639,21 +7703,27 @@ var init_metrics_store = __esm({
7639
7703
  }
7640
7704
  recordRequest(req, metrics) {
7641
7705
  if (req.isStatic) return;
7706
+ this.dirty = true;
7642
7707
  const key = getEndpointKey(req.method, req.path);
7643
7708
  let acc = this.accumulators.get(key);
7644
7709
  if (!acc) {
7710
+ if (this.accumulators.size >= MAX_UNIQUE_ENDPOINTS) return;
7645
7711
  acc = createAccumulator();
7646
7712
  this.accumulators.set(key, acc);
7647
7713
  }
7648
- acc.durations.push(req.durationMs);
7649
- acc.queryCounts.push(metrics.queryCount);
7650
- if (req.statusCode >= 400) acc.errorCount++;
7714
+ if (acc.durations.length < MAX_ACCUMULATOR_ENTRIES) {
7715
+ acc.durations.push(req.durationMs);
7716
+ }
7717
+ if (acc.queryCounts.length < MAX_ACCUMULATOR_ENTRIES) {
7718
+ acc.queryCounts.push(metrics.queryCount);
7719
+ }
7720
+ if (isErrorStatus(req.statusCode)) acc.errorCount++;
7651
7721
  acc.totalDurationSum += req.durationMs;
7652
7722
  acc.totalRequestCount++;
7653
7723
  acc.totalQuerySum += metrics.queryCount;
7654
7724
  acc.totalQueryTimeMs += metrics.queryTimeMs;
7655
7725
  acc.totalFetchTimeMs += metrics.fetchTimeMs;
7656
- if (req.statusCode >= 400) acc.totalErrorCount++;
7726
+ if (isErrorStatus(req.statusCode)) acc.totalErrorCount++;
7657
7727
  const timestamp = Math.round(
7658
7728
  Date.now() - (performance.now() - req.startedAt)
7659
7729
  );
@@ -7667,10 +7737,13 @@ var init_metrics_store = __esm({
7667
7737
  };
7668
7738
  let pending2 = this.pendingPoints.get(key);
7669
7739
  if (!pending2) {
7740
+ if (this.pendingPoints.size >= MAX_UNIQUE_ENDPOINTS) return;
7670
7741
  pending2 = [];
7671
7742
  this.pendingPoints.set(key, pending2);
7672
7743
  }
7673
- pending2.push(point);
7744
+ if (pending2.length < MAX_ACCUMULATOR_ENTRIES) {
7745
+ pending2.push(point);
7746
+ }
7674
7747
  }
7675
7748
  getAll() {
7676
7749
  return this.data.endpoints;
@@ -7693,7 +7766,7 @@ var init_metrics_store = __esm({
7693
7766
  for (const [endpoint, requests] of merged) {
7694
7767
  if (requests.length === 0) continue;
7695
7768
  const durations = requests.map((r) => r.durationMs);
7696
- const errors = requests.filter((r) => r.statusCode >= 400).length;
7769
+ const errors = requests.filter((r) => isErrorStatus(r.statusCode)).length;
7697
7770
  const totalQueries = requests.reduce((s, r) => s + r.queryCount, 0);
7698
7771
  const totalQueryTime = requests.reduce((s, r) => s + (r.queryTimeMs ?? 0), 0);
7699
7772
  const totalFetchTime = requests.reduce((s, r) => s + (r.fetchTimeMs ?? 0), 0);
@@ -7723,6 +7796,7 @@ var init_metrics_store = __esm({
7723
7796
  this.endpointIndex.clear();
7724
7797
  this.accumulators.clear();
7725
7798
  this.pendingPoints.clear();
7799
+ this.dirty = false;
7726
7800
  this.persistence.remove();
7727
7801
  }
7728
7802
  flush(sync = false) {
@@ -7763,11 +7837,13 @@ var init_metrics_store = __esm({
7763
7837
  epMetrics.dataPoints = existing.concat(points).slice(-METRICS_MAX_DATA_POINTS);
7764
7838
  }
7765
7839
  this.pendingPoints.clear();
7840
+ if (!this.dirty) return;
7766
7841
  if (sync) {
7767
7842
  this.persistence.saveSync(this.data);
7768
7843
  } else {
7769
7844
  this.persistence.save(this.data);
7770
7845
  }
7846
+ this.dirty = false;
7771
7847
  }
7772
7848
  getOrCreateEndpoint(endpoint) {
7773
7849
  let ep = this.endpointIndex.get(endpoint);
@@ -7784,9 +7860,9 @@ var init_metrics_store = __esm({
7784
7860
 
7785
7861
  // src/store/metrics/persistence.ts
7786
7862
  import { readFile as readFile4 } from "fs/promises";
7787
- import { readFileSync as readFileSync4, existsSync as existsSync6, unlinkSync } from "fs";
7863
+ import { readFileSync as readFileSync4, existsSync as existsSync6, unlinkSync as unlinkSync2 } from "fs";
7788
7864
  import { resolve as resolve3 } from "path";
7789
- var FileMetricsPersistence;
7865
+ var DEFAULT_METRICS, FileMetricsPersistence;
7790
7866
  var init_persistence = __esm({
7791
7867
  "src/store/metrics/persistence.ts"() {
7792
7868
  "use strict";
@@ -7795,45 +7871,41 @@ var init_persistence = __esm({
7795
7871
  init_fs();
7796
7872
  init_log();
7797
7873
  init_type_guards();
7874
+ DEFAULT_METRICS = { version: 1, endpoints: [] };
7798
7875
  FileMetricsPersistence = class {
7799
7876
  metricsPath;
7800
7877
  writer;
7801
- constructor(rootDir) {
7802
- this.metricsPath = resolve3(rootDir, METRICS_FILE);
7878
+ constructor(dataDir) {
7879
+ this.metricsPath = resolve3(dataDir, METRICS_FILE);
7803
7880
  this.writer = new AtomicWriter({
7804
- dir: resolve3(rootDir, METRICS_DIR),
7881
+ dir: dataDir,
7805
7882
  filePath: this.metricsPath,
7806
- gitignoreEntry: METRICS_DIR,
7807
7883
  label: "metrics"
7808
7884
  });
7809
7885
  }
7810
7886
  load() {
7811
7887
  try {
7812
7888
  if (existsSync6(this.metricsPath)) {
7813
- const raw = readFileSync4(this.metricsPath, "utf-8");
7814
- const parsed = JSON.parse(raw);
7815
- if (parsed?.version === 1 && Array.isArray(parsed.endpoints)) {
7816
- return parsed;
7817
- }
7889
+ return this.parseMetrics(readFileSync4(this.metricsPath, "utf-8"));
7818
7890
  }
7819
7891
  } catch (err) {
7820
- brakitWarn(`failed to load metrics: ${getErrorMessage(err)}`);
7892
+ brakitWarn(`failed to load ${this.metricsPath}: ${getErrorMessage(err)}`);
7821
7893
  }
7822
- return { version: 1, endpoints: [] };
7894
+ return { ...DEFAULT_METRICS };
7823
7895
  }
7824
7896
  async loadAsync() {
7825
7897
  try {
7826
7898
  if (await fileExists(this.metricsPath)) {
7827
- const raw = await readFile4(this.metricsPath, "utf-8");
7828
- const parsed = JSON.parse(raw);
7829
- if (parsed?.version === 1 && Array.isArray(parsed.endpoints)) {
7830
- return parsed;
7831
- }
7899
+ return this.parseMetrics(await readFile4(this.metricsPath, "utf-8"));
7832
7900
  }
7833
7901
  } catch (err) {
7834
- brakitWarn(`failed to load metrics: ${getErrorMessage(err)}`);
7902
+ brakitWarn(`failed to load ${this.metricsPath}: ${getErrorMessage(err)}`);
7835
7903
  }
7836
- return { version: 1, endpoints: [] };
7904
+ return { ...DEFAULT_METRICS };
7905
+ }
7906
+ /** Parse and validate metrics JSON, returning default empty data on invalid input. */
7907
+ parseMetrics(raw) {
7908
+ return validateMetricsData(JSON.parse(raw)) ?? { ...DEFAULT_METRICS };
7837
7909
  }
7838
7910
  save(data) {
7839
7911
  this.writer.writeAsync(JSON.stringify(data));
@@ -7844,9 +7916,10 @@ var init_persistence = __esm({
7844
7916
  remove() {
7845
7917
  try {
7846
7918
  if (existsSync6(this.metricsPath)) {
7847
- unlinkSync(this.metricsPath);
7919
+ unlinkSync2(this.metricsPath);
7848
7920
  }
7849
- } catch {
7921
+ } catch (err) {
7922
+ brakitDebug(`failed to remove metrics file: ${getErrorMessage(err)}`);
7850
7923
  }
7851
7924
  }
7852
7925
  };
@@ -7883,14 +7956,14 @@ function colorTitle(severity, text) {
7883
7956
  function truncate(s, max = TERMINAL_TRUNCATE_LENGTH) {
7884
7957
  return s.length <= max ? s : s.slice(0, max - 1) + "\u2026";
7885
7958
  }
7886
- function formatConsoleLine(insight, suffix) {
7887
- const icon = severityIcon(insight.severity);
7888
- const title = colorTitle(insight.severity, insight.title);
7889
- const desc = pc.dim(truncate(insight.desc) + (suffix ?? ""));
7959
+ function formatConsoleLine(issue, suffix) {
7960
+ const icon = severityIcon(issue.severity);
7961
+ const title = colorTitle(issue.severity, issue.title);
7962
+ const desc = pc.dim(truncate(issue.desc) + (suffix ?? ""));
7890
7963
  let line = ` ${icon} ${title} \u2014 ${desc}`;
7891
- if (insight.detail) {
7964
+ if (issue.detail) {
7892
7965
  line += `
7893
- ${pc.dim("\u2514 " + insight.detail)}`;
7966
+ ${pc.dim("\u2514 " + issue.detail)}`;
7894
7967
  }
7895
7968
  return line;
7896
7969
  }
@@ -7900,26 +7973,37 @@ function startTerminalInsights(registry, proxyPort) {
7900
7973
  const printedKeys = /* @__PURE__ */ new Set();
7901
7974
  const resolvedKeys = /* @__PURE__ */ new Set();
7902
7975
  const dashUrl = `localhost:${proxyPort}${DASHBOARD_PREFIX}`;
7903
- return bus.on("analysis:updated", ({ statefulInsights }) => {
7976
+ return bus.on("analysis:updated", ({ issues }) => {
7904
7977
  const newLines = [];
7905
7978
  const resolvedLines = [];
7906
- for (const si of statefulInsights) {
7979
+ const regressedLines = [];
7980
+ for (const si of issues) {
7907
7981
  if (si.state === "resolved") {
7908
- if (resolvedKeys.has(si.key)) continue;
7909
- resolvedKeys.add(si.key);
7910
- printedKeys.delete(si.key);
7911
- const title = pc.green(pc.bold(`\u2713 ${si.insight.title}`));
7912
- const desc = pc.dim(truncate(si.insight.desc));
7982
+ if (resolvedKeys.has(si.issueId)) continue;
7983
+ resolvedKeys.add(si.issueId);
7984
+ printedKeys.delete(si.issueId);
7985
+ const title = pc.green(pc.bold(`\u2713 ${si.issue.title}`));
7986
+ const desc = pc.dim(truncate(si.issue.desc));
7913
7987
  resolvedLines.push(` ${title} \u2014 ${desc} ${pc.green("resolved")}`);
7914
7988
  continue;
7915
7989
  }
7916
- resolvedKeys.delete(si.key);
7917
- if (si.insight.severity === "info") continue;
7918
- if (printedKeys.has(si.key)) continue;
7919
- printedKeys.add(si.key);
7990
+ if (si.state === "regressed") {
7991
+ if (!printedKeys.has(si.issueId)) {
7992
+ printedKeys.add(si.issueId);
7993
+ resolvedKeys.delete(si.issueId);
7994
+ const title = pc.red(pc.bold(`\u26A0 ${si.issue.title}`));
7995
+ const desc = pc.dim(truncate(si.issue.desc));
7996
+ regressedLines.push(` ${title} \u2014 ${desc} ${pc.red("regressed")}`);
7997
+ }
7998
+ continue;
7999
+ }
8000
+ resolvedKeys.delete(si.issueId);
8001
+ if (si.issue.severity === "info") continue;
8002
+ if (printedKeys.has(si.issueId)) continue;
8003
+ printedKeys.add(si.issueId);
7920
8004
  let suffix;
7921
- if (si.insight.type === "slow") {
7922
- const endpoint = extractEndpointFromDesc(si.insight.desc);
8005
+ if (si.issue.rule === "slow") {
8006
+ const endpoint = si.issue.endpoint;
7923
8007
  if (endpoint) {
7924
8008
  const ep = metricsStore.getEndpoint(endpoint);
7925
8009
  if (ep && ep.sessions.length > 1) {
@@ -7928,7 +8012,7 @@ function startTerminalInsights(registry, proxyPort) {
7928
8012
  }
7929
8013
  }
7930
8014
  }
7931
- newLines.push(formatConsoleLine(si.insight, suffix));
8015
+ newLines.push(formatConsoleLine(si.issue, suffix));
7932
8016
  }
7933
8017
  if (newLines.length > 0) {
7934
8018
  print("");
@@ -7936,6 +8020,12 @@ function startTerminalInsights(registry, proxyPort) {
7936
8020
  print("");
7937
8021
  print(` ${pc.magenta(pc.bold("brakit"))} ${pc.dim("\u2192")} ${pc.dim("Dashboard:")} ${pc.underline(`http://${dashUrl}`)} ${pc.dim("or ask your AI:")} ${pc.bold('"Fix brakit findings"')}`);
7938
8022
  }
8023
+ if (regressedLines.length > 0) {
8024
+ print("");
8025
+ for (const line of regressedLines) print(line);
8026
+ print("");
8027
+ print(` ${pc.magenta(pc.bold("brakit"))} ${pc.dim("\u2192")} ${pc.red("Issues came back after being resolved!")}`);
8028
+ }
7939
8029
  if (resolvedLines.length > 0) {
7940
8030
  print("");
7941
8031
  for (const line of resolvedLines) print(line);
@@ -7952,7 +8042,6 @@ var init_terminal = __esm({
7952
8042
  init_constants();
7953
8043
  init_limits();
7954
8044
  init_severity();
7955
- init_endpoint();
7956
8045
  SEVERITY_COLOR = {
7957
8046
  critical: pc.red,
7958
8047
  warning: pc.yellow,
@@ -8050,12 +8139,7 @@ function decompressAsync(body, encoding) {
8050
8139
  if (!decompressor) return Promise.resolve(body);
8051
8140
  return new Promise((resolve5) => {
8052
8141
  decompressor(body, (err, result) => {
8053
- if (err) {
8054
- brakitDebug(`decompress failed: ${err.message}`);
8055
- resolve5(body);
8056
- } else {
8057
- resolve5(result);
8058
- }
8142
+ resolve5(err ? body : result);
8059
8143
  });
8060
8144
  });
8061
8145
  }
@@ -8072,18 +8156,23 @@ function captureInProcess(req, res, requestId, requestStore) {
8072
8156
  let resSize = 0;
8073
8157
  const originalWrite = res.write;
8074
8158
  const originalEnd = res.end;
8159
+ let truncated = false;
8075
8160
  res.write = function(...args) {
8076
8161
  try {
8077
8162
  const chunk = args[0];
8078
- if (chunk != null && typeof chunk !== "function" && resSize < DEFAULT_MAX_BODY_CAPTURE) {
8079
- const buf = toBuffer(chunk);
8080
- if (buf) {
8081
- resChunks.push(buf);
8082
- resSize += buf.length;
8163
+ if (chunk != null && typeof chunk !== "function") {
8164
+ if (resSize < DEFAULT_MAX_BODY_CAPTURE) {
8165
+ const buf = toBuffer(chunk);
8166
+ if (buf) {
8167
+ resChunks.push(buf);
8168
+ resSize += buf.length;
8169
+ }
8170
+ } else {
8171
+ truncated = true;
8083
8172
  }
8084
8173
  }
8085
8174
  } catch (e) {
8086
- brakitDebug(`capture write: ${e.message}`);
8175
+ brakitDebug(`capture write: ${getErrorMessage(e)}`);
8087
8176
  }
8088
8177
  return originalWrite.apply(this, args);
8089
8178
  };
@@ -8097,7 +8186,7 @@ function captureInProcess(req, res, requestId, requestStore) {
8097
8186
  }
8098
8187
  }
8099
8188
  } catch (e) {
8100
- brakitDebug(`capture end: ${e.message}`);
8189
+ brakitDebug(`capture end: ${getErrorMessage(e)}`);
8101
8190
  }
8102
8191
  const result = originalEnd.apply(this, args);
8103
8192
  const endTime = performance.now();
@@ -8109,7 +8198,7 @@ function captureInProcess(req, res, requestId, requestStore) {
8109
8198
  void (async () => {
8110
8199
  try {
8111
8200
  let body = capturedChunks.length > 0 ? Buffer.concat(capturedChunks) : null;
8112
- if (body && encoding) {
8201
+ if (body && encoding && !truncated) {
8113
8202
  body = await decompressAsync(body, encoding);
8114
8203
  }
8115
8204
  requestStore.capture({
@@ -8127,7 +8216,7 @@ function captureInProcess(req, res, requestId, requestStore) {
8127
8216
  config: { maxBodyCapture: DEFAULT_MAX_BODY_CAPTURE }
8128
8217
  });
8129
8218
  } catch (e) {
8130
- brakitDebug(`capture store: ${e.message}`);
8219
+ brakitDebug(`capture store: ${getErrorMessage(e)}`);
8131
8220
  }
8132
8221
  })();
8133
8222
  return result;
@@ -8138,6 +8227,7 @@ var init_capture = __esm({
8138
8227
  "use strict";
8139
8228
  init_constants();
8140
8229
  init_log();
8230
+ init_type_guards();
8141
8231
  }
8142
8232
  });
8143
8233
 
@@ -8216,7 +8306,7 @@ __export(setup_exports, {
8216
8306
  setup: () => setup
8217
8307
  });
8218
8308
  import { readFile as readFile5, mkdir as mkdir2, writeFile as writeFile3 } from "fs/promises";
8219
- import { existsSync as existsSync7, unlinkSync as unlinkSync2 } from "fs";
8309
+ import { existsSync as existsSync7, unlinkSync as unlinkSync3 } from "fs";
8220
8310
  import { resolve as resolve4 } from "path";
8221
8311
  function setup() {
8222
8312
  if (initPromise) return initPromise;
@@ -8224,6 +8314,7 @@ function setup() {
8224
8314
  return initPromise;
8225
8315
  }
8226
8316
  async function doSetup() {
8317
+ brakitDebug(`[setup] doSetup called at ${(/* @__PURE__ */ new Date()).toISOString()}`);
8227
8318
  const bus = new EventBus();
8228
8319
  const registry = new ServiceRegistry();
8229
8320
  const requestStore = new RequestStore();
@@ -8254,7 +8345,9 @@ async function doSetup() {
8254
8345
  const cwd = process.cwd();
8255
8346
  let framework = "unknown";
8256
8347
  try {
8257
- const pkg = JSON.parse(await readFile5(resolve4(cwd, "package.json"), "utf-8"));
8348
+ const pkg = JSON.parse(
8349
+ await readFile5(resolve4(cwd, "package.json"), "utf-8")
8350
+ );
8258
8351
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
8259
8352
  framework = detectFrameworkFromDeps(allDeps);
8260
8353
  } catch {
@@ -8265,12 +8358,13 @@ async function doSetup() {
8265
8358
  false,
8266
8359
  adapterRegistry.getActive().map((a) => a.name)
8267
8360
  );
8268
- const metricsStore = new MetricsStore(new FileMetricsPersistence(cwd));
8361
+ const dataDir = getProjectDataDir(cwd);
8362
+ const metricsStore = new MetricsStore(new FileMetricsPersistence(dataDir));
8269
8363
  metricsStore.start();
8270
8364
  registry.register("metrics-store", metricsStore);
8271
- const findingStore = new FindingStore(cwd);
8272
- findingStore.start();
8273
- registry.register("finding-store", findingStore);
8365
+ const issueStore = new IssueStore(dataDir);
8366
+ issueStore.start();
8367
+ registry.register("issue-store", issueStore);
8274
8368
  const analysisEngine = new AnalysisEngine(registry);
8275
8369
  analysisEngine.start();
8276
8370
  registry.register("analysis-engine", analysisEngine);
@@ -8297,6 +8391,7 @@ async function doSetup() {
8297
8391
  requestStore,
8298
8392
  onFirstRequest(port) {
8299
8393
  setBrakitPort(port);
8394
+ brakitDebug(`[setup] onFirstRequest fired, port=${port}`);
8300
8395
  void (async () => {
8301
8396
  try {
8302
8397
  const dir = resolve4(cwd, METRICS_DIR);
@@ -8304,19 +8399,29 @@ async function doSetup() {
8304
8399
  const portPath = resolve4(cwd, PORT_FILE);
8305
8400
  try {
8306
8401
  const old = await readFile5(portPath, "utf-8");
8307
- if (old.trim() && old.trim() !== String(port)) {
8308
- brakitDebug(`Overwriting stale port file (was ${old.trim()}, now ${port})`);
8402
+ if (old.trim() === String(port)) {
8403
+ brakitDebug(`[setup] port file already correct, skipping write`);
8404
+ return;
8405
+ }
8406
+ if (old.trim()) {
8407
+ brakitDebug(
8408
+ `Overwriting stale port file (was ${old.trim()}, now ${port})`
8409
+ );
8309
8410
  }
8310
8411
  } catch {
8412
+ brakitDebug(`[setup] no existing port file, will create`);
8311
8413
  }
8312
8414
  await writeFile3(portPath, String(port));
8415
+ brakitDebug(`[setup] wrote port file: ${portPath}`);
8313
8416
  } catch (err) {
8314
- brakitDebug(`port file write failed: ${err.message}`);
8417
+ brakitDebug(`port file write failed: ${getErrorMessage(err)}`);
8315
8418
  }
8316
8419
  })();
8317
8420
  terminalDispose = startTerminalInsights(registry, port);
8318
- process.stdout.write(` brakit v${VERSION} \u2014 http://localhost:${port}${DASHBOARD_PREFIX}
8319
- `);
8421
+ process.stdout.write(
8422
+ ` brakit v${VERSION} \u2014 http://localhost:${port}${DASHBOARD_PREFIX}
8423
+ `
8424
+ );
8320
8425
  }
8321
8426
  });
8322
8427
  let telemetrySent = false;
@@ -8336,12 +8441,13 @@ async function doSetup() {
8336
8441
  uninstallInterceptor();
8337
8442
  terminalDispose?.();
8338
8443
  analysisEngine.stop();
8339
- findingStore.stop();
8444
+ issueStore.stop();
8340
8445
  metricsStore.stop();
8341
8446
  try {
8342
8447
  const portPath = resolve4(cwd, PORT_FILE);
8343
- if (existsSync7(portPath)) unlinkSync2(portPath);
8344
- } catch {
8448
+ if (existsSync7(portPath)) unlinkSync3(portPath);
8449
+ } catch (err) {
8450
+ brakitDebug(`[setup] port file cleanup failed: ${getErrorMessage(err)}`);
8345
8451
  }
8346
8452
  };
8347
8453
  health.setTeardown(runTeardown);
@@ -8369,7 +8475,7 @@ var init_setup = __esm({
8369
8475
  init_error_store();
8370
8476
  init_query_store();
8371
8477
  init_store();
8372
- init_finding_store();
8478
+ init_issue_store();
8373
8479
  init_engine();
8374
8480
  init_terminal();
8375
8481
  init_src();
@@ -8377,6 +8483,8 @@ var init_setup = __esm({
8377
8483
  init_health2();
8378
8484
  init_interceptor();
8379
8485
  init_log();
8486
+ init_type_guards();
8487
+ init_fs();
8380
8488
  init_project();
8381
8489
  init_telemetry2();
8382
8490
  initPromise = null;