brakit 0.10.0 → 0.10.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.d.ts CHANGED
@@ -1,5 +1,38 @@
1
1
  import { IncomingHttpHeaders } from 'node:http';
2
2
 
3
+ /**
4
+ * Canonical type definitions shared between server and browser client.
5
+ *
6
+ * Both `src/types/telemetry.ts` (server) and
7
+ * `src/dashboard/client/store/types.ts` (browser) import from here.
8
+ * This is the single source of truth for union types used across the
9
+ * bundling boundary.
10
+ */
11
+ type DbDriver = "pg" | "mysql2" | "prisma" | "asyncpg" | "sqlalchemy" | "sdk";
12
+ type LogLevel = "log" | "warn" | "error" | "info" | "debug";
13
+ type NormalizedOp = "SELECT" | "INSERT" | "UPDATE" | "DELETE" | "OTHER";
14
+ type Severity = "critical" | "warning" | "info";
15
+ /**
16
+ * State machine for issue lifecycle:
17
+ *
18
+ * open ──→ fixing ──→ resolved
19
+ * │ │ │
20
+ * │ ▼ ▼
21
+ * │ regressed (pruned after PRUNE_ISSUE_TTL_MS)
22
+ * │
23
+ * └──→ stale (no traffic for STALE_ISSUE_TTL_MS)
24
+ */
25
+ type IssueState = "open" | "fixing" | "resolved" | "stale" | "regressed";
26
+ type IssueSource = "passive";
27
+ type IssueCategory = "security" | "performance" | "reliability";
28
+ type AiFixStatus = "fixed" | "wont_fix";
29
+ interface SourceLocation {
30
+ file: string;
31
+ line: number;
32
+ column?: number;
33
+ fn?: string;
34
+ }
35
+
3
36
  type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";
4
37
  type FlatHeaders = Record<string, string>;
