@saasontools/strauss-kb 0.1.16 → 0.1.18

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,12 +78,14 @@ __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,
84
85
  backlinks: () => backlinks,
85
86
  buildContext: () => buildContext,
86
87
  catalog: () => catalog,
88
+ classifyDrift: () => classifyDrift,
87
89
  composeDecisionRecord: () => composeDecisionRecord,
88
90
  composeInputSchema: () => composeInputSchema,
89
91
  composeLinkSchema: () => composeLinkSchema,
@@ -92,9 +94,14 @@ __export(index_exports, {
92
94
  contextProfileBudgets: () => contextProfileBudgets,
93
95
  createKbMcpServer: () => createKbMcpServer,
94
96
  decisionInputSchema: () => decisionInputSchema,
97
+ defaultAnchorResolvers: () => defaultAnchorResolvers,
95
98
  detectAnchorDrift: () => detectAnchorDrift,
96
99
  doctor: () => doctor,
97
100
  edgeNeighbours: () => edgeNeighbours,
101
+ ensureGrammar: () => ensureGrammar,
102
+ grammarHints: () => grammarHints,
103
+ grammarManifest: () => grammarManifest,
104
+ grammarsCacheRoot: () => grammarsCacheRoot,
98
105
  hashAnchorText: () => hashAnchorText,
99
106
  impact: () => impact,
100
107
  inboundIndex: () => inboundIndex,
@@ -112,6 +119,7 @@ __export(index_exports, {
112
119
  kbRecordFrontmatterSchema: () => kbRecordFrontmatterSchema,
113
120
  kbSourceSchema: () => kbSourceSchema,
114
121
  kbVerifiedEventSchema: () => kbVerifiedEventSchema,
122
+ languageForFile: () => languageForFile,
115
123
  listPins: () => listPins,
116
124
  loadQmd: () => loadQmd,
117
125
  matchToDiff: () => matchToDiff,
@@ -121,9 +129,11 @@ __export(index_exports, {
121
129
  parseLog: () => parseLog,
122
130
  parseMarkdownWithFrontmatter: () => parseMarkdownWithFrontmatter,
123
131
  pinBase: () => pinBase,
132
+ prepareResolvers: () => prepareResolvers,
124
133
  readMergedPins: () => readMergedPins,
125
134
  readPinsLayer: () => readPinsLayer,
126
135
  readRemoteAnchors: () => readRemoteAnchors,
136
+ reassessPacket: () => reassessPacket,
127
137
  regexResolver: () => regexResolver,
128
138
  renderCatalogLine: () => renderCatalogLine,
129
139
  renderIndex: () => renderIndex,
@@ -131,6 +141,7 @@ __export(index_exports, {
131
141
  renderLogEntry: () => renderLogEntry,
132
142
  repoCacheDir: () => repoCacheDir,
133
143
  resolveAnchor: () => resolveAnchor,
144
+ resolveAnchorSpan: () => resolveAnchorSpan,
134
145
  resolveHeads: () => resolveHeads,
135
146
  resolveHits: () => resolveHits,
136
147
  resolvePinPath: () => resolvePinPath,
@@ -143,14 +154,20 @@ __export(index_exports, {
143
154
  syncInstructions: () => syncInstructions,
144
155
  toHookJson: () => toHookJson,
145
156
  trace: () => trace,
157
+ treeSitterLanguages: () => treeSitterLanguages,
158
+ unifiedDiff: () => unifiedDiff,
146
159
  unpinBase: () => unpinBase,
147
160
  validateBundle: () => validateBundle
148
161
  });
149
162
  module.exports = __toCommonJS(index_exports);
150
163
 
164
+ // ../../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
165
+ 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;
166
+ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
167
+
151
168
  // src/kb-store.ts
152
- var import_promises4 = require("fs/promises");
153
- var import_node_path4 = require("path");
169
+ var import_promises6 = require("fs/promises");
170
+ var import_node_path7 = require("path");
154
171
 
155
172
  // src/concurrency.ts
156
173
  var DEFAULT_IO_CONCURRENCY = 16;
@@ -167,9 +184,9 @@ async function mapLimit(items, limit, fn) {
167
184
  { length: Math.min(limit, items.length) },
168
185
  async () => {
169
186
  while (!failed && next < items.length) {
170
- const at = next++;
187
+ const at2 = next++;
171
188
  try {
172
- out[at] = await fn(items[at], at);
189
+ out[at2] = await fn(items[at2], at2);
173
190
  } catch (error) {
174
191
  failed = true;
175
192
  throw error;
@@ -248,10 +265,24 @@ var kbAnchorSchema = import_zod.z.object({
248
265
  hash: import_zod.z.string().regex(/^sha256:[0-9a-f]{64}$/, {
249
266
  message: "hash must be sha256:<64 hex chars>"
250
267
  }).optional(),
268
+ /**
269
+ * What `hash` was taken over: the span's raw text, or the normalised token
270
+ * stream a parser sees (`ast`). Absent means `raw`, which is what every
271
+ * anchor stamped before this field carries, so old hashes keep comparing
272
+ * the way they were written. An `ast` hash is blind to whitespace and
273
+ * comments, so reformatting the anchored code is not drift.
274
+ */
275
+ hash_kind: import_zod.z.enum(["raw", "ast"]).optional(),
251
276
  /** ISO 8601 timestamp of the last successful resolution. */
252
277
  resolved_at: import_zod.z.string().min(1).optional(),
253
278
  /** Line count of the text the hash was taken over. */
254
- lines: import_zod.z.number().int().positive().optional()
279
+ lines: import_zod.z.number().int().positive().optional(),
280
+ /**
281
+ * Which resolver produced the hashed span. Absent means an anchor stamped
282
+ * before resolvers were named, which is read as `regex` — the only one
283
+ * there was. A hash from a different resolver is drift, not a match.
284
+ */
285
+ resolver: import_zod.z.enum(["tree-sitter", "regex"]).optional()
255
286
  }).strict();
256
287
  var kbLinkSchema = import_zod.z.object({
257
288
  target: import_zod.z.string().min(1),
@@ -656,8 +687,8 @@ function safeSegment(value) {
656
687
  function revRef(rev) {
657
688
  const safe = rev.replace(/[^A-Za-z0-9_-]/g, "-").slice(0, 64);
658
689
  let hash = 5381;
659
- for (let at = 0; at < rev.length; at++) {
660
- hash = (hash * 33 ^ rev.charCodeAt(at)) >>> 0;
690
+ for (let at2 = 0; at2 < rev.length; at2++) {
691
+ hash = (hash * 33 ^ rev.charCodeAt(at2)) >>> 0;
661
692
  }
662
693
  return `refs/strauss/${safe}-${hash.toString(16)}`;
663
694
  }
@@ -766,8 +797,8 @@ function repoUrlIsSafe(repo) {
766
797
  if (!scheme?.[1]) return false;
767
798
  if (!allowed.includes(scheme[1].toLowerCase())) return false;
768
799
  const authority = url.slice(scheme[0].length).split("/")[0] ?? "";
769
- const at = authority.lastIndexOf("@");
770
- return at < 0 || !authority.slice(0, at).includes(":");
800
+ const at2 = authority.lastIndexOf("@");
801
+ return at2 < 0 || !authority.slice(0, at2).includes(":");
771
802
  }
772
803
  function protocolArgs() {
773
804
  const allowed = allowedProtocols();
@@ -818,18 +849,18 @@ async function readOneRepo(repo, url, declared, context) {
818
849
  if (!repoUrlIsSafe(url)) return all({ ok: false, reason: "repo-invalid" });
819
850
  const cache = cachePathFor(repo, context.cacheDir);
820
851
  if (!cache) return all({ ok: false, reason: "remote-unreachable" });
821
- const rejected = /* @__PURE__ */ new Map();
852
+ const rejected2 = /* @__PURE__ */ new Map();
822
853
  const usable = [];
823
854
  for (const want of wants) {
824
855
  const reason = wantReason(want);
825
856
  if (reason)
826
- rejected.set(wantKey(repo, want.ref, want.file), { ok: false, reason });
857
+ rejected2.set(wantKey(repo, want.ref, want.file), { ok: false, reason });
827
858
  else usable.push(want);
828
859
  }
829
- if (!usable.length) return rejected;
860
+ if (!usable.length) return rejected2;
830
861
  wants = usable;
831
862
  const opened = await openCache(cache, url, context);
832
- if (opened) return new Map([...rejected, ...all(opened)]);
863
+ if (opened) return new Map([...rejected2, ...all(opened)]);
833
864
  const wantsDefault = wants.some((want) => want.ref === void 0);
834
865
  const branch = wantsDefault ? await defaultBranch(cache, context) : {};
835
866
  const revs = /* @__PURE__ */ new Map();
@@ -856,9 +887,9 @@ async function readOneRepo(repo, url, declared, context) {
856
887
  }
857
888
  );
858
889
  return new Map([
859
- ...rejected,
890
+ ...rejected2,
860
891
  ...wants.map(
861
- (want, at) => [wantKey(repo, want.ref, want.file), reads[at]]
892
+ (want, at2) => [wantKey(repo, want.ref, want.file), reads[at2]]
862
893
  )
863
894
  ]);
864
895
  }
@@ -908,13 +939,13 @@ async function defaultBranch(cache, context) {
908
939
  if (!listed.ok) {
909
940
  const reason = transportReason(listed.stderr);
910
941
  if (reason !== "ref-not-found") {
911
- const cached2 = await cachedBranch(cache);
912
- return cached2 ? { name: cached2 } : { reason };
942
+ const cached3 = await cachedBranch(cache);
943
+ return cached3 ? { name: cached3 } : { reason };
913
944
  }
914
945
  }
915
946
  }
916
- const cached = await cachedBranch(cache);
917
- if (cached) return { name: cached };
947
+ const cached2 = await cachedBranch(cache);
948
+ if (cached2) return { name: cached2 };
918
949
  return {
919
950
  reason: context.offline ? "remote-unreachable" : "default-branch-unknown"
920
951
  };
@@ -941,8 +972,8 @@ async function ensureRev(cache, rev, context) {
941
972
  cwd: cache
942
973
  }
943
974
  );
944
- const cached = have.ok && have.stdout.trim().length > 0;
945
- if (cached && (context.offline || IMMUTABLE_REV.test(rev))) return void 0;
975
+ const cached2 = have.ok && have.stdout.trim().length > 0;
976
+ if (cached2 && (context.offline || IMMUTABLE_REV.test(rev))) return void 0;
946
977
  if (context.offline) return { ok: false, reason: "remote-unreachable" };
947
978
  const fetched = await git(
948
979
  [
@@ -958,7 +989,7 @@ async function ensureRev(cache, rev, context) {
958
989
  );
959
990
  if (!fetched.ok) {
960
991
  const reason = transportReason(fetched.stderr);
961
- if (cached && reason !== "ref-not-found") return void 0;
992
+ if (cached2 && reason !== "ref-not-found") return void 0;
962
993
  return { ok: false, reason };
963
994
  }
964
995
  const head = await git(["rev-parse", "FETCH_HEAD"], { cwd: cache });
@@ -1047,7 +1078,8 @@ function warningAnchor(entry) {
1047
1078
  diffSize,
1048
1079
  ...reason !== void 0 ? { reason } : {},
1049
1080
  ...repo !== void 0 ? { repo } : {},
1050
- ...remoteState !== void 0 ? { remoteState } : {}
1081
+ ...remoteState !== void 0 ? { remoteState } : {},
1082
+ ...entry.class !== void 0 ? { class: entry.class } : {}
1051
1083
  };
1052
1084
  }
1053
1085
  function resolveHeads(from, byId) {
@@ -1058,8 +1090,8 @@ function resolveHeads(from, byId) {
1058
1090
  while (queue.length) {
1059
1091
  const current = queue.shift();
1060
1092
  const next = successors(current, byId);
1061
- for (const missing of next.missing) {
1062
- warnings.push({ kind: "broken-chain", missing });
1093
+ for (const missing2 of next.missing) {
1094
+ warnings.push({ kind: "broken-chain", missing: missing2 });
1063
1095
  }
1064
1096
  if (!next.records.length) {
1065
1097
  if (current.conceptId !== from.conceptId)
@@ -1090,13 +1122,13 @@ function successors(record, byId) {
1090
1122
  }
1091
1123
  }
1092
1124
  const records = [];
1093
- const missing = [];
1125
+ const missing2 = [];
1094
1126
  for (const id of ids) {
1095
1127
  const found = byId.get(id);
1096
1128
  if (found) records.push(found);
1097
- else missing.push(id);
1129
+ else missing2.push(id);
1098
1130
  }
1099
- return { records, missing };
1131
+ return { records, missing: missing2 };
1100
1132
  }
1101
1133
 
1102
1134
  // src/kb-stamp.ts
@@ -1218,67 +1250,658 @@ async function readAnchorFiles(files, read, concurrency = DEFAULT_IO_CONCURRENCY
1218
1250
  return { ok: false, reason: "file-unreadable" };
1219
1251
  }
1220
1252
  });
1221
- return new Map(wanted.map((file, at) => [file, results[at]]));
1253
+ return new Map(wanted.map((file, at2) => [file, results[at2]]));
1222
1254
  }
1223
1255
 
1224
1256
  // src/anchor-resolver/resolver.ts
1257
+ var import_node_crypto4 = require("crypto");
1258
+
1259
+ // src/tree-sitter-resolver/languages.ts
1260
+ var import_node_path5 = require("path");
1261
+
1262
+ // src/grammars/index.ts
1263
+ var import_promises4 = require("fs/promises");
1264
+
1265
+ // src/grammars/store.ts
1225
1266
  var import_node_crypto2 = require("crypto");
1267
+ var import_promises3 = require("fs/promises");
1268
+ var import_node_os2 = require("os");
1269
+ var import_node_path3 = require("path");
1270
+ function grammarsCacheRoot(override) {
1271
+ return override ?? process.env["STRAUSS_KB_GRAMMARS_DIR"] ?? (0, import_node_path3.join)((0, import_node_os2.homedir)(), ".strauss", "grammars");
1272
+ }
1273
+ function grammarCachePath(root, language, sha2564, extension = "wasm") {
1274
+ return (0, import_node_path3.join)(root, language, `${sha2564.slice(0, 12)}.${extension}`);
1275
+ }
1276
+ function sha2562(bytes) {
1277
+ return (0, import_node_crypto2.createHash)("sha256").update(bytes).digest("hex");
1278
+ }
1279
+ function matches(bytes, entry) {
1280
+ if (entry.bytes !== void 0 && bytes.byteLength !== entry.bytes)
1281
+ return false;
1282
+ return sha2562(bytes) === entry.sha256;
1283
+ }
1284
+ async function verifyCached(path, entry) {
1285
+ let bytes;
1286
+ try {
1287
+ bytes = await (0, import_promises3.readFile)(path);
1288
+ } catch {
1289
+ return false;
1290
+ }
1291
+ if (matches(bytes, entry)) return true;
1292
+ await (0, import_promises3.rm)(path, { force: true });
1293
+ return false;
1294
+ }
1295
+ async function writeCached(path, bytes) {
1296
+ await (0, import_promises3.mkdir)((0, import_node_path3.dirname)(path), { recursive: true });
1297
+ const temporary = `${path}.${process.pid}.${(0, import_node_crypto2.randomBytes)(4).toString("hex")}.tmp`;
1298
+ try {
1299
+ await (0, import_promises3.writeFile)(temporary, bytes);
1300
+ await (0, import_promises3.rename)(temporary, path);
1301
+ } catch (error) {
1302
+ await (0, import_promises3.rm)(temporary, { force: true });
1303
+ throw error;
1304
+ }
1305
+ }
1306
+
1307
+ // src/grammars/fetch.ts
1308
+ var ATTEMPTS = 3;
1309
+ var BACKOFF_MS = 250;
1310
+ function grammarUrl(url, override) {
1311
+ const base2 = grammarsBaseUrl(override);
1312
+ if (!base2) return url;
1313
+ let root = base2;
1314
+ while (root.endsWith("/")) root = root.slice(0, -1);
1315
+ const pinned = new URL(url);
1316
+ return `${root}${pinned.pathname}${pinned.search}`;
1317
+ }
1318
+ function grammarsBaseUrl(override) {
1319
+ return override ?? process.env["STRAUSS_KB_GRAMMARS_URL"];
1320
+ }
1321
+ async function downloadPart(url, name, entry, options = {}) {
1322
+ const log = options.log ?? ((line) => void process.stderr.write(line));
1323
+ const weight = entry.bytes === void 0 ? "" : ` (${size(entry.bytes)} from manifest)`;
1324
+ log(`strauss-kb: downloading ${name}${weight} from ${url}
1325
+ `);
1326
+ let cause = "";
1327
+ for (let attempt = 1; attempt <= ATTEMPTS; attempt++) {
1328
+ const outcome = await attemptDownload(url, entry, options.fetchTimeoutMs);
1329
+ if ("bytes" in outcome) return outcome;
1330
+ cause = outcome.cause;
1331
+ log(
1332
+ `strauss-kb: ${name} attempt ${attempt}/${ATTEMPTS} failed: ${cause}
1333
+ `
1334
+ );
1335
+ if (!outcome.retry) break;
1336
+ if (attempt < ATTEMPTS) await pause(BACKOFF_MS * attempt);
1337
+ }
1338
+ log(`strauss-kb: ${name} not downloaded: ${cause}
1339
+ `);
1340
+ return { cause };
1341
+ }
1342
+ async function attemptDownload(url, entry, timeoutMs) {
1343
+ try {
1344
+ const response = await fetch(url, {
1345
+ signal: AbortSignal.timeout(fetchTimeoutMs(timeoutMs))
1346
+ });
1347
+ if (!response.ok) {
1348
+ return {
1349
+ cause: `HTTP ${response.status}`,
1350
+ retry: response.status >= 500 || response.status === 429
1351
+ };
1352
+ }
1353
+ const bytes = new Uint8Array(await response.arrayBuffer());
1354
+ if (!matches(bytes, entry))
1355
+ return { cause: "sha256 mismatch", retry: false };
1356
+ return { bytes };
1357
+ } catch (error) {
1358
+ const timedOut = error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError");
1359
+ return { cause: timedOut ? "timeout" : "network error", retry: true };
1360
+ }
1361
+ }
1362
+ function size(bytes) {
1363
+ return bytes >= 1024 * 1024 ? `${(bytes / (1024 * 1024)).toFixed(1)} MB` : `${Math.round(bytes / 1024)} KB`;
1364
+ }
1365
+ function pause(ms) {
1366
+ return new Promise((resolve6) => setTimeout(resolve6, ms));
1367
+ }
1368
+
1369
+ // src/grammars/manifest.ts
1370
+ var import_node_fs = require("fs");
1371
+ var import_node_path4 = require("path");
1372
+ var import_node_url = require("url");
1373
+
1374
+ // src/grammars/model.ts
1375
+ var import_zod2 = require("zod");
1376
+ var sha2563 = import_zod2.z.string().regex(/^[0-9a-f]{64}$/);
1377
+ var grammarWasmSchema = import_zod2.z.object({
1378
+ url: import_zod2.z.string().min(1),
1379
+ sha256: sha2563,
1380
+ bytes: import_zod2.z.number().int().positive()
1381
+ });
1382
+ var grammarTagsSchema = import_zod2.z.object({ url: import_zod2.z.string().min(1), sha256: sha2563 });
1383
+ var grammarPackSchema = import_zod2.z.object({
1384
+ package: import_zod2.z.string().min(1),
1385
+ wasm: grammarWasmSchema,
1386
+ tags: import_zod2.z.array(grammarTagsSchema),
1387
+ license: import_zod2.z.string().min(1),
1388
+ extensions: import_zod2.z.array(import_zod2.z.string().min(1))
1389
+ });
1390
+ var grammarManifestSchema = import_zod2.z.object({
1391
+ /** The runtime the packs were proved against. */
1392
+ webTreeSitter: import_zod2.z.string().min(1),
1393
+ linguist: import_zod2.z.object({ tag: import_zod2.z.string().min(1), commit: import_zod2.z.string().min(1) }),
1394
+ packs: import_zod2.z.record(import_zod2.z.string().min(1), grammarPackSchema)
1395
+ });
1396
+
1397
+ // src/grammars/manifest.ts
1398
+ var cached;
1399
+ function grammarManifest() {
1400
+ cached ??= grammarManifestSchema.parse(
1401
+ JSON.parse((0, import_node_fs.readFileSync)(grammarsDataPath("manifest.json"), "utf8"))
1402
+ );
1403
+ return cached;
1404
+ }
1405
+ function grammarsDataPath(...segments) {
1406
+ let dir = (0, import_node_path4.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
1407
+ for (let up = 0; up < 5; up++) {
1408
+ const candidate = (0, import_node_path4.join)(dir, "grammars");
1409
+ if ((0, import_node_fs.existsSync)((0, import_node_path4.join)(candidate, "manifest.json")))
1410
+ return (0, import_node_path4.join)(candidate, ...segments);
1411
+ dir = (0, import_node_path4.dirname)(dir);
1412
+ }
1413
+ throw new Error("grammars/manifest.json is missing from the package");
1414
+ }
1415
+
1416
+ // src/grammars/index.ts
1417
+ var inFlight = /* @__PURE__ */ new Map();
1418
+ var missing = /* @__PURE__ */ new Map();
1419
+ var uncompilable = /* @__PURE__ */ new Map();
1420
+ var rejected = /* @__PURE__ */ new Map();
1421
+ function grammarsDownloadDisabled() {
1422
+ return process.env["STRAUSS_KB_GRAMMARS"] === "off";
1423
+ }
1424
+ async function ensureGrammar(language, options = {}) {
1425
+ const pack2 = grammarManifest().packs[language];
1426
+ if (!pack2) return null;
1427
+ const root = grammarsCacheRoot(options.cacheRoot);
1428
+ const wasm = grammarCachePath(root, language, pack2.wasm.sha256);
1429
+ const key2 = `${wasm} ${grammarsBaseUrl(options.baseUrl) ?? ""}`;
1430
+ const existing = inFlight.get(key2);
1431
+ if (existing) return existing;
1432
+ const pending = (async () => {
1433
+ const grammar = await ensurePart(
1434
+ wasm,
1435
+ `tree-sitter-${language}`,
1436
+ pack2.wasm,
1437
+ options
1438
+ );
1439
+ if (grammar !== true)
1440
+ return miss(language, `grammar tree-sitter-${language}`, grammar);
1441
+ const parts = [];
1442
+ const total = pack2.tags.length;
1443
+ for (const [at2, part] of pack2.tags.entries()) {
1444
+ const name = `${language} tags${total > 1 ? ` part ${at2 + 1}/${total}` : ""}`;
1445
+ const path = grammarCachePath(root, language, part.sha256, "scm");
1446
+ const held = await ensurePart(path, name, part, options);
1447
+ if (held !== true) return miss(language, name, held);
1448
+ parts.push(`; ${part.url}
1449
+ ${lf(await (0, import_promises4.readFile)(path, "utf8"))}`);
1450
+ }
1451
+ missing.delete(language);
1452
+ return { wasm, query: total ? parts.join("\n") : void 0 };
1453
+ })();
1454
+ inFlight.set(key2, pending);
1455
+ const result = await pending;
1456
+ if (result === null) inFlight.delete(key2);
1457
+ return result;
1458
+ }
1459
+ async function ensurePart(path, name, entry, options) {
1460
+ if (await verifyCached(path, entry)) return true;
1461
+ if (options.offline === true || grammarsDownloadDisabled()) return {};
1462
+ const download = await downloadPart(
1463
+ grammarUrl(entry.url, options.baseUrl),
1464
+ name,
1465
+ entry,
1466
+ options
1467
+ );
1468
+ if ("cause" in download) return { cause: download.cause };
1469
+ await writeCached(path, download.bytes).catch(() => null);
1470
+ return true;
1471
+ }
1472
+ function miss(language, subject, failure) {
1473
+ missing.set(language, { subject, ...failure });
1474
+ return null;
1475
+ }
1476
+ function lf(body) {
1477
+ return body.replace(/\r\n/g, "\n");
1478
+ }
1479
+ function noteUncompilableQuery(language, cause) {
1480
+ uncompilable.set(language, cause);
1481
+ }
1482
+ function noteRejectedGrammar(language, cause) {
1483
+ rejected.set(language, cause);
1484
+ }
1485
+ function grammarHints() {
1486
+ const manifest = grammarManifest();
1487
+ const packs = manifest.packs;
1488
+ const lines = /* @__PURE__ */ new Map();
1489
+ for (const [language, { subject, cause }] of missing)
1490
+ lines.set(
1491
+ language,
1492
+ `${subject} not cached${cause ? ` (${cause})` : ""}; run online once, or set STRAUSS_KB_GRAMMARS_DIR`
1493
+ );
1494
+ for (const [language, cause] of rejected)
1495
+ lines.set(
1496
+ language,
1497
+ `${packs[language]?.package ?? `tree-sitter-${language}`} rejected by web-tree-sitter ${manifest.webTreeSitter}${cause ? `: ${cause}` : ""}; re-pin with pnpm grammars pin ${language}`
1498
+ );
1499
+ for (const [language, cause] of uncompilable)
1500
+ lines.set(
1501
+ language,
1502
+ `tags query for ${language} does not compile against ${packs[language]?.package ?? `tree-sitter-${language}`}: ${cause}; re-pin with pnpm grammars pin ${language}`
1503
+ );
1504
+ return [...lines].sort(([a], [b]) => a.localeCompare(b)).map(([, line]) => line);
1505
+ }
1506
+
1507
+ // src/tree-sitter-resolver/languages.ts
1508
+ var table;
1509
+ function extensionTable() {
1510
+ const manifest = grammarManifest();
1511
+ if (table?.of !== manifest)
1512
+ table = {
1513
+ of: manifest,
1514
+ extensions: Object.fromEntries(
1515
+ Object.entries(manifest.packs).flatMap(
1516
+ ([language, pack2]) => pack2.extensions.map((extension) => [extension, language])
1517
+ )
1518
+ )
1519
+ };
1520
+ return table.extensions;
1521
+ }
1522
+ function hasQuery(language) {
1523
+ return (grammarManifest().packs[language]?.tags.length ?? 0) > 0;
1524
+ }
1525
+ function languageForFile(file) {
1526
+ const language = extensionTable()[(0, import_node_path5.extname)(file).toLowerCase()];
1527
+ return language && hasQuery(language) ? language : void 0;
1528
+ }
1529
+ function treeSitterLanguages() {
1530
+ return [...new Set(Object.values(extensionTable()))].filter(hasQuery).sort();
1531
+ }
1532
+
1533
+ // src/tree-sitter-resolver/resolver.ts
1534
+ var import_node_crypto3 = require("crypto");
1535
+ var import_web_tree_sitter = require("web-tree-sitter");
1536
+
1537
+ // src/tree-sitter-resolver/definitions.ts
1538
+ var SCOPE_ONLY = "reference.implementation";
1539
+ function index(tree, query) {
1540
+ const byName = /* @__PURE__ */ new Map();
1541
+ for (const match of query.matches(tree.rootNode)) {
1542
+ const nameNode = match.captures.find((capture) => capture.name === "name");
1543
+ const defNode = match.captures.find(
1544
+ (capture) => capture.name.startsWith("definition.") || capture.name === SCOPE_ONLY
1545
+ );
1546
+ if (!nameNode || !defNode) continue;
1547
+ const candidate = {
1548
+ node: defNode.node,
1549
+ name: nameNode.node.text,
1550
+ target: defNode.name !== SCOPE_ONLY
1551
+ };
1552
+ const existing = byName.get(nameNode.node.id);
1553
+ if (existing && width(existing.node) <= width(candidate.node)) continue;
1554
+ byName.set(nameNode.node.id, candidate);
1555
+ }
1556
+ const definitions = [...byName.values()];
1557
+ return {
1558
+ tree,
1559
+ byNodeId: new Map(
1560
+ definitions.map((definition) => [definition.node.id, definition])
1561
+ ),
1562
+ definitions
1563
+ };
1564
+ }
1565
+ function select(parsed, wanted) {
1566
+ const matches3 = parsed.definitions.filter(
1567
+ (definition) => definition.target && endsWith(chainOf(definition, parsed.byNodeId), wanted)
1568
+ );
1569
+ if (matches3.length < 2) return matches3;
1570
+ const bodied = matches3.filter(
1571
+ (definition) => definition.node.childForFieldName("body") !== null
1572
+ );
1573
+ return bodied.length === 1 ? bodied : matches3;
1574
+ }
1575
+ function chainOf(definition, byNodeId) {
1576
+ const chain = [definition.name];
1577
+ const receiver = definition.node.childForFieldName("receiver");
1578
+ const type = receiver && typeNameIn(receiver);
1579
+ if (type) chain.unshift(type);
1580
+ for (let node = definition.node.parent; node; node = node.parent) {
1581
+ const enclosing = byNodeId.get(node.id);
1582
+ if (enclosing && enclosing.node !== definition.node)
1583
+ chain.unshift(enclosing.name);
1584
+ }
1585
+ return chain;
1586
+ }
1587
+ function typeNameIn(receiver) {
1588
+ const stack = [receiver];
1589
+ while (stack.length) {
1590
+ const node = stack.pop();
1591
+ if (node.type === "type_identifier") return node.text;
1592
+ for (let at2 = 0; at2 < node.childCount; at2++) {
1593
+ const child = node.child(at2);
1594
+ if (child) stack.push(child);
1595
+ }
1596
+ }
1597
+ return void 0;
1598
+ }
1599
+ function endsWith(chain, wanted) {
1600
+ if (wanted.length > chain.length) return false;
1601
+ const offset = chain.length - wanted.length;
1602
+ return wanted.every((segment, at2) => chain[offset + at2] === segment);
1603
+ }
1604
+ function width(node) {
1605
+ return node.endIndex - node.startIndex;
1606
+ }
1607
+ function spanOf(definition, source) {
1608
+ let start = definition.node;
1609
+ let end = definition.node;
1610
+ for (let sibling = start.previousSibling; sibling?.type === "decorator"; sibling = sibling.previousSibling) {
1611
+ start = sibling;
1612
+ }
1613
+ const parent = end.parent;
1614
+ if (parent?.type === "export_statement" && parent.childForFieldName("declaration")?.id === end.id) {
1615
+ start = parent;
1616
+ end = parent;
1617
+ }
1618
+ const lines = source.split("\n");
1619
+ const startLine = start.startPosition.row;
1620
+ const endLine = end.endPosition.column === 0 && end.endPosition.row > startLine ? end.endPosition.row - 1 : end.endPosition.row;
1621
+ return {
1622
+ text: lines.slice(startLine, endLine + 1).join("\n"),
1623
+ startLine: startLine + 1,
1624
+ endLine: endLine + 1
1625
+ };
1626
+ }
1627
+
1628
+ // src/tree-sitter-resolver/tokens.ts
1629
+ function tokens(root) {
1630
+ const out = [];
1631
+ const stack = [root];
1632
+ while (stack.length) {
1633
+ const node = stack.pop();
1634
+ if (node.type.includes("comment")) continue;
1635
+ if (node.childCount === 0) {
1636
+ const text = node.text.trim();
1637
+ if (text) out.push(text);
1638
+ continue;
1639
+ }
1640
+ for (let at2 = node.childCount - 1; at2 >= 0; at2--) {
1641
+ const child = node.child(at2);
1642
+ if (child) stack.push(child);
1643
+ }
1644
+ }
1645
+ return out;
1646
+ }
1647
+
1648
+ // src/tree-sitter-resolver/resolver.ts
1649
+ var TREE_CACHE_LIMIT = 32;
1650
+ var TreeSitterResolver = class {
1651
+ name = "tree-sitter";
1652
+ grammars;
1653
+ loaded = /* @__PURE__ */ new Map();
1654
+ trees = /* @__PURE__ */ new Map();
1655
+ parser;
1656
+ initialized = false;
1657
+ /** Cache effectiveness, for tests and for the latency numbers. */
1658
+ stats = { parses: 0, cacheHits: 0 };
1659
+ constructor(options = {}) {
1660
+ this.grammars = options;
1661
+ }
1662
+ /**
1663
+ * Loads the grammars these files need, once per language per process,
1664
+ * downloading each one on first use.
1665
+ *
1666
+ * A grammar that will not load is remembered as unavailable rather than
1667
+ * retried per anchor, and never throws: an unobtainable WASM is a finding.
1668
+ */
1669
+ async prepare(files) {
1670
+ const wanted = /* @__PURE__ */ new Set();
1671
+ for (const file of files) {
1672
+ const language = languageForFile(file);
1673
+ if (language && !this.loaded.has(language)) wanted.add(language);
1674
+ }
1675
+ if (!wanted.size) return;
1676
+ if (!this.initialized) {
1677
+ try {
1678
+ await import_web_tree_sitter.Parser.init();
1679
+ this.parser = new import_web_tree_sitter.Parser();
1680
+ this.initialized = true;
1681
+ } catch {
1682
+ for (const language of wanted) this.loaded.set(language, null);
1683
+ return;
1684
+ }
1685
+ }
1686
+ const languages = [...wanted];
1687
+ const loaded = await mapLimit(
1688
+ languages,
1689
+ Math.min(DEFAULT_IO_CONCURRENCY, languages.length),
1690
+ (language) => this.load(language)
1691
+ );
1692
+ languages.forEach(
1693
+ (language, at2) => this.loaded.set(language, loaded[at2] ?? null)
1694
+ );
1695
+ }
1696
+ /**
1697
+ * An unobtainable grammar, one this runtime refuses, and a query that will
1698
+ * not compile are three faults with three repairs; all are reported through
1699
+ * the grammars module so every hint has one home.
1700
+ */
1701
+ async load(language) {
1702
+ let pack2;
1703
+ try {
1704
+ pack2 = await ensureGrammar(language, this.grammars);
1705
+ } catch {
1706
+ return null;
1707
+ }
1708
+ if (!pack2?.query) return null;
1709
+ let grammar;
1710
+ try {
1711
+ grammar = await import_web_tree_sitter.Language.load(pack2.wasm);
1712
+ } catch (error) {
1713
+ noteRejectedGrammar(language, why(error));
1714
+ return null;
1715
+ }
1716
+ try {
1717
+ return { language: grammar, query: new import_web_tree_sitter.Query(grammar, pack2.query) };
1718
+ } catch (error) {
1719
+ noteUncompilableQuery(language, why(error));
1720
+ return null;
1721
+ }
1722
+ }
1723
+ /**
1724
+ * Abstains on an extension with no grammar so the regex resolver gets a
1725
+ * turn; reports `resolver-unavailable` when the grammar exists in principle
1726
+ * but could not be loaded, because falling back there would silently trade a
1727
+ * precise span for a guessed one.
1728
+ */
1729
+ attempt(source, symbol, file) {
1730
+ const language = file ? languageForFile(file) : void 0;
1731
+ if (!language) return { kind: "abstain" };
1732
+ if (!this.loaded.has(language)) return { kind: "abstain" };
1733
+ const loaded = this.loaded.get(language);
1734
+ if (!loaded) return { kind: "unresolved", reason: "resolver-unavailable" };
1735
+ const parsed = this.parse(language, loaded, source);
1736
+ if (!parsed) return { kind: "unresolved", reason: "resolver-unavailable" };
1737
+ const wanted = symbol.split(".").filter(Boolean);
1738
+ if (!wanted.length)
1739
+ return { kind: "unresolved", reason: "symbol-not-found" };
1740
+ const matches3 = select(parsed, wanted);
1741
+ if (!matches3.length)
1742
+ return { kind: "unresolved", reason: "symbol-not-found" };
1743
+ if (matches3.length > 1)
1744
+ return { kind: "unresolved", reason: "symbol-ambiguous" };
1745
+ return { kind: "resolved", span: spanOf(matches3[0], source) };
1746
+ }
1747
+ resolve(source, symbol, file) {
1748
+ const attempt = this.attempt(source, symbol, file);
1749
+ return attempt.kind === "resolved" ? attempt.span : null;
1750
+ }
1751
+ /** Parsed trees are keyed by content hash, so an unchanged file parses once. */
1752
+ parse(language, loaded, source) {
1753
+ const key2 = `${language}:${(0, import_node_crypto3.createHash)("sha256").update(source).digest("hex")}`;
1754
+ const cached2 = this.trees.get(key2);
1755
+ if (cached2) {
1756
+ this.stats.cacheHits += 1;
1757
+ return cached2;
1758
+ }
1759
+ const parser = this.parser;
1760
+ if (!parser) return null;
1761
+ let parsed;
1762
+ try {
1763
+ parser.setLanguage(loaded.language);
1764
+ const tree = parser.parse(source);
1765
+ if (!tree) return null;
1766
+ parsed = index(tree, loaded.query);
1767
+ } catch {
1768
+ return null;
1769
+ }
1770
+ this.stats.parses += 1;
1771
+ if (this.trees.size >= TREE_CACHE_LIMIT) {
1772
+ const oldest = this.trees.keys().next();
1773
+ if (!oldest.done) {
1774
+ this.trees.get(oldest.value)?.tree.delete();
1775
+ this.trees.delete(oldest.value);
1776
+ }
1777
+ }
1778
+ this.trees.set(key2, parsed);
1779
+ return parsed;
1780
+ }
1781
+ /**
1782
+ * Every definition this file declares, as dotted symbol and span.
1783
+ *
1784
+ * The inverse of `attempt`: that asks "where is this name", this asks "what
1785
+ * names are here". `moved` needs the second — the stored hash has to be
1786
+ * looked for at every definition in the repository, and there is no name to
1787
+ * ask about, since the whole question is which name now carries that code.
1788
+ */
1789
+ spans(source, file) {
1790
+ const language = languageForFile(file);
1791
+ if (!language) return [];
1792
+ const loaded = this.loaded.get(language);
1793
+ if (!loaded) return [];
1794
+ const parsed = this.parse(language, loaded, source);
1795
+ if (!parsed) return [];
1796
+ return parsed.definitions.filter((definition) => definition.target).map((definition) => ({
1797
+ symbol: chainOf(definition, parsed.byNodeId).join("."),
1798
+ span: spanOf(definition, source)
1799
+ }));
1800
+ }
1801
+ /**
1802
+ * The token stream of a span: every leaf the parser sees, comments dropped,
1803
+ * joined by single spaces.
1804
+ *
1805
+ * This is what makes a reformat not be drift. Hashing it rather than the raw
1806
+ * text means indentation, line breaks, trailing commas the formatter moved,
1807
+ * and every comment above or inside the definition are outside the hash —
1808
+ * and a renamed identifier or a changed literal is still inside it, because
1809
+ * those are leaves.
1810
+ *
1811
+ * `null` when the file has no grammar, the grammar would not load, or the
1812
+ * text will not parse: no normalisation is better than a guessed one.
1813
+ */
1814
+ normalize(text, file) {
1815
+ const language = file ? languageForFile(file) : void 0;
1816
+ if (!language) return null;
1817
+ const loaded = this.loaded.get(language);
1818
+ if (!loaded) return null;
1819
+ const parser = this.parser;
1820
+ if (!parser) return null;
1821
+ let tree;
1822
+ try {
1823
+ parser.setLanguage(loaded.language);
1824
+ tree = parser.parse(text);
1825
+ } catch {
1826
+ return null;
1827
+ }
1828
+ if (!tree) return null;
1829
+ try {
1830
+ return tokens(tree.rootNode).join(" ");
1831
+ } finally {
1832
+ tree.delete();
1833
+ }
1834
+ }
1835
+ /** Drops cached trees. Grammars stay loaded — they are immutable. */
1836
+ reset() {
1837
+ for (const parsed of this.trees.values()) parsed.tree.delete();
1838
+ this.trees.clear();
1839
+ this.stats.parses = 0;
1840
+ this.stats.cacheHits = 0;
1841
+ }
1842
+ };
1843
+ function why(error) {
1844
+ const text = error instanceof Error ? error.message : String(error);
1845
+ return text || "no reason given";
1846
+ }
1847
+
1848
+ // src/anchor-resolver/resolver.ts
1226
1849
  var PARENT_SCOPE_LINES = 50;
1227
1850
  var CLEAN_STATE = { blockComment: false, template: false };
1228
1851
  function stripLine(line, state) {
1229
1852
  let out = "";
1230
- let index = 0;
1853
+ let index2 = 0;
1231
1854
  let { blockComment, template } = state;
1232
- while (index < line.length) {
1233
- const char = line[index];
1234
- const next = line[index + 1];
1855
+ while (index2 < line.length) {
1856
+ const char = line[index2];
1857
+ const next = line[index2 + 1];
1235
1858
  if (blockComment) {
1236
1859
  if (char === "*" && next === "/") {
1237
1860
  blockComment = false;
1238
- index += 2;
1861
+ index2 += 2;
1239
1862
  continue;
1240
1863
  }
1241
- index += 1;
1864
+ index2 += 1;
1242
1865
  continue;
1243
1866
  }
1244
1867
  if (template) {
1245
1868
  if (char === "\\") {
1246
- index += 2;
1869
+ index2 += 2;
1247
1870
  continue;
1248
1871
  }
1249
1872
  if (char === "`") template = false;
1250
- index += 1;
1873
+ index2 += 1;
1251
1874
  continue;
1252
1875
  }
1253
1876
  if (char === "/" && next === "*") {
1254
1877
  blockComment = true;
1255
- index += 2;
1878
+ index2 += 2;
1256
1879
  continue;
1257
1880
  }
1258
1881
  if (char === "/" && next === "/") break;
1259
1882
  if (char === "`") {
1260
1883
  template = true;
1261
- index += 1;
1884
+ index2 += 1;
1262
1885
  continue;
1263
1886
  }
1264
1887
  if (char === "'" || char === '"') {
1265
1888
  const quote = char;
1266
- index += 1;
1267
- while (index < line.length) {
1268
- if (line[index] === "\\") {
1269
- index += 2;
1889
+ index2 += 1;
1890
+ while (index2 < line.length) {
1891
+ if (line[index2] === "\\") {
1892
+ index2 += 2;
1270
1893
  continue;
1271
1894
  }
1272
- if (line[index] === quote) {
1273
- index += 1;
1895
+ if (line[index2] === quote) {
1896
+ index2 += 1;
1274
1897
  break;
1275
1898
  }
1276
- index += 1;
1899
+ index2 += 1;
1277
1900
  }
1278
1901
  continue;
1279
1902
  }
1280
1903
  out += char;
1281
- index += 1;
1904
+ index2 += 1;
1282
1905
  }
1283
1906
  return { code: out, state: { blockComment, template } };
1284
1907
  }
@@ -1293,8 +1916,8 @@ function captureBraceBlock(lines, matchLine) {
1293
1916
  let depth = 0;
1294
1917
  let opened = false;
1295
1918
  let state = CLEAN_STATE;
1296
- for (let index = matchLine; index < lines.length; index++) {
1297
- const stripped = stripLine(lines[index] ?? "", state);
1919
+ for (let index2 = matchLine; index2 < lines.length; index2++) {
1920
+ const stripped = stripLine(lines[index2] ?? "", state);
1298
1921
  state = stripped.state;
1299
1922
  for (const char of stripped.code) {
1300
1923
  if (char === "{") {
@@ -1303,10 +1926,10 @@ function captureBraceBlock(lines, matchLine) {
1303
1926
  } else if (char === "}") {
1304
1927
  depth = Math.max(0, depth - 1);
1305
1928
  } else if (char === ";" && !opened) {
1306
- return span(lines, matchLine, index);
1929
+ return span(lines, matchLine, index2);
1307
1930
  }
1308
1931
  }
1309
- if (opened && depth === 0) return span(lines, matchLine, index);
1932
+ if (opened && depth === 0) return span(lines, matchLine, index2);
1310
1933
  }
1311
1934
  return null;
1312
1935
  }
@@ -1315,22 +1938,22 @@ function captureIndentedBlock(lines, matchLine) {
1315
1938
  const header = lines[matchLine] ?? "";
1316
1939
  const indent = header.length - header.trimStart().length;
1317
1940
  let headerEnd = -1;
1318
- for (let index = matchLine; index < lines.length && index <= matchLine + 20; index++) {
1319
- const code = stripLine(lines[index] ?? "", CLEAN_STATE).code.trimEnd();
1941
+ for (let index2 = matchLine; index2 < lines.length && index2 <= matchLine + 20; index2++) {
1942
+ const code = stripLine(lines[index2] ?? "", CLEAN_STATE).code.trimEnd();
1320
1943
  if (code.endsWith(":")) {
1321
- headerEnd = index;
1944
+ headerEnd = index2;
1322
1945
  break;
1323
1946
  }
1324
- if (code.includes(":")) return span(lines, matchLine, index);
1947
+ if (code.includes(":")) return span(lines, matchLine, index2);
1325
1948
  }
1326
1949
  if (headerEnd === -1) return null;
1327
1950
  let end = headerEnd;
1328
- for (let index = headerEnd + 1; index < lines.length; index++) {
1329
- const line = lines[index] ?? "";
1951
+ for (let index2 = headerEnd + 1; index2 < lines.length; index2++) {
1952
+ const line = lines[index2] ?? "";
1330
1953
  if (line.trim() === "") continue;
1331
1954
  const lineIndent = line.length - line.trimStart().length;
1332
1955
  if (lineIndent <= indent) break;
1333
- end = index;
1956
+ end = index2;
1334
1957
  }
1335
1958
  return end === headerEnd ? null : span(lines, matchLine, end);
1336
1959
  }
@@ -1354,15 +1977,15 @@ var regexResolver = {
1354
1977
  const lines = source.split("\n");
1355
1978
  for (const tier of TIERS) {
1356
1979
  const pattern = tier(escaped);
1357
- let candidates = lines.map((line, index) => ({ line, index })).filter((entry) => pattern.test(entry.line)).map((entry) => entry.index);
1980
+ let candidates = lines.map((line, index2) => ({ line, index: index2 })).filter((entry) => pattern.test(entry.line)).map((entry) => entry.index);
1358
1981
  if (!candidates.length) continue;
1359
1982
  if (parentPattern && candidates.length > 1) {
1360
1983
  const distances = candidates.map(
1361
- (index) => distanceToParent(lines, index, parentPattern)
1984
+ (index2) => distanceToParent(lines, index2, parentPattern)
1362
1985
  );
1363
1986
  const nearest = Math.min(...distances);
1364
1987
  if (Number.isFinite(nearest)) {
1365
- candidates = candidates.filter((_, at) => distances[at] === nearest);
1988
+ candidates = candidates.filter((_, at2) => distances[at2] === nearest);
1366
1989
  }
1367
1990
  }
1368
1991
  if (candidates.length !== 1) return null;
@@ -1375,34 +1998,86 @@ var regexResolver = {
1375
1998
  function escapeRegExp(value) {
1376
1999
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1377
2000
  }
1378
- function distanceToParent(lines, index, parent) {
1379
- const floor = Math.max(0, index - PARENT_SCOPE_LINES);
1380
- for (let at = index; at >= floor; at--) {
1381
- if (parent.test(lines[at] ?? "")) return index - at;
2001
+ function distanceToParent(lines, index2, parent) {
2002
+ const floor = Math.max(0, index2 - PARENT_SCOPE_LINES);
2003
+ for (let at2 = index2; at2 >= floor; at2--) {
2004
+ if (parent.test(lines[at2] ?? "")) return index2 - at2;
1382
2005
  }
1383
2006
  return Number.POSITIVE_INFINITY;
1384
2007
  }
1385
2008
  function hashAnchorText(text) {
1386
- return `sha256:${(0, import_node_crypto2.createHash)("sha256").update(text.replace(/\r\n/g, "\n")).digest("hex")}`;
2009
+ return `sha256:${(0, import_node_crypto4.createHash)("sha256").update(text.replace(/\r\n/g, "\n")).digest("hex")}`;
1387
2010
  }
1388
2011
  function resolveAnchor(source, anchor, resolver = regexResolver) {
2012
+ const outcome = resolveAnchorSpan(source, anchor, [resolver]);
2013
+ return outcome.ok ? outcome.span : null;
2014
+ }
2015
+ function resolveAnchorSpan(source, anchor, resolvers = [regexResolver]) {
1389
2016
  const normalized = source.replace(/\r\n/g, "\n");
1390
2017
  if (!anchor.symbol) {
1391
2018
  const lines = normalized.split("\n");
1392
2019
  if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
1393
2020
  return {
1394
- text: normalized,
1395
- startLine: 1,
1396
- endLine: Math.max(1, lines.length)
2021
+ ok: true,
2022
+ span: {
2023
+ text: normalized,
2024
+ startLine: 1,
2025
+ endLine: Math.max(1, lines.length)
2026
+ }
1397
2027
  };
1398
2028
  }
1399
- return resolver.resolve(normalized, anchor.symbol);
2029
+ for (const resolver of resolvers) {
2030
+ const attempt = resolver.attempt ? resolver.attempt(normalized, anchor.symbol, anchor.file) : fromResolve(resolver, normalized, anchor.symbol, anchor.file);
2031
+ if (attempt.kind === "abstain") continue;
2032
+ if (attempt.kind === "unresolved") {
2033
+ if (attempt.reason === "symbol-not-found") continue;
2034
+ return { ok: false, reason: attempt.reason };
2035
+ }
2036
+ const tokens2 = resolver.normalize?.(attempt.span.text, anchor.file);
2037
+ return {
2038
+ ok: true,
2039
+ span: attempt.span,
2040
+ ...isResolverName(resolver.name) ? { resolver: resolver.name } : {},
2041
+ ...tokens2 ? { normalized: tokens2 } : {}
2042
+ };
2043
+ }
2044
+ return { ok: false, reason: "symbol-not-found" };
2045
+ }
2046
+ function fromResolve(resolver, source, symbol, file) {
2047
+ const span2 = resolver.resolve(source, symbol, file);
2048
+ return span2 ? { kind: "resolved", span: span2 } : { kind: "unresolved", reason: "symbol-not-found" };
2049
+ }
2050
+ function isResolverName(name) {
2051
+ return name === "tree-sitter" || name === "regex";
2052
+ }
2053
+ async function prepareResolvers(resolvers, files) {
2054
+ for (const resolver of resolvers) await resolver.prepare?.(files);
2055
+ }
2056
+ function defaultAnchorResolvers(grammars = {}) {
2057
+ return [new TreeSitterResolver(grammars), regexResolver];
2058
+ }
2059
+ function resolverChanged(source, anchor, produced) {
2060
+ const previous = anchor.resolver ?? "regex";
2061
+ if (!produced || !anchor.symbol || previous === produced) return false;
2062
+ if (previous !== "regex") return false;
2063
+ const before = regexResolver.resolve(
2064
+ source.replace(/\r\n/g, "\n"),
2065
+ anchor.symbol
2066
+ );
2067
+ return before !== null && hashAnchorText(before.text) === anchor.hash;
2068
+ }
2069
+ function anchorHashOf(anchor, outcome) {
2070
+ const stored = anchor.hash ? anchor.hash_kind ?? "raw" : void 0;
2071
+ const wanted = stored ?? (outcome.normalized ? "ast" : "raw");
2072
+ return wanted === "ast" && outcome.normalized ? { hash: hashAnchorText(outcome.normalized), kind: "ast" } : { hash: hashAnchorText(outcome.span.text), kind: "raw" };
1400
2073
  }
1401
2074
 
1402
2075
  // src/anchor-resolver/drift.ts
1403
2076
  async function detectAnchorDrift(records, options = {}) {
1404
2077
  const repoRoot = options.repoRoot ?? process.cwd();
1405
- const resolver = options.resolver ?? regexResolver;
2078
+ const resolvers = options.resolvers ?? (options.resolver ? [options.resolver] : defaultAnchorResolvers({
2079
+ offline: options.remote?.offline === true
2080
+ }));
1406
2081
  const origin = new LazyOrigin(repoRoot);
1407
2082
  const planned = /* @__PURE__ */ new Map();
1408
2083
  let declaresRepo = false;
@@ -1440,12 +2115,16 @@ async function detectAnchorDrift(records, options = {}) {
1440
2115
  ),
1441
2116
  (options.readRemote ?? readRemoteAnchors)(wants, options.remote ?? {})
1442
2117
  ]);
2118
+ await prepareResolvers(resolvers, [
2119
+ ...files,
2120
+ ...wants.map((want) => want.file)
2121
+ ]);
1443
2122
  const drift = /* @__PURE__ */ new Map();
1444
2123
  for (const record of records) {
1445
2124
  const entries = [];
1446
2125
  for (const { anchor, foreign } of planned.get(record.conceptId) ?? []) {
1447
2126
  entries.push(
1448
- foreign ? remoteEntry(anchor, remote, resolver) : localEntry(anchor, reads.get(anchor.file), resolver)
2127
+ foreign ? remoteEntry(anchor, remote, resolvers) : localEntry(anchor, reads.get(anchor.file), resolvers)
1449
2128
  );
1450
2129
  }
1451
2130
  if (entries.length) drift.set(record.conceptId, entries);
@@ -1471,60 +2150,104 @@ function unresolved(anchor, reason, repo) {
1471
2150
  state: "unresolved",
1472
2151
  diffSize: null,
1473
2152
  ...reason ? { reason } : {},
1474
- ...repo ? { repo } : {}
2153
+ ...repo ? { repo } : {},
2154
+ ...classOf(reason)
1475
2155
  };
1476
2156
  }
1477
- function hashIn(source, anchor, resolver) {
1478
- const resolved = resolveAnchor(source, anchor, resolver);
1479
- if (!resolved) return null;
2157
+ function provisionalDriftClass(entry) {
2158
+ if (entry.state === "unresolved") {
2159
+ return entry.reason === "file-missing" || entry.reason === "symbol-not-found" ? "gone" : void 0;
2160
+ }
2161
+ return entry.state === "drifted" ? "changed" : void 0;
2162
+ }
2163
+ function classOf(reason) {
2164
+ const settled = provisionalDriftClass({ state: "unresolved", reason });
2165
+ return settled ? { class: settled } : {};
2166
+ }
2167
+ function hashIn(source, anchor, resolvers) {
2168
+ const outcome = resolveAnchorSpan(source, anchor, resolvers);
2169
+ if (!outcome.ok) return { ok: false, reason: outcome.reason };
2170
+ const { hash, kind } = anchorHashOf(anchor, outcome);
2171
+ return {
2172
+ ok: true,
2173
+ current: {
2174
+ hash,
2175
+ kind,
2176
+ lines: outcome.span.endLine - outcome.span.startLine + 1,
2177
+ ...outcome.resolver ? { resolver: outcome.resolver } : {}
2178
+ }
2179
+ };
2180
+ }
2181
+ function resolverExtras(source, anchor, current) {
1480
2182
  return {
1481
- hash: hashAnchorText(resolved.text),
1482
- lines: resolved.endLine - resolved.startLine + 1
2183
+ ...current.resolver ? { resolver: current.resolver } : {},
2184
+ ...current.hash !== anchor.hash && resolverChanged(source, anchor, current.resolver) ? { reason: "resolver-changed" } : {}
1483
2185
  };
1484
2186
  }
1485
2187
  function compared(anchor, current, extra = {}) {
2188
+ const matched = current.hash === anchor.hash;
1486
2189
  return {
1487
2190
  ...base(anchor),
1488
- state: current.hash === anchor.hash ? "match" : "drifted",
2191
+ state: matched ? "match" : "drifted",
1489
2192
  currentHash: current.hash,
2193
+ hashKind: current.kind,
1490
2194
  diffSize: anchor.lines === void 0 ? null : Math.abs(current.lines - anchor.lines),
2195
+ ...matched ? {} : { class: "changed" },
1491
2196
  ...extra
1492
2197
  };
1493
2198
  }
1494
- function localEntry(anchor, read, resolver) {
2199
+ function localEntry(anchor, read, resolvers) {
1495
2200
  if (!read.ok) return unresolved(anchor, read.reason);
1496
- const current = hashIn(read.source, anchor, resolver);
1497
- return current ? compared(anchor, current) : unresolved(anchor, "symbol-not-found");
2201
+ const found = hashIn(read.source, anchor, resolvers);
2202
+ if (!found.ok) return unresolved(anchor, found.reason);
2203
+ return compared(
2204
+ anchor,
2205
+ found.current,
2206
+ resolverExtras(read.source, anchor, found.current)
2207
+ );
1498
2208
  }
1499
- function remoteEntry(anchor, remote, resolver) {
2209
+ function remoteEntry(anchor, remote, resolvers) {
1500
2210
  const repo = anchor.repo;
1501
2211
  const key2 = normalizeRepoUrl(repo);
1502
2212
  const atDefault = remote.get(wantKey(key2, void 0, anchor.file));
1503
2213
  const primary = anchor.ref ? remote.get(wantKey(key2, anchor.ref, anchor.file)) : atDefault;
1504
2214
  if (!primary) return unresolved(anchor, "remote-unreachable", repo);
1505
2215
  if (!primary.ok) return unresolved(anchor, primary.reason, repo);
1506
- const current = hashIn(primary.source, anchor, resolver);
1507
- if (!current) return unresolved(anchor, "symbol-not-found", repo);
1508
- if (!anchor.ref) return compared(anchor, current, { repo });
2216
+ const found = hashIn(primary.source, anchor, resolvers);
2217
+ if (!found.ok) return unresolved(anchor, found.reason, repo);
2218
+ const current = found.current;
2219
+ const extras = resolverExtras(primary.source, anchor, current);
2220
+ if (!anchor.ref) return compared(anchor, current, { repo, ...extras });
1509
2221
  if (current.hash !== anchor.hash) {
1510
- return compared(anchor, current, { repo, remoteState: "drifted-from-ref" });
2222
+ return compared(anchor, current, {
2223
+ repo,
2224
+ ...extras,
2225
+ remoteState: "drifted-from-ref"
2226
+ });
1511
2227
  }
1512
- const head = atDefault?.ok ? hashIn(atDefault.source, anchor, resolver) : null;
1513
- return head && head.hash !== anchor.hash ? {
1514
- ...compared(anchor, head, { repo }),
2228
+ const head = atDefault?.ok ? hashIn(atDefault.source, anchor, resolvers) : null;
2229
+ return head?.ok && head.current.hash !== anchor.hash ? {
2230
+ ...compared(anchor, head.current, {
2231
+ repo,
2232
+ ...head.current.resolver ? { resolver: head.current.resolver } : {}
2233
+ }),
1515
2234
  state: "drifted",
1516
2235
  remoteState: "drifted-on-default"
1517
- } : compared(anchor, current, { repo, remoteState: "matches-ref" });
2236
+ } : compared(anchor, current, {
2237
+ repo,
2238
+ ...extras,
2239
+ remoteState: "matches-ref"
2240
+ });
1518
2241
  }
1519
2242
 
1520
2243
  // src/search-index.ts
1521
- var import_promises3 = require("fs/promises");
1522
- var import_node_path3 = require("path");
2244
+ var import_promises5 = require("fs/promises");
2245
+ var import_node_path6 = require("path");
1523
2246
 
1524
2247
  // src/kb-log.ts
1525
- var import_zod2 = require("zod");
2248
+ var import_zod3 = require("zod");
1526
2249
  var LOG_FILE = "log.jsonl";
1527
- var kbLogEntrySchema = import_zod2.z.object({
2250
+ var kbLogEntrySchema = import_zod3.z.object({
1528
2251
  // Validated, not just `min(1)`: `at` is a sort key (see `parseLog`
1529
2252
  // below), and a value that isn't actually chronological — a Unix
1530
2253
  // timestamp, a human-typed date, garbage — would sort wrong without
@@ -1533,12 +2256,12 @@ var kbLogEntrySchema = import_zod2.z.object({
1533
2256
  // and rejects everything else, including a non-`Z` offset — so a
1534
2257
  // malformed `at` is reported the same way a malformed line already is,
1535
2258
  // rather than silently sorting into the wrong place.
1536
- at: import_zod2.z.iso.datetime(),
1537
- by: import_zod2.z.string().min(1),
1538
- operation: import_zod2.z.string().min(1),
1539
- conceptId: import_zod2.z.string().min(1),
2259
+ at: import_zod3.z.iso.datetime(),
2260
+ by: import_zod3.z.string().min(1),
2261
+ operation: import_zod3.z.string().min(1),
2262
+ conceptId: import_zod3.z.string().min(1),
1540
2263
  /** Second concept id, where the operation relates two — supersession. */
1541
- target: import_zod2.z.string().min(1).optional()
2264
+ target: import_zod3.z.string().min(1).optional()
1542
2265
  }).strict();
1543
2266
  function renderLogEntry(entry) {
1544
2267
  return `${JSON.stringify(kbLogEntrySchema.parse(entry))}
@@ -1548,18 +2271,18 @@ function parseLog(raw) {
1548
2271
  const entries = [];
1549
2272
  const malformed = [];
1550
2273
  const seen = /* @__PURE__ */ new Set();
1551
- raw.split("\n").forEach((text, index) => {
2274
+ raw.split("\n").forEach((text, index2) => {
1552
2275
  if (!text.trim()) return;
1553
2276
  let value;
1554
2277
  try {
1555
2278
  value = JSON.parse(text);
1556
2279
  } catch {
1557
- malformed.push({ line: index + 1, text });
2280
+ malformed.push({ line: index2 + 1, text });
1558
2281
  return;
1559
2282
  }
1560
2283
  const parsed = kbLogEntrySchema.safeParse(value);
1561
2284
  if (!parsed.success) {
1562
- malformed.push({ line: index + 1, text });
2285
+ malformed.push({ line: index2 + 1, text });
1563
2286
  return;
1564
2287
  }
1565
2288
  const key2 = JSON.stringify(parsed.data);
@@ -1582,7 +2305,7 @@ async function searchBase(bundlePath2, query, options = {}) {
1582
2305
  let store = null;
1583
2306
  try {
1584
2307
  store = await qmd.createStore({
1585
- dbPath: (0, import_node_path3.join)(bundlePath2, SEARCH_INDEX_FILE),
2308
+ dbPath: (0, import_node_path6.join)(bundlePath2, SEARCH_INDEX_FILE),
1586
2309
  config: {
1587
2310
  collections: {
1588
2311
  [COLLECTION]: {
@@ -1617,7 +2340,7 @@ async function searchBase(bundlePath2, query, options = {}) {
1617
2340
  }
1618
2341
  }
1619
2342
  async function isStale(bundlePath2) {
1620
- const indexAt = await (0, import_promises3.stat)((0, import_node_path3.join)(bundlePath2, SEARCH_INDEX_FILE)).then((s) => s.mtimeMs).catch(() => 0);
2343
+ const indexAt = await (0, import_promises5.stat)((0, import_node_path6.join)(bundlePath2, SEARCH_INDEX_FILE)).then((s) => s.mtimeMs).catch(() => 0);
1621
2344
  if (!indexAt) return true;
1622
2345
  const { readdir: readdir2 } = await import("fs/promises");
1623
2346
  const names = (await readdir2(bundlePath2).catch(() => [])).filter(
@@ -1626,8 +2349,8 @@ async function isStale(bundlePath2) {
1626
2349
  let stale = false;
1627
2350
  await mapLimit(names, DEFAULT_IO_CONCURRENCY, async (name) => {
1628
2351
  if (stale) return;
1629
- const at = await (0, import_promises3.stat)((0, import_node_path3.join)(bundlePath2, name)).then((s) => s.mtimeMs).catch(() => 0);
1630
- if (at > indexAt) stale = true;
2352
+ const at2 = await (0, import_promises5.stat)((0, import_node_path6.join)(bundlePath2, name)).then((s) => s.mtimeMs).catch(() => 0);
2353
+ if (at2 > indexAt) stale = true;
1631
2354
  });
1632
2355
  return stale;
1633
2356
  }
@@ -1922,8 +2645,8 @@ function trace(seedId, bundle, options = {}) {
1922
2645
  return [...reached.values()].sort(byGeneratedAt);
1923
2646
  }
1924
2647
  function byGeneratedAt(left, right) {
1925
- const at = (step) => step.record.frontmatter.generated?.at ?? "";
1926
- return at(left).localeCompare(at(right)) || left.depth - right.depth;
2648
+ const at2 = (step) => step.record.frontmatter.generated?.at ?? "";
2649
+ return at2(left).localeCompare(at2(right)) || left.depth - right.depth;
1927
2650
  }
1928
2651
 
1929
2652
  // src/pack.ts
@@ -2006,8 +2729,8 @@ function byRank(left, right) {
2006
2729
  ) || left.record.conceptId.localeCompare(right.record.conceptId);
2007
2730
  }
2008
2731
  function typeRank(record) {
2009
- const index = TYPE_PRIORITY.indexOf(record.frontmatter.type);
2010
- return index === -1 ? TYPE_PRIORITY.length : index;
2732
+ const index2 = TYPE_PRIORITY.indexOf(record.frontmatter.type);
2733
+ return index2 === -1 ? TYPE_PRIORITY.length : index2;
2011
2734
  }
2012
2735
 
2013
2736
  // src/catalog.ts
@@ -2216,7 +2939,7 @@ function appendUnionMergeLine(contents) {
2216
2939
  }
2217
2940
 
2218
2941
  // src/kb-store.ts
2219
- var KB_DIR = (0, import_node_path4.join)(".strauss", "kb");
2942
+ var KB_DIR = (0, import_node_path7.join)(".strauss", "kb");
2220
2943
  var STORE_OWNED = /* @__PURE__ */ new Set([INDEX_FILE, LOG_FILE, SEARCH_INDEX_FILE]);
2221
2944
  var DEFAULT_LOAD_BUDGET = 25e3;
2222
2945
  var KbStore = class {
@@ -2247,7 +2970,7 @@ var KbStore = class {
2247
2970
  const conceptId2 = `${input.type}.${input.slug}`;
2248
2971
  const root = this.root(bundlePath2);
2249
2972
  const target = this.recordPath(bundlePath2, conceptId2);
2250
- await (0, import_promises4.mkdir)(root, { recursive: true });
2973
+ await (0, import_promises6.mkdir)(root, { recursive: true });
2251
2974
  await this.publish(
2252
2975
  target,
2253
2976
  stringifyMarkdownWithFrontmatter(input.body, frontmatter),
@@ -2286,7 +3009,7 @@ var KbStore = class {
2286
3009
  const target = this.recordPath(bundlePath2, conceptId2);
2287
3010
  let raw;
2288
3011
  try {
2289
- raw = await (0, import_promises4.readFile)(target, "utf8");
3012
+ raw = await (0, import_promises6.readFile)(target, "utf8");
2290
3013
  } catch {
2291
3014
  return null;
2292
3015
  }
@@ -2303,7 +3026,7 @@ var KbStore = class {
2303
3026
  const root = this.root(bundlePath2);
2304
3027
  let names;
2305
3028
  try {
2306
- names = await (0, import_promises4.readdir)(root);
3029
+ names = await (0, import_promises6.readdir)(root);
2307
3030
  } catch {
2308
3031
  return [];
2309
3032
  }
@@ -2311,7 +3034,7 @@ var KbStore = class {
2311
3034
  const records = await mapLimit(
2312
3035
  wanted,
2313
3036
  DEFAULT_IO_CONCURRENCY,
2314
- async ({ name, conceptId: conceptId2 }) => this.parse(conceptId2, await (0, import_promises4.readFile)((0, import_node_path4.join)(root, name), "utf8"))
3037
+ async ({ name, conceptId: conceptId2 }) => this.parse(conceptId2, await (0, import_promises6.readFile)((0, import_node_path7.join)(root, name), "utf8"))
2315
3038
  );
2316
3039
  return records.filter((record) => record !== null);
2317
3040
  }
@@ -2359,8 +3082,8 @@ var KbStore = class {
2359
3082
  * and the refusal is logged under its own operation name — `mutate` only
2360
3083
  * logs what it publishes.
2361
3084
  */
2362
- async verify(bundlePath2, conceptId2, note, actor = "unknown", at = (/* @__PURE__ */ new Date()).toISOString()) {
2363
- const event = kbVerifiedEventSchema.parse({ by: actor, at, note });
3085
+ async verify(bundlePath2, conceptId2, note, actor = "unknown", at2 = (/* @__PURE__ */ new Date()).toISOString()) {
3086
+ const event = kbVerifiedEventSchema.parse({ by: actor, at: at2, note });
2364
3087
  const existing = await this.read(bundlePath2, conceptId2);
2365
3088
  if (!existing) throw new KbRecordNotFoundError(conceptId2);
2366
3089
  const generatedBy = existing.frontmatter.generated?.by;
@@ -2412,14 +3135,14 @@ var KbStore = class {
2412
3135
  return superseded;
2413
3136
  }
2414
3137
  /** Resolves an open question, stamping who answered and when. */
2415
- async answer(bundlePath2, conceptId2, answer, actor = "unknown", at = (/* @__PURE__ */ new Date()).toISOString()) {
3138
+ async answer(bundlePath2, conceptId2, answer, actor = "unknown", at2 = (/* @__PURE__ */ new Date()).toISOString()) {
2416
3139
  return this.mutate(
2417
3140
  bundlePath2,
2418
3141
  conceptId2,
2419
3142
  (frontmatter) => ({
2420
3143
  ...frontmatter,
2421
3144
  strauss_status: "resolved",
2422
- strauss_answered: { by: actor, at }
3145
+ strauss_answered: { by: actor, at: at2 }
2423
3146
  }),
2424
3147
  { operation: "answer", by: actor },
2425
3148
  (body) => `${body.trimEnd()}
@@ -2468,7 +3191,7 @@ ${answer}
2468
3191
  if (found.length) return found;
2469
3192
  }
2470
3193
  const lowered = needle.toLowerCase();
2471
- return bundle.filter((record) => matches(record, lowered));
3194
+ return bundle.filter((record) => matches2(record, lowered));
2472
3195
  }
2473
3196
  /**
2474
3197
  * Anchor drift over the records about to be handed back. Like the search
@@ -2588,24 +3311,34 @@ ${answer}
2588
3311
  }
2589
3312
  /**
2590
3313
  * `load`'s digest without `load`'s bodies — the same records, adjudicated
2591
- * the same way, handed back as a stamp. Skips the anchor drift pass, which
2592
- * reads source files and only ever adds warnings: no warning reaches the
2593
- * digest, so the value is identical to the one `load` returns.
3314
+ * the same way, handed back as a stamp.
3315
+ *
3316
+ * Drift is counted but kept out of the digest, which is what lets the reload
3317
+ * hook ask one question and get two answers: whether the base moved, and
3318
+ * whether the code under it did. A `load` and a `stamp` of the same base
3319
+ * still agree on the digest, because no warning has ever reached it.
2594
3320
  */
2595
- async stamp(bundlePath2) {
3321
+ async stamp(bundlePath2, options = {}) {
2596
3322
  const bundle = await this.list(bundlePath2);
2597
3323
  const adjudicated = adjudicate(bundle, bundle, /* @__PURE__ */ new Date());
2598
3324
  const current = adjudicated.filter((hit) => hit.standing !== "superseded");
2599
3325
  const superseded = adjudicated.filter((hit) => hit.standing === "superseded").map(stub);
2600
3326
  const stamped = bundleStamp(current, superseded);
2601
- const dates = bundle.map((record) => record.frontmatter.generated?.at ?? null).filter((at) => typeof at === "string").sort();
3327
+ const dates = bundle.map((record) => record.frontmatter.generated?.at ?? null).filter((at2) => typeof at2 === "string").sort();
3328
+ const drift = await this.detectDrift(bundle, options.repoRoot);
3329
+ const drifted2 = drift === void 0 ? null : [...drift.values()].filter(
3330
+ (entries) => entries.some(
3331
+ (entry) => entry.state !== "match" && !isUncheckedReason(entry.reason)
3332
+ )
3333
+ ).length;
2602
3334
  return {
2603
3335
  path: bundlePath2,
2604
3336
  digest: stamped.digest,
2605
3337
  recordCount: bundle.length,
2606
3338
  superseded: superseded.length,
2607
3339
  newestAt: dates.at(-1) ?? null,
2608
- records: stamped.records
3340
+ records: stamped.records,
3341
+ drifted: drifted2
2609
3342
  };
2610
3343
  }
2611
3344
  /** How a position was arrived at, as a timeline. See `trace.ts`. */
@@ -2638,11 +3371,11 @@ ${answer}
2638
3371
  async readIndex(bundlePath2) {
2639
3372
  const root = this.root(bundlePath2);
2640
3373
  const expected = renderIndex(await this.list(bundlePath2));
2641
- const stored = await (0, import_promises4.readFile)((0, import_node_path4.join)(root, INDEX_FILE), "utf8").catch(
3374
+ const stored = await (0, import_promises6.readFile)((0, import_node_path7.join)(root, INDEX_FILE), "utf8").catch(
2642
3375
  () => null
2643
3376
  );
2644
3377
  if (indexIsStale(stored, expected)) {
2645
- await this.publish((0, import_node_path4.join)(root, INDEX_FILE), expected, true, INDEX_FILE);
3378
+ await this.publish((0, import_node_path7.join)(root, INDEX_FILE), expected, true, INDEX_FILE);
2646
3379
  this.logger.info?.({
2647
3380
  operation: "kb.index.repair",
2648
3381
  bundlePath: root,
@@ -2659,8 +3392,8 @@ ${answer}
2659
3392
  * knows which agent touched what. So a bad line is surfaced and left alone.
2660
3393
  */
2661
3394
  async readLog(bundlePath2) {
2662
- const raw = await (0, import_promises4.readFile)(
2663
- (0, import_node_path4.join)(this.root(bundlePath2), LOG_FILE),
3395
+ const raw = await (0, import_promises6.readFile)(
3396
+ (0, import_node_path7.join)(this.root(bundlePath2), LOG_FILE),
2664
3397
  "utf8"
2665
3398
  ).catch(() => "");
2666
3399
  const result = parseLog(raw);
@@ -2711,14 +3444,14 @@ ${answer}
2711
3444
  }
2712
3445
  async mutate(bundlePath2, conceptId2, change, entry, changeBody = (body) => body) {
2713
3446
  const target = this.recordPath(bundlePath2, conceptId2);
2714
- const before = await (0, import_promises4.readFile)(target, "utf8").catch(() => null);
3447
+ const before = await (0, import_promises6.readFile)(target, "utf8").catch(() => null);
2715
3448
  if (before === null) throw new KbRecordNotFoundError(conceptId2);
2716
3449
  const parsed = this.parse(conceptId2, before);
2717
3450
  if (!parsed) throw new KbRecordNotFoundError(conceptId2);
2718
3451
  const frontmatter = change(parsed.frontmatter);
2719
3452
  const body = changeBody(parsed.body);
2720
3453
  const contents = stringifyMarkdownWithFrontmatter(body, frontmatter);
2721
- const witness = await (0, import_promises4.readFile)(target, "utf8").catch(() => null);
3454
+ const witness = await (0, import_promises6.readFile)(target, "utf8").catch(() => null);
2722
3455
  if (witness === null || sha256(witness) !== sha256(before)) {
2723
3456
  throw new KbWriteConflictError(conceptId2);
2724
3457
  }
@@ -2744,20 +3477,20 @@ ${answer}
2744
3477
  */
2745
3478
  async publish(target, contents, overwrite, conceptId2) {
2746
3479
  const staging = `${target}.${process.pid}.tmp`;
2747
- await (0, import_promises4.writeFile)(staging, contents, "utf8");
3480
+ await (0, import_promises6.writeFile)(staging, contents, "utf8");
2748
3481
  try {
2749
3482
  if (overwrite) {
2750
- await (0, import_promises4.rename)(staging, target);
3483
+ await (0, import_promises6.rename)(staging, target);
2751
3484
  return;
2752
3485
  }
2753
- await (0, import_promises4.link)(staging, target);
3486
+ await (0, import_promises6.link)(staging, target);
2754
3487
  } catch (error) {
2755
3488
  if (error.code === "EEXIST") {
2756
3489
  throw new KbRecordAlreadyExistsError(conceptId2);
2757
3490
  }
2758
3491
  throw error;
2759
3492
  } finally {
2760
- await (0, import_promises4.unlink)(staging).catch(() => void 0);
3493
+ await (0, import_promises6.unlink)(staging).catch(() => void 0);
2761
3494
  }
2762
3495
  }
2763
3496
  /**
@@ -2801,18 +3534,18 @@ ${answer}
2801
3534
  * file must not fail the mutation it guards.
2802
3535
  */
2803
3536
  async ensureGitattributes(root) {
2804
- const target = (0, import_node_path4.join)(root, GITATTRIBUTES_FILE);
3537
+ const target = (0, import_node_path7.join)(root, GITATTRIBUTES_FILE);
2805
3538
  try {
2806
3539
  let existing;
2807
3540
  try {
2808
- existing = await (0, import_promises4.readFile)(target, "utf8");
3541
+ existing = await (0, import_promises6.readFile)(target, "utf8");
2809
3542
  } catch (error) {
2810
3543
  if (error.code !== "ENOENT") throw error;
2811
3544
  existing = null;
2812
3545
  }
2813
3546
  if (existing === null) {
2814
3547
  try {
2815
- await (0, import_promises4.writeFile)(target, appendUnionMergeLine(""), {
3548
+ await (0, import_promises6.writeFile)(target, appendUnionMergeLine(""), {
2816
3549
  encoding: "utf8",
2817
3550
  flag: "wx"
2818
3551
  });
@@ -2833,7 +3566,7 @@ ${answer}
2833
3566
  return;
2834
3567
  }
2835
3568
  if (!hasMergeDeclaration(existing)) {
2836
- await (0, import_promises4.appendFile)(target, appendUnionMergeLine(existing), "utf8");
3569
+ await (0, import_promises6.appendFile)(target, appendUnionMergeLine(existing), "utf8");
2837
3570
  this.logger.info?.({
2838
3571
  operation: "kb.gitattributes.ensure",
2839
3572
  bundlePath: root,
@@ -2852,7 +3585,7 @@ ${answer}
2852
3585
  async record(root, entry) {
2853
3586
  await this.ensureGitattributes(root);
2854
3587
  const line = renderLogEntry({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry });
2855
- await (0, import_promises4.appendFile)((0, import_node_path4.join)(root, LOG_FILE), line, "utf8").catch((error) => {
3588
+ await (0, import_promises6.appendFile)((0, import_node_path7.join)(root, LOG_FILE), line, "utf8").catch((error) => {
2856
3589
  this.logger.warn?.({
2857
3590
  operation: "kb.log.append",
2858
3591
  outcome: "failed",
@@ -2878,18 +3611,18 @@ ${answer}
2878
3611
  };
2879
3612
  }
2880
3613
  root(bundlePath2) {
2881
- return (0, import_node_path4.resolve)(bundlePath2);
3614
+ return (0, import_node_path7.resolve)(bundlePath2);
2882
3615
  }
2883
3616
  // Concept ids are `<type>.<slug>` and map to a single file directly under the
2884
3617
  // bundle root; anything carrying a separator would escape it.
2885
3618
  recordPath(bundlePath2, conceptId2) {
2886
- if (conceptId2.includes(import_node_path4.sep) || conceptId2.includes("/")) {
3619
+ if (conceptId2.includes(import_node_path7.sep) || conceptId2.includes("/")) {
2887
3620
  throw new KbInvalidConceptIdError(
2888
3621
  "concept id must not contain a path separator",
2889
3622
  { conceptId: conceptId2 }
2890
3623
  );
2891
3624
  }
2892
- return (0, import_node_path4.join)(this.root(bundlePath2), `${conceptId2}.md`);
3625
+ return (0, import_node_path7.join)(this.root(bundlePath2), `${conceptId2}.md`);
2893
3626
  }
2894
3627
  };
2895
3628
  function estimateTokens(record) {
@@ -2916,7 +3649,7 @@ function stub(hit) {
2916
3649
  at: hit.record.frontmatter.generated?.at ?? null
2917
3650
  };
2918
3651
  }
2919
- function matches(record, needle) {
3652
+ function matches2(record, needle) {
2920
3653
  const { title, description } = record.frontmatter;
2921
3654
  return [record.conceptId, title, description, record.body].some(
2922
3655
  (field) => field?.toLowerCase().includes(needle)
@@ -2929,37 +3662,37 @@ function normalizeActor(id) {
2929
3662
  }
2930
3663
 
2931
3664
  // src/compose.ts
2932
- var import_zod3 = require("zod");
2933
- var composeLinkSchema = import_zod3.z.object({
3665
+ var import_zod4 = require("zod");
3666
+ var composeLinkSchema = import_zod4.z.object({
2934
3667
  target: kbConceptIdSchema,
2935
- rel: import_zod3.z.enum(KB_LINK_RELS)
3668
+ rel: import_zod4.z.enum(KB_LINK_RELS)
2936
3669
  }).strict();
2937
- var composeInputSchema = import_zod3.z.object({
2938
- slug: import_zod3.z.string().min(1),
3670
+ var composeInputSchema = import_zod4.z.object({
3671
+ slug: import_zod4.z.string().min(1),
2939
3672
  /** One line, in the reader's terms. Becomes OKF `title`. */
2940
- title: import_zod3.z.string().min(1),
3673
+ title: import_zod4.z.string().min(1),
2941
3674
  /** The consequence — what breaks if this is wrong. Becomes `description`. */
2942
- why: import_zod3.z.string().min(1),
3675
+ why: import_zod4.z.string().min(1),
2943
3676
  /** Keyed by section heading from the type's spec. Unknown keys rejected. */
2944
- sections: import_zod3.z.record(import_zod3.z.string(), import_zod3.z.string().min(1)).optional(),
2945
- anchors: import_zod3.z.array(kbAnchorSchema).optional(),
2946
- sources: import_zod3.z.array(kbSourceSchema).optional(),
3677
+ sections: import_zod4.z.record(import_zod4.z.string(), import_zod4.z.string().min(1)).optional(),
3678
+ anchors: import_zod4.z.array(kbAnchorSchema).optional(),
3679
+ sources: import_zod4.z.array(kbSourceSchema).optional(),
2947
3680
  /** No source exists, as a claim rather than a sentinel in `sources`. */
2948
- assumption: import_zod3.z.boolean().optional(),
3681
+ assumption: import_zod4.z.boolean().optional(),
2949
3682
  /**
2950
3683
  * OKF `stale_after`: the absolute date this record stops being trusted.
2951
3684
  * Anything the outside world can change — pricing, quotas, versions,
2952
3685
  * reception counts — should carry one.
2953
3686
  */
2954
- stale_after: import_zod3.z.string().regex(/^\d{4}-\d{2}-\d{2}$/, {
3687
+ stale_after: import_zod4.z.string().regex(/^\d{4}-\d{2}-\d{2}$/, {
2955
3688
  message: "stale_after must be YYYY-MM-DD"
2956
3689
  }).refine((date) => !Number.isNaN(Date.parse(date)), {
2957
3690
  message: "stale_after must be a real date"
2958
3691
  }).optional(),
2959
- verify: import_zod3.z.array(import_zod3.z.string().min(1)).optional(),
2960
- tags: import_zod3.z.array(import_zod3.z.string().min(1)).optional(),
3692
+ verify: import_zod4.z.array(import_zod4.z.string().min(1)).optional(),
3693
+ tags: import_zod4.z.array(import_zod4.z.string().min(1)).optional(),
2961
3694
  /** Concept ids this record relates to; rendered as body links. */
2962
- relatedConceptIds: import_zod3.z.array(kbConceptIdSchema).optional(),
3695
+ relatedConceptIds: import_zod4.z.array(kbConceptIdSchema).optional(),
2963
3696
  /**
2964
3697
  * Typed causal edges, source → target: `{ target: "fact.b", rel:
2965
3698
  * "depends_on" }` on record A says A needs B. Stored in frontmatter and
@@ -2967,16 +3700,16 @@ var composeInputSchema = import_zod3.z.object({
2967
3700
  * reader that knows only OKF. The vocabulary goes into the description from
2968
3701
  * the same table the walk uses, so `kb_schema` emits it.
2969
3702
  */
2970
- links: import_zod3.z.array(composeLinkSchema).max(64).optional().describe(
3703
+ links: import_zod4.z.array(composeLinkSchema).max(64).optional().describe(
2971
3704
  `Typed causal edges, source \u2192 target \u2014 a link on this record says this record <rel> the target. ${KB_LINK_RELS.map(
2972
3705
  (rel) => `${rel}: ${LINK_RELS[rel].purpose}`
2973
3706
  ).join("; ")}.`
2974
3707
  ),
2975
3708
  /** Concept ids this record replaces. The store settles the backlinks. */
2976
- supersedes: import_zod3.z.array(kbConceptIdSchema).max(32).optional(),
2977
- materiality: import_zod3.z.enum(KB_MATERIALITIES).optional(),
2978
- confidence: import_zod3.z.enum(KB_CONFIDENCES).optional(),
2979
- owner: import_zod3.z.string().min(1).optional()
3709
+ supersedes: import_zod4.z.array(kbConceptIdSchema).max(32).optional(),
3710
+ materiality: import_zod4.z.enum(KB_MATERIALITIES).optional(),
3711
+ confidence: import_zod4.z.enum(KB_CONFIDENCES).optional(),
3712
+ owner: import_zod4.z.string().min(1).optional()
2980
3713
  }).strict();
2981
3714
  function composeRecord(type, input, writtenBy, writtenAt) {
2982
3715
  const parsed = composeInputSchema.parse(input);
@@ -3053,9 +3786,9 @@ ${text}`);
3053
3786
  // src/kb-pins/budgets.ts
3054
3787
  function asBudgets(value) {
3055
3788
  if (value === null || typeof value !== "object") return {};
3056
- const table = value;
3789
+ const table2 = value;
3057
3790
  const pick = (key2, min) => {
3058
- const raw = table[key2];
3791
+ const raw = table2[key2];
3059
3792
  return typeof raw === "number" && Number.isInteger(raw) && raw >= min ? raw : void 0;
3060
3793
  };
3061
3794
  const budgetTokens = pick("budgetTokens", 1);
@@ -3066,9 +3799,9 @@ function asBudgets(value) {
3066
3799
  };
3067
3800
  }
3068
3801
  function contextProfileBudgets(manifest, profile) {
3069
- const table = manifest.context;
3070
- if (table === null || typeof table !== "object") return {};
3071
- const entries = table;
3802
+ const table2 = manifest.context;
3803
+ if (table2 === null || typeof table2 !== "object") return {};
3804
+ const entries = table2;
3072
3805
  return {
3073
3806
  ...asBudgets(entries["default"]),
3074
3807
  ...profile ? asBudgets(entries[profile]) : {}
@@ -3099,23 +3832,23 @@ var KbBaseFrozenError = class extends Error {
3099
3832
  };
3100
3833
 
3101
3834
  // src/kb-pins/frozen.ts
3102
- var import_node_path7 = require("path");
3835
+ var import_node_path10 = require("path");
3103
3836
 
3104
3837
  // src/kb-pins/layers.ts
3105
- var import_promises5 = require("fs/promises");
3106
- var import_node_os2 = require("os");
3107
- var import_node_path6 = require("path");
3838
+ var import_promises7 = require("fs/promises");
3839
+ var import_node_os3 = require("os");
3840
+ var import_node_path9 = require("path");
3108
3841
 
3109
3842
  // src/kb-pins/model.ts
3110
- var import_node_path5 = require("path");
3111
- var import_zod4 = require("zod");
3112
- var PINS_FILE = (0, import_node_path5.join)(".strauss", "kb-pins.json");
3113
- var PINS_LOCAL_FILE = (0, import_node_path5.join)(".strauss", "kb-pins.local.json");
3843
+ var import_node_path8 = require("path");
3844
+ var import_zod5 = require("zod");
3845
+ var PINS_FILE = (0, import_node_path8.join)(".strauss", "kb-pins.json");
3846
+ var PINS_LOCAL_FILE = (0, import_node_path8.join)(".strauss", "kb-pins.local.json");
3114
3847
  var PIN_LAYERS = ["project", "local", "user"];
3115
- var pinSchema = import_zod4.z.object({
3848
+ var pinSchema = import_zod5.z.object({
3116
3849
  /** Relative to the manifest's root, so the file is committable. */
3117
- path: import_zod4.z.string().min(1),
3118
- pinnedAt: import_zod4.z.string().min(1).optional(),
3850
+ path: import_zod5.z.string().min(1),
3851
+ pinnedAt: import_zod5.z.string().min(1).optional(),
3119
3852
  /**
3120
3853
  * How `context` renders this base. `full` preloads the whole base into
3121
3854
  * the block regardless of the full-under threshold — for a base whose
@@ -3125,7 +3858,7 @@ var pinSchema = import_zod4.z.object({
3125
3858
  * Absent: the profile's full-under threshold decides. Invalid values
3126
3859
  * degrade to absent rather than failing the manifest.
3127
3860
  */
3128
- mode: import_zod4.z.enum(["full", "index"]).optional().catch(void 0),
3861
+ mode: import_zod5.z.enum(["full", "index"]).optional().catch(void 0),
3129
3862
  /**
3130
3863
  * Context profiles this pin surfaces in (e.g. only at session-start,
3131
3864
  * not per turn). Absent: every profile. A run without a profile sees
@@ -3133,17 +3866,17 @@ var pinSchema = import_zod4.z.object({
3133
3866
  * that skill at point of use than pinned at all — pins are what every
3134
3867
  * session should see.
3135
3868
  */
3136
- profiles: import_zod4.z.array(import_zod4.z.string()).optional().catch(void 0),
3869
+ profiles: import_zod5.z.array(import_zod5.z.string()).optional().catch(void 0),
3137
3870
  /**
3138
3871
  * The base is concluded — a finished piece of research, a frozen ADR
3139
3872
  * set. Write commands against it refuse while this workspace holds the
3140
3873
  * pin, and `context` labels it read-only. Workspace policy, not base
3141
3874
  * state: the base itself stays copyable and writable elsewhere.
3142
3875
  */
3143
- frozen: import_zod4.z.boolean().optional().catch(void 0)
3876
+ frozen: import_zod5.z.boolean().optional().catch(void 0)
3144
3877
  }).passthrough();
3145
- var pinsManifestSchema = import_zod4.z.object({
3146
- pins: import_zod4.z.array(pinSchema).default([]),
3878
+ var pinsManifestSchema = import_zod5.z.object({
3879
+ pins: import_zod5.z.array(pinSchema).default([]),
3147
3880
  /**
3148
3881
  * Per-repo budgets for the `context` command, keyed by profile —
3149
3882
  * `"session-start"`, `"compact"`, `"turn"`, or `"default"` for all of
@@ -3152,18 +3885,18 @@ var pinsManifestSchema = import_zod4.z.object({
3152
3885
  * the index at every session start. `contextProfileBudgets` does the
3153
3886
  * tolerant read.
3154
3887
  */
3155
- context: import_zod4.z.unknown().optional()
3888
+ context: import_zod5.z.unknown().optional()
3156
3889
  }).passthrough();
3157
3890
 
3158
3891
  // src/kb-pins/layers.ts
3159
3892
  function userRoot() {
3160
- return process.env.STRAUSS_KB_USER_ROOT || (0, import_node_os2.homedir)();
3893
+ return process.env.STRAUSS_KB_USER_ROOT || (0, import_node_os3.homedir)();
3161
3894
  }
3162
3895
  function layerRoot(workspaceDir, layer) {
3163
- return layer === "user" ? userRoot() : (0, import_node_path6.resolve)(workspaceDir);
3896
+ return layer === "user" ? userRoot() : (0, import_node_path9.resolve)(workspaceDir);
3164
3897
  }
3165
3898
  function layerFile(workspaceDir, layer) {
3166
- return (0, import_node_path6.join)(
3899
+ return (0, import_node_path9.join)(
3167
3900
  layerRoot(workspaceDir, layer),
3168
3901
  layer === "local" ? PINS_LOCAL_FILE : PINS_FILE
3169
3902
  );
@@ -3172,7 +3905,7 @@ async function readPinsLayer(workspaceDir, layer) {
3172
3905
  const file = layerFile(workspaceDir, layer);
3173
3906
  let raw;
3174
3907
  try {
3175
- raw = await (0, import_promises5.readFile)(file, "utf8");
3908
+ raw = await (0, import_promises7.readFile)(file, "utf8");
3176
3909
  } catch {
3177
3910
  return { pins: [] };
3178
3911
  }
@@ -3196,16 +3929,16 @@ async function readPinsLayer(workspaceDir, layer) {
3196
3929
  }
3197
3930
  async function writePinsLayer(workspaceDir, layer, manifest) {
3198
3931
  const file = layerFile(workspaceDir, layer);
3199
- await (0, import_promises5.mkdir)((0, import_node_path6.dirname)(file), { recursive: true });
3200
- await (0, import_promises5.writeFile)(file, `${JSON.stringify(manifest, null, 2)}
3932
+ await (0, import_promises7.mkdir)((0, import_node_path9.dirname)(file), { recursive: true });
3933
+ await (0, import_promises7.writeFile)(file, `${JSON.stringify(manifest, null, 2)}
3201
3934
  `, "utf8");
3202
3935
  }
3203
3936
  function resolvePinPath(rootDir, path) {
3204
- 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));
3937
+ 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));
3205
3938
  }
3206
3939
  function storablePath(rootDir, bundlePath2) {
3207
- const rel = (0, import_node_path6.relative)((0, import_node_path6.resolve)(rootDir), (0, import_node_path6.resolve)(bundlePath2));
3208
- return (rel === "" ? "." : rel).split(import_node_path6.sep).join("/");
3940
+ const rel = (0, import_node_path9.relative)((0, import_node_path9.resolve)(rootDir), (0, import_node_path9.resolve)(bundlePath2));
3941
+ return (rel === "" ? "." : rel).split(import_node_path9.sep).join("/");
3209
3942
  }
3210
3943
  async function readMergedPins(workspaceDir) {
3211
3944
  const manifests = {};
@@ -3233,7 +3966,7 @@ async function readMergedPins(workspaceDir) {
3233
3966
  // src/kb-pins/frozen.ts
3234
3967
  async function assertBaseNotFrozen(workspaceDir, bundlePath2) {
3235
3968
  const merged = await readMergedPins(workspaceDir);
3236
- const absolute = (0, import_node_path7.resolve)(bundlePath2);
3969
+ const absolute = (0, import_node_path10.resolve)(bundlePath2);
3237
3970
  const pin = merged.pins.find((entry) => entry.absolutePath === absolute);
3238
3971
  if (pin?.frozen === true) {
3239
3972
  throw new KbBaseFrozenError(pin.path, pin.layer);
@@ -3262,7 +3995,7 @@ async function listPins(store, workspaceDir) {
3262
3995
  }
3263
3996
 
3264
3997
  // src/kb-pins/pin.ts
3265
- async function pinBase(store, workspaceDir, bundlePath2, at, options = {}) {
3998
+ async function pinBase(store, workspaceDir, bundlePath2, at2, options = {}) {
3266
3999
  const layer = options.layer ?? "project";
3267
4000
  const root = layerRoot(workspaceDir, layer);
3268
4001
  const manifest = await readPinsLayer(workspaceDir, layer);
@@ -3290,7 +4023,7 @@ async function pinBase(store, workspaceDir, bundlePath2, at, options = {}) {
3290
4023
  return {
3291
4024
  path: existing.path,
3292
4025
  layer,
3293
- pinnedAt: existing.pinnedAt ?? at,
4026
+ pinnedAt: existing.pinnedAt ?? at2,
3294
4027
  alreadyPinned: true,
3295
4028
  ...updated.mode ? { mode: updated.mode } : {},
3296
4029
  ...updated.profiles ? { profiles: updated.profiles } : {},
@@ -3300,7 +4033,7 @@ async function pinBase(store, workspaceDir, bundlePath2, at, options = {}) {
3300
4033
  }
3301
4034
  const entry = {
3302
4035
  path: storablePath(root, bundlePath2),
3303
- pinnedAt: at,
4036
+ pinnedAt: at2,
3304
4037
  ...fields
3305
4038
  };
3306
4039
  await writePinsLayer(workspaceDir, layer, {
@@ -3310,7 +4043,7 @@ async function pinBase(store, workspaceDir, bundlePath2, at, options = {}) {
3310
4043
  return {
3311
4044
  path: entry.path,
3312
4045
  layer,
3313
- pinnedAt: at,
4046
+ pinnedAt: at2,
3314
4047
  alreadyPinned: false,
3315
4048
  ...fields,
3316
4049
  ...warning ? { warning } : {}
@@ -3318,7 +4051,7 @@ async function pinBase(store, workspaceDir, bundlePath2, at, options = {}) {
3318
4051
  }
3319
4052
 
3320
4053
  // src/kb-pins/unpin.ts
3321
- var import_node_path8 = require("path");
4054
+ var import_node_path11 = require("path");
3322
4055
  async function unpinBase(workspaceDir, bundlePath2) {
3323
4056
  const layers = [];
3324
4057
  for (const layer of PIN_LAYERS) {
@@ -3339,14 +4072,14 @@ async function unpinBase(workspaceDir, bundlePath2) {
3339
4072
  }
3340
4073
  }
3341
4074
  return {
3342
- path: storablePath((0, import_node_path8.resolve)(workspaceDir), bundlePath2),
4075
+ path: storablePath((0, import_node_path11.resolve)(workspaceDir), bundlePath2),
3343
4076
  removed: layers.length > 0,
3344
4077
  layers
3345
4078
  };
3346
4079
  }
3347
4080
 
3348
4081
  // src/kb-context.ts
3349
- var import_promises6 = require("fs/promises");
4082
+ var import_promises8 = require("fs/promises");
3350
4083
  var HEADING2 = "## Knowledge bases (pinned)";
3351
4084
  var DEFAULT_CONTEXT_BUDGET = 4e3;
3352
4085
  var CONTEXT_PROFILES = {
@@ -3550,13 +4283,13 @@ function toHookJson(block, event) {
3550
4283
  var CONTEXT_BEGIN = "<!-- strauss-kb:begin -->";
3551
4284
  var CONTEXT_END = "<!-- strauss-kb:end -->";
3552
4285
  async function syncInstructions(file, block) {
3553
- const existing = await (0, import_promises6.readFile)(file, "utf8").catch(() => null);
4286
+ const existing = await (0, import_promises8.readFile)(file, "utf8").catch(() => null);
3554
4287
  const region = block ? `${CONTEXT_BEGIN}
3555
4288
  ${block.trim()}
3556
4289
  ${CONTEXT_END}` : null;
3557
4290
  if (existing === null) {
3558
4291
  if (!region) return { file, action: "unchanged" };
3559
- await (0, import_promises6.writeFile)(file, `${region}
4292
+ await (0, import_promises8.writeFile)(file, `${region}
3560
4293
  `, "utf8");
3561
4294
  return { file, action: "created" };
3562
4295
  }
@@ -3567,11 +4300,11 @@ ${CONTEXT_END}` : null;
3567
4300
  const after = existing.slice(end + CONTEXT_END.length);
3568
4301
  const next = region ? `${before}${region}${after}` : `${before.replace(/\n+$/, "\n")}${after.replace(/^\n+/, "\n")}`;
3569
4302
  if (next === existing) return { file, action: "unchanged" };
3570
- await (0, import_promises6.writeFile)(file, next, "utf8");
4303
+ await (0, import_promises8.writeFile)(file, next, "utf8");
3571
4304
  return { file, action: region ? "replaced" : "removed" };
3572
4305
  }
3573
4306
  if (!region) return { file, action: "unchanged" };
3574
- await (0, import_promises6.writeFile)(
4307
+ await (0, import_promises8.writeFile)(
3575
4308
  file,
3576
4309
  `${existing.replace(/\n*$/, "\n\n")}${region}
3577
4310
  `,
@@ -3581,14 +4314,14 @@ ${CONTEXT_END}` : null;
3581
4314
  }
3582
4315
 
3583
4316
  // src/json-schema.ts
3584
- var import_zod5 = require("zod");
4317
+ var import_zod6 = require("zod");
3585
4318
  function kbJsonSchemas() {
3586
4319
  return {
3587
- recordFrontmatter: import_zod5.z.toJSONSchema(kbRecordFrontmatterSchema, {
4320
+ recordFrontmatter: import_zod6.z.toJSONSchema(kbRecordFrontmatterSchema, {
3588
4321
  io: "input"
3589
4322
  }),
3590
- composeInput: import_zod5.z.toJSONSchema(composeInputSchema, { io: "input" }),
3591
- logEntry: import_zod5.z.toJSONSchema(kbLogEntrySchema, { io: "input" })
4323
+ composeInput: import_zod6.z.toJSONSchema(composeInputSchema, { io: "input" }),
4324
+ logEntry: import_zod6.z.toJSONSchema(kbLogEntrySchema, { io: "input" })
3592
4325
  };
3593
4326
  }
3594
4327
 
@@ -3598,7 +4331,7 @@ function matchToDiff(files, records, options = {}) {
3598
4331
  const anchored = records.filter(
3599
4332
  (record) => (record.frontmatter.strauss_anchors ?? []).length > 0
3600
4333
  );
3601
- const matches2 = [];
4334
+ const matches3 = [];
3602
4335
  for (const file of files) {
3603
4336
  const candidates = anchored.map((record) => ({
3604
4337
  record,
@@ -3617,7 +4350,7 @@ function matchToDiff(files, records, options = {}) {
3617
4350
  hits.push(record);
3618
4351
  }
3619
4352
  if (!hits.length) continue;
3620
- matches2.push({
4353
+ matches3.push({
3621
4354
  filePath: file.filePath,
3622
4355
  hunk,
3623
4356
  records: order(adjudicate(hits, records, options.now)),
@@ -3625,7 +4358,7 @@ function matchToDiff(files, records, options = {}) {
3625
4358
  });
3626
4359
  }
3627
4360
  }
3628
- return matches2;
4361
+ return matches3;
3629
4362
  }
3630
4363
  function place(anchors, filePath, hunk, ranges) {
3631
4364
  let fallback = "miss";
@@ -3774,6 +4507,18 @@ var CHECK_HEADLINES = {
3774
4507
  unchecked: "an anchor in another repository nothing could reach"
3775
4508
  };
3776
4509
  var DAY_MS = 864e5;
4510
+ function anchorResolverCounts(bundle) {
4511
+ let treeSitter = 0;
4512
+ let regex = 0;
4513
+ for (const record of bundle) {
4514
+ for (const anchor of record.frontmatter.strauss_anchors ?? []) {
4515
+ if (!anchor.hash || !anchor.symbol) continue;
4516
+ if (anchor.resolver === "tree-sitter") treeSitter += 1;
4517
+ else regex += 1;
4518
+ }
4519
+ }
4520
+ return { total: treeSitter + regex, treeSitter, regex };
4521
+ }
3777
4522
  function doctor(bundle, options = {}) {
3778
4523
  const thresholds = {
3779
4524
  expiringDays: options.expiringDays ?? DEFAULT_EXPIRING_DAYS,
@@ -3809,6 +4554,7 @@ function doctor(bundle, options = {}) {
3809
4554
  counts,
3810
4555
  groups,
3811
4556
  findingCount,
4557
+ anchorResolvers: anchorResolverCounts(bundle),
3812
4558
  healthy: findingCount === 0
3813
4559
  };
3814
4560
  }
@@ -3825,18 +4571,18 @@ function expired(hits, now) {
3825
4571
  for (const hit of hits) {
3826
4572
  const raw = hit.record.frontmatter.stale_after;
3827
4573
  if (!raw) continue;
3828
- const at = Date.parse(raw);
3829
- if (Number.isNaN(at)) {
4574
+ const at2 = Date.parse(raw);
4575
+ if (Number.isNaN(at2)) {
3830
4576
  findings.push(
3831
4577
  finding(hit.record, `stale_after "${raw}" is not a readable date`)
3832
4578
  );
3833
4579
  continue;
3834
4580
  }
3835
- if (at < now.getTime()) {
4581
+ if (at2 < now.getTime()) {
3836
4582
  findings.push(
3837
4583
  finding(
3838
4584
  hit.record,
3839
- `stale since ${raw} (${daysBetween(at, now.getTime())} days ago)`
4585
+ `stale since ${raw} (${daysBetween(at2, now.getTime())} days ago)`
3840
4586
  )
3841
4587
  );
3842
4588
  }
@@ -3849,12 +4595,12 @@ function expiring(hits, now, withinDays) {
3849
4595
  for (const hit of hits) {
3850
4596
  const raw = hit.record.frontmatter.stale_after;
3851
4597
  if (!raw) continue;
3852
- const at = Date.parse(raw);
3853
- if (Number.isNaN(at) || at < now.getTime() || at > horizon) continue;
4598
+ const at2 = Date.parse(raw);
4599
+ if (Number.isNaN(at2) || at2 < now.getTime() || at2 > horizon) continue;
3854
4600
  findings.push(
3855
4601
  finding(
3856
4602
  hit.record,
3857
- `goes stale ${raw} (in ${daysBetween(now.getTime(), at)} days)`
4603
+ `goes stale ${raw} (in ${daysBetween(now.getTime(), at2)} days)`
3858
4604
  )
3859
4605
  );
3860
4606
  }
@@ -4024,13 +4770,16 @@ function anchorFindings(hits, kind, headline) {
4024
4770
  );
4025
4771
  }
4026
4772
  function describeAnchor(anchor) {
4027
- const at = anchor.symbol ? `${anchor.file}:${anchor.symbol}` : anchor.file;
4028
- if (anchor.reason) return `${at} (${anchor.reason})`;
4773
+ const at2 = anchor.symbol ? `${anchor.file}:${anchor.symbol}` : anchor.file;
4774
+ if (anchor.class === "gone") {
4775
+ return `${at2} gone${anchor.reason ? ` (${anchor.reason})` : ""}`;
4776
+ }
4777
+ if (anchor.reason) return `${at2} (${anchor.reason})`;
4029
4778
  if (anchor.remoteState === "drifted-on-default") {
4030
- return `${at} (matches ref, moved on the default branch)`;
4779
+ return `${at2} (matches ref, moved on the default branch)`;
4031
4780
  }
4032
- if (anchor.diffSize === null) return `${at} (changed, size unrecorded)`;
4033
- return anchor.diffSize === 0 ? `${at} (content changed, same line count)` : `${at} (${anchor.diffSize} line${anchor.diffSize === 1 ? "" : "s"} apart)`;
4781
+ if (anchor.diffSize === null) return `${at2} (changed, size unrecorded)`;
4782
+ return anchor.diffSize === 0 ? `${at2} (content changed, same line count)` : `${at2} (${anchor.diffSize} line${anchor.diffSize === 1 ? "" : "s"} apart)`;
4034
4783
  }
4035
4784
  function replaces(later, earlier) {
4036
4785
  return (later.frontmatter.strauss_supersedes ?? []).includes(earlier.conceptId) || earlier.frontmatter.strauss_superseded_by === later.conceptId;
@@ -4047,20 +4796,20 @@ function daysBetween(from, to) {
4047
4796
  return Math.max(0, Math.floor((to - from) / DAY_MS));
4048
4797
  }
4049
4798
  function ageInDays(record, now) {
4050
- const at = record.frontmatter.generated?.at;
4051
- if (!at) return null;
4052
- const written = Date.parse(at);
4799
+ const at2 = record.frontmatter.generated?.at;
4800
+ if (!at2) return null;
4801
+ const written = Date.parse(at2);
4053
4802
  if (Number.isNaN(written)) return null;
4054
4803
  return daysBetween(written, now.getTime());
4055
4804
  }
4056
4805
 
4057
4806
  // src/decision-record.ts
4058
- var import_zod6 = require("zod");
4807
+ var import_zod7 = require("zod");
4059
4808
  var DECISION_TYPE = "decision";
4060
4809
  var NO_DECISION_SLUG = "none";
4061
4810
  var decisionInputSchema = composeInputSchema.omit({ sections: true }).extend({
4062
- alternative: import_zod6.z.string().min(1).optional(),
4063
- impact: import_zod6.z.string().min(1).optional()
4811
+ alternative: import_zod7.z.string().min(1).optional(),
4812
+ impact: import_zod7.z.string().min(1).optional()
4064
4813
  }).strict();
4065
4814
  function composeDecisionRecord(input, writtenBy, writtenAt) {
4066
4815
  const { alternative, impact: impact2, ...rest } = input;
@@ -4102,13 +4851,13 @@ function selectDecisions(records) {
4102
4851
  }
4103
4852
 
4104
4853
  // src/commands/anchor-resolve.ts
4105
- var import_zod8 = require("zod");
4854
+ var import_zod9 = require("zod");
4106
4855
 
4107
4856
  // src/commands/model.ts
4108
- var import_zod7 = require("zod");
4109
- var bundlePath = import_zod7.z.string().min(1).describe("Absolute path to the knowledge base directory.");
4110
- var conceptId = import_zod7.z.string().min(1).describe("e.g. decision.cursor-v2");
4111
- var REPO_ROOT = import_zod7.z.string().min(1).optional().describe(
4857
+ var import_zod8 = require("zod");
4858
+ var bundlePath = import_zod8.z.string().min(1).describe("Absolute path to the knowledge base directory.");
4859
+ var conceptId = import_zod8.z.string().min(1).describe("e.g. decision.cursor-v2");
4860
+ var REPO_ROOT = import_zod8.z.string().min(1).optional().describe(
4112
4861
  "Where the anchored source lives, for the drift check. Defaults to the working directory."
4113
4862
  );
4114
4863
  function define(command) {
@@ -4121,9 +4870,9 @@ function argvFlag(argv, name) {
4121
4870
  if (!value2) throw new KbMissingFlagValueError(name);
4122
4871
  return value2;
4123
4872
  }
4124
- const at = argv.indexOf(name);
4125
- if (at === -1) return void 0;
4126
- const value = argv[at + 1];
4873
+ const at2 = argv.indexOf(name);
4874
+ if (at2 === -1) return void 0;
4875
+ const value = argv[at2 + 1];
4127
4876
  if (value === void 0 || value.startsWith("--")) {
4128
4877
  throw new KbMissingFlagValueError(name);
4129
4878
  }
@@ -4131,22 +4880,30 @@ function argvFlag(argv, name) {
4131
4880
  }
4132
4881
 
4133
4882
  // src/commands/anchor-resolve.ts
4883
+ function resolverSummary(results) {
4884
+ const names = [
4885
+ ...new Set(
4886
+ results.flatMap((entry) => entry.resolver ? [entry.resolver] : [])
4887
+ )
4888
+ ].sort();
4889
+ return names.length ? `${names.join(" + ")} resolver` : "whole-file";
4890
+ }
4134
4891
  var anchorResolveCommand = define({
4135
4892
  name: "anchor-resolve",
4136
4893
  tool: "kb_anchor_resolve",
4137
4894
  usage: "anchor-resolve <concept-id> [--repo-root <path>] [--offline] [--rebaseline] [--restamp]",
4138
4895
  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.",
4139
- input: import_zod8.z.object({
4896
+ input: import_zod9.z.object({
4140
4897
  bundlePath,
4141
4898
  conceptId,
4142
- repoRoot: import_zod8.z.string().min(1).optional(),
4143
- offline: import_zod8.z.boolean().optional().describe(
4899
+ repoRoot: import_zod9.z.string().min(1).optional(),
4900
+ offline: import_zod9.z.boolean().optional().describe(
4144
4901
  "Resolve foreign anchors from the local repo cache only, never fetching."
4145
4902
  ),
4146
- rebaseline: import_zod8.z.boolean().optional().describe(
4903
+ rebaseline: import_zod9.z.boolean().optional().describe(
4147
4904
  "Accept the current code as the new baseline for anchors that drifted."
4148
4905
  ),
4149
- restamp: import_zod8.z.boolean().optional().describe(
4906
+ restamp: import_zod9.z.boolean().optional().describe(
4150
4907
  "Refresh `resolved_at` on anchors that already match. Off by default, so a green run writes nothing."
4151
4908
  )
4152
4909
  }),
@@ -4175,6 +4932,11 @@ var anchorResolveCommand = define({
4175
4932
  const updated = [];
4176
4933
  let dirty = false;
4177
4934
  const sources = await readSources(anchors, root, offline === true);
4935
+ const resolvers = defaultAnchorResolvers({ offline: offline === true });
4936
+ await prepareResolvers(
4937
+ resolvers,
4938
+ anchors.map((anchor) => anchor.file)
4939
+ );
4178
4940
  for (const anchor of anchors) {
4179
4941
  const base2 = {
4180
4942
  file: anchor.file,
@@ -4191,27 +4953,39 @@ var anchorResolveCommand = define({
4191
4953
  updated.push(anchor);
4192
4954
  continue;
4193
4955
  }
4194
- const resolved = resolveAnchor(source.source, anchor);
4195
- if (!resolved) {
4956
+ const outcome = resolveAnchorSpan(source.source, anchor, resolvers);
4957
+ if (!outcome.ok) {
4196
4958
  results.push({
4197
4959
  ...base2,
4198
4960
  state: "unresolved",
4199
- reason: "symbol-not-found"
4961
+ reason: outcome.reason
4200
4962
  });
4201
4963
  updated.push(anchor);
4202
4964
  continue;
4203
4965
  }
4204
- const currentHash = hashAnchorText(resolved.text);
4966
+ const resolved = outcome.span;
4967
+ const producedBy = outcome.resolver;
4968
+ const { hash: currentHash, kind } = anchorHashOf(anchor, outcome);
4205
4969
  const currentLines = resolved.endLine - resolved.startLine + 1;
4970
+ const stampedKind = outcome.normalized ? "ast" : "raw";
4971
+ const stampedHash = outcome.normalized ? anchorHashOf({ ...anchor, hash: void 0 }, outcome).hash : currentHash;
4206
4972
  const stamped = {
4207
4973
  ...anchor,
4208
- hash: currentHash,
4974
+ hash: stampedHash,
4975
+ hash_kind: stampedKind,
4209
4976
  lines: currentLines,
4210
- resolved_at: now()
4977
+ resolved_at: now(),
4978
+ ...producedBy ? { resolver: producedBy } : {}
4211
4979
  };
4212
4980
  const pinned = anchor.ref !== void 0 && source.repo !== void 0;
4213
4981
  if (!anchor.hash) {
4214
- results.push({ ...base2, state: "stamped", currentHash });
4982
+ results.push({
4983
+ ...base2,
4984
+ state: "stamped",
4985
+ currentHash: stampedHash,
4986
+ hashKind: stampedKind,
4987
+ ...producedBy ? { resolver: producedBy } : {}
4988
+ });
4215
4989
  updated.push(stamped);
4216
4990
  dirty = true;
4217
4991
  continue;
@@ -4221,7 +4995,12 @@ var anchorResolveCommand = define({
4221
4995
  ...base2,
4222
4996
  state: "drifted",
4223
4997
  currentHash,
4998
+ hashKind: kind,
4224
4999
  diffSize: lineDelta(anchor, currentLines),
5000
+ ...producedBy ? { resolver: producedBy } : {},
5001
+ // A regex-stamped anchor re-read by tree-sitter drifts because the
5002
+ // resolver changed, not because the code did.
5003
+ ...resolverChanged(source.source, anchor, producedBy) ? { reason: "resolver-changed" } : {},
4225
5004
  ...pinned ? { remoteState: "drifted-from-ref" } : {},
4226
5005
  ...rebaseline ? { rebaselined: true } : {}
4227
5006
  });
@@ -4229,7 +5008,7 @@ var anchorResolveCommand = define({
4229
5008
  if (rebaseline) dirty = true;
4230
5009
  continue;
4231
5010
  }
4232
- const onDefault = pinned ? headHash(source, anchor) : void 0;
5011
+ const onDefault = pinned ? headHash(source, anchor, resolvers) : void 0;
4233
5012
  if (onDefault && onDefault.hash !== anchor.hash) {
4234
5013
  results.push({
4235
5014
  ...base2,
@@ -4245,6 +5024,8 @@ var anchorResolveCommand = define({
4245
5024
  ...base2,
4246
5025
  state: "match",
4247
5026
  currentHash,
5027
+ hashKind: kind,
5028
+ ...producedBy ? { resolver: producedBy } : {},
4248
5029
  ...pinned ? { remoteState: "matches-ref" } : {}
4249
5030
  });
4250
5031
  const refresh = restamp || anchor.resolved_at === void 0;
@@ -4262,19 +5043,21 @@ var anchorResolveCommand = define({
4262
5043
  if (!frozen) await store.updateAnchors(path, id, updated, actor);
4263
5044
  }
4264
5045
  const frozenNote = frozen ? { frozen: true, note: "base is frozen: nothing was stamped" } : {};
5046
+ const hints = grammarHints();
5047
+ const hintNote = hints.length ? { hints } : {};
4265
5048
  const unreachable = results.filter(
4266
5049
  (entry) => isUncheckedReason(entry.reason)
4267
5050
  ).length;
4268
5051
  const checked = results.length - unreachable;
4269
- const matches2 = results.filter((entry) => entry.state === "match").length;
4270
- const note = `${matches2}/${checked} anchors match${unreachable ? `, ${unreachable} unreachable` : ""}`;
4271
- const clean = checked > 0 && matches2 === checked && unreachable === 0;
5052
+ const matches3 = results.filter((entry) => entry.state === "match").length;
5053
+ const note = `${matches3}/${checked} anchors match${unreachable ? `, ${unreachable} unreachable` : ""}`;
5054
+ const clean = checked > 0 && matches3 === checked && unreachable === 0;
4272
5055
  if (clean) {
4273
5056
  try {
4274
5057
  await store.verify(
4275
5058
  path,
4276
5059
  id,
4277
- `anchor-resolve: ${note} (regex resolver)`,
5060
+ `anchor-resolve: ${note} (${resolverSummary(results)})`,
4278
5061
  actor,
4279
5062
  now()
4280
5063
  );
@@ -4285,17 +5068,25 @@ var anchorResolveCommand = define({
4285
5068
  results,
4286
5069
  verified: false,
4287
5070
  verifyRefused: "self-verification",
4288
- ...frozenNote
5071
+ ...frozenNote,
5072
+ ...hintNote
4289
5073
  };
4290
5074
  }
4291
- return { conceptId: id, results, verified: true, ...frozenNote };
5075
+ return {
5076
+ conceptId: id,
5077
+ results,
5078
+ verified: true,
5079
+ ...frozenNote,
5080
+ ...hintNote
5081
+ };
4292
5082
  }
4293
5083
  return {
4294
5084
  conceptId: id,
4295
5085
  results,
4296
5086
  verified: false,
4297
5087
  ...unreachable ? { note } : {},
4298
- ...frozenNote
5088
+ ...frozenNote,
5089
+ ...hintNote
4299
5090
  };
4300
5091
  },
4301
5092
  // A stored hash that no longer resolves is a broken anchor, not an absence:
@@ -4311,13 +5102,13 @@ var anchorResolveCommand = define({
4311
5102
  function lineDelta(anchor, current) {
4312
5103
  return anchor.lines === void 0 ? null : Math.abs(current - anchor.lines);
4313
5104
  }
4314
- function headHash(source, anchor) {
5105
+ function headHash(source, anchor, resolvers) {
4315
5106
  if (source.head === void 0) return void 0;
4316
- const resolved = resolveAnchor(source.head, anchor);
4317
- if (!resolved) return void 0;
5107
+ const outcome = resolveAnchorSpan(source.head, anchor, resolvers);
5108
+ if (!outcome.ok) return void 0;
4318
5109
  return {
4319
- hash: hashAnchorText(resolved.text),
4320
- lines: resolved.endLine - resolved.startLine + 1
5110
+ hash: hashAnchorText(outcome.span.text),
5111
+ lines: outcome.span.endLine - outcome.span.startLine + 1
4321
5112
  };
4322
5113
  }
4323
5114
  async function readSources(anchors, root, offline) {
@@ -4367,13 +5158,13 @@ async function readSources(anchors, root, offline) {
4367
5158
  }
4368
5159
 
4369
5160
  // src/commands/answer.ts
4370
- var import_zod9 = require("zod");
5161
+ var import_zod10 = require("zod");
4371
5162
  var answerCommand = define({
4372
5163
  name: "answer",
4373
5164
  tool: "kb_answer",
4374
5165
  usage: "answer <concept-id> <answer...>",
4375
5166
  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.",
4376
- input: import_zod9.z.object({ bundlePath, conceptId, answer: import_zod9.z.string().min(1) }),
5167
+ input: import_zod10.z.object({ bundlePath, conceptId, answer: import_zod10.z.string().min(1) }),
4377
5168
  fromArgv: (argv, path) => ({
4378
5169
  bundlePath: path,
4379
5170
  conceptId: argv[1],
@@ -4387,27 +5178,27 @@ var answerCommand = define({
4387
5178
  });
4388
5179
 
4389
5180
  // src/commands/backlinks.ts
4390
- var import_zod10 = require("zod");
5181
+ var import_zod11 = require("zod");
4391
5182
  var backlinksCommand = define({
4392
5183
  name: "backlinks",
4393
5184
  tool: "kb_backlinks",
4394
5185
  usage: "backlinks <concept-id>",
4395
5186
  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.",
4396
- input: import_zod10.z.object({ bundlePath, conceptId }),
5187
+ input: import_zod11.z.object({ bundlePath, conceptId }),
4397
5188
  fromArgv: (argv, path) => ({ bundlePath: path, conceptId: argv[1] }),
4398
5189
  run: async ({ store }, { bundlePath: path, conceptId: id }) => store.backlinks(path, id)
4399
5190
  });
4400
5191
 
4401
5192
  // src/commands/catalog.ts
4402
- var import_zod11 = require("zod");
5193
+ var import_zod12 = require("zod");
4403
5194
  var catalogCommand = define({
4404
5195
  name: "catalog",
4405
5196
  tool: "kb_catalog",
4406
5197
  usage: "catalog [type]",
4407
5198
  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.",
4408
- input: import_zod11.z.object({
5199
+ input: import_zod12.z.object({
4409
5200
  bundlePath,
4410
- type: import_zod11.z.enum(KB_RECORD_TYPES).optional()
5201
+ type: import_zod12.z.enum(KB_RECORD_TYPES).optional()
4411
5202
  }),
4412
5203
  fromArgv: (argv, path) => ({
4413
5204
  bundlePath: path,
@@ -4462,26 +5253,26 @@ function count(value, noun) {
4462
5253
  }
4463
5254
 
4464
5255
  // src/commands/context.ts
4465
- var import_zod12 = require("zod");
5256
+ var import_zod13 = require("zod");
4466
5257
  var contextCommand = define({
4467
5258
  name: "context",
4468
5259
  tool: "kb_context",
4469
5260
  usage: "context [--profile NAME] [--budget N] [--full-under N] [--format json] [--event NAME]",
4470
5261
  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.",
4471
- input: import_zod12.z.object({
4472
- budgetTokens: import_zod12.z.number().int().positive().optional().describe(
5262
+ input: import_zod13.z.object({
5263
+ budgetTokens: import_zod13.z.number().int().positive().optional().describe(
4473
5264
  "Ceiling on the whole emitted block; past it the command refuses with a list of bases rather than truncating. Defaults to 4000."
4474
5265
  ),
4475
- fullUnderTokens: import_zod12.z.number().int().positive().optional().describe(
5266
+ fullUnderTokens: import_zod13.z.number().int().positive().optional().describe(
4476
5267
  "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."
4477
5268
  ),
4478
- profile: import_zod12.z.string().optional().describe(
5269
+ profile: import_zod13.z.string().optional().describe(
4479
5270
  "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."
4480
5271
  ),
4481
- format: import_zod12.z.enum(["markdown", "json"]).optional().describe(
5272
+ format: import_zod13.z.enum(["markdown", "json"]).optional().describe(
4482
5273
  "CLI envelope for hook protocols that require strict JSON on stdout. MCP callers omit this \u2014 the block itself is identical."
4483
5274
  ),
4484
- event: import_zod12.z.string().optional().describe(
5275
+ event: import_zod13.z.string().optional().describe(
4485
5276
  "hookEventName stamped into the JSON envelope. Only meaningful with format=json."
4486
5277
  )
4487
5278
  }),
@@ -4517,14 +5308,567 @@ var contextCommand = define({
4517
5308
  });
4518
5309
 
4519
5310
  // src/commands/doctor.ts
4520
- var import_zod13 = require("zod");
4521
- var days = (what, fallback) => import_zod13.z.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
5311
+ var import_zod15 = require("zod");
5312
+
5313
+ // src/drift/git.ts
5314
+ var import_node_child_process3 = require("child_process");
5315
+ var import_node_util3 = require("util");
5316
+ var execFileAsync3 = (0, import_node_util3.promisify)(import_node_child_process3.execFile);
5317
+ var MAX_GIT_OUTPUT_BYTES = 1048576;
5318
+ var GIT_TIMEOUT_MS = 5e3;
5319
+ async function git2(cwd, args) {
5320
+ const env = { ...process.env };
5321
+ delete env["GIT_DIR"];
5322
+ delete env["GIT_WORK_TREE"];
5323
+ delete env["GIT_INDEX_FILE"];
5324
+ try {
5325
+ const { stdout } = await execFileAsync3("git", ["-C", cwd, ...args], {
5326
+ timeout: GIT_TIMEOUT_MS,
5327
+ maxBuffer: MAX_GIT_OUTPUT_BYTES,
5328
+ env
5329
+ });
5330
+ return { ok: true, stdout };
5331
+ } catch {
5332
+ return { ok: false };
5333
+ }
5334
+ }
5335
+ async function listRepoFiles(repoRoot) {
5336
+ const result = await git2(repoRoot, ["ls-files", "-z", "--cached"]);
5337
+ if (!result.ok) return [];
5338
+ return result.stdout.split("\0").filter(Boolean);
5339
+ }
5340
+ async function readOldSource(repoRoot, anchor) {
5341
+ if (!filePathIsSafe(anchor.file))
5342
+ return { ok: false, reason: "unrecoverable" };
5343
+ if (anchor.ref && refShapeIsSafe(anchor.ref)) {
5344
+ const shown2 = await showFile(repoRoot, anchor.ref, anchor.file);
5345
+ if (shown2 !== null) {
5346
+ return {
5347
+ ok: true,
5348
+ source: shown2,
5349
+ origin: { kind: "ref", ref: anchor.ref }
5350
+ };
5351
+ }
5352
+ }
5353
+ const at2 = anchor.resolved_at;
5354
+ if (!at2 || Number.isNaN(Date.parse(at2))) {
5355
+ return { ok: false, reason: "unrecoverable" };
5356
+ }
5357
+ const found = await git2(repoRoot, [
5358
+ "log",
5359
+ "-1",
5360
+ "--format=%H",
5361
+ `--before=${at2}`,
5362
+ "--end-of-options",
5363
+ "HEAD",
5364
+ "--",
5365
+ anchor.file
5366
+ ]);
5367
+ const sha = found.ok ? found.stdout.trim() : "";
5368
+ if (!sha || !refShapeIsSafe(sha))
5369
+ return { ok: false, reason: "unrecoverable" };
5370
+ const shown = await showFile(repoRoot, sha, anchor.file);
5371
+ if (shown === null) return { ok: false, reason: "unrecoverable" };
5372
+ return { ok: true, source: shown, origin: { kind: "history", ref: sha } };
5373
+ }
5374
+ async function showFile(repoRoot, ref, file) {
5375
+ const path = file.replace(/^\.\//, "");
5376
+ const result = await git2(repoRoot, [
5377
+ "show",
5378
+ "--end-of-options",
5379
+ `${ref}:${path}`
5380
+ ]);
5381
+ return result.ok ? result.stdout : null;
5382
+ }
5383
+
5384
+ // src/drift/moved.ts
5385
+ var import_promises9 = require("fs/promises");
5386
+ var MAX_MOVED_SEARCH_FILES = 2e3;
5387
+ var SEARCH_BATCH = 64;
5388
+ function movedSearch(repoRoot, options = {}) {
5389
+ const read = options.reader ?? anchorFileReader(repoRoot);
5390
+ const sizeOf = options.sizeOf ?? diskSize(repoRoot);
5391
+ const resolver = new TreeSitterResolver();
5392
+ let repoFiles;
5393
+ const prepared = /* @__PURE__ */ new Set();
5394
+ const filesForLanguage = async (language) => {
5395
+ repoFiles ??= listRepoFiles(repoRoot);
5396
+ return (await repoFiles).filter((file) => languageForFile(file) === language).slice(0, MAX_MOVED_SEARCH_FILES);
5397
+ };
5398
+ return {
5399
+ async find(anchor) {
5400
+ const stored = anchor.hash;
5401
+ if (!stored) return void 0;
5402
+ const language = languageForFile(anchor.file);
5403
+ if (!language) return sameFileWindow(anchor, read, stored);
5404
+ const candidates = await filesForLanguage(language);
5405
+ if (!prepared.has(language)) {
5406
+ await resolver.prepare(candidates.length ? candidates : [anchor.file]);
5407
+ prepared.add(language);
5408
+ }
5409
+ const floor = anchor.lines ?? 0;
5410
+ for (let at2 = 0; at2 < candidates.length; at2 += SEARCH_BATCH) {
5411
+ const batch = candidates.slice(at2, at2 + SEARCH_BATCH);
5412
+ const hits = await mapLimit(
5413
+ batch,
5414
+ DEFAULT_IO_CONCURRENCY,
5415
+ async (file) => {
5416
+ const size2 = await sizeOf(file);
5417
+ if (size2 !== null && size2 < floor) return void 0;
5418
+ return matchIn(resolver, read, anchor, stored, file);
5419
+ }
5420
+ );
5421
+ const found = hits.find((hit) => hit !== void 0);
5422
+ if (found) return found;
5423
+ }
5424
+ return void 0;
5425
+ }
5426
+ };
5427
+ }
5428
+ async function matchIn(resolver, read, anchor, stored, file) {
5429
+ const source = await read(file);
5430
+ if (!source.ok) return void 0;
5431
+ const normalized = source.source.replace(/\r\n/g, "\n");
5432
+ for (const found of resolver.spans(normalized, file)) {
5433
+ const text = anchor.hash_kind === "ast" ? resolver.normalize(found.span.text, file) : found.span.text;
5434
+ if (text === null || hashAnchorText(text) !== stored) continue;
5435
+ if (file === anchor.file && found.symbol === anchor.symbol) continue;
5436
+ return {
5437
+ file,
5438
+ symbol: found.symbol,
5439
+ startLine: found.span.startLine,
5440
+ endLine: found.span.endLine
5441
+ };
5442
+ }
5443
+ return void 0;
5444
+ }
5445
+ function diskSize(repoRoot) {
5446
+ return async (file) => {
5447
+ const path = anchorFilePath(repoRoot, file);
5448
+ if (path === null) return null;
5449
+ try {
5450
+ return (await (0, import_promises9.stat)(path)).size;
5451
+ } catch {
5452
+ return null;
5453
+ }
5454
+ };
5455
+ }
5456
+ async function sameFileWindow(anchor, read, stored) {
5457
+ const height = anchor.lines;
5458
+ if (!height || anchor.hash_kind === "ast") return void 0;
5459
+ const source = await read(anchor.file);
5460
+ if (!source.ok) return void 0;
5461
+ const lines = source.source.replace(/\r\n/g, "\n").split("\n");
5462
+ for (let at2 = 0; at2 + height <= lines.length; at2++) {
5463
+ if (hashAnchorText(lines.slice(at2, at2 + height).join("\n")) !== stored) {
5464
+ continue;
5465
+ }
5466
+ return {
5467
+ file: anchor.file,
5468
+ ...anchor.symbol ? { symbol: anchor.symbol } : {},
5469
+ startLine: at2 + 1,
5470
+ endLine: at2 + height
5471
+ };
5472
+ }
5473
+ return void 0;
5474
+ }
5475
+
5476
+ // src/drift/classify.ts
5477
+ async function classifyDrift(repoRoot, record, entries, options = {}) {
5478
+ const anchors = (record.frontmatter.strauss_anchors ?? []).filter(
5479
+ (anchor) => anchor.hash
5480
+ );
5481
+ const reader = options.reader ?? anchorFileReader(repoRoot);
5482
+ const treeSitter = new TreeSitterResolver();
5483
+ const resolvers = [treeSitter, regexResolver];
5484
+ const search = options.search ?? movedSearch(repoRoot, { ...options.reader ? { reader } : {} });
5485
+ const wanted = [];
5486
+ entries.forEach((entry, at2) => {
5487
+ const anchor = anchors[at2];
5488
+ if (!anchor) return;
5489
+ if (entry.state === "match" || isUncheckedReason(entry.reason)) return;
5490
+ wanted.push({ anchor, entry });
5491
+ });
5492
+ if (!wanted.length) return [];
5493
+ await prepareResolvers(
5494
+ resolvers,
5495
+ wanted.map(({ anchor }) => anchor.file)
5496
+ );
5497
+ const out = [];
5498
+ for (const { anchor, entry } of wanted) {
5499
+ const movedTo = await search.find(anchor);
5500
+ if (movedTo) {
5501
+ out.push({
5502
+ anchor,
5503
+ entry: { ...entry, class: "moved", movedTo },
5504
+ class: "moved"
5505
+ });
5506
+ continue;
5507
+ }
5508
+ const newText = await currentText(reader, anchor, resolvers);
5509
+ const old = options.withHistory === false ? { ok: false, reason: "unrecoverable" } : await readOldSource(repoRoot, anchor);
5510
+ const oldText = old.ok ? spanIn(old.source, anchor, resolvers) : void 0;
5511
+ const settled = newText !== void 0 && oldText !== void 0 && sameTokens(treeSitter, anchor.file, oldText, newText) ? "cosmetic" : entry.class ?? "changed";
5512
+ out.push({
5513
+ anchor,
5514
+ entry: { ...entry, class: settled },
5515
+ class: settled,
5516
+ ...newText !== void 0 ? { newText } : {},
5517
+ ...oldText !== void 0 ? { oldText } : {},
5518
+ ...old.ok ? { oldOrigin: old.origin } : {}
5519
+ });
5520
+ }
5521
+ return out;
5522
+ }
5523
+ function sameTokens(resolver, file, before, after) {
5524
+ if (before === after) return false;
5525
+ const left = resolver.normalize(before, file);
5526
+ const right = resolver.normalize(after, file);
5527
+ return left !== null && left === right;
5528
+ }
5529
+ async function currentText(reader, anchor, resolvers) {
5530
+ const read = await reader(anchor.file);
5531
+ if (!read.ok) return void 0;
5532
+ return spanIn(read.source, anchor, resolvers);
5533
+ }
5534
+ function spanIn(source, anchor, resolvers) {
5535
+ const outcome = resolveAnchorSpan(source, anchor, resolvers);
5536
+ return outcome.ok ? outcome.span.text : void 0;
5537
+ }
5538
+
5539
+ // src/drift/diff.ts
5540
+ var MAX_ANCHOR_DIFF_LINES = 200;
5541
+ var PACKET_DIFF_LINE_BUDGET = 200;
5542
+ var MIN_ANCHOR_DIFF_LINES = 12;
5543
+ function diffBudget(anchors) {
5544
+ if (anchors <= 0) return MAX_ANCHOR_DIFF_LINES;
5545
+ return Math.min(
5546
+ MAX_ANCHOR_DIFF_LINES,
5547
+ Math.max(
5548
+ MIN_ANCHOR_DIFF_LINES,
5549
+ Math.floor(PACKET_DIFF_LINE_BUDGET / anchors)
5550
+ )
5551
+ );
5552
+ }
5553
+ function unifiedDiff(before, after, options = {}) {
5554
+ const max = options.maxLines ?? MAX_ANCHOR_DIFF_LINES;
5555
+ const left = before.replace(/\r\n/g, "\n").split("\n");
5556
+ const right = after.replace(/\r\n/g, "\n").split("\n");
5557
+ const body = [];
5558
+ let added = 0;
5559
+ let removed = 0;
5560
+ for (const edit of edits(left, right)) {
5561
+ if (edit.kind === "same") body.push(` ${edit.line}`);
5562
+ else if (edit.kind === "remove") {
5563
+ body.push(`-${edit.line}`);
5564
+ removed += 1;
5565
+ } else {
5566
+ body.push(`+${edit.line}`);
5567
+ added += 1;
5568
+ }
5569
+ }
5570
+ const truncated = body.length > max;
5571
+ const shown = truncated ? body.slice(0, max) : body;
5572
+ const header = `@@ -1,${left.length} +1,${right.length} @@${options.oldLabel ? ` ${options.oldLabel} \u2192 ${options.newLabel ?? ""}`.trimEnd() : ""}`;
5573
+ const lines = [header, ...shown];
5574
+ if (truncated) lines.push(`\u2026 ${body.length - max} more diff lines`);
5575
+ return { text: lines.join("\n"), added, removed, truncated };
5576
+ }
5577
+ function edits(left, right) {
5578
+ const rows = left.length;
5579
+ const cols = right.length;
5580
+ const table2 = Array.from(
5581
+ { length: rows + 1 },
5582
+ () => new Array(cols + 1).fill(0)
5583
+ );
5584
+ for (let row2 = rows - 1; row2 >= 0; row2--) {
5585
+ for (let col2 = cols - 1; col2 >= 0; col2--) {
5586
+ table2[row2][col2] = left[row2] === right[col2] ? table2[row2 + 1][col2 + 1] + 1 : Math.max(
5587
+ table2[row2 + 1][col2],
5588
+ table2[row2][col2 + 1]
5589
+ );
5590
+ }
5591
+ }
5592
+ const out = [];
5593
+ let row = 0;
5594
+ let col = 0;
5595
+ while (row < rows && col < cols) {
5596
+ if (left[row] === right[col]) {
5597
+ out.push({ kind: "same", line: left[row] });
5598
+ row += 1;
5599
+ col += 1;
5600
+ } else if (table2[row + 1][col] >= table2[row][col + 1]) {
5601
+ out.push({ kind: "remove", line: left[row] });
5602
+ row += 1;
5603
+ } else {
5604
+ out.push({ kind: "add", line: right[col] });
5605
+ col += 1;
5606
+ }
5607
+ }
5608
+ for (; row < rows; row++)
5609
+ out.push({ kind: "remove", line: left[row] });
5610
+ for (; col < cols; col++)
5611
+ out.push({ kind: "add", line: right[col] });
5612
+ return out;
5613
+ }
5614
+
5615
+ // src/drift/packet.ts
5616
+ var PRESUMED_INVALID = [
5617
+ "fact",
5618
+ "constraint",
5619
+ "contract"
5620
+ ];
5621
+ var RATIONALE_SURVIVES = ["decision", "risk"];
5622
+ var DEFAULT_NOTES = {
5623
+ "presumed-invalidated": "the code this claim was taken from changed; presume it no longer holds until re-read",
5624
+ "rationale-may-survive": "the reasoning may outlive the code that implemented it; check whether it does",
5625
+ review: "re-read the record against the new code"
5626
+ };
5627
+ async function reassessPacket(repoRoot, record, entries, options = {}) {
5628
+ const classified = await classifyDrift(repoRoot, record, entries, {
5629
+ ...options.reader ? { reader: options.reader } : {},
5630
+ ...options.search ? { search: options.search } : {},
5631
+ withHistory: options.withDiff !== false
5632
+ });
5633
+ const open = classified.filter(
5634
+ (found) => found.class === "changed" || found.class === "gone"
5635
+ );
5636
+ if (!open.length) return { packet: null, classified };
5637
+ const budget = diffBudget(open.length);
5638
+ const anchors = open.map(
5639
+ (found) => anchorPacket(found, options.withDiff === true, budget)
5640
+ );
5641
+ const type = record.frontmatter.type;
5642
+ const fallback = isKbRecordType(type) ? PRESUMED_INVALID.includes(type) ? "presumed-invalidated" : RATIONALE_SURVIVES.includes(type) ? "rationale-may-survive" : "review" : "review";
5643
+ return {
5644
+ classified,
5645
+ packet: {
5646
+ conceptId: record.conceptId,
5647
+ title: record.frontmatter.title ?? null,
5648
+ type,
5649
+ standing: options.standing ?? "unsettled",
5650
+ why: record.frontmatter.description ?? null,
5651
+ claim: claimOf(record),
5652
+ anchors,
5653
+ impact: (options.impact?.impacted ?? []).map((entry) => ({
5654
+ conceptId: entry.conceptId,
5655
+ title: entry.title,
5656
+ standing: entry.standing,
5657
+ depth: entry.depth
5658
+ })),
5659
+ impactTruncated: options.impact?.truncated ?? false,
5660
+ default: fallback,
5661
+ defaultNote: DEFAULT_NOTES[fallback]
5662
+ }
5663
+ };
5664
+ }
5665
+ function anchorPacket(found, withDiff, maxLines) {
5666
+ const { entry } = found;
5667
+ const base2 = {
5668
+ file: entry.file,
5669
+ ...entry.symbol ? { symbol: entry.symbol } : {},
5670
+ class: found.class,
5671
+ ...entry.reason ? { reason: entry.reason } : {},
5672
+ storedHash: entry.storedHash,
5673
+ ...entry.currentHash ? { currentHash: entry.currentHash } : {},
5674
+ diffSize: entry.diffSize,
5675
+ ...entry.movedTo ? { movedTo: entry.movedTo } : {}
5676
+ };
5677
+ if (!withDiff) return base2;
5678
+ if (found.oldText === void 0 || !found.oldOrigin) {
5679
+ return { ...base2, diff: { status: "unrecoverable" } };
5680
+ }
5681
+ const rendered = unifiedDiff(found.oldText, found.newText ?? "", {
5682
+ maxLines
5683
+ });
5684
+ return {
5685
+ ...base2,
5686
+ diff: {
5687
+ status: "ok",
5688
+ source: found.oldOrigin.kind,
5689
+ ref: found.oldOrigin.ref,
5690
+ unified: rendered.text,
5691
+ added: rendered.added,
5692
+ removed: rendered.removed,
5693
+ truncated: rendered.truncated
5694
+ }
5695
+ };
5696
+ }
5697
+ function claimOf(record) {
5698
+ const type = record.frontmatter.type;
5699
+ const section = isKbRecordType(type) ? RECORD_TYPES[type].sections[0] : void 0;
5700
+ if (!section) return null;
5701
+ const lines = record.body.replace(/\r\n/g, "\n").split("\n");
5702
+ const start = lines.findIndex(
5703
+ (line) => line.trim().toLowerCase() === `## ${section}`.toLowerCase()
5704
+ );
5705
+ if (start < 0) return null;
5706
+ const rest = lines.slice(start + 1);
5707
+ const end = rest.findIndex((line) => line.startsWith("## "));
5708
+ const text = (end < 0 ? rest : rest.slice(0, end)).join("\n").trim();
5709
+ return text ? { section, text } : null;
5710
+ }
5711
+
5712
+ // src/commands/reassess.ts
5713
+ var import_zod14 = require("zod");
5714
+ var reassessCommand = define({
5715
+ name: "reassess",
5716
+ tool: "kb_reassess",
5717
+ usage: "reassess <concept-id> [--repo-root <path>] [--with-diff]",
5718
+ description: "One drifted record, as something to judge: its claim, each anchor's drift class, the old-vs-new span diff, and the records that depend on it. Formatting-only drift is dropped. Empty when there is nothing to reassess. Writes: relocates moved anchors, keeping their hash; never verifies, supersedes, or changes standing.",
5719
+ input: import_zod14.z.object({
5720
+ bundlePath,
5721
+ conceptId,
5722
+ repoRoot: REPO_ROOT,
5723
+ withDiff: import_zod14.z.boolean().optional().describe(
5724
+ "Recover each anchor's committed span and render the diff. Reads git history."
5725
+ )
5726
+ }),
5727
+ fromArgv: (argv, path) => {
5728
+ const repoRoot = argvFlag(argv, "--repo-root");
5729
+ return {
5730
+ bundlePath: path,
5731
+ conceptId: argv[1],
5732
+ ...repoRoot !== void 0 ? { repoRoot } : {},
5733
+ ...argv.includes("--with-diff") ? { withDiff: true } : {}
5734
+ };
5735
+ },
5736
+ run: async ({ store, actor }, { bundlePath: path, conceptId: id, repoRoot, withDiff }) => {
5737
+ const root = repoRoot ?? process.cwd();
5738
+ const bundle = await store.list(path);
5739
+ const record = bundle.find((entry) => entry.conceptId === id);
5740
+ if (!record) throw new KbRecordNotFoundError(id);
5741
+ const drift = await store.detectDrift([record], repoRoot);
5742
+ const entries = drift?.get(id) ?? [];
5743
+ if (!entries.some((entry) => entry.state !== "match")) {
5744
+ return { conceptId: id, packet: null, rebaselined: [], cosmetic: 0 };
5745
+ }
5746
+ const standing = adjudicate(bundle, bundle).find(
5747
+ (hit) => hit.record.conceptId === id
5748
+ )?.standing;
5749
+ const impact2 = await store.impact(path, id);
5750
+ const { packet, classified } = await reassessPacket(root, record, entries, {
5751
+ ...withDiff ? { withDiff: true } : {},
5752
+ impact: impact2,
5753
+ ...standing ? { standing } : {}
5754
+ });
5755
+ const moves = classified.filter((found) => found.class === "moved");
5756
+ let frozen = false;
5757
+ const rebaselined = [];
5758
+ if (moves.length) {
5759
+ const relocated = /* @__PURE__ */ new Map();
5760
+ for (const found of moves) {
5761
+ const to = found.entry.movedTo;
5762
+ if (!to) continue;
5763
+ relocated.set(found.anchor, {
5764
+ ...found.anchor,
5765
+ file: to.file,
5766
+ ...to.symbol ? { symbol: to.symbol } : {}
5767
+ });
5768
+ rebaselined.push({
5769
+ file: found.anchor.file,
5770
+ ...found.anchor.symbol ? { symbol: found.anchor.symbol } : {},
5771
+ toFile: to.file,
5772
+ ...to.symbol ? { toSymbol: to.symbol } : {}
5773
+ });
5774
+ }
5775
+ try {
5776
+ await assertBaseNotFrozen(process.cwd(), path);
5777
+ } catch (error) {
5778
+ if (!(error instanceof KbBaseFrozenError)) throw error;
5779
+ frozen = true;
5780
+ }
5781
+ if (!frozen) {
5782
+ await store.updateAnchors(
5783
+ path,
5784
+ id,
5785
+ (record.frontmatter.strauss_anchors ?? []).map(
5786
+ (anchor) => relocated.get(anchor) ?? anchor
5787
+ ),
5788
+ actor
5789
+ );
5790
+ }
5791
+ }
5792
+ return {
5793
+ conceptId: id,
5794
+ packet,
5795
+ rebaselined: frozen ? [] : rebaselined,
5796
+ cosmetic: classified.filter((found) => found.class === "cosmetic").length,
5797
+ ...frozen ? {
5798
+ frozen: true,
5799
+ note: "base is frozen: nothing was rebaselined"
5800
+ } : {}
5801
+ };
5802
+ },
5803
+ render: (result) => renderReassess(result)
5804
+ });
5805
+ function renderReassess(result) {
5806
+ const lines = [];
5807
+ for (const move of result.rebaselined) {
5808
+ lines.push(
5809
+ `rebaselined: ${at(move.file, move.symbol)} \u2192 ${at(move.toFile, move.toSymbol)} (same code, new address)`
5810
+ );
5811
+ }
5812
+ if (result.cosmetic) {
5813
+ lines.push(
5814
+ `${result.cosmetic} anchor${result.cosmetic === 1 ? "" : "s"} changed formatting only.`
5815
+ );
5816
+ }
5817
+ if (result.note) lines.push(result.note);
5818
+ const packet = result.packet;
5819
+ if (!packet) {
5820
+ lines.push(`${result.conceptId}: nothing to reassess.`);
5821
+ return lines.join("\n");
5822
+ }
5823
+ lines.push(
5824
+ "",
5825
+ `# ${packet.conceptId}${packet.title ? ` \u2014 ${packet.title}` : ""}`,
5826
+ `type: ${packet.type} standing: ${packet.standing}`,
5827
+ ...packet.why ? [`why: ${packet.why}`] : [],
5828
+ ...packet.claim ? ["", `## ${packet.claim.section}`, packet.claim.text] : [],
5829
+ "",
5830
+ `## Anchors (${packet.anchors.length})`
5831
+ );
5832
+ for (const anchor of packet.anchors) {
5833
+ lines.push(
5834
+ `- ${at(anchor.file, anchor.symbol)} \u2014 ${anchor.class}${anchor.reason ? ` (${anchor.reason})` : ""}`
5835
+ );
5836
+ if (!anchor.diff) continue;
5837
+ if (anchor.diff.status === "unrecoverable") {
5838
+ lines.push(
5839
+ " diff: unrecoverable \u2014 no committed span to compare against"
5840
+ );
5841
+ continue;
5842
+ }
5843
+ lines.push(
5844
+ ` diff vs ${anchor.diff.ref} (${anchor.diff.source}): +${anchor.diff.added} \u2212${anchor.diff.removed}`,
5845
+ ...anchor.diff.unified.split("\n").map((line) => ` ${line}`)
5846
+ );
5847
+ }
5848
+ if (packet.impact.length) {
5849
+ lines.push("", `## Impact (${packet.impact.length})`);
5850
+ for (const entry of packet.impact) {
5851
+ lines.push(
5852
+ `- ${entry.conceptId} [${entry.standing}]${entry.title ? ` \u2014 ${entry.title}` : ""}`
5853
+ );
5854
+ }
5855
+ if (packet.impactTruncated) lines.push("- \u2026 walk truncated");
5856
+ }
5857
+ lines.push("", `Default: ${packet.default} \u2014 ${packet.defaultNote}.`);
5858
+ return lines.join("\n");
5859
+ }
5860
+ function at(file, symbol) {
5861
+ return symbol ? `${file}:${symbol}` : file;
5862
+ }
5863
+
5864
+ // src/commands/doctor.ts
5865
+ var days = (what, fallback) => import_zod15.z.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
4522
5866
  var doctorCommand = define({
4523
5867
  name: "doctor",
4524
5868
  tool: "kb_doctor",
4525
- usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--offline] [--strict]",
4526
- 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.",
4527
- input: import_zod13.z.object({
5869
+ usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--offline] [--strict] [--drifted [--with-diff]]",
5870
+ 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. `drifted` narrows it to a reassessment packet per drifted record, `with_diff` adding each anchor's old-vs-new span.",
5871
+ input: import_zod15.z.object({
4528
5872
  bundlePath,
4529
5873
  repoRoot: REPO_ROOT,
4530
5874
  expiringDays: days(
@@ -4539,11 +5883,17 @@ var doctorCommand = define({
4539
5883
  "How long a record may stay `open` or `proposed` before `aging` reports it, in days.",
4540
5884
  DEFAULT_AGING_DAYS
4541
5885
  ),
4542
- offline: import_zod13.z.boolean().optional().describe(
5886
+ offline: import_zod15.z.boolean().optional().describe(
4543
5887
  "Read foreign anchors from the local repo cache only, never fetching."
4544
5888
  ),
4545
- strict: import_zod13.z.boolean().optional().describe(
5889
+ strict: import_zod15.z.boolean().optional().describe(
4546
5890
  "Turn an expired record into a non-zero exit for the CLI. No effect on the report itself."
5891
+ ),
5892
+ drifted: import_zod15.z.boolean().optional().describe(
5893
+ "Report only drift, as a reassessment packet per record: claim, per-anchor class, and what depends on it."
5894
+ ),
5895
+ withDiff: import_zod15.z.boolean().optional().describe(
5896
+ "With `drifted`: recover each anchor's committed span and render the old-vs-new diff. Reads git history."
4547
5897
  )
4548
5898
  }),
4549
5899
  // Presence, not truthiness: `--expiring-days ""` is a caller who meant
@@ -4562,7 +5912,9 @@ var doctorCommand = define({
4562
5912
  ...unverified2 !== void 0 ? { unverifiedDays: Number(unverified2) } : {},
4563
5913
  ...agingDays !== void 0 ? { agingDays: Number(agingDays) } : {},
4564
5914
  ...argv.includes("--offline") ? { offline: true } : {},
4565
- ...argv.includes("--strict") ? { strict: true } : {}
5915
+ ...argv.includes("--strict") ? { strict: true } : {},
5916
+ ...argv.includes("--drifted") ? { drifted: true } : {},
5917
+ ...argv.includes("--with-diff") ? { withDiff: true } : {}
4566
5918
  };
4567
5919
  },
4568
5920
  run: async ({ store, now }, {
@@ -4571,7 +5923,9 @@ var doctorCommand = define({
4571
5923
  unverifiedDays,
4572
5924
  agingDays,
4573
5925
  repoRoot,
4574
- offline
5926
+ offline,
5927
+ drifted: drifted2,
5928
+ withDiff
4575
5929
  }) => {
4576
5930
  const checkedAt = now();
4577
5931
  const records = await store.list(path);
@@ -4585,7 +5939,54 @@ var doctorCommand = define({
4585
5939
  ...anchorDrift !== void 0 ? { anchorDrift } : {},
4586
5940
  now: new Date(checkedAt)
4587
5941
  });
4588
- return { bundlePath: path, checkedAt, ...report };
5942
+ const hints = grammarHints();
5943
+ if (!drifted2) {
5944
+ return {
5945
+ bundlePath: path,
5946
+ checkedAt,
5947
+ ...report,
5948
+ ...hints.length ? { hints } : {}
5949
+ };
5950
+ }
5951
+ const standings = new Map(
5952
+ adjudicate(records, records, new Date(checkedAt)).map((hit) => [
5953
+ hit.record.conceptId,
5954
+ hit.standing
5955
+ ])
5956
+ );
5957
+ const packets = [];
5958
+ const rebaselinable = [];
5959
+ const search = movedSearch(repoRoot ?? process.cwd());
5960
+ for (const found of report.groups.find((g) => g.check === "drifted")?.findings ?? []) {
5961
+ const record = records.find(
5962
+ (entry) => entry.conceptId === found.conceptId
5963
+ );
5964
+ if (!record) continue;
5965
+ const standing = standings.get(record.conceptId);
5966
+ const built = await reassessPacket(
5967
+ repoRoot ?? process.cwd(),
5968
+ record,
5969
+ anchorDrift?.get(record.conceptId) ?? [],
5970
+ {
5971
+ ...withDiff ? { withDiff: true } : {},
5972
+ impact: await store.impact(path, record.conceptId),
5973
+ ...standing ? { standing } : {},
5974
+ search
5975
+ }
5976
+ );
5977
+ if (built.packet) packets.push(built.packet);
5978
+ if (built.classified.some((entry) => entry.class === "moved")) {
5979
+ rebaselinable.push(record.conceptId);
5980
+ }
5981
+ }
5982
+ return {
5983
+ bundlePath: path,
5984
+ checkedAt,
5985
+ ...report,
5986
+ packets,
5987
+ rebaselinable,
5988
+ ...hints.length ? { hints } : {}
5989
+ };
4589
5990
  },
4590
5991
  render: (result) => render2(result),
4591
5992
  // Only expiry, and only under --strict. The other seven checks report debt a
@@ -4597,18 +5998,22 @@ var doctorCommand = define({
4597
5998
  failsWhen: (result, input) => input.strict === true && result.counts.expired > 0
4598
5999
  });
4599
6000
  function render2(result) {
6001
+ if (result.packets) return renderPackets(result);
4600
6002
  const { thresholds } = result;
4601
6003
  const lines = [
4602
6004
  `# KB Doctor \u2014 ${result.bundlePath}`,
4603
6005
  `records: ${result.recordCount}`,
4604
6006
  `thresholds: expiring within ${thresholds.expiringDays}d, unverified over ${thresholds.unverifiedDays}d, aging over ${thresholds.agingDays}d`,
4605
6007
  `checked: ${result.checkedAt}`,
6008
+ ...result.anchorResolvers.total ? [
6009
+ `anchors: ${result.anchorResolvers.total} hashed \u2014 ${result.anchorResolvers.treeSitter} tree-sitter, ${result.anchorResolvers.regex} regex`
6010
+ ] : [],
4606
6011
  ""
4607
6012
  ];
4608
- const width = Math.max(...result.groups.map((group2) => group2.check.length));
6013
+ const width2 = Math.max(...result.groups.map((group2) => group2.check.length));
4609
6014
  for (const group2 of result.groups) {
4610
6015
  lines.push(
4611
- ` ${group2.check.padEnd(width)} ${String(group2.count).padStart(3)} ${group2.headline}`
6016
+ ` ${group2.check.padEnd(width2)} ${String(group2.count).padStart(3)} ${group2.headline}`
4612
6017
  );
4613
6018
  }
4614
6019
  for (const group2 of result.groups) {
@@ -4620,27 +6025,52 @@ function render2(result) {
4620
6025
  );
4621
6026
  }
4622
6027
  }
6028
+ for (const hint of result.hints ?? []) lines.push("", hint);
4623
6029
  lines.push(
4624
6030
  "",
4625
6031
  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.`
4626
6032
  );
4627
6033
  return lines.join("\n");
4628
6034
  }
6035
+ function renderPackets(result) {
6036
+ const packets = result.packets ?? [];
6037
+ const lines = [
6038
+ `# KB Drift \u2014 ${result.bundlePath}`,
6039
+ `checked: ${result.checkedAt}`,
6040
+ `${packets.length} record${packets.length === 1 ? "" : "s"} need a reading; ${result.counts.drifted} drifted in all.`
6041
+ ];
6042
+ if (result.rebaselinable?.length) {
6043
+ lines.push(
6044
+ `moved, rebaseline with \`kb_reassess\`: ${result.rebaselinable.join(", ")}`
6045
+ );
6046
+ }
6047
+ for (const packet of packets) {
6048
+ lines.push(
6049
+ renderReassess({
6050
+ conceptId: packet.conceptId,
6051
+ packet,
6052
+ rebaselined: [],
6053
+ cosmetic: 0
6054
+ })
6055
+ );
6056
+ }
6057
+ return lines.join("\n");
6058
+ }
4629
6059
 
4630
6060
  // src/commands/impact.ts
4631
- var import_zod14 = require("zod");
6061
+ var import_zod16 = require("zod");
4632
6062
  var impactCommand = define({
4633
6063
  name: "impact",
4634
6064
  tool: "kb_impact",
4635
6065
  usage: "impact <concept-id> [--depth N] [--rels a,b]",
4636
6066
  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.",
4637
- input: import_zod14.z.object({
6067
+ input: import_zod16.z.object({
4638
6068
  bundlePath,
4639
6069
  conceptId,
4640
- depth: import_zod14.z.number().int().positive().optional().describe(
6070
+ depth: import_zod16.z.number().int().positive().optional().describe(
4641
6071
  "Hops out from the record. Unbounded when omitted; a walk this cuts reports truncated: true."
4642
6072
  ),
4643
- rels: import_zod14.z.array(import_zod14.z.enum(KB_CAUSAL_LINK_RELS)).optional().describe(
6073
+ rels: import_zod16.z.array(import_zod16.z.enum(KB_CAUSAL_LINK_RELS)).optional().describe(
4644
6074
  "Narrow which rels the walk follows. Defaults to every rel that carries a dependence \u2014 all but related_to."
4645
6075
  )
4646
6076
  }),
@@ -4661,13 +6091,13 @@ var impactCommand = define({
4661
6091
  });
4662
6092
 
4663
6093
  // src/commands/list.ts
4664
- var import_zod15 = require("zod");
6094
+ var import_zod17 = require("zod");
4665
6095
  var listCommand = define({
4666
6096
  name: "list",
4667
6097
  tool: "kb_list",
4668
6098
  usage: "list [type]",
4669
6099
  description: "Every record, optionally one type. For enumerating; use kb_query for a question.",
4670
- input: import_zod15.z.object({ bundlePath, type: import_zod15.z.enum(KB_RECORD_TYPES).optional() }),
6100
+ input: import_zod17.z.object({ bundlePath, type: import_zod17.z.enum(KB_RECORD_TYPES).optional() }),
4671
6101
  fromArgv: (argv, path) => ({ bundlePath: path, type: argv[1] }),
4672
6102
  run: async ({ store }, { bundlePath: path, type }) => (await store.list(path, type)).map((record) => ({
4673
6103
  conceptId: record.conceptId,
@@ -4679,17 +6109,17 @@ var listCommand = define({
4679
6109
  });
4680
6110
 
4681
6111
  // src/commands/load.ts
4682
- var import_zod16 = require("zod");
6112
+ var import_zod18 = require("zod");
4683
6113
  var loadCommand = define({
4684
6114
  name: "load",
4685
6115
  tool: "kb_load",
4686
6116
  usage: "load [type] [--budget N | --all] [--repo-root PATH]",
4687
6117
  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.",
4688
- input: import_zod16.z.object({
6118
+ input: import_zod18.z.object({
4689
6119
  bundlePath,
4690
- type: import_zod16.z.enum(KB_RECORD_TYPES).optional(),
4691
- budgetTokens: import_zod16.z.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
4692
- all: import_zod16.z.boolean().optional().describe(
6120
+ type: import_zod18.z.enum(KB_RECORD_TYPES).optional(),
6121
+ budgetTokens: import_zod18.z.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
6122
+ all: import_zod18.z.boolean().optional().describe(
4693
6123
  "Loads the entire base regardless of size, bypassing the token budget; mutually exclusive with budgetTokens."
4694
6124
  ),
4695
6125
  repoRoot: REPO_ROOT
@@ -4731,25 +6161,25 @@ var loadCommand = define({
4731
6161
  });
4732
6162
 
4733
6163
  // src/commands/log.ts
4734
- var import_zod17 = require("zod");
6164
+ var import_zod19 = require("zod");
4735
6165
  var logCommand = define({
4736
6166
  name: "log",
4737
6167
  tool: "kb_log",
4738
6168
  usage: "log",
4739
6169
  description: "Who touched what, and when. Append-only; malformed lines are reported, never repaired.",
4740
- input: import_zod17.z.object({ bundlePath }),
6170
+ input: import_zod19.z.object({ bundlePath }),
4741
6171
  fromArgv: (_argv, path) => ({ bundlePath: path }),
4742
6172
  run: ({ store }, { bundlePath: path }) => store.readLog(path)
4743
6173
  });
4744
6174
 
4745
6175
  // src/commands/no-decision.ts
4746
- var import_zod18 = require("zod");
6176
+ var import_zod20 = require("zod");
4747
6177
  var noDecisionCommand = define({
4748
6178
  name: "no-decision",
4749
6179
  tool: "kb_no_decision",
4750
6180
  usage: "no-decision <reason...>",
4751
6181
  description: "Record in one sentence that a piece of work had nothing to decide. Idempotent.",
4752
- input: import_zod18.z.object({ bundlePath, reason: import_zod18.z.string().min(1) }),
6182
+ input: import_zod20.z.object({ bundlePath, reason: import_zod20.z.string().min(1) }),
4753
6183
  fromArgv: (argv, path) => ({
4754
6184
  bundlePath: path,
4755
6185
  reason: argv.slice(1).join(" ").trim()
@@ -4766,20 +6196,20 @@ var noDecisionCommand = define({
4766
6196
  });
4767
6197
 
4768
6198
  // src/commands/pack.ts
4769
- var import_zod19 = require("zod");
6199
+ var import_zod21 = require("zod");
4770
6200
  var packCommand = define({
4771
6201
  name: "pack",
4772
6202
  tool: "kb_pack",
4773
6203
  usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
4774
6204
  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.",
4775
- input: import_zod19.z.object({
6205
+ input: import_zod21.z.object({
4776
6206
  bundlePath,
4777
6207
  conceptId,
4778
- hops: import_zod19.z.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
4779
- maxNodes: import_zod19.z.number().int().positive().optional().describe(
6208
+ hops: import_zod21.z.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
6209
+ maxNodes: import_zod21.z.number().int().positive().optional().describe(
4780
6210
  "How many records the pack may hold, root included. Defaults to 20."
4781
6211
  ),
4782
- budgetTokens: import_zod19.z.number().int().positive().optional().describe(
6212
+ budgetTokens: import_zod21.z.number().int().positive().optional().describe(
4783
6213
  "Approximate token ceiling over what is actually emitted. Defaults to 25000."
4784
6214
  )
4785
6215
  }),
@@ -4804,12 +6234,12 @@ var packCommand = define({
4804
6234
  return render3(result, path, now());
4805
6235
  }
4806
6236
  });
4807
- function render3(result, bundle, at) {
6237
+ function render3(result, bundle, at2) {
4808
6238
  const lines = [
4809
6239
  `# KB Pack \u2014 ${result.root}`,
4810
6240
  `bundle: ${bundle}`,
4811
6241
  `budget: ~${result.tokensLoaded} of ${result.budgetTokens} tokens, ${result.recordCount} records`,
4812
- `packed: ${at}`,
6242
+ `packed: ${at2}`,
4813
6243
  "",
4814
6244
  `## Records (${result.records.length})`
4815
6245
  ];
@@ -4866,22 +6296,22 @@ function warningLabel(warning) {
4866
6296
  }
4867
6297
 
4868
6298
  // src/commands/pin.ts
4869
- var import_zod20 = require("zod");
6299
+ var import_zod22 = require("zod");
4870
6300
  var pinCommand = define({
4871
6301
  name: "pin",
4872
6302
  tool: "kb_pin",
4873
6303
  usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
4874
6304
  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.",
4875
- input: import_zod20.z.object({
6305
+ input: import_zod22.z.object({
4876
6306
  bundlePath,
4877
- mode: import_zod20.z.enum(["full", "index"]).optional().describe(
6307
+ mode: import_zod22.z.enum(["full", "index"]).optional().describe(
4878
6308
  "full: always emit this base's records whole (still under the block budget); index: never upgrade. Absent: the profile's full-under threshold decides."
4879
6309
  ),
4880
- profiles: import_zod20.z.array(import_zod20.z.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
4881
- layer: import_zod20.z.enum(["project", "local", "user"]).optional().describe(
6310
+ profiles: import_zod22.z.array(import_zod22.z.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
6311
+ layer: import_zod22.z.enum(["project", "local", "user"]).optional().describe(
4882
6312
  "Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
4883
6313
  ),
4884
- frozen: import_zod20.z.boolean().optional().describe(
6314
+ frozen: import_zod22.z.boolean().optional().describe(
4885
6315
  "true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
4886
6316
  )
4887
6317
  }),
@@ -4910,29 +6340,29 @@ var pinCommand = define({
4910
6340
  });
4911
6341
 
4912
6342
  // src/commands/pins.ts
4913
- var import_zod21 = require("zod");
6343
+ var import_zod23 = require("zod");
4914
6344
  var pinsCommand = define({
4915
6345
  name: "pins",
4916
6346
  tool: "kb_pins",
4917
6347
  usage: "pins",
4918
6348
  description: "Every pinned base across the manifest layers, with its layer and whether it resolves to records. Takes no bundlePath.",
4919
- input: import_zod21.z.object({}),
6349
+ input: import_zod23.z.object({}),
4920
6350
  fromArgv: () => ({}),
4921
6351
  run: ({ store }) => listPins(store, process.cwd())
4922
6352
  });
4923
6353
 
4924
6354
  // src/commands/query.ts
4925
- var import_zod22 = require("zod");
6355
+ var import_zod24 = require("zod");
4926
6356
  var queryCommand = define({
4927
6357
  name: "query",
4928
6358
  tool: "kb_query",
4929
6359
  usage: "query <text...> [--repo-root PATH]",
4930
6360
  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.",
4931
- input: import_zod22.z.object({
6361
+ input: import_zod24.z.object({
4932
6362
  bundlePath,
4933
- text: import_zod22.z.string().optional(),
4934
- type: import_zod22.z.enum(KB_RECORD_TYPES).optional(),
4935
- includeNonCurrent: import_zod22.z.boolean().optional(),
6363
+ text: import_zod24.z.string().optional(),
6364
+ type: import_zod24.z.enum(KB_RECORD_TYPES).optional(),
6365
+ includeNonCurrent: import_zod24.z.boolean().optional(),
4936
6366
  repoRoot: REPO_ROOT
4937
6367
  }),
4938
6368
  // `--repo-root` is a flag, so its value must not fall into the search text.
@@ -4964,43 +6394,43 @@ var queryCommand = define({
4964
6394
  });
4965
6395
 
4966
6396
  // src/commands/read-index.ts
4967
- var import_zod23 = require("zod");
6397
+ var import_zod25 = require("zod");
4968
6398
  var readIndexCommand = define({
4969
6399
  name: "index",
4970
6400
  tool: "kb_index",
4971
6401
  usage: "index",
4972
6402
  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.",
4973
- input: import_zod23.z.object({ bundlePath }),
6403
+ input: import_zod25.z.object({ bundlePath }),
4974
6404
  fromArgv: (_argv, path) => ({ bundlePath: path }),
4975
6405
  run: ({ store }, { bundlePath: path }) => store.readIndex(path)
4976
6406
  });
4977
6407
 
4978
6408
  // src/commands/schema.ts
4979
- var import_zod24 = require("zod");
6409
+ var import_zod26 = require("zod");
4980
6410
  var schemaCommand = define({
4981
6411
  name: "schema",
4982
6412
  tool: "kb_schema",
4983
6413
  usage: "schema",
4984
6414
  description: "JSON Schema for frontmatter, write input, and log entries, generated from the enforcing code.",
4985
- input: import_zod24.z.object({}),
6415
+ input: import_zod26.z.object({}),
4986
6416
  fromArgv: () => ({}),
4987
6417
  run: () => Promise.resolve(kbJsonSchemas())
4988
6418
  });
4989
6419
 
4990
6420
  // src/commands/stamp.ts
4991
- var import_promises7 = require("fs/promises");
4992
- var import_zod25 = require("zod");
6421
+ var import_promises10 = require("fs/promises");
6422
+ var import_zod27 = require("zod");
4993
6423
  var DIGEST = /^[0-9a-f]{64}$/;
4994
6424
  var stampCommand = define({
4995
6425
  name: "stamp",
4996
6426
  tool: "kb_stamp",
4997
6427
  usage: "stamp [--bundle PATH] [--since DIGEST|FILE]",
4998
- 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.",
4999
- input: import_zod25.z.object({
5000
- bundlePath: import_zod25.z.string().min(1).optional().describe(
6428
+ description: "Content stamp of a base \u2014 `load`'s digest, record counts, per-record digests, how many records have drifted anchors \u2014 without any bodies. Takes no bundlePath to stamp every pinned base. With `since`, reports only the bases that moved, naming the changed ids. Reads, never writes.",
6429
+ input: import_zod27.z.object({
6430
+ bundlePath: import_zod27.z.string().min(1).optional().describe(
5001
6431
  "Absolute path to one knowledge base. Omit to stamp every pinned base."
5002
6432
  ),
5003
- since: import_zod25.z.string().min(1).optional().describe(
6433
+ since: import_zod27.z.string().min(1).optional().describe(
5004
6434
  "Prior digest, or path to a prior `stamp --json`; only moved bases return, with changed ids when the baseline is a file."
5005
6435
  )
5006
6436
  }),
@@ -5039,7 +6469,7 @@ var stampCommand = define({
5039
6469
  return reports;
5040
6470
  },
5041
6471
  render: (result) => result.map((report) => {
5042
- const counts = `${report.recordCount} record(s), ${report.superseded} superseded`;
6472
+ const counts = `${report.recordCount} record(s), ${report.superseded} superseded${report.drifted ? `, ${report.drifted} drifted` : ""}`;
5043
6473
  const head = `${report.path} ${report.digest} ${counts}${report.newestAt ? ` newest ${report.newestAt}` : ""}`;
5044
6474
  return report.changed?.length ? `${head}
5045
6475
  changed: ${report.changed.join(", ")}` : head;
@@ -5062,7 +6492,7 @@ async function readBaseline(since) {
5062
6492
  if (DIGEST.test(since)) return { digest: since, byPath: /* @__PURE__ */ new Map() };
5063
6493
  let parsed;
5064
6494
  try {
5065
- parsed = JSON.parse(await (0, import_promises7.readFile)(since, "utf8"));
6495
+ parsed = JSON.parse(await (0, import_promises10.readFile)(since, "utf8"));
5066
6496
  } catch {
5067
6497
  throw new KbStampBaselineError(since);
5068
6498
  }
@@ -5086,16 +6516,16 @@ async function readBaseline(since) {
5086
6516
  }
5087
6517
 
5088
6518
  // src/commands/status.ts
5089
- var import_zod26 = require("zod");
6519
+ var import_zod28 = require("zod");
5090
6520
  var statusCommand = define({
5091
6521
  name: "status",
5092
6522
  tool: "kb_status",
5093
6523
  usage: "status <concept-id> <status>",
5094
6524
  description: "Move a record's status. Compare-and-swap: a concurrent change fails instead of being overwritten.",
5095
- input: import_zod26.z.object({
6525
+ input: import_zod28.z.object({
5096
6526
  bundlePath,
5097
6527
  conceptId,
5098
- status: import_zod26.z.enum(KB_RECORD_STATUSES)
6528
+ status: import_zod28.z.enum(KB_RECORD_STATUSES)
5099
6529
  }),
5100
6530
  fromArgv: (argv, path) => ({
5101
6531
  bundlePath: path,
@@ -5110,13 +6540,13 @@ var statusCommand = define({
5110
6540
  });
5111
6541
 
5112
6542
  // src/commands/supersede.ts
5113
- var import_zod27 = require("zod");
6543
+ var import_zod29 = require("zod");
5114
6544
  var supersedeCommand = define({
5115
6545
  name: "supersede",
5116
6546
  tool: "kb_supersede",
5117
6547
  usage: "supersede <concept-id> <replacement-id>",
5118
6548
  description: "Mark a record superseded by another, linked in both directions. Use instead of editing a record whose meaning changed.",
5119
- input: import_zod27.z.object({ bundlePath, conceptId, replacementId: conceptId }),
6549
+ input: import_zod29.z.object({ bundlePath, conceptId, replacementId: conceptId }),
5120
6550
  fromArgv: (argv, path) => ({
5121
6551
  bundlePath: path,
5122
6552
  conceptId: argv[1],
@@ -5130,16 +6560,16 @@ var supersedeCommand = define({
5130
6560
  });
5131
6561
 
5132
6562
  // src/commands/sync-instructions.ts
5133
- var import_zod28 = require("zod");
6563
+ var import_zod30 = require("zod");
5134
6564
  var syncInstructionsCommand = define({
5135
6565
  name: "sync-instructions",
5136
6566
  usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
5137
6567
  description: "CLI-only: plant the kb_context block between sentinel comments in AGENTS.md or CLAUDE.md, idempotently.",
5138
- input: import_zod28.z.object({
5139
- file: import_zod28.z.string().min(1).describe("The instruction file to edit in place."),
5140
- budgetTokens: import_zod28.z.number().int().positive().optional(),
5141
- fullUnderTokens: import_zod28.z.number().int().positive().optional(),
5142
- profile: import_zod28.z.string().optional()
6568
+ input: import_zod30.z.object({
6569
+ file: import_zod30.z.string().min(1).describe("The instruction file to edit in place."),
6570
+ budgetTokens: import_zod30.z.number().int().positive().optional(),
6571
+ fullUnderTokens: import_zod30.z.number().int().positive().optional(),
6572
+ profile: import_zod30.z.string().optional()
5143
6573
  }),
5144
6574
  fromArgv: (argv) => {
5145
6575
  const budget = argvFlag(argv, "--budget");
@@ -5165,17 +6595,17 @@ var syncInstructionsCommand = define({
5165
6595
  });
5166
6596
 
5167
6597
  // src/commands/trace.ts
5168
- var import_zod29 = require("zod");
6598
+ var import_zod31 = require("zod");
5169
6599
  var traceCommand = define({
5170
6600
  name: "trace",
5171
6601
  tool: "kb_trace",
5172
6602
  usage: "trace <concept-id> [edges...]",
5173
6603
  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".',
5174
- input: import_zod29.z.object({
6604
+ input: import_zod31.z.object({
5175
6605
  bundlePath,
5176
6606
  conceptId,
5177
- edges: import_zod29.z.array(import_zod29.z.enum(TRACE_EDGES)).optional(),
5178
- depth: import_zod29.z.number().int().positive().optional()
6607
+ edges: import_zod31.z.array(import_zod31.z.enum(TRACE_EDGES)).optional(),
6608
+ depth: import_zod31.z.number().int().positive().optional()
5179
6609
  }),
5180
6610
  fromArgv: (argv, path) => ({
5181
6611
  bundlePath: path,
@@ -5197,37 +6627,37 @@ var traceCommand = define({
5197
6627
  });
5198
6628
 
5199
6629
  // src/commands/types.ts
5200
- var import_zod30 = require("zod");
6630
+ var import_zod32 = require("zod");
5201
6631
  var typesCommand = define({
5202
6632
  name: "types",
5203
6633
  tool: "kb_types",
5204
6634
  usage: "types",
5205
6635
  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.",
5206
- input: import_zod30.z.object({}),
6636
+ input: import_zod32.z.object({}),
5207
6637
  fromArgv: () => ({}),
5208
6638
  run: () => Promise.resolve(RECORD_TYPES)
5209
6639
  });
5210
6640
 
5211
6641
  // src/commands/unpin.ts
5212
- var import_zod31 = require("zod");
6642
+ var import_zod33 = require("zod");
5213
6643
  var unpinCommand = define({
5214
6644
  name: "unpin",
5215
6645
  tool: "kb_unpin",
5216
6646
  usage: "unpin [bundle-path]",
5217
6647
  description: "Remove a base from every manifest layer that holds it. Reports the layers touched.",
5218
- input: import_zod31.z.object({ bundlePath }),
6648
+ input: import_zod33.z.object({ bundlePath }),
5219
6649
  fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
5220
6650
  run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
5221
6651
  });
5222
6652
 
5223
6653
  // src/commands/validate.ts
5224
- var import_zod32 = require("zod");
6654
+ var import_zod34 = require("zod");
5225
6655
  var validateCommand = define({
5226
6656
  name: "validate",
5227
6657
  tool: "kb_validate",
5228
6658
  usage: "validate",
5229
6659
  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.",
5230
- input: import_zod32.z.object({ bundlePath }),
6660
+ input: import_zod34.z.object({ bundlePath }),
5231
6661
  fromArgv: (_argv, path) => ({ bundlePath: path }),
5232
6662
  run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
5233
6663
  // Warnings never fail the exit code; every other severity does.
@@ -5237,16 +6667,16 @@ var validateCommand = define({
5237
6667
  });
5238
6668
 
5239
6669
  // src/commands/verify.ts
5240
- var import_zod33 = require("zod");
6670
+ var import_zod35 = require("zod");
5241
6671
  var verifyCommand = define({
5242
6672
  name: "verify",
5243
6673
  tool: "kb_verify",
5244
6674
  usage: "verify <concept-id> --note <text>",
5245
6675
  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.",
5246
- input: import_zod33.z.object({
6676
+ input: import_zod35.z.object({
5247
6677
  bundlePath,
5248
6678
  conceptId,
5249
- note: import_zod33.z.string().refine((s) => s.trim().length > 0, {
6679
+ note: import_zod35.z.string().refine((s) => s.trim().length > 0, {
5250
6680
  message: "note must say what the check found"
5251
6681
  })
5252
6682
  }),
@@ -5266,15 +6696,15 @@ var verifyCommand = define({
5266
6696
  });
5267
6697
 
5268
6698
  // src/commands/write.ts
5269
- var import_zod34 = require("zod");
6699
+ var import_zod36 = require("zod");
5270
6700
  var writeCommand = define({
5271
6701
  name: "write",
5272
6702
  tool: "kb_write",
5273
6703
  usage: "write <type> < record.json",
5274
6704
  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.",
5275
- input: import_zod34.z.object({
6705
+ input: import_zod36.z.object({
5276
6706
  bundlePath,
5277
- type: import_zod34.z.enum(KB_RECORD_TYPES),
6707
+ type: import_zod36.z.enum(KB_RECORD_TYPES),
5278
6708
  input: composeInputSchema
5279
6709
  }),
5280
6710
  fromArgv: async (argv, path, stdin) => ({
@@ -5298,13 +6728,13 @@ var writeCommand = define({
5298
6728
  });
5299
6729
 
5300
6730
  // src/commands/write-decision.ts
5301
- var import_zod35 = require("zod");
6731
+ var import_zod37 = require("zod");
5302
6732
  var writeDecisionCommand = define({
5303
6733
  name: "write-decision",
5304
6734
  tool: "kb_write_decision",
5305
6735
  usage: "write-decision < decision.json",
5306
6736
  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.",
5307
- input: import_zod35.z.object({ bundlePath, input: decisionInputSchema }),
6737
+ input: import_zod37.z.object({ bundlePath, input: decisionInputSchema }),
5308
6738
  fromArgv: async (_argv, path, stdin) => ({
5309
6739
  bundlePath: path,
5310
6740
  input: JSON.parse(await stdin())
@@ -5334,6 +6764,7 @@ var KB_COMMANDS = [
5334
6764
  answerCommand,
5335
6765
  verifyCommand,
5336
6766
  anchorResolveCommand,
6767
+ reassessCommand,
5337
6768
  loadCommand,
5338
6769
  catalogCommand,
5339
6770
  packCommand,
@@ -5364,7 +6795,7 @@ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
5364
6795
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
5365
6796
 
5366
6797
  // src/version.ts
5367
- var VERSION = true ? "0.1.16" : "0.0.0-dev";
6798
+ var VERSION = true ? "0.1.18" : "0.0.0-dev";
5368
6799
 
5369
6800
  // src/mcp.ts
5370
6801
  function createKbMcpServer() {
@@ -5403,7 +6834,7 @@ async function runKbMcpServer() {
5403
6834
  }
5404
6835
 
5405
6836
  // src/cli.ts
5406
- var import_node_path9 = require("path");
6837
+ var import_node_path12 = require("path");
5407
6838
  async function runKbCli(argv) {
5408
6839
  const { flags, literal } = takeLiteral(argv);
5409
6840
  const {
@@ -5458,21 +6889,21 @@ async function runKbCli(argv) {
5458
6889
  `);
5459
6890
  }
5460
6891
  function takeLiteral(argv) {
5461
- const at = argv.indexOf("--");
5462
- if (at === -1) return { flags: argv, literal: [] };
5463
- return { flags: argv.slice(0, at), literal: argv.slice(at + 1) };
6892
+ const at2 = argv.indexOf("--");
6893
+ if (at2 === -1) return { flags: argv, literal: [] };
6894
+ return { flags: argv.slice(0, at2), literal: argv.slice(at2 + 1) };
5464
6895
  }
5465
6896
  function takeBundle(argv) {
5466
- const at = argv.indexOf("--bundle");
5467
- if (at === -1) {
5468
- return { bundle: (0, import_node_path9.join)(process.cwd(), KB_DIR), explicit: false, rest: argv };
6897
+ const at2 = argv.indexOf("--bundle");
6898
+ if (at2 === -1) {
6899
+ return { bundle: (0, import_node_path12.join)(process.cwd(), KB_DIR), explicit: false, rest: argv };
5469
6900
  }
5470
- const bundle = argv[at + 1];
6901
+ const bundle = argv[at2 + 1];
5471
6902
  if (!bundle) die("--bundle requires a path");
5472
6903
  return {
5473
6904
  bundle,
5474
6905
  explicit: true,
5475
- rest: [...argv.slice(0, at), ...argv.slice(at + 2)]
6906
+ rest: [...argv.slice(0, at2), ...argv.slice(at2 + 2)]
5476
6907
  };
5477
6908
  }
5478
6909
  function readStdin() {
@@ -5495,14 +6926,14 @@ function summarise(description) {
5495
6926
  return sentence.length > 78 ? `${sentence.slice(0, 75)}\u2026` : sentence;
5496
6927
  }
5497
6928
  function usage() {
5498
- const width = Math.max(...KB_COMMANDS.map((command) => command.usage.length));
6929
+ const width2 = Math.max(...KB_COMMANDS.map((command) => command.usage.length));
5499
6930
  return [
5500
6931
  "strauss-kb \u2014 knowledge base commands",
5501
6932
  "",
5502
6933
  "Usage: strauss-kb [--bundle PATH] <command> [args]",
5503
6934
  "",
5504
6935
  ...KB_COMMANDS.map(
5505
- (command) => ` ${command.usage.padEnd(width)} ${summarise(command.description)}`
6936
+ (command) => ` ${command.usage.padEnd(width2)} ${summarise(command.description)}`
5506
6937
  ),
5507
6938
  "",
5508
6939
  ` --bundle PATH defaults to ./${KB_DIR}`,
@@ -5563,12 +6994,14 @@ function usage() {
5563
6994
  RECORD_TYPES,
5564
6995
  SEARCH_INDEX_FILE,
5565
6996
  TRACE_EDGES,
6997
+ TreeSitterResolver,
5566
6998
  adjudicate,
5567
6999
  anchorFilePath,
5568
7000
  assertBaseNotFrozen,
5569
7001
  backlinks,
5570
7002
  buildContext,
5571
7003
  catalog,
7004
+ classifyDrift,
5572
7005
  composeDecisionRecord,
5573
7006
  composeInputSchema,
5574
7007
  composeLinkSchema,
@@ -5577,9 +7010,14 @@ function usage() {
5577
7010
  contextProfileBudgets,
5578
7011
  createKbMcpServer,
5579
7012
  decisionInputSchema,
7013
+ defaultAnchorResolvers,
5580
7014
  detectAnchorDrift,
5581
7015
  doctor,
5582
7016
  edgeNeighbours,
7017
+ ensureGrammar,
7018
+ grammarHints,
7019
+ grammarManifest,
7020
+ grammarsCacheRoot,
5583
7021
  hashAnchorText,
5584
7022
  impact,
5585
7023
  inboundIndex,
@@ -5597,6 +7035,7 @@ function usage() {
5597
7035
  kbRecordFrontmatterSchema,
5598
7036
  kbSourceSchema,
5599
7037
  kbVerifiedEventSchema,
7038
+ languageForFile,
5600
7039
  listPins,
5601
7040
  loadQmd,
5602
7041
  matchToDiff,
@@ -5606,9 +7045,11 @@ function usage() {
5606
7045
  parseLog,
5607
7046
  parseMarkdownWithFrontmatter,
5608
7047
  pinBase,
7048
+ prepareResolvers,
5609
7049
  readMergedPins,
5610
7050
  readPinsLayer,
5611
7051
  readRemoteAnchors,
7052
+ reassessPacket,
5612
7053
  regexResolver,
5613
7054
  renderCatalogLine,
5614
7055
  renderIndex,
@@ -5616,6 +7057,7 @@ function usage() {
5616
7057
  renderLogEntry,
5617
7058
  repoCacheDir,
5618
7059
  resolveAnchor,
7060
+ resolveAnchorSpan,
5619
7061
  resolveHeads,
5620
7062
  resolveHits,
5621
7063
  resolvePinPath,
@@ -5628,6 +7070,8 @@ function usage() {
5628
7070
  syncInstructions,
5629
7071
  toHookJson,
5630
7072
  trace,
7073
+ treeSitterLanguages,
7074
+ unifiedDiff,
5631
7075
  unpinBase,
5632
7076
  validateBundle
5633
7077
  });