brakit 0.8.5 → 0.8.7

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
@@ -2,7 +2,7 @@
2
2
 
3
3
  <p align="center">
4
4
  <b>AI writes your code. Brakit watches what it does.</b> <br />
5
- Every request, query, and security issue, caught before you ship. <br />
5
+ Every request, query, and API call mapped to the action that triggered it. See your entire backend at a glance. <br />
6
6
  <b>Open source · Local first · Zero config · AI-native via MCP</b>
7
7
  </p>
8
8
 
@@ -32,6 +32,12 @@
32
32
 
33
33
  ---
34
34
 
35
+ <p align="center">
36
+ <img width="700" src="docs/images/actions.png" alt="Brakit Actions — endpoints grouped by user action" />
37
+ <br />
38
+ <sub>Every endpoint grouped by the action that triggered it — see "Sign Up" and "Load Dashboard", not 47 raw requests</sub>
39
+ </p>
40
+
35
41
  <p align="center">
36
42
  <img width="700" src="docs/images/dashboard.png" alt="Brakit Dashboard — issues surfaced automatically" />
37
43
  <br />
@@ -41,7 +47,7 @@
41
47
  <p align="center">
42
48
  <img width="700" src="docs/images/vscode.png" alt="Claude reading Brakit findings in VS Code" />
43
49
  <br />
44
- <sub>Claude reads findings via MCP and fixes your code</sub>
50
+ <sub>Claude reads issues via MCP and fixes your code</sub>
45
51
  </p>
46
52
 
47
53
  ## Quick Start
@@ -56,7 +62,7 @@ That's it. Brakit detects your framework, adds itself as a devDependency, and cr
56
62
  npm run dev
57
63
  ```
58
64
 
59
- Dashboard at `http://localhost:<port>/__brakit`. Insights in the terminal.
65
+ Dashboard at `http://localhost:<port>/__brakit`. Issues in the terminal.
60
66
 
61
67
  > **Requirements:** Node.js >= 18 and a project with `package.json`.
62
68
 
@@ -74,7 +80,7 @@ Dashboard at `http://localhost:<port>/__brakit`. Insights in the terminal.
74
80
  - **Full server tracing** — fetch calls, DB queries, console logs, errors — zero code changes
75
81
  - **Response overfetch** — large JSON responses with many fields your client doesn't use
76
82
  - **Performance tracking** — health grades and p95 trends across dev sessions
77
- - **AI-native via MCP** — Claude Code and Cursor can query findings, inspect endpoints, and verify fixes directly
83
+ - **AI-native via MCP** — Claude Code and Cursor can query issues, inspect endpoints, and verify fixes directly
78
84
 
79
85
  ---
80
86
 
package/dist/api.d.ts CHANGED
@@ -63,6 +63,7 @@ interface TelemetryEntry {
63
63
  timestamp: number;
64
64
  }
