brakit 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,6 +22,9 @@
22
22
  <a href="https://typescriptlang.org">
23
23
  <img src="https://img.shields.io/badge/built%20with-TypeScript-3178c6.svg" alt="TypeScript" />
24
24
  </a>
25
+ <a href="https://www.npmjs.com/package/brakit">
26
+ <img src="https://img.shields.io/npm/v/brakit" alt="npm version" />
27
+ </a>
25
28
  <a href="CONTRIBUTING.md">
26
29
  <img src="https://img.shields.io/badge/PRs-Welcome-brightgreen" alt="PRs welcome!" />
27
30
  </a>
@@ -30,7 +33,15 @@
30
33
  ---
31
34
 
32
35
  <p align="center">
33
- <img width="700" src="docs/images/dashboard.png" alt="Brakit Dashboard" />
36
+ <img width="700" src="docs/images/dashboard.png" alt="Brakit Dashboard — issues surfaced automatically" />
37
+ <br />
38
+ <sub>Brakit catches N+1 queries, PII leaks, and slow endpoints as you develop</sub>
39
+ </p>
40
+
41
+ <p align="center">
42
+ <img width="700" src="docs/images/vscode.png" alt="Claude reading Brakit findings in VS Code" />
43
+ <br />
44
+ <sub>Claude reads findings via MCP and fixes your code</sub>
34
45
  </p>
35
46
 
36
47
  ## Quick Start
package/dist/api.d.ts CHANGED
@@ -149,7 +149,9 @@ interface RequestMetrics {
149
149
  fetchTimeMs: number;
150
150
  }
151
151
 
