brakit 0.8.0 → 0.8.2

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
@@ -1,20 +1,12 @@
1
1
  // src/store/finding-store.ts
2
- import {
3
- readFileSync as readFileSync2,
4
- writeFileSync as writeFileSync2,
5
- existsSync as existsSync2,
6
- mkdirSync as mkdirSync2,
7
- renameSync
8
- } from "fs";
9
- import { writeFile as writeFile2, mkdir, rename } from "fs/promises";
2
+ import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
10
3
  import { resolve as resolve2 } from "path";
11
4
 
12
5
  // src/constants/routes.ts
13
6
  var DASHBOARD_PREFIX = "/__brakit";
14
7
 
15
8
  // src/constants/limits.ts
16
- var MAX_REQUEST_ENTRIES = 1e3;
17
- var MAX_TELEMETRY_ENTRIES = 1e3;
9
+ var MAX_INGEST_BYTES = 10 * 1024 * 1024;
18
10
 
19
11
  // src/constants/thresholds.ts
20
12
  var FLOW_GAP_MS = 5e3;
@@ -43,12 +35,34 @@ var QUERY_COUNT_REGRESSION_RATIO = 1.5;
43
35
  var OVERFETCH_MANY_FIELDS = 12;
44
36
  var OVERFETCH_UNWRAP_MIN_SIZE = 3;
45
37
  var MAX_DUPLICATE_INSIGHTS = 3;
38
+ var INSIGHT_WINDOW_PER_ENDPOINT = 2;
39
+ var RESOLVE_AFTER_ABSENCES = 3;
40
+ var RESOLVED_INSIGHT_TTL_MS = 18e5;
46
41
 
47
42
  // src/constants/metrics.ts
48
43
  var METRICS_DIR = ".brakit";
49
44
  var FINDINGS_FILE = ".brakit/findings.json";
50
45
  var FINDINGS_FLUSH_INTERVAL_MS = 1e4;
51
46
 
47
+ // src/constants/severity.ts
48
+ var SEVERITY_CRITICAL = "critical";
49
+ var SEVERITY_WARNING = "warning";
50
+ var SEVERITY_INFO = "info";
51
+ var SEVERITY_ICON_MAP = {
52
+ [SEVERITY_CRITICAL]: { icon: "\u2717", cls: "critical" },
53
+ [SEVERITY_WARNING]: { icon: "\u26A0", cls: "warning" },
54
+ [SEVERITY_INFO]: { icon: "\u2139", cls: "info" }
55
+ };
56
+
57
+ // src/utils/atomic-writer.ts
58
+ import {
59
+ writeFileSync as writeFileSync2,
60
+ existsSync as existsSync2,
61
+ mkdirSync as mkdirSync2,
62
+ renameSync
63
+ } from "fs";
64
+ import { writeFile as writeFile2, mkdir, rename } from "fs/promises";
65
+
52
66
  // src/utils/fs.ts
53
67
  import { access } from "fs/promises";
54
68
  import { existsSync, readFileSync, writeFileSync } from "fs";
@@ -75,6 +89,70 @@ function ensureGitignore(dir, entry) {
75
89
  }
76
90
  }
77
91
 