5
38
  interface TracedRequest {
@@ -20,7 +53,7 @@ interface TracedRequest {
20
53
  }
21
54
  type RequestListener = (req: TracedRequest) => void;
22
55
 
23
- type Framework = "nextjs" | "remix" | "nuxt" | "vite" | "astro" | "flask" | "fastapi" | "django" | "custom" | "unknown";
56
+ type Framework = "nextjs" | "remix" | "nuxt" | "vite" | "astro" | "express" | "fastify" | "koa" | "hono" | "nestjs" | "hapi" | "adonis" | "sails" | "flask" | "fastapi" | "django" | "custom" | "unknown";
24
57
  interface DetectedProject {
25
58
  framework: Framework;
26
59
  devCommand: string;
@@ -69,9 +102,10 @@ interface TracedFetch extends TelemetryEntry {
69
102
  method: string;
70
103
  statusCode: number;
71
104
  durationMs: number;
105
+ callSite?: SourceLocation;
72
106
  }
73
107
  interface TracedLog extends TelemetryEntry {
74
- level: "log" | "warn" | "error" | "info" | "debug";
108
+ level: LogLevel;
75
109
  message: string;
76
110
  }
77
111
  interface TracedError extends TelemetryEntry {
@@ -79,9 +113,8 @@ interface TracedError extends TelemetryEntry {
79
113
  message: string;
80
114
  stack?: string;
81
115
  }
82
- type NormalizedOp = "SELECT" | "INSERT" | "UPDATE" | "DELETE" | "OTHER";
83
116
  interface TracedQuery extends TelemetryEntry {
84
- driver: "pg" | "mysql2" | "prisma" | "asyncpg" | "sqlalchemy" | "sdk";
117
+ driver: DbDriver;
85
118
  sql?: string;
86
119
  model?: string;
87
120
  operation?: string;
@@ -91,6 +124,7 @@ interface TracedQuery extends TelemetryEntry {
91
124
  table?: string;
92
125
  source?: string;
93
126
  parentFetchId?: string;
127
+ callSite?: SourceLocation;
94
128
  }
95
129
  type TelemetryEvent = {
96
130
  type: "fetch";
@@ -157,8 +191,6 @@ interface RequestMetrics {
157
191
  fetchTimeMs: number;
158
192
  }
159
193
 
160
- /** Shared severity levels used by both security findings and insights. */
161
- type Severity = "critical" | "warning" | "info";
162
194
  type SecuritySeverity = Severity;
163
195
  interface SecurityFinding {
164
196
  severity: SecuritySeverity;
@@ -171,10 +203,6 @@ interface SecurityFinding {
171
203
  count: number;
172
204
  }
173
205
 
174
- type IssueState = "open" | "fixing" | "resolved" | "stale" | "regressed";
175
- type IssueSource = "passive";
176
- type IssueCategory = "security" | "performance" | "reliability";
177
- type AiFixStatus = "fixed" | "wont_fix";
178
206
  interface Issue {
179
207
  category: IssueCategory;
180
208
  /** Rule identifier: "slow", "n1", "exposed-secret", etc. */
@@ -188,6 +216,8 @@ interface Issue {
188
216
  endpoint?: string;
189
217
  /** Dashboard tab to link to (e.g., "requests", "queries", "security"). */
190
218
  nav?: string;
219
+ /** Occurrence count for rules that aggregate (e.g., N+1 query count). */
220
+ count?: number;
191
221
  }
192
222
  interface StatefulIssue {
193
223
  /** Stable ID derived from rule + endpoint + description hash. */
@@ -202,12 +232,11 @@ interface StatefulIssue {
202
232
  occurrences: number;
203
233
  /**
204
234
  * Number of requests to this endpoint that did NOT reproduce the issue
205
- * since it was last seen. Used for evidence-based resolution.
235
+ * since it was last seen. Used for evidence-based resolution:
236
+ * after CLEAN_HITS_FOR_RESOLUTION clean hits the issue auto-resolves.
206
237
  */
207
238
  cleanHitsSinceLastSeen: number;
208
- /** What AI reported after attempting a fix. */
209
239
  aiStatus: AiFixStatus | null;
210
- /** AI's summary of what was done or why it can't be fixed. */
211
240
  aiNotes: string | null;
212
241
  }
213
242
  interface IssuesData {
@@ -227,10 +256,11 @@ declare class IssueStore {
227
256
  stop(): void;
228
257
  upsert(issue: Issue, source: IssueSource): StatefulIssue;
229
258
  /**
230
- * Reconcile issues against the current analysis results using evidence-based resolution.
231
- *
232
- * @param currentIssueIds - IDs of issues detected in the current analysis cycle
233
- * @param activeEndpoints - Endpoints that had requests in the current cycle
259
+ * Evidence-based reconciliation: for each active issue whose endpoint had
260
+ * traffic but the issue was NOT re-detected, increment cleanHitsSinceLastSeen.
261
+ * After CLEAN_HITS_FOR_RESOLUTION consecutive clean cycles, auto-resolve.
262
+ * Issues on endpoints with no recent traffic are marked stale after STALE_ISSUE_TTL_MS.
263
+ * Resolved and stale issues are pruned after their respective TTLs expire.
234
264
  */
235
265
  reconcile(currentIssueIds: Set<string>, activeEndpoints: Set<string>): void;
236
266
  transition(issueId: string, state: IssueState): boolean;
@@ -244,7 +274,6 @@ declare class IssueStore {
244
274
  private loadAsync;
245
275
  /** Sync load for tests only — not used in production paths. */
246
276
  loadSync(): void;
247
- /** Parse and populate issues from a raw JSON string. */
248
277
  private hydrate;
249
278
  private flush;
250
279
  private flushSync;
@@ -407,7 +436,6 @@ declare class RequestStore {
407
436
  }
408
437
 
409
438
  type TelemetryListener<T> = (entry: T) => void;
410
- /** Read-only view of a TelemetryStore — used by API handlers that only query data. */
411
439
  interface ReadonlyTelemetryStore {
412
440
  getAll(): readonly TelemetryEntry[];
413
441
  getByRequest(requestId: string): TelemetryEntry[];
package/dist/api.js CHANGED
@@ -232,7 +232,7 @@ var IssueStore = class {
232
232
  existing.occurrences++;
233
233
  existing.issue = issue;
234
234
  existing.cleanHitsSinceLastSeen = 0;
235
- if (existing.state === "resolved" || existing.state === "stale") {
235
+ if (existing.aiStatus !== "wont_fix" && (existing.state === "resolved" || existing.state === "stale")) {
236
236
  existing.state = "regressed";
237
237
  existing.resolvedAt = null;
238
238
  }
@@ -258,10 +258,11 @@ var IssueStore = class {
258
258
  return stateful;
259
259
  }
260
260
  /**
261
- * Reconcile issues against the current analysis results using evidence-based resolution.
262
- *
263
- * @param currentIssueIds - IDs of issues detected in the current analysis cycle
264
- * @param activeEndpoints - Endpoints that had requests in the current cycle
261
+ * Evidence-based reconciliation: for each active issue whose endpoint had
262
+ * traffic but the issue was NOT re-detected, increment cleanHitsSinceLastSeen.
263
+ * After CLEAN_HITS_FOR_RESOLUTION consecutive clean cycles, auto-resolve.
264
+ * Issues on endpoints with no recent traffic are marked stale after STALE_ISSUE_TTL_MS.
265
+ * Resolved and stale issues are pruned after their respective TTLs expire.
265
266
  */
266
267
  reconcile(currentIssueIds, activeEndpoints) {
267
268
  const now = Date.now();
@@ -359,9 +360,15 @@ var IssueStore = class {
359
360
  brakitDebug(`IssueStore: could not load issues file, starting fresh: ${err}`);
360
361
  }
361
362
  }
362
- /** Parse and populate issues from a raw JSON string. */
363
363
  hydrate(raw) {
364
- const validated = validateIssuesData(JSON.parse(raw));
364
+ let parsed;
365
+ try {
366
+ parsed = JSON.parse(raw);
367
+ } catch (err) {
368
+ brakitDebug(`IssueStore: corrupt JSON in issues file, starting fresh: ${err}`);
369
+ return;
370
+ }
371
+ const validated = validateIssuesData(parsed);
365
372
  if (!validated) return;
366
373
  for (const issue of validated.issues) {
367
374
  this.issues.set(issue.issueId, issue);
@@ -374,8 +381,12 @@ var IssueStore = class {
374
381
  }
375
382
  flushSync() {
376
383
  if (!this.dirty) return;
377
- this.writer.writeSync(this.serialize());
378
- this.dirty = false;
384
+ try {
385
+ this.writer.writeSync(this.serialize());
386
+ this.dirty = false;
387
+ } catch (err) {
388
+ brakitDebug(`IssueStore: flush failed, will retry: ${err}`);
389
+ }
379
390
  }
380
391
  serialize() {
381
392
  const data = {
@@ -390,12 +401,102 @@ var IssueStore = class {
390
401
  import { readFile as readFile3, readdir } from "fs/promises";
391
402
  import { existsSync as existsSync4 } from "fs";
392
403
  import { join as join2, relative } from "path";
404
+
405
+ // src/constants/detection.ts
406
+ var KNOWN_DEPENDENCY_NAMES = [
407
+ // -- Frameworks (meta) --
408
+ "next",
409
+ "@remix-run/dev",
410
+ "nuxt",
411
+ "astro",
412
+ // -- Frameworks (backend) --
413
+ "@nestjs/core",
414
+ "@adonisjs/core",
415
+ "sails",
416
+ "express",
417
+ "fastify",
418
+ "hono",
419
+ "koa",
420
+ "@hapi/hapi",
421
+ "elysia",
422
+ "h3",
423
+ "nitro",
424
+ "@trpc/server",
425
+ // -- Bundlers --
426
+ "vite",
427
+ // -- ORM / query builders --
428
+ "prisma",
429
+ "@prisma/client",
430
+ "drizzle-orm",
431
+ "typeorm",
432
+ "sequelize",
433
+ "mongoose",
434
+ "kysely",
435
+ "knex",
436
+ "@mikro-orm/core",
437
+ "objection",
438
+ // -- DB drivers --
439
+ "pg",
440
+ "mysql2",
441
+ "mongodb",
442
+ "better-sqlite3",
443
+ "@libsql/client",
444
+ "@planetscale/database",
445
+ "ioredis",
446
+ "redis",
447
+ // -- Auth --
448
+ "lucia",
449
+ "next-auth",
450
+ "@auth/core",
451
+ "passport",
452
+ // -- Queues / messaging --
453
+ "bullmq",
454
+ "amqplib",
455
+ "kafkajs",
456
+ // -- Validation --
457
+ "zod",
458
+ "joi",
459
+ "yup",
460
+ "arktype",
461
+ "valibot",
462
+ // -- HTTP clients --
463
+ "axios",
464
+ "got",
465
+ "ky",
466
+ "undici",
467
+ // -- Realtime --
468
+ "socket.io",
469
+ "ws",
470
+ // -- CSS / styling --
471
+ "tailwindcss",
472
+ // -- Testing --
473
+ "vitest",
474
+ "jest",
475
+ "mocha",
476
+ // -- Runtime indicators --
477
+ "bun-types",
478
+ "@types/bun"
479
+ ];
480
+ var KNOWN_DEPENDENCY_SET = new Set(KNOWN_DEPENDENCY_NAMES);
481
+
482
+ // src/detect/project.ts
393
483
  var FRAMEWORKS = [
484
+ // Meta-frameworks first (they bundle Express/Vite internally)
394
485
  { name: "nextjs", dep: "next", devCmd: "next dev", bin: "next", defaultPort: 3e3, devArgs: ["dev", "--port"] },
395
486
  { name: "remix", dep: "@remix-run/dev", devCmd: "remix dev", bin: "remix", defaultPort: 3e3, devArgs: ["dev"] },
396
487
  { name: "nuxt", dep: "nuxt", devCmd: "nuxt dev", bin: "nuxt", defaultPort: 3e3, devArgs: ["dev", "--port"] },
397
- { name: "vite", dep: "vite", devCmd: "vite", bin: "vite", defaultPort: 5173, devArgs: ["--port"] },
398
- { name: "astro", dep: "astro", devCmd: "astro dev", bin: "astro", defaultPort: 4321, devArgs: ["dev", "--port"] }
488
+ { name: "astro", dep: "astro", devCmd: "astro dev", bin: "astro", defaultPort: 4321, devArgs: ["dev", "--port"] },
489
+ { name: "nestjs", dep: "@nestjs/core", devCmd: "nest start", bin: "nest", defaultPort: 3e3, devArgs: ["--watch"] },
490
+ { name: "adonis", dep: "@adonisjs/core", devCmd: "node ace serve", bin: "ace", defaultPort: 3333, devArgs: ["serve", "--watch"] },
491
+ { name: "sails", dep: "sails", devCmd: "sails lift", bin: "sails", defaultPort: 1337, devArgs: ["lift"] },
492
+ // Server frameworks
493
+ { name: "hono", dep: "hono", devCmd: "node", bin: "node", defaultPort: 3e3 },
494
+ { name: "fastify", dep: "fastify", devCmd: "node", bin: "node", defaultPort: 3e3 },
495
+ { name: "koa", dep: "koa", devCmd: "node", bin: "node", defaultPort: 3e3 },
496
+ { name: "hapi", dep: "@hapi/hapi", devCmd: "node", bin: "node", defaultPort: 3e3 },
497
+ { name: "express", dep: "express", devCmd: "node", bin: "node", defaultPort: 3e3 },
498
+ // Bundlers (last — likely used alongside a framework above)
499
+ { name: "vite", dep: "vite", devCmd: "vite", bin: "vite", defaultPort: 5173, devArgs: ["--port"] }
399
500
  ];
400
501
  async function detectProject(rootDir) {
401
502
  const pkgPath = join2(rootDir, "package.json");
@@ -407,23 +508,22 @@ async function detectProject(rootDir) {
407
508
  const devCommand = matched?.devCmd ?? "";
408
509
  const devBin = matched ? join2(rootDir, "node_modules", ".bin", matched.bin) : "";
409
510
  const defaultPort = matched?.defaultPort ?? 3e3;
410
- const packageManager = await detectPackageManager(rootDir);
511
+ const packageManager = detectPackageManager(rootDir);
411
512
  return { framework, devCommand, devBin, defaultPort, packageManager };
412
513
  }
413
- async function detectPackageManager(rootDir) {
414
- if (await fileExists(join2(rootDir, "bun.lockb"))) return "bun";
415
- if (await fileExists(join2(rootDir, "bun.lock"))) return "bun";
416
- if (await fileExists(join2(rootDir, "pnpm-lock.yaml"))) return "pnpm";
417
- if (await fileExists(join2(rootDir, "yarn.lock"))) return "yarn";
418
- if (await fileExists(join2(rootDir, "package-lock.json"))) return "npm";
419
- return "unknown";
420
- }
421
514
  function detectFrameworkFromDeps(allDeps) {
422
515
  for (const f of FRAMEWORKS) {
423
516
  if (allDeps[f.dep]) return f.name;
424
517
  }
425
518
  return "unknown";
426
519
  }
520
+ function detectPackageManager(rootDir) {
521
+ if (existsSync4(join2(rootDir, "bun.lockb")) || existsSync4(join2(rootDir, "bun.lock"))) return "bun";
522
+ if (existsSync4(join2(rootDir, "pnpm-lock.yaml"))) return "pnpm";
523
+ if (existsSync4(join2(rootDir, "yarn.lock"))) return "yarn";
524
+ if (existsSync4(join2(rootDir, "package-lock.json"))) return "npm";
525
+ return "unknown";
526
+ }
427
527
 
428
528
  // src/instrument/adapter-registry.ts
429
529
  var AdapterRegistry = class {
@@ -1133,14 +1233,14 @@ function createDefaultScanner() {
1133
1233
  // src/core/disposable.ts
1134
1234
  var SubscriptionBag = class {
1135
1235
  constructor() {
1136
- this.items = [];
1236
+ this.items = /* @__PURE__ */ new Set();
1137
1237
  }
1138
1238
  add(teardown) {
1139
- this.items.push(typeof teardown === "function" ? { dispose: teardown } : teardown);
1239
+ this.items.add(typeof teardown === "function" ? { dispose: teardown } : teardown);
1140
1240
  }
1141
1241
  dispose() {
1142
1242
  for (const d of this.items) d.dispose();
1143
- this.items.length = 0;
1243
+ this.items.clear();
1144
1244
  }
1145
1245
  };
1146
1246
 
@@ -1170,14 +1270,10 @@ var DASHBOARD_API_GRAPH = `${DASHBOARD_PREFIX}/api/graph`;
1170
1270
  var VALID_TABS_TUPLE = [
1171
1271
  "overview",
1172
1272
  "actions",
1173
- "requests",
1174
- "fetches",
1175
- "queries",
1176
- "errors",
1177
- "logs",
1273
+ "insights",
1178
1274
  "performance",
1179
- "security",
1180
- "graph"
1275
+ "graph",
1276
+ "explorer"
1181
1277
  ];
1182
1278
  var VALID_TABS = new Set(VALID_TABS_TUPLE);
1183
1279
 
@@ -2411,7 +2507,7 @@ var AnalysisEngine = class {
2411
2507
  };
2412
2508
 
2413
2509
  // src/index.ts
2414
- var VERSION = "0.10.0";
2510
+ var VERSION = "0.10.2";
2415
2511
  export {
2416
2512
  AdapterRegistry,
2417
2513
  AnalysisEngine,