brakit 0.8.4 → 0.8.6

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