92
+ // src/utils/log.ts
93
+ var PREFIX = "[brakit]";
94
+ function brakitWarn(message) {
95
+ process.stderr.write(`${PREFIX} ${message}
96
+ `);
97
+ }
98
+
99
+ // src/utils/atomic-writer.ts
100
+ var AtomicWriter = class {
101
+ constructor(opts) {
102
+ this.opts = opts;
103
+ this.tmpPath = opts.filePath + ".tmp";
104
+ }
105
+ tmpPath;
106
+ writing = false;
107
+ pendingContent = null;
108
+ writeSync(content) {
109
+ try {
110
+ this.ensureDir();
111
+ writeFileSync2(this.tmpPath, content);
112
+ renameSync(this.tmpPath, this.opts.filePath);
113
+ } catch (err) {
114
+ brakitWarn(`failed to save ${this.opts.label}: ${err.message}`);
115
+ }
116
+ }
117
+ async writeAsync(content) {
118
+ if (this.writing) {
119
+ this.pendingContent = content;
120
+ return;
121
+ }
122
+ this.writing = true;
123
+ try {
124
+ await this.ensureDirAsync();
125
+ await writeFile2(this.tmpPath, content);
126
+ await rename(this.tmpPath, this.opts.filePath);
127
+ } catch (err) {
128
+ brakitWarn(`failed to save ${this.opts.label}: ${err.message}`);
129
+ } finally {
130
+ this.writing = false;
131
+ if (this.pendingContent !== null) {
132
+ const next = this.pendingContent;
133
+ this.pendingContent = null;
134
+ this.writeAsync(next);
135
+ }
136
+ }
137
+ }
138
+ ensureDir() {
139
+ if (!existsSync2(this.opts.dir)) {
140
+ mkdirSync2(this.opts.dir, { recursive: true });
141
+ if (this.opts.gitignoreEntry) {
142
+ ensureGitignore(this.opts.dir, this.opts.gitignoreEntry);
143
+ }
144
+ }
145
+ }
146
+ async ensureDirAsync() {
147
+ if (!existsSync2(this.opts.dir)) {
148
+ await mkdir(this.opts.dir, { recursive: true });
149
+ if (this.opts.gitignoreEntry) {
150
+ ensureGitignore(this.opts.dir, this.opts.gitignoreEntry);
151
+ }
152
+ }
153
+ }
154
+ };
155
+
78
156
  // src/store/finding-id.ts
79
157
  import { createHash } from "crypto";
