@saasontools/strauss-kb 0.1.12 → 0.1.14

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
@@ -40,10 +40,12 @@ __export(index_exports, {
40
40
  DEFAULT_LOAD_BUDGET: () => DEFAULT_LOAD_BUDGET,
41
41
  DEFAULT_PACK_HOPS: () => DEFAULT_PACK_HOPS,
42
42
  DEFAULT_PACK_MAX_NODES: () => DEFAULT_PACK_MAX_NODES,
43
+ DEFAULT_TYPED_LINK_RELS: () => DEFAULT_TYPED_LINK_RELS,
43
44
  DEFAULT_UNVERIFIED_DAYS: () => DEFAULT_UNVERIFIED_DAYS,
44
45
  ErrorTypes: () => ErrorTypes,
45
46
  Fault: () => Fault,
46
47
  INDEX_FILE: () => INDEX_FILE,
48
+ KB_CAUSAL_LINK_RELS: () => KB_CAUSAL_LINK_RELS,
47
49
  KB_COMMANDS: () => KB_COMMANDS,
48
50
  KB_COMMANDS_BY_NAME: () => KB_COMMANDS_BY_NAME,
49
51
  KB_CONCEPT_ID_PATTERN: () => KB_CONCEPT_ID_PATTERN,
@@ -51,6 +53,7 @@ __export(index_exports, {
51
53
  KB_DIR: () => KB_DIR,
52
54
  KB_DOCTOR_CHECKS: () => KB_DOCTOR_CHECKS,
53
55
  KB_EDGE_KINDS: () => KB_EDGE_KINDS,
56
+ KB_LINK_RELS: () => KB_LINK_RELS,
54
57
  KB_MATERIALITIES: () => KB_MATERIALITIES,
55
58
  KB_RECORD_STATUSES: () => KB_RECORD_STATUSES,
56
59
  KB_RECORD_TYPES: () => KB_RECORD_TYPES,
@@ -64,7 +67,9 @@ __export(index_exports, {
64
67
  KbRecordNotFoundError: () => KbRecordNotFoundError,
65
68
  KbSelfVerificationError: () => KbSelfVerificationError,
66
69
  KbStore: () => KbStore,
70
+ KbUnknownLinkRelError: () => KbUnknownLinkRelError,
67
71
  KbWriteConflictError: () => KbWriteConflictError,
72
+ LINK_RELS: () => LINK_RELS,
68
73
  LOG_FILE: () => LOG_FILE,
69
74
  NO_DECISION_SLUG: () => NO_DECISION_SLUG,
70
75
  PINS_FILE: () => PINS_FILE,
@@ -76,10 +81,12 @@ __export(index_exports, {
76
81
  adjudicate: () => adjudicate,
77
82
  anchorFilePath: () => anchorFilePath,
78
83
  assertBaseNotFrozen: () => assertBaseNotFrozen,
84
+ backlinks: () => backlinks,
79
85
  buildContext: () => buildContext,
80
86
  catalog: () => catalog,
81
87
  composeDecisionRecord: () => composeDecisionRecord,
82
88
  composeInputSchema: () => composeInputSchema,
89
+ composeLinkSchema: () => composeLinkSchema,
83
90
  composeNoDecisionRecord: () => composeNoDecisionRecord,
84
91
  composeRecord: () => composeRecord,
85
92
  contextProfileBudgets: () => contextProfileBudgets,
@@ -89,13 +96,17 @@ __export(index_exports, {
89
96
  doctor: () => doctor,
90
97
  edgeNeighbours: () => edgeNeighbours,
91
98
  hashAnchorText: () => hashAnchorText,
99
+ impact: () => impact,
100
+ inboundIndex: () => inboundIndex,
92
101
  indexIsStale: () => indexIsStale,
102
+ isKbLinkRel: () => isKbLinkRel,
93
103
  isKbRecordType: () => isKbRecordType,
94
104
  isNoDecisionRecord: () => isNoDecisionRecord,
95
105
  kbActorStampSchema: () => kbActorStampSchema,
96
106
  kbAnchorSchema: () => kbAnchorSchema,
97
107
  kbConceptIdSchema: () => kbConceptIdSchema,
98
108
  kbJsonSchemas: () => kbJsonSchemas,
109
+ kbLinkSchema: () => kbLinkSchema,
99
110
  kbLogEntrySchema: () => kbLogEntrySchema,
100
111
  kbRecordFrontmatterSchema: () => kbRecordFrontmatterSchema,
101
112
  kbSourceSchema: () => kbSourceSchema,
@@ -239,6 +250,10 @@ var kbAnchorSchema = import_zod.z.object({
239
250
  /** Line count of the text the hash was taken over. */
240
251
  lines: import_zod.z.number().int().positive().optional()
241
252
  }).strict();
253
+ var kbLinkSchema = import_zod.z.object({
254
+ target: import_zod.z.string().min(1),
255
+ rel: import_zod.z.string().min(1)
256
+ }).passthrough();
242
257
  var KB_RECORD_TYPES = [
243
258
  "fact",
244
259
  "requirement",
@@ -290,6 +305,10 @@ var kbRecordFrontmatterSchema = import_zod.z.object({
290
305
  // strauss extensions — see the module comment.
291
306
  strauss_anchors: import_zod.z.array(kbAnchorSchema).optional(),
292
307
  strauss_verify: import_zod.z.array(import_zod.z.string().min(1)).optional(),
308
+ // Typed causal edges, source → target, living on the source. `A depends_on
309
+ // B` means A needs B, so `kb_impact` walks these inbound: what breaks if B
310
+ // changes is whatever declared a dependence on it.
311
+ strauss_links: import_zod.z.array(kbLinkSchema).optional(),
293
312
  // Total after parsing, tolerant before it. Our producers must supply a
294
313
  // status — an absent one would leave every reader inventing its own default
295
314
  // — but OKF calls a concept carrying only `type` fully conformant, so
@@ -322,6 +341,7 @@ var ErrorTypes = /* @__PURE__ */ ((ErrorTypes2) => {
322
341
  ErrorTypes2["KbPackBudgetExceeded"] = "KbPackBudgetExceeded";
323
342
  ErrorTypes2["KbRecordNotFound"] = "KbRecordNotFound";
324
343
  ErrorTypes2["KbSelfVerification"] = "KbSelfVerification";
344
+ ErrorTypes2["KbUnknownLinkRel"] = "KbUnknownLinkRel";
325
345
  ErrorTypes2["KbWriteConflict"] = "KbWriteConflict";
326
346
  return ErrorTypes2;
327
347
  })(ErrorTypes || {});
@@ -430,6 +450,23 @@ var KbPackBudgetExceededError = class extends BaseError {
430
450
  budgetTokens;
431
451
  excluded;
432
452
  };
453
+ var KbUnknownLinkRelError = class extends BaseError {
454
+ constructor(rel, expected) {
455
+ super({
456
+ message: `kb: ${rel} is not a rel a walk can follow \u2014 expected one of ${expected.join(", ")}`,
457
+ errorType: "KbUnknownLinkRel" /* KbUnknownLinkRel */,
458
+ code: 400,
459
+ fault: "User" /* User */,
460
+ retriable: false,
461
+ reportToUser: true,
462
+ details: { rel, expected: expected.join(", ") }
463
+ });
464
+ this.rel = rel;
465
+ this.expected = expected;
466
+ }
467
+ rel;
468
+ expected;
469
+ };
433
470
  var KbMissingFlagValueError = class extends BaseError {
434
471
  constructor(flag) {
435
472
  super({
@@ -1150,9 +1187,135 @@ async function loadQmd(logger) {
1150
1187
  }
1151
1188
  }
1152
1189
 
1190
+ // src/record-types.ts
1191
+ var RECORD_TYPES = {
1192
+ fact: {
1193
+ purpose: "Observed or sourced fact",
1194
+ sections: ["Claim", "Evidence", "Implication"],
1195
+ initialStatus: "accepted"
1196
+ },
1197
+ requirement: {
1198
+ purpose: "Required behavior or outcome",
1199
+ sections: ["Claim", "Evidence", "Implication"],
1200
+ initialStatus: "proposed"
1201
+ },
1202
+ constraint: {
1203
+ purpose: "Limitation, compatibility boundary, policy, or restriction",
1204
+ sections: ["Claim", "Evidence", "Implication"],
1205
+ initialStatus: "accepted"
1206
+ },
1207
+ decision: {
1208
+ purpose: "Chosen or proposed direction",
1209
+ sections: ["Decision", "Rationale", "Rejected", "Impact"],
1210
+ initialStatus: "accepted"
1211
+ },
1212
+ assumption: {
1213
+ purpose: "Unsourced or not-yet-confirmed working assumption",
1214
+ sections: ["Claim", "Why we think so", "What would falsify it"],
1215
+ initialStatus: "draft"
1216
+ },
1217
+ "open-question": {
1218
+ purpose: "Question needing resolution",
1219
+ sections: ["Question", "Why it matters", "Default assumption"],
1220
+ initialStatus: "open"
1221
+ },
1222
+ risk: {
1223
+ purpose: "Something that can go wrong",
1224
+ sections: ["Risk", "Why it matters", "Mitigation", "Verification"],
1225
+ initialStatus: "open"
1226
+ },
1227
+ contract: {
1228
+ purpose: "API, data, event, schema, or permission contract",
1229
+ sections: ["Contract", "Producer", "Consumer", "Compatibility"],
1230
+ initialStatus: "proposed"
1231
+ },
1232
+ flow: {
1233
+ purpose: "Sequence, lifecycle, or state behavior",
1234
+ sections: ["Flow", "Trigger", "Steps", "Failure modes"],
1235
+ initialStatus: "accepted"
1236
+ },
1237
+ "affected-system": {
1238
+ purpose: "Component, service, package, integration, or external system",
1239
+ sections: ["System", "How it is affected", "Blast radius"],
1240
+ initialStatus: "accepted"
1241
+ },
1242
+ "test-obligation": {
1243
+ purpose: "Behavior or contract that must be verified",
1244
+ sections: ["Obligation", "Why it matters", "How to verify"],
1245
+ initialStatus: "open"
1246
+ },
1247
+ "source-note": {
1248
+ purpose: "Extracted note from source material",
1249
+ sections: ["Note", "Where it came from"],
1250
+ initialStatus: "accepted"
1251
+ }
1252
+ };
1253
+ function isKbRecordType(value) {
1254
+ return Object.prototype.hasOwnProperty.call(RECORD_TYPES, value);
1255
+ }
1256
+ var KB_LINK_RELS = [
1257
+ "depends_on",
1258
+ "constrains",
1259
+ "informs",
1260
+ "blocks",
1261
+ "invalidates",
1262
+ "verified_by",
1263
+ "satisfies",
1264
+ "related_to"
1265
+ ];
1266
+ var LINK_RELS = {
1267
+ depends_on: {
1268
+ purpose: "The source needs the target to hold; the source breaks if the target changes",
1269
+ phrase: "Depends on",
1270
+ dependant: "source"
1271
+ },
1272
+ constrains: {
1273
+ purpose: "The source bounds what the target may do; the target breaks if the constraint changes",
1274
+ phrase: "Constrains",
1275
+ dependant: "target"
1276
+ },
1277
+ informs: {
1278
+ purpose: "The source shaped the target without binding it; the target is what needs revisiting",
1279
+ phrase: "Informs",
1280
+ dependant: "target"
1281
+ },
1282
+ blocks: {
1283
+ purpose: "The target cannot proceed until the source is settled; the target is what waits",
1284
+ phrase: "Blocks",
1285
+ dependant: "target"
1286
+ },
1287
+ invalidates: {
1288
+ purpose: "The source makes the target no longer hold; the target is what stops holding",
1289
+ phrase: "Invalidates",
1290
+ dependant: "target"
1291
+ },
1292
+ verified_by: {
1293
+ purpose: "The target is the check that confirms the source; the source's confirmation moves with it",
1294
+ phrase: "Verified by",
1295
+ dependant: "source"
1296
+ },
1297
+ satisfies: {
1298
+ purpose: "The source discharges the target's requirement; the source must change if the requirement does",
1299
+ phrase: "Satisfies",
1300
+ dependant: "source"
1301
+ },
1302
+ related_to: {
1303
+ purpose: "A pointer worth following, with no claim of dependence",
1304
+ phrase: "Relates to",
1305
+ dependant: null
1306
+ }
1307
+ };
1308
+ var KB_CAUSAL_LINK_RELS = KB_LINK_RELS.filter(
1309
+ (rel) => LINK_RELS[rel].dependant !== null
1310
+ );
1311
+ function isKbLinkRel(value) {
1312
+ return Object.prototype.hasOwnProperty.call(LINK_RELS, value);
1313
+ }
1314
+
1153
1315
  // src/kb-edges.ts
1154
1316
  var KB_EDGE_KINDS = [
1155
1317
  "body-link",
1318
+ "typed-link",
1156
1319
  "supersession",
1157
1320
  "anchor",
1158
1321
  "source"
@@ -1161,10 +1324,11 @@ var BODY_LINK_TARGET = new RegExp(
1161
1324
  `\\]\\((${KB_CONCEPT_ID_PATTERN.source.replace(/^\^|\$$/g, "")})\\.md\\)`,
1162
1325
  "g"
1163
1326
  );
1164
- function neighbours(from, bundle, kinds = KB_EDGE_KINDS) {
1327
+ var DEFAULT_TYPED_LINK_RELS = KB_LINK_RELS;
1328
+ function neighbours(from, bundle, kinds = KB_EDGE_KINDS, linkRels = DEFAULT_TYPED_LINK_RELS) {
1165
1329
  const found = /* @__PURE__ */ new Map();
1166
1330
  for (const kind of kinds) {
1167
- for (const record of edgeNeighbours(from, bundle, kind)) {
1331
+ for (const record of edgeNeighbours(from, bundle, kind, linkRels)) {
1168
1332
  const existing = found.get(record.conceptId);
1169
1333
  if (existing) {
1170
1334
  if (!existing.via.includes(kind)) existing.via.push(kind);
@@ -1175,7 +1339,7 @@ function neighbours(from, bundle, kinds = KB_EDGE_KINDS) {
1175
1339
  }
1176
1340
  return [...found.values()];
1177
1341
  }
1178
- function edgeNeighbours(from, bundle, kind) {
1342
+ function edgeNeighbours(from, bundle, kind, linkRels = DEFAULT_TYPED_LINK_RELS) {
1179
1343
  switch (kind) {
1180
1344
  // A link whose target is not in the bundle is legal per compose.ts —
1181
1345
  // records are routinely written before the ones they point at exist — so
@@ -1189,6 +1353,21 @@ function edgeNeighbours(from, bundle, kind) {
1189
1353
  (candidate) => candidate.conceptId !== from.conceptId && targets.has(candidate.conceptId)
1190
1354
  );
1191
1355
  }
1356
+ // Outbound only, like `body-link`, and for the same reason: this is what
1357
+ // the record declares about itself. A missing target is legal — the walk
1358
+ // skips it, and `kb_validate` is what reports it as a warning. A rel
1359
+ // outside `linkRels` is skipped too, which is how an unknown rel stays
1360
+ // untraversable everywhere rather than one walk at a time.
1361
+ case "typed-link": {
1362
+ const allowed = new Set(linkRels);
1363
+ const targets = new Set(
1364
+ (from.frontmatter.strauss_links ?? []).filter((link2) => allowed.has(link2.rel)).map((link2) => link2.target)
1365
+ );
1366
+ if (!targets.size) return [];
1367
+ return bundle.filter(
1368
+ (candidate) => candidate.conceptId !== from.conceptId && targets.has(candidate.conceptId)
1369
+ );
1370
+ }
1192
1371
  // Both directions and both pointers: `supersede()` writes the pair, but a
1193
1372
  // hand-edit can leave one side behind, and a walk trusting one pointer
1194
1373
  // would miss a replacement the bundle openly declares.
@@ -1227,7 +1406,12 @@ function anchorsTouch(left, right) {
1227
1406
  }
1228
1407
 
1229
1408
  // src/trace.ts
1230
- var TRACE_EDGES = ["supersession", "anchor", "source"];
1409
+ var TRACE_EDGES = [
1410
+ "typed-link",
1411
+ "supersession",
1412
+ "anchor",
1413
+ "source"
1414
+ ];
1231
1415
  function trace(seedId, bundle, options = {}) {
1232
1416
  const edges = options.edges?.length ? options.edges : TRACE_EDGES;
1233
1417
  const maxDepth = options.depth ?? 3;
@@ -1242,7 +1426,12 @@ function trace(seedId, bundle, options = {}) {
1242
1426
  const next = [];
1243
1427
  for (const from of frontier) {
1244
1428
  for (const edge of edges) {
1245
- for (const record of edgeNeighbours(from, bundle, edge)) {
1429
+ for (const record of edgeNeighbours(
1430
+ from,
1431
+ bundle,
1432
+ edge,
1433
+ KB_CAUSAL_LINK_RELS
1434
+ )) {
1246
1435
  const existing = reached.get(record.conceptId);
1247
1436
  if (existing) {
1248
1437
  if (existing.depth > 0 && !existing.via.includes(edge)) {
@@ -1394,6 +1583,141 @@ function renderCatalogLine(entry) {
1394
1583
  return `- ${parts.join(" \xB7 ")}`;
1395
1584
  }
1396
1585
 
1586
+ // src/kb-links/inbound.ts
1587
+ function inboundIndex(bundle) {
1588
+ const byTarget = /* @__PURE__ */ new Map();
1589
+ for (const record of bundle) {
1590
+ for (const link2 of record.frontmatter.strauss_links ?? []) {
1591
+ if (link2.target === record.conceptId) continue;
1592
+ const edges = byTarget.get(link2.target) ?? [];
1593
+ if (edges.some(
1594
+ (edge) => edge.from === record.conceptId && edge.rel === link2.rel
1595
+ )) {
1596
+ continue;
1597
+ }
1598
+ edges.push({ from: record.conceptId, rel: link2.rel });
1599
+ byTarget.set(link2.target, edges);
1600
+ }
1601
+ }
1602
+ return byTarget;
1603
+ }
1604
+
1605
+ // src/kb-links/backlinks.ts
1606
+ function backlinks(targetId, bundle) {
1607
+ const byId = new Map(bundle.map((record) => [record.conceptId, record]));
1608
+ if (!byId.has(targetId)) throw new KbRecordNotFoundError(targetId);
1609
+ const standingOf = new Map(
1610
+ adjudicate(bundle, bundle).map((hit) => [hit.record.conceptId, hit])
1611
+ );
1612
+ const rows = [];
1613
+ for (const edge of inboundIndex(bundle).get(targetId) ?? []) {
1614
+ const record = byId.get(edge.from);
1615
+ if (!record) continue;
1616
+ const hit = standingOf.get(edge.from);
1617
+ rows.push({
1618
+ ...edge,
1619
+ title: record.frontmatter.title ?? null,
1620
+ standing: hit?.standing ?? "unsettled",
1621
+ warnings: hit?.warnings ?? []
1622
+ });
1623
+ }
1624
+ return {
1625
+ target: targetId,
1626
+ backlinks: rows.sort(
1627
+ (left, right) => left.from.localeCompare(right.from) || left.rel.localeCompare(right.rel)
1628
+ )
1629
+ };
1630
+ }
1631
+
1632
+ // src/kb-links/impact.ts
1633
+ function impact(targetId, bundle, options = {}) {
1634
+ const byId = new Map(bundle.map((record) => [record.conceptId, record]));
1635
+ if (!byId.has(targetId)) throw new KbRecordNotFoundError(targetId);
1636
+ const rels = resolveRels(options.rels);
1637
+ const maxDepth = options.depth ?? Number.POSITIVE_INFINITY;
1638
+ const inbound = inboundIndex(bundle);
1639
+ const standingOf = new Map(
1640
+ adjudicate(bundle, bundle).map((hit) => [hit.record.conceptId, hit])
1641
+ );
1642
+ const reached = /* @__PURE__ */ new Map();
1643
+ const stopped = [];
1644
+ let frontier = [targetId];
1645
+ let depth = 0;
1646
+ while (frontier.length && depth < maxDepth) {
1647
+ depth += 1;
1648
+ const next = [];
1649
+ const consider = (dependantId, edge) => {
1650
+ if (dependantId === targetId) return;
1651
+ const existing = reached.get(dependantId);
1652
+ if (existing) {
1653
+ if (!hasEdge(existing.via, edge)) existing.via.push(edge);
1654
+ return;
1655
+ }
1656
+ const record = byId.get(dependantId);
1657
+ if (!record) return;
1658
+ const hit = standingOf.get(dependantId);
1659
+ const entry = {
1660
+ conceptId: dependantId,
1661
+ title: record.frontmatter.title ?? null,
1662
+ standing: hit?.standing ?? "unsettled",
1663
+ warnings: hit?.warnings ?? [],
1664
+ depth,
1665
+ via: [edge]
1666
+ };
1667
+ reached.set(dependantId, entry);
1668
+ if (entry.standing === "superseded" || entry.standing === "rejected") {
1669
+ stopped.push(dependantId);
1670
+ return;
1671
+ }
1672
+ next.push(dependantId);
1673
+ };
1674
+ for (const id of frontier) {
1675
+ for (const edge of inbound.get(id) ?? []) {
1676
+ if (!rels.has(edge.rel)) continue;
1677
+ if (dependantEnd(edge.rel) !== "source") continue;
1678
+ consider(edge.from, { source: edge.from, target: id, rel: edge.rel });
1679
+ }
1680
+ for (const link2 of byId.get(id)?.frontmatter.strauss_links ?? []) {
1681
+ if (!rels.has(link2.rel)) continue;
1682
+ if (dependantEnd(link2.rel) !== "target") continue;
1683
+ if (link2.target === id) continue;
1684
+ consider(link2.target, {
1685
+ source: id,
1686
+ target: link2.target,
1687
+ rel: link2.rel
1688
+ });
1689
+ }
1690
+ }
1691
+ frontier = next;
1692
+ }
1693
+ return {
1694
+ root: targetId,
1695
+ impacted: [...reached.values()].sort(
1696
+ (left, right) => left.depth - right.depth || left.conceptId.localeCompare(right.conceptId)
1697
+ ),
1698
+ stopped: stopped.sort(),
1699
+ truncated: frontier.length > 0,
1700
+ unexpanded: [...frontier].sort()
1701
+ };
1702
+ }
1703
+ function resolveRels(rels) {
1704
+ if (!rels?.length) return new Set(KB_CAUSAL_LINK_RELS);
1705
+ for (const rel of rels) {
1706
+ if (!isKbLinkRel(rel) || LINK_RELS[rel].dependant === null) {
1707
+ throw new KbUnknownLinkRelError(rel, KB_CAUSAL_LINK_RELS);
1708
+ }
1709
+ }
1710
+ return new Set(rels);
1711
+ }
1712
+ function dependantEnd(rel) {
1713
+ return isKbLinkRel(rel) ? LINK_RELS[rel].dependant : null;
1714
+ }
1715
+ function hasEdge(edges, edge) {
1716
+ return edges.some(
1717
+ (existing) => existing.source === edge.source && existing.target === edge.target && existing.rel === edge.rel
1718
+ );
1719
+ }
1720
+
1397
1721
  // src/kb-gitattributes.ts
1398
1722
  var GITATTRIBUTES_FILE = ".gitattributes";
1399
1723
  var UNION_MERGE_LINE = `${LOG_FILE} text eol=lf merge=union`;
@@ -1797,6 +2121,14 @@ ${answer}
1797
2121
  async pack(bundlePath2, rootId, options = {}) {
1798
2122
  return pack(await this.list(bundlePath2), rootId, options);
1799
2123
  }
2124
+ /** What breaks if this record changes. See `kb-links/impact.ts`. */
2125
+ async impact(bundlePath2, targetId, options = {}) {
2126
+ return impact(targetId, await this.list(bundlePath2), options);
2127
+ }
2128
+ /** Who points at this record, one hop. See `kb-links/backlinks.ts`. */
2129
+ async backlinks(bundlePath2, targetId) {
2130
+ return backlinks(targetId, await this.list(bundlePath2));
2131
+ }
1800
2132
  /**
1801
2133
  * The stored index, rebuilt if it disagrees with the records.
1802
2134
  *
@@ -2116,75 +2448,12 @@ function bundleDigest(records, superseded) {
2116
2448
  return digest(entries.join("\n"));
2117
2449
  }
2118
2450
 
2119
- // src/record-types.ts
2120
- var RECORD_TYPES = {
2121
- fact: {
2122
- purpose: "Observed or sourced fact",
2123
- sections: ["Claim", "Evidence", "Implication"],
2124
- initialStatus: "accepted"
2125
- },
2126
- requirement: {
2127
- purpose: "Required behavior or outcome",
2128
- sections: ["Claim", "Evidence", "Implication"],
2129
- initialStatus: "proposed"
2130
- },
2131
- constraint: {
2132
- purpose: "Limitation, compatibility boundary, policy, or restriction",
2133
- sections: ["Claim", "Evidence", "Implication"],
2134
- initialStatus: "accepted"
2135
- },
2136
- decision: {
2137
- purpose: "Chosen or proposed direction",
2138
- sections: ["Decision", "Rationale", "Rejected", "Impact"],
2139
- initialStatus: "accepted"
2140
- },
2141
- assumption: {
2142
- purpose: "Unsourced or not-yet-confirmed working assumption",
2143
- sections: ["Claim", "Why we think so", "What would falsify it"],
2144
- initialStatus: "draft"
2145
- },
2146
- "open-question": {
2147
- purpose: "Question needing resolution",
2148
- sections: ["Question", "Why it matters", "Default assumption"],
2149
- initialStatus: "open"
2150
- },
2151
- risk: {
2152
- purpose: "Something that can go wrong",
2153
- sections: ["Risk", "Why it matters", "Mitigation", "Verification"],
2154
- initialStatus: "open"
2155
- },
2156
- contract: {
2157
- purpose: "API, data, event, schema, or permission contract",
2158
- sections: ["Contract", "Producer", "Consumer", "Compatibility"],
2159
- initialStatus: "proposed"
2160
- },
2161
- flow: {
2162
- purpose: "Sequence, lifecycle, or state behavior",
2163
- sections: ["Flow", "Trigger", "Steps", "Failure modes"],
2164
- initialStatus: "accepted"
2165
- },
2166
- "affected-system": {
2167
- purpose: "Component, service, package, integration, or external system",
2168
- sections: ["System", "How it is affected", "Blast radius"],
2169
- initialStatus: "accepted"
2170
- },
2171
- "test-obligation": {
2172
- purpose: "Behavior or contract that must be verified",
2173
- sections: ["Obligation", "Why it matters", "How to verify"],
2174
- initialStatus: "open"
2175
- },
2176
- "source-note": {
2177
- purpose: "Extracted note from source material",
2178
- sections: ["Note", "Where it came from"],
2179
- initialStatus: "accepted"
2180
- }
2181
- };
2182
- function isKbRecordType(value) {
2183
- return Object.prototype.hasOwnProperty.call(RECORD_TYPES, value);
2184
- }
2185
-
2186
2451
  // src/compose.ts
2187
2452
  var import_zod3 = require("zod");
2453
+ var composeLinkSchema = import_zod3.z.object({
2454
+ target: kbConceptIdSchema,
2455
+ rel: import_zod3.z.enum(KB_LINK_RELS)
2456
+ }).strict();
2188
2457
  var composeInputSchema = import_zod3.z.object({
2189
2458
  slug: import_zod3.z.string().min(1),
2190
2459
  /** One line, in the reader's terms. Becomes OKF `title`. */
@@ -2211,6 +2480,18 @@ var composeInputSchema = import_zod3.z.object({
2211
2480
  tags: import_zod3.z.array(import_zod3.z.string().min(1)).optional(),
2212
2481
  /** Concept ids this record relates to; rendered as body links. */
2213
2482
  relatedConceptIds: import_zod3.z.array(kbConceptIdSchema).optional(),
2483
+ /**
2484
+ * Typed causal edges, source → target: `{ target: "fact.b", rel:
2485
+ * "depends_on" }` on record A says A needs B. Stored in frontmatter and
2486
+ * also rendered as one prose sentence each, so the meaning survives a
2487
+ * reader that knows only OKF. The vocabulary goes into the description from
2488
+ * the same table the walk uses, so `kb_schema` emits it.
2489
+ */
2490
+ links: import_zod3.z.array(composeLinkSchema).max(64).optional().describe(
2491
+ `Typed causal edges, source \u2192 target \u2014 a link on this record says this record <rel> the target. ${KB_LINK_RELS.map(
2492
+ (rel) => `${rel}: ${LINK_RELS[rel].purpose}`
2493
+ ).join("; ")}.`
2494
+ ),
2214
2495
  /** Concept ids this record replaces. The store settles the backlinks. */
2215
2496
  supersedes: import_zod3.z.array(kbConceptIdSchema).max(32).optional(),
2216
2497
  materiality: import_zod3.z.enum(KB_MATERIALITIES).optional(),
@@ -2250,6 +2531,15 @@ function composeRecord(type, input, writtenBy, writtenAt) {
2250
2531
  if (parsed.owner) frontmatter.strauss_owner = parsed.owner;
2251
2532
  if (parsed.supersedes?.length)
2252
2533
  frontmatter.strauss_supersedes = parsed.supersedes;
2534
+ const selfLink = parsed.links?.find(
2535
+ (link2) => link2.target === `${type}.${parsed.slug}`
2536
+ );
2537
+ if (selfLink) {
2538
+ throw new Error(
2539
+ `kb: ${type}.${parsed.slug} cannot ${selfLink.rel} itself \u2014 a link must name another record`
2540
+ );
2541
+ }
2542
+ if (parsed.links?.length) frontmatter.strauss_links = parsed.links;
2253
2543
  const blocks = [];
2254
2544
  for (const heading of spec.sections) {
2255
2545
  const text = sections[heading];
@@ -2261,6 +2551,11 @@ ${text}`);
2261
2551
  for (const related of parsed.relatedConceptIds ?? []) {
2262
2552
  blocks.push(`Relates to [${related}](${related}.md).`);
2263
2553
  }
2554
+ for (const link2 of parsed.links ?? []) {
2555
+ blocks.push(
2556
+ `${LINK_RELS[link2.rel].phrase} [${link2.target}](${link2.target}.md).`
2557
+ );
2558
+ }
2264
2559
  if (parsed.sources?.length) {
2265
2560
  blocks.push(
2266
2561
  parsed.sources.map((source) => `[^${source.id}]: ${source.title ?? source.resource}`).join("\n")
@@ -2901,7 +3196,7 @@ function normalize(path) {
2901
3196
  function validateBundle(records) {
2902
3197
  const byId = new Map(records.map((record) => [record.conceptId, record]));
2903
3198
  const problems = [];
2904
- const report = (check, conceptId2, note) => problems.push({ check, conceptId: conceptId2, note });
3199
+ const report = (check, conceptId2, note, severity = "error") => problems.push({ check, conceptId: conceptId2, note, severity });
2905
3200
  for (const record of records) {
2906
3201
  const { conceptId: conceptId2, frontmatter: fm } = record;
2907
3202
  if (!isKbRecordType(fm.type)) {
@@ -2925,6 +3220,36 @@ function validateBundle(records) {
2925
3220
  report("supersedes", conceptId2, `${old} is not marked superseded`);
2926
3221
  }
2927
3222
  }
3223
+ for (const link2 of fm.strauss_links ?? []) {
3224
+ if (!isKbLinkRel(link2.rel)) {
3225
+ report(
3226
+ "link_rel",
3227
+ conceptId2,
3228
+ `unknown rel "${link2.rel}" on link to ${link2.target} \u2014 expected one of ${KB_LINK_RELS.join(", ")}`
3229
+ );
3230
+ }
3231
+ if (!KB_CONCEPT_ID_PATTERN.test(link2.target)) {
3232
+ report(
3233
+ "link_target",
3234
+ conceptId2,
3235
+ `target "${link2.target}" is not a valid concept id \u2014 expected <type>.<slug>, both kebab-case`
3236
+ );
3237
+ } else if (link2.target === conceptId2) {
3238
+ report(
3239
+ "link_target",
3240
+ conceptId2,
3241
+ `links to itself (${link2.rel})`,
3242
+ "warning"
3243
+ );
3244
+ } else if (!byId.has(link2.target)) {
3245
+ report(
3246
+ "link_target",
3247
+ conceptId2,
3248
+ `target ${link2.target} is not in the bundle`,
3249
+ "warning"
3250
+ );
3251
+ }
3252
+ }
2928
3253
  if (fm.strauss_assumption && fm.sources?.length) {
2929
3254
  report("assumption", conceptId2, "marked an assumption but cites sources");
2930
3255
  }
@@ -3219,7 +3544,7 @@ var decisionInputSchema = composeInputSchema.omit({ sections: true }).extend({
3219
3544
  impact: import_zod6.z.string().min(1).optional()
3220
3545
  }).strict();
3221
3546
  function composeDecisionRecord(input, writtenBy, writtenAt) {
3222
- const { alternative, impact, ...rest } = input;
3547
+ const { alternative, impact: impact2, ...rest } = input;
3223
3548
  return composeRecord(
3224
3549
  DECISION_TYPE,
3225
3550
  {
@@ -3228,7 +3553,7 @@ function composeDecisionRecord(input, writtenBy, writtenAt) {
3228
3553
  Decision: input.title,
3229
3554
  Rationale: input.why,
3230
3555
  ...alternative ? { Rejected: alternative } : {},
3231
- ...impact ? { Impact: impact } : {}
3556
+ ...impact2 ? { Impact: impact2 } : {}
3232
3557
  }
3233
3558
  },
3234
3559
  writtenBy,
@@ -3453,7 +3778,7 @@ var answerCommand = define({
3453
3778
  name: "answer",
3454
3779
  tool: "kb_answer",
3455
3780
  usage: "answer <concept-id> <answer...>",
3456
- description: "Resolve an open question: sets the status, stamps who answered and when, and appends an Answer section. If the answer overturns an assumption or a decision, that is a supersession \u2014 do it explicitly.",
3781
+ description: "Resolve an open question: set status, stamp who and when, append an Answer section. If the answer overturns a decision or assumption, supersede that record explicitly.",
3457
3782
  input: import_zod9.z.object({ bundlePath, conceptId, answer: import_zod9.z.string().min(1) }),
3458
3783
  fromArgv: (argv, path) => ({
3459
3784
  bundlePath: path,
@@ -3467,16 +3792,28 @@ var answerCommand = define({
3467
3792
  }
3468
3793
  });
3469
3794
 
3470
- // src/commands/catalog.ts
3795
+ // src/commands/backlinks.ts
3471
3796
  var import_zod10 = require("zod");
3797
+ var backlinksCommand = define({
3798
+ name: "backlinks",
3799
+ tool: "kb_backlinks",
3800
+ usage: "backlinks <concept-id>",
3801
+ description: "Who points at this record: every inbound typed causal link (`strauss_links`), one hop, every rel including `related_to`, each with its rel and the standing of the record that made it. Use it when you need the exact edges \u2014 reviewing or renaming a record.",
3802
+ input: import_zod10.z.object({ bundlePath, conceptId }),
3803
+ fromArgv: (argv, path) => ({ bundlePath: path, conceptId: argv[1] }),
3804
+ run: async ({ store }, { bundlePath: path, conceptId: id }) => store.backlinks(path, id)
3805
+ });
3806
+
3807
+ // src/commands/catalog.ts
3808
+ var import_zod11 = require("zod");
3472
3809
  var catalogCommand = define({
3473
3810
  name: "catalog",
3474
3811
  tool: "kb_catalog",
3475
3812
  usage: "catalog [type]",
3476
3813
  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.",
3477
- input: import_zod10.z.object({
3814
+ input: import_zod11.z.object({
3478
3815
  bundlePath,
3479
- type: import_zod10.z.enum(KB_RECORD_TYPES).optional()
3816
+ type: import_zod11.z.enum(KB_RECORD_TYPES).optional()
3480
3817
  }),
3481
3818
  fromArgv: (argv, path) => ({
3482
3819
  bundlePath: path,
@@ -3531,26 +3868,26 @@ function count(value, noun) {
3531
3868
  }
3532
3869
 
3533
3870
  // src/commands/context.ts
3534
- var import_zod11 = require("zod");
3871
+ var import_zod12 = require("zod");
3535
3872
  var contextCommand = define({
3536
3873
  name: "context",
3537
3874
  tool: "kb_context",
3538
3875
  usage: "context [--profile NAME] [--budget N] [--full-under N] [--format json] [--event NAME]",
3539
- description: "The pinned-base index block, for injection at every context birth \u2014 startup, clear, resume, and after compaction. An index, not the content: concept ids, titles and standing, with the bodies left behind kb_load at the point of use. Emits nothing when nothing is pinned. Refuses with the list of bases and their sizes rather than truncating past its budget. Budgets resolve most-specific-first: explicit flags, then the workspace manifests' `context` tables (per profile, over their `default`), then the built-in profile (session-start, compact, turn), then package defaults \u2014 so a repo tunes its own numbers in .strauss/kb-pins.json without touching hook commands. Like kb_schema and kb_types this takes no bundlePath \u2014 it reads the workspace pin manifests, because which bases a session should see is workspace state, not a property of one base.",
3540
- input: import_zod11.z.object({
3541
- budgetTokens: import_zod11.z.number().int().positive().optional().describe(
3876
+ 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.",
3877
+ input: import_zod12.z.object({
3878
+ budgetTokens: import_zod12.z.number().int().positive().optional().describe(
3542
3879
  "Ceiling on the whole emitted block; past it the command refuses with a list of bases rather than truncating. Defaults to 4000."
3543
3880
  ),
3544
- fullUnderTokens: import_zod11.z.number().int().positive().optional().describe(
3881
+ fullUnderTokens: import_zod12.z.number().int().positive().optional().describe(
3545
3882
  "Per-base rendering threshold, applied before the budget: a base whose complete load fits under this arrives as full records instead of index lines, and the whole block still answers to budgetTokens. Off by default \u2014 index-only is the safe default at a context birth, because injected bodies outlive the qualifiers on them; the session-start profile opts tiny bases in at 1500."
3546
3883
  ),
3547
- profile: import_zod11.z.string().optional().describe(
3884
+ profile: import_zod12.z.string().optional().describe(
3548
3885
  "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."
3549
3886
  ),
3550
- format: import_zod11.z.enum(["markdown", "json"]).optional().describe(
3887
+ format: import_zod12.z.enum(["markdown", "json"]).optional().describe(
3551
3888
  "CLI envelope for hook protocols that require strict JSON on stdout. MCP callers omit this \u2014 the block itself is identical."
3552
3889
  ),
3553
- event: import_zod11.z.string().optional().describe(
3890
+ event: import_zod12.z.string().optional().describe(
3554
3891
  "hookEventName stamped into the JSON envelope. Only meaningful with format=json."
3555
3892
  )
3556
3893
  }),
@@ -3586,14 +3923,14 @@ var contextCommand = define({
3586
3923
  });
3587
3924
 
3588
3925
  // src/commands/doctor.ts
3589
- var import_zod12 = require("zod");
3590
- var days = (what, fallback) => import_zod12.z.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
3926
+ var import_zod13 = require("zod");
3927
+ var days = (what, fallback) => import_zod13.z.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
3591
3928
  var doctorCommand = define({
3592
3929
  name: "doctor",
3593
3930
  tool: "kb_doctor",
3594
3931
  usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--strict]",
3595
3932
  description: "Read-only health sweep: expired, expiring, unverified, aging, orphaned, broken-supersession, superseded-but-cited, drifted 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.",
3596
- input: import_zod12.z.object({
3933
+ input: import_zod13.z.object({
3597
3934
  bundlePath,
3598
3935
  repoRoot: REPO_ROOT,
3599
3936
  expiringDays: days(
@@ -3608,7 +3945,7 @@ var doctorCommand = define({
3608
3945
  "How long a record may stay `open` or `proposed` before `aging` reports it, in days.",
3609
3946
  DEFAULT_AGING_DAYS
3610
3947
  ),
3611
- strict: import_zod12.z.boolean().optional().describe(
3948
+ strict: import_zod13.z.boolean().optional().describe(
3612
3949
  "Turn an expired record into a non-zero exit for the CLI. No effect on the report itself."
3613
3950
  )
3614
3951
  }),
@@ -3683,14 +4020,47 @@ function render2(result) {
3683
4020
  return lines.join("\n");
3684
4021
  }
3685
4022
 
4023
+ // src/commands/impact.ts
4024
+ var import_zod14 = require("zod");
4025
+ var impactCommand = define({
4026
+ name: "impact",
4027
+ tool: "kb_impact",
4028
+ usage: "impact <concept-id> [--depth N] [--rels a,b]",
4029
+ 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.",
4030
+ input: import_zod14.z.object({
4031
+ bundlePath,
4032
+ conceptId,
4033
+ depth: import_zod14.z.number().int().positive().optional().describe(
4034
+ "Hops out from the record. Unbounded when omitted; a walk this cuts reports truncated: true."
4035
+ ),
4036
+ rels: import_zod14.z.array(import_zod14.z.enum(KB_CAUSAL_LINK_RELS)).optional().describe(
4037
+ "Narrow which rels the walk follows. Defaults to every rel that carries a dependence \u2014 all but related_to."
4038
+ )
4039
+ }),
4040
+ fromArgv: (argv, path) => {
4041
+ const depth = argvFlag(argv, "--depth");
4042
+ const rels = argvFlag(argv, "--rels");
4043
+ return {
4044
+ bundlePath: path,
4045
+ conceptId: argv[1],
4046
+ ...depth ? { depth: Number(depth) } : {},
4047
+ ...rels ? { rels: rels.split(",").filter(Boolean) } : {}
4048
+ };
4049
+ },
4050
+ run: async ({ store }, { bundlePath: path, conceptId: id, depth, rels }) => store.impact(path, id, {
4051
+ ...depth !== void 0 ? { depth } : {},
4052
+ ...rels?.length ? { rels } : {}
4053
+ })
4054
+ });
4055
+
3686
4056
  // src/commands/list.ts
3687
- var import_zod13 = require("zod");
4057
+ var import_zod15 = require("zod");
3688
4058
  var listCommand = define({
3689
4059
  name: "list",
3690
4060
  tool: "kb_list",
3691
4061
  usage: "list [type]",
3692
- description: "Every record, optionally narrowed to one type. Use kb_query when you have a question; this is for enumerating.",
3693
- input: import_zod13.z.object({ bundlePath, type: import_zod13.z.enum(KB_RECORD_TYPES).optional() }),
4062
+ description: "Every record, optionally one type. For enumerating; use kb_query for a question.",
4063
+ input: import_zod15.z.object({ bundlePath, type: import_zod15.z.enum(KB_RECORD_TYPES).optional() }),
3694
4064
  fromArgv: (argv, path) => ({ bundlePath: path, type: argv[1] }),
3695
4065
  run: async ({ store }, { bundlePath: path, type }) => (await store.list(path, type)).map((record) => ({
3696
4066
  conceptId: record.conceptId,
@@ -3702,17 +4072,17 @@ var listCommand = define({
3702
4072
  });
3703
4073
 
3704
4074
  // src/commands/load.ts
3705
- var import_zod14 = require("zod");
4075
+ var import_zod16 = require("zod");
3706
4076
  var loadCommand = define({
3707
4077
  name: "load",
3708
4078
  tool: "kb_load",
3709
4079
  usage: "load [type] [--budget N | --all] [--repo-root PATH]",
3710
- description: "Loads the whole knowledge base at once, each record with its standing. Superseded records arrive as stubs; rejected and open records arrive whole. Refuses past the token budget \u2014 call kb_catalog, kb_pack on it; `all` bypasses the budget. Never read record files directly. Cache-stable; `digest` is the base's content stamp \u2014 hooks use it to tell you when to reload.",
3711
- input: import_zod14.z.object({
4080
+ 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.",
4081
+ input: import_zod16.z.object({
3712
4082
  bundlePath,
3713
- type: import_zod14.z.enum(KB_RECORD_TYPES).optional(),
3714
- budgetTokens: import_zod14.z.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
3715
- all: import_zod14.z.boolean().optional().describe(
4083
+ type: import_zod16.z.enum(KB_RECORD_TYPES).optional(),
4084
+ budgetTokens: import_zod16.z.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
4085
+ all: import_zod16.z.boolean().optional().describe(
3716
4086
  "Loads the entire base regardless of size, bypassing the token budget; mutually exclusive with budgetTokens."
3717
4087
  ),
3718
4088
  repoRoot: REPO_ROOT
@@ -3754,25 +4124,25 @@ var loadCommand = define({
3754
4124
  });
3755
4125
 
3756
4126
  // src/commands/log.ts
3757
- var import_zod15 = require("zod");
4127
+ var import_zod17 = require("zod");
3758
4128
  var logCommand = define({
3759
4129
  name: "log",
3760
4130
  tool: "kb_log",
3761
4131
  usage: "log",
3762
- description: "What touched what, and when. The only artifact here that cannot be reconstructed from the records, so malformed lines are reported rather than repaired.",
3763
- input: import_zod15.z.object({ bundlePath }),
4132
+ description: "Who touched what, and when. Append-only; malformed lines are reported, never repaired.",
4133
+ input: import_zod17.z.object({ bundlePath }),
3764
4134
  fromArgv: (_argv, path) => ({ bundlePath: path }),
3765
4135
  run: ({ store }, { bundlePath: path }) => store.readLog(path)
3766
4136
  });
3767
4137
 
3768
4138
  // src/commands/no-decision.ts
3769
- var import_zod16 = require("zod");
4139
+ var import_zod18 = require("zod");
3770
4140
  var noDecisionCommand = define({
3771
4141
  name: "no-decision",
3772
4142
  tool: "kb_no_decision",
3773
4143
  usage: "no-decision <reason...>",
3774
- description: 'Claim in one sentence that there was nothing to decide. Gating on "did you write a decision?" rewards writing a junk one; gating on "did you answer?" does not, so silence has to be expressible. Idempotent \u2014 restating it is not a collision.',
3775
- input: import_zod16.z.object({ bundlePath, reason: import_zod16.z.string().min(1) }),
4144
+ description: "Record in one sentence that a piece of work had nothing to decide. Idempotent.",
4145
+ input: import_zod18.z.object({ bundlePath, reason: import_zod18.z.string().min(1) }),
3776
4146
  fromArgv: (argv, path) => ({
3777
4147
  bundlePath: path,
3778
4148
  reason: argv.slice(1).join(" ").trim()
@@ -3789,20 +4159,20 @@ var noDecisionCommand = define({
3789
4159
  });
3790
4160
 
3791
4161
  // src/commands/pack.ts
3792
- var import_zod17 = require("zod");
4162
+ var import_zod19 = require("zod");
3793
4163
  var packCommand = define({
3794
4164
  name: "pack",
3795
4165
  tool: "kb_pack",
3796
4166
  usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
3797
- description: "The bounded neighbourhood around one record: everything within `hops` of the root, ranked and cut to `maxNodes`, with every cut record named under Excluded \u2014 a named gap is knowable, a silent one is not. Prefer this over kb_load when the base is too large to hold whole and the work centres on one record; prefer it over kb_query when the question needs the governed neighbourhood \u2014 what was settled and what binds near this record \u2014 rather than a lookup by wording. Superseded records arrive as name, replacement and date stubs exactly as kb_load emits them: their bodies no longer hold, and kb_trace has the history. Refuses outright rather than truncating when the pack would exceed its token budget \u2014 a partial pack is indistinguishable from a complete one \u2014 reporting the record count and every already-cut id so the caller can lower hops or maxNodes, or raise the budget. The header carries the bundle, root, budget and a timestamp; everything below the header is byte-identical across runs over an unchanged base, so two packs can be diffed and a changed byte means changed knowledge. This tool (with kb_load, kb_query and kb_trace) is the only supported way to read a base; a raw file read bypasses supersession resolution and returns replaced records as if current.",
3798
- input: import_zod17.z.object({
4167
+ 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.",
4168
+ input: import_zod19.z.object({
3799
4169
  bundlePath,
3800
4170
  conceptId,
3801
- hops: import_zod17.z.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
3802
- maxNodes: import_zod17.z.number().int().positive().optional().describe(
4171
+ hops: import_zod19.z.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
4172
+ maxNodes: import_zod19.z.number().int().positive().optional().describe(
3803
4173
  "How many records the pack may hold, root included. Defaults to 20."
3804
4174
  ),
3805
- budgetTokens: import_zod17.z.number().int().positive().optional().describe(
4175
+ budgetTokens: import_zod19.z.number().int().positive().optional().describe(
3806
4176
  "Approximate token ceiling over what is actually emitted. Defaults to 25000."
3807
4177
  )
3808
4178
  }),
@@ -3889,22 +4259,22 @@ function warningLabel(warning) {
3889
4259
  }
3890
4260
 
3891
4261
  // src/commands/pin.ts
3892
- var import_zod18 = require("zod");
4262
+ var import_zod20 = require("zod");
3893
4263
  var pinCommand = define({
3894
4264
  name: "pin",
3895
4265
  tool: "kb_pin",
3896
4266
  usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
3897
- description: "Pin a base into a workspace pin manifest, so `context` surfaces it at every context birth. Three layers, nearest wins: the committed project manifest (.strauss/kb-pins.json, the default), `--local` (.strauss/kb-pins.local.json, personal and gitignored), and `--user` (~/.strauss/kb-pins.json, every workspace). Idempotent \u2014 re-pinning changes nothing unless --mode, --profiles, or --frozen/--unfreeze are given, which update just those fields. `--mode full` preloads the whole base into the block regardless of the full-under threshold; `--mode index` never upgrades. `--profiles` scopes the pin to named context profiles. `--frozen` marks the base concluded: write commands against it refuse and `context` labels it read-only. A path with no records yet succeeds with a warning; bases are routinely pinned before they are populated. Pins are workspace state: the pinned base itself is never touched.",
3898
- input: import_zod18.z.object({
4267
+ 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.",
4268
+ input: import_zod20.z.object({
3899
4269
  bundlePath,
3900
- mode: import_zod18.z.enum(["full", "index"]).optional().describe(
4270
+ mode: import_zod20.z.enum(["full", "index"]).optional().describe(
3901
4271
  "full: always emit this base's records whole (still under the block budget); index: never upgrade. Absent: the profile's full-under threshold decides."
3902
4272
  ),
3903
- profiles: import_zod18.z.array(import_zod18.z.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
3904
- layer: import_zod18.z.enum(["project", "local", "user"]).optional().describe(
4273
+ profiles: import_zod20.z.array(import_zod20.z.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
4274
+ layer: import_zod20.z.enum(["project", "local", "user"]).optional().describe(
3905
4275
  "Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
3906
4276
  ),
3907
- frozen: import_zod18.z.boolean().optional().describe(
4277
+ frozen: import_zod20.z.boolean().optional().describe(
3908
4278
  "true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
3909
4279
  )
3910
4280
  }),
@@ -3933,29 +4303,29 @@ var pinCommand = define({
3933
4303
  });
3934
4304
 
3935
4305
  // src/commands/pins.ts
3936
- var import_zod19 = require("zod");
4306
+ var import_zod21 = require("zod");
3937
4307
  var pinsCommand = define({
3938
4308
  name: "pins",
3939
4309
  tool: "kb_pins",
3940
4310
  usage: "pins",
3941
- description: "Every pinned base across the manifest layers, each with its layer and whether it currently resolves to readable records. Reads the workspace manifests rather than any one base, like kb_context.",
3942
- input: import_zod19.z.object({}),
4311
+ description: "Every pinned base across the manifest layers, with its layer and whether it resolves to records. Takes no bundlePath.",
4312
+ input: import_zod21.z.object({}),
3943
4313
  fromArgv: () => ({}),
3944
4314
  run: ({ store }) => listPins(store, process.cwd())
3945
4315
  });
3946
4316
 
3947
4317
  // src/commands/query.ts
3948
- var import_zod20 = require("zod");
4318
+ var import_zod22 = require("zod");
3949
4319
  var queryCommand = define({
3950
4320
  name: "query",
3951
4321
  tool: "kb_query",
3952
4322
  usage: "query <text...> [--repo-root PATH]",
3953
4323
  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.",
3954
- input: import_zod20.z.object({
4324
+ input: import_zod22.z.object({
3955
4325
  bundlePath,
3956
- text: import_zod20.z.string().optional(),
3957
- type: import_zod20.z.enum(KB_RECORD_TYPES).optional(),
3958
- includeNonCurrent: import_zod20.z.boolean().optional(),
4326
+ text: import_zod22.z.string().optional(),
4327
+ type: import_zod22.z.enum(KB_RECORD_TYPES).optional(),
4328
+ includeNonCurrent: import_zod22.z.boolean().optional(),
3959
4329
  repoRoot: REPO_ROOT
3960
4330
  }),
3961
4331
  // `--repo-root` is a flag, so its value must not fall into the search text.
@@ -3987,40 +4357,40 @@ var queryCommand = define({
3987
4357
  });
3988
4358
 
3989
4359
  // src/commands/read-index.ts
3990
- var import_zod21 = require("zod");
4360
+ var import_zod23 = require("zod");
3991
4361
  var readIndexCommand = define({
3992
4362
  name: "index",
3993
4363
  tool: "kb_index",
3994
4364
  usage: "index",
3995
- description: "The index, rebuilt if it disagrees with the records. One call gives the whole shape of the base: title, type, status, and description per record. The cheap re-orientation call after compaction or deep in a long session \u2014 a few hundred tokens; call it (or kb_context, when bases are pinned) first, then kb_load or fetch by concept id.",
3996
- input: import_zod21.z.object({ bundlePath }),
4365
+ 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.",
4366
+ input: import_zod23.z.object({ bundlePath }),
3997
4367
  fromArgv: (_argv, path) => ({ bundlePath: path }),
3998
4368
  run: ({ store }, { bundlePath: path }) => store.readIndex(path)
3999
4369
  });
4000
4370
 
4001
4371
  // src/commands/schema.ts
4002
- var import_zod22 = require("zod");
4372
+ var import_zod24 = require("zod");
4003
4373
  var schemaCommand = define({
4004
4374
  name: "schema",
4005
4375
  tool: "kb_schema",
4006
4376
  usage: "schema",
4007
- description: "JSON Schema for the frontmatter, the write input, and log entries \u2014 generated from the code that enforces them, so it cannot drift from what a write will accept.",
4008
- input: import_zod22.z.object({}),
4377
+ description: "JSON Schema for frontmatter, write input, and log entries, generated from the enforcing code.",
4378
+ input: import_zod24.z.object({}),
4009
4379
  fromArgv: () => ({}),
4010
4380
  run: () => Promise.resolve(kbJsonSchemas())
4011
4381
  });
4012
4382
 
4013
4383
  // src/commands/status.ts
4014
- var import_zod23 = require("zod");
4384
+ var import_zod25 = require("zod");
4015
4385
  var statusCommand = define({
4016
4386
  name: "status",
4017
4387
  tool: "kb_status",
4018
4388
  usage: "status <concept-id> <status>",
4019
- description: "Move a record's status, leaving everything else alone. Uses a compare-and-swap, so a concurrent change fails loudly rather than being overwritten.",
4020
- input: import_zod23.z.object({
4389
+ description: "Move a record's status. Compare-and-swap: a concurrent change fails instead of being overwritten.",
4390
+ input: import_zod25.z.object({
4021
4391
  bundlePath,
4022
4392
  conceptId,
4023
- status: import_zod23.z.enum(KB_RECORD_STATUSES)
4393
+ status: import_zod25.z.enum(KB_RECORD_STATUSES)
4024
4394
  }),
4025
4395
  fromArgv: (argv, path) => ({
4026
4396
  bundlePath: path,
@@ -4035,13 +4405,13 @@ var statusCommand = define({
4035
4405
  });
4036
4406
 
4037
4407
  // src/commands/supersede.ts
4038
- var import_zod24 = require("zod");
4408
+ var import_zod26 = require("zod");
4039
4409
  var supersedeCommand = define({
4040
4410
  name: "supersede",
4041
4411
  tool: "kb_supersede",
4042
4412
  usage: "supersede <concept-id> <replacement-id>",
4043
- description: "Mark a record superseded by another, linking both directions. Use this rather than editing a record whose meaning changed \u2014 a record that quietly becomes something else invalidates every reference to it, and the earlier understanding is what a later trace needs.",
4044
- input: import_zod24.z.object({ bundlePath, conceptId, replacementId: conceptId }),
4413
+ description: "Mark a record superseded by another, linked in both directions. Use instead of editing a record whose meaning changed.",
4414
+ input: import_zod26.z.object({ bundlePath, conceptId, replacementId: conceptId }),
4045
4415
  fromArgv: (argv, path) => ({
4046
4416
  bundlePath: path,
4047
4417
  conceptId: argv[1],
@@ -4055,16 +4425,16 @@ var supersedeCommand = define({
4055
4425
  });
4056
4426
 
4057
4427
  // src/commands/sync-instructions.ts
4058
- var import_zod25 = require("zod");
4428
+ var import_zod27 = require("zod");
4059
4429
  var syncInstructionsCommand = define({
4060
4430
  name: "sync-instructions",
4061
4431
  usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
4062
- description: "Idempotently plant the `context` block between sentinel comments in an instruction file (AGENTS.md, CLAUDE.md), creating the block when absent and leaving everything outside the sentinels alone. CLI-only: this is file plumbing for runtimes whose instruction files are re-read where their conversations are not, not an agent capability \u2014 the capability is kb_context.",
4063
- input: import_zod25.z.object({
4064
- file: import_zod25.z.string().min(1).describe("The instruction file to edit in place."),
4065
- budgetTokens: import_zod25.z.number().int().positive().optional(),
4066
- fullUnderTokens: import_zod25.z.number().int().positive().optional(),
4067
- profile: import_zod25.z.string().optional()
4432
+ description: "CLI-only: plant the kb_context block between sentinel comments in AGENTS.md or CLAUDE.md, idempotently.",
4433
+ input: import_zod27.z.object({
4434
+ file: import_zod27.z.string().min(1).describe("The instruction file to edit in place."),
4435
+ budgetTokens: import_zod27.z.number().int().positive().optional(),
4436
+ fullUnderTokens: import_zod27.z.number().int().positive().optional(),
4437
+ profile: import_zod27.z.string().optional()
4068
4438
  }),
4069
4439
  fromArgv: (argv) => {
4070
4440
  const budget = argvFlag(argv, "--budget");
@@ -4090,17 +4460,17 @@ var syncInstructionsCommand = define({
4090
4460
  });
4091
4461
 
4092
4462
  // src/commands/trace.ts
4093
- var import_zod26 = require("zod");
4463
+ var import_zod28 = require("zod");
4094
4464
  var traceCommand = define({
4095
4465
  name: "trace",
4096
4466
  tool: "kb_trace",
4097
4467
  usage: "trace <concept-id> [edges...]",
4098
- description: 'How a position was arrived at, as a timeline ordered by when each record was written. Deliberately includes rejected, draft, and superseded records \u2014 in a history those are the content, not noise. Follows supersession, shared code anchors, and shared sources. Use when the question is "why is this the way it is" rather than "what do we hold now". This tool (with kb_load and kb_query) is the only supported way to read a base; a raw file read bypasses supersession resolution and returns replaced records as if current.',
4099
- input: import_zod26.z.object({
4468
+ 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".',
4469
+ input: import_zod28.z.object({
4100
4470
  bundlePath,
4101
4471
  conceptId,
4102
- edges: import_zod26.z.array(import_zod26.z.enum(TRACE_EDGES)).optional(),
4103
- depth: import_zod26.z.number().int().positive().optional()
4472
+ edges: import_zod28.z.array(import_zod28.z.enum(TRACE_EDGES)).optional(),
4473
+ depth: import_zod28.z.number().int().positive().optional()
4104
4474
  }),
4105
4475
  fromArgv: (argv, path) => ({
4106
4476
  bundlePath: path,
@@ -4122,53 +4492,56 @@ var traceCommand = define({
4122
4492
  });
4123
4493
 
4124
4494
  // src/commands/types.ts
4125
- var import_zod27 = require("zod");
4495
+ var import_zod29 = require("zod");
4126
4496
  var typesCommand = define({
4127
4497
  name: "types",
4128
4498
  tool: "kb_types",
4129
4499
  usage: "types",
4130
4500
  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.",
4131
- input: import_zod27.z.object({}),
4501
+ input: import_zod29.z.object({}),
4132
4502
  fromArgv: () => ({}),
4133
4503
  run: () => Promise.resolve(RECORD_TYPES)
4134
4504
  });
4135
4505
 
4136
4506
  // src/commands/unpin.ts
4137
- var import_zod28 = require("zod");
4507
+ var import_zod30 = require("zod");
4138
4508
  var unpinCommand = define({
4139
4509
  name: "unpin",
4140
4510
  tool: "kb_unpin",
4141
4511
  usage: "unpin [bundle-path]",
4142
- description: "Remove a base from every pin manifest layer that holds it \u2014 project, local, and user \u2014 because unpinned means gone, not still injected from another file. Reports which layers were touched.",
4143
- input: import_zod28.z.object({ bundlePath }),
4512
+ description: "Remove a base from every manifest layer that holds it. Reports the layers touched.",
4513
+ input: import_zod30.z.object({ bundlePath }),
4144
4514
  fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
4145
4515
  run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
4146
4516
  });
4147
4517
 
4148
4518
  // src/commands/validate.ts
4149
- var import_zod29 = require("zod");
4519
+ var import_zod31 = require("zod");
4150
4520
  var validateCommand = define({
4151
4521
  name: "validate",
4152
4522
  tool: "kb_validate",
4153
4523
  usage: "validate",
4154
- description: "Check pointers no single record can see: supersession links that disagree between the two records, and assumptions that cite sources. Per-record shape is enforced on every read, so a problem here means someone edited a file by hand.",
4155
- input: import_zod29.z.object({ bundlePath }),
4524
+ 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.",
4525
+ input: import_zod31.z.object({ bundlePath }),
4156
4526
  fromArgv: (_argv, path) => ({ bundlePath: path }),
4157
4527
  run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
4158
- failsWhen: (result) => Array.isArray(result) && result.length > 0
4528
+ // Warnings never fail the exit code; every other severity does.
4529
+ failsWhen: (result) => Array.isArray(result) && result.some(
4530
+ (problem) => problem.severity !== "warning"
4531
+ )
4159
4532
  });
4160
4533
 
4161
4534
  // src/commands/verify.ts
4162
- var import_zod30 = require("zod");
4535
+ var import_zod32 = require("zod");
4163
4536
  var verifyCommand = define({
4164
4537
  name: "verify",
4165
4538
  tool: "kb_verify",
4166
4539
  usage: "verify <concept-id> --note <text>",
4167
- description: "Append one verified[] event \u2014 who checked the record, when, and what the check found. Appends only; prior events are never rewritten. A record's own generator is refused unless the actor is human: re-reading your own output is not an independent check.",
4168
- input: import_zod30.z.object({
4540
+ 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.",
4541
+ input: import_zod32.z.object({
4169
4542
  bundlePath,
4170
4543
  conceptId,
4171
- note: import_zod30.z.string().refine((s) => s.trim().length > 0, {
4544
+ note: import_zod32.z.string().refine((s) => s.trim().length > 0, {
4172
4545
  message: "note must say what the check found"
4173
4546
  })
4174
4547
  }),
@@ -4188,23 +4561,15 @@ var verifyCommand = define({
4188
4561
  });
4189
4562
 
4190
4563
  // src/commands/write.ts
4191
- var import_zod31 = require("zod");
4564
+ var import_zod33 = require("zod");
4192
4565
  var writeCommand = define({
4193
4566
  name: "write",
4194
4567
  tool: "kb_write",
4195
4568
  usage: "write <type> < record.json",
4196
- description: [
4197
- "Write one record. Search first \u2014 the same knowledge filed twice under different slugs is how a base rots, and a duplicate concept id is rejected rather than overwritten. Call kb_types for the sections each type accepts.",
4198
- "",
4199
- "Judgment the tool cannot enforce for you:",
4200
- "- An unsourced claim is an `assumption` record with assumption: true, never a `fact` with a vague source. The distinction is what lets a later reader separate what was established from what was guessed.",
4201
- "- When two records conflict, say so in a `risk`, an `open-question`, or a superseding `decision`. Quietly picking a winner destroys the disagreement, which is usually the useful part.",
4202
- "- Prefer a new record over overloading an existing one, and keep each short. A record nobody finishes reading is not durable memory.",
4203
- "- Records are never deleted; supersede instead, so the earlier reasoning stays inspectable."
4204
- ].join("\n"),
4205
- input: import_zod31.z.object({
4569
+ 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.",
4570
+ input: import_zod33.z.object({
4206
4571
  bundlePath,
4207
- type: import_zod31.z.enum(KB_RECORD_TYPES),
4572
+ type: import_zod33.z.enum(KB_RECORD_TYPES),
4208
4573
  input: composeInputSchema
4209
4574
  }),
4210
4575
  fromArgv: async (argv, path, stdin) => ({
@@ -4228,20 +4593,13 @@ var writeCommand = define({
4228
4593
  });
4229
4594
 
4230
4595
  // src/commands/write-decision.ts
4231
- var import_zod32 = require("zod");
4596
+ var import_zod34 = require("zod");
4232
4597
  var writeDecisionCommand = define({
4233
4598
  name: "write-decision",
4234
4599
  tool: "kb_write_decision",
4235
4600
  usage: "write-decision < decision.json",
4236
- description: [
4237
- "Write a decision. Takes `alternative` and `impact` as fields rather than free sections, because what was rejected is the part a later reader cannot reconstruct from the code \u2014 a heading is too easy to leave empty.",
4238
- "",
4239
- "What belongs in one:",
4240
- '- Record a decision when a later reader would otherwise "simplify" the constraint away. If the diff already answers the question, there is nothing here to write.',
4241
- "- `alternative` is what you turned down and why, not a list of everything considered.",
4242
- "- A reference to material you read goes in `sources`; a reference to code goes in `anchors`; a reference to another record goes in `relatedConceptIds`."
4243
- ].join("\n"),
4244
- input: import_zod32.z.object({ bundlePath, input: decisionInputSchema }),
4601
+ 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.",
4602
+ input: import_zod34.z.object({ bundlePath, input: decisionInputSchema }),
4245
4603
  fromArgv: async (_argv, path, stdin) => ({
4246
4604
  bundlePath: path,
4247
4605
  input: JSON.parse(await stdin())
@@ -4276,6 +4634,8 @@ var KB_COMMANDS = [
4276
4634
  packCommand,
4277
4635
  queryCommand,
4278
4636
  traceCommand,
4637
+ impactCommand,
4638
+ backlinksCommand,
4279
4639
  listCommand,
4280
4640
  readIndexCommand,
4281
4641
  logCommand,
@@ -4298,7 +4658,7 @@ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
4298
4658
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
4299
4659
 
4300
4660
  // src/version.ts
4301
- var VERSION = true ? "0.1.12" : "0.0.0-dev";
4661
+ var VERSION = true ? "0.1.14" : "0.0.0-dev";
4302
4662
 
4303
4663
  // src/mcp.ts
4304
4664
  function createKbMcpServer() {
@@ -4450,10 +4810,12 @@ function usage() {
4450
4810
  DEFAULT_LOAD_BUDGET,
4451
4811
  DEFAULT_PACK_HOPS,
4452
4812
  DEFAULT_PACK_MAX_NODES,
4813
+ DEFAULT_TYPED_LINK_RELS,
4453
4814
  DEFAULT_UNVERIFIED_DAYS,
4454
4815
  ErrorTypes,
4455
4816
  Fault,
4456
4817
  INDEX_FILE,
4818
+ KB_CAUSAL_LINK_RELS,
4457
4819
  KB_COMMANDS,
4458
4820
  KB_COMMANDS_BY_NAME,
4459
4821
  KB_CONCEPT_ID_PATTERN,
@@ -4461,6 +4823,7 @@ function usage() {
4461
4823
  KB_DIR,
4462
4824
  KB_DOCTOR_CHECKS,
4463
4825
  KB_EDGE_KINDS,
4826
+ KB_LINK_RELS,
4464
4827
  KB_MATERIALITIES,
4465
4828
  KB_RECORD_STATUSES,
4466
4829
  KB_RECORD_TYPES,
@@ -4474,7 +4837,9 @@ function usage() {
4474
4837
  KbRecordNotFoundError,
4475
4838
  KbSelfVerificationError,
4476
4839
  KbStore,
4840
+ KbUnknownLinkRelError,
4477
4841
  KbWriteConflictError,
4842
+ LINK_RELS,
4478
4843
  LOG_FILE,
4479
4844
  NO_DECISION_SLUG,
4480
4845
  PINS_FILE,
@@ -4486,10 +4851,12 @@ function usage() {
4486
4851
  adjudicate,
4487
4852
  anchorFilePath,
4488
4853
  assertBaseNotFrozen,
4854
+ backlinks,
4489
4855
  buildContext,
4490
4856
  catalog,
4491
4857
  composeDecisionRecord,
4492
4858
  composeInputSchema,
4859
+ composeLinkSchema,
4493
4860
  composeNoDecisionRecord,
4494
4861
  composeRecord,
4495
4862
  contextProfileBudgets,
@@ -4499,13 +4866,17 @@ function usage() {
4499
4866
  doctor,
4500
4867
  edgeNeighbours,
4501
4868
  hashAnchorText,
4869
+ impact,
4870
+ inboundIndex,
4502
4871
  indexIsStale,
4872
+ isKbLinkRel,
4503
4873
  isKbRecordType,
4504
4874
  isNoDecisionRecord,
4505
4875
  kbActorStampSchema,
4506
4876
  kbAnchorSchema,
4507
4877
  kbConceptIdSchema,
4508
4878
  kbJsonSchemas,
4879
+ kbLinkSchema,
4509
4880
  kbLogEntrySchema,
4510
4881
  kbRecordFrontmatterSchema,
4511
4882
  kbSourceSchema,