@saasontools/strauss-kb 0.1.16 → 0.1.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -78,6 +78,7 @@ __export(index_exports, {
78
78
  RECORD_TYPES: () => RECORD_TYPES,
79
79
  SEARCH_INDEX_FILE: () => SEARCH_INDEX_FILE,
80
80
  TRACE_EDGES: () => TRACE_EDGES,
81
+ TreeSitterResolver: () => TreeSitterResolver,
81
82
  adjudicate: () => adjudicate,
82
83
  anchorFilePath: () => anchorFilePath,
83
84
  assertBaseNotFrozen: () => assertBaseNotFrozen,
@@ -92,9 +93,14 @@ __export(index_exports, {
92
93
  contextProfileBudgets: () => contextProfileBudgets,
93
94
  createKbMcpServer: () => createKbMcpServer,
94
95
  decisionInputSchema: () => decisionInputSchema,
96
+ defaultAnchorResolvers: () => defaultAnchorResolvers,
95
97
  detectAnchorDrift: () => detectAnchorDrift,
96
98
  doctor: () => doctor,
97
99
  edgeNeighbours: () => edgeNeighbours,
100
+ ensureGrammar: () => ensureGrammar,
101
+ grammarHints: () => grammarHints,
102
+ grammarManifest: () => grammarManifest,
103
+ grammarsCacheRoot: () => grammarsCacheRoot,
98
104
  hashAnchorText: () => hashAnchorText,
99
105
  impact: () => impact,
100
106
  inboundIndex: () => inboundIndex,
@@ -112,6 +118,7 @@ __export(index_exports, {
112
118
  kbRecordFrontmatterSchema: () => kbRecordFrontmatterSchema,
113
119
  kbSourceSchema: () => kbSourceSchema,
114
120
  kbVerifiedEventSchema: () => kbVerifiedEventSchema,
121
+ languageForFile: () => languageForFile,
115
122
  listPins: () => listPins,
116
123
  loadQmd: () => loadQmd,
117
124
  matchToDiff: () => matchToDiff,
@@ -121,6 +128,7 @@ __export(index_exports, {
121
128
  parseLog: () => parseLog,
122
129
  parseMarkdownWithFrontmatter: () => parseMarkdownWithFrontmatter,
123
130
  pinBase: () => pinBase,
131
+ prepareResolvers: () => prepareResolvers,
124
132
  readMergedPins: () => readMergedPins,
125
133
  readPinsLayer: () => readPinsLayer,
126
134
  readRemoteAnchors: () => readRemoteAnchors,
@@ -131,6 +139,7 @@ __export(index_exports, {
131
139
  renderLogEntry: () => renderLogEntry,
132
140
  repoCacheDir: () => repoCacheDir,
133
141
  resolveAnchor: () => resolveAnchor,
142
+ resolveAnchorSpan: () => resolveAnchorSpan,
134
143
  resolveHeads: () => resolveHeads,
135
144
  resolveHits: () => resolveHits,
136
145
  resolvePinPath: () => resolvePinPath,
@@ -143,14 +152,19 @@ __export(index_exports, {
143
152
  syncInstructions: () => syncInstructions,
144
153
  toHookJson: () => toHookJson,
145
154
  trace: () => trace,
155
+ treeSitterLanguages: () => treeSitterLanguages,
146
156
  unpinBase: () => unpinBase,
147
157
  validateBundle: () => validateBundle
148
158
  });
149
159
  module.exports = __toCommonJS(index_exports);
150
160
 
161
+ // ../../node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.26_supports-color@8.1.1_typescript@6.0.3_yaml@2.9.0/node_modules/tsup/assets/cjs_shims.js
162
+ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
163
+ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
164
+
151
165
  // src/kb-store.ts
152
- var import_promises4 = require("fs/promises");
153
- var import_node_path4 = require("path");
166
+ var import_promises6 = require("fs/promises");
167
+ var import_node_path7 = require("path");
154
168
 
155
169
  // src/concurrency.ts
156
170
  var DEFAULT_IO_CONCURRENCY = 16;
@@ -251,7 +265,13 @@ var kbAnchorSchema = import_zod.z.object({
251
265
  /** ISO 8601 timestamp of the last successful resolution. */
252
266
  resolved_at: import_zod.z.string().min(1).optional(),
253
267
  /** Line count of the text the hash was taken over. */
254
- lines: import_zod.z.number().int().positive().optional()
268
+ lines: import_zod.z.number().int().positive().optional(),
269
+ /**
270
+ * Which resolver produced the hashed span. Absent means an anchor stamped
271
+ * before resolvers were named, which is read as `regex` — the only one
272
+ * there was. A hash from a different resolver is drift, not a match.
273
+ */
274
+ resolver: import_zod.z.enum(["tree-sitter", "regex"]).optional()
255
275
  }).strict();
256
276
  var kbLinkSchema = import_zod.z.object({
257
277
  target: import_zod.z.string().min(1),
@@ -818,18 +838,18 @@ async function readOneRepo(repo, url, declared, context) {
818
838
  if (!repoUrlIsSafe(url)) return all({ ok: false, reason: "repo-invalid" });
819
839
  const cache = cachePathFor(repo, context.cacheDir);
820
840
  if (!cache) return all({ ok: false, reason: "remote-unreachable" });
821
- const rejected = /* @__PURE__ */ new Map();
841
+ const rejected2 = /* @__PURE__ */ new Map();
822
842
  const usable = [];
823
843
  for (const want of wants) {
824
844
  const reason = wantReason(want);
825
845
  if (reason)
826
- rejected.set(wantKey(repo, want.ref, want.file), { ok: false, reason });
846
+ rejected2.set(wantKey(repo, want.ref, want.file), { ok: false, reason });
827
847
  else usable.push(want);
828
848
  }
829
- if (!usable.length) return rejected;
849
+ if (!usable.length) return rejected2;
830
850
  wants = usable;
831
851
  const opened = await openCache(cache, url, context);
832
- if (opened) return new Map([...rejected, ...all(opened)]);
852
+ if (opened) return new Map([...rejected2, ...all(opened)]);
833
853
  const wantsDefault = wants.some((want) => want.ref === void 0);
834
854
  const branch = wantsDefault ? await defaultBranch(cache, context) : {};
835
855
  const revs = /* @__PURE__ */ new Map();
@@ -856,7 +876,7 @@ async function readOneRepo(repo, url, declared, context) {
856
876
  }
857
877
  );
858
878
  return new Map([
859
- ...rejected,
879
+ ...rejected2,
860
880
  ...wants.map(
861
881
  (want, at) => [wantKey(repo, want.ref, want.file), reads[at]]
862
882
  )
@@ -908,13 +928,13 @@ async function defaultBranch(cache, context) {
908
928
  if (!listed.ok) {
909
929
  const reason = transportReason(listed.stderr);
910
930
  if (reason !== "ref-not-found") {
911
- const cached2 = await cachedBranch(cache);
912
- return cached2 ? { name: cached2 } : { reason };
931
+ const cached3 = await cachedBranch(cache);
932
+ return cached3 ? { name: cached3 } : { reason };
913
933
  }
914
934
  }
915
935
  }
916
- const cached = await cachedBranch(cache);
917
- if (cached) return { name: cached };
936
+ const cached2 = await cachedBranch(cache);
937
+ if (cached2) return { name: cached2 };
918
938
  return {
919
939
  reason: context.offline ? "remote-unreachable" : "default-branch-unknown"
920
940
  };
@@ -941,8 +961,8 @@ async function ensureRev(cache, rev, context) {
941
961
  cwd: cache
942
962
  }
943
963
  );
944
- const cached = have.ok && have.stdout.trim().length > 0;
945
- if (cached && (context.offline || IMMUTABLE_REV.test(rev))) return void 0;
964
+ const cached2 = have.ok && have.stdout.trim().length > 0;
965
+ if (cached2 && (context.offline || IMMUTABLE_REV.test(rev))) return void 0;
946
966
  if (context.offline) return { ok: false, reason: "remote-unreachable" };
947
967
  const fetched = await git(
948
968
  [
@@ -958,7 +978,7 @@ async function ensureRev(cache, rev, context) {
958
978
  );
959
979
  if (!fetched.ok) {
960
980
  const reason = transportReason(fetched.stderr);
961
- if (cached && reason !== "ref-not-found") return void 0;
981
+ if (cached2 && reason !== "ref-not-found") return void 0;
962
982
  return { ok: false, reason };
963
983
  }
964
984
  const head = await git(["rev-parse", "FETCH_HEAD"], { cwd: cache });
@@ -1058,8 +1078,8 @@ function resolveHeads(from, byId) {
1058
1078
  while (queue.length) {
1059
1079
  const current = queue.shift();
1060
1080
  const next = successors(current, byId);
1061
- for (const missing of next.missing) {
1062
- warnings.push({ kind: "broken-chain", missing });
1081
+ for (const missing2 of next.missing) {
1082
+ warnings.push({ kind: "broken-chain", missing: missing2 });
1063
1083
  }
1064
1084
  if (!next.records.length) {
1065
1085
  if (current.conceptId !== from.conceptId)
@@ -1090,13 +1110,13 @@ function successors(record, byId) {
1090
1110
  }
1091
1111
  }
1092
1112
  const records = [];
1093
- const missing = [];
1113
+ const missing2 = [];
1094
1114
  for (const id of ids) {
1095
1115
  const found = byId.get(id);
1096
1116
  if (found) records.push(found);
1097
- else missing.push(id);
1117
+ else missing2.push(id);
1098
1118
  }
1099
- return { records, missing };
1119
+ return { records, missing: missing2 };
1100
1120
  }
1101
1121
 
1102
1122
  // src/kb-stamp.ts
@@ -1222,63 +1242,580 @@ async function readAnchorFiles(files, read, concurrency = DEFAULT_IO_CONCURRENCY
1222
1242
  }
1223
1243
 
1224
1244
  // src/anchor-resolver/resolver.ts
1245
+ var import_node_crypto4 = require("crypto");
1246
+
1247
+ // src/tree-sitter-resolver/languages.ts
1248
+ var import_node_path5 = require("path");
1249
+
1250
+ // src/grammars/index.ts
1251
+ var import_promises4 = require("fs/promises");
1252
+
1253
+ // src/grammars/store.ts
1225
1254
  var import_node_crypto2 = require("crypto");
1255
+ var import_promises3 = require("fs/promises");
1256
+ var import_node_os2 = require("os");
1257
+ var import_node_path3 = require("path");
1258
+ function grammarsCacheRoot(override) {
1259
+ return override ?? process.env["STRAUSS_KB_GRAMMARS_DIR"] ?? (0, import_node_path3.join)((0, import_node_os2.homedir)(), ".strauss", "grammars");
1260
+ }
1261
+ function grammarCachePath(root, language, sha2564, extension = "wasm") {
1262
+ return (0, import_node_path3.join)(root, language, `${sha2564.slice(0, 12)}.${extension}`);
1263
+ }
1264
+ function sha2562(bytes) {
1265
+ return (0, import_node_crypto2.createHash)("sha256").update(bytes).digest("hex");
1266
+ }
1267
+ function matches(bytes, entry) {
1268
+ if (entry.bytes !== void 0 && bytes.byteLength !== entry.bytes)
1269
+ return false;
1270
+ return sha2562(bytes) === entry.sha256;
1271
+ }
1272
+ async function verifyCached(path, entry) {
1273
+ let bytes;
1274
+ try {
1275
+ bytes = await (0, import_promises3.readFile)(path);
1276
+ } catch {
1277
+ return false;
1278
+ }
1279
+ if (matches(bytes, entry)) return true;
1280
+ await (0, import_promises3.rm)(path, { force: true });
1281
+ return false;
1282
+ }
1283
+ async function writeCached(path, bytes) {
1284
+ await (0, import_promises3.mkdir)((0, import_node_path3.dirname)(path), { recursive: true });
1285
+ const temporary = `${path}.${process.pid}.${(0, import_node_crypto2.randomBytes)(4).toString("hex")}.tmp`;
1286
+ try {
1287
+ await (0, import_promises3.writeFile)(temporary, bytes);
1288
+ await (0, import_promises3.rename)(temporary, path);
1289
+ } catch (error) {
1290
+ await (0, import_promises3.rm)(temporary, { force: true });
1291
+ throw error;
1292
+ }
1293
+ }
1294
+
1295
+ // src/grammars/fetch.ts
1296
+ var ATTEMPTS = 3;
1297
+ var BACKOFF_MS = 250;
1298
+ function grammarUrl(url, override) {
1299
+ const base2 = grammarsBaseUrl(override);
1300
+ if (!base2) return url;
1301
+ let root = base2;
1302
+ while (root.endsWith("/")) root = root.slice(0, -1);
1303
+ const pinned = new URL(url);
1304
+ return `${root}${pinned.pathname}${pinned.search}`;
1305
+ }
1306
+ function grammarsBaseUrl(override) {
1307
+ return override ?? process.env["STRAUSS_KB_GRAMMARS_URL"];
1308
+ }
1309
+ async function downloadPart(url, name, entry, options = {}) {
1310
+ const log = options.log ?? ((line) => void process.stderr.write(line));
1311
+ const weight = entry.bytes === void 0 ? "" : ` (${size(entry.bytes)} from manifest)`;
1312
+ log(`strauss-kb: downloading ${name}${weight} from ${url}
1313
+ `);
1314
+ let cause = "";
1315
+ for (let attempt = 1; attempt <= ATTEMPTS; attempt++) {
1316
+ const outcome = await attemptDownload(url, entry, options.fetchTimeoutMs);
1317
+ if ("bytes" in outcome) return outcome;
1318
+ cause = outcome.cause;
1319
+ log(
1320
+ `strauss-kb: ${name} attempt ${attempt}/${ATTEMPTS} failed: ${cause}
1321
+ `
1322
+ );
1323
+ if (!outcome.retry) break;
1324
+ if (attempt < ATTEMPTS) await pause(BACKOFF_MS * attempt);
1325
+ }
1326
+ log(`strauss-kb: ${name} not downloaded: ${cause}
1327
+ `);
1328
+ return { cause };
1329
+ }
1330
+ async function attemptDownload(url, entry, timeoutMs) {
1331
+ try {
1332
+ const response = await fetch(url, {
1333
+ signal: AbortSignal.timeout(fetchTimeoutMs(timeoutMs))
1334
+ });
1335
+ if (!response.ok) {
1336
+ return {
1337
+ cause: `HTTP ${response.status}`,
1338
+ retry: response.status >= 500 || response.status === 429
1339
+ };
1340
+ }
1341
+ const bytes = new Uint8Array(await response.arrayBuffer());
1342
+ if (!matches(bytes, entry))
1343
+ return { cause: "sha256 mismatch", retry: false };
1344
+ return { bytes };
1345
+ } catch (error) {
1346
+ const timedOut = error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError");
1347
+ return { cause: timedOut ? "timeout" : "network error", retry: true };
1348
+ }
1349
+ }
1350
+ function size(bytes) {
1351
+ return bytes >= 1024 * 1024 ? `${(bytes / (1024 * 1024)).toFixed(1)} MB` : `${Math.round(bytes / 1024)} KB`;
1352
+ }
1353
+ function pause(ms) {
1354
+ return new Promise((resolve6) => setTimeout(resolve6, ms));
1355
+ }
1356
+
1357
+ // src/grammars/manifest.ts
1358
+ var import_node_fs = require("fs");
1359
+ var import_node_path4 = require("path");
1360
+ var import_node_url = require("url");
1361
+
1362
+ // src/grammars/model.ts
1363
+ var import_zod2 = require("zod");
1364
+ var sha2563 = import_zod2.z.string().regex(/^[0-9a-f]{64}$/);
1365
+ var grammarWasmSchema = import_zod2.z.object({
1366
+ url: import_zod2.z.string().min(1),
1367
+ sha256: sha2563,
1368
+ bytes: import_zod2.z.number().int().positive()
1369
+ });
1370
+ var grammarTagsSchema = import_zod2.z.object({ url: import_zod2.z.string().min(1), sha256: sha2563 });
1371
+ var grammarPackSchema = import_zod2.z.object({
1372
+ package: import_zod2.z.string().min(1),
1373
+ wasm: grammarWasmSchema,
1374
+ tags: import_zod2.z.array(grammarTagsSchema),
1375
+ license: import_zod2.z.string().min(1),
1376
+ extensions: import_zod2.z.array(import_zod2.z.string().min(1))
1377
+ });
1378
+ var grammarManifestSchema = import_zod2.z.object({
1379
+ /** The runtime the packs were proved against. */
1380
+ webTreeSitter: import_zod2.z.string().min(1),
1381
+ linguist: import_zod2.z.object({ tag: import_zod2.z.string().min(1), commit: import_zod2.z.string().min(1) }),
1382
+ packs: import_zod2.z.record(import_zod2.z.string().min(1), grammarPackSchema)
1383
+ });
1384
+
1385
+ // src/grammars/manifest.ts
1386
+ var cached;
1387
+ function grammarManifest() {
1388
+ cached ??= grammarManifestSchema.parse(
1389
+ JSON.parse((0, import_node_fs.readFileSync)(grammarsDataPath("manifest.json"), "utf8"))
1390
+ );
1391
+ return cached;
1392
+ }
1393
+ function grammarsDataPath(...segments) {
1394
+ let dir = (0, import_node_path4.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
1395
+ for (let up = 0; up < 5; up++) {
1396
+ const candidate = (0, import_node_path4.join)(dir, "grammars");
1397
+ if ((0, import_node_fs.existsSync)((0, import_node_path4.join)(candidate, "manifest.json")))
1398
+ return (0, import_node_path4.join)(candidate, ...segments);
1399
+ dir = (0, import_node_path4.dirname)(dir);
1400
+ }
1401
+ throw new Error("grammars/manifest.json is missing from the package");
1402
+ }
1403
+
1404
+ // src/grammars/index.ts
1405
+ var inFlight = /* @__PURE__ */ new Map();
1406
+ var missing = /* @__PURE__ */ new Map();
1407
+ var uncompilable = /* @__PURE__ */ new Map();
1408
+ var rejected = /* @__PURE__ */ new Map();
1409
+ function grammarsDownloadDisabled() {
1410
+ return process.env["STRAUSS_KB_GRAMMARS"] === "off";
1411
+ }
1412
+ async function ensureGrammar(language, options = {}) {
1413
+ const pack2 = grammarManifest().packs[language];
1414
+ if (!pack2) return null;
1415
+ const root = grammarsCacheRoot(options.cacheRoot);
1416
+ const wasm = grammarCachePath(root, language, pack2.wasm.sha256);
1417
+ const key2 = `${wasm} ${grammarsBaseUrl(options.baseUrl) ?? ""}`;
1418
+ const existing = inFlight.get(key2);
1419
+ if (existing) return existing;
1420
+ const pending = (async () => {
1421
+ const grammar = await ensurePart(
1422
+ wasm,
1423
+ `tree-sitter-${language}`,
1424
+ pack2.wasm,
1425
+ options
1426
+ );
1427
+ if (grammar !== true)
1428
+ return miss(language, `grammar tree-sitter-${language}`, grammar);
1429
+ const parts = [];
1430
+ const total = pack2.tags.length;
1431
+ for (const [at, part] of pack2.tags.entries()) {
1432
+ const name = `${language} tags${total > 1 ? ` part ${at + 1}/${total}` : ""}`;
1433
+ const path = grammarCachePath(root, language, part.sha256, "scm");
1434
+ const held = await ensurePart(path, name, part, options);
1435
+ if (held !== true) return miss(language, name, held);
1436
+ parts.push(`; ${part.url}
1437
+ ${lf(await (0, import_promises4.readFile)(path, "utf8"))}`);
1438
+ }
1439
+ missing.delete(language);
1440
+ return { wasm, query: total ? parts.join("\n") : void 0 };
1441
+ })();
1442
+ inFlight.set(key2, pending);
1443
+ const result = await pending;
1444
+ if (result === null) inFlight.delete(key2);
1445
+ return result;
1446
+ }
1447
+ async function ensurePart(path, name, entry, options) {
1448
+ if (await verifyCached(path, entry)) return true;
1449
+ if (options.offline === true || grammarsDownloadDisabled()) return {};
1450
+ const download = await downloadPart(
1451
+ grammarUrl(entry.url, options.baseUrl),
1452
+ name,
1453
+ entry,
1454
+ options
1455
+ );
1456
+ if ("cause" in download) return { cause: download.cause };
1457
+ await writeCached(path, download.bytes).catch(() => null);
1458
+ return true;
1459
+ }
1460
+ function miss(language, subject, failure) {
1461
+ missing.set(language, { subject, ...failure });
1462
+ return null;
1463
+ }
1464
+ function lf(body) {
1465
+ return body.replace(/\r\n/g, "\n");
1466
+ }
1467
+ function noteUncompilableQuery(language, cause) {
1468
+ uncompilable.set(language, cause);
1469
+ }
1470
+ function noteRejectedGrammar(language, cause) {
1471
+ rejected.set(language, cause);
1472
+ }
1473
+ function grammarHints() {
1474
+ const manifest = grammarManifest();
1475
+ const packs = manifest.packs;
1476
+ const lines = /* @__PURE__ */ new Map();
1477
+ for (const [language, { subject, cause }] of missing)
1478
+ lines.set(
1479
+ language,
1480
+ `${subject} not cached${cause ? ` (${cause})` : ""}; run online once, or set STRAUSS_KB_GRAMMARS_DIR`
1481
+ );
1482
+ for (const [language, cause] of rejected)
1483
+ lines.set(
1484
+ language,
1485
+ `${packs[language]?.package ?? `tree-sitter-${language}`} rejected by web-tree-sitter ${manifest.webTreeSitter}${cause ? `: ${cause}` : ""}; re-pin with pnpm grammars pin ${language}`
1486
+ );
1487
+ for (const [language, cause] of uncompilable)
1488
+ lines.set(
1489
+ language,
1490
+ `tags query for ${language} does not compile against ${packs[language]?.package ?? `tree-sitter-${language}`}: ${cause}; re-pin with pnpm grammars pin ${language}`
1491
+ );
1492
+ return [...lines].sort(([a], [b]) => a.localeCompare(b)).map(([, line]) => line);
1493
+ }
1494
+
1495
+ // src/tree-sitter-resolver/languages.ts
1496
+ var table;
1497
+ function extensionTable() {
1498
+ const manifest = grammarManifest();
1499
+ if (table?.of !== manifest)
1500
+ table = {
1501
+ of: manifest,
1502
+ extensions: Object.fromEntries(
1503
+ Object.entries(manifest.packs).flatMap(
1504
+ ([language, pack2]) => pack2.extensions.map((extension) => [extension, language])
1505
+ )
1506
+ )
1507
+ };
1508
+ return table.extensions;
1509
+ }
1510
+ function hasQuery(language) {
1511
+ return (grammarManifest().packs[language]?.tags.length ?? 0) > 0;
1512
+ }
1513
+ function languageForFile(file) {
1514
+ const language = extensionTable()[(0, import_node_path5.extname)(file).toLowerCase()];
1515
+ return language && hasQuery(language) ? language : void 0;
1516
+ }
1517
+ function treeSitterLanguages() {
1518
+ return [...new Set(Object.values(extensionTable()))].filter(hasQuery).sort();
1519
+ }
1520
+
1521
+ // src/tree-sitter-resolver/resolver.ts
1522
+ var import_node_crypto3 = require("crypto");
1523
+ var import_web_tree_sitter = require("web-tree-sitter");
1524
+
1525
+ // src/tree-sitter-resolver/definitions.ts
1526
+ var SCOPE_ONLY = "reference.implementation";
1527
+ function index(tree, query) {
1528
+ const byName = /* @__PURE__ */ new Map();
1529
+ for (const match of query.matches(tree.rootNode)) {
1530
+ const nameNode = match.captures.find((capture) => capture.name === "name");
1531
+ const defNode = match.captures.find(
1532
+ (capture) => capture.name.startsWith("definition.") || capture.name === SCOPE_ONLY
1533
+ );
1534
+ if (!nameNode || !defNode) continue;
1535
+ const candidate = {
1536
+ node: defNode.node,
1537
+ name: nameNode.node.text,
1538
+ target: defNode.name !== SCOPE_ONLY
1539
+ };
1540
+ const existing = byName.get(nameNode.node.id);
1541
+ if (existing && width(existing.node) <= width(candidate.node)) continue;
1542
+ byName.set(nameNode.node.id, candidate);
1543
+ }
1544
+ const definitions = [...byName.values()];
1545
+ return {
1546
+ tree,
1547
+ byNodeId: new Map(
1548
+ definitions.map((definition) => [definition.node.id, definition])
1549
+ ),
1550
+ definitions
1551
+ };
1552
+ }
1553
+ function select(parsed, wanted) {
1554
+ const matches3 = parsed.definitions.filter(
1555
+ (definition) => definition.target && endsWith(chainOf(definition, parsed.byNodeId), wanted)
1556
+ );
1557
+ if (matches3.length < 2) return matches3;
1558
+ const bodied = matches3.filter(
1559
+ (definition) => definition.node.childForFieldName("body") !== null
1560
+ );
1561
+ return bodied.length === 1 ? bodied : matches3;
1562
+ }
1563
+ function chainOf(definition, byNodeId) {
1564
+ const chain = [definition.name];
1565
+ const receiver = definition.node.childForFieldName("receiver");
1566
+ const type = receiver && typeNameIn(receiver);
1567
+ if (type) chain.unshift(type);
1568
+ for (let node = definition.node.parent; node; node = node.parent) {
1569
+ const enclosing = byNodeId.get(node.id);
1570
+ if (enclosing && enclosing.node !== definition.node)
1571
+ chain.unshift(enclosing.name);
1572
+ }
1573
+ return chain;
1574
+ }
1575
+ function typeNameIn(receiver) {
1576
+ const stack = [receiver];
1577
+ while (stack.length) {
1578
+ const node = stack.pop();
1579
+ if (node.type === "type_identifier") return node.text;
1580
+ for (let at = 0; at < node.childCount; at++) {
1581
+ const child = node.child(at);
1582
+ if (child) stack.push(child);
1583
+ }
1584
+ }
1585
+ return void 0;
1586
+ }
1587
+ function endsWith(chain, wanted) {
1588
+ if (wanted.length > chain.length) return false;
1589
+ const offset = chain.length - wanted.length;
1590
+ return wanted.every((segment, at) => chain[offset + at] === segment);
1591
+ }
1592
+ function width(node) {
1593
+ return node.endIndex - node.startIndex;
1594
+ }
1595
+ function spanOf(definition, source) {
1596
+ let start = definition.node;
1597
+ let end = definition.node;
1598
+ for (let sibling = start.previousSibling; sibling?.type === "decorator"; sibling = sibling.previousSibling) {
1599
+ start = sibling;
1600
+ }
1601
+ const parent = end.parent;
1602
+ if (parent?.type === "export_statement" && parent.childForFieldName("declaration")?.id === end.id) {
1603
+ start = parent;
1604
+ end = parent;
1605
+ }
1606
+ const lines = source.split("\n");
1607
+ const startLine = start.startPosition.row;
1608
+ const endLine = end.endPosition.column === 0 && end.endPosition.row > startLine ? end.endPosition.row - 1 : end.endPosition.row;
1609
+ return {
1610
+ text: lines.slice(startLine, endLine + 1).join("\n"),
1611
+ startLine: startLine + 1,
1612
+ endLine: endLine + 1
1613
+ };
1614
+ }
1615
+
1616
+ // src/tree-sitter-resolver/resolver.ts
1617
+ var TREE_CACHE_LIMIT = 32;
1618
+ var TreeSitterResolver = class {
1619
+ name = "tree-sitter";
1620
+ grammars;
1621
+ loaded = /* @__PURE__ */ new Map();
1622
+ trees = /* @__PURE__ */ new Map();
1623
+ parser;
1624
+ initialized = false;
1625
+ /** Cache effectiveness, for tests and for the latency numbers. */
1626
+ stats = { parses: 0, cacheHits: 0 };
1627
+ constructor(options = {}) {
1628
+ this.grammars = options;
1629
+ }
1630
+ /**
1631
+ * Loads the grammars these files need, once per language per process,
1632
+ * downloading each one on first use.
1633
+ *
1634
+ * A grammar that will not load is remembered as unavailable rather than
1635
+ * retried per anchor, and never throws: an unobtainable WASM is a finding.
1636
+ */
1637
+ async prepare(files) {
1638
+ const wanted = /* @__PURE__ */ new Set();
1639
+ for (const file of files) {
1640
+ const language = languageForFile(file);
1641
+ if (language && !this.loaded.has(language)) wanted.add(language);
1642
+ }
1643
+ if (!wanted.size) return;
1644
+ if (!this.initialized) {
1645
+ try {
1646
+ await import_web_tree_sitter.Parser.init();
1647
+ this.parser = new import_web_tree_sitter.Parser();
1648
+ this.initialized = true;
1649
+ } catch {
1650
+ for (const language of wanted) this.loaded.set(language, null);
1651
+ return;
1652
+ }
1653
+ }
1654
+ const languages = [...wanted];
1655
+ const loaded = await mapLimit(
1656
+ languages,
1657
+ Math.min(DEFAULT_IO_CONCURRENCY, languages.length),
1658
+ (language) => this.load(language)
1659
+ );
1660
+ languages.forEach(
1661
+ (language, at) => this.loaded.set(language, loaded[at] ?? null)
1662
+ );
1663
+ }
1664
+ /**
1665
+ * An unobtainable grammar, one this runtime refuses, and a query that will
1666
+ * not compile are three faults with three repairs; all are reported through
1667
+ * the grammars module so every hint has one home.
1668
+ */
1669
+ async load(language) {
1670
+ let pack2;
1671
+ try {
1672
+ pack2 = await ensureGrammar(language, this.grammars);
1673
+ } catch {
1674
+ return null;
1675
+ }
1676
+ if (!pack2?.query) return null;
1677
+ let grammar;
1678
+ try {
1679
+ grammar = await import_web_tree_sitter.Language.load(pack2.wasm);
1680
+ } catch (error) {
1681
+ noteRejectedGrammar(language, why(error));
1682
+ return null;
1683
+ }
1684
+ try {
1685
+ return { language: grammar, query: new import_web_tree_sitter.Query(grammar, pack2.query) };
1686
+ } catch (error) {
1687
+ noteUncompilableQuery(language, why(error));
1688
+ return null;
1689
+ }
1690
+ }
1691
+ /**
1692
+ * Abstains on an extension with no grammar so the regex resolver gets a
1693
+ * turn; reports `resolver-unavailable` when the grammar exists in principle
1694
+ * but could not be loaded, because falling back there would silently trade a
1695
+ * precise span for a guessed one.
1696
+ */
1697
+ attempt(source, symbol, file) {
1698
+ const language = file ? languageForFile(file) : void 0;
1699
+ if (!language) return { kind: "abstain" };
1700
+ if (!this.loaded.has(language)) return { kind: "abstain" };
1701
+ const loaded = this.loaded.get(language);
1702
+ if (!loaded) return { kind: "unresolved", reason: "resolver-unavailable" };
1703
+ const parsed = this.parse(language, loaded, source);
1704
+ if (!parsed) return { kind: "unresolved", reason: "resolver-unavailable" };
1705
+ const wanted = symbol.split(".").filter(Boolean);
1706
+ if (!wanted.length)
1707
+ return { kind: "unresolved", reason: "symbol-not-found" };
1708
+ const matches3 = select(parsed, wanted);
1709
+ if (!matches3.length)
1710
+ return { kind: "unresolved", reason: "symbol-not-found" };
1711
+ if (matches3.length > 1)
1712
+ return { kind: "unresolved", reason: "symbol-ambiguous" };
1713
+ return { kind: "resolved", span: spanOf(matches3[0], source) };
1714
+ }
1715
+ resolve(source, symbol, file) {
1716
+ const attempt = this.attempt(source, symbol, file);
1717
+ return attempt.kind === "resolved" ? attempt.span : null;
1718
+ }
1719
+ /** Parsed trees are keyed by content hash, so an unchanged file parses once. */
1720
+ parse(language, loaded, source) {
1721
+ const key2 = `${language}:${(0, import_node_crypto3.createHash)("sha256").update(source).digest("hex")}`;
1722
+ const cached2 = this.trees.get(key2);
1723
+ if (cached2) {
1724
+ this.stats.cacheHits += 1;
1725
+ return cached2;
1726
+ }
1727
+ const parser = this.parser;
1728
+ if (!parser) return null;
1729
+ let parsed;
1730
+ try {
1731
+ parser.setLanguage(loaded.language);
1732
+ const tree = parser.parse(source);
1733
+ if (!tree) return null;
1734
+ parsed = index(tree, loaded.query);
1735
+ } catch {
1736
+ return null;
1737
+ }
1738
+ this.stats.parses += 1;
1739
+ if (this.trees.size >= TREE_CACHE_LIMIT) {
1740
+ const oldest = this.trees.keys().next();
1741
+ if (!oldest.done) {
1742
+ this.trees.get(oldest.value)?.tree.delete();
1743
+ this.trees.delete(oldest.value);
1744
+ }
1745
+ }
1746
+ this.trees.set(key2, parsed);
1747
+ return parsed;
1748
+ }
1749
+ /** Drops cached trees. Grammars stay loaded — they are immutable. */
1750
+ reset() {
1751
+ for (const parsed of this.trees.values()) parsed.tree.delete();
1752
+ this.trees.clear();
1753
+ this.stats.parses = 0;
1754
+ this.stats.cacheHits = 0;
1755
+ }
1756
+ };
1757
+ function why(error) {
1758
+ const text = error instanceof Error ? error.message : String(error);
1759
+ return text || "no reason given";
1760
+ }
1761
+
1762
+ // src/anchor-resolver/resolver.ts
1226
1763
  var PARENT_SCOPE_LINES = 50;
1227
1764
  var CLEAN_STATE = { blockComment: false, template: false };
1228
1765
  function stripLine(line, state) {
1229
1766
  let out = "";
1230
- let index = 0;
1767
+ let index2 = 0;
1231
1768
  let { blockComment, template } = state;
1232
- while (index < line.length) {
1233
- const char = line[index];
1234
- const next = line[index + 1];
1769
+ while (index2 < line.length) {
1770
+ const char = line[index2];
1771
+ const next = line[index2 + 1];
1235
1772
  if (blockComment) {
1236
1773
  if (char === "*" && next === "/") {
1237
1774
  blockComment = false;
1238
- index += 2;
1775
+ index2 += 2;
1239
1776
  continue;
1240
1777
  }
1241
- index += 1;
1778
+ index2 += 1;
1242
1779
  continue;
1243
1780
  }
1244
1781
  if (template) {
1245
1782
  if (char === "\\") {
1246
- index += 2;
1783
+ index2 += 2;
1247
1784
  continue;
1248
1785
  }
1249
1786
  if (char === "`") template = false;
1250
- index += 1;
1787
+ index2 += 1;
1251
1788
  continue;
1252
1789
  }
1253
1790
  if (char === "/" && next === "*") {
1254
1791
  blockComment = true;
1255
- index += 2;
1792
+ index2 += 2;
1256
1793
  continue;
1257
1794
  }
1258
1795
  if (char === "/" && next === "/") break;
1259
1796
  if (char === "`") {
1260
1797
  template = true;
1261
- index += 1;
1798
+ index2 += 1;
1262
1799
  continue;
1263
1800
  }
1264
1801
  if (char === "'" || char === '"') {
1265
1802
  const quote = char;
1266
- index += 1;
1267
- while (index < line.length) {
1268
- if (line[index] === "\\") {
1269
- index += 2;
1803
+ index2 += 1;
1804
+ while (index2 < line.length) {
1805
+ if (line[index2] === "\\") {
1806
+ index2 += 2;
1270
1807
  continue;
1271
1808
  }
1272
- if (line[index] === quote) {
1273
- index += 1;
1809
+ if (line[index2] === quote) {
1810
+ index2 += 1;
1274
1811
  break;
1275
1812
  }
1276
- index += 1;
1813
+ index2 += 1;
1277
1814
  }
1278
1815
  continue;
1279
1816
  }
1280
1817
  out += char;
1281
- index += 1;
1818
+ index2 += 1;
1282
1819
  }
1283
1820
  return { code: out, state: { blockComment, template } };
1284
1821
  }
@@ -1293,8 +1830,8 @@ function captureBraceBlock(lines, matchLine) {
1293
1830
  let depth = 0;
1294
1831
  let opened = false;
1295
1832
  let state = CLEAN_STATE;
1296
- for (let index = matchLine; index < lines.length; index++) {
1297
- const stripped = stripLine(lines[index] ?? "", state);
1833
+ for (let index2 = matchLine; index2 < lines.length; index2++) {
1834
+ const stripped = stripLine(lines[index2] ?? "", state);
1298
1835
  state = stripped.state;
1299
1836
  for (const char of stripped.code) {
1300
1837
  if (char === "{") {
@@ -1303,10 +1840,10 @@ function captureBraceBlock(lines, matchLine) {
1303
1840
  } else if (char === "}") {
1304
1841
  depth = Math.max(0, depth - 1);
1305
1842
  } else if (char === ";" && !opened) {
1306
- return span(lines, matchLine, index);
1843
+ return span(lines, matchLine, index2);
1307
1844
  }
1308
1845
  }
1309
- if (opened && depth === 0) return span(lines, matchLine, index);
1846
+ if (opened && depth === 0) return span(lines, matchLine, index2);
1310
1847
  }
1311
1848
  return null;
1312
1849
  }
@@ -1315,22 +1852,22 @@ function captureIndentedBlock(lines, matchLine) {
1315
1852
  const header = lines[matchLine] ?? "";
1316
1853
  const indent = header.length - header.trimStart().length;
1317
1854
  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();
1855
+ for (let index2 = matchLine; index2 < lines.length && index2 <= matchLine + 20; index2++) {
1856
+ const code = stripLine(lines[index2] ?? "", CLEAN_STATE).code.trimEnd();
1320
1857
  if (code.endsWith(":")) {
1321
- headerEnd = index;
1858
+ headerEnd = index2;
1322
1859
  break;
1323
1860
  }
1324
- if (code.includes(":")) return span(lines, matchLine, index);
1861
+ if (code.includes(":")) return span(lines, matchLine, index2);
1325
1862
  }
1326
1863
  if (headerEnd === -1) return null;
1327
1864
  let end = headerEnd;
1328
- for (let index = headerEnd + 1; index < lines.length; index++) {
1329
- const line = lines[index] ?? "";
1865
+ for (let index2 = headerEnd + 1; index2 < lines.length; index2++) {
1866
+ const line = lines[index2] ?? "";
1330
1867
  if (line.trim() === "") continue;
1331
1868
  const lineIndent = line.length - line.trimStart().length;
1332
1869
  if (lineIndent <= indent) break;
1333
- end = index;
1870
+ end = index2;
1334
1871
  }
1335
1872
  return end === headerEnd ? null : span(lines, matchLine, end);
1336
1873
  }
@@ -1354,11 +1891,11 @@ var regexResolver = {
1354
1891
  const lines = source.split("\n");
1355
1892
  for (const tier of TIERS) {
1356
1893
  const pattern = tier(escaped);
1357
- let candidates = lines.map((line, index) => ({ line, index })).filter((entry) => pattern.test(entry.line)).map((entry) => entry.index);
1894
+ let candidates = lines.map((line, index2) => ({ line, index: index2 })).filter((entry) => pattern.test(entry.line)).map((entry) => entry.index);
1358
1895
  if (!candidates.length) continue;
1359
1896
  if (parentPattern && candidates.length > 1) {
1360
1897
  const distances = candidates.map(
1361
- (index) => distanceToParent(lines, index, parentPattern)
1898
+ (index2) => distanceToParent(lines, index2, parentPattern)
1362
1899
  );
1363
1900
  const nearest = Math.min(...distances);
1364
1901
  if (Number.isFinite(nearest)) {
@@ -1375,34 +1912,79 @@ var regexResolver = {
1375
1912
  function escapeRegExp(value) {
1376
1913
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1377
1914
  }
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;
1915
+ function distanceToParent(lines, index2, parent) {
1916
+ const floor = Math.max(0, index2 - PARENT_SCOPE_LINES);
1917
+ for (let at = index2; at >= floor; at--) {
1918
+ if (parent.test(lines[at] ?? "")) return index2 - at;
1382
1919
  }
1383
1920
  return Number.POSITIVE_INFINITY;
1384
1921
  }
1385
1922
  function hashAnchorText(text) {
1386
- return `sha256:${(0, import_node_crypto2.createHash)("sha256").update(text.replace(/\r\n/g, "\n")).digest("hex")}`;
1923
+ return `sha256:${(0, import_node_crypto4.createHash)("sha256").update(text.replace(/\r\n/g, "\n")).digest("hex")}`;
1387
1924
  }
1388
1925
  function resolveAnchor(source, anchor, resolver = regexResolver) {
1926
+ const outcome = resolveAnchorSpan(source, anchor, [resolver]);
1927
+ return outcome.ok ? outcome.span : null;
1928
+ }
1929
+ function resolveAnchorSpan(source, anchor, resolvers = [regexResolver]) {
1389
1930
  const normalized = source.replace(/\r\n/g, "\n");
1390
1931
  if (!anchor.symbol) {
1391
1932
  const lines = normalized.split("\n");
1392
1933
  if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
1393
1934
  return {
1394
- text: normalized,
1395
- startLine: 1,
1396
- endLine: Math.max(1, lines.length)
1935
+ ok: true,
1936
+ span: {
1937
+ text: normalized,
1938
+ startLine: 1,
1939
+ endLine: Math.max(1, lines.length)
1940
+ }
1941
+ };
1942
+ }
1943
+ for (const resolver of resolvers) {
1944
+ const attempt = resolver.attempt ? resolver.attempt(normalized, anchor.symbol, anchor.file) : fromResolve(resolver, normalized, anchor.symbol, anchor.file);
1945
+ if (attempt.kind === "abstain") continue;
1946
+ if (attempt.kind === "unresolved") {
1947
+ if (attempt.reason === "symbol-not-found") continue;
1948
+ return { ok: false, reason: attempt.reason };
1949
+ }
1950
+ return {
1951
+ ok: true,
1952
+ span: attempt.span,
1953
+ ...isResolverName(resolver.name) ? { resolver: resolver.name } : {}
1397
1954
  };
1398
1955
  }
1399
- return resolver.resolve(normalized, anchor.symbol);
1956
+ return { ok: false, reason: "symbol-not-found" };
1957
+ }
1958
+ function fromResolve(resolver, source, symbol, file) {
1959
+ const span2 = resolver.resolve(source, symbol, file);
1960
+ return span2 ? { kind: "resolved", span: span2 } : { kind: "unresolved", reason: "symbol-not-found" };
1961
+ }
1962
+ function isResolverName(name) {
1963
+ return name === "tree-sitter" || name === "regex";
1964
+ }
1965
+ async function prepareResolvers(resolvers, files) {
1966
+ for (const resolver of resolvers) await resolver.prepare?.(files);
1967
+ }
1968
+ function defaultAnchorResolvers(grammars = {}) {
1969
+ return [new TreeSitterResolver(grammars), regexResolver];
1970
+ }
1971
+ function resolverChanged(source, anchor, produced) {
1972
+ const previous = anchor.resolver ?? "regex";
1973
+ if (!produced || !anchor.symbol || previous === produced) return false;
1974
+ if (previous !== "regex") return false;
1975
+ const before = regexResolver.resolve(
1976
+ source.replace(/\r\n/g, "\n"),
1977
+ anchor.symbol
1978
+ );
1979
+ return before !== null && hashAnchorText(before.text) === anchor.hash;
1400
1980
  }
1401
1981
 
1402
1982
  // src/anchor-resolver/drift.ts
1403
1983
  async function detectAnchorDrift(records, options = {}) {
1404
1984
  const repoRoot = options.repoRoot ?? process.cwd();
1405
- const resolver = options.resolver ?? regexResolver;
1985
+ const resolvers = options.resolvers ?? (options.resolver ? [options.resolver] : defaultAnchorResolvers({
1986
+ offline: options.remote?.offline === true
1987
+ }));
1406
1988
  const origin = new LazyOrigin(repoRoot);
1407
1989
  const planned = /* @__PURE__ */ new Map();
1408
1990
  let declaresRepo = false;
@@ -1440,12 +2022,16 @@ async function detectAnchorDrift(records, options = {}) {
1440
2022
  ),
1441
2023
  (options.readRemote ?? readRemoteAnchors)(wants, options.remote ?? {})
1442
2024
  ]);
2025
+ await prepareResolvers(resolvers, [
2026
+ ...files,
2027
+ ...wants.map((want) => want.file)
2028
+ ]);
1443
2029
  const drift = /* @__PURE__ */ new Map();
1444
2030
  for (const record of records) {
1445
2031
  const entries = [];
1446
2032
  for (const { anchor, foreign } of planned.get(record.conceptId) ?? []) {
1447
2033
  entries.push(
1448
- foreign ? remoteEntry(anchor, remote, resolver) : localEntry(anchor, reads.get(anchor.file), resolver)
2034
+ foreign ? remoteEntry(anchor, remote, resolvers) : localEntry(anchor, reads.get(anchor.file), resolvers)
1449
2035
  );
1450
2036
  }
1451
2037
  if (entries.length) drift.set(record.conceptId, entries);
@@ -1474,12 +2060,22 @@ function unresolved(anchor, reason, repo) {
1474
2060
  ...repo ? { repo } : {}
1475
2061
  };
1476
2062
  }
1477
- function hashIn(source, anchor, resolver) {
1478
- const resolved = resolveAnchor(source, anchor, resolver);
1479
- if (!resolved) return null;
2063
+ function hashIn(source, anchor, resolvers) {
2064
+ const outcome = resolveAnchorSpan(source, anchor, resolvers);
2065
+ if (!outcome.ok) return { ok: false, reason: outcome.reason };
2066
+ return {
2067
+ ok: true,
2068
+ current: {
2069
+ hash: hashAnchorText(outcome.span.text),
2070
+ lines: outcome.span.endLine - outcome.span.startLine + 1,
2071
+ ...outcome.resolver ? { resolver: outcome.resolver } : {}
2072
+ }
2073
+ };
2074
+ }
2075
+ function resolverExtras(source, anchor, current) {
1480
2076
  return {
1481
- hash: hashAnchorText(resolved.text),
1482
- lines: resolved.endLine - resolved.startLine + 1
2077
+ ...current.resolver ? { resolver: current.resolver } : {},
2078
+ ...current.hash !== anchor.hash && resolverChanged(source, anchor, current.resolver) ? { reason: "resolver-changed" } : {}
1483
2079
  };
1484
2080
  }
1485
2081
  function compared(anchor, current, extra = {}) {
@@ -1491,40 +2087,58 @@ function compared(anchor, current, extra = {}) {
1491
2087
  ...extra
1492
2088
  };
1493
2089
  }
1494
- function localEntry(anchor, read, resolver) {
2090
+ function localEntry(anchor, read, resolvers) {
1495
2091
  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");
2092
+ const found = hashIn(read.source, anchor, resolvers);
2093
+ if (!found.ok) return unresolved(anchor, found.reason);
2094
+ return compared(
2095
+ anchor,
2096
+ found.current,
2097
+ resolverExtras(read.source, anchor, found.current)
2098
+ );
1498
2099
  }
1499
- function remoteEntry(anchor, remote, resolver) {
2100
+ function remoteEntry(anchor, remote, resolvers) {
1500
2101
  const repo = anchor.repo;
1501
2102
  const key2 = normalizeRepoUrl(repo);
1502
2103
  const atDefault = remote.get(wantKey(key2, void 0, anchor.file));
1503
2104
  const primary = anchor.ref ? remote.get(wantKey(key2, anchor.ref, anchor.file)) : atDefault;
1504
2105
  if (!primary) return unresolved(anchor, "remote-unreachable", repo);
1505
2106
  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 });
2107
+ const found = hashIn(primary.source, anchor, resolvers);
2108
+ if (!found.ok) return unresolved(anchor, found.reason, repo);
2109
+ const current = found.current;
2110
+ const extras = resolverExtras(primary.source, anchor, current);
2111
+ if (!anchor.ref) return compared(anchor, current, { repo, ...extras });
1509
2112
  if (current.hash !== anchor.hash) {
1510
- return compared(anchor, current, { repo, remoteState: "drifted-from-ref" });
2113
+ return compared(anchor, current, {
2114
+ repo,
2115
+ ...extras,
2116
+ remoteState: "drifted-from-ref"
2117
+ });
1511
2118
  }
1512
- const head = atDefault?.ok ? hashIn(atDefault.source, anchor, resolver) : null;
1513
- return head && head.hash !== anchor.hash ? {
1514
- ...compared(anchor, head, { repo }),
2119
+ const head = atDefault?.ok ? hashIn(atDefault.source, anchor, resolvers) : null;
2120
+ return head?.ok && head.current.hash !== anchor.hash ? {
2121
+ ...compared(anchor, head.current, {
2122
+ repo,
2123
+ ...head.current.resolver ? { resolver: head.current.resolver } : {}
2124
+ }),
1515
2125
  state: "drifted",
1516
2126
  remoteState: "drifted-on-default"
1517
- } : compared(anchor, current, { repo, remoteState: "matches-ref" });
2127
+ } : compared(anchor, current, {
2128
+ repo,
2129
+ ...extras,
2130
+ remoteState: "matches-ref"
2131
+ });
1518
2132
  }
1519
2133
 
1520
2134
  // src/search-index.ts
1521
- var import_promises3 = require("fs/promises");
1522
- var import_node_path3 = require("path");
2135
+ var import_promises5 = require("fs/promises");
2136
+ var import_node_path6 = require("path");
1523
2137
 
1524
2138
  // src/kb-log.ts
1525
- var import_zod2 = require("zod");
2139
+ var import_zod3 = require("zod");
1526
2140
  var LOG_FILE = "log.jsonl";
1527
- var kbLogEntrySchema = import_zod2.z.object({
2141
+ var kbLogEntrySchema = import_zod3.z.object({
1528
2142
  // Validated, not just `min(1)`: `at` is a sort key (see `parseLog`
1529
2143
  // below), and a value that isn't actually chronological — a Unix
1530
2144
  // timestamp, a human-typed date, garbage — would sort wrong without
@@ -1533,12 +2147,12 @@ var kbLogEntrySchema = import_zod2.z.object({
1533
2147
  // and rejects everything else, including a non-`Z` offset — so a
1534
2148
  // malformed `at` is reported the same way a malformed line already is,
1535
2149
  // 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),
2150
+ at: import_zod3.z.iso.datetime(),
2151
+ by: import_zod3.z.string().min(1),
2152
+ operation: import_zod3.z.string().min(1),
2153
+ conceptId: import_zod3.z.string().min(1),
1540
2154
  /** Second concept id, where the operation relates two — supersession. */
1541
- target: import_zod2.z.string().min(1).optional()
2155
+ target: import_zod3.z.string().min(1).optional()
1542
2156
  }).strict();
1543
2157
  function renderLogEntry(entry) {
1544
2158
  return `${JSON.stringify(kbLogEntrySchema.parse(entry))}
@@ -1548,18 +2162,18 @@ function parseLog(raw) {
1548
2162
  const entries = [];
1549
2163
  const malformed = [];
1550
2164
  const seen = /* @__PURE__ */ new Set();
1551
- raw.split("\n").forEach((text, index) => {
2165
+ raw.split("\n").forEach((text, index2) => {
1552
2166
  if (!text.trim()) return;
1553
2167
  let value;
1554
2168
  try {
1555
2169
  value = JSON.parse(text);
1556
2170
  } catch {
1557
- malformed.push({ line: index + 1, text });
2171
+ malformed.push({ line: index2 + 1, text });
1558
2172
  return;
1559
2173
  }
1560
2174
  const parsed = kbLogEntrySchema.safeParse(value);
1561
2175
  if (!parsed.success) {
1562
- malformed.push({ line: index + 1, text });
2176
+ malformed.push({ line: index2 + 1, text });
1563
2177
  return;
1564
2178
  }
1565
2179
  const key2 = JSON.stringify(parsed.data);
@@ -1582,7 +2196,7 @@ async function searchBase(bundlePath2, query, options = {}) {
1582
2196
  let store = null;
1583
2197
  try {
1584
2198
  store = await qmd.createStore({
1585
- dbPath: (0, import_node_path3.join)(bundlePath2, SEARCH_INDEX_FILE),
2199
+ dbPath: (0, import_node_path6.join)(bundlePath2, SEARCH_INDEX_FILE),
1586
2200
  config: {
1587
2201
  collections: {
1588
2202
  [COLLECTION]: {
@@ -1617,7 +2231,7 @@ async function searchBase(bundlePath2, query, options = {}) {
1617
2231
  }
1618
2232
  }
1619
2233
  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);
2234
+ const indexAt = await (0, import_promises5.stat)((0, import_node_path6.join)(bundlePath2, SEARCH_INDEX_FILE)).then((s) => s.mtimeMs).catch(() => 0);
1621
2235
  if (!indexAt) return true;
1622
2236
  const { readdir: readdir2 } = await import("fs/promises");
1623
2237
  const names = (await readdir2(bundlePath2).catch(() => [])).filter(
@@ -1626,7 +2240,7 @@ async function isStale(bundlePath2) {
1626
2240
  let stale = false;
1627
2241
  await mapLimit(names, DEFAULT_IO_CONCURRENCY, async (name) => {
1628
2242
  if (stale) return;
1629
- const at = await (0, import_promises3.stat)((0, import_node_path3.join)(bundlePath2, name)).then((s) => s.mtimeMs).catch(() => 0);
2243
+ const at = await (0, import_promises5.stat)((0, import_node_path6.join)(bundlePath2, name)).then((s) => s.mtimeMs).catch(() => 0);
1630
2244
  if (at > indexAt) stale = true;
1631
2245
  });
1632
2246
  return stale;
@@ -2006,8 +2620,8 @@ function byRank(left, right) {
2006
2620
  ) || left.record.conceptId.localeCompare(right.record.conceptId);
2007
2621
  }
2008
2622
  function typeRank(record) {
2009
- const index = TYPE_PRIORITY.indexOf(record.frontmatter.type);
2010
- return index === -1 ? TYPE_PRIORITY.length : index;
2623
+ const index2 = TYPE_PRIORITY.indexOf(record.frontmatter.type);
2624
+ return index2 === -1 ? TYPE_PRIORITY.length : index2;
2011
2625
  }
2012
2626
 
2013
2627
  // src/catalog.ts
@@ -2216,7 +2830,7 @@ function appendUnionMergeLine(contents) {
2216
2830
  }
2217
2831
 
2218
2832
  // src/kb-store.ts
2219
- var KB_DIR = (0, import_node_path4.join)(".strauss", "kb");
2833
+ var KB_DIR = (0, import_node_path7.join)(".strauss", "kb");
2220
2834
  var STORE_OWNED = /* @__PURE__ */ new Set([INDEX_FILE, LOG_FILE, SEARCH_INDEX_FILE]);
2221
2835
  var DEFAULT_LOAD_BUDGET = 25e3;
2222
2836
  var KbStore = class {
@@ -2247,7 +2861,7 @@ var KbStore = class {
2247
2861
  const conceptId2 = `${input.type}.${input.slug}`;
2248
2862
  const root = this.root(bundlePath2);
2249
2863
  const target = this.recordPath(bundlePath2, conceptId2);
2250
- await (0, import_promises4.mkdir)(root, { recursive: true });
2864
+ await (0, import_promises6.mkdir)(root, { recursive: true });
2251
2865
  await this.publish(
2252
2866
  target,
2253
2867
  stringifyMarkdownWithFrontmatter(input.body, frontmatter),
@@ -2286,7 +2900,7 @@ var KbStore = class {
2286
2900
  const target = this.recordPath(bundlePath2, conceptId2);
2287
2901
  let raw;
2288
2902
  try {
2289
- raw = await (0, import_promises4.readFile)(target, "utf8");
2903
+ raw = await (0, import_promises6.readFile)(target, "utf8");
2290
2904
  } catch {
2291
2905
  return null;
2292
2906
  }
@@ -2303,7 +2917,7 @@ var KbStore = class {
2303
2917
  const root = this.root(bundlePath2);
2304
2918
  let names;
2305
2919
  try {
2306
- names = await (0, import_promises4.readdir)(root);
2920
+ names = await (0, import_promises6.readdir)(root);
2307
2921
  } catch {
2308
2922
  return [];
2309
2923
  }
@@ -2311,7 +2925,7 @@ var KbStore = class {
2311
2925
  const records = await mapLimit(
2312
2926
  wanted,
2313
2927
  DEFAULT_IO_CONCURRENCY,
2314
- async ({ name, conceptId: conceptId2 }) => this.parse(conceptId2, await (0, import_promises4.readFile)((0, import_node_path4.join)(root, name), "utf8"))
2928
+ async ({ name, conceptId: conceptId2 }) => this.parse(conceptId2, await (0, import_promises6.readFile)((0, import_node_path7.join)(root, name), "utf8"))
2315
2929
  );
2316
2930
  return records.filter((record) => record !== null);
2317
2931
  }
@@ -2468,7 +3082,7 @@ ${answer}
2468
3082
  if (found.length) return found;
2469
3083
  }
2470
3084
  const lowered = needle.toLowerCase();
2471
- return bundle.filter((record) => matches(record, lowered));
3085
+ return bundle.filter((record) => matches2(record, lowered));
2472
3086
  }
2473
3087
  /**
2474
3088
  * Anchor drift over the records about to be handed back. Like the search
@@ -2638,11 +3252,11 @@ ${answer}
2638
3252
  async readIndex(bundlePath2) {
2639
3253
  const root = this.root(bundlePath2);
2640
3254
  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(
3255
+ const stored = await (0, import_promises6.readFile)((0, import_node_path7.join)(root, INDEX_FILE), "utf8").catch(
2642
3256
  () => null
2643
3257
  );
2644
3258
  if (indexIsStale(stored, expected)) {
2645
- await this.publish((0, import_node_path4.join)(root, INDEX_FILE), expected, true, INDEX_FILE);
3259
+ await this.publish((0, import_node_path7.join)(root, INDEX_FILE), expected, true, INDEX_FILE);
2646
3260
  this.logger.info?.({
2647
3261
  operation: "kb.index.repair",
2648
3262
  bundlePath: root,
@@ -2659,8 +3273,8 @@ ${answer}
2659
3273
  * knows which agent touched what. So a bad line is surfaced and left alone.
2660
3274
  */
2661
3275
  async readLog(bundlePath2) {
2662
- const raw = await (0, import_promises4.readFile)(
2663
- (0, import_node_path4.join)(this.root(bundlePath2), LOG_FILE),
3276
+ const raw = await (0, import_promises6.readFile)(
3277
+ (0, import_node_path7.join)(this.root(bundlePath2), LOG_FILE),
2664
3278
  "utf8"
2665
3279
  ).catch(() => "");
2666
3280
  const result = parseLog(raw);
@@ -2711,14 +3325,14 @@ ${answer}
2711
3325
  }
2712
3326
  async mutate(bundlePath2, conceptId2, change, entry, changeBody = (body) => body) {
2713
3327
  const target = this.recordPath(bundlePath2, conceptId2);
2714
- const before = await (0, import_promises4.readFile)(target, "utf8").catch(() => null);
3328
+ const before = await (0, import_promises6.readFile)(target, "utf8").catch(() => null);
2715
3329
  if (before === null) throw new KbRecordNotFoundError(conceptId2);
2716
3330
  const parsed = this.parse(conceptId2, before);
2717
3331
  if (!parsed) throw new KbRecordNotFoundError(conceptId2);
2718
3332
  const frontmatter = change(parsed.frontmatter);
2719
3333
  const body = changeBody(parsed.body);
2720
3334
  const contents = stringifyMarkdownWithFrontmatter(body, frontmatter);
2721
- const witness = await (0, import_promises4.readFile)(target, "utf8").catch(() => null);
3335
+ const witness = await (0, import_promises6.readFile)(target, "utf8").catch(() => null);
2722
3336
  if (witness === null || sha256(witness) !== sha256(before)) {
2723
3337
  throw new KbWriteConflictError(conceptId2);
2724
3338
  }
@@ -2744,20 +3358,20 @@ ${answer}
2744
3358
  */
2745
3359
  async publish(target, contents, overwrite, conceptId2) {
2746
3360
  const staging = `${target}.${process.pid}.tmp`;
2747
- await (0, import_promises4.writeFile)(staging, contents, "utf8");
3361
+ await (0, import_promises6.writeFile)(staging, contents, "utf8");
2748
3362
  try {
2749
3363
  if (overwrite) {
2750
- await (0, import_promises4.rename)(staging, target);
3364
+ await (0, import_promises6.rename)(staging, target);
2751
3365
  return;
2752
3366
  }
2753
- await (0, import_promises4.link)(staging, target);
3367
+ await (0, import_promises6.link)(staging, target);
2754
3368
  } catch (error) {
2755
3369
  if (error.code === "EEXIST") {
2756
3370
  throw new KbRecordAlreadyExistsError(conceptId2);
2757
3371
  }
2758
3372
  throw error;
2759
3373
  } finally {
2760
- await (0, import_promises4.unlink)(staging).catch(() => void 0);
3374
+ await (0, import_promises6.unlink)(staging).catch(() => void 0);
2761
3375
  }
2762
3376
  }
2763
3377
  /**
@@ -2801,18 +3415,18 @@ ${answer}
2801
3415
  * file must not fail the mutation it guards.
2802
3416
  */
2803
3417
  async ensureGitattributes(root) {
2804
- const target = (0, import_node_path4.join)(root, GITATTRIBUTES_FILE);
3418
+ const target = (0, import_node_path7.join)(root, GITATTRIBUTES_FILE);
2805
3419
  try {
2806
3420
  let existing;
2807
3421
  try {
2808
- existing = await (0, import_promises4.readFile)(target, "utf8");
3422
+ existing = await (0, import_promises6.readFile)(target, "utf8");
2809
3423
  } catch (error) {
2810
3424
  if (error.code !== "ENOENT") throw error;
2811
3425
  existing = null;
2812
3426
  }
2813
3427
  if (existing === null) {
2814
3428
  try {
2815
- await (0, import_promises4.writeFile)(target, appendUnionMergeLine(""), {
3429
+ await (0, import_promises6.writeFile)(target, appendUnionMergeLine(""), {
2816
3430
  encoding: "utf8",
2817
3431
  flag: "wx"
2818
3432
  });
@@ -2833,7 +3447,7 @@ ${answer}
2833
3447
  return;
2834
3448
  }
2835
3449
  if (!hasMergeDeclaration(existing)) {
2836
- await (0, import_promises4.appendFile)(target, appendUnionMergeLine(existing), "utf8");
3450
+ await (0, import_promises6.appendFile)(target, appendUnionMergeLine(existing), "utf8");
2837
3451
  this.logger.info?.({
2838
3452
  operation: "kb.gitattributes.ensure",
2839
3453
  bundlePath: root,
@@ -2852,7 +3466,7 @@ ${answer}
2852
3466
  async record(root, entry) {
2853
3467
  await this.ensureGitattributes(root);
2854
3468
  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) => {
3469
+ await (0, import_promises6.appendFile)((0, import_node_path7.join)(root, LOG_FILE), line, "utf8").catch((error) => {
2856
3470
  this.logger.warn?.({
2857
3471
  operation: "kb.log.append",
2858
3472
  outcome: "failed",
@@ -2878,18 +3492,18 @@ ${answer}
2878
3492
  };
2879
3493
  }
2880
3494
  root(bundlePath2) {
2881
- return (0, import_node_path4.resolve)(bundlePath2);
3495
+ return (0, import_node_path7.resolve)(bundlePath2);
2882
3496
  }
2883
3497
  // Concept ids are `<type>.<slug>` and map to a single file directly under the
2884
3498
  // bundle root; anything carrying a separator would escape it.
2885
3499
  recordPath(bundlePath2, conceptId2) {
2886
- if (conceptId2.includes(import_node_path4.sep) || conceptId2.includes("/")) {
3500
+ if (conceptId2.includes(import_node_path7.sep) || conceptId2.includes("/")) {
2887
3501
  throw new KbInvalidConceptIdError(
2888
3502
  "concept id must not contain a path separator",
2889
3503
  { conceptId: conceptId2 }
2890
3504
  );
2891
3505
  }
2892
- return (0, import_node_path4.join)(this.root(bundlePath2), `${conceptId2}.md`);
3506
+ return (0, import_node_path7.join)(this.root(bundlePath2), `${conceptId2}.md`);
2893
3507
  }
2894
3508
  };
2895
3509
  function estimateTokens(record) {
@@ -2916,7 +3530,7 @@ function stub(hit) {
2916
3530
  at: hit.record.frontmatter.generated?.at ?? null
2917
3531
  };
2918
3532
  }
2919
- function matches(record, needle) {
3533
+ function matches2(record, needle) {
2920
3534
  const { title, description } = record.frontmatter;
2921
3535
  return [record.conceptId, title, description, record.body].some(
2922
3536
  (field) => field?.toLowerCase().includes(needle)
@@ -2929,37 +3543,37 @@ function normalizeActor(id) {
2929
3543
  }
2930
3544
 
2931
3545
  // src/compose.ts
2932
- var import_zod3 = require("zod");
2933
- var composeLinkSchema = import_zod3.z.object({
3546
+ var import_zod4 = require("zod");
3547
+ var composeLinkSchema = import_zod4.z.object({
2934
3548
  target: kbConceptIdSchema,
2935
- rel: import_zod3.z.enum(KB_LINK_RELS)
3549
+ rel: import_zod4.z.enum(KB_LINK_RELS)
2936
3550
  }).strict();
2937
- var composeInputSchema = import_zod3.z.object({
2938
- slug: import_zod3.z.string().min(1),
3551
+ var composeInputSchema = import_zod4.z.object({
3552
+ slug: import_zod4.z.string().min(1),
2939
3553
  /** One line, in the reader's terms. Becomes OKF `title`. */
2940
- title: import_zod3.z.string().min(1),
3554
+ title: import_zod4.z.string().min(1),
2941
3555
  /** The consequence — what breaks if this is wrong. Becomes `description`. */
2942
- why: import_zod3.z.string().min(1),
3556
+ why: import_zod4.z.string().min(1),
2943
3557
  /** 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(),
3558
+ sections: import_zod4.z.record(import_zod4.z.string(), import_zod4.z.string().min(1)).optional(),
3559
+ anchors: import_zod4.z.array(kbAnchorSchema).optional(),
3560
+ sources: import_zod4.z.array(kbSourceSchema).optional(),
2947
3561
  /** No source exists, as a claim rather than a sentinel in `sources`. */
2948
- assumption: import_zod3.z.boolean().optional(),
3562
+ assumption: import_zod4.z.boolean().optional(),
2949
3563
  /**
2950
3564
  * OKF `stale_after`: the absolute date this record stops being trusted.
2951
3565
  * Anything the outside world can change — pricing, quotas, versions,
2952
3566
  * reception counts — should carry one.
2953
3567
  */
2954
- stale_after: import_zod3.z.string().regex(/^\d{4}-\d{2}-\d{2}$/, {
3568
+ stale_after: import_zod4.z.string().regex(/^\d{4}-\d{2}-\d{2}$/, {
2955
3569
  message: "stale_after must be YYYY-MM-DD"
2956
3570
  }).refine((date) => !Number.isNaN(Date.parse(date)), {
2957
3571
  message: "stale_after must be a real date"
2958
3572
  }).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(),
3573
+ verify: import_zod4.z.array(import_zod4.z.string().min(1)).optional(),
3574
+ tags: import_zod4.z.array(import_zod4.z.string().min(1)).optional(),
2961
3575
  /** Concept ids this record relates to; rendered as body links. */
2962
- relatedConceptIds: import_zod3.z.array(kbConceptIdSchema).optional(),
3576
+ relatedConceptIds: import_zod4.z.array(kbConceptIdSchema).optional(),
2963
3577
  /**
2964
3578
  * Typed causal edges, source → target: `{ target: "fact.b", rel:
2965
3579
  * "depends_on" }` on record A says A needs B. Stored in frontmatter and
@@ -2967,16 +3581,16 @@ var composeInputSchema = import_zod3.z.object({
2967
3581
  * reader that knows only OKF. The vocabulary goes into the description from
2968
3582
  * the same table the walk uses, so `kb_schema` emits it.
2969
3583
  */
2970
- links: import_zod3.z.array(composeLinkSchema).max(64).optional().describe(
3584
+ links: import_zod4.z.array(composeLinkSchema).max(64).optional().describe(
2971
3585
  `Typed causal edges, source \u2192 target \u2014 a link on this record says this record <rel> the target. ${KB_LINK_RELS.map(
2972
3586
  (rel) => `${rel}: ${LINK_RELS[rel].purpose}`
2973
3587
  ).join("; ")}.`
2974
3588
  ),
2975
3589
  /** 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()
3590
+ supersedes: import_zod4.z.array(kbConceptIdSchema).max(32).optional(),
3591
+ materiality: import_zod4.z.enum(KB_MATERIALITIES).optional(),
3592
+ confidence: import_zod4.z.enum(KB_CONFIDENCES).optional(),
3593
+ owner: import_zod4.z.string().min(1).optional()
2980
3594
  }).strict();
2981
3595
  function composeRecord(type, input, writtenBy, writtenAt) {
2982
3596
  const parsed = composeInputSchema.parse(input);
@@ -3053,9 +3667,9 @@ ${text}`);
3053
3667
  // src/kb-pins/budgets.ts
3054
3668
  function asBudgets(value) {
3055
3669
  if (value === null || typeof value !== "object") return {};
3056
- const table = value;
3670
+ const table2 = value;
3057
3671
  const pick = (key2, min) => {
3058
- const raw = table[key2];
3672
+ const raw = table2[key2];
3059
3673
  return typeof raw === "number" && Number.isInteger(raw) && raw >= min ? raw : void 0;
3060
3674
  };
3061
3675
  const budgetTokens = pick("budgetTokens", 1);
@@ -3066,9 +3680,9 @@ function asBudgets(value) {
3066
3680
  };
3067
3681
  }
3068
3682
  function contextProfileBudgets(manifest, profile) {
3069
- const table = manifest.context;
3070
- if (table === null || typeof table !== "object") return {};
3071
- const entries = table;
3683
+ const table2 = manifest.context;
3684
+ if (table2 === null || typeof table2 !== "object") return {};
3685
+ const entries = table2;
3072
3686
  return {
3073
3687
  ...asBudgets(entries["default"]),
3074
3688
  ...profile ? asBudgets(entries[profile]) : {}
@@ -3099,23 +3713,23 @@ var KbBaseFrozenError = class extends Error {
3099
3713
  };
3100
3714
 
3101
3715
  // src/kb-pins/frozen.ts
3102
- var import_node_path7 = require("path");
3716
+ var import_node_path10 = require("path");
3103
3717
 
3104
3718
  // 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");
3719
+ var import_promises7 = require("fs/promises");
3720
+ var import_node_os3 = require("os");
3721
+ var import_node_path9 = require("path");
3108
3722
 
3109
3723
  // 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");
3724
+ var import_node_path8 = require("path");
3725
+ var import_zod5 = require("zod");
3726
+ var PINS_FILE = (0, import_node_path8.join)(".strauss", "kb-pins.json");
3727
+ var PINS_LOCAL_FILE = (0, import_node_path8.join)(".strauss", "kb-pins.local.json");
3114
3728
  var PIN_LAYERS = ["project", "local", "user"];
3115
- var pinSchema = import_zod4.z.object({
3729
+ var pinSchema = import_zod5.z.object({
3116
3730
  /** 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(),
3731
+ path: import_zod5.z.string().min(1),
3732
+ pinnedAt: import_zod5.z.string().min(1).optional(),
3119
3733
  /**
3120
3734
  * How `context` renders this base. `full` preloads the whole base into
3121
3735
  * the block regardless of the full-under threshold — for a base whose
@@ -3125,7 +3739,7 @@ var pinSchema = import_zod4.z.object({
3125
3739
  * Absent: the profile's full-under threshold decides. Invalid values
3126
3740
  * degrade to absent rather than failing the manifest.
3127
3741
  */
3128
- mode: import_zod4.z.enum(["full", "index"]).optional().catch(void 0),
3742
+ mode: import_zod5.z.enum(["full", "index"]).optional().catch(void 0),
3129
3743
  /**
3130
3744
  * Context profiles this pin surfaces in (e.g. only at session-start,
3131
3745
  * not per turn). Absent: every profile. A run without a profile sees
@@ -3133,17 +3747,17 @@ var pinSchema = import_zod4.z.object({
3133
3747
  * that skill at point of use than pinned at all — pins are what every
3134
3748
  * session should see.
3135
3749
  */
3136
- profiles: import_zod4.z.array(import_zod4.z.string()).optional().catch(void 0),
3750
+ profiles: import_zod5.z.array(import_zod5.z.string()).optional().catch(void 0),
3137
3751
  /**
3138
3752
  * The base is concluded — a finished piece of research, a frozen ADR
3139
3753
  * set. Write commands against it refuse while this workspace holds the
3140
3754
  * pin, and `context` labels it read-only. Workspace policy, not base
3141
3755
  * state: the base itself stays copyable and writable elsewhere.
3142
3756
  */
3143
- frozen: import_zod4.z.boolean().optional().catch(void 0)
3757
+ frozen: import_zod5.z.boolean().optional().catch(void 0)
3144
3758
  }).passthrough();
3145
- var pinsManifestSchema = import_zod4.z.object({
3146
- pins: import_zod4.z.array(pinSchema).default([]),
3759
+ var pinsManifestSchema = import_zod5.z.object({
3760
+ pins: import_zod5.z.array(pinSchema).default([]),
3147
3761
  /**
3148
3762
  * Per-repo budgets for the `context` command, keyed by profile —
3149
3763
  * `"session-start"`, `"compact"`, `"turn"`, or `"default"` for all of
@@ -3152,18 +3766,18 @@ var pinsManifestSchema = import_zod4.z.object({
3152
3766
  * the index at every session start. `contextProfileBudgets` does the
3153
3767
  * tolerant read.
3154
3768
  */
3155
- context: import_zod4.z.unknown().optional()
3769
+ context: import_zod5.z.unknown().optional()
3156
3770
  }).passthrough();
3157
3771
 
3158
3772
  // src/kb-pins/layers.ts
3159
3773
  function userRoot() {
3160
- return process.env.STRAUSS_KB_USER_ROOT || (0, import_node_os2.homedir)();
3774
+ return process.env.STRAUSS_KB_USER_ROOT || (0, import_node_os3.homedir)();
3161
3775
  }
3162
3776
  function layerRoot(workspaceDir, layer) {
3163
- return layer === "user" ? userRoot() : (0, import_node_path6.resolve)(workspaceDir);
3777
+ return layer === "user" ? userRoot() : (0, import_node_path9.resolve)(workspaceDir);
3164
3778
  }
3165
3779
  function layerFile(workspaceDir, layer) {
3166
- return (0, import_node_path6.join)(
3780
+ return (0, import_node_path9.join)(
3167
3781
  layerRoot(workspaceDir, layer),
3168
3782
  layer === "local" ? PINS_LOCAL_FILE : PINS_FILE
3169
3783
  );
@@ -3172,7 +3786,7 @@ async function readPinsLayer(workspaceDir, layer) {
3172
3786
  const file = layerFile(workspaceDir, layer);
3173
3787
  let raw;
3174
3788
  try {
3175
- raw = await (0, import_promises5.readFile)(file, "utf8");
3789
+ raw = await (0, import_promises7.readFile)(file, "utf8");
3176
3790
  } catch {
3177
3791
  return { pins: [] };
3178
3792
  }
@@ -3196,16 +3810,16 @@ async function readPinsLayer(workspaceDir, layer) {
3196
3810
  }
3197
3811
  async function writePinsLayer(workspaceDir, layer, manifest) {
3198
3812
  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)}
3813
+ await (0, import_promises7.mkdir)((0, import_node_path9.dirname)(file), { recursive: true });
3814
+ await (0, import_promises7.writeFile)(file, `${JSON.stringify(manifest, null, 2)}
3201
3815
  `, "utf8");
3202
3816
  }
3203
3817
  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));
3818
+ return (0, import_node_path9.isAbsolute)(path) ? (0, import_node_path9.resolve)(path) : (0, import_node_path9.resolve)(rootDir, path.split("/").join(import_node_path9.sep));
3205
3819
  }
3206
3820
  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("/");
3821
+ const rel = (0, import_node_path9.relative)((0, import_node_path9.resolve)(rootDir), (0, import_node_path9.resolve)(bundlePath2));
3822
+ return (rel === "" ? "." : rel).split(import_node_path9.sep).join("/");
3209
3823
  }
3210
3824
  async function readMergedPins(workspaceDir) {
3211
3825
  const manifests = {};
@@ -3233,7 +3847,7 @@ async function readMergedPins(workspaceDir) {
3233
3847
  // src/kb-pins/frozen.ts
3234
3848
  async function assertBaseNotFrozen(workspaceDir, bundlePath2) {
3235
3849
  const merged = await readMergedPins(workspaceDir);
3236
- const absolute = (0, import_node_path7.resolve)(bundlePath2);
3850
+ const absolute = (0, import_node_path10.resolve)(bundlePath2);
3237
3851
  const pin = merged.pins.find((entry) => entry.absolutePath === absolute);
3238
3852
  if (pin?.frozen === true) {
3239
3853
  throw new KbBaseFrozenError(pin.path, pin.layer);
@@ -3318,7 +3932,7 @@ async function pinBase(store, workspaceDir, bundlePath2, at, options = {}) {
3318
3932
  }
3319
3933
 
3320
3934
  // src/kb-pins/unpin.ts
3321
- var import_node_path8 = require("path");
3935
+ var import_node_path11 = require("path");
3322
3936
  async function unpinBase(workspaceDir, bundlePath2) {
3323
3937
  const layers = [];
3324
3938
  for (const layer of PIN_LAYERS) {
@@ -3339,14 +3953,14 @@ async function unpinBase(workspaceDir, bundlePath2) {
3339
3953
  }
3340
3954
  }
3341
3955
  return {
3342
- path: storablePath((0, import_node_path8.resolve)(workspaceDir), bundlePath2),
3956
+ path: storablePath((0, import_node_path11.resolve)(workspaceDir), bundlePath2),
3343
3957
  removed: layers.length > 0,
3344
3958
  layers
3345
3959
  };
3346
3960
  }
3347
3961
 
3348
3962
  // src/kb-context.ts
3349
- var import_promises6 = require("fs/promises");
3963
+ var import_promises8 = require("fs/promises");
3350
3964
  var HEADING2 = "## Knowledge bases (pinned)";
3351
3965
  var DEFAULT_CONTEXT_BUDGET = 4e3;
3352
3966
  var CONTEXT_PROFILES = {
@@ -3550,13 +4164,13 @@ function toHookJson(block, event) {
3550
4164
  var CONTEXT_BEGIN = "<!-- strauss-kb:begin -->";
3551
4165
  var CONTEXT_END = "<!-- strauss-kb:end -->";
3552
4166
  async function syncInstructions(file, block) {
3553
- const existing = await (0, import_promises6.readFile)(file, "utf8").catch(() => null);
4167
+ const existing = await (0, import_promises8.readFile)(file, "utf8").catch(() => null);
3554
4168
  const region = block ? `${CONTEXT_BEGIN}
3555
4169
  ${block.trim()}
3556
4170
  ${CONTEXT_END}` : null;
3557
4171
  if (existing === null) {
3558
4172
  if (!region) return { file, action: "unchanged" };
3559
- await (0, import_promises6.writeFile)(file, `${region}
4173
+ await (0, import_promises8.writeFile)(file, `${region}
3560
4174
  `, "utf8");
3561
4175
  return { file, action: "created" };
3562
4176
  }
@@ -3567,11 +4181,11 @@ ${CONTEXT_END}` : null;
3567
4181
  const after = existing.slice(end + CONTEXT_END.length);
3568
4182
  const next = region ? `${before}${region}${after}` : `${before.replace(/\n+$/, "\n")}${after.replace(/^\n+/, "\n")}`;
3569
4183
  if (next === existing) return { file, action: "unchanged" };
3570
- await (0, import_promises6.writeFile)(file, next, "utf8");
4184
+ await (0, import_promises8.writeFile)(file, next, "utf8");
3571
4185
  return { file, action: region ? "replaced" : "removed" };
3572
4186
  }
3573
4187
  if (!region) return { file, action: "unchanged" };
3574
- await (0, import_promises6.writeFile)(
4188
+ await (0, import_promises8.writeFile)(
3575
4189
  file,
3576
4190
  `${existing.replace(/\n*$/, "\n\n")}${region}
3577
4191
  `,
@@ -3581,14 +4195,14 @@ ${CONTEXT_END}` : null;
3581
4195
  }
3582
4196
 
3583
4197
  // src/json-schema.ts
3584
- var import_zod5 = require("zod");
4198
+ var import_zod6 = require("zod");
3585
4199
  function kbJsonSchemas() {
3586
4200
  return {
3587
- recordFrontmatter: import_zod5.z.toJSONSchema(kbRecordFrontmatterSchema, {
4201
+ recordFrontmatter: import_zod6.z.toJSONSchema(kbRecordFrontmatterSchema, {
3588
4202
  io: "input"
3589
4203
  }),
3590
- composeInput: import_zod5.z.toJSONSchema(composeInputSchema, { io: "input" }),
3591
- logEntry: import_zod5.z.toJSONSchema(kbLogEntrySchema, { io: "input" })
4204
+ composeInput: import_zod6.z.toJSONSchema(composeInputSchema, { io: "input" }),
4205
+ logEntry: import_zod6.z.toJSONSchema(kbLogEntrySchema, { io: "input" })
3592
4206
  };
3593
4207
  }
3594
4208
 
@@ -3598,7 +4212,7 @@ function matchToDiff(files, records, options = {}) {
3598
4212
  const anchored = records.filter(
3599
4213
  (record) => (record.frontmatter.strauss_anchors ?? []).length > 0
3600
4214
  );
3601
- const matches2 = [];
4215
+ const matches3 = [];
3602
4216
  for (const file of files) {
3603
4217
  const candidates = anchored.map((record) => ({
3604
4218
  record,
@@ -3617,7 +4231,7 @@ function matchToDiff(files, records, options = {}) {
3617
4231
  hits.push(record);
3618
4232
  }
3619
4233
  if (!hits.length) continue;
3620
- matches2.push({
4234
+ matches3.push({
3621
4235
  filePath: file.filePath,
3622
4236
  hunk,
3623
4237
  records: order(adjudicate(hits, records, options.now)),
@@ -3625,7 +4239,7 @@ function matchToDiff(files, records, options = {}) {
3625
4239
  });
3626
4240
  }
3627
4241
  }
3628
- return matches2;
4242
+ return matches3;
3629
4243
  }
3630
4244
  function place(anchors, filePath, hunk, ranges) {
3631
4245
  let fallback = "miss";
@@ -3774,6 +4388,18 @@ var CHECK_HEADLINES = {
3774
4388
  unchecked: "an anchor in another repository nothing could reach"
3775
4389
  };
3776
4390
  var DAY_MS = 864e5;
4391
+ function anchorResolverCounts(bundle) {
4392
+ let treeSitter = 0;
4393
+ let regex = 0;
4394
+ for (const record of bundle) {
4395
+ for (const anchor of record.frontmatter.strauss_anchors ?? []) {
4396
+ if (!anchor.hash || !anchor.symbol) continue;
4397
+ if (anchor.resolver === "tree-sitter") treeSitter += 1;
4398
+ else regex += 1;
4399
+ }
4400
+ }
4401
+ return { total: treeSitter + regex, treeSitter, regex };
4402
+ }
3777
4403
  function doctor(bundle, options = {}) {
3778
4404
  const thresholds = {
3779
4405
  expiringDays: options.expiringDays ?? DEFAULT_EXPIRING_DAYS,
@@ -3809,6 +4435,7 @@ function doctor(bundle, options = {}) {
3809
4435
  counts,
3810
4436
  groups,
3811
4437
  findingCount,
4438
+ anchorResolvers: anchorResolverCounts(bundle),
3812
4439
  healthy: findingCount === 0
3813
4440
  };
3814
4441
  }
@@ -4055,12 +4682,12 @@ function ageInDays(record, now) {
4055
4682
  }
4056
4683
 
4057
4684
  // src/decision-record.ts
4058
- var import_zod6 = require("zod");
4685
+ var import_zod7 = require("zod");
4059
4686
  var DECISION_TYPE = "decision";
4060
4687
  var NO_DECISION_SLUG = "none";
4061
4688
  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()
4689
+ alternative: import_zod7.z.string().min(1).optional(),
4690
+ impact: import_zod7.z.string().min(1).optional()
4064
4691
  }).strict();
4065
4692
  function composeDecisionRecord(input, writtenBy, writtenAt) {
4066
4693
  const { alternative, impact: impact2, ...rest } = input;
@@ -4102,13 +4729,13 @@ function selectDecisions(records) {
4102
4729
  }
4103
4730
 
4104
4731
  // src/commands/anchor-resolve.ts
4105
- var import_zod8 = require("zod");
4732
+ var import_zod9 = require("zod");
4106
4733
 
4107
4734
  // 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(
4735
+ var import_zod8 = require("zod");
4736
+ var bundlePath = import_zod8.z.string().min(1).describe("Absolute path to the knowledge base directory.");
4737
+ var conceptId = import_zod8.z.string().min(1).describe("e.g. decision.cursor-v2");
4738
+ var REPO_ROOT = import_zod8.z.string().min(1).optional().describe(
4112
4739
  "Where the anchored source lives, for the drift check. Defaults to the working directory."
4113
4740
  );
4114
4741
  function define(command) {
@@ -4131,22 +4758,30 @@ function argvFlag(argv, name) {
4131
4758
  }
4132
4759
 
4133
4760
  // src/commands/anchor-resolve.ts
4761
+ function resolverSummary(results) {
4762
+ const names = [
4763
+ ...new Set(
4764
+ results.flatMap((entry) => entry.resolver ? [entry.resolver] : [])
4765
+ )
4766
+ ].sort();
4767
+ return names.length ? `${names.join(" + ")} resolver` : "whole-file";
4768
+ }
4134
4769
  var anchorResolveCommand = define({
4135
4770
  name: "anchor-resolve",
4136
4771
  tool: "kb_anchor_resolve",
4137
4772
  usage: "anchor-resolve <concept-id> [--repo-root <path>] [--offline] [--rebaseline] [--restamp]",
4138
4773
  description: "Resolve a record's anchors: stamp a hash onto anchors that lack one, report drift where the code moved. An anchor naming another repository is read from that remote through a bare cache; --offline uses the cache only. kb_verify's mechanical counterpart \u2014 reach for it when the question is whether the code still is what it was. Exits non-zero on drift.",
4139
- input: import_zod8.z.object({
4774
+ input: import_zod9.z.object({
4140
4775
  bundlePath,
4141
4776
  conceptId,
4142
- repoRoot: import_zod8.z.string().min(1).optional(),
4143
- offline: import_zod8.z.boolean().optional().describe(
4777
+ repoRoot: import_zod9.z.string().min(1).optional(),
4778
+ offline: import_zod9.z.boolean().optional().describe(
4144
4779
  "Resolve foreign anchors from the local repo cache only, never fetching."
4145
4780
  ),
4146
- rebaseline: import_zod8.z.boolean().optional().describe(
4781
+ rebaseline: import_zod9.z.boolean().optional().describe(
4147
4782
  "Accept the current code as the new baseline for anchors that drifted."
4148
4783
  ),
4149
- restamp: import_zod8.z.boolean().optional().describe(
4784
+ restamp: import_zod9.z.boolean().optional().describe(
4150
4785
  "Refresh `resolved_at` on anchors that already match. Off by default, so a green run writes nothing."
4151
4786
  )
4152
4787
  }),
@@ -4175,6 +4810,11 @@ var anchorResolveCommand = define({
4175
4810
  const updated = [];
4176
4811
  let dirty = false;
4177
4812
  const sources = await readSources(anchors, root, offline === true);
4813
+ const resolvers = defaultAnchorResolvers({ offline: offline === true });
4814
+ await prepareResolvers(
4815
+ resolvers,
4816
+ anchors.map((anchor) => anchor.file)
4817
+ );
4178
4818
  for (const anchor of anchors) {
4179
4819
  const base2 = {
4180
4820
  file: anchor.file,
@@ -4191,27 +4831,35 @@ var anchorResolveCommand = define({
4191
4831
  updated.push(anchor);
4192
4832
  continue;
4193
4833
  }
4194
- const resolved = resolveAnchor(source.source, anchor);
4195
- if (!resolved) {
4834
+ const outcome = resolveAnchorSpan(source.source, anchor, resolvers);
4835
+ if (!outcome.ok) {
4196
4836
  results.push({
4197
4837
  ...base2,
4198
4838
  state: "unresolved",
4199
- reason: "symbol-not-found"
4839
+ reason: outcome.reason
4200
4840
  });
4201
4841
  updated.push(anchor);
4202
4842
  continue;
4203
4843
  }
4844
+ const resolved = outcome.span;
4845
+ const producedBy = outcome.resolver;
4204
4846
  const currentHash = hashAnchorText(resolved.text);
4205
4847
  const currentLines = resolved.endLine - resolved.startLine + 1;
4206
4848
  const stamped = {
4207
4849
  ...anchor,
4208
4850
  hash: currentHash,
4209
4851
  lines: currentLines,
4210
- resolved_at: now()
4852
+ resolved_at: now(),
4853
+ ...producedBy ? { resolver: producedBy } : {}
4211
4854
  };
4212
4855
  const pinned = anchor.ref !== void 0 && source.repo !== void 0;
4213
4856
  if (!anchor.hash) {
4214
- results.push({ ...base2, state: "stamped", currentHash });
4857
+ results.push({
4858
+ ...base2,
4859
+ state: "stamped",
4860
+ currentHash,
4861
+ ...producedBy ? { resolver: producedBy } : {}
4862
+ });
4215
4863
  updated.push(stamped);
4216
4864
  dirty = true;
4217
4865
  continue;
@@ -4222,6 +4870,10 @@ var anchorResolveCommand = define({
4222
4870
  state: "drifted",
4223
4871
  currentHash,
4224
4872
  diffSize: lineDelta(anchor, currentLines),
4873
+ ...producedBy ? { resolver: producedBy } : {},
4874
+ // A regex-stamped anchor re-read by tree-sitter drifts because the
4875
+ // resolver changed, not because the code did.
4876
+ ...resolverChanged(source.source, anchor, producedBy) ? { reason: "resolver-changed" } : {},
4225
4877
  ...pinned ? { remoteState: "drifted-from-ref" } : {},
4226
4878
  ...rebaseline ? { rebaselined: true } : {}
4227
4879
  });
@@ -4229,7 +4881,7 @@ var anchorResolveCommand = define({
4229
4881
  if (rebaseline) dirty = true;
4230
4882
  continue;
4231
4883
  }
4232
- const onDefault = pinned ? headHash(source, anchor) : void 0;
4884
+ const onDefault = pinned ? headHash(source, anchor, resolvers) : void 0;
4233
4885
  if (onDefault && onDefault.hash !== anchor.hash) {
4234
4886
  results.push({
4235
4887
  ...base2,
@@ -4245,6 +4897,7 @@ var anchorResolveCommand = define({
4245
4897
  ...base2,
4246
4898
  state: "match",
4247
4899
  currentHash,
4900
+ ...producedBy ? { resolver: producedBy } : {},
4248
4901
  ...pinned ? { remoteState: "matches-ref" } : {}
4249
4902
  });
4250
4903
  const refresh = restamp || anchor.resolved_at === void 0;
@@ -4262,19 +4915,21 @@ var anchorResolveCommand = define({
4262
4915
  if (!frozen) await store.updateAnchors(path, id, updated, actor);
4263
4916
  }
4264
4917
  const frozenNote = frozen ? { frozen: true, note: "base is frozen: nothing was stamped" } : {};
4918
+ const hints = grammarHints();
4919
+ const hintNote = hints.length ? { hints } : {};
4265
4920
  const unreachable = results.filter(
4266
4921
  (entry) => isUncheckedReason(entry.reason)
4267
4922
  ).length;
4268
4923
  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;
4924
+ const matches3 = results.filter((entry) => entry.state === "match").length;
4925
+ const note = `${matches3}/${checked} anchors match${unreachable ? `, ${unreachable} unreachable` : ""}`;
4926
+ const clean = checked > 0 && matches3 === checked && unreachable === 0;
4272
4927
  if (clean) {
4273
4928
  try {
4274
4929
  await store.verify(
4275
4930
  path,
4276
4931
  id,
4277
- `anchor-resolve: ${note} (regex resolver)`,
4932
+ `anchor-resolve: ${note} (${resolverSummary(results)})`,
4278
4933
  actor,
4279
4934
  now()
4280
4935
  );
@@ -4285,17 +4940,25 @@ var anchorResolveCommand = define({
4285
4940
  results,
4286
4941
  verified: false,
4287
4942
  verifyRefused: "self-verification",
4288
- ...frozenNote
4943
+ ...frozenNote,
4944
+ ...hintNote
4289
4945
  };
4290
4946
  }
4291
- return { conceptId: id, results, verified: true, ...frozenNote };
4947
+ return {
4948
+ conceptId: id,
4949
+ results,
4950
+ verified: true,
4951
+ ...frozenNote,
4952
+ ...hintNote
4953
+ };
4292
4954
  }
4293
4955
  return {
4294
4956
  conceptId: id,
4295
4957
  results,
4296
4958
  verified: false,
4297
4959
  ...unreachable ? { note } : {},
4298
- ...frozenNote
4960
+ ...frozenNote,
4961
+ ...hintNote
4299
4962
  };
4300
4963
  },
4301
4964
  // A stored hash that no longer resolves is a broken anchor, not an absence:
@@ -4311,13 +4974,13 @@ var anchorResolveCommand = define({
4311
4974
  function lineDelta(anchor, current) {
4312
4975
  return anchor.lines === void 0 ? null : Math.abs(current - anchor.lines);
4313
4976
  }
4314
- function headHash(source, anchor) {
4977
+ function headHash(source, anchor, resolvers) {
4315
4978
  if (source.head === void 0) return void 0;
4316
- const resolved = resolveAnchor(source.head, anchor);
4317
- if (!resolved) return void 0;
4979
+ const outcome = resolveAnchorSpan(source.head, anchor, resolvers);
4980
+ if (!outcome.ok) return void 0;
4318
4981
  return {
4319
- hash: hashAnchorText(resolved.text),
4320
- lines: resolved.endLine - resolved.startLine + 1
4982
+ hash: hashAnchorText(outcome.span.text),
4983
+ lines: outcome.span.endLine - outcome.span.startLine + 1
4321
4984
  };
4322
4985
  }
4323
4986
  async function readSources(anchors, root, offline) {
@@ -4367,13 +5030,13 @@ async function readSources(anchors, root, offline) {
4367
5030
  }
4368
5031
 
4369
5032
  // src/commands/answer.ts
4370
- var import_zod9 = require("zod");
5033
+ var import_zod10 = require("zod");
4371
5034
  var answerCommand = define({
4372
5035
  name: "answer",
4373
5036
  tool: "kb_answer",
4374
5037
  usage: "answer <concept-id> <answer...>",
4375
5038
  description: "Resolve an open question: set status, stamp who and when, append an Answer section. If the answer overturns a decision or assumption, supersede that record explicitly.",
4376
- input: import_zod9.z.object({ bundlePath, conceptId, answer: import_zod9.z.string().min(1) }),
5039
+ input: import_zod10.z.object({ bundlePath, conceptId, answer: import_zod10.z.string().min(1) }),
4377
5040
  fromArgv: (argv, path) => ({
4378
5041
  bundlePath: path,
4379
5042
  conceptId: argv[1],
@@ -4387,27 +5050,27 @@ var answerCommand = define({
4387
5050
  });
4388
5051
 
4389
5052
  // src/commands/backlinks.ts
4390
- var import_zod10 = require("zod");
5053
+ var import_zod11 = require("zod");
4391
5054
  var backlinksCommand = define({
4392
5055
  name: "backlinks",
4393
5056
  tool: "kb_backlinks",
4394
5057
  usage: "backlinks <concept-id>",
4395
5058
  description: "Who points at this record: every inbound typed causal link (`strauss_links`), one hop, every rel including `related_to`, each with its rel and the standing of the record that made it. Use it when you need the exact edges \u2014 reviewing or renaming a record.",
4396
- input: import_zod10.z.object({ bundlePath, conceptId }),
5059
+ input: import_zod11.z.object({ bundlePath, conceptId }),
4397
5060
  fromArgv: (argv, path) => ({ bundlePath: path, conceptId: argv[1] }),
4398
5061
  run: async ({ store }, { bundlePath: path, conceptId: id }) => store.backlinks(path, id)
4399
5062
  });
4400
5063
 
4401
5064
  // src/commands/catalog.ts
4402
- var import_zod11 = require("zod");
5065
+ var import_zod12 = require("zod");
4403
5066
  var catalogCommand = define({
4404
5067
  name: "catalog",
4405
5068
  tool: "kb_catalog",
4406
5069
  usage: "catalog [type]",
4407
5070
  description: "Lists every record as one line \u2014 concept id, type, title, standing, and a stale flag \u2014 at roughly thirty tokens each. Pick this over kb_load once kb_load refuses: kb_catalog never refuses. Superseded records show only their replacement; fetch bodies with kb_load, kb_pack, kb_query, or kb_trace.",
4408
- input: import_zod11.z.object({
5071
+ input: import_zod12.z.object({
4409
5072
  bundlePath,
4410
- type: import_zod11.z.enum(KB_RECORD_TYPES).optional()
5073
+ type: import_zod12.z.enum(KB_RECORD_TYPES).optional()
4411
5074
  }),
4412
5075
  fromArgv: (argv, path) => ({
4413
5076
  bundlePath: path,
@@ -4462,26 +5125,26 @@ function count(value, noun) {
4462
5125
  }
4463
5126
 
4464
5127
  // src/commands/context.ts
4465
- var import_zod12 = require("zod");
5128
+ var import_zod13 = require("zod");
4466
5129
  var contextCommand = define({
4467
5130
  name: "context",
4468
5131
  tool: "kb_context",
4469
5132
  usage: "context [--profile NAME] [--budget N] [--full-under N] [--format json] [--event NAME]",
4470
5133
  description: "Index block of pinned bases (ids, titles, standing) for injection at context birth. Takes no bundlePath \u2014 reads the workspace pin manifests. Empty when nothing is pinned; refuses over budget rather than truncating. Budget precedence: flags, then the manifest `context[profile]` over `context.default`, then the built-in profile, then package defaults.",
4471
- input: import_zod12.z.object({
4472
- budgetTokens: import_zod12.z.number().int().positive().optional().describe(
5134
+ input: import_zod13.z.object({
5135
+ budgetTokens: import_zod13.z.number().int().positive().optional().describe(
4473
5136
  "Ceiling on the whole emitted block; past it the command refuses with a list of bases rather than truncating. Defaults to 4000."
4474
5137
  ),
4475
- fullUnderTokens: import_zod12.z.number().int().positive().optional().describe(
5138
+ fullUnderTokens: import_zod13.z.number().int().positive().optional().describe(
4476
5139
  "Per-base rendering threshold, applied before the budget: a base whose complete load fits under this arrives as full records instead of index lines, and the whole block still answers to budgetTokens. Off by default \u2014 index-only is the safe default at a context birth, because injected bodies outlive the qualifiers on them; the session-start profile opts tiny bases in at 1500."
4477
5140
  ),
4478
- profile: import_zod12.z.string().optional().describe(
5141
+ profile: import_zod13.z.string().optional().describe(
4479
5142
  "Named budget set: built-ins are session-start (full-under 1500), compact and turn (budget 2500); the manifests' `context` tables override per repo. Unknown names fall through to defaults rather than failing."
4480
5143
  ),
4481
- format: import_zod12.z.enum(["markdown", "json"]).optional().describe(
5144
+ format: import_zod13.z.enum(["markdown", "json"]).optional().describe(
4482
5145
  "CLI envelope for hook protocols that require strict JSON on stdout. MCP callers omit this \u2014 the block itself is identical."
4483
5146
  ),
4484
- event: import_zod12.z.string().optional().describe(
5147
+ event: import_zod13.z.string().optional().describe(
4485
5148
  "hookEventName stamped into the JSON envelope. Only meaningful with format=json."
4486
5149
  )
4487
5150
  }),
@@ -4517,14 +5180,14 @@ var contextCommand = define({
4517
5180
  });
4518
5181
 
4519
5182
  // 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}.`);
5183
+ var import_zod14 = require("zod");
5184
+ var days = (what, fallback) => import_zod14.z.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
4522
5185
  var doctorCommand = define({
4523
5186
  name: "doctor",
4524
5187
  tool: "kb_doctor",
4525
5188
  usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--offline] [--strict]",
4526
5189
  description: "Read-only health sweep: expired, expiring, unverified, aging, orphaned, broken-supersession, superseded-but-cited, drifted and unchecked anchors. Every group is reported even when empty; nothing is written or re-stamped. Use it when picking up a base you have not touched in a while; kb_validate only checks that pointers between records agree.",
4527
- input: import_zod13.z.object({
5190
+ input: import_zod14.z.object({
4528
5191
  bundlePath,
4529
5192
  repoRoot: REPO_ROOT,
4530
5193
  expiringDays: days(
@@ -4539,10 +5202,10 @@ var doctorCommand = define({
4539
5202
  "How long a record may stay `open` or `proposed` before `aging` reports it, in days.",
4540
5203
  DEFAULT_AGING_DAYS
4541
5204
  ),
4542
- offline: import_zod13.z.boolean().optional().describe(
5205
+ offline: import_zod14.z.boolean().optional().describe(
4543
5206
  "Read foreign anchors from the local repo cache only, never fetching."
4544
5207
  ),
4545
- strict: import_zod13.z.boolean().optional().describe(
5208
+ strict: import_zod14.z.boolean().optional().describe(
4546
5209
  "Turn an expired record into a non-zero exit for the CLI. No effect on the report itself."
4547
5210
  )
4548
5211
  }),
@@ -4585,7 +5248,13 @@ var doctorCommand = define({
4585
5248
  ...anchorDrift !== void 0 ? { anchorDrift } : {},
4586
5249
  now: new Date(checkedAt)
4587
5250
  });
4588
- return { bundlePath: path, checkedAt, ...report };
5251
+ const hints = grammarHints();
5252
+ return {
5253
+ bundlePath: path,
5254
+ checkedAt,
5255
+ ...report,
5256
+ ...hints.length ? { hints } : {}
5257
+ };
4589
5258
  },
4590
5259
  render: (result) => render2(result),
4591
5260
  // Only expiry, and only under --strict. The other seven checks report debt a
@@ -4603,12 +5272,15 @@ function render2(result) {
4603
5272
  `records: ${result.recordCount}`,
4604
5273
  `thresholds: expiring within ${thresholds.expiringDays}d, unverified over ${thresholds.unverifiedDays}d, aging over ${thresholds.agingDays}d`,
4605
5274
  `checked: ${result.checkedAt}`,
5275
+ ...result.anchorResolvers.total ? [
5276
+ `anchors: ${result.anchorResolvers.total} hashed \u2014 ${result.anchorResolvers.treeSitter} tree-sitter, ${result.anchorResolvers.regex} regex`
5277
+ ] : [],
4606
5278
  ""
4607
5279
  ];
4608
- const width = Math.max(...result.groups.map((group2) => group2.check.length));
5280
+ const width2 = Math.max(...result.groups.map((group2) => group2.check.length));
4609
5281
  for (const group2 of result.groups) {
4610
5282
  lines.push(
4611
- ` ${group2.check.padEnd(width)} ${String(group2.count).padStart(3)} ${group2.headline}`
5283
+ ` ${group2.check.padEnd(width2)} ${String(group2.count).padStart(3)} ${group2.headline}`
4612
5284
  );
4613
5285
  }
4614
5286
  for (const group2 of result.groups) {
@@ -4620,6 +5292,7 @@ function render2(result) {
4620
5292
  );
4621
5293
  }
4622
5294
  }
5295
+ for (const hint of result.hints ?? []) lines.push("", hint);
4623
5296
  lines.push(
4624
5297
  "",
4625
5298
  result.healthy ? "Nothing to repair." : `${result.findingCount} finding${result.findingCount === 1 ? "" : "s"} across ${result.groups.filter((group2) => group2.count).length} of ${result.groups.length} checks.`
@@ -4628,19 +5301,19 @@ function render2(result) {
4628
5301
  }
4629
5302
 
4630
5303
  // src/commands/impact.ts
4631
- var import_zod14 = require("zod");
5304
+ var import_zod15 = require("zod");
4632
5305
  var impactCommand = define({
4633
5306
  name: "impact",
4634
5307
  tool: "kb_impact",
4635
5308
  usage: "impact <concept-id> [--depth N] [--rels a,b]",
4636
5309
  description: "What breaks if this record changes: its transitive set of dependants, each with its standing. Each rel declares which of its ends depends on the other, and the walk follows each rel in its own direction. Naming `related_to` or an unknown rel in `rels` is an error. kb_backlinks gives one flat hop.",
4637
- input: import_zod14.z.object({
5310
+ input: import_zod15.z.object({
4638
5311
  bundlePath,
4639
5312
  conceptId,
4640
- depth: import_zod14.z.number().int().positive().optional().describe(
5313
+ depth: import_zod15.z.number().int().positive().optional().describe(
4641
5314
  "Hops out from the record. Unbounded when omitted; a walk this cuts reports truncated: true."
4642
5315
  ),
4643
- rels: import_zod14.z.array(import_zod14.z.enum(KB_CAUSAL_LINK_RELS)).optional().describe(
5316
+ rels: import_zod15.z.array(import_zod15.z.enum(KB_CAUSAL_LINK_RELS)).optional().describe(
4644
5317
  "Narrow which rels the walk follows. Defaults to every rel that carries a dependence \u2014 all but related_to."
4645
5318
  )
4646
5319
  }),
@@ -4661,13 +5334,13 @@ var impactCommand = define({
4661
5334
  });
4662
5335
 
4663
5336
  // src/commands/list.ts
4664
- var import_zod15 = require("zod");
5337
+ var import_zod16 = require("zod");
4665
5338
  var listCommand = define({
4666
5339
  name: "list",
4667
5340
  tool: "kb_list",
4668
5341
  usage: "list [type]",
4669
5342
  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() }),
5343
+ input: import_zod16.z.object({ bundlePath, type: import_zod16.z.enum(KB_RECORD_TYPES).optional() }),
4671
5344
  fromArgv: (argv, path) => ({ bundlePath: path, type: argv[1] }),
4672
5345
  run: async ({ store }, { bundlePath: path, type }) => (await store.list(path, type)).map((record) => ({
4673
5346
  conceptId: record.conceptId,
@@ -4679,17 +5352,17 @@ var listCommand = define({
4679
5352
  });
4680
5353
 
4681
5354
  // src/commands/load.ts
4682
- var import_zod16 = require("zod");
5355
+ var import_zod17 = require("zod");
4683
5356
  var loadCommand = define({
4684
5357
  name: "load",
4685
5358
  tool: "kb_load",
4686
5359
  usage: "load [type] [--budget N | --all] [--repo-root PATH]",
4687
5360
  description: "Load the whole base, each record with its standing \u2014 call it first, at the point of use, since compaction drops it. Superseded records arrive as stubs; kb_trace has the history. Over budget it refuses: kb_catalog, then kb_pack, or narrow with `type`; `all` bypasses. Never read record files directly \u2014 only kb_* tools resolve supersession. `digest` stamps the base's content, so hooks know when to reload.",
4688
- input: import_zod16.z.object({
5361
+ input: import_zod17.z.object({
4689
5362
  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(
5363
+ type: import_zod17.z.enum(KB_RECORD_TYPES).optional(),
5364
+ budgetTokens: import_zod17.z.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
5365
+ all: import_zod17.z.boolean().optional().describe(
4693
5366
  "Loads the entire base regardless of size, bypassing the token budget; mutually exclusive with budgetTokens."
4694
5367
  ),
4695
5368
  repoRoot: REPO_ROOT
@@ -4731,25 +5404,25 @@ var loadCommand = define({
4731
5404
  });
4732
5405
 
4733
5406
  // src/commands/log.ts
4734
- var import_zod17 = require("zod");
5407
+ var import_zod18 = require("zod");
4735
5408
  var logCommand = define({
4736
5409
  name: "log",
4737
5410
  tool: "kb_log",
4738
5411
  usage: "log",
4739
5412
  description: "Who touched what, and when. Append-only; malformed lines are reported, never repaired.",
4740
- input: import_zod17.z.object({ bundlePath }),
5413
+ input: import_zod18.z.object({ bundlePath }),
4741
5414
  fromArgv: (_argv, path) => ({ bundlePath: path }),
4742
5415
  run: ({ store }, { bundlePath: path }) => store.readLog(path)
4743
5416
  });
4744
5417
 
4745
5418
  // src/commands/no-decision.ts
4746
- var import_zod18 = require("zod");
5419
+ var import_zod19 = require("zod");
4747
5420
  var noDecisionCommand = define({
4748
5421
  name: "no-decision",
4749
5422
  tool: "kb_no_decision",
4750
5423
  usage: "no-decision <reason...>",
4751
5424
  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) }),
5425
+ input: import_zod19.z.object({ bundlePath, reason: import_zod19.z.string().min(1) }),
4753
5426
  fromArgv: (argv, path) => ({
4754
5427
  bundlePath: path,
4755
5428
  reason: argv.slice(1).join(" ").trim()
@@ -4766,20 +5439,20 @@ var noDecisionCommand = define({
4766
5439
  });
4767
5440
 
4768
5441
  // src/commands/pack.ts
4769
- var import_zod19 = require("zod");
5442
+ var import_zod20 = require("zod");
4770
5443
  var packCommand = define({
4771
5444
  name: "pack",
4772
5445
  tool: "kb_pack",
4773
5446
  usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
4774
5447
  description: "Bounded neighbourhood around one record: within `hops`, ranked, cut to `maxNodes`, with every cut record named under Excluded. Use when the base is over kb_load's budget and the work centres on a record you can name. Refuses over budget rather than truncating. Everything below the header is byte-stable across runs. Resolves supersession like kb_load.",
4775
- input: import_zod19.z.object({
5448
+ input: import_zod20.z.object({
4776
5449
  bundlePath,
4777
5450
  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(
5451
+ hops: import_zod20.z.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
5452
+ maxNodes: import_zod20.z.number().int().positive().optional().describe(
4780
5453
  "How many records the pack may hold, root included. Defaults to 20."
4781
5454
  ),
4782
- budgetTokens: import_zod19.z.number().int().positive().optional().describe(
5455
+ budgetTokens: import_zod20.z.number().int().positive().optional().describe(
4783
5456
  "Approximate token ceiling over what is actually emitted. Defaults to 25000."
4784
5457
  )
4785
5458
  }),
@@ -4866,22 +5539,22 @@ function warningLabel(warning) {
4866
5539
  }
4867
5540
 
4868
5541
  // src/commands/pin.ts
4869
- var import_zod20 = require("zod");
5542
+ var import_zod21 = require("zod");
4870
5543
  var pinCommand = define({
4871
5544
  name: "pin",
4872
5545
  tool: "kb_pin",
4873
5546
  usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
4874
5547
  description: "Pin a base into a workspace manifest so kb_context surfaces it. Layers, nearest wins: project `.strauss/kb-pins.json` (default), `--local` (personal, gitignored), `--user` (`~/.strauss`). Idempotent; `--mode full|index`, `--profiles`, `--frozen`/`--unfreeze` update only those fields. A path with no records pins with a warning. Never touches the base itself.",
4875
- input: import_zod20.z.object({
5548
+ input: import_zod21.z.object({
4876
5549
  bundlePath,
4877
- mode: import_zod20.z.enum(["full", "index"]).optional().describe(
5550
+ mode: import_zod21.z.enum(["full", "index"]).optional().describe(
4878
5551
  "full: always emit this base's records whole (still under the block budget); index: never upgrade. Absent: the profile's full-under threshold decides."
4879
5552
  ),
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(
5553
+ profiles: import_zod21.z.array(import_zod21.z.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
5554
+ layer: import_zod21.z.enum(["project", "local", "user"]).optional().describe(
4882
5555
  "Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
4883
5556
  ),
4884
- frozen: import_zod20.z.boolean().optional().describe(
5557
+ frozen: import_zod21.z.boolean().optional().describe(
4885
5558
  "true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
4886
5559
  )
4887
5560
  }),
@@ -4910,29 +5583,29 @@ var pinCommand = define({
4910
5583
  });
4911
5584
 
4912
5585
  // src/commands/pins.ts
4913
- var import_zod21 = require("zod");
5586
+ var import_zod22 = require("zod");
4914
5587
  var pinsCommand = define({
4915
5588
  name: "pins",
4916
5589
  tool: "kb_pins",
4917
5590
  usage: "pins",
4918
5591
  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({}),
5592
+ input: import_zod22.z.object({}),
4920
5593
  fromArgv: () => ({}),
4921
5594
  run: ({ store }) => listPins(store, process.cwd())
4922
5595
  });
4923
5596
 
4924
5597
  // src/commands/query.ts
4925
- var import_zod22 = require("zod");
5598
+ var import_zod23 = require("zod");
4926
5599
  var queryCommand = define({
4927
5600
  name: "query",
4928
5601
  tool: "kb_query",
4929
5602
  usage: "query <text...> [--repo-root PATH]",
4930
5603
  description: "Search; every hit carries its standing. Flagged, never filtered: a superseded hit returns with its replacement, a rejected one is marked. Prefer kb_load when the base fits its budget \u2014 a full read beats search. Results are volatile: place them at the tail, not the cached prefix. Never read record files directly.",
4931
- input: import_zod22.z.object({
5604
+ input: import_zod23.z.object({
4932
5605
  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(),
5606
+ text: import_zod23.z.string().optional(),
5607
+ type: import_zod23.z.enum(KB_RECORD_TYPES).optional(),
5608
+ includeNonCurrent: import_zod23.z.boolean().optional(),
4936
5609
  repoRoot: REPO_ROOT
4937
5610
  }),
4938
5611
  // `--repo-root` is a flag, so its value must not fall into the search text.
@@ -4964,43 +5637,43 @@ var queryCommand = define({
4964
5637
  });
4965
5638
 
4966
5639
  // src/commands/read-index.ts
4967
- var import_zod23 = require("zod");
5640
+ var import_zod24 = require("zod");
4968
5641
  var readIndexCommand = define({
4969
5642
  name: "index",
4970
5643
  tool: "kb_index",
4971
5644
  usage: "index",
4972
5645
  description: "The index \u2014 title, type, status, description per record \u2014 rebuilt if stale. Cheapest re-orientation after compaction: call it (or kb_context) first, then kb_load or fetch by id.",
4973
- input: import_zod23.z.object({ bundlePath }),
5646
+ input: import_zod24.z.object({ bundlePath }),
4974
5647
  fromArgv: (_argv, path) => ({ bundlePath: path }),
4975
5648
  run: ({ store }, { bundlePath: path }) => store.readIndex(path)
4976
5649
  });
4977
5650
 
4978
5651
  // src/commands/schema.ts
4979
- var import_zod24 = require("zod");
5652
+ var import_zod25 = require("zod");
4980
5653
  var schemaCommand = define({
4981
5654
  name: "schema",
4982
5655
  tool: "kb_schema",
4983
5656
  usage: "schema",
4984
5657
  description: "JSON Schema for frontmatter, write input, and log entries, generated from the enforcing code.",
4985
- input: import_zod24.z.object({}),
5658
+ input: import_zod25.z.object({}),
4986
5659
  fromArgv: () => ({}),
4987
5660
  run: () => Promise.resolve(kbJsonSchemas())
4988
5661
  });
4989
5662
 
4990
5663
  // src/commands/stamp.ts
4991
- var import_promises7 = require("fs/promises");
4992
- var import_zod25 = require("zod");
5664
+ var import_promises9 = require("fs/promises");
5665
+ var import_zod26 = require("zod");
4993
5666
  var DIGEST = /^[0-9a-f]{64}$/;
4994
5667
  var stampCommand = define({
4995
5668
  name: "stamp",
4996
5669
  tool: "kb_stamp",
4997
5670
  usage: "stamp [--bundle PATH] [--since DIGEST|FILE]",
4998
5671
  description: "Content stamp of a base \u2014 `load`'s digest, record counts, per-record digests \u2014 without any bodies. Takes no bundlePath to stamp every pinned base. With `since`, reports only the bases that moved, naming the changed ids when the baseline is a prior stamp; silent when nothing changed. Reads, never writes.",
4999
- input: import_zod25.z.object({
5000
- bundlePath: import_zod25.z.string().min(1).optional().describe(
5672
+ input: import_zod26.z.object({
5673
+ bundlePath: import_zod26.z.string().min(1).optional().describe(
5001
5674
  "Absolute path to one knowledge base. Omit to stamp every pinned base."
5002
5675
  ),
5003
- since: import_zod25.z.string().min(1).optional().describe(
5676
+ since: import_zod26.z.string().min(1).optional().describe(
5004
5677
  "Prior digest, or path to a prior `stamp --json`; only moved bases return, with changed ids when the baseline is a file."
5005
5678
  )
5006
5679
  }),
@@ -5062,7 +5735,7 @@ async function readBaseline(since) {
5062
5735
  if (DIGEST.test(since)) return { digest: since, byPath: /* @__PURE__ */ new Map() };
5063
5736
  let parsed;
5064
5737
  try {
5065
- parsed = JSON.parse(await (0, import_promises7.readFile)(since, "utf8"));
5738
+ parsed = JSON.parse(await (0, import_promises9.readFile)(since, "utf8"));
5066
5739
  } catch {
5067
5740
  throw new KbStampBaselineError(since);
5068
5741
  }
@@ -5086,16 +5759,16 @@ async function readBaseline(since) {
5086
5759
  }
5087
5760
 
5088
5761
  // src/commands/status.ts
5089
- var import_zod26 = require("zod");
5762
+ var import_zod27 = require("zod");
5090
5763
  var statusCommand = define({
5091
5764
  name: "status",
5092
5765
  tool: "kb_status",
5093
5766
  usage: "status <concept-id> <status>",
5094
5767
  description: "Move a record's status. Compare-and-swap: a concurrent change fails instead of being overwritten.",
5095
- input: import_zod26.z.object({
5768
+ input: import_zod27.z.object({
5096
5769
  bundlePath,
5097
5770
  conceptId,
5098
- status: import_zod26.z.enum(KB_RECORD_STATUSES)
5771
+ status: import_zod27.z.enum(KB_RECORD_STATUSES)
5099
5772
  }),
5100
5773
  fromArgv: (argv, path) => ({
5101
5774
  bundlePath: path,
@@ -5110,13 +5783,13 @@ var statusCommand = define({
5110
5783
  });
5111
5784
 
5112
5785
  // src/commands/supersede.ts
5113
- var import_zod27 = require("zod");
5786
+ var import_zod28 = require("zod");
5114
5787
  var supersedeCommand = define({
5115
5788
  name: "supersede",
5116
5789
  tool: "kb_supersede",
5117
5790
  usage: "supersede <concept-id> <replacement-id>",
5118
5791
  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 }),
5792
+ input: import_zod28.z.object({ bundlePath, conceptId, replacementId: conceptId }),
5120
5793
  fromArgv: (argv, path) => ({
5121
5794
  bundlePath: path,
5122
5795
  conceptId: argv[1],
@@ -5130,16 +5803,16 @@ var supersedeCommand = define({
5130
5803
  });
5131
5804
 
5132
5805
  // src/commands/sync-instructions.ts
5133
- var import_zod28 = require("zod");
5806
+ var import_zod29 = require("zod");
5134
5807
  var syncInstructionsCommand = define({
5135
5808
  name: "sync-instructions",
5136
5809
  usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
5137
5810
  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()
5811
+ input: import_zod29.z.object({
5812
+ file: import_zod29.z.string().min(1).describe("The instruction file to edit in place."),
5813
+ budgetTokens: import_zod29.z.number().int().positive().optional(),
5814
+ fullUnderTokens: import_zod29.z.number().int().positive().optional(),
5815
+ profile: import_zod29.z.string().optional()
5143
5816
  }),
5144
5817
  fromArgv: (argv) => {
5145
5818
  const budget = argvFlag(argv, "--budget");
@@ -5165,17 +5838,17 @@ var syncInstructionsCommand = define({
5165
5838
  });
5166
5839
 
5167
5840
  // src/commands/trace.ts
5168
- var import_zod29 = require("zod");
5841
+ var import_zod30 = require("zod");
5169
5842
  var traceCommand = define({
5170
5843
  name: "trace",
5171
5844
  tool: "kb_trace",
5172
5845
  usage: "trace <concept-id> [edges...]",
5173
5846
  description: 'Timeline of how a position was reached, ordered by write time, following supersession, shared anchors and shared sources. Includes rejected, draft and superseded records \u2014 in a history they are the content. For "why is it like this"; kb_load answers "what holds now".',
5174
- input: import_zod29.z.object({
5847
+ input: import_zod30.z.object({
5175
5848
  bundlePath,
5176
5849
  conceptId,
5177
- edges: import_zod29.z.array(import_zod29.z.enum(TRACE_EDGES)).optional(),
5178
- depth: import_zod29.z.number().int().positive().optional()
5850
+ edges: import_zod30.z.array(import_zod30.z.enum(TRACE_EDGES)).optional(),
5851
+ depth: import_zod30.z.number().int().positive().optional()
5179
5852
  }),
5180
5853
  fromArgv: (argv, path) => ({
5181
5854
  bundlePath: path,
@@ -5197,37 +5870,37 @@ var traceCommand = define({
5197
5870
  });
5198
5871
 
5199
5872
  // src/commands/types.ts
5200
- var import_zod30 = require("zod");
5873
+ var import_zod31 = require("zod");
5201
5874
  var typesCommand = define({
5202
5875
  name: "types",
5203
5876
  tool: "kb_types",
5204
5877
  usage: "types",
5205
5878
  description: "The twelve record types with their purpose, body sections, and starting status. Read this before writing rather than guessing headings \u2014 a section the type does not define is rejected.",
5206
- input: import_zod30.z.object({}),
5879
+ input: import_zod31.z.object({}),
5207
5880
  fromArgv: () => ({}),
5208
5881
  run: () => Promise.resolve(RECORD_TYPES)
5209
5882
  });
5210
5883
 
5211
5884
  // src/commands/unpin.ts
5212
- var import_zod31 = require("zod");
5885
+ var import_zod32 = require("zod");
5213
5886
  var unpinCommand = define({
5214
5887
  name: "unpin",
5215
5888
  tool: "kb_unpin",
5216
5889
  usage: "unpin [bundle-path]",
5217
5890
  description: "Remove a base from every manifest layer that holds it. Reports the layers touched.",
5218
- input: import_zod31.z.object({ bundlePath }),
5891
+ input: import_zod32.z.object({ bundlePath }),
5219
5892
  fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
5220
5893
  run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
5221
5894
  });
5222
5895
 
5223
5896
  // src/commands/validate.ts
5224
- var import_zod32 = require("zod");
5897
+ var import_zod33 = require("zod");
5225
5898
  var validateCommand = define({
5226
5899
  name: "validate",
5227
5900
  tool: "kb_validate",
5228
5901
  usage: "validate",
5229
5902
  description: "Check pointers no single record can see: supersession links that disagree between the two records, typed causal links, and assumptions that cite sources. Each finding carries a severity: errors fail the exit code, warnings do not.",
5230
- input: import_zod32.z.object({ bundlePath }),
5903
+ input: import_zod33.z.object({ bundlePath }),
5231
5904
  fromArgv: (_argv, path) => ({ bundlePath: path }),
5232
5905
  run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
5233
5906
  // Warnings never fail the exit code; every other severity does.
@@ -5237,16 +5910,16 @@ var validateCommand = define({
5237
5910
  });
5238
5911
 
5239
5912
  // src/commands/verify.ts
5240
- var import_zod33 = require("zod");
5913
+ var import_zod34 = require("zod");
5241
5914
  var verifyCommand = define({
5242
5915
  name: "verify",
5243
5916
  tool: "kb_verify",
5244
5917
  usage: "verify <concept-id> --note <text>",
5245
5918
  description: "Append a verified[] event: who checked, when, and what was found. Append-only. A record's own generator is refused unless the actor is `human:`-prefixed.",
5246
- input: import_zod33.z.object({
5919
+ input: import_zod34.z.object({
5247
5920
  bundlePath,
5248
5921
  conceptId,
5249
- note: import_zod33.z.string().refine((s) => s.trim().length > 0, {
5922
+ note: import_zod34.z.string().refine((s) => s.trim().length > 0, {
5250
5923
  message: "note must say what the check found"
5251
5924
  })
5252
5925
  }),
@@ -5266,15 +5939,15 @@ var verifyCommand = define({
5266
5939
  });
5267
5940
 
5268
5941
  // src/commands/write.ts
5269
- var import_zod34 = require("zod");
5942
+ var import_zod35 = require("zod");
5270
5943
  var writeCommand = define({
5271
5944
  name: "write",
5272
5945
  tool: "kb_write",
5273
5946
  usage: "write <type> < record.json",
5274
5947
  description: "Write one record. Search first \u2014 a duplicate concept id is rejected, not overwritten; kb_types lists each type's sections. An unsourced claim is an `assumption` with assumption: true, never a vague `fact`. Conflicting records get a `risk`, `open-question`, or superseding `decision`. Prefer a new short record over overloading one. Never delete; supersede.",
5275
- input: import_zod34.z.object({
5948
+ input: import_zod35.z.object({
5276
5949
  bundlePath,
5277
- type: import_zod34.z.enum(KB_RECORD_TYPES),
5950
+ type: import_zod35.z.enum(KB_RECORD_TYPES),
5278
5951
  input: composeInputSchema
5279
5952
  }),
5280
5953
  fromArgv: async (argv, path, stdin) => ({
@@ -5298,13 +5971,13 @@ var writeCommand = define({
5298
5971
  });
5299
5972
 
5300
5973
  // src/commands/write-decision.ts
5301
- var import_zod35 = require("zod");
5974
+ var import_zod36 = require("zod");
5302
5975
  var writeDecisionCommand = define({
5303
5976
  name: "write-decision",
5304
5977
  tool: "kb_write_decision",
5305
5978
  usage: "write-decision < decision.json",
5306
5979
  description: "Write a decision, with `alternative` (what was rejected and why) and `impact` as fields. Record one when a later reader would otherwise simplify the constraint away; skip when the diff already answers it. `sources` for material read, `anchors` for code, `relatedConceptIds` for records.",
5307
- input: import_zod35.z.object({ bundlePath, input: decisionInputSchema }),
5980
+ input: import_zod36.z.object({ bundlePath, input: decisionInputSchema }),
5308
5981
  fromArgv: async (_argv, path, stdin) => ({
5309
5982
  bundlePath: path,
5310
5983
  input: JSON.parse(await stdin())
@@ -5364,7 +6037,7 @@ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
5364
6037
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
5365
6038
 
5366
6039
  // src/version.ts
5367
- var VERSION = true ? "0.1.16" : "0.0.0-dev";
6040
+ var VERSION = true ? "0.1.17" : "0.0.0-dev";
5368
6041
 
5369
6042
  // src/mcp.ts
5370
6043
  function createKbMcpServer() {
@@ -5403,7 +6076,7 @@ async function runKbMcpServer() {
5403
6076
  }
5404
6077
 
5405
6078
  // src/cli.ts
5406
- var import_node_path9 = require("path");
6079
+ var import_node_path12 = require("path");
5407
6080
  async function runKbCli(argv) {
5408
6081
  const { flags, literal } = takeLiteral(argv);
5409
6082
  const {
@@ -5465,7 +6138,7 @@ function takeLiteral(argv) {
5465
6138
  function takeBundle(argv) {
5466
6139
  const at = argv.indexOf("--bundle");
5467
6140
  if (at === -1) {
5468
- return { bundle: (0, import_node_path9.join)(process.cwd(), KB_DIR), explicit: false, rest: argv };
6141
+ return { bundle: (0, import_node_path12.join)(process.cwd(), KB_DIR), explicit: false, rest: argv };
5469
6142
  }
5470
6143
  const bundle = argv[at + 1];
5471
6144
  if (!bundle) die("--bundle requires a path");
@@ -5495,14 +6168,14 @@ function summarise(description) {
5495
6168
  return sentence.length > 78 ? `${sentence.slice(0, 75)}\u2026` : sentence;
5496
6169
  }
5497
6170
  function usage() {
5498
- const width = Math.max(...KB_COMMANDS.map((command) => command.usage.length));
6171
+ const width2 = Math.max(...KB_COMMANDS.map((command) => command.usage.length));
5499
6172
  return [
5500
6173
  "strauss-kb \u2014 knowledge base commands",
5501
6174
  "",
5502
6175
  "Usage: strauss-kb [--bundle PATH] <command> [args]",
5503
6176
  "",
5504
6177
  ...KB_COMMANDS.map(
5505
- (command) => ` ${command.usage.padEnd(width)} ${summarise(command.description)}`
6178
+ (command) => ` ${command.usage.padEnd(width2)} ${summarise(command.description)}`
5506
6179
  ),
5507
6180
  "",
5508
6181
  ` --bundle PATH defaults to ./${KB_DIR}`,
@@ -5563,6 +6236,7 @@ function usage() {
5563
6236
  RECORD_TYPES,
5564
6237
  SEARCH_INDEX_FILE,
5565
6238
  TRACE_EDGES,
6239
+ TreeSitterResolver,
5566
6240
  adjudicate,
5567
6241
  anchorFilePath,
5568
6242
  assertBaseNotFrozen,
@@ -5577,9 +6251,14 @@ function usage() {
5577
6251
  contextProfileBudgets,
5578
6252
  createKbMcpServer,
5579
6253
  decisionInputSchema,
6254
+ defaultAnchorResolvers,
5580
6255
  detectAnchorDrift,
5581
6256
  doctor,
5582
6257
  edgeNeighbours,
6258
+ ensureGrammar,
6259
+ grammarHints,
6260
+ grammarManifest,
6261
+ grammarsCacheRoot,
5583
6262
  hashAnchorText,
5584
6263
  impact,
5585
6264
  inboundIndex,
@@ -5597,6 +6276,7 @@ function usage() {
5597
6276
  kbRecordFrontmatterSchema,
5598
6277
  kbSourceSchema,
5599
6278
  kbVerifiedEventSchema,
6279
+ languageForFile,
5600
6280
  listPins,
5601
6281
  loadQmd,
5602
6282
  matchToDiff,
@@ -5606,6 +6286,7 @@ function usage() {
5606
6286
  parseLog,
5607
6287
  parseMarkdownWithFrontmatter,
5608
6288
  pinBase,
6289
+ prepareResolvers,
5609
6290
  readMergedPins,
5610
6291
  readPinsLayer,
5611
6292
  readRemoteAnchors,
@@ -5616,6 +6297,7 @@ function usage() {
5616
6297
  renderLogEntry,
5617
6298
  repoCacheDir,
5618
6299
  resolveAnchor,
6300
+ resolveAnchorSpan,
5619
6301
  resolveHeads,
5620
6302
  resolveHits,
5621
6303
  resolvePinPath,
@@ -5628,6 +6310,7 @@ function usage() {
5628
6310
  syncInstructions,
5629
6311
  toHookJson,
5630
6312
  trace,
6313
+ treeSitterLanguages,
5631
6314
  unpinBase,
5632
6315
  validateBundle
5633
6316
  });