152
- type SecuritySeverity = "critical" | "warning" | "info";
152
+ /** Shared severity levels used by both security findings and insights. */
153
+ type Severity = "critical" | "warning" | "info";
154
+ type SecuritySeverity = Severity;
153
155
  interface SecurityFinding {
154
156
  severity: SecuritySeverity;
155
157
  rule: string;
@@ -178,6 +180,60 @@ interface FindingsData {
178
180
  findings: StatefulFinding[];
179
181
  }
180
182
 
183
+ type InsightSeverity = Severity;
184
+ type InsightType = "n1" | "cross-endpoint" | "redundant-query" | "error" | "error-hotspot" | "duplicate" | "slow" | "query-heavy" | "select-star" | "high-rows" | "large-response" | "response-overfetch" | "security" | "regression";
185
+ interface Insight {
186
+ severity: InsightSeverity;
187
+ type: InsightType;
188
+ title: string;
189
+ desc: string;
190
+ hint: string;
191
+ detail?: string;
192
+ nav?: string;
193
+ }
194
+ interface InsightContext {
195
+ requests: readonly TracedRequest[];
196
+ queries: readonly TracedQuery[];
197
+ errors: readonly TracedError[];
198
+ flows: readonly RequestFlow[];
199
+ fetches: readonly TracedFetch[];
200
+ previousMetrics?: readonly EndpointMetrics[];
201
+ securityFindings?: readonly SecurityFinding[];
202
+ }
203
+ interface EndpointGroup {
204
+ total: number;
205
+ errors: number;
206
+ totalDuration: number;
207
+ queryCount: number;
208
+ totalSize: number;
209
+ totalQueryTimeMs: number;
210
+ totalFetchTimeMs: number;
211
+ queryShapeDurations: Map<string, {
212
+ totalMs: number;
213
+ count: number;
214
+ label: string;
215
+ }>;
216
+ }
217
+ interface PreparedInsightContext extends InsightContext {
218
+ nonStatic: readonly TracedRequest[];
219
+ queriesByReq: ReadonlyMap<string, TracedQuery[]>;
220
+ fetchesByReq: ReadonlyMap<string, TracedFetch[]>;
221
+ reqById: ReadonlyMap<string, TracedRequest>;
222
+ endpointGroups: ReadonlyMap<string, EndpointGroup>;
223
+ }
224
+
225
+ type InsightState = "open" | "resolved";
226
+ interface StatefulInsight {
227
+ key: string;
228
+ state: InsightState;
229
+ insight: Insight;
230
+ firstSeenAt: number;
231
+ lastSeenAt: number;
232
+ resolvedAt: number | null;
233
+ /** Consecutive recompute cycles where the insight was not detected. */
234
+ consecutiveAbsences: number;
235
+ }
236
+
181
237
  declare class FindingStore {
182
238
  private rootDir;
183
239
  private findings;
@@ -231,48 +287,6 @@ declare class SecurityScanner {
231
287
  }
232
288
  declare function createDefaultScanner(): SecurityScanner;
233
289
 
234
- type InsightSeverity = "critical" | "warning" | "info";
235
- type InsightType = "n1" | "cross-endpoint" | "redundant-query" | "error" | "error-hotspot" | "duplicate" | "slow" | "query-heavy" | "select-star" | "high-rows" | "large-response" | "response-overfetch" | "security" | "regression";
236
- interface Insight {
237
- severity: InsightSeverity;
238
- type: InsightType;
239
- title: string;
240
- desc: string;
241
- hint: string;
242
- detail?: string;
243
- nav?: string;
244
- }
245
- interface InsightContext {
246
- requests: readonly TracedRequest[];
247
- queries: readonly TracedQuery[];
248
- errors: readonly TracedError[];
249
- flows: readonly RequestFlow[];
250
- fetches: readonly TracedFetch[];
251
- previousMetrics?: readonly EndpointMetrics[];
252
- securityFindings?: readonly SecurityFinding[];
253
- }
254
- interface EndpointGroup {
255
- total: number;
256
- errors: number;
257
- totalDuration: number;
258
- queryCount: number;
259
- totalSize: number;
260
- totalQueryTimeMs: number;
261
- totalFetchTimeMs: number;
262
- queryShapeDurations: Map<string, {
263
- totalMs: number;
264
- count: number;
265
- label: string;
266
- }>;
267
- }
268
- interface PreparedInsightContext extends InsightContext {
269
- nonStatic: readonly TracedRequest[];
270
- queriesByReq: ReadonlyMap<string, TracedQuery[]>;
271
- fetchesByReq: ReadonlyMap<string, TracedFetch[]>;
272
- reqById: ReadonlyMap<string, TracedRequest>;
273
- endpointGroups: ReadonlyMap<string, EndpointGroup>;
274
- }
275
-
276
290
  interface InsightRule {
277
291
  id: InsightType;
278
292
  check(ctx: PreparedInsightContext): Insight[];
@@ -326,14 +340,22 @@ declare class MetricsStore {
326
340
  private getOrCreateEndpoint;
327
341
  }
328
342
 
329
- type AnalysisListener = (insights: Insight[], findings: SecurityFinding[]) => void;
343
+ interface AnalysisUpdate {
344
+ insights: Insight[];
345
+ findings: SecurityFinding[];
346
+ statefulFindings: readonly StatefulFinding[];
347
+ statefulInsights: readonly StatefulInsight[];
348
+ }
349
+ type AnalysisListener = (update: AnalysisUpdate) => void;
330
350
  declare class AnalysisEngine {
331
351
  private metricsStore;
332
352
  private findingStore?;
333
353
  private debounceMs;
334
354
  private scanner;
355
+ private insightTracker;
335
356
  private cachedInsights;
336
357
  private cachedFindings;
358
+ private cachedStatefulInsights;
337
359
  private debounceTimer;
338
360
  private listeners;
339
361
  private boundRequestListener;
@@ -347,6 +369,8 @@ declare class AnalysisEngine {
347
369
  offUpdate(fn: AnalysisListener): void;
348
370
  getInsights(): readonly Insight[];
349
371
  getFindings(): readonly SecurityFinding[];
372
+ getStatefulFindings(): readonly StatefulFinding[];
373
+ getStatefulInsights(): readonly StatefulInsight[];
350
374
  private scheduleRecompute;
351
375
  recompute(): void;
352
376
  }
package/dist/api.js CHANGED
@@ -15,6 +15,7 @@ var DASHBOARD_PREFIX = "/__brakit";
15
15
  // src/constants/limits.ts
16
16
  var MAX_REQUEST_ENTRIES = 1e3;
17
17
  var MAX_TELEMETRY_ENTRIES = 1e3;
18
+ var MAX_INGEST_BYTES = 10 * 1024 * 1024;
18
19
 
19
20
  // src/constants/thresholds.ts
20
21
  var FLOW_GAP_MS = 5e3;
@@ -43,6 +44,9 @@ var QUERY_COUNT_REGRESSION_RATIO = 1.5;
43
44
  var OVERFETCH_MANY_FIELDS = 12;
44
45
  var OVERFETCH_UNWRAP_MIN_SIZE = 3;
45
46
  var MAX_DUPLICATE_INSIGHTS = 3;
47
+ var INSIGHT_WINDOW_PER_ENDPOINT = 2;
48
+ var RESOLVE_AFTER_ABSENCES = 3;
49
+ var RESOLVED_INSIGHT_TTL_MS = 18e5;
46
50
 
47
51
  // src/constants/metrics.ts
48
52
  var METRICS_DIR = ".brakit";
@@ -970,6 +974,10 @@ import { randomUUID as randomUUID2 } from "crypto";
970
974
  function getEndpointKey(method, path) {
971
975
  return `${method} ${path}`;
972
976
  }
977
+ var ENDPOINT_PREFIX_RE = /^(\S+\s+\S+)/;
978
+ function extractEndpointFromDesc(desc) {
979
+ return desc.match(ENDPOINT_PREFIX_RE)?.[1] ?? null;
980
+ }
973
981
 
974
982
  // src/store/metrics/persistence.ts
975
983
  import {
@@ -1410,6 +1418,23 @@ function createEndpointGroup() {
1410
1418
  queryShapeDurations: /* @__PURE__ */ new Map()
1411
1419
  };
1412
1420
  }
1421
+ function windowByEndpoint(requests) {
1422
+ const byEndpoint = /* @__PURE__ */ new Map();
1423
+ for (const r of requests) {
1424
+ const ep = getEndpointKey(r.method, r.path);
1425
+ let list = byEndpoint.get(ep);
1426
+ if (!list) {
1427
+ list = [];
1428
+ byEndpoint.set(ep, list);
1429
+ }
1430
+ list.push(r);
1431
+ }
1432
+ const windowed = [];
1433
+ for (const [, reqs] of byEndpoint) {
1434
+ windowed.push(...reqs.slice(-INSIGHT_WINDOW_PER_ENDPOINT));
1435
+ }
1436
+ return windowed;
1437
+ }
1413
1438
  function prepareContext(ctx) {
1414
1439
  const nonStatic = ctx.requests.filter(
1415
1440
  (r) => !r.isStatic && (!r.path || !r.path.startsWith(DASHBOARD_PREFIX))
@@ -1417,8 +1442,9 @@ function prepareContext(ctx) {
1417
1442
  const queriesByReq = groupBy(ctx.queries, (q) => q.parentRequestId);
1418
1443
  const fetchesByReq = groupBy(ctx.fetches, (f) => f.parentRequestId);
1419
1444
  const reqById = new Map(nonStatic.map((r) => [r.id, r]));
1445
+ const recent = windowByEndpoint(nonStatic);
1420
1446
  const endpointGroups = /* @__PURE__ */ new Map();
1421
- for (const r of nonStatic) {
1447
+ for (const r of recent) {
1422
1448
  const ep = getEndpointKey(r.method, r.path);
1423
1449
  let g = endpointGroups.get(ep);
1424
1450
  if (!g) {
@@ -1990,6 +2016,61 @@ function computeInsights(ctx) {
1990
2016
  return createDefaultInsightRunner().run(ctx);
1991
2017
  }
1992
2018
 
2019
+ // src/analysis/insight-tracker.ts
2020
+ function computeInsightKey(insight) {
2021
+ const identifier = extractEndpointFromDesc(insight.desc) ?? insight.title;
2022
+ return `${insight.type}:${identifier}`;
2023
+ }
2024
+ var InsightTracker = class {
2025
+ tracked = /* @__PURE__ */ new Map();
2026
+ reconcile(current) {
2027
+ const currentKeys = /* @__PURE__ */ new Set();
2028
+ const now = Date.now();
2029
+ for (const insight of current) {
2030
+ const key = computeInsightKey(insight);
2031
+ currentKeys.add(key);
2032
+ const existing = this.tracked.get(key);
2033
+ if (existing) {
2034
+ existing.insight = insight;
2035
+ existing.lastSeenAt = now;
2036
+ existing.consecutiveAbsences = 0;
2037
+ if (existing.state === "resolved") {
2038
+ existing.state = "open";
2039
+ existing.resolvedAt = null;
2040
+ }
2041
+ } else {
2042
+ this.tracked.set(key, {
2043
+ key,
2044
+ state: "open",
2045
+ insight,
2046
+ firstSeenAt: now,
2047
+ lastSeenAt: now,
2048
+ resolvedAt: null,
2049
+ consecutiveAbsences: 0
2050
+ });
2051
+ }
2052
+ }
2053
+ for (const [key, stateful] of this.tracked) {
2054
+ if (stateful.state === "open" && !currentKeys.has(stateful.key)) {
2055
+ stateful.consecutiveAbsences++;
2056
+ if (stateful.consecutiveAbsences >= RESOLVE_AFTER_ABSENCES) {
2057
+ stateful.state = "resolved";
2058
+ stateful.resolvedAt = now;
2059
+ }
2060
+ } else if (stateful.state === "resolved" && stateful.resolvedAt !== null && now - stateful.resolvedAt > RESOLVED_INSIGHT_TTL_MS) {
2061
+ this.tracked.delete(key);
2062
+ }
2063
+ }
2064
+ return [...this.tracked.values()];
2065
+ }
2066
+ getAll() {
2067
+ return [...this.tracked.values()];
2068
+ }
2069
+ clear() {
2070
+ this.tracked.clear();
2071
+ }
2072
+ };
2073
+
1993
2074
  // src/analysis/engine.ts
1994
2075
  var AnalysisEngine = class {
1995
2076
  constructor(metricsStore, findingStore, debounceMs = 300) {
@@ -2003,8 +2084,10 @@ var AnalysisEngine = class {
2003
2084
  this.boundLogListener = () => this.scheduleRecompute();
2004
2085
  }
2005
2086
  scanner;
2087
+ insightTracker = new InsightTracker();
2006
2088
  cachedInsights = [];
2007
2089
  cachedFindings = [];
2090
+ cachedStatefulInsights = [];
2008
2091
  debounceTimer = null;
2009
2092
  listeners = [];
2010
2093
  boundRequestListener;
@@ -2040,6 +2123,12 @@ var AnalysisEngine = class {
2040
2123
  getFindings() {
2041
2124
  return this.cachedFindings;
2042
2125
  }
2126
+ getStatefulFindings() {
2127
+ return this.findingStore?.getAll() ?? [];
2128
+ }
2129
+ getStatefulInsights() {
2130
+ return this.cachedStatefulInsights;
2131
+ }
2043
2132
  scheduleRecompute() {
2044
2133
  if (this.debounceTimer) return;
2045
2134
  this.debounceTimer = setTimeout(() => {
@@ -2070,9 +2159,16 @@ var AnalysisEngine = class {
2070
2159
  previousMetrics: this.metricsStore.getAll(),
2071
2160
  securityFindings: this.cachedFindings
2072
2161
  });
2162
+ this.cachedStatefulInsights = this.insightTracker.reconcile(this.cachedInsights);
2163
+ const update = {
2164
+ insights: this.cachedInsights,
2165
+ findings: this.cachedFindings,
2166
+ statefulFindings: this.getStatefulFindings(),
2167
+ statefulInsights: this.cachedStatefulInsights
2168
+ };
2073
2169
  for (const fn of this.listeners) {
2074
2170
  try {
2075
- fn(this.cachedInsights, this.cachedFindings);
2171
+ fn(update);
2076
2172
  } catch {
2077
2173
  }
2078
2174
  }
@@ -2080,7 +2176,7 @@ var AnalysisEngine = class {
2080
2176
  };
2081
2177
 
2082
2178
  // src/index.ts
2083
- var VERSION = "0.8.0";
2179
+ var VERSION = "0.8.1";
2084
2180
  export {
2085
2181
  AdapterRegistry,
2086
2182
  AnalysisEngine,
@@ -28,12 +28,13 @@ var init_routes = __esm({
28
28
  });
29
29
 
30
30
  // src/constants/limits.ts
31
- var MAX_REQUEST_ENTRIES, MAX_TELEMETRY_ENTRIES;
31
+ var MAX_REQUEST_ENTRIES, MAX_TELEMETRY_ENTRIES, MAX_INGEST_BYTES;
32
32
  var init_limits = __esm({
33
33
  "src/constants/limits.ts"() {
34
34
  "use strict";
35
35
  MAX_REQUEST_ENTRIES = 1e3;
36
36
  MAX_TELEMETRY_ENTRIES = 1e3;
37
+ MAX_INGEST_BYTES = 10 * 1024 * 1024;
37
38
  }
38
39
  });
39
40
 
@@ -83,7 +84,7 @@ var init_mcp = __esm({
83
84
  "src/constants/mcp.ts"() {
84
85
  "use strict";
85
86
  MCP_SERVER_NAME = "brakit";
86
- MCP_SERVER_VERSION = "0.8.0";
87
+ MCP_SERVER_VERSION = "0.8.1";
87
88
  INITIAL_DISCOVERY_TIMEOUT_MS = 5e3;
88
89
  LAZY_DISCOVERY_TIMEOUT_MS = 2e3;
89
90
  CLIENT_FETCH_TIMEOUT_MS = 1e4;
@@ -300,7 +301,9 @@ async function enrichFindings(client) {
300
301
  context
301
302
  });
302
303
  }
303
- for (const i of insightsData.insights) {
304
+ for (const si of insightsData.insights) {
305
+ if (si.state === "resolved") continue;
306
+ const i = si.insight;
304
307
  if (!ENRICHMENT_SEVERITY_FILTER.includes(i.severity)) continue;
305
308
  const endpoint = i.nav ?? "global";
306
309
  enriched.push({
@@ -690,13 +693,14 @@ var init_get_report = __esm({
690
693
  (s, ep) => s + ep.summary.totalRequests,
691
694
  0
692
695
  );
696
+ const openInsightCount = insightsData.insights.filter((si) => si.state === "open").length;
693
697
  const lines = [
694
698
  "=== Brakit Report ===",
695
699
  "",
696
700
  `Endpoints observed: ${metricsData.endpoints.length}`,
697
701
  `Total requests captured: ${totalRequests}`,
698
702
  `Active security rules: ${securityData.findings.length} finding(s)`,
699
- `Performance insights: ${insightsData.insights.length} insight(s)`,
703
+ `Performance insights: ${openInsightCount} open, ${insightsData.insights.length - openInsightCount} resolved`,
700
704
  "",
701
705
  "--- Finding Summary ---",
702
706
  `Total: ${findings.length}`,
@@ -1642,6 +1646,7 @@ init_constants();
1642
1646
  // src/analysis/insights/prepare.ts
1643
1647
  init_endpoint();
1644
1648
  init_constants();
1649
+ init_thresholds();
1645
1650
 
1646
1651
  // src/analysis/insights/rules/n1.ts
1647
1652
  init_endpoint();
@@ -1683,8 +1688,12 @@ init_thresholds();
1683
1688
  // src/analysis/insights/rules/regression.ts
1684
1689
  init_thresholds();
1685
1690
 
1691
+ // src/analysis/insight-tracker.ts
1692
+ init_endpoint();
1693
+ init_thresholds();
1694
+
1686
1695
  // src/index.ts
1687
- var VERSION = "0.8.0";
1696
+ var VERSION = "0.8.1";
1688
1697
 
1689
1698
  // src/cli/commands/install.ts
1690
1699
  var IMPORT_LINE = `import "brakit";`;
@@ -22,7 +22,7 @@ var DASHBOARD_API_FINDINGS = "/__brakit/api/findings";
22
22
 
23
23
  // src/constants/mcp.ts
24
24
  var MCP_SERVER_NAME = "brakit";
25
- var MCP_SERVER_VERSION = "0.8.0";
25
+ var MCP_SERVER_VERSION = "0.8.1";
26
26
  var INITIAL_DISCOVERY_TIMEOUT_MS = 5e3;
27
27
  var LAZY_DISCOVERY_TIMEOUT_MS = 2e3;
28
28
  var CLIENT_FETCH_TIMEOUT_MS = 1e4;
@@ -110,6 +110,9 @@ var BrakitClient = class {
110
110
  import { readFileSync, existsSync } from "fs";
111
111
  import { resolve } from "path";
112
112
 
113
+ // src/constants/limits.ts
114
+ var MAX_INGEST_BYTES = 10 * 1024 * 1024;
115
+
113
116
  // src/constants/metrics.ts
114
117
  var PORT_FILE = ".brakit/port";
115
118
 
@@ -204,7 +207,9 @@ async function enrichFindings(client) {
204
207
  context
205
208
  });
206
209
  }
207
- for (const i of insightsData.insights) {
210
+ for (const si of insightsData.insights) {
211
+ if (si.state === "resolved") continue;
212
+ const i = si.insight;
208
213
  if (!ENRICHMENT_SEVERITY_FILTER.includes(i.severity)) continue;
209
214
  const endpoint = i.nav ?? "global";
210
215
  enriched.push({
@@ -553,13 +558,14 @@ var getReport = {
553
558
  (s, ep) => s + ep.summary.totalRequests,
554
559
  0
555
560
  );
561
+ const openInsightCount = insightsData.insights.filter((si) => si.state === "open").length;
556
562
  const lines = [
557
563
  "=== Brakit Report ===",
558
564
  "",
559
565
  `Endpoints observed: ${metricsData.endpoints.length}`,
560
566
  `Total requests captured: ${totalRequests}`,
561
567
  `Active security rules: ${securityData.findings.length} finding(s)`,
562
- `Performance insights: ${insightsData.insights.length} insight(s)`,
568
+ `Performance insights: ${openInsightCount} open, ${insightsData.insights.length - openInsightCount} resolved`,
563
569
  "",
564
570
  "--- Finding Summary ---",
565
571
  `Total: ${findings.length}`,