brainjar 0.3.747 → 0.3.975

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 +1430 -330
  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.975";
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() {
@@ -5851,6 +6039,8 @@ function validateFlowDiagram(d) {
5851
6039
  if (!short(f.title, 120)) errs.push("title: missing or too long");
5852
6040
  if (f.summary !== void 0 && !short(f.summary, 300))
5853
6041
  errs.push("summary: empty or too long");
6042
+ if (f.explanation !== void 0 && !short(f.explanation, 4e3))
6043
+ errs.push("explanation: empty or too long");
5854
6044
  const lanes = Array.isArray(f.lanes) ? f.lanes : [];
5855
6045
  if (lanes.length < 2 || lanes.length > 6)
5856
6046
  errs.push("lanes: need 2-6 phase bands");
@@ -5863,22 +6053,31 @@ function validateFlowDiagram(d) {
5863
6053
  } else laneKeys.add(l.key);
5864
6054
  }
5865
6055
  const nodes = Array.isArray(f.nodes) ? f.nodes : [];
5866
- if (nodes.length < 3 || nodes.length > 14)
5867
- errs.push("nodes: need 3-14 nodes");
6056
+ if (nodes.length < 3 || nodes.length > 18)
6057
+ errs.push("nodes: need 3-18 nodes");
5868
6058
  const ids = /* @__PURE__ */ new Set();
5869
- for (const n of nodes) {
5870
- if (!short(n?.id, 40) || !short(n?.title, 60)) {
6059
+ for (const n2 of nodes) {
6060
+ if (!short(n2?.id, 40) || !short(n2?.title, 60)) {
5871
6061
  errs.push("nodes: each needs id and title");
5872
6062
  continue;
5873
6063
  }
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`);
6064
+ if (ids.has(n2.id)) errs.push(`nodes: duplicate id ${n2.id}`);
6065
+ ids.add(n2.id);
6066
+ if (!laneKeys.has(n2.lane ?? "")) errs.push(`nodes: ${n2.id} unknown lane`);
6067
+ if (!NODE_KINDS.has(n2.kind ?? "")) errs.push(`nodes: ${n2.id} unknown kind`);
6068
+ if (n2.subtitle !== void 0 && !short(n2.subtitle, 60))
6069
+ errs.push(`nodes: ${n2.id} oversized subtitle`);
6070
+ if (n2.evidence !== void 0 && !Array.isArray(n2.evidence))
6071
+ errs.push(`nodes: ${n2.id} evidence must be a list`);
6072
+ if (Array.isArray(n2.evidence)) {
6073
+ if (n2.evidence.length > 12)
6074
+ errs.push(`nodes: ${n2.id} too many evidence entries`);
6075
+ for (const ev of n2.evidence)
6076
+ if (!short(ev, 200) || /[-]/.test(ev))
6077
+ errs.push(`nodes: ${n2.id} evidence entry is not a short string`);
6078
+ }
6079
+ if (n2.description !== void 0 && !short(n2.description, 600))
6080
+ errs.push(`nodes: ${n2.id} description empty or too long`);
5882
6081
  }
5883
6082
  const edges = Array.isArray(f.edges) ? f.edges : [];
5884
6083
  if (edges.length < 2 || edges.length > 24)
@@ -5889,7 +6088,7 @@ function validateFlowDiagram(d) {
5889
6088
  continue;
5890
6089
  }
5891
6090
  if (e.from === e.to) errs.push("edges: self-loop");
5892
- if (e.label !== void 0 && !short(e.label, 40))
6091
+ if (e.label !== void 0 && !short(e.label, 60))
5893
6092
  errs.push("edges: oversized label");
5894
6093
  if (e.kind !== void 0 && !EDGE_KINDS.has(e.kind))
5895
6094
  errs.push("edges: unknown kind");
@@ -5957,7 +6156,55 @@ var init_capture_events = __esm({
5957
6156
  tool_name: 200,
5958
6157
  reason: 200
5959
6158
  };
5960
- cut = (s, n) => s === void 0 ? void 0 : s.length > n ? s.slice(0, n) : s;
6159
+ cut = (s, n2) => s === void 0 ? void 0 : s.length > n2 ? s.slice(0, n2) : s;
6160
+ }
6161
+ });
6162
+
6163
+ // packages/shared/src/emoji.ts
6164
+ function isEmoji(s) {
6165
+ return ONE_EMOJI.test(s);
6166
+ }
6167
+ var ONE_EMOJI;
6168
+ var init_emoji = __esm({
6169
+ "packages/shared/src/emoji.ts"() {
6170
+ "use strict";
6171
+ ONE_EMOJI = new RegExp("^\\p{RGI_Emoji}$", "v");
6172
+ }
6173
+ });
6174
+
6175
+ // packages/shared/src/password.ts
6176
+ var init_password = __esm({
6177
+ "packages/shared/src/password.ts"() {
6178
+ "use strict";
6179
+ }
6180
+ });
6181
+
6182
+ // packages/shared/src/totp.ts
6183
+ var init_totp = __esm({
6184
+ "packages/shared/src/totp.ts"() {
6185
+ "use strict";
6186
+ }
6187
+ });
6188
+
6189
+ // packages/shared/src/brand.ts
6190
+ var BRAND_MARK_PATH, BRAND_ICON_SVG;
6191
+ var init_brand = __esm({
6192
+ "packages/shared/src/brand.ts"() {
6193
+ "use strict";
6194
+ 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";
6195
+ 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>';
6196
+ }
6197
+ });
6198
+
6199
+ // packages/shared/src/summary-report.ts
6200
+ var CONTROL;
6201
+ var init_summary_report = __esm({
6202
+ "packages/shared/src/summary-report.ts"() {
6203
+ "use strict";
6204
+ CONTROL = new RegExp(
6205
+ "[\\u0000-\\u0008\\u000b\\u000c\\u000e-\\u001f\\u007f]",
6206
+ "g"
6207
+ );
5961
6208
  }
5962
6209
  });
5963
6210
 
@@ -5973,6 +6220,11 @@ var init_src = __esm({
5973
6220
  init_sanitize();
5974
6221
  init_flow();
5975
6222
  init_capture_events();
6223
+ init_emoji();
6224
+ init_password();
6225
+ init_totp();
6226
+ init_brand();
6227
+ init_summary_report();
5976
6228
  PROSE_STYLE = "Never use em dashes (\u2014) or en dashes (\u2013) anywhere; use commas, colons, periods, or parentheses instead.";
5977
6229
  }
5978
6230
  });
@@ -6538,12 +6790,12 @@ var init_repository = __esm({
6538
6790
  * first (five logs cover a week completely, where a newest-N slice of raw objects sees only
6539
6791
  * the last hours of a busy range).
6540
6792
  */
6541
- async dailyLogsInRange(startIso, endIso, limit = 50) {
6793
+ async dailyLogsInRange(startIso, endIso, limit = 50, order = "desc") {
6542
6794
  return (await this.pool.query(
6543
6795
  `SELECT ${MO_COLUMNS}, ${EV_AGG} FROM memory_object
6544
6796
  WHERE brain_id = $1 AND type = 'daily_log'
6545
6797
  AND occurred_at >= $2::timestamptz AND occurred_at < $3::timestamptz
6546
- ORDER BY occurred_at DESC LIMIT $4`,
6798
+ ORDER BY occurred_at ${order === "asc" ? "ASC" : "DESC"} LIMIT $4`,
6547
6799
  [this.brainId, startIso, endIso, limit]
6548
6800
  )).rows;
6549
6801
  }
@@ -6729,6 +6981,20 @@ var init_repository = __esm({
6729
6981
  return r.rows;
6730
6982
  }
6731
6983
  // --- Overview / summaries ---
6984
+ /** Compact freshness inputs for the whole Brain, without loading bodies or N+1 queries. */
6985
+ async memoryFreshnessRows() {
6986
+ return (await this.pool.query(
6987
+ `SELECT m.id, m.type, m.title, m.updated_at AS "updatedAt",
6988
+ m.last_verified_at AS "lastVerifiedAt", m.status,
6989
+ EXISTS (SELECT 1 FROM superseded_object s WHERE s.object_id = m.id) AS superseded,
6990
+ EXISTS (SELECT 1 FROM evidence_link el
6991
+ JOIN raw_artifact ra ON ra.id = el.raw_artifact_id
6992
+ WHERE el.memory_object_id = m.id AND ra.last_seen_at > m.updated_at) AS "evidenceChanged"
6993
+ FROM memory_object m WHERE m.brain_id = $1
6994
+ ORDER BY m.updated_at DESC, m.id`,
6995
+ [this.brainId]
6996
+ )).rows;
6997
+ }
6732
6998
  /** Memory-object counts for this Brain, grouped by type and confidence (dashboard overview). */
6733
6999
  async counts() {
6734
7000
  const rows = (await this.pool.query(
@@ -6777,7 +7043,8 @@ var init_repository = __esm({
6777
7043
  return (await this.pool.query(
6778
7044
  `SELECT * FROM summary WHERE brain_id = $1
6779
7045
  AND ($3 OR created_by IS NULL OR created_by = $2)
6780
- ORDER BY created_at DESC`,
7046
+ ORDER BY period_start DESC NULLS LAST,
7047
+ COALESCE(period_end, period_start) DESC, created_at DESC`,
6781
7048
  [this.brainId, viewerId ?? null, viewerId === void 0]
6782
7049
  )).rows;
6783
7050
  }
@@ -6797,8 +7064,8 @@ var init_repository = __esm({
6797
7064
  if (!isConfidence(confidence))
6798
7065
  throw new Error(`invalid confidence: ${confidence}`);
6799
7066
  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 *`,
7067
+ `INSERT INTO summary (brain_id, timescale, title, body, period_start, period_end, confidence, created_by, chronicle, data_through)
7068
+ VALUES ($1,$2,$3,$4,$5::date,$6::date,$7,$8,$9::jsonb,$10::date) RETURNING *`,
6802
7069
  [
6803
7070
  this.brainId,
6804
7071
  input.timescale,
@@ -6807,7 +7074,9 @@ var init_repository = __esm({
6807
7074
  input.periodStartIso ?? null,
6808
7075
  input.periodEndIso ?? null,
6809
7076
  confidence,
6810
- input.createdBy ?? null
7077
+ input.createdBy ?? null,
7078
+ input.chronicle ? JSON.stringify(input.chronicle) : null,
7079
+ input.dataThroughIso ?? null
6811
7080
  ]
6812
7081
  );
6813
7082
  return r.rows[0];
@@ -6842,14 +7111,69 @@ var init_repository = __esm({
6842
7111
  */
6843
7112
  async updateSummary(id, patch) {
6844
7113
  const r = await this.pool.query(
6845
- `UPDATE summary SET title = $3, body = $4, confidence = $5
7114
+ `UPDATE summary SET title = $3, body = $4, confidence = $5,
7115
+ chronicle = COALESCE($6::jsonb, chronicle),
7116
+ data_through = COALESCE($7::date, data_through)
6846
7117
  WHERE id = $1 AND brain_id = $2 RETURNING *`,
6847
- [id, this.brainId, patch.title, patch.body, patch.confidence]
7118
+ [
7119
+ id,
7120
+ this.brainId,
7121
+ patch.title,
7122
+ patch.body,
7123
+ patch.confidence,
7124
+ patch.chronicle ? JSON.stringify(patch.chronicle) : null,
7125
+ patch.dataThroughIso ?? null
7126
+ ]
6848
7127
  );
6849
7128
  const row = r.rows[0];
6850
7129
  if (!row) throw new Error("summary not found in this Brain");
6851
7130
  return row;
6852
7131
  }
7132
+ /**
7133
+ * Claim the report job for one floor (summaries review S01): returns true for exactly one
7134
+ * caller per (summary, hash), so two web processes never write the same report twice. A
7135
+ * claim older than ten minutes counts as abandoned (the process died mid-call) and may be
7136
+ * taken again; `force` retakes a failed one on the reader's request.
7137
+ */
7138
+ async claimSummaryReport(id, hash, opts = {}) {
7139
+ const r = await this.pool.query(
7140
+ `UPDATE summary
7141
+ SET report_status = 'pending', report_status_hash = $3,
7142
+ report_status_at = now(), report_error = NULL
7143
+ WHERE id = $1 AND brain_id = $2
7144
+ AND report_hash IS DISTINCT FROM $3
7145
+ AND (report_status_hash IS DISTINCT FROM $3
7146
+ OR report_status IS NULL
7147
+ OR report_status_at < now() - interval '10 minutes'
7148
+ OR (report_status = 'failed' AND $4))`,
7149
+ [id, this.brainId, hash, Boolean(opts.force)]
7150
+ );
7151
+ return (r.rowCount ?? 0) > 0;
7152
+ }
7153
+ /** Store the validated report for the floor it was written from; the job is done. */
7154
+ async saveSummaryReport(id, report, hash) {
7155
+ const r = await this.pool.query(
7156
+ `UPDATE summary
7157
+ SET report = $3::jsonb, report_hash = $4, report_at = now(),
7158
+ report_status = NULL, report_status_hash = NULL,
7159
+ report_status_at = NULL, report_error = NULL
7160
+ WHERE id = $1 AND brain_id = $2 RETURNING *`,
7161
+ [id, this.brainId, JSON.stringify(report), hash]
7162
+ );
7163
+ const row = r.rows[0];
7164
+ if (!row) throw new Error("summary not found in this Brain");
7165
+ return row;
7166
+ }
7167
+ /** Record why the report for this floor could not be written; the page shows it. */
7168
+ async failSummaryReport(id, hash, error) {
7169
+ await this.pool.query(
7170
+ `UPDATE summary
7171
+ SET report_status = 'failed', report_status_hash = $3,
7172
+ report_status_at = now(), report_error = $4
7173
+ WHERE id = $1 AND brain_id = $2`,
7174
+ [id, this.brainId, hash, error.slice(0, 200)]
7175
+ );
7176
+ }
6853
7177
  /** Store the synthesized prose layer with the floor hash it was built from. */
6854
7178
  async saveSummarySynthesis(id, text, hash) {
6855
7179
  const r = await this.pool.query(
@@ -6862,13 +7186,23 @@ var init_repository = __esm({
6862
7186
  return row;
6863
7187
  }
6864
7188
  // --- Keyword search (Postgres FTS) ---
6865
- /** Ranked full-text keyword search over title/why/body in this Brain. */
7189
+ /**
7190
+ * Ranked full-text search over title/why/body in this Brain where any query word counts
7191
+ * (documents sharing more and rarer words rank first). websearch_to_tsquery required EVERY
7192
+ * word, so a sentence-length question or ticket intent matched nothing: measured on 314 real
7193
+ * chat questions, 15% got a keyword hit, with OR nearly all do. plainto_tsquery still stems
7194
+ * and drops stop words; its AND becomes OR. Documents holding every word come first and are
7195
+ * flagged, so fusion can keep trusting them as before.
7196
+ */
6866
7197
  async searchKeyword(query, limit = 10) {
6867
7198
  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
7199
+ `SELECT ${MO_COLUMNS}, ${EV_AGG}, ts_rank(tsv, q.kq) AS rank,
7200
+ tsv @@ q.all_kq AS all_words
7201
+ FROM memory_object,
7202
+ (SELECT plainto_tsquery('english', $1) AS all_kq,
7203
+ replace(plainto_tsquery('english', $1)::text, ' & ', ' | ')::tsquery AS kq) q
7204
+ WHERE brain_id = $2 AND tsv @@ q.kq
7205
+ ORDER BY all_words DESC, rank DESC
6872
7206
  LIMIT $3`,
6873
7207
  [query, this.brainId, limit]
6874
7208
  );
@@ -6958,8 +7292,10 @@ var init_repository = __esm({
6958
7292
  */
6959
7293
  async saveDocPage(input) {
6960
7294
  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
7295
+ `INSERT INTO doc_page (brain_id, slug, version, kind, title, model, markdown, source_hash, deleted, icon)
7296
+ SELECT $1, $2, COALESCE(max(version), 0) + 1, $3, $4, $5, $6, $7, $8,
7297
+ COALESCE((SELECT icon FROM doc_page WHERE brain_id = $1 AND slug = $2
7298
+ ORDER BY version DESC LIMIT 1), $9)
6963
7299
  FROM doc_page WHERE brain_id = $1 AND slug = $2
6964
7300
  RETURNING *`,
6965
7301
  [
@@ -6970,11 +7306,21 @@ var init_repository = __esm({
6970
7306
  JSON.stringify(input.model),
6971
7307
  input.markdown,
6972
7308
  input.sourceHash,
6973
- input.deleted ?? false
7309
+ input.deleted ?? false,
7310
+ input.icon ?? null
6974
7311
  ]
6975
7312
  );
6976
7313
  return r.rows[0];
6977
7314
  }
7315
+ /** Set the icon on a page's current version; false when the page does not exist. */
7316
+ async setDocIcon(slug, icon) {
7317
+ const r = await this.pool.query(
7318
+ `UPDATE doc_page SET icon = $3 WHERE brain_id = $1 AND slug = $2
7319
+ AND version = (SELECT max(version) FROM doc_page WHERE brain_id = $1 AND slug = $2)`,
7320
+ [this.brainId, slug, icon]
7321
+ );
7322
+ return (r.rowCount ?? 0) > 0;
7323
+ }
6978
7324
  /** The current documentation: latest version per slug, tombstones excluded, entry first. */
6979
7325
  async latestDocPages() {
6980
7326
  return this.docPagesAsOf();
@@ -7037,35 +7383,48 @@ var init_repository = __esm({
7037
7383
  * ponytail: tsvector computed per query; add a stored generated column + GIN index when a
7038
7384
  * Brain's doc tree grows past a few hundred pages.
7039
7385
  */
7040
- async docSearch(query, limit = 8) {
7386
+ /**
7387
+ * Docs search over the stored pages as they stood at `at` (default: now), so the time
7388
+ * lens keeps its point in time (docs review R06). A page whose title equals the query
7389
+ * ranks first, then title prefixes and title mentions, then full-text rank: an exact
7390
+ * page never hides behind pages that merely mention it (R12).
7391
+ */
7392
+ async docSearch(query, limit = 8, at) {
7041
7393
  const terms = query.trim().toLowerCase().split(/\s+/).map((t) => t.replace(/[^\p{L}\p{N}]/gu, "")).filter(Boolean).slice(0, 8);
7042
7394
  if (terms.length === 0) return [];
7043
7395
  const tsq = terms.map((t, i) => i === terms.length - 1 ? `${t}:*` : t).join(" & ");
7396
+ const plain = query.trim().toLowerCase().replace(/\s+/g, " ");
7044
7397
  const r = await this.pool.query(
7045
7398
  `WITH latest AS (
7046
7399
  SELECT DISTINCT ON (slug) * FROM doc_page
7047
- WHERE brain_id = $1
7400
+ WHERE brain_id = $1 AND generated_at <= COALESCE($4, now())
7048
7401
  ORDER BY slug, version DESC
7049
7402
  )
7050
7403
  SELECT slug, kind, title,
7051
7404
  ts_headline('english', c.prose, q,
7052
7405
  'StartSel=[[, StopSel=]], MaxWords=18, MinWords=8') AS snippet
7053
7406
  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.
7407
+ -- Snippets read as prose: fenced code, inline backticks, link targets (web,
7408
+ -- repo-relative, and the docs' own page: scheme), and markdown punctuation
7409
+ -- stripped for display only; the search vector keeps the raw markdown so code
7410
+ -- and config names stay findable. (Template literal: regex backslashes are
7411
+ -- doubled, a single one silently vanishes and the pattern never matches.)
7057
7412
  LATERAL (SELECT regexp_replace(regexp_replace(regexp_replace(
7058
7413
  markdown, '\`\`\`[^\`]*\`\`\`', ' ', 'g'),
7059
- ']((https?|brainjar-doku)[^)]*)', ']', 'g'),
7414
+ '\\]\\((https?:|brainjar-doku/|features/|page:)[^)]*\\)', ']', 'g'),
7060
7415
  '[\`#*_|]', '', 'g') AS prose) c
7061
7416
  WHERE NOT deleted
7062
7417
  AND (setweight(to_tsvector('english', title), 'A')
7063
7418
  || 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
7419
+ ORDER BY
7420
+ (lower(title) = $5) DESC,
7421
+ (lower(title) LIKE $5 || '%') DESC,
7422
+ (position($5 in lower(title)) > 0) DESC,
7423
+ ts_rank(
7424
+ setweight(to_tsvector('english', title), 'A')
7425
+ || setweight(to_tsvector('english', markdown), 'B'), q) DESC
7067
7426
  LIMIT $3`,
7068
- [this.brainId, tsq, limit]
7427
+ [this.brainId, tsq, limit, at ?? null, plain]
7069
7428
  );
7070
7429
  return r.rows;
7071
7430
  }
@@ -7239,13 +7598,45 @@ var init_repository = __esm({
7239
7598
  }
7240
7599
  });
7241
7600
 
7601
+ // packages/db/src/ratelimit.ts
7602
+ var init_ratelimit2 = __esm({
7603
+ "packages/db/src/ratelimit.ts"() {
7604
+ "use strict";
7605
+ init_pool();
7606
+ }
7607
+ });
7608
+
7609
+ // packages/db/src/auth-event.ts
7610
+ var init_auth_event = __esm({
7611
+ "packages/db/src/auth-event.ts"() {
7612
+ "use strict";
7613
+ init_pool();
7614
+ }
7615
+ });
7616
+
7617
+ // packages/db/src/email-token.ts
7618
+ var init_email_token = __esm({
7619
+ "packages/db/src/email-token.ts"() {
7620
+ "use strict";
7621
+ init_pool();
7622
+ }
7623
+ });
7624
+
7242
7625
  // packages/db/src/auth.ts
7243
- var DEFAULT_TTL_HOURS;
7626
+ var DEFAULT_TTL_HOURS, LOGIN_FAILURE_WINDOW_MS, SCRYPT, REAUTH_WINDOW_MS, AVATAR_MAX_BYTES;
7244
7627
  var init_auth = __esm({
7245
7628
  "packages/db/src/auth.ts"() {
7246
7629
  "use strict";
7630
+ init_src();
7247
7631
  init_pool();
7632
+ init_ratelimit2();
7633
+ init_auth_event();
7634
+ init_email_token();
7248
7635
  DEFAULT_TTL_HOURS = 24 * 90;
7636
+ LOGIN_FAILURE_WINDOW_MS = 15 * 6e4;
7637
+ SCRYPT = { N: 16384, r: 8, p: 1, maxmem: 64 * 1024 * 1024 };
7638
+ REAUTH_WINDOW_MS = 5 * 6e4;
7639
+ AVATAR_MAX_BYTES = 256 * 1024;
7249
7640
  }
7250
7641
  });
7251
7642
 
@@ -7269,11 +7660,43 @@ var init_oauth = __esm({
7269
7660
  }
7270
7661
  });
7271
7662
 
7663
+ // packages/db/src/totp.ts
7664
+ var CHALLENGE_TTL_MS;
7665
+ var init_totp2 = __esm({
7666
+ "packages/db/src/totp.ts"() {
7667
+ "use strict";
7668
+ init_src();
7669
+ init_pool();
7670
+ CHALLENGE_TTL_MS = 5 * 6e4;
7671
+ }
7672
+ });
7673
+
7674
+ // packages/db/src/passkey.ts
7675
+ var CHALLENGE_TTL_MS2;
7676
+ var init_passkey = __esm({
7677
+ "packages/db/src/passkey.ts"() {
7678
+ "use strict";
7679
+ init_pool();
7680
+ CHALLENGE_TTL_MS2 = 5 * 6e4;
7681
+ }
7682
+ });
7683
+
7684
+ // packages/db/src/identity.ts
7685
+ var STATE_TTL_MS;
7686
+ var init_identity = __esm({
7687
+ "packages/db/src/identity.ts"() {
7688
+ "use strict";
7689
+ init_pool();
7690
+ STATE_TTL_MS = 5 * 6e4;
7691
+ }
7692
+ });
7693
+
7272
7694
  // packages/db/src/team.ts
7273
7695
  var init_team = __esm({
7274
7696
  "packages/db/src/team.ts"() {
7275
7697
  "use strict";
7276
7698
  init_pool();
7699
+ init_auth();
7277
7700
  }
7278
7701
  });
7279
7702
 
@@ -7333,14 +7756,27 @@ var init_settings = __esm({
7333
7756
  APP_SETTING_KEYS = [
7334
7757
  "capture_mode",
7335
7758
  // transcript privacy: full | distilled | off
7336
- "author",
7337
- // developer identity stamped on captured reasoning
7338
- "promotion_threshold",
7339
- // signal/noise gate (0..1)
7340
7759
  "rationale_model",
7341
7760
  // Anthropic model for rationale/consolidation
7342
- "public_url"
7761
+ "public_url",
7343
7762
  // this server's address as clients reach it (wizard command, MCP config)
7763
+ // Outgoing mail (BJ-103); the password is a secret and lives next to the provider keys.
7764
+ "smtp_host",
7765
+ "smtp_port",
7766
+ "smtp_user",
7767
+ "smtp_secure",
7768
+ // "1" = TLS from the start (465), else STARTTLS (587)
7769
+ "mail_from",
7770
+ // Sign in with a provider (BJ-112); the client secrets are secrets, see below.
7771
+ "google_client_id",
7772
+ "github_client_id",
7773
+ // The legal pages the signup checkbox links to (BJ-113).
7774
+ "terms_url",
7775
+ "privacy_url",
7776
+ // Voice chat (voice handoff, 2026-09-21): an explicit "true" or "false"; absent means on.
7777
+ "voice_enabled",
7778
+ // The GPT-Live voice (voice picker, 2026-09-22): an id from VOICE_OPTIONS; absent = default.
7779
+ "voice_name"
7344
7780
  ];
7345
7781
  PROVIDER_KEY_SETTINGS = {
7346
7782
  anthropic: "anthropic_api_key",
@@ -7373,11 +7809,20 @@ var init_tickets = __esm({
7373
7809
  }
7374
7810
  });
7375
7811
 
7812
+ // packages/db/src/jobs.ts
7813
+ var init_jobs = __esm({
7814
+ "packages/db/src/jobs.ts"() {
7815
+ "use strict";
7816
+ init_pool();
7817
+ }
7818
+ });
7819
+
7376
7820
  // packages/db/src/ruleset.ts
7377
7821
  var init_ruleset = __esm({
7378
7822
  "packages/db/src/ruleset.ts"() {
7379
7823
  "use strict";
7380
7824
  init_pool();
7825
+ init_tickets();
7381
7826
  }
7382
7827
  });
7383
7828
 
@@ -7414,12 +7859,19 @@ var init_src2 = __esm({
7414
7859
  init_auth();
7415
7860
  init_api_token();
7416
7861
  init_oauth();
7862
+ init_ratelimit2();
7863
+ init_auth_event();
7864
+ init_email_token();
7865
+ init_totp2();
7866
+ init_passkey();
7867
+ init_identity();
7417
7868
  init_team();
7418
7869
  init_rbac();
7419
7870
  init_settings();
7420
7871
  init_notifications();
7421
7872
  init_conversations();
7422
7873
  init_tickets();
7874
+ init_jobs();
7423
7875
  init_ruleset();
7424
7876
  init_usage();
7425
7877
  init_audit_origin();
@@ -7671,14 +8123,14 @@ var init_values = __esm({
7671
8123
  };
7672
8124
  isArray = (val) => (isArray = Array.isArray, isArray(val));
7673
8125
  isReadonlyArray = isArray;
7674
- validatePositiveInteger = (name, n) => {
7675
- if (typeof n !== "number" || !Number.isInteger(n)) {
8126
+ validatePositiveInteger = (name, n2) => {
8127
+ if (typeof n2 !== "number" || !Number.isInteger(n2)) {
7676
8128
  throw new AnthropicError(`${name} must be an integer`);
7677
8129
  }
7678
- if (n < 0) {
8130
+ if (n2 < 0) {
7679
8131
  throw new AnthropicError(`${name} must be a positive integer`);
7680
8132
  }
7681
- return n;
8133
+ return n2;
7682
8134
  };
7683
8135
  safeJSON = (text) => {
7684
8136
  try {
@@ -7950,9 +8402,9 @@ function concatBytes(buffers) {
7950
8402
  }
7951
8403
  return output;
7952
8404
  }
7953
- function encodeUTF8(str) {
8405
+ function encodeUTF8(str2) {
7954
8406
  let encoder;
7955
- return (encodeUTF8_ ?? (encoder = new globalThis.TextEncoder(), encodeUTF8_ = encoder.encode.bind(encoder)))(str);
8407
+ return (encodeUTF8_ ?? (encoder = new globalThis.TextEncoder(), encodeUTF8_ = encoder.encode.bind(encoder)))(str2);
7956
8408
  }
7957
8409
  function decodeUTF8(bytes) {
7958
8410
  let decoder;
@@ -8171,12 +8623,12 @@ async function* iterSSEChunks(iterator) {
8171
8623
  yield data;
8172
8624
  }
8173
8625
  }
8174
- function partition(str, delimiter) {
8175
- const index = str.indexOf(delimiter);
8626
+ function partition(str2, delimiter) {
8627
+ const index = str2.indexOf(delimiter);
8176
8628
  if (index !== -1) {
8177
- return [str.substring(0, index), delimiter, str.substring(index + delimiter.length)];
8629
+ return [str2.substring(0, index), delimiter, str2.substring(index + delimiter.length)];
8178
8630
  }
8179
- return [str, "", ""];
8631
+ return [str2, "", ""];
8180
8632
  }
8181
8633
  var _Stream_client, Stream, SSEDecoder;
8182
8634
  var init_streaming = __esm({
@@ -8891,8 +9343,8 @@ var init_headers = __esm({
8891
9343
  });
8892
9344
 
8893
9345
  // node_modules/@anthropic-ai/sdk/internal/utils/path.mjs
8894
- function encodeURIPath(str) {
8895
- return str.replace(/[^A-Za-z0-9\-._~!$&'()*+,;=:@]+/g, encodeURIComponent);
9346
+ function encodeURIPath(str2) {
9347
+ return str2.replace(/[^A-Za-z0-9\-._~!$&'()*+,;=:@]+/g, encodeURIComponent);
8896
9348
  }
8897
9349
  var EMPTY, createPathTagFunction, path3;
8898
9350
  var init_path = __esm({
@@ -13716,7 +14168,7 @@ ${line}
13716
14168
  `.split("\n");
13717
14169
  } else {
13718
14170
  const i = lines.findIndex(
13719
- (l, n) => n >= range[0] && n < range[1] && /^\s*url\s*=/.test(l)
14171
+ (l, n2) => n2 >= range[0] && n2 < range[1] && /^\s*url\s*=/.test(l)
13720
14172
  );
13721
14173
  next = [...lines];
13722
14174
  if (i >= 0) next[i] = line;
@@ -13936,17 +14388,23 @@ var init_daily_log = __esm({
13936
14388
  });
13937
14389
 
13938
14390
  // packages/ingest/src/summaries.ts
13939
- var SYNTH_SYSTEM;
14391
+ var REPORT_SYSTEM;
13940
14392
  var init_summaries = __esm({
13941
14393
  "packages/ingest/src/summaries.ts"() {
13942
14394
  "use strict";
13943
14395
  init_src2();
13944
14396
  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.",
14397
+ REPORT_SYSTEM = [
14398
+ "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.",
14399
+ "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).",
14400
+ "Return ONLY a JSON object, no prose around it, no code fence, with this shape:",
14401
+ '{"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]}]}',
14402
+ "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.",
14403
+ "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.",
14404
+ "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.",
14405
+ "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.",
14406
+ "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.",
14407
+ "No file paths, function names, migration numbers or shell commands. Write in English.",
13950
14408
  PROSE_STYLE
13951
14409
  ].join(" ");
13952
14410
  }
@@ -13972,11 +14430,11 @@ function slugify(title) {
13972
14430
  }
13973
14431
  function humans(names) {
13974
14432
  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);
14433
+ for (const n2 of names) {
14434
+ if (!n2 || n2 === "system") continue;
14435
+ count.set(n2, (count.get(n2) ?? 0) + 1);
13978
14436
  }
13979
- return [...count.entries()].sort((a, b) => b[1] - a[1]).map(([n]) => n);
14437
+ return [...count.entries()].sort((a, b) => b[1] - a[1]).map(([n2]) => n2);
13980
14438
  }
13981
14439
  function moduleGroup(modulePath) {
13982
14440
  const segs = modulePath.split("/");
@@ -14027,7 +14485,8 @@ function buildDokuModel(input) {
14027
14485
  changes,
14028
14486
  decisions,
14029
14487
  locations,
14030
- evidenceUris
14488
+ evidenceUris,
14489
+ linkedDecisionIds
14031
14490
  }) => {
14032
14491
  let slug = slugify(feature.title);
14033
14492
  for (let i = 2; usedSlugs.has(slug) || RESERVED_SLUGS.has(slug); i++)
@@ -14066,13 +14525,16 @@ function buildDokuModel(input) {
14066
14525
  owner: explicitOwner ?? contributors[0] ?? null,
14067
14526
  contributors,
14068
14527
  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),
14528
+ decisions: dedupeDecisions(decisions).map(
14529
+ (d) => ({
14530
+ title: d.title,
14531
+ why: d.why,
14532
+ by: d.created_by,
14533
+ date: fmtDate(d.occurred_at),
14534
+ objectId: d.id,
14535
+ ...linkedDecisionIds ? { via: linkedDecisionIds.has(d.id) ? "link" : "location" } : {}
14536
+ })
14537
+ ).sort((a, b) => a.date < b.date ? 1 : -1),
14076
14538
  history,
14077
14539
  updated: history[0].date
14078
14540
  };
@@ -14147,8 +14609,19 @@ function renderEntryPage(model, base = "brainjar-doku/", narrative = null) {
14147
14609
  if (more.length) parts.push("## More", more.join("\n"));
14148
14610
  return parts.join("\n\n") + "\n";
14149
14611
  }
14150
- function renderArchitecturePage(model) {
14612
+ function renderArchitecturePage(model, synthesized = {}) {
14151
14613
  const parts = [`# How it works`, DOKU_NOTICE];
14614
+ if (synthesized.narrative) parts.push(synthesized.narrative);
14615
+ for (const f of synthesized.flows ?? []) {
14616
+ parts.push(
14617
+ [
14618
+ `## ${f.title}`,
14619
+ ...f.summary ? [f.summary] : [],
14620
+ ...f.explanation ? [f.explanation] : [],
14621
+ flowLanesMarkdown(f)
14622
+ ].join("\n\n")
14623
+ );
14624
+ }
14152
14625
  const idx = new Map(model.architecture.groups.map((g, i) => [g.name, i]));
14153
14626
  const lines = ["```mermaid", "flowchart LR"];
14154
14627
  for (const g of model.architecture.groups) {
@@ -14162,7 +14635,7 @@ function renderArchitecturePage(model) {
14162
14635
  );
14163
14636
  }
14164
14637
  lines.push("```");
14165
- parts.push(lines.join("\n"));
14638
+ parts.push("## Module groups", lines.join("\n"));
14166
14639
  parts.push(
14167
14640
  "## Modules by group",
14168
14641
  model.architecture.groups.map(
@@ -14179,6 +14652,234 @@ function firstSentence(s) {
14179
14652
  const cut2 = t.slice(0, 200);
14180
14653
  return `${cut2.slice(0, cut2.lastIndexOf(" "))}\u2026`;
14181
14654
  }
14655
+ function parsePresentation(raw, ctx) {
14656
+ const bad = (error) => ({ presentation: null, error, dropped: [] });
14657
+ const o = raw;
14658
+ if (!o || typeof o !== "object") return bad("not an object");
14659
+ if (!str(o.summary, LIMIT.summary))
14660
+ return bad("summary: missing or too long");
14661
+ if (!Array.isArray(o.blocks) || o.blocks.length === 0)
14662
+ return bad("blocks: need a non-empty list");
14663
+ if (o.blocks.length > MAX_BLOCKS)
14664
+ return bad(`blocks: more than ${MAX_BLOCKS}`);
14665
+ const blocks = [];
14666
+ const dropped = [];
14667
+ const flowsPlaced = /* @__PURE__ */ new Set();
14668
+ for (const [i, cand] of o.blocks.entries()) {
14669
+ const b = cand;
14670
+ const at = `block ${i + 1}`;
14671
+ if (!b || typeof b !== "object") return bad(`${at}: not an object`);
14672
+ const title = b.title;
14673
+ switch (b.kind) {
14674
+ case "text": {
14675
+ if (title !== void 0 && !str(title, LIMIT.title))
14676
+ return bad(`${at}: text title too long`);
14677
+ if (!str(b.markdown, LIMIT.markdown))
14678
+ return bad(`${at}: text markdown missing or too long`);
14679
+ blocks.push({
14680
+ kind: "text",
14681
+ ...title ? { title: title.trim() } : {},
14682
+ markdown: b.markdown.trim()
14683
+ });
14684
+ break;
14685
+ }
14686
+ case "steps": {
14687
+ if (!str(title, LIMIT.title)) return bad(`${at}: steps need a title`);
14688
+ const items = b.items;
14689
+ if (!Array.isArray(items) || items.length === 0 || items.length > LIMIT.stepItems)
14690
+ return bad(`${at}: steps need 1-${LIMIT.stepItems} items`);
14691
+ const out = [];
14692
+ for (const it of items) {
14693
+ const s = it;
14694
+ if (!str(s?.title, LIMIT.title) || !str(s?.body, LIMIT.stepBody, 0))
14695
+ return bad(`${at}: step needs title and body`);
14696
+ out.push({ title: s.title.trim(), body: s.body.trim() });
14697
+ }
14698
+ blocks.push({ kind: "steps", title: title.trim(), items: out });
14699
+ break;
14700
+ }
14701
+ case "table": {
14702
+ if (!str(title, LIMIT.title)) return bad(`${at}: table needs a title`);
14703
+ const columns = b.columns;
14704
+ if (!Array.isArray(columns) || columns.length < 2 || columns.length > LIMIT.columns || !columns.every((c) => str(c, 80)))
14705
+ return bad(`${at}: table needs 2-${LIMIT.columns} column names`);
14706
+ const rows = b.rows;
14707
+ if (!Array.isArray(rows) || rows.length === 0 || rows.length > LIMIT.rows)
14708
+ return bad(`${at}: table needs 1-${LIMIT.rows} rows`);
14709
+ const outRows = [];
14710
+ for (const r of rows) {
14711
+ if (!Array.isArray(r) || r.length !== columns.length || !r.every((c) => typeof c === "string" && c.length <= LIMIT.cell))
14712
+ return bad(
14713
+ `${at}: every row needs exactly ${columns.length} cells`
14714
+ );
14715
+ outRows.push(r.map((c) => c.trim()));
14716
+ }
14717
+ blocks.push({
14718
+ kind: "table",
14719
+ title: title.trim(),
14720
+ columns: columns.map((c) => c.trim()),
14721
+ rows: outRows
14722
+ });
14723
+ break;
14724
+ }
14725
+ case "endpoint": {
14726
+ const method = typeof b.method === "string" ? b.method.trim().toUpperCase() : "";
14727
+ if (!HTTP_METHODS.has(method)) return bad(`${at}: unknown HTTP method`);
14728
+ if (!str(b.path, LIMIT.endpointPath) || !/^\/\S*$/.test(b.path.trim()))
14729
+ return bad(`${at}: endpoint path must start with /`);
14730
+ if (!str(b.summary, LIMIT.endpointSummary))
14731
+ return bad(`${at}: endpoint needs a summary`);
14732
+ if (b.auth !== void 0 && !str(b.auth, LIMIT.auth))
14733
+ return bad(`${at}: endpoint auth too long`);
14734
+ const path20 = b.path.trim();
14735
+ if (!ctx.factSheet.includes(path20)) {
14736
+ dropped.push(`endpoint ${method} ${path20} (not in the fact sheet)`);
14737
+ break;
14738
+ }
14739
+ blocks.push({
14740
+ kind: "endpoint",
14741
+ method,
14742
+ path: path20,
14743
+ summary: b.summary.trim(),
14744
+ ...b.auth ? { auth: b.auth.trim() } : {}
14745
+ });
14746
+ break;
14747
+ }
14748
+ case "code": {
14749
+ if (!str(title, LIMIT.title)) return bad(`${at}: code needs a title`);
14750
+ if (typeof b.language !== "string" || !/^[a-z0-9+#.-]{1,20}$/i.test(b.language.trim()))
14751
+ return bad(`${at}: code needs a language tag`);
14752
+ if (!str(b.code, LIMIT.code))
14753
+ return bad(`${at}: code missing or too long`);
14754
+ blocks.push({
14755
+ kind: "code",
14756
+ title: title.trim(),
14757
+ language: b.language.trim().toLowerCase(),
14758
+ code: b.code.replace(/\s+$/, ""),
14759
+ ...b.illustrative === true ? { illustrative: true } : {}
14760
+ });
14761
+ break;
14762
+ }
14763
+ case "callout": {
14764
+ if (b.tone !== "note" && b.tone !== "warning")
14765
+ return bad(`${at}: callout tone must be note or warning`);
14766
+ if (!str(b.text, LIMIT.callout)) return bad(`${at}: callout text`);
14767
+ blocks.push({ kind: "callout", tone: b.tone, text: b.text.trim() });
14768
+ break;
14769
+ }
14770
+ case "flow": {
14771
+ if (!str(title, LIMIT.title)) return bad(`${at}: flow needs a title`);
14772
+ if (b.source !== "flowDiagram" && b.source !== "flow")
14773
+ return bad(`${at}: flow source must be flowDiagram or flow`);
14774
+ const exists2 = b.source === "flowDiagram" ? ctx.hasFlowDiagram : ctx.hasFlow;
14775
+ if (!exists2 || flowsPlaced.has(b.source)) {
14776
+ dropped.push(
14777
+ `flow ${b.source} (${exists2 ? "placed twice" : "no such diagram"})`
14778
+ );
14779
+ break;
14780
+ }
14781
+ flowsPlaced.add(b.source);
14782
+ blocks.push({ kind: "flow", title: title.trim(), source: b.source });
14783
+ break;
14784
+ }
14785
+ default:
14786
+ return bad(`${at}: unknown kind ${String(b.kind)}`);
14787
+ }
14788
+ }
14789
+ if (blocks.length === 0) return bad("blocks: nothing left after validation");
14790
+ return {
14791
+ presentation: { version: 1, summary: o.summary.trim(), blocks },
14792
+ error: null,
14793
+ dropped
14794
+ };
14795
+ }
14796
+ function flowLanesMarkdown(d) {
14797
+ return d.lanes.map((l) => {
14798
+ const nodes = d.nodes.filter((n2) => n2.lane === l.key).map((n2) => n2.title).join(", ");
14799
+ return `- **${l.title}**: ${nodes || "(no stations)"}`;
14800
+ }).join("\n");
14801
+ }
14802
+ function renderBlocksMarkdown(p, diagrams = {}) {
14803
+ const parts = [];
14804
+ for (const b of p.blocks) {
14805
+ switch (b.kind) {
14806
+ case "text":
14807
+ parts.push([h2(b.title), b.markdown].filter(Boolean).join("\n\n"));
14808
+ break;
14809
+ case "steps":
14810
+ parts.push(
14811
+ [
14812
+ h2(b.title),
14813
+ b.items.map(
14814
+ (s, i) => `${i + 1}. **${s.title}**${s.body ? ` ${s.body}` : ""}`
14815
+ ).join("\n")
14816
+ ].join("\n\n")
14817
+ );
14818
+ break;
14819
+ case "table":
14820
+ parts.push(
14821
+ [
14822
+ h2(b.title),
14823
+ [
14824
+ `| ${b.columns.map(mdCell).join(" | ")} |`,
14825
+ `|${b.columns.map(() => "---").join("|")}|`,
14826
+ ...b.rows.map((r) => `| ${r.map(mdCell).join(" | ")} |`)
14827
+ ].join("\n")
14828
+ ].join("\n\n")
14829
+ );
14830
+ break;
14831
+ case "endpoint":
14832
+ parts.push(
14833
+ [
14834
+ `### \`${b.method} ${b.path}\``,
14835
+ b.summary,
14836
+ ...b.auth ? [`Authentication: ${b.auth}`] : []
14837
+ ].join("\n\n")
14838
+ );
14839
+ break;
14840
+ case "code":
14841
+ parts.push(
14842
+ [
14843
+ `**${b.title}**${b.illustrative ? " (illustrative example, replace the placeholders)" : ""}`,
14844
+ `\`\`\`${b.language}
14845
+ ${b.code}
14846
+ \`\`\``
14847
+ ].join("\n\n")
14848
+ );
14849
+ break;
14850
+ case "callout":
14851
+ parts.push(
14852
+ `> **${b.tone === "warning" ? "Warning" : "Note"}:** ${b.text.replace(/\n/g, "\n> ")}`
14853
+ );
14854
+ break;
14855
+ case "flow": {
14856
+ const lines = [h2(b.title)];
14857
+ if (b.source === "flowDiagram" && diagrams.flowDiagram) {
14858
+ const d = diagrams.flowDiagram;
14859
+ lines.push(
14860
+ `_Runtime flow diagram: ${d.title}. Interactive on the web page; its lanes and stations:_`
14861
+ );
14862
+ lines.push(flowLanesMarkdown(d));
14863
+ } else if (b.source === "flow" && diagrams.flow) {
14864
+ lines.push(
14865
+ diagrams.flow.steps.map((s, i) => `${i + 1}. ${s}`).join("\n")
14866
+ );
14867
+ }
14868
+ parts.push(lines.join("\n\n"));
14869
+ break;
14870
+ }
14871
+ }
14872
+ }
14873
+ return parts.join("\n\n");
14874
+ }
14875
+ function pageBody(f) {
14876
+ if (f.presentation)
14877
+ return [
14878
+ f.presentation.summary,
14879
+ renderBlocksMarkdown(f.presentation, f)
14880
+ ].join("\n\n");
14881
+ return f.body ?? null;
14882
+ }
14182
14883
  function renderFeaturePage(f) {
14183
14884
  const meta = [
14184
14885
  "| | |",
@@ -14189,8 +14890,9 @@ function renderFeaturePage(f) {
14189
14890
  ...f.locations.length ? [`| Code | ${f.locations.map((l) => `\`${l}\``).join(", ")} |`] : []
14190
14891
  ];
14191
14892
  const parts = [`# ${f.docTitle ?? f.title}`, meta.join("\n")];
14192
- if (f.body) {
14193
- parts.push(f.body);
14893
+ const body = pageBody(f);
14894
+ if (body) {
14895
+ parts.push(body);
14194
14896
  } else {
14195
14897
  if (f.narrative) parts.push(f.narrative);
14196
14898
  parts.push("## What it does", f.what);
@@ -14236,7 +14938,7 @@ function renderCategoryPage(model) {
14236
14938
  function renderChangelogPage(model) {
14237
14939
  const parts = [`# Changelog`, DOKU_NOTICE];
14238
14940
  for (const day of model.changelog) {
14239
- const counts = Object.entries(day.counts).map(([t, n]) => `${n} ${t}${n === 1 ? "" : "s"}`).join(" \xB7 ");
14941
+ const counts = Object.entries(day.counts).map(([t, n2]) => `${n2} ${t}${n2 === 1 ? "" : "s"}`).join(" \xB7 ");
14240
14942
  const who = day.contributors.length ? ` \xB7 by ${day.contributors.join(", ")}` : "";
14241
14943
  parts.push(
14242
14944
  `## ${day.date}`,
@@ -14333,7 +15035,7 @@ function renderDokuPages(model) {
14333
15035
  }
14334
15036
  return pages;
14335
15037
  }
14336
- var normTitle, DOC_SECTIONS, fmtDate, MAX_GROUPS, MAX_HIGHLIGHTS, HIGHLIGHT_TYPES, RESERVED_SLUGS, DOKU_NOTICE, mermaidId, mermaidLabel, mdCell, asRecord;
15038
+ 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
15039
  var init_doku_model = __esm({
14338
15040
  "packages/ingest/src/doku-model.ts"() {
14339
15041
  "use strict";
@@ -14360,6 +15062,33 @@ var init_doku_model = __esm({
14360
15062
  mermaidId = (name, i) => `g${i}`;
14361
15063
  mermaidLabel = (s) => s.replace(/"/g, "'");
14362
15064
  mdCell = (s) => (s ?? "").replace(/\|/g, "\\|").replace(/\s+/g, " ").trim();
15065
+ HTTP_METHODS = /* @__PURE__ */ new Set([
15066
+ "GET",
15067
+ "POST",
15068
+ "PUT",
15069
+ "PATCH",
15070
+ "DELETE",
15071
+ "HEAD",
15072
+ "OPTIONS"
15073
+ ]);
15074
+ MAX_BLOCKS = 40;
15075
+ LIMIT = {
15076
+ title: 120,
15077
+ summary: 300,
15078
+ markdown: 2e4,
15079
+ stepItems: 20,
15080
+ stepBody: 1e3,
15081
+ columns: 6,
15082
+ rows: 40,
15083
+ cell: 400,
15084
+ endpointPath: 200,
15085
+ endpointSummary: 400,
15086
+ auth: 200,
15087
+ code: 6e3,
15088
+ callout: 600
15089
+ };
15090
+ str = (v, max, min = 1) => typeof v === "string" && v.trim().length >= min && v.length <= max;
15091
+ h2 = (t) => t ? `## ${t.trim()}` : null;
14363
15092
  asRecord = (v) => v;
14364
15093
  }
14365
15094
  });
@@ -14397,10 +15126,10 @@ async function loadDokuData(brainId, root) {
14397
15126
  relations: ["changes", "affects", "mentions"]
14398
15127
  });
14399
15128
  const changes = neighbors.filter(
14400
- (n) => n.id !== feature.id && n.type === "event"
15129
+ (n2) => n2.id !== feature.id && n2.type === "event"
14401
15130
  );
14402
15131
  const linkedDecisions = neighbors.filter(
14403
- (n) => n.id !== feature.id && n.type === "decision"
15132
+ (n2) => n2.id !== feature.id && n2.type === "decision"
14404
15133
  );
14405
15134
  const linkedModules = neighbors.filter(isModule).map(moduleName);
14406
15135
  const locations = [
@@ -14419,7 +15148,14 @@ async function loadDokuData(brainId, root) {
14419
15148
  feature.id,
14420
15149
  ...changes.map((c) => c.id)
14421
15150
  ]);
14422
- return { feature, changes, decisions, locations, evidenceUris };
15151
+ return {
15152
+ feature,
15153
+ changes,
15154
+ decisions,
15155
+ locations,
15156
+ evidenceUris,
15157
+ linkedDecisionIds: new Set(linkedDecisions.map((d) => d.id))
15158
+ };
14423
15159
  })
14424
15160
  );
14425
15161
  const days = (await Promise.all(
@@ -14457,20 +15193,50 @@ async function loadDokuData(brainId, root) {
14457
15193
  scripts
14458
15194
  };
14459
15195
  }
14460
- function parseDocPage(raw, categoryKeys) {
15196
+ function aiState(m) {
15197
+ return {
15198
+ factHash: typeof m?.factHash === "string" ? m.factHash : "",
15199
+ aiIncomplete: m?.aiIncomplete === true,
15200
+ aiAttempts: typeof m?.aiAttempts === "number" ? m.aiAttempts : 0
15201
+ };
15202
+ }
15203
+ function aiComplete(kind, m) {
15204
+ if (!m) return false;
15205
+ const text = (v) => typeof v === "string" && v.trim().length > 0;
15206
+ switch (kind) {
15207
+ case "entry":
15208
+ case "architecture":
15209
+ return text(m.narrative);
15210
+ case "feature":
15211
+ return (Boolean(m.presentation) || text(m.body)) && text(m.category);
15212
+ case "category":
15213
+ return text(m.body);
15214
+ default:
15215
+ return true;
15216
+ }
15217
+ }
15218
+ function parseDocPage(raw, categoryKeys, ctx) {
15219
+ const bad = (error2) => ({ page: null, error: error2, dropped: [] });
14461
15220
  const match = raw.match(/\{[\s\S]*\}/);
14462
- if (!match) return null;
15221
+ if (!match) return bad("no JSON");
15222
+ let obj;
14463
15223
  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 };
15224
+ obj = JSON.parse(match[0]);
14471
15225
  } catch {
14472
- return null;
14473
- }
15226
+ return bad("invalid JSON (truncated reply?)");
15227
+ }
15228
+ const title = typeof obj.title === "string" ? obj.title.trim() : "";
15229
+ const category = typeof obj.category === "string" ? obj.category.trim() : "";
15230
+ if (!title || title.length > 120) return bad("title: missing or too long");
15231
+ if (!categoryKeys.includes(category)) return bad("category: unknown key");
15232
+ const icon = typeof obj.icon === "string" && isEmoji(obj.icon.trim()) ? obj.icon.trim() : null;
15233
+ const { presentation, error, dropped } = parsePresentation(obj, ctx);
15234
+ if (!presentation) return bad(error ?? "presentation invalid");
15235
+ return {
15236
+ page: { title, category, presentation, icon },
15237
+ error: null,
15238
+ dropped
15239
+ };
14474
15240
  }
14475
15241
  function parseTaxonomy(raw) {
14476
15242
  const match = raw.match(/\{[\s\S]*\}/);
@@ -14485,25 +15251,53 @@ function parseTaxonomy(raw) {
14485
15251
  const name = typeof r.name === "string" ? r.name.trim() : "";
14486
15252
  const description = typeof r.description === "string" ? r.description.trim() : "";
14487
15253
  if (!/^[a-z0-9-]{2,40}$/.test(key) || !name || !description) return null;
15254
+ if (name.length > 80 || description.length > 240) return null;
14488
15255
  out.push({ key, name, description });
14489
15256
  }
14490
- if (out.length < 3 || out.length > 8) return null;
15257
+ if (out.length < 1 || out.length > 40) return null;
14491
15258
  if (new Set(out.map((c) => c.key)).size !== out.length) return null;
14492
15259
  return out;
14493
15260
  } catch {
14494
15261
  return null;
14495
15262
  }
14496
15263
  }
15264
+ function parseRefile(raw, slugs, keys) {
15265
+ let pages = null;
15266
+ const match = raw.match(/\{[\s\S]*\}/);
15267
+ if (match) {
15268
+ try {
15269
+ const obj = JSON.parse(match[0]);
15270
+ if (typeof obj.pages === "object" && obj.pages !== null && !Array.isArray(obj.pages))
15271
+ pages = obj.pages;
15272
+ } catch {
15273
+ }
15274
+ }
15275
+ if (!pages) {
15276
+ pages = {};
15277
+ for (const m of raw.matchAll(/"([^"\n]+)"\s*:\s*"([^"\n]*)"/g))
15278
+ pages[m[1]] = m[2];
15279
+ }
15280
+ const known = new Set(keys);
15281
+ const out = {};
15282
+ for (const slug of slugs) {
15283
+ const key = pages[slug];
15284
+ if (typeof key === "string" && known.has(key.trim()))
15285
+ out[slug] = key.trim();
15286
+ }
15287
+ return Object.keys(out).length ? out : null;
15288
+ }
14497
15289
  function parseCategoryBody(raw, memberSlugs) {
14498
15290
  let body = raw.trim();
14499
- if (!body || body.length > 9e3) return null;
15291
+ if (!body || body.length > 2e4) return null;
14500
15292
  const known = new Set(memberSlugs);
14501
15293
  body = body.replace(
14502
15294
  /\[([^\]]+)\]\(page:([^)\s]+)\)/g,
14503
15295
  (whole, text, slug) => known.has(slug) ? whole : text
14504
15296
  );
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;
15297
+ body = body.replace(
15298
+ /\(page:([^)\s]+)\)/g,
15299
+ (whole, slug) => known.has(slug) ? whole : ""
15300
+ );
14507
15301
  return body;
14508
15302
  }
14509
15303
  function archNotesInput(model) {
@@ -14597,13 +15391,19 @@ function parseArchFlows(raw, knownModules) {
14597
15391
  const errors = [];
14598
15392
  for (const cand of obj.flows.slice(0, 4)) {
14599
15393
  const f = cand;
15394
+ if (f && typeof f === "object") {
15395
+ const x = f.explanation;
15396
+ if (typeof x === "string" && x.trim() && x.length <= 4e3)
15397
+ f.explanation = x.trim();
15398
+ else delete f.explanation;
15399
+ }
14600
15400
  if (Array.isArray(f?.nodes)) {
14601
- for (const n of f.nodes) {
14602
- if (Array.isArray(n?.evidence)) {
14603
- n.evidence = n.evidence.filter(
15401
+ for (const n2 of f.nodes) {
15402
+ if (Array.isArray(n2?.evidence)) {
15403
+ n2.evidence = n2.evidence.filter(
14604
15404
  (p) => typeof p === "string" && knownModules.has(p)
14605
15405
  );
14606
- if (n.evidence.length === 0) delete n.evidence;
15406
+ if (n2.evidence.length === 0) delete n2.evidence;
14607
15407
  }
14608
15408
  }
14609
15409
  }
@@ -14627,12 +15427,12 @@ function parseFeatureFlow(raw, slug, knownPaths) {
14627
15427
  const f = obj.flow;
14628
15428
  f.slug = `${slug.slice(0, 59)}-flow`;
14629
15429
  if (Array.isArray(f?.nodes)) {
14630
- for (const n of f.nodes) {
14631
- if (Array.isArray(n?.evidence)) {
14632
- n.evidence = n.evidence.filter(
15430
+ for (const n2 of f.nodes) {
15431
+ if (Array.isArray(n2?.evidence)) {
15432
+ n2.evidence = n2.evidence.filter(
14633
15433
  (p) => typeof p === "string" && knownPaths.has(p)
14634
15434
  );
14635
- if (n.evidence.length === 0) delete n.evidence;
15435
+ if (n2.evidence.length === 0) delete n2.evidence;
14636
15436
  }
14637
15437
  }
14638
15438
  }
@@ -14657,7 +15457,7 @@ function parseFlow(raw, configKeys) {
14657
15457
  const edges = obj.edges;
14658
15458
  if (!Array.isArray(edges) || edges.length === 0 || !edges.every(
14659
15459
  (e) => Array.isArray(e) && e.length === 2 && e.every(
14660
- (n) => Number.isInteger(n) && n >= 0 && n < steps.length
15460
+ (n2) => Number.isInteger(n2) && n2 >= 0 && n2 < steps.length
14661
15461
  ) && e[0] !== e[1]
14662
15462
  ))
14663
15463
  return bad("edges: need valid [from,to] index pairs");
@@ -14705,9 +15505,16 @@ async function generateDokuInner(brainId, opts = {}) {
14705
15505
  const repo = brainRepo(brainId);
14706
15506
  const meta = new Map((await repo.docPageMeta()).map((m) => [m.slug, m]));
14707
15507
  const storedBySlug = /* @__PURE__ */ new Map();
15508
+ const storedRows = /* @__PURE__ */ new Map();
14708
15509
  try {
14709
- for (const p of await repo.latestDocPages())
15510
+ for (const p of await repo.latestDocPages()) {
14710
15511
  storedBySlug.set(p.slug, p.model);
15512
+ storedRows.set(p.slug, {
15513
+ title: p.title,
15514
+ model: p.model,
15515
+ markdown: p.markdown
15516
+ });
15517
+ }
14711
15518
  } catch {
14712
15519
  }
14713
15520
  const takenTitle = /* @__PURE__ */ new Map();
@@ -14726,10 +15533,11 @@ async function generateDokuInner(brainId, opts = {}) {
14726
15533
  const stored = storedBySlug.get("index")?.categories;
14727
15534
  if (Array.isArray(stored) && stored.length) categories = stored;
14728
15535
  }
15536
+ const storedTaxonomy = JSON.stringify(categories);
14729
15537
  const hasNewFeature = pages.some(
14730
15538
  (p) => p.kind === "feature" && !meta.get(p.slug)
14731
15539
  );
14732
- if (opts.llm && (categories.length === 0 || hasNewFeature)) {
15540
+ if (opts.llm && (categories.length === 0 || hasNewFeature || opts.retaxonomy)) {
14733
15541
  try {
14734
15542
  const next = parseTaxonomy(
14735
15543
  await opts.llm.complete({
@@ -14743,7 +15551,9 @@ async function generateDokuInner(brainId, opts = {}) {
14743
15551
  (f) => `- ${f.title}: ${firstSentence(f.what)}`
14744
15552
  )
14745
15553
  ].join("\n"),
14746
- maxTokens: 900
15554
+ // Room for a large product's taxonomy; an invalid or clipped reply keeps the
15555
+ // stored one (parseTaxonomy returns null).
15556
+ maxTokens: 3e3
14747
15557
  })
14748
15558
  );
14749
15559
  if (next) categories = next;
@@ -14751,42 +15561,82 @@ async function generateDokuInner(brainId, opts = {}) {
14751
15561
  }
14752
15562
  }
14753
15563
  const categoryKeys = categories.map((c) => c.key);
15564
+ const taxonomyChanged = JSON.stringify(categories) !== storedTaxonomy;
15565
+ const endpointFacts = [];
15566
+ for (const [slug, m] of storedBySlug) {
15567
+ const p = m.presentation;
15568
+ if (!Array.isArray(p?.blocks)) continue;
15569
+ for (const b of p.blocks)
15570
+ if (b.kind === "endpoint")
15571
+ endpointFacts.push({
15572
+ slug,
15573
+ title: typeof m.docTitle === "string" ? m.docTitle : slug,
15574
+ method: b.method,
15575
+ path: b.path,
15576
+ summary: b.summary,
15577
+ ...b.auth ? { auth: b.auth } : {}
15578
+ });
15579
+ }
14754
15580
  let written = 0;
14755
15581
  let polished = false;
14756
15582
  const seen = /* @__PURE__ */ new Set();
14757
15583
  const featureInfo = [];
15584
+ const save = async (input) => {
15585
+ await repo.saveDocPage(input);
15586
+ storedRows.set(input.slug, {
15587
+ title: input.title,
15588
+ model: input.model,
15589
+ markdown: input.markdown
15590
+ });
15591
+ storedBySlug.set(input.slug, input.model);
15592
+ written++;
15593
+ };
14758
15594
  for (const page of pages) {
14759
15595
  seen.add(page.slug);
14760
15596
  const rawStructure = page.kind === "architecture" ? (() => {
14761
15597
  const raw = page.model.raw;
14762
15598
  return raw ? sha1(JSON.stringify([raw.modules, raw.edges])) : "";
14763
15599
  })() : "";
15600
+ const factHash = sha1(page.markdown);
14764
15601
  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"
15602
+ page.markdown + (page.kind === "entry" ? "v4" : page.kind === "feature" ? (
15603
+ // f7: the block presentation model (docs redesign) regenerates every
15604
+ // feature page once; run with `only` first for a representative sample.
15605
+ "f7"
14768
15606
  ) : page.kind === "architecture" ? (
14769
- // a5: more flows, tighter labels — existing brains regenerate the page once.
14770
- "a5" + rawStructure
15607
+ // a6: flow explanations + full markdown (docs hubs) regenerate the page once.
15608
+ "a6" + rawStructure
14771
15609
  ) : "")
14772
15610
  );
14773
15611
  const prev = meta.get(page.slug);
14774
- if (prev && !prev.deleted && prev.source_hash === sourceHash && !storedCollisions.has(page.slug)) {
15612
+ const stored = storedBySlug.get(page.slug);
15613
+ const state = aiState(stored);
15614
+ const required = Boolean(opts.llm && page.polishable);
15615
+ const outsideSample = Boolean(opts.only && !opts.only.includes(page.slug));
15616
+ const unchanged = Boolean(
15617
+ prev && !prev.deleted && prev.source_hash === sourceHash
15618
+ );
15619
+ const retry = required && unchanged && (state.aiIncomplete || !aiComplete(page.kind, stored)) && (state.aiAttempts < MAX_AI_ATTEMPTS || Boolean(opts.forceRetry));
15620
+ const entryTaxonomy = page.kind === "entry" && taxonomyChanged;
15621
+ if (prev && !prev.deleted && (outsideSample || unchanged && !storedCollisions.has(page.slug) && !retry && !entryTaxonomy)) {
14775
15622
  if (page.kind === "feature") {
14776
- const stored = storedBySlug.get(page.slug);
15623
+ const s = stored;
14777
15624
  const f = page.model;
14778
15625
  featureInfo.push({
14779
15626
  slug: page.slug,
14780
- title: typeof stored?.docTitle === "string" ? stored.docTitle : page.title,
15627
+ title: typeof s?.docTitle === "string" ? s.docTitle : page.title,
14781
15628
  what: f.what,
14782
15629
  updated: f.updated,
14783
- category: typeof stored?.category === "string" ? stored.category : null
15630
+ category: typeof s?.category === "string" ? s.category : null
14784
15631
  });
14785
15632
  }
14786
15633
  continue;
14787
15634
  }
15635
+ const sameFacts = stored !== void 0 && state.factHash === factHash;
14788
15636
  let markdown = page.markdown;
14789
15637
  let model = page.model;
15638
+ let icon = null;
15639
+ let ok2 = !required;
14790
15640
  if (opts.llm && page.polishable && page.kind === "architecture") {
14791
15641
  try {
14792
15642
  const { notes } = parseArchNotes(
@@ -14800,6 +15650,7 @@ async function generateDokuInner(brainId, opts = {}) {
14800
15650
  if (notes) {
14801
15651
  model = { ...model, narrative: notes.narrative, roles: notes.roles };
14802
15652
  polished = true;
15653
+ ok2 = true;
14803
15654
  }
14804
15655
  } catch {
14805
15656
  }
@@ -14818,61 +15669,47 @@ async function generateDokuInner(brainId, opts = {}) {
14818
15669
  }
14819
15670
  } catch {
14820
15671
  }
15672
+ if (!ok2 && sameFacts) {
15673
+ const s = stored;
15674
+ const kept = model;
15675
+ model = {
15676
+ ...model,
15677
+ ...typeof s.narrative === "string" ? { narrative: s.narrative, roles: s.roles } : {},
15678
+ ...!kept.flows && Array.isArray(s.flows) ? { flows: s.flows } : {}
15679
+ };
15680
+ }
15681
+ const arch = model;
15682
+ if (arch.narrative || arch.flows?.length)
15683
+ markdown = renderArchitecturePage(doku, {
15684
+ narrative: arch.narrative ?? null,
15685
+ flows: arch.flows ?? []
15686
+ });
14821
15687
  } else if (page.kind === "entry") {
14822
15688
  model = { ...page.model, categories };
14823
15689
  if (opts.llm && page.polishable) {
14824
15690
  try {
14825
- const narrative = (await opts.llm.complete({
15691
+ const narrative2 = (await opts.llm.complete({
14826
15692
  system: ENTRY_NARRATIVE_SYSTEM,
14827
15693
  prompt: page.markdown,
14828
- maxTokens: 1200
15694
+ maxTokens: 2500
14829
15695
  })).trim();
14830
- if (narrative) {
14831
- model = { ...model, narrative };
14832
- markdown = renderEntryPage(doku, "brainjar-doku/", narrative);
15696
+ if (narrative2) {
15697
+ model = { ...model, narrative: narrative2 };
14833
15698
  polished = true;
15699
+ ok2 = true;
14834
15700
  }
14835
15701
  } catch {
14836
15702
  }
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;
15703
+ if (!ok2 && sameFacts) {
15704
+ const s = stored;
15705
+ if (typeof s.narrative === "string" && s.narrative.trim())
15706
+ model = { ...model, narrative: s.narrative };
14873
15707
  }
14874
- } catch {
14875
15708
  }
15709
+ const narrative = model.narrative;
15710
+ if (narrative)
15711
+ markdown = renderEntryPage(doku, "brainjar-doku/", narrative);
15712
+ } else if (opts.llm && page.polishable) {
14876
15713
  try {
14877
15714
  const { flow } = parseFlow(
14878
15715
  await opts.llm.complete({
@@ -14912,7 +15749,123 @@ async function generateDokuInner(brainId, opts = {}) {
14912
15749
  console.warn(`doku: feature flow failed (${page.slug}):`, e);
14913
15750
  }
14914
15751
  }
15752
+ try {
15753
+ const diagrams = model;
15754
+ const shared = endpointFacts.filter(
15755
+ (e) => e.slug !== page.slug && page.markdown.includes(e.path)
15756
+ );
15757
+ const sharedLines = shared.map(
15758
+ (e) => `- ${e.method} ${e.path}: ${e.summary}${e.auth ? ` Authentication: ${e.auth}` : ""} (page: ${e.slug}, "${e.title}")`
15759
+ );
15760
+ const ctx = {
15761
+ factSheet: [page.markdown, ...sharedLines].join("\n"),
15762
+ hasFlowDiagram: Boolean(diagrams.flowDiagram),
15763
+ hasFlow: Boolean(diagrams.flow)
15764
+ };
15765
+ const prompt = [
15766
+ "Categories (pick exactly one key):",
15767
+ ...categories.length ? categories.map((c) => `- ${c.key}: ${c.name} (${c.description})`) : ["- general: General (everything)"],
15768
+ "",
15769
+ "Available diagrams (the only allowed flow block sources):",
15770
+ ...ctx.hasFlowDiagram ? [
15771
+ `- flowDiagram: "${diagrams.flowDiagram.title}" (the feature's runtime flow, lanes and stations)`
15772
+ ] : [],
15773
+ ...ctx.hasFlow && !ctx.hasFlowDiagram ? ["- flow: the feature's runtime steps as a simple chain"] : [],
15774
+ ...!ctx.hasFlowDiagram && !ctx.hasFlow ? ["- (none)"] : [],
15775
+ "",
15776
+ ...sharedLines.length ? [
15777
+ "Documented elsewhere for the same endpoint (proven facts; reuse them, link the page):",
15778
+ ...sharedLines,
15779
+ ""
15780
+ ] : [],
15781
+ "Fact sheet:",
15782
+ page.markdown
15783
+ ].join("\n");
15784
+ const keys = categoryKeys.length ? categoryKeys : ["general"];
15785
+ let synth = parseDocPage(
15786
+ await opts.llm.complete({
15787
+ system: PAGE_SYSTEM,
15788
+ prompt,
15789
+ maxTokens: 7e3
15790
+ }),
15791
+ keys,
15792
+ ctx
15793
+ );
15794
+ if (!synth.page) {
15795
+ console.warn(
15796
+ `doku: page rejected (${page.slug}): ${synth.error}; retrying once`
15797
+ );
15798
+ synth = parseDocPage(
15799
+ await opts.llm.complete({
15800
+ system: PAGE_SYSTEM,
15801
+ prompt,
15802
+ maxTokens: 1e4
15803
+ }),
15804
+ keys,
15805
+ ctx
15806
+ );
15807
+ }
15808
+ if (synth.dropped.length)
15809
+ console.warn(
15810
+ `doku: blocks dropped (${page.slug}): ${synth.dropped.join("; ")}`
15811
+ );
15812
+ if (synth.page) {
15813
+ const title = pickDocTitle(
15814
+ synth.page.title,
15815
+ page.title,
15816
+ page.slug,
15817
+ takenTitle
15818
+ );
15819
+ const f = {
15820
+ ...model,
15821
+ presentation: synth.page.presentation,
15822
+ docTitle: title,
15823
+ category: synth.page.category
15824
+ };
15825
+ f.body = pageBody(f);
15826
+ model = f;
15827
+ markdown = renderFeaturePage(f);
15828
+ page.title = title;
15829
+ icon = synth.page.icon;
15830
+ polished = true;
15831
+ ok2 = true;
15832
+ } else {
15833
+ console.warn(
15834
+ `doku: page rejected twice (${page.slug}): ${synth.error}`
15835
+ );
15836
+ }
15837
+ } catch (e) {
15838
+ console.warn(`doku: page synthesis failed (${page.slug}):`, e);
15839
+ }
15840
+ if (!ok2 && sameFacts && aiComplete("feature", stored)) {
15841
+ const s = stored;
15842
+ const fresh = model;
15843
+ const f = {
15844
+ ...model,
15845
+ ...s.presentation ? { presentation: s.presentation } : {},
15846
+ body: s.body ?? null,
15847
+ docTitle: s.docTitle ?? null,
15848
+ category: s.category ?? null,
15849
+ ...!fresh.flow && s.flow ? { flow: s.flow } : {},
15850
+ ...!fresh.flowDiagram && s.flowDiagram ? { flowDiagram: s.flowDiagram } : {}
15851
+ };
15852
+ model = f;
15853
+ markdown = renderFeaturePage(f);
15854
+ if (f.docTitle) page.title = f.docTitle;
15855
+ }
15856
+ if (!ok2) {
15857
+ const s = stored;
15858
+ const m = model;
15859
+ if (typeof m.category !== "string" && typeof s?.category === "string" && categoryKeys.includes(s.category))
15860
+ model = { ...model, category: s.category };
15861
+ }
14915
15862
  }
15863
+ model = {
15864
+ ...model,
15865
+ factHash,
15866
+ aiIncomplete: required ? !ok2 : !aiComplete(page.kind, model),
15867
+ aiAttempts: required && !ok2 ? (sameFacts ? state.aiAttempts : 0) + 1 : 0
15868
+ };
14916
15869
  if (page.kind === "feature") {
14917
15870
  const m = model;
14918
15871
  const f = page.model;
@@ -14924,15 +15877,68 @@ async function generateDokuInner(brainId, opts = {}) {
14924
15877
  category: typeof m.category === "string" ? m.category : null
14925
15878
  });
14926
15879
  }
14927
- await repo.saveDocPage({
15880
+ await save({
14928
15881
  slug: page.slug,
14929
15882
  kind: page.kind,
14930
15883
  title: page.title,
14931
15884
  model,
14932
15885
  markdown,
14933
- sourceHash
15886
+ sourceHash,
15887
+ icon
14934
15888
  });
14935
- written++;
15889
+ }
15890
+ if (opts.llm && categoryKeys.length) {
15891
+ const candidates = featureInfo.filter(
15892
+ (f) => taxonomyChanged || opts.retaxonomy || !f.category || !categoryKeys.includes(f.category)
15893
+ );
15894
+ if (candidates.length) {
15895
+ try {
15896
+ const filed = parseRefile(
15897
+ await opts.llm.complete({
15898
+ system: REFILE_SYSTEM,
15899
+ prompt: [
15900
+ "Categories:",
15901
+ ...categories.map(
15902
+ (c) => `- ${c.key}: ${c.name} (${c.description})`
15903
+ ),
15904
+ "",
15905
+ "Pages:",
15906
+ ...candidates.map(
15907
+ (f) => `- ${f.slug}: ${f.title}: ${firstSentence(f.what)}`
15908
+ )
15909
+ ].join("\n"),
15910
+ maxTokens: Math.min(16e3, 300 + candidates.length * 60)
15911
+ }),
15912
+ candidates.map((f) => f.slug),
15913
+ categoryKeys
15914
+ );
15915
+ if (filed) {
15916
+ const unfiled = candidates.filter((f) => !filed[f.slug]).length;
15917
+ if (unfiled)
15918
+ console.warn(
15919
+ `doku: re-filing left ${unfiled} of ${candidates.length} pages as they were`
15920
+ );
15921
+ for (const f of candidates) {
15922
+ const key = filed[f.slug];
15923
+ if (!key || key === f.category) continue;
15924
+ const row = storedRows.get(f.slug);
15925
+ const prev = meta.get(f.slug);
15926
+ if (!row) continue;
15927
+ f.category = key;
15928
+ await save({
15929
+ slug: f.slug,
15930
+ kind: "feature",
15931
+ title: row.title,
15932
+ model: { ...row.model, category: key },
15933
+ markdown: row.markdown,
15934
+ sourceHash: prev?.source_hash ?? sha1(row.markdown)
15935
+ });
15936
+ }
15937
+ } else console.warn("doku: re-filing rejected (invalid reply)");
15938
+ } catch (e) {
15939
+ console.warn("doku: re-filing failed:", e);
15940
+ }
15941
+ }
14936
15942
  }
14937
15943
  for (const c of categories) {
14938
15944
  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 +15958,18 @@ async function generateDokuInner(brainId, opts = {}) {
14952
15958
  body: null,
14953
15959
  members
14954
15960
  };
14955
- const sourceHash = sha1(renderCategoryPage(catModel) + "c1");
15961
+ const factHash = sha1(renderCategoryPage(catModel));
15962
+ const sourceHash = sha1(renderCategoryPage(catModel) + "c2");
14956
15963
  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;
15964
+ const stored = storedBySlug.get(slug);
15965
+ const state = aiState(stored);
15966
+ const unchanged = Boolean(
15967
+ prev && !prev.deleted && prev.source_hash === sourceHash
15968
+ );
15969
+ const retry = Boolean(opts.llm) && unchanged && !aiComplete("category", stored) && (state.aiAttempts < MAX_AI_ATTEMPTS || Boolean(opts.forceRetry));
15970
+ if (unchanged && !retry) continue;
15971
+ const sameFacts = stored !== void 0 && state.factHash === factHash;
15972
+ let ok2 = !opts.llm;
14960
15973
  if (opts.llm) {
14961
15974
  try {
14962
15975
  const body = parseCategoryBody(
@@ -14971,28 +15984,37 @@ async function generateDokuInner(brainId, opts = {}) {
14971
15984
  (m) => `- ${m.slug}: ${m.title}: ${firstSentence(m.what)}`
14972
15985
  )
14973
15986
  ].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
15987
+ // Structured markdown for a large area needs room; a clipped body would
15988
+ // lose its last sections.
15989
+ maxTokens: 4e3
14977
15990
  }),
14978
15991
  members.map((m) => m.slug)
14979
15992
  );
14980
15993
  if (body) {
14981
15994
  catModel.body = body;
14982
15995
  polished = true;
15996
+ ok2 = true;
14983
15997
  }
14984
15998
  } catch {
14985
15999
  }
16000
+ if (!ok2 && sameFacts) {
16001
+ const s = stored;
16002
+ if (typeof s.body === "string" && s.body.trim()) catModel.body = s.body;
16003
+ }
14986
16004
  }
14987
- await repo.saveDocPage({
16005
+ await save({
14988
16006
  slug,
14989
16007
  kind: "category",
14990
16008
  title: c.name,
14991
- model: catModel,
16009
+ model: {
16010
+ ...catModel,
16011
+ factHash,
16012
+ aiIncomplete: opts.llm ? !ok2 : !catModel.body,
16013
+ aiAttempts: opts.llm && !ok2 ? (sameFacts ? state.aiAttempts : 0) + 1 : 0
16014
+ },
14992
16015
  markdown: renderCategoryPage(catModel),
14993
16016
  sourceHash
14994
16017
  });
14995
- written++;
14996
16018
  }
14997
16019
  let deleted = 0;
14998
16020
  for (const [slug, m] of meta) {
@@ -15061,7 +16083,7 @@ async function exportDoku(brainId, root) {
15061
16083
  const sync = await writeDokuTree(root, files);
15062
16084
  return { pages: files.length, ...sync };
15063
16085
  }
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;
16086
+ 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
16087
  var init_doku = __esm({
15066
16088
  "packages/ingest/src/doku.ts"() {
15067
16089
  "use strict";
@@ -15081,41 +16103,57 @@ var init_doku = __esm({
15081
16103
  moduleName = (o) => String(o.body.name ?? o.title);
15082
16104
  isRepoPath = (name) => !/^([a-zA-Z]:[/\\]|[/\\])/.test(name) && !name.includes("node_modules") && !name.includes(".claude/");
15083
16105
  normPath = (p) => p.replace(/\\/g, "/").replace(/^\.\//, "").toLowerCase();
16106
+ MAX_AI_ATTEMPTS = 3;
15084
16107
  PAGE_SYSTEM = [
15085
16108
  "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.',
16109
+ 'Respond ONLY with a JSON object of exactly this shape: {"title": "...", "category": "...", "icon": "...", "summary": "...", "blocks": [...]}. No code fences, nothing outside the JSON.',
16110
+ '"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
16111
  '"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
16112
  '"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).",
16113
+ '"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.',
16114
+ '"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:',
16115
+ '{"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.',
16116
+ '{"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.',
16117
+ '{"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.',
16118
+ '{"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.',
16119
+ '{"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.',
16120
+ '{"kind":"callout","tone":"note","text":"..."}: one short prerequisite, effect, or limit. "warning" only for a concrete consequence the reader must avoid; otherwise "note".',
16121
+ `{"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.`,
16122
+ "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.",
16123
+ "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.",
16124
+ "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).",
16125
+ "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
16126
  PROSE_STYLE
15094
16127
  ].join(" ");
15095
16128
  TAXONOMY_SYSTEM = [
15096
16129
  "You maintain the category structure of a software product's documentation.",
15097
16130
  "You are given the product's feature list and the EXISTING categories (possibly empty).",
15098
16131
  '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.",
16132
+ "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
16133
  "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
16134
  "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.',
16135
+ '"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
16136
  PROSE_STYLE
15104
16137
  ].join(" ");
16138
+ REFILE_SYSTEM = [
16139
+ "You file documentation pages into the given categories of a software product.",
16140
+ 'Respond ONLY with a JSON object of exactly this shape: {"pages": {"<slug>": "<category key>"}}. No code fences, nothing outside the JSON.',
16141
+ "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."
16142
+ ].join(" ");
15105
16143
  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.",
16144
+ "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.",
16145
+ "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.",
16146
+ "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.",
16147
+ "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.",
16148
+ "No H1 heading (the category name renders above). Ground everything in the given summaries; invent nothing.",
15111
16149
  PROSE_STYLE
15112
16150
  ].join(" ");
15113
16151
  ENTRY_NARRATIVE_SYSTEM = [
15114
16152
  "You write the opening description for a software project's documentation.",
15115
16153
  "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.",
16154
+ "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
16155
  "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.",
16156
+ "Ground every statement in the given features; invent nothing, no marketing superlatives, no H1 heading.",
15119
16157
  PROSE_STYLE
15120
16158
  ].join(" ");
15121
16159
  FLOW_SYSTEM = [
@@ -15137,7 +16175,8 @@ var init_doku = __esm({
15137
16175
  ].join(" ");
15138
16176
  ARCH_FLOWS_SYSTEM = [
15139
16177
  "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.',
16178
+ '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.',
16179
+ '"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
16180
  "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
16181
  "lanes are 3-5 PHASES of the flow in order (intake, processing, storage, ...), titled in plain words. Never folder names.",
15143
16182
  "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).",
@@ -15301,9 +16340,9 @@ var init_verify_agent = __esm({
15301
16340
  }
15302
16341
  });
15303
16342
 
15304
- // packages/ingest/src/verify-poll.ts
15305
- var init_verify_poll = __esm({
15306
- "packages/ingest/src/verify-poll.ts"() {
16343
+ // packages/ingest/src/verify-start.ts
16344
+ var init_verify_start = __esm({
16345
+ "packages/ingest/src/verify-start.ts"() {
15307
16346
  "use strict";
15308
16347
  init_src2();
15309
16348
  init_src();
@@ -15312,14 +16351,26 @@ var init_verify_poll = __esm({
15312
16351
  }
15313
16352
  });
15314
16353
 
15315
- // packages/ingest/src/verify-start.ts
15316
- var init_verify_start = __esm({
15317
- "packages/ingest/src/verify-start.ts"() {
16354
+ // packages/ingest/src/verify-poll.ts
16355
+ var init_verify_poll = __esm({
16356
+ "packages/ingest/src/verify-poll.ts"() {
15318
16357
  "use strict";
15319
16358
  init_src2();
15320
16359
  init_src();
15321
16360
  init_repo_reader();
15322
16361
  init_verify_agent();
16362
+ init_verify_start();
16363
+ }
16364
+ });
16365
+
16366
+ // packages/ingest/src/job-worker.ts
16367
+ var init_job_worker = __esm({
16368
+ "packages/ingest/src/job-worker.ts"() {
16369
+ "use strict";
16370
+ init_src2();
16371
+ init_deepscan();
16372
+ init_routing();
16373
+ init_verify_start();
15323
16374
  }
15324
16375
  });
15325
16376
 
@@ -15401,6 +16452,7 @@ var init_src3 = __esm({
15401
16452
  init_verify_agent();
15402
16453
  init_verify_poll();
15403
16454
  init_verify_start();
16455
+ init_job_worker();
15404
16456
  init_ask_agent_model();
15405
16457
  init_scheduler();
15406
16458
  registerConnector(
@@ -16646,9 +17698,9 @@ function commitShas8(text) {
16646
17698
  return [...out];
16647
17699
  }
16648
17700
  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();
17701
+ const n2 = typeof ts === "string" && /^\d+$/.test(ts) ? Number(ts) : ts;
17702
+ if (typeof n2 === "number" && Number.isFinite(n2))
17703
+ return new Date(n2).toISOString();
16652
17704
  if (typeof ts === "string" && !Number.isNaN(Date.parse(ts)))
16653
17705
  return new Date(ts).toISOString();
16654
17706
  return now;
@@ -16860,16 +17912,12 @@ async function linkForSpool(file) {
16860
17912
  async function shipSpool(file, link) {
16861
17913
  const events = readSpool(file);
16862
17914
  if (!events.length) return;
16863
- const r = await captureApi("/capture/events", {
17915
+ await captureApi("/capture/events", {
16864
17916
  method: "POST",
16865
- body: {
16866
- brain: link.brainId,
16867
- events,
16868
- author: loadEnv().BRAINJAR_AUTHOR?.trim() || null
16869
- }
17917
+ body: { brain: link.brainId, events }
16870
17918
  });
16871
17919
  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` : "")
17920
+ `brainjar: session sent (${events.length} events), the server reads it in the background`
16873
17921
  );
16874
17922
  }
16875
17923
  function parseHookArgs(argv) {
@@ -16968,23 +18016,19 @@ async function syncRepo(root) {
16968
18016
  const connector = createConnector("git_local", { cwd: root }, {});
16969
18017
  const artifacts = await connector.sync(project.cursor ?? void 0);
16970
18018
  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
- }
18019
+ const r = await captureApi("/capture/artifacts", {
18020
+ method: "POST",
18021
+ body: {
18022
+ brain: link.brainId,
18023
+ sourceId: project.sourceId,
18024
+ artifacts,
18025
+ cursor: maxUpdatedAt(artifacts) ?? project.cursor
16981
18026
  }
16982
- );
18027
+ });
16983
18028
  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
- }
18029
+ console.error(
18030
+ `brainjar: ${r.inserted} new commits sent, the server reads them in the background`
18031
+ );
16988
18032
  const graph = await scanStructure(root);
16989
18033
  await captureApi("/capture/structure", {
16990
18034
  method: "POST",
@@ -17041,12 +18085,67 @@ var init_sync2 = __esm({
17041
18085
  }
17042
18086
  });
17043
18087
 
18088
+ // packages/cli/src/wait.ts
18089
+ function progressLine(p) {
18090
+ const left = p.pending > 0 ? `${n(p.pending)} commits and sessions still to read` : `${n(p.queued + p.running)} capture jobs still running`;
18091
+ const queue = p.ahead > 0 ? ` (${n(p.ahead)} jobs of other projects ahead)` : "";
18092
+ return ` \u2026 ${left}${queue}`;
18093
+ }
18094
+ async function waitForServer(brainId, since, opts = {}) {
18095
+ const poll = opts.poll ?? (() => captureApi(
18096
+ `/capture/progress?brain=${brainId}&since=${encodeURIComponent(since.toISOString())}`
18097
+ ));
18098
+ const sleep2 = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
18099
+ const write = opts.write ?? ((s) => void process.stdout.write(s));
18100
+ const tty = opts.tty ?? Boolean(process.stdout.isTTY);
18101
+ const intervalMs = opts.intervalMs ?? 2e3;
18102
+ let shown = "";
18103
+ let shownAt = 0;
18104
+ let misses = 0;
18105
+ for (; ; ) {
18106
+ let p;
18107
+ try {
18108
+ p = await poll();
18109
+ misses = 0;
18110
+ } catch {
18111
+ if (++misses * intervalMs >= 6e4) {
18112
+ if (tty && shown) write("\n");
18113
+ return null;
18114
+ }
18115
+ await sleep2(intervalMs);
18116
+ continue;
18117
+ }
18118
+ if (p.queued + p.running === 0) {
18119
+ if (tty && shown) write("\r\x1B[K");
18120
+ return p;
18121
+ }
18122
+ const line = progressLine(p);
18123
+ if (tty) write(`\r\x1B[K${line}`);
18124
+ else if (line !== shown && Date.now() - shownAt >= 15e3) {
18125
+ write(`${line}
18126
+ `);
18127
+ shownAt = Date.now();
18128
+ }
18129
+ shown = line;
18130
+ await sleep2(intervalMs);
18131
+ }
18132
+ }
18133
+ var n;
18134
+ var init_wait = __esm({
18135
+ "packages/cli/src/wait.ts"() {
18136
+ "use strict";
18137
+ init_api_client();
18138
+ n = (x) => x.toLocaleString("en-US");
18139
+ }
18140
+ });
18141
+
17044
18142
  // packages/cli/src/init.ts
17045
18143
  var init_exports = {};
17046
18144
  __export(init_exports, {
17047
18145
  init: () => init,
17048
18146
  initMain: () => initMain
17049
18147
  });
18148
+ import { hostname } from "node:os";
17050
18149
  import { readFile as readFile12 } from "node:fs/promises";
17051
18150
  async function init(argv) {
17052
18151
  const skipTranscripts = argv.includes("--no-transcripts");
@@ -17056,6 +18155,7 @@ async function init(argv) {
17056
18155
  const tokenFlag = flag("--token");
17057
18156
  const root = await repoRoot();
17058
18157
  const name = repoName(root);
18158
+ const startedAt = new Date(Date.now() - 6e4);
17059
18159
  console.log(`
17060
18160
  \u{1F9E0} BrainJar init \u2014 ${name}
17061
18161
  repo: ${root}`);
@@ -17083,6 +18183,7 @@ async function init(argv) {
17083
18183
  method: "POST",
17084
18184
  body: {
17085
18185
  name,
18186
+ machine: hostname(),
17086
18187
  ...brainFlag ? { brainId: brainFlag } : reuse ? { brainId: reuse.brainId } : {},
17087
18188
  ...reuse ? { sourceId: reuse.sourceId } : {},
17088
18189
  ...remote ? { remote } : {}
@@ -17104,61 +18205,49 @@ async function init(argv) {
17104
18205
  }
17105
18206
  step(3, "Scanning code + ingesting git history\u2026");
17106
18207
  const graph = await scanStructure(root);
17107
- const structure = await captureApi(
17108
- "/capture/structure",
17109
- { method: "POST", body: { brain: conn.brainId, graph } }
17110
- );
18208
+ await captureApi("/capture/structure", {
18209
+ method: "POST",
18210
+ body: { brain: conn.brainId, graph }
18211
+ });
17111
18212
  const connector = createConnector("git_local", { cwd: root }, {});
17112
18213
  const artifacts = await connector.sync(conn.cursor ?? void 0);
17113
18214
  let commits = 0;
17114
- let objects = 0;
17115
18215
  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
- }
18216
+ const r = await captureApi("/capture/artifacts", {
18217
+ method: "POST",
18218
+ body: {
18219
+ brain: conn.brainId,
18220
+ sourceId: conn.sourceId,
18221
+ artifacts,
18222
+ cursor: maxUpdatedAt(artifacts) ?? conn.cursor
17126
18223
  }
17127
- );
18224
+ });
17128
18225
  commits = r.inserted;
17129
- objects = r.objects;
17130
18226
  }
17131
18227
  ok(
17132
- `${structure.modules} modules, ${commits} new commits \u2192 ${objects} objects`
18228
+ `${graph.modules.length} modules, ${commits} new commits sent to the server`
17133
18229
  );
17134
18230
  step(4, "Backfilling past agent sessions\u2026");
17135
18231
  if (skipTranscripts) {
17136
18232
  ok("skipped (--no-transcripts)");
17137
18233
  } else {
17138
- const author = loadEnv().BRAINJAR_AUTHOR?.trim() || null;
17139
18234
  const files = await transcriptFiles(root);
17140
- let prompts = 0;
17141
- let failures = 0;
17142
- let consolidated = 0;
17143
18235
  let truncatedCount = 0;
17144
18236
  let skipped = 0;
17145
18237
  for (const f of files) {
17146
18238
  try {
17147
18239
  const { text, truncated } = capTranscript(await readFile12(f, "utf8"));
17148
18240
  if (truncated) truncatedCount++;
17149
- const r = await captureApi("/capture/transcript", {
18241
+ await captureApi("/capture/transcript", {
17150
18242
  method: "POST",
17151
- body: { brain: conn.brainId, text, author, backfill: true }
18243
+ body: { brain: conn.brainId, text, backfill: true }
17152
18244
  });
17153
- prompts += r.prompts;
17154
- failures += r.failures;
17155
- consolidated += r.consolidated;
17156
18245
  } catch {
17157
18246
  skipped++;
17158
18247
  }
17159
18248
  }
17160
18249
  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` : "")
18250
+ `${files.length - skipped} sessions sent to the server` + (truncatedCount ? ` (${truncatedCount} oversized, newest 40MB kept)` : "") + (skipped ? `, ${skipped} skipped on errors` : "")
17162
18251
  );
17163
18252
  }
17164
18253
  step(5, "Semantic search\u2026");
@@ -17208,6 +18297,15 @@ async function init(argv) {
17208
18297
  } catch {
17209
18298
  ok("skipped (could not write the pointer)");
17210
18299
  }
18300
+ step(8, "Filling the Brain on the server\u2026");
18301
+ console.log(" Stopping this (Ctrl+C) is safe: the server keeps working.");
18302
+ const filled = await waitForServer(conn.brainId, startedAt);
18303
+ if (!filled) ok("lost the connection to the server; it keeps working");
18304
+ else if (filled.failed > 0)
18305
+ ok(
18306
+ `done; ${filled.failed} capture jobs failed for good (see the bell in the web app)`
18307
+ );
18308
+ else ok("done");
17211
18309
  try {
17212
18310
  await captureApi("/capture/connect", {
17213
18311
  method: "POST",
@@ -17215,6 +18313,7 @@ async function init(argv) {
17215
18313
  brainId: conn.brainId,
17216
18314
  sourceId: conn.sourceId,
17217
18315
  name,
18316
+ machine: hostname(),
17218
18317
  done: true,
17219
18318
  tools: tools.map((t) => t.id)
17220
18319
  }
@@ -17254,8 +18353,9 @@ var init_init = __esm({
17254
18353
  init_src();
17255
18354
  init_api_client();
17256
18355
  init_sync2();
17257
- step = (n, msg) => console.log(`
17258
- [${n}/7] ${msg}`);
18356
+ init_wait();
18357
+ step = (n2, msg) => console.log(`
18358
+ [${n2}/8] ${msg}`);
17259
18359
  ok = (msg) => console.log(` \u2713 ${msg}`);
17260
18360
  }
17261
18361
  });
@@ -17528,13 +18628,13 @@ function selectDeepScanFiles(candidates, graph, maxFiles = MAX_FILES) {
17528
18628
  }
17529
18629
  };
17530
18630
  for (const bucket of BUCKET_CAPS) {
17531
- let n = 0;
18631
+ let n2 = 0;
17532
18632
  const ordered = [...candidates].sort().reverse();
17533
18633
  for (const p of ordered) {
17534
- if (n >= bucket.cap) break;
18634
+ if (n2 >= bucket.cap) break;
17535
18635
  if (bucket.test(p) && !has.has(p)) {
17536
18636
  take(p);
17537
- n++;
18637
+ n2++;
17538
18638
  }
17539
18639
  }
17540
18640
  }