80
158
  function computeFindingId(finding) {
@@ -86,18 +164,21 @@ function computeFindingId(finding) {
86
164
  var FindingStore = class {
87
165
  constructor(rootDir) {
88
166
  this.rootDir = rootDir;
89
- this.metricsDir = resolve2(rootDir, METRICS_DIR);
167
+ const metricsDir = resolve2(rootDir, METRICS_DIR);
90
168
  this.findingsPath = resolve2(rootDir, FINDINGS_FILE);
91
- this.tmpPath = this.findingsPath + ".tmp";
169
+ this.writer = new AtomicWriter({
170
+ dir: metricsDir,
171
+ filePath: this.findingsPath,
172
+ gitignoreEntry: METRICS_DIR,
173
+ label: "findings"
174
+ });
92
175
  this.load();
93
176
  }
94
177
  findings = /* @__PURE__ */ new Map();
95
178
  flushTimer = null;
96
179
  dirty = false;
97
- writing = false;
180
+ writer;
98
181
  findingsPath;
99
- tmpPath;
100
- metricsDir;
101
182
  start() {
102
183
  this.flushTimer = setInterval(
103
184
  () => this.flush(),
@@ -151,6 +232,15 @@ var FindingStore = class {
151
232
  this.dirty = true;
152
233
  return true;
153
234
  }
235
+ /**
236
+ * Reconcile passive findings against the current analysis results.
237
+ *
238
+ * Passive findings are detected by continuous scanning (not user-triggered).
239
+ * When a previously-seen finding is absent from the current results, it means
240
+ * the issue has been fixed — transition it to "resolved" automatically.
241
+ * Active findings (from MCP verify-fix) are not auto-resolved because they
242
+ * require explicit verification.
243
+ */
154
244
  reconcilePassive(currentFindings) {
155
245
  const currentIds = new Set(currentFindings.map(computeFindingId));
156
246
  for (const [id, stateful] of this.findings) {
@@ -176,7 +266,7 @@ var FindingStore = class {
176
266
  }
177
267
  load() {
178
268
  try {
179
- if (existsSync2(this.findingsPath)) {
269
+ if (existsSync3(this.findingsPath)) {
180
270
  const raw = readFileSync2(this.findingsPath, "utf-8");
181
271
  const parsed = JSON.parse(raw);
182
272
  if (parsed?.version === 1 && Array.isArray(parsed.findings)) {
@@ -190,56 +280,20 @@ var FindingStore = class {
190
280
  }
191
281
  flush() {
192
282
  if (!this.dirty) return;
193
- this.writeAsync();
283
+ this.writer.writeAsync(this.serialize());
284
+ this.dirty = false;
194
285
  }
195
286
  flushSync() {
196
287
  if (!this.dirty) return;
197
- try {
198
- this.ensureDir();
199
- const data = {
200
- version: 1,
201
- findings: [...this.findings.values()]
202
- };
203
- writeFileSync2(this.tmpPath, JSON.stringify(data));
204
- renameSync(this.tmpPath, this.findingsPath);
205
- this.dirty = false;
206
- } catch (err) {
207
- process.stderr.write(
208
- `[brakit] failed to save findings: ${err.message}
209
- `
210
- );
211
- }
288
+ this.writer.writeSync(this.serialize());
289
+ this.dirty = false;
212
290
  }
213
- async writeAsync() {
214
- if (this.writing) return;
215
- this.writing = true;
216
- try {
217
- if (!existsSync2(this.metricsDir)) {
218
- await mkdir(this.metricsDir, { recursive: true });
219
- ensureGitignore(this.metricsDir, METRICS_DIR);
220
- }
221
- const data = {
222
- version: 1,
223
- findings: [...this.findings.values()]
224
- };
225
- await writeFile2(this.tmpPath, JSON.stringify(data));
226
- await rename(this.tmpPath, this.findingsPath);
227
- this.dirty = false;
228
- } catch (err) {
229
- process.stderr.write(
230
- `[brakit] failed to save findings: ${err.message}
231
- `
232
- );
233
- } finally {
234
- this.writing = false;
235
- if (this.dirty) this.writeAsync();
236
- }
237
- }
238
- ensureDir() {
239
- if (!existsSync2(this.metricsDir)) {
240
- mkdirSync2(this.metricsDir, { recursive: true });
241
- ensureGitignore(this.metricsDir, METRICS_DIR);
242
- }
291
+ serialize() {
292
+ const data = {
293
+ version: 1,
294
+ findings: [...this.findings.values()]
295
+ };
296
+ return JSON.stringify(data);
243
297
  }
244
298
  };
245
299
 
@@ -825,166 +879,20 @@ function createDefaultScanner() {
825
879
  return scanner;
826
880
  }
827
881
 
828
- // src/utils/static-patterns.ts
829
- var STATIC_PATTERNS = [
830
- /^\/_next\//,
831
- /\.(?:js|css|map|ico|png|jpg|jpeg|gif|svg|webp|woff2?|ttf|eot)$/,
832
- /^\/favicon/,
833
- /^\/__nextjs/
834
- ];
835
- function isStaticPath(urlPath) {
836
- return STATIC_PATTERNS.some((p) => p.test(urlPath));
837
- }
838
-
839
- // src/store/request-store.ts
840
- function flattenHeaders(headers) {
841
- const flat = {};
842
- for (const [key, value] of Object.entries(headers)) {
843
- if (value === void 0) continue;
844
- flat[key] = Array.isArray(value) ? value.join(", ") : value;
845
- }
846
- return flat;
847
- }
848
- var RequestStore = class {
849
- constructor(maxEntries = MAX_REQUEST_ENTRIES) {
850
- this.maxEntries = maxEntries;
851
- }
852
- requests = [];
853
- listeners = [];
854
- capture(input) {
855
- const url = input.url;
856
- const path = url.split("?")[0];
857
- let requestBodyStr = null;
858
- if (input.requestBody && input.requestBody.length > 0) {
859
- requestBodyStr = input.requestBody.subarray(0, input.config.maxBodyCapture).toString("utf-8");
860
- }
861
- let responseBodyStr = null;
862
- if (input.responseBody && input.responseBody.length > 0) {
863
- const ct = input.responseContentType;
864
- if (ct.includes("json") || ct.includes("text") || ct.includes("html")) {
865
- responseBodyStr = input.responseBody.subarray(0, input.config.maxBodyCapture).toString("utf-8");
866
- }
867
- }
868
- const entry = {
869
- id: input.requestId,
870
- method: input.method,
871
- url,
872
- path,
873
- headers: flattenHeaders(input.requestHeaders),
874
- requestBody: requestBodyStr,
875
- statusCode: input.statusCode,
876
- responseHeaders: flattenHeaders(input.responseHeaders),
877
- responseBody: responseBodyStr,
878
- startedAt: input.startTime,
879
- durationMs: Math.round((input.endTime ?? performance.now()) - input.startTime),
880
- responseSize: input.responseBody?.length ?? 0,
881
- isStatic: isStaticPath(path)
882
- };
883
- this.requests.push(entry);
884
- if (this.requests.length > this.maxEntries) {
885
- this.requests.shift();
886
- }
887
- for (const fn of this.listeners) {
888
- fn(entry);
889
- }
890
- return entry;
891
- }
892
- getAll() {
893
- return this.requests;
894
- }
895
- clear() {
896
- this.requests.length = 0;
882
+ // src/core/disposable.ts
883
+ var SubscriptionBag = class {
884
+ items = [];
885
+ add(teardown) {
886
+ this.items.push(typeof teardown === "function" ? { dispose: teardown } : teardown);
897
887
  }
898
- onRequest(fn) {
899
- this.listeners.push(fn);
900
- }
901
- offRequest(fn) {
902
- const idx = this.listeners.indexOf(fn);
903
- if (idx !== -1) this.listeners.splice(idx, 1);
888
+ dispose() {
889
+ for (const d of this.items) d.dispose();
890
+ this.items.length = 0;
904
891
  }
905
892
  };
906
893
 
907
- // src/store/request-log.ts
908
- var defaultStore = new RequestStore();
909
- var getRequests = () => defaultStore.getAll();
910
- var onRequest = (fn) => defaultStore.onRequest(fn);
911
- var offRequest = (fn) => defaultStore.offRequest(fn);
912
-
913
- // src/store/telemetry-store.ts
914
- import { randomUUID } from "crypto";
915
- var TelemetryStore = class {
916
- constructor(maxEntries = MAX_TELEMETRY_ENTRIES) {
917
- this.maxEntries = maxEntries;
918
- }
919
- entries = [];
920
- listeners = [];
921
- add(data) {
922
- const entry = { id: randomUUID(), ...data };
923
- this.entries.push(entry);
924
- if (this.entries.length > this.maxEntries) this.entries.shift();
925
- for (const fn of this.listeners) fn(entry);
926
- return entry;
927
- }
928
- getAll() {
929
- return this.entries;
930
- }
931
- getByRequest(requestId) {
932
- return this.entries.filter((e) => e.parentRequestId === requestId);
933
- }
934
- clear() {
935
- this.entries.length = 0;
936
- }
937
- onEntry(fn) {
938
- this.listeners.push(fn);
939
- }
940
- offEntry(fn) {
941
- const idx = this.listeners.indexOf(fn);
942
- if (idx !== -1) this.listeners.splice(idx, 1);
943
- }
944
- };
945
-
946
- // src/store/fetch-store.ts
947
- var FetchStore = class extends TelemetryStore {
948
- };
949
- var defaultFetchStore = new FetchStore();
950
-
951
- // src/store/log-store.ts
952
- var LogStore = class extends TelemetryStore {
953
- };
954
- var defaultLogStore = new LogStore();
955
-
956
- // src/store/error-store.ts
957
- var ErrorStore = class extends TelemetryStore {
958
- };
959
- var defaultErrorStore = new ErrorStore();
960
-
961
- // src/store/query-store.ts
962
- var QueryStore = class extends TelemetryStore {
963
- };
964
- var defaultQueryStore = new QueryStore();
965
-
966
- // src/store/metrics/metrics-store.ts
967
- import { randomUUID as randomUUID2 } from "crypto";
968
-
969
- // src/utils/endpoint.ts
970
- function getEndpointKey(method, path) {
971
- return `${method} ${path}`;
972
- }
973
-
974
- // src/store/metrics/persistence.ts
975
- import {
976
- readFileSync as readFileSync3,
977
- writeFileSync as writeFileSync3,
978
- mkdirSync as mkdirSync3,
979
- existsSync as existsSync3,
980
- unlinkSync,
981
- renameSync as renameSync2
982
- } from "fs";
983
- import { writeFile as writeFile3, mkdir as mkdir2, rename as rename2 } from "fs/promises";
984
- import { resolve as resolve3 } from "path";
985
-
986
894
  // src/analysis/group.ts
987
- import { randomUUID as randomUUID3 } from "crypto";
895
+ import { randomUUID } from "crypto";
988
896
 
989
897
  // src/analysis/categorize.ts
990
898
  function detectCategory(req) {
@@ -1280,7 +1188,7 @@ function buildFlow(rawRequests) {
1280
1188
  const redundancyPct = nonStaticCount > 0 ? Math.round(duplicateCount / nonStaticCount * 100) : 0;
1281
1189
  const sourcePage = getDominantSourcePage(rawRequests);
1282
1190
  return {
1283
- id: randomUUID3(),
1191
+ id: randomUUID(),
1284
1192
  label: deriveFlowLabel(requests, sourcePage),
1285
1193
  requests,
1286
1194
  startTime,
@@ -1352,6 +1260,15 @@ function groupBy(items, keyFn) {
1352
1260
  return map;
1353
1261
  }
1354
1262
 
1263
+ // src/utils/endpoint.ts
1264
+ function getEndpointKey(method, path) {
1265
+ return `${method} ${path}`;
1266
+ }
1267
+ var ENDPOINT_PREFIX_RE = /^(\S+\s+\S+)/;
1268
+ function extractEndpointFromDesc(desc) {
1269
+ return desc.match(ENDPOINT_PREFIX_RE)?.[1] ?? null;
1270
+ }
1271
+
1355
1272
  // src/instrument/adapters/normalize.ts
1356
1273
  function normalizeSQL(sql) {
1357
1274
  if (!sql) return { op: "OTHER", table: "" };
@@ -1410,6 +1327,23 @@ function createEndpointGroup() {
1410
1327
  queryShapeDurations: /* @__PURE__ */ new Map()
1411
1328
  };
1412
1329
  }
1330
+ function windowByEndpoint(requests) {
1331
+ const byEndpoint = /* @__PURE__ */ new Map();
1332
+ for (const r of requests) {
1333
+ const ep = getEndpointKey(r.method, r.path);
1334
+ let list = byEndpoint.get(ep);
1335
+ if (!list) {
1336
+ list = [];
1337
+ byEndpoint.set(ep, list);
1338
+ }
1339
+ list.push(r);
1340
+ }
1341
+ const windowed = [];
1342
+ for (const [, reqs] of byEndpoint) {
1343
+ windowed.push(...reqs.slice(-INSIGHT_WINDOW_PER_ENDPOINT));
1344
+ }
1345
+ return windowed;
1346
+ }
1413
1347
  function prepareContext(ctx) {
1414
1348
  const nonStatic = ctx.requests.filter(
1415
1349
  (r) => !r.isStatic && (!r.path || !r.path.startsWith(DASHBOARD_PREFIX))
@@ -1417,8 +1351,9 @@ function prepareContext(ctx) {
1417
1351
  const queriesByReq = groupBy(ctx.queries, (q) => q.parentRequestId);
1418
1352
  const fetchesByReq = groupBy(ctx.fetches, (f) => f.parentRequestId);
1419
1353
  const reqById = new Map(nonStatic.map((r) => [r.id, r]));
1354
+ const recent = windowByEndpoint(nonStatic);
1420
1355
  const endpointGroups = /* @__PURE__ */ new Map();
1421
- for (const r of nonStatic) {
1356
+ for (const r of recent) {
1422
1357
  const ep = getEndpointKey(r.method, r.path);
1423
1358
  let g = endpointGroups.get(ep);
1424
1359
  if (!g) {
@@ -1990,56 +1925,101 @@ function computeInsights(ctx) {
1990
1925
  return createDefaultInsightRunner().run(ctx);
1991
1926
  }
1992
1927
 
1928
+ // src/analysis/insight-tracker.ts
1929
+ function computeInsightKey(insight) {
1930
+ const identifier = extractEndpointFromDesc(insight.desc) ?? insight.title;
1931
+ return `${insight.type}:${identifier}`;
1932
+ }
1933
+ var InsightTracker = class {
1934
+ tracked = /* @__PURE__ */ new Map();
1935
+ reconcile(current) {
1936
+ const currentKeys = /* @__PURE__ */ new Set();
1937
+ const now = Date.now();
1938
+ for (const insight of current) {
1939
+ const key = computeInsightKey(insight);
1940
+ currentKeys.add(key);
1941
+ const existing = this.tracked.get(key);
1942
+ if (existing) {
1943
+ existing.insight = insight;
1944
+ existing.lastSeenAt = now;
1945
+ existing.consecutiveAbsences = 0;
1946
+ if (existing.state === "resolved") {
1947
+ existing.state = "open";
1948
+ existing.resolvedAt = null;
1949
+ }
1950
+ } else {
1951
+ this.tracked.set(key, {
1952
+ key,
1953
+ state: "open",
1954
+ insight,
1955
+ firstSeenAt: now,
1956
+ lastSeenAt: now,
1957
+ resolvedAt: null,
1958
+ consecutiveAbsences: 0
1959
+ });
1960
+ }
1961
+ }
1962
+ for (const [key, stateful] of this.tracked) {
1963
+ if (stateful.state === "open" && !currentKeys.has(stateful.key)) {
1964
+ stateful.consecutiveAbsences++;
1965
+ if (stateful.consecutiveAbsences >= RESOLVE_AFTER_ABSENCES) {
1966
+ stateful.state = "resolved";
1967
+ stateful.resolvedAt = now;
1968
+ }
1969
+ } else if (stateful.state === "resolved" && stateful.resolvedAt !== null && now - stateful.resolvedAt > RESOLVED_INSIGHT_TTL_MS) {
1970
+ this.tracked.delete(key);
1971
+ }
1972
+ }
1973
+ return [...this.tracked.values()];
1974
+ }
1975
+ getAll() {
1976
+ return [...this.tracked.values()];
1977
+ }
1978
+ clear() {
1979
+ this.tracked.clear();
1980
+ }
1981
+ };
1982
+
1993
1983
  // src/analysis/engine.ts
1994
1984
  var AnalysisEngine = class {
1995
- constructor(metricsStore, findingStore, debounceMs = 300) {
1996
- this.metricsStore = metricsStore;
1997
- this.findingStore = findingStore;
1985
+ constructor(registry, debounceMs = 300) {
1986
+ this.registry = registry;
1998
1987
  this.debounceMs = debounceMs;
1999
1988
  this.scanner = createDefaultScanner();
2000
- this.boundRequestListener = () => this.scheduleRecompute();
2001
- this.boundQueryListener = () => this.scheduleRecompute();
2002
- this.boundErrorListener = () => this.scheduleRecompute();
2003
- this.boundLogListener = () => this.scheduleRecompute();
2004
1989
  }
2005
1990
  scanner;
1991
+ insightTracker = new InsightTracker();
2006
1992
  cachedInsights = [];
2007
1993
  cachedFindings = [];
1994
+ cachedStatefulInsights = [];
2008
1995
  debounceTimer = null;
2009
- listeners = [];
2010
- boundRequestListener;
2011
- boundQueryListener;
2012
- boundErrorListener;
2013
- boundLogListener;
1996
+ subs = new SubscriptionBag();
2014
1997
  start() {
2015
- onRequest(this.boundRequestListener);
2016
- defaultQueryStore.onEntry(this.boundQueryListener);
2017
- defaultErrorStore.onEntry(this.boundErrorListener);
2018
- defaultLogStore.onEntry(this.boundLogListener);
1998
+ const bus = this.registry.get("event-bus");
1999
+ this.subs.add(bus.on("request:completed", () => this.scheduleRecompute()));
2000
+ this.subs.add(bus.on("telemetry:query", () => this.scheduleRecompute()));
2001
+ this.subs.add(bus.on("telemetry:error", () => this.scheduleRecompute()));
2002
+ this.subs.add(bus.on("telemetry:log", () => this.scheduleRecompute()));
2019
2003
  }
2020
2004
  stop() {
2021
- offRequest(this.boundRequestListener);
2022
- defaultQueryStore.offEntry(this.boundQueryListener);
2023
- defaultErrorStore.offEntry(this.boundErrorListener);
2024
- defaultLogStore.offEntry(this.boundLogListener);
2005
+ this.subs.dispose();
2025
2006
  if (this.debounceTimer) {
2026
2007
  clearTimeout(this.debounceTimer);
2027
2008
  this.debounceTimer = null;
2028
2009
  }
2029
2010
  }
2030
- onUpdate(fn) {
2031
- this.listeners.push(fn);
2032
- }
2033
- offUpdate(fn) {
2034
- const idx = this.listeners.indexOf(fn);
2035
- if (idx !== -1) this.listeners.splice(idx, 1);
2036
- }
2037
2011
  getInsights() {
2038
2012
  return this.cachedInsights;
2039
2013
  }
2040
2014
  getFindings() {
2041
2015
  return this.cachedFindings;
2042
2016
  }
2017
+ getStatefulFindings() {
2018
+ return this.registry.has("finding-store") ? this.registry.get("finding-store").getAll() : [];
2019
+ }
2020
+ getStatefulInsights() {
2021
+ return this.cachedStatefulInsights;
2022
+ }
2043
2023
  scheduleRecompute() {
2044
2024
  if (this.debounceTimer) return;
2045
2025
  this.debounceTimer = setTimeout(() => {
@@ -2048,18 +2028,19 @@ var AnalysisEngine = class {
2048
2028
  }, this.debounceMs);
2049
2029
  }
2050
2030
  recompute() {
2051
- const requests = getRequests();
2052
- const queries = defaultQueryStore.getAll();
2053
- const errors = defaultErrorStore.getAll();
2054
- const logs = defaultLogStore.getAll();
2055
- const fetches = defaultFetchStore.getAll();
2031
+ const requests = this.registry.get("request-store").getAll();
2032
+ const queries = this.registry.get("query-store").getAll();
2033
+ const errors = this.registry.get("error-store").getAll();
2034
+ const logs = this.registry.get("log-store").getAll();
2035
+ const fetches = this.registry.get("fetch-store").getAll();
2056
2036
  const flows = groupRequestsIntoFlows(requests);
2057
2037
  this.cachedFindings = this.scanner.scan({ requests, logs });
2058
- if (this.findingStore) {
2038
+ if (this.registry.has("finding-store")) {
2039
+ const findingStore = this.registry.get("finding-store");
2059
2040
  for (const finding of this.cachedFindings) {
2060
- this.findingStore.upsert(finding, "passive");
2041
+ findingStore.upsert(finding, "passive");
2061
2042
  }
2062
- this.findingStore.reconcilePassive(this.cachedFindings);
2043
+ findingStore.reconcilePassive(this.cachedFindings);
2063
2044
  }
2064
2045
  this.cachedInsights = computeInsights({
2065
2046
  requests,
@@ -2067,20 +2048,22 @@ var AnalysisEngine = class {
2067
2048
  errors,
2068
2049
  flows,
2069
2050
  fetches,
2070
- previousMetrics: this.metricsStore.getAll(),
2051
+ previousMetrics: this.registry.get("metrics-store").getAll(),
2071
2052
  securityFindings: this.cachedFindings
2072
2053
  });
2073
- for (const fn of this.listeners) {
2074
- try {
2075
- fn(this.cachedInsights, this.cachedFindings);
2076
- } catch {
2077
- }
2078
- }
2054
+ this.cachedStatefulInsights = this.insightTracker.reconcile(this.cachedInsights);
2055
+ const update = {
2056
+ insights: this.cachedInsights,
2057
+ findings: this.cachedFindings,
2058
+ statefulFindings: this.getStatefulFindings(),
2059
+ statefulInsights: this.cachedStatefulInsights
2060
+ };
2061
+ this.registry.get("event-bus").emit("analysis:updated", update);
2079
2062
  }
2080
2063
  };
2081
2064
 
2082
2065
  // src/index.ts
2083
- var VERSION = "0.8.0";
2066
+ var VERSION = "0.8.2";
2084
2067
  export {
2085
2068
  AdapterRegistry,
2086
2069
  AnalysisEngine,