@saasontools/strauss-kb 0.1.17 → 0.1.19

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
@@ -85,6 +85,7 @@ __export(index_exports, {
85
85
  backlinks: () => backlinks,
86
86
  buildContext: () => buildContext,
87
87
  catalog: () => catalog,
88
+ classifyDrift: () => classifyDrift,
88
89
  composeDecisionRecord: () => composeDecisionRecord,
89
90
  composeInputSchema: () => composeInputSchema,
90
91
  composeLinkSchema: () => composeLinkSchema,
@@ -122,6 +123,7 @@ __export(index_exports, {
122
123
  listPins: () => listPins,
123
124
  loadQmd: () => loadQmd,
124
125
  matchToDiff: () => matchToDiff,
126
+ matchesTags: () => matchesTags,
125
127
  mergedContextBudgets: () => mergedContextBudgets,
126
128
  neighbours: () => neighbours,
127
129
  pack: () => pack,
@@ -132,6 +134,7 @@ __export(index_exports, {
132
134
  readMergedPins: () => readMergedPins,
133
135
  readPinsLayer: () => readPinsLayer,
134
136
  readRemoteAnchors: () => readRemoteAnchors,
137
+ reassessPacket: () => reassessPacket,
135
138
  regexResolver: () => regexResolver,
136
139
  renderCatalogLine: () => renderCatalogLine,
137
140
  renderIndex: () => renderIndex,
@@ -153,6 +156,7 @@ __export(index_exports, {
153
156
  toHookJson: () => toHookJson,
154
157
  trace: () => trace,
155
158
  treeSitterLanguages: () => treeSitterLanguages,
159
+ unifiedDiff: () => unifiedDiff,
156
160
  unpinBase: () => unpinBase,
157
161
  validateBundle: () => validateBundle
158
162
  });
@@ -181,9 +185,9 @@ async function mapLimit(items, limit, fn) {
181
185
  { length: Math.min(limit, items.length) },
182
186
  async () => {
183
187
  while (!failed && next < items.length) {
184
- const at = next++;
188
+ const at2 = next++;
185
189
  try {
186
- out[at] = await fn(items[at], at);
190
+ out[at2] = await fn(items[at2], at2);
187
191
  } catch (error) {
188
192
  failed = true;
189
193
  throw error;
@@ -262,6 +266,14 @@ var kbAnchorSchema = import_zod.z.object({
262
266
  hash: import_zod.z.string().regex(/^sha256:[0-9a-f]{64}$/, {
263
267
  message: "hash must be sha256:<64 hex chars>"
264
268
  }).optional(),
269
+ /**
270
+ * What `hash` was taken over: the span's raw text, or the normalised token
271
+ * stream a parser sees (`ast`). Absent means `raw`, which is what every
272
+ * anchor stamped before this field carries, so old hashes keep comparing
273
+ * the way they were written. An `ast` hash is blind to whitespace and
274
+ * comments, so reformatting the anchored code is not drift.
275
+ */
276
+ hash_kind: import_zod.z.enum(["raw", "ast"]).optional(),
265
277
  /** ISO 8601 timestamp of the last successful resolution. */
266
278
  resolved_at: import_zod.z.string().min(1).optional(),
267
279
  /** Line count of the text the hash was taken over. */
@@ -676,8 +688,8 @@ function safeSegment(value) {
676
688
  function revRef(rev) {
677
689
  const safe = rev.replace(/[^A-Za-z0-9_-]/g, "-").slice(0, 64);
678
690
  let hash = 5381;
679
- for (let at = 0; at < rev.length; at++) {
680
- hash = (hash * 33 ^ rev.charCodeAt(at)) >>> 0;
691
+ for (let at2 = 0; at2 < rev.length; at2++) {
692
+ hash = (hash * 33 ^ rev.charCodeAt(at2)) >>> 0;
681
693
  }
682
694
  return `refs/strauss/${safe}-${hash.toString(16)}`;
683
695
  }
@@ -786,8 +798,8 @@ function repoUrlIsSafe(repo) {
786
798
  if (!scheme?.[1]) return false;
787
799
  if (!allowed.includes(scheme[1].toLowerCase())) return false;
788
800
  const authority = url.slice(scheme[0].length).split("/")[0] ?? "";
789
- const at = authority.lastIndexOf("@");
790
- return at < 0 || !authority.slice(0, at).includes(":");
801
+ const at2 = authority.lastIndexOf("@");
802
+ return at2 < 0 || !authority.slice(0, at2).includes(":");
791
803
  }
792
804
  function protocolArgs() {
793
805
  const allowed = allowedProtocols();
@@ -878,7 +890,7 @@ async function readOneRepo(repo, url, declared, context) {
878
890
  return new Map([
879
891
  ...rejected2,
880
892
  ...wants.map(
881
- (want, at) => [wantKey(repo, want.ref, want.file), reads[at]]
893
+ (want, at2) => [wantKey(repo, want.ref, want.file), reads[at2]]
882
894
  )
883
895
  ]);
884
896
  }
@@ -1067,7 +1079,8 @@ function warningAnchor(entry) {
1067
1079
  diffSize,
1068
1080
  ...reason !== void 0 ? { reason } : {},
1069
1081
  ...repo !== void 0 ? { repo } : {},
1070
- ...remoteState !== void 0 ? { remoteState } : {}
1082
+ ...remoteState !== void 0 ? { remoteState } : {},
1083
+ ...entry.class !== void 0 ? { class: entry.class } : {}
1071
1084
  };
1072
1085
  }
1073
1086
  function resolveHeads(from, byId) {
@@ -1238,7 +1251,7 @@ async function readAnchorFiles(files, read, concurrency = DEFAULT_IO_CONCURRENCY
1238
1251
  return { ok: false, reason: "file-unreadable" };
1239
1252
  }
1240
1253
  });
1241
- return new Map(wanted.map((file, at) => [file, results[at]]));
1254
+ return new Map(wanted.map((file, at2) => [file, results[at2]]));
1242
1255
  }
1243
1256
 
1244
1257
  // src/anchor-resolver/resolver.ts
@@ -1428,8 +1441,8 @@ async function ensureGrammar(language, options = {}) {
1428
1441
  return miss(language, `grammar tree-sitter-${language}`, grammar);
1429
1442
  const parts = [];
1430
1443
  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}` : ""}`;
1444
+ for (const [at2, part] of pack2.tags.entries()) {
1445
+ const name = `${language} tags${total > 1 ? ` part ${at2 + 1}/${total}` : ""}`;
1433
1446
  const path = grammarCachePath(root, language, part.sha256, "scm");
1434
1447
  const held = await ensurePart(path, name, part, options);
1435
1448
  if (held !== true) return miss(language, name, held);
@@ -1577,8 +1590,8 @@ function typeNameIn(receiver) {
1577
1590
  while (stack.length) {
1578
1591
  const node = stack.pop();
1579
1592
  if (node.type === "type_identifier") return node.text;
1580
- for (let at = 0; at < node.childCount; at++) {
1581
- const child = node.child(at);
1593
+ for (let at2 = 0; at2 < node.childCount; at2++) {
1594
+ const child = node.child(at2);
1582
1595
  if (child) stack.push(child);
1583
1596
  }
1584
1597
  }
@@ -1587,7 +1600,7 @@ function typeNameIn(receiver) {
1587
1600
  function endsWith(chain, wanted) {
1588
1601
  if (wanted.length > chain.length) return false;
1589
1602
  const offset = chain.length - wanted.length;
1590
- return wanted.every((segment, at) => chain[offset + at] === segment);
1603
+ return wanted.every((segment, at2) => chain[offset + at2] === segment);
1591
1604
  }
1592
1605
  function width(node) {
1593
1606
  return node.endIndex - node.startIndex;
@@ -1613,6 +1626,26 @@ function spanOf(definition, source) {
1613
1626
  };
1614
1627
  }
1615
1628
 
1629
+ // src/tree-sitter-resolver/tokens.ts
1630
+ function tokens(root) {
1631
+ const out = [];
1632
+ const stack = [root];
1633
+ while (stack.length) {
1634
+ const node = stack.pop();
1635
+ if (node.type.includes("comment")) continue;
1636
+ if (node.childCount === 0) {
1637
+ const text = node.text.trim();
1638
+ if (text) out.push(text);
1639
+ continue;
1640
+ }
1641
+ for (let at2 = node.childCount - 1; at2 >= 0; at2--) {
1642
+ const child = node.child(at2);
1643
+ if (child) stack.push(child);
1644
+ }
1645
+ }
1646
+ return out;
1647
+ }
1648
+
1616
1649
  // src/tree-sitter-resolver/resolver.ts
1617
1650
  var TREE_CACHE_LIMIT = 32;
1618
1651
  var TreeSitterResolver = class {
@@ -1658,7 +1691,7 @@ var TreeSitterResolver = class {
1658
1691
  (language) => this.load(language)
1659
1692
  );
1660
1693
  languages.forEach(
1661
- (language, at) => this.loaded.set(language, loaded[at] ?? null)
1694
+ (language, at2) => this.loaded.set(language, loaded[at2] ?? null)
1662
1695
  );
1663
1696
  }
1664
1697
  /**
@@ -1746,6 +1779,60 @@ var TreeSitterResolver = class {
1746
1779
  this.trees.set(key2, parsed);
1747
1780
  return parsed;
1748
1781
  }
1782
+ /**
1783
+ * Every definition this file declares, as dotted symbol and span.
1784
+ *
1785
+ * The inverse of `attempt`: that asks "where is this name", this asks "what
1786
+ * names are here". `moved` needs the second — the stored hash has to be
1787
+ * looked for at every definition in the repository, and there is no name to
1788
+ * ask about, since the whole question is which name now carries that code.
1789
+ */
1790
+ spans(source, file) {
1791
+ const language = languageForFile(file);
1792
+ if (!language) return [];
1793
+ const loaded = this.loaded.get(language);
1794
+ if (!loaded) return [];
1795
+ const parsed = this.parse(language, loaded, source);
1796
+ if (!parsed) return [];
1797
+ return parsed.definitions.filter((definition) => definition.target).map((definition) => ({
1798
+ symbol: chainOf(definition, parsed.byNodeId).join("."),
1799
+ span: spanOf(definition, source)
1800
+ }));
1801
+ }
1802
+ /**
1803
+ * The token stream of a span: every leaf the parser sees, comments dropped,
1804
+ * joined by single spaces.
1805
+ *
1806
+ * This is what makes a reformat not be drift. Hashing it rather than the raw
1807
+ * text means indentation, line breaks, trailing commas the formatter moved,
1808
+ * and every comment above or inside the definition are outside the hash —
1809
+ * and a renamed identifier or a changed literal is still inside it, because
1810
+ * those are leaves.
1811
+ *
1812
+ * `null` when the file has no grammar, the grammar would not load, or the
1813
+ * text will not parse: no normalisation is better than a guessed one.
1814
+ */
1815
+ normalize(text, file) {
1816
+ const language = file ? languageForFile(file) : void 0;
1817
+ if (!language) return null;
1818
+ const loaded = this.loaded.get(language);
1819
+ if (!loaded) return null;
1820
+ const parser = this.parser;
1821
+ if (!parser) return null;
1822
+ let tree;
1823
+ try {
1824
+ parser.setLanguage(loaded.language);
1825
+ tree = parser.parse(text);
1826
+ } catch {
1827
+ return null;
1828
+ }
1829
+ if (!tree) return null;
1830
+ try {
1831
+ return tokens(tree.rootNode).join(" ");
1832
+ } finally {
1833
+ tree.delete();
1834
+ }
1835
+ }
1749
1836
  /** Drops cached trees. Grammars stay loaded — they are immutable. */
1750
1837
  reset() {
1751
1838
  for (const parsed of this.trees.values()) parsed.tree.delete();
@@ -1899,7 +1986,7 @@ var regexResolver = {
1899
1986
  );
1900
1987
  const nearest = Math.min(...distances);
1901
1988
  if (Number.isFinite(nearest)) {
1902
- candidates = candidates.filter((_, at) => distances[at] === nearest);
1989
+ candidates = candidates.filter((_, at2) => distances[at2] === nearest);
1903
1990
  }
1904
1991
  }
1905
1992
  if (candidates.length !== 1) return null;
@@ -1914,8 +2001,8 @@ function escapeRegExp(value) {
1914
2001
  }
1915
2002
  function distanceToParent(lines, index2, parent) {
1916
2003
  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;
2004
+ for (let at2 = index2; at2 >= floor; at2--) {
2005
+ if (parent.test(lines[at2] ?? "")) return index2 - at2;
1919
2006
  }
1920
2007
  return Number.POSITIVE_INFINITY;
1921
2008
  }
@@ -1947,10 +2034,12 @@ function resolveAnchorSpan(source, anchor, resolvers = [regexResolver]) {
1947
2034
  if (attempt.reason === "symbol-not-found") continue;
1948
2035
  return { ok: false, reason: attempt.reason };
1949
2036
  }
2037
+ const tokens2 = resolver.normalize?.(attempt.span.text, anchor.file);
1950
2038
  return {
1951
2039
  ok: true,
1952
2040
  span: attempt.span,
1953
- ...isResolverName(resolver.name) ? { resolver: resolver.name } : {}
2041
+ ...isResolverName(resolver.name) ? { resolver: resolver.name } : {},
2042
+ ...tokens2 ? { normalized: tokens2 } : {}
1954
2043
  };
1955
2044
  }
1956
2045
  return { ok: false, reason: "symbol-not-found" };
@@ -1978,6 +2067,11 @@ function resolverChanged(source, anchor, produced) {
1978
2067
  );
1979
2068
  return before !== null && hashAnchorText(before.text) === anchor.hash;
1980
2069
  }
2070
+ function anchorHashOf(anchor, outcome) {
2071
+ const stored = anchor.hash ? anchor.hash_kind ?? "raw" : void 0;
2072
+ const wanted = stored ?? (outcome.normalized ? "ast" : "raw");
2073
+ return wanted === "ast" && outcome.normalized ? { hash: hashAnchorText(outcome.normalized), kind: "ast" } : { hash: hashAnchorText(outcome.span.text), kind: "raw" };
2074
+ }
1981
2075
 
1982
2076
  // src/anchor-resolver/drift.ts
1983
2077
  async function detectAnchorDrift(records, options = {}) {
@@ -2057,16 +2151,29 @@ function unresolved(anchor, reason, repo) {
2057
2151
  state: "unresolved",
2058
2152
  diffSize: null,
2059
2153
  ...reason ? { reason } : {},
2060
- ...repo ? { repo } : {}
2154
+ ...repo ? { repo } : {},
2155
+ ...classOf(reason)
2061
2156
  };
2062
2157
  }
2158
+ function provisionalDriftClass(entry) {
2159
+ if (entry.state === "unresolved") {
2160
+ return entry.reason === "file-missing" || entry.reason === "symbol-not-found" ? "gone" : void 0;
2161
+ }
2162
+ return entry.state === "drifted" ? "changed" : void 0;
2163
+ }
2164
+ function classOf(reason) {
2165
+ const settled = provisionalDriftClass({ state: "unresolved", reason });
2166
+ return settled ? { class: settled } : {};
2167
+ }
2063
2168
  function hashIn(source, anchor, resolvers) {
2064
2169
  const outcome = resolveAnchorSpan(source, anchor, resolvers);
2065
2170
  if (!outcome.ok) return { ok: false, reason: outcome.reason };
2171
+ const { hash, kind } = anchorHashOf(anchor, outcome);
2066
2172
  return {
2067
2173
  ok: true,
2068
2174
  current: {
2069
- hash: hashAnchorText(outcome.span.text),
2175
+ hash,
2176
+ kind,
2070
2177
  lines: outcome.span.endLine - outcome.span.startLine + 1,
2071
2178
  ...outcome.resolver ? { resolver: outcome.resolver } : {}
2072
2179
  }
@@ -2079,11 +2186,14 @@ function resolverExtras(source, anchor, current) {
2079
2186
  };
2080
2187
  }
2081
2188
  function compared(anchor, current, extra = {}) {
2189
+ const matched = current.hash === anchor.hash;
2082
2190
  return {
2083
2191
  ...base(anchor),
2084
- state: current.hash === anchor.hash ? "match" : "drifted",
2192
+ state: matched ? "match" : "drifted",
2085
2193
  currentHash: current.hash,
2194
+ hashKind: current.kind,
2086
2195
  diffSize: anchor.lines === void 0 ? null : Math.abs(current.lines - anchor.lines),
2196
+ ...matched ? {} : { class: "changed" },
2087
2197
  ...extra
2088
2198
  };
2089
2199
  }
@@ -2240,8 +2350,8 @@ async function isStale(bundlePath2) {
2240
2350
  let stale = false;
2241
2351
  await mapLimit(names, DEFAULT_IO_CONCURRENCY, async (name) => {
2242
2352
  if (stale) return;
2243
- const at = await (0, import_promises5.stat)((0, import_node_path6.join)(bundlePath2, name)).then((s) => s.mtimeMs).catch(() => 0);
2244
- if (at > indexAt) stale = true;
2353
+ const at2 = await (0, import_promises5.stat)((0, import_node_path6.join)(bundlePath2, name)).then((s) => s.mtimeMs).catch(() => 0);
2354
+ if (at2 > indexAt) stale = true;
2245
2355
  });
2246
2356
  return stale;
2247
2357
  }
@@ -2536,8 +2646,8 @@ function trace(seedId, bundle, options = {}) {
2536
2646
  return [...reached.values()].sort(byGeneratedAt);
2537
2647
  }
2538
2648
  function byGeneratedAt(left, right) {
2539
- const at = (step) => step.record.frontmatter.generated?.at ?? "";
2540
- return at(left).localeCompare(at(right)) || left.depth - right.depth;
2649
+ const at2 = (step) => step.record.frontmatter.generated?.at ?? "";
2650
+ return at2(left).localeCompare(at2(right)) || left.depth - right.depth;
2541
2651
  }
2542
2652
 
2543
2653
  // src/pack.ts
@@ -2624,6 +2734,13 @@ function typeRank(record) {
2624
2734
  return index2 === -1 ? TYPE_PRIORITY.length : index2;
2625
2735
  }
2626
2736
 
2737
+ // src/kb-tags.ts
2738
+ function matchesTags(record, filter) {
2739
+ if (!filter.tags?.length && !filter.excludeTags?.length) return true;
2740
+ const carried = new Set(record.frontmatter.tags ?? []);
2741
+ return (filter.tags ?? []).every((tag) => carried.has(tag)) && !(filter.excludeTags ?? []).some((tag) => carried.has(tag));
2742
+ }
2743
+
2627
2744
  // src/catalog.ts
2628
2745
  var EMPTY_STANDINGS = {
2629
2746
  current: 0,
@@ -2634,7 +2751,7 @@ var EMPTY_STANDINGS = {
2634
2751
  };
2635
2752
  function catalog(bundle, options = {}) {
2636
2753
  const wanted = options.type ? bundle.filter((record) => record.frontmatter.type === options.type) : bundle;
2637
- const entries = adjudicate(wanted, bundle, options.now ?? /* @__PURE__ */ new Date()).map((hit) => ({
2754
+ const entries = adjudicate(wanted, bundle, options.now ?? /* @__PURE__ */ new Date()).filter((hit) => matchesTags(hit.record, options)).map((hit) => ({
2638
2755
  conceptId: hit.record.conceptId,
2639
2756
  type: hit.record.frontmatter.type,
2640
2757
  title: hit.record.frontmatter.title ?? null,
@@ -2907,13 +3024,16 @@ var KbStore = class {
2907
3024
  return this.parse(conceptId2, raw);
2908
3025
  }
2909
3026
  /**
2910
- * Every record in the bundle, optionally narrowed to one type.
3027
+ * Every record in the bundle, optionally narrowed to one type and to the
3028
+ * records carrying every tag in `filter.tags`. Selection only — `excludeTags`
3029
+ * is not taken here, because `query`, `catalog` and `load` read through this
3030
+ * and must adjudicate over the whole base.
2911
3031
  *
2912
3032
  * A file that fails to parse is skipped and logged rather than thrown: one
2913
3033
  * malformed record — hand-edited, or written by a producer we don't know —
2914
3034
  * must not make the whole bundle unreadable.
2915
3035
  */
2916
- async list(bundlePath2, type) {
3036
+ async list(bundlePath2, type, filter = {}) {
2917
3037
  const root = this.root(bundlePath2);
2918
3038
  let names;
2919
3039
  try {
@@ -2927,7 +3047,9 @@ var KbStore = class {
2927
3047
  DEFAULT_IO_CONCURRENCY,
2928
3048
  async ({ name, conceptId: conceptId2 }) => this.parse(conceptId2, await (0, import_promises6.readFile)((0, import_node_path7.join)(root, name), "utf8"))
2929
3049
  );
2930
- return records.filter((record) => record !== null);
3050
+ return records.filter(
3051
+ (record) => record !== null && matchesTags(record, filter)
3052
+ );
2931
3053
  }
2932
3054
  /**
2933
3055
  * Moves a record's status, preserving everything else.
@@ -2973,8 +3095,8 @@ var KbStore = class {
2973
3095
  * and the refusal is logged under its own operation name — `mutate` only
2974
3096
  * logs what it publishes.
2975
3097
  */
2976
- async verify(bundlePath2, conceptId2, note, actor = "unknown", at = (/* @__PURE__ */ new Date()).toISOString()) {
2977
- const event = kbVerifiedEventSchema.parse({ by: actor, at, note });
3098
+ async verify(bundlePath2, conceptId2, note, actor = "unknown", at2 = (/* @__PURE__ */ new Date()).toISOString()) {
3099
+ const event = kbVerifiedEventSchema.parse({ by: actor, at: at2, note });
2978
3100
  const existing = await this.read(bundlePath2, conceptId2);
2979
3101
  if (!existing) throw new KbRecordNotFoundError(conceptId2);
2980
3102
  const generatedBy = existing.frontmatter.generated?.by;
@@ -3026,14 +3148,14 @@ var KbStore = class {
3026
3148
  return superseded;
3027
3149
  }
3028
3150
  /** Resolves an open question, stamping who answered and when. */
3029
- async answer(bundlePath2, conceptId2, answer, actor = "unknown", at = (/* @__PURE__ */ new Date()).toISOString()) {
3151
+ async answer(bundlePath2, conceptId2, answer, actor = "unknown", at2 = (/* @__PURE__ */ new Date()).toISOString()) {
3030
3152
  return this.mutate(
3031
3153
  bundlePath2,
3032
3154
  conceptId2,
3033
3155
  (frontmatter) => ({
3034
3156
  ...frontmatter,
3035
3157
  strauss_status: "resolved",
3036
- strauss_answered: { by: actor, at }
3158
+ strauss_answered: { by: actor, at: at2 }
3037
3159
  }),
3038
3160
  { operation: "answer", by: actor },
3039
3161
  (body) => `${body.trimEnd()}
@@ -3067,9 +3189,10 @@ ${answer}
3067
3189
  /* @__PURE__ */ new Date(),
3068
3190
  await this.detectDrift(narrowed, options.repoRoot)
3069
3191
  );
3070
- if (options.includeNonCurrent) return adjudicated;
3071
- const present = new Set(adjudicated.map((hit) => hit.record.conceptId));
3072
- return adjudicated.filter(
3192
+ const kept = adjudicated.filter((hit) => matchesTags(hit.record, options));
3193
+ if (options.includeNonCurrent) return kept;
3194
+ const present = new Set(kept.map((hit) => hit.record.conceptId));
3195
+ return kept.filter(
3073
3196
  (hit) => hit.standing !== "superseded" || !hit.heads.some((head) => present.has(head.conceptId))
3074
3197
  );
3075
3198
  }
@@ -3172,14 +3295,17 @@ ${answer}
3172
3295
  /* @__PURE__ */ new Date(),
3173
3296
  await this.detectDrift(wanted, options.repoRoot)
3174
3297
  );
3175
- const records = adjudicated.filter((hit) => hit.standing !== "superseded");
3176
- const superseded = adjudicated.filter((hit) => hit.standing === "superseded").map(stub);
3298
+ const kept = adjudicated.filter(
3299
+ (hit) => matchesTags(hit.record, { excludeTags: options.excludeTags })
3300
+ );
3301
+ const records = kept.filter((hit) => hit.standing !== "superseded");
3302
+ const superseded = kept.filter((hit) => hit.standing === "superseded").map(stub);
3177
3303
  const approxTokens2 = records.reduce((total, hit) => total + estimateTokens(hit.record), 0) + superseded.reduce((total, entry) => total + estimateStubTokens(entry), 0);
3178
3304
  const bundleDigestValue = bundleDigest(records, superseded);
3179
3305
  if (!options.all && approxTokens2 > budgetTokens) {
3180
3306
  return {
3181
3307
  loaded: false,
3182
- recordCount: wanted.length,
3308
+ recordCount: kept.length,
3183
3309
  approxTokens: approxTokens2,
3184
3310
  budgetTokens,
3185
3311
  message: refusalMessage({
@@ -3192,7 +3318,7 @@ ${answer}
3192
3318
  }
3193
3319
  return {
3194
3320
  loaded: true,
3195
- recordCount: wanted.length,
3321
+ recordCount: kept.length,
3196
3322
  tokensLoaded: approxTokens2,
3197
3323
  budgetTokens: options.all ? null : budgetTokens,
3198
3324
  records,
@@ -3202,24 +3328,34 @@ ${answer}
3202
3328
  }
3203
3329
  /**
3204
3330
  * `load`'s digest without `load`'s bodies — the same records, adjudicated
3205
- * the same way, handed back as a stamp. Skips the anchor drift pass, which
3206
- * reads source files and only ever adds warnings: no warning reaches the
3207
- * digest, so the value is identical to the one `load` returns.
3331
+ * the same way, handed back as a stamp.
3332
+ *
3333
+ * Drift is counted but kept out of the digest, which is what lets the reload
3334
+ * hook ask one question and get two answers: whether the base moved, and
3335
+ * whether the code under it did. A `load` and a `stamp` of the same base
3336
+ * still agree on the digest, because no warning has ever reached it.
3208
3337
  */
3209
- async stamp(bundlePath2) {
3338
+ async stamp(bundlePath2, options = {}) {
3210
3339
  const bundle = await this.list(bundlePath2);
3211
3340
  const adjudicated = adjudicate(bundle, bundle, /* @__PURE__ */ new Date());
3212
3341
  const current = adjudicated.filter((hit) => hit.standing !== "superseded");
3213
3342
  const superseded = adjudicated.filter((hit) => hit.standing === "superseded").map(stub);
3214
3343
  const stamped = bundleStamp(current, superseded);
3215
- const dates = bundle.map((record) => record.frontmatter.generated?.at ?? null).filter((at) => typeof at === "string").sort();
3344
+ const dates = bundle.map((record) => record.frontmatter.generated?.at ?? null).filter((at2) => typeof at2 === "string").sort();
3345
+ const drift = await this.detectDrift(bundle, options.repoRoot);
3346
+ const drifted2 = drift === void 0 ? null : [...drift.values()].filter(
3347
+ (entries) => entries.some(
3348
+ (entry) => entry.state !== "match" && !isUncheckedReason(entry.reason)
3349
+ )
3350
+ ).length;
3216
3351
  return {
3217
3352
  path: bundlePath2,
3218
3353
  digest: stamped.digest,
3219
3354
  recordCount: bundle.length,
3220
3355
  superseded: superseded.length,
3221
3356
  newestAt: dates.at(-1) ?? null,
3222
- records: stamped.records
3357
+ records: stamped.records,
3358
+ drifted: drifted2
3223
3359
  };
3224
3360
  }
3225
3361
  /** How a position was arrived at, as a timeline. See `trace.ts`. */
@@ -3669,14 +3805,17 @@ function asBudgets(value) {
3669
3805
  if (value === null || typeof value !== "object") return {};
3670
3806
  const table2 = value;
3671
3807
  const pick = (key2, min) => {
3672
- const raw = table2[key2];
3673
- return typeof raw === "number" && Number.isInteger(raw) && raw >= min ? raw : void 0;
3808
+ const raw2 = table2[key2];
3809
+ return typeof raw2 === "number" && Number.isInteger(raw2) && raw2 >= min ? raw2 : void 0;
3674
3810
  };
3675
3811
  const budgetTokens = pick("budgetTokens", 1);
3676
3812
  const fullUnderTokens = pick("fullUnderTokens", 0);
3813
+ const raw = table2["excludeTags"];
3814
+ const excludeTags = Array.isArray(raw) ? raw.filter((tag) => typeof tag === "string" && tag !== "") : void 0;
3677
3815
  return {
3678
3816
  ...budgetTokens ? { budgetTokens } : {},
3679
- ...fullUnderTokens !== void 0 ? { fullUnderTokens } : {}
3817
+ ...fullUnderTokens !== void 0 ? { fullUnderTokens } : {},
3818
+ ...excludeTags ? { excludeTags } : {}
3680
3819
  };
3681
3820
  }
3682
3821
  function contextProfileBudgets(manifest, profile) {
@@ -3876,7 +4015,7 @@ async function listPins(store, workspaceDir) {
3876
4015
  }
3877
4016
 
3878
4017
  // src/kb-pins/pin.ts
3879
- async function pinBase(store, workspaceDir, bundlePath2, at, options = {}) {
4018
+ async function pinBase(store, workspaceDir, bundlePath2, at2, options = {}) {
3880
4019
  const layer = options.layer ?? "project";
3881
4020
  const root = layerRoot(workspaceDir, layer);
3882
4021
  const manifest = await readPinsLayer(workspaceDir, layer);
@@ -3904,7 +4043,7 @@ async function pinBase(store, workspaceDir, bundlePath2, at, options = {}) {
3904
4043
  return {
3905
4044
  path: existing.path,
3906
4045
  layer,
3907
- pinnedAt: existing.pinnedAt ?? at,
4046
+ pinnedAt: existing.pinnedAt ?? at2,
3908
4047
  alreadyPinned: true,
3909
4048
  ...updated.mode ? { mode: updated.mode } : {},
3910
4049
  ...updated.profiles ? { profiles: updated.profiles } : {},
@@ -3914,7 +4053,7 @@ async function pinBase(store, workspaceDir, bundlePath2, at, options = {}) {
3914
4053
  }
3915
4054
  const entry = {
3916
4055
  path: storablePath(root, bundlePath2),
3917
- pinnedAt: at,
4056
+ pinnedAt: at2,
3918
4057
  ...fields
3919
4058
  };
3920
4059
  await writePinsLayer(workspaceDir, layer, {
@@ -3924,7 +4063,7 @@ async function pinBase(store, workspaceDir, bundlePath2, at, options = {}) {
3924
4063
  return {
3925
4064
  path: entry.path,
3926
4065
  layer,
3927
- pinnedAt: at,
4066
+ pinnedAt: at2,
3928
4067
  alreadyPinned: false,
3929
4068
  ...fields,
3930
4069
  ...warning ? { warning } : {}
@@ -3992,7 +4131,7 @@ function preamble() {
3992
4131
  "tokens."
3993
4132
  ].join("\n");
3994
4133
  }
3995
- async function renderBase(store, path, absolutePath, fullUnderTokens, pinMode, budgetTokens) {
4134
+ async function renderBase(store, path, absolutePath, fullUnderTokens, pinMode, budgetTokens, excludeTags) {
3996
4135
  const bundle = await store.list(absolutePath);
3997
4136
  if (bundle.length === 0) {
3998
4137
  return {
@@ -4006,7 +4145,8 @@ async function renderBase(store, path, absolutePath, fullUnderTokens, pinMode, b
4006
4145
  let degradedFrom;
4007
4146
  if (fullCap > 0) {
4008
4147
  const full = await store.load(absolutePath, {
4009
- budgetTokens: fullCap
4148
+ budgetTokens: fullCap,
4149
+ excludeTags
4010
4150
  });
4011
4151
  if (!full.loaded && pinMode === "full") {
4012
4152
  degradedFrom = { approxTokens: full.approxTokens };
@@ -4036,7 +4176,9 @@ async function renderBase(store, path, absolutePath, fullUnderTokens, pinMode, b
4036
4176
  };
4037
4177
  }
4038
4178
  }
4039
- const adjudicated = adjudicate(bundle, bundle);
4179
+ const adjudicated = adjudicate(bundle, bundle).filter(
4180
+ (hit) => matchesTags(hit.record, { excludeTags })
4181
+ );
4040
4182
  const lines = adjudicated.filter((hit) => hit.standing !== "superseded").map((hit) => renderIndexLine(hit.record));
4041
4183
  const superseded = adjudicated.filter((hit) => hit.standing === "superseded").map(
4042
4184
  (hit) => `- \`${hit.record.conceptId}\` \u2192 superseded by ${hit.heads.map((head) => `\`${head.conceptId}\``).join(", ") || "(missing replacement)"}`
@@ -4057,6 +4199,7 @@ async function buildContext(store, workspaceDir, options = {}) {
4057
4199
  const fromManifest = mergedContextBudgets(merged, options.profile);
4058
4200
  budgetTokens = options.budgetTokens ?? fromManifest.budgetTokens ?? builtin.budgetTokens ?? DEFAULT_CONTEXT_BUDGET;
4059
4201
  fullUnderTokens = options.fullUnderTokens ?? fromManifest.fullUnderTokens ?? builtin.fullUnderTokens ?? 0;
4202
+ const excludeTags = options.excludeTags ?? fromManifest.excludeTags ?? builtin.excludeTags ?? [];
4060
4203
  const pins = merged.pins.filter(
4061
4204
  (pin) => !pin.profiles?.length || !options.profile || pin.profiles.includes(options.profile)
4062
4205
  );
@@ -4077,7 +4220,8 @@ async function buildContext(store, workspaceDir, options = {}) {
4077
4220
  pin.absolutePath,
4078
4221
  fullUnderTokens,
4079
4222
  pin.mode,
4080
- budgetTokens
4223
+ budgetTokens,
4224
+ excludeTags
4081
4225
  ),
4082
4226
  frozen: pin.frozen === true
4083
4227
  }))
@@ -4452,18 +4596,18 @@ function expired(hits, now) {
4452
4596
  for (const hit of hits) {
4453
4597
  const raw = hit.record.frontmatter.stale_after;
4454
4598
  if (!raw) continue;
4455
- const at = Date.parse(raw);
4456
- if (Number.isNaN(at)) {
4599
+ const at2 = Date.parse(raw);
4600
+ if (Number.isNaN(at2)) {
4457
4601
  findings.push(
4458
4602
  finding(hit.record, `stale_after "${raw}" is not a readable date`)
4459
4603
  );
4460
4604
  continue;
4461
4605
  }
4462
- if (at < now.getTime()) {
4606
+ if (at2 < now.getTime()) {
4463
4607
  findings.push(
4464
4608
  finding(
4465
4609
  hit.record,
4466
- `stale since ${raw} (${daysBetween(at, now.getTime())} days ago)`
4610
+ `stale since ${raw} (${daysBetween(at2, now.getTime())} days ago)`
4467
4611
  )
4468
4612
  );
4469
4613
  }
@@ -4476,12 +4620,12 @@ function expiring(hits, now, withinDays) {
4476
4620
  for (const hit of hits) {
4477
4621
  const raw = hit.record.frontmatter.stale_after;
4478
4622
  if (!raw) continue;
4479
- const at = Date.parse(raw);
4480
- if (Number.isNaN(at) || at < now.getTime() || at > horizon) continue;
4623
+ const at2 = Date.parse(raw);
4624
+ if (Number.isNaN(at2) || at2 < now.getTime() || at2 > horizon) continue;
4481
4625
  findings.push(
4482
4626
  finding(
4483
4627
  hit.record,
4484
- `goes stale ${raw} (in ${daysBetween(now.getTime(), at)} days)`
4628
+ `goes stale ${raw} (in ${daysBetween(now.getTime(), at2)} days)`
4485
4629
  )
4486
4630
  );
4487
4631
  }
@@ -4651,13 +4795,16 @@ function anchorFindings(hits, kind, headline) {
4651
4795
  );
4652
4796
  }
4653
4797
  function describeAnchor(anchor) {
4654
- const at = anchor.symbol ? `${anchor.file}:${anchor.symbol}` : anchor.file;
4655
- if (anchor.reason) return `${at} (${anchor.reason})`;
4798
+ const at2 = anchor.symbol ? `${anchor.file}:${anchor.symbol}` : anchor.file;
4799
+ if (anchor.class === "gone") {
4800
+ return `${at2} gone${anchor.reason ? ` (${anchor.reason})` : ""}`;
4801
+ }
4802
+ if (anchor.reason) return `${at2} (${anchor.reason})`;
4656
4803
  if (anchor.remoteState === "drifted-on-default") {
4657
- return `${at} (matches ref, moved on the default branch)`;
4804
+ return `${at2} (matches ref, moved on the default branch)`;
4658
4805
  }
4659
- if (anchor.diffSize === null) return `${at} (changed, size unrecorded)`;
4660
- return anchor.diffSize === 0 ? `${at} (content changed, same line count)` : `${at} (${anchor.diffSize} line${anchor.diffSize === 1 ? "" : "s"} apart)`;
4806
+ if (anchor.diffSize === null) return `${at2} (changed, size unrecorded)`;
4807
+ return anchor.diffSize === 0 ? `${at2} (content changed, same line count)` : `${at2} (${anchor.diffSize} line${anchor.diffSize === 1 ? "" : "s"} apart)`;
4661
4808
  }
4662
4809
  function replaces(later, earlier) {
4663
4810
  return (later.frontmatter.strauss_supersedes ?? []).includes(earlier.conceptId) || earlier.frontmatter.strauss_superseded_by === later.conceptId;
@@ -4674,9 +4821,9 @@ function daysBetween(from, to) {
4674
4821
  return Math.max(0, Math.floor((to - from) / DAY_MS));
4675
4822
  }
4676
4823
  function ageInDays(record, now) {
4677
- const at = record.frontmatter.generated?.at;
4678
- if (!at) return null;
4679
- const written = Date.parse(at);
4824
+ const at2 = record.frontmatter.generated?.at;
4825
+ if (!at2) return null;
4826
+ const written = Date.parse(at2);
4680
4827
  if (Number.isNaN(written)) return null;
4681
4828
  return daysBetween(written, now.getTime());
4682
4829
  }
@@ -4735,6 +4882,9 @@ var import_zod9 = require("zod");
4735
4882
  var import_zod8 = require("zod");
4736
4883
  var bundlePath = import_zod8.z.string().min(1).describe("Absolute path to the knowledge base directory.");
4737
4884
  var conceptId = import_zod8.z.string().min(1).describe("e.g. decision.cursor-v2");
4885
+ var TAGS = import_zod8.z.array(import_zod8.z.string().min(1)).optional().describe(
4886
+ "Keep only records carrying every one of these frontmatter tags. Matched exactly."
4887
+ );
4738
4888
  var REPO_ROOT = import_zod8.z.string().min(1).optional().describe(
4739
4889
  "Where the anchored source lives, for the drift check. Defaults to the working directory."
4740
4890
  );
@@ -4748,14 +4898,49 @@ function argvFlag(argv, name) {
4748
4898
  if (!value2) throw new KbMissingFlagValueError(name);
4749
4899
  return value2;
4750
4900
  }
4751
- const at = argv.indexOf(name);
4752
- if (at === -1) return void 0;
4753
- const value = argv[at + 1];
4901
+ const at2 = argv.indexOf(name);
4902
+ if (at2 === -1) return void 0;
4903
+ const value = argv[at2 + 1];
4754
4904
  if (value === void 0 || value.startsWith("--")) {
4755
4905
  throw new KbMissingFlagValueError(name);
4756
4906
  }
4757
4907
  return value;
4758
4908
  }
4909
+ function argvFlags(argv, name) {
4910
+ const values = [];
4911
+ for (const [at2, arg] of argv.entries()) {
4912
+ if (arg.startsWith(`${name}=`)) {
4913
+ const value = arg.slice(name.length + 1);
4914
+ if (!value) throw new KbMissingFlagValueError(name);
4915
+ values.push(value);
4916
+ } else if (arg === name) {
4917
+ const value = argv[at2 + 1];
4918
+ if (value === void 0 || value.startsWith("--")) {
4919
+ throw new KbMissingFlagValueError(name);
4920
+ }
4921
+ values.push(value);
4922
+ }
4923
+ }
4924
+ return values;
4925
+ }
4926
+ function argvWithout(argv, ...names) {
4927
+ const kept = [];
4928
+ for (let at2 = 0; at2 < argv.length; at2 += 1) {
4929
+ const arg = argv[at2];
4930
+ if (names.some((name) => arg.startsWith(`${name}=`))) continue;
4931
+ if (names.includes(arg)) {
4932
+ at2 += 1;
4933
+ continue;
4934
+ }
4935
+ kept.push(arg);
4936
+ }
4937
+ return kept;
4938
+ }
4939
+ function argvPositional(argv, ...names) {
4940
+ return argvWithout(argv.slice(1), ...names).find(
4941
+ (arg) => !arg.startsWith("--")
4942
+ );
4943
+ }
4759
4944
 
4760
4945
  // src/commands/anchor-resolve.ts
4761
4946
  function resolverSummary(results) {
@@ -4843,11 +5028,14 @@ var anchorResolveCommand = define({
4843
5028
  }
4844
5029
  const resolved = outcome.span;
4845
5030
  const producedBy = outcome.resolver;
4846
- const currentHash = hashAnchorText(resolved.text);
5031
+ const { hash: currentHash, kind } = anchorHashOf(anchor, outcome);
4847
5032
  const currentLines = resolved.endLine - resolved.startLine + 1;
5033
+ const stampedKind = outcome.normalized ? "ast" : "raw";
5034
+ const stampedHash = outcome.normalized ? anchorHashOf({ ...anchor, hash: void 0 }, outcome).hash : currentHash;
4848
5035
  const stamped = {
4849
5036
  ...anchor,
4850
- hash: currentHash,
5037
+ hash: stampedHash,
5038
+ hash_kind: stampedKind,
4851
5039
  lines: currentLines,
4852
5040
  resolved_at: now(),
4853
5041
  ...producedBy ? { resolver: producedBy } : {}
@@ -4857,7 +5045,8 @@ var anchorResolveCommand = define({
4857
5045
  results.push({
4858
5046
  ...base2,
4859
5047
  state: "stamped",
4860
- currentHash,
5048
+ currentHash: stampedHash,
5049
+ hashKind: stampedKind,
4861
5050
  ...producedBy ? { resolver: producedBy } : {}
4862
5051
  });
4863
5052
  updated.push(stamped);
@@ -4869,6 +5058,7 @@ var anchorResolveCommand = define({
4869
5058
  ...base2,
4870
5059
  state: "drifted",
4871
5060
  currentHash,
5061
+ hashKind: kind,
4872
5062
  diffSize: lineDelta(anchor, currentLines),
4873
5063
  ...producedBy ? { resolver: producedBy } : {},
4874
5064
  // A regex-stamped anchor re-read by tree-sitter drifts because the
@@ -4897,6 +5087,7 @@ var anchorResolveCommand = define({
4897
5087
  ...base2,
4898
5088
  state: "match",
4899
5089
  currentHash,
5090
+ hashKind: kind,
4900
5091
  ...producedBy ? { resolver: producedBy } : {},
4901
5092
  ...pinned ? { remoteState: "matches-ref" } : {}
4902
5093
  });
@@ -5066,25 +5257,39 @@ var import_zod12 = require("zod");
5066
5257
  var catalogCommand = define({
5067
5258
  name: "catalog",
5068
5259
  tool: "kb_catalog",
5069
- usage: "catalog [type]",
5260
+ usage: "catalog [type] [--tag T]...",
5070
5261
  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.",
5071
5262
  input: import_zod12.z.object({
5072
5263
  bundlePath,
5073
- type: import_zod12.z.enum(KB_RECORD_TYPES).optional()
5074
- }),
5075
- fromArgv: (argv, path) => ({
5076
- bundlePath: path,
5077
- ...argv[1] && !argv[1].startsWith("--") ? { type: argv[1] } : {}
5264
+ type: import_zod12.z.enum(KB_RECORD_TYPES).optional(),
5265
+ tags: TAGS
5078
5266
  }),
5079
- run: async ({ store }, { bundlePath: path, type }) => render(
5080
- await store.catalog(path, { ...type ? { type } : {} }),
5267
+ fromArgv: (argv, path) => {
5268
+ const tags = argvFlags(argv, "--tag");
5269
+ const type = argvPositional(argv, "--tag");
5270
+ return {
5271
+ bundlePath: path,
5272
+ ...type ? { type } : {},
5273
+ ...tags.length ? { tags } : {}
5274
+ };
5275
+ },
5276
+ run: async ({ store }, { bundlePath: path, type, tags }) => render(
5277
+ await store.catalog(path, {
5278
+ ...type ? { type } : {},
5279
+ ...tags ? { tags } : {}
5280
+ }),
5081
5281
  path,
5082
- type
5282
+ type,
5283
+ tags
5083
5284
  )
5084
5285
  });
5085
- function render(result, bundle, type) {
5286
+ function render(result, bundle, type, tags) {
5287
+ const narrowed = [
5288
+ ...type ? [type] : [],
5289
+ ...tags?.length ? [`tags: ${tags.join(", ")}`] : []
5290
+ ].join(" \xB7 ");
5086
5291
  const lines = [
5087
- `# KB Catalog${type ? ` \u2014 ${type}` : ""}`,
5292
+ `# KB Catalog${narrowed ? ` \u2014 ${narrowed}` : ""}`,
5088
5293
  `bundle: ${bundle}`,
5089
5294
  `${count(result.recordCount, "record")}: ${standingCounts(result)}`
5090
5295
  ];
@@ -5096,7 +5301,7 @@ function render(result, bundle, type) {
5096
5301
  lines.push("");
5097
5302
  if (!result.entries.length) {
5098
5303
  lines.push(
5099
- type ? `(no records of type ${type})` : "(no records \u2014 this base is empty)"
5304
+ narrowed ? `(no records matching ${narrowed})` : "(no records \u2014 this base is empty)"
5100
5305
  );
5101
5306
  } else {
5102
5307
  for (const entry of result.entries) lines.push(renderCatalogLine(entry));
@@ -5129,7 +5334,7 @@ var import_zod13 = require("zod");
5129
5334
  var contextCommand = define({
5130
5335
  name: "context",
5131
5336
  tool: "kb_context",
5132
- usage: "context [--profile NAME] [--budget N] [--full-under N] [--format json] [--event NAME]",
5337
+ usage: "context [--profile NAME] [--budget N] [--full-under N] [--exclude-tag T]... [--format json] [--event NAME]",
5133
5338
  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.",
5134
5339
  input: import_zod13.z.object({
5135
5340
  budgetTokens: import_zod13.z.number().int().positive().optional().describe(
@@ -5141,6 +5346,9 @@ var contextCommand = define({
5141
5346
  profile: import_zod13.z.string().optional().describe(
5142
5347
  "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."
5143
5348
  ),
5349
+ excludeTags: import_zod13.z.array(import_zod13.z.string().min(1)).optional().describe(
5350
+ "Frontmatter tags whose records stay out of the block. The base stays pinned and stays readable by tool; resolved like the budgets."
5351
+ ),
5144
5352
  format: import_zod13.z.enum(["markdown", "json"]).optional().describe(
5145
5353
  "CLI envelope for hook protocols that require strict JSON on stdout. MCP callers omit this \u2014 the block itself is identical."
5146
5354
  ),
@@ -5154,19 +5362,22 @@ var contextCommand = define({
5154
5362
  const profile = argvFlag(argv, "--profile");
5155
5363
  const format = argvFlag(argv, "--format");
5156
5364
  const event = argvFlag(argv, "--event");
5365
+ const excludeTags = argvFlags(argv, "--exclude-tag");
5157
5366
  return {
5158
5367
  ...budget ? { budgetTokens: Number(budget) } : {},
5159
5368
  ...fullUnder ? { fullUnderTokens: Number(fullUnder) } : {},
5160
5369
  ...profile ? { profile } : {},
5370
+ ...excludeTags.length ? { excludeTags } : {},
5161
5371
  ...format ? { format } : {},
5162
5372
  ...event ? { event } : {}
5163
5373
  };
5164
5374
  },
5165
- run: async ({ store }, { budgetTokens, fullUnderTokens, profile, format, event }) => {
5375
+ run: async ({ store }, { budgetTokens, fullUnderTokens, profile, excludeTags, format, event }) => {
5166
5376
  const result = await buildContext(store, process.cwd(), {
5167
5377
  ...budgetTokens ? { budgetTokens } : {},
5168
5378
  ...fullUnderTokens ? { fullUnderTokens } : {},
5169
5379
  ...profile ? { profile } : {},
5380
+ ...excludeTags ? { excludeTags } : {},
5170
5381
  // Degradations — a full pin that could not fit, a refused block — go
5171
5382
  // to stderr as well as into the block itself: stderr is diagnostics on
5172
5383
  // both surfaces (hooks discard it, MCP logs it), so an operator can
@@ -5180,14 +5391,567 @@ var contextCommand = define({
5180
5391
  });
5181
5392
 
5182
5393
  // src/commands/doctor.ts
5394
+ var import_zod15 = require("zod");
5395
+
5396
+ // src/drift/git.ts
5397
+ var import_node_child_process3 = require("child_process");
5398
+ var import_node_util3 = require("util");
5399
+ var execFileAsync3 = (0, import_node_util3.promisify)(import_node_child_process3.execFile);
5400
+ var MAX_GIT_OUTPUT_BYTES = 1048576;
5401
+ var GIT_TIMEOUT_MS = 5e3;
5402
+ async function git2(cwd, args) {
5403
+ const env = { ...process.env };
5404
+ delete env["GIT_DIR"];
5405
+ delete env["GIT_WORK_TREE"];
5406
+ delete env["GIT_INDEX_FILE"];
5407
+ try {
5408
+ const { stdout } = await execFileAsync3("git", ["-C", cwd, ...args], {
5409
+ timeout: GIT_TIMEOUT_MS,
5410
+ maxBuffer: MAX_GIT_OUTPUT_BYTES,
5411
+ env
5412
+ });
5413
+ return { ok: true, stdout };
5414
+ } catch {
5415
+ return { ok: false };
5416
+ }
5417
+ }
5418
+ async function listRepoFiles(repoRoot) {
5419
+ const result = await git2(repoRoot, ["ls-files", "-z", "--cached"]);
5420
+ if (!result.ok) return [];
5421
+ return result.stdout.split("\0").filter(Boolean);
5422
+ }
5423
+ async function readOldSource(repoRoot, anchor) {
5424
+ if (!filePathIsSafe(anchor.file))
5425
+ return { ok: false, reason: "unrecoverable" };
5426
+ if (anchor.ref && refShapeIsSafe(anchor.ref)) {
5427
+ const shown2 = await showFile(repoRoot, anchor.ref, anchor.file);
5428
+ if (shown2 !== null) {
5429
+ return {
5430
+ ok: true,
5431
+ source: shown2,
5432
+ origin: { kind: "ref", ref: anchor.ref }
5433
+ };
5434
+ }
5435
+ }
5436
+ const at2 = anchor.resolved_at;
5437
+ if (!at2 || Number.isNaN(Date.parse(at2))) {
5438
+ return { ok: false, reason: "unrecoverable" };
5439
+ }
5440
+ const found = await git2(repoRoot, [
5441
+ "log",
5442
+ "-1",
5443
+ "--format=%H",
5444
+ `--before=${at2}`,
5445
+ "--end-of-options",
5446
+ "HEAD",
5447
+ "--",
5448
+ anchor.file
5449
+ ]);
5450
+ const sha = found.ok ? found.stdout.trim() : "";
5451
+ if (!sha || !refShapeIsSafe(sha))
5452
+ return { ok: false, reason: "unrecoverable" };
5453
+ const shown = await showFile(repoRoot, sha, anchor.file);
5454
+ if (shown === null) return { ok: false, reason: "unrecoverable" };
5455
+ return { ok: true, source: shown, origin: { kind: "history", ref: sha } };
5456
+ }
5457
+ async function showFile(repoRoot, ref, file) {
5458
+ const path = file.replace(/^\.\//, "");
5459
+ const result = await git2(repoRoot, [
5460
+ "show",
5461
+ "--end-of-options",
5462
+ `${ref}:${path}`
5463
+ ]);
5464
+ return result.ok ? result.stdout : null;
5465
+ }
5466
+
5467
+ // src/drift/moved.ts
5468
+ var import_promises9 = require("fs/promises");
5469
+ var MAX_MOVED_SEARCH_FILES = 2e3;
5470
+ var SEARCH_BATCH = 64;
5471
+ function movedSearch(repoRoot, options = {}) {
5472
+ const read = options.reader ?? anchorFileReader(repoRoot);
5473
+ const sizeOf = options.sizeOf ?? diskSize(repoRoot);
5474
+ const resolver = new TreeSitterResolver();
5475
+ let repoFiles;
5476
+ const prepared = /* @__PURE__ */ new Set();
5477
+ const filesForLanguage = async (language) => {
5478
+ repoFiles ??= listRepoFiles(repoRoot);
5479
+ return (await repoFiles).filter((file) => languageForFile(file) === language).slice(0, MAX_MOVED_SEARCH_FILES);
5480
+ };
5481
+ return {
5482
+ async find(anchor) {
5483
+ const stored = anchor.hash;
5484
+ if (!stored) return void 0;
5485
+ const language = languageForFile(anchor.file);
5486
+ if (!language) return sameFileWindow(anchor, read, stored);
5487
+ const candidates = await filesForLanguage(language);
5488
+ if (!prepared.has(language)) {
5489
+ await resolver.prepare(candidates.length ? candidates : [anchor.file]);
5490
+ prepared.add(language);
5491
+ }
5492
+ const floor = anchor.lines ?? 0;
5493
+ for (let at2 = 0; at2 < candidates.length; at2 += SEARCH_BATCH) {
5494
+ const batch = candidates.slice(at2, at2 + SEARCH_BATCH);
5495
+ const hits = await mapLimit(
5496
+ batch,
5497
+ DEFAULT_IO_CONCURRENCY,
5498
+ async (file) => {
5499
+ const size2 = await sizeOf(file);
5500
+ if (size2 !== null && size2 < floor) return void 0;
5501
+ return matchIn(resolver, read, anchor, stored, file);
5502
+ }
5503
+ );
5504
+ const found = hits.find((hit) => hit !== void 0);
5505
+ if (found) return found;
5506
+ }
5507
+ return void 0;
5508
+ }
5509
+ };
5510
+ }
5511
+ async function matchIn(resolver, read, anchor, stored, file) {
5512
+ const source = await read(file);
5513
+ if (!source.ok) return void 0;
5514
+ const normalized = source.source.replace(/\r\n/g, "\n");
5515
+ for (const found of resolver.spans(normalized, file)) {
5516
+ const text = anchor.hash_kind === "ast" ? resolver.normalize(found.span.text, file) : found.span.text;
5517
+ if (text === null || hashAnchorText(text) !== stored) continue;
5518
+ if (file === anchor.file && found.symbol === anchor.symbol) continue;
5519
+ return {
5520
+ file,
5521
+ symbol: found.symbol,
5522
+ startLine: found.span.startLine,
5523
+ endLine: found.span.endLine
5524
+ };
5525
+ }
5526
+ return void 0;
5527
+ }
5528
+ function diskSize(repoRoot) {
5529
+ return async (file) => {
5530
+ const path = anchorFilePath(repoRoot, file);
5531
+ if (path === null) return null;
5532
+ try {
5533
+ return (await (0, import_promises9.stat)(path)).size;
5534
+ } catch {
5535
+ return null;
5536
+ }
5537
+ };
5538
+ }
5539
+ async function sameFileWindow(anchor, read, stored) {
5540
+ const height = anchor.lines;
5541
+ if (!height || anchor.hash_kind === "ast") return void 0;
5542
+ const source = await read(anchor.file);
5543
+ if (!source.ok) return void 0;
5544
+ const lines = source.source.replace(/\r\n/g, "\n").split("\n");
5545
+ for (let at2 = 0; at2 + height <= lines.length; at2++) {
5546
+ if (hashAnchorText(lines.slice(at2, at2 + height).join("\n")) !== stored) {
5547
+ continue;
5548
+ }
5549
+ return {
5550
+ file: anchor.file,
5551
+ ...anchor.symbol ? { symbol: anchor.symbol } : {},
5552
+ startLine: at2 + 1,
5553
+ endLine: at2 + height
5554
+ };
5555
+ }
5556
+ return void 0;
5557
+ }
5558
+
5559
+ // src/drift/classify.ts
5560
+ async function classifyDrift(repoRoot, record, entries, options = {}) {
5561
+ const anchors = (record.frontmatter.strauss_anchors ?? []).filter(
5562
+ (anchor) => anchor.hash
5563
+ );
5564
+ const reader = options.reader ?? anchorFileReader(repoRoot);
5565
+ const treeSitter = new TreeSitterResolver();
5566
+ const resolvers = [treeSitter, regexResolver];
5567
+ const search = options.search ?? movedSearch(repoRoot, { ...options.reader ? { reader } : {} });
5568
+ const wanted = [];
5569
+ entries.forEach((entry, at2) => {
5570
+ const anchor = anchors[at2];
5571
+ if (!anchor) return;
5572
+ if (entry.state === "match" || isUncheckedReason(entry.reason)) return;
5573
+ wanted.push({ anchor, entry });
5574
+ });
5575
+ if (!wanted.length) return [];
5576
+ await prepareResolvers(
5577
+ resolvers,
5578
+ wanted.map(({ anchor }) => anchor.file)
5579
+ );
5580
+ const out = [];
5581
+ for (const { anchor, entry } of wanted) {
5582
+ const movedTo = await search.find(anchor);
5583
+ if (movedTo) {
5584
+ out.push({
5585
+ anchor,
5586
+ entry: { ...entry, class: "moved", movedTo },
5587
+ class: "moved"
5588
+ });
5589
+ continue;
5590
+ }
5591
+ const newText = await currentText(reader, anchor, resolvers);
5592
+ const old = options.withHistory === false ? { ok: false, reason: "unrecoverable" } : await readOldSource(repoRoot, anchor);
5593
+ const oldText = old.ok ? spanIn(old.source, anchor, resolvers) : void 0;
5594
+ const settled = newText !== void 0 && oldText !== void 0 && sameTokens(treeSitter, anchor.file, oldText, newText) ? "cosmetic" : entry.class ?? "changed";
5595
+ out.push({
5596
+ anchor,
5597
+ entry: { ...entry, class: settled },
5598
+ class: settled,
5599
+ ...newText !== void 0 ? { newText } : {},
5600
+ ...oldText !== void 0 ? { oldText } : {},
5601
+ ...old.ok ? { oldOrigin: old.origin } : {}
5602
+ });
5603
+ }
5604
+ return out;
5605
+ }
5606
+ function sameTokens(resolver, file, before, after) {
5607
+ if (before === after) return false;
5608
+ const left = resolver.normalize(before, file);
5609
+ const right = resolver.normalize(after, file);
5610
+ return left !== null && left === right;
5611
+ }
5612
+ async function currentText(reader, anchor, resolvers) {
5613
+ const read = await reader(anchor.file);
5614
+ if (!read.ok) return void 0;
5615
+ return spanIn(read.source, anchor, resolvers);
5616
+ }
5617
+ function spanIn(source, anchor, resolvers) {
5618
+ const outcome = resolveAnchorSpan(source, anchor, resolvers);
5619
+ return outcome.ok ? outcome.span.text : void 0;
5620
+ }
5621
+
5622
+ // src/drift/diff.ts
5623
+ var MAX_ANCHOR_DIFF_LINES = 200;
5624
+ var PACKET_DIFF_LINE_BUDGET = 200;
5625
+ var MIN_ANCHOR_DIFF_LINES = 12;
5626
+ function diffBudget(anchors) {
5627
+ if (anchors <= 0) return MAX_ANCHOR_DIFF_LINES;
5628
+ return Math.min(
5629
+ MAX_ANCHOR_DIFF_LINES,
5630
+ Math.max(
5631
+ MIN_ANCHOR_DIFF_LINES,
5632
+ Math.floor(PACKET_DIFF_LINE_BUDGET / anchors)
5633
+ )
5634
+ );
5635
+ }
5636
+ function unifiedDiff(before, after, options = {}) {
5637
+ const max = options.maxLines ?? MAX_ANCHOR_DIFF_LINES;
5638
+ const left = before.replace(/\r\n/g, "\n").split("\n");
5639
+ const right = after.replace(/\r\n/g, "\n").split("\n");
5640
+ const body = [];
5641
+ let added = 0;
5642
+ let removed = 0;
5643
+ for (const edit of edits(left, right)) {
5644
+ if (edit.kind === "same") body.push(` ${edit.line}`);
5645
+ else if (edit.kind === "remove") {
5646
+ body.push(`-${edit.line}`);
5647
+ removed += 1;
5648
+ } else {
5649
+ body.push(`+${edit.line}`);
5650
+ added += 1;
5651
+ }
5652
+ }
5653
+ const truncated = body.length > max;
5654
+ const shown = truncated ? body.slice(0, max) : body;
5655
+ const header = `@@ -1,${left.length} +1,${right.length} @@${options.oldLabel ? ` ${options.oldLabel} \u2192 ${options.newLabel ?? ""}`.trimEnd() : ""}`;
5656
+ const lines = [header, ...shown];
5657
+ if (truncated) lines.push(`\u2026 ${body.length - max} more diff lines`);
5658
+ return { text: lines.join("\n"), added, removed, truncated };
5659
+ }
5660
+ function edits(left, right) {
5661
+ const rows = left.length;
5662
+ const cols = right.length;
5663
+ const table2 = Array.from(
5664
+ { length: rows + 1 },
5665
+ () => new Array(cols + 1).fill(0)
5666
+ );
5667
+ for (let row2 = rows - 1; row2 >= 0; row2--) {
5668
+ for (let col2 = cols - 1; col2 >= 0; col2--) {
5669
+ table2[row2][col2] = left[row2] === right[col2] ? table2[row2 + 1][col2 + 1] + 1 : Math.max(
5670
+ table2[row2 + 1][col2],
5671
+ table2[row2][col2 + 1]
5672
+ );
5673
+ }
5674
+ }
5675
+ const out = [];
5676
+ let row = 0;
5677
+ let col = 0;
5678
+ while (row < rows && col < cols) {
5679
+ if (left[row] === right[col]) {
5680
+ out.push({ kind: "same", line: left[row] });
5681
+ row += 1;
5682
+ col += 1;
5683
+ } else if (table2[row + 1][col] >= table2[row][col + 1]) {
5684
+ out.push({ kind: "remove", line: left[row] });
5685
+ row += 1;
5686
+ } else {
5687
+ out.push({ kind: "add", line: right[col] });
5688
+ col += 1;
5689
+ }
5690
+ }
5691
+ for (; row < rows; row++)
5692
+ out.push({ kind: "remove", line: left[row] });
5693
+ for (; col < cols; col++)
5694
+ out.push({ kind: "add", line: right[col] });
5695
+ return out;
5696
+ }
5697
+
5698
+ // src/drift/packet.ts
5699
+ var PRESUMED_INVALID = [
5700
+ "fact",
5701
+ "constraint",
5702
+ "contract"
5703
+ ];
5704
+ var RATIONALE_SURVIVES = ["decision", "risk"];
5705
+ var DEFAULT_NOTES = {
5706
+ "presumed-invalidated": "the code this claim was taken from changed; presume it no longer holds until re-read",
5707
+ "rationale-may-survive": "the reasoning may outlive the code that implemented it; check whether it does",
5708
+ review: "re-read the record against the new code"
5709
+ };
5710
+ async function reassessPacket(repoRoot, record, entries, options = {}) {
5711
+ const classified = await classifyDrift(repoRoot, record, entries, {
5712
+ ...options.reader ? { reader: options.reader } : {},
5713
+ ...options.search ? { search: options.search } : {},
5714
+ withHistory: options.withDiff !== false
5715
+ });
5716
+ const open = classified.filter(
5717
+ (found) => found.class === "changed" || found.class === "gone"
5718
+ );
5719
+ if (!open.length) return { packet: null, classified };
5720
+ const budget = diffBudget(open.length);
5721
+ const anchors = open.map(
5722
+ (found) => anchorPacket(found, options.withDiff === true, budget)
5723
+ );
5724
+ const type = record.frontmatter.type;
5725
+ const fallback = isKbRecordType(type) ? PRESUMED_INVALID.includes(type) ? "presumed-invalidated" : RATIONALE_SURVIVES.includes(type) ? "rationale-may-survive" : "review" : "review";
5726
+ return {
5727
+ classified,
5728
+ packet: {
5729
+ conceptId: record.conceptId,
5730
+ title: record.frontmatter.title ?? null,
5731
+ type,
5732
+ standing: options.standing ?? "unsettled",
5733
+ why: record.frontmatter.description ?? null,
5734
+ claim: claimOf(record),
5735
+ anchors,
5736
+ impact: (options.impact?.impacted ?? []).map((entry) => ({
5737
+ conceptId: entry.conceptId,
5738
+ title: entry.title,
5739
+ standing: entry.standing,
5740
+ depth: entry.depth
5741
+ })),
5742
+ impactTruncated: options.impact?.truncated ?? false,
5743
+ default: fallback,
5744
+ defaultNote: DEFAULT_NOTES[fallback]
5745
+ }
5746
+ };
5747
+ }
5748
+ function anchorPacket(found, withDiff, maxLines) {
5749
+ const { entry } = found;
5750
+ const base2 = {
5751
+ file: entry.file,
5752
+ ...entry.symbol ? { symbol: entry.symbol } : {},
5753
+ class: found.class,
5754
+ ...entry.reason ? { reason: entry.reason } : {},
5755
+ storedHash: entry.storedHash,
5756
+ ...entry.currentHash ? { currentHash: entry.currentHash } : {},
5757
+ diffSize: entry.diffSize,
5758
+ ...entry.movedTo ? { movedTo: entry.movedTo } : {}
5759
+ };
5760
+ if (!withDiff) return base2;
5761
+ if (found.oldText === void 0 || !found.oldOrigin) {
5762
+ return { ...base2, diff: { status: "unrecoverable" } };
5763
+ }
5764
+ const rendered = unifiedDiff(found.oldText, found.newText ?? "", {
5765
+ maxLines
5766
+ });
5767
+ return {
5768
+ ...base2,
5769
+ diff: {
5770
+ status: "ok",
5771
+ source: found.oldOrigin.kind,
5772
+ ref: found.oldOrigin.ref,
5773
+ unified: rendered.text,
5774
+ added: rendered.added,
5775
+ removed: rendered.removed,
5776
+ truncated: rendered.truncated
5777
+ }
5778
+ };
5779
+ }
5780
+ function claimOf(record) {
5781
+ const type = record.frontmatter.type;
5782
+ const section = isKbRecordType(type) ? RECORD_TYPES[type].sections[0] : void 0;
5783
+ if (!section) return null;
5784
+ const lines = record.body.replace(/\r\n/g, "\n").split("\n");
5785
+ const start = lines.findIndex(
5786
+ (line) => line.trim().toLowerCase() === `## ${section}`.toLowerCase()
5787
+ );
5788
+ if (start < 0) return null;
5789
+ const rest = lines.slice(start + 1);
5790
+ const end = rest.findIndex((line) => line.startsWith("## "));
5791
+ const text = (end < 0 ? rest : rest.slice(0, end)).join("\n").trim();
5792
+ return text ? { section, text } : null;
5793
+ }
5794
+
5795
+ // src/commands/reassess.ts
5183
5796
  var import_zod14 = require("zod");
5184
- var days = (what, fallback) => import_zod14.z.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
5797
+ var reassessCommand = define({
5798
+ name: "reassess",
5799
+ tool: "kb_reassess",
5800
+ usage: "reassess <concept-id> [--repo-root <path>] [--with-diff]",
5801
+ description: "One drifted record, as something to judge: its claim, each anchor's drift class, the old-vs-new span diff, and the records that depend on it. Formatting-only drift is dropped. Empty when there is nothing to reassess. Writes: relocates moved anchors, keeping their hash; never verifies, supersedes, or changes standing.",
5802
+ input: import_zod14.z.object({
5803
+ bundlePath,
5804
+ conceptId,
5805
+ repoRoot: REPO_ROOT,
5806
+ withDiff: import_zod14.z.boolean().optional().describe(
5807
+ "Recover each anchor's committed span and render the diff. Reads git history."
5808
+ )
5809
+ }),
5810
+ fromArgv: (argv, path) => {
5811
+ const repoRoot = argvFlag(argv, "--repo-root");
5812
+ return {
5813
+ bundlePath: path,
5814
+ conceptId: argv[1],
5815
+ ...repoRoot !== void 0 ? { repoRoot } : {},
5816
+ ...argv.includes("--with-diff") ? { withDiff: true } : {}
5817
+ };
5818
+ },
5819
+ run: async ({ store, actor }, { bundlePath: path, conceptId: id, repoRoot, withDiff }) => {
5820
+ const root = repoRoot ?? process.cwd();
5821
+ const bundle = await store.list(path);
5822
+ const record = bundle.find((entry) => entry.conceptId === id);
5823
+ if (!record) throw new KbRecordNotFoundError(id);
5824
+ const drift = await store.detectDrift([record], repoRoot);
5825
+ const entries = drift?.get(id) ?? [];
5826
+ if (!entries.some((entry) => entry.state !== "match")) {
5827
+ return { conceptId: id, packet: null, rebaselined: [], cosmetic: 0 };
5828
+ }
5829
+ const standing = adjudicate(bundle, bundle).find(
5830
+ (hit) => hit.record.conceptId === id
5831
+ )?.standing;
5832
+ const impact2 = await store.impact(path, id);
5833
+ const { packet, classified } = await reassessPacket(root, record, entries, {
5834
+ ...withDiff ? { withDiff: true } : {},
5835
+ impact: impact2,
5836
+ ...standing ? { standing } : {}
5837
+ });
5838
+ const moves = classified.filter((found) => found.class === "moved");
5839
+ let frozen = false;
5840
+ const rebaselined = [];
5841
+ if (moves.length) {
5842
+ const relocated = /* @__PURE__ */ new Map();
5843
+ for (const found of moves) {
5844
+ const to = found.entry.movedTo;
5845
+ if (!to) continue;
5846
+ relocated.set(found.anchor, {
5847
+ ...found.anchor,
5848
+ file: to.file,
5849
+ ...to.symbol ? { symbol: to.symbol } : {}
5850
+ });
5851
+ rebaselined.push({
5852
+ file: found.anchor.file,
5853
+ ...found.anchor.symbol ? { symbol: found.anchor.symbol } : {},
5854
+ toFile: to.file,
5855
+ ...to.symbol ? { toSymbol: to.symbol } : {}
5856
+ });
5857
+ }
5858
+ try {
5859
+ await assertBaseNotFrozen(process.cwd(), path);
5860
+ } catch (error) {
5861
+ if (!(error instanceof KbBaseFrozenError)) throw error;
5862
+ frozen = true;
5863
+ }
5864
+ if (!frozen) {
5865
+ await store.updateAnchors(
5866
+ path,
5867
+ id,
5868
+ (record.frontmatter.strauss_anchors ?? []).map(
5869
+ (anchor) => relocated.get(anchor) ?? anchor
5870
+ ),
5871
+ actor
5872
+ );
5873
+ }
5874
+ }
5875
+ return {
5876
+ conceptId: id,
5877
+ packet,
5878
+ rebaselined: frozen ? [] : rebaselined,
5879
+ cosmetic: classified.filter((found) => found.class === "cosmetic").length,
5880
+ ...frozen ? {
5881
+ frozen: true,
5882
+ note: "base is frozen: nothing was rebaselined"
5883
+ } : {}
5884
+ };
5885
+ },
5886
+ render: (result) => renderReassess(result)
5887
+ });
5888
+ function renderReassess(result) {
5889
+ const lines = [];
5890
+ for (const move of result.rebaselined) {
5891
+ lines.push(
5892
+ `rebaselined: ${at(move.file, move.symbol)} \u2192 ${at(move.toFile, move.toSymbol)} (same code, new address)`
5893
+ );
5894
+ }
5895
+ if (result.cosmetic) {
5896
+ lines.push(
5897
+ `${result.cosmetic} anchor${result.cosmetic === 1 ? "" : "s"} changed formatting only.`
5898
+ );
5899
+ }
5900
+ if (result.note) lines.push(result.note);
5901
+ const packet = result.packet;
5902
+ if (!packet) {
5903
+ lines.push(`${result.conceptId}: nothing to reassess.`);
5904
+ return lines.join("\n");
5905
+ }
5906
+ lines.push(
5907
+ "",
5908
+ `# ${packet.conceptId}${packet.title ? ` \u2014 ${packet.title}` : ""}`,
5909
+ `type: ${packet.type} standing: ${packet.standing}`,
5910
+ ...packet.why ? [`why: ${packet.why}`] : [],
5911
+ ...packet.claim ? ["", `## ${packet.claim.section}`, packet.claim.text] : [],
5912
+ "",
5913
+ `## Anchors (${packet.anchors.length})`
5914
+ );
5915
+ for (const anchor of packet.anchors) {
5916
+ lines.push(
5917
+ `- ${at(anchor.file, anchor.symbol)} \u2014 ${anchor.class}${anchor.reason ? ` (${anchor.reason})` : ""}`
5918
+ );
5919
+ if (!anchor.diff) continue;
5920
+ if (anchor.diff.status === "unrecoverable") {
5921
+ lines.push(
5922
+ " diff: unrecoverable \u2014 no committed span to compare against"
5923
+ );
5924
+ continue;
5925
+ }
5926
+ lines.push(
5927
+ ` diff vs ${anchor.diff.ref} (${anchor.diff.source}): +${anchor.diff.added} \u2212${anchor.diff.removed}`,
5928
+ ...anchor.diff.unified.split("\n").map((line) => ` ${line}`)
5929
+ );
5930
+ }
5931
+ if (packet.impact.length) {
5932
+ lines.push("", `## Impact (${packet.impact.length})`);
5933
+ for (const entry of packet.impact) {
5934
+ lines.push(
5935
+ `- ${entry.conceptId} [${entry.standing}]${entry.title ? ` \u2014 ${entry.title}` : ""}`
5936
+ );
5937
+ }
5938
+ if (packet.impactTruncated) lines.push("- \u2026 walk truncated");
5939
+ }
5940
+ lines.push("", `Default: ${packet.default} \u2014 ${packet.defaultNote}.`);
5941
+ return lines.join("\n");
5942
+ }
5943
+ function at(file, symbol) {
5944
+ return symbol ? `${file}:${symbol}` : file;
5945
+ }
5946
+
5947
+ // src/commands/doctor.ts
5948
+ var days = (what, fallback) => import_zod15.z.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
5185
5949
  var doctorCommand = define({
5186
5950
  name: "doctor",
5187
5951
  tool: "kb_doctor",
5188
- usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--offline] [--strict]",
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.",
5190
- input: import_zod14.z.object({
5952
+ usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--offline] [--strict] [--drifted [--with-diff]]",
5953
+ description: "Read-only health sweep: expired, expiring, unverified, aging, orphaned, broken-supersession, superseded-but-cited, drifted and unchecked anchors. Every group is reported even when empty; nothing is written or re-stamped. `drifted` narrows it to a reassessment packet per drifted record, `with_diff` adding each anchor's old-vs-new span.",
5954
+ input: import_zod15.z.object({
5191
5955
  bundlePath,
5192
5956
  repoRoot: REPO_ROOT,
5193
5957
  expiringDays: days(
@@ -5202,11 +5966,17 @@ var doctorCommand = define({
5202
5966
  "How long a record may stay `open` or `proposed` before `aging` reports it, in days.",
5203
5967
  DEFAULT_AGING_DAYS
5204
5968
  ),
5205
- offline: import_zod14.z.boolean().optional().describe(
5969
+ offline: import_zod15.z.boolean().optional().describe(
5206
5970
  "Read foreign anchors from the local repo cache only, never fetching."
5207
5971
  ),
5208
- strict: import_zod14.z.boolean().optional().describe(
5972
+ strict: import_zod15.z.boolean().optional().describe(
5209
5973
  "Turn an expired record into a non-zero exit for the CLI. No effect on the report itself."
5974
+ ),
5975
+ drifted: import_zod15.z.boolean().optional().describe(
5976
+ "Report only drift, as a reassessment packet per record: claim, per-anchor class, and what depends on it."
5977
+ ),
5978
+ withDiff: import_zod15.z.boolean().optional().describe(
5979
+ "With `drifted`: recover each anchor's committed span and render the old-vs-new diff. Reads git history."
5210
5980
  )
5211
5981
  }),
5212
5982
  // Presence, not truthiness: `--expiring-days ""` is a caller who meant
@@ -5225,7 +5995,9 @@ var doctorCommand = define({
5225
5995
  ...unverified2 !== void 0 ? { unverifiedDays: Number(unverified2) } : {},
5226
5996
  ...agingDays !== void 0 ? { agingDays: Number(agingDays) } : {},
5227
5997
  ...argv.includes("--offline") ? { offline: true } : {},
5228
- ...argv.includes("--strict") ? { strict: true } : {}
5998
+ ...argv.includes("--strict") ? { strict: true } : {},
5999
+ ...argv.includes("--drifted") ? { drifted: true } : {},
6000
+ ...argv.includes("--with-diff") ? { withDiff: true } : {}
5229
6001
  };
5230
6002
  },
5231
6003
  run: async ({ store, now }, {
@@ -5234,7 +6006,9 @@ var doctorCommand = define({
5234
6006
  unverifiedDays,
5235
6007
  agingDays,
5236
6008
  repoRoot,
5237
- offline
6009
+ offline,
6010
+ drifted: drifted2,
6011
+ withDiff
5238
6012
  }) => {
5239
6013
  const checkedAt = now();
5240
6014
  const records = await store.list(path);
@@ -5249,10 +6023,51 @@ var doctorCommand = define({
5249
6023
  now: new Date(checkedAt)
5250
6024
  });
5251
6025
  const hints = grammarHints();
6026
+ if (!drifted2) {
6027
+ return {
6028
+ bundlePath: path,
6029
+ checkedAt,
6030
+ ...report,
6031
+ ...hints.length ? { hints } : {}
6032
+ };
6033
+ }
6034
+ const standings = new Map(
6035
+ adjudicate(records, records, new Date(checkedAt)).map((hit) => [
6036
+ hit.record.conceptId,
6037
+ hit.standing
6038
+ ])
6039
+ );
6040
+ const packets = [];
6041
+ const rebaselinable = [];
6042
+ const search = movedSearch(repoRoot ?? process.cwd());
6043
+ for (const found of report.groups.find((g) => g.check === "drifted")?.findings ?? []) {
6044
+ const record = records.find(
6045
+ (entry) => entry.conceptId === found.conceptId
6046
+ );
6047
+ if (!record) continue;
6048
+ const standing = standings.get(record.conceptId);
6049
+ const built = await reassessPacket(
6050
+ repoRoot ?? process.cwd(),
6051
+ record,
6052
+ anchorDrift?.get(record.conceptId) ?? [],
6053
+ {
6054
+ ...withDiff ? { withDiff: true } : {},
6055
+ impact: await store.impact(path, record.conceptId),
6056
+ ...standing ? { standing } : {},
6057
+ search
6058
+ }
6059
+ );
6060
+ if (built.packet) packets.push(built.packet);
6061
+ if (built.classified.some((entry) => entry.class === "moved")) {
6062
+ rebaselinable.push(record.conceptId);
6063
+ }
6064
+ }
5252
6065
  return {
5253
6066
  bundlePath: path,
5254
6067
  checkedAt,
5255
6068
  ...report,
6069
+ packets,
6070
+ rebaselinable,
5256
6071
  ...hints.length ? { hints } : {}
5257
6072
  };
5258
6073
  },
@@ -5266,6 +6081,7 @@ var doctorCommand = define({
5266
6081
  failsWhen: (result, input) => input.strict === true && result.counts.expired > 0
5267
6082
  });
5268
6083
  function render2(result) {
6084
+ if (result.packets) return renderPackets(result);
5269
6085
  const { thresholds } = result;
5270
6086
  const lines = [
5271
6087
  `# KB Doctor \u2014 ${result.bundlePath}`,
@@ -5299,21 +6115,45 @@ function render2(result) {
5299
6115
  );
5300
6116
  return lines.join("\n");
5301
6117
  }
6118
+ function renderPackets(result) {
6119
+ const packets = result.packets ?? [];
6120
+ const lines = [
6121
+ `# KB Drift \u2014 ${result.bundlePath}`,
6122
+ `checked: ${result.checkedAt}`,
6123
+ `${packets.length} record${packets.length === 1 ? "" : "s"} need a reading; ${result.counts.drifted} drifted in all.`
6124
+ ];
6125
+ if (result.rebaselinable?.length) {
6126
+ lines.push(
6127
+ `moved, rebaseline with \`kb_reassess\`: ${result.rebaselinable.join(", ")}`
6128
+ );
6129
+ }
6130
+ for (const packet of packets) {
6131
+ lines.push(
6132
+ renderReassess({
6133
+ conceptId: packet.conceptId,
6134
+ packet,
6135
+ rebaselined: [],
6136
+ cosmetic: 0
6137
+ })
6138
+ );
6139
+ }
6140
+ return lines.join("\n");
6141
+ }
5302
6142
 
5303
6143
  // src/commands/impact.ts
5304
- var import_zod15 = require("zod");
6144
+ var import_zod16 = require("zod");
5305
6145
  var impactCommand = define({
5306
6146
  name: "impact",
5307
6147
  tool: "kb_impact",
5308
6148
  usage: "impact <concept-id> [--depth N] [--rels a,b]",
5309
6149
  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.",
5310
- input: import_zod15.z.object({
6150
+ input: import_zod16.z.object({
5311
6151
  bundlePath,
5312
6152
  conceptId,
5313
- depth: import_zod15.z.number().int().positive().optional().describe(
6153
+ depth: import_zod16.z.number().int().positive().optional().describe(
5314
6154
  "Hops out from the record. Unbounded when omitted; a walk this cuts reports truncated: true."
5315
6155
  ),
5316
- rels: import_zod15.z.array(import_zod15.z.enum(KB_CAUSAL_LINK_RELS)).optional().describe(
6156
+ rels: import_zod16.z.array(import_zod16.z.enum(KB_CAUSAL_LINK_RELS)).optional().describe(
5317
6157
  "Narrow which rels the walk follows. Defaults to every rel that carries a dependence \u2014 all but related_to."
5318
6158
  )
5319
6159
  }),
@@ -5334,35 +6174,49 @@ var impactCommand = define({
5334
6174
  });
5335
6175
 
5336
6176
  // src/commands/list.ts
5337
- var import_zod16 = require("zod");
6177
+ var import_zod17 = require("zod");
5338
6178
  var listCommand = define({
5339
6179
  name: "list",
5340
6180
  tool: "kb_list",
5341
- usage: "list [type]",
5342
- description: "Every record, optionally one type. For enumerating; use kb_query for a question.",
5343
- input: import_zod16.z.object({ bundlePath, type: import_zod16.z.enum(KB_RECORD_TYPES).optional() }),
5344
- fromArgv: (argv, path) => ({ bundlePath: path, type: argv[1] }),
5345
- run: async ({ store }, { bundlePath: path, type }) => (await store.list(path, type)).map((record) => ({
5346
- conceptId: record.conceptId,
5347
- title: record.frontmatter.title ?? null,
5348
- description: record.frontmatter.description ?? null,
5349
- status: record.frontmatter.strauss_status,
5350
- anchors: record.frontmatter.strauss_anchors ?? []
5351
- }))
6181
+ usage: "list [type] [--tag T]...",
6182
+ description: "Every record, optionally one type or tag. For enumerating; use kb_query for a question.",
6183
+ input: import_zod17.z.object({
6184
+ bundlePath,
6185
+ type: import_zod17.z.enum(KB_RECORD_TYPES).optional(),
6186
+ tags: TAGS
6187
+ }),
6188
+ fromArgv: (argv, path) => {
6189
+ const tags = argvFlags(argv, "--tag");
6190
+ const type = argvPositional(argv, "--tag");
6191
+ return {
6192
+ bundlePath: path,
6193
+ ...type ? { type } : {},
6194
+ ...tags.length ? { tags } : {}
6195
+ };
6196
+ },
6197
+ run: async ({ store }, { bundlePath: path, type, tags }) => (await store.list(path, type, { ...tags ? { tags } : {} })).map(
6198
+ (record) => ({
6199
+ conceptId: record.conceptId,
6200
+ title: record.frontmatter.title ?? null,
6201
+ description: record.frontmatter.description ?? null,
6202
+ status: record.frontmatter.strauss_status,
6203
+ anchors: record.frontmatter.strauss_anchors ?? []
6204
+ })
6205
+ )
5352
6206
  });
5353
6207
 
5354
6208
  // src/commands/load.ts
5355
- var import_zod17 = require("zod");
6209
+ var import_zod18 = require("zod");
5356
6210
  var loadCommand = define({
5357
6211
  name: "load",
5358
6212
  tool: "kb_load",
5359
6213
  usage: "load [type] [--budget N | --all] [--repo-root PATH]",
5360
6214
  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.",
5361
- input: import_zod17.z.object({
6215
+ input: import_zod18.z.object({
5362
6216
  bundlePath,
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(
6217
+ type: import_zod18.z.enum(KB_RECORD_TYPES).optional(),
6218
+ budgetTokens: import_zod18.z.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
6219
+ all: import_zod18.z.boolean().optional().describe(
5366
6220
  "Loads the entire base regardless of size, bypassing the token budget; mutually exclusive with budgetTokens."
5367
6221
  ),
5368
6222
  repoRoot: REPO_ROOT
@@ -5404,25 +6258,25 @@ var loadCommand = define({
5404
6258
  });
5405
6259
 
5406
6260
  // src/commands/log.ts
5407
- var import_zod18 = require("zod");
6261
+ var import_zod19 = require("zod");
5408
6262
  var logCommand = define({
5409
6263
  name: "log",
5410
6264
  tool: "kb_log",
5411
6265
  usage: "log",
5412
6266
  description: "Who touched what, and when. Append-only; malformed lines are reported, never repaired.",
5413
- input: import_zod18.z.object({ bundlePath }),
6267
+ input: import_zod19.z.object({ bundlePath }),
5414
6268
  fromArgv: (_argv, path) => ({ bundlePath: path }),
5415
6269
  run: ({ store }, { bundlePath: path }) => store.readLog(path)
5416
6270
  });
5417
6271
 
5418
6272
  // src/commands/no-decision.ts
5419
- var import_zod19 = require("zod");
6273
+ var import_zod20 = require("zod");
5420
6274
  var noDecisionCommand = define({
5421
6275
  name: "no-decision",
5422
6276
  tool: "kb_no_decision",
5423
6277
  usage: "no-decision <reason...>",
5424
6278
  description: "Record in one sentence that a piece of work had nothing to decide. Idempotent.",
5425
- input: import_zod19.z.object({ bundlePath, reason: import_zod19.z.string().min(1) }),
6279
+ input: import_zod20.z.object({ bundlePath, reason: import_zod20.z.string().min(1) }),
5426
6280
  fromArgv: (argv, path) => ({
5427
6281
  bundlePath: path,
5428
6282
  reason: argv.slice(1).join(" ").trim()
@@ -5439,20 +6293,20 @@ var noDecisionCommand = define({
5439
6293
  });
5440
6294
 
5441
6295
  // src/commands/pack.ts
5442
- var import_zod20 = require("zod");
6296
+ var import_zod21 = require("zod");
5443
6297
  var packCommand = define({
5444
6298
  name: "pack",
5445
6299
  tool: "kb_pack",
5446
6300
  usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
5447
6301
  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.",
5448
- input: import_zod20.z.object({
6302
+ input: import_zod21.z.object({
5449
6303
  bundlePath,
5450
6304
  conceptId,
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(
6305
+ hops: import_zod21.z.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
6306
+ maxNodes: import_zod21.z.number().int().positive().optional().describe(
5453
6307
  "How many records the pack may hold, root included. Defaults to 20."
5454
6308
  ),
5455
- budgetTokens: import_zod20.z.number().int().positive().optional().describe(
6309
+ budgetTokens: import_zod21.z.number().int().positive().optional().describe(
5456
6310
  "Approximate token ceiling over what is actually emitted. Defaults to 25000."
5457
6311
  )
5458
6312
  }),
@@ -5477,12 +6331,12 @@ var packCommand = define({
5477
6331
  return render3(result, path, now());
5478
6332
  }
5479
6333
  });
5480
- function render3(result, bundle, at) {
6334
+ function render3(result, bundle, at2) {
5481
6335
  const lines = [
5482
6336
  `# KB Pack \u2014 ${result.root}`,
5483
6337
  `bundle: ${bundle}`,
5484
6338
  `budget: ~${result.tokensLoaded} of ${result.budgetTokens} tokens, ${result.recordCount} records`,
5485
- `packed: ${at}`,
6339
+ `packed: ${at2}`,
5486
6340
  "",
5487
6341
  `## Records (${result.records.length})`
5488
6342
  ];
@@ -5539,22 +6393,22 @@ function warningLabel(warning) {
5539
6393
  }
5540
6394
 
5541
6395
  // src/commands/pin.ts
5542
- var import_zod21 = require("zod");
6396
+ var import_zod22 = require("zod");
5543
6397
  var pinCommand = define({
5544
6398
  name: "pin",
5545
6399
  tool: "kb_pin",
5546
6400
  usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
5547
6401
  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.",
5548
- input: import_zod21.z.object({
6402
+ input: import_zod22.z.object({
5549
6403
  bundlePath,
5550
- mode: import_zod21.z.enum(["full", "index"]).optional().describe(
6404
+ mode: import_zod22.z.enum(["full", "index"]).optional().describe(
5551
6405
  "full: always emit this base's records whole (still under the block budget); index: never upgrade. Absent: the profile's full-under threshold decides."
5552
6406
  ),
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(
6407
+ profiles: import_zod22.z.array(import_zod22.z.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
6408
+ layer: import_zod22.z.enum(["project", "local", "user"]).optional().describe(
5555
6409
  "Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
5556
6410
  ),
5557
- frozen: import_zod21.z.boolean().optional().describe(
6411
+ frozen: import_zod22.z.boolean().optional().describe(
5558
6412
  "true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
5559
6413
  )
5560
6414
  }),
@@ -5583,47 +6437,49 @@ var pinCommand = define({
5583
6437
  });
5584
6438
 
5585
6439
  // src/commands/pins.ts
5586
- var import_zod22 = require("zod");
6440
+ var import_zod23 = require("zod");
5587
6441
  var pinsCommand = define({
5588
6442
  name: "pins",
5589
6443
  tool: "kb_pins",
5590
6444
  usage: "pins",
5591
6445
  description: "Every pinned base across the manifest layers, with its layer and whether it resolves to records. Takes no bundlePath.",
5592
- input: import_zod22.z.object({}),
6446
+ input: import_zod23.z.object({}),
5593
6447
  fromArgv: () => ({}),
5594
6448
  run: ({ store }) => listPins(store, process.cwd())
5595
6449
  });
5596
6450
 
5597
6451
  // src/commands/query.ts
5598
- var import_zod23 = require("zod");
6452
+ var import_zod24 = require("zod");
5599
6453
  var queryCommand = define({
5600
6454
  name: "query",
5601
6455
  tool: "kb_query",
5602
- usage: "query <text...> [--repo-root PATH]",
6456
+ usage: "query <text...> [--tag T]... [--repo-root PATH]",
5603
6457
  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.",
5604
- input: import_zod23.z.object({
6458
+ input: import_zod24.z.object({
5605
6459
  bundlePath,
5606
- text: import_zod23.z.string().optional(),
5607
- type: import_zod23.z.enum(KB_RECORD_TYPES).optional(),
5608
- includeNonCurrent: import_zod23.z.boolean().optional(),
6460
+ text: import_zod24.z.string().optional(),
6461
+ type: import_zod24.z.enum(KB_RECORD_TYPES).optional(),
6462
+ includeNonCurrent: import_zod24.z.boolean().optional(),
6463
+ tags: TAGS,
5609
6464
  repoRoot: REPO_ROOT
5610
6465
  }),
5611
- // `--repo-root` is a flag, so its value must not fall into the search text.
6466
+ // Both are flags, so neither's value may fall into the search text.
5612
6467
  fromArgv: (argv, path) => {
5613
6468
  const repoRoot = argvFlag(argv, "--repo-root");
5614
- const words = argv.slice(1);
5615
- const flag = words.indexOf("--repo-root");
5616
- if (flag !== -1) words.splice(flag, 2);
6469
+ const tags = argvFlags(argv, "--tag");
6470
+ const words = argvWithout(argv.slice(1), "--repo-root", "--tag");
5617
6471
  return {
5618
6472
  bundlePath: path,
5619
6473
  text: words.join(" ").trim(),
5620
6474
  includeNonCurrent: true,
6475
+ ...tags.length ? { tags } : {},
5621
6476
  ...repoRoot !== void 0 ? { repoRoot } : {}
5622
6477
  };
5623
6478
  },
5624
- run: async ({ store }, { bundlePath: path, text, type, includeNonCurrent, repoRoot }) => (await store.query(path, text ?? "", {
6479
+ run: async ({ store }, { bundlePath: path, text, type, includeNonCurrent, tags, repoRoot }) => (await store.query(path, text ?? "", {
5625
6480
  ...type ? { type } : {},
5626
6481
  includeNonCurrent: includeNonCurrent === true,
6482
+ ...tags ? { tags } : {},
5627
6483
  ...repoRoot !== void 0 ? { repoRoot } : {}
5628
6484
  })).map((hit) => ({
5629
6485
  conceptId: hit.record.conceptId,
@@ -5637,43 +6493,43 @@ var queryCommand = define({
5637
6493
  });
5638
6494
 
5639
6495
  // src/commands/read-index.ts
5640
- var import_zod24 = require("zod");
6496
+ var import_zod25 = require("zod");
5641
6497
  var readIndexCommand = define({
5642
6498
  name: "index",
5643
6499
  tool: "kb_index",
5644
6500
  usage: "index",
5645
6501
  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.",
5646
- input: import_zod24.z.object({ bundlePath }),
6502
+ input: import_zod25.z.object({ bundlePath }),
5647
6503
  fromArgv: (_argv, path) => ({ bundlePath: path }),
5648
6504
  run: ({ store }, { bundlePath: path }) => store.readIndex(path)
5649
6505
  });
5650
6506
 
5651
6507
  // src/commands/schema.ts
5652
- var import_zod25 = require("zod");
6508
+ var import_zod26 = require("zod");
5653
6509
  var schemaCommand = define({
5654
6510
  name: "schema",
5655
6511
  tool: "kb_schema",
5656
6512
  usage: "schema",
5657
6513
  description: "JSON Schema for frontmatter, write input, and log entries, generated from the enforcing code.",
5658
- input: import_zod25.z.object({}),
6514
+ input: import_zod26.z.object({}),
5659
6515
  fromArgv: () => ({}),
5660
6516
  run: () => Promise.resolve(kbJsonSchemas())
5661
6517
  });
5662
6518
 
5663
6519
  // src/commands/stamp.ts
5664
- var import_promises9 = require("fs/promises");
5665
- var import_zod26 = require("zod");
6520
+ var import_promises10 = require("fs/promises");
6521
+ var import_zod27 = require("zod");
5666
6522
  var DIGEST = /^[0-9a-f]{64}$/;
5667
6523
  var stampCommand = define({
5668
6524
  name: "stamp",
5669
6525
  tool: "kb_stamp",
5670
6526
  usage: "stamp [--bundle PATH] [--since DIGEST|FILE]",
5671
- description: "Content stamp of a base \u2014 `load`'s digest, record counts, per-record digests \u2014 without any bodies. Takes no bundlePath to stamp every pinned base. With `since`, reports only the bases that moved, naming the changed ids when the baseline is a prior stamp; silent when nothing changed. Reads, never writes.",
5672
- input: import_zod26.z.object({
5673
- bundlePath: import_zod26.z.string().min(1).optional().describe(
6527
+ description: "Content stamp of a base \u2014 `load`'s digest, record counts, per-record digests, how many records have drifted anchors \u2014 without any bodies. Takes no bundlePath to stamp every pinned base. With `since`, reports only the bases that moved, naming the changed ids. Reads, never writes.",
6528
+ input: import_zod27.z.object({
6529
+ bundlePath: import_zod27.z.string().min(1).optional().describe(
5674
6530
  "Absolute path to one knowledge base. Omit to stamp every pinned base."
5675
6531
  ),
5676
- since: import_zod26.z.string().min(1).optional().describe(
6532
+ since: import_zod27.z.string().min(1).optional().describe(
5677
6533
  "Prior digest, or path to a prior `stamp --json`; only moved bases return, with changed ids when the baseline is a file."
5678
6534
  )
5679
6535
  }),
@@ -5712,7 +6568,7 @@ var stampCommand = define({
5712
6568
  return reports;
5713
6569
  },
5714
6570
  render: (result) => result.map((report) => {
5715
- const counts = `${report.recordCount} record(s), ${report.superseded} superseded`;
6571
+ const counts = `${report.recordCount} record(s), ${report.superseded} superseded${report.drifted ? `, ${report.drifted} drifted` : ""}`;
5716
6572
  const head = `${report.path} ${report.digest} ${counts}${report.newestAt ? ` newest ${report.newestAt}` : ""}`;
5717
6573
  return report.changed?.length ? `${head}
5718
6574
  changed: ${report.changed.join(", ")}` : head;
@@ -5735,7 +6591,7 @@ async function readBaseline(since) {
5735
6591
  if (DIGEST.test(since)) return { digest: since, byPath: /* @__PURE__ */ new Map() };
5736
6592
  let parsed;
5737
6593
  try {
5738
- parsed = JSON.parse(await (0, import_promises9.readFile)(since, "utf8"));
6594
+ parsed = JSON.parse(await (0, import_promises10.readFile)(since, "utf8"));
5739
6595
  } catch {
5740
6596
  throw new KbStampBaselineError(since);
5741
6597
  }
@@ -5759,16 +6615,16 @@ async function readBaseline(since) {
5759
6615
  }
5760
6616
 
5761
6617
  // src/commands/status.ts
5762
- var import_zod27 = require("zod");
6618
+ var import_zod28 = require("zod");
5763
6619
  var statusCommand = define({
5764
6620
  name: "status",
5765
6621
  tool: "kb_status",
5766
6622
  usage: "status <concept-id> <status>",
5767
6623
  description: "Move a record's status. Compare-and-swap: a concurrent change fails instead of being overwritten.",
5768
- input: import_zod27.z.object({
6624
+ input: import_zod28.z.object({
5769
6625
  bundlePath,
5770
6626
  conceptId,
5771
- status: import_zod27.z.enum(KB_RECORD_STATUSES)
6627
+ status: import_zod28.z.enum(KB_RECORD_STATUSES)
5772
6628
  }),
5773
6629
  fromArgv: (argv, path) => ({
5774
6630
  bundlePath: path,
@@ -5783,13 +6639,13 @@ var statusCommand = define({
5783
6639
  });
5784
6640
 
5785
6641
  // src/commands/supersede.ts
5786
- var import_zod28 = require("zod");
6642
+ var import_zod29 = require("zod");
5787
6643
  var supersedeCommand = define({
5788
6644
  name: "supersede",
5789
6645
  tool: "kb_supersede",
5790
6646
  usage: "supersede <concept-id> <replacement-id>",
5791
6647
  description: "Mark a record superseded by another, linked in both directions. Use instead of editing a record whose meaning changed.",
5792
- input: import_zod28.z.object({ bundlePath, conceptId, replacementId: conceptId }),
6648
+ input: import_zod29.z.object({ bundlePath, conceptId, replacementId: conceptId }),
5793
6649
  fromArgv: (argv, path) => ({
5794
6650
  bundlePath: path,
5795
6651
  conceptId: argv[1],
@@ -5803,16 +6659,16 @@ var supersedeCommand = define({
5803
6659
  });
5804
6660
 
5805
6661
  // src/commands/sync-instructions.ts
5806
- var import_zod29 = require("zod");
6662
+ var import_zod30 = require("zod");
5807
6663
  var syncInstructionsCommand = define({
5808
6664
  name: "sync-instructions",
5809
6665
  usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
5810
6666
  description: "CLI-only: plant the kb_context block between sentinel comments in AGENTS.md or CLAUDE.md, idempotently.",
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()
6667
+ input: import_zod30.z.object({
6668
+ file: import_zod30.z.string().min(1).describe("The instruction file to edit in place."),
6669
+ budgetTokens: import_zod30.z.number().int().positive().optional(),
6670
+ fullUnderTokens: import_zod30.z.number().int().positive().optional(),
6671
+ profile: import_zod30.z.string().optional()
5816
6672
  }),
5817
6673
  fromArgv: (argv) => {
5818
6674
  const budget = argvFlag(argv, "--budget");
@@ -5838,17 +6694,17 @@ var syncInstructionsCommand = define({
5838
6694
  });
5839
6695
 
5840
6696
  // src/commands/trace.ts
5841
- var import_zod30 = require("zod");
6697
+ var import_zod31 = require("zod");
5842
6698
  var traceCommand = define({
5843
6699
  name: "trace",
5844
6700
  tool: "kb_trace",
5845
6701
  usage: "trace <concept-id> [edges...]",
5846
6702
  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".',
5847
- input: import_zod30.z.object({
6703
+ input: import_zod31.z.object({
5848
6704
  bundlePath,
5849
6705
  conceptId,
5850
- edges: import_zod30.z.array(import_zod30.z.enum(TRACE_EDGES)).optional(),
5851
- depth: import_zod30.z.number().int().positive().optional()
6706
+ edges: import_zod31.z.array(import_zod31.z.enum(TRACE_EDGES)).optional(),
6707
+ depth: import_zod31.z.number().int().positive().optional()
5852
6708
  }),
5853
6709
  fromArgv: (argv, path) => ({
5854
6710
  bundlePath: path,
@@ -5870,37 +6726,37 @@ var traceCommand = define({
5870
6726
  });
5871
6727
 
5872
6728
  // src/commands/types.ts
5873
- var import_zod31 = require("zod");
6729
+ var import_zod32 = require("zod");
5874
6730
  var typesCommand = define({
5875
6731
  name: "types",
5876
6732
  tool: "kb_types",
5877
6733
  usage: "types",
5878
6734
  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.",
5879
- input: import_zod31.z.object({}),
6735
+ input: import_zod32.z.object({}),
5880
6736
  fromArgv: () => ({}),
5881
6737
  run: () => Promise.resolve(RECORD_TYPES)
5882
6738
  });
5883
6739
 
5884
6740
  // src/commands/unpin.ts
5885
- var import_zod32 = require("zod");
6741
+ var import_zod33 = require("zod");
5886
6742
  var unpinCommand = define({
5887
6743
  name: "unpin",
5888
6744
  tool: "kb_unpin",
5889
6745
  usage: "unpin [bundle-path]",
5890
6746
  description: "Remove a base from every manifest layer that holds it. Reports the layers touched.",
5891
- input: import_zod32.z.object({ bundlePath }),
6747
+ input: import_zod33.z.object({ bundlePath }),
5892
6748
  fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
5893
6749
  run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
5894
6750
  });
5895
6751
 
5896
6752
  // src/commands/validate.ts
5897
- var import_zod33 = require("zod");
6753
+ var import_zod34 = require("zod");
5898
6754
  var validateCommand = define({
5899
6755
  name: "validate",
5900
6756
  tool: "kb_validate",
5901
6757
  usage: "validate",
5902
6758
  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.",
5903
- input: import_zod33.z.object({ bundlePath }),
6759
+ input: import_zod34.z.object({ bundlePath }),
5904
6760
  fromArgv: (_argv, path) => ({ bundlePath: path }),
5905
6761
  run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
5906
6762
  // Warnings never fail the exit code; every other severity does.
@@ -5910,16 +6766,16 @@ var validateCommand = define({
5910
6766
  });
5911
6767
 
5912
6768
  // src/commands/verify.ts
5913
- var import_zod34 = require("zod");
6769
+ var import_zod35 = require("zod");
5914
6770
  var verifyCommand = define({
5915
6771
  name: "verify",
5916
6772
  tool: "kb_verify",
5917
6773
  usage: "verify <concept-id> --note <text>",
5918
6774
  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.",
5919
- input: import_zod34.z.object({
6775
+ input: import_zod35.z.object({
5920
6776
  bundlePath,
5921
6777
  conceptId,
5922
- note: import_zod34.z.string().refine((s) => s.trim().length > 0, {
6778
+ note: import_zod35.z.string().refine((s) => s.trim().length > 0, {
5923
6779
  message: "note must say what the check found"
5924
6780
  })
5925
6781
  }),
@@ -5939,15 +6795,15 @@ var verifyCommand = define({
5939
6795
  });
5940
6796
 
5941
6797
  // src/commands/write.ts
5942
- var import_zod35 = require("zod");
6798
+ var import_zod36 = require("zod");
5943
6799
  var writeCommand = define({
5944
6800
  name: "write",
5945
6801
  tool: "kb_write",
5946
6802
  usage: "write <type> < record.json",
5947
6803
  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.",
5948
- input: import_zod35.z.object({
6804
+ input: import_zod36.z.object({
5949
6805
  bundlePath,
5950
- type: import_zod35.z.enum(KB_RECORD_TYPES),
6806
+ type: import_zod36.z.enum(KB_RECORD_TYPES),
5951
6807
  input: composeInputSchema
5952
6808
  }),
5953
6809
  fromArgv: async (argv, path, stdin) => ({
@@ -5971,13 +6827,13 @@ var writeCommand = define({
5971
6827
  });
5972
6828
 
5973
6829
  // src/commands/write-decision.ts
5974
- var import_zod36 = require("zod");
6830
+ var import_zod37 = require("zod");
5975
6831
  var writeDecisionCommand = define({
5976
6832
  name: "write-decision",
5977
6833
  tool: "kb_write_decision",
5978
6834
  usage: "write-decision < decision.json",
5979
6835
  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.",
5980
- input: import_zod36.z.object({ bundlePath, input: decisionInputSchema }),
6836
+ input: import_zod37.z.object({ bundlePath, input: decisionInputSchema }),
5981
6837
  fromArgv: async (_argv, path, stdin) => ({
5982
6838
  bundlePath: path,
5983
6839
  input: JSON.parse(await stdin())
@@ -6007,6 +6863,7 @@ var KB_COMMANDS = [
6007
6863
  answerCommand,
6008
6864
  verifyCommand,
6009
6865
  anchorResolveCommand,
6866
+ reassessCommand,
6010
6867
  loadCommand,
6011
6868
  catalogCommand,
6012
6869
  packCommand,
@@ -6037,7 +6894,7 @@ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
6037
6894
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
6038
6895
 
6039
6896
  // src/version.ts
6040
- var VERSION = true ? "0.1.17" : "0.0.0-dev";
6897
+ var VERSION = true ? "0.1.19" : "0.0.0-dev";
6041
6898
 
6042
6899
  // src/mcp.ts
6043
6900
  function createKbMcpServer() {
@@ -6131,21 +6988,21 @@ async function runKbCli(argv) {
6131
6988
  `);
6132
6989
  }
6133
6990
  function takeLiteral(argv) {
6134
- const at = argv.indexOf("--");
6135
- if (at === -1) return { flags: argv, literal: [] };
6136
- return { flags: argv.slice(0, at), literal: argv.slice(at + 1) };
6991
+ const at2 = argv.indexOf("--");
6992
+ if (at2 === -1) return { flags: argv, literal: [] };
6993
+ return { flags: argv.slice(0, at2), literal: argv.slice(at2 + 1) };
6137
6994
  }
6138
6995
  function takeBundle(argv) {
6139
- const at = argv.indexOf("--bundle");
6140
- if (at === -1) {
6996
+ const at2 = argv.indexOf("--bundle");
6997
+ if (at2 === -1) {
6141
6998
  return { bundle: (0, import_node_path12.join)(process.cwd(), KB_DIR), explicit: false, rest: argv };
6142
6999
  }
6143
- const bundle = argv[at + 1];
7000
+ const bundle = argv[at2 + 1];
6144
7001
  if (!bundle) die("--bundle requires a path");
6145
7002
  return {
6146
7003
  bundle,
6147
7004
  explicit: true,
6148
- rest: [...argv.slice(0, at), ...argv.slice(at + 2)]
7005
+ rest: [...argv.slice(0, at2), ...argv.slice(at2 + 2)]
6149
7006
  };
6150
7007
  }
6151
7008
  function readStdin() {
@@ -6243,6 +7100,7 @@ function usage() {
6243
7100
  backlinks,
6244
7101
  buildContext,
6245
7102
  catalog,
7103
+ classifyDrift,
6246
7104
  composeDecisionRecord,
6247
7105
  composeInputSchema,
6248
7106
  composeLinkSchema,
@@ -6280,6 +7138,7 @@ function usage() {
6280
7138
  listPins,
6281
7139
  loadQmd,
6282
7140
  matchToDiff,
7141
+ matchesTags,
6283
7142
  mergedContextBudgets,
6284
7143
  neighbours,
6285
7144
  pack,
@@ -6290,6 +7149,7 @@ function usage() {
6290
7149
  readMergedPins,
6291
7150
  readPinsLayer,
6292
7151
  readRemoteAnchors,
7152
+ reassessPacket,
6293
7153
  regexResolver,
6294
7154
  renderCatalogLine,
6295
7155
  renderIndex,
@@ -6311,6 +7171,7 @@ function usage() {
6311
7171
  toHookJson,
6312
7172
  trace,
6313
7173
  treeSitterLanguages,
7174
+ unifiedDiff,
6314
7175
  unpinBase,
6315
7176
  validateBundle
6316
7177
  });