brainjar 0.3.976 → 0.3.977

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.
Files changed (2) hide show
  1. package/brainjar.js +465 -88
  2. package/package.json +1 -1
package/brainjar.js CHANGED
@@ -107,7 +107,7 @@ var CLI_VERSION, CACHE, DAY;
107
107
  var init_version_check = __esm({
108
108
  "packages/cli/src/version-check.ts"() {
109
109
  "use strict";
110
- CLI_VERSION = "0.3.976";
110
+ CLI_VERSION = "0.3.977";
111
111
  CACHE = path.join(os.homedir(), ".brainjar", "version-check.json");
112
112
  DAY = 864e5;
113
113
  }
@@ -5926,7 +5926,9 @@ var init_config = __esm({
5926
5926
  // Launch pricing made permanent (official pricing page, checked 2026-08-18).
5927
5927
  "claude-sonnet-5": { input: 2, cachedInput: 0.2, output: 10 },
5928
5928
  "claude-haiku-4-5": { input: 1, cachedInput: 0.1, output: 5 },
5929
- "gpt-5.6-luna": { input: 0.2, cachedInput: 0.02, output: 1.2 }
5929
+ "gpt-5.6-luna": { input: 0.2, cachedInput: 0.02, output: 1.2 },
5930
+ // Released 2026-09-22 (official pricing page, checked 2026-09-25).
5931
+ "gpt-6-luna": { input: 0.1, cachedInput: 0.01, output: 0.5 }
5930
5932
  };
5931
5933
  DEFAULT_TENANT_ID = "00000000-0000-0000-0000-000000000002";
5932
5934
  SECRETS = {
@@ -6083,7 +6085,7 @@ function validateFlowDiagram(d) {
6083
6085
  if (n2.evidence.length > 12)
6084
6086
  errs.push(`nodes: ${n2.id} too many evidence entries`);
6085
6087
  for (const ev of n2.evidence)
6086
- if (!short(ev, 200) || /[-]/.test(ev))
6088
+ if (!short(ev, 200) || /[\x00-\x1f]/.test(ev))
6087
6089
  errs.push(`nodes: ${n2.id} evidence entry is not a short string`);
6088
6090
  }
6089
6091
  if (n2.description !== void 0 && !short(n2.description, 600))
@@ -13182,42 +13184,280 @@ function resolveSpecifier(fromFile, spec, known) {
13182
13184
  for (const probe of [
13183
13185
  clean,
13184
13186
  c,
13185
- ...SOURCE_EXTS.map((e) => clean + e),
13186
- ...SOURCE_EXTS.map((e) => `${clean}/index${e}`)
13187
+ ...JS_EXTS.map((e) => clean + e),
13188
+ ...JS_EXTS.map((e) => `${clean}/index${e}`)
13187
13189
  ]) {
13188
13190
  if (known.has(probe)) return probe;
13189
13191
  }
13190
13192
  }
13191
13193
  return null;
13192
13194
  }
13195
+ function resolveBySuffix(segments, exts, fromFile, idx, inits = []) {
13196
+ if (segments.length === 0) return null;
13197
+ const want = segments.join("/");
13198
+ const last = segments[segments.length - 1];
13199
+ const hits = [
13200
+ ...(idx.byName.get(last) ?? []).filter(
13201
+ (f) => exts.includes(ext(f)) && (stem(f) === want || stem(f).endsWith(`/${want}`))
13202
+ ),
13203
+ ...inits.flatMap(
13204
+ (init2) => (idx.byName.get(stem(init2)) ?? []).filter(
13205
+ (f) => f.endsWith(`/${want}/${init2}`) || f === `${want}/${init2}`
13206
+ )
13207
+ )
13208
+ ];
13209
+ if (hits.length === 0) return null;
13210
+ return hits.sort(
13211
+ (a, b) => nearness(b, fromFile) - nearness(a, fromFile) || a.length - b.length
13212
+ )[0];
13213
+ }
13214
+ function pythonTargets(file, src, idx) {
13215
+ const out = [];
13216
+ const mod = (base, segs) => resolveBySuffix([...base, ...segs], [".py"], file, idx, ["__init__.py"]);
13217
+ for (const m of src.matchAll(PY_FROM)) {
13218
+ const dots = m[1].length;
13219
+ const segs = m[2] ? m[2].split(".") : [];
13220
+ const names = m[3].split(",").map((n2) => n2.trim().split(/\s+/)[0]);
13221
+ if (dots > 0) {
13222
+ let dir = path4.posix.dirname(file);
13223
+ for (let i = 1; i < dots; i++) dir = path4.posix.dirname(dir);
13224
+ const base = dir === "." ? [] : dir.split("/");
13225
+ const exact = (s) => {
13226
+ const p = [...base, ...s].join("/");
13227
+ return idx.known.has(`${p}.py`) ? `${p}.py` : idx.known.has(`${p}/__init__.py`) ? `${p}/__init__.py` : null;
13228
+ };
13229
+ const target = segs.length ? exact(segs) : null;
13230
+ if (target) out.push(target);
13231
+ for (const n2 of names) {
13232
+ const sub = exact([...segs, n2]);
13233
+ if (sub) out.push(sub);
13234
+ }
13235
+ } else if (segs.length) {
13236
+ const sub = names.map((n2) => mod([], [...segs, n2])).filter(Boolean);
13237
+ const target = resolveLongest(segs, (s) => mod([], s));
13238
+ out.push(...sub, ...target ? [target] : []);
13239
+ }
13240
+ }
13241
+ for (const m of src.matchAll(PY_IMPORT))
13242
+ for (const spec of m[1].split(",")) {
13243
+ const target = resolveLongest(spec.trim().split("."), (s) => mod([], s));
13244
+ if (target) out.push(target);
13245
+ }
13246
+ return out;
13247
+ }
13248
+ function rustTargets(file, src, idx) {
13249
+ const out = [];
13250
+ const dir = path4.posix.dirname(file);
13251
+ const base = path4.posix.basename(file, ".rs");
13252
+ const own = ["lib", "main", "mod"].includes(base) ? dir : `${dir}/${base}`;
13253
+ const crateSrc = [...idx.crates.values()].filter((s) => file.startsWith(`${s}/`)).sort((a, b) => b.length - a.length)[0];
13254
+ const inDir = (d, segs) => {
13255
+ const p = [d, ...segs].join("/");
13256
+ return idx.known.has(`${p}.rs`) ? `${p}.rs` : idx.known.has(`${p}/mod.rs`) ? `${p}/mod.rs` : null;
13257
+ };
13258
+ for (const m of src.matchAll(RS_MOD)) {
13259
+ const hit = inDir(own, [m[1]]);
13260
+ if (hit) out.push(hit);
13261
+ }
13262
+ for (const m of src.matchAll(RS_USE))
13263
+ for (const use of expandUse(m[1])) {
13264
+ const [head, ...rest] = use.replace(/^::/, "").split("::");
13265
+ let root;
13266
+ if (head === "crate") root = crateSrc;
13267
+ else if (head === "self") root = own;
13268
+ else if (head === "super")
13269
+ root = ["lib", "main", "mod"].includes(base) ? path4.posix.dirname(dir) : dir;
13270
+ else if (head && idx.crates.has(head)) {
13271
+ root = idx.crates.get(head);
13272
+ if (rest.length === 0) {
13273
+ const lib = `${root}/lib.rs`;
13274
+ if (idx.known.has(lib)) out.push(lib);
13275
+ continue;
13276
+ }
13277
+ }
13278
+ if (!root) continue;
13279
+ const hit = resolveLongest(rest, (s) => inDir(root, s));
13280
+ if (hit) out.push(hit);
13281
+ else if (head !== "self" && head !== "super") {
13282
+ const lib = `${root}/lib.rs`;
13283
+ if (idx.known.has(lib)) out.push(lib);
13284
+ }
13285
+ }
13286
+ return out;
13287
+ }
13288
+ function goTargets(file, src, idx) {
13289
+ const specs = [
13290
+ ...[...src.matchAll(GO_BLOCK)].flatMap(
13291
+ (m) => [...m[1].matchAll(/"([^"]+)"/g)].map((x) => x[1])
13292
+ ),
13293
+ ...[...src.matchAll(GO_ONE)].map((m) => m[1])
13294
+ ];
13295
+ const out = [];
13296
+ for (const spec of specs)
13297
+ for (const [mod, dir] of idx.goModules) {
13298
+ if (spec !== mod && !spec.startsWith(`${mod}/`)) continue;
13299
+ const pkgDir = [dir === "." ? "" : dir, spec.slice(mod.length + 1)].filter(Boolean).join("/");
13300
+ const rep = (idx.byDir.get(pkgDir) ?? []).find(
13301
+ (f) => f.endsWith(".go") && !f.endsWith("_test.go")
13302
+ );
13303
+ if (rep) out.push(rep);
13304
+ }
13305
+ return out;
13306
+ }
13307
+ function swiftTargets(src, idx) {
13308
+ const out = [];
13309
+ for (const m of src.matchAll(
13310
+ /^[ \t]*(?:@\w+[ \t]+)*import[ \t]+(?:\w+[ \t]+)?(\w+)/gm
13311
+ )) {
13312
+ for (const [dir, files] of idx.byDir) {
13313
+ if (!(dir === `Sources/${m[1]}` || dir.endsWith(`/Sources/${m[1]}`) || dir.includes(`/Sources/${m[1]}/`)))
13314
+ continue;
13315
+ const rep = files.find((f) => f.endsWith(".swift") || f.endsWith(".h"));
13316
+ if (rep) {
13317
+ out.push(rep);
13318
+ break;
13319
+ }
13320
+ }
13321
+ }
13322
+ return out;
13323
+ }
13324
+ function jvmTargets(file, src, idx) {
13325
+ const out = [];
13326
+ for (const m of src.matchAll(
13327
+ /^[ \t]*import[ \t]+(?:static[ \t]+)?([\w.]+)/gm
13328
+ )) {
13329
+ const hit = resolveLongest(
13330
+ m[1].split("."),
13331
+ (s) => resolveBySuffix(s, [".java", ".kt"], file, idx)
13332
+ );
13333
+ if (hit) out.push(hit);
13334
+ }
13335
+ return out;
13336
+ }
13337
+ function cTargets(file, src, idx) {
13338
+ const out = [];
13339
+ for (const m of src.matchAll(
13340
+ /^[ \t]*#[ \t]*(?:include|import)[ \t]*"([^"]+)"/gm
13341
+ )) {
13342
+ const rel = path4.posix.normalize(
13343
+ path4.posix.join(path4.posix.dirname(file), m[1])
13344
+ );
13345
+ if (idx.known.has(rel)) out.push(rel);
13346
+ else {
13347
+ const hit = resolveBySuffix(
13348
+ stem(m[1]).split("/"),
13349
+ [ext(m[1])],
13350
+ file,
13351
+ idx
13352
+ );
13353
+ if (hit) out.push(hit);
13354
+ }
13355
+ }
13356
+ return out;
13357
+ }
13193
13358
  async function scanStructure(root) {
13194
13359
  const entries = await readdir(root, { recursive: true, withFileTypes: true });
13195
13360
  const files = [];
13361
+ const cargo = [];
13362
+ const gomod = [];
13196
13363
  for (const e of entries) {
13197
13364
  if (!e.isFile()) continue;
13198
13365
  const parent = e.parentPath ?? e.path ?? root;
13199
13366
  const rel = toPosix(path4.relative(root, path4.join(parent, e.name)));
13200
13367
  if (rel.split("/").some((seg) => SKIP_DIRS.has(seg))) continue;
13201
- if (SOURCE_EXTS.some((ext) => rel.endsWith(ext))) files.push(rel);
13368
+ if (SOURCE_EXTS.some((x) => rel.endsWith(x))) files.push(rel);
13369
+ else if (e.name === "Cargo.toml") cargo.push(rel);
13370
+ else if (e.name === "go.mod") gomod.push(rel);
13371
+ }
13372
+ const read = (f) => readFile(path4.join(root, f), "utf8");
13373
+ const idx = {
13374
+ known: new Set(files),
13375
+ byName: /* @__PURE__ */ new Map(),
13376
+ byDir: /* @__PURE__ */ new Map(),
13377
+ crates: /* @__PURE__ */ new Map(),
13378
+ goModules: /* @__PURE__ */ new Map()
13379
+ };
13380
+ for (const f of [...files].sort()) {
13381
+ const name = path4.posix.basename(stem(f));
13382
+ idx.byName.set(name, [...idx.byName.get(name) ?? [], f]);
13383
+ const d = path4.posix.dirname(f);
13384
+ idx.byDir.set(d, [...idx.byDir.get(d) ?? [], f]);
13385
+ }
13386
+ for (const c of cargo) {
13387
+ const pkg = (await read(c)).match(
13388
+ /\[package\][^[]*?\bname\s*=\s*"([^"]+)"/
13389
+ );
13390
+ const dir = path4.posix.dirname(c);
13391
+ if (pkg)
13392
+ idx.crates.set(
13393
+ pkg[1].replace(/-/g, "_"),
13394
+ `${dir === "." ? "" : `${dir}/`}src`.replace(/^\//, "")
13395
+ );
13202
13396
  }
13203
- const known = new Set(files);
13397
+ for (const g of gomod) {
13398
+ const mod = (await read(g)).match(/^module\s+(\S+)/m);
13399
+ if (mod) idx.goModules.set(mod[1], path4.posix.dirname(g));
13400
+ }
13401
+ const exported = /* @__PURE__ */ new Map();
13402
+ for (const f of files.filter((x) => x.endsWith(".rs")))
13403
+ for (const m of (await read(f)).matchAll(RS_EXPORT))
13404
+ if (!exported.has(m[1])) exported.set(m[1], f);
13405
+ const names = [...exported.keys()].slice(0, 2e3);
13406
+ const callRe = names.length ? new RegExp(
13407
+ `\\b(${names.map((n2) => n2.replace(/[^\w]/g, "")).join("|")})\\s*\\(`,
13408
+ "g"
13409
+ ) : null;
13410
+ const seen = /* @__PURE__ */ new Set();
13204
13411
  const edges = [];
13412
+ const add = (from, to) => {
13413
+ if (!to || to === from || !idx.known.has(to)) return;
13414
+ const k = JSON.stringify([from, to]);
13415
+ if (seen.has(k)) return;
13416
+ seen.add(k);
13417
+ edges.push({ from, to });
13418
+ };
13205
13419
  for (const file of files) {
13206
- const source = await readFile(path4.join(root, file), "utf8");
13207
- for (const spec of extractImports(source)) {
13208
- const to = resolveSpecifier(file, spec, known);
13209
- if (to && to !== file) edges.push({ from: file, to });
13210
- }
13420
+ const src = await read(file);
13421
+ const x = ext(file);
13422
+ if (JS_EXTS.includes(x))
13423
+ for (const spec of extractImports(src))
13424
+ add(file, resolveSpecifier(file, spec, idx.known));
13425
+ else if (x === ".py")
13426
+ for (const t of pythonTargets(file, src, idx)) add(file, t);
13427
+ else if (x === ".rs")
13428
+ for (const t of rustTargets(file, src, idx)) add(file, t);
13429
+ else if (x === ".go")
13430
+ for (const t of goTargets(file, src, idx)) add(file, t);
13431
+ else if (x === ".swift")
13432
+ for (const t of swiftTargets(src, idx)) add(file, t);
13433
+ else if (x === ".java" || x === ".kt")
13434
+ for (const t of jvmTargets(file, src, idx)) add(file, t);
13435
+ else if (C_EXTS.includes(x))
13436
+ for (const t of cTargets(file, src, idx)) add(file, t);
13437
+ if (callRe && x !== ".rs")
13438
+ for (const m of src.matchAll(callRe))
13439
+ add(file, exported.get(m[1]) ?? null);
13211
13440
  }
13212
13441
  return { modules: files.sort(), edges };
13213
13442
  }
13214
- var SOURCE_EXTS, SKIP_DIRS, IMPORT_RE, toPosix;
13443
+ var JS_EXTS, C_EXTS, SOURCE_EXTS, SKIP_DIRS, IMPORT_RE, toPosix, ext, stem, nearness, resolveLongest, PY_FROM, PY_IMPORT, RS_MOD, RS_USE, expandUse, GO_BLOCK, GO_ONE, RS_EXPORT;
13215
13444
  var init_structure = __esm({
13216
13445
  "packages/ingest/src/structure.ts"() {
13217
13446
  "use strict";
13218
13447
  init_src2();
13219
13448
  init_entities();
13220
- SOURCE_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
13449
+ JS_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
13450
+ C_EXTS = [".c", ".h", ".cc", ".cpp", ".hpp", ".m", ".mm"];
13451
+ SOURCE_EXTS = [
13452
+ ...JS_EXTS,
13453
+ ".py",
13454
+ ".rs",
13455
+ ".go",
13456
+ ".swift",
13457
+ ".java",
13458
+ ".kt",
13459
+ ...C_EXTS
13460
+ ];
13221
13461
  SKIP_DIRS = /* @__PURE__ */ new Set([
13222
13462
  "node_modules",
13223
13463
  ".git",
@@ -13227,10 +13467,51 @@ var init_structure = __esm({
13227
13467
  ".next",
13228
13468
  "dist",
13229
13469
  "build",
13230
- "coverage"
13470
+ "coverage",
13471
+ // Build output and dependency caches of the other languages.
13472
+ "target",
13473
+ ".build",
13474
+ "__pycache__",
13475
+ ".venv",
13476
+ "venv",
13477
+ "vendor",
13478
+ "Pods",
13479
+ "DerivedData",
13480
+ ".gradle"
13231
13481
  ]);
13232
13482
  IMPORT_RE = /(?:import|export)\s+[^'"]*?from\s+['"]([^'"]+)['"]|import\s*\(\s*['"]([^'"]+)['"]\s*\)|require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
13233
13483
  toPosix = (p) => p.split(path4.sep).join("/");
13484
+ ext = (f) => path4.posix.extname(f);
13485
+ stem = (f) => f.slice(0, f.length - ext(f).length);
13486
+ nearness = (a, b) => {
13487
+ const x = a.split("/");
13488
+ const y = b.split("/");
13489
+ let i = 0;
13490
+ while (i < x.length && i < y.length && x[i] === y[i]) i++;
13491
+ return i;
13492
+ };
13493
+ resolveLongest = (segments, resolve) => {
13494
+ for (let n2 = segments.length; n2 > 0; n2--) {
13495
+ const hit = resolve(segments.slice(0, n2));
13496
+ if (hit) return hit;
13497
+ }
13498
+ return null;
13499
+ };
13500
+ PY_FROM = /^[ \t]*from[ \t]+(\.*)([\w.]*)[ \t]+import[ \t]+\(?([^\n#)]+)/gm;
13501
+ PY_IMPORT = /^[ \t]*import[ \t]+([\w.]+(?:[ \t]*,[ \t]*[\w.]+)*)/gm;
13502
+ RS_MOD = /^[ \t]*(?:pub(?:\([^)]*\))?[ \t]+)?mod[ \t]+(\w+)[ \t]*;/gm;
13503
+ RS_USE = /\buse[ \t]+([^;]+);/g;
13504
+ expandUse = (use) => {
13505
+ const clean = use.replace(/\s+/g, "");
13506
+ const brace = clean.match(/^(.*?)::\{(.*)\}$/);
13507
+ if (!brace) return [clean.replace(/::\*$/, "")];
13508
+ return brace[2].split(",").filter(Boolean).map(
13509
+ (part) => part === "self" ? brace[1] : `${brace[1]}::${part.replace(/::\*$/, "")}`
13510
+ );
13511
+ };
13512
+ GO_BLOCK = /\bimport[ \t]*\(([\s\S]*?)\)/g;
13513
+ GO_ONE = /\bimport[ \t]+(?:[\w.]+[ \t]+)?"([^"]+)"/g;
13514
+ RS_EXPORT = /extern[ \t]+"C"[ \t]+fn[ \t]+([A-Za-z_]\w{2,})/g;
13234
13515
  }
13235
13516
  });
13236
13517
 
@@ -15456,6 +15737,45 @@ function aiState(m) {
15456
15737
  aiAttempts: typeof m?.aiAttempts === "number" ? m.aiAttempts : 0
15457
15738
  };
15458
15739
  }
15740
+ function aiFacts(kind, m) {
15741
+ if (!m) return null;
15742
+ if (kind === "feature") {
15743
+ const f = m;
15744
+ return JSON.stringify([
15745
+ f.title,
15746
+ f.what,
15747
+ f.behavior ?? null,
15748
+ f.config ?? [],
15749
+ f.locations ?? [],
15750
+ f.why ?? null
15751
+ ]);
15752
+ }
15753
+ if (kind === "entry") {
15754
+ const e = m;
15755
+ return JSON.stringify([
15756
+ e.brainName,
15757
+ e.description ?? null,
15758
+ (e.architecture?.groups ?? []).map((g) => g.name),
15759
+ // Tuples, never objects: a stored model comes back from jsonb with its keys reordered.
15760
+ (e.architecture?.edges ?? []).map((x) => [x.from, x.to]),
15761
+ (e.features ?? []).map((f) => [f.slug, f.title, f.area, f.what])
15762
+ ]);
15763
+ }
15764
+ return null;
15765
+ }
15766
+ function withStoredAi(model, stored) {
15767
+ const s = stored;
15768
+ const fresh = model;
15769
+ return {
15770
+ ...model,
15771
+ ...s.presentation ? { presentation: s.presentation } : {},
15772
+ body: s.body ?? null,
15773
+ docTitle: s.docTitle ?? null,
15774
+ category: s.category ?? null,
15775
+ ...!fresh.flow && s.flow ? { flow: s.flow } : {},
15776
+ ...!fresh.flowDiagram && s.flowDiagram ? { flowDiagram: s.flowDiagram } : {}
15777
+ };
15778
+ }
15459
15779
  function aiComplete(kind, m) {
15460
15780
  if (!m) return false;
15461
15781
  const text = (v) => typeof v === "string" && v.trim().length > 0;
@@ -15471,6 +15791,28 @@ function aiComplete(kind, m) {
15471
15791
  return true;
15472
15792
  }
15473
15793
  }
15794
+ function pagePrompt(input) {
15795
+ const { categories, flowDiagramTitle, hasFlow, sharedLines = [] } = input;
15796
+ return [
15797
+ "Categories (pick exactly one key):",
15798
+ ...categories.length ? categories.map((c) => `- ${c.key}: ${c.name} (${c.description})`) : ["- general: General (everything)"],
15799
+ "",
15800
+ "Available diagrams (the only allowed flow block sources):",
15801
+ ...flowDiagramTitle ? [
15802
+ `- flowDiagram: "${flowDiagramTitle}" (the feature's runtime flow, lanes and stations)`
15803
+ ] : [],
15804
+ ...hasFlow && !flowDiagramTitle ? ["- flow: the feature's runtime steps as a simple chain"] : [],
15805
+ ...!flowDiagramTitle && !hasFlow ? ["- (none)"] : [],
15806
+ "",
15807
+ ...sharedLines.length ? [
15808
+ "Documented elsewhere for the same endpoint (proven facts; reuse them, link the page):",
15809
+ ...sharedLines,
15810
+ ""
15811
+ ] : [],
15812
+ "Fact sheet:",
15813
+ input.factSheet
15814
+ ].join("\n");
15815
+ }
15474
15816
  function parseDocPage(raw, categoryKeys, ctx) {
15475
15817
  const bad = (error2) => ({ page: null, error: error2, dropped: [] });
15476
15818
  const match = raw.match(/\{[\s\S]*\}/);
@@ -15614,16 +15956,32 @@ function parseArchNotes(raw, groups) {
15614
15956
  };
15615
15957
  }
15616
15958
  function archFlowsInput(model) {
15959
+ const modules = (model.raw?.modules ?? []).filter(
15960
+ (m) => !/\.test\.[a-z]+$/.test(m)
15961
+ );
15962
+ const cited = new Set(model.features.flatMap((f) => f.locations));
15617
15963
  const files = /* @__PURE__ */ new Map();
15618
- for (const m of model.raw?.modules ?? []) {
15619
- if (/\.test\.[a-z]+$/.test(m)) continue;
15964
+ for (const m of [
15965
+ ...modules.filter((m2) => cited.has(m2)),
15966
+ ...modules.filter((m2) => !cited.has(m2))
15967
+ ]) {
15620
15968
  const g = moduleGroup(m);
15621
15969
  const list = files.get(g) ?? [];
15622
- if (list.length < 8) list.push(m);
15970
+ if (list.length < 12) list.push(m);
15623
15971
  files.set(g, list);
15624
15972
  }
15973
+ const spanning = model.features.flatMap((f) => {
15974
+ const groups = [...new Set(f.locations.map(moduleGroup))];
15975
+ return groups.length >= 2 ? [`- ${f.title}: ${groups.join(", ")}`] : [];
15976
+ });
15625
15977
  return [
15626
15978
  archNotesInput(model),
15979
+ ...spanning.length ? [
15980
+ "",
15981
+ "## Features spanning groups (each feature's code lives in all of these groups)",
15982
+ "",
15983
+ ...spanning
15984
+ ] : [],
15627
15985
  "",
15628
15986
  "## Files (the ONLY paths you may cite as evidence, copied exactly)",
15629
15987
  ...[...files.entries()].flatMap(([g, list]) => [
@@ -15888,45 +16246,69 @@ async function generateDokuInner(brainId, opts = {}) {
15888
16246
  }
15889
16247
  continue;
15890
16248
  }
15891
- const sameFacts = stored !== void 0 && state.factHash === factHash;
16249
+ const facts = aiFacts(page.kind, page.model);
16250
+ const sameFacts = stored !== void 0 && (facts !== null ? aiFacts(page.kind, stored) === facts : state.factHash === factHash);
15892
16251
  let markdown = page.markdown;
15893
16252
  let model = page.model;
15894
16253
  let icon = null;
15895
16254
  let ok2 = !required;
15896
- if (opts.llm && page.polishable && page.kind === "architecture") {
15897
- try {
15898
- const { notes } = parseArchNotes(
15899
- await opts.llm.complete({
15900
- system: ARCH_SYSTEM,
15901
- prompt: archNotesInput(doku),
15902
- maxTokens: 800
15903
- }),
15904
- doku.architecture.groups.map((g) => g.name)
15905
- );
15906
- if (notes) {
15907
- model = { ...model, narrative: notes.narrative, roles: notes.roles };
15908
- polished = true;
15909
- ok2 = true;
16255
+ if (page.polishable && page.kind === "architecture") {
16256
+ const inputs = {
16257
+ notes: sha1(archNotesInput(doku)),
16258
+ flows: sha1(archFlowsInput(doku))
16259
+ };
16260
+ const s = stored ?? {};
16261
+ let notesFor;
16262
+ let flowsFor;
16263
+ if (typeof s.narrative === "string" && s.aiInputs?.notes === inputs.notes) {
16264
+ model = { ...model, narrative: s.narrative, roles: s.roles };
16265
+ notesFor = inputs.notes;
16266
+ ok2 = true;
16267
+ } else if (opts.llm) {
16268
+ try {
16269
+ const { notes } = parseArchNotes(
16270
+ await opts.llm.complete({
16271
+ system: ARCH_SYSTEM,
16272
+ prompt: archNotesInput(doku),
16273
+ maxTokens: 800
16274
+ }),
16275
+ doku.architecture.groups.map((g) => g.name)
16276
+ );
16277
+ if (notes) {
16278
+ model = {
16279
+ ...model,
16280
+ narrative: notes.narrative,
16281
+ roles: notes.roles
16282
+ };
16283
+ notesFor = inputs.notes;
16284
+ polished = true;
16285
+ ok2 = true;
16286
+ }
16287
+ } catch {
15910
16288
  }
15911
- } catch {
15912
16289
  }
15913
- try {
15914
- const { flows } = parseArchFlows(
15915
- await opts.llm.complete({
15916
- system: ARCH_FLOWS_SYSTEM,
15917
- prompt: archFlowsInput(doku),
15918
- maxTokens: 4500
15919
- }),
15920
- new Set(doku.raw?.modules ?? [])
15921
- );
15922
- if (flows.length > 0) {
15923
- model = { ...model, flows };
15924
- polished = true;
16290
+ if (Array.isArray(s.flows) && s.flows.length > 0 && s.aiInputs?.flows === inputs.flows) {
16291
+ model = { ...model, flows: s.flows };
16292
+ flowsFor = inputs.flows;
16293
+ } else if (opts.llm) {
16294
+ try {
16295
+ const { flows } = parseArchFlows(
16296
+ await opts.llm.complete({
16297
+ system: ARCH_FLOWS_SYSTEM,
16298
+ prompt: archFlowsInput(doku),
16299
+ maxTokens: 4500
16300
+ }),
16301
+ new Set(doku.raw?.modules ?? [])
16302
+ );
16303
+ if (flows.length > 0) {
16304
+ model = { ...model, flows };
16305
+ flowsFor = inputs.flows;
16306
+ polished = true;
16307
+ }
16308
+ } catch {
15925
16309
  }
15926
- } catch {
15927
16310
  }
15928
16311
  if (!ok2 && sameFacts) {
15929
- const s = stored;
15930
16312
  const kept = model;
15931
16313
  model = {
15932
16314
  ...model,
@@ -15934,6 +16316,14 @@ async function generateDokuInner(brainId, opts = {}) {
15934
16316
  ...!kept.flows && Array.isArray(s.flows) ? { flows: s.flows } : {}
15935
16317
  };
15936
16318
  }
16319
+ const present = model;
16320
+ model = {
16321
+ ...model,
16322
+ aiInputs: {
16323
+ ...typeof present.narrative === "string" ? { notes: notesFor ?? s.aiInputs?.notes } : {},
16324
+ ...Array.isArray(present.flows) ? { flows: flowsFor ?? s.aiInputs?.flows } : {}
16325
+ }
16326
+ };
15937
16327
  const arch = model;
15938
16328
  if (arch.narrative || arch.flows?.length)
15939
16329
  markdown = renderArchitecturePage(doku, {
@@ -15942,7 +16332,11 @@ async function generateDokuInner(brainId, opts = {}) {
15942
16332
  });
15943
16333
  } else if (page.kind === "entry") {
15944
16334
  model = { ...page.model, categories };
15945
- if (opts.llm && page.polishable) {
16335
+ const s = stored;
16336
+ if (sameFacts && typeof s?.narrative === "string" && s.narrative.trim()) {
16337
+ model = { ...model, narrative: s.narrative };
16338
+ ok2 = true;
16339
+ } else if (opts.llm && page.polishable) {
15946
16340
  try {
15947
16341
  const narrative2 = (await opts.llm.complete({
15948
16342
  system: ENTRY_NARRATIVE_SYSTEM,
@@ -15956,15 +16350,16 @@ async function generateDokuInner(brainId, opts = {}) {
15956
16350
  }
15957
16351
  } catch {
15958
16352
  }
15959
- if (!ok2 && sameFacts) {
15960
- const s = stored;
15961
- if (typeof s.narrative === "string" && s.narrative.trim())
15962
- model = { ...model, narrative: s.narrative };
15963
- }
15964
16353
  }
15965
16354
  const narrative = model.narrative;
15966
16355
  if (narrative)
15967
16356
  markdown = renderEntryPage(doku, "brainjar-doku/", narrative);
16357
+ } else if (page.kind === "feature" && sameFacts && !storedCollisions.has(page.slug) && aiComplete("feature", stored)) {
16358
+ const f = withStoredAi(page.model, stored);
16359
+ model = f;
16360
+ markdown = renderFeaturePage(f);
16361
+ if (f.docTitle) page.title = f.docTitle;
16362
+ ok2 = true;
15968
16363
  } else if (opts.llm && page.polishable) {
15969
16364
  try {
15970
16365
  const { flow } = parseFlow(
@@ -16018,25 +16413,13 @@ async function generateDokuInner(brainId, opts = {}) {
16018
16413
  hasFlowDiagram: Boolean(diagrams.flowDiagram),
16019
16414
  hasFlow: Boolean(diagrams.flow)
16020
16415
  };
16021
- const prompt = [
16022
- "Categories (pick exactly one key):",
16023
- ...categories.length ? categories.map((c) => `- ${c.key}: ${c.name} (${c.description})`) : ["- general: General (everything)"],
16024
- "",
16025
- "Available diagrams (the only allowed flow block sources):",
16026
- ...ctx.hasFlowDiagram ? [
16027
- `- flowDiagram: "${diagrams.flowDiagram.title}" (the feature's runtime flow, lanes and stations)`
16028
- ] : [],
16029
- ...ctx.hasFlow && !ctx.hasFlowDiagram ? ["- flow: the feature's runtime steps as a simple chain"] : [],
16030
- ...!ctx.hasFlowDiagram && !ctx.hasFlow ? ["- (none)"] : [],
16031
- "",
16032
- ...sharedLines.length ? [
16033
- "Documented elsewhere for the same endpoint (proven facts; reuse them, link the page):",
16034
- ...sharedLines,
16035
- ""
16036
- ] : [],
16037
- "Fact sheet:",
16038
- page.markdown
16039
- ].join("\n");
16416
+ const prompt = pagePrompt({
16417
+ categories,
16418
+ factSheet: page.markdown,
16419
+ flowDiagramTitle: diagrams.flowDiagram?.title ?? null,
16420
+ hasFlow: ctx.hasFlow,
16421
+ sharedLines
16422
+ });
16040
16423
  const keys = categoryKeys.length ? categoryKeys : ["general"];
16041
16424
  let synth = parseDocPage(
16042
16425
  await opts.llm.complete({
@@ -16066,17 +16449,20 @@ async function generateDokuInner(brainId, opts = {}) {
16066
16449
  `doku: blocks dropped (${page.slug}): ${synth.dropped.join("; ")}`
16067
16450
  );
16068
16451
  if (synth.page) {
16069
- const title = pickDocTitle(
16452
+ const s = stored;
16453
+ const same = s?.title === page.model.title && !storedCollisions.has(page.slug);
16454
+ const title = same && s?.docTitle ? s.docTitle : pickDocTitle(
16070
16455
  synth.page.title,
16071
16456
  page.title,
16072
16457
  page.slug,
16073
16458
  takenTitle
16074
16459
  );
16460
+ const category = same && s?.category && categoryKeys.includes(s.category) ? s.category : synth.page.category;
16075
16461
  const f = {
16076
16462
  ...model,
16077
16463
  presentation: synth.page.presentation,
16078
16464
  docTitle: title,
16079
- category: synth.page.category
16465
+ category
16080
16466
  };
16081
16467
  f.body = pageBody(f);
16082
16468
  model = f;
@@ -16094,17 +16480,7 @@ async function generateDokuInner(brainId, opts = {}) {
16094
16480
  console.warn(`doku: page synthesis failed (${page.slug}):`, e);
16095
16481
  }
16096
16482
  if (!ok2 && sameFacts && aiComplete("feature", stored)) {
16097
- const s = stored;
16098
- const fresh = model;
16099
- const f = {
16100
- ...model,
16101
- ...s.presentation ? { presentation: s.presentation } : {},
16102
- body: s.body ?? null,
16103
- docTitle: s.docTitle ?? null,
16104
- category: s.category ?? null,
16105
- ...!fresh.flow && s.flow ? { flow: s.flow } : {},
16106
- ...!fresh.flowDiagram && s.flowDiagram ? { flowDiagram: s.flowDiagram } : {}
16107
- };
16483
+ const f = withStoredAi(model, stored);
16108
16484
  model = f;
16109
16485
  markdown = renderFeaturePage(f);
16110
16486
  if (f.docTitle) page.title = f.docTitle;
@@ -16377,7 +16753,7 @@ var init_doku = __esm({
16377
16753
  `{"kind":"flow","title":"...","source":"flowDiagram"}: places the feature's existing runtime diagram where it helps the explanation; allowed only when the input lists that diagram as available, at most once.`,
16378
16754
  "Composition: explanation and outcome before technical detail. A feature exposing an API documents what it is, why you need it, and each endpoint with its parameters, request and response shape, and error cases, all from the facts only. A user-facing feature explains what is shown and how to use it, with steps only for a real action sequence. A pipeline explains what goes in, what comes out, and when it runs. A configuration-heavy feature tabulates its settings and explains precedence and effects. A feature containing several of these covers each. Mix kinds freely; there is no fixed template and no fixed number of blocks.",
16379
16755
  "Length follows the subject: an extensive feature gets several text sections with subheadings, the relationships, prerequisites, special cases, and reasons explained, and a concrete example; a small feature stays short. Never break a coherent explanation into bullet points for layout reasons and never cut it short. Explain a term at its first use; state the concrete consequence for the reader.",
16380
- "Keep every fact; invent nothing. Missing facts stay missing: leave optional blocks out, and when an indispensable piece of information is not available, say so in one plain sentence addressed to the reader (for example: The exact request path is not documented yet.), never mentioning a fact sheet, an input, or a model. Do not repeat the change history or the decision list (both are shown separately). No H1 heading (the title renders above).",
16756
+ "Keep every fact; invent nothing. Missing facts stay missing: leave optional blocks out, and when an indispensable piece of information is not available, say so in one plain sentence addressed to the reader (for example: The exact request path is not documented yet.), never mentioning a fact sheet, an input, or a model. The change history and the decision list are shown separately below the page: never reproduce them as a list or table. Their content still counts as facts. The page describes the CURRENT state: where a newer history entry or decision added, changed, replaced, or removed something, the text states the current state (for example that a capability no longer exists, or which mechanism replaced another), and the reasons that explain the current design belong in the explanation. When facts conflict, the newer one wins. Never replace a stated fact with what is usual elsewhere. No H1 heading (the title renders above).",
16381
16757
  "When the input lists facts documented elsewhere for the SAME endpoint or interface, use them: they are proven, so the endpoint may be shown here as well, and the other page may be linked with a short readable link text in the form [text](page:slug), copying the slug exactly. Never merge pages because their titles look alike.",
16382
16758
  PROSE_STYLE
16383
16759
  ].join(" ");
@@ -16433,11 +16809,12 @@ var init_doku = __esm({
16433
16809
  "You read one architecture overview of a software system (module groups with their features and dependencies, plus real file paths) and produce the system's main RUNTIME flows: how a request, session, or piece of data actually moves through the system.",
16434
16810
  'Respond ONLY with JSON of exactly this shape: {"flows": [{"slug": "...", "title": "...", "summary": "...", "explanation": "...", "lanes": [{"key": "...", "title": "..."}], "nodes": [{"id": "...", "lane": "...", "title": "...", "subtitle": "...", "kind": "...", "evidence": ["path"]}], "edges": [{"from": "...", "to": "...", "label": "...", "kind": "..."}]}]}. Nothing outside the JSON.',
16435
16811
  '"explanation" walks a reader through THIS flow in markdown (2-5 short paragraphs, ### subheadings where they help, at most 3500 characters): what starts it, what each phase does, what comes out. Describe an order only where the edges show it; where paths run in parallel or depend on a gate, say so instead of inventing a sequence. Name the same stations as the nodes.',
16436
- "Produce 2 to 4 flows, one per MAIN runtime path a newcomer must understand (for example: how input becomes stored data, how a query becomes an answer, how an external agent connects, how background verification runs). Cover every major path the features support; do not stop at two when the system clearly has more.",
16812
+ 'Produce up to 4 flows, one per MAIN runtime path a newcomer must understand (for example: how input becomes stored data, how a query becomes an answer, how an external agent connects, how background verification runs). Cover every major path the listed features describe and no path they do not: when they describe fewer paths, produce fewer flows, and when no feature is documented at all, respond {"flows": []}.',
16437
16813
  "lanes are 3-5 PHASES of the flow in order (intake, processing, storage, ...), titled in plain words. Never folder names.",
16438
16814
  "nodes are the runtime components in those phases: 4-10 per flow. kind is exactly one of: actor (human or external agent), surface (UI or endpoint), logic (application work), policy (a gate: auth, quota, validation), action (an outward call such as an LLM or external API), data (a store). title at most 3 words; subtitle at most 3 words (under 24 characters).",
16439
16815
  "evidence lists 1-3 file paths copied EXACTLY from the Files section that implement the node. Never invent or modify a path; omit evidence rather than guess.",
16440
16816
  'edges carry meaning: label with WHAT flows (at most 3 words, under 20 characters, never counts). kind is "primary" for the main path, "return" for a reply going back, "policy" for a gate outcome, "async" for background work.',
16817
+ "Draw an edge only where the input shows the connection: a listed dependency between the two groups, a feature whose code spans both groups (Features spanning groups), or one feature describing both steps. Never connect components because such systems usually are connected; a path the input does not show stays out of the flow.",
16441
16818
  "Ground everything in the listed features and dependencies. Invent nothing."
16442
16819
  ].join(" ");
16443
16820
  FEATURE_FLOW_SYSTEM = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brainjar",
3
- "version": "0.3.976",
3
+ "version": "0.3.977",
4
4
  "description": "BrainJar CLI: turn a repository into a live, self-documenting Brain.",
5
5
  "type": "module",
6
6
  "bin": {