brainjar 0.3.747 → 0.3.976

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 +1762 -376
  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.747";
110
+ CLI_VERSION = "0.3.976";
111
111
  CACHE = path.join(os.homedir(), ".brainjar", "version-check.json");
112
112
  DAY = 864e5;
113
113
  }
@@ -1344,6 +1344,8 @@ var require_defaults = __commonJS({
1344
1344
  idleTimeoutMillis: 3e4,
1345
1345
  client_encoding: "",
1346
1346
  ssl: false,
1347
+ // SSL negotiation style: 'postgres' (traditional SSLRequest) or 'direct'
1348
+ sslnegotiation: void 0,
1347
1349
  application_name: void 0,
1348
1350
  fallback_application_name: void 0,
1349
1351
  options: void 0,
@@ -1481,20 +1483,20 @@ var require_utils = __commonJS({
1481
1483
  }
1482
1484
  return config;
1483
1485
  }
1484
- var escapeIdentifier2 = function(str) {
1485
- return '"' + str.replace(/"/g, '""') + '"';
1486
+ var escapeIdentifier2 = function(str2) {
1487
+ return '"' + str2.replace(/"/g, '""') + '"';
1486
1488
  };
1487
- var escapeLiteral2 = function(str) {
1489
+ var escapeLiteral2 = function(str2) {
1488
1490
  let hasBackslash = false;
1489
1491
  let escaped = "'";
1490
- if (str == null) {
1492
+ if (str2 == null) {
1491
1493
  return "''";
1492
1494
  }
1493
- if (typeof str !== "string") {
1495
+ if (typeof str2 !== "string") {
1494
1496
  return "''";
1495
1497
  }
1496
- for (let i = 0; i < str.length; i++) {
1497
- const c = str[i];
1498
+ for (let i = 0; i < str2.length; i++) {
1499
+ const c = str2[i];
1498
1500
  if (c === "'") {
1499
1501
  escaped += c + c;
1500
1502
  } else if (c === "\\") {
@@ -1909,22 +1911,22 @@ var require_type_overrides = __commonJS({
1909
1911
  var require_pg_connection_string = __commonJS({
1910
1912
  "node_modules/pg-connection-string/index.js"(exports, module) {
1911
1913
  "use strict";
1912
- function parse(str, options = {}) {
1913
- if (str.charAt(0) === "/") {
1914
- const config2 = str.split(" ");
1914
+ function parse(str2, options = {}) {
1915
+ if (str2.charAt(0) === "/") {
1916
+ const config2 = str2.split(" ");
1915
1917
  return { host: config2[0], database: config2[1] };
1916
1918
  }
1917
1919
  const config = /* @__PURE__ */ Object.create(null);
1918
1920
  let result;
1919
1921
  let dummyHost = false;
1920
- if (/ |%[^a-f0-9]|%[a-f0-9][^a-f0-9]/i.test(str)) {
1921
- str = encodeURI(str).replace(/%25(\d\d)/g, "%$1");
1922
+ if (/ |%[^a-f0-9]|%[a-f0-9][^a-f0-9]/i.test(str2)) {
1923
+ str2 = encodeURI(str2).replace(/%25(\d\d)/g, "%$1");
1922
1924
  }
1923
1925
  try {
1924
1926
  try {
1925
- result = new URL(str, "postgres://base");
1927
+ result = new URL(str2, "postgres://base");
1926
1928
  } catch (e) {
1927
- result = new URL(str.replace("@/", "@___DUMMY___/"), "postgres://base");
1929
+ result = new URL(str2.replace("@/", "@___DUMMY___/"), "postgres://base");
1928
1930
  dummyHost = true;
1929
1931
  }
1930
1932
  } catch (err) {
@@ -1942,11 +1944,11 @@ var require_pg_connection_string = __commonJS({
1942
1944
  config.client_encoding = result.searchParams.get("encoding");
1943
1945
  return config;
1944
1946
  }
1945
- const hostname = dummyHost ? "" : result.hostname;
1947
+ const hostname2 = dummyHost ? "" : result.hostname;
1946
1948
  if (!config.host) {
1947
- config.host = decodeURIComponent(hostname);
1948
- } else if (hostname && /^%2f/i.test(hostname)) {
1949
- result.pathname = hostname + result.pathname;
1949
+ config.host = decodeURIComponent(hostname2);
1950
+ } else if (hostname2 && /^%2f/i.test(hostname2)) {
1951
+ result.pathname = hostname2 + result.pathname;
1950
1952
  }
1951
1953
  if (!config.port) {
1952
1954
  config.port = result.port;
@@ -1962,6 +1964,9 @@ var require_pg_connection_string = __commonJS({
1962
1964
  if (config.sslcert || config.sslkey || config.sslrootcert || config.sslmode) {
1963
1965
  config.ssl = {};
1964
1966
  }
1967
+ if (config.sslnegotiation === "direct" && config.ssl === void 0) {
1968
+ config.ssl = true;
1969
+ }
1965
1970
  const fs = config.sslcert || config.sslkey || config.sslrootcert ? __require("fs") : null;
1966
1971
  if (config.sslcert) {
1967
1972
  config.ssl.cert = fs.readFileSync(config.sslcert).toString();
@@ -2067,8 +2072,8 @@ var require_pg_connection_string = __commonJS({
2067
2072
  }, /* @__PURE__ */ Object.create(null));
2068
2073
  return poolConfig;
2069
2074
  }
2070
- function parseIntoClientConfig(str) {
2071
- return toClientConfig(parse(str));
2075
+ function parseIntoClientConfig(str2) {
2076
+ return toClientConfig(parse(str2));
2072
2077
  }
2073
2078
  function deprecatedSslModeWarning(sslmode) {
2074
2079
  if (!deprecatedSslModeWarning.warned && typeof process !== "undefined" && process.emitWarning) {
@@ -2167,6 +2172,15 @@ var require_connection_parameters = __commonJS({
2167
2172
  enumerable: false
2168
2173
  });
2169
2174
  }
2175
+ this.sslnegotiation = val("sslnegotiation", config, "PGSSLNEGOTIATION");
2176
+ if (this.sslnegotiation !== void 0 && this.sslnegotiation !== "postgres" && this.sslnegotiation !== "direct") {
2177
+ throw new Error(
2178
+ `Invalid sslnegotiation value: "${this.sslnegotiation}". Valid values are "postgres" and "direct".`
2179
+ );
2180
+ }
2181
+ if (this.sslnegotiation === "direct" && !this.ssl) {
2182
+ throw new Error("sslnegotiation=direct requires SSL to be enabled");
2183
+ }
2170
2184
  this.client_encoding = val("client_encoding", config);
2171
2185
  this.replication = val("replication", config);
2172
2186
  this.isDomainSocket = !(this.host || "").indexOf("/");
@@ -2205,6 +2219,7 @@ var require_connection_parameters = __commonJS({
2205
2219
  add(params, ssl, "sslkey");
2206
2220
  add(params, ssl, "sslcert");
2207
2221
  add(params, ssl, "sslrootcert");
2222
+ add(params, this, "sslnegotiation");
2208
2223
  if (this.database) {
2209
2224
  params.push("dbname=" + quoteParamValue(this.database));
2210
2225
  }
@@ -2446,7 +2461,7 @@ var require_query = __commonJS({
2446
2461
  if (typeof this.text !== "string" && typeof this.name !== "string") {
2447
2462
  return new Error("A query must have either text or a name. Supplying neither is unsupported.");
2448
2463
  }
2449
- const previous = connection.parsedStatements[this.name];
2464
+ const previous = connection.parsedStatements[this.name] || connection.submittedNamedStatements[this.name];
2450
2465
  if (this.text && previous && this.text !== previous) {
2451
2466
  return new Error(`Prepared statements must be unique - '${this.name}' was used for a different statement`);
2452
2467
  }
@@ -2466,7 +2481,7 @@ var require_query = __commonJS({
2466
2481
  return null;
2467
2482
  }
2468
2483
  hasBeenParsed(connection) {
2469
- return this.name && connection.parsedStatements[this.name];
2484
+ return this.name && (connection.parsedStatements[this.name] || connection.submittedNamedStatements[this.name]);
2470
2485
  }
2471
2486
  handlePortalSuspended(connection) {
2472
2487
  this._getRows(connection, this.rows);
@@ -2490,6 +2505,9 @@ var require_query = __commonJS({
2490
2505
  name: this.name,
2491
2506
  types: this.types
2492
2507
  });
2508
+ if (this.name) {
2509
+ connection.submittedNamedStatements[this.name] = this.text;
2510
+ }
2493
2511
  }
2494
2512
  try {
2495
2513
  connection.bind({
@@ -2500,6 +2518,8 @@ var require_query = __commonJS({
2500
2518
  valueMapper: utils.prepareValue
2501
2519
  });
2502
2520
  } catch (err) {
2521
+ connection.close({ type: "S", name: this.name });
2522
+ connection.sync();
2503
2523
  this.handleError(err, connection);
2504
2524
  return;
2505
2525
  }
@@ -2739,6 +2759,25 @@ var require_buffer_writer = __commonJS({
2739
2759
  this.offset += len;
2740
2760
  return this;
2741
2761
  }
2762
+ // Write an Int32 byte-length prefix immediately followed by the string's UTF-8
2763
+ // bytes. Postgres' Bind wire format prefixes every parameter with its length,
2764
+ // and doing it in one method computes Buffer.byteLength ONCE — the previous
2765
+ // `addInt32(Buffer.byteLength(s)).addString(s)` pairing scanned the string
2766
+ // three times (byteLength for the prefix, byteLength again inside addString,
2767
+ // then the encode), which is costly for large text parameters.
2768
+ addInt32PrefixedString(string) {
2769
+ const len = Buffer.byteLength(string);
2770
+ this.ensure(4 + len);
2771
+ const buffer = this.buffer;
2772
+ let offset = this.offset;
2773
+ buffer[offset++] = len >>> 24 & 255;
2774
+ buffer[offset++] = len >>> 16 & 255;
2775
+ buffer[offset++] = len >>> 8 & 255;
2776
+ buffer[offset++] = len >>> 0 & 255;
2777
+ buffer.write(string, offset, "utf-8");
2778
+ this.offset = offset + len;
2779
+ return this;
2780
+ }
2742
2781
  add(otherBuffer) {
2743
2782
  this.ensure(otherBuffer.length);
2744
2783
  otherBuffer.copy(this.buffer, this.offset);
@@ -2760,6 +2799,10 @@ var require_buffer_writer = __commonJS({
2760
2799
  this.buffer = Buffer.allocUnsafe(this.size);
2761
2800
  return result;
2762
2801
  }
2802
+ clear() {
2803
+ this.offset = 5;
2804
+ this.headerPosition = 0;
2805
+ }
2763
2806
  };
2764
2807
  exports.Writer = Writer;
2765
2808
  }
@@ -2796,7 +2839,7 @@ var require_serializer = __commonJS({
2796
2839
  );
2797
2840
  };
2798
2841
  var sendSASLInitialResponseMessage = function(mechanism, initialResponse) {
2799
- writer.addCString(mechanism).addInt32(Buffer.byteLength(initialResponse)).addString(initialResponse);
2842
+ writer.addCString(mechanism).addInt32PrefixedString(initialResponse);
2800
2843
  return writer.flush(
2801
2844
  112
2802
2845
  /* code.startup */
@@ -2855,8 +2898,7 @@ var require_serializer = __commonJS({
2855
2898
  0
2856
2899
  /* ParamType.STRING */
2857
2900
  );
2858
- paramWriter.addInt32(Buffer.byteLength(mappedVal));
2859
- paramWriter.addString(mappedVal);
2901
+ paramWriter.addInt32PrefixedString(mappedVal);
2860
2902
  }
2861
2903
  }
2862
2904
  };
@@ -2868,7 +2910,13 @@ var require_serializer = __commonJS({
2868
2910
  const len = values.length;
2869
2911
  writer.addCString(portal).addCString(statement);
2870
2912
  writer.addInt16(len);
2871
- writeValues(values, config.valueMapper);
2913
+ try {
2914
+ writeValues(values, config.valueMapper);
2915
+ } catch (err) {
2916
+ writer.clear();
2917
+ paramWriter.clear();
2918
+ throw err;
2919
+ }
2872
2920
  writer.addInt16(len);
2873
2921
  writer.add(paramWriter.flush());
2874
2922
  writer.addInt16(1);
@@ -3026,7 +3074,7 @@ var require_buffer_reader = __commonJS({
3026
3074
  cstring() {
3027
3075
  const start = this.offset;
3028
3076
  let end = start;
3029
- while (this.buffer[end++] !== 0) {
3077
+ while (this.buffer[end++]) {
3030
3078
  }
3031
3079
  this.offset = end;
3032
3080
  return this.buffer.toString(this.encoding, start, end - 1);
@@ -3248,7 +3296,7 @@ var require_parser = __commonJS({
3248
3296
  const parameterCount = reader.int16();
3249
3297
  const message = new messages_1.ParameterDescriptionMessage(LATEINIT_LENGTH, parameterCount);
3250
3298
  for (let i = 0; i < parameterCount; i++) {
3251
- message.dataTypeIDs[i] = reader.int32();
3299
+ message.dataTypeIDs[i] = reader.uint32();
3252
3300
  }
3253
3301
  return message;
3254
3302
  };
@@ -3353,7 +3401,8 @@ var require_dist = __commonJS({
3353
3401
  "node_modules/pg-protocol/dist/index.js"(exports) {
3354
3402
  "use strict";
3355
3403
  Object.defineProperty(exports, "__esModule", { value: true });
3356
- exports.DatabaseError = exports.serialize = exports.parse = void 0;
3404
+ exports.DatabaseError = exports.serialize = void 0;
3405
+ exports.parse = parse;
3357
3406
  var messages_1 = require_messages();
3358
3407
  Object.defineProperty(exports, "DatabaseError", { enumerable: true, get: function() {
3359
3408
  return messages_1.DatabaseError;
@@ -3368,7 +3417,6 @@ var require_dist = __commonJS({
3368
3417
  stream.on("data", (buffer) => parser.parse(buffer, callback));
3369
3418
  return new Promise((resolve) => stream.on("end", () => resolve()));
3370
3419
  }
3371
- exports.parse = parse;
3372
3420
  }
3373
3421
  });
3374
3422
 
@@ -3453,7 +3501,8 @@ var require_connection = __commonJS({
3453
3501
  "use strict";
3454
3502
  var EventEmitter = __require("events").EventEmitter;
3455
3503
  var { parse, serialize } = require_dist();
3456
- var { getStream, getSecureStream } = require_stream();
3504
+ var stream = require_stream();
3505
+ var { getStream } = stream;
3457
3506
  var flushBuffer = serialize.flush();
3458
3507
  var syncBuffer = serialize.sync();
3459
3508
  var endBuffer = serialize.end();
@@ -3468,7 +3517,9 @@ var require_connection = __commonJS({
3468
3517
  this._keepAlive = config.keepAlive;
3469
3518
  this._keepAliveInitialDelayMillis = config.keepAliveInitialDelayMillis;
3470
3519
  this.parsedStatements = {};
3520
+ this.submittedNamedStatements = {};
3471
3521
  this.ssl = config.ssl || false;
3522
+ this.sslNegotiation = config.sslNegotiation || "postgres";
3472
3523
  this._ending = false;
3473
3524
  this._emitMessage = false;
3474
3525
  const self = this;
@@ -3502,6 +3553,11 @@ var require_connection = __commonJS({
3502
3553
  if (!this.ssl) {
3503
3554
  return this.attachListeners(this.stream);
3504
3555
  }
3556
+ if (this.sslNegotiation === "direct") {
3557
+ return this.stream.once("connect", function() {
3558
+ self.upgradeToSSL(host, reportStreamError);
3559
+ });
3560
+ }
3505
3561
  this.stream.once("data", function(buffer) {
3506
3562
  const responseCode = buffer.toString("utf8");
3507
3563
  switch (responseCode) {
@@ -3514,31 +3570,38 @@ var require_connection = __commonJS({
3514
3570
  self.stream.end();
3515
3571
  return self.emit("error", new Error("There was an error establishing an SSL connection"));
3516
3572
  }
3517
- const options = {
3518
- socket: self.stream
3519
- };
3520
- if (self.ssl !== true) {
3521
- Object.assign(options, self.ssl);
3522
- if ("key" in self.ssl) {
3523
- options.key = self.ssl.key;
3524
- }
3525
- }
3526
- const net = __require("net");
3527
- if (net.isIP && net.isIP(host) === 0) {
3528
- options.servername = host;
3529
- }
3530
- try {
3531
- self.stream = getSecureStream(options);
3532
- } catch (err) {
3533
- return self.emit("error", err);
3534
- }
3535
- self.attachListeners(self.stream);
3536
- self.stream.on("error", reportStreamError);
3537
- self.emit("sslconnect");
3573
+ self.upgradeToSSL(host, reportStreamError);
3538
3574
  });
3539
3575
  }
3540
- attachListeners(stream) {
3541
- parse(stream, (msg) => {
3576
+ upgradeToSSL(host, reportStreamError) {
3577
+ const self = this;
3578
+ const options = {
3579
+ socket: self.stream
3580
+ };
3581
+ if (self.ssl !== true) {
3582
+ Object.assign(options, self.ssl);
3583
+ if ("key" in self.ssl) {
3584
+ options.key = self.ssl.key;
3585
+ }
3586
+ }
3587
+ if (self.sslNegotiation === "direct") {
3588
+ options.ALPNProtocols = ["postgresql"];
3589
+ }
3590
+ const net = __require("net");
3591
+ if (net.isIP && net.isIP(host) === 0) {
3592
+ options.servername = host;
3593
+ }
3594
+ try {
3595
+ self.stream = stream.getSecureStream(options);
3596
+ } catch (err) {
3597
+ return self.emit("error", err);
3598
+ }
3599
+ self.attachListeners(self.stream);
3600
+ self.stream.on("error", reportStreamError);
3601
+ self.emit("sslconnect");
3602
+ }
3603
+ attachListeners(stream2) {
3604
+ parse(stream2, (msg) => {
3542
3605
  const eventName = msg.name === "error" ? "errorMessage" : msg.name;
3543
3606
  if (this._emitMessage) {
3544
3607
  this.emit("message", msg);
@@ -3966,8 +4029,8 @@ var require_client = __commonJS({
3966
4029
  return Number.isFinite(value) ? value : defaultValue;
3967
4030
  }
3968
4031
  if (typeof value === "string" && value.trim() !== "") {
3969
- const n = Number(value);
3970
- return Number.isFinite(n) ? n : defaultValue;
4032
+ const n2 = Number(value);
4033
+ return Number.isFinite(n2) ? n2 : defaultValue;
3971
4034
  }
3972
4035
  return defaultValue;
3973
4036
  }
@@ -4005,15 +4068,19 @@ var require_client = __commonJS({
4005
4068
  this.connection = c.connection || new Connection2({
4006
4069
  stream: c.stream,
4007
4070
  ssl: this.connectionParameters.ssl,
4071
+ sslNegotiation: this.connectionParameters.sslnegotiation,
4008
4072
  keepAlive: c.keepAlive || false,
4009
4073
  keepAliveInitialDelayMillis: c.keepAliveInitialDelayMillis || 0,
4010
4074
  encoding: this.connectionParameters.client_encoding || "utf8"
4011
4075
  });
4012
4076
  this._queryQueue = [];
4077
+ this._sentQueryQueue = [];
4078
+ this.pipeline = Boolean(c.pipeline);
4013
4079
  this.binary = c.binary || defaults2.binary;
4014
4080
  this.processID = null;
4015
4081
  this.secretKey = null;
4016
4082
  this.ssl = this.connectionParameters.ssl || false;
4083
+ this.sslNegotiation = this.connectionParameters.sslnegotiation || "postgres";
4017
4084
  if (this.ssl && this.ssl.key) {
4018
4085
  Object.defineProperty(this.ssl, "key", {
4019
4086
  enumerable: false
@@ -4043,6 +4110,8 @@ var require_client = __commonJS({
4043
4110
  enqueueError(activeQuery);
4044
4111
  this._activeQuery = null;
4045
4112
  }
4113
+ this._sentQueryQueue.forEach(enqueueError);
4114
+ this._sentQueryQueue.length = 0;
4046
4115
  this._queryQueue.forEach(enqueueError);
4047
4116
  this._queryQueue.length = 0;
4048
4117
  }
@@ -4074,7 +4143,9 @@ var require_client = __commonJS({
4074
4143
  }
4075
4144
  con.on("connect", function() {
4076
4145
  if (self.ssl) {
4077
- con.requestSsl();
4146
+ if (self.sslNegotiation !== "direct") {
4147
+ con.requestSsl();
4148
+ }
4078
4149
  } else {
4079
4150
  con.startup(self.getStartupConf());
4080
4151
  }
@@ -4283,6 +4354,9 @@ var require_client = __commonJS({
4283
4354
  return;
4284
4355
  }
4285
4356
  this._activeQuery = null;
4357
+ if (activeQuery.name) {
4358
+ delete this.connection.submittedNamedStatements[activeQuery.name];
4359
+ }
4286
4360
  activeQuery.handleError(msg, this.connection);
4287
4361
  }
4288
4362
  _handleRowDescription(msg) {
@@ -4339,6 +4413,7 @@ var require_client = __commonJS({
4339
4413
  }
4340
4414
  if (activeQuery.name) {
4341
4415
  this.connection.parsedStatements[activeQuery.name] = activeQuery.text;
4416
+ delete this.connection.submittedNamedStatements[activeQuery.name];
4342
4417
  }
4343
4418
  }
4344
4419
  _handleCopyInResponse(msg) {
@@ -4405,6 +4480,9 @@ var require_client = __commonJS({
4405
4480
  });
4406
4481
  } else if (client._queryQueue.indexOf(query) !== -1) {
4407
4482
  client._queryQueue.splice(client._queryQueue.indexOf(query), 1);
4483
+ } else if (client._sentQueryQueue.indexOf(query) !== -1) {
4484
+ query.callback = () => {
4485
+ };
4408
4486
  }
4409
4487
  }
4410
4488
  setTypeParser(oid, format, parseFn) {
@@ -4416,13 +4494,17 @@ var require_client = __commonJS({
4416
4494
  // escapeIdentifier and escapeLiteral moved to utility functions & exported
4417
4495
  // on PG
4418
4496
  // re-exported here for backwards compatibility
4419
- escapeIdentifier(str) {
4420
- return utils.escapeIdentifier(str);
4497
+ escapeIdentifier(str2) {
4498
+ return utils.escapeIdentifier(str2);
4421
4499
  }
4422
- escapeLiteral(str) {
4423
- return utils.escapeLiteral(str);
4500
+ escapeLiteral(str2) {
4501
+ return utils.escapeLiteral(str2);
4424
4502
  }
4425
4503
  _pulseQueryQueue() {
4504
+ if (this.pipeline) {
4505
+ this._pulsePipelinedQueryQueue();
4506
+ return;
4507
+ }
4426
4508
  if (this.readyForQuery === true) {
4427
4509
  this._activeQuery = this._queryQueue.shift();
4428
4510
  const activeQuery = this._getActiveQuery();
@@ -4443,6 +4525,30 @@ var require_client = __commonJS({
4443
4525
  }
4444
4526
  }
4445
4527
  }
4528
+ _pulsePipelinedQueryQueue() {
4529
+ if (!this._connected || !this._queryable) {
4530
+ return;
4531
+ }
4532
+ while (this._queryQueue.length > 0) {
4533
+ const query = this._queryQueue.shift();
4534
+ this.hasExecuted = true;
4535
+ const queryError = query.submit(this.connection);
4536
+ if (queryError) {
4537
+ process.nextTick(() => {
4538
+ query.handleError(queryError, this.connection);
4539
+ });
4540
+ continue;
4541
+ }
4542
+ this._sentQueryQueue.push(query);
4543
+ }
4544
+ if (this.readyForQuery && !this._activeQuery && this._sentQueryQueue.length > 0) {
4545
+ this._activeQuery = this._sentQueryQueue.shift();
4546
+ this.readyForQuery = false;
4547
+ }
4548
+ if (!this._activeQuery && this._sentQueryQueue.length === 0 && this._queryQueue.length === 0 && this.hasExecuted) {
4549
+ this.emit("drain");
4550
+ }
4551
+ }
4446
4552
  query(config, values, callback) {
4447
4553
  let query;
4448
4554
  let result;
@@ -4486,6 +4592,9 @@ var require_client = __commonJS({
4486
4592
  const index = this._queryQueue.indexOf(query);
4487
4593
  if (index > -1) {
4488
4594
  this._queryQueue.splice(index, 1);
4595
+ } else if (this.pipeline) {
4596
+ this.connection.stream.destroy();
4597
+ return;
4489
4598
  }
4490
4599
  this._pulseQueryQueue();
4491
4600
  }, readTimeout);
@@ -4512,7 +4621,7 @@ var require_client = __commonJS({
4512
4621
  });
4513
4622
  return result;
4514
4623
  }
4515
- if (this._queryQueue.length > 0) {
4624
+ if (this._queryQueue.length > 0 && !this.pipeline) {
4516
4625
  queryQueueLengthDeprecationNotice();
4517
4626
  }
4518
4627
  this._queryQueue.push(query);
@@ -4538,7 +4647,11 @@ var require_client = __commonJS({
4538
4647
  return this._Promise.resolve();
4539
4648
  }
4540
4649
  }
4541
- if (this._getActiveQuery() || !this._queryable) {
4650
+ if (!this._queryable) {
4651
+ this.connection.stream.destroy();
4652
+ } else if (this.pipeline && (this._getActiveQuery() || this._sentQueryQueue.length > 0 || this._queryQueue.length > 0)) {
4653
+ this.once("drain", () => this.connection.end());
4654
+ } else if (this._getActiveQuery()) {
4542
4655
  this.connection.stream.destroy();
4543
4656
  } else {
4544
4657
  this.connection.end();
@@ -5028,7 +5141,7 @@ var require_query2 = __commonJS({
5028
5141
  sourceFunction: "routine"
5029
5142
  };
5030
5143
  NativeQuery.prototype.handleError = function(err) {
5031
- const fields = this.native.pq.resultErrorFields();
5144
+ const fields = this.native && this.native.pq.resultErrorFields();
5032
5145
  if (fields) {
5033
5146
  for (const key in fields) {
5034
5147
  const normalizedFieldName = errorFieldMap[key] || key;
@@ -5161,6 +5274,8 @@ var require_client2 = __commonJS({
5161
5274
  this._connecting = false;
5162
5275
  this._connected = false;
5163
5276
  this._queryable = true;
5277
+ this.pipeline = Boolean(config.pipeline);
5278
+ this._pipelineInFlight = false;
5164
5279
  const cp = this.connectionParameters = new ConnectionParameters(config);
5165
5280
  if (config.nativeConnectionString) cp.nativeConnectionString = config.nativeConnectionString;
5166
5281
  this.user = cp.user;
@@ -5304,7 +5419,7 @@ var require_client2 = __commonJS({
5304
5419
  });
5305
5420
  return result;
5306
5421
  }
5307
- if (this._queryQueue.length > 0) {
5422
+ if (this._queryQueue.length > 0 && !this.pipeline) {
5308
5423
  queryQueueLengthDeprecationNotice();
5309
5424
  }
5310
5425
  this._queryQueue.push(query);
@@ -5326,14 +5441,21 @@ var require_client2 = __commonJS({
5326
5441
  cb = (err) => err ? reject(err) : resolve();
5327
5442
  });
5328
5443
  }
5329
- this.native.end(function() {
5330
- self._connected = false;
5331
- self._errorAllQueries(new Error("Connection terminated"));
5332
- process.nextTick(() => {
5333
- self.emit("end");
5334
- if (cb) cb();
5444
+ const doEnd = function() {
5445
+ self.native.end(function() {
5446
+ self._connected = false;
5447
+ self._errorAllQueries(new Error("Connection terminated"));
5448
+ process.nextTick(() => {
5449
+ self.emit("end");
5450
+ if (cb) cb();
5451
+ });
5335
5452
  });
5336
- });
5453
+ };
5454
+ if (this.pipeline && (this._pipelineInFlight || this._queryQueue.length > 0)) {
5455
+ this.once("drain", doEnd);
5456
+ } else {
5457
+ doEnd();
5458
+ }
5337
5459
  return result;
5338
5460
  };
5339
5461
  Client2.prototype._hasActiveQuery = function() {
@@ -5343,6 +5465,9 @@ var require_client2 = __commonJS({
5343
5465
  if (!this._connected) {
5344
5466
  return;
5345
5467
  }
5468
+ if (this.pipeline && !initialConnection) {
5469
+ return this._pulsePipelinedQueryQueue();
5470
+ }
5346
5471
  if (this._hasActiveQuery()) {
5347
5472
  return;
5348
5473
  }
@@ -5360,6 +5485,69 @@ var require_client2 = __commonJS({
5360
5485
  self._pulseQueryQueue();
5361
5486
  });
5362
5487
  };
5488
+ Client2.prototype._pulsePipelinedQueryQueue = function() {
5489
+ if (!this._connected || this._pipelineInFlight) {
5490
+ return;
5491
+ }
5492
+ if (this._queryQueue.length === 0) {
5493
+ if (this.hasExecuted) {
5494
+ this.emit("drain");
5495
+ }
5496
+ return;
5497
+ }
5498
+ this._pipelineInFlight = true;
5499
+ const self = this;
5500
+ const queries = [];
5501
+ const nativeQueries = [];
5502
+ const utils = require_utils();
5503
+ while (this._queryQueue.length > 0) {
5504
+ const query = this._queryQueue.shift();
5505
+ this.hasExecuted = true;
5506
+ nativeQueries.push(query);
5507
+ const values = query.values ? query.values.map(utils.prepareValue) : null;
5508
+ const pipelineEntry = { text: query.text, name: query.name };
5509
+ if (values) {
5510
+ pipelineEntry.values = values;
5511
+ }
5512
+ if (query.name && this.namedQueries[query.name]) {
5513
+ pipelineEntry._alreadyPrepared = true;
5514
+ }
5515
+ queries.push(pipelineEntry);
5516
+ }
5517
+ this.native.pipeline(queries, function(err, results) {
5518
+ self._pipelineInFlight = false;
5519
+ if (err) {
5520
+ for (let i = 0; i < nativeQueries.length; i++) {
5521
+ const q = nativeQueries[i];
5522
+ q.native = self.native;
5523
+ q.handleError(err);
5524
+ }
5525
+ self._pulsePipelinedQueryQueue();
5526
+ return;
5527
+ }
5528
+ for (let i = 0; i < nativeQueries.length; i++) {
5529
+ const q = nativeQueries[i];
5530
+ const r = results[i];
5531
+ q.native = self.native;
5532
+ if (r.err) {
5533
+ q.handleError(r.err);
5534
+ } else {
5535
+ if (q.name) {
5536
+ self.namedQueries[q.name] = q.text;
5537
+ }
5538
+ q.state = "end";
5539
+ q.emit("end", r.result);
5540
+ if (q.callback) {
5541
+ q.callback(null, r.result);
5542
+ }
5543
+ }
5544
+ setImmediate(function() {
5545
+ q.emit("_done");
5546
+ });
5547
+ }
5548
+ self._pulsePipelinedQueryQueue();
5549
+ });
5550
+ };
5363
5551
  Client2.prototype.cancel = function(query) {
5364
5552
  if (this._activeQuery === query) {
5365
5553
  this.native.cancel(function() {
@@ -5707,6 +5895,11 @@ function loadEnv() {
5707
5895
  }
5708
5896
  return { ...cachedFileVars, ...process.env };
5709
5897
  }
5898
+ function estimateCostUsd(model, tokens) {
5899
+ const [base = model, tier] = model.split(":");
5900
+ const p = MODEL_PRICES[base] ?? MODEL_PRICES[MODEL_TIERS.standard];
5901
+ return (tier === "flex" ? 0.5 : 1) * (tokens.input * p.input + tokens.cachedInput * p.cachedInput + tokens.output * p.output) / 1e6;
5902
+ }
5710
5903
  function loadConfig(env = loadEnv()) {
5711
5904
  return new Config(env);
5712
5905
  }
@@ -5719,10 +5912,22 @@ function redactUrlPassword(url) {
5719
5912
  return url;
5720
5913
  }
5721
5914
  }
5722
- var cachedFileVars, DEFAULT_TENANT_ID, SECRETS, CORPORATE_API_KEY_ENV, DEFAULT_DATABASE_URL, MissingSecretError, Config;
5915
+ var cachedFileVars, MODEL_TIERS, MODEL_PRICES, DEFAULT_TENANT_ID, SECRETS, CORPORATE_API_KEY_ENV, DEFAULT_DATABASE_URL, MissingSecretError, Config;
5723
5916
  var init_config = __esm({
5724
5917
  "packages/shared/src/config.ts"() {
5725
5918
  "use strict";
5919
+ MODEL_TIERS = {
5920
+ fast: "claude-haiku-4-5",
5921
+ standard: "claude-sonnet-5",
5922
+ premium: "claude-opus-5"
5923
+ };
5924
+ MODEL_PRICES = {
5925
+ "claude-opus-5": { input: 5, cachedInput: 0.5, output: 25 },
5926
+ // Launch pricing made permanent (official pricing page, checked 2026-08-18).
5927
+ "claude-sonnet-5": { input: 2, cachedInput: 0.2, output: 10 },
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 }
5930
+ };
5726
5931
  DEFAULT_TENANT_ID = "00000000-0000-0000-0000-000000000002";
5727
5932
  SECRETS = {
5728
5933
  anthropicApiKey: {
@@ -5828,13 +6033,6 @@ var init_config = __esm({
5828
6033
  }
5829
6034
  });
5830
6035
 
5831
- // packages/shared/src/ratelimit.ts
5832
- var init_ratelimit = __esm({
5833
- "packages/shared/src/ratelimit.ts"() {
5834
- "use strict";
5835
- }
5836
- });
5837
-
5838
6036
  // packages/shared/src/sanitize.ts
5839
6037
  var init_sanitize = __esm({
5840
6038
  "packages/shared/src/sanitize.ts"() {
@@ -5851,6 +6049,8 @@ function validateFlowDiagram(d) {
5851
6049
  if (!short(f.title, 120)) errs.push("title: missing or too long");
5852
6050
  if (f.summary !== void 0 && !short(f.summary, 300))
5853
6051
  errs.push("summary: empty or too long");
6052
+ if (f.explanation !== void 0 && !short(f.explanation, 4e3))
6053
+ errs.push("explanation: empty or too long");
5854
6054
  const lanes = Array.isArray(f.lanes) ? f.lanes : [];
5855
6055
  if (lanes.length < 2 || lanes.length > 6)
5856
6056
  errs.push("lanes: need 2-6 phase bands");
@@ -5863,22 +6063,31 @@ function validateFlowDiagram(d) {
5863
6063
  } else laneKeys.add(l.key);
5864
6064
  }
5865
6065
  const nodes = Array.isArray(f.nodes) ? f.nodes : [];
5866
- if (nodes.length < 3 || nodes.length > 14)
5867
- errs.push("nodes: need 3-14 nodes");
6066
+ if (nodes.length < 3 || nodes.length > 18)
6067
+ errs.push("nodes: need 3-18 nodes");
5868
6068
  const ids = /* @__PURE__ */ new Set();
5869
- for (const n of nodes) {
5870
- if (!short(n?.id, 40) || !short(n?.title, 60)) {
6069
+ for (const n2 of nodes) {
6070
+ if (!short(n2?.id, 40) || !short(n2?.title, 60)) {
5871
6071
  errs.push("nodes: each needs id and title");
5872
6072
  continue;
5873
6073
  }
5874
- if (ids.has(n.id)) errs.push(`nodes: duplicate id ${n.id}`);
5875
- ids.add(n.id);
5876
- if (!laneKeys.has(n.lane ?? "")) errs.push(`nodes: ${n.id} unknown lane`);
5877
- if (!NODE_KINDS.has(n.kind ?? "")) errs.push(`nodes: ${n.id} unknown kind`);
5878
- if (n.subtitle !== void 0 && !short(n.subtitle, 60))
5879
- errs.push(`nodes: ${n.id} oversized subtitle`);
5880
- if (n.evidence !== void 0 && !Array.isArray(n.evidence))
5881
- errs.push(`nodes: ${n.id} evidence must be a list`);
6074
+ if (ids.has(n2.id)) errs.push(`nodes: duplicate id ${n2.id}`);
6075
+ ids.add(n2.id);
6076
+ if (!laneKeys.has(n2.lane ?? "")) errs.push(`nodes: ${n2.id} unknown lane`);
6077
+ if (!NODE_KINDS.has(n2.kind ?? "")) errs.push(`nodes: ${n2.id} unknown kind`);
6078
+ if (n2.subtitle !== void 0 && !short(n2.subtitle, 60))
6079
+ errs.push(`nodes: ${n2.id} oversized subtitle`);
6080
+ if (n2.evidence !== void 0 && !Array.isArray(n2.evidence))
6081
+ errs.push(`nodes: ${n2.id} evidence must be a list`);
6082
+ if (Array.isArray(n2.evidence)) {
6083
+ if (n2.evidence.length > 12)
6084
+ errs.push(`nodes: ${n2.id} too many evidence entries`);
6085
+ for (const ev of n2.evidence)
6086
+ if (!short(ev, 200) || /[-]/.test(ev))
6087
+ errs.push(`nodes: ${n2.id} evidence entry is not a short string`);
6088
+ }
6089
+ if (n2.description !== void 0 && !short(n2.description, 600))
6090
+ errs.push(`nodes: ${n2.id} description empty or too long`);
5882
6091
  }
5883
6092
  const edges = Array.isArray(f.edges) ? f.edges : [];
5884
6093
  if (edges.length < 2 || edges.length > 24)
@@ -5889,7 +6098,7 @@ function validateFlowDiagram(d) {
5889
6098
  continue;
5890
6099
  }
5891
6100
  if (e.from === e.to) errs.push("edges: self-loop");
5892
- if (e.label !== void 0 && !short(e.label, 40))
6101
+ if (e.label !== void 0 && !short(e.label, 60))
5893
6102
  errs.push("edges: oversized label");
5894
6103
  if (e.kind !== void 0 && !EDGE_KINDS.has(e.kind))
5895
6104
  errs.push("edges: unknown kind");
@@ -5957,7 +6166,55 @@ var init_capture_events = __esm({
5957
6166
  tool_name: 200,
5958
6167
  reason: 200
5959
6168
  };
5960
- cut = (s, n) => s === void 0 ? void 0 : s.length > n ? s.slice(0, n) : s;
6169
+ cut = (s, n2) => s === void 0 ? void 0 : s.length > n2 ? s.slice(0, n2) : s;
6170
+ }
6171
+ });
6172
+
6173
+ // packages/shared/src/emoji.ts
6174
+ function isEmoji(s) {
6175
+ return ONE_EMOJI.test(s);
6176
+ }
6177
+ var ONE_EMOJI;
6178
+ var init_emoji = __esm({
6179
+ "packages/shared/src/emoji.ts"() {
6180
+ "use strict";
6181
+ ONE_EMOJI = new RegExp("^\\p{RGI_Emoji}$", "v");
6182
+ }
6183
+ });
6184
+
6185
+ // packages/shared/src/password.ts
6186
+ var init_password = __esm({
6187
+ "packages/shared/src/password.ts"() {
6188
+ "use strict";
6189
+ }
6190
+ });
6191
+
6192
+ // packages/shared/src/totp.ts
6193
+ var init_totp = __esm({
6194
+ "packages/shared/src/totp.ts"() {
6195
+ "use strict";
6196
+ }
6197
+ });
6198
+
6199
+ // packages/shared/src/brand.ts
6200
+ var BRAND_MARK_PATH, BRAND_ICON_SVG;
6201
+ var init_brand = __esm({
6202
+ "packages/shared/src/brand.ts"() {
6203
+ "use strict";
6204
+ BRAND_MARK_PATH = "M14.47 157.37 L191.23 245.75 A24.00 24.00 0 0 1 204.50 267.21 L204.50 519.96 A12.00 12.00 0 0 1 187.13 530.70 L9.95 442.11 A18.00 18.00 0 0 1 0.00 426.01 L0.00 166.31 A10.00 10.00 0 0 1 14.47 157.37 Z M17.89 102.06 L203.22 9.39 A42.00 42.00 0 0 1 240.78 9.39 L421.89 99.94 A40.00 40.00 0 0 1 444.00 135.72 L444.00 195.70 A28.00 28.00 0 0 1 428.52 220.74 L253.97 308.01 A10.00 10.00 0 0 1 239.50 299.07 L239.50 249.29 A30.00 30.00 0 0 0 222.92 222.46 L17.89 119.94 A10.00 10.00 0 0 1 17.89 102.06 Z M256.08 332.67 L382.11 269.66 A40.00 40.00 0 0 1 417.89 269.66 L467.39 294.41 A40.00 40.00 0 0 1 489.50 330.19 L489.50 425.70 A10.00 10.00 0 0 1 483.97 434.65 L256.87 548.20 A12.00 12.00 0 0 1 239.50 537.46 L239.50 359.51 A30.00 30.00 0 0 1 256.08 332.67 Z";
6205
+ BRAND_ICON_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><rect width="640" height="640" rx="120" fill="#ffffff"/><g transform="translate(117.48,89.6) scale(0.82747)" fill="#1c1c1a"><path d="' + BRAND_MARK_PATH + '"/></g></svg>';
6206
+ }
6207
+ });
6208
+
6209
+ // packages/shared/src/summary-report.ts
6210
+ var CONTROL;
6211
+ var init_summary_report = __esm({
6212
+ "packages/shared/src/summary-report.ts"() {
6213
+ "use strict";
6214
+ CONTROL = new RegExp(
6215
+ "[\\u0000-\\u0008\\u000b\\u000c\\u000e-\\u001f\\u007f]",
6216
+ "g"
6217
+ );
5961
6218
  }
5962
6219
  });
5963
6220
 
@@ -5969,10 +6226,14 @@ var init_src = __esm({
5969
6226
  init_memory();
5970
6227
  init_confidence();
5971
6228
  init_config();
5972
- init_ratelimit();
5973
6229
  init_sanitize();
5974
6230
  init_flow();
5975
6231
  init_capture_events();
6232
+ init_emoji();
6233
+ init_password();
6234
+ init_totp();
6235
+ init_brand();
6236
+ init_summary_report();
5976
6237
  PROSE_STYLE = "Never use em dashes (\u2014) or en dashes (\u2013) anywhere; use commas, colons, periods, or parentheses instead.";
5977
6238
  }
5978
6239
  });
@@ -5986,8 +6247,11 @@ function getPool() {
5986
6247
  }
5987
6248
  pool = new esm_default.Pool({
5988
6249
  connectionString: config.databaseUrl,
6250
+ // Per process (BJ-137): many instances times the default would fill Postgres; each role
6251
+ // sets its own in the compose file. PgBouncer in front is the cloud ticket's (BJ-140).
6252
+ max: Number(process.env.DATABASE_POOL_MAX) || 10,
5989
6253
  // Session GUC set in the startup packet — race-free, applies before any query.
5990
- options: `-c app.tenant_id=${config.tenantId}`
6254
+ options: `-c app.tenant_id=${config.tenantId} -c hnsw.iterative_scan=strict_order`
5991
6255
  });
5992
6256
  pool.on("error", () => {
5993
6257
  });
@@ -6538,12 +6802,12 @@ var init_repository = __esm({
6538
6802
  * first (five logs cover a week completely, where a newest-N slice of raw objects sees only
6539
6803
  * the last hours of a busy range).
6540
6804
  */
6541
- async dailyLogsInRange(startIso, endIso, limit = 50) {
6805
+ async dailyLogsInRange(startIso, endIso, limit = 50, order = "desc") {
6542
6806
  return (await this.pool.query(
6543
6807
  `SELECT ${MO_COLUMNS}, ${EV_AGG} FROM memory_object
6544
6808
  WHERE brain_id = $1 AND type = 'daily_log'
6545
6809
  AND occurred_at >= $2::timestamptz AND occurred_at < $3::timestamptz
6546
- ORDER BY occurred_at DESC LIMIT $4`,
6810
+ ORDER BY occurred_at ${order === "asc" ? "ASC" : "DESC"} LIMIT $4`,
6547
6811
  [this.brainId, startIso, endIso, limit]
6548
6812
  )).rows;
6549
6813
  }
@@ -6729,6 +6993,20 @@ var init_repository = __esm({
6729
6993
  return r.rows;
6730
6994
  }
6731
6995
  // --- Overview / summaries ---
6996
+ /** Compact freshness inputs for the whole Brain, without loading bodies or N+1 queries. */
6997
+ async memoryFreshnessRows() {
6998
+ return (await this.pool.query(
6999
+ `SELECT m.id, m.type, m.title, m.updated_at AS "updatedAt",
7000
+ m.last_verified_at AS "lastVerifiedAt", m.status,
7001
+ EXISTS (SELECT 1 FROM superseded_object s WHERE s.object_id = m.id) AS superseded,
7002
+ EXISTS (SELECT 1 FROM evidence_link el
7003
+ JOIN raw_artifact ra ON ra.id = el.raw_artifact_id
7004
+ WHERE el.memory_object_id = m.id AND ra.last_seen_at > m.updated_at) AS "evidenceChanged"
7005
+ FROM memory_object m WHERE m.brain_id = $1
7006
+ ORDER BY m.updated_at DESC, m.id`,
7007
+ [this.brainId]
7008
+ )).rows;
7009
+ }
6732
7010
  /** Memory-object counts for this Brain, grouped by type and confidence (dashboard overview). */
6733
7011
  async counts() {
6734
7012
  const rows = (await this.pool.query(
@@ -6777,7 +7055,8 @@ var init_repository = __esm({
6777
7055
  return (await this.pool.query(
6778
7056
  `SELECT * FROM summary WHERE brain_id = $1
6779
7057
  AND ($3 OR created_by IS NULL OR created_by = $2)
6780
- ORDER BY created_at DESC`,
7058
+ ORDER BY period_start DESC NULLS LAST,
7059
+ COALESCE(period_end, period_start) DESC, created_at DESC`,
6781
7060
  [this.brainId, viewerId ?? null, viewerId === void 0]
6782
7061
  )).rows;
6783
7062
  }
@@ -6797,8 +7076,8 @@ var init_repository = __esm({
6797
7076
  if (!isConfidence(confidence))
6798
7077
  throw new Error(`invalid confidence: ${confidence}`);
6799
7078
  const r = await this.pool.query(
6800
- `INSERT INTO summary (brain_id, timescale, title, body, period_start, period_end, confidence, created_by)
6801
- VALUES ($1,$2,$3,$4,$5::date,$6::date,$7,$8) RETURNING *`,
7079
+ `INSERT INTO summary (brain_id, timescale, title, body, period_start, period_end, confidence, created_by, chronicle, data_through)
7080
+ VALUES ($1,$2,$3,$4,$5::date,$6::date,$7,$8,$9::jsonb,$10::date) RETURNING *`,
6802
7081
  [
6803
7082
  this.brainId,
6804
7083
  input.timescale,
@@ -6807,7 +7086,9 @@ var init_repository = __esm({
6807
7086
  input.periodStartIso ?? null,
6808
7087
  input.periodEndIso ?? null,
6809
7088
  confidence,
6810
- input.createdBy ?? null
7089
+ input.createdBy ?? null,
7090
+ input.chronicle ? JSON.stringify(input.chronicle) : null,
7091
+ input.dataThroughIso ?? null
6811
7092
  ]
6812
7093
  );
6813
7094
  return r.rows[0];
@@ -6842,14 +7123,69 @@ var init_repository = __esm({
6842
7123
  */
6843
7124
  async updateSummary(id, patch) {
6844
7125
  const r = await this.pool.query(
6845
- `UPDATE summary SET title = $3, body = $4, confidence = $5
7126
+ `UPDATE summary SET title = $3, body = $4, confidence = $5,
7127
+ chronicle = COALESCE($6::jsonb, chronicle),
7128
+ data_through = COALESCE($7::date, data_through)
6846
7129
  WHERE id = $1 AND brain_id = $2 RETURNING *`,
6847
- [id, this.brainId, patch.title, patch.body, patch.confidence]
7130
+ [
7131
+ id,
7132
+ this.brainId,
7133
+ patch.title,
7134
+ patch.body,
7135
+ patch.confidence,
7136
+ patch.chronicle ? JSON.stringify(patch.chronicle) : null,
7137
+ patch.dataThroughIso ?? null
7138
+ ]
7139
+ );
7140
+ const row = r.rows[0];
7141
+ if (!row) throw new Error("summary not found in this Brain");
7142
+ return row;
7143
+ }
7144
+ /**
7145
+ * Claim the report job for one floor (summaries review S01): returns true for exactly one
7146
+ * caller per (summary, hash), so two web processes never write the same report twice. A
7147
+ * claim older than ten minutes counts as abandoned (the process died mid-call) and may be
7148
+ * taken again; `force` retakes a failed one on the reader's request.
7149
+ */
7150
+ async claimSummaryReport(id, hash, opts = {}) {
7151
+ const r = await this.pool.query(
7152
+ `UPDATE summary
7153
+ SET report_status = 'pending', report_status_hash = $3,
7154
+ report_status_at = now(), report_error = NULL
7155
+ WHERE id = $1 AND brain_id = $2
7156
+ AND report_hash IS DISTINCT FROM $3
7157
+ AND (report_status_hash IS DISTINCT FROM $3
7158
+ OR report_status IS NULL
7159
+ OR report_status_at < now() - interval '10 minutes'
7160
+ OR (report_status = 'failed' AND $4))`,
7161
+ [id, this.brainId, hash, Boolean(opts.force)]
7162
+ );
7163
+ return (r.rowCount ?? 0) > 0;
7164
+ }
7165
+ /** Store the validated report for the floor it was written from; the job is done. */
7166
+ async saveSummaryReport(id, report, hash) {
7167
+ const r = await this.pool.query(
7168
+ `UPDATE summary
7169
+ SET report = $3::jsonb, report_hash = $4, report_at = now(),
7170
+ report_status = NULL, report_status_hash = NULL,
7171
+ report_status_at = NULL, report_error = NULL
7172
+ WHERE id = $1 AND brain_id = $2 RETURNING *`,
7173
+ [id, this.brainId, JSON.stringify(report), hash]
6848
7174
  );
6849
7175
  const row = r.rows[0];
6850
7176
  if (!row) throw new Error("summary not found in this Brain");
6851
7177
  return row;
6852
7178
  }
7179
+ /** Record why the report for this floor could not be written; the page shows it. */
7180
+ async failSummaryReport(id, hash, error) {
7181
+ await this.pool.query(
7182
+ `UPDATE summary
7183
+ SET report_status = 'failed', report_status_hash = $3,
7184
+ report_status_at = now(), report_error = $4
7185
+ WHERE id = $1 AND brain_id = $2`,
7186
+ [id, this.brainId, hash, error.slice(0, 200)]
7187
+ );
7188
+ }
6853
7189
  /** Store the synthesized prose layer with the floor hash it was built from. */
6854
7190
  async saveSummarySynthesis(id, text, hash) {
6855
7191
  const r = await this.pool.query(
@@ -6862,13 +7198,23 @@ var init_repository = __esm({
6862
7198
  return row;
6863
7199
  }
6864
7200
  // --- Keyword search (Postgres FTS) ---
6865
- /** Ranked full-text keyword search over title/why/body in this Brain. */
7201
+ /**
7202
+ * Ranked full-text search over title/why/body in this Brain where any query word counts
7203
+ * (documents sharing more and rarer words rank first). websearch_to_tsquery required EVERY
7204
+ * word, so a sentence-length question or ticket intent matched nothing: measured on 314 real
7205
+ * chat questions, 15% got a keyword hit, with OR nearly all do. plainto_tsquery still stems
7206
+ * and drops stop words; its AND becomes OR. Documents holding every word come first and are
7207
+ * flagged, so fusion can keep trusting them as before.
7208
+ */
6866
7209
  async searchKeyword(query, limit = 10) {
6867
7210
  const r = await this.pool.query(
6868
- `SELECT ${MO_COLUMNS}, ${EV_AGG}, ts_rank(tsv, websearch_to_tsquery('english', $1)) AS rank
6869
- FROM memory_object
6870
- WHERE brain_id = $2 AND tsv @@ websearch_to_tsquery('english', $1)
6871
- ORDER BY rank DESC
7211
+ `SELECT ${MO_COLUMNS}, ${EV_AGG}, ts_rank(tsv, q.kq) AS rank,
7212
+ tsv @@ q.all_kq AS all_words
7213
+ FROM memory_object,
7214
+ (SELECT plainto_tsquery('english', $1) AS all_kq,
7215
+ replace(plainto_tsquery('english', $1)::text, ' & ', ' | ')::tsquery AS kq) q
7216
+ WHERE brain_id = $2 AND tsv @@ q.kq
7217
+ ORDER BY all_words DESC, rank DESC
6872
7218
  LIMIT $3`,
6873
7219
  [query, this.brainId, limit]
6874
7220
  );
@@ -6958,8 +7304,10 @@ var init_repository = __esm({
6958
7304
  */
6959
7305
  async saveDocPage(input) {
6960
7306
  const r = await this.pool.query(
6961
- `INSERT INTO doc_page (brain_id, slug, version, kind, title, model, markdown, source_hash, deleted)
6962
- SELECT $1, $2, COALESCE(max(version), 0) + 1, $3, $4, $5, $6, $7, $8
7307
+ `INSERT INTO doc_page (brain_id, slug, version, kind, title, model, markdown, source_hash, deleted, icon)
7308
+ SELECT $1, $2, COALESCE(max(version), 0) + 1, $3, $4, $5, $6, $7, $8,
7309
+ COALESCE((SELECT icon FROM doc_page WHERE brain_id = $1 AND slug = $2
7310
+ ORDER BY version DESC LIMIT 1), $9)
6963
7311
  FROM doc_page WHERE brain_id = $1 AND slug = $2
6964
7312
  RETURNING *`,
6965
7313
  [
@@ -6970,11 +7318,21 @@ var init_repository = __esm({
6970
7318
  JSON.stringify(input.model),
6971
7319
  input.markdown,
6972
7320
  input.sourceHash,
6973
- input.deleted ?? false
7321
+ input.deleted ?? false,
7322
+ input.icon ?? null
6974
7323
  ]
6975
7324
  );
6976
7325
  return r.rows[0];
6977
7326
  }
7327
+ /** Set the icon on a page's current version; false when the page does not exist. */
7328
+ async setDocIcon(slug, icon) {
7329
+ const r = await this.pool.query(
7330
+ `UPDATE doc_page SET icon = $3 WHERE brain_id = $1 AND slug = $2
7331
+ AND version = (SELECT max(version) FROM doc_page WHERE brain_id = $1 AND slug = $2)`,
7332
+ [this.brainId, slug, icon]
7333
+ );
7334
+ return (r.rowCount ?? 0) > 0;
7335
+ }
6978
7336
  /** The current documentation: latest version per slug, tombstones excluded, entry first. */
6979
7337
  async latestDocPages() {
6980
7338
  return this.docPagesAsOf();
@@ -7037,35 +7395,48 @@ var init_repository = __esm({
7037
7395
  * ponytail: tsvector computed per query; add a stored generated column + GIN index when a
7038
7396
  * Brain's doc tree grows past a few hundred pages.
7039
7397
  */
7040
- async docSearch(query, limit = 8) {
7398
+ /**
7399
+ * Docs search over the stored pages as they stood at `at` (default: now), so the time
7400
+ * lens keeps its point in time (docs review R06). A page whose title equals the query
7401
+ * ranks first, then title prefixes and title mentions, then full-text rank: an exact
7402
+ * page never hides behind pages that merely mention it (R12).
7403
+ */
7404
+ async docSearch(query, limit = 8, at) {
7041
7405
  const terms = query.trim().toLowerCase().split(/\s+/).map((t) => t.replace(/[^\p{L}\p{N}]/gu, "")).filter(Boolean).slice(0, 8);
7042
7406
  if (terms.length === 0) return [];
7043
7407
  const tsq = terms.map((t, i) => i === terms.length - 1 ? `${t}:*` : t).join(" & ");
7408
+ const plain = query.trim().toLowerCase().replace(/\s+/g, " ");
7044
7409
  const r = await this.pool.query(
7045
7410
  `WITH latest AS (
7046
7411
  SELECT DISTINCT ON (slug) * FROM doc_page
7047
- WHERE brain_id = $1
7412
+ WHERE brain_id = $1 AND generated_at <= COALESCE($4, now())
7048
7413
  ORDER BY slug, version DESC
7049
7414
  )
7050
7415
  SELECT slug, kind, title,
7051
7416
  ts_headline('english', c.prose, q,
7052
7417
  'StartSel=[[, StopSel=]], MaxWords=18, MinWords=8') AS snippet
7053
7418
  FROM latest, to_tsquery('english', $2) q,
7054
- -- Snippets read as prose: fenced code, inline backticks, link URLs, and markdown
7055
- -- punctuation stripped for display only; the search vector keeps the raw markdown
7056
- -- so code and config names stay findable.
7419
+ -- Snippets read as prose: fenced code, inline backticks, link targets (web,
7420
+ -- repo-relative, and the docs' own page: scheme), and markdown punctuation
7421
+ -- stripped for display only; the search vector keeps the raw markdown so code
7422
+ -- and config names stay findable. (Template literal: regex backslashes are
7423
+ -- doubled, a single one silently vanishes and the pattern never matches.)
7057
7424
  LATERAL (SELECT regexp_replace(regexp_replace(regexp_replace(
7058
7425
  markdown, '\`\`\`[^\`]*\`\`\`', ' ', 'g'),
7059
- ']((https?|brainjar-doku)[^)]*)', ']', 'g'),
7426
+ '\\]\\((https?:|brainjar-doku/|features/|page:)[^)]*\\)', ']', 'g'),
7060
7427
  '[\`#*_|]', '', 'g') AS prose) c
7061
7428
  WHERE NOT deleted
7062
7429
  AND (setweight(to_tsvector('english', title), 'A')
7063
7430
  || setweight(to_tsvector('english', markdown), 'B')) @@ q
7064
- ORDER BY ts_rank(
7065
- setweight(to_tsvector('english', title), 'A')
7066
- || setweight(to_tsvector('english', markdown), 'B'), q) DESC
7431
+ ORDER BY
7432
+ (lower(title) = $5) DESC,
7433
+ (lower(title) LIKE $5 || '%') DESC,
7434
+ (position($5 in lower(title)) > 0) DESC,
7435
+ ts_rank(
7436
+ setweight(to_tsvector('english', title), 'A')
7437
+ || setweight(to_tsvector('english', markdown), 'B'), q) DESC
7067
7438
  LIMIT $3`,
7068
- [this.brainId, tsq, limit]
7439
+ [this.brainId, tsq, limit, at ?? null, plain]
7069
7440
  );
7070
7441
  return r.rows;
7071
7442
  }
@@ -7239,13 +7610,45 @@ var init_repository = __esm({
7239
7610
  }
7240
7611
  });
7241
7612
 
7613
+ // packages/db/src/ratelimit.ts
7614
+ var init_ratelimit = __esm({
7615
+ "packages/db/src/ratelimit.ts"() {
7616
+ "use strict";
7617
+ init_pool();
7618
+ }
7619
+ });
7620
+
7621
+ // packages/db/src/auth-event.ts
7622
+ var init_auth_event = __esm({
7623
+ "packages/db/src/auth-event.ts"() {
7624
+ "use strict";
7625
+ init_pool();
7626
+ }
7627
+ });
7628
+
7629
+ // packages/db/src/email-token.ts
7630
+ var init_email_token = __esm({
7631
+ "packages/db/src/email-token.ts"() {
7632
+ "use strict";
7633
+ init_pool();
7634
+ }
7635
+ });
7636
+
7242
7637
  // packages/db/src/auth.ts
7243
- var DEFAULT_TTL_HOURS;
7638
+ var DEFAULT_TTL_HOURS, LOGIN_FAILURE_WINDOW_MS, SCRYPT, REAUTH_WINDOW_MS, AVATAR_MAX_BYTES;
7244
7639
  var init_auth = __esm({
7245
7640
  "packages/db/src/auth.ts"() {
7246
7641
  "use strict";
7642
+ init_src();
7247
7643
  init_pool();
7644
+ init_ratelimit();
7645
+ init_auth_event();
7646
+ init_email_token();
7248
7647
  DEFAULT_TTL_HOURS = 24 * 90;
7648
+ LOGIN_FAILURE_WINDOW_MS = 15 * 6e4;
7649
+ SCRYPT = { N: 16384, r: 8, p: 1, maxmem: 64 * 1024 * 1024 };
7650
+ REAUTH_WINDOW_MS = 5 * 6e4;
7651
+ AVATAR_MAX_BYTES = 256 * 1024;
7249
7652
  }
7250
7653
  });
7251
7654
 
@@ -7269,11 +7672,43 @@ var init_oauth = __esm({
7269
7672
  }
7270
7673
  });
7271
7674
 
7675
+ // packages/db/src/totp.ts
7676
+ var CHALLENGE_TTL_MS;
7677
+ var init_totp2 = __esm({
7678
+ "packages/db/src/totp.ts"() {
7679
+ "use strict";
7680
+ init_src();
7681
+ init_pool();
7682
+ CHALLENGE_TTL_MS = 5 * 6e4;
7683
+ }
7684
+ });
7685
+
7686
+ // packages/db/src/passkey.ts
7687
+ var CHALLENGE_TTL_MS2;
7688
+ var init_passkey = __esm({
7689
+ "packages/db/src/passkey.ts"() {
7690
+ "use strict";
7691
+ init_pool();
7692
+ CHALLENGE_TTL_MS2 = 5 * 6e4;
7693
+ }
7694
+ });
7695
+
7696
+ // packages/db/src/identity.ts
7697
+ var STATE_TTL_MS;
7698
+ var init_identity = __esm({
7699
+ "packages/db/src/identity.ts"() {
7700
+ "use strict";
7701
+ init_pool();
7702
+ STATE_TTL_MS = 5 * 6e4;
7703
+ }
7704
+ });
7705
+
7272
7706
  // packages/db/src/team.ts
7273
7707
  var init_team = __esm({
7274
7708
  "packages/db/src/team.ts"() {
7275
7709
  "use strict";
7276
7710
  init_pool();
7711
+ init_auth();
7277
7712
  }
7278
7713
  });
7279
7714
 
@@ -7333,14 +7768,42 @@ var init_settings = __esm({
7333
7768
  APP_SETTING_KEYS = [
7334
7769
  "capture_mode",
7335
7770
  // transcript privacy: full | distilled | off
7336
- "author",
7337
- // developer identity stamped on captured reasoning
7338
- "promotion_threshold",
7339
- // signal/noise gate (0..1)
7340
7771
  "rationale_model",
7341
7772
  // Anthropic model for rationale/consolidation
7342
- "public_url"
7773
+ "public_url",
7343
7774
  // this server's address as clients reach it (wizard command, MCP config)
7775
+ // Outgoing mail (BJ-103); the password is a secret and lives next to the provider keys.
7776
+ "smtp_host",
7777
+ "smtp_port",
7778
+ "smtp_user",
7779
+ "smtp_secure",
7780
+ // "1" = TLS from the start (465), else STARTTLS (587)
7781
+ "mail_from",
7782
+ // Sign in with a provider (BJ-112); the client secrets are secrets, see below.
7783
+ "google_client_id",
7784
+ "github_client_id",
7785
+ // The legal pages the signup checkbox links to (BJ-113).
7786
+ "terms_url",
7787
+ "privacy_url",
7788
+ // Voice chat (voice handoff, 2026-09-21): an explicit "true" or "false"; absent means on.
7789
+ "voice_enabled",
7790
+ // The GPT-Live voice (voice picker, 2026-09-22): an id from VOICE_OPTIONS; absent = default.
7791
+ "voice_name",
7792
+ // AI cost cap (BJ-139), set on the Operator page; env overrides, defaults in ingest/allowance.
7793
+ "ai_contingent",
7794
+ // "on" | "off": monthly contingent per workspace (default on in SaaS)
7795
+ "ai_free_monthly_usd",
7796
+ // Free workspace, USD per month
7797
+ "ai_team_seat_monthly_usd",
7798
+ // Team, USD per seat and month (applies once licenses exist)
7799
+ "ai_operator_exempt",
7800
+ // "true" | "false": operator accounts are never capped
7801
+ "ai_daily_budget_usd",
7802
+ // the instance's emergency brake on the company key; empty = off
7803
+ "ai_budget_released_day",
7804
+ // YYYY-MM-DD the operator lifted today's brake
7805
+ "ai_budget_alerted_day"
7806
+ // YYYY-MM-DD the operator was told the brake tripped
7344
7807
  ];
7345
7808
  PROVIDER_KEY_SETTINGS = {
7346
7809
  anthropic: "anthropic_api_key",
@@ -7373,11 +7836,112 @@ var init_tickets = __esm({
7373
7836
  }
7374
7837
  });
7375
7838
 
7839
+ // packages/db/src/jobs.ts
7840
+ var CAPTURE_JOB_KINDS, USER_JOB_KINDS;
7841
+ var init_jobs = __esm({
7842
+ "packages/db/src/jobs.ts"() {
7843
+ "use strict";
7844
+ init_pool();
7845
+ CAPTURE_JOB_KINDS = [
7846
+ "capture-extract",
7847
+ "capture-session",
7848
+ "capture-transcript",
7849
+ "capture-structure"
7850
+ ];
7851
+ USER_JOB_KINDS = [
7852
+ ...CAPTURE_JOB_KINDS,
7853
+ "deepscan",
7854
+ "verify",
7855
+ "summary-report",
7856
+ "resynthesize"
7857
+ ];
7858
+ }
7859
+ });
7860
+
7861
+ // packages/db/src/spend.ts
7862
+ function dayKey(at = /* @__PURE__ */ new Date()) {
7863
+ return `day:${at.toISOString().slice(0, 10)}`;
7864
+ }
7865
+ async function addSpend(brainId, usd, at = /* @__PURE__ */ new Date(), pool2 = getPool()) {
7866
+ if (!(usd > 0)) return;
7867
+ await pool2.query(
7868
+ `INSERT INTO llm_spend (key, usd)
7869
+ SELECT k, $2 FROM (
7870
+ SELECT $3::text AS k
7871
+ UNION ALL
7872
+ SELECT 'ws:' || b.owner_id || ':' || $4 FROM brain b
7873
+ WHERE b.id = $1 AND b.owner_id IS NOT NULL
7874
+ ) keys
7875
+ ON CONFLICT (key) DO UPDATE SET usd = llm_spend.usd + EXCLUDED.usd, updated_at = now()`,
7876
+ [brainId, usd, dayKey(at), at.toISOString().slice(0, 7)]
7877
+ );
7878
+ }
7879
+ var init_spend = __esm({
7880
+ "packages/db/src/spend.ts"() {
7881
+ "use strict";
7882
+ init_pool();
7883
+ }
7884
+ });
7885
+
7886
+ // packages/db/src/provider-rate.ts
7887
+ async function saveRate(keyId2, model, s, pool2 = getPool()) {
7888
+ await pool2.query(
7889
+ `INSERT INTO llm_rate (key_id, model, requests_limit, requests_remaining, requests_reset_at,
7890
+ tokens_limit, tokens_remaining, tokens_reset_at, observed_at)
7891
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
7892
+ ON CONFLICT (key_id, model) DO UPDATE SET
7893
+ requests_limit = EXCLUDED.requests_limit,
7894
+ requests_remaining = EXCLUDED.requests_remaining,
7895
+ requests_reset_at = EXCLUDED.requests_reset_at,
7896
+ tokens_limit = EXCLUDED.tokens_limit,
7897
+ tokens_remaining = EXCLUDED.tokens_remaining,
7898
+ tokens_reset_at = EXCLUDED.tokens_reset_at,
7899
+ observed_at = EXCLUDED.observed_at
7900
+ WHERE llm_rate.observed_at <= EXCLUDED.observed_at`,
7901
+ [
7902
+ keyId2,
7903
+ model,
7904
+ s.requestsLimit,
7905
+ s.requestsRemaining,
7906
+ s.requestsResetAt,
7907
+ s.tokensLimit,
7908
+ s.tokensRemaining,
7909
+ s.tokensResetAt,
7910
+ s.observedAt
7911
+ ]
7912
+ );
7913
+ }
7914
+ async function loadRate(keyId2, model, pool2 = getPool()) {
7915
+ const r = await pool2.query(
7916
+ "SELECT * FROM llm_rate WHERE key_id = $1 AND model = $2",
7917
+ [keyId2, model]
7918
+ );
7919
+ const x = r.rows[0];
7920
+ if (!x) return null;
7921
+ const n2 = (v) => v === null ? null : Number(v);
7922
+ return {
7923
+ requestsLimit: n2(x.requests_limit),
7924
+ requestsRemaining: n2(x.requests_remaining),
7925
+ requestsResetAt: x.requests_reset_at,
7926
+ tokensLimit: n2(x.tokens_limit),
7927
+ tokensRemaining: n2(x.tokens_remaining),
7928
+ tokensResetAt: x.tokens_reset_at,
7929
+ observedAt: x.observed_at
7930
+ };
7931
+ }
7932
+ var init_provider_rate = __esm({
7933
+ "packages/db/src/provider-rate.ts"() {
7934
+ "use strict";
7935
+ init_pool();
7936
+ }
7937
+ });
7938
+
7376
7939
  // packages/db/src/ruleset.ts
7377
7940
  var init_ruleset = __esm({
7378
7941
  "packages/db/src/ruleset.ts"() {
7379
7942
  "use strict";
7380
7943
  init_pool();
7944
+ init_tickets();
7381
7945
  }
7382
7946
  });
7383
7947
 
@@ -7396,11 +7960,21 @@ async function recordLlmUsage(u) {
7396
7960
  Math.max(0, Math.round(u.outputTokens))
7397
7961
  ]
7398
7962
  );
7963
+ await addSpend(
7964
+ u.brainId ?? null,
7965
+ estimateCostUsd(u.model, {
7966
+ input: u.inputTokens,
7967
+ cachedInput: u.cachedInputTokens,
7968
+ output: u.outputTokens
7969
+ })
7970
+ );
7399
7971
  }
7400
7972
  var init_usage = __esm({
7401
7973
  "packages/db/src/usage.ts"() {
7402
7974
  "use strict";
7975
+ init_src();
7403
7976
  init_pool();
7977
+ init_spend();
7404
7978
  }
7405
7979
  });
7406
7980
 
@@ -7414,12 +7988,21 @@ var init_src2 = __esm({
7414
7988
  init_auth();
7415
7989
  init_api_token();
7416
7990
  init_oauth();
7991
+ init_ratelimit();
7992
+ init_auth_event();
7993
+ init_email_token();
7994
+ init_totp2();
7995
+ init_passkey();
7996
+ init_identity();
7417
7997
  init_team();
7418
7998
  init_rbac();
7419
7999
  init_settings();
7420
8000
  init_notifications();
7421
8001
  init_conversations();
7422
8002
  init_tickets();
8003
+ init_jobs();
8004
+ init_spend();
8005
+ init_provider_rate();
7423
8006
  init_ruleset();
7424
8007
  init_usage();
7425
8008
  init_audit_origin();
@@ -7671,14 +8254,14 @@ var init_values = __esm({
7671
8254
  };
7672
8255
  isArray = (val) => (isArray = Array.isArray, isArray(val));
7673
8256
  isReadonlyArray = isArray;
7674
- validatePositiveInteger = (name, n) => {
7675
- if (typeof n !== "number" || !Number.isInteger(n)) {
8257
+ validatePositiveInteger = (name, n2) => {
8258
+ if (typeof n2 !== "number" || !Number.isInteger(n2)) {
7676
8259
  throw new AnthropicError(`${name} must be an integer`);
7677
8260
  }
7678
- if (n < 0) {
8261
+ if (n2 < 0) {
7679
8262
  throw new AnthropicError(`${name} must be a positive integer`);
7680
8263
  }
7681
- return n;
8264
+ return n2;
7682
8265
  };
7683
8266
  safeJSON = (text) => {
7684
8267
  try {
@@ -7950,9 +8533,9 @@ function concatBytes(buffers) {
7950
8533
  }
7951
8534
  return output;
7952
8535
  }
7953
- function encodeUTF8(str) {
8536
+ function encodeUTF8(str2) {
7954
8537
  let encoder;
7955
- return (encodeUTF8_ ?? (encoder = new globalThis.TextEncoder(), encodeUTF8_ = encoder.encode.bind(encoder)))(str);
8538
+ return (encodeUTF8_ ?? (encoder = new globalThis.TextEncoder(), encodeUTF8_ = encoder.encode.bind(encoder)))(str2);
7956
8539
  }
7957
8540
  function decodeUTF8(bytes) {
7958
8541
  let decoder;
@@ -8171,12 +8754,12 @@ async function* iterSSEChunks(iterator) {
8171
8754
  yield data;
8172
8755
  }
8173
8756
  }
8174
- function partition(str, delimiter) {
8175
- const index = str.indexOf(delimiter);
8757
+ function partition(str2, delimiter) {
8758
+ const index = str2.indexOf(delimiter);
8176
8759
  if (index !== -1) {
8177
- return [str.substring(0, index), delimiter, str.substring(index + delimiter.length)];
8760
+ return [str2.substring(0, index), delimiter, str2.substring(index + delimiter.length)];
8178
8761
  }
8179
- return [str, "", ""];
8762
+ return [str2, "", ""];
8180
8763
  }
8181
8764
  var _Stream_client, Stream, SSEDecoder;
8182
8765
  var init_streaming = __esm({
@@ -8891,8 +9474,8 @@ var init_headers = __esm({
8891
9474
  });
8892
9475
 
8893
9476
  // node_modules/@anthropic-ai/sdk/internal/utils/path.mjs
8894
- function encodeURIPath(str) {
8895
- return str.replace(/[^A-Za-z0-9\-._~!$&'()*+,;=:@]+/g, encodeURIComponent);
9477
+ function encodeURIPath(str2) {
9478
+ return str2.replace(/[^A-Za-z0-9\-._~!$&'()*+,;=:@]+/g, encodeURIComponent);
8896
9479
  }
8897
9480
  var EMPTY, createPathTagFunction, path3;
8898
9481
  var init_path = __esm({
@@ -12288,11 +12871,118 @@ var init_sdk = __esm({
12288
12871
  }
12289
12872
  });
12290
12873
 
12874
+ // packages/ingest/src/provider-rate.ts
12875
+ import { createHash as createHash2 } from "node:crypto";
12876
+ function parseDuration(s) {
12877
+ if (!s) return null;
12878
+ let ms = 0;
12879
+ let hit = false;
12880
+ for (const [, n2, unit] of s.matchAll(/(\d+(?:\.\d+)?)(ms|h|m|s)/g)) {
12881
+ hit = true;
12882
+ ms += Number(n2) * UNIT_MS[unit];
12883
+ }
12884
+ return hit ? ms : null;
12885
+ }
12886
+ function parseRateHeaders(h, now = /* @__PURE__ */ new Date()) {
12887
+ const num = (a, b) => {
12888
+ const v = h.get(a) ?? h.get(b);
12889
+ const n2 = v === null ? NaN : Number(v);
12890
+ return Number.isFinite(n2) ? n2 : null;
12891
+ };
12892
+ const at = (openai, anthropic) => {
12893
+ const d = parseDuration(h.get(openai));
12894
+ if (d !== null) return new Date(now.getTime() + d);
12895
+ const t = Date.parse(h.get(anthropic) ?? "");
12896
+ return Number.isFinite(t) ? new Date(t) : null;
12897
+ };
12898
+ const s = {
12899
+ requestsLimit: num(
12900
+ "x-ratelimit-limit-requests",
12901
+ "anthropic-ratelimit-requests-limit"
12902
+ ),
12903
+ requestsRemaining: num(
12904
+ "x-ratelimit-remaining-requests",
12905
+ "anthropic-ratelimit-requests-remaining"
12906
+ ),
12907
+ requestsResetAt: at(
12908
+ "x-ratelimit-reset-requests",
12909
+ "anthropic-ratelimit-requests-reset"
12910
+ ),
12911
+ tokensLimit: num(
12912
+ "x-ratelimit-limit-tokens",
12913
+ "anthropic-ratelimit-tokens-limit"
12914
+ ),
12915
+ tokensRemaining: num(
12916
+ "x-ratelimit-remaining-tokens",
12917
+ "anthropic-ratelimit-tokens-remaining"
12918
+ ),
12919
+ tokensResetAt: at(
12920
+ "x-ratelimit-reset-tokens",
12921
+ "anthropic-ratelimit-tokens-reset"
12922
+ ),
12923
+ observedAt: now
12924
+ };
12925
+ return s.requestsLimit === null && s.tokensLimit === null ? null : s;
12926
+ }
12927
+ function roomWaitMs(s, now = /* @__PURE__ */ new Date(), reserve = RESERVE) {
12928
+ const wait = (limit, remaining, resetAt) => {
12929
+ if (!limit || remaining === null || !resetAt) return 0;
12930
+ const need = reserve * limit;
12931
+ if (remaining >= need || resetAt <= now) return 0;
12932
+ const span = resetAt.getTime() - s.observedAt.getTime();
12933
+ const okAt = s.observedAt.getTime() + span * (need - remaining) / (limit - remaining);
12934
+ return Math.max(0, Math.ceil(okAt - now.getTime()));
12935
+ };
12936
+ return Math.max(
12937
+ wait(s.requestsLimit, s.requestsRemaining, s.requestsResetAt),
12938
+ wait(s.tokensLimit, s.tokensRemaining, s.tokensResetAt)
12939
+ );
12940
+ }
12941
+ async function awaitRoom(id, model) {
12942
+ for (; ; ) {
12943
+ const s = await loadRate(id, model).catch(() => null);
12944
+ const ms = s ? roomWaitMs(s) : 0;
12945
+ if (ms <= 0) return;
12946
+ await sleep2(Math.min(ms, MAX_NAP_MS) + Math.random() * 250);
12947
+ }
12948
+ }
12949
+ function noteRate(id, model, headers) {
12950
+ const s = headers ? parseRateHeaders(headers) : null;
12951
+ if (s) void saveRate(id, model, s).catch(() => {
12952
+ });
12953
+ }
12954
+ function rateFetch(apiKey, model, automatic) {
12955
+ const id = keyId(apiKey);
12956
+ return async (input, init2) => {
12957
+ if (automatic) await awaitRoom(id, model);
12958
+ const res = await fetch(input, init2);
12959
+ noteRate(id, model, res.headers);
12960
+ return res;
12961
+ };
12962
+ }
12963
+ var RESERVE, MAX_NAP_MS, sleep2, isAutomatic, keyId, UNIT_MS;
12964
+ var init_provider_rate2 = __esm({
12965
+ "packages/ingest/src/provider-rate.ts"() {
12966
+ "use strict";
12967
+ init_src2();
12968
+ RESERVE = 0.2;
12969
+ MAX_NAP_MS = 6e4;
12970
+ sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
12971
+ isAutomatic = (meta) => !meta?.actor && !meta?.waiting;
12972
+ keyId = (apiKey) => createHash2("sha256").update(apiKey).digest("hex").slice(0, 16);
12973
+ UNIT_MS = { ms: 1, s: 1e3, m: 6e4, h: 36e5 };
12974
+ }
12975
+ });
12976
+
12291
12977
  // packages/ingest/src/llm.ts
12292
12978
  function rationaleModelId(env = process.env) {
12293
12979
  return env.BRAINJAR_RATIONALE_MODEL?.trim() || "claude-opus-5";
12294
12980
  }
12295
12981
  function logUsage(meta, model, usage) {
12982
+ if (!meta?.brainId)
12983
+ console.warn(
12984
+ `brainjar: AI call without a Brain (workload ${meta?.workload ?? "other"}); it counts only toward the daily budget`
12985
+ );
12296
12986
  void recordLlmUsage({
12297
12987
  brainId: meta?.brainId ?? null,
12298
12988
  workload: meta?.workload ?? "other",
@@ -12318,6 +13008,7 @@ var init_llm = __esm({
12318
13008
  "use strict";
12319
13009
  init_sdk();
12320
13010
  init_src2();
13011
+ init_provider_rate2();
12321
13012
  cachedSystem = (system) => [
12322
13013
  {
12323
13014
  type: "text",
@@ -12330,9 +13021,12 @@ var init_llm = __esm({
12330
13021
  model;
12331
13022
  meta;
12332
13023
  constructor(opts) {
12333
- this.client = new Anthropic({ apiKey: opts.apiKey });
12334
13024
  this.model = opts.model ?? rationaleModelId();
12335
13025
  this.meta = opts.meta;
13026
+ this.client = new Anthropic({
13027
+ apiKey: opts.apiKey,
13028
+ fetch: rateFetch(opts.apiKey, this.model, isAutomatic(opts.meta))
13029
+ });
12336
13030
  }
12337
13031
  async complete({
12338
13032
  system,
@@ -12379,17 +13073,29 @@ var init_llm = __esm({
12379
13073
  var init_openai = __esm({
12380
13074
  "packages/ingest/src/openai.ts"() {
12381
13075
  "use strict";
13076
+ init_src();
12382
13077
  init_llm();
13078
+ init_provider_rate2();
12383
13079
  }
12384
13080
  });
12385
13081
 
12386
- // packages/ingest/src/routing.ts
12387
- var init_routing = __esm({
12388
- "packages/ingest/src/routing.ts"() {
13082
+ // packages/ingest/src/allowance.ts
13083
+ var init_allowance = __esm({
13084
+ "packages/ingest/src/allowance.ts"() {
12389
13085
  "use strict";
12390
13086
  init_src2();
12391
13087
  init_src();
12392
- init_llm();
13088
+ }
13089
+ });
13090
+
13091
+ // packages/ingest/src/routing.ts
13092
+ var init_routing = __esm({
13093
+ "packages/ingest/src/routing.ts"() {
13094
+ "use strict";
13095
+ init_src2();
13096
+ init_src();
13097
+ init_allowance();
13098
+ init_llm();
12393
13099
  init_openai();
12394
13100
  }
12395
13101
  });
@@ -13716,7 +14422,7 @@ ${line}
13716
14422
  `.split("\n");
13717
14423
  } else {
13718
14424
  const i = lines.findIndex(
13719
- (l, n) => n >= range[0] && n < range[1] && /^\s*url\s*=/.test(l)
14425
+ (l, n2) => n2 >= range[0] && n2 < range[1] && /^\s*url\s*=/.test(l)
13720
14426
  );
13721
14427
  next = [...lines];
13722
14428
  if (i >= 0) next[i] = line;
@@ -13936,17 +14642,25 @@ var init_daily_log = __esm({
13936
14642
  });
13937
14643
 
13938
14644
  // packages/ingest/src/summaries.ts
13939
- var SYNTH_SYSTEM;
14645
+ var REPORT_SYSTEM;
13940
14646
  var init_summaries = __esm({
13941
14647
  "packages/ingest/src/summaries.ts"() {
13942
14648
  "use strict";
13943
14649
  init_src2();
13944
14650
  init_src();
13945
- SYNTH_SYSTEM = [
13946
- "You turn an engineering chronology into the period summary a product manager shares.",
13947
- "Account for every distinct piece of work in the input: group related items, drop none.",
13948
- "Return only bullet lines, one per line, each starting with '- '. No headings, no preamble, no code fences.",
13949
- "Product language: describe user-facing behavior and benefit; no file paths, no function names, no migration numbers.",
14651
+ init_routing();
14652
+ init_allowance();
14653
+ REPORT_SYSTEM = [
14654
+ "You write the period report of a software project for the people who work on it: what changed, why it matters, what was decided, what is still open.",
14655
+ "Input: the captured records inside the period, numbered, oldest first, each with its date, time, type and title. Record types: feature (behavior that was built or changed), decision (a recorded direction, NOT a delivery), failure (an approach that did not work), event (a commit or another recorded event), note (a note or follow-up).",
14656
+ "Return ONLY a JSON object, no prose around it, no code fence, with this shape:",
14657
+ '{"lead": string, "sections": [{"heading": string, "body": string, "sources": [number]}], "decisions": [{"title": string, "body": string, "sources": [number]}], "open": [{"title": string, "body": string, "sources": [number]}], "timeline": [{"date": "YYYY-MM-DD", "title": string, "body": string, "sources": [number]}]}',
14658
+ "lead: at most three sentences, about sixty words, a reader understands in thirty seconds: the most important changes and what they mean for the people using the project.",
14659
+ "sections: two to six themes that emerge from THESE records (never a fixed list of topics); each body is one to three short paragraphs of plain product language; bullets only for genuinely parallel items; no headings inside a body; sources lists the record numbers the section rests on.",
14660
+ "Say only what the records show. Delivered behavior needs feature or event records; a decision record means something was decided, not that it shipped, so describe it as decided. Never invent numbers, dates, names or sources.",
14661
+ "decisions: only decisions that a decision record documents, cited by number; an empty array otherwise. open: only follow-ups the records name (failure or note records); when the records do not show whether it is resolved, say so; an empty array otherwise. timeline: only when at least three distinct milestones stand out on different days; each entry's date must be the date of a record it cites; an empty array otherwise.",
14662
+ "Fit the length to the data: a short period with few records gets a short report. A month is read as a month: condense to its themes instead of listing weeks. A year is read as an arc: what changed between its start and its end.",
14663
+ "No file paths, function names, migration numbers or shell commands. Write in English.",
13950
14664
  PROSE_STYLE
13951
14665
  ].join(" ");
13952
14666
  }
@@ -13972,11 +14686,11 @@ function slugify(title) {
13972
14686
  }
13973
14687
  function humans(names) {
13974
14688
  const count = /* @__PURE__ */ new Map();
13975
- for (const n of names) {
13976
- if (!n || n === "system") continue;
13977
- count.set(n, (count.get(n) ?? 0) + 1);
14689
+ for (const n2 of names) {
14690
+ if (!n2 || n2 === "system") continue;
14691
+ count.set(n2, (count.get(n2) ?? 0) + 1);
13978
14692
  }
13979
- return [...count.entries()].sort((a, b) => b[1] - a[1]).map(([n]) => n);
14693
+ return [...count.entries()].sort((a, b) => b[1] - a[1]).map(([n2]) => n2);
13980
14694
  }
13981
14695
  function moduleGroup(modulePath) {
13982
14696
  const segs = modulePath.split("/");
@@ -14027,7 +14741,8 @@ function buildDokuModel(input) {
14027
14741
  changes,
14028
14742
  decisions,
14029
14743
  locations,
14030
- evidenceUris
14744
+ evidenceUris,
14745
+ linkedDecisionIds
14031
14746
  }) => {
14032
14747
  let slug = slugify(feature.title);
14033
14748
  for (let i = 2; usedSlugs.has(slug) || RESERVED_SLUGS.has(slug); i++)
@@ -14066,13 +14781,16 @@ function buildDokuModel(input) {
14066
14781
  owner: explicitOwner ?? contributors[0] ?? null,
14067
14782
  contributors,
14068
14783
  narrative: null,
14069
- decisions: dedupeDecisions(decisions).map((d) => ({
14070
- title: d.title,
14071
- why: d.why,
14072
- by: d.created_by,
14073
- date: fmtDate(d.occurred_at),
14074
- objectId: d.id
14075
- })).sort((a, b) => a.date < b.date ? 1 : -1),
14784
+ decisions: dedupeDecisions(decisions).map(
14785
+ (d) => ({
14786
+ title: d.title,
14787
+ why: d.why,
14788
+ by: d.created_by,
14789
+ date: fmtDate(d.occurred_at),
14790
+ objectId: d.id,
14791
+ ...linkedDecisionIds ? { via: linkedDecisionIds.has(d.id) ? "link" : "location" } : {}
14792
+ })
14793
+ ).sort((a, b) => a.date < b.date ? 1 : -1),
14076
14794
  history,
14077
14795
  updated: history[0].date
14078
14796
  };
@@ -14147,8 +14865,19 @@ function renderEntryPage(model, base = "brainjar-doku/", narrative = null) {
14147
14865
  if (more.length) parts.push("## More", more.join("\n"));
14148
14866
  return parts.join("\n\n") + "\n";
14149
14867
  }
14150
- function renderArchitecturePage(model) {
14868
+ function renderArchitecturePage(model, synthesized = {}) {
14151
14869
  const parts = [`# How it works`, DOKU_NOTICE];
14870
+ if (synthesized.narrative) parts.push(synthesized.narrative);
14871
+ for (const f of synthesized.flows ?? []) {
14872
+ parts.push(
14873
+ [
14874
+ `## ${f.title}`,
14875
+ ...f.summary ? [f.summary] : [],
14876
+ ...f.explanation ? [f.explanation] : [],
14877
+ flowLanesMarkdown(f)
14878
+ ].join("\n\n")
14879
+ );
14880
+ }
14152
14881
  const idx = new Map(model.architecture.groups.map((g, i) => [g.name, i]));
14153
14882
  const lines = ["```mermaid", "flowchart LR"];
14154
14883
  for (const g of model.architecture.groups) {
@@ -14162,7 +14891,7 @@ function renderArchitecturePage(model) {
14162
14891
  );
14163
14892
  }
14164
14893
  lines.push("```");
14165
- parts.push(lines.join("\n"));
14894
+ parts.push("## Module groups", lines.join("\n"));
14166
14895
  parts.push(
14167
14896
  "## Modules by group",
14168
14897
  model.architecture.groups.map(
@@ -14179,6 +14908,234 @@ function firstSentence(s) {
14179
14908
  const cut2 = t.slice(0, 200);
14180
14909
  return `${cut2.slice(0, cut2.lastIndexOf(" "))}\u2026`;
14181
14910
  }
14911
+ function parsePresentation(raw, ctx) {
14912
+ const bad = (error) => ({ presentation: null, error, dropped: [] });
14913
+ const o = raw;
14914
+ if (!o || typeof o !== "object") return bad("not an object");
14915
+ if (!str(o.summary, LIMIT.summary))
14916
+ return bad("summary: missing or too long");
14917
+ if (!Array.isArray(o.blocks) || o.blocks.length === 0)
14918
+ return bad("blocks: need a non-empty list");
14919
+ if (o.blocks.length > MAX_BLOCKS)
14920
+ return bad(`blocks: more than ${MAX_BLOCKS}`);
14921
+ const blocks = [];
14922
+ const dropped = [];
14923
+ const flowsPlaced = /* @__PURE__ */ new Set();
14924
+ for (const [i, cand] of o.blocks.entries()) {
14925
+ const b = cand;
14926
+ const at = `block ${i + 1}`;
14927
+ if (!b || typeof b !== "object") return bad(`${at}: not an object`);
14928
+ const title = b.title;
14929
+ switch (b.kind) {
14930
+ case "text": {
14931
+ if (title !== void 0 && !str(title, LIMIT.title))
14932
+ return bad(`${at}: text title too long`);
14933
+ if (!str(b.markdown, LIMIT.markdown))
14934
+ return bad(`${at}: text markdown missing or too long`);
14935
+ blocks.push({
14936
+ kind: "text",
14937
+ ...title ? { title: title.trim() } : {},
14938
+ markdown: b.markdown.trim()
14939
+ });
14940
+ break;
14941
+ }
14942
+ case "steps": {
14943
+ if (!str(title, LIMIT.title)) return bad(`${at}: steps need a title`);
14944
+ const items = b.items;
14945
+ if (!Array.isArray(items) || items.length === 0 || items.length > LIMIT.stepItems)
14946
+ return bad(`${at}: steps need 1-${LIMIT.stepItems} items`);
14947
+ const out = [];
14948
+ for (const it of items) {
14949
+ const s = it;
14950
+ if (!str(s?.title, LIMIT.title) || !str(s?.body, LIMIT.stepBody, 0))
14951
+ return bad(`${at}: step needs title and body`);
14952
+ out.push({ title: s.title.trim(), body: s.body.trim() });
14953
+ }
14954
+ blocks.push({ kind: "steps", title: title.trim(), items: out });
14955
+ break;
14956
+ }
14957
+ case "table": {
14958
+ if (!str(title, LIMIT.title)) return bad(`${at}: table needs a title`);
14959
+ const columns = b.columns;
14960
+ if (!Array.isArray(columns) || columns.length < 2 || columns.length > LIMIT.columns || !columns.every((c) => str(c, 80)))
14961
+ return bad(`${at}: table needs 2-${LIMIT.columns} column names`);
14962
+ const rows = b.rows;
14963
+ if (!Array.isArray(rows) || rows.length === 0 || rows.length > LIMIT.rows)
14964
+ return bad(`${at}: table needs 1-${LIMIT.rows} rows`);
14965
+ const outRows = [];
14966
+ for (const r of rows) {
14967
+ if (!Array.isArray(r) || r.length !== columns.length || !r.every((c) => typeof c === "string" && c.length <= LIMIT.cell))
14968
+ return bad(
14969
+ `${at}: every row needs exactly ${columns.length} cells`
14970
+ );
14971
+ outRows.push(r.map((c) => c.trim()));
14972
+ }
14973
+ blocks.push({
14974
+ kind: "table",
14975
+ title: title.trim(),
14976
+ columns: columns.map((c) => c.trim()),
14977
+ rows: outRows
14978
+ });
14979
+ break;
14980
+ }
14981
+ case "endpoint": {
14982
+ const method = typeof b.method === "string" ? b.method.trim().toUpperCase() : "";
14983
+ if (!HTTP_METHODS.has(method)) return bad(`${at}: unknown HTTP method`);
14984
+ if (!str(b.path, LIMIT.endpointPath) || !/^\/\S*$/.test(b.path.trim()))
14985
+ return bad(`${at}: endpoint path must start with /`);
14986
+ if (!str(b.summary, LIMIT.endpointSummary))
14987
+ return bad(`${at}: endpoint needs a summary`);
14988
+ if (b.auth !== void 0 && !str(b.auth, LIMIT.auth))
14989
+ return bad(`${at}: endpoint auth too long`);
14990
+ const path20 = b.path.trim();
14991
+ if (!ctx.factSheet.includes(path20)) {
14992
+ dropped.push(`endpoint ${method} ${path20} (not in the fact sheet)`);
14993
+ break;
14994
+ }
14995
+ blocks.push({
14996
+ kind: "endpoint",
14997
+ method,
14998
+ path: path20,
14999
+ summary: b.summary.trim(),
15000
+ ...b.auth ? { auth: b.auth.trim() } : {}
15001
+ });
15002
+ break;
15003
+ }
15004
+ case "code": {
15005
+ if (!str(title, LIMIT.title)) return bad(`${at}: code needs a title`);
15006
+ if (typeof b.language !== "string" || !/^[a-z0-9+#.-]{1,20}$/i.test(b.language.trim()))
15007
+ return bad(`${at}: code needs a language tag`);
15008
+ if (!str(b.code, LIMIT.code))
15009
+ return bad(`${at}: code missing or too long`);
15010
+ blocks.push({
15011
+ kind: "code",
15012
+ title: title.trim(),
15013
+ language: b.language.trim().toLowerCase(),
15014
+ code: b.code.replace(/\s+$/, ""),
15015
+ ...b.illustrative === true ? { illustrative: true } : {}
15016
+ });
15017
+ break;
15018
+ }
15019
+ case "callout": {
15020
+ if (b.tone !== "note" && b.tone !== "warning")
15021
+ return bad(`${at}: callout tone must be note or warning`);
15022
+ if (!str(b.text, LIMIT.callout)) return bad(`${at}: callout text`);
15023
+ blocks.push({ kind: "callout", tone: b.tone, text: b.text.trim() });
15024
+ break;
15025
+ }
15026
+ case "flow": {
15027
+ if (!str(title, LIMIT.title)) return bad(`${at}: flow needs a title`);
15028
+ if (b.source !== "flowDiagram" && b.source !== "flow")
15029
+ return bad(`${at}: flow source must be flowDiagram or flow`);
15030
+ const exists2 = b.source === "flowDiagram" ? ctx.hasFlowDiagram : ctx.hasFlow;
15031
+ if (!exists2 || flowsPlaced.has(b.source)) {
15032
+ dropped.push(
15033
+ `flow ${b.source} (${exists2 ? "placed twice" : "no such diagram"})`
15034
+ );
15035
+ break;
15036
+ }
15037
+ flowsPlaced.add(b.source);
15038
+ blocks.push({ kind: "flow", title: title.trim(), source: b.source });
15039
+ break;
15040
+ }
15041
+ default:
15042
+ return bad(`${at}: unknown kind ${String(b.kind)}`);
15043
+ }
15044
+ }
15045
+ if (blocks.length === 0) return bad("blocks: nothing left after validation");
15046
+ return {
15047
+ presentation: { version: 1, summary: o.summary.trim(), blocks },
15048
+ error: null,
15049
+ dropped
15050
+ };
15051
+ }
15052
+ function flowLanesMarkdown(d) {
15053
+ return d.lanes.map((l) => {
15054
+ const nodes = d.nodes.filter((n2) => n2.lane === l.key).map((n2) => n2.title).join(", ");
15055
+ return `- **${l.title}**: ${nodes || "(no stations)"}`;
15056
+ }).join("\n");
15057
+ }
15058
+ function renderBlocksMarkdown(p, diagrams = {}) {
15059
+ const parts = [];
15060
+ for (const b of p.blocks) {
15061
+ switch (b.kind) {
15062
+ case "text":
15063
+ parts.push([h2(b.title), b.markdown].filter(Boolean).join("\n\n"));
15064
+ break;
15065
+ case "steps":
15066
+ parts.push(
15067
+ [
15068
+ h2(b.title),
15069
+ b.items.map(
15070
+ (s, i) => `${i + 1}. **${s.title}**${s.body ? ` ${s.body}` : ""}`
15071
+ ).join("\n")
15072
+ ].join("\n\n")
15073
+ );
15074
+ break;
15075
+ case "table":
15076
+ parts.push(
15077
+ [
15078
+ h2(b.title),
15079
+ [
15080
+ `| ${b.columns.map(mdCell).join(" | ")} |`,
15081
+ `|${b.columns.map(() => "---").join("|")}|`,
15082
+ ...b.rows.map((r) => `| ${r.map(mdCell).join(" | ")} |`)
15083
+ ].join("\n")
15084
+ ].join("\n\n")
15085
+ );
15086
+ break;
15087
+ case "endpoint":
15088
+ parts.push(
15089
+ [
15090
+ `### \`${b.method} ${b.path}\``,
15091
+ b.summary,
15092
+ ...b.auth ? [`Authentication: ${b.auth}`] : []
15093
+ ].join("\n\n")
15094
+ );
15095
+ break;
15096
+ case "code":
15097
+ parts.push(
15098
+ [
15099
+ `**${b.title}**${b.illustrative ? " (illustrative example, replace the placeholders)" : ""}`,
15100
+ `\`\`\`${b.language}
15101
+ ${b.code}
15102
+ \`\`\``
15103
+ ].join("\n\n")
15104
+ );
15105
+ break;
15106
+ case "callout":
15107
+ parts.push(
15108
+ `> **${b.tone === "warning" ? "Warning" : "Note"}:** ${b.text.replace(/\n/g, "\n> ")}`
15109
+ );
15110
+ break;
15111
+ case "flow": {
15112
+ const lines = [h2(b.title)];
15113
+ if (b.source === "flowDiagram" && diagrams.flowDiagram) {
15114
+ const d = diagrams.flowDiagram;
15115
+ lines.push(
15116
+ `_Runtime flow diagram: ${d.title}. Interactive on the web page; its lanes and stations:_`
15117
+ );
15118
+ lines.push(flowLanesMarkdown(d));
15119
+ } else if (b.source === "flow" && diagrams.flow) {
15120
+ lines.push(
15121
+ diagrams.flow.steps.map((s, i) => `${i + 1}. ${s}`).join("\n")
15122
+ );
15123
+ }
15124
+ parts.push(lines.join("\n\n"));
15125
+ break;
15126
+ }
15127
+ }
15128
+ }
15129
+ return parts.join("\n\n");
15130
+ }
15131
+ function pageBody(f) {
15132
+ if (f.presentation)
15133
+ return [
15134
+ f.presentation.summary,
15135
+ renderBlocksMarkdown(f.presentation, f)
15136
+ ].join("\n\n");
15137
+ return f.body ?? null;
15138
+ }
14182
15139
  function renderFeaturePage(f) {
14183
15140
  const meta = [
14184
15141
  "| | |",
@@ -14189,8 +15146,9 @@ function renderFeaturePage(f) {
14189
15146
  ...f.locations.length ? [`| Code | ${f.locations.map((l) => `\`${l}\``).join(", ")} |`] : []
14190
15147
  ];
14191
15148
  const parts = [`# ${f.docTitle ?? f.title}`, meta.join("\n")];
14192
- if (f.body) {
14193
- parts.push(f.body);
15149
+ const body = pageBody(f);
15150
+ if (body) {
15151
+ parts.push(body);
14194
15152
  } else {
14195
15153
  if (f.narrative) parts.push(f.narrative);
14196
15154
  parts.push("## What it does", f.what);
@@ -14236,7 +15194,7 @@ function renderCategoryPage(model) {
14236
15194
  function renderChangelogPage(model) {
14237
15195
  const parts = [`# Changelog`, DOKU_NOTICE];
14238
15196
  for (const day of model.changelog) {
14239
- const counts = Object.entries(day.counts).map(([t, n]) => `${n} ${t}${n === 1 ? "" : "s"}`).join(" \xB7 ");
15197
+ const counts = Object.entries(day.counts).map(([t, n2]) => `${n2} ${t}${n2 === 1 ? "" : "s"}`).join(" \xB7 ");
14240
15198
  const who = day.contributors.length ? ` \xB7 by ${day.contributors.join(", ")}` : "";
14241
15199
  parts.push(
14242
15200
  `## ${day.date}`,
@@ -14333,7 +15291,7 @@ function renderDokuPages(model) {
14333
15291
  }
14334
15292
  return pages;
14335
15293
  }
14336
- var normTitle, DOC_SECTIONS, fmtDate, MAX_GROUPS, MAX_HIGHLIGHTS, HIGHLIGHT_TYPES, RESERVED_SLUGS, DOKU_NOTICE, mermaidId, mermaidLabel, mdCell, asRecord;
15294
+ var normTitle, DOC_SECTIONS, fmtDate, MAX_GROUPS, MAX_HIGHLIGHTS, HIGHLIGHT_TYPES, RESERVED_SLUGS, DOKU_NOTICE, mermaidId, mermaidLabel, mdCell, HTTP_METHODS, MAX_BLOCKS, LIMIT, str, h2, asRecord;
14337
15295
  var init_doku_model = __esm({
14338
15296
  "packages/ingest/src/doku-model.ts"() {
14339
15297
  "use strict";
@@ -14360,12 +15318,39 @@ var init_doku_model = __esm({
14360
15318
  mermaidId = (name, i) => `g${i}`;
14361
15319
  mermaidLabel = (s) => s.replace(/"/g, "'");
14362
15320
  mdCell = (s) => (s ?? "").replace(/\|/g, "\\|").replace(/\s+/g, " ").trim();
15321
+ HTTP_METHODS = /* @__PURE__ */ new Set([
15322
+ "GET",
15323
+ "POST",
15324
+ "PUT",
15325
+ "PATCH",
15326
+ "DELETE",
15327
+ "HEAD",
15328
+ "OPTIONS"
15329
+ ]);
15330
+ MAX_BLOCKS = 40;
15331
+ LIMIT = {
15332
+ title: 120,
15333
+ summary: 300,
15334
+ markdown: 2e4,
15335
+ stepItems: 20,
15336
+ stepBody: 1e3,
15337
+ columns: 6,
15338
+ rows: 40,
15339
+ cell: 400,
15340
+ endpointPath: 200,
15341
+ endpointSummary: 400,
15342
+ auth: 200,
15343
+ code: 6e3,
15344
+ callout: 600
15345
+ };
15346
+ str = (v, max, min = 1) => typeof v === "string" && v.trim().length >= min && v.length <= max;
15347
+ h2 = (t) => t ? `## ${t.trim()}` : null;
14363
15348
  asRecord = (v) => v;
14364
15349
  }
14365
15350
  });
14366
15351
 
14367
15352
  // packages/ingest/src/doku.ts
14368
- import { createHash as createHash2 } from "node:crypto";
15353
+ import { createHash as createHash3 } from "node:crypto";
14369
15354
  import { mkdir as mkdir10, readFile as readFile10, rm as rm6, stat, writeFile as writeFile10 } from "node:fs/promises";
14370
15355
  import path15 from "node:path";
14371
15356
  function bodyLocations(o) {
@@ -14397,10 +15382,10 @@ async function loadDokuData(brainId, root) {
14397
15382
  relations: ["changes", "affects", "mentions"]
14398
15383
  });
14399
15384
  const changes = neighbors.filter(
14400
- (n) => n.id !== feature.id && n.type === "event"
15385
+ (n2) => n2.id !== feature.id && n2.type === "event"
14401
15386
  );
14402
15387
  const linkedDecisions = neighbors.filter(
14403
- (n) => n.id !== feature.id && n.type === "decision"
15388
+ (n2) => n2.id !== feature.id && n2.type === "decision"
14404
15389
  );
14405
15390
  const linkedModules = neighbors.filter(isModule).map(moduleName);
14406
15391
  const locations = [
@@ -14419,7 +15404,14 @@ async function loadDokuData(brainId, root) {
14419
15404
  feature.id,
14420
15405
  ...changes.map((c) => c.id)
14421
15406
  ]);
14422
- return { feature, changes, decisions, locations, evidenceUris };
15407
+ return {
15408
+ feature,
15409
+ changes,
15410
+ decisions,
15411
+ locations,
15412
+ evidenceUris,
15413
+ linkedDecisionIds: new Set(linkedDecisions.map((d) => d.id))
15414
+ };
14423
15415
  })
14424
15416
  );
14425
15417
  const days = (await Promise.all(
@@ -14457,20 +15449,50 @@ async function loadDokuData(brainId, root) {
14457
15449
  scripts
14458
15450
  };
14459
15451
  }
14460
- function parseDocPage(raw, categoryKeys) {
15452
+ function aiState(m) {
15453
+ return {
15454
+ factHash: typeof m?.factHash === "string" ? m.factHash : "",
15455
+ aiIncomplete: m?.aiIncomplete === true,
15456
+ aiAttempts: typeof m?.aiAttempts === "number" ? m.aiAttempts : 0
15457
+ };
15458
+ }
15459
+ function aiComplete(kind, m) {
15460
+ if (!m) return false;
15461
+ const text = (v) => typeof v === "string" && v.trim().length > 0;
15462
+ switch (kind) {
15463
+ case "entry":
15464
+ case "architecture":
15465
+ return text(m.narrative);
15466
+ case "feature":
15467
+ return (Boolean(m.presentation) || text(m.body)) && text(m.category);
15468
+ case "category":
15469
+ return text(m.body);
15470
+ default:
15471
+ return true;
15472
+ }
15473
+ }
15474
+ function parseDocPage(raw, categoryKeys, ctx) {
15475
+ const bad = (error2) => ({ page: null, error: error2, dropped: [] });
14461
15476
  const match = raw.match(/\{[\s\S]*\}/);
14462
- if (!match) return null;
15477
+ if (!match) return bad("no JSON");
15478
+ let obj;
14463
15479
  try {
14464
- const obj = JSON.parse(match[0]);
14465
- const title = typeof obj.title === "string" ? obj.title.trim() : "";
14466
- const category = typeof obj.category === "string" ? obj.category.trim() : "";
14467
- const body = typeof obj.body === "string" ? obj.body.trim() : "";
14468
- if (!title || title.length > 120 || !body) return null;
14469
- if (!categoryKeys.includes(category)) return null;
14470
- return { title, category, body };
15480
+ obj = JSON.parse(match[0]);
14471
15481
  } catch {
14472
- return null;
14473
- }
15482
+ return bad("invalid JSON (truncated reply?)");
15483
+ }
15484
+ const title = typeof obj.title === "string" ? obj.title.trim() : "";
15485
+ const category = typeof obj.category === "string" ? obj.category.trim() : "";
15486
+ if (!title || title.length > 120) return bad("title: missing or too long");
15487
+ if (!categoryKeys.includes(category)) return bad("category: unknown key");
15488
+ const icon = typeof obj.icon === "string" && isEmoji(obj.icon.trim()) ? obj.icon.trim() : null;
15489
+ const { presentation, error, dropped } = parsePresentation(obj, ctx);
15490
+ if (!presentation) return bad(error ?? "presentation invalid");
15491
+ return {
15492
+ page: { title, category, presentation, icon },
15493
+ error: null,
15494
+ dropped
15495
+ };
14474
15496
  }
14475
15497
  function parseTaxonomy(raw) {
14476
15498
  const match = raw.match(/\{[\s\S]*\}/);
@@ -14485,25 +15507,53 @@ function parseTaxonomy(raw) {
14485
15507
  const name = typeof r.name === "string" ? r.name.trim() : "";
14486
15508
  const description = typeof r.description === "string" ? r.description.trim() : "";
14487
15509
  if (!/^[a-z0-9-]{2,40}$/.test(key) || !name || !description) return null;
15510
+ if (name.length > 80 || description.length > 240) return null;
14488
15511
  out.push({ key, name, description });
14489
15512
  }
14490
- if (out.length < 3 || out.length > 8) return null;
15513
+ if (out.length < 1 || out.length > 40) return null;
14491
15514
  if (new Set(out.map((c) => c.key)).size !== out.length) return null;
14492
15515
  return out;
14493
15516
  } catch {
14494
15517
  return null;
14495
15518
  }
14496
15519
  }
15520
+ function parseRefile(raw, slugs, keys) {
15521
+ let pages = null;
15522
+ const match = raw.match(/\{[\s\S]*\}/);
15523
+ if (match) {
15524
+ try {
15525
+ const obj = JSON.parse(match[0]);
15526
+ if (typeof obj.pages === "object" && obj.pages !== null && !Array.isArray(obj.pages))
15527
+ pages = obj.pages;
15528
+ } catch {
15529
+ }
15530
+ }
15531
+ if (!pages) {
15532
+ pages = {};
15533
+ for (const m of raw.matchAll(/"([^"\n]+)"\s*:\s*"([^"\n]*)"/g))
15534
+ pages[m[1]] = m[2];
15535
+ }
15536
+ const known = new Set(keys);
15537
+ const out = {};
15538
+ for (const slug of slugs) {
15539
+ const key = pages[slug];
15540
+ if (typeof key === "string" && known.has(key.trim()))
15541
+ out[slug] = key.trim();
15542
+ }
15543
+ return Object.keys(out).length ? out : null;
15544
+ }
14497
15545
  function parseCategoryBody(raw, memberSlugs) {
14498
15546
  let body = raw.trim();
14499
- if (!body || body.length > 9e3) return null;
15547
+ if (!body || body.length > 2e4) return null;
14500
15548
  const known = new Set(memberSlugs);
14501
15549
  body = body.replace(
14502
15550
  /\[([^\]]+)\]\(page:([^)\s]+)\)/g,
14503
15551
  (whole, text, slug) => known.has(slug) ? whole : text
14504
15552
  );
14505
- const links = [...body.matchAll(/\(page:([^)\s]+)\)/g)].map((m) => m[1]);
14506
- if (links.length === 0 || links.some((s) => !known.has(s))) return null;
15553
+ body = body.replace(
15554
+ /\(page:([^)\s]+)\)/g,
15555
+ (whole, slug) => known.has(slug) ? whole : ""
15556
+ );
14507
15557
  return body;
14508
15558
  }
14509
15559
  function archNotesInput(model) {
@@ -14597,13 +15647,19 @@ function parseArchFlows(raw, knownModules) {
14597
15647
  const errors = [];
14598
15648
  for (const cand of obj.flows.slice(0, 4)) {
14599
15649
  const f = cand;
15650
+ if (f && typeof f === "object") {
15651
+ const x = f.explanation;
15652
+ if (typeof x === "string" && x.trim() && x.length <= 4e3)
15653
+ f.explanation = x.trim();
15654
+ else delete f.explanation;
15655
+ }
14600
15656
  if (Array.isArray(f?.nodes)) {
14601
- for (const n of f.nodes) {
14602
- if (Array.isArray(n?.evidence)) {
14603
- n.evidence = n.evidence.filter(
15657
+ for (const n2 of f.nodes) {
15658
+ if (Array.isArray(n2?.evidence)) {
15659
+ n2.evidence = n2.evidence.filter(
14604
15660
  (p) => typeof p === "string" && knownModules.has(p)
14605
15661
  );
14606
- if (n.evidence.length === 0) delete n.evidence;
15662
+ if (n2.evidence.length === 0) delete n2.evidence;
14607
15663
  }
14608
15664
  }
14609
15665
  }
@@ -14627,12 +15683,12 @@ function parseFeatureFlow(raw, slug, knownPaths) {
14627
15683
  const f = obj.flow;
14628
15684
  f.slug = `${slug.slice(0, 59)}-flow`;
14629
15685
  if (Array.isArray(f?.nodes)) {
14630
- for (const n of f.nodes) {
14631
- if (Array.isArray(n?.evidence)) {
14632
- n.evidence = n.evidence.filter(
15686
+ for (const n2 of f.nodes) {
15687
+ if (Array.isArray(n2?.evidence)) {
15688
+ n2.evidence = n2.evidence.filter(
14633
15689
  (p) => typeof p === "string" && knownPaths.has(p)
14634
15690
  );
14635
- if (n.evidence.length === 0) delete n.evidence;
15691
+ if (n2.evidence.length === 0) delete n2.evidence;
14636
15692
  }
14637
15693
  }
14638
15694
  }
@@ -14657,7 +15713,7 @@ function parseFlow(raw, configKeys) {
14657
15713
  const edges = obj.edges;
14658
15714
  if (!Array.isArray(edges) || edges.length === 0 || !edges.every(
14659
15715
  (e) => Array.isArray(e) && e.length === 2 && e.every(
14660
- (n) => Number.isInteger(n) && n >= 0 && n < steps.length
15716
+ (n2) => Number.isInteger(n2) && n2 >= 0 && n2 < steps.length
14661
15717
  ) && e[0] !== e[1]
14662
15718
  ))
14663
15719
  return bad("edges: need valid [from,to] index pairs");
@@ -14705,9 +15761,16 @@ async function generateDokuInner(brainId, opts = {}) {
14705
15761
  const repo = brainRepo(brainId);
14706
15762
  const meta = new Map((await repo.docPageMeta()).map((m) => [m.slug, m]));
14707
15763
  const storedBySlug = /* @__PURE__ */ new Map();
15764
+ const storedRows = /* @__PURE__ */ new Map();
14708
15765
  try {
14709
- for (const p of await repo.latestDocPages())
15766
+ for (const p of await repo.latestDocPages()) {
14710
15767
  storedBySlug.set(p.slug, p.model);
15768
+ storedRows.set(p.slug, {
15769
+ title: p.title,
15770
+ model: p.model,
15771
+ markdown: p.markdown
15772
+ });
15773
+ }
14711
15774
  } catch {
14712
15775
  }
14713
15776
  const takenTitle = /* @__PURE__ */ new Map();
@@ -14726,10 +15789,11 @@ async function generateDokuInner(brainId, opts = {}) {
14726
15789
  const stored = storedBySlug.get("index")?.categories;
14727
15790
  if (Array.isArray(stored) && stored.length) categories = stored;
14728
15791
  }
15792
+ const storedTaxonomy = JSON.stringify(categories);
14729
15793
  const hasNewFeature = pages.some(
14730
15794
  (p) => p.kind === "feature" && !meta.get(p.slug)
14731
15795
  );
14732
- if (opts.llm && (categories.length === 0 || hasNewFeature)) {
15796
+ if (opts.llm && (categories.length === 0 || hasNewFeature || opts.retaxonomy)) {
14733
15797
  try {
14734
15798
  const next = parseTaxonomy(
14735
15799
  await opts.llm.complete({
@@ -14743,7 +15807,9 @@ async function generateDokuInner(brainId, opts = {}) {
14743
15807
  (f) => `- ${f.title}: ${firstSentence(f.what)}`
14744
15808
  )
14745
15809
  ].join("\n"),
14746
- maxTokens: 900
15810
+ // Room for a large product's taxonomy; an invalid or clipped reply keeps the
15811
+ // stored one (parseTaxonomy returns null).
15812
+ maxTokens: 3e3
14747
15813
  })
14748
15814
  );
14749
15815
  if (next) categories = next;
@@ -14751,42 +15817,82 @@ async function generateDokuInner(brainId, opts = {}) {
14751
15817
  }
14752
15818
  }
14753
15819
  const categoryKeys = categories.map((c) => c.key);
15820
+ const taxonomyChanged = JSON.stringify(categories) !== storedTaxonomy;
15821
+ const endpointFacts = [];
15822
+ for (const [slug, m] of storedBySlug) {
15823
+ const p = m.presentation;
15824
+ if (!Array.isArray(p?.blocks)) continue;
15825
+ for (const b of p.blocks)
15826
+ if (b.kind === "endpoint")
15827
+ endpointFacts.push({
15828
+ slug,
15829
+ title: typeof m.docTitle === "string" ? m.docTitle : slug,
15830
+ method: b.method,
15831
+ path: b.path,
15832
+ summary: b.summary,
15833
+ ...b.auth ? { auth: b.auth } : {}
15834
+ });
15835
+ }
14754
15836
  let written = 0;
14755
15837
  let polished = false;
14756
15838
  const seen = /* @__PURE__ */ new Set();
14757
15839
  const featureInfo = [];
15840
+ const save = async (input) => {
15841
+ await repo.saveDocPage(input);
15842
+ storedRows.set(input.slug, {
15843
+ title: input.title,
15844
+ model: input.model,
15845
+ markdown: input.markdown
15846
+ });
15847
+ storedBySlug.set(input.slug, input.model);
15848
+ written++;
15849
+ };
14758
15850
  for (const page of pages) {
14759
15851
  seen.add(page.slug);
14760
15852
  const rawStructure = page.kind === "architecture" ? (() => {
14761
15853
  const raw = page.model.raw;
14762
15854
  return raw ? sha1(JSON.stringify([raw.modules, raw.edges])) : "";
14763
15855
  })() : "";
15856
+ const factHash = sha1(page.markdown);
14764
15857
  const sourceHash = sha1(
14765
- page.markdown + (page.kind === "entry" ? "v3" : page.kind === "feature" ? (
14766
- // f6: flow slug fix + retry after the silent 0-of-44 run.
14767
- "f6"
15858
+ page.markdown + (page.kind === "entry" ? "v4" : page.kind === "feature" ? (
15859
+ // f7: the block presentation model (docs redesign) regenerates every
15860
+ // feature page once; run with `only` first for a representative sample.
15861
+ "f7"
14768
15862
  ) : page.kind === "architecture" ? (
14769
- // a5: more flows, tighter labels — existing brains regenerate the page once.
14770
- "a5" + rawStructure
15863
+ // a6: flow explanations + full markdown (docs hubs) regenerate the page once.
15864
+ "a6" + rawStructure
14771
15865
  ) : "")
14772
15866
  );
14773
15867
  const prev = meta.get(page.slug);
14774
- if (prev && !prev.deleted && prev.source_hash === sourceHash && !storedCollisions.has(page.slug)) {
15868
+ const stored = storedBySlug.get(page.slug);
15869
+ const state = aiState(stored);
15870
+ const required = Boolean(opts.llm && page.polishable);
15871
+ const outsideSample = Boolean(opts.only && !opts.only.includes(page.slug));
15872
+ const unchanged = Boolean(
15873
+ prev && !prev.deleted && prev.source_hash === sourceHash
15874
+ );
15875
+ const retry = required && unchanged && (state.aiIncomplete || !aiComplete(page.kind, stored)) && (state.aiAttempts < MAX_AI_ATTEMPTS || Boolean(opts.forceRetry));
15876
+ const entryTaxonomy = page.kind === "entry" && taxonomyChanged;
15877
+ if (prev && !prev.deleted && (outsideSample || unchanged && !storedCollisions.has(page.slug) && !retry && !entryTaxonomy)) {
14775
15878
  if (page.kind === "feature") {
14776
- const stored = storedBySlug.get(page.slug);
15879
+ const s = stored;
14777
15880
  const f = page.model;
14778
15881
  featureInfo.push({
14779
15882
  slug: page.slug,
14780
- title: typeof stored?.docTitle === "string" ? stored.docTitle : page.title,
15883
+ title: typeof s?.docTitle === "string" ? s.docTitle : page.title,
14781
15884
  what: f.what,
14782
15885
  updated: f.updated,
14783
- category: typeof stored?.category === "string" ? stored.category : null
15886
+ category: typeof s?.category === "string" ? s.category : null
14784
15887
  });
14785
15888
  }
14786
15889
  continue;
14787
15890
  }
15891
+ const sameFacts = stored !== void 0 && state.factHash === factHash;
14788
15892
  let markdown = page.markdown;
14789
15893
  let model = page.model;
15894
+ let icon = null;
15895
+ let ok2 = !required;
14790
15896
  if (opts.llm && page.polishable && page.kind === "architecture") {
14791
15897
  try {
14792
15898
  const { notes } = parseArchNotes(
@@ -14800,6 +15906,7 @@ async function generateDokuInner(brainId, opts = {}) {
14800
15906
  if (notes) {
14801
15907
  model = { ...model, narrative: notes.narrative, roles: notes.roles };
14802
15908
  polished = true;
15909
+ ok2 = true;
14803
15910
  }
14804
15911
  } catch {
14805
15912
  }
@@ -14818,61 +15925,47 @@ async function generateDokuInner(brainId, opts = {}) {
14818
15925
  }
14819
15926
  } catch {
14820
15927
  }
15928
+ if (!ok2 && sameFacts) {
15929
+ const s = stored;
15930
+ const kept = model;
15931
+ model = {
15932
+ ...model,
15933
+ ...typeof s.narrative === "string" ? { narrative: s.narrative, roles: s.roles } : {},
15934
+ ...!kept.flows && Array.isArray(s.flows) ? { flows: s.flows } : {}
15935
+ };
15936
+ }
15937
+ const arch = model;
15938
+ if (arch.narrative || arch.flows?.length)
15939
+ markdown = renderArchitecturePage(doku, {
15940
+ narrative: arch.narrative ?? null,
15941
+ flows: arch.flows ?? []
15942
+ });
14821
15943
  } else if (page.kind === "entry") {
14822
15944
  model = { ...page.model, categories };
14823
15945
  if (opts.llm && page.polishable) {
14824
15946
  try {
14825
- const narrative = (await opts.llm.complete({
15947
+ const narrative2 = (await opts.llm.complete({
14826
15948
  system: ENTRY_NARRATIVE_SYSTEM,
14827
15949
  prompt: page.markdown,
14828
- maxTokens: 1200
15950
+ maxTokens: 2500
14829
15951
  })).trim();
14830
- if (narrative) {
14831
- model = { ...model, narrative };
14832
- markdown = renderEntryPage(doku, "brainjar-doku/", narrative);
15952
+ if (narrative2) {
15953
+ model = { ...model, narrative: narrative2 };
14833
15954
  polished = true;
15955
+ ok2 = true;
14834
15956
  }
14835
15957
  } catch {
14836
15958
  }
14837
- }
14838
- } else if (opts.llm && page.polishable) {
14839
- try {
14840
- const synth = parseDocPage(
14841
- await opts.llm.complete({
14842
- system: PAGE_SYSTEM,
14843
- prompt: [
14844
- "Categories (pick exactly one key):",
14845
- ...categories.length ? categories.map(
14846
- (c) => `- ${c.key}: ${c.name} (${c.description})`
14847
- ) : ["- general: General (everything)"],
14848
- "",
14849
- "Fact sheet:",
14850
- page.markdown
14851
- ].join("\n"),
14852
- maxTokens: 2500
14853
- }),
14854
- categoryKeys.length ? categoryKeys : ["general"]
14855
- );
14856
- if (synth) {
14857
- const title = pickDocTitle(
14858
- synth.title,
14859
- page.title,
14860
- page.slug,
14861
- takenTitle
14862
- );
14863
- const f = {
14864
- ...page.model,
14865
- body: synth.body,
14866
- docTitle: title,
14867
- category: synth.category
14868
- };
14869
- model = f;
14870
- markdown = renderFeaturePage(f);
14871
- page.title = title;
14872
- polished = true;
15959
+ if (!ok2 && sameFacts) {
15960
+ const s = stored;
15961
+ if (typeof s.narrative === "string" && s.narrative.trim())
15962
+ model = { ...model, narrative: s.narrative };
14873
15963
  }
14874
- } catch {
14875
15964
  }
15965
+ const narrative = model.narrative;
15966
+ if (narrative)
15967
+ markdown = renderEntryPage(doku, "brainjar-doku/", narrative);
15968
+ } else if (opts.llm && page.polishable) {
14876
15969
  try {
14877
15970
  const { flow } = parseFlow(
14878
15971
  await opts.llm.complete({
@@ -14912,7 +16005,123 @@ async function generateDokuInner(brainId, opts = {}) {
14912
16005
  console.warn(`doku: feature flow failed (${page.slug}):`, e);
14913
16006
  }
14914
16007
  }
16008
+ try {
16009
+ const diagrams = model;
16010
+ const shared = endpointFacts.filter(
16011
+ (e) => e.slug !== page.slug && page.markdown.includes(e.path)
16012
+ );
16013
+ const sharedLines = shared.map(
16014
+ (e) => `- ${e.method} ${e.path}: ${e.summary}${e.auth ? ` Authentication: ${e.auth}` : ""} (page: ${e.slug}, "${e.title}")`
16015
+ );
16016
+ const ctx = {
16017
+ factSheet: [page.markdown, ...sharedLines].join("\n"),
16018
+ hasFlowDiagram: Boolean(diagrams.flowDiagram),
16019
+ hasFlow: Boolean(diagrams.flow)
16020
+ };
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");
16040
+ const keys = categoryKeys.length ? categoryKeys : ["general"];
16041
+ let synth = parseDocPage(
16042
+ await opts.llm.complete({
16043
+ system: PAGE_SYSTEM,
16044
+ prompt,
16045
+ maxTokens: 7e3
16046
+ }),
16047
+ keys,
16048
+ ctx
16049
+ );
16050
+ if (!synth.page) {
16051
+ console.warn(
16052
+ `doku: page rejected (${page.slug}): ${synth.error}; retrying once`
16053
+ );
16054
+ synth = parseDocPage(
16055
+ await opts.llm.complete({
16056
+ system: PAGE_SYSTEM,
16057
+ prompt,
16058
+ maxTokens: 1e4
16059
+ }),
16060
+ keys,
16061
+ ctx
16062
+ );
16063
+ }
16064
+ if (synth.dropped.length)
16065
+ console.warn(
16066
+ `doku: blocks dropped (${page.slug}): ${synth.dropped.join("; ")}`
16067
+ );
16068
+ if (synth.page) {
16069
+ const title = pickDocTitle(
16070
+ synth.page.title,
16071
+ page.title,
16072
+ page.slug,
16073
+ takenTitle
16074
+ );
16075
+ const f = {
16076
+ ...model,
16077
+ presentation: synth.page.presentation,
16078
+ docTitle: title,
16079
+ category: synth.page.category
16080
+ };
16081
+ f.body = pageBody(f);
16082
+ model = f;
16083
+ markdown = renderFeaturePage(f);
16084
+ page.title = title;
16085
+ icon = synth.page.icon;
16086
+ polished = true;
16087
+ ok2 = true;
16088
+ } else {
16089
+ console.warn(
16090
+ `doku: page rejected twice (${page.slug}): ${synth.error}`
16091
+ );
16092
+ }
16093
+ } catch (e) {
16094
+ console.warn(`doku: page synthesis failed (${page.slug}):`, e);
16095
+ }
16096
+ 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
+ };
16108
+ model = f;
16109
+ markdown = renderFeaturePage(f);
16110
+ if (f.docTitle) page.title = f.docTitle;
16111
+ }
16112
+ if (!ok2) {
16113
+ const s = stored;
16114
+ const m = model;
16115
+ if (typeof m.category !== "string" && typeof s?.category === "string" && categoryKeys.includes(s.category))
16116
+ model = { ...model, category: s.category };
16117
+ }
14915
16118
  }
16119
+ model = {
16120
+ ...model,
16121
+ factHash,
16122
+ aiIncomplete: required ? !ok2 : !aiComplete(page.kind, model),
16123
+ aiAttempts: required && !ok2 ? (sameFacts ? state.aiAttempts : 0) + 1 : 0
16124
+ };
14916
16125
  if (page.kind === "feature") {
14917
16126
  const m = model;
14918
16127
  const f = page.model;
@@ -14924,15 +16133,68 @@ async function generateDokuInner(brainId, opts = {}) {
14924
16133
  category: typeof m.category === "string" ? m.category : null
14925
16134
  });
14926
16135
  }
14927
- await repo.saveDocPage({
16136
+ await save({
14928
16137
  slug: page.slug,
14929
16138
  kind: page.kind,
14930
16139
  title: page.title,
14931
16140
  model,
14932
16141
  markdown,
14933
- sourceHash
16142
+ sourceHash,
16143
+ icon
14934
16144
  });
14935
- written++;
16145
+ }
16146
+ if (opts.llm && categoryKeys.length) {
16147
+ const candidates = featureInfo.filter(
16148
+ (f) => taxonomyChanged || opts.retaxonomy || !f.category || !categoryKeys.includes(f.category)
16149
+ );
16150
+ if (candidates.length) {
16151
+ try {
16152
+ const filed = parseRefile(
16153
+ await opts.llm.complete({
16154
+ system: REFILE_SYSTEM,
16155
+ prompt: [
16156
+ "Categories:",
16157
+ ...categories.map(
16158
+ (c) => `- ${c.key}: ${c.name} (${c.description})`
16159
+ ),
16160
+ "",
16161
+ "Pages:",
16162
+ ...candidates.map(
16163
+ (f) => `- ${f.slug}: ${f.title}: ${firstSentence(f.what)}`
16164
+ )
16165
+ ].join("\n"),
16166
+ maxTokens: Math.min(16e3, 300 + candidates.length * 60)
16167
+ }),
16168
+ candidates.map((f) => f.slug),
16169
+ categoryKeys
16170
+ );
16171
+ if (filed) {
16172
+ const unfiled = candidates.filter((f) => !filed[f.slug]).length;
16173
+ if (unfiled)
16174
+ console.warn(
16175
+ `doku: re-filing left ${unfiled} of ${candidates.length} pages as they were`
16176
+ );
16177
+ for (const f of candidates) {
16178
+ const key = filed[f.slug];
16179
+ if (!key || key === f.category) continue;
16180
+ const row = storedRows.get(f.slug);
16181
+ const prev = meta.get(f.slug);
16182
+ if (!row) continue;
16183
+ f.category = key;
16184
+ await save({
16185
+ slug: f.slug,
16186
+ kind: "feature",
16187
+ title: row.title,
16188
+ model: { ...row.model, category: key },
16189
+ markdown: row.markdown,
16190
+ sourceHash: prev?.source_hash ?? sha1(row.markdown)
16191
+ });
16192
+ }
16193
+ } else console.warn("doku: re-filing rejected (invalid reply)");
16194
+ } catch (e) {
16195
+ console.warn("doku: re-filing failed:", e);
16196
+ }
16197
+ }
14936
16198
  }
14937
16199
  for (const c of categories) {
14938
16200
  const members = featureInfo.filter((f) => f.category === c.key).sort((a, b) => a.title.localeCompare(b.title)).map(({ slug: slug2, title, what, updated }) => ({
@@ -14952,11 +16214,18 @@ async function generateDokuInner(brainId, opts = {}) {
14952
16214
  body: null,
14953
16215
  members
14954
16216
  };
14955
- const sourceHash = sha1(renderCategoryPage(catModel) + "c1");
16217
+ const factHash = sha1(renderCategoryPage(catModel));
16218
+ const sourceHash = sha1(renderCategoryPage(catModel) + "c2");
14956
16219
  const prev = meta.get(slug);
14957
- const storedBody = storedBySlug.get(slug)?.body;
14958
- if (prev && !prev.deleted && prev.source_hash === sourceHash && !(opts.llm && typeof storedBody !== "string"))
14959
- continue;
16220
+ const stored = storedBySlug.get(slug);
16221
+ const state = aiState(stored);
16222
+ const unchanged = Boolean(
16223
+ prev && !prev.deleted && prev.source_hash === sourceHash
16224
+ );
16225
+ const retry = Boolean(opts.llm) && unchanged && !aiComplete("category", stored) && (state.aiAttempts < MAX_AI_ATTEMPTS || Boolean(opts.forceRetry));
16226
+ if (unchanged && !retry) continue;
16227
+ const sameFacts = stored !== void 0 && state.factHash === factHash;
16228
+ let ok2 = !opts.llm;
14960
16229
  if (opts.llm) {
14961
16230
  try {
14962
16231
  const body = parseCategoryBody(
@@ -14971,28 +16240,37 @@ async function generateDokuInner(brainId, opts = {}) {
14971
16240
  (m) => `- ${m.slug}: ${m.title}: ${firstSentence(m.what)}`
14972
16241
  )
14973
16242
  ].join("\n"),
14974
- // Weaving 15+ member links into prose needs room; a clipped body loses its
14975
- // trailing links and voids the story.
14976
- maxTokens: 2e3
16243
+ // Structured markdown for a large area needs room; a clipped body would
16244
+ // lose its last sections.
16245
+ maxTokens: 4e3
14977
16246
  }),
14978
16247
  members.map((m) => m.slug)
14979
16248
  );
14980
16249
  if (body) {
14981
16250
  catModel.body = body;
14982
16251
  polished = true;
16252
+ ok2 = true;
14983
16253
  }
14984
16254
  } catch {
14985
16255
  }
16256
+ if (!ok2 && sameFacts) {
16257
+ const s = stored;
16258
+ if (typeof s.body === "string" && s.body.trim()) catModel.body = s.body;
16259
+ }
14986
16260
  }
14987
- await repo.saveDocPage({
16261
+ await save({
14988
16262
  slug,
14989
16263
  kind: "category",
14990
16264
  title: c.name,
14991
- model: catModel,
16265
+ model: {
16266
+ ...catModel,
16267
+ factHash,
16268
+ aiIncomplete: opts.llm ? !ok2 : !catModel.body,
16269
+ aiAttempts: opts.llm && !ok2 ? (sameFacts ? state.aiAttempts : 0) + 1 : 0
16270
+ },
14992
16271
  markdown: renderCategoryPage(catModel),
14993
16272
  sourceHash
14994
16273
  });
14995
- written++;
14996
16274
  }
14997
16275
  let deleted = 0;
14998
16276
  for (const [slug, m] of meta) {
@@ -15061,7 +16339,7 @@ async function exportDoku(brainId, root) {
15061
16339
  const sync = await writeDokuTree(root, files);
15062
16340
  return { pages: files.length, ...sync };
15063
16341
  }
15064
- var DOKU_FILENAME, DOKU_DIR, MANIFEST, CHANGELOG_DAYS, sha1, exists, isModule, moduleName, isRepoPath, normPath, PAGE_SYSTEM, TAXONOMY_SYSTEM, CATEGORY_SYSTEM, ENTRY_NARRATIVE_SYSTEM, FLOW_SYSTEM, ARCH_SYSTEM, ARCH_FLOWS_SYSTEM, FEATURE_FLOW_SYSTEM, normTitle2, generateDoku;
16342
+ var DOKU_FILENAME, DOKU_DIR, MANIFEST, CHANGELOG_DAYS, sha1, exists, isModule, moduleName, isRepoPath, normPath, MAX_AI_ATTEMPTS, PAGE_SYSTEM, TAXONOMY_SYSTEM, REFILE_SYSTEM, CATEGORY_SYSTEM, ENTRY_NARRATIVE_SYSTEM, FLOW_SYSTEM, ARCH_SYSTEM, ARCH_FLOWS_SYSTEM, FEATURE_FLOW_SYSTEM, normTitle2, generateDoku;
15065
16343
  var init_doku = __esm({
15066
16344
  "packages/ingest/src/doku.ts"() {
15067
16345
  "use strict";
@@ -15072,7 +16350,7 @@ var init_doku = __esm({
15072
16350
  DOKU_DIR = "brainjar-doku";
15073
16351
  MANIFEST = ".manifest.json";
15074
16352
  CHANGELOG_DAYS = 60;
15075
- sha1 = (s) => createHash2("sha1").update(s).digest("hex");
16353
+ sha1 = (s) => createHash3("sha1").update(s).digest("hex");
15076
16354
  exists = (p) => stat(p).then(
15077
16355
  () => true,
15078
16356
  () => false
@@ -15081,41 +16359,57 @@ var init_doku = __esm({
15081
16359
  moduleName = (o) => String(o.body.name ?? o.title);
15082
16360
  isRepoPath = (name) => !/^([a-zA-Z]:[/\\]|[/\\])/.test(name) && !name.includes("node_modules") && !name.includes(".claude/");
15083
16361
  normPath = (p) => p.replace(/\\/g, "/").replace(/^\.\//, "").toLowerCase();
16362
+ MAX_AI_ATTEMPTS = 3;
15084
16363
  PAGE_SYSTEM = [
15085
16364
  "You write the documentation page for ONE feature, from its fact sheet.",
15086
- 'Respond ONLY with a JSON object of exactly this shape: {"title": "...", "category": "...", "body": "..."}. No code fences, nothing outside the JSON.',
16365
+ 'Respond ONLY with a JSON object of exactly this shape: {"title": "...", "category": "...", "icon": "...", "summary": "...", "blocks": [...]}. No code fences, nothing outside the JSON.',
16366
+ '"icon" is exactly ONE emoji that a reader would associate with this feature (a lock for auth, a magnifying glass for search, a bell for notifications); never text.',
15087
16367
  '"title" is a short human page title in product language (a name, not a sentence): never a file path, commit message, or code identifier.',
15088
16368
  '"category" is EXACTLY one key from the category list given with the input; pick the best fit, never invent a key.',
15089
- '"body" is the page content as markdown (## headings, paragraphs, lists, tables where they help). Every doc is INDIVIDUAL: compose the sections this content needs, never a fixed template.',
15090
- "A feature exposing an API documents what it is, why you need it, and the endpoints or tools with their parameters. A user-facing feature explains what is shown, why it exists, and how to use it. A pipeline explains what goes in, what comes out, and when it runs. A feature containing several of these covers each of them.",
15091
- "Open with what the reader can DO or GET with this feature (the outcome), then the mechanics.",
15092
- "Keep every fact; invent nothing. Do not repeat the change history (it is appended separately). No H1 heading (the title renders above).",
16369
+ '"summary" is 1-2 plain sentences (at most 240 characters) saying what the reader can DO or GET with this feature. It opens the page; it never replaces the description.',
16370
+ '"blocks" is the page content as an ordered list of content blocks; the app decides layout and colors. Each block is an object with "kind", one of:',
16371
+ '{"kind":"text","title":"...","markdown":"..."}: the explanation itself, in full prose: as many connected paragraphs as the subject needs, ### subheadings (never # or ##; "title" is the section heading), lists, inline code, links. The feature description LIVES in text blocks; several text blocks may alternate with other kinds.',
16372
+ '{"kind":"steps","title":"...","items":[{"title":"...","body":"..."}]}: only for a real sequence of actions a person performs (setup, usage, an operator workflow), never for numbering facts.',
16373
+ '{"kind":"table","title":"...","columns":["..."],"rows":[["..."]]}: settings, parameters, error cases, or differences where the columns enable a real comparison. Every row has exactly as many cells as there are columns. Never a table for a single sentence.',
16374
+ '{"kind":"endpoint","method":"GET","path":"/...","summary":"...","auth":"..."}: one concrete HTTP endpoint, ONLY when the fact sheet names that exact method and path; "auth" only when the fact sheet states the authentication. Never derive an endpoint from a file name or a feature title.',
16375
+ '{"kind":"code","title":"...","language":"bash","code":"...","illustrative":true}: a request, response, or command example. Illustrative examples use placeholders such as <API_TOKEN>, never real keys or private content, and never show behavior the fact sheet does not state.',
16376
+ '{"kind":"callout","tone":"note","text":"..."}: one short prerequisite, effect, or limit. "warning" only for a concrete consequence the reader must avoid; otherwise "note".',
16377
+ `{"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
+ "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
+ "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).",
16381
+ "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.",
15093
16382
  PROSE_STYLE
15094
16383
  ].join(" ");
15095
16384
  TAXONOMY_SYSTEM = [
15096
16385
  "You maintain the category structure of a software product's documentation.",
15097
16386
  "You are given the product's feature list and the EXISTING categories (possibly empty).",
15098
16387
  'Respond ONLY with a JSON object of exactly this shape: {"categories": [{"key": "...", "name": "...", "description": "..."}]}. No code fences, nothing outside the JSON.',
15099
- "Between 3 and 8 categories. Product language a new product manager understands, never code module names.",
16388
+ "The NUMBER of categories follows the product: one distinct area of the product is one category. A small product may need one or two; a large one may need more than eight. Never one category per feature, never a category for a single page when it belongs to an existing area, and never padding to reach a count. Product language a new product manager understands, never code module names.",
15100
16389
  "STABILITY WINS: keep every existing category (same key, same name) that still fits; only add a category when several features fit nothing, only rename or drop on clear misfit.",
15101
16390
  "Group low-level technical internals (schema files, build plumbing) into ONE category rather than spreading them.",
15102
- '"key" is a short kebab-case identifier; "description" is 1-2 sentences on what the area covers, it doubles as the landing-page intro.',
16391
+ '"key" is a short kebab-case identifier; "description" is ONE short plain sentence (at most 120 characters) saying what the area is for, written for a reader who does not know the product; the details belong to the category page, not here.',
15103
16392
  PROSE_STYLE
15104
16393
  ].join(" ");
16394
+ REFILE_SYSTEM = [
16395
+ "You file documentation pages into the given categories of a software product.",
16396
+ 'Respond ONLY with a JSON object of exactly this shape: {"pages": {"<slug>": "<category key>"}}. No code fences, nothing outside the JSON.',
16397
+ "Cover EVERY listed slug exactly once with exactly one key from the category list; choose by the page's subject as its title and summary describe it. Never invent keys or slugs."
16398
+ ].join(" ");
15105
16399
  CATEGORY_SYSTEM = [
15106
- "You write the landing page for ONE documentation category: the story of this product area.",
15107
- "You are given the category (name, description) and its member pages (slug, title, summary).",
15108
- "Respond with 2-6 plain markdown paragraphs (no headings, no lists) telling how this area works as a whole: what it solves and how its parts play together.",
15109
- "Weave EVERY member page into the text as an inline markdown link, exactly the form [Title](page:slug), copying the slug character for character from the input, so the reader follows links from the sentence they are reading.",
15110
- "Ground everything in the given summaries; invent nothing.",
16400
+ "You write the overview page for ONE documentation category: what this product area does, how its parts relate, and what a reader needs to know before opening a page.",
16401
+ "You are given the category (name, description) and its member pages (slug, title, summary). The complete member list is rendered separately below your text, so you never have to mention every page.",
16402
+ "Respond with structured markdown: an opening paragraph, then ## and ### headings with as many paragraphs as the subject needs, and lists only where they help. Explain the area's job, the relationships between its parts, prerequisites, limits, and terms at first use. Long is fine when the content needs it; never pad a small area.",
16403
+ "Link a page only where the link helps the reader at that point, with a short readable link text, exactly the form [text](page:slug), copying the slug character for character from the input. Never invent a slug and never turn a page title into a new page.",
16404
+ "No H1 heading (the category name renders above). Ground everything in the given summaries; invent nothing.",
15111
16405
  PROSE_STYLE
15112
16406
  ].join(" ");
15113
16407
  ENTRY_NARRATIVE_SYSTEM = [
15114
16408
  "You write the opening description for a software project's documentation.",
15115
16409
  "You are given the documentation entry page (the project's feature list) as markdown.",
15116
- "Respond with 4-7 short plain-text paragraphs a newcomer understands, covering in order: what the project IS and the problem it solves and for whom; what it DOES (the core loop in plain words); its SURFACES and integrations (the ways people and tools connect to it); and how the pieces fit together.",
16410
+ "Respond in markdown: first one short plain paragraph saying what the project IS and for whom, then ## sections a newcomer understands, covering in order: the problem it solves; what it DOES (the core loop in plain words); its SURFACES and integrations (the ways people and tools connect to it); and how the pieces fit together. Paragraphs carry the explanation; a list only where items are truly parallel.",
15117
16411
  "Complete enough that the reader could present the product afterwards, without descending into single-feature detail.",
15118
- "Ground every statement in the given features; invent nothing, no marketing superlatives, no headings, no lists.",
16412
+ "Ground every statement in the given features; invent nothing, no marketing superlatives, no H1 heading.",
15119
16413
  PROSE_STYLE
15120
16414
  ].join(" ");
15121
16415
  FLOW_SYSTEM = [
@@ -15137,7 +16431,8 @@ var init_doku = __esm({
15137
16431
  ].join(" ");
15138
16432
  ARCH_FLOWS_SYSTEM = [
15139
16433
  "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.",
15140
- 'Respond ONLY with JSON of exactly this shape: {"flows": [{"slug": "...", "title": "...", "summary": "...", "lanes": [{"key": "...", "title": "..."}], "nodes": [{"id": "...", "lane": "...", "title": "...", "subtitle": "...", "kind": "...", "evidence": ["path"]}], "edges": [{"from": "...", "to": "...", "label": "...", "kind": "..."}]}]}. No markdown, nothing outside the JSON.',
16434
+ '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
+ '"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.',
15141
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.",
15142
16437
  "lanes are 3-5 PHASES of the flow in order (intake, processing, storage, ...), titled in plain words. Never folder names.",
15143
16438
  "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).",
@@ -15272,10 +16567,12 @@ var init_verify_agent = __esm({
15272
16567
  init_sdk();
15273
16568
  init_src2();
15274
16569
  init_src();
16570
+ init_allowance();
15275
16571
  init_product_facts();
15276
16572
  init_doku();
15277
16573
  init_llm();
15278
16574
  init_openai();
16575
+ init_provider_rate2();
15279
16576
  init_routing();
15280
16577
  VERIFY_SYSTEM = [
15281
16578
  "You are BrainJar's verification agent. You read a software repository and record durable product knowledge, with one iron rule: prose is never truth, code is.",
@@ -15301,9 +16598,9 @@ var init_verify_agent = __esm({
15301
16598
  }
15302
16599
  });
15303
16600
 
15304
- // packages/ingest/src/verify-poll.ts
15305
- var init_verify_poll = __esm({
15306
- "packages/ingest/src/verify-poll.ts"() {
16601
+ // packages/ingest/src/verify-start.ts
16602
+ var init_verify_start = __esm({
16603
+ "packages/ingest/src/verify-start.ts"() {
15307
16604
  "use strict";
15308
16605
  init_src2();
15309
16606
  init_src();
@@ -15312,14 +16609,49 @@ var init_verify_poll = __esm({
15312
16609
  }
15313
16610
  });
15314
16611
 
15315
- // packages/ingest/src/verify-start.ts
15316
- var init_verify_start = __esm({
15317
- "packages/ingest/src/verify-start.ts"() {
16612
+ // packages/ingest/src/verify-poll.ts
16613
+ var init_verify_poll = __esm({
16614
+ "packages/ingest/src/verify-poll.ts"() {
15318
16615
  "use strict";
15319
16616
  init_src2();
15320
16617
  init_src();
15321
16618
  init_repo_reader();
15322
16619
  init_verify_agent();
16620
+ init_verify_start();
16621
+ }
16622
+ });
16623
+
16624
+ // packages/ingest/src/scheduler.ts
16625
+ var init_scheduler = __esm({
16626
+ "packages/ingest/src/scheduler.ts"() {
16627
+ "use strict";
16628
+ init_src2();
16629
+ init_daily_log();
16630
+ init_summaries();
16631
+ }
16632
+ });
16633
+
16634
+ // packages/ingest/src/job-worker.ts
16635
+ var init_job_worker = __esm({
16636
+ "packages/ingest/src/job-worker.ts"() {
16637
+ "use strict";
16638
+ init_src2();
16639
+ init_deepscan();
16640
+ init_doku();
16641
+ init_scheduler();
16642
+ init_summaries();
16643
+ init_verify_poll();
16644
+ init_routing();
16645
+ init_allowance();
16646
+ init_verify_start();
16647
+ }
16648
+ });
16649
+
16650
+ // packages/ingest/src/job-clock.ts
16651
+ var init_job_clock = __esm({
16652
+ "packages/ingest/src/job-clock.ts"() {
16653
+ "use strict";
16654
+ init_src2();
15323
16655
  }
15324
16656
  });
15325
16657
 
@@ -15330,18 +16662,10 @@ var init_ask_agent_model = __esm({
15330
16662
  init_sdk();
15331
16663
  init_src2();
15332
16664
  init_src();
16665
+ init_allowance();
15333
16666
  init_llm();
15334
16667
  init_openai();
15335
- }
15336
- });
15337
-
15338
- // packages/ingest/src/scheduler.ts
15339
- var init_scheduler = __esm({
15340
- "packages/ingest/src/scheduler.ts"() {
15341
- "use strict";
15342
- init_src2();
15343
- init_daily_log();
15344
- init_summaries();
16668
+ init_provider_rate2();
15345
16669
  }
15346
16670
  });
15347
16671
 
@@ -15364,6 +16688,7 @@ var init_src3 = __esm({
15364
16688
  init_llm();
15365
16689
  init_openai();
15366
16690
  init_routing();
16691
+ init_allowance();
15367
16692
  init_rationale_extractor();
15368
16693
  init_extraction();
15369
16694
  init_entities();
@@ -15401,6 +16726,8 @@ var init_src3 = __esm({
15401
16726
  init_verify_agent();
15402
16727
  init_verify_poll();
15403
16728
  init_verify_start();
16729
+ init_job_worker();
16730
+ init_job_clock();
15404
16731
  init_ask_agent_model();
15405
16732
  init_scheduler();
15406
16733
  registerConnector(
@@ -15453,7 +16780,24 @@ async function captureApi(pathAndQuery, init2 = {}) {
15453
16780
  }
15454
16781
  return body;
15455
16782
  }
15456
- var TokenError, TOKEN_HELP;
16783
+ async function uploadArtifacts(brain, sourceId, artifacts, cursor3) {
16784
+ let inserted = 0;
16785
+ for (let i = 0; i < artifacts.length; i += UPLOAD_PORTION) {
16786
+ const last = i + UPLOAD_PORTION >= artifacts.length;
16787
+ const r = await captureApi("/capture/artifacts", {
16788
+ method: "POST",
16789
+ body: {
16790
+ brain,
16791
+ sourceId,
16792
+ artifacts: artifacts.slice(i, i + UPLOAD_PORTION),
16793
+ cursor: last ? cursor3 : null
16794
+ }
16795
+ });
16796
+ inserted += r.inserted;
16797
+ }
16798
+ return inserted;
16799
+ }
16800
+ var TokenError, TOKEN_HELP, UPLOAD_PORTION;
15457
16801
  var init_api_client = __esm({
15458
16802
  "packages/cli/src/api-client.ts"() {
15459
16803
  "use strict";
@@ -15461,6 +16805,7 @@ var init_api_client = __esm({
15461
16805
  TokenError = class extends Error {
15462
16806
  };
15463
16807
  TOKEN_HELP = "BrainJar token revoked or invalid. Create a new token under Account in the web app, then run `npx brainjar init --brain <id> --token <token>` once in any project on this machine.";
16808
+ UPLOAD_PORTION = 2e3;
15464
16809
  }
15465
16810
  });
15466
16811
 
@@ -15693,7 +17038,7 @@ var init_debt = __esm({
15693
17038
  });
15694
17039
 
15695
17040
  // packages/cli/src/adapters/claude-code.ts
15696
- import { createHash as createHash3 } from "node:crypto";
17041
+ import { createHash as createHash4 } from "node:crypto";
15697
17042
  function targetOf(input) {
15698
17043
  const i = input ?? {};
15699
17044
  for (const k of ["command", "file_path", "notebook_path"]) {
@@ -15738,7 +17083,7 @@ var init_claude_code = __esm({
15738
17083
  "use strict";
15739
17084
  init_src();
15740
17085
  EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
15741
- short2 = (s) => createHash3("sha256").update(s).digest("hex").slice(0, 16);
17086
+ short2 = (s) => createHash4("sha256").update(s).digest("hex").slice(0, 16);
15742
17087
  claudeCode2 = {
15743
17088
  id: "claude-code",
15744
17089
  map(payload, now) {
@@ -15837,7 +17182,7 @@ var init_claude_code = __esm({
15837
17182
  });
15838
17183
 
15839
17184
  // packages/cli/src/adapters/cursor.ts
15840
- import { createHash as createHash4 } from "node:crypto";
17185
+ import { createHash as createHash5 } from "node:crypto";
15841
17186
  function targetOf2(input) {
15842
17187
  const i = input ?? {};
15843
17188
  for (const k of ["command", "file_path", "path", "target_file"]) {
@@ -15858,7 +17203,7 @@ var init_cursor = __esm({
15858
17203
  "packages/cli/src/adapters/cursor.ts"() {
15859
17204
  "use strict";
15860
17205
  init_src();
15861
- short3 = (s) => createHash4("sha256").update(s).digest("hex").slice(0, 16);
17206
+ short3 = (s) => createHash5("sha256").update(s).digest("hex").slice(0, 16);
15862
17207
  cursor2 = {
15863
17208
  id: "cursor",
15864
17209
  map(payload, now) {
@@ -15976,7 +17321,7 @@ var init_cursor = __esm({
15976
17321
  });
15977
17322
 
15978
17323
  // packages/cli/src/adapters/codex.ts
15979
- import { createHash as createHash5 } from "node:crypto";
17324
+ import { createHash as createHash6 } from "node:crypto";
15980
17325
  function patchFiles(patch) {
15981
17326
  const out = /* @__PURE__ */ new Set();
15982
17327
  for (const m of patch.matchAll(
@@ -16012,7 +17357,7 @@ var init_codex = __esm({
16012
17357
  "use strict";
16013
17358
  init_src();
16014
17359
  PATCH_TOOLS = /* @__PURE__ */ new Set(["apply_patch", "Edit", "Write"]);
16015
- short4 = (s) => createHash5("sha256").update(s).digest("hex").slice(0, 16);
17360
+ short4 = (s) => createHash6("sha256").update(s).digest("hex").slice(0, 16);
16016
17361
  codex2 = {
16017
17362
  id: "codex",
16018
17363
  map(payload, now) {
@@ -16109,7 +17454,7 @@ var init_codex = __esm({
16109
17454
  });
16110
17455
 
16111
17456
  // packages/cli/src/adapters/copilot.ts
16112
- import { createHash as createHash6 } from "node:crypto";
17457
+ import { createHash as createHash7 } from "node:crypto";
16113
17458
  function targetOf3(args) {
16114
17459
  if (!args || typeof args !== "object") return "";
16115
17460
  const a = args;
@@ -16142,7 +17487,7 @@ var init_copilot = __esm({
16142
17487
  "use strict";
16143
17488
  init_src();
16144
17489
  EDIT_TOOLS2 = /* @__PURE__ */ new Set(["edit", "create"]);
16145
- short5 = (s) => createHash6("sha256").update(s).digest("hex").slice(0, 16);
17490
+ short5 = (s) => createHash7("sha256").update(s).digest("hex").slice(0, 16);
16146
17491
  EVENT_ALIASES = {
16147
17492
  SessionStart: "sessionStart",
16148
17493
  SessionEnd: "sessionEnd",
@@ -16251,7 +17596,7 @@ var init_copilot = __esm({
16251
17596
  });
16252
17597
 
16253
17598
  // packages/cli/src/adapters/gemini.ts
16254
- import { createHash as createHash7 } from "node:crypto";
17599
+ import { createHash as createHash8 } from "node:crypto";
16255
17600
  function targetOf4(input) {
16256
17601
  const i = input ?? {};
16257
17602
  for (const k of ["command", "file_path"]) {
@@ -16273,7 +17618,7 @@ var init_gemini = __esm({
16273
17618
  "use strict";
16274
17619
  init_src();
16275
17620
  EDIT_TOOLS3 = /* @__PURE__ */ new Set(["write_file", "replace"]);
16276
- short6 = (s) => createHash7("sha256").update(s).digest("hex").slice(0, 16);
17621
+ short6 = (s) => createHash8("sha256").update(s).digest("hex").slice(0, 16);
16277
17622
  gemini2 = {
16278
17623
  id: "gemini",
16279
17624
  map(payload, now) {
@@ -16369,7 +17714,7 @@ var init_gemini = __esm({
16369
17714
  });
16370
17715
 
16371
17716
  // packages/cli/src/adapters/kiro.ts
16372
- import { createHash as createHash8 } from "node:crypto";
17717
+ import { createHash as createHash9 } from "node:crypto";
16373
17718
  function targetOf5(input) {
16374
17719
  const i = input ?? {};
16375
17720
  for (const k of ["command", "cmd", "path", "file_path", "filePath"]) {
@@ -16400,7 +17745,7 @@ var init_kiro = __esm({
16400
17745
  "use strict";
16401
17746
  init_src();
16402
17747
  EDIT_TOOLS4 = /* @__PURE__ */ new Set(["fs_write", "write", "str_replace", "fs_append"]);
16403
- short7 = (s) => createHash8("sha256").update(s).digest("hex").slice(0, 16);
17748
+ short7 = (s) => createHash9("sha256").update(s).digest("hex").slice(0, 16);
16404
17749
  EVENTS5 = {
16405
17750
  agentSpawn: "start",
16406
17751
  sessionStart: "start",
@@ -16516,7 +17861,7 @@ var init_kiro = __esm({
16516
17861
  });
16517
17862
 
16518
17863
  // packages/cli/src/adapters/opencode.ts
16519
- import { createHash as createHash9 } from "node:crypto";
17864
+ import { createHash as createHash10 } from "node:crypto";
16520
17865
  function commitShas7(text) {
16521
17866
  const out = /* @__PURE__ */ new Set();
16522
17867
  for (const m of (text ?? "").matchAll(/\[[^\]\n]* ([0-9a-f]{7,40})\]/g)) {
@@ -16530,7 +17875,7 @@ var init_opencode = __esm({
16530
17875
  "use strict";
16531
17876
  init_src();
16532
17877
  EDIT_TOOLS5 = /* @__PURE__ */ new Set(["edit", "write"]);
16533
- short8 = (s) => createHash9("sha256").update(s).digest("hex").slice(0, 16);
17878
+ short8 = (s) => createHash10("sha256").update(s).digest("hex").slice(0, 16);
16534
17879
  opencode2 = {
16535
17880
  id: "opencode",
16536
17881
  map(payload, now) {
@@ -16630,7 +17975,7 @@ var init_opencode = __esm({
16630
17975
  });
16631
17976
 
16632
17977
  // packages/cli/src/adapters/cline.ts
16633
- import { createHash as createHash10 } from "node:crypto";
17978
+ import { createHash as createHash11 } from "node:crypto";
16634
17979
  function targetOf6(params) {
16635
17980
  const i = params ?? {};
16636
17981
  for (const k of ["command", "path"]) {
@@ -16646,9 +17991,9 @@ function commitShas8(text) {
16646
17991
  return [...out];
16647
17992
  }
16648
17993
  function stamp2(ts, now) {
16649
- const n = typeof ts === "string" && /^\d+$/.test(ts) ? Number(ts) : ts;
16650
- if (typeof n === "number" && Number.isFinite(n))
16651
- return new Date(n).toISOString();
17994
+ const n2 = typeof ts === "string" && /^\d+$/.test(ts) ? Number(ts) : ts;
17995
+ if (typeof n2 === "number" && Number.isFinite(n2))
17996
+ return new Date(n2).toISOString();
16652
17997
  if (typeof ts === "string" && !Number.isNaN(Date.parse(ts)))
16653
17998
  return new Date(ts).toISOString();
16654
17999
  return now;
@@ -16662,7 +18007,7 @@ var init_cline = __esm({
16662
18007
  "use strict";
16663
18008
  init_src();
16664
18009
  EDIT_TOOLS6 = /* @__PURE__ */ new Set(["write_to_file", "replace_in_file", "apply_patch"]);
16665
- short9 = (s) => createHash10("sha256").update(s).digest("hex").slice(0, 16);
18010
+ short9 = (s) => createHash11("sha256").update(s).digest("hex").slice(0, 16);
16666
18011
  CLINE_REPLY = JSON.stringify({ cancel: false });
16667
18012
  cline2 = {
16668
18013
  id: "cline",
@@ -16860,16 +18205,12 @@ async function linkForSpool(file) {
16860
18205
  async function shipSpool(file, link) {
16861
18206
  const events = readSpool(file);
16862
18207
  if (!events.length) return;
16863
- const r = await captureApi("/capture/events", {
18208
+ await captureApi("/capture/events", {
16864
18209
  method: "POST",
16865
- body: {
16866
- brain: link.brainId,
16867
- events,
16868
- author: loadEnv().BRAINJAR_AUTHOR?.trim() || null
16869
- }
18210
+ body: { brain: link.brainId, events }
16870
18211
  });
16871
18212
  console.error(
16872
- `brainjar: session captured \u2014 ${r.prompts ?? 0} intents, ${r.failures ?? 0} failures, ${(r.skippedNoise ?? 0) + (r.skippedFailures ?? 0)} noise skipped` + (r.consolidated ? `, ${r.consolidated} product facts` : "")
18213
+ `brainjar: session sent (${events.length} events), the server reads it in the background`
16873
18214
  );
16874
18215
  }
16875
18216
  function parseHookArgs(argv) {
@@ -16968,23 +18309,16 @@ async function syncRepo(root) {
16968
18309
  const connector = createConnector("git_local", { cwd: root }, {});
16969
18310
  const artifacts = await connector.sync(project.cursor ?? void 0);
16970
18311
  if (artifacts.length > 0) {
16971
- const r = await captureApi(
16972
- "/capture/artifacts",
16973
- {
16974
- method: "POST",
16975
- body: {
16976
- brain: link.brainId,
16977
- sourceId: project.sourceId,
16978
- artifacts,
16979
- cursor: maxUpdatedAt(artifacts) ?? project.cursor
16980
- }
16981
- }
18312
+ const inserted = await uploadArtifacts(
18313
+ link.brainId,
18314
+ project.sourceId,
18315
+ artifacts,
18316
+ maxUpdatedAt(artifacts) ?? project.cursor
16982
18317
  );
16983
- if (r.inserted > 0) {
16984
- console.error(`brainjar: ${r.inserted} new commits captured`);
16985
- if (r.linked > 0) {
16986
- console.error(`brainjar: ${r.linked} session-commit links ensured`);
16987
- }
18318
+ if (inserted > 0) {
18319
+ console.error(
18320
+ `brainjar: ${inserted} new commits sent, the server reads them in the background`
18321
+ );
16988
18322
  const graph = await scanStructure(root);
16989
18323
  await captureApi("/capture/structure", {
16990
18324
  method: "POST",
@@ -17041,12 +18375,67 @@ var init_sync2 = __esm({
17041
18375
  }
17042
18376
  });
17043
18377
 
18378
+ // packages/cli/src/wait.ts
18379
+ function progressLine(p) {
18380
+ const left = p.pending > 0 ? `${n(p.pending)} commits and sessions still to read` : `${n(p.queued + p.running)} capture jobs still running`;
18381
+ const queue = p.ahead > 0 ? ` (${n(p.ahead)} jobs of other projects ahead)` : "";
18382
+ return ` \u2026 ${left}${queue}`;
18383
+ }
18384
+ async function waitForServer(brainId, since, opts = {}) {
18385
+ const poll = opts.poll ?? (() => captureApi(
18386
+ `/capture/progress?brain=${brainId}&since=${encodeURIComponent(since.toISOString())}`
18387
+ ));
18388
+ const sleep3 = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
18389
+ const write = opts.write ?? ((s) => void process.stdout.write(s));
18390
+ const tty = opts.tty ?? Boolean(process.stdout.isTTY);
18391
+ const intervalMs = opts.intervalMs ?? 2e3;
18392
+ let shown = "";
18393
+ let shownAt = 0;
18394
+ let misses = 0;
18395
+ for (; ; ) {
18396
+ let p;
18397
+ try {
18398
+ p = await poll();
18399
+ misses = 0;
18400
+ } catch {
18401
+ if (++misses * intervalMs >= 6e4) {
18402
+ if (tty && shown) write("\n");
18403
+ return null;
18404
+ }
18405
+ await sleep3(intervalMs);
18406
+ continue;
18407
+ }
18408
+ if (p.queued + p.running === 0) {
18409
+ if (tty && shown) write("\r\x1B[K");
18410
+ return p;
18411
+ }
18412
+ const line = progressLine(p);
18413
+ if (tty) write(`\r\x1B[K${line}`);
18414
+ else if (line !== shown && Date.now() - shownAt >= 15e3) {
18415
+ write(`${line}
18416
+ `);
18417
+ shownAt = Date.now();
18418
+ }
18419
+ shown = line;
18420
+ await sleep3(intervalMs);
18421
+ }
18422
+ }
18423
+ var n;
18424
+ var init_wait = __esm({
18425
+ "packages/cli/src/wait.ts"() {
18426
+ "use strict";
18427
+ init_api_client();
18428
+ n = (x) => x.toLocaleString("en-US");
18429
+ }
18430
+ });
18431
+
17044
18432
  // packages/cli/src/init.ts
17045
18433
  var init_exports = {};
17046
18434
  __export(init_exports, {
17047
18435
  init: () => init,
17048
18436
  initMain: () => initMain
17049
18437
  });
18438
+ import { hostname } from "node:os";
17050
18439
  import { readFile as readFile12 } from "node:fs/promises";
17051
18440
  async function init(argv) {
17052
18441
  const skipTranscripts = argv.includes("--no-transcripts");
@@ -17056,6 +18445,7 @@ async function init(argv) {
17056
18445
  const tokenFlag = flag("--token");
17057
18446
  const root = await repoRoot();
17058
18447
  const name = repoName(root);
18448
+ const startedAt = new Date(Date.now() - 6e4);
17059
18449
  console.log(`
17060
18450
  \u{1F9E0} BrainJar init \u2014 ${name}
17061
18451
  repo: ${root}`);
@@ -17083,6 +18473,7 @@ async function init(argv) {
17083
18473
  method: "POST",
17084
18474
  body: {
17085
18475
  name,
18476
+ machine: hostname(),
17086
18477
  ...brainFlag ? { brainId: brainFlag } : reuse ? { brainId: reuse.brainId } : {},
17087
18478
  ...reuse ? { sourceId: reuse.sourceId } : {},
17088
18479
  ...remote ? { remote } : {}
@@ -17104,61 +18495,45 @@ async function init(argv) {
17104
18495
  }
17105
18496
  step(3, "Scanning code + ingesting git history\u2026");
17106
18497
  const graph = await scanStructure(root);
17107
- const structure = await captureApi(
17108
- "/capture/structure",
17109
- { method: "POST", body: { brain: conn.brainId, graph } }
17110
- );
18498
+ await captureApi("/capture/structure", {
18499
+ method: "POST",
18500
+ body: { brain: conn.brainId, graph }
18501
+ });
17111
18502
  const connector = createConnector("git_local", { cwd: root }, {});
17112
18503
  const artifacts = await connector.sync(conn.cursor ?? void 0);
17113
18504
  let commits = 0;
17114
- let objects = 0;
17115
18505
  if (artifacts.length > 0) {
17116
- const r = await captureApi(
17117
- "/capture/artifacts",
17118
- {
17119
- method: "POST",
17120
- body: {
17121
- brain: conn.brainId,
17122
- sourceId: conn.sourceId,
17123
- artifacts,
17124
- cursor: maxUpdatedAt(artifacts) ?? conn.cursor
17125
- }
17126
- }
18506
+ commits = await uploadArtifacts(
18507
+ conn.brainId,
18508
+ conn.sourceId,
18509
+ artifacts,
18510
+ maxUpdatedAt(artifacts) ?? conn.cursor
17127
18511
  );
17128
- commits = r.inserted;
17129
- objects = r.objects;
17130
18512
  }
17131
18513
  ok(
17132
- `${structure.modules} modules, ${commits} new commits \u2192 ${objects} objects`
18514
+ `${graph.modules.length} modules, ${commits} new commits sent to the server`
17133
18515
  );
17134
18516
  step(4, "Backfilling past agent sessions\u2026");
17135
18517
  if (skipTranscripts) {
17136
18518
  ok("skipped (--no-transcripts)");
17137
18519
  } else {
17138
- const author = loadEnv().BRAINJAR_AUTHOR?.trim() || null;
17139
18520
  const files = await transcriptFiles(root);
17140
- let prompts = 0;
17141
- let failures = 0;
17142
- let consolidated = 0;
17143
18521
  let truncatedCount = 0;
17144
18522
  let skipped = 0;
17145
18523
  for (const f of files) {
17146
18524
  try {
17147
18525
  const { text, truncated } = capTranscript(await readFile12(f, "utf8"));
17148
18526
  if (truncated) truncatedCount++;
17149
- const r = await captureApi("/capture/transcript", {
18527
+ await captureApi("/capture/transcript", {
17150
18528
  method: "POST",
17151
- body: { brain: conn.brainId, text, author, backfill: true }
18529
+ body: { brain: conn.brainId, text, backfill: true }
17152
18530
  });
17153
- prompts += r.prompts;
17154
- failures += r.failures;
17155
- consolidated += r.consolidated;
17156
18531
  } catch {
17157
18532
  skipped++;
17158
18533
  }
17159
18534
  }
17160
18535
  ok(
17161
- `${files.length - skipped} sessions \u2192 ${prompts} reasoning, ${failures} failures` + (consolidated ? `, ${consolidated} product facts` : "") + " captured" + (truncatedCount ? ` (${truncatedCount} oversized, newest 40MB kept)` : "") + (skipped ? `, ${skipped} skipped on errors` : "")
18536
+ `${files.length - skipped} sessions sent to the server` + (truncatedCount ? ` (${truncatedCount} oversized, newest 40MB kept)` : "") + (skipped ? `, ${skipped} skipped on errors` : "")
17162
18537
  );
17163
18538
  }
17164
18539
  step(5, "Semantic search\u2026");
@@ -17208,6 +18583,15 @@ async function init(argv) {
17208
18583
  } catch {
17209
18584
  ok("skipped (could not write the pointer)");
17210
18585
  }
18586
+ step(8, "Filling the Brain on the server\u2026");
18587
+ console.log(" Stopping this (Ctrl+C) is safe: the server keeps working.");
18588
+ const filled = await waitForServer(conn.brainId, startedAt);
18589
+ if (!filled) ok("lost the connection to the server; it keeps working");
18590
+ else if (filled.failed > 0)
18591
+ ok(
18592
+ `done; ${filled.failed} capture jobs failed for good (see the bell in the web app)`
18593
+ );
18594
+ else ok("done");
17211
18595
  try {
17212
18596
  await captureApi("/capture/connect", {
17213
18597
  method: "POST",
@@ -17215,6 +18599,7 @@ async function init(argv) {
17215
18599
  brainId: conn.brainId,
17216
18600
  sourceId: conn.sourceId,
17217
18601
  name,
18602
+ machine: hostname(),
17218
18603
  done: true,
17219
18604
  tools: tools.map((t) => t.id)
17220
18605
  }
@@ -17254,8 +18639,9 @@ var init_init = __esm({
17254
18639
  init_src();
17255
18640
  init_api_client();
17256
18641
  init_sync2();
17257
- step = (n, msg) => console.log(`
17258
- [${n}/7] ${msg}`);
18642
+ init_wait();
18643
+ step = (n2, msg) => console.log(`
18644
+ [${n2}/8] ${msg}`);
17259
18645
  ok = (msg) => console.log(` \u2713 ${msg}`);
17260
18646
  }
17261
18647
  });
@@ -17528,13 +18914,13 @@ function selectDeepScanFiles(candidates, graph, maxFiles = MAX_FILES) {
17528
18914
  }
17529
18915
  };
17530
18916
  for (const bucket of BUCKET_CAPS) {
17531
- let n = 0;
18917
+ let n2 = 0;
17532
18918
  const ordered = [...candidates].sort().reverse();
17533
18919
  for (const p of ordered) {
17534
- if (n >= bucket.cap) break;
18920
+ if (n2 >= bucket.cap) break;
17535
18921
  if (bucket.test(p) && !has.has(p)) {
17536
18922
  take(p);
17537
- n++;
18923
+ n2++;
17538
18924
  }
17539
18925
  }
17540
18926
  }