@saasontools/strauss-kb 0.1.15 → 0.1.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -78,6 +78,7 @@ __export(index_exports, {
78
78
  RECORD_TYPES: () => RECORD_TYPES,
79
79
  SEARCH_INDEX_FILE: () => SEARCH_INDEX_FILE,
80
80
  TRACE_EDGES: () => TRACE_EDGES,
81
+ TreeSitterResolver: () => TreeSitterResolver,
81
82
  adjudicate: () => adjudicate,
82
83
  anchorFilePath: () => anchorFilePath,
83
84
  assertBaseNotFrozen: () => assertBaseNotFrozen,
@@ -92,9 +93,14 @@ __export(index_exports, {
92
93
  contextProfileBudgets: () => contextProfileBudgets,
93
94
  createKbMcpServer: () => createKbMcpServer,
94
95
  decisionInputSchema: () => decisionInputSchema,
96
+ defaultAnchorResolvers: () => defaultAnchorResolvers,
95
97
  detectAnchorDrift: () => detectAnchorDrift,
96
98
  doctor: () => doctor,
97
99
  edgeNeighbours: () => edgeNeighbours,
100
+ ensureGrammar: () => ensureGrammar,
101
+ grammarHints: () => grammarHints,
102
+ grammarManifest: () => grammarManifest,
103
+ grammarsCacheRoot: () => grammarsCacheRoot,
98
104
  hashAnchorText: () => hashAnchorText,
99
105
  impact: () => impact,
100
106
  inboundIndex: () => inboundIndex,
@@ -112,6 +118,7 @@ __export(index_exports, {
112
118
  kbRecordFrontmatterSchema: () => kbRecordFrontmatterSchema,
113
119
  kbSourceSchema: () => kbSourceSchema,
114
120
  kbVerifiedEventSchema: () => kbVerifiedEventSchema,
121
+ languageForFile: () => languageForFile,
115
122
  listPins: () => listPins,
116
123
  loadQmd: () => loadQmd,
117
124
  matchToDiff: () => matchToDiff,
@@ -121,6 +128,7 @@ __export(index_exports, {
121
128
  parseLog: () => parseLog,
122
129
  parseMarkdownWithFrontmatter: () => parseMarkdownWithFrontmatter,
123
130
  pinBase: () => pinBase,
131
+ prepareResolvers: () => prepareResolvers,
124
132
  readMergedPins: () => readMergedPins,
125
133
  readPinsLayer: () => readPinsLayer,
126
134
  readRemoteAnchors: () => readRemoteAnchors,
@@ -131,6 +139,7 @@ __export(index_exports, {
131
139
  renderLogEntry: () => renderLogEntry,
132
140
  repoCacheDir: () => repoCacheDir,
133
141
  resolveAnchor: () => resolveAnchor,
142
+ resolveAnchorSpan: () => resolveAnchorSpan,
134
143
  resolveHeads: () => resolveHeads,
135
144
  resolveHits: () => resolveHits,
136
145
  resolvePinPath: () => resolvePinPath,
@@ -143,15 +152,19 @@ __export(index_exports, {
143
152
  syncInstructions: () => syncInstructions,
144
153
  toHookJson: () => toHookJson,
145
154
  trace: () => trace,
155
+ treeSitterLanguages: () => treeSitterLanguages,
146
156
  unpinBase: () => unpinBase,
147
157
  validateBundle: () => validateBundle
148
158
  });
149
159
  module.exports = __toCommonJS(index_exports);
150
160
 
161
+ // ../../node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.26_supports-color@8.1.1_typescript@6.0.3_yaml@2.9.0/node_modules/tsup/assets/cjs_shims.js
162
+ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
163
+ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
164
+
151
165
  // src/kb-store.ts
152
- var import_node_crypto2 = require("crypto");
153
- var import_promises4 = require("fs/promises");
154
- var import_node_path4 = require("path");
166
+ var import_promises6 = require("fs/promises");
167
+ var import_node_path7 = require("path");
155
168
 
156
169
  // src/concurrency.ts
157
170
  var DEFAULT_IO_CONCURRENCY = 16;
@@ -252,7 +265,13 @@ var kbAnchorSchema = import_zod.z.object({
252
265
  /** ISO 8601 timestamp of the last successful resolution. */
253
266
  resolved_at: import_zod.z.string().min(1).optional(),
254
267
  /** Line count of the text the hash was taken over. */
255
- lines: import_zod.z.number().int().positive().optional()
268
+ lines: import_zod.z.number().int().positive().optional(),
269
+ /**
270
+ * Which resolver produced the hashed span. Absent means an anchor stamped
271
+ * before resolvers were named, which is read as `regex` — the only one
272
+ * there was. A hash from a different resolver is drift, not a match.
273
+ */
274
+ resolver: import_zod.z.enum(["tree-sitter", "regex"]).optional()
256
275
  }).strict();
257
276
  var kbLinkSchema = import_zod.z.object({
258
277
  target: import_zod.z.string().min(1),
@@ -345,6 +364,8 @@ var ErrorTypes = /* @__PURE__ */ ((ErrorTypes2) => {
345
364
  ErrorTypes2["KbPackBudgetExceeded"] = "KbPackBudgetExceeded";
346
365
  ErrorTypes2["KbRecordNotFound"] = "KbRecordNotFound";
347
366
  ErrorTypes2["KbSelfVerification"] = "KbSelfVerification";
367
+ ErrorTypes2["KbStampBaselineUnreadable"] = "KbStampBaselineUnreadable";
368
+ ErrorTypes2["KbStampDigestBaselineAmbiguous"] = "KbStampDigestBaselineAmbiguous";
348
369
  ErrorTypes2["KbUnknownLinkRel"] = "KbUnknownLinkRel";
349
370
  ErrorTypes2["KbWriteConflict"] = "KbWriteConflict";
350
371
  return ErrorTypes2;
@@ -499,6 +520,36 @@ var KbInvalidConceptIdError = class extends BaseError {
499
520
  });
500
521
  }
501
522
  };
523
+ var KbStampBaselineError = class extends BaseError {
524
+ constructor(since) {
525
+ super({
526
+ message: `kb: --since ${since} is neither a 64-character digest nor a readable stamp file`,
527
+ errorType: "KbStampBaselineUnreadable" /* KbStampBaselineUnreadable */,
528
+ code: 400,
529
+ fault: "User" /* User */,
530
+ retriable: false,
531
+ reportToUser: true,
532
+ details: { since }
533
+ });
534
+ this.since = since;
535
+ }
536
+ since;
537
+ };
538
+ var KbStampDigestBaselineError = class extends BaseError {
539
+ constructor(since) {
540
+ super({
541
+ message: `kb: --since ${since} is a digest, which needs --bundle (one base) \u2014 a file baseline works for many`,
542
+ errorType: "KbStampDigestBaselineAmbiguous" /* KbStampDigestBaselineAmbiguous */,
543
+ code: 400,
544
+ fault: "User" /* User */,
545
+ retriable: false,
546
+ reportToUser: true,
547
+ details: { since }
548
+ });
549
+ this.since = since;
550
+ }
551
+ since;
552
+ };
502
553
 
503
554
  // src/kb-index.ts
504
555
  var INDEX_FILE = "INDEX.md";
@@ -787,18 +838,18 @@ async function readOneRepo(repo, url, declared, context) {
787
838
  if (!repoUrlIsSafe(url)) return all({ ok: false, reason: "repo-invalid" });
788
839
  const cache = cachePathFor(repo, context.cacheDir);
789
840
  if (!cache) return all({ ok: false, reason: "remote-unreachable" });
790
- const rejected = /* @__PURE__ */ new Map();
841
+ const rejected2 = /* @__PURE__ */ new Map();
791
842
  const usable = [];
792
843
  for (const want of wants) {
793
844
  const reason = wantReason(want);
794
845
  if (reason)
795
- rejected.set(wantKey(repo, want.ref, want.file), { ok: false, reason });
846
+ rejected2.set(wantKey(repo, want.ref, want.file), { ok: false, reason });
796
847
  else usable.push(want);
797
848
  }
798
- if (!usable.length) return rejected;
849
+ if (!usable.length) return rejected2;
799
850
  wants = usable;
800
851
  const opened = await openCache(cache, url, context);
801
- if (opened) return new Map([...rejected, ...all(opened)]);
852
+ if (opened) return new Map([...rejected2, ...all(opened)]);
802
853
  const wantsDefault = wants.some((want) => want.ref === void 0);
803
854
  const branch = wantsDefault ? await defaultBranch(cache, context) : {};
804
855
  const revs = /* @__PURE__ */ new Map();
@@ -825,7 +876,7 @@ async function readOneRepo(repo, url, declared, context) {
825
876
  }
826
877
  );
827
878
  return new Map([
828
- ...rejected,
879
+ ...rejected2,
829
880
  ...wants.map(
830
881
  (want, at) => [wantKey(repo, want.ref, want.file), reads[at]]
831
882
  )
@@ -877,13 +928,13 @@ async function defaultBranch(cache, context) {
877
928
  if (!listed.ok) {
878
929
  const reason = transportReason(listed.stderr);
879
930
  if (reason !== "ref-not-found") {
880
- const cached2 = await cachedBranch(cache);
881
- return cached2 ? { name: cached2 } : { reason };
931
+ const cached3 = await cachedBranch(cache);
932
+ return cached3 ? { name: cached3 } : { reason };
882
933
  }
883
934
  }
884
935
  }
885
- const cached = await cachedBranch(cache);
886
- if (cached) return { name: cached };
936
+ const cached2 = await cachedBranch(cache);
937
+ if (cached2) return { name: cached2 };
887
938
  return {
888
939
  reason: context.offline ? "remote-unreachable" : "default-branch-unknown"
889
940
  };
@@ -910,8 +961,8 @@ async function ensureRev(cache, rev, context) {
910
961
  cwd: cache
911
962
  }
912
963
  );
913
- const cached = have.ok && have.stdout.trim().length > 0;
914
- if (cached && (context.offline || IMMUTABLE_REV.test(rev))) return void 0;
964
+ const cached2 = have.ok && have.stdout.trim().length > 0;
965
+ if (cached2 && (context.offline || IMMUTABLE_REV.test(rev))) return void 0;
915
966
  if (context.offline) return { ok: false, reason: "remote-unreachable" };
916
967
  const fetched = await git(
917
968
  [
@@ -927,7 +978,7 @@ async function ensureRev(cache, rev, context) {
927
978
  );
928
979
  if (!fetched.ok) {
929
980
  const reason = transportReason(fetched.stderr);
930
- if (cached && reason !== "ref-not-found") return void 0;
981
+ if (cached2 && reason !== "ref-not-found") return void 0;
931
982
  return { ok: false, reason };
932
983
  }
933
984
  const head = await git(["rev-parse", "FETCH_HEAD"], { cwd: cache });
@@ -1027,8 +1078,8 @@ function resolveHeads(from, byId) {
1027
1078
  while (queue.length) {
1028
1079
  const current = queue.shift();
1029
1080
  const next = successors(current, byId);
1030
- for (const missing of next.missing) {
1031
- warnings.push({ kind: "broken-chain", missing });
1081
+ for (const missing2 of next.missing) {
1082
+ warnings.push({ kind: "broken-chain", missing: missing2 });
1032
1083
  }
1033
1084
  if (!next.records.length) {
1034
1085
  if (current.conceptId !== from.conceptId)
@@ -1059,13 +1110,45 @@ function successors(record, byId) {
1059
1110
  }
1060
1111
  }
1061
1112
  const records = [];
1062
- const missing = [];
1113
+ const missing2 = [];
1063
1114
  for (const id of ids) {
1064
1115
  const found = byId.get(id);
1065
1116
  if (found) records.push(found);
1066
- else missing.push(id);
1117
+ else missing2.push(id);
1067
1118
  }
1068
- return { records, missing };
1119
+ return { records, missing: missing2 };
1120
+ }
1121
+
1122
+ // src/kb-stamp.ts
1123
+ var import_node_crypto = require("crypto");
1124
+ function sha256(contents) {
1125
+ return (0, import_node_crypto.createHash)("sha256").update(contents).digest("hex");
1126
+ }
1127
+ function bundleStamp(records, superseded) {
1128
+ const entries = [
1129
+ ...records.map((hit) => ({
1130
+ conceptId: hit.record.conceptId,
1131
+ digest: `current:${sha256(
1132
+ stringifyMarkdownWithFrontmatter(
1133
+ hit.record.body,
1134
+ hit.record.frontmatter
1135
+ )
1136
+ )}`
1137
+ })),
1138
+ ...superseded.map((entry) => ({
1139
+ conceptId: entry.conceptId,
1140
+ digest: `superseded:${sha256(JSON.stringify(entry))}`
1141
+ }))
1142
+ ].sort((a, b) => a.conceptId < b.conceptId ? -1 : 1);
1143
+ return {
1144
+ digest: sha256(
1145
+ entries.map((entry) => `${entry.conceptId}:${entry.digest}`).join("\n")
1146
+ ),
1147
+ records: entries
1148
+ };
1149
+ }
1150
+ function bundleDigest(records, superseded) {
1151
+ return bundleStamp(records, superseded).digest;
1069
1152
  }
1070
1153
 
1071
1154
  // src/anchor-resolver/read.ts
@@ -1159,63 +1242,580 @@ async function readAnchorFiles(files, read, concurrency = DEFAULT_IO_CONCURRENCY
1159
1242
  }
1160
1243
 
1161
1244
  // src/anchor-resolver/resolver.ts
1162
- var import_node_crypto = require("crypto");
1245
+ var import_node_crypto4 = require("crypto");
1246
+
1247
+ // src/tree-sitter-resolver/languages.ts
1248
+ var import_node_path5 = require("path");
1249
+
1250
+ // src/grammars/index.ts
1251
+ var import_promises4 = require("fs/promises");
1252
+
1253
+ // src/grammars/store.ts
1254
+ var import_node_crypto2 = require("crypto");
1255
+ var import_promises3 = require("fs/promises");
1256
+ var import_node_os2 = require("os");
1257
+ var import_node_path3 = require("path");
1258
+ function grammarsCacheRoot(override) {
1259
+ return override ?? process.env["STRAUSS_KB_GRAMMARS_DIR"] ?? (0, import_node_path3.join)((0, import_node_os2.homedir)(), ".strauss", "grammars");
1260
+ }
1261
+ function grammarCachePath(root, language, sha2564, extension = "wasm") {
1262
+ return (0, import_node_path3.join)(root, language, `${sha2564.slice(0, 12)}.${extension}`);
1263
+ }
1264
+ function sha2562(bytes) {
1265
+ return (0, import_node_crypto2.createHash)("sha256").update(bytes).digest("hex");
1266
+ }
1267
+ function matches(bytes, entry) {
1268
+ if (entry.bytes !== void 0 && bytes.byteLength !== entry.bytes)
1269
+ return false;
1270
+ return sha2562(bytes) === entry.sha256;
1271
+ }
1272
+ async function verifyCached(path, entry) {
1273
+ let bytes;
1274
+ try {
1275
+ bytes = await (0, import_promises3.readFile)(path);
1276
+ } catch {
1277
+ return false;
1278
+ }
1279
+ if (matches(bytes, entry)) return true;
1280
+ await (0, import_promises3.rm)(path, { force: true });
1281
+ return false;
1282
+ }
1283
+ async function writeCached(path, bytes) {
1284
+ await (0, import_promises3.mkdir)((0, import_node_path3.dirname)(path), { recursive: true });
1285
+ const temporary = `${path}.${process.pid}.${(0, import_node_crypto2.randomBytes)(4).toString("hex")}.tmp`;
1286
+ try {
1287
+ await (0, import_promises3.writeFile)(temporary, bytes);
1288
+ await (0, import_promises3.rename)(temporary, path);
1289
+ } catch (error) {
1290
+ await (0, import_promises3.rm)(temporary, { force: true });
1291
+ throw error;
1292
+ }
1293
+ }
1294
+
1295
+ // src/grammars/fetch.ts
1296
+ var ATTEMPTS = 3;
1297
+ var BACKOFF_MS = 250;
1298
+ function grammarUrl(url, override) {
1299
+ const base2 = grammarsBaseUrl(override);
1300
+ if (!base2) return url;
1301
+ let root = base2;
1302
+ while (root.endsWith("/")) root = root.slice(0, -1);
1303
+ const pinned = new URL(url);
1304
+ return `${root}${pinned.pathname}${pinned.search}`;
1305
+ }
1306
+ function grammarsBaseUrl(override) {
1307
+ return override ?? process.env["STRAUSS_KB_GRAMMARS_URL"];
1308
+ }
1309
+ async function downloadPart(url, name, entry, options = {}) {
1310
+ const log = options.log ?? ((line) => void process.stderr.write(line));
1311
+ const weight = entry.bytes === void 0 ? "" : ` (${size(entry.bytes)} from manifest)`;
1312
+ log(`strauss-kb: downloading ${name}${weight} from ${url}
1313
+ `);
1314
+ let cause = "";
1315
+ for (let attempt = 1; attempt <= ATTEMPTS; attempt++) {
1316
+ const outcome = await attemptDownload(url, entry, options.fetchTimeoutMs);
1317
+ if ("bytes" in outcome) return outcome;
1318
+ cause = outcome.cause;
1319
+ log(
1320
+ `strauss-kb: ${name} attempt ${attempt}/${ATTEMPTS} failed: ${cause}
1321
+ `
1322
+ );
1323
+ if (!outcome.retry) break;
1324
+ if (attempt < ATTEMPTS) await pause(BACKOFF_MS * attempt);
1325
+ }
1326
+ log(`strauss-kb: ${name} not downloaded: ${cause}
1327
+ `);
1328
+ return { cause };
1329
+ }
1330
+ async function attemptDownload(url, entry, timeoutMs) {
1331
+ try {
1332
+ const response = await fetch(url, {
1333
+ signal: AbortSignal.timeout(fetchTimeoutMs(timeoutMs))
1334
+ });
1335
+ if (!response.ok) {
1336
+ return {
1337
+ cause: `HTTP ${response.status}`,
1338
+ retry: response.status >= 500 || response.status === 429
1339
+ };
1340
+ }
1341
+ const bytes = new Uint8Array(await response.arrayBuffer());
1342
+ if (!matches(bytes, entry))
1343
+ return { cause: "sha256 mismatch", retry: false };
1344
+ return { bytes };
1345
+ } catch (error) {
1346
+ const timedOut = error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError");
1347
+ return { cause: timedOut ? "timeout" : "network error", retry: true };
1348
+ }
1349
+ }
1350
+ function size(bytes) {
1351
+ return bytes >= 1024 * 1024 ? `${(bytes / (1024 * 1024)).toFixed(1)} MB` : `${Math.round(bytes / 1024)} KB`;
1352
+ }
1353
+ function pause(ms) {
1354
+ return new Promise((resolve6) => setTimeout(resolve6, ms));
1355
+ }
1356
+
1357
+ // src/grammars/manifest.ts
1358
+ var import_node_fs = require("fs");
1359
+ var import_node_path4 = require("path");
1360
+ var import_node_url = require("url");
1361
+
1362
+ // src/grammars/model.ts
1363
+ var import_zod2 = require("zod");
1364
+ var sha2563 = import_zod2.z.string().regex(/^[0-9a-f]{64}$/);
1365
+ var grammarWasmSchema = import_zod2.z.object({
1366
+ url: import_zod2.z.string().min(1),
1367
+ sha256: sha2563,
1368
+ bytes: import_zod2.z.number().int().positive()
1369
+ });
1370
+ var grammarTagsSchema = import_zod2.z.object({ url: import_zod2.z.string().min(1), sha256: sha2563 });
1371
+ var grammarPackSchema = import_zod2.z.object({
1372
+ package: import_zod2.z.string().min(1),
1373
+ wasm: grammarWasmSchema,
1374
+ tags: import_zod2.z.array(grammarTagsSchema),
1375
+ license: import_zod2.z.string().min(1),
1376
+ extensions: import_zod2.z.array(import_zod2.z.string().min(1))
1377
+ });
1378
+ var grammarManifestSchema = import_zod2.z.object({
1379
+ /** The runtime the packs were proved against. */
1380
+ webTreeSitter: import_zod2.z.string().min(1),
1381
+ linguist: import_zod2.z.object({ tag: import_zod2.z.string().min(1), commit: import_zod2.z.string().min(1) }),
1382
+ packs: import_zod2.z.record(import_zod2.z.string().min(1), grammarPackSchema)
1383
+ });
1384
+
1385
+ // src/grammars/manifest.ts
1386
+ var cached;
1387
+ function grammarManifest() {
1388
+ cached ??= grammarManifestSchema.parse(
1389
+ JSON.parse((0, import_node_fs.readFileSync)(grammarsDataPath("manifest.json"), "utf8"))
1390
+ );
1391
+ return cached;
1392
+ }
1393
+ function grammarsDataPath(...segments) {
1394
+ let dir = (0, import_node_path4.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
1395
+ for (let up = 0; up < 5; up++) {
1396
+ const candidate = (0, import_node_path4.join)(dir, "grammars");
1397
+ if ((0, import_node_fs.existsSync)((0, import_node_path4.join)(candidate, "manifest.json")))
1398
+ return (0, import_node_path4.join)(candidate, ...segments);
1399
+ dir = (0, import_node_path4.dirname)(dir);
1400
+ }
1401
+ throw new Error("grammars/manifest.json is missing from the package");
1402
+ }
1403
+
1404
+ // src/grammars/index.ts
1405
+ var inFlight = /* @__PURE__ */ new Map();
1406
+ var missing = /* @__PURE__ */ new Map();
1407
+ var uncompilable = /* @__PURE__ */ new Map();
1408
+ var rejected = /* @__PURE__ */ new Map();
1409
+ function grammarsDownloadDisabled() {
1410
+ return process.env["STRAUSS_KB_GRAMMARS"] === "off";
1411
+ }
1412
+ async function ensureGrammar(language, options = {}) {
1413
+ const pack2 = grammarManifest().packs[language];
1414
+ if (!pack2) return null;
1415
+ const root = grammarsCacheRoot(options.cacheRoot);
1416
+ const wasm = grammarCachePath(root, language, pack2.wasm.sha256);
1417
+ const key2 = `${wasm} ${grammarsBaseUrl(options.baseUrl) ?? ""}`;
1418
+ const existing = inFlight.get(key2);
1419
+ if (existing) return existing;
1420
+ const pending = (async () => {
1421
+ const grammar = await ensurePart(
1422
+ wasm,
1423
+ `tree-sitter-${language}`,
1424
+ pack2.wasm,
1425
+ options
1426
+ );
1427
+ if (grammar !== true)
1428
+ return miss(language, `grammar tree-sitter-${language}`, grammar);
1429
+ const parts = [];
1430
+ const total = pack2.tags.length;
1431
+ for (const [at, part] of pack2.tags.entries()) {
1432
+ const name = `${language} tags${total > 1 ? ` part ${at + 1}/${total}` : ""}`;
1433
+ const path = grammarCachePath(root, language, part.sha256, "scm");
1434
+ const held = await ensurePart(path, name, part, options);
1435
+ if (held !== true) return miss(language, name, held);
1436
+ parts.push(`; ${part.url}
1437
+ ${lf(await (0, import_promises4.readFile)(path, "utf8"))}`);
1438
+ }
1439
+ missing.delete(language);
1440
+ return { wasm, query: total ? parts.join("\n") : void 0 };
1441
+ })();
1442
+ inFlight.set(key2, pending);
1443
+ const result = await pending;
1444
+ if (result === null) inFlight.delete(key2);
1445
+ return result;
1446
+ }
1447
+ async function ensurePart(path, name, entry, options) {
1448
+ if (await verifyCached(path, entry)) return true;
1449
+ if (options.offline === true || grammarsDownloadDisabled()) return {};
1450
+ const download = await downloadPart(
1451
+ grammarUrl(entry.url, options.baseUrl),
1452
+ name,
1453
+ entry,
1454
+ options
1455
+ );
1456
+ if ("cause" in download) return { cause: download.cause };
1457
+ await writeCached(path, download.bytes).catch(() => null);
1458
+ return true;
1459
+ }
1460
+ function miss(language, subject, failure) {
1461
+ missing.set(language, { subject, ...failure });
1462
+ return null;
1463
+ }
1464
+ function lf(body) {
1465
+ return body.replace(/\r\n/g, "\n");
1466
+ }
1467
+ function noteUncompilableQuery(language, cause) {
1468
+ uncompilable.set(language, cause);
1469
+ }
1470
+ function noteRejectedGrammar(language, cause) {
1471
+ rejected.set(language, cause);
1472
+ }
1473
+ function grammarHints() {
1474
+ const manifest = grammarManifest();
1475
+ const packs = manifest.packs;
1476
+ const lines = /* @__PURE__ */ new Map();
1477
+ for (const [language, { subject, cause }] of missing)
1478
+ lines.set(
1479
+ language,
1480
+ `${subject} not cached${cause ? ` (${cause})` : ""}; run online once, or set STRAUSS_KB_GRAMMARS_DIR`
1481
+ );
1482
+ for (const [language, cause] of rejected)
1483
+ lines.set(
1484
+ language,
1485
+ `${packs[language]?.package ?? `tree-sitter-${language}`} rejected by web-tree-sitter ${manifest.webTreeSitter}${cause ? `: ${cause}` : ""}; re-pin with pnpm grammars pin ${language}`
1486
+ );
1487
+ for (const [language, cause] of uncompilable)
1488
+ lines.set(
1489
+ language,
1490
+ `tags query for ${language} does not compile against ${packs[language]?.package ?? `tree-sitter-${language}`}: ${cause}; re-pin with pnpm grammars pin ${language}`
1491
+ );
1492
+ return [...lines].sort(([a], [b]) => a.localeCompare(b)).map(([, line]) => line);
1493
+ }
1494
+
1495
+ // src/tree-sitter-resolver/languages.ts
1496
+ var table;
1497
+ function extensionTable() {
1498
+ const manifest = grammarManifest();
1499
+ if (table?.of !== manifest)
1500
+ table = {
1501
+ of: manifest,
1502
+ extensions: Object.fromEntries(
1503
+ Object.entries(manifest.packs).flatMap(
1504
+ ([language, pack2]) => pack2.extensions.map((extension) => [extension, language])
1505
+ )
1506
+ )
1507
+ };
1508
+ return table.extensions;
1509
+ }
1510
+ function hasQuery(language) {
1511
+ return (grammarManifest().packs[language]?.tags.length ?? 0) > 0;
1512
+ }
1513
+ function languageForFile(file) {
1514
+ const language = extensionTable()[(0, import_node_path5.extname)(file).toLowerCase()];
1515
+ return language && hasQuery(language) ? language : void 0;
1516
+ }
1517
+ function treeSitterLanguages() {
1518
+ return [...new Set(Object.values(extensionTable()))].filter(hasQuery).sort();
1519
+ }
1520
+
1521
+ // src/tree-sitter-resolver/resolver.ts
1522
+ var import_node_crypto3 = require("crypto");
1523
+ var import_web_tree_sitter = require("web-tree-sitter");
1524
+
1525
+ // src/tree-sitter-resolver/definitions.ts
1526
+ var SCOPE_ONLY = "reference.implementation";
1527
+ function index(tree, query) {
1528
+ const byName = /* @__PURE__ */ new Map();
1529
+ for (const match of query.matches(tree.rootNode)) {
1530
+ const nameNode = match.captures.find((capture) => capture.name === "name");
1531
+ const defNode = match.captures.find(
1532
+ (capture) => capture.name.startsWith("definition.") || capture.name === SCOPE_ONLY
1533
+ );
1534
+ if (!nameNode || !defNode) continue;
1535
+ const candidate = {
1536
+ node: defNode.node,
1537
+ name: nameNode.node.text,
1538
+ target: defNode.name !== SCOPE_ONLY
1539
+ };
1540
+ const existing = byName.get(nameNode.node.id);
1541
+ if (existing && width(existing.node) <= width(candidate.node)) continue;
1542
+ byName.set(nameNode.node.id, candidate);
1543
+ }
1544
+ const definitions = [...byName.values()];
1545
+ return {
1546
+ tree,
1547
+ byNodeId: new Map(
1548
+ definitions.map((definition) => [definition.node.id, definition])
1549
+ ),
1550
+ definitions
1551
+ };
1552
+ }
1553
+ function select(parsed, wanted) {
1554
+ const matches3 = parsed.definitions.filter(
1555
+ (definition) => definition.target && endsWith(chainOf(definition, parsed.byNodeId), wanted)
1556
+ );
1557
+ if (matches3.length < 2) return matches3;
1558
+ const bodied = matches3.filter(
1559
+ (definition) => definition.node.childForFieldName("body") !== null
1560
+ );
1561
+ return bodied.length === 1 ? bodied : matches3;
1562
+ }
1563
+ function chainOf(definition, byNodeId) {
1564
+ const chain = [definition.name];
1565
+ const receiver = definition.node.childForFieldName("receiver");
1566
+ const type = receiver && typeNameIn(receiver);
1567
+ if (type) chain.unshift(type);
1568
+ for (let node = definition.node.parent; node; node = node.parent) {
1569
+ const enclosing = byNodeId.get(node.id);
1570
+ if (enclosing && enclosing.node !== definition.node)
1571
+ chain.unshift(enclosing.name);
1572
+ }
1573
+ return chain;
1574
+ }
1575
+ function typeNameIn(receiver) {
1576
+ const stack = [receiver];
1577
+ while (stack.length) {
1578
+ const node = stack.pop();
1579
+ if (node.type === "type_identifier") return node.text;
1580
+ for (let at = 0; at < node.childCount; at++) {
1581
+ const child = node.child(at);
1582
+ if (child) stack.push(child);
1583
+ }
1584
+ }
1585
+ return void 0;
1586
+ }
1587
+ function endsWith(chain, wanted) {
1588
+ if (wanted.length > chain.length) return false;
1589
+ const offset = chain.length - wanted.length;
1590
+ return wanted.every((segment, at) => chain[offset + at] === segment);
1591
+ }
1592
+ function width(node) {
1593
+ return node.endIndex - node.startIndex;
1594
+ }
1595
+ function spanOf(definition, source) {
1596
+ let start = definition.node;
1597
+ let end = definition.node;
1598
+ for (let sibling = start.previousSibling; sibling?.type === "decorator"; sibling = sibling.previousSibling) {
1599
+ start = sibling;
1600
+ }
1601
+ const parent = end.parent;
1602
+ if (parent?.type === "export_statement" && parent.childForFieldName("declaration")?.id === end.id) {
1603
+ start = parent;
1604
+ end = parent;
1605
+ }
1606
+ const lines = source.split("\n");
1607
+ const startLine = start.startPosition.row;
1608
+ const endLine = end.endPosition.column === 0 && end.endPosition.row > startLine ? end.endPosition.row - 1 : end.endPosition.row;
1609
+ return {
1610
+ text: lines.slice(startLine, endLine + 1).join("\n"),
1611
+ startLine: startLine + 1,
1612
+ endLine: endLine + 1
1613
+ };
1614
+ }
1615
+
1616
+ // src/tree-sitter-resolver/resolver.ts
1617
+ var TREE_CACHE_LIMIT = 32;
1618
+ var TreeSitterResolver = class {
1619
+ name = "tree-sitter";
1620
+ grammars;
1621
+ loaded = /* @__PURE__ */ new Map();
1622
+ trees = /* @__PURE__ */ new Map();
1623
+ parser;
1624
+ initialized = false;
1625
+ /** Cache effectiveness, for tests and for the latency numbers. */
1626
+ stats = { parses: 0, cacheHits: 0 };
1627
+ constructor(options = {}) {
1628
+ this.grammars = options;
1629
+ }
1630
+ /**
1631
+ * Loads the grammars these files need, once per language per process,
1632
+ * downloading each one on first use.
1633
+ *
1634
+ * A grammar that will not load is remembered as unavailable rather than
1635
+ * retried per anchor, and never throws: an unobtainable WASM is a finding.
1636
+ */
1637
+ async prepare(files) {
1638
+ const wanted = /* @__PURE__ */ new Set();
1639
+ for (const file of files) {
1640
+ const language = languageForFile(file);
1641
+ if (language && !this.loaded.has(language)) wanted.add(language);
1642
+ }
1643
+ if (!wanted.size) return;
1644
+ if (!this.initialized) {
1645
+ try {
1646
+ await import_web_tree_sitter.Parser.init();
1647
+ this.parser = new import_web_tree_sitter.Parser();
1648
+ this.initialized = true;
1649
+ } catch {
1650
+ for (const language of wanted) this.loaded.set(language, null);
1651
+ return;
1652
+ }
1653
+ }
1654
+ const languages = [...wanted];
1655
+ const loaded = await mapLimit(
1656
+ languages,
1657
+ Math.min(DEFAULT_IO_CONCURRENCY, languages.length),
1658
+ (language) => this.load(language)
1659
+ );
1660
+ languages.forEach(
1661
+ (language, at) => this.loaded.set(language, loaded[at] ?? null)
1662
+ );
1663
+ }
1664
+ /**
1665
+ * An unobtainable grammar, one this runtime refuses, and a query that will
1666
+ * not compile are three faults with three repairs; all are reported through
1667
+ * the grammars module so every hint has one home.
1668
+ */
1669
+ async load(language) {
1670
+ let pack2;
1671
+ try {
1672
+ pack2 = await ensureGrammar(language, this.grammars);
1673
+ } catch {
1674
+ return null;
1675
+ }
1676
+ if (!pack2?.query) return null;
1677
+ let grammar;
1678
+ try {
1679
+ grammar = await import_web_tree_sitter.Language.load(pack2.wasm);
1680
+ } catch (error) {
1681
+ noteRejectedGrammar(language, why(error));
1682
+ return null;
1683
+ }
1684
+ try {
1685
+ return { language: grammar, query: new import_web_tree_sitter.Query(grammar, pack2.query) };
1686
+ } catch (error) {
1687
+ noteUncompilableQuery(language, why(error));
1688
+ return null;
1689
+ }
1690
+ }
1691
+ /**
1692
+ * Abstains on an extension with no grammar so the regex resolver gets a
1693
+ * turn; reports `resolver-unavailable` when the grammar exists in principle
1694
+ * but could not be loaded, because falling back there would silently trade a
1695
+ * precise span for a guessed one.
1696
+ */
1697
+ attempt(source, symbol, file) {
1698
+ const language = file ? languageForFile(file) : void 0;
1699
+ if (!language) return { kind: "abstain" };
1700
+ if (!this.loaded.has(language)) return { kind: "abstain" };
1701
+ const loaded = this.loaded.get(language);
1702
+ if (!loaded) return { kind: "unresolved", reason: "resolver-unavailable" };
1703
+ const parsed = this.parse(language, loaded, source);
1704
+ if (!parsed) return { kind: "unresolved", reason: "resolver-unavailable" };
1705
+ const wanted = symbol.split(".").filter(Boolean);
1706
+ if (!wanted.length)
1707
+ return { kind: "unresolved", reason: "symbol-not-found" };
1708
+ const matches3 = select(parsed, wanted);
1709
+ if (!matches3.length)
1710
+ return { kind: "unresolved", reason: "symbol-not-found" };
1711
+ if (matches3.length > 1)
1712
+ return { kind: "unresolved", reason: "symbol-ambiguous" };
1713
+ return { kind: "resolved", span: spanOf(matches3[0], source) };
1714
+ }
1715
+ resolve(source, symbol, file) {
1716
+ const attempt = this.attempt(source, symbol, file);
1717
+ return attempt.kind === "resolved" ? attempt.span : null;
1718
+ }
1719
+ /** Parsed trees are keyed by content hash, so an unchanged file parses once. */
1720
+ parse(language, loaded, source) {
1721
+ const key2 = `${language}:${(0, import_node_crypto3.createHash)("sha256").update(source).digest("hex")}`;
1722
+ const cached2 = this.trees.get(key2);
1723
+ if (cached2) {
1724
+ this.stats.cacheHits += 1;
1725
+ return cached2;
1726
+ }
1727
+ const parser = this.parser;
1728
+ if (!parser) return null;
1729
+ let parsed;
1730
+ try {
1731
+ parser.setLanguage(loaded.language);
1732
+ const tree = parser.parse(source);
1733
+ if (!tree) return null;
1734
+ parsed = index(tree, loaded.query);
1735
+ } catch {
1736
+ return null;
1737
+ }
1738
+ this.stats.parses += 1;
1739
+ if (this.trees.size >= TREE_CACHE_LIMIT) {
1740
+ const oldest = this.trees.keys().next();
1741
+ if (!oldest.done) {
1742
+ this.trees.get(oldest.value)?.tree.delete();
1743
+ this.trees.delete(oldest.value);
1744
+ }
1745
+ }
1746
+ this.trees.set(key2, parsed);
1747
+ return parsed;
1748
+ }
1749
+ /** Drops cached trees. Grammars stay loaded — they are immutable. */
1750
+ reset() {
1751
+ for (const parsed of this.trees.values()) parsed.tree.delete();
1752
+ this.trees.clear();
1753
+ this.stats.parses = 0;
1754
+ this.stats.cacheHits = 0;
1755
+ }
1756
+ };
1757
+ function why(error) {
1758
+ const text = error instanceof Error ? error.message : String(error);
1759
+ return text || "no reason given";
1760
+ }
1761
+
1762
+ // src/anchor-resolver/resolver.ts
1163
1763
  var PARENT_SCOPE_LINES = 50;
1164
1764
  var CLEAN_STATE = { blockComment: false, template: false };
1165
1765
  function stripLine(line, state) {
1166
1766
  let out = "";
1167
- let index = 0;
1767
+ let index2 = 0;
1168
1768
  let { blockComment, template } = state;
1169
- while (index < line.length) {
1170
- const char = line[index];
1171
- const next = line[index + 1];
1769
+ while (index2 < line.length) {
1770
+ const char = line[index2];
1771
+ const next = line[index2 + 1];
1172
1772
  if (blockComment) {
1173
1773
  if (char === "*" && next === "/") {
1174
1774
  blockComment = false;
1175
- index += 2;
1775
+ index2 += 2;
1176
1776
  continue;
1177
1777
  }
1178
- index += 1;
1778
+ index2 += 1;
1179
1779
  continue;
1180
1780
  }
1181
1781
  if (template) {
1182
1782
  if (char === "\\") {
1183
- index += 2;
1783
+ index2 += 2;
1184
1784
  continue;
1185
1785
  }
1186
1786
  if (char === "`") template = false;
1187
- index += 1;
1787
+ index2 += 1;
1188
1788
  continue;
1189
1789
  }
1190
1790
  if (char === "/" && next === "*") {
1191
1791
  blockComment = true;
1192
- index += 2;
1792
+ index2 += 2;
1193
1793
  continue;
1194
1794
  }
1195
1795
  if (char === "/" && next === "/") break;
1196
1796
  if (char === "`") {
1197
1797
  template = true;
1198
- index += 1;
1798
+ index2 += 1;
1199
1799
  continue;
1200
1800
  }
1201
1801
  if (char === "'" || char === '"') {
1202
1802
  const quote = char;
1203
- index += 1;
1204
- while (index < line.length) {
1205
- if (line[index] === "\\") {
1206
- index += 2;
1803
+ index2 += 1;
1804
+ while (index2 < line.length) {
1805
+ if (line[index2] === "\\") {
1806
+ index2 += 2;
1207
1807
  continue;
1208
1808
  }
1209
- if (line[index] === quote) {
1210
- index += 1;
1809
+ if (line[index2] === quote) {
1810
+ index2 += 1;
1211
1811
  break;
1212
1812
  }
1213
- index += 1;
1813
+ index2 += 1;
1214
1814
  }
1215
1815
  continue;
1216
1816
  }
1217
1817
  out += char;
1218
- index += 1;
1818
+ index2 += 1;
1219
1819
  }
1220
1820
  return { code: out, state: { blockComment, template } };
1221
1821
  }
@@ -1230,8 +1830,8 @@ function captureBraceBlock(lines, matchLine) {
1230
1830
  let depth = 0;
1231
1831
  let opened = false;
1232
1832
  let state = CLEAN_STATE;
1233
- for (let index = matchLine; index < lines.length; index++) {
1234
- const stripped = stripLine(lines[index] ?? "", state);
1833
+ for (let index2 = matchLine; index2 < lines.length; index2++) {
1834
+ const stripped = stripLine(lines[index2] ?? "", state);
1235
1835
  state = stripped.state;
1236
1836
  for (const char of stripped.code) {
1237
1837
  if (char === "{") {
@@ -1240,10 +1840,10 @@ function captureBraceBlock(lines, matchLine) {
1240
1840
  } else if (char === "}") {
1241
1841
  depth = Math.max(0, depth - 1);
1242
1842
  } else if (char === ";" && !opened) {
1243
- return span(lines, matchLine, index);
1843
+ return span(lines, matchLine, index2);
1244
1844
  }
1245
1845
  }
1246
- if (opened && depth === 0) return span(lines, matchLine, index);
1846
+ if (opened && depth === 0) return span(lines, matchLine, index2);
1247
1847
  }
1248
1848
  return null;
1249
1849
  }
@@ -1252,22 +1852,22 @@ function captureIndentedBlock(lines, matchLine) {
1252
1852
  const header = lines[matchLine] ?? "";
1253
1853
  const indent = header.length - header.trimStart().length;
1254
1854
  let headerEnd = -1;
1255
- for (let index = matchLine; index < lines.length && index <= matchLine + 20; index++) {
1256
- const code = stripLine(lines[index] ?? "", CLEAN_STATE).code.trimEnd();
1855
+ for (let index2 = matchLine; index2 < lines.length && index2 <= matchLine + 20; index2++) {
1856
+ const code = stripLine(lines[index2] ?? "", CLEAN_STATE).code.trimEnd();
1257
1857
  if (code.endsWith(":")) {
1258
- headerEnd = index;
1858
+ headerEnd = index2;
1259
1859
  break;
1260
1860
  }
1261
- if (code.includes(":")) return span(lines, matchLine, index);
1861
+ if (code.includes(":")) return span(lines, matchLine, index2);
1262
1862
  }
1263
1863
  if (headerEnd === -1) return null;
1264
1864
  let end = headerEnd;
1265
- for (let index = headerEnd + 1; index < lines.length; index++) {
1266
- const line = lines[index] ?? "";
1865
+ for (let index2 = headerEnd + 1; index2 < lines.length; index2++) {
1866
+ const line = lines[index2] ?? "";
1267
1867
  if (line.trim() === "") continue;
1268
1868
  const lineIndent = line.length - line.trimStart().length;
1269
1869
  if (lineIndent <= indent) break;
1270
- end = index;
1870
+ end = index2;
1271
1871
  }
1272
1872
  return end === headerEnd ? null : span(lines, matchLine, end);
1273
1873
  }
@@ -1291,11 +1891,11 @@ var regexResolver = {
1291
1891
  const lines = source.split("\n");
1292
1892
  for (const tier of TIERS) {
1293
1893
  const pattern = tier(escaped);
1294
- let candidates = lines.map((line, index) => ({ line, index })).filter((entry) => pattern.test(entry.line)).map((entry) => entry.index);
1894
+ let candidates = lines.map((line, index2) => ({ line, index: index2 })).filter((entry) => pattern.test(entry.line)).map((entry) => entry.index);
1295
1895
  if (!candidates.length) continue;
1296
1896
  if (parentPattern && candidates.length > 1) {
1297
1897
  const distances = candidates.map(
1298
- (index) => distanceToParent(lines, index, parentPattern)
1898
+ (index2) => distanceToParent(lines, index2, parentPattern)
1299
1899
  );
1300
1900
  const nearest = Math.min(...distances);
1301
1901
  if (Number.isFinite(nearest)) {
@@ -1312,34 +1912,79 @@ var regexResolver = {
1312
1912
  function escapeRegExp(value) {
1313
1913
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1314
1914
  }
1315
- function distanceToParent(lines, index, parent) {
1316
- const floor = Math.max(0, index - PARENT_SCOPE_LINES);
1317
- for (let at = index; at >= floor; at--) {
1318
- if (parent.test(lines[at] ?? "")) return index - at;
1915
+ function distanceToParent(lines, index2, parent) {
1916
+ const floor = Math.max(0, index2 - PARENT_SCOPE_LINES);
1917
+ for (let at = index2; at >= floor; at--) {
1918
+ if (parent.test(lines[at] ?? "")) return index2 - at;
1319
1919
  }
1320
1920
  return Number.POSITIVE_INFINITY;
1321
1921
  }
1322
1922
  function hashAnchorText(text) {
1323
- return `sha256:${(0, import_node_crypto.createHash)("sha256").update(text.replace(/\r\n/g, "\n")).digest("hex")}`;
1923
+ return `sha256:${(0, import_node_crypto4.createHash)("sha256").update(text.replace(/\r\n/g, "\n")).digest("hex")}`;
1324
1924
  }
1325
1925
  function resolveAnchor(source, anchor, resolver = regexResolver) {
1926
+ const outcome = resolveAnchorSpan(source, anchor, [resolver]);
1927
+ return outcome.ok ? outcome.span : null;
1928
+ }
1929
+ function resolveAnchorSpan(source, anchor, resolvers = [regexResolver]) {
1326
1930
  const normalized = source.replace(/\r\n/g, "\n");
1327
1931
  if (!anchor.symbol) {
1328
1932
  const lines = normalized.split("\n");
1329
1933
  if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
1330
1934
  return {
1331
- text: normalized,
1332
- startLine: 1,
1333
- endLine: Math.max(1, lines.length)
1935
+ ok: true,
1936
+ span: {
1937
+ text: normalized,
1938
+ startLine: 1,
1939
+ endLine: Math.max(1, lines.length)
1940
+ }
1941
+ };
1942
+ }
1943
+ for (const resolver of resolvers) {
1944
+ const attempt = resolver.attempt ? resolver.attempt(normalized, anchor.symbol, anchor.file) : fromResolve(resolver, normalized, anchor.symbol, anchor.file);
1945
+ if (attempt.kind === "abstain") continue;
1946
+ if (attempt.kind === "unresolved") {
1947
+ if (attempt.reason === "symbol-not-found") continue;
1948
+ return { ok: false, reason: attempt.reason };
1949
+ }
1950
+ return {
1951
+ ok: true,
1952
+ span: attempt.span,
1953
+ ...isResolverName(resolver.name) ? { resolver: resolver.name } : {}
1334
1954
  };
1335
1955
  }
1336
- return resolver.resolve(normalized, anchor.symbol);
1956
+ return { ok: false, reason: "symbol-not-found" };
1957
+ }
1958
+ function fromResolve(resolver, source, symbol, file) {
1959
+ const span2 = resolver.resolve(source, symbol, file);
1960
+ return span2 ? { kind: "resolved", span: span2 } : { kind: "unresolved", reason: "symbol-not-found" };
1961
+ }
1962
+ function isResolverName(name) {
1963
+ return name === "tree-sitter" || name === "regex";
1964
+ }
1965
+ async function prepareResolvers(resolvers, files) {
1966
+ for (const resolver of resolvers) await resolver.prepare?.(files);
1967
+ }
1968
+ function defaultAnchorResolvers(grammars = {}) {
1969
+ return [new TreeSitterResolver(grammars), regexResolver];
1970
+ }
1971
+ function resolverChanged(source, anchor, produced) {
1972
+ const previous = anchor.resolver ?? "regex";
1973
+ if (!produced || !anchor.symbol || previous === produced) return false;
1974
+ if (previous !== "regex") return false;
1975
+ const before = regexResolver.resolve(
1976
+ source.replace(/\r\n/g, "\n"),
1977
+ anchor.symbol
1978
+ );
1979
+ return before !== null && hashAnchorText(before.text) === anchor.hash;
1337
1980
  }
1338
1981
 
1339
1982
  // src/anchor-resolver/drift.ts
1340
1983
  async function detectAnchorDrift(records, options = {}) {
1341
1984
  const repoRoot = options.repoRoot ?? process.cwd();
1342
- const resolver = options.resolver ?? regexResolver;
1985
+ const resolvers = options.resolvers ?? (options.resolver ? [options.resolver] : defaultAnchorResolvers({
1986
+ offline: options.remote?.offline === true
1987
+ }));
1343
1988
  const origin = new LazyOrigin(repoRoot);
1344
1989
  const planned = /* @__PURE__ */ new Map();
1345
1990
  let declaresRepo = false;
@@ -1377,12 +2022,16 @@ async function detectAnchorDrift(records, options = {}) {
1377
2022
  ),
1378
2023
  (options.readRemote ?? readRemoteAnchors)(wants, options.remote ?? {})
1379
2024
  ]);
2025
+ await prepareResolvers(resolvers, [
2026
+ ...files,
2027
+ ...wants.map((want) => want.file)
2028
+ ]);
1380
2029
  const drift = /* @__PURE__ */ new Map();
1381
2030
  for (const record of records) {
1382
2031
  const entries = [];
1383
2032
  for (const { anchor, foreign } of planned.get(record.conceptId) ?? []) {
1384
2033
  entries.push(
1385
- foreign ? remoteEntry(anchor, remote, resolver) : localEntry(anchor, reads.get(anchor.file), resolver)
2034
+ foreign ? remoteEntry(anchor, remote, resolvers) : localEntry(anchor, reads.get(anchor.file), resolvers)
1386
2035
  );
1387
2036
  }
1388
2037
  if (entries.length) drift.set(record.conceptId, entries);
@@ -1411,12 +2060,22 @@ function unresolved(anchor, reason, repo) {
1411
2060
  ...repo ? { repo } : {}
1412
2061
  };
1413
2062
  }
1414
- function hashIn(source, anchor, resolver) {
1415
- const resolved = resolveAnchor(source, anchor, resolver);
1416
- if (!resolved) return null;
2063
+ function hashIn(source, anchor, resolvers) {
2064
+ const outcome = resolveAnchorSpan(source, anchor, resolvers);
2065
+ if (!outcome.ok) return { ok: false, reason: outcome.reason };
2066
+ return {
2067
+ ok: true,
2068
+ current: {
2069
+ hash: hashAnchorText(outcome.span.text),
2070
+ lines: outcome.span.endLine - outcome.span.startLine + 1,
2071
+ ...outcome.resolver ? { resolver: outcome.resolver } : {}
2072
+ }
2073
+ };
2074
+ }
2075
+ function resolverExtras(source, anchor, current) {
1417
2076
  return {
1418
- hash: hashAnchorText(resolved.text),
1419
- lines: resolved.endLine - resolved.startLine + 1
2077
+ ...current.resolver ? { resolver: current.resolver } : {},
2078
+ ...current.hash !== anchor.hash && resolverChanged(source, anchor, current.resolver) ? { reason: "resolver-changed" } : {}
1420
2079
  };
1421
2080
  }
1422
2081
  function compared(anchor, current, extra = {}) {
@@ -1428,40 +2087,58 @@ function compared(anchor, current, extra = {}) {
1428
2087
  ...extra
1429
2088
  };
1430
2089
  }
1431
- function localEntry(anchor, read, resolver) {
2090
+ function localEntry(anchor, read, resolvers) {
1432
2091
  if (!read.ok) return unresolved(anchor, read.reason);
1433
- const current = hashIn(read.source, anchor, resolver);
1434
- return current ? compared(anchor, current) : unresolved(anchor, "symbol-not-found");
2092
+ const found = hashIn(read.source, anchor, resolvers);
2093
+ if (!found.ok) return unresolved(anchor, found.reason);
2094
+ return compared(
2095
+ anchor,
2096
+ found.current,
2097
+ resolverExtras(read.source, anchor, found.current)
2098
+ );
1435
2099
  }
1436
- function remoteEntry(anchor, remote, resolver) {
2100
+ function remoteEntry(anchor, remote, resolvers) {
1437
2101
  const repo = anchor.repo;
1438
2102
  const key2 = normalizeRepoUrl(repo);
1439
2103
  const atDefault = remote.get(wantKey(key2, void 0, anchor.file));
1440
2104
  const primary = anchor.ref ? remote.get(wantKey(key2, anchor.ref, anchor.file)) : atDefault;
1441
2105
  if (!primary) return unresolved(anchor, "remote-unreachable", repo);
1442
2106
  if (!primary.ok) return unresolved(anchor, primary.reason, repo);
1443
- const current = hashIn(primary.source, anchor, resolver);
1444
- if (!current) return unresolved(anchor, "symbol-not-found", repo);
1445
- if (!anchor.ref) return compared(anchor, current, { repo });
2107
+ const found = hashIn(primary.source, anchor, resolvers);
2108
+ if (!found.ok) return unresolved(anchor, found.reason, repo);
2109
+ const current = found.current;
2110
+ const extras = resolverExtras(primary.source, anchor, current);
2111
+ if (!anchor.ref) return compared(anchor, current, { repo, ...extras });
1446
2112
  if (current.hash !== anchor.hash) {
1447
- return compared(anchor, current, { repo, remoteState: "drifted-from-ref" });
2113
+ return compared(anchor, current, {
2114
+ repo,
2115
+ ...extras,
2116
+ remoteState: "drifted-from-ref"
2117
+ });
1448
2118
  }
1449
- const head = atDefault?.ok ? hashIn(atDefault.source, anchor, resolver) : null;
1450
- return head && head.hash !== anchor.hash ? {
1451
- ...compared(anchor, head, { repo }),
2119
+ const head = atDefault?.ok ? hashIn(atDefault.source, anchor, resolvers) : null;
2120
+ return head?.ok && head.current.hash !== anchor.hash ? {
2121
+ ...compared(anchor, head.current, {
2122
+ repo,
2123
+ ...head.current.resolver ? { resolver: head.current.resolver } : {}
2124
+ }),
1452
2125
  state: "drifted",
1453
2126
  remoteState: "drifted-on-default"
1454
- } : compared(anchor, current, { repo, remoteState: "matches-ref" });
2127
+ } : compared(anchor, current, {
2128
+ repo,
2129
+ ...extras,
2130
+ remoteState: "matches-ref"
2131
+ });
1455
2132
  }
1456
2133
 
1457
2134
  // src/search-index.ts
1458
- var import_promises3 = require("fs/promises");
1459
- var import_node_path3 = require("path");
2135
+ var import_promises5 = require("fs/promises");
2136
+ var import_node_path6 = require("path");
1460
2137
 
1461
2138
  // src/kb-log.ts
1462
- var import_zod2 = require("zod");
2139
+ var import_zod3 = require("zod");
1463
2140
  var LOG_FILE = "log.jsonl";
1464
- var kbLogEntrySchema = import_zod2.z.object({
2141
+ var kbLogEntrySchema = import_zod3.z.object({
1465
2142
  // Validated, not just `min(1)`: `at` is a sort key (see `parseLog`
1466
2143
  // below), and a value that isn't actually chronological — a Unix
1467
2144
  // timestamp, a human-typed date, garbage — would sort wrong without
@@ -1470,12 +2147,12 @@ var kbLogEntrySchema = import_zod2.z.object({
1470
2147
  // and rejects everything else, including a non-`Z` offset — so a
1471
2148
  // malformed `at` is reported the same way a malformed line already is,
1472
2149
  // rather than silently sorting into the wrong place.
1473
- at: import_zod2.z.iso.datetime(),
1474
- by: import_zod2.z.string().min(1),
1475
- operation: import_zod2.z.string().min(1),
1476
- conceptId: import_zod2.z.string().min(1),
2150
+ at: import_zod3.z.iso.datetime(),
2151
+ by: import_zod3.z.string().min(1),
2152
+ operation: import_zod3.z.string().min(1),
2153
+ conceptId: import_zod3.z.string().min(1),
1477
2154
  /** Second concept id, where the operation relates two — supersession. */
1478
- target: import_zod2.z.string().min(1).optional()
2155
+ target: import_zod3.z.string().min(1).optional()
1479
2156
  }).strict();
1480
2157
  function renderLogEntry(entry) {
1481
2158
  return `${JSON.stringify(kbLogEntrySchema.parse(entry))}
@@ -1485,18 +2162,18 @@ function parseLog(raw) {
1485
2162
  const entries = [];
1486
2163
  const malformed = [];
1487
2164
  const seen = /* @__PURE__ */ new Set();
1488
- raw.split("\n").forEach((text, index) => {
2165
+ raw.split("\n").forEach((text, index2) => {
1489
2166
  if (!text.trim()) return;
1490
2167
  let value;
1491
2168
  try {
1492
2169
  value = JSON.parse(text);
1493
2170
  } catch {
1494
- malformed.push({ line: index + 1, text });
2171
+ malformed.push({ line: index2 + 1, text });
1495
2172
  return;
1496
2173
  }
1497
2174
  const parsed = kbLogEntrySchema.safeParse(value);
1498
2175
  if (!parsed.success) {
1499
- malformed.push({ line: index + 1, text });
2176
+ malformed.push({ line: index2 + 1, text });
1500
2177
  return;
1501
2178
  }
1502
2179
  const key2 = JSON.stringify(parsed.data);
@@ -1519,7 +2196,7 @@ async function searchBase(bundlePath2, query, options = {}) {
1519
2196
  let store = null;
1520
2197
  try {
1521
2198
  store = await qmd.createStore({
1522
- dbPath: (0, import_node_path3.join)(bundlePath2, SEARCH_INDEX_FILE),
2199
+ dbPath: (0, import_node_path6.join)(bundlePath2, SEARCH_INDEX_FILE),
1523
2200
  config: {
1524
2201
  collections: {
1525
2202
  [COLLECTION]: {
@@ -1554,7 +2231,7 @@ async function searchBase(bundlePath2, query, options = {}) {
1554
2231
  }
1555
2232
  }
1556
2233
  async function isStale(bundlePath2) {
1557
- const indexAt = await (0, import_promises3.stat)((0, import_node_path3.join)(bundlePath2, SEARCH_INDEX_FILE)).then((s) => s.mtimeMs).catch(() => 0);
2234
+ const indexAt = await (0, import_promises5.stat)((0, import_node_path6.join)(bundlePath2, SEARCH_INDEX_FILE)).then((s) => s.mtimeMs).catch(() => 0);
1558
2235
  if (!indexAt) return true;
1559
2236
  const { readdir: readdir2 } = await import("fs/promises");
1560
2237
  const names = (await readdir2(bundlePath2).catch(() => [])).filter(
@@ -1563,7 +2240,7 @@ async function isStale(bundlePath2) {
1563
2240
  let stale = false;
1564
2241
  await mapLimit(names, DEFAULT_IO_CONCURRENCY, async (name) => {
1565
2242
  if (stale) return;
1566
- const at = await (0, import_promises3.stat)((0, import_node_path3.join)(bundlePath2, name)).then((s) => s.mtimeMs).catch(() => 0);
2243
+ const at = await (0, import_promises5.stat)((0, import_node_path6.join)(bundlePath2, name)).then((s) => s.mtimeMs).catch(() => 0);
1567
2244
  if (at > indexAt) stale = true;
1568
2245
  });
1569
2246
  return stale;
@@ -1943,8 +2620,8 @@ function byRank(left, right) {
1943
2620
  ) || left.record.conceptId.localeCompare(right.record.conceptId);
1944
2621
  }
1945
2622
  function typeRank(record) {
1946
- const index = TYPE_PRIORITY.indexOf(record.frontmatter.type);
1947
- return index === -1 ? TYPE_PRIORITY.length : index;
2623
+ const index2 = TYPE_PRIORITY.indexOf(record.frontmatter.type);
2624
+ return index2 === -1 ? TYPE_PRIORITY.length : index2;
1948
2625
  }
1949
2626
 
1950
2627
  // src/catalog.ts
@@ -2153,7 +2830,7 @@ function appendUnionMergeLine(contents) {
2153
2830
  }
2154
2831
 
2155
2832
  // src/kb-store.ts
2156
- var KB_DIR = (0, import_node_path4.join)(".strauss", "kb");
2833
+ var KB_DIR = (0, import_node_path7.join)(".strauss", "kb");
2157
2834
  var STORE_OWNED = /* @__PURE__ */ new Set([INDEX_FILE, LOG_FILE, SEARCH_INDEX_FILE]);
2158
2835
  var DEFAULT_LOAD_BUDGET = 25e3;
2159
2836
  var KbStore = class {
@@ -2184,7 +2861,7 @@ var KbStore = class {
2184
2861
  const conceptId2 = `${input.type}.${input.slug}`;
2185
2862
  const root = this.root(bundlePath2);
2186
2863
  const target = this.recordPath(bundlePath2, conceptId2);
2187
- await (0, import_promises4.mkdir)(root, { recursive: true });
2864
+ await (0, import_promises6.mkdir)(root, { recursive: true });
2188
2865
  await this.publish(
2189
2866
  target,
2190
2867
  stringifyMarkdownWithFrontmatter(input.body, frontmatter),
@@ -2223,7 +2900,7 @@ var KbStore = class {
2223
2900
  const target = this.recordPath(bundlePath2, conceptId2);
2224
2901
  let raw;
2225
2902
  try {
2226
- raw = await (0, import_promises4.readFile)(target, "utf8");
2903
+ raw = await (0, import_promises6.readFile)(target, "utf8");
2227
2904
  } catch {
2228
2905
  return null;
2229
2906
  }
@@ -2240,7 +2917,7 @@ var KbStore = class {
2240
2917
  const root = this.root(bundlePath2);
2241
2918
  let names;
2242
2919
  try {
2243
- names = await (0, import_promises4.readdir)(root);
2920
+ names = await (0, import_promises6.readdir)(root);
2244
2921
  } catch {
2245
2922
  return [];
2246
2923
  }
@@ -2248,7 +2925,7 @@ var KbStore = class {
2248
2925
  const records = await mapLimit(
2249
2926
  wanted,
2250
2927
  DEFAULT_IO_CONCURRENCY,
2251
- async ({ name, conceptId: conceptId2 }) => this.parse(conceptId2, await (0, import_promises4.readFile)((0, import_node_path4.join)(root, name), "utf8"))
2928
+ async ({ name, conceptId: conceptId2 }) => this.parse(conceptId2, await (0, import_promises6.readFile)((0, import_node_path7.join)(root, name), "utf8"))
2252
2929
  );
2253
2930
  return records.filter((record) => record !== null);
2254
2931
  }
@@ -2405,7 +3082,7 @@ ${answer}
2405
3082
  if (found.length) return found;
2406
3083
  }
2407
3084
  const lowered = needle.toLowerCase();
2408
- return bundle.filter((record) => matches(record, lowered));
3085
+ return bundle.filter((record) => matches2(record, lowered));
2409
3086
  }
2410
3087
  /**
2411
3088
  * Anchor drift over the records about to be handed back. Like the search
@@ -2523,6 +3200,28 @@ ${answer}
2523
3200
  digest: bundleDigestValue
2524
3201
  };
2525
3202
  }
3203
+ /**
3204
+ * `load`'s digest without `load`'s bodies — the same records, adjudicated
3205
+ * the same way, handed back as a stamp. Skips the anchor drift pass, which
3206
+ * reads source files and only ever adds warnings: no warning reaches the
3207
+ * digest, so the value is identical to the one `load` returns.
3208
+ */
3209
+ async stamp(bundlePath2) {
3210
+ const bundle = await this.list(bundlePath2);
3211
+ const adjudicated = adjudicate(bundle, bundle, /* @__PURE__ */ new Date());
3212
+ const current = adjudicated.filter((hit) => hit.standing !== "superseded");
3213
+ const superseded = adjudicated.filter((hit) => hit.standing === "superseded").map(stub);
3214
+ const stamped = bundleStamp(current, superseded);
3215
+ const dates = bundle.map((record) => record.frontmatter.generated?.at ?? null).filter((at) => typeof at === "string").sort();
3216
+ return {
3217
+ path: bundlePath2,
3218
+ digest: stamped.digest,
3219
+ recordCount: bundle.length,
3220
+ superseded: superseded.length,
3221
+ newestAt: dates.at(-1) ?? null,
3222
+ records: stamped.records
3223
+ };
3224
+ }
2526
3225
  /** How a position was arrived at, as a timeline. See `trace.ts`. */
2527
3226
  async trace(bundlePath2, seedId, options = {}) {
2528
3227
  return trace(seedId, await this.list(bundlePath2), options);
@@ -2553,11 +3252,11 @@ ${answer}
2553
3252
  async readIndex(bundlePath2) {
2554
3253
  const root = this.root(bundlePath2);
2555
3254
  const expected = renderIndex(await this.list(bundlePath2));
2556
- const stored = await (0, import_promises4.readFile)((0, import_node_path4.join)(root, INDEX_FILE), "utf8").catch(
3255
+ const stored = await (0, import_promises6.readFile)((0, import_node_path7.join)(root, INDEX_FILE), "utf8").catch(
2557
3256
  () => null
2558
3257
  );
2559
3258
  if (indexIsStale(stored, expected)) {
2560
- await this.publish((0, import_node_path4.join)(root, INDEX_FILE), expected, true, INDEX_FILE);
3259
+ await this.publish((0, import_node_path7.join)(root, INDEX_FILE), expected, true, INDEX_FILE);
2561
3260
  this.logger.info?.({
2562
3261
  operation: "kb.index.repair",
2563
3262
  bundlePath: root,
@@ -2574,8 +3273,8 @@ ${answer}
2574
3273
  * knows which agent touched what. So a bad line is surfaced and left alone.
2575
3274
  */
2576
3275
  async readLog(bundlePath2) {
2577
- const raw = await (0, import_promises4.readFile)(
2578
- (0, import_node_path4.join)(this.root(bundlePath2), LOG_FILE),
3276
+ const raw = await (0, import_promises6.readFile)(
3277
+ (0, import_node_path7.join)(this.root(bundlePath2), LOG_FILE),
2579
3278
  "utf8"
2580
3279
  ).catch(() => "");
2581
3280
  const result = parseLog(raw);
@@ -2626,15 +3325,15 @@ ${answer}
2626
3325
  }
2627
3326
  async mutate(bundlePath2, conceptId2, change, entry, changeBody = (body) => body) {
2628
3327
  const target = this.recordPath(bundlePath2, conceptId2);
2629
- const before = await (0, import_promises4.readFile)(target, "utf8").catch(() => null);
3328
+ const before = await (0, import_promises6.readFile)(target, "utf8").catch(() => null);
2630
3329
  if (before === null) throw new KbRecordNotFoundError(conceptId2);
2631
3330
  const parsed = this.parse(conceptId2, before);
2632
3331
  if (!parsed) throw new KbRecordNotFoundError(conceptId2);
2633
3332
  const frontmatter = change(parsed.frontmatter);
2634
3333
  const body = changeBody(parsed.body);
2635
3334
  const contents = stringifyMarkdownWithFrontmatter(body, frontmatter);
2636
- const witness = await (0, import_promises4.readFile)(target, "utf8").catch(() => null);
2637
- if (witness === null || digest(witness) !== digest(before)) {
3335
+ const witness = await (0, import_promises6.readFile)(target, "utf8").catch(() => null);
3336
+ if (witness === null || sha256(witness) !== sha256(before)) {
2638
3337
  throw new KbWriteConflictError(conceptId2);
2639
3338
  }
2640
3339
  await this.publish(target, contents, true, conceptId2);
@@ -2659,20 +3358,20 @@ ${answer}
2659
3358
  */
2660
3359
  async publish(target, contents, overwrite, conceptId2) {
2661
3360
  const staging = `${target}.${process.pid}.tmp`;
2662
- await (0, import_promises4.writeFile)(staging, contents, "utf8");
3361
+ await (0, import_promises6.writeFile)(staging, contents, "utf8");
2663
3362
  try {
2664
3363
  if (overwrite) {
2665
- await (0, import_promises4.rename)(staging, target);
3364
+ await (0, import_promises6.rename)(staging, target);
2666
3365
  return;
2667
3366
  }
2668
- await (0, import_promises4.link)(staging, target);
3367
+ await (0, import_promises6.link)(staging, target);
2669
3368
  } catch (error) {
2670
3369
  if (error.code === "EEXIST") {
2671
3370
  throw new KbRecordAlreadyExistsError(conceptId2);
2672
3371
  }
2673
3372
  throw error;
2674
3373
  } finally {
2675
- await (0, import_promises4.unlink)(staging).catch(() => void 0);
3374
+ await (0, import_promises6.unlink)(staging).catch(() => void 0);
2676
3375
  }
2677
3376
  }
2678
3377
  /**
@@ -2716,18 +3415,18 @@ ${answer}
2716
3415
  * file must not fail the mutation it guards.
2717
3416
  */
2718
3417
  async ensureGitattributes(root) {
2719
- const target = (0, import_node_path4.join)(root, GITATTRIBUTES_FILE);
3418
+ const target = (0, import_node_path7.join)(root, GITATTRIBUTES_FILE);
2720
3419
  try {
2721
3420
  let existing;
2722
3421
  try {
2723
- existing = await (0, import_promises4.readFile)(target, "utf8");
3422
+ existing = await (0, import_promises6.readFile)(target, "utf8");
2724
3423
  } catch (error) {
2725
3424
  if (error.code !== "ENOENT") throw error;
2726
3425
  existing = null;
2727
3426
  }
2728
3427
  if (existing === null) {
2729
3428
  try {
2730
- await (0, import_promises4.writeFile)(target, appendUnionMergeLine(""), {
3429
+ await (0, import_promises6.writeFile)(target, appendUnionMergeLine(""), {
2731
3430
  encoding: "utf8",
2732
3431
  flag: "wx"
2733
3432
  });
@@ -2748,7 +3447,7 @@ ${answer}
2748
3447
  return;
2749
3448
  }
2750
3449
  if (!hasMergeDeclaration(existing)) {
2751
- await (0, import_promises4.appendFile)(target, appendUnionMergeLine(existing), "utf8");
3450
+ await (0, import_promises6.appendFile)(target, appendUnionMergeLine(existing), "utf8");
2752
3451
  this.logger.info?.({
2753
3452
  operation: "kb.gitattributes.ensure",
2754
3453
  bundlePath: root,
@@ -2767,7 +3466,7 @@ ${answer}
2767
3466
  async record(root, entry) {
2768
3467
  await this.ensureGitattributes(root);
2769
3468
  const line = renderLogEntry({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry });
2770
- await (0, import_promises4.appendFile)((0, import_node_path4.join)(root, LOG_FILE), line, "utf8").catch((error) => {
3469
+ await (0, import_promises6.appendFile)((0, import_node_path7.join)(root, LOG_FILE), line, "utf8").catch((error) => {
2771
3470
  this.logger.warn?.({
2772
3471
  operation: "kb.log.append",
2773
3472
  outcome: "failed",
@@ -2793,18 +3492,18 @@ ${answer}
2793
3492
  };
2794
3493
  }
2795
3494
  root(bundlePath2) {
2796
- return (0, import_node_path4.resolve)(bundlePath2);
3495
+ return (0, import_node_path7.resolve)(bundlePath2);
2797
3496
  }
2798
3497
  // Concept ids are `<type>.<slug>` and map to a single file directly under the
2799
3498
  // bundle root; anything carrying a separator would escape it.
2800
3499
  recordPath(bundlePath2, conceptId2) {
2801
- if (conceptId2.includes(import_node_path4.sep) || conceptId2.includes("/")) {
3500
+ if (conceptId2.includes(import_node_path7.sep) || conceptId2.includes("/")) {
2802
3501
  throw new KbInvalidConceptIdError(
2803
3502
  "concept id must not contain a path separator",
2804
3503
  { conceptId: conceptId2 }
2805
3504
  );
2806
3505
  }
2807
- return (0, import_node_path4.join)(this.root(bundlePath2), `${conceptId2}.md`);
3506
+ return (0, import_node_path7.join)(this.root(bundlePath2), `${conceptId2}.md`);
2808
3507
  }
2809
3508
  };
2810
3509
  function estimateTokens(record) {
@@ -2831,7 +3530,7 @@ function stub(hit) {
2831
3530
  at: hit.record.frontmatter.generated?.at ?? null
2832
3531
  };
2833
3532
  }
2834
- function matches(record, needle) {
3533
+ function matches2(record, needle) {
2835
3534
  const { title, description } = record.frontmatter;
2836
3535
  return [record.conceptId, title, description, record.body].some(
2837
3536
  (field) => field?.toLowerCase().includes(needle)
@@ -2842,58 +3541,39 @@ function normalizeActor(id) {
2842
3541
  if (colon === -1) return id.toLowerCase();
2843
3542
  return id.slice(0, colon + 1).toLowerCase() + id.slice(colon + 1);
2844
3543
  }
2845
- function digest(contents) {
2846
- return (0, import_node_crypto2.createHash)("sha256").update(contents).digest("hex");
2847
- }
2848
- function bundleDigest(records, superseded) {
2849
- const entries = [
2850
- ...records.map(
2851
- (hit) => `${hit.record.conceptId}:current:${digest(
2852
- stringifyMarkdownWithFrontmatter(
2853
- hit.record.body,
2854
- hit.record.frontmatter
2855
- )
2856
- )}`
2857
- ),
2858
- ...superseded.map(
2859
- (entry) => `${entry.conceptId}:superseded:${digest(JSON.stringify(entry))}`
2860
- )
2861
- ].sort();
2862
- return digest(entries.join("\n"));
2863
- }
2864
3544
 
2865
3545
  // src/compose.ts
2866
- var import_zod3 = require("zod");
2867
- var composeLinkSchema = import_zod3.z.object({
3546
+ var import_zod4 = require("zod");
3547
+ var composeLinkSchema = import_zod4.z.object({
2868
3548
  target: kbConceptIdSchema,
2869
- rel: import_zod3.z.enum(KB_LINK_RELS)
3549
+ rel: import_zod4.z.enum(KB_LINK_RELS)
2870
3550
  }).strict();
2871
- var composeInputSchema = import_zod3.z.object({
2872
- slug: import_zod3.z.string().min(1),
3551
+ var composeInputSchema = import_zod4.z.object({
3552
+ slug: import_zod4.z.string().min(1),
2873
3553
  /** One line, in the reader's terms. Becomes OKF `title`. */
2874
- title: import_zod3.z.string().min(1),
3554
+ title: import_zod4.z.string().min(1),
2875
3555
  /** The consequence — what breaks if this is wrong. Becomes `description`. */
2876
- why: import_zod3.z.string().min(1),
3556
+ why: import_zod4.z.string().min(1),
2877
3557
  /** Keyed by section heading from the type's spec. Unknown keys rejected. */
2878
- sections: import_zod3.z.record(import_zod3.z.string(), import_zod3.z.string().min(1)).optional(),
2879
- anchors: import_zod3.z.array(kbAnchorSchema).optional(),
2880
- sources: import_zod3.z.array(kbSourceSchema).optional(),
3558
+ sections: import_zod4.z.record(import_zod4.z.string(), import_zod4.z.string().min(1)).optional(),
3559
+ anchors: import_zod4.z.array(kbAnchorSchema).optional(),
3560
+ sources: import_zod4.z.array(kbSourceSchema).optional(),
2881
3561
  /** No source exists, as a claim rather than a sentinel in `sources`. */
2882
- assumption: import_zod3.z.boolean().optional(),
3562
+ assumption: import_zod4.z.boolean().optional(),
2883
3563
  /**
2884
3564
  * OKF `stale_after`: the absolute date this record stops being trusted.
2885
3565
  * Anything the outside world can change — pricing, quotas, versions,
2886
3566
  * reception counts — should carry one.
2887
3567
  */
2888
- stale_after: import_zod3.z.string().regex(/^\d{4}-\d{2}-\d{2}$/, {
3568
+ stale_after: import_zod4.z.string().regex(/^\d{4}-\d{2}-\d{2}$/, {
2889
3569
  message: "stale_after must be YYYY-MM-DD"
2890
3570
  }).refine((date) => !Number.isNaN(Date.parse(date)), {
2891
3571
  message: "stale_after must be a real date"
2892
3572
  }).optional(),
2893
- verify: import_zod3.z.array(import_zod3.z.string().min(1)).optional(),
2894
- tags: import_zod3.z.array(import_zod3.z.string().min(1)).optional(),
3573
+ verify: import_zod4.z.array(import_zod4.z.string().min(1)).optional(),
3574
+ tags: import_zod4.z.array(import_zod4.z.string().min(1)).optional(),
2895
3575
  /** Concept ids this record relates to; rendered as body links. */
2896
- relatedConceptIds: import_zod3.z.array(kbConceptIdSchema).optional(),
3576
+ relatedConceptIds: import_zod4.z.array(kbConceptIdSchema).optional(),
2897
3577
  /**
2898
3578
  * Typed causal edges, source → target: `{ target: "fact.b", rel:
2899
3579
  * "depends_on" }` on record A says A needs B. Stored in frontmatter and
@@ -2901,16 +3581,16 @@ var composeInputSchema = import_zod3.z.object({
2901
3581
  * reader that knows only OKF. The vocabulary goes into the description from
2902
3582
  * the same table the walk uses, so `kb_schema` emits it.
2903
3583
  */
2904
- links: import_zod3.z.array(composeLinkSchema).max(64).optional().describe(
3584
+ links: import_zod4.z.array(composeLinkSchema).max(64).optional().describe(
2905
3585
  `Typed causal edges, source \u2192 target \u2014 a link on this record says this record <rel> the target. ${KB_LINK_RELS.map(
2906
3586
  (rel) => `${rel}: ${LINK_RELS[rel].purpose}`
2907
3587
  ).join("; ")}.`
2908
3588
  ),
2909
3589
  /** Concept ids this record replaces. The store settles the backlinks. */
2910
- supersedes: import_zod3.z.array(kbConceptIdSchema).max(32).optional(),
2911
- materiality: import_zod3.z.enum(KB_MATERIALITIES).optional(),
2912
- confidence: import_zod3.z.enum(KB_CONFIDENCES).optional(),
2913
- owner: import_zod3.z.string().min(1).optional()
3590
+ supersedes: import_zod4.z.array(kbConceptIdSchema).max(32).optional(),
3591
+ materiality: import_zod4.z.enum(KB_MATERIALITIES).optional(),
3592
+ confidence: import_zod4.z.enum(KB_CONFIDENCES).optional(),
3593
+ owner: import_zod4.z.string().min(1).optional()
2914
3594
  }).strict();
2915
3595
  function composeRecord(type, input, writtenBy, writtenAt) {
2916
3596
  const parsed = composeInputSchema.parse(input);
@@ -2987,9 +3667,9 @@ ${text}`);
2987
3667
  // src/kb-pins/budgets.ts
2988
3668
  function asBudgets(value) {
2989
3669
  if (value === null || typeof value !== "object") return {};
2990
- const table = value;
3670
+ const table2 = value;
2991
3671
  const pick = (key2, min) => {
2992
- const raw = table[key2];
3672
+ const raw = table2[key2];
2993
3673
  return typeof raw === "number" && Number.isInteger(raw) && raw >= min ? raw : void 0;
2994
3674
  };
2995
3675
  const budgetTokens = pick("budgetTokens", 1);
@@ -3000,9 +3680,9 @@ function asBudgets(value) {
3000
3680
  };
3001
3681
  }
3002
3682
  function contextProfileBudgets(manifest, profile) {
3003
- const table = manifest.context;
3004
- if (table === null || typeof table !== "object") return {};
3005
- const entries = table;
3683
+ const table2 = manifest.context;
3684
+ if (table2 === null || typeof table2 !== "object") return {};
3685
+ const entries = table2;
3006
3686
  return {
3007
3687
  ...asBudgets(entries["default"]),
3008
3688
  ...profile ? asBudgets(entries[profile]) : {}
@@ -3033,23 +3713,23 @@ var KbBaseFrozenError = class extends Error {
3033
3713
  };
3034
3714
 
3035
3715
  // src/kb-pins/frozen.ts
3036
- var import_node_path7 = require("path");
3716
+ var import_node_path10 = require("path");
3037
3717
 
3038
3718
  // src/kb-pins/layers.ts
3039
- var import_promises5 = require("fs/promises");
3040
- var import_node_os2 = require("os");
3041
- var import_node_path6 = require("path");
3719
+ var import_promises7 = require("fs/promises");
3720
+ var import_node_os3 = require("os");
3721
+ var import_node_path9 = require("path");
3042
3722
 
3043
3723
  // src/kb-pins/model.ts
3044
- var import_node_path5 = require("path");
3045
- var import_zod4 = require("zod");
3046
- var PINS_FILE = (0, import_node_path5.join)(".strauss", "kb-pins.json");
3047
- var PINS_LOCAL_FILE = (0, import_node_path5.join)(".strauss", "kb-pins.local.json");
3724
+ var import_node_path8 = require("path");
3725
+ var import_zod5 = require("zod");
3726
+ var PINS_FILE = (0, import_node_path8.join)(".strauss", "kb-pins.json");
3727
+ var PINS_LOCAL_FILE = (0, import_node_path8.join)(".strauss", "kb-pins.local.json");
3048
3728
  var PIN_LAYERS = ["project", "local", "user"];
3049
- var pinSchema = import_zod4.z.object({
3729
+ var pinSchema = import_zod5.z.object({
3050
3730
  /** Relative to the manifest's root, so the file is committable. */
3051
- path: import_zod4.z.string().min(1),
3052
- pinnedAt: import_zod4.z.string().min(1).optional(),
3731
+ path: import_zod5.z.string().min(1),
3732
+ pinnedAt: import_zod5.z.string().min(1).optional(),
3053
3733
  /**
3054
3734
  * How `context` renders this base. `full` preloads the whole base into
3055
3735
  * the block regardless of the full-under threshold — for a base whose
@@ -3059,7 +3739,7 @@ var pinSchema = import_zod4.z.object({
3059
3739
  * Absent: the profile's full-under threshold decides. Invalid values
3060
3740
  * degrade to absent rather than failing the manifest.
3061
3741
  */
3062
- mode: import_zod4.z.enum(["full", "index"]).optional().catch(void 0),
3742
+ mode: import_zod5.z.enum(["full", "index"]).optional().catch(void 0),
3063
3743
  /**
3064
3744
  * Context profiles this pin surfaces in (e.g. only at session-start,
3065
3745
  * not per turn). Absent: every profile. A run without a profile sees
@@ -3067,17 +3747,17 @@ var pinSchema = import_zod4.z.object({
3067
3747
  * that skill at point of use than pinned at all — pins are what every
3068
3748
  * session should see.
3069
3749
  */
3070
- profiles: import_zod4.z.array(import_zod4.z.string()).optional().catch(void 0),
3750
+ profiles: import_zod5.z.array(import_zod5.z.string()).optional().catch(void 0),
3071
3751
  /**
3072
3752
  * The base is concluded — a finished piece of research, a frozen ADR
3073
3753
  * set. Write commands against it refuse while this workspace holds the
3074
3754
  * pin, and `context` labels it read-only. Workspace policy, not base
3075
3755
  * state: the base itself stays copyable and writable elsewhere.
3076
3756
  */
3077
- frozen: import_zod4.z.boolean().optional().catch(void 0)
3757
+ frozen: import_zod5.z.boolean().optional().catch(void 0)
3078
3758
  }).passthrough();
3079
- var pinsManifestSchema = import_zod4.z.object({
3080
- pins: import_zod4.z.array(pinSchema).default([]),
3759
+ var pinsManifestSchema = import_zod5.z.object({
3760
+ pins: import_zod5.z.array(pinSchema).default([]),
3081
3761
  /**
3082
3762
  * Per-repo budgets for the `context` command, keyed by profile —
3083
3763
  * `"session-start"`, `"compact"`, `"turn"`, or `"default"` for all of
@@ -3086,18 +3766,18 @@ var pinsManifestSchema = import_zod4.z.object({
3086
3766
  * the index at every session start. `contextProfileBudgets` does the
3087
3767
  * tolerant read.
3088
3768
  */
3089
- context: import_zod4.z.unknown().optional()
3769
+ context: import_zod5.z.unknown().optional()
3090
3770
  }).passthrough();
3091
3771
 
3092
3772
  // src/kb-pins/layers.ts
3093
3773
  function userRoot() {
3094
- return process.env.STRAUSS_KB_USER_ROOT || (0, import_node_os2.homedir)();
3774
+ return process.env.STRAUSS_KB_USER_ROOT || (0, import_node_os3.homedir)();
3095
3775
  }
3096
3776
  function layerRoot(workspaceDir, layer) {
3097
- return layer === "user" ? userRoot() : (0, import_node_path6.resolve)(workspaceDir);
3777
+ return layer === "user" ? userRoot() : (0, import_node_path9.resolve)(workspaceDir);
3098
3778
  }
3099
3779
  function layerFile(workspaceDir, layer) {
3100
- return (0, import_node_path6.join)(
3780
+ return (0, import_node_path9.join)(
3101
3781
  layerRoot(workspaceDir, layer),
3102
3782
  layer === "local" ? PINS_LOCAL_FILE : PINS_FILE
3103
3783
  );
@@ -3106,7 +3786,7 @@ async function readPinsLayer(workspaceDir, layer) {
3106
3786
  const file = layerFile(workspaceDir, layer);
3107
3787
  let raw;
3108
3788
  try {
3109
- raw = await (0, import_promises5.readFile)(file, "utf8");
3789
+ raw = await (0, import_promises7.readFile)(file, "utf8");
3110
3790
  } catch {
3111
3791
  return { pins: [] };
3112
3792
  }
@@ -3130,16 +3810,16 @@ async function readPinsLayer(workspaceDir, layer) {
3130
3810
  }
3131
3811
  async function writePinsLayer(workspaceDir, layer, manifest) {
3132
3812
  const file = layerFile(workspaceDir, layer);
3133
- await (0, import_promises5.mkdir)((0, import_node_path6.dirname)(file), { recursive: true });
3134
- await (0, import_promises5.writeFile)(file, `${JSON.stringify(manifest, null, 2)}
3813
+ await (0, import_promises7.mkdir)((0, import_node_path9.dirname)(file), { recursive: true });
3814
+ await (0, import_promises7.writeFile)(file, `${JSON.stringify(manifest, null, 2)}
3135
3815
  `, "utf8");
3136
3816
  }
3137
3817
  function resolvePinPath(rootDir, path) {
3138
- return (0, import_node_path6.isAbsolute)(path) ? (0, import_node_path6.resolve)(path) : (0, import_node_path6.resolve)(rootDir, path.split("/").join(import_node_path6.sep));
3818
+ return (0, import_node_path9.isAbsolute)(path) ? (0, import_node_path9.resolve)(path) : (0, import_node_path9.resolve)(rootDir, path.split("/").join(import_node_path9.sep));
3139
3819
  }
3140
3820
  function storablePath(rootDir, bundlePath2) {
3141
- const rel = (0, import_node_path6.relative)((0, import_node_path6.resolve)(rootDir), (0, import_node_path6.resolve)(bundlePath2));
3142
- return (rel === "" ? "." : rel).split(import_node_path6.sep).join("/");
3821
+ const rel = (0, import_node_path9.relative)((0, import_node_path9.resolve)(rootDir), (0, import_node_path9.resolve)(bundlePath2));
3822
+ return (rel === "" ? "." : rel).split(import_node_path9.sep).join("/");
3143
3823
  }
3144
3824
  async function readMergedPins(workspaceDir) {
3145
3825
  const manifests = {};
@@ -3167,7 +3847,7 @@ async function readMergedPins(workspaceDir) {
3167
3847
  // src/kb-pins/frozen.ts
3168
3848
  async function assertBaseNotFrozen(workspaceDir, bundlePath2) {
3169
3849
  const merged = await readMergedPins(workspaceDir);
3170
- const absolute = (0, import_node_path7.resolve)(bundlePath2);
3850
+ const absolute = (0, import_node_path10.resolve)(bundlePath2);
3171
3851
  const pin = merged.pins.find((entry) => entry.absolutePath === absolute);
3172
3852
  if (pin?.frozen === true) {
3173
3853
  throw new KbBaseFrozenError(pin.path, pin.layer);
@@ -3252,7 +3932,7 @@ async function pinBase(store, workspaceDir, bundlePath2, at, options = {}) {
3252
3932
  }
3253
3933
 
3254
3934
  // src/kb-pins/unpin.ts
3255
- var import_node_path8 = require("path");
3935
+ var import_node_path11 = require("path");
3256
3936
  async function unpinBase(workspaceDir, bundlePath2) {
3257
3937
  const layers = [];
3258
3938
  for (const layer of PIN_LAYERS) {
@@ -3273,14 +3953,14 @@ async function unpinBase(workspaceDir, bundlePath2) {
3273
3953
  }
3274
3954
  }
3275
3955
  return {
3276
- path: storablePath((0, import_node_path8.resolve)(workspaceDir), bundlePath2),
3956
+ path: storablePath((0, import_node_path11.resolve)(workspaceDir), bundlePath2),
3277
3957
  removed: layers.length > 0,
3278
3958
  layers
3279
3959
  };
3280
3960
  }
3281
3961
 
3282
3962
  // src/kb-context.ts
3283
- var import_promises6 = require("fs/promises");
3963
+ var import_promises8 = require("fs/promises");
3284
3964
  var HEADING2 = "## Knowledge bases (pinned)";
3285
3965
  var DEFAULT_CONTEXT_BUDGET = 4e3;
3286
3966
  var CONTEXT_PROFILES = {
@@ -3484,13 +4164,13 @@ function toHookJson(block, event) {
3484
4164
  var CONTEXT_BEGIN = "<!-- strauss-kb:begin -->";
3485
4165
  var CONTEXT_END = "<!-- strauss-kb:end -->";
3486
4166
  async function syncInstructions(file, block) {
3487
- const existing = await (0, import_promises6.readFile)(file, "utf8").catch(() => null);
4167
+ const existing = await (0, import_promises8.readFile)(file, "utf8").catch(() => null);
3488
4168
  const region = block ? `${CONTEXT_BEGIN}
3489
4169
  ${block.trim()}
3490
4170
  ${CONTEXT_END}` : null;
3491
4171
  if (existing === null) {
3492
4172
  if (!region) return { file, action: "unchanged" };
3493
- await (0, import_promises6.writeFile)(file, `${region}
4173
+ await (0, import_promises8.writeFile)(file, `${region}
3494
4174
  `, "utf8");
3495
4175
  return { file, action: "created" };
3496
4176
  }
@@ -3501,11 +4181,11 @@ ${CONTEXT_END}` : null;
3501
4181
  const after = existing.slice(end + CONTEXT_END.length);
3502
4182
  const next = region ? `${before}${region}${after}` : `${before.replace(/\n+$/, "\n")}${after.replace(/^\n+/, "\n")}`;
3503
4183
  if (next === existing) return { file, action: "unchanged" };
3504
- await (0, import_promises6.writeFile)(file, next, "utf8");
4184
+ await (0, import_promises8.writeFile)(file, next, "utf8");
3505
4185
  return { file, action: region ? "replaced" : "removed" };
3506
4186
  }
3507
4187
  if (!region) return { file, action: "unchanged" };
3508
- await (0, import_promises6.writeFile)(
4188
+ await (0, import_promises8.writeFile)(
3509
4189
  file,
3510
4190
  `${existing.replace(/\n*$/, "\n\n")}${region}
3511
4191
  `,
@@ -3515,14 +4195,14 @@ ${CONTEXT_END}` : null;
3515
4195
  }
3516
4196
 
3517
4197
  // src/json-schema.ts
3518
- var import_zod5 = require("zod");
4198
+ var import_zod6 = require("zod");
3519
4199
  function kbJsonSchemas() {
3520
4200
  return {
3521
- recordFrontmatter: import_zod5.z.toJSONSchema(kbRecordFrontmatterSchema, {
4201
+ recordFrontmatter: import_zod6.z.toJSONSchema(kbRecordFrontmatterSchema, {
3522
4202
  io: "input"
3523
4203
  }),
3524
- composeInput: import_zod5.z.toJSONSchema(composeInputSchema, { io: "input" }),
3525
- logEntry: import_zod5.z.toJSONSchema(kbLogEntrySchema, { io: "input" })
4204
+ composeInput: import_zod6.z.toJSONSchema(composeInputSchema, { io: "input" }),
4205
+ logEntry: import_zod6.z.toJSONSchema(kbLogEntrySchema, { io: "input" })
3526
4206
  };
3527
4207
  }
3528
4208
 
@@ -3532,7 +4212,7 @@ function matchToDiff(files, records, options = {}) {
3532
4212
  const anchored = records.filter(
3533
4213
  (record) => (record.frontmatter.strauss_anchors ?? []).length > 0
3534
4214
  );
3535
- const matches2 = [];
4215
+ const matches3 = [];
3536
4216
  for (const file of files) {
3537
4217
  const candidates = anchored.map((record) => ({
3538
4218
  record,
@@ -3551,7 +4231,7 @@ function matchToDiff(files, records, options = {}) {
3551
4231
  hits.push(record);
3552
4232
  }
3553
4233
  if (!hits.length) continue;
3554
- matches2.push({
4234
+ matches3.push({
3555
4235
  filePath: file.filePath,
3556
4236
  hunk,
3557
4237
  records: order(adjudicate(hits, records, options.now)),
@@ -3559,7 +4239,7 @@ function matchToDiff(files, records, options = {}) {
3559
4239
  });
3560
4240
  }
3561
4241
  }
3562
- return matches2;
4242
+ return matches3;
3563
4243
  }
3564
4244
  function place(anchors, filePath, hunk, ranges) {
3565
4245
  let fallback = "miss";
@@ -3708,6 +4388,18 @@ var CHECK_HEADLINES = {
3708
4388
  unchecked: "an anchor in another repository nothing could reach"
3709
4389
  };
3710
4390
  var DAY_MS = 864e5;
4391
+ function anchorResolverCounts(bundle) {
4392
+ let treeSitter = 0;
4393
+ let regex = 0;
4394
+ for (const record of bundle) {
4395
+ for (const anchor of record.frontmatter.strauss_anchors ?? []) {
4396
+ if (!anchor.hash || !anchor.symbol) continue;
4397
+ if (anchor.resolver === "tree-sitter") treeSitter += 1;
4398
+ else regex += 1;
4399
+ }
4400
+ }
4401
+ return { total: treeSitter + regex, treeSitter, regex };
4402
+ }
3711
4403
  function doctor(bundle, options = {}) {
3712
4404
  const thresholds = {
3713
4405
  expiringDays: options.expiringDays ?? DEFAULT_EXPIRING_DAYS,
@@ -3743,6 +4435,7 @@ function doctor(bundle, options = {}) {
3743
4435
  counts,
3744
4436
  groups,
3745
4437
  findingCount,
4438
+ anchorResolvers: anchorResolverCounts(bundle),
3746
4439
  healthy: findingCount === 0
3747
4440
  };
3748
4441
  }
@@ -3989,12 +4682,12 @@ function ageInDays(record, now) {
3989
4682
  }
3990
4683
 
3991
4684
  // src/decision-record.ts
3992
- var import_zod6 = require("zod");
4685
+ var import_zod7 = require("zod");
3993
4686
  var DECISION_TYPE = "decision";
3994
4687
  var NO_DECISION_SLUG = "none";
3995
4688
  var decisionInputSchema = composeInputSchema.omit({ sections: true }).extend({
3996
- alternative: import_zod6.z.string().min(1).optional(),
3997
- impact: import_zod6.z.string().min(1).optional()
4689
+ alternative: import_zod7.z.string().min(1).optional(),
4690
+ impact: import_zod7.z.string().min(1).optional()
3998
4691
  }).strict();
3999
4692
  function composeDecisionRecord(input, writtenBy, writtenAt) {
4000
4693
  const { alternative, impact: impact2, ...rest } = input;
@@ -4036,13 +4729,13 @@ function selectDecisions(records) {
4036
4729
  }
4037
4730
 
4038
4731
  // src/commands/anchor-resolve.ts
4039
- var import_zod8 = require("zod");
4732
+ var import_zod9 = require("zod");
4040
4733
 
4041
4734
  // src/commands/model.ts
4042
- var import_zod7 = require("zod");
4043
- var bundlePath = import_zod7.z.string().min(1).describe("Absolute path to the knowledge base directory.");
4044
- var conceptId = import_zod7.z.string().min(1).describe("e.g. decision.cursor-v2");
4045
- var REPO_ROOT = import_zod7.z.string().min(1).optional().describe(
4735
+ var import_zod8 = require("zod");
4736
+ var bundlePath = import_zod8.z.string().min(1).describe("Absolute path to the knowledge base directory.");
4737
+ var conceptId = import_zod8.z.string().min(1).describe("e.g. decision.cursor-v2");
4738
+ var REPO_ROOT = import_zod8.z.string().min(1).optional().describe(
4046
4739
  "Where the anchored source lives, for the drift check. Defaults to the working directory."
4047
4740
  );
4048
4741
  function define(command) {
@@ -4065,22 +4758,30 @@ function argvFlag(argv, name) {
4065
4758
  }
4066
4759
 
4067
4760
  // src/commands/anchor-resolve.ts
4761
+ function resolverSummary(results) {
4762
+ const names = [
4763
+ ...new Set(
4764
+ results.flatMap((entry) => entry.resolver ? [entry.resolver] : [])
4765
+ )
4766
+ ].sort();
4767
+ return names.length ? `${names.join(" + ")} resolver` : "whole-file";
4768
+ }
4068
4769
  var anchorResolveCommand = define({
4069
4770
  name: "anchor-resolve",
4070
4771
  tool: "kb_anchor_resolve",
4071
4772
  usage: "anchor-resolve <concept-id> [--repo-root <path>] [--offline] [--rebaseline] [--restamp]",
4072
4773
  description: "Resolve a record's anchors: stamp a hash onto anchors that lack one, report drift where the code moved. An anchor naming another repository is read from that remote through a bare cache; --offline uses the cache only. kb_verify's mechanical counterpart \u2014 reach for it when the question is whether the code still is what it was. Exits non-zero on drift.",
4073
- input: import_zod8.z.object({
4774
+ input: import_zod9.z.object({
4074
4775
  bundlePath,
4075
4776
  conceptId,
4076
- repoRoot: import_zod8.z.string().min(1).optional(),
4077
- offline: import_zod8.z.boolean().optional().describe(
4777
+ repoRoot: import_zod9.z.string().min(1).optional(),
4778
+ offline: import_zod9.z.boolean().optional().describe(
4078
4779
  "Resolve foreign anchors from the local repo cache only, never fetching."
4079
4780
  ),
4080
- rebaseline: import_zod8.z.boolean().optional().describe(
4781
+ rebaseline: import_zod9.z.boolean().optional().describe(
4081
4782
  "Accept the current code as the new baseline for anchors that drifted."
4082
4783
  ),
4083
- restamp: import_zod8.z.boolean().optional().describe(
4784
+ restamp: import_zod9.z.boolean().optional().describe(
4084
4785
  "Refresh `resolved_at` on anchors that already match. Off by default, so a green run writes nothing."
4085
4786
  )
4086
4787
  }),
@@ -4109,6 +4810,11 @@ var anchorResolveCommand = define({
4109
4810
  const updated = [];
4110
4811
  let dirty = false;
4111
4812
  const sources = await readSources(anchors, root, offline === true);
4813
+ const resolvers = defaultAnchorResolvers({ offline: offline === true });
4814
+ await prepareResolvers(
4815
+ resolvers,
4816
+ anchors.map((anchor) => anchor.file)
4817
+ );
4112
4818
  for (const anchor of anchors) {
4113
4819
  const base2 = {
4114
4820
  file: anchor.file,
@@ -4125,27 +4831,35 @@ var anchorResolveCommand = define({
4125
4831
  updated.push(anchor);
4126
4832
  continue;
4127
4833
  }
4128
- const resolved = resolveAnchor(source.source, anchor);
4129
- if (!resolved) {
4834
+ const outcome = resolveAnchorSpan(source.source, anchor, resolvers);
4835
+ if (!outcome.ok) {
4130
4836
  results.push({
4131
4837
  ...base2,
4132
4838
  state: "unresolved",
4133
- reason: "symbol-not-found"
4839
+ reason: outcome.reason
4134
4840
  });
4135
4841
  updated.push(anchor);
4136
4842
  continue;
4137
4843
  }
4844
+ const resolved = outcome.span;
4845
+ const producedBy = outcome.resolver;
4138
4846
  const currentHash = hashAnchorText(resolved.text);
4139
4847
  const currentLines = resolved.endLine - resolved.startLine + 1;
4140
4848
  const stamped = {
4141
4849
  ...anchor,
4142
4850
  hash: currentHash,
4143
4851
  lines: currentLines,
4144
- resolved_at: now()
4852
+ resolved_at: now(),
4853
+ ...producedBy ? { resolver: producedBy } : {}
4145
4854
  };
4146
4855
  const pinned = anchor.ref !== void 0 && source.repo !== void 0;
4147
4856
  if (!anchor.hash) {
4148
- results.push({ ...base2, state: "stamped", currentHash });
4857
+ results.push({
4858
+ ...base2,
4859
+ state: "stamped",
4860
+ currentHash,
4861
+ ...producedBy ? { resolver: producedBy } : {}
4862
+ });
4149
4863
  updated.push(stamped);
4150
4864
  dirty = true;
4151
4865
  continue;
@@ -4156,6 +4870,10 @@ var anchorResolveCommand = define({
4156
4870
  state: "drifted",
4157
4871
  currentHash,
4158
4872
  diffSize: lineDelta(anchor, currentLines),
4873
+ ...producedBy ? { resolver: producedBy } : {},
4874
+ // A regex-stamped anchor re-read by tree-sitter drifts because the
4875
+ // resolver changed, not because the code did.
4876
+ ...resolverChanged(source.source, anchor, producedBy) ? { reason: "resolver-changed" } : {},
4159
4877
  ...pinned ? { remoteState: "drifted-from-ref" } : {},
4160
4878
  ...rebaseline ? { rebaselined: true } : {}
4161
4879
  });
@@ -4163,7 +4881,7 @@ var anchorResolveCommand = define({
4163
4881
  if (rebaseline) dirty = true;
4164
4882
  continue;
4165
4883
  }
4166
- const onDefault = pinned ? headHash(source, anchor) : void 0;
4884
+ const onDefault = pinned ? headHash(source, anchor, resolvers) : void 0;
4167
4885
  if (onDefault && onDefault.hash !== anchor.hash) {
4168
4886
  results.push({
4169
4887
  ...base2,
@@ -4179,6 +4897,7 @@ var anchorResolveCommand = define({
4179
4897
  ...base2,
4180
4898
  state: "match",
4181
4899
  currentHash,
4900
+ ...producedBy ? { resolver: producedBy } : {},
4182
4901
  ...pinned ? { remoteState: "matches-ref" } : {}
4183
4902
  });
4184
4903
  const refresh = restamp || anchor.resolved_at === void 0;
@@ -4196,19 +4915,21 @@ var anchorResolveCommand = define({
4196
4915
  if (!frozen) await store.updateAnchors(path, id, updated, actor);
4197
4916
  }
4198
4917
  const frozenNote = frozen ? { frozen: true, note: "base is frozen: nothing was stamped" } : {};
4918
+ const hints = grammarHints();
4919
+ const hintNote = hints.length ? { hints } : {};
4199
4920
  const unreachable = results.filter(
4200
4921
  (entry) => isUncheckedReason(entry.reason)
4201
4922
  ).length;
4202
4923
  const checked = results.length - unreachable;
4203
- const matches2 = results.filter((entry) => entry.state === "match").length;
4204
- const note = `${matches2}/${checked} anchors match${unreachable ? `, ${unreachable} unreachable` : ""}`;
4205
- const clean = checked > 0 && matches2 === checked && unreachable === 0;
4924
+ const matches3 = results.filter((entry) => entry.state === "match").length;
4925
+ const note = `${matches3}/${checked} anchors match${unreachable ? `, ${unreachable} unreachable` : ""}`;
4926
+ const clean = checked > 0 && matches3 === checked && unreachable === 0;
4206
4927
  if (clean) {
4207
4928
  try {
4208
4929
  await store.verify(
4209
4930
  path,
4210
4931
  id,
4211
- `anchor-resolve: ${note} (regex resolver)`,
4932
+ `anchor-resolve: ${note} (${resolverSummary(results)})`,
4212
4933
  actor,
4213
4934
  now()
4214
4935
  );
@@ -4219,17 +4940,25 @@ var anchorResolveCommand = define({
4219
4940
  results,
4220
4941
  verified: false,
4221
4942
  verifyRefused: "self-verification",
4222
- ...frozenNote
4943
+ ...frozenNote,
4944
+ ...hintNote
4223
4945
  };
4224
4946
  }
4225
- return { conceptId: id, results, verified: true, ...frozenNote };
4947
+ return {
4948
+ conceptId: id,
4949
+ results,
4950
+ verified: true,
4951
+ ...frozenNote,
4952
+ ...hintNote
4953
+ };
4226
4954
  }
4227
4955
  return {
4228
4956
  conceptId: id,
4229
4957
  results,
4230
4958
  verified: false,
4231
4959
  ...unreachable ? { note } : {},
4232
- ...frozenNote
4960
+ ...frozenNote,
4961
+ ...hintNote
4233
4962
  };
4234
4963
  },
4235
4964
  // A stored hash that no longer resolves is a broken anchor, not an absence:
@@ -4245,13 +4974,13 @@ var anchorResolveCommand = define({
4245
4974
  function lineDelta(anchor, current) {
4246
4975
  return anchor.lines === void 0 ? null : Math.abs(current - anchor.lines);
4247
4976
  }
4248
- function headHash(source, anchor) {
4977
+ function headHash(source, anchor, resolvers) {
4249
4978
  if (source.head === void 0) return void 0;
4250
- const resolved = resolveAnchor(source.head, anchor);
4251
- if (!resolved) return void 0;
4979
+ const outcome = resolveAnchorSpan(source.head, anchor, resolvers);
4980
+ if (!outcome.ok) return void 0;
4252
4981
  return {
4253
- hash: hashAnchorText(resolved.text),
4254
- lines: resolved.endLine - resolved.startLine + 1
4982
+ hash: hashAnchorText(outcome.span.text),
4983
+ lines: outcome.span.endLine - outcome.span.startLine + 1
4255
4984
  };
4256
4985
  }
4257
4986
  async function readSources(anchors, root, offline) {
@@ -4301,13 +5030,13 @@ async function readSources(anchors, root, offline) {
4301
5030
  }
4302
5031
 
4303
5032
  // src/commands/answer.ts
4304
- var import_zod9 = require("zod");
5033
+ var import_zod10 = require("zod");
4305
5034
  var answerCommand = define({
4306
5035
  name: "answer",
4307
5036
  tool: "kb_answer",
4308
5037
  usage: "answer <concept-id> <answer...>",
4309
5038
  description: "Resolve an open question: set status, stamp who and when, append an Answer section. If the answer overturns a decision or assumption, supersede that record explicitly.",
4310
- input: import_zod9.z.object({ bundlePath, conceptId, answer: import_zod9.z.string().min(1) }),
5039
+ input: import_zod10.z.object({ bundlePath, conceptId, answer: import_zod10.z.string().min(1) }),
4311
5040
  fromArgv: (argv, path) => ({
4312
5041
  bundlePath: path,
4313
5042
  conceptId: argv[1],
@@ -4321,27 +5050,27 @@ var answerCommand = define({
4321
5050
  });
4322
5051
 
4323
5052
  // src/commands/backlinks.ts
4324
- var import_zod10 = require("zod");
5053
+ var import_zod11 = require("zod");
4325
5054
  var backlinksCommand = define({
4326
5055
  name: "backlinks",
4327
5056
  tool: "kb_backlinks",
4328
5057
  usage: "backlinks <concept-id>",
4329
5058
  description: "Who points at this record: every inbound typed causal link (`strauss_links`), one hop, every rel including `related_to`, each with its rel and the standing of the record that made it. Use it when you need the exact edges \u2014 reviewing or renaming a record.",
4330
- input: import_zod10.z.object({ bundlePath, conceptId }),
5059
+ input: import_zod11.z.object({ bundlePath, conceptId }),
4331
5060
  fromArgv: (argv, path) => ({ bundlePath: path, conceptId: argv[1] }),
4332
5061
  run: async ({ store }, { bundlePath: path, conceptId: id }) => store.backlinks(path, id)
4333
5062
  });
4334
5063
 
4335
5064
  // src/commands/catalog.ts
4336
- var import_zod11 = require("zod");
5065
+ var import_zod12 = require("zod");
4337
5066
  var catalogCommand = define({
4338
5067
  name: "catalog",
4339
5068
  tool: "kb_catalog",
4340
5069
  usage: "catalog [type]",
4341
5070
  description: "Lists every record as one line \u2014 concept id, type, title, standing, and a stale flag \u2014 at roughly thirty tokens each. Pick this over kb_load once kb_load refuses: kb_catalog never refuses. Superseded records show only their replacement; fetch bodies with kb_load, kb_pack, kb_query, or kb_trace.",
4342
- input: import_zod11.z.object({
5071
+ input: import_zod12.z.object({
4343
5072
  bundlePath,
4344
- type: import_zod11.z.enum(KB_RECORD_TYPES).optional()
5073
+ type: import_zod12.z.enum(KB_RECORD_TYPES).optional()
4345
5074
  }),
4346
5075
  fromArgv: (argv, path) => ({
4347
5076
  bundlePath: path,
@@ -4396,26 +5125,26 @@ function count(value, noun) {
4396
5125
  }
4397
5126
 
4398
5127
  // src/commands/context.ts
4399
- var import_zod12 = require("zod");
5128
+ var import_zod13 = require("zod");
4400
5129
  var contextCommand = define({
4401
5130
  name: "context",
4402
5131
  tool: "kb_context",
4403
5132
  usage: "context [--profile NAME] [--budget N] [--full-under N] [--format json] [--event NAME]",
4404
5133
  description: "Index block of pinned bases (ids, titles, standing) for injection at context birth. Takes no bundlePath \u2014 reads the workspace pin manifests. Empty when nothing is pinned; refuses over budget rather than truncating. Budget precedence: flags, then the manifest `context[profile]` over `context.default`, then the built-in profile, then package defaults.",
4405
- input: import_zod12.z.object({
4406
- budgetTokens: import_zod12.z.number().int().positive().optional().describe(
5134
+ input: import_zod13.z.object({
5135
+ budgetTokens: import_zod13.z.number().int().positive().optional().describe(
4407
5136
  "Ceiling on the whole emitted block; past it the command refuses with a list of bases rather than truncating. Defaults to 4000."
4408
5137
  ),
4409
- fullUnderTokens: import_zod12.z.number().int().positive().optional().describe(
5138
+ fullUnderTokens: import_zod13.z.number().int().positive().optional().describe(
4410
5139
  "Per-base rendering threshold, applied before the budget: a base whose complete load fits under this arrives as full records instead of index lines, and the whole block still answers to budgetTokens. Off by default \u2014 index-only is the safe default at a context birth, because injected bodies outlive the qualifiers on them; the session-start profile opts tiny bases in at 1500."
4411
5140
  ),
4412
- profile: import_zod12.z.string().optional().describe(
5141
+ profile: import_zod13.z.string().optional().describe(
4413
5142
  "Named budget set: built-ins are session-start (full-under 1500), compact and turn (budget 2500); the manifests' `context` tables override per repo. Unknown names fall through to defaults rather than failing."
4414
5143
  ),
4415
- format: import_zod12.z.enum(["markdown", "json"]).optional().describe(
5144
+ format: import_zod13.z.enum(["markdown", "json"]).optional().describe(
4416
5145
  "CLI envelope for hook protocols that require strict JSON on stdout. MCP callers omit this \u2014 the block itself is identical."
4417
5146
  ),
4418
- event: import_zod12.z.string().optional().describe(
5147
+ event: import_zod13.z.string().optional().describe(
4419
5148
  "hookEventName stamped into the JSON envelope. Only meaningful with format=json."
4420
5149
  )
4421
5150
  }),
@@ -4451,14 +5180,14 @@ var contextCommand = define({
4451
5180
  });
4452
5181
 
4453
5182
  // src/commands/doctor.ts
4454
- var import_zod13 = require("zod");
4455
- var days = (what, fallback) => import_zod13.z.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
5183
+ var import_zod14 = require("zod");
5184
+ var days = (what, fallback) => import_zod14.z.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
4456
5185
  var doctorCommand = define({
4457
5186
  name: "doctor",
4458
5187
  tool: "kb_doctor",
4459
5188
  usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--offline] [--strict]",
4460
5189
  description: "Read-only health sweep: expired, expiring, unverified, aging, orphaned, broken-supersession, superseded-but-cited, drifted and unchecked anchors. Every group is reported even when empty; nothing is written or re-stamped. Use it when picking up a base you have not touched in a while; kb_validate only checks that pointers between records agree.",
4461
- input: import_zod13.z.object({
5190
+ input: import_zod14.z.object({
4462
5191
  bundlePath,
4463
5192
  repoRoot: REPO_ROOT,
4464
5193
  expiringDays: days(
@@ -4473,10 +5202,10 @@ var doctorCommand = define({
4473
5202
  "How long a record may stay `open` or `proposed` before `aging` reports it, in days.",
4474
5203
  DEFAULT_AGING_DAYS
4475
5204
  ),
4476
- offline: import_zod13.z.boolean().optional().describe(
5205
+ offline: import_zod14.z.boolean().optional().describe(
4477
5206
  "Read foreign anchors from the local repo cache only, never fetching."
4478
5207
  ),
4479
- strict: import_zod13.z.boolean().optional().describe(
5208
+ strict: import_zod14.z.boolean().optional().describe(
4480
5209
  "Turn an expired record into a non-zero exit for the CLI. No effect on the report itself."
4481
5210
  )
4482
5211
  }),
@@ -4519,7 +5248,13 @@ var doctorCommand = define({
4519
5248
  ...anchorDrift !== void 0 ? { anchorDrift } : {},
4520
5249
  now: new Date(checkedAt)
4521
5250
  });
4522
- return { bundlePath: path, checkedAt, ...report };
5251
+ const hints = grammarHints();
5252
+ return {
5253
+ bundlePath: path,
5254
+ checkedAt,
5255
+ ...report,
5256
+ ...hints.length ? { hints } : {}
5257
+ };
4523
5258
  },
4524
5259
  render: (result) => render2(result),
4525
5260
  // Only expiry, and only under --strict. The other seven checks report debt a
@@ -4537,12 +5272,15 @@ function render2(result) {
4537
5272
  `records: ${result.recordCount}`,
4538
5273
  `thresholds: expiring within ${thresholds.expiringDays}d, unverified over ${thresholds.unverifiedDays}d, aging over ${thresholds.agingDays}d`,
4539
5274
  `checked: ${result.checkedAt}`,
5275
+ ...result.anchorResolvers.total ? [
5276
+ `anchors: ${result.anchorResolvers.total} hashed \u2014 ${result.anchorResolvers.treeSitter} tree-sitter, ${result.anchorResolvers.regex} regex`
5277
+ ] : [],
4540
5278
  ""
4541
5279
  ];
4542
- const width = Math.max(...result.groups.map((group2) => group2.check.length));
5280
+ const width2 = Math.max(...result.groups.map((group2) => group2.check.length));
4543
5281
  for (const group2 of result.groups) {
4544
5282
  lines.push(
4545
- ` ${group2.check.padEnd(width)} ${String(group2.count).padStart(3)} ${group2.headline}`
5283
+ ` ${group2.check.padEnd(width2)} ${String(group2.count).padStart(3)} ${group2.headline}`
4546
5284
  );
4547
5285
  }
4548
5286
  for (const group2 of result.groups) {
@@ -4554,6 +5292,7 @@ function render2(result) {
4554
5292
  );
4555
5293
  }
4556
5294
  }
5295
+ for (const hint of result.hints ?? []) lines.push("", hint);
4557
5296
  lines.push(
4558
5297
  "",
4559
5298
  result.healthy ? "Nothing to repair." : `${result.findingCount} finding${result.findingCount === 1 ? "" : "s"} across ${result.groups.filter((group2) => group2.count).length} of ${result.groups.length} checks.`
@@ -4562,19 +5301,19 @@ function render2(result) {
4562
5301
  }
4563
5302
 
4564
5303
  // src/commands/impact.ts
4565
- var import_zod14 = require("zod");
5304
+ var import_zod15 = require("zod");
4566
5305
  var impactCommand = define({
4567
5306
  name: "impact",
4568
5307
  tool: "kb_impact",
4569
5308
  usage: "impact <concept-id> [--depth N] [--rels a,b]",
4570
5309
  description: "What breaks if this record changes: its transitive set of dependants, each with its standing. Each rel declares which of its ends depends on the other, and the walk follows each rel in its own direction. Naming `related_to` or an unknown rel in `rels` is an error. kb_backlinks gives one flat hop.",
4571
- input: import_zod14.z.object({
5310
+ input: import_zod15.z.object({
4572
5311
  bundlePath,
4573
5312
  conceptId,
4574
- depth: import_zod14.z.number().int().positive().optional().describe(
5313
+ depth: import_zod15.z.number().int().positive().optional().describe(
4575
5314
  "Hops out from the record. Unbounded when omitted; a walk this cuts reports truncated: true."
4576
5315
  ),
4577
- rels: import_zod14.z.array(import_zod14.z.enum(KB_CAUSAL_LINK_RELS)).optional().describe(
5316
+ rels: import_zod15.z.array(import_zod15.z.enum(KB_CAUSAL_LINK_RELS)).optional().describe(
4578
5317
  "Narrow which rels the walk follows. Defaults to every rel that carries a dependence \u2014 all but related_to."
4579
5318
  )
4580
5319
  }),
@@ -4595,13 +5334,13 @@ var impactCommand = define({
4595
5334
  });
4596
5335
 
4597
5336
  // src/commands/list.ts
4598
- var import_zod15 = require("zod");
5337
+ var import_zod16 = require("zod");
4599
5338
  var listCommand = define({
4600
5339
  name: "list",
4601
5340
  tool: "kb_list",
4602
5341
  usage: "list [type]",
4603
5342
  description: "Every record, optionally one type. For enumerating; use kb_query for a question.",
4604
- input: import_zod15.z.object({ bundlePath, type: import_zod15.z.enum(KB_RECORD_TYPES).optional() }),
5343
+ input: import_zod16.z.object({ bundlePath, type: import_zod16.z.enum(KB_RECORD_TYPES).optional() }),
4605
5344
  fromArgv: (argv, path) => ({ bundlePath: path, type: argv[1] }),
4606
5345
  run: async ({ store }, { bundlePath: path, type }) => (await store.list(path, type)).map((record) => ({
4607
5346
  conceptId: record.conceptId,
@@ -4613,17 +5352,17 @@ var listCommand = define({
4613
5352
  });
4614
5353
 
4615
5354
  // src/commands/load.ts
4616
- var import_zod16 = require("zod");
5355
+ var import_zod17 = require("zod");
4617
5356
  var loadCommand = define({
4618
5357
  name: "load",
4619
5358
  tool: "kb_load",
4620
5359
  usage: "load [type] [--budget N | --all] [--repo-root PATH]",
4621
5360
  description: "Load the whole base, each record with its standing \u2014 call it first, at the point of use, since compaction drops it. Superseded records arrive as stubs; kb_trace has the history. Over budget it refuses: kb_catalog, then kb_pack, or narrow with `type`; `all` bypasses. Never read record files directly \u2014 only kb_* tools resolve supersession. `digest` stamps the base's content, so hooks know when to reload.",
4622
- input: import_zod16.z.object({
5361
+ input: import_zod17.z.object({
4623
5362
  bundlePath,
4624
- type: import_zod16.z.enum(KB_RECORD_TYPES).optional(),
4625
- budgetTokens: import_zod16.z.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
4626
- all: import_zod16.z.boolean().optional().describe(
5363
+ type: import_zod17.z.enum(KB_RECORD_TYPES).optional(),
5364
+ budgetTokens: import_zod17.z.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
5365
+ all: import_zod17.z.boolean().optional().describe(
4627
5366
  "Loads the entire base regardless of size, bypassing the token budget; mutually exclusive with budgetTokens."
4628
5367
  ),
4629
5368
  repoRoot: REPO_ROOT
@@ -4665,25 +5404,25 @@ var loadCommand = define({
4665
5404
  });
4666
5405
 
4667
5406
  // src/commands/log.ts
4668
- var import_zod17 = require("zod");
5407
+ var import_zod18 = require("zod");
4669
5408
  var logCommand = define({
4670
5409
  name: "log",
4671
5410
  tool: "kb_log",
4672
5411
  usage: "log",
4673
5412
  description: "Who touched what, and when. Append-only; malformed lines are reported, never repaired.",
4674
- input: import_zod17.z.object({ bundlePath }),
5413
+ input: import_zod18.z.object({ bundlePath }),
4675
5414
  fromArgv: (_argv, path) => ({ bundlePath: path }),
4676
5415
  run: ({ store }, { bundlePath: path }) => store.readLog(path)
4677
5416
  });
4678
5417
 
4679
5418
  // src/commands/no-decision.ts
4680
- var import_zod18 = require("zod");
5419
+ var import_zod19 = require("zod");
4681
5420
  var noDecisionCommand = define({
4682
5421
  name: "no-decision",
4683
5422
  tool: "kb_no_decision",
4684
5423
  usage: "no-decision <reason...>",
4685
5424
  description: "Record in one sentence that a piece of work had nothing to decide. Idempotent.",
4686
- input: import_zod18.z.object({ bundlePath, reason: import_zod18.z.string().min(1) }),
5425
+ input: import_zod19.z.object({ bundlePath, reason: import_zod19.z.string().min(1) }),
4687
5426
  fromArgv: (argv, path) => ({
4688
5427
  bundlePath: path,
4689
5428
  reason: argv.slice(1).join(" ").trim()
@@ -4700,20 +5439,20 @@ var noDecisionCommand = define({
4700
5439
  });
4701
5440
 
4702
5441
  // src/commands/pack.ts
4703
- var import_zod19 = require("zod");
5442
+ var import_zod20 = require("zod");
4704
5443
  var packCommand = define({
4705
5444
  name: "pack",
4706
5445
  tool: "kb_pack",
4707
5446
  usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
4708
5447
  description: "Bounded neighbourhood around one record: within `hops`, ranked, cut to `maxNodes`, with every cut record named under Excluded. Use when the base is over kb_load's budget and the work centres on a record you can name. Refuses over budget rather than truncating. Everything below the header is byte-stable across runs. Resolves supersession like kb_load.",
4709
- input: import_zod19.z.object({
5448
+ input: import_zod20.z.object({
4710
5449
  bundlePath,
4711
5450
  conceptId,
4712
- hops: import_zod19.z.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
4713
- maxNodes: import_zod19.z.number().int().positive().optional().describe(
5451
+ hops: import_zod20.z.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
5452
+ maxNodes: import_zod20.z.number().int().positive().optional().describe(
4714
5453
  "How many records the pack may hold, root included. Defaults to 20."
4715
5454
  ),
4716
- budgetTokens: import_zod19.z.number().int().positive().optional().describe(
5455
+ budgetTokens: import_zod20.z.number().int().positive().optional().describe(
4717
5456
  "Approximate token ceiling over what is actually emitted. Defaults to 25000."
4718
5457
  )
4719
5458
  }),
@@ -4800,22 +5539,22 @@ function warningLabel(warning) {
4800
5539
  }
4801
5540
 
4802
5541
  // src/commands/pin.ts
4803
- var import_zod20 = require("zod");
5542
+ var import_zod21 = require("zod");
4804
5543
  var pinCommand = define({
4805
5544
  name: "pin",
4806
5545
  tool: "kb_pin",
4807
5546
  usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
4808
5547
  description: "Pin a base into a workspace manifest so kb_context surfaces it. Layers, nearest wins: project `.strauss/kb-pins.json` (default), `--local` (personal, gitignored), `--user` (`~/.strauss`). Idempotent; `--mode full|index`, `--profiles`, `--frozen`/`--unfreeze` update only those fields. A path with no records pins with a warning. Never touches the base itself.",
4809
- input: import_zod20.z.object({
5548
+ input: import_zod21.z.object({
4810
5549
  bundlePath,
4811
- mode: import_zod20.z.enum(["full", "index"]).optional().describe(
5550
+ mode: import_zod21.z.enum(["full", "index"]).optional().describe(
4812
5551
  "full: always emit this base's records whole (still under the block budget); index: never upgrade. Absent: the profile's full-under threshold decides."
4813
5552
  ),
4814
- profiles: import_zod20.z.array(import_zod20.z.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
4815
- layer: import_zod20.z.enum(["project", "local", "user"]).optional().describe(
5553
+ profiles: import_zod21.z.array(import_zod21.z.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
5554
+ layer: import_zod21.z.enum(["project", "local", "user"]).optional().describe(
4816
5555
  "Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
4817
5556
  ),
4818
- frozen: import_zod20.z.boolean().optional().describe(
5557
+ frozen: import_zod21.z.boolean().optional().describe(
4819
5558
  "true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
4820
5559
  )
4821
5560
  }),
@@ -4844,29 +5583,29 @@ var pinCommand = define({
4844
5583
  });
4845
5584
 
4846
5585
  // src/commands/pins.ts
4847
- var import_zod21 = require("zod");
5586
+ var import_zod22 = require("zod");
4848
5587
  var pinsCommand = define({
4849
5588
  name: "pins",
4850
5589
  tool: "kb_pins",
4851
5590
  usage: "pins",
4852
5591
  description: "Every pinned base across the manifest layers, with its layer and whether it resolves to records. Takes no bundlePath.",
4853
- input: import_zod21.z.object({}),
5592
+ input: import_zod22.z.object({}),
4854
5593
  fromArgv: () => ({}),
4855
5594
  run: ({ store }) => listPins(store, process.cwd())
4856
5595
  });
4857
5596
 
4858
5597
  // src/commands/query.ts
4859
- var import_zod22 = require("zod");
5598
+ var import_zod23 = require("zod");
4860
5599
  var queryCommand = define({
4861
5600
  name: "query",
4862
5601
  tool: "kb_query",
4863
5602
  usage: "query <text...> [--repo-root PATH]",
4864
5603
  description: "Search; every hit carries its standing. Flagged, never filtered: a superseded hit returns with its replacement, a rejected one is marked. Prefer kb_load when the base fits its budget \u2014 a full read beats search. Results are volatile: place them at the tail, not the cached prefix. Never read record files directly.",
4865
- input: import_zod22.z.object({
5604
+ input: import_zod23.z.object({
4866
5605
  bundlePath,
4867
- text: import_zod22.z.string().optional(),
4868
- type: import_zod22.z.enum(KB_RECORD_TYPES).optional(),
4869
- includeNonCurrent: import_zod22.z.boolean().optional(),
5606
+ text: import_zod23.z.string().optional(),
5607
+ type: import_zod23.z.enum(KB_RECORD_TYPES).optional(),
5608
+ includeNonCurrent: import_zod23.z.boolean().optional(),
4870
5609
  repoRoot: REPO_ROOT
4871
5610
  }),
4872
5611
  // `--repo-root` is a flag, so its value must not fall into the search text.
@@ -4898,40 +5637,138 @@ var queryCommand = define({
4898
5637
  });
4899
5638
 
4900
5639
  // src/commands/read-index.ts
4901
- var import_zod23 = require("zod");
5640
+ var import_zod24 = require("zod");
4902
5641
  var readIndexCommand = define({
4903
5642
  name: "index",
4904
5643
  tool: "kb_index",
4905
5644
  usage: "index",
4906
5645
  description: "The index \u2014 title, type, status, description per record \u2014 rebuilt if stale. Cheapest re-orientation after compaction: call it (or kb_context) first, then kb_load or fetch by id.",
4907
- input: import_zod23.z.object({ bundlePath }),
5646
+ input: import_zod24.z.object({ bundlePath }),
4908
5647
  fromArgv: (_argv, path) => ({ bundlePath: path }),
4909
5648
  run: ({ store }, { bundlePath: path }) => store.readIndex(path)
4910
5649
  });
4911
5650
 
4912
5651
  // src/commands/schema.ts
4913
- var import_zod24 = require("zod");
5652
+ var import_zod25 = require("zod");
4914
5653
  var schemaCommand = define({
4915
5654
  name: "schema",
4916
5655
  tool: "kb_schema",
4917
5656
  usage: "schema",
4918
5657
  description: "JSON Schema for frontmatter, write input, and log entries, generated from the enforcing code.",
4919
- input: import_zod24.z.object({}),
5658
+ input: import_zod25.z.object({}),
4920
5659
  fromArgv: () => ({}),
4921
5660
  run: () => Promise.resolve(kbJsonSchemas())
4922
5661
  });
4923
5662
 
5663
+ // src/commands/stamp.ts
5664
+ var import_promises9 = require("fs/promises");
5665
+ var import_zod26 = require("zod");
5666
+ var DIGEST = /^[0-9a-f]{64}$/;
5667
+ var stampCommand = define({
5668
+ name: "stamp",
5669
+ tool: "kb_stamp",
5670
+ usage: "stamp [--bundle PATH] [--since DIGEST|FILE]",
5671
+ description: "Content stamp of a base \u2014 `load`'s digest, record counts, per-record digests \u2014 without any bodies. Takes no bundlePath to stamp every pinned base. With `since`, reports only the bases that moved, naming the changed ids when the baseline is a prior stamp; silent when nothing changed. Reads, never writes.",
5672
+ input: import_zod26.z.object({
5673
+ bundlePath: import_zod26.z.string().min(1).optional().describe(
5674
+ "Absolute path to one knowledge base. Omit to stamp every pinned base."
5675
+ ),
5676
+ since: import_zod26.z.string().min(1).optional().describe(
5677
+ "Prior digest, or path to a prior `stamp --json`; only moved bases return, with changed ids when the baseline is a file."
5678
+ )
5679
+ }),
5680
+ fromArgv: (argv, path, _stdin, bundleExplicit) => {
5681
+ const since = argvFlag(argv, "--since");
5682
+ return {
5683
+ ...bundleExplicit ? { bundlePath: path } : {},
5684
+ ...since !== void 0 ? { since } : {}
5685
+ };
5686
+ },
5687
+ run: async ({ store }, { bundlePath: bundlePath2, since }) => {
5688
+ const targets = bundlePath2 ? [bundlePath2] : (await readMergedPins(process.cwd())).pins.map(
5689
+ (pin) => pin.absolutePath
5690
+ );
5691
+ if (since !== void 0 && DIGEST.test(since) && targets.length > 1) {
5692
+ throw new KbStampDigestBaselineError(since);
5693
+ }
5694
+ const stamps = await Promise.all(
5695
+ targets.map((target) => store.stamp(target))
5696
+ );
5697
+ if (since === void 0) {
5698
+ return stamps.map((stamp) => ({ ...stamp, changed: null }));
5699
+ }
5700
+ const baseline = await readBaseline(since);
5701
+ const reports = [];
5702
+ for (const stamp of stamps) {
5703
+ const before = baseline.byPath.get(stamp.path);
5704
+ if (baseline.digest !== null) {
5705
+ if (baseline.digest === stamp.digest) continue;
5706
+ reports.push({ ...stamp, changed: null });
5707
+ continue;
5708
+ }
5709
+ if (before && before.digest === stamp.digest) continue;
5710
+ reports.push({ ...stamp, changed: changedIds(before?.records, stamp) });
5711
+ }
5712
+ return reports;
5713
+ },
5714
+ render: (result) => result.map((report) => {
5715
+ const counts = `${report.recordCount} record(s), ${report.superseded} superseded`;
5716
+ const head = `${report.path} ${report.digest} ${counts}${report.newestAt ? ` newest ${report.newestAt}` : ""}`;
5717
+ return report.changed?.length ? `${head}
5718
+ changed: ${report.changed.join(", ")}` : head;
5719
+ }).join("\n")
5720
+ });
5721
+ function changedIds(before, stamp) {
5722
+ const now = new Map(
5723
+ stamp.records.map((record) => [record.conceptId, record.digest])
5724
+ );
5725
+ const ids = /* @__PURE__ */ new Set();
5726
+ for (const [conceptId2, digest] of now) {
5727
+ if (before?.get(conceptId2) !== digest) ids.add(conceptId2);
5728
+ }
5729
+ for (const conceptId2 of before?.keys() ?? []) {
5730
+ if (!now.has(conceptId2)) ids.add(conceptId2);
5731
+ }
5732
+ return [...ids].sort();
5733
+ }
5734
+ async function readBaseline(since) {
5735
+ if (DIGEST.test(since)) return { digest: since, byPath: /* @__PURE__ */ new Map() };
5736
+ let parsed;
5737
+ try {
5738
+ parsed = JSON.parse(await (0, import_promises9.readFile)(since, "utf8"));
5739
+ } catch {
5740
+ throw new KbStampBaselineError(since);
5741
+ }
5742
+ const entries = Array.isArray(parsed) ? parsed : parsed?.stamps ?? [];
5743
+ const byPath = /* @__PURE__ */ new Map();
5744
+ for (const entry of entries) {
5745
+ if (typeof entry?.path !== "string" || typeof entry?.digest !== "string") {
5746
+ continue;
5747
+ }
5748
+ byPath.set(entry.path, {
5749
+ digest: entry.digest,
5750
+ records: new Map(
5751
+ (entry.records ?? []).map((record) => [
5752
+ record.conceptId,
5753
+ record.digest
5754
+ ])
5755
+ )
5756
+ });
5757
+ }
5758
+ return { digest: null, byPath };
5759
+ }
5760
+
4924
5761
  // src/commands/status.ts
4925
- var import_zod25 = require("zod");
5762
+ var import_zod27 = require("zod");
4926
5763
  var statusCommand = define({
4927
5764
  name: "status",
4928
5765
  tool: "kb_status",
4929
5766
  usage: "status <concept-id> <status>",
4930
5767
  description: "Move a record's status. Compare-and-swap: a concurrent change fails instead of being overwritten.",
4931
- input: import_zod25.z.object({
5768
+ input: import_zod27.z.object({
4932
5769
  bundlePath,
4933
5770
  conceptId,
4934
- status: import_zod25.z.enum(KB_RECORD_STATUSES)
5771
+ status: import_zod27.z.enum(KB_RECORD_STATUSES)
4935
5772
  }),
4936
5773
  fromArgv: (argv, path) => ({
4937
5774
  bundlePath: path,
@@ -4946,13 +5783,13 @@ var statusCommand = define({
4946
5783
  });
4947
5784
 
4948
5785
  // src/commands/supersede.ts
4949
- var import_zod26 = require("zod");
5786
+ var import_zod28 = require("zod");
4950
5787
  var supersedeCommand = define({
4951
5788
  name: "supersede",
4952
5789
  tool: "kb_supersede",
4953
5790
  usage: "supersede <concept-id> <replacement-id>",
4954
5791
  description: "Mark a record superseded by another, linked in both directions. Use instead of editing a record whose meaning changed.",
4955
- input: import_zod26.z.object({ bundlePath, conceptId, replacementId: conceptId }),
5792
+ input: import_zod28.z.object({ bundlePath, conceptId, replacementId: conceptId }),
4956
5793
  fromArgv: (argv, path) => ({
4957
5794
  bundlePath: path,
4958
5795
  conceptId: argv[1],
@@ -4966,16 +5803,16 @@ var supersedeCommand = define({
4966
5803
  });
4967
5804
 
4968
5805
  // src/commands/sync-instructions.ts
4969
- var import_zod27 = require("zod");
5806
+ var import_zod29 = require("zod");
4970
5807
  var syncInstructionsCommand = define({
4971
5808
  name: "sync-instructions",
4972
5809
  usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
4973
5810
  description: "CLI-only: plant the kb_context block between sentinel comments in AGENTS.md or CLAUDE.md, idempotently.",
4974
- input: import_zod27.z.object({
4975
- file: import_zod27.z.string().min(1).describe("The instruction file to edit in place."),
4976
- budgetTokens: import_zod27.z.number().int().positive().optional(),
4977
- fullUnderTokens: import_zod27.z.number().int().positive().optional(),
4978
- profile: import_zod27.z.string().optional()
5811
+ input: import_zod29.z.object({
5812
+ file: import_zod29.z.string().min(1).describe("The instruction file to edit in place."),
5813
+ budgetTokens: import_zod29.z.number().int().positive().optional(),
5814
+ fullUnderTokens: import_zod29.z.number().int().positive().optional(),
5815
+ profile: import_zod29.z.string().optional()
4979
5816
  }),
4980
5817
  fromArgv: (argv) => {
4981
5818
  const budget = argvFlag(argv, "--budget");
@@ -5001,17 +5838,17 @@ var syncInstructionsCommand = define({
5001
5838
  });
5002
5839
 
5003
5840
  // src/commands/trace.ts
5004
- var import_zod28 = require("zod");
5841
+ var import_zod30 = require("zod");
5005
5842
  var traceCommand = define({
5006
5843
  name: "trace",
5007
5844
  tool: "kb_trace",
5008
5845
  usage: "trace <concept-id> [edges...]",
5009
5846
  description: 'Timeline of how a position was reached, ordered by write time, following supersession, shared anchors and shared sources. Includes rejected, draft and superseded records \u2014 in a history they are the content. For "why is it like this"; kb_load answers "what holds now".',
5010
- input: import_zod28.z.object({
5847
+ input: import_zod30.z.object({
5011
5848
  bundlePath,
5012
5849
  conceptId,
5013
- edges: import_zod28.z.array(import_zod28.z.enum(TRACE_EDGES)).optional(),
5014
- depth: import_zod28.z.number().int().positive().optional()
5850
+ edges: import_zod30.z.array(import_zod30.z.enum(TRACE_EDGES)).optional(),
5851
+ depth: import_zod30.z.number().int().positive().optional()
5015
5852
  }),
5016
5853
  fromArgv: (argv, path) => ({
5017
5854
  bundlePath: path,
@@ -5033,37 +5870,37 @@ var traceCommand = define({
5033
5870
  });
5034
5871
 
5035
5872
  // src/commands/types.ts
5036
- var import_zod29 = require("zod");
5873
+ var import_zod31 = require("zod");
5037
5874
  var typesCommand = define({
5038
5875
  name: "types",
5039
5876
  tool: "kb_types",
5040
5877
  usage: "types",
5041
5878
  description: "The twelve record types with their purpose, body sections, and starting status. Read this before writing rather than guessing headings \u2014 a section the type does not define is rejected.",
5042
- input: import_zod29.z.object({}),
5879
+ input: import_zod31.z.object({}),
5043
5880
  fromArgv: () => ({}),
5044
5881
  run: () => Promise.resolve(RECORD_TYPES)
5045
5882
  });
5046
5883
 
5047
5884
  // src/commands/unpin.ts
5048
- var import_zod30 = require("zod");
5885
+ var import_zod32 = require("zod");
5049
5886
  var unpinCommand = define({
5050
5887
  name: "unpin",
5051
5888
  tool: "kb_unpin",
5052
5889
  usage: "unpin [bundle-path]",
5053
5890
  description: "Remove a base from every manifest layer that holds it. Reports the layers touched.",
5054
- input: import_zod30.z.object({ bundlePath }),
5891
+ input: import_zod32.z.object({ bundlePath }),
5055
5892
  fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
5056
5893
  run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
5057
5894
  });
5058
5895
 
5059
5896
  // src/commands/validate.ts
5060
- var import_zod31 = require("zod");
5897
+ var import_zod33 = require("zod");
5061
5898
  var validateCommand = define({
5062
5899
  name: "validate",
5063
5900
  tool: "kb_validate",
5064
5901
  usage: "validate",
5065
5902
  description: "Check pointers no single record can see: supersession links that disagree between the two records, typed causal links, and assumptions that cite sources. Each finding carries a severity: errors fail the exit code, warnings do not.",
5066
- input: import_zod31.z.object({ bundlePath }),
5903
+ input: import_zod33.z.object({ bundlePath }),
5067
5904
  fromArgv: (_argv, path) => ({ bundlePath: path }),
5068
5905
  run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
5069
5906
  // Warnings never fail the exit code; every other severity does.
@@ -5073,16 +5910,16 @@ var validateCommand = define({
5073
5910
  });
5074
5911
 
5075
5912
  // src/commands/verify.ts
5076
- var import_zod32 = require("zod");
5913
+ var import_zod34 = require("zod");
5077
5914
  var verifyCommand = define({
5078
5915
  name: "verify",
5079
5916
  tool: "kb_verify",
5080
5917
  usage: "verify <concept-id> --note <text>",
5081
5918
  description: "Append a verified[] event: who checked, when, and what was found. Append-only. A record's own generator is refused unless the actor is `human:`-prefixed.",
5082
- input: import_zod32.z.object({
5919
+ input: import_zod34.z.object({
5083
5920
  bundlePath,
5084
5921
  conceptId,
5085
- note: import_zod32.z.string().refine((s) => s.trim().length > 0, {
5922
+ note: import_zod34.z.string().refine((s) => s.trim().length > 0, {
5086
5923
  message: "note must say what the check found"
5087
5924
  })
5088
5925
  }),
@@ -5102,15 +5939,15 @@ var verifyCommand = define({
5102
5939
  });
5103
5940
 
5104
5941
  // src/commands/write.ts
5105
- var import_zod33 = require("zod");
5942
+ var import_zod35 = require("zod");
5106
5943
  var writeCommand = define({
5107
5944
  name: "write",
5108
5945
  tool: "kb_write",
5109
5946
  usage: "write <type> < record.json",
5110
5947
  description: "Write one record. Search first \u2014 a duplicate concept id is rejected, not overwritten; kb_types lists each type's sections. An unsourced claim is an `assumption` with assumption: true, never a vague `fact`. Conflicting records get a `risk`, `open-question`, or superseding `decision`. Prefer a new short record over overloading one. Never delete; supersede.",
5111
- input: import_zod33.z.object({
5948
+ input: import_zod35.z.object({
5112
5949
  bundlePath,
5113
- type: import_zod33.z.enum(KB_RECORD_TYPES),
5950
+ type: import_zod35.z.enum(KB_RECORD_TYPES),
5114
5951
  input: composeInputSchema
5115
5952
  }),
5116
5953
  fromArgv: async (argv, path, stdin) => ({
@@ -5134,13 +5971,13 @@ var writeCommand = define({
5134
5971
  });
5135
5972
 
5136
5973
  // src/commands/write-decision.ts
5137
- var import_zod34 = require("zod");
5974
+ var import_zod36 = require("zod");
5138
5975
  var writeDecisionCommand = define({
5139
5976
  name: "write-decision",
5140
5977
  tool: "kb_write_decision",
5141
5978
  usage: "write-decision < decision.json",
5142
5979
  description: "Write a decision, with `alternative` (what was rejected and why) and `impact` as fields. Record one when a later reader would otherwise simplify the constraint away; skip when the diff already answers it. `sources` for material read, `anchors` for code, `relatedConceptIds` for records.",
5143
- input: import_zod34.z.object({ bundlePath, input: decisionInputSchema }),
5980
+ input: import_zod36.z.object({ bundlePath, input: decisionInputSchema }),
5144
5981
  fromArgv: async (_argv, path, stdin) => ({
5145
5982
  bundlePath: path,
5146
5983
  input: JSON.parse(await stdin())
@@ -5180,6 +6017,7 @@ var KB_COMMANDS = [
5180
6017
  listCommand,
5181
6018
  readIndexCommand,
5182
6019
  logCommand,
6020
+ stampCommand,
5183
6021
  validateCommand,
5184
6022
  doctorCommand,
5185
6023
  schemaCommand,
@@ -5199,7 +6037,7 @@ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
5199
6037
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
5200
6038
 
5201
6039
  // src/version.ts
5202
- var VERSION = true ? "0.1.15" : "0.0.0-dev";
6040
+ var VERSION = true ? "0.1.17" : "0.0.0-dev";
5203
6041
 
5204
6042
  // src/mcp.ts
5205
6043
  function createKbMcpServer() {
@@ -5238,10 +6076,14 @@ async function runKbMcpServer() {
5238
6076
  }
5239
6077
 
5240
6078
  // src/cli.ts
5241
- var import_node_path9 = require("path");
6079
+ var import_node_path12 = require("path");
5242
6080
  async function runKbCli(argv) {
5243
6081
  const { flags, literal } = takeLiteral(argv);
5244
- const { bundle, rest: withFlags } = takeBundle(flags);
6082
+ const {
6083
+ bundle,
6084
+ explicit: bundleExplicit,
6085
+ rest: withFlags
6086
+ } = takeBundle(flags);
5245
6087
  const name = withFlags[0] ?? "";
5246
6088
  if (!name || name === "-h" || name === "--help") {
5247
6089
  process.stdout.write(usage());
@@ -5262,7 +6104,7 @@ async function runKbCli(argv) {
5262
6104
  ...json ? withFlags.filter((argument) => argument !== "--json") : withFlags,
5263
6105
  ...literal
5264
6106
  ];
5265
- const raw = await command.fromArgv(rest, bundle, readStdin);
6107
+ const raw = await command.fromArgv(rest, bundle, readStdin, bundleExplicit);
5266
6108
  const parsed = command.input.safeParse(raw);
5267
6109
  if (!parsed.success) {
5268
6110
  die(
@@ -5284,6 +6126,7 @@ async function runKbCli(argv) {
5284
6126
  if (command.failsWhen?.(result, parsed.data)) process.exitCode = 1;
5285
6127
  if (result === "") return;
5286
6128
  const text = command.render && !json ? command.render(result) : typeof result === "string" ? result : JSON.stringify(result, null, 2);
6129
+ if (text === "") return;
5287
6130
  process.stdout.write(text.endsWith("\n") ? text : `${text}
5288
6131
  `);
5289
6132
  }
@@ -5295,11 +6138,15 @@ function takeLiteral(argv) {
5295
6138
  function takeBundle(argv) {
5296
6139
  const at = argv.indexOf("--bundle");
5297
6140
  if (at === -1) {
5298
- return { bundle: (0, import_node_path9.join)(process.cwd(), KB_DIR), rest: argv };
6141
+ return { bundle: (0, import_node_path12.join)(process.cwd(), KB_DIR), explicit: false, rest: argv };
5299
6142
  }
5300
6143
  const bundle = argv[at + 1];
5301
6144
  if (!bundle) die("--bundle requires a path");
5302
- return { bundle, rest: [...argv.slice(0, at), ...argv.slice(at + 2)] };
6145
+ return {
6146
+ bundle,
6147
+ explicit: true,
6148
+ rest: [...argv.slice(0, at), ...argv.slice(at + 2)]
6149
+ };
5303
6150
  }
5304
6151
  function readStdin() {
5305
6152
  return new Promise((resolve6, reject) => {
@@ -5321,14 +6168,14 @@ function summarise(description) {
5321
6168
  return sentence.length > 78 ? `${sentence.slice(0, 75)}\u2026` : sentence;
5322
6169
  }
5323
6170
  function usage() {
5324
- const width = Math.max(...KB_COMMANDS.map((command) => command.usage.length));
6171
+ const width2 = Math.max(...KB_COMMANDS.map((command) => command.usage.length));
5325
6172
  return [
5326
6173
  "strauss-kb \u2014 knowledge base commands",
5327
6174
  "",
5328
6175
  "Usage: strauss-kb [--bundle PATH] <command> [args]",
5329
6176
  "",
5330
6177
  ...KB_COMMANDS.map(
5331
- (command) => ` ${command.usage.padEnd(width)} ${summarise(command.description)}`
6178
+ (command) => ` ${command.usage.padEnd(width2)} ${summarise(command.description)}`
5332
6179
  ),
5333
6180
  "",
5334
6181
  ` --bundle PATH defaults to ./${KB_DIR}`,
@@ -5389,6 +6236,7 @@ function usage() {
5389
6236
  RECORD_TYPES,
5390
6237
  SEARCH_INDEX_FILE,
5391
6238
  TRACE_EDGES,
6239
+ TreeSitterResolver,
5392
6240
  adjudicate,
5393
6241
  anchorFilePath,
5394
6242
  assertBaseNotFrozen,
@@ -5403,9 +6251,14 @@ function usage() {
5403
6251
  contextProfileBudgets,
5404
6252
  createKbMcpServer,
5405
6253
  decisionInputSchema,
6254
+ defaultAnchorResolvers,
5406
6255
  detectAnchorDrift,
5407
6256
  doctor,
5408
6257
  edgeNeighbours,
6258
+ ensureGrammar,
6259
+ grammarHints,
6260
+ grammarManifest,
6261
+ grammarsCacheRoot,
5409
6262
  hashAnchorText,
5410
6263
  impact,
5411
6264
  inboundIndex,
@@ -5423,6 +6276,7 @@ function usage() {
5423
6276
  kbRecordFrontmatterSchema,
5424
6277
  kbSourceSchema,
5425
6278
  kbVerifiedEventSchema,
6279
+ languageForFile,
5426
6280
  listPins,
5427
6281
  loadQmd,
5428
6282
  matchToDiff,
@@ -5432,6 +6286,7 @@ function usage() {
5432
6286
  parseLog,
5433
6287
  parseMarkdownWithFrontmatter,
5434
6288
  pinBase,
6289
+ prepareResolvers,
5435
6290
  readMergedPins,
5436
6291
  readPinsLayer,
5437
6292
  readRemoteAnchors,
@@ -5442,6 +6297,7 @@ function usage() {
5442
6297
  renderLogEntry,
5443
6298
  repoCacheDir,
5444
6299
  resolveAnchor,
6300
+ resolveAnchorSpan,
5445
6301
  resolveHeads,
5446
6302
  resolveHits,
5447
6303
  resolvePinPath,
@@ -5454,6 +6310,7 @@ function usage() {
5454
6310
  syncInstructions,
5455
6311
  toHookJson,
5456
6312
  trace,
6313
+ treeSitterLanguages,
5457
6314
  unpinBase,
5458
6315
  validateBundle
5459
6316
  });