brainblast 0.4.3 → 0.5.0

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
@@ -184,7 +184,7 @@ All types are exported: `Rule`, `CheckResult`, `CostReport`, `AccountFlow`,
184
184
 
185
185
  ```sh
186
186
  npm install
187
- npm test # unit suite (180 tests)
187
+ npm test # unit suite (214 tests)
188
188
  npm run prove # end-to-end: generated tests RED on vulnerable, GREEN on fixed
189
189
  npm run build # produce dist/ (the published artifact)
190
190
  ```
@@ -1486,63 +1486,113 @@ function loadRules(dir) {
1486
1486
  return rules2;
1487
1487
  }
1488
1488
 
1489
+ // src/packs.ts
1490
+ import { existsSync, readdirSync as readdirSync4, readFileSync as readFileSync5, statSync as statSync3 } from "fs";
1491
+ import { join as join5 } from "path";
1492
+ import { parse as parse2 } from "yaml";
1493
+ var PACK_MANIFEST_FILE = "brainblast-pack.yaml";
1494
+ function validatePackManifest(m, file) {
1495
+ const errs = [];
1496
+ if (!m || typeof m !== "object") {
1497
+ throw new Error(`invalid pack manifest in ${file}: not a mapping`);
1498
+ }
1499
+ if (!m.id || typeof m.id !== "string") errs.push("missing id");
1500
+ if (!m.name || typeof m.name !== "string") errs.push("missing name");
1501
+ if (!m.version || typeof m.version !== "string") errs.push("missing version");
1502
+ if (!m.author || typeof m.author !== "string") errs.push("missing author");
1503
+ if (errs.length) throw new Error(`invalid pack manifest in ${file}: ${errs.join("; ")}`);
1504
+ }
1505
+ function loadPack(dir) {
1506
+ const manifestPath = join5(dir, PACK_MANIFEST_FILE);
1507
+ const raw = parse2(readFileSync5(manifestPath, "utf8"));
1508
+ validatePackManifest(raw, manifestPath);
1509
+ const manifest = raw;
1510
+ const rulesDir = join5(dir, "rules");
1511
+ const rules2 = existsSync(rulesDir) ? loadRules(rulesDir).map((r) => ({
1512
+ ...r,
1513
+ pack: { id: manifest.id, version: manifest.version, author: manifest.author }
1514
+ })) : [];
1515
+ return { manifest, rules: rules2 };
1516
+ }
1517
+ function loadPacksFromDir(packsDir) {
1518
+ if (!existsSync(packsDir)) return [];
1519
+ const out = [];
1520
+ for (const entry of readdirSync4(packsDir).sort()) {
1521
+ const dir = join5(packsDir, entry);
1522
+ if (!statSync3(dir).isDirectory()) continue;
1523
+ if (!existsSync(join5(dir, PACK_MANIFEST_FILE))) continue;
1524
+ out.push(loadPack(dir));
1525
+ }
1526
+ return out;
1527
+ }
1528
+
1489
1529
  // rules/index.ts
1490
- import { existsSync } from "fs";
1491
- import { dirname, join as join5 } from "path";
1530
+ import { existsSync as existsSync2 } from "fs";
1531
+ import { dirname, join as join6 } from "path";
1492
1532
  import { fileURLToPath } from "url";
1493
1533
  function bundledRulesDir() {
1494
1534
  const here = dirname(fileURLToPath(import.meta.url));
1495
- if (existsSync(join5(here, "stripe-webhook-raw-body.yaml"))) return here;
1496
- const sub = join5(here, "rules");
1497
- if (existsSync(join5(sub, "stripe-webhook-raw-body.yaml"))) return sub;
1535
+ if (existsSync2(join6(here, "stripe-webhook-raw-body.yaml"))) return here;
1536
+ const sub = join6(here, "rules");
1537
+ if (existsSync2(join6(sub, "stripe-webhook-raw-body.yaml"))) return sub;
1498
1538
  return here;
1499
1539
  }
1500
1540
  var rules = loadRules(bundledRulesDir());
1501
1541
 
1502
1542
  // src/resolveRules.ts