65
65
  interface TracedFetch extends TelemetryEntry {
66
+ fetchId?: string;
66
67
  url: string;
67
68
  method: string;
68
69
  statusCode: number;
@@ -75,11 +76,11 @@ interface TracedLog extends TelemetryEntry {
75
76
  interface TracedError extends TelemetryEntry {
76
77
  name: string;
77
78
  message: string;
78
- stack: string;
79
+ stack?: string;
79
80
  }
80
81
  type NormalizedOp = "SELECT" | "INSERT" | "UPDATE" | "DELETE" | "OTHER";
81
82
  interface TracedQuery extends TelemetryEntry {
82
- driver: "pg" | "mysql2" | "prisma" | "sdk";
83
+ driver: "pg" | "mysql2" | "prisma" | "asyncpg" | "sqlalchemy" | "sdk";
83
84
  sql?: string;
84
85
  model?: string;
85
86
  operation?: string;
@@ -88,6 +89,7 @@ interface TracedQuery extends TelemetryEntry {
88
89
  normalizedOp?: NormalizedOp;
89
90
  table?: string;
90
91
  source?: string;
92
+ parentFetchId?: string;
91
93
  }
92
94
  type TelemetryEvent = {
93
95
  type: "fetch";
@@ -160,117 +162,81 @@ interface SecurityFinding {
160
162
  count: number;
161
163
  }
162
164
 
163
- declare const FINDINGS_DATA_VERSION = 1;
164
-
165
- type FindingState = "open" | "fixing" | "resolved";
166
- type FindingSource = "passive";
165
+ type IssueState = "open" | "fixing" | "resolved" | "stale" | "regressed";
166
+ type IssueSource = "passive";
167
+ type IssueCategory = "security" | "performance" | "reliability";
167
168
  type AiFixStatus = "fixed" | "wont_fix";
168
- interface StatefulFinding {
169
- /** Stable ID derived from rule + endpoint + description hash */
170
- findingId: string;
171
- state: FindingState;
172
- source: FindingSource;
173
- finding: SecurityFinding;
174
- firstSeenAt: number;
175
- lastSeenAt: number;
176
- resolvedAt: number | null;
177
- occurrences: number;
178
- /** What AI reported after attempting a fix */
179
- aiStatus: AiFixStatus | null;
180
- /** AI's summary of what was done or why it can't be fixed */
181
- aiNotes: string | null;
182
- }
183
- interface FindingsData {
184
- version: typeof FINDINGS_DATA_VERSION;
185
- findings: StatefulFinding[];
186
- }
187
-
188
- type InsightSeverity = Severity;
189
- type InsightType = "n1" | "cross-endpoint" | "redundant-query" | "error" | "error-hotspot" | "duplicate" | "slow" | "query-heavy" | "select-star" | "high-rows" | "large-response" | "response-overfetch" | "security" | "regression";
190
- interface Insight {
191
- severity: InsightSeverity;
192
- type: InsightType;
169
+ interface Issue {
170
+ category: IssueCategory;
171
+ /** Rule identifier: "slow", "n1", "exposed-secret", etc. */
172
+ rule: string;
173
+ severity: Severity;
193
174
  title: string;
194
175
  desc: string;
195
176
  hint: string;
196
177
  detail?: string;
178
+ /** Explicit endpoint key (e.g., "GET /api/users"). */
179
+ endpoint?: string;
180
+ /** Dashboard tab to link to (e.g., "requests", "queries", "security"). */
197
181
  nav?: string;
198
182
  }
199
- interface InsightContext {
200
- requests: readonly TracedRequest[];
201
- queries: readonly TracedQuery[];
202
- errors: readonly TracedError[];
203
- flows: readonly RequestFlow[];
204
- fetches: readonly TracedFetch[];
205
- previousMetrics?: readonly EndpointMetrics[];
206
- securityFindings?: readonly SecurityFinding[];
207
- }
208
- interface EndpointGroup {
209
- total: number;
210
- errors: number;
211
- totalDuration: number;
212
- queryCount: number;
213
- totalSize: number;
214
- totalQueryTimeMs: number;
215
- totalFetchTimeMs: number;
216
- queryShapeDurations: Map<string, {
217
- totalMs: number;
218
- count: number;
219
- label: string;
220
- }>;
221
- }
222
- interface PreparedInsightContext extends InsightContext {
223
- nonStatic: readonly TracedRequest[];
224
- queriesByReq: ReadonlyMap<string, TracedQuery[]>;
225
- fetchesByReq: ReadonlyMap<string, TracedFetch[]>;
226
- reqById: ReadonlyMap<string, TracedRequest>;
227
- endpointGroups: ReadonlyMap<string, EndpointGroup>;
228
- }
229
-
230
- type InsightState = FindingState;
231
- interface StatefulInsight {
232
- key: string;
233
- state: InsightState;
234
- insight: Insight;
183
+ interface StatefulIssue {
184
+ /** Stable ID derived from rule + endpoint + description hash. */
185
+ issueId: string;
186
+ state: IssueState;
187
+ source: IssueSource;
188
+ category: IssueCategory;
189
+ issue: Issue;
235
190
  firstSeenAt: number;
236
191
  lastSeenAt: number;
237
192
  resolvedAt: number | null;
238
- /** Consecutive recompute cycles where the insight was not detected. */
239
- consecutiveAbsences: number;
193
+ occurrences: number;
194
+ /**
195
+ * Number of requests to this endpoint that did NOT reproduce the issue
196
+ * since it was last seen. Used for evidence-based resolution.
197
+ */
198
+ cleanHitsSinceLastSeen: number;
199
+ /** What AI reported after attempting a fix. */
240
200
  aiStatus: AiFixStatus | null;
201
+ /** AI's summary of what was done or why it can't be fixed. */
241
202
  aiNotes: string | null;
242
203
  }
204
+ interface IssuesData {
205
+ version: 2;
206
+ issues: StatefulIssue[];
207
+ }
243
208
 
244
- declare class FindingStore {
245
- private rootDir;
246
- private findings;
209
+ declare class IssueStore {
210
+ private dataDir;
211
+ private issues;
247
212
  private flushTimer;
248
213
  private dirty;
249
214
  private readonly writer;
250
- private readonly findingsPath;
251
- constructor(rootDir: string);
215
+ private readonly issuesPath;
216
+ constructor(dataDir: string);
252
217
  start(): void;
253
218
  stop(): void;
254
- upsert(finding: SecurityFinding, source: FindingSource): StatefulFinding;
255
- transition(findingId: string, state: FindingState): boolean;
256
- reportFix(findingId: string, status: AiFixStatus, notes: string): boolean;
219
+ upsert(issue: Issue, source: IssueSource): StatefulIssue;
257
220
  /**
258
- * Reconcile passive findings against the current analysis results.
221
+ * Reconcile issues against the current analysis results using evidence-based resolution.
259
222
  *
260
- * Passive findings are detected by continuous scanning (not user-triggered).
261
- * When a previously-seen finding is absent from the current results, it means
262
- * the issue has been fixed — transition it to "resolved" automatically.
263
- * Active findings (from MCP verify-fix) are not auto-resolved because they
264
- * require explicit verification.
223
+ * @param currentIssueIds - IDs of issues detected in the current analysis cycle
224
+ * @param activeEndpoints - Endpoints that had requests in the current cycle
265
225
  */
266
- reconcilePassive(currentFindings: readonly SecurityFinding[]): void;
267
- getAll(): readonly StatefulFinding[];
268
- getByState(state: FindingState): readonly StatefulFinding[];
269
- get(findingId: string): StatefulFinding | undefined;
226
+ reconcile(currentIssueIds: Set<string>, activeEndpoints: Set<string>): void;
227
+ transition(issueId: string, state: IssueState): boolean;
228
+ reportFix(issueId: string, status: AiFixStatus, notes: string): boolean;
229
+ getAll(): readonly StatefulIssue[];
230
+ getByState(state: IssueState): readonly StatefulIssue[];
231
+ getByCategory(category: IssueCategory): readonly StatefulIssue[];
232
+ get(issueId: string): StatefulIssue | undefined;
270
233
  clear(): void;
234
+ isDirty(): boolean;
271
235
  private loadAsync;
272
236
  /** Sync load for tests only — not used in production paths. */
273
237
  loadSync(): void;
238
+ /** Parse and populate issues from a raw JSON string. */
239
+ private hydrate;
274
240
  private flush;
275
241
  private flushSync;
276
242
  private serialize;
@@ -283,9 +249,15 @@ interface BrakitAdapter {
283
249
  unpatch?(): void;
284
250
  }
285
251
 
252
+ /** Pre-parsed JSON bodies keyed by request ID, built once per scan cycle. */
253
+ interface ParsedBodyCache {
254
+ response: Map<string, unknown>;
255
+ request: Map<string, unknown>;
256
+ }
286
257
  interface SecurityContext {
287
258
  requests: readonly TracedRequest[];
288
259
  logs: readonly TracedLog[];
260
+ parsedBodies: ParsedBodyCache;
289
261
  }
290
262
  interface SecurityRule {
291
263
  id: string;
@@ -298,11 +270,56 @@ interface SecurityRule {
298
270
  declare class SecurityScanner {
299
271
  private rules;
300
272
  register(rule: SecurityRule): void;
301
- scan(ctx: SecurityContext): SecurityFinding[];
273
+ scan(input: {
274
+ requests: readonly TracedRequest[];
275
+ logs: readonly TracedLog[];
276
+ }): SecurityFinding[];
302
277
  getRules(): readonly SecurityRule[];
303
278
  }
304
279
  declare function createDefaultScanner(): SecurityScanner;
305
280
 
281
+ type InsightSeverity = Severity;
282
+ type InsightType = "n1" | "cross-endpoint" | "redundant-query" | "error" | "error-hotspot" | "duplicate" | "slow" | "query-heavy" | "select-star" | "high-rows" | "large-response" | "response-overfetch" | "security" | "regression";
283
+ interface Insight {
284
+ severity: InsightSeverity;
285
+ type: InsightType;
286
+ title: string;
287
+ desc: string;
288
+ hint: string;
289
+ detail?: string;
290
+ nav?: string;
291
+ }
292
+ interface InsightContext {
293
+ requests: readonly TracedRequest[];
294
+ queries: readonly TracedQuery[];
295
+ errors: readonly TracedError[];
296
+ flows: readonly RequestFlow[];
297
+ fetches: readonly TracedFetch[];
298
+ previousMetrics?: readonly EndpointMetrics[];
299
+ securityFindings?: readonly SecurityFinding[];
300
+ }
301
+ interface EndpointGroup {
302
+ total: number;
303
+ errors: number;
304
+ totalDuration: number;
305
+ queryCount: number;
306
+ totalSize: number;
307
+ totalQueryTimeMs: number;
308
+ totalFetchTimeMs: number;
309
+ queryShapeDurations: Map<string, {
310
+ totalMs: number;
311
+ count: number;
312
+ label: string;
313
+ }>;
314
+ }
315
+ interface PreparedInsightContext extends InsightContext {
316
+ nonStatic: readonly TracedRequest[];
317
+ queriesByReq: ReadonlyMap<string, TracedQuery[]>;
318
+ fetchesByReq: ReadonlyMap<string, TracedFetch[]>;
319
+ reqById: ReadonlyMap<string, TracedRequest>;
320
+ endpointGroups: ReadonlyMap<string, EndpointGroup>;
321
+ }
322
+
306
323
  interface InsightRule {
307
324
  id: InsightType;
308
325
  check(ctx: PreparedInsightContext): Insight[];
@@ -331,8 +348,7 @@ declare class AdapterRegistry {
331
348
  interface AnalysisUpdate {
332
349
  insights: readonly Insight[];
333
350
  findings: readonly SecurityFinding[];
334
- statefulFindings: readonly StatefulFinding[];
335
- statefulInsights: readonly StatefulInsight[];
351
+ issues: readonly StatefulIssue[];
336
352
  }
337
353
  interface ChannelMap {
338
354
  "telemetry:fetch": Omit<TracedFetch, "id">;
@@ -341,7 +357,7 @@ interface ChannelMap {
341
357
  "telemetry:error": Omit<TracedError, "id">;
342
358
  "request:completed": TracedRequest;
343
359
  "analysis:updated": AnalysisUpdate;
344
- "findings:changed": readonly StatefulFinding[];
360
+ "issues:changed": readonly StatefulIssue[];
345
361
  "store:cleared": undefined;
346
362
  }
347
363
  type Listener<T> = (data: T) => void;
@@ -367,6 +383,10 @@ interface CaptureInput {
367
383
  config: Pick<BrakitConfig, "maxBodyCapture">;
368
384
  }
369
385
 
386
+ interface Lifecycle {
387
+ start(): void;
388
+ stop(): void;
389
+ }
370
390
  interface TelemetryStoreInterface<T extends TelemetryEntry> {
371
391
  add(data: Omit<T, "id">): T;
372
392
  getAll(): readonly T[];
@@ -379,36 +399,28 @@ interface RequestStoreInterface {
379
399
  getAll(): readonly TracedRequest[];
380
400
  clear(): void;
381
401
  }
382
- interface MetricsStoreInterface {
402
+ interface MetricsStoreInterface extends Lifecycle {
383
403
  recordRequest(req: TracedRequest, metrics: RequestMetrics): void;
384
404
  getAll(): readonly EndpointMetrics[];
385
405
  getEndpoint(endpoint: string): EndpointMetrics | undefined;
386
406
  getLiveEndpoints(): LiveEndpointData[];
387
407
  reset(): void;
388
- start(): void;
389
- stop(): void;
390
408
  }
391
- interface FindingStoreInterface {
392
- upsert(finding: SecurityFinding, source: FindingSource): StatefulFinding;
393
- transition(findingId: string, state: FindingState): boolean;
394
- reportFix(findingId: string, status: AiFixStatus, notes: string): boolean;
395
- reconcilePassive(findings: readonly SecurityFinding[]): void;
396
- getAll(): readonly StatefulFinding[];
397
- getByState(state: FindingState): readonly StatefulFinding[];
398
- get(findingId: string): StatefulFinding | undefined;
409
+ interface IssueStoreInterface extends Lifecycle {
410
+ upsert(issue: Issue, source: IssueSource): StatefulIssue;
411
+ transition(issueId: string, state: IssueState): boolean;
412
+ reportFix(issueId: string, status: AiFixStatus, notes: string): boolean;
413
+ reconcile(currentIssueIds: Set<string>, activeEndpoints: Set<string>): void;
414
+ getAll(): readonly StatefulIssue[];
415
+ getByState(state: IssueState): readonly StatefulIssue[];
416
+ getByCategory(category: IssueCategory): readonly StatefulIssue[];
417
+ get(issueId: string): StatefulIssue | undefined;
399
418
  clear(): void;
400
- start(): void;
401
- stop(): void;
402
419
  }
403
- interface AnalysisEngineInterface {
404
- start(): void;
405
- stop(): void;
420
+ interface AnalysisEngineInterface extends Lifecycle {
406
421
  recompute(): void;
407
422
  getInsights(): readonly Insight[];
408
423
  getFindings(): readonly SecurityFinding[];
409
- getStatefulInsights(): readonly StatefulInsight[];
410
- getStatefulFindings(): readonly StatefulFinding[];
411
- reportInsightFix(enrichedId: string, status: AiFixStatus, notes: string): boolean;
412
424
  }
413
425
 
414
426
  interface ServiceMap {
@@ -419,7 +431,7 @@ interface ServiceMap {
419
431
  "log-store": TelemetryStoreInterface<TracedLog>;
420
432
  "error-store": TelemetryStoreInterface<TracedError>;
421
433
  "metrics-store": MetricsStoreInterface;
422
- "finding-store": FindingStoreInterface;
434
+ "issue-store": IssueStoreInterface;
423
435
  "analysis-engine": AnalysisEngineInterface;
424
436
  }
425
437
  declare class ServiceRegistry {
@@ -433,10 +445,8 @@ declare class AnalysisEngine {
433
445
  private registry;
434
446
  private debounceMs;
435
447
  private scanner;
436
- private insightTracker;
437
448
  private cachedInsights;
438
449
  private cachedFindings;
439
- private cachedStatefulInsights;
440
450
  private debounceTimer;
441
451
  private subs;
442
452
  constructor(registry: ServiceRegistry, debounceMs?: number);
@@ -444,13 +454,10 @@ declare class AnalysisEngine {
444
454
  stop(): void;
445
455
  getInsights(): readonly Insight[];
446
456
  getFindings(): readonly SecurityFinding[];
447
- getStatefulFindings(): readonly StatefulFinding[];
448
- getStatefulInsights(): readonly StatefulInsight[];
449
- reportInsightFix(enrichedId: string, status: AiFixStatus, notes: string): boolean;
450
457
  private scheduleRecompute;
451
458
  recompute(): void;
452
459
  }
453
460
 
454
461
  declare const VERSION: string;
455
462
 
456
- export { AdapterRegistry, AnalysisEngine, type BrakitAdapter, type BrakitConfig, type DetectedProject, type FindingSource, type FindingState, FindingStore, type FindingsData, type FlatHeaders, type Framework, type HttpMethod, type Insight, type InsightContext, type InsightRule, InsightRunner, type NormalizedOp, type RequestCategory, type RequestListener, type SecurityContext, type SecurityFinding, type SecurityRule, SecurityScanner, type SecuritySeverity, type StatefulFinding, type TracedRequest, VERSION, computeInsights, createDefaultInsightRunner, createDefaultScanner, detectProject };
463
+ export { AdapterRegistry, AnalysisEngine, type BrakitAdapter, type BrakitConfig, type DetectedProject, type FlatHeaders, type Framework, type HttpMethod, type Insight, type InsightContext, type InsightRule, InsightRunner, type IssueCategory, type IssueSource, type IssueState, IssueStore, type IssuesData, type NormalizedOp, type RequestCategory, type RequestListener, type SecurityContext, type SecurityFinding, type SecurityRule, SecurityScanner, type SecuritySeverity, type StatefulIssue, type TracedRequest, VERSION, computeInsights, createDefaultInsightRunner, createDefaultScanner, detectProject };