brakit 0.7.3 → 0.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/api.js CHANGED
@@ -98,6 +98,8 @@ var MASKED_RE = /^\*+$|\[REDACTED\]|\[FILTERED\]|CHANGE_ME|^x{3,}$/i;
98
98
  var EMAIL_RE = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/;
99
99
  var INTERNAL_ID_KEYS = /^(id|_id|userId|user_id|createdBy|updatedBy|organizationId|org_id|tenantId|tenant_id)$/;
100
100
  var INTERNAL_ID_SUFFIX = /Id$|_id$/;
101
+ var SELECT_STAR_RE = /^SELECT\s+\*/i;
102
+ var SELECT_DOT_STAR_RE = /\.\*\s+FROM/i;
101
103
  var RULE_HINTS = {
102
104
  "exposed-secret": "Never include secret fields in API responses. Strip sensitive fields before returning.",
103
105
  "token-in-url": "Pass tokens in the Authorization header, not URL query parameters.",
@@ -618,6 +620,13 @@ var OVERFETCH_MIN_REQUESTS = 2;
618
620
  var OVERFETCH_MIN_FIELDS = 8;
619
621
  var OVERFETCH_MIN_INTERNAL_IDS = 2;
620
622
  var OVERFETCH_NULL_RATIO = 0.3;
623
+ var REGRESSION_PCT_THRESHOLD = 50;
624
+ var REGRESSION_MIN_INCREASE_MS = 200;
625
+ var REGRESSION_MIN_REQUESTS = 5;
626
+ var QUERY_COUNT_REGRESSION_RATIO = 1.5;
627
+ var OVERFETCH_MANY_FIELDS = 12;
628
+ var OVERFETCH_UNWRAP_MIN_SIZE = 3;
629
+ var MAX_DUPLICATE_INSIGHTS = 3;
621
630
 
622
631
  // src/utils/static-patterns.ts
623
632
  var STATIC_PATTERNS = [
@@ -670,7 +679,7 @@ var RequestStore = class {
670
679
  responseHeaders: flattenHeaders(input.responseHeaders),
671
680
  responseBody: responseBodyStr,
672
681
  startedAt: input.startTime,
673
- durationMs: Math.round(performance.now() - input.startTime),
682
+ durationMs: Math.round((input.endTime ?? performance.now()) - input.startTime),
674
683
  responseSize: input.responseBody?.length ?? 0,
675
684
  isStatic: isStaticPath(path)
676
685
  };
@@ -760,14 +769,21 @@ var defaultQueryStore = new QueryStore();
760
769
  // src/store/metrics/metrics-store.ts
761
770
  import { randomUUID as randomUUID2 } from "crypto";
762
771
 
772
+ // src/utils/endpoint.ts
773
+ function getEndpointKey(method, path) {
774
+ return `${method} ${path}`;
775
+ }
776
+
763
777
  // src/store/metrics/persistence.ts
764
778
  import {
765
779
  readFileSync as readFileSync2,
766
780
  writeFileSync as writeFileSync2,
767
781
  mkdirSync as mkdirSync2,
768
782
  existsSync as existsSync2,
769
- unlinkSync
783
+ unlinkSync,
784
+ renameSync
770
785
  } from "fs";
786
+ import { writeFile as writeFile2, mkdir, rename } from "fs/promises";
771
787
  import { resolve as resolve2 } from "path";
772
788
 
773
789
  // src/analysis/group.ts
@@ -1123,6 +1139,22 @@ function deriveFlowLabel(requests, sourcePage) {
1123
1139
  return trigger.label;
1124
1140
  }
1125
1141
 
1142
+ // src/utils/collections.ts
1143
+ function groupBy(items, keyFn) {
1144
+ const map = /* @__PURE__ */ new Map();
1145
+ for (const item of items) {
1146
+ const key = keyFn(item);
1147
+ if (key == null) continue;
1148
+ let arr = map.get(key);
1149
+ if (!arr) {
1150
+ arr = [];
1151
+ map.set(key, arr);
1152
+ }
1153
+ arr.push(item);
1154
+ }
1155
+ return map;
1156
+ }
1157
+
1126
1158
  // src/instrument/adapters/normalize.ts
1127
1159
  function normalizeSQL(sql) {
1128
1160
  if (!sql) return { op: "OTHER", table: "" };
@@ -1155,7 +1187,7 @@ function normalizeQueryParams(sql) {
1155
1187
  return n;
1156
1188
  }
1157
1189
 
1158
- // src/analysis/insights.ts
1190
+ // src/analysis/insights/query-helpers.ts
1159
1191
  function getQueryShape(q) {
1160
1192
  if (q.sql) return normalizeQueryParams(q.sql) ?? "";
1161
1193
  return `${q.operation ?? q.normalizedOp ?? "?"}:${q.model ?? q.table ?? ""}`;
@@ -1167,143 +1199,234 @@ function getQueryInfo(q) {
1167
1199
  table: q.table ?? q.model ?? ""
1168
1200
  };
1169
1201
  }
1170
- function formatDuration(ms) {
1171
- if (ms < 1e3) return `${ms}ms`;
1172
- return `${(ms / 1e3).toFixed(1)}s`;
1173
- }
1174
- function formatSize(bytes) {
1175
- if (bytes < 1024) return `${bytes}B`;
1176
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
1177
- return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
1202
+
1203
+ // src/analysis/insights/prepare.ts
1204
+ function createEndpointGroup() {
1205
+ return {
1206
+ total: 0,
1207
+ errors: 0,
1208
+ totalDuration: 0,
1209
+ queryCount: 0,
1210
+ totalSize: 0,
1211
+ totalQueryTimeMs: 0,
1212
+ totalFetchTimeMs: 0,
1213
+ queryShapeDurations: /* @__PURE__ */ new Map()
1214
+ };
1178
1215
  }
1179
- function computeInsights(ctx) {
1180
- const insights = [];
1216
+ function prepareContext(ctx) {
1181
1217
  const nonStatic = ctx.requests.filter(
1182
1218
  (r) => !r.isStatic && (!r.path || !r.path.startsWith(DASHBOARD_PREFIX))
1183
1219
  );
1184
- const queriesByReq = /* @__PURE__ */ new Map();
1185
- for (const q of ctx.queries) {
1186
- if (!q.parentRequestId) continue;
1187
- let arr = queriesByReq.get(q.parentRequestId);
1188
- if (!arr) {
1189
- arr = [];
1190
- queriesByReq.set(q.parentRequestId, arr);
1220
+ const queriesByReq = groupBy(ctx.queries, (q) => q.parentRequestId);
1221
+ const fetchesByReq = groupBy(ctx.fetches, (f) => f.parentRequestId);
1222
+ const reqById = new Map(nonStatic.map((r) => [r.id, r]));
1223
+ const endpointGroups = /* @__PURE__ */ new Map();
1224
+ for (const r of nonStatic) {
1225
+ const ep = getEndpointKey(r.method, r.path);
1226
+ let g = endpointGroups.get(ep);
1227
+ if (!g) {
1228
+ g = createEndpointGroup();
1229
+ endpointGroups.set(ep, g);
1191
1230
  }
1192
- arr.push(q);
1193
- }
1194
- const reqById = /* @__PURE__ */ new Map();
1195
- for (const r of nonStatic) reqById.set(r.id, r);
1196
- const n1Seen = /* @__PURE__ */ new Set();
1197
- for (const [reqId, reqQueries] of queriesByReq) {
1198
- const req = reqById.get(reqId);
1199
- if (!req) continue;
1200
- const endpoint = `${req.method} ${req.path}`;
1201
- const shapeGroups = /* @__PURE__ */ new Map();
1231
+ g.total++;
1232
+ if (r.statusCode >= 400) g.errors++;
1233
+ g.totalDuration += r.durationMs;
1234
+ g.totalSize += r.responseSize ?? 0;
1235
+ const reqQueries = queriesByReq.get(r.id) ?? [];
1236
+ g.queryCount += reqQueries.length;
1202
1237
  for (const q of reqQueries) {
1238
+ g.totalQueryTimeMs += q.durationMs;
1203
1239
  const shape = getQueryShape(q);
1204
- let group = shapeGroups.get(shape);
1205
- if (!group) {
1206
- group = { count: 0, distinctSql: /* @__PURE__ */ new Set(), first: q };
1207
- shapeGroups.set(shape, group);
1240
+ const info = getQueryInfo(q);
1241
+ let sd = g.queryShapeDurations.get(shape);
1242
+ if (!sd) {
1243
+ sd = { totalMs: 0, count: 0, label: info.op + (info.table ? ` ${info.table}` : "") };
1244
+ g.queryShapeDurations.set(shape, sd);
1208
1245
  }
1209
- group.count++;
1210
- group.distinctSql.add(q.sql ?? shape);
1246
+ sd.totalMs += q.durationMs;
1247
+ sd.count++;
1211
1248
  }
1212
- for (const [, sg] of shapeGroups) {
1213
- if (sg.count <= N1_QUERY_THRESHOLD || sg.distinctSql.size <= 1) continue;
1214
- const info = getQueryInfo(sg.first);
1215
- const key = `${endpoint}:${info.op}:${info.table || "unknown"}`;
1216
- if (n1Seen.has(key)) continue;
1217
- n1Seen.add(key);
1218
- insights.push({
1219
- severity: "critical",
1220
- type: "n1",
1221
- title: "N+1 Query Pattern",
1222
- desc: `${endpoint} runs ${sg.count}x ${info.op} ${info.table} with different params in a single request`,
1223
- hint: "This typically happens when fetching related data in a loop. Use a batch query, JOIN, or include/eager-load to fetch all records at once.",
1224
- nav: "queries"
1225
- });
1249
+ const reqFetches = fetchesByReq.get(r.id) ?? [];
1250
+ for (const f of reqFetches) {
1251
+ g.totalFetchTimeMs += f.durationMs;
1226
1252
  }
1227
1253
  }
1228
- const ceQueryMap = /* @__PURE__ */ new Map();
1229
- const ceAllEndpoints = /* @__PURE__ */ new Set();
1230
- for (const [reqId, reqQueries] of queriesByReq) {
1231
- const req = reqById.get(reqId);
1232
- if (!req) continue;
1233
- const endpoint = `${req.method} ${req.path}`;
1234
- ceAllEndpoints.add(endpoint);
1235
- const seenInReq = /* @__PURE__ */ new Set();
1236
- for (const q of reqQueries) {
1237
- const shape = getQueryShape(q);
1238
- let entry = ceQueryMap.get(shape);
1239
- if (!entry) {
1240
- entry = { endpoints: /* @__PURE__ */ new Set(), count: 0, first: q };
1241
- ceQueryMap.set(shape, entry);
1242
- }
1243
- entry.count++;
1244
- if (!seenInReq.has(shape)) {
1245
- seenInReq.add(shape);
1246
- entry.endpoints.add(endpoint);
1254
+ return {
1255
+ ...ctx,
1256
+ nonStatic,
1257
+ queriesByReq,
1258
+ fetchesByReq,
1259
+ reqById,
1260
+ endpointGroups
1261
+ };
1262
+ }
1263
+
1264
+ // src/analysis/insights/runner.ts
1265
+ var SEVERITY_ORDER = { critical: 0, warning: 1, info: 2 };
1266
+ var InsightRunner = class {
1267
+ rules = [];
1268
+ register(rule) {
1269
+ this.rules.push(rule);
1270
+ }
1271
+ run(ctx) {
1272
+ const prepared = prepareContext(ctx);
1273
+ const insights = [];
1274
+ for (const rule of this.rules) {
1275
+ try {
1276
+ insights.push(...rule.check(prepared));
1277
+ } catch {
1247
1278
  }
1248
1279
  }
1280
+ insights.sort(
1281
+ (a, b) => (SEVERITY_ORDER[a.severity] ?? 2) - (SEVERITY_ORDER[b.severity] ?? 2)
1282
+ );
1283
+ return insights;
1249
1284
  }
1250
- if (ceAllEndpoints.size >= CROSS_ENDPOINT_MIN_ENDPOINTS) {
1251
- for (const [, cem] of ceQueryMap) {
1252
- if (cem.count < CROSS_ENDPOINT_MIN_OCCURRENCES) continue;
1253
- if (cem.endpoints.size < CROSS_ENDPOINT_MIN_ENDPOINTS) continue;
1254
- const pct = Math.round(cem.endpoints.size / ceAllEndpoints.size * 100);
1255
- if (pct < CROSS_ENDPOINT_PCT) continue;
1256
- const info = getQueryInfo(cem.first);
1257
- const label = info.op + (info.table ? ` ${info.table}` : "");
1258
- insights.push({
1259
- severity: "warning",
1260
- type: "cross-endpoint",
1261
- title: "Repeated Query Across Endpoints",
1262
- desc: `${label} runs on ${cem.endpoints.size} of ${ceAllEndpoints.size} endpoints (${pct}%).`,
1263
- hint: "This query runs on most of your endpoints. Load it once in middleware or cache the result to avoid redundant database calls.",
1264
- nav: "queries"
1265
- });
1285
+ };
1286
+
1287
+ // src/analysis/insights/rules/n1.ts
1288
+ var n1Rule = {
1289
+ id: "n1",
1290
+ check(ctx) {
1291
+ const insights = [];
1292
+ const seen = /* @__PURE__ */ new Set();
1293
+ for (const [reqId, reqQueries] of ctx.queriesByReq) {
1294
+ const req = ctx.reqById.get(reqId);
1295
+ if (!req) continue;
1296
+ const endpoint = getEndpointKey(req.method, req.path);
1297
+ const shapeGroups = /* @__PURE__ */ new Map();
1298
+ for (const q of reqQueries) {
1299
+ const shape = getQueryShape(q);
1300
+ let group = shapeGroups.get(shape);
1301
+ if (!group) {
1302
+ group = { count: 0, distinctSql: /* @__PURE__ */ new Set(), first: q };
1303
+ shapeGroups.set(shape, group);
1304
+ }
1305
+ group.count++;
1306
+ group.distinctSql.add(q.sql ?? shape);
1307
+ }
1308
+ for (const [, sg] of shapeGroups) {
1309
+ if (sg.count <= N1_QUERY_THRESHOLD || sg.distinctSql.size <= 1) continue;
1310
+ const info = getQueryInfo(sg.first);
1311
+ const key = `${endpoint}:${info.op}:${info.table || "unknown"}`;
1312
+ if (seen.has(key)) continue;
1313
+ seen.add(key);
1314
+ insights.push({
1315
+ severity: "critical",
1316
+ type: "n1",
1317
+ title: "N+1 Query Pattern",
1318
+ desc: `${endpoint} runs ${sg.count}x ${info.op} ${info.table} with different params in a single request`,
1319
+ hint: "This typically happens when fetching related data in a loop. Use a batch query, JOIN, or include/eager-load to fetch all records at once.",
1320
+ nav: "queries"
1321
+ });
1322
+ }
1266
1323
  }
1324
+ return insights;
1267
1325
  }
1268
- const rqSeen = /* @__PURE__ */ new Set();
1269
- for (const [reqId, reqQueries] of queriesByReq) {
1270
- const req = reqById.get(reqId);
1271
- if (!req) continue;
1272
- const endpoint = `${req.method} ${req.path}`;
1273
- const exact = /* @__PURE__ */ new Map();
1274
- for (const q of reqQueries) {
1275
- if (!q.sql) continue;
1276
- let entry = exact.get(q.sql);
1277
- if (!entry) {
1278
- entry = { count: 0, first: q };
1279
- exact.set(q.sql, entry);
1326
+ };
1327
+
1328
+ // src/analysis/insights/rules/cross-endpoint.ts
1329
+ var crossEndpointRule = {
1330
+ id: "cross-endpoint",
1331
+ check(ctx) {
1332
+ const insights = [];
1333
+ const queryMap = /* @__PURE__ */ new Map();
1334
+ const allEndpoints = /* @__PURE__ */ new Set();
1335
+ for (const [reqId, reqQueries] of ctx.queriesByReq) {
1336
+ const req = ctx.reqById.get(reqId);
1337
+ if (!req) continue;
1338
+ const endpoint = getEndpointKey(req.method, req.path);
1339
+ allEndpoints.add(endpoint);
1340
+ const seenInReq = /* @__PURE__ */ new Set();
1341
+ for (const q of reqQueries) {
1342
+ const shape = getQueryShape(q);
1343
+ let entry = queryMap.get(shape);
1344
+ if (!entry) {
1345
+ entry = { endpoints: /* @__PURE__ */ new Set(), count: 0, first: q };
1346
+ queryMap.set(shape, entry);
1347
+ }
1348
+ entry.count++;
1349
+ if (!seenInReq.has(shape)) {
1350
+ seenInReq.add(shape);
1351
+ entry.endpoints.add(endpoint);
1352
+ }
1280
1353
  }
1281
- entry.count++;
1282
1354
  }
1283
- for (const [, e] of exact) {
1284
- if (e.count < REDUNDANT_QUERY_MIN_COUNT) continue;
1285
- const info = getQueryInfo(e.first);
1286
- const label = info.op + (info.table ? ` ${info.table}` : "");
1287
- const dedupKey = `${endpoint}:${label}`;
1288
- if (rqSeen.has(dedupKey)) continue;
1289
- rqSeen.add(dedupKey);
1290
- insights.push({
1291
- severity: "warning",
1292
- type: "redundant-query",
1293
- title: "Redundant Query",
1294
- desc: `${label} runs ${e.count}x with identical params in ${endpoint}.`,
1295
- hint: "The exact same query with identical parameters runs multiple times in one request. Cache the first result or lift the query to a shared function.",
1296
- nav: "queries"
1297
- });
1355
+ if (allEndpoints.size >= CROSS_ENDPOINT_MIN_ENDPOINTS) {
1356
+ for (const [, cem] of queryMap) {
1357
+ if (cem.count < CROSS_ENDPOINT_MIN_OCCURRENCES) continue;
1358
+ if (cem.endpoints.size < CROSS_ENDPOINT_MIN_ENDPOINTS) continue;
1359
+ const p = Math.round(cem.endpoints.size / allEndpoints.size * 100);
1360
+ if (p < CROSS_ENDPOINT_PCT) continue;
1361
+ const info = getQueryInfo(cem.first);
1362
+ const label = info.op + (info.table ? ` ${info.table}` : "");
1363
+ insights.push({
1364
+ severity: "warning",
1365
+ type: "cross-endpoint",
1366
+ title: "Repeated Query Across Endpoints",
1367
+ desc: `${label} runs on ${cem.endpoints.size} of ${allEndpoints.size} endpoints (${p}%).`,
1368
+ hint: "This query runs on most of your endpoints. Load it once in middleware or cache the result to avoid redundant database calls.",
1369
+ nav: "queries"
1370
+ });
1371
+ }
1298
1372
  }
1373
+ return insights;
1299
1374
  }
1300
- if (ctx.errors.length > 0) {
1301
- const errGroups = /* @__PURE__ */ new Map();
1375
+ };
1376
+
1377
+ // src/analysis/insights/rules/redundant-query.ts
1378
+ var redundantQueryRule = {
1379
+ id: "redundant-query",
1380
+ check(ctx) {
1381
+ const insights = [];
1382
+ const seen = /* @__PURE__ */ new Set();
1383
+ for (const [reqId, reqQueries] of ctx.queriesByReq) {
1384
+ const req = ctx.reqById.get(reqId);
1385
+ if (!req) continue;
1386
+ const endpoint = getEndpointKey(req.method, req.path);
1387
+ const exact = /* @__PURE__ */ new Map();
1388
+ for (const q of reqQueries) {
1389
+ if (!q.sql) continue;
1390
+ let entry = exact.get(q.sql);
1391
+ if (!entry) {
1392
+ entry = { count: 0, first: q };
1393
+ exact.set(q.sql, entry);
1394
+ }
1395
+ entry.count++;
1396
+ }
1397
+ for (const [, e] of exact) {
1398
+ if (e.count < REDUNDANT_QUERY_MIN_COUNT) continue;
1399
+ const info = getQueryInfo(e.first);
1400
+ const label = info.op + (info.table ? ` ${info.table}` : "");
1401
+ const dedupKey = `${endpoint}:${label}`;
1402
+ if (seen.has(dedupKey)) continue;
1403
+ seen.add(dedupKey);
1404
+ insights.push({
1405
+ severity: "warning",
1406
+ type: "redundant-query",
1407
+ title: "Redundant Query",
1408
+ desc: `${label} runs ${e.count}x with identical params in ${endpoint}.`,
1409
+ hint: "The exact same query with identical parameters runs multiple times in one request. Cache the first result or lift the query to a shared function.",
1410
+ nav: "queries"
1411
+ });
1412
+ }
1413
+ }
1414
+ return insights;
1415
+ }
1416
+ };
1417
+
1418
+ // src/analysis/insights/rules/error.ts
1419
+ var errorRule = {
1420
+ id: "error",
1421
+ check(ctx) {
1422
+ if (ctx.errors.length === 0) return [];
1423
+ const insights = [];
1424
+ const groups = /* @__PURE__ */ new Map();
1302
1425
  for (const e of ctx.errors) {
1303
1426
  const name = e.name || "Error";
1304
- errGroups.set(name, (errGroups.get(name) ?? 0) + 1);
1427
+ groups.set(name, (groups.get(name) ?? 0) + 1);
1305
1428
  }
1306
- for (const [name, cnt] of errGroups) {
1429
+ for (const [name, cnt] of groups) {
1307
1430
  insights.push({
1308
1431
  severity: "critical",
1309
1432
  type: "error",
@@ -1313,235 +1436,391 @@ function computeInsights(ctx) {
1313
1436
  nav: "errors"
1314
1437
  });
1315
1438
  }
1439
+ return insights;
1316
1440
  }
1317
- const endpointGroups = /* @__PURE__ */ new Map();
1318
- for (const r of nonStatic) {
1319
- const ep = `${r.method} ${r.path}`;
1320
- let g = endpointGroups.get(ep);
1321
- if (!g) {
1322
- g = { total: 0, errors: 0, totalDuration: 0, queryCount: 0, totalSize: 0 };
1323
- endpointGroups.set(ep, g);
1441
+ };
1442
+
1443
+ // src/analysis/insights/rules/error-hotspot.ts
1444
+ var errorHotspotRule = {
1445
+ id: "error-hotspot",
1446
+ check(ctx) {
1447
+ const insights = [];
1448
+ for (const [ep, g] of ctx.endpointGroups) {
1449
+ if (g.total < MIN_REQUESTS_FOR_INSIGHT) continue;
1450
+ const errorRate = Math.round(g.errors / g.total * 100);
1451
+ if (errorRate >= ERROR_RATE_THRESHOLD_PCT) {
1452
+ insights.push({
1453
+ severity: "critical",
1454
+ type: "error-hotspot",
1455
+ title: "Error Hotspot",
1456
+ desc: `${ep} \u2014 ${errorRate}% error rate (${g.errors}/${g.total} requests)`,
1457
+ hint: "This endpoint frequently returns errors. Check the response bodies for error details and stack traces.",
1458
+ nav: "requests"
1459
+ });
1460
+ }
1324
1461
  }
1325
- g.total++;
1326
- if (r.statusCode >= 400) g.errors++;
1327
- g.totalDuration += r.durationMs;
1328
- g.queryCount += (queriesByReq.get(r.id) ?? []).length;
1329
- g.totalSize += r.responseSize ?? 0;
1462
+ return insights;
1330
1463
  }
1331
- for (const [ep, g] of endpointGroups) {
1332
- if (g.total < MIN_REQUESTS_FOR_INSIGHT) continue;
1333
- const errorRate = Math.round(g.errors / g.total * 100);
1334
- if (errorRate >= ERROR_RATE_THRESHOLD_PCT) {
1464
+ };
1465
+
1466
+ // src/analysis/insights/rules/duplicate.ts
1467
+ var duplicateRule = {
1468
+ id: "duplicate",
1469
+ check(ctx) {
1470
+ const dupCounts = /* @__PURE__ */ new Map();
1471
+ const flowCount = /* @__PURE__ */ new Map();
1472
+ for (const flow of ctx.flows) {
1473
+ if (!flow.requests) continue;
1474
+ const seenInFlow = /* @__PURE__ */ new Set();
1475
+ for (const fr of flow.requests) {
1476
+ if (!fr.isDuplicate) continue;
1477
+ const dupKey = `${fr.method} ${fr.label ?? fr.path ?? fr.url}`;
1478
+ dupCounts.set(dupKey, (dupCounts.get(dupKey) ?? 0) + 1);
1479
+ if (!seenInFlow.has(dupKey)) {
1480
+ seenInFlow.add(dupKey);
1481
+ flowCount.set(dupKey, (flowCount.get(dupKey) ?? 0) + 1);
1482
+ }
1483
+ }
1484
+ }
1485
+ const dupEntries = [...dupCounts.entries()].map(([key, count]) => ({ key, count, flows: flowCount.get(key) ?? 0 })).sort((a, b) => b.count - a.count);
1486
+ const insights = [];
1487
+ for (let i = 0; i < Math.min(dupEntries.length, MAX_DUPLICATE_INSIGHTS); i++) {
1488
+ const d = dupEntries[i];
1335
1489
  insights.push({
1336
- severity: "critical",
1337
- type: "error-hotspot",
1338
- title: "Error Hotspot",
1339
- desc: `${ep} \u2014 ${errorRate}% error rate (${g.errors}/${g.total} requests)`,
1340
- hint: "This endpoint frequently returns errors. Check the response bodies for error details and stack traces.",
1341
- nav: "requests"
1490
+ severity: "warning",
1491
+ type: "duplicate",
1492
+ title: "Duplicate API Call",
1493
+ desc: `${d.key} loaded ${d.count}x as duplicate across ${d.flows} action${d.flows !== 1 ? "s" : ""}`,
1494
+ hint: "Multiple components independently fetch the same endpoint. Lift the fetch to a parent component, use a data cache, or deduplicate with React Query / SWR.",
1495
+ nav: "actions"
1342
1496
  });
1343
1497
  }
1498
+ return insights;
1344
1499
  }
1345
- const dupCounts = /* @__PURE__ */ new Map();
1346
- const flowCount = /* @__PURE__ */ new Map();
1347
- for (const flow of ctx.flows) {
1348
- if (!flow.requests) continue;
1349
- const seenInFlow = /* @__PURE__ */ new Set();
1350
- for (const fr of flow.requests) {
1351
- if (!fr.isDuplicate) continue;
1352
- const dupKey = `${fr.method} ${fr.label ?? fr.path ?? fr.url}`;
1353
- dupCounts.set(dupKey, (dupCounts.get(dupKey) ?? 0) + 1);
1354
- if (!seenInFlow.has(dupKey)) {
1355
- seenInFlow.add(dupKey);
1356
- flowCount.set(dupKey, (flowCount.get(dupKey) ?? 0) + 1);
1500
+ };
1501
+
1502
+ // src/utils/format.ts
1503
+ function formatDuration(ms) {
1504
+ if (ms < 1e3) return `${ms}ms`;
1505
+ return `${(ms / 1e3).toFixed(1)}s`;
1506
+ }
1507
+ function formatSize(bytes) {
1508
+ if (bytes < 1024) return `${bytes}B`;
1509
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
1510
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
1511
+ }
1512
+ function pct(part, total) {
1513
+ return total > 0 ? Math.round(part / total * 100) : 0;
1514
+ }
1515
+
1516
+ // src/analysis/insights/rules/slow.ts
1517
+ var slowRule = {
1518
+ id: "slow",
1519
+ check(ctx) {
1520
+ const insights = [];
1521
+ for (const [ep, g] of ctx.endpointGroups) {
1522
+ if (g.total < MIN_REQUESTS_FOR_INSIGHT) continue;
1523
+ const avgMs = Math.round(g.totalDuration / g.total);
1524
+ if (avgMs < SLOW_ENDPOINT_THRESHOLD_MS) continue;
1525
+ const avgQueryMs = Math.round(g.totalQueryTimeMs / g.total);
1526
+ const avgFetchMs = Math.round(g.totalFetchTimeMs / g.total);
1527
+ const avgAppMs = Math.max(0, avgMs - avgQueryMs - avgFetchMs);
1528
+ const parts = [];
1529
+ if (avgQueryMs > 0) parts.push(`DB ${formatDuration(avgQueryMs)} ${pct(avgQueryMs, avgMs)}%`);
1530
+ if (avgFetchMs > 0) parts.push(`Fetch ${formatDuration(avgFetchMs)} ${pct(avgFetchMs, avgMs)}%`);
1531
+ if (avgAppMs > 0) parts.push(`App ${formatDuration(avgAppMs)} ${pct(avgAppMs, avgMs)}%`);
1532
+ const breakdown = parts.length > 0 ? ` [${parts.join(" \xB7 ")}]` : "";
1533
+ let detail;
1534
+ let slowestMs = 0;
1535
+ for (const [, sd] of g.queryShapeDurations) {
1536
+ const avgShapeMs = sd.totalMs / sd.count;
1537
+ if (avgShapeMs > slowestMs) {
1538
+ slowestMs = avgShapeMs;
1539
+ detail = `Slowest query: ${sd.label} \u2014 avg ${formatDuration(Math.round(avgShapeMs))} (${sd.count}x)`;
1540
+ }
1357
1541
  }
1358
- }
1359
- }
1360
- const dupEntries = [...dupCounts.entries()].map(([key, count]) => ({ key, count, flows: flowCount.get(key) ?? 0 })).sort((a, b) => b.count - a.count);
1361
- for (let i = 0; i < Math.min(dupEntries.length, 3); i++) {
1362
- const d = dupEntries[i];
1363
- insights.push({
1364
- severity: "warning",
1365
- type: "duplicate",
1366
- title: "Duplicate API Call",
1367
- desc: `${d.key} loaded ${d.count}x as duplicate across ${d.flows} action${d.flows !== 1 ? "s" : ""}`,
1368
- hint: "Multiple components independently fetch the same endpoint. Lift the fetch to a parent component, use a data cache, or deduplicate with React Query / SWR.",
1369
- nav: "actions"
1370
- });
1371
- }
1372
- for (const [ep, g] of endpointGroups) {
1373
- if (g.total < MIN_REQUESTS_FOR_INSIGHT) continue;
1374
- const avgMs = Math.round(g.totalDuration / g.total);
1375
- if (avgMs >= SLOW_ENDPOINT_THRESHOLD_MS) {
1376
1542
  insights.push({
1377
1543
  severity: "warning",
1378
1544
  type: "slow",
1379
1545
  title: "Slow Endpoint",
1380
- desc: `${ep} \u2014 avg ${formatDuration(avgMs)} across ${g.total} request${g.total !== 1 ? "s" : ""}`,
1381
- hint: "Consistently slow responses hurt user experience. Check the Queries tab to see if database queries are the bottleneck.",
1546
+ desc: `${ep} \u2014 avg ${formatDuration(avgMs)}${breakdown}`,
1547
+ hint: avgQueryMs >= avgFetchMs && avgQueryMs >= avgAppMs ? "Most time is in database queries. Check the Queries tab for slow or redundant queries." : avgFetchMs >= avgQueryMs && avgFetchMs >= avgAppMs ? "Most time is in outbound HTTP calls. Check if upstream services are slow or if calls can be parallelized." : "Most time is in application code. Profile the handler for CPU-heavy operations or blocking calls.",
1548
+ detail,
1382
1549
  nav: "requests"
1383
1550
  });
1384
1551
  }
1552
+ return insights;
1385
1553
  }
1386
- for (const [ep, g] of endpointGroups) {
1387
- if (g.total < MIN_REQUESTS_FOR_INSIGHT) continue;
1388
- const avgQueries = Math.round(g.queryCount / g.total);
1389
- if (avgQueries > HIGH_QUERY_COUNT_PER_REQ) {
1554
+ };
1555
+
1556
+ // src/analysis/insights/rules/query-heavy.ts
1557
+ var queryHeavyRule = {
1558
+ id: "query-heavy",
1559
+ check(ctx) {
1560
+ const insights = [];
1561
+ for (const [ep, g] of ctx.endpointGroups) {
1562
+ if (g.total < MIN_REQUESTS_FOR_INSIGHT) continue;
1563
+ const avgQueries = Math.round(g.queryCount / g.total);
1564
+ if (avgQueries > HIGH_QUERY_COUNT_PER_REQ) {
1565
+ insights.push({
1566
+ severity: "warning",
1567
+ type: "query-heavy",
1568
+ title: "Query-Heavy Endpoint",
1569
+ desc: `${ep} \u2014 avg ${avgQueries} queries/request`,
1570
+ hint: "Too many queries per request increases latency. Combine queries with JOINs, use batch operations, or reduce the number of data fetches.",
1571
+ nav: "queries"
1572
+ });
1573
+ }
1574
+ }
1575
+ return insights;
1576
+ }
1577
+ };
1578
+
1579
+ // src/analysis/insights/rules/select-star.ts
1580
+ var selectStarRule = {
1581
+ id: "select-star",
1582
+ check(ctx) {
1583
+ const seen = /* @__PURE__ */ new Map();
1584
+ for (const [, reqQueries] of ctx.queriesByReq) {
1585
+ for (const q of reqQueries) {
1586
+ if (!q.sql) continue;
1587
+ const isSelectStar = SELECT_STAR_RE.test(q.sql.trim()) || SELECT_DOT_STAR_RE.test(q.sql);
1588
+ if (!isSelectStar) continue;
1589
+ const info = getQueryInfo(q);
1590
+ const key = info.table || "unknown";
1591
+ seen.set(key, (seen.get(key) ?? 0) + 1);
1592
+ }
1593
+ }
1594
+ const insights = [];
1595
+ for (const [table, count] of seen) {
1596
+ if (count < OVERFETCH_MIN_REQUESTS) continue;
1390
1597
  insights.push({
1391
1598
  severity: "warning",
1392
- type: "query-heavy",
1393
- title: "Query-Heavy Endpoint",
1394
- desc: `${ep} \u2014 avg ${avgQueries} queries/request`,
1395
- hint: "Too many queries per request increases latency. Combine queries with JOINs, use batch operations, or reduce the number of data fetches.",
1599
+ type: "select-star",
1600
+ title: "SELECT * Query",
1601
+ desc: `SELECT * on ${table} \u2014 ${count} occurrence${count !== 1 ? "s" : ""}`,
1602
+ hint: "SELECT * fetches all columns including ones you don\u2019t need. Specify only required columns to reduce data transfer and memory usage.",
1396
1603
  nav: "queries"
1397
1604
  });
1398
1605
  }
1606
+ return insights;
1399
1607
  }
1400
- const selectStarSeen = /* @__PURE__ */ new Map();
1401
- for (const [, reqQueries] of queriesByReq) {
1402
- for (const q of reqQueries) {
1403
- if (!q.sql) continue;
1404
- const isSelectStar = /^SELECT\s+\*/i.test(q.sql.trim()) || /\.\*\s+FROM/i.test(q.sql);
1405
- if (!isSelectStar) continue;
1406
- const info = getQueryInfo(q);
1407
- const key = info.table || "unknown";
1408
- selectStarSeen.set(key, (selectStarSeen.get(key) ?? 0) + 1);
1608
+ };
1609
+
1610
+ // src/analysis/insights/rules/high-rows.ts
1611
+ var highRowsRule = {
1612
+ id: "high-rows",
1613
+ check(ctx) {
1614
+ const seen = /* @__PURE__ */ new Map();
1615
+ for (const [, reqQueries] of ctx.queriesByReq) {
1616
+ for (const q of reqQueries) {
1617
+ if (!q.rowCount || q.rowCount <= HIGH_ROW_COUNT) continue;
1618
+ const info = getQueryInfo(q);
1619
+ const key = `${info.op} ${info.table || "unknown"}`;
1620
+ let entry = seen.get(key);
1621
+ if (!entry) {
1622
+ entry = { max: 0, count: 0 };
1623
+ seen.set(key, entry);
1624
+ }
1625
+ entry.count++;
1626
+ if (q.rowCount > entry.max) entry.max = q.rowCount;
1627
+ }
1409
1628
  }
1629
+ const insights = [];
1630
+ for (const [key, hrs] of seen) {
1631
+ if (hrs.count < OVERFETCH_MIN_REQUESTS) continue;
1632
+ insights.push({
1633
+ severity: "warning",
1634
+ type: "high-rows",
1635
+ title: "Large Result Set",
1636
+ desc: `${key} returns ${hrs.max}+ rows (${hrs.count}x)`,
1637
+ hint: "Fetching many rows slows responses and wastes memory. Add a LIMIT clause, implement pagination, or filter with a WHERE condition.",
1638
+ nav: "queries"
1639
+ });
1640
+ }
1641
+ return insights;
1410
1642
  }
1411
- for (const [table, count] of selectStarSeen) {
1412
- if (count < OVERFETCH_MIN_REQUESTS) continue;
1413
- insights.push({
1414
- severity: "warning",
1415
- type: "select-star",
1416
- title: "SELECT * Query",
1417
- desc: `SELECT * on ${table} \u2014 ${count} occurrence${count !== 1 ? "s" : ""}`,
1418
- hint: "SELECT * fetches all columns including ones you don\u2019t need. Specify only required columns to reduce data transfer and memory usage.",
1419
- nav: "queries"
1420
- });
1421
- }
1422
- const highRowSeen = /* @__PURE__ */ new Map();
1423
- for (const [, reqQueries] of queriesByReq) {
1424
- for (const q of reqQueries) {
1425
- if (!q.rowCount || q.rowCount <= HIGH_ROW_COUNT) continue;
1426
- const info = getQueryInfo(q);
1427
- const key = `${info.op} ${info.table || "unknown"}`;
1428
- let entry = highRowSeen.get(key);
1429
- if (!entry) {
1430
- entry = { max: 0, count: 0 };
1431
- highRowSeen.set(key, entry);
1643
+ };
1644
+
1645
+ // src/utils/response.ts
1646
+ function unwrapResponse2(parsed) {
1647
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
1648
+ const obj = parsed;
1649
+ const keys = Object.keys(obj);
1650
+ if (keys.length > 3) return parsed;
1651
+ let best = null;
1652
+ let bestSize = 0;
1653
+ for (const key of keys) {
1654
+ const val = obj[key];
1655
+ if (Array.isArray(val) && val.length > bestSize) {
1656
+ best = val;
1657
+ bestSize = val.length;
1658
+ } else if (val && typeof val === "object" && !Array.isArray(val)) {
1659
+ const size = Object.keys(val).length;
1660
+ if (size > bestSize) {
1661
+ best = val;
1662
+ bestSize = size;
1432
1663
  }
1433
- entry.count++;
1434
- if (q.rowCount > entry.max) entry.max = q.rowCount;
1435
1664
  }
1436
1665
  }
1437
- for (const [key, hrs] of highRowSeen) {
1438
- if (hrs.count < OVERFETCH_MIN_REQUESTS) continue;
1439
- insights.push({
1440
- severity: "warning",
1441
- type: "high-rows",
1442
- title: "Large Result Set",
1443
- desc: `${key} returns ${hrs.max}+ rows (${hrs.count}x)`,
1444
- hint: "Fetching many rows slows responses and wastes memory. Add a LIMIT clause, implement pagination, or filter with a WHERE condition.",
1445
- nav: "queries"
1446
- });
1447
- }
1448
- const overfetchSeen = /* @__PURE__ */ new Set();
1449
- for (const r of nonStatic) {
1450
- if (r.statusCode >= 400 || !r.responseBody) continue;
1451
- const ep = `${r.method} ${r.path}`;
1452
- if (overfetchSeen.has(ep)) continue;
1453
- let parsed;
1454
- try {
1455
- parsed = JSON.parse(r.responseBody);
1456
- } catch {
1457
- continue;
1458
- }
1459
- let target = parsed;
1460
- if (target && typeof target === "object" && !Array.isArray(target)) {
1461
- const topKeys = Object.keys(target);
1462
- if (topKeys.length <= 3) {
1463
- let best = null;
1464
- let bestSize = 0;
1465
- for (const k of topKeys) {
1466
- const val = target[k];
1467
- if (Array.isArray(val) && val.length > bestSize) {
1468
- best = val;
1469
- bestSize = val.length;
1470
- } else if (val && typeof val === "object" && !Array.isArray(val)) {
1471
- const size = Object.keys(val).length;
1472
- if (size > bestSize) {
1473
- best = val;
1474
- bestSize = size;
1475
- }
1476
- }
1477
- }
1478
- if (best && bestSize >= 3) target = best;
1666
+ return best && bestSize >= OVERFETCH_UNWRAP_MIN_SIZE ? best : parsed;
1667
+ }
1668
+
1669
+ // src/analysis/insights/rules/response-overfetch.ts
1670
+ var responseOverfetchRule = {
1671
+ id: "response-overfetch",
1672
+ check(ctx) {
1673
+ const insights = [];
1674
+ const seen = /* @__PURE__ */ new Set();
1675
+ for (const r of ctx.nonStatic) {
1676
+ if (r.statusCode >= 400 || !r.responseBody) continue;
1677
+ const ep = getEndpointKey(r.method, r.path);
1678
+ if (seen.has(ep)) continue;
1679
+ let parsed;
1680
+ try {
1681
+ parsed = JSON.parse(r.responseBody);
1682
+ } catch {
1683
+ continue;
1684
+ }
1685
+ const target = unwrapResponse2(parsed);
1686
+ const inspectObj = Array.isArray(target) && target.length > 0 ? target[0] : target;
1687
+ if (!inspectObj || typeof inspectObj !== "object" || Array.isArray(inspectObj)) continue;
1688
+ const fields = Object.keys(inspectObj);
1689
+ if (fields.length < OVERFETCH_MIN_FIELDS) continue;
1690
+ let internalIdCount = 0;
1691
+ let nullCount = 0;
1692
+ for (const key of fields) {
1693
+ if (INTERNAL_ID_SUFFIX.test(key) || key === "id" || key === "_id") internalIdCount++;
1694
+ const val = inspectObj[key];
1695
+ if (val === null || val === void 0) nullCount++;
1696
+ }
1697
+ const nullRatio = nullCount / fields.length;
1698
+ const reasons = [];
1699
+ if (internalIdCount >= OVERFETCH_MIN_INTERNAL_IDS) reasons.push(`${internalIdCount} internal ID fields`);
1700
+ if (nullRatio >= OVERFETCH_NULL_RATIO) reasons.push(`${Math.round(nullRatio * 100)}% null fields`);
1701
+ if (reasons.length === 0 && fields.length >= OVERFETCH_MANY_FIELDS) {
1702
+ reasons.push(`${fields.length} fields returned`);
1703
+ }
1704
+ if (reasons.length > 0) {
1705
+ seen.add(ep);
1706
+ insights.push({
1707
+ severity: "info",
1708
+ type: "response-overfetch",
1709
+ title: "Response Overfetch",
1710
+ desc: `${ep} \u2014 ${reasons.join(", ")}`,
1711
+ hint: "This response returns more data than the client likely needs. Use a DTO or select only required fields to reduce payload size and avoid leaking internal structure.",
1712
+ nav: "requests"
1713
+ });
1479
1714
  }
1480
1715
  }
1481
- const inspectObj = Array.isArray(target) && target.length > 0 ? target[0] : target;
1482
- if (!inspectObj || typeof inspectObj !== "object" || Array.isArray(inspectObj)) continue;
1483
- const fields = Object.keys(inspectObj);
1484
- if (fields.length < OVERFETCH_MIN_FIELDS) continue;
1485
- let internalIdCount = 0;
1486
- let nullCount = 0;
1487
- for (const key of fields) {
1488
- if (INTERNAL_ID_SUFFIX.test(key) || key === "id" || key === "_id") internalIdCount++;
1489
- const val = inspectObj[key];
1490
- if (val === null || val === void 0) nullCount++;
1491
- }
1492
- const nullRatio = nullCount / fields.length;
1493
- const reasons = [];
1494
- if (internalIdCount >= OVERFETCH_MIN_INTERNAL_IDS) reasons.push(`${internalIdCount} internal ID fields`);
1495
- if (nullRatio >= OVERFETCH_NULL_RATIO) reasons.push(`${Math.round(nullRatio * 100)}% null fields`);
1496
- if (fields.length >= OVERFETCH_MIN_FIELDS && reasons.length === 0 && fields.length >= 12) {
1497
- reasons.push(`${fields.length} fields returned`);
1498
- }
1499
- if (reasons.length > 0) {
1500
- overfetchSeen.add(ep);
1501
- insights.push({
1502
- severity: "info",
1503
- type: "response-overfetch",
1504
- title: "Response Overfetch",
1505
- desc: `${ep} \u2014 ${reasons.join(", ")}`,
1506
- hint: "This response returns more data than the client likely needs. Use a DTO or select only required fields to reduce payload size and avoid leaking internal structure.",
1507
- nav: "requests"
1508
- });
1509
- }
1716
+ return insights;
1510
1717
  }
1511
- for (const [ep, g] of endpointGroups) {
1512
- if (g.total < OVERFETCH_MIN_REQUESTS) continue;
1513
- const avgSize = Math.round(g.totalSize / g.total);
1514
- if (avgSize > LARGE_RESPONSE_BYTES) {
1515
- insights.push({
1516
- severity: "info",
1517
- type: "large-response",
1518
- title: "Large Response",
1519
- desc: `${ep} \u2014 avg ${formatSize(avgSize)} response`,
1520
- hint: "Large API responses increase network transfer time. Implement pagination, field filtering, or response compression.",
1521
- nav: "requests"
1522
- });
1718
+ };
1719
+
1720
+ // src/analysis/insights/rules/large-response.ts
1721
+ var largeResponseRule = {
1722
+ id: "large-response",
1723
+ check(ctx) {
1724
+ const insights = [];
1725
+ for (const [ep, g] of ctx.endpointGroups) {
1726
+ if (g.total < OVERFETCH_MIN_REQUESTS) continue;
1727
+ const avgSize = Math.round(g.totalSize / g.total);
1728
+ if (avgSize > LARGE_RESPONSE_BYTES) {
1729
+ insights.push({
1730
+ severity: "info",
1731
+ type: "large-response",
1732
+ title: "Large Response",
1733
+ desc: `${ep} \u2014 avg ${formatSize(avgSize)} response`,
1734
+ hint: "Large API responses increase network transfer time. Implement pagination, field filtering, or response compression.",
1735
+ nav: "requests"
1736
+ });
1737
+ }
1523
1738
  }
1739
+ return insights;
1524
1740
  }
1525
- if (ctx.securityFindings) {
1526
- for (const f of ctx.securityFindings) {
1527
- insights.push({
1528
- severity: f.severity,
1529
- type: "security",
1530
- title: f.title,
1531
- desc: f.desc,
1532
- hint: f.hint,
1533
- nav: "security"
1534
- });
1741
+ };
1742
+
1743
+ // src/analysis/insights/rules/regression.ts
1744
+ var regressionRule = {
1745
+ id: "regression",
1746
+ check(ctx) {
1747
+ if (!ctx.previousMetrics || ctx.previousMetrics.length === 0) return [];
1748
+ const insights = [];
1749
+ for (const epMetrics of ctx.previousMetrics) {
1750
+ if (epMetrics.sessions.length < 2) continue;
1751
+ const prev = epMetrics.sessions[epMetrics.sessions.length - 2];
1752
+ const current = epMetrics.sessions[epMetrics.sessions.length - 1];
1753
+ if (prev.requestCount < REGRESSION_MIN_REQUESTS || current.requestCount < REGRESSION_MIN_REQUESTS) continue;
1754
+ const p95Increase = current.p95DurationMs - prev.p95DurationMs;
1755
+ const p95PctChange = prev.p95DurationMs > 0 ? Math.round(p95Increase / prev.p95DurationMs * 100) : 0;
1756
+ if (p95Increase >= REGRESSION_MIN_INCREASE_MS && p95PctChange >= REGRESSION_PCT_THRESHOLD) {
1757
+ insights.push({
1758
+ severity: "warning",
1759
+ type: "regression",
1760
+ title: "Performance Regression",
1761
+ desc: `${epMetrics.endpoint} p95 degraded ${formatDuration(prev.p95DurationMs)} \u2192 ${formatDuration(current.p95DurationMs)} (+${p95PctChange}%)`,
1762
+ hint: "This endpoint is slower than the previous session. Check if recent code changes added queries or processing.",
1763
+ nav: "graph"
1764
+ });
1765
+ }
1766
+ if (prev.avgQueryCount > 0 && current.avgQueryCount > prev.avgQueryCount * QUERY_COUNT_REGRESSION_RATIO) {
1767
+ insights.push({
1768
+ severity: "warning",
1769
+ type: "regression",
1770
+ title: "Query Count Regression",
1771
+ desc: `${epMetrics.endpoint} queries/request increased ${prev.avgQueryCount} \u2192 ${current.avgQueryCount}`,
1772
+ hint: "This endpoint is making more database queries than before. Check for new N+1 patterns or removed query optimizations.",
1773
+ nav: "queries"
1774
+ });
1775
+ }
1535
1776
  }
1777
+ return insights;
1778
+ }
1779
+ };
1780
+
1781
+ // src/analysis/insights/rules/security.ts
1782
+ var securityRule = {
1783
+ id: "security",
1784
+ check(ctx) {
1785
+ if (!ctx.securityFindings) return [];
1786
+ return ctx.securityFindings.map((f) => ({
1787
+ severity: f.severity,
1788
+ type: "security",
1789
+ title: f.title,
1790
+ desc: f.desc,
1791
+ hint: f.hint,
1792
+ nav: "security"
1793
+ }));
1536
1794
  }
1537
- const severityOrder = { critical: 0, warning: 1, info: 2 };
1538
- insights.sort((a, b) => (severityOrder[a.severity] ?? 2) - (severityOrder[b.severity] ?? 2));
1539
- return insights;
1795
+ };
1796
+
1797
+ // src/analysis/insights/index.ts
1798
+ function createDefaultInsightRunner() {
1799
+ const runner = new InsightRunner();
1800
+ runner.register(n1Rule);
1801
+ runner.register(crossEndpointRule);
1802
+ runner.register(redundantQueryRule);
1803
+ runner.register(errorRule);
1804
+ runner.register(errorHotspotRule);
1805
+ runner.register(duplicateRule);
1806
+ runner.register(slowRule);
1807
+ runner.register(queryHeavyRule);
1808
+ runner.register(selectStarRule);
1809
+ runner.register(highRowsRule);
1810
+ runner.register(responseOverfetchRule);
1811
+ runner.register(largeResponseRule);
1812
+ runner.register(regressionRule);
1813
+ runner.register(securityRule);
1814
+ return runner;
1815
+ }
1816
+ function computeInsights(ctx) {
1817
+ return createDefaultInsightRunner().run(ctx);
1540
1818
  }
1541
1819
 
1542
1820
  // src/analysis/engine.ts
1543
1821
  var AnalysisEngine = class {
1544
- constructor(debounceMs = 300) {
1822
+ constructor(metricsStore, debounceMs = 300) {
1823
+ this.metricsStore = metricsStore;
1545
1824
  this.debounceMs = debounceMs;
1546
1825
  this.scanner = createDefaultScanner();
1547
1826
  this.boundRequestListener = () => this.scheduleRecompute();
@@ -1599,6 +1878,7 @@ var AnalysisEngine = class {
1599
1878
  const queries = defaultQueryStore.getAll();
1600
1879
  const errors = defaultErrorStore.getAll();
1601
1880
  const logs = defaultLogStore.getAll();
1881
+ const fetches = defaultFetchStore.getAll();
1602
1882
  const flows = groupRequestsIntoFlows(requests);
1603
1883
  this.cachedFindings = this.scanner.scan({ requests, logs });
1604
1884
  this.cachedInsights = computeInsights({
@@ -1606,6 +1886,8 @@ var AnalysisEngine = class {
1606
1886
  queries,
1607
1887
  errors,
1608
1888
  flows,
1889
+ fetches,
1890
+ previousMetrics: this.metricsStore.getAll(),
1609
1891
  securityFindings: this.cachedFindings
1610
1892
  });
1611
1893
  for (const fn of this.listeners) {
@@ -1618,13 +1900,15 @@ var AnalysisEngine = class {
1618
1900
  };
1619
1901
 
1620
1902
  // src/index.ts
1621
- var VERSION = "0.7.3";
1903
+ var VERSION = "0.7.5";
1622
1904
  export {
1623
1905
  AdapterRegistry,
1624
1906
  AnalysisEngine,
1907
+ InsightRunner,
1625
1908
  SecurityScanner,
1626
1909
  VERSION,
1627
1910
  computeInsights,
1911
+ createDefaultInsightRunner,
1628
1912
  createDefaultScanner,
1629
1913
  detectProject
1630
1914
  };