1503
- import { existsSync as existsSync2 } from "fs";
1504
- import { join as join6 } from "path";
1505
- function resolveRules(targetDir) {
1543
+ import { existsSync as existsSync3 } from "fs";
1544
+ import { join as join7 } from "path";
1545
+ function resolveRules(targetDir, extraPackDirs = []) {
1506
1546
  const all = [...rules];
1507
- const projDir = join6(targetDir, ".agent-research", "rules");
1508
- if (existsSync2(projDir)) {
1509
- const seen = new Set(all.map((r) => r.id));
1510
- for (const r of loadRules(projDir)) {
1547
+ const seen = new Set(all.map((r) => r.id));
1548
+ const addRules = (rules2, sourceLabel) => {
1549
+ for (const r of rules2) {
1511
1550
  if (seen.has(r.id)) {
1512
- console.warn(`brainblast: project rule '${r.id}' shadows a bundled rule; keeping bundled.`);
1551
+ console.warn(`brainblast: rule '${r.id}' from ${sourceLabel} shadows an existing rule; keeping the first one loaded.`);
1513
1552
  continue;
1514
1553
  }
1515
1554
  all.push(r);
1516
1555
  seen.add(r.id);
1517
1556
  }
1557
+ };
1558
+ const projDir = join7(targetDir, ".agent-research", "rules");
1559
+ if (existsSync3(projDir)) {
1560
+ addRules(loadRules(projDir), "project rules");
1561
+ }
1562
+ for (const { manifest, rules: rules2 } of loadPacksFromDir(join7(targetDir, ".agent-research", "packs"))) {
1563
+ addRules(rules2, `pack '${manifest.id}'`);
1564
+ }
1565
+ for (const dir of extraPackDirs) {
1566
+ const { manifest, rules: rules2 } = loadPack(dir);
1567
+ addRules(rules2, `pack '${manifest.id}' (${dir})`);
1518
1568
  }
1519
1569
  return all;
1520
1570
  }
1521
1571
 
1522
1572
  // src/trustGraph/directory.ts
1523
- import { readFileSync as readFileSync5, existsSync as existsSync3 } from "fs";
1573
+ import { readFileSync as readFileSync6, existsSync as existsSync4 } from "fs";
1524
1574
  import { fileURLToPath as fileURLToPath2 } from "url";
1525
- import { join as join7 } from "path";
1526
- import { parse as parse2 } from "yaml";
1575
+ import { join as join8 } from "path";
1576
+ import { parse as parse3 } from "yaml";
1527
1577
  var cache = null;
1528
1578
  function bundledPath() {
1529
1579
  const here = fileURLToPath2(new URL(".", import.meta.url));
1530
1580
  const candidates = [
1531
- join7(here, "programs", "directory.yaml"),
1581
+ join8(here, "programs", "directory.yaml"),
1532
1582
  // dist/programs/directory.yaml
1533
- join7(here, "..", "..", "programs", "directory.yaml"),
1583
+ join8(here, "..", "..", "programs", "directory.yaml"),
1534
1584
  // src/../../programs/
1535
- join7(here, "..", "programs", "directory.yaml")
1585
+ join8(here, "..", "programs", "directory.yaml")
1536
1586
  // fallback
1537
1587
  ];
1538
1588
  for (const c of candidates) {
1539
- if (existsSync3(c)) return c;
1589
+ if (existsSync4(c)) return c;
1540
1590
  }
1541
1591
  return candidates[0];
1542
1592
  }
1543
1593
  function loadDirectory(path = bundledPath()) {
1544
1594
  if (cache && path === bundledPath()) return cache;
1545
- const raw = parse2(readFileSync5(path, "utf8"));
1595
+ const raw = parse3(readFileSync6(path, "utf8"));
1546
1596
  if (!raw || !Array.isArray(raw.programs)) {
1547
1597
  throw new Error(`invalid program directory at ${path}: missing 'programs' array`);
1548
1598
  }
@@ -1613,23 +1663,23 @@ function isValidSolanaAddress(s) {
1613
1663
  }
1614
1664
 
1615
1665
  // src/trustGraph/programCache.ts
1616
- import { readFileSync as readFileSync6, writeFileSync, mkdirSync, existsSync as existsSync4 } from "fs";
1617
- import { join as join8, dirname as dirname2 } from "path";
1666
+ import { readFileSync as readFileSync7, writeFileSync, mkdirSync, existsSync as existsSync5 } from "fs";
1667
+ import { join as join9, dirname as dirname2 } from "path";
1618
1668
  import { homedir } from "os";
1619
1669
  var DEFAULT_TTL_HOURS = 168;
1620
1670
  var SCHEMA_VERSION = "1.0";
1621
1671
  function defaultCachePath() {
1622
1672
  const envOverride = process.env["BRAINBLAST_CACHE_PATH"];
1623
- return envOverride ?? join8(homedir(), ".brainblast", "program-cache.json");
1673
+ return envOverride ?? join9(homedir(), ".brainblast", "program-cache.json");
1624
1674
  }
1625
1675
  function emptyCache() {
1626
1676
  return { schemaVersion: SCHEMA_VERSION, entries: {} };
1627
1677
  }
1628
1678
  function loadProgramCache(cachePath) {
1629
1679
  const path = cachePath ?? defaultCachePath();
1630
- if (!existsSync4(path)) return emptyCache();
1680
+ if (!existsSync5(path)) return emptyCache();
1631
1681
  try {
1632
- const raw = JSON.parse(readFileSync6(path, "utf8"));
1682
+ const raw = JSON.parse(readFileSync7(path, "utf8"));
1633
1683
  if (raw?.schemaVersion !== SCHEMA_VERSION) {
1634
1684
  return emptyCache();
1635
1685
  }
@@ -2206,7 +2256,7 @@ function startWatch(targetDir, opts = {}) {
2206
2256
  }
2207
2257
 
2208
2258
  // src/fixers/applyDiff.ts
2209
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "fs";
2259
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync2 } from "fs";
2210
2260
  function parseDiff(diff) {
2211
2261
  const lines = diff.split("\n");
2212
2262
  const fileLine = lines.find((l) => l.startsWith("+++ b"));
@@ -2223,7 +2273,7 @@ function parseDiff(diff) {
2223
2273
  }
2224
2274
  function applyDiffToFile(diff) {
2225
2275
  const { filePath, oldStart, oldCount, newLines } = parseDiff(diff);
2226
- const content = readFileSync7(filePath, "utf8");
2276
+ const content = readFileSync8(filePath, "utf8");
2227
2277
  const fileLines = content.split("\n");
2228
2278
  const removedLines = diff.split("\n").filter((l) => l.startsWith("-") && !l.startsWith("---")).map((l) => l.slice(1));
2229
2279
  const actual = fileLines.slice(oldStart - 1, oldStart - 1 + oldCount);
@@ -2233,6 +2283,156 @@ function applyDiffToFile(diff) {
2233
2283
  return true;
2234
2284
  }
2235
2285
 
2286
+ // src/pack.ts
2287
+ import { existsSync as existsSync6, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
2288
+ import { join as join10 } from "path";
2289
+ function initPack(dir, opts) {
2290
+ if (existsSync6(join10(dir, PACK_MANIFEST_FILE))) {
2291
+ throw new Error(`${dir} already contains a ${PACK_MANIFEST_FILE}`);
2292
+ }
2293
+ const manifest = {
2294
+ id: opts.id,
2295
+ name: opts.name ?? opts.id,
2296
+ version: opts.version ?? "0.1.0",
2297
+ author: opts.author ?? "unknown",
2298
+ ...opts.description ? { description: opts.description } : {}
2299
+ };
2300
+ mkdirSync2(dir, { recursive: true });
2301
+ mkdirSync2(join10(dir, "rules"), { recursive: true });
2302
+ mkdirSync2(join10(dir, "fixtures"), { recursive: true });
2303
+ const manifestYaml = [
2304
+ `id: ${manifest.id}`,
2305
+ `name: ${manifest.name}`,
2306
+ `version: ${manifest.version}`,
2307
+ `author: ${manifest.author}`,
2308
+ ...manifest.description ? [`description: ${manifest.description}`] : [],
2309
+ ""
2310
+ ].join("\n");
2311
+ const manifestFile = join10(dir, PACK_MANIFEST_FILE);
2312
+ writeFileSync3(manifestFile, manifestYaml, "utf8");
2313
+ return manifestFile;
2314
+ }
2315
+ function validatePack(dir) {
2316
+ const { manifest, rules: rules2 } = loadPack(dir);
2317
+ const fixturesRoot = join10(dir, "fixtures");
2318
+ const ruleResults = rules2.map((rule) => {
2319
+ const ruleFixturesDir = join10(fixturesRoot, rule.id);
2320
+ const vulnerableDir = join10(ruleFixturesDir, "vulnerable");
2321
+ const fixedDir = join10(ruleFixturesDir, "fixed");
2322
+ if (!existsSync6(vulnerableDir) || !existsSync6(fixedDir)) {
2323
+ return {
2324
+ ruleId: rule.id,
2325
+ status: "missing-fixtures",
2326
+ detail: `no fixtures/${rule.id}/{vulnerable,fixed}/ directory \u2014 prove gate skipped`
2327
+ };
2328
+ }
2329
+ const redChecks = auditWithRule(vulnerableDir, rule);
2330
+ const redFails = redChecks.filter((c) => c.result === "fail");
2331
+ if (redFails.length === 0) {
2332
+ return {
2333
+ ruleId: rule.id,
2334
+ status: "red-failed",
2335
+ detail: `expected at least one FAIL against fixtures/${rule.id}/vulnerable/, got none`
2336
+ };
2337
+ }
2338
+ const greenChecks = auditWithRule(fixedDir, rule);
2339
+ const greenFails = greenChecks.filter((c) => c.result === "fail");
2340
+ if (greenFails.length > 0) {
2341
+ return {
2342
+ ruleId: rule.id,
2343
+ status: "green-failed",
2344
+ detail: `expected no FAIL against fixtures/${rule.id}/fixed/, got ${greenFails.length}`
2345
+ };
2346
+ }
2347
+ return { ruleId: rule.id, status: "ok", detail: "RED -> GREEN proven" };
2348
+ });
2349
+ const ok = ruleResults.every((r) => r.status === "ok" || r.status === "missing-fixtures");
2350
+ return { manifest, rules: rules2, ruleResults, ok };
2351
+ }
2352
+
2353
+ // src/telemetry.ts
2354
+ import { createHash, randomUUID } from "crypto";
2355
+ import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
2356
+ import { execFileSync as execFileSync3 } from "child_process";
2357
+ import { homedir as homedir2 } from "os";
2358
+ import { dirname as dirname3, join as join11, resolve } from "path";
2359
+ function sha256Hex(s) {
2360
+ return createHash("sha256").update(s).digest("hex");
2361
+ }
2362
+ function isTelemetryEnabled(targetDir) {
2363
+ const env = process.env.BRAINBLAST_TELEMETRY;
2364
+ if (env === "1" || env === "true") return true;
2365
+ if (env === "0" || env === "false") return false;
2366
+ const configPath = join11(targetDir, ".agent-research", "config.json");
2367
+ if (!existsSync7(configPath)) return false;
2368
+ try {
2369
+ const cfg = JSON.parse(readFileSync9(configPath, "utf8"));
2370
+ return cfg?.telemetry === true;
2371
+ } catch {
2372
+ return false;
2373
+ }
2374
+ }
2375
+ function getUserHash() {
2376
+ const idPath = join11(homedir2(), ".brainblast", "telemetry-id");
2377
+ let id;
2378
+ if (existsSync7(idPath)) {
2379
+ id = readFileSync9(idPath, "utf8").trim();
2380
+ } else {
2381
+ id = randomUUID();
2382
+ mkdirSync3(dirname3(idPath), { recursive: true });
2383
+ writeFileSync4(idPath, id, "utf8");
2384
+ }
2385
+ return sha256Hex(id).slice(0, 16);
2386
+ }
2387
+ function getRepoHash(targetDir) {
2388
+ let key = "";
2389
+ try {
2390
+ key = execFileSync3("git", ["config", "--get", "remote.origin.url"], {
2391
+ cwd: targetDir,
2392
+ encoding: "utf8",
2393
+ stdio: ["ignore", "pipe", "ignore"]
2394
+ }).trim();
2395
+ } catch {
2396
+ }
2397
+ if (!key) key = resolve(targetDir);
2398
+ return sha256Hex(key).slice(0, 16);
2399
+ }
2400
+ function telemetryFilePath(targetDir) {
2401
+ return join11(targetDir, ".agent-research", "telemetry.ndjson");
2402
+ }
2403
+ var DEFAULT_REGISTRY_URL = "https://registry.brainblast.tech";
2404
+ async function submitTelemetry(targetDir, registryUrl = process.env.BRAINBLAST_REGISTRY_URL || DEFAULT_REGISTRY_URL) {
2405
+ const file = telemetryFilePath(targetDir);
2406
+ if (!existsSync7(file)) {
2407
+ return { submitted: 0, accepted: 0, rejected: 0, graduations: [] };
2408
+ }
2409
+ const events = readFileSync9(file, "utf8").trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
2410
+ if (events.length === 0) {
2411
+ return { submitted: 0, accepted: 0, rejected: 0, graduations: [] };
2412
+ }
2413
+ const res = await fetch(`${registryUrl.replace(/\/$/, "")}/api/telemetry`, {
2414
+ method: "POST",
2415
+ headers: { "content-type": "application/json" },
2416
+ body: JSON.stringify({ events })
2417
+ });
2418
+ if (!res.ok) {
2419
+ const body = await res.text().catch(() => "");
2420
+ throw new Error(`telemetry submit failed: ${res.status} ${res.statusText} ${body}`.trim());
2421
+ }
2422
+ const json = await res.json();
2423
+ return { submitted: events.length, ...json };
2424
+ }
2425
+ function recordGraduationEvents(targetDir, events) {
2426
+ if (events.length === 0) return;
2427
+ const file = telemetryFilePath(targetDir);
2428
+ mkdirSync3(dirname3(file), { recursive: true });
2429
+ const repo_hash = getRepoHash(targetDir);
2430
+ const user_hash = getUserHash();
2431
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2432
+ const lines = events.map((e) => JSON.stringify({ ...e, repo_hash, user_hash, timestamp })).join("\n");
2433
+ appendFileSync(file, lines + "\n", "utf8");
2434
+ }
2435
+
2236
2436
  export {
2237
2437
  findCandidates,
2238
2438
  findConfigCandidates,
@@ -2247,6 +2447,10 @@ export {
2247
2447
  renderTest,
2248
2448
  testKinds,
2249
2449
  loadRules,
2450
+ PACK_MANIFEST_FILE,
2451
+ validatePackManifest,
2452
+ loadPack,
2453
+ loadPacksFromDir,
2250
2454
  rules,
2251
2455
  resolveRules,
2252
2456
  loadDirectory,
@@ -2271,5 +2475,14 @@ export {
2271
2475
  runIncrementalScan,
2272
2476
  startWatch,
2273
2477
  parseDiff,
2274
- applyDiffToFile
2478
+ applyDiffToFile,
2479
+ initPack,
2480
+ validatePack,
2481
+ isTelemetryEnabled,
2482
+ getUserHash,
2483
+ getRepoHash,
2484
+ telemetryFilePath,
2485
+ DEFAULT_REGISTRY_URL,
2486
+ submitTelemetry,
2487
+ recordGraduationEvents
2275
2488
  };
package/dist/cli.js CHANGED
@@ -7,14 +7,20 @@ import {
7
7
  cacheSize,
8
8
  defaultCachePath,
9
9
  getChangedRanges,
10
+ initPack,
11
+ isTelemetryEnabled,
10
12
  isValidSolanaAddress,
11
13
  loadProgramCache,
12
14
  parseDiff,
15
+ recordGraduationEvents,
13
16
  renderCostReportMd,
14
17
  renderTrustGraphMd,
15
18
  resolveRules,
16
- startWatch
17
- } from "./chunk-XQUQOBXZ.js";
19
+ startWatch,
20
+ submitTelemetry,
21
+ telemetryFilePath,
22
+ validatePack
23
+ } from "./chunk-5LJXC66F.js";
18
24
 
19
25
  // src/cli.ts
20
26
  import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
@@ -94,10 +100,25 @@ function updateMemory(memory, checks2, now = /* @__PURE__ */ new Date()) {
94
100
  // src/cli.ts
95
101
  import { execFileSync } from "child_process";
96
102
  var args = process.argv.slice(2);
103
+ function parsePackDirs(argv) {
104
+ const idx = argv.indexOf("--packs");
105
+ if (idx < 0) return [];
106
+ const value = argv[idx + 1];
107
+ if (!value || value.startsWith("--")) return [];
108
+ return value.split(",").map((s) => s.trim()).filter(Boolean);
109
+ }
97
110
  if (args[0] === "trust-graph") {
98
111
  await runTrustGraph(args.slice(1));
99
112
  process.exit(0);
100
113
  }
114
+ if (args[0] === "pack") {
115
+ runPack(args.slice(1));
116
+ process.exit(0);
117
+ }
118
+ if (args[0] === "telemetry") {
119
+ await runTelemetry(args.slice(1));
120
+ process.exit(0);
121
+ }
101
122
  if (args[0] === "watch") {
102
123
  const watchDir = args.find((a, i) => i > 0 && !a.startsWith("--")) ?? process.cwd();
103
124
  startWatch(watchDir);
@@ -119,7 +140,7 @@ if (sinceIdx >= 0 && !since) {
119
140
  console.error("error: --since requires a <ref> argument, e.g. --since origin/main");
120
141
  process.exit(2);
121
142
  }
122
- var rules = resolveRules(targetDir);
143
+ var rules = resolveRules(targetDir, parsePackDirs(args));
123
144
  var changedRanges;
124
145
  if (since) {
125
146
  try {
@@ -211,6 +232,75 @@ if (ci) {
211
232
  const gateFail = fails > 0 || strict && cantTell > 0;
212
233
  process.exit(gateFail ? 1 : 0);
213
234
  }
235
+ function runPack(argv) {
236
+ const sub = argv[0];
237
+ if (sub === "init") {
238
+ const dir = argv.find((a, i) => i > 0 && !a.startsWith("--") && argv[i - 1] !== "--id" && argv[i - 1] !== "--name" && argv[i - 1] !== "--author" && argv[i - 1] !== "--version" && argv[i - 1] !== "--description");
239
+ const flag = (name) => {
240
+ const idx = argv.indexOf(`--${name}`);
241
+ return idx >= 0 ? argv[idx + 1] : void 0;
242
+ };
243
+ const id = flag("id");
244
+ if (!dir || !id) {
245
+ console.error("usage: brainblast pack init <dir> --id <pack-id> [--name <name>] [--author <author>] [--version <semver>] [--description <text>]");
246
+ process.exit(2);
247
+ }
248
+ const manifestFile = initPack(dir, {
249
+ id,
250
+ name: flag("name"),
251
+ author: flag("author"),
252
+ version: flag("version"),
253
+ description: flag("description")
254
+ });
255
+ console.log(`brainblast pack init: wrote ${manifestFile}`);
256
+ console.log(` rules: ${join2(dir, "rules")}/`);
257
+ console.log(` fixtures: ${join2(dir, "fixtures")}/`);
258
+ return;
259
+ }
260
+ if (sub === "validate") {
261
+ const dir = argv.find((a, i) => i > 0 && !a.startsWith("--"));
262
+ if (!dir) {
263
+ console.error("usage: brainblast pack validate <dir>");
264
+ process.exit(2);
265
+ }
266
+ const result = validatePack(dir);
267
+ console.log(`pack: ${result.manifest.id} v${result.manifest.version} (${result.manifest.author})`);
268
+ console.log(` ${result.rules.length} rule(s)`);
269
+ for (const r of result.ruleResults) {
270
+ const marker = r.status === "ok" ? "OK" : r.status === "missing-fixtures" ? "WARN" : "FAIL";
271
+ console.log(` [${marker}] ${r.ruleId}: ${r.detail}`);
272
+ }
273
+ process.exit(result.ok ? 0 : 1);
274
+ }
275
+ console.error("usage: brainblast pack <init|validate> ...");
276
+ process.exit(2);
277
+ }
278
+ async function runTelemetry(argv) {
279
+ const sub = argv[0];
280
+ if (sub !== "submit") {
281
+ console.error("usage: brainblast telemetry submit [targetDir]");
282
+ process.exit(2);
283
+ }
284
+ const targetDir2 = argv.find((a, i) => i > 0 && !a.startsWith("--")) ?? process.cwd();
285
+ try {
286
+ const result = await submitTelemetry(targetDir2);
287
+ if (result.submitted === 0) {
288
+ console.log(`brainblast telemetry submit: no events to submit (${telemetryFilePath(targetDir2)} is empty or missing)`);
289
+ return;
290
+ }
291
+ console.log(`brainblast telemetry submit: sent ${result.submitted} event(s) \u2014 ${result.accepted} accepted, ${result.rejected} rate-limited`);
292
+ for (const g of result.graduations) {
293
+ if (g.graduated) {
294
+ console.log(` [GRADUATED] ${g.pack_id}/${g.rule_id} (${g.distinct_pairs} distinct repo/user pairs)`);
295
+ } else {
296
+ console.log(` [PROGRESS] ${g.pack_id}/${g.rule_id} ${g.distinct_pairs}/5 distinct repo/user pairs`);
297
+ }
298
+ }
299
+ } catch (e) {
300
+ console.error(`brainblast telemetry submit: ${e.message ?? String(e)}`);
301
+ process.exit(1);
302
+ }
303
+ }
214
304
  async function runTrustGraph(argv) {
215
305
  const rpcIdx = argv.indexOf("--rpc");
216
306
  const rpcUrl = rpcIdx >= 0 ? argv[rpcIdx + 1] : void 0;
@@ -248,7 +338,7 @@ async function runFix(argv) {
248
338
  const apply = argv.includes("--apply");
249
339
  const branch = argv.includes("--branch");
250
340
  const targetDir2 = argv.find((a) => !a.startsWith("--")) ?? process.cwd();
251
- const rules2 = resolveRules(targetDir2);
341
+ const rules2 = resolveRules(targetDir2, parsePackDirs(argv));
252
342
  const { checks: before } = audit(targetDir2, rules2);
253
343
  const fixable = before.filter((c) => c.result === "fail" && c.fix?.diff);
254
344
  if (fixable.length === 0) {
@@ -300,6 +390,15 @@ Warning: ${stillFailing.length} fix(es) applied but the rule still fails:`);
300
390
  } else if (applied > 0) {
301
391
  console.log("All applied fixes now pass (or cant_tell) on re-audit. \u2713");
302
392
  }
393
+ if (isTelemetryEnabled(targetDir2)) {
394
+ const graduated = fixable.filter((c) => !stillFailing.includes(c));
395
+ const events = graduated.map((c) => rules2.find((r) => r.id === c.ruleId)).filter((r) => !!r?.pack).map((r) => ({ pack_id: r.pack.id, rule_id: r.id }));
396
+ if (events.length > 0) {
397
+ recordGraduationEvents(targetDir2, events);
398
+ console.log(`
399
+ Telemetry: recorded ${events.length} graduation event(s) to ${telemetryFilePath(targetDir2)}`);
400
+ }
401
+ }
303
402
  if (branch && applied > 0) {
304
403
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
305
404
  const branchName = `brainblast/auto-fix-${ts}`;
package/dist/index.d.ts CHANGED
@@ -156,6 +156,23 @@ interface Rule {
156
156
  kind: string;
157
157
  params?: Record<string, any>;
158
158
  };
159
+ /**
160
+ * Provenance for rules loaded from a third-party rule pack (see
161
+ * src/packs.ts). Absent for bundled rules. Stamped by the pack loader from
162
+ * the pack's brainblast-pack.yaml manifest, not set by the rule author.
163
+ */
164
+ pack?: {
165
+ id: string;
166
+ version: string;
167
+ author?: string;
168
+ };
169
+ }
170
+ interface PackManifest {
171
+ id: string;
172
+ name: string;
173
+ version: string;
174
+ author: string;
175
+ description?: string;
159
176
  }
160
177
  type Checker = (candidate: Candidate, params: any) => CheckOutcome;
161
178
  type RustChecker = (candidate: RustCandidate, params: any) => CheckOutcome;
@@ -224,7 +241,7 @@ declare function audit(targetDir: string, rules: Rule[], changedRanges?: Changed
224
241
  };
225
242
  };
226
243
 
227
- declare function resolveRules(targetDir: string): Rule[];
244
+ declare function resolveRules(targetDir: string, extraPackDirs?: string[]): Rule[];
228
245
 
229
246
  declare function loadRules(dir: string): Rule[];
230
247
 
@@ -285,6 +302,70 @@ interface ParsedDiff {
285
302
  declare function parseDiff(diff: string): ParsedDiff;
286
303
  declare function applyDiffToFile(diff: string): boolean;
287
304
 
305
+ declare const PACK_MANIFEST_FILE = "brainblast-pack.yaml";
306
+ declare function validatePackManifest(m: any, file: string): void;
307
+ declare function loadPack(dir: string): {
308
+ manifest: PackManifest;
309
+ rules: Rule[];
310
+ };
311
+ declare function loadPacksFromDir(packsDir: string): {
312
+ manifest: PackManifest;
313
+ rules: Rule[];
314
+ }[];
315
+
316
+ interface PackInitOptions {
317
+ id: string;
318
+ name?: string;
319
+ author?: string;
320
+ version?: string;
321
+ description?: string;
322
+ }
323
+ declare function initPack(dir: string, opts: PackInitOptions): string;
324
+ interface PackValidateResult {
325
+ manifest: PackManifest;
326
+ rules: Rule[];
327
+ /** Per-rule prove-gate results. */
328
+ ruleResults: PackRuleValidation[];
329
+ /** True if the manifest is valid, every rule loaded cleanly, and every rule with fixtures passed RED->GREEN. */
330
+ ok: boolean;
331
+ }
332
+ interface PackRuleValidation {
333
+ ruleId: string;
334
+ /** "ok" | "missing-fixtures" | "red-failed" | "green-failed" */
335
+ status: "ok" | "missing-fixtures" | "red-failed" | "green-failed";
336
+ detail: string;
337
+ }
338
+ declare function validatePack(dir: string): PackValidateResult;
339
+
340
+ interface GraduationEvent {
341
+ pack_id: string;
342
+ rule_id: string;
343
+ repo_hash: string;
344
+ user_hash: string;
345
+ timestamp: string;
346
+ }
347
+ declare function isTelemetryEnabled(targetDir: string): boolean;
348
+ declare function getUserHash(): string;
349
+ declare function getRepoHash(targetDir: string): string;
350
+ declare function telemetryFilePath(targetDir: string): string;
351
+ declare const DEFAULT_REGISTRY_URL = "https://registry.brainblast.tech";
352
+ interface TelemetrySubmitResult {
353
+ submitted: number;
354
+ accepted: number;
355
+ rejected: number;
356
+ graduations: {
357
+ pack_id: string;
358
+ rule_id: string;
359
+ distinct_pairs: number;
360
+ graduated: boolean;
361
+ }[];
362
+ }
363
+ declare function submitTelemetry(targetDir: string, registryUrl?: string): Promise<TelemetrySubmitResult>;
364
+ declare function recordGraduationEvents(targetDir: string, events: {
365
+ pack_id: string;
366
+ rule_id: string;
367
+ }[]): void;
368
+
288
369
  type UpgradeAuthorityKind = "renounced" | "single-key" | "multisig" | "dao" | "unknown";
289
370
  type UpgradeAuthoritySource = "directory" | "rpc" | "research";
290
371
  interface UpgradeAuthority {
@@ -414,4 +495,4 @@ declare function isEntryExpired(entry: ProgramCacheEntry, ttlHoursOverride?: num
414
495
  */
415
496
  declare function cacheSize(cache: ProgramCache, ttlHoursOverride?: number): number;
416
497
 
417
- export { type AccountFlow, type AuditRef, type BuildOpts, type Candidate, type ChangedRanges, type CheckOutcome, type CheckResult, type CheckResultKind, type Checker, type ConfigCandidate, type ConfigChecker, type CostReport, DEFAULT_TTL_HOURS, type OnChainProgram, type ParityNote, type ParsedDiff, type PriorityFeePosture, type ProgramCache, type ProgramCacheEntry, type Recoverability, type Rule, type RustAccountField, type RustCandidate, type RustChecker, type Severity, type TrustGraph, type UpgradeAuthority, type UpgradeAuthorityKind, type UpgradeAuthoritySource, type VerifiedBuildState, type WatchEvent, type WatchOptions, analyzeCosts, applyDiffToFile, audit, auditWithRule, base58Decode, base58Encode, buildTrustGraph, rules as bundledRules, cacheSize, checkerKinds, defaultCachePath, fileChanged, findCandidates, findConfigCandidates, generateTestForResult, getCacheEntry, getCacheEntryMeta, getChangedRanges, getWorkingTreeChanges, isEntryExpired, isValidSolanaAddress, lamportsToSol, loadDirectory, loadProgramCache, loadRules, parseDiff, putCacheEntry, rangeChanged, renderCostReportMd, renderTest, renderTrustGraphMd, rentExemptMinimum, resolveRules, runChecker, runIncrementalScan, saveProgramCache, startWatch, testKinds };
498
+ export { type AccountFlow, type AuditRef, type BuildOpts, type Candidate, type ChangedRanges, type CheckOutcome, type CheckResult, type CheckResultKind, type Checker, type ConfigCandidate, type ConfigChecker, type CostReport, DEFAULT_REGISTRY_URL, DEFAULT_TTL_HOURS, type GraduationEvent, type OnChainProgram, PACK_MANIFEST_FILE, type PackInitOptions, type PackManifest, type PackRuleValidation, type PackValidateResult, type ParityNote, type ParsedDiff, type PriorityFeePosture, type ProgramCache, type ProgramCacheEntry, type Recoverability, type Rule, type RustAccountField, type RustCandidate, type RustChecker, type Severity, type TelemetrySubmitResult, type TrustGraph, type UpgradeAuthority, type UpgradeAuthorityKind, type UpgradeAuthoritySource, type VerifiedBuildState, type WatchEvent, type WatchOptions, analyzeCosts, applyDiffToFile, audit, auditWithRule, base58Decode, base58Encode, buildTrustGraph, rules as bundledRules, cacheSize, checkerKinds, defaultCachePath, fileChanged, findCandidates, findConfigCandidates, generateTestForResult, getCacheEntry, getCacheEntryMeta, getChangedRanges, getRepoHash, getUserHash, getWorkingTreeChanges, initPack, isEntryExpired, isTelemetryEnabled, isValidSolanaAddress, lamportsToSol, loadDirectory, loadPack, loadPacksFromDir, loadProgramCache, loadRules, parseDiff, putCacheEntry, rangeChanged, recordGraduationEvents, renderCostReportMd, renderTest, renderTrustGraphMd, rentExemptMinimum, resolveRules, runChecker, runIncrementalScan, saveProgramCache, startWatch, submitTelemetry, telemetryFilePath, testKinds, validatePack, validatePackManifest };
package/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import {
2
+ DEFAULT_REGISTRY_URL,
2
3
  DEFAULT_TTL_HOURS,
4
+ PACK_MANIFEST_FILE,
3
5
  analyzeCosts,
4
6
  applyDiffToFile,
5
7
  audit,
@@ -16,16 +18,23 @@ import {
16
18
  getCacheEntry,
17
19
  getCacheEntryMeta,
18
20
  getChangedRanges,
21
+ getRepoHash,
22
+ getUserHash,
19
23
  getWorkingTreeChanges,
24
+ initPack,
20
25
  isEntryExpired,
26
+ isTelemetryEnabled,
21
27
  isValidSolanaAddress,
22
28
  lamportsToSol,
23
29
  loadDirectory,
30
+ loadPack,
31
+ loadPacksFromDir,
24
32
  loadProgramCache,
25
33
  loadRules,
26
34
  parseDiff,
27
35
  putCacheEntry,
28
36
  rangeChanged,
37
+ recordGraduationEvents,
29
38
  renderCostReportMd,
30
39
  renderTest,
31
40
  renderTrustGraphMd,
@@ -36,8 +45,12 @@ import {
36
45
  runIncrementalScan,
37
46
  saveProgramCache,
38
47
  startWatch,
39
- testKinds
40
- } from "./chunk-XQUQOBXZ.js";
48
+ submitTelemetry,
49
+ telemetryFilePath,
50
+ testKinds,
51
+ validatePack,
52
+ validatePackManifest
53
+ } from "./chunk-5LJXC66F.js";
41
54
 
42
55
  // src/generate.ts
43
56
  import { writeFileSync, mkdirSync } from "fs";
@@ -53,7 +66,9 @@ function generateTestForResult(result, rule, outPath) {
53
66
  return outPath;
54
67
  }
55
68
  export {
69
+ DEFAULT_REGISTRY_URL,
56
70
  DEFAULT_TTL_HOURS,
71
+ PACK_MANIFEST_FILE,
57
72
  analyzeCosts,
58
73
  applyDiffToFile,
59
74
  audit,
@@ -72,16 +87,23 @@ export {
72
87
  getCacheEntry,
73
88
  getCacheEntryMeta,
74
89
  getChangedRanges,
90
+ getRepoHash,
91
+ getUserHash,
75
92
  getWorkingTreeChanges,
93
+ initPack,
76
94
  isEntryExpired,
95
+ isTelemetryEnabled,
77
96
  isValidSolanaAddress,
78
97
  lamportsToSol,
79
98
  loadDirectory,
99
+ loadPack,
100
+ loadPacksFromDir,
80
101
  loadProgramCache,
81
102
  loadRules,
82
103
  parseDiff,
83
104
  putCacheEntry,
84
105
  rangeChanged,
106
+ recordGraduationEvents,
85
107
  renderCostReportMd,
86
108
  renderTest,
87
109
  renderTrustGraphMd,
@@ -91,5 +113,9 @@ export {
91
113
  runIncrementalScan,
92
114
  saveProgramCache,
93
115
  startWatch,
94
- testKinds
116
+ submitTelemetry,
117
+ telemetryFilePath,
118
+ testKinds,
119
+ validatePack,
120
+ validatePackManifest
95
121
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brainblast",
3
- "version": "0.4.3",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "description": "Deterministic auditor for catastrophic AI-integration bugs: scan a repo, find the silent money/auth traps, and generate the behavioral test that proves they're fixed.",
6
6
  "keywords": [