@yurtsever/capsa 0.1.0-alpha.6 → 0.1.0-alpha.8

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 (3) hide show
  1. package/README.md +3 -2
  2. package/dist/index.js +745 -276
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2807,8 +2807,8 @@ var require_validate = __commonJS({
2807
2807
  return data;
2808
2808
  }
2809
2809
  let expr = data;
2810
- const segments3 = jsonPointer.split("/");
2811
- for (const segment of segments3) {
2810
+ const segments4 = jsonPointer.split("/");
2811
+ for (const segment of segments4) {
2812
2812
  if (segment) {
2813
2813
  data = (0, codegen_1._)`${data}${(0, codegen_1.getProperty)((0, util_1.unescapeJsonPointer)(segment))}`;
2814
2814
  expr = (0, codegen_1._)`${expr} && ${data}`;
@@ -3870,49 +3870,49 @@ var require_fast_uri = __commonJS({
3870
3870
  schemelessOptions.skipEscape = true;
3871
3871
  return serialize(resolved, schemelessOptions);
3872
3872
  }
3873
- function resolveComponent(base, relative6, options, skipNormalization) {
3873
+ function resolveComponent(base, relative7, options, skipNormalization) {
3874
3874
  const target = {};
3875
3875
  if (!skipNormalization) {
3876
3876
  base = parse3(serialize(base, options), options);
3877
- relative6 = parse3(serialize(relative6, options), options);
3877
+ relative7 = parse3(serialize(relative7, options), options);
3878
3878
  }
3879
3879
  options = options || {};
3880
- if (!options.tolerant && relative6.scheme) {
3881
- target.scheme = relative6.scheme;
3882
- target.userinfo = relative6.userinfo;
3883
- target.host = relative6.host;
3884
- target.port = relative6.port;
3885
- target.path = removeDotSegments(relative6.path || "");
3886
- target.query = relative6.query;
3880
+ if (!options.tolerant && relative7.scheme) {
3881
+ target.scheme = relative7.scheme;
3882
+ target.userinfo = relative7.userinfo;
3883
+ target.host = relative7.host;
3884
+ target.port = relative7.port;
3885
+ target.path = removeDotSegments(relative7.path || "");
3886
+ target.query = relative7.query;
3887
3887
  } else {
3888
- if (relative6.userinfo !== void 0 || relative6.host !== void 0 || relative6.port !== void 0) {
3889
- target.userinfo = relative6.userinfo;
3890
- target.host = relative6.host;
3891
- target.port = relative6.port;
3892
- target.path = removeDotSegments(relative6.path || "");
3893
- target.query = relative6.query;
3888
+ if (relative7.userinfo !== void 0 || relative7.host !== void 0 || relative7.port !== void 0) {
3889
+ target.userinfo = relative7.userinfo;
3890
+ target.host = relative7.host;
3891
+ target.port = relative7.port;
3892
+ target.path = removeDotSegments(relative7.path || "");
3893
+ target.query = relative7.query;
3894
3894
  } else {
3895
- if (!relative6.path) {
3895
+ if (!relative7.path) {
3896
3896
  target.path = base.path;
3897
- if (relative6.query !== void 0) {
3898
- target.query = relative6.query;
3897
+ if (relative7.query !== void 0) {
3898
+ target.query = relative7.query;
3899
3899
  } else {
3900
3900
  target.query = base.query;
3901
3901
  }
3902
3902
  } else {
3903
- if (relative6.path[0] === "/") {
3904
- target.path = removeDotSegments(relative6.path);
3903
+ if (relative7.path[0] === "/") {
3904
+ target.path = removeDotSegments(relative7.path);
3905
3905
  } else {
3906
3906
  if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) {
3907
- target.path = "/" + relative6.path;
3907
+ target.path = "/" + relative7.path;
3908
3908
  } else if (!base.path) {
3909
- target.path = relative6.path;
3909
+ target.path = relative7.path;
3910
3910
  } else {
3911
- target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative6.path;
3911
+ target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative7.path;
3912
3912
  }
3913
3913
  target.path = removeDotSegments(target.path);
3914
3914
  }
3915
- target.query = relative6.query;
3915
+ target.query = relative7.query;
3916
3916
  }
3917
3917
  target.userinfo = base.userinfo;
3918
3918
  target.host = base.host;
@@ -3920,7 +3920,7 @@ var require_fast_uri = __commonJS({
3920
3920
  }
3921
3921
  target.scheme = base.scheme;
3922
3922
  }
3923
- target.fragment = relative6.fragment;
3923
+ target.fragment = relative7.fragment;
3924
3924
  return target;
3925
3925
  }
3926
3926
  function equal(uriA, uriB, options) {
@@ -4621,15 +4621,15 @@ var require_core = __commonJS({
4621
4621
  errorsText(errors = this.errors, { separator = ", ", dataVar = "data" } = {}) {
4622
4622
  if (!errors || errors.length === 0)
4623
4623
  return "No errors";
4624
- return errors.map((e) => `${dataVar}${e.instancePath} ${e.message}`).reduce((text, msg) => text + separator + msg);
4624
+ return errors.map((e) => `${dataVar}${e.instancePath} ${e.message}`).reduce((text2, msg) => text2 + separator + msg);
4625
4625
  }
4626
4626
  $dataMetaSchema(metaSchema, keywordsJsonPointers) {
4627
4627
  const rules = this.RULES.all;
4628
4628
  metaSchema = JSON.parse(JSON.stringify(metaSchema));
4629
4629
  for (const jsonPointer of keywordsJsonPointers) {
4630
- const segments3 = jsonPointer.split("/").slice(1);
4630
+ const segments4 = jsonPointer.split("/").slice(1);
4631
4631
  let keywords = metaSchema;
4632
- for (const seg of segments3)
4632
+ for (const seg of segments4)
4633
4633
  keywords = keywords[seg];
4634
4634
  for (const key in rules) {
4635
4635
  const rule = rules[key];
@@ -7196,8 +7196,8 @@ var require_dist = __commonJS({
7196
7196
  });
7197
7197
 
7198
7198
  // src/index.ts
7199
- import { readFile as readFile7 } from "fs/promises";
7200
- import { dirname as dirname9, resolve as resolve4 } from "path";
7199
+ import { readFile as readFile8 } from "fs/promises";
7200
+ import { dirname as dirname10, resolve as resolve4 } from "path";
7201
7201
  import { fileURLToPath as fileURLToPath2 } from "url";
7202
7202
 
7203
7203
  // ../core/dist/scanner.js
@@ -7247,7 +7247,7 @@ async function scan(adapters, options = {}) {
7247
7247
  const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
7248
7248
  const issues = [];
7249
7249
  const items = [];
7250
- const seenItemPaths = /* @__PURE__ */ new Set();
7250
+ const seenItemPaths = /* @__PURE__ */ new Map();
7251
7251
  const scannedRoots = [];
7252
7252
  const formatAdapters = adapters.filter((a) => !isSourceAdapter(a));
7253
7253
  const sourceAdapters = adapters.filter(isSourceAdapter);
@@ -7259,10 +7259,19 @@ async function scan(adapters, options = {}) {
7259
7259
  rootSet.add(resolve(r));
7260
7260
  }
7261
7261
  const roots = [...rootSet].filter((r) => ![...rootSet].some((other) => other !== r && r.startsWith(other + "/")));
7262
- const addItem = (item) => {
7263
- if (seenItemPaths.has(item.path))
7262
+ const addItem = (item, adapterId) => {
7263
+ const owner = seenItemPaths.get(item.path);
7264
+ if (owner !== void 0) {
7265
+ if (owner !== adapterId) {
7266
+ issues.push({
7267
+ path: item.path,
7268
+ adapter: adapterId,
7269
+ message: `claimed by both "${owner}" and "${adapterId}"; kept "${owner}" \u2014 two adapters' conventions overlap here`
7270
+ });
7271
+ }
7264
7272
  return;
7265
- seenItemPaths.add(item.path);
7273
+ }
7274
+ seenItemPaths.set(item.path, adapterId);
7266
7275
  items.push(item);
7267
7276
  };
7268
7277
  const handleEntry = async (entry) => {
@@ -7281,8 +7290,13 @@ async function scan(adapters, options = {}) {
7281
7290
  if (!matched)
7282
7291
  continue;
7283
7292
  try {
7284
- for (const item of await adapter.parse(entry, env))
7285
- addItem(item);
7293
+ const parsed = await adapter.parse(entry, env);
7294
+ const result = Array.isArray(parsed) ? { items: parsed } : parsed;
7295
+ for (const item of result.items)
7296
+ addItem(item, adapter.id);
7297
+ for (const issue2 of result.issues ?? []) {
7298
+ issues.push({ adapter: adapter.id, ...issue2 });
7299
+ }
7286
7300
  } catch (err) {
7287
7301
  issues.push({
7288
7302
  path: entry.absPath,
@@ -7356,7 +7370,7 @@ async function scan(adapters, options = {}) {
7356
7370
  for (const adapter of sourceAdapters) {
7357
7371
  try {
7358
7372
  for (const item of await adapter.collect(root, env))
7359
- addItem(item);
7373
+ addItem(item, adapter.id);
7360
7374
  } catch (err) {
7361
7375
  issues.push({
7362
7376
  path: root,
@@ -7414,20 +7428,24 @@ function findProjectRoot(startDir, homeDir) {
7414
7428
 
7415
7429
  // ../core/dist/chunk.js
7416
7430
  var DEFAULT_MAX_CHARS = 2e3;
7417
- function chunkMarkdown(markdown, maxChars = DEFAULT_MAX_CHARS) {
7431
+ var DEFAULT_MAX_HEADING_LEVEL = 6;
7432
+ function chunkMarkdown(markdown, options = {}) {
7433
+ const maxChars = options.maxChars ?? DEFAULT_MAX_CHARS;
7434
+ const maxHeadingLevel = options.maxHeadingLevel ?? DEFAULT_MAX_HEADING_LEVEL;
7418
7435
  const body = parseFrontmatter(markdown).body;
7419
7436
  const chunks = [];
7420
7437
  const trail = [];
7421
7438
  let buffer = [];
7422
7439
  let inFence = false;
7440
+ let openPath = [];
7441
+ const path = () => trail.filter((s) => Boolean(s));
7423
7442
  const flush = () => {
7424
- const text = buffer.join("\n").trim();
7443
+ const text2 = buffer.join("\n").trim();
7425
7444
  buffer = [];
7426
- if (!text)
7445
+ if (!text2)
7427
7446
  return;
7428
- const headingPath = trail.filter((s) => Boolean(s));
7429
- for (const part of splitLong(text, maxChars)) {
7430
- chunks.push({ index: chunks.length, headingPath, text: part });
7447
+ for (const part of splitLong(text2, maxChars)) {
7448
+ chunks.push({ index: chunks.length, headingPath: openPath, text: part });
7431
7449
  }
7432
7450
  };
7433
7451
  for (const line of body.split(/\r?\n/)) {
@@ -7435,10 +7453,13 @@ function chunkMarkdown(markdown, maxChars = DEFAULT_MAX_CHARS) {
7435
7453
  inFence = !inFence;
7436
7454
  const heading = !inFence && /^(#{1,6})\s+(.*\S)\s*$/.exec(line);
7437
7455
  if (heading) {
7438
- flush();
7439
7456
  const level = heading[1].length;
7457
+ if (level <= maxHeadingLevel)
7458
+ flush();
7440
7459
  trail.splice(level - 1);
7441
7460
  trail[level - 1] = heading[2];
7461
+ if (buffer.length === 0)
7462
+ openPath = path();
7442
7463
  buffer.push(line);
7443
7464
  continue;
7444
7465
  }
@@ -7447,25 +7468,58 @@ function chunkMarkdown(markdown, maxChars = DEFAULT_MAX_CHARS) {
7447
7468
  flush();
7448
7469
  return chunks;
7449
7470
  }
7450
- function splitLong(text, maxChars) {
7451
- if (text.length <= maxChars)
7452
- return [text];
7471
+ function splitLong(text2, maxChars) {
7472
+ if (text2.length <= maxChars)
7473
+ return [text2];
7453
7474
  const out = [];
7454
7475
  let current = "";
7455
- for (const para of text.split(/\n{2,}/)) {
7456
- if (current && current.length + para.length + 2 > maxChars) {
7476
+ for (const { sep: sep7, piece } of pieces(text2, maxChars)) {
7477
+ if (current && current.length + sep7.length + piece.length > maxChars) {
7457
7478
  out.push(current);
7458
- current = para;
7479
+ current = piece;
7459
7480
  } else {
7460
- current = current ? `${current}
7461
-
7462
- ${para}` : para;
7481
+ current = current ? current + sep7 + piece : piece;
7463
7482
  }
7464
7483
  }
7465
7484
  if (current)
7466
7485
  out.push(current);
7467
7486
  return out;
7468
7487
  }
7488
+ var SENTENCE_BREAK = /(?<=[.!?…]["'’”»)\]]*)(\s+)/;
7489
+ function* pieces(text2, maxChars) {
7490
+ let sep7 = "";
7491
+ for (const para of text2.split(/\n{2,}/)) {
7492
+ if (para.length <= maxChars) {
7493
+ yield { sep: sep7, piece: para };
7494
+ } else {
7495
+ const parts = para.split(SENTENCE_BREAK);
7496
+ for (let i = 0; i < parts.length; i += 2) {
7497
+ yield* cut(parts[i], sep7, maxChars);
7498
+ sep7 = parts[i + 1] ?? "";
7499
+ }
7500
+ }
7501
+ sep7 = "\n\n";
7502
+ }
7503
+ }
7504
+ function* cut(sentence, sep7, maxChars) {
7505
+ let rest = sentence;
7506
+ while (rest.length > maxChars) {
7507
+ const window = rest.slice(0, maxChars + 1);
7508
+ let at = window.lastIndexOf("\n");
7509
+ if (at < maxChars / 2)
7510
+ at = Math.max(at, window.lastIndexOf(" "));
7511
+ if (at <= 0)
7512
+ at = Math.max(maxChars, 1);
7513
+ const piece = rest.slice(0, at).trimEnd();
7514
+ const gap = /^\s*/.exec(rest.slice(piece.length))[0];
7515
+ if (piece)
7516
+ yield { sep: sep7, piece };
7517
+ sep7 = piece ? gap : sep7 + gap;
7518
+ rest = rest.slice(piece.length + gap.length);
7519
+ }
7520
+ if (rest)
7521
+ yield { sep: sep7, piece: rest };
7522
+ }
7469
7523
 
7470
7524
  // ../adapter-claude-skills/dist/index.js
7471
7525
  import { readFile } from "fs/promises";
@@ -7516,10 +7570,10 @@ import { readFile as readFile2 } from "fs/promises";
7516
7570
  import { basename as basename2, dirname as dirname3, join as join4, relative as relative2 } from "path";
7517
7571
  var INSTRUCTION_FILES = /* @__PURE__ */ new Set(["CLAUDE.md", "CLAUDE.local.md", "AGENTS.md"]);
7518
7572
  var MAX_DESCRIPTION_LENGTH = 120;
7519
- function truncate(text) {
7520
- if (text.length <= MAX_DESCRIPTION_LENGTH)
7521
- return text;
7522
- return text.slice(0, MAX_DESCRIPTION_LENGTH - 1).trimEnd() + "\u2026";
7573
+ function truncate(text2) {
7574
+ if (text2.length <= MAX_DESCRIPTION_LENGTH)
7575
+ return text2;
7576
+ return text2.slice(0, MAX_DESCRIPTION_LENGTH - 1).trimEnd() + "\u2026";
7523
7577
  }
7524
7578
  function deriveDescription(raw2) {
7525
7579
  let firstNonEmpty;
@@ -7583,8 +7637,8 @@ var LEGACY_FILE = ".cursorrules";
7583
7637
  var MDC_EXTENSION = ".mdc";
7584
7638
  var RULES_MARKER = sep2 + join5(".cursor", "rules") + sep2;
7585
7639
  var MAX_DESCRIPTION_LENGTH2 = 120;
7586
- function firstLine(text) {
7587
- for (const line of text.split(/\r?\n/)) {
7640
+ function firstLine(text2) {
7641
+ for (const line of text2.split(/\r?\n/)) {
7588
7642
  const trimmed = line.trim();
7589
7643
  if (!trimmed)
7590
7644
  continue;
@@ -7958,10 +8012,10 @@ var checkOptionalParameter = (path) => {
7958
8012
  if (path.charCodeAt(path.length - 1) !== 63 || !path.includes(":")) {
7959
8013
  return null;
7960
8014
  }
7961
- const segments3 = path.split("/");
8015
+ const segments4 = path.split("/");
7962
8016
  const results = [];
7963
8017
  let basePath = "";
7964
- segments3.forEach((segment) => {
8018
+ segments4.forEach((segment) => {
7965
8019
  if (segment !== "" && !/\:/.test(segment)) {
7966
8020
  basePath += "/" + segment;
7967
8021
  } else if (/\:/.test(segment)) {
@@ -8178,7 +8232,7 @@ var HonoRequest = class {
8178
8232
  * ```
8179
8233
  */
8180
8234
  json() {
8181
- return this.#cachedBody("text").then((text) => JSON.parse(text));
8235
+ return this.#cachedBody("text").then((text2) => JSON.parse(text2));
8182
8236
  }
8183
8237
  /**
8184
8238
  * `.text()` can parse Request body of type `text/plain`
@@ -8744,9 +8798,9 @@ var Context = class {
8744
8798
  * })
8745
8799
  * ```
8746
8800
  */
8747
- text = (text, arg, headers) => {
8748
- return !this.#preparedHeaders && !this.#status && !arg && !headers && !this.finalized ? new Response(text) : this.#newResponse(
8749
- text,
8801
+ text = (text2, arg, headers) => {
8802
+ return !this.#preparedHeaders && !this.#status && !arg && !headers && !this.finalized ? new Response(text2) : this.#newResponse(
8803
+ text2,
8750
8804
  arg,
8751
8805
  setDefaultContentType(TEXT_PLAIN, headers)
8752
8806
  );
@@ -10050,8 +10104,8 @@ function ollamaEmbedder(options = {}) {
10050
10104
  }
10051
10105
  };
10052
10106
  }
10053
- function estimateTokens(text) {
10054
- return Math.ceil(text.length / 4);
10107
+ function estimateTokens(text2) {
10108
+ return Math.ceil(text2.length / 4);
10055
10109
  }
10056
10110
 
10057
10111
  // ../index/dist/store.js
@@ -10074,7 +10128,10 @@ function loadSqlite() {
10074
10128
  }
10075
10129
  var DB_DIR = ".capsa";
10076
10130
  var DB_FILE = "index.db";
10077
- var SCHEMA_VERSION = 5;
10131
+ var SCHEMA_VERSION = 11;
10132
+ var META_INDEXED_AT = "indexed_at";
10133
+ var META_INDEX_FAILED_AT = "index_failed_at";
10134
+ var META_INDEX_ERROR = "index_error";
10078
10135
  var CONTEXT_LOG_DDL = `
10079
10136
  -- Every context delivery. This is the measurement for the MVP gate and
10080
10137
  -- the seed of the organisation tier's audit log. Alone among these
@@ -10088,6 +10145,12 @@ var CONTEXT_LOG_DDL = `
10088
10145
  query TEXT NOT NULL,
10089
10146
  delivered TEXT NOT NULL,
10090
10147
  tokens_est INTEGER NOT NULL,
10148
+ -- The ceiling this call ran under. Without it a row says what was
10149
+ -- spent but not what it was allowed to spend, and two deliveries are
10150
+ -- only comparable when that number matches. NULL on rows written
10151
+ -- before v6: they ran under a budget nobody recorded, and filling in
10152
+ -- today's default would be a guess wearing the costume of data.
10153
+ tokens_budget INTEGER,
10091
10154
  duration_ms INTEGER NOT NULL
10092
10155
  );
10093
10156
  `;
@@ -10109,24 +10172,44 @@ var Store = class {
10109
10172
  }
10110
10173
  /** True when this index was written by an older schema and must be rebuilt. */
10111
10174
  needsRebuild() {
10112
- const row = this.db.prepare("SELECT value FROM meta WHERE key = 'schema'").get();
10113
- return row?.value !== String(SCHEMA_VERSION);
10175
+ return this.getMeta("schema") !== String(SCHEMA_VERSION);
10176
+ }
10177
+ getMeta(key) {
10178
+ const row = this.db.prepare("SELECT value FROM meta WHERE key = ?").get(key);
10179
+ return row?.value;
10180
+ }
10181
+ setMeta(key, value) {
10182
+ this.db.prepare("INSERT OR REPLACE INTO meta(key, value) VALUES (?, ?)").run(key, value);
10183
+ }
10184
+ deleteMeta(key) {
10185
+ this.db.prepare("DELETE FROM meta WHERE key = ?").run(key);
10114
10186
  }
10115
10187
  /**
10116
10188
  * Drop all indexed content (not the log, not decisions) and mark the schema
10117
- * current. The virtual tables are dropped and recreated, not emptied:
10118
- * `CREATE VIRTUAL TABLE IF NOT EXISTS` would keep an old tokenizer alive
10119
- * through a rebuild which once left FTS without stemming after v2.
10189
+ * current. Every one of these tables is dropped and recreated, not emptied,
10190
+ * because `CREATE TABLE IF NOT EXISTS` in migrate() is a no-op against a
10191
+ * table that already exists under its old shape. For the virtual tables that
10192
+ * once left FTS without stemming after v2; for `items` it meant a schema
10193
+ * bump could not add a column at all — v7 added `title` and the rebuild it
10194
+ * asked for failed with "table items has no column named title", because
10195
+ * DELETE had emptied the v6 table and left it standing.
10196
+ *
10197
+ * Nothing here is a source of truth: items and chunks are derived from files
10198
+ * on disk and from `decisions`, which is one of the two tables a reset
10199
+ * spares.
10120
10200
  */
10121
10201
  reset() {
10122
10202
  this.db.exec(`
10123
10203
  DROP TABLE IF EXISTS chunks_fts;
10124
10204
  DROP TABLE IF EXISTS chunks_vec;
10125
- DELETE FROM chunks;
10126
- DELETE FROM items;
10205
+ DROP TABLE IF EXISTS chunks;
10206
+ DROP TABLE IF EXISTS items;
10127
10207
  `);
10128
10208
  this.migrate();
10129
- this.db.prepare("INSERT OR REPLACE INTO meta(key, value) VALUES ('schema', ?)").run(String(SCHEMA_VERSION));
10209
+ this.setMeta("schema", String(SCHEMA_VERSION));
10210
+ this.deleteMeta(META_INDEXED_AT);
10211
+ this.deleteMeta(META_INDEX_FAILED_AT);
10212
+ this.deleteMeta(META_INDEX_ERROR);
10130
10213
  }
10131
10214
  migrate() {
10132
10215
  this.db.exec(`
@@ -10137,12 +10220,14 @@ var Store = class {
10137
10220
  format TEXT NOT NULL,
10138
10221
  kind TEXT NOT NULL,
10139
10222
  name TEXT NOT NULL,
10223
+ title TEXT NOT NULL,
10140
10224
  path TEXT NOT NULL UNIQUE,
10141
10225
  rel_path TEXT NOT NULL,
10142
10226
  content_hash TEXT NOT NULL,
10143
10227
  status TEXT,
10144
10228
  updated_at TEXT,
10145
10229
  tags TEXT,
10230
+ metadata TEXT,
10146
10231
  origin TEXT NOT NULL DEFAULT 'local',
10147
10232
  indexed_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
10148
10233
  );
@@ -10179,27 +10264,40 @@ var Store = class {
10179
10264
  ${CONTEXT_LOG_DDL}
10180
10265
  `);
10181
10266
  this.migrateContextLog();
10182
- this.db.prepare("INSERT OR REPLACE INTO meta(key, value) VALUES ('dimensions', ?)").run(String(this.dimensions));
10267
+ this.setMeta("dimensions", String(this.dimensions));
10183
10268
  }
10184
10269
  /**
10185
10270
  * `context_log` outlives every rebuild, so it is the one table that has to
10186
- * be migrated in place instead of recreated. v5 replaced `chunk_ids` with
10187
- * `delivered`: a chunk id is a rowid the next `capsa index` reassigns, so an
10188
- * older row ended up naming whatever text inherited its ids the measurement
10189
- * quietly decayed. Rows written before v5 keep everything that still means
10190
- * something (when, who asked, what for, at what cost) and carry an empty
10191
- * delivery, because their ids can no longer be resolved honestly.
10271
+ * be migrated in place instead of recreated twice now.
10272
+ *
10273
+ * v5 replaced `chunk_ids` with `delivered`: a chunk id is a rowid the next
10274
+ * `capsa index` reassigns, so an older row ended up naming whatever text
10275
+ * inherited its ids the measurement quietly decayed. Rows written before
10276
+ * v5 keep everything that still means something (when, who asked, what for,
10277
+ * at what cost) and carry an empty delivery, because their ids can no longer
10278
+ * be resolved honestly.
10279
+ *
10280
+ * v6 added `tokens_budget`. A delivery of 1727 tokens against one of 758
10281
+ * reads like a saving until you learn the first ran on a larger ceiling;
10282
+ * that comparison was attempted on this repo's own log and could not be
10283
+ * settled, because the number was never written down. Old rows get NULL
10284
+ * rather than a backfilled default — an unknown budget is a fact about the
10285
+ * measurement, and inventing one would hide exactly the gap this closes.
10286
+ *
10287
+ * Keyed on the newest column, so a v4 database reaches v6 in one pass.
10192
10288
  */
10193
10289
  migrateContextLog() {
10194
10290
  const columns = this.db.prepare("PRAGMA table_info(context_log)").all();
10195
- if (columns.some((c) => c.name === "delivered"))
10291
+ const names = new Set(columns.map((c) => c.name));
10292
+ if (names.has("tokens_budget"))
10196
10293
  return;
10294
+ const delivered = names.has("delivered") ? "delivered" : "'[]'";
10197
10295
  this.db.exec(`
10198
- ALTER TABLE context_log RENAME TO context_log_v4;
10296
+ ALTER TABLE context_log RENAME TO context_log_old;
10199
10297
  ${CONTEXT_LOG_DDL}
10200
- INSERT INTO context_log (id, created_at, source, query, delivered, tokens_est, duration_ms)
10201
- SELECT id, created_at, source, query, '[]', tokens_est, duration_ms FROM context_log_v4;
10202
- DROP TABLE context_log_v4;
10298
+ INSERT INTO context_log (id, created_at, source, query, delivered, tokens_est, tokens_budget, duration_ms)
10299
+ SELECT id, created_at, source, query, ${delivered}, tokens_est, NULL, duration_ms FROM context_log_old;
10300
+ DROP TABLE context_log_old;
10203
10301
  `);
10204
10302
  }
10205
10303
  getItem(path) {
@@ -10217,20 +10315,22 @@ var Store = class {
10217
10315
  this.db.prepare("DELETE FROM items WHERE id = ?").run(id);
10218
10316
  }
10219
10317
  upsertItem(row) {
10220
- this.db.prepare(`INSERT INTO items (id, format, kind, name, path, rel_path, content_hash, status, updated_at, tags)
10221
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
10318
+ this.db.prepare(`INSERT INTO items (id, format, kind, name, title, path, rel_path, content_hash, status, updated_at, tags, metadata)
10319
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
10222
10320
  ON CONFLICT(id) DO UPDATE SET
10223
10321
  format = excluded.format, kind = excluded.kind, name = excluded.name,
10322
+ title = excluded.title,
10224
10323
  rel_path = excluded.rel_path, content_hash = excluded.content_hash,
10225
10324
  status = excluded.status, updated_at = excluded.updated_at, tags = excluded.tags,
10226
- indexed_at = strftime('%Y-%m-%dT%H:%M:%fZ','now')`).run(row.id, row.format, row.kind, row.name, row.path, row.rel_path, row.content_hash, row.status, row.updated_at, row.tags);
10325
+ metadata = excluded.metadata,
10326
+ indexed_at = strftime('%Y-%m-%dT%H:%M:%fZ','now')`).run(row.id, row.format, row.kind, row.name, row.title, row.path, row.rel_path, row.content_hash, row.status, row.updated_at, row.tags, row.metadata);
10227
10327
  }
10228
10328
  /**
10229
10329
  * `text` is what the user sees; `searchText` (chunk + provenance header)
10230
10330
  * is what FTS and the embedder see. Falls back to `text` when omitted.
10231
10331
  */
10232
- insertChunk(itemId, idx, headingPath, text, embedding, searchText = text) {
10233
- const res = this.db.prepare("INSERT INTO chunks (item_id, idx, heading_path, text) VALUES (?, ?, ?, ?)").run(itemId, idx, JSON.stringify(headingPath), text);
10332
+ insertChunk(itemId, idx, headingPath, text2, embedding, searchText = text2) {
10333
+ const res = this.db.prepare("INSERT INTO chunks (item_id, idx, heading_path, text) VALUES (?, ?, ?, ?)").run(itemId, idx, JSON.stringify(headingPath), text2);
10234
10334
  const id = Number(res.lastInsertRowid);
10235
10335
  this.db.prepare("INSERT INTO chunks_fts (rowid, text) VALUES (?, ?)").run(id, searchText);
10236
10336
  this.db.prepare("INSERT INTO chunks_vec (chunk_id, embedding) VALUES (?, ?)").run(BigInt(id), new Uint8Array(embedding.buffer, embedding.byteOffset, embedding.byteLength));
@@ -10254,7 +10354,7 @@ var Store = class {
10254
10354
  return [];
10255
10355
  const placeholders = ids.map(() => "?").join(",");
10256
10356
  const rows = this.db.prepare(`SELECT c.id, c.item_id, c.idx, c.heading_path, c.text,
10257
- i.id AS i_id, i.format, i.kind, i.name, i.path, i.rel_path, i.content_hash, i.status, i.updated_at, i.tags
10357
+ i.id AS i_id, i.format, i.kind, i.name, i.title, i.path, i.rel_path, i.content_hash, i.status, i.updated_at, i.tags, i.metadata
10258
10358
  FROM chunks c JOIN items i ON i.id = c.item_id WHERE c.id IN (${placeholders})`).all(...ids);
10259
10359
  return rows.map((r) => ({
10260
10360
  id: r["id"],
@@ -10267,12 +10367,14 @@ var Store = class {
10267
10367
  format: r["format"],
10268
10368
  kind: r["kind"],
10269
10369
  name: r["name"],
10370
+ title: r["title"],
10270
10371
  path: r["path"],
10271
10372
  rel_path: r["rel_path"],
10272
10373
  content_hash: r["content_hash"],
10273
10374
  status: r["status"],
10274
10375
  updated_at: r["updated_at"],
10275
- tags: r["tags"]
10376
+ tags: r["tags"],
10377
+ metadata: r["metadata"]
10276
10378
  }
10277
10379
  }));
10278
10380
  }
@@ -10284,7 +10386,7 @@ var Store = class {
10284
10386
  return this.db.prepare("SELECT * FROM items ORDER BY kind, rel_path").all();
10285
10387
  }
10286
10388
  logContext(entry) {
10287
- this.db.prepare("INSERT INTO context_log (source, query, delivered, tokens_est, duration_ms) VALUES (?, ?, ?, ?, ?)").run(entry.source, entry.query, JSON.stringify(entry.delivered), entry.tokensEst, entry.durationMs);
10389
+ this.db.prepare("INSERT INTO context_log (source, query, delivered, tokens_est, tokens_budget, duration_ms) VALUES (?, ?, ?, ?, ?, ?)").run(entry.source, entry.query, JSON.stringify(entry.delivered), entry.tokensEst, entry.tokensBudget, entry.durationMs);
10288
10390
  }
10289
10391
  /**
10290
10392
  * Returns the stored row, not just its id: `created_at` is SQL's to write,
@@ -10308,12 +10410,116 @@ var Store = class {
10308
10410
 
10309
10411
  // ../index/dist/indexer.js
10310
10412
  import { createHash as createHash2 } from "crypto";
10311
- import { relative as relative5 } from "path";
10413
+ import { relative as relative6 } from "path";
10414
+
10415
+ // ../adapter-documents/dist/index.js
10416
+ import { readFile as readFile5 } from "fs/promises";
10417
+ import { basename as basename4, dirname as dirname7, relative as relative3, sep as sep4 } from "path";
10418
+ var BOOK_DIR = "books";
10419
+ var OUTLINE_FILE = "index.md";
10420
+ var CHAPTER_HEADING_LEVEL = 3;
10421
+ var CHAPTER_MAX_CHARS = 1e3;
10422
+ function segments(relPath) {
10423
+ return relPath.split(/[\\/]/).filter(Boolean);
10424
+ }
10425
+ function documentsAdapter() {
10426
+ return {
10427
+ id: "documents",
10428
+ displayName: "Document",
10429
+ matches(entry) {
10430
+ if (!entry.isFile || !entry.name.toLowerCase().endsWith(".md"))
10431
+ return false;
10432
+ return segments(entry.relPath).slice(0, -1).some((p) => p.toLowerCase() === BOOK_DIR);
10433
+ },
10434
+ async parse(entry, env) {
10435
+ let raw2;
10436
+ try {
10437
+ raw2 = await readFile5(entry.absPath, "utf8");
10438
+ } catch {
10439
+ return { items: [] };
10440
+ }
10441
+ const fm = parseFrontmatter(raw2);
10442
+ const book = fm.data["book"];
10443
+ if (!book) {
10444
+ return {
10445
+ items: [],
10446
+ issues: [
10447
+ {
10448
+ path: entry.absPath,
10449
+ message: `no \`book:\` in frontmatter \u2014 a markdown file under ${BOOK_DIR}/ is a chapter of a document or it is nothing`
10450
+ }
10451
+ ]
10452
+ };
10453
+ }
10454
+ const projectRoot = findProjectRoot(dirname7(entry.absPath), env.homeDir);
10455
+ const relPathInProject = relative3(projectRoot, entry.absPath);
10456
+ const outline = entry.name.toLowerCase() === OUTLINE_FILE;
10457
+ const title = fm.data["title"] || firstHeading(fm.body) || basename4(entry.name, ".md");
10458
+ const chapter = fm.data["chapter"];
10459
+ const metadata = {
10460
+ relPathInProject,
10461
+ book,
10462
+ title,
10463
+ ...fm.data["author"] ? { author: fm.data["author"] } : {},
10464
+ ...chapter ? { chapter: number(chapter) } : {},
10465
+ ...pages("printed_pages", fm.data["printed_pages"]),
10466
+ ...pages("pdf_pages", fm.data["pdf_pages"])
10467
+ };
10468
+ return {
10469
+ items: [
10470
+ {
10471
+ id: stableId(entry.absPath),
10472
+ format: "documents",
10473
+ formatLabel: outline ? "Outline" : "Document",
10474
+ name: `${basename4(projectRoot)}${sep4}${relPathInProject}`,
10475
+ // What the item calls itself, and what a pinned item's
10476
+ // neighbourhood is searched with: a chapter title alone
10477
+ // ("Conclusion") belongs to nine books at once.
10478
+ description: outline ? `${book} \u2014 Contents` : `${book} \u2014 ${chapterLabel(chapter, title)}`,
10479
+ path: entry.absPath,
10480
+ scope: "project",
10481
+ projectRoot,
10482
+ content: raw2,
10483
+ metadata,
10484
+ knowledge: { kind: outline ? "outline" : "document" },
10485
+ // An outline is delivered whole or not at all — a table of
10486
+ // contents retrieved three lines at a time is noise. Empty, so
10487
+ // the indexer stores it and embeds nothing of it.
10488
+ chunks: outline ? [] : chapterChunks(raw2)
10489
+ }
10490
+ ]
10491
+ };
10492
+ }
10493
+ };
10494
+ }
10495
+ function chapterLabel(chapter, title) {
10496
+ return chapter === void 0 || chapter === "" ? title : `Ch. ${chapter} ${title}`;
10497
+ }
10498
+ function chapterChunks(raw2) {
10499
+ return chunkMarkdown(raw2, { maxHeadingLevel: CHAPTER_HEADING_LEVEL, maxChars: CHAPTER_MAX_CHARS });
10500
+ }
10501
+ function pages(key, value) {
10502
+ if (!value)
10503
+ return {};
10504
+ const parts = value.replace(/^\[|\]$/g, "").split(",").map((p) => p.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
10505
+ return parts.length ? { [key]: parts.map(number) } : {};
10506
+ }
10507
+ function number(value) {
10508
+ return /^\d+$/.test(value) ? Number(value) : value;
10509
+ }
10510
+ function firstHeading(body) {
10511
+ for (const line of body.split(/\r?\n/)) {
10512
+ const m = /^#{1,6}\s+(.*\S)\s*$/.exec(line.trim());
10513
+ if (m)
10514
+ return m[1];
10515
+ }
10516
+ return void 0;
10517
+ }
10312
10518
 
10313
10519
  // ../adapter-git-log/dist/index.js
10314
10520
  import { execFile } from "child_process";
10315
10521
  import { existsSync as existsSync2 } from "fs";
10316
- import { basename as basename4, join as join7 } from "path";
10522
+ import { basename as basename5, join as join7 } from "path";
10317
10523
  import { promisify } from "util";
10318
10524
  var execFileAsync = promisify(execFile);
10319
10525
  var DEFAULTS = {
@@ -10416,7 +10622,7 @@ function toItem(root, c, opts) {
10416
10622
  author: c.author,
10417
10623
  subject,
10418
10624
  files: c.files,
10419
- repo: basename4(root)
10625
+ repo: basename5(root)
10420
10626
  },
10421
10627
  knowledge: {
10422
10628
  kind: "commit",
@@ -10461,18 +10667,27 @@ function conventionalTags(subject) {
10461
10667
  const tags = [m[1], ...m[2] ? [m[2]] : []];
10462
10668
  return tags;
10463
10669
  }
10464
- function firstLine2(text) {
10465
- return (text ?? "").split("\n").find((l) => l.trim()) ?? "";
10670
+ function firstLine2(text2) {
10671
+ return (text2 ?? "").split("\n").find((l) => l.trim()) ?? "";
10466
10672
  }
10467
10673
 
10468
10674
  // ../adapter-runbooks/dist/index.js
10469
- import { readFile as readFile5, stat as stat3 } from "fs/promises";
10470
- import { basename as basename5, dirname as dirname7, relative as relative3, sep as sep4 } from "path";
10675
+ import { readFile as readFile6, stat as stat3 } from "fs/promises";
10676
+ import { basename as basename6, dirname as dirname8, relative as relative4, sep as sep5 } from "path";
10471
10677
  var DOC_DIRS = /* @__PURE__ */ new Set(["docs", "doc", "runbooks", "runbook", "wiki", "adr", "decisions"]);
10472
- var EXCLUDED_DIRS = /* @__PURE__ */ new Set(["tickets", "issues", "todo", "backlog", "plans", "plan", "roadmap", "milestones", "milestone", "node_modules"]);
10473
- function segments(relPath) {
10678
+ var CLAIMED_ELSEWHERE = /* @__PURE__ */ new Set(["skill.md"]);
10679
+ var EXCLUDED_DIRS = /* @__PURE__ */ new Set(["books", "tickets", "issues", "todo", "backlog", "plans", "plan", "roadmap", "milestones", "milestone", "node_modules"]);
10680
+ function segments2(relPath) {
10474
10681
  return relPath.split(/[\\/]/).filter(Boolean);
10475
10682
  }
10683
+ function kindOf(relPathInProject) {
10684
+ const parts = segments2(relPathInProject).map((p) => p.toLowerCase());
10685
+ if (parts.some((p) => p === "adr" || p === "decisions"))
10686
+ return "decision";
10687
+ if (parts.some((p) => p === "runbooks" || p === "runbook"))
10688
+ return "runbook";
10689
+ return "doc";
10690
+ }
10476
10691
  function runbooksAdapter() {
10477
10692
  return {
10478
10693
  id: "runbooks",
@@ -10480,7 +10695,9 @@ function runbooksAdapter() {
10480
10695
  matches(entry) {
10481
10696
  if (!entry.isFile || !entry.name.toLowerCase().endsWith(".md"))
10482
10697
  return false;
10483
- const parts = segments(entry.relPath).slice(0, -1);
10698
+ if (CLAIMED_ELSEWHERE.has(entry.name.toLowerCase()))
10699
+ return false;
10700
+ const parts = segments2(entry.relPath).slice(0, -1);
10484
10701
  if (!parts.some((p) => DOC_DIRS.has(p.toLowerCase())))
10485
10702
  return false;
10486
10703
  return !parts.some((p) => EXCLUDED_DIRS.has(p.toLowerCase()));
@@ -10489,24 +10706,23 @@ function runbooksAdapter() {
10489
10706
  let raw2;
10490
10707
  let mtime;
10491
10708
  try {
10492
- raw2 = await readFile5(entry.absPath, "utf8");
10709
+ raw2 = await readFile6(entry.absPath, "utf8");
10493
10710
  mtime = (await stat3(entry.absPath)).mtime;
10494
10711
  } catch {
10495
10712
  return [];
10496
10713
  }
10497
- const projectRoot = findProjectRoot(dirname7(entry.absPath), env.homeDir);
10498
- const relPathInProject = relative3(projectRoot, entry.absPath);
10714
+ const projectRoot = findProjectRoot(dirname8(entry.absPath), env.homeDir);
10715
+ const relPathInProject = relative4(projectRoot, entry.absPath);
10499
10716
  const fm = parseFrontmatter(raw2);
10500
10717
  const data = fm.data;
10501
- const title = data["title"] || firstHeading(raw2) || basename5(entry.name, ".md");
10502
- const kindFromPath = segments(relPathInProject).some((p) => ["adr", "decisions"].includes(p.toLowerCase())) ? "decision" : "runbook";
10718
+ const title = data["title"] || firstHeading2(raw2) || basename6(entry.name, ".md");
10503
10719
  const tags = parseTags(data["tags"]);
10504
10720
  return [
10505
10721
  {
10506
10722
  id: stableId(entry.absPath),
10507
10723
  format: "runbooks",
10508
10724
  formatLabel: "Runbook",
10509
- name: `${basename5(projectRoot)}${sep4}${relPathInProject}`,
10725
+ name: `${basename6(projectRoot)}${sep5}${relPathInProject}`,
10510
10726
  description: title,
10511
10727
  path: entry.absPath,
10512
10728
  scope: "project",
@@ -10514,7 +10730,7 @@ function runbooksAdapter() {
10514
10730
  content: raw2,
10515
10731
  metadata: { relPathInProject, title },
10516
10732
  knowledge: {
10517
- kind: kindFromPath,
10733
+ kind: kindOf(relPathInProject),
10518
10734
  updatedAt: mtime?.toISOString(),
10519
10735
  tags
10520
10736
  }
@@ -10529,7 +10745,7 @@ function parseTags(value) {
10529
10745
  const tags = value.replace(/^\[|\]$/g, "").split(",").map((t) => t.trim()).filter(Boolean);
10530
10746
  return tags.length ? tags : void 0;
10531
10747
  }
10532
- function firstHeading(raw2) {
10748
+ function firstHeading2(raw2) {
10533
10749
  for (const line of raw2.split(/\r?\n/)) {
10534
10750
  const m = /^#{1,6}\s+(.*\S)\s*$/.exec(line.trim());
10535
10751
  if (m)
@@ -10539,12 +10755,12 @@ function firstHeading(raw2) {
10539
10755
  }
10540
10756
 
10541
10757
  // ../adapter-tickets/dist/index.js
10542
- import { readFile as readFile6, stat as stat4 } from "fs/promises";
10543
- import { basename as basename6, dirname as dirname8, relative as relative4, sep as sep5 } from "path";
10758
+ import { readFile as readFile7, stat as stat4 } from "fs/promises";
10759
+ import { basename as basename7, dirname as dirname9, relative as relative5, sep as sep6 } from "path";
10544
10760
  var TICKET_DIRS = /* @__PURE__ */ new Set(["tickets", "issues", "todo", "backlog"]);
10545
10761
  var PLAN_DIRS = /* @__PURE__ */ new Set(["plans", "plan", "roadmap", "milestones", "milestone"]);
10546
10762
  var CHECKBOX = /^\s*[-*+]\s+\[( |x|X)\]\s+/gm;
10547
- function segments2(relPath) {
10763
+ function segments3(relPath) {
10548
10764
  return relPath.split(/[\\/]/).filter(Boolean);
10549
10765
  }
10550
10766
  function deriveStatus(raw2) {
@@ -10582,33 +10798,33 @@ function ticketsAdapter() {
10582
10798
  matches(entry) {
10583
10799
  if (!entry.isFile || !entry.name.toLowerCase().endsWith(".md"))
10584
10800
  return false;
10585
- const parts = segments2(entry.relPath).slice(0, -1).map((p) => p.toLowerCase());
10801
+ const parts = segments3(entry.relPath).slice(0, -1).map((p) => p.toLowerCase());
10586
10802
  return parts.some((p) => TICKET_DIRS.has(p) || PLAN_DIRS.has(p));
10587
10803
  },
10588
10804
  async parse(entry, env) {
10589
10805
  let raw2;
10590
10806
  let mtime;
10591
10807
  try {
10592
- raw2 = await readFile6(entry.absPath, "utf8");
10808
+ raw2 = await readFile7(entry.absPath, "utf8");
10593
10809
  mtime = (await stat4(entry.absPath)).mtime;
10594
10810
  } catch {
10595
10811
  return [];
10596
10812
  }
10597
- const projectRoot = findProjectRoot(dirname8(entry.absPath), env.homeDir);
10598
- const relPathInProject = relative4(projectRoot, entry.absPath);
10599
- const isPlan = segments2(relPathInProject).slice(0, -1).some((p) => PLAN_DIRS.has(p.toLowerCase()));
10813
+ const projectRoot = findProjectRoot(dirname9(entry.absPath), env.homeDir);
10814
+ const relPathInProject = relative5(projectRoot, entry.absPath);
10815
+ const isPlan = segments3(relPathInProject).slice(0, -1).some((p) => PLAN_DIRS.has(p.toLowerCase()));
10600
10816
  const fm = parseFrontmatter(raw2);
10601
10817
  const data = fm.data;
10602
10818
  const derived2 = deriveStatus(raw2);
10603
10819
  const status = normaliseStatus(data["status"]) ?? derived2.status;
10604
- const title = data["title"] || firstHeading2(raw2) || basename6(entry.name, ".md");
10820
+ const title = data["title"] || firstHeading3(raw2) || basename7(entry.name, ".md");
10605
10821
  const tags = parseTags2(data["tags"]);
10606
10822
  return [
10607
10823
  {
10608
10824
  id: stableId(entry.absPath),
10609
10825
  format: "tickets",
10610
10826
  formatLabel: isPlan ? "Plan" : "Ticket",
10611
- name: `${basename6(projectRoot)}${sep5}${relPathInProject}`,
10827
+ name: `${basename7(projectRoot)}${sep6}${relPathInProject}`,
10612
10828
  description: title,
10613
10829
  path: entry.absPath,
10614
10830
  scope: "project",
@@ -10636,7 +10852,7 @@ function parseTags2(value) {
10636
10852
  const tags = value.replace(/^\[|\]$/g, "").split(",").map((t) => t.trim()).filter(Boolean);
10637
10853
  return tags.length ? tags : void 0;
10638
10854
  }
10639
- function firstHeading2(raw2) {
10855
+ function firstHeading3(raw2) {
10640
10856
  for (const line of raw2.split(/\r?\n/)) {
10641
10857
  const m = /^#{1,6}\s+(.*\S)\s*$/.exec(line.trim());
10642
10858
  if (m)
@@ -10713,6 +10929,7 @@ function knowledgeAdapters() {
10713
10929
  cursorRulesAdapter(),
10714
10930
  runbooksAdapter(),
10715
10931
  ticketsAdapter(),
10932
+ documentsAdapter(),
10716
10933
  gitLogAdapter()
10717
10934
  ];
10718
10935
  }
@@ -10720,76 +10937,116 @@ function openStore(projectRoot, embedder) {
10720
10937
  return new Store(dbPath(projectRoot), embedder.dimensions);
10721
10938
  }
10722
10939
  async function indexProject(projectRoot, embedder, onProgress) {
10723
- const started = Date.now();
10724
10940
  const store = openStore(projectRoot, embedder);
10725
10941
  try {
10726
- if (store.needsRebuild()) {
10727
- onProgress?.({ phase: "rebuild" });
10728
- store.reset();
10729
- }
10730
- onProgress?.({ phase: "scan" });
10731
- const result = await scan(knowledgeAdapters(), { roots: [projectRoot] });
10732
- const items = result.items.filter((i) => i.scope === "project" && isInside(projectRoot, i.path));
10733
- let indexed = 0;
10734
- let skipped = 0;
10735
- const seen = /* @__PURE__ */ new Set();
10736
- for (const item of items) {
10737
- seen.add(item.path);
10738
- const hash2 = sha1(item.content);
10739
- const existing = store.getItem(item.path);
10740
- if (existing && existing.content_hash === hash2) {
10741
- skipped++;
10742
- continue;
10743
- }
10744
- if (existing)
10745
- store.deleteItem(existing.id);
10746
- await indexItem(store, embedder, item, projectRoot, hash2);
10747
- indexed++;
10748
- onProgress?.({ phase: "embed", scanned: items.length, indexed, skipped });
10749
- }
10750
- const decisions = await indexDecisions(store, embedder, projectRoot);
10751
- for (const path of decisions.paths)
10752
- seen.add(path);
10753
- indexed += decisions.indexed;
10754
- skipped += decisions.skipped;
10755
- let removed = 0;
10756
- for (const row of store.listItems()) {
10757
- if (!seen.has(row.path)) {
10758
- store.deleteItem(row.id);
10759
- removed++;
10760
- }
10761
- }
10762
- const out = {
10763
- scanned: items.length + decisions.paths.length,
10764
- indexed,
10765
- skipped,
10766
- removed,
10767
- issues: result.issues.map((i) => ({ path: i.path, message: i.message })),
10768
- durationMs: Date.now() - started
10769
- };
10770
- onProgress?.({ phase: "done", ...out, issues: out.issues.length });
10771
- return out;
10942
+ return await indexInto(store, projectRoot, embedder, { onProgress });
10772
10943
  } finally {
10773
10944
  store.close();
10774
10945
  }
10775
10946
  }
10776
- async function indexDecisions(store, embedder, projectRoot) {
10777
- const out = { paths: [], indexed: 0, skipped: 0 };
10778
- for (const row of store.listDecisions()) {
10779
- const item = decisionItem(projectRoot, row);
10780
- out.paths.push(item.path);
10947
+ var TooManyChangesError = class extends Error {
10948
+ changed;
10949
+ limit;
10950
+ constructor(changed, limit) {
10951
+ super(`${changed} items changed since the last index \u2014 run \`capsa index\``);
10952
+ this.changed = changed;
10953
+ this.limit = limit;
10954
+ this.name = "TooManyChangesError";
10955
+ }
10956
+ };
10957
+ async function indexInto(store, projectRoot, embedder, options = {}) {
10958
+ const { onProgress, maxChanges } = options;
10959
+ const started = Date.now();
10960
+ if (store.needsRebuild()) {
10961
+ onProgress?.({ phase: "rebuild" });
10962
+ store.reset();
10963
+ }
10964
+ onProgress?.({ phase: "scan" });
10965
+ const result = await scan(knowledgeAdapters(), { roots: [projectRoot] });
10966
+ const items = result.items.filter((i) => i.scope === "project" && isInside(projectRoot, i.path));
10967
+ const all = [...items, ...decisionItems(store, projectRoot)];
10968
+ const { changed, skipped } = changedItems(store, all);
10969
+ if (maxChanges !== void 0 && changed.length > maxChanges) {
10970
+ throw new TooManyChangesError(changed.length, maxChanges);
10971
+ }
10972
+ let indexed = 0;
10973
+ for (const c of changed) {
10974
+ await indexItem(store, embedder, c.item, projectRoot, c.hash, c.previousId);
10975
+ indexed++;
10976
+ onProgress?.({ phase: "embed", scanned: all.length, indexed, skipped });
10977
+ }
10978
+ const seen = new Set(all.map((i) => i.path));
10979
+ let removed = 0;
10980
+ for (const row of store.listItems()) {
10981
+ if (!seen.has(row.path)) {
10982
+ store.deleteItem(row.id);
10983
+ removed++;
10984
+ }
10985
+ }
10986
+ const out = {
10987
+ scanned: all.length,
10988
+ indexed,
10989
+ skipped,
10990
+ removed,
10991
+ issues: result.issues.map((i) => ({ path: i.path, message: i.message })),
10992
+ durationMs: Date.now() - started
10993
+ };
10994
+ store.setMeta(META_INDEXED_AT, new Date(started).toISOString());
10995
+ store.deleteMeta(META_INDEX_FAILED_AT);
10996
+ store.deleteMeta(META_INDEX_ERROR);
10997
+ onProgress?.({ phase: "done", ...out, issues: out.issues.length });
10998
+ return out;
10999
+ }
11000
+ var DEFAULT_MAX_INDEX_AGE_MS = 6e4;
11001
+ var DEFAULT_MAX_REFRESH_CHANGES = 20;
11002
+ async function freshenIndex(store, projectRoot, embedder, options = {}) {
11003
+ const maxAge = options.maxAgeMs ?? DEFAULT_MAX_INDEX_AGE_MS;
11004
+ const maxChanges = options.maxChanges ?? DEFAULT_MAX_REFRESH_CHANGES;
11005
+ const now = Date.now();
11006
+ if (ageMs(store.getMeta(META_INDEXED_AT), now) <= maxAge)
11007
+ return { refreshed: false, stale: false };
11008
+ if (store.needsRebuild()) {
11009
+ return {
11010
+ refreshed: false,
11011
+ stale: true,
11012
+ reason: "this index was never built or its schema changed \u2014 run `capsa index`"
11013
+ };
11014
+ }
11015
+ if (ageMs(store.getMeta(META_INDEX_FAILED_AT), now) <= maxAge) {
11016
+ return { refreshed: false, stale: true, reason: store.getMeta(META_INDEX_ERROR) ?? "the last index run failed" };
11017
+ }
11018
+ try {
11019
+ const res = await indexInto(store, projectRoot, embedder, { maxChanges });
11020
+ return { refreshed: true, stale: false, indexed: res.indexed, removed: res.removed };
11021
+ } catch (err) {
11022
+ const reason = err instanceof Error ? err.message : String(err);
11023
+ store.setMeta(META_INDEX_FAILED_AT, (/* @__PURE__ */ new Date()).toISOString());
11024
+ store.setMeta(META_INDEX_ERROR, reason);
11025
+ return { refreshed: false, stale: true, reason };
11026
+ }
11027
+ }
11028
+ function ageMs(iso, now) {
11029
+ if (!iso)
11030
+ return Number.POSITIVE_INFINITY;
11031
+ const t = Date.parse(iso);
11032
+ return Number.isNaN(t) ? Number.POSITIVE_INFINITY : now - t;
11033
+ }
11034
+ function changedItems(store, items) {
11035
+ const changed = [];
11036
+ let skipped = 0;
11037
+ for (const item of items) {
10781
11038
  const hash2 = sha1(item.content);
10782
11039
  const existing = store.getItem(item.path);
10783
11040
  if (existing && existing.content_hash === hash2) {
10784
- out.skipped++;
11041
+ skipped++;
10785
11042
  continue;
10786
11043
  }
10787
- if (existing)
10788
- store.deleteItem(existing.id);
10789
- await indexItem(store, embedder, item, projectRoot, hash2);
10790
- out.indexed++;
11044
+ changed.push({ item, hash: hash2, previousId: existing?.id });
10791
11045
  }
10792
- return out;
11046
+ return { changed, skipped };
11047
+ }
11048
+ function decisionItems(store, projectRoot) {
11049
+ return store.listDecisions().map((row) => decisionItem(projectRoot, row));
10793
11050
  }
10794
11051
  async function recordDecision(store, embedder, projectRoot, input2) {
10795
11052
  const row = store.recordDecision(input2.title, input2.body, input2.related);
@@ -10801,37 +11058,44 @@ async function recordDecision(store, embedder, projectRoot, input2) {
10801
11058
  return { id: row.id, indexed: false, reason: err.message };
10802
11059
  }
10803
11060
  }
10804
- async function indexItem(store, embedder, item, projectRoot, hash2) {
10805
- const chunks = chunkMarkdown(item.content);
11061
+ async function indexItem(store, embedder, item, projectRoot, hash2, previousId) {
11062
+ const chunks = item.chunks ?? chunkMarkdown(item.content);
10806
11063
  const searchTexts = chunks.map((c) => withContext(item, c.headingPath, c.text));
10807
11064
  const vectors = searchTexts.length ? await embedder.embed(searchTexts) : [];
11065
+ if (previousId)
11066
+ store.deleteItem(previousId);
10808
11067
  const k = item.knowledge;
10809
11068
  store.upsertItem({
10810
11069
  id: item.id,
10811
11070
  format: item.format,
10812
11071
  kind: k?.kind ?? "instruction",
10813
11072
  name: item.name,
11073
+ // The same expression the provenance header uses (see withContext): the
11074
+ // title an item claims for itself, falling back to its name when the
11075
+ // format has none to give.
11076
+ title: item.description ?? item.name,
10814
11077
  path: item.path,
10815
- rel_path: relative5(projectRoot, item.path),
11078
+ rel_path: relative6(projectRoot, item.path),
10816
11079
  content_hash: hash2,
10817
11080
  status: k?.status ?? null,
10818
11081
  updated_at: k?.updatedAt ?? null,
10819
- tags: k?.tags ? JSON.stringify(k.tags) : null
11082
+ tags: k?.tags ? JSON.stringify(k.tags) : null,
11083
+ metadata: item.metadata ? JSON.stringify(item.metadata) : null
10820
11084
  });
10821
11085
  chunks.forEach((c, i) => store.insertChunk(item.id, c.index, c.headingPath, c.text, vectors[i], searchTexts[i]));
10822
11086
  }
10823
- function withContext(item, headingPath, text) {
11087
+ function withContext(item, headingPath, text2) {
10824
11088
  const kind = item.knowledge?.kind ?? "instruction";
10825
11089
  const head = [kind, item.description ?? item.name, ...headingPath].filter(Boolean).join(" \u203A ");
10826
11090
  return `${head}
10827
11091
 
10828
- ${text}`;
11092
+ ${text2}`;
10829
11093
  }
10830
11094
  function sha1(s) {
10831
11095
  return createHash2("sha1").update(s).digest("hex");
10832
11096
  }
10833
11097
  function isInside(root, path) {
10834
- const rel = relative5(root, path);
11098
+ const rel = relative6(root, path);
10835
11099
  return !!rel && !rel.startsWith("..") && !rel.includes(":");
10836
11100
  }
10837
11101
 
@@ -10852,6 +11116,13 @@ var DEFAULT_WEIGHTS = {
10852
11116
  plan: 1.05,
10853
11117
  ticket: 1,
10854
11118
  runbook: 1,
11119
+ // A book is reference, not project knowledge: neutral until the context
11120
+ // log says otherwise. It carries no recency and no status either — see
11121
+ // the documents adapter on what it refuses to claim.
11122
+ document: 1,
11123
+ // Never ranked: an outline has no chunks to rank. The map is total, so
11124
+ // it needs a number; 1.0 is the one that says nothing.
11125
+ outline: 1,
10855
11126
  doc: 0.95,
10856
11127
  wiki: 0.95,
10857
11128
  commit: 0.85
@@ -10884,15 +11155,19 @@ function rrf(lists, k = DEFAULT_WEIGHTS.rrfK) {
10884
11155
 
10885
11156
  // ../index/dist/pin.js
10886
11157
  var LEADING_ID = /^([A-Za-z]{1,8}-?\d{1,5}(?:-\d{1,5})?|\d{2,6})(?=[-_.\s]|$)/;
11158
+ var POSITIONAL_KINDS = /* @__PURE__ */ new Set(["document", "outline"]);
10887
11159
  function leadingId(relPath) {
10888
11160
  const base = relPath.split(/[\\/]/).pop() ?? "";
10889
11161
  return LEADING_ID.exec(base)?.[1];
10890
11162
  }
11163
+ function pinnableId(item) {
11164
+ return POSITIONAL_KINDS.has(item.kind) ? void 0 : leadingId(item.rel_path);
11165
+ }
10891
11166
  function pinnedItems(query, items) {
10892
11167
  const q = query.toLowerCase();
10893
11168
  const idCount = /* @__PURE__ */ new Map();
10894
11169
  for (const item of items) {
10895
- const id = leadingId(item.rel_path)?.toLowerCase();
11170
+ const id = pinnableId(item)?.toLowerCase();
10896
11171
  if (id)
10897
11172
  idCount.set(id, (idCount.get(id) ?? 0) + 1);
10898
11173
  }
@@ -10903,7 +11178,7 @@ function pinnedItems(query, items) {
10903
11178
  out.push(item);
10904
11179
  continue;
10905
11180
  }
10906
- const id = leadingId(item.rel_path);
11181
+ const id = pinnableId(item);
10907
11182
  if (!id || (idCount.get(id.toLowerCase()) ?? 0) > 1)
10908
11183
  continue;
10909
11184
  const re = new RegExp(`(^|[^\\w-])${escape2(id)}(?=$|[^\\w-])`, "i");
@@ -10923,6 +11198,7 @@ async function retrieve(store, embedder, query, options = {}) {
10923
11198
  const candidates = options.candidates ?? 30;
10924
11199
  const weights = options.weights ?? DEFAULT_WEIGHTS;
10925
11200
  const minRelative = options.minRelativeScore ?? 0.5;
11201
+ const perItemCap = { ...DEFAULT_PER_ITEM_CAP, ...options.perItemCap };
10926
11202
  const perItem = /* @__PURE__ */ new Map();
10927
11203
  const chunks = [];
10928
11204
  const debug = [];
@@ -10932,11 +11208,13 @@ async function retrieve(store, embedder, query, options = {}) {
10932
11208
  for (const c of store.chunksForItem(item.id)) {
10933
11209
  const t = estimateTokens(c.text);
10934
11210
  const fits = tokens + t <= maxTokens || chunks.length === 0;
11211
+ const metadata = parseMetadata(item.metadata);
10935
11212
  if (options.debug) {
10936
11213
  debug.push({
10937
11214
  chunkId: c.id,
10938
11215
  relPath: item.rel_path,
10939
11216
  headingPath: JSON.parse(c.heading_path),
11217
+ metadata,
10940
11218
  kind: item.kind,
10941
11219
  status: item.status,
10942
11220
  vecRank: null,
@@ -10961,16 +11239,25 @@ async function retrieve(store, embedder, query, options = {}) {
10961
11239
  relPath: item.rel_path,
10962
11240
  headingPath: JSON.parse(c.heading_path),
10963
11241
  text: c.text,
10964
- tokens: t
11242
+ tokens: t,
11243
+ ...metadata ? { metadata } : {}
10965
11244
  });
10966
11245
  }
10967
11246
  }
10968
11247
  const pinnedIds = new Set(pinned.map((i) => i.id));
10969
11248
  const neighbourCutoff = pinned.length ? Math.max(minRelative, 0.75) : minRelative;
10970
11249
  const neighbourBudget = pinned.length ? Math.min(maxTokens, tokens + Math.floor(maxTokens / 3)) : maxTokens;
10971
- const [qv] = await embedder.embed([query]);
10972
- const vec = qv ? store.vectorSearch(qv, candidates).map((r) => r.chunkId) : [];
10973
- const fts = store.textSearch(query, candidates).map((r) => r.chunkId);
11250
+ const searchText = neighbourQuery(query, pinned);
11251
+ let vec = [];
11252
+ let degraded;
11253
+ try {
11254
+ const [qv] = await embedder.embed([searchText]);
11255
+ if (qv)
11256
+ vec = store.vectorSearch(qv, candidates).map((r) => r.chunkId);
11257
+ } catch (err) {
11258
+ degraded = err instanceof Error ? err.message : String(err);
11259
+ }
11260
+ const fts = store.textSearch(searchText, candidates).map((r) => r.chunkId);
10974
11261
  const fused = rrf([vec, fts], weights.rrfK);
10975
11262
  const rows = store.chunksWithItems([...fused.keys()]).filter((r) => !pinnedIds.has(r.item_id));
10976
11263
  const now = Date.now();
@@ -10979,24 +11266,45 @@ async function retrieve(store, embedder, query, options = {}) {
10979
11266
  const factor = signalFactor({ kind: r.item.kind, status: r.item.status, updatedAt: r.item.updated_at }, weights, now);
10980
11267
  return { row: r, fusedScore, factor, score: fusedScore * factor };
10981
11268
  }).sort((a, b) => b.score - a.score);
10982
- const best = scored[0]?.score ?? 0;
10983
- let cut = tokens >= maxTokens;
10984
- let kept = 0;
10985
- for (const { row, fusedScore, factor, score } of scored) {
10986
- let why = "kept";
10987
- if (cut || kept > 0 && score < best * neighbourCutoff) {
10988
- cut = true;
10989
- why = "cutoff";
10990
- } else if ((perItem.get(row.item_id) ?? 0) >= 2) {
10991
- why = "per-item cap";
10992
- } else if (tokens + estimateTokens(row.text) > neighbourBudget && chunks.length > 0) {
10993
- why = "budget";
11269
+ const neighbourhood = [];
11270
+ for (const c of scored) {
11271
+ if (options.excludeRootInstructions && isRootInstruction(c.row.item.rel_path)) {
11272
+ if (options.debug) {
11273
+ debug.push({
11274
+ chunkId: c.row.id,
11275
+ relPath: c.row.item.rel_path,
11276
+ headingPath: JSON.parse(c.row.heading_path),
11277
+ metadata: parseMetadata(c.row.item.metadata),
11278
+ kind: c.row.item.kind,
11279
+ status: c.row.item.status,
11280
+ vecRank: rankIn(vec, c.row.id),
11281
+ ftsRank: rankIn(fts, c.row.id),
11282
+ fused: c.fusedScore,
11283
+ factor: c.factor,
11284
+ score: c.score,
11285
+ kept: false,
11286
+ why: "in prompt"
11287
+ });
11288
+ }
11289
+ continue;
10994
11290
  }
11291
+ neighbourhood.push(c);
11292
+ }
11293
+ const verdicts = keep(neighbourhood.map((c) => ({
11294
+ itemId: c.row.item_id,
11295
+ kind: c.row.item.kind,
11296
+ score: c.score,
11297
+ tokens: estimateTokens(c.row.text)
11298
+ })), { budget: neighbourBudget, minRelativeScore: neighbourCutoff, perItemCap }, { tokens, chunks: chunks.length, perItem });
11299
+ for (const [i, { row, fusedScore, factor, score }] of neighbourhood.entries()) {
11300
+ const why = verdicts[i];
11301
+ const metadata = parseMetadata(row.item.metadata);
10995
11302
  if (options.debug) {
10996
11303
  debug.push({
10997
11304
  chunkId: row.id,
10998
11305
  relPath: row.item.rel_path,
10999
11306
  headingPath: JSON.parse(row.heading_path),
11307
+ metadata,
11000
11308
  kind: row.item.kind,
11001
11309
  status: row.item.status,
11002
11310
  vecRank: rankIn(vec, row.id),
@@ -11011,9 +11319,7 @@ async function retrieve(store, embedder, query, options = {}) {
11011
11319
  if (why !== "kept")
11012
11320
  continue;
11013
11321
  const t = estimateTokens(row.text);
11014
- perItem.set(row.item_id, (perItem.get(row.item_id) ?? 0) + 1);
11015
11322
  tokens += t;
11016
- kept++;
11017
11323
  chunks.push({
11018
11324
  chunkId: row.id,
11019
11325
  score,
@@ -11023,31 +11329,123 @@ async function retrieve(store, embedder, query, options = {}) {
11023
11329
  relPath: row.item.rel_path,
11024
11330
  headingPath: JSON.parse(row.heading_path),
11025
11331
  text: row.text,
11026
- tokens: t
11332
+ tokens: t,
11333
+ ...metadata ? { metadata } : {}
11027
11334
  });
11028
- if (tokens >= neighbourBudget)
11029
- cut = true;
11030
11335
  }
11031
11336
  const durationMs = Date.now() - started;
11032
11337
  store.logContext({
11033
- source: options.source ?? "unknown",
11338
+ // A keyword-only delivery is logged under its own source. The week-4
11339
+ // gate is read from this table, and a run with half the retriever
11340
+ // missing is not a sample of how the ranking performs — it should show
11341
+ // up as its own bucket in a `group by source`, not quietly average in.
11342
+ source: (options.source ?? "unknown") + (degraded ? ":degraded" : ""),
11034
11343
  query,
11035
11344
  delivered: chunks.map((c) => ({ rel_path: c.relPath, heading_path: c.headingPath, tokens: c.tokens })),
11036
11345
  tokensEst: tokens,
11346
+ // The ceiling, not the spend: two rows are only comparable when this
11347
+ // matches, and retrieval stops at the relevance cliff well before it.
11348
+ tokensBudget: maxTokens,
11037
11349
  durationMs
11038
11350
  });
11039
- return { query, chunks, tokensEst: tokens, durationMs, ...options.debug ? { debug } : {} };
11351
+ return {
11352
+ query,
11353
+ searchText,
11354
+ chunks,
11355
+ tokensEst: tokens,
11356
+ durationMs,
11357
+ ...degraded ? { degraded } : {},
11358
+ ...options.debug ? { debug } : {}
11359
+ };
11360
+ }
11361
+ var PER_ITEM_CAP = 2;
11362
+ var DOCUMENT_PER_ITEM_CAP = 6;
11363
+ var DEFAULT_PER_ITEM_CAP = {
11364
+ instruction: PER_ITEM_CAP,
11365
+ doc: PER_ITEM_CAP,
11366
+ runbook: PER_ITEM_CAP,
11367
+ ticket: PER_ITEM_CAP,
11368
+ plan: PER_ITEM_CAP,
11369
+ decision: PER_ITEM_CAP,
11370
+ wiki: PER_ITEM_CAP,
11371
+ commit: PER_ITEM_CAP,
11372
+ document: DOCUMENT_PER_ITEM_CAP,
11373
+ // An outline has no chunks; the number is here because the map is total.
11374
+ outline: PER_ITEM_CAP
11375
+ };
11376
+ function keep(candidates, rules, start = { tokens: 0, chunks: 0, perItem: /* @__PURE__ */ new Map() }) {
11377
+ const best = candidates[0]?.score ?? 0;
11378
+ const perItem = new Map(start.perItem);
11379
+ let tokens = start.tokens;
11380
+ let chunks = start.chunks;
11381
+ let kept = 0;
11382
+ let end = tokens >= rules.budget ? "budget" : void 0;
11383
+ return candidates.map((c) => {
11384
+ if (end)
11385
+ return end;
11386
+ if (kept > 0 && c.score < best * rules.minRelativeScore)
11387
+ return end = "cutoff";
11388
+ const taken = perItem.get(c.itemId) ?? 0;
11389
+ if (taken >= (rules.perItemCap[c.kind] ?? PER_ITEM_CAP))
11390
+ return "per-item cap";
11391
+ if (chunks > 0 && tokens + c.tokens > rules.budget)
11392
+ return end = "budget";
11393
+ perItem.set(c.itemId, taken + 1);
11394
+ tokens += c.tokens;
11395
+ chunks++;
11396
+ kept++;
11397
+ if (tokens >= rules.budget)
11398
+ end = "budget";
11399
+ return "kept";
11400
+ });
11401
+ }
11402
+ function neighbourQuery(query, pinned) {
11403
+ if (pinned.length === 0)
11404
+ return query;
11405
+ return pinned.map((i) => i.title).join("; ");
11406
+ }
11407
+ var ROOT_INSTRUCTION_FILES = /* @__PURE__ */ new Set(["CLAUDE.md", "CLAUDE.local.md", "AGENTS.md"]);
11408
+ function isRootInstruction(relPath) {
11409
+ return ROOT_INSTRUCTION_FILES.has(relPath);
11040
11410
  }
11041
11411
  function rankIn(list, id) {
11042
11412
  const i = list.indexOf(id);
11043
11413
  return i === -1 ? null : i + 1;
11044
11414
  }
11415
+ function parseMetadata(json2) {
11416
+ if (!json2)
11417
+ return void 0;
11418
+ try {
11419
+ const parsed = JSON.parse(json2);
11420
+ return parsed && typeof parsed === "object" ? parsed : void 0;
11421
+ } catch {
11422
+ return void 0;
11423
+ }
11424
+ }
11425
+ function whereOf(c) {
11426
+ const book = text(c.metadata?.["book"]);
11427
+ if (c.kind !== "document" || !book)
11428
+ return [c.relPath, ...c.headingPath].join(" \u203A ");
11429
+ const title = text(c.metadata?.["title"]);
11430
+ const chapter = text(c.metadata?.["chapter"]);
11431
+ const head = [chapter ? `Ch. ${chapter}` : "", title].filter(Boolean).join(" ");
11432
+ const first = c.headingPath[0];
11433
+ const rest = title && first && first.toLowerCase().includes(title.toLowerCase()) ? c.headingPath.slice(1) : c.headingPath;
11434
+ return [book, head, ...rest].filter(Boolean).join(" \u203A ");
11435
+ }
11436
+ function text(value) {
11437
+ return value === void 0 || value === null ? "" : String(value);
11438
+ }
11045
11439
  function formatDebug(result) {
11046
11440
  if (!result.debug)
11047
11441
  return "";
11048
- const lines = [" vec fts factor score keep where"];
11442
+ const lines = [
11443
+ `stage 2 searched: ${JSON.stringify(result.searchText)}${result.searchText === result.query ? "" : " (from the pinned titles)"}`,
11444
+ "",
11445
+ " vec fts factor score keep where"
11446
+ ];
11049
11447
  for (const d of result.debug) {
11050
- const where = [d.relPath, ...d.headingPath].join(" \u203A ").slice(0, 70);
11448
+ const where = whereOf(d).slice(0, 100);
11051
11449
  lines.push([
11052
11450
  String(d.vecRank ?? "-").padStart(5),
11053
11451
  String(d.ftsRank ?? "-").padStart(4),
@@ -11063,14 +11461,32 @@ function formatContext(result) {
11063
11461
  if (result.chunks.length === 0)
11064
11462
  return "(no relevant project knowledge found)";
11065
11463
  return result.chunks.map((c) => {
11066
- const where = [c.relPath, ...c.headingPath].join(" \u203A ");
11067
- const meta3 = [c.kind, c.status, c.updatedAt?.slice(0, 10)].filter(Boolean).join(", ");
11068
- return `### ${where}
11464
+ const meta3 = citation(c) ?? [c.kind, c.status, c.updatedAt?.slice(0, 10)].filter(Boolean).join(", ");
11465
+ return `### ${whereOf(c)}
11069
11466
  _${meta3}_
11070
11467
 
11071
11468
  ${c.text.trim()}`;
11072
11469
  }).join("\n\n");
11073
11470
  }
11471
+ function citation(c) {
11472
+ const book = text(c.metadata?.["book"]);
11473
+ if (c.kind !== "document" || !book)
11474
+ return void 0;
11475
+ const title = text(c.metadata?.["title"]);
11476
+ const chapter = text(c.metadata?.["chapter"]);
11477
+ const head = [chapter ? `Ch. ${chapter}` : "", title ? `"${title}"` : ""].filter(Boolean).join(" ");
11478
+ const pp = pageRange(c.metadata?.["printed_pages"]);
11479
+ return [book, text(c.metadata?.["author"]), head, pp ? `pp. ${pp}` : ""].filter(Boolean).join(", ");
11480
+ }
11481
+ function pageRange(value) {
11482
+ if (!Array.isArray(value) || value.length === 0)
11483
+ return void 0;
11484
+ const from = text(value[0]);
11485
+ const to = text(value[value.length - 1]);
11486
+ if (!from)
11487
+ return void 0;
11488
+ return !to || to === from ? from : `${from}\u2013${to}`;
11489
+ }
11074
11490
 
11075
11491
  // ../index/dist/state.js
11076
11492
  function projectState(store) {
@@ -11081,7 +11497,8 @@ function projectState(store) {
11081
11497
  const live = (i) => (i.kind === "ticket" || i.kind === "plan") && (i.status === "open" || i.status === "in-progress");
11082
11498
  const rank = (i) => i.status === "in-progress" ? 0 : 1;
11083
11499
  const open2 = items.filter(live).sort((a, b) => rank(a) - rank(b) || a.rel_path.localeCompare(b.rel_path)).map((i) => ({ relPath: i.rel_path, name: i.name, status: i.status ?? "unknown", updatedAt: i.updated_at }));
11084
- const recentlyChanged = items.filter((i) => i.kind !== "commit").sort((a, b) => (b.updated_at ?? "").localeCompare(a.updated_at ?? "")).slice(0, 10).map((i) => ({ relPath: i.rel_path, kind: i.kind, updatedAt: i.updated_at }));
11500
+ const STILL = /* @__PURE__ */ new Set(["commit", "document", "outline"]);
11501
+ const recentlyChanged = items.filter((i) => !STILL.has(i.kind)).sort((a, b) => (b.updated_at ?? "").localeCompare(a.updated_at ?? "")).slice(0, 10).map((i) => ({ relPath: i.rel_path, kind: i.kind, updatedAt: i.updated_at }));
11085
11502
  const decisions = store.db.prepare("SELECT id, created_at, title FROM decisions ORDER BY id DESC LIMIT 10").all().map((d) => ({ id: d.id, createdAt: d.created_at, title: d.title }));
11086
11503
  return { counts, open: open2, recentlyChanged, decisions };
11087
11504
  }
@@ -16673,7 +17090,7 @@ __export(regexes_exports, {
16673
17090
  nanoid: () => nanoid,
16674
17091
  nanoidOfLength: () => nanoidOfLength,
16675
17092
  null: () => _null,
16676
- number: () => number,
17093
+ number: () => number2,
16677
17094
  rfc5322Email: () => rfc5322Email,
16678
17095
  sha1_base64: () => sha1_base64,
16679
17096
  sha1_base64url: () => sha1_base64url,
@@ -16774,7 +17191,7 @@ var string = (params) => {
16774
17191
  };
16775
17192
  var bigint = /^-?\d+n?$/;
16776
17193
  var integer = /^-?\d+$/;
16777
- var number = /^-?\d+(?:\.\d+)?$/;
17194
+ var number2 = /^-?\d+(?:\.\d+)?$/;
16778
17195
  var boolean = /^(?:true|false)$/i;
16779
17196
  var _null = /^null$/i;
16780
17197
  var _undefined = /^undefined$/i;
@@ -17901,7 +18318,7 @@ var $ZodCustomStringFormat = /* @__PURE__ */ $constructor("$ZodCustomStringForma
17901
18318
  });
17902
18319
  var $ZodNumber = /* @__PURE__ */ $constructor("$ZodNumber", (inst, def) => {
17903
18320
  $ZodType.init(inst, def);
17904
- inst._zod.pattern = number;
18321
+ inst._zod.pattern = number2;
17905
18322
  inst._zod.parse = (payload, _ctx) => {
17906
18323
  if (def.coerce)
17907
18324
  try {
@@ -18978,7 +19395,7 @@ var $ZodRecord = /* @__PURE__ */ $constructor("$ZodRecord", (inst, def) => {
18978
19395
  if (keyResult instanceof Promise) {
18979
19396
  throw new Error("Async schemas not supported in object keys currently");
18980
19397
  }
18981
- const checkNumericKey = typeof key === "string" && number.test(key) && keyResult.issues.length;
19398
+ const checkNumericKey = typeof key === "string" && number2.test(key) && keyResult.issues.length;
18982
19399
  if (checkNumericKey) {
18983
19400
  const retryResult = def.keyType._zod.run({ value: Number(key), issues: [] }, ctx);
18984
19401
  if (retryResult instanceof Promise) {
@@ -19545,7 +19962,7 @@ function leafPattern(schema) {
19545
19962
  if (minimum !== void 0 || maximum !== void 0)
19546
19963
  return string({ minimum, maximum }).source;
19547
19964
  const own2 = schema._zod.pattern;
19548
- return (isInt && own2 === number ? integer : own2)?.source;
19965
+ return (isInt && own2 === number2 ? integer : own2)?.source;
19549
19966
  }
19550
19967
  function partPattern(schema) {
19551
19968
  const def = schema._zod.def;
@@ -24043,11 +24460,11 @@ function ko_default() {
24043
24460
  }
24044
24461
 
24045
24462
  // ../../node_modules/.bun/zod@4.6.5/node_modules/zod/v4/locales/lt.js
24046
- var capitalizeFirstCharacter = (text) => {
24047
- return text.charAt(0).toUpperCase() + text.slice(1);
24463
+ var capitalizeFirstCharacter = (text2) => {
24464
+ return text2.charAt(0).toUpperCase() + text2.slice(1);
24048
24465
  };
24049
- function getUnitTypeFromNumber(number4) {
24050
- const abs = Math.abs(number4);
24466
+ function getUnitTypeFromNumber(number5) {
24467
+ const abs = Math.abs(number5);
24051
24468
  const last = abs % 10;
24052
24469
  const last2 = abs % 100;
24053
24470
  if (last2 >= 11 && last2 <= 19 || last === 0)
@@ -29019,7 +29436,7 @@ function generateRecordCheck(doc, ctx, schema, accessor) {
29019
29436
  if (keyFn.definite === false)
29020
29437
  ctx.definite = false;
29021
29438
  const keyFast = addConstant(ctx, keyFn);
29022
- const numericConst = addConstant(ctx, number);
29439
+ const numericConst = addConstant(ctx, number2);
29023
29440
  const outKeyVar = newVar(ctx);
29024
29441
  emitOwnKeys(doc, ctx, accessor, kVar, (d) => {
29025
29442
  d.write(`let ${outKeyVar} = ${keyFast}(${kVar});`);
@@ -31327,7 +31744,7 @@ function stringifyKeyNames(bySchema, json2, visited) {
31327
31744
  return rest;
31328
31745
  rest.type = "string";
31329
31746
  if (!values)
31330
- rest.pattern = (types.includes("number") ? number : integer).source;
31747
+ rest.pattern = (types.includes("number") ? number2 : integer).source;
31331
31748
  return rest;
31332
31749
  }
31333
31750
  var pendingRecords = /* @__PURE__ */ new WeakMap();
@@ -32229,7 +32646,7 @@ __export(external_exports, {
32229
32646
  null: () => _null3,
32230
32647
  nullable: () => nullable,
32231
32648
  nullish: () => nullish2,
32232
- number: () => number2,
32649
+ number: () => number3,
32233
32650
  object: () => object2,
32234
32651
  optional: () => optional,
32235
32652
  output: () => output,
@@ -32442,7 +32859,7 @@ __export(schemas_exports2, {
32442
32859
  null: () => _null3,
32443
32860
  nullable: () => nullable,
32444
32861
  nullish: () => nullish2,
32445
- number: () => number2,
32862
+ number: () => number3,
32446
32863
  object: () => object2,
32447
32864
  optional: () => optional,
32448
32865
  partialRecord: () => partialRecord,
@@ -33188,7 +33605,7 @@ var ZodNumber2 = /* @__PURE__ */ $constructor(
33188
33605
  }
33189
33606
  })
33190
33607
  );
33191
- function number2(params) {
33608
+ function number3(params) {
33192
33609
  return _number(ZodNumber2, params);
33193
33610
  }
33194
33611
  var ZodNumberFormat = /* @__PURE__ */ $constructor("ZodNumberFormat", (inst, def) => {
@@ -34028,7 +34445,7 @@ var stringbool = (...args) => _stringbool({
34028
34445
  }, ...args);
34029
34446
  function json(params) {
34030
34447
  const jsonSchema = lazy(() => {
34031
- return union([string2(params), number2(), boolean2(), _null3(), array(jsonSchema), record(string2(), jsonSchema)]);
34448
+ return union([string2(params), number3(), boolean2(), _null3(), array(jsonSchema), record(string2(), jsonSchema)]);
34032
34449
  });
34033
34450
  return jsonSchema;
34034
34451
  }
@@ -34872,13 +35289,13 @@ __export(coerce_exports2, {
34872
35289
  bigint: () => bigint3,
34873
35290
  boolean: () => boolean3,
34874
35291
  date: () => date4,
34875
- number: () => number3,
35292
+ number: () => number4,
34876
35293
  string: () => string3
34877
35294
  });
34878
35295
  function string3(params) {
34879
35296
  return _coercedString(ZodString2, params);
34880
35297
  }
34881
- function number3(params) {
35298
+ function number4(params) {
34882
35299
  return _coercedNumber(ZodNumber2, params);
34883
35300
  }
34884
35301
  function boolean3(params) {
@@ -34897,20 +35314,20 @@ var SUPPORTED_PROTOCOL_VERSIONS = [LATEST_PROTOCOL_VERSION, "2025-06-18", "2025-
34897
35314
  var RELATED_TASK_META_KEY = "io.modelcontextprotocol/related-task";
34898
35315
  var JSONRPC_VERSION = "2.0";
34899
35316
  var AssertObjectSchema = custom((v) => v !== null && (typeof v === "object" || typeof v === "function"));
34900
- var ProgressTokenSchema = union([string2(), number2().int()]);
35317
+ var ProgressTokenSchema = union([string2(), number3().int()]);
34901
35318
  var CursorSchema = string2();
34902
35319
  var TaskCreationParamsSchema = looseObject({
34903
35320
  /**
34904
35321
  * Requested duration in milliseconds to retain task from creation.
34905
35322
  */
34906
- ttl: number2().optional(),
35323
+ ttl: number3().optional(),
34907
35324
  /**
34908
35325
  * Time in milliseconds to wait between task status requests.
34909
35326
  */
34910
- pollInterval: number2().optional()
35327
+ pollInterval: number3().optional()
34911
35328
  });
34912
35329
  var TaskMetadataSchema = object2({
34913
- ttl: number2().optional()
35330
+ ttl: number3().optional()
34914
35331
  });
34915
35332
  var RelatedTaskMetadataSchema = object2({
34916
35333
  taskId: string2()
@@ -34965,7 +35382,7 @@ var ResultSchema = looseObject({
34965
35382
  */
34966
35383
  _meta: RequestMetaSchema.optional()
34967
35384
  });
34968
- var RequestIdSchema = union([string2(), number2().int()]);
35385
+ var RequestIdSchema = union([string2(), number3().int()]);
34969
35386
  var JSONRPCRequestSchema = object2({
34970
35387
  jsonrpc: literal(JSONRPC_VERSION),
34971
35388
  id: RequestIdSchema,
@@ -35001,7 +35418,7 @@ var JSONRPCErrorResponseSchema = object2({
35001
35418
  /**
35002
35419
  * The error type that occurred.
35003
35420
  */
35004
- code: number2().int(),
35421
+ code: number3().int(),
35005
35422
  /**
35006
35423
  * A short description of the error. The message SHOULD be limited to a concise single sentence.
35007
35424
  */
@@ -35300,11 +35717,11 @@ var ProgressSchema = object2({
35300
35717
  /**
35301
35718
  * The progress thus far. This should increase every time progress is made, even if the total is unknown.
35302
35719
  */
35303
- progress: number2(),
35720
+ progress: number3(),
35304
35721
  /**
35305
35722
  * Total number of items to process (or total progress required), if known.
35306
35723
  */
35307
- total: optional(number2()),
35724
+ total: optional(number3()),
35308
35725
  /**
35309
35726
  * An optional message describing the current progress.
35310
35727
  */
@@ -35347,7 +35764,7 @@ var TaskSchema = object2({
35347
35764
  * Time in milliseconds to keep task results available after completion.
35348
35765
  * If null, the task has unlimited lifetime until manually cleaned up.
35349
35766
  */
35350
- ttl: union([number2(), _null3()]),
35767
+ ttl: union([number3(), _null3()]),
35351
35768
  /**
35352
35769
  * ISO 8601 timestamp when the task was created.
35353
35770
  */
@@ -35356,7 +35773,7 @@ var TaskSchema = object2({
35356
35773
  * ISO 8601 timestamp when the task was last updated.
35357
35774
  */
35358
35775
  lastUpdatedAt: string2(),
35359
- pollInterval: optional(number2()),
35776
+ pollInterval: optional(number3()),
35360
35777
  /**
35361
35778
  * Optional diagnostic message for failed tasks or other status information.
35362
35779
  */
@@ -35441,7 +35858,7 @@ var AnnotationsSchema = object2({
35441
35858
  /**
35442
35859
  * Importance hint for the resource, from 0 (least) to 1 (most).
35443
35860
  */
35444
- priority: number2().min(0).max(1).optional(),
35861
+ priority: number3().min(0).max(1).optional(),
35445
35862
  /**
35446
35863
  * ISO 8601 timestamp for the most recent modification.
35447
35864
  */
@@ -35469,7 +35886,7 @@ var ResourceSchema = object2({
35469
35886
  *
35470
35887
  * This can be used by Hosts to display file sizes and estimate context window usage.
35471
35888
  */
35472
- size: optional(number2()),
35889
+ size: optional(number3()),
35473
35890
  /**
35474
35891
  * Optional annotations for the client.
35475
35892
  */
@@ -35892,7 +36309,7 @@ var ListChangedOptionsBaseSchema = object2({
35892
36309
  *
35893
36310
  * @default 300
35894
36311
  */
35895
- debounceMs: number2().int().nonnegative().default(300)
36312
+ debounceMs: number3().int().nonnegative().default(300)
35896
36313
  });
35897
36314
  var LoggingLevelSchema = _enum2(["debug", "info", "notice", "warning", "error", "critical", "alert", "emergency"]);
35898
36315
  var SetLevelRequestParamsSchema = BaseRequestParamsSchema.extend({
@@ -35937,15 +36354,15 @@ var ModelPreferencesSchema = object2({
35937
36354
  /**
35938
36355
  * How much to prioritize cost when selecting a model.
35939
36356
  */
35940
- costPriority: number2().min(0).max(1).optional(),
36357
+ costPriority: number3().min(0).max(1).optional(),
35941
36358
  /**
35942
36359
  * How much to prioritize sampling speed (latency) when selecting a model.
35943
36360
  */
35944
- speedPriority: number2().min(0).max(1).optional(),
36361
+ speedPriority: number3().min(0).max(1).optional(),
35945
36362
  /**
35946
36363
  * How much to prioritize intelligence and capabilities when selecting a model.
35947
36364
  */
35948
- intelligencePriority: number2().min(0).max(1).optional()
36365
+ intelligencePriority: number3().min(0).max(1).optional()
35949
36366
  });
35950
36367
  var ToolChoiceSchema = object2({
35951
36368
  /**
@@ -36003,13 +36420,13 @@ var CreateMessageRequestParamsSchema = TaskAugmentedRequestParamsSchema.extend({
36003
36420
  * declares ClientCapabilities.sampling.context. These values may be removed in future spec releases.
36004
36421
  */
36005
36422
  includeContext: _enum2(["none", "thisServer", "allServers"]).optional(),
36006
- temperature: number2().optional(),
36423
+ temperature: number3().optional(),
36007
36424
  /**
36008
36425
  * The requested maximum number of tokens to sample (to prevent runaway completions).
36009
36426
  *
36010
36427
  * The client MAY choose to sample fewer tokens than the requested maximum.
36011
36428
  */
36012
- maxTokens: number2().int(),
36429
+ maxTokens: number3().int(),
36013
36430
  stopSequences: array(string2()).optional(),
36014
36431
  /**
36015
36432
  * Optional metadata to pass through to the LLM provider. The format of this metadata is provider-specific.
@@ -36086,8 +36503,8 @@ var StringSchemaSchema = object2({
36086
36503
  type: literal("string"),
36087
36504
  title: string2().optional(),
36088
36505
  description: string2().optional(),
36089
- minLength: number2().optional(),
36090
- maxLength: number2().optional(),
36506
+ minLength: number3().optional(),
36507
+ maxLength: number3().optional(),
36091
36508
  format: _enum2(["email", "uri", "date", "date-time"]).optional(),
36092
36509
  default: string2().optional()
36093
36510
  });
@@ -36095,9 +36512,9 @@ var NumberSchemaSchema = object2({
36095
36512
  type: _enum2(["number", "integer"]),
36096
36513
  title: string2().optional(),
36097
36514
  description: string2().optional(),
36098
- minimum: number2().optional(),
36099
- maximum: number2().optional(),
36100
- default: number2().optional()
36515
+ minimum: number3().optional(),
36516
+ maximum: number3().optional(),
36517
+ default: number3().optional()
36101
36518
  });
36102
36519
  var UntitledSingleSelectEnumSchemaSchema = object2({
36103
36520
  type: literal("string"),
@@ -36129,8 +36546,8 @@ var UntitledMultiSelectEnumSchemaSchema = object2({
36129
36546
  type: literal("array"),
36130
36547
  title: string2().optional(),
36131
36548
  description: string2().optional(),
36132
- minItems: number2().optional(),
36133
- maxItems: number2().optional(),
36549
+ minItems: number3().optional(),
36550
+ maxItems: number3().optional(),
36134
36551
  items: object2({
36135
36552
  type: literal("string"),
36136
36553
  enum: array(string2())
@@ -36141,8 +36558,8 @@ var TitledMultiSelectEnumSchemaSchema = object2({
36141
36558
  type: literal("array"),
36142
36559
  title: string2().optional(),
36143
36560
  description: string2().optional(),
36144
- minItems: number2().optional(),
36145
- maxItems: number2().optional(),
36561
+ minItems: number3().optional(),
36562
+ maxItems: number3().optional(),
36146
36563
  items: object2({
36147
36564
  anyOf: array(object2({
36148
36565
  const: string2(),
@@ -36223,7 +36640,7 @@ var ElicitResultSchema = ResultSchema.extend({
36223
36640
  * Per MCP spec, content is "typically omitted" for decline/cancel actions.
36224
36641
  * We normalize null to undefined for leniency while maintaining type compatibility.
36225
36642
  */
36226
- content: preprocess((val) => val === null ? void 0 : val, record(string2(), union([string2(), number2(), boolean2(), array(string2())])).optional())
36643
+ content: preprocess((val) => val === null ? void 0 : val, record(string2(), union([string2(), number3(), boolean2(), array(string2())])).optional())
36227
36644
  });
36228
36645
  var ResourceTemplateReferenceSchema = object2({
36229
36646
  type: literal("ref/resource"),
@@ -36286,7 +36703,7 @@ var CompleteResultSchema = ResultSchema.extend({
36286
36703
  /**
36287
36704
  * The total number of completion options available. This can exceed the number of values actually sent in the response.
36288
36705
  */
36289
- total: optional(number2().int()),
36706
+ total: optional(number3().int()),
36290
36707
  /**
36291
36708
  * Indicates whether there are additional completion options beyond those provided in the current response, even if the exact total is unknown.
36292
36709
  */
@@ -40384,21 +40801,31 @@ var StdioServerTransport = class {
40384
40801
  function createCapsaServer(options) {
40385
40802
  const embedder = options.embedder ?? ollamaEmbedder();
40386
40803
  const server = new McpServer({ name: "capsa", version: "0.0.1" });
40804
+ const ensureFresh = freshener(options, embedder);
40387
40805
  server.registerTool("get_context", {
40388
40806
  title: "Get project context",
40389
- description: "Return the smallest set of project knowledge (runbooks, tickets, plans, decisions, instruction files) relevant to a task. Call this before starting work on a task; pass the task as you understand it.",
40807
+ description: 'Return the smallest set of project knowledge (runbooks, tickets, plans, decisions, instruction files) relevant to a task. Call this before starting work on a task; pass the task as you understand it. Answers are markdown: each passage has a `###` heading saying where it is from and an italic line under it. For a chapter of an indexed book that line is its citation \u2014 `_<book>, <author>, Ch. <n> "<title>", pp. <from>\u2013<to>_` \u2014 so quote the book, the chapter and the printed pages when you use the passage, rather than the file it was read from.',
40390
40808
  inputSchema: {
40391
40809
  task: external_exports.string().describe("The task or question, in one or two sentences"),
40392
- maxTokens: external_exports.number().int().min(200).max(6e3).optional().describe("Token budget, default 1500")
40810
+ // 3000 is a ceiling on a ceiling: retrieval stops at the relevance
40811
+ // cliff long before it fills a budget, so a larger number only ever
40812
+ // buys a longer tail of near-misses.
40813
+ maxTokens: external_exports.number().int().min(200).max(3e3).optional().describe("Token budget, default 1500. Raise it when this project is a library of reference documents rather than a codebase: a book section runs several hundred tokens, so 1500 is a handful of them.")
40393
40814
  }
40394
40815
  }, async ({ task, maxTokens }) => {
40816
+ const freshness = await ensureFresh();
40395
40817
  const store = openStore(options.projectRoot, embedder);
40396
40818
  try {
40397
- const result = await retrieve(store, embedder, task, { maxTokens, source: "mcp:get_context" });
40819
+ const result = await retrieve(store, embedder, task, {
40820
+ maxTokens,
40821
+ source: "mcp:get_context",
40822
+ excludeRootInstructions: true
40823
+ });
40824
+ const caveat = caveats(freshness, result.degraded);
40398
40825
  const footer = `
40399
40826
 
40400
40827
  ---
40401
- _capsa: ${result.chunks.length} chunks, ~${result.tokensEst} tokens, ${result.durationMs} ms_`;
40828
+ _capsa: ${result.chunks.length} chunks, ~${result.tokensEst} tokens, ${result.durationMs} ms${caveat ? ` \xB7 ${caveat}` : ""}_`;
40402
40829
  return { content: [{ type: "text", text: formatContext(result) + footer }] };
40403
40830
  } finally {
40404
40831
  store.close();
@@ -40409,9 +40836,14 @@ _capsa: ${result.chunks.length} chunks, ~${result.tokensEst} tokens, ${result.du
40409
40836
  description: "Where the project stands: open tickets and plans, recent decisions, recently changed knowledge.",
40410
40837
  inputSchema: {}
40411
40838
  }, async () => {
40839
+ const freshness = await ensureFresh();
40412
40840
  const store = openStore(options.projectRoot, embedder);
40413
40841
  try {
40414
- return { content: [{ type: "text", text: formatState(projectState(store)) }] };
40842
+ const caveat = caveats(freshness);
40843
+ const text2 = formatState(projectState(store)) + (caveat ? `
40844
+
40845
+ _${caveat}_` : "");
40846
+ return { content: [{ type: "text", text: text2 }] };
40415
40847
  } finally {
40416
40848
  store.close();
40417
40849
  }
@@ -40440,6 +40872,41 @@ _capsa: ${result.chunks.length} chunks, ~${result.tokensEst} tokens, ${result.du
40440
40872
  });
40441
40873
  return server;
40442
40874
  }
40875
+ function freshener(options, embedder) {
40876
+ const maxAgeMs = options.maxIndexAgeMs ?? DEFAULT_MAX_INDEX_AGE_MS;
40877
+ let inFlight = null;
40878
+ return () => {
40879
+ if (!inFlight) {
40880
+ const run = (async () => {
40881
+ let store;
40882
+ try {
40883
+ store = openStore(options.projectRoot, embedder);
40884
+ return await freshenIndex(store, options.projectRoot, embedder, { maxAgeMs });
40885
+ } catch (err) {
40886
+ return { refreshed: false, stale: true, reason: err instanceof Error ? err.message : String(err) };
40887
+ } finally {
40888
+ store?.close();
40889
+ }
40890
+ })();
40891
+ inFlight = run;
40892
+ void run.finally(() => {
40893
+ if (inFlight === run)
40894
+ inFlight = null;
40895
+ });
40896
+ }
40897
+ return inFlight;
40898
+ };
40899
+ }
40900
+ function caveats(freshness, degraded) {
40901
+ const parts = [];
40902
+ if (freshness.stale) {
40903
+ parts.push(`index may be out of date \u2014 ${freshness.reason ?? "the last index run did not finish"}`);
40904
+ }
40905
+ if (degraded) {
40906
+ parts.push(freshness.stale ? "keyword-only search (no embeddings)" : `keyword-only search \u2014 ${degraded}`);
40907
+ }
40908
+ return parts.join(" \xB7 ");
40909
+ }
40443
40910
  async function serveStdio(options) {
40444
40911
  const server = createCapsaServer(options);
40445
40912
  await server.connect(new StdioServerTransport());
@@ -40516,6 +40983,8 @@ async function runSearchCommand(args) {
40516
40983
  } else {
40517
40984
  console.log(formatContext(res));
40518
40985
  }
40986
+ if (res.degraded) console.error(`
40987
+ capsa: keyword-only search \u2014 ${res.degraded}`);
40519
40988
  console.error(`
40520
40989
  ${res.chunks.length} chunks, ~${res.tokensEst} tokens, ${res.durationMs} ms`);
40521
40990
  } finally {
@@ -40569,8 +41038,8 @@ agent explicitly records.
40569
41038
  `;
40570
41039
  async function readVersion() {
40571
41040
  try {
40572
- const pkgPath = resolve4(dirname9(fileURLToPath2(import.meta.url)), "../package.json");
40573
- const pkg = JSON.parse(await readFile7(pkgPath, "utf8"));
41041
+ const pkgPath = resolve4(dirname10(fileURLToPath2(import.meta.url)), "../package.json");
41042
+ const pkg = JSON.parse(await readFile8(pkgPath, "utf8"));
40574
41043
  const version2 = pkg.version;
40575
41044
  return typeof version2 === "string" ? version2 : "unknown";
40576
41045
  } catch {