@saasontools/strauss-kb 0.1.5 → 0.1.7

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/mcp-main.cjs CHANGED
@@ -46,6 +46,11 @@ var kbActorStampSchema = import_zod.z.object({
46
46
  by: import_zod.z.string().min(1),
47
47
  at: import_zod.z.string().min(1)
48
48
  }).passthrough();
49
+ var kbVerifiedEventSchema = kbActorStampSchema.extend({
50
+ note: import_zod.z.string().refine((s) => s.trim().length > 0, {
51
+ message: "note must say what the check found"
52
+ })
53
+ });
49
54
  var kbAnchorSchema = import_zod.z.object({
50
55
  file: import_zod.z.string().min(1),
51
56
  symbol: import_zod.z.string().min(1).optional()
@@ -1152,23 +1157,123 @@ var noDecisionCommand = define({
1152
1157
  }
1153
1158
  });
1154
1159
 
1155
- // src/commands/pin.ts
1160
+ // src/commands/pack.ts
1156
1161
  var import_zod12 = require("zod");
1162
+ var packCommand = define({
1163
+ name: "pack",
1164
+ tool: "kb_pack",
1165
+ usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
1166
+ 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.",
1167
+ input: import_zod12.z.object({
1168
+ bundlePath,
1169
+ conceptId,
1170
+ hops: import_zod12.z.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
1171
+ maxNodes: import_zod12.z.number().int().positive().optional().describe(
1172
+ "How many records the pack may hold, root included. Defaults to 20."
1173
+ ),
1174
+ budgetTokens: import_zod12.z.number().int().positive().optional().describe(
1175
+ "Approximate token ceiling over what is actually emitted. Defaults to 25000."
1176
+ )
1177
+ }),
1178
+ fromArgv: (argv, path) => {
1179
+ const hops = argvFlag(argv, "--hops");
1180
+ const maxNodes = argvFlag(argv, "--max-nodes");
1181
+ const budget = argvFlag(argv, "--budget");
1182
+ return {
1183
+ bundlePath: path,
1184
+ conceptId: argv[1],
1185
+ ...hops ? { hops: Number(hops) } : {},
1186
+ ...maxNodes ? { maxNodes: Number(maxNodes) } : {},
1187
+ ...budget ? { budgetTokens: Number(budget) } : {}
1188
+ };
1189
+ },
1190
+ run: async ({ store, now }, { bundlePath: path, conceptId: root, hops, maxNodes, budgetTokens }) => {
1191
+ const result = await store.pack(path, root, {
1192
+ ...hops !== void 0 ? { hops } : {},
1193
+ ...maxNodes !== void 0 ? { maxNodes } : {},
1194
+ ...budgetTokens !== void 0 ? { budgetTokens } : {}
1195
+ });
1196
+ return render(result, path, now());
1197
+ }
1198
+ });
1199
+ function render(result, bundle, at) {
1200
+ const lines = [
1201
+ `# KB Pack \u2014 ${result.root}`,
1202
+ `bundle: ${bundle}`,
1203
+ `budget: ~${result.tokensLoaded} of ${result.budgetTokens} tokens, ${result.recordCount} records`,
1204
+ `packed: ${at}`,
1205
+ "",
1206
+ `## Records (${result.records.length})`
1207
+ ];
1208
+ for (const record of result.records) {
1209
+ lines.push(
1210
+ "",
1211
+ `### ${record.conceptId}${record.title ? ` \u2014 ${record.title}` : ""} [${record.standing}]`
1212
+ );
1213
+ if (record.warnings.length) {
1214
+ lines.push(`warnings: ${record.warnings.map(warningLabel).join("; ")}`);
1215
+ }
1216
+ if (record.anchors.length) {
1217
+ lines.push(
1218
+ `anchors: ${record.anchors.map(
1219
+ (anchor) => anchor.symbol ? `${anchor.file}#${anchor.symbol}` : anchor.file
1220
+ ).join(", ")}`
1221
+ );
1222
+ }
1223
+ lines.push("", record.body.trimEnd());
1224
+ }
1225
+ if (result.superseded.length) {
1226
+ lines.push("", `## Superseded (${result.superseded.length})`);
1227
+ for (const entry of result.superseded) {
1228
+ lines.push(
1229
+ `- ${entry.conceptId} \u2192 ${entry.supersededBy.join(", ") || "(no surviving head)"}${entry.at ? ` (${entry.at})` : ""}`
1230
+ );
1231
+ }
1232
+ }
1233
+ if (result.excluded.length) {
1234
+ lines.push("", `## Excluded (${result.excluded.length})`);
1235
+ for (const cut of result.excluded) lines.push(`- ${cut}`);
1236
+ }
1237
+ return lines.join("\n");
1238
+ }
1239
+ function warningLabel(warning) {
1240
+ switch (warning.kind) {
1241
+ case "superseded":
1242
+ return `superseded by ${warning.by.join(", ")}`;
1243
+ case "unsettled":
1244
+ return `unsettled (${warning.status})`;
1245
+ case "broken-chain":
1246
+ return `broken chain \u2014 ${warning.missing} is not in the bundle`;
1247
+ case "chain-cycle":
1248
+ return `chain cycle through ${warning.through.join(" \u2192 ")}`;
1249
+ case "forked-chain":
1250
+ return `forked chain \u2014 heads ${warning.heads.join(", ")}`;
1251
+ case "stale":
1252
+ return `stale since ${warning.staleAfter}`;
1253
+ case "unresolved-question":
1254
+ return "unresolved question";
1255
+ default:
1256
+ return warning.kind;
1257
+ }
1258
+ }
1259
+
1260
+ // src/commands/pin.ts
1261
+ var import_zod13 = require("zod");
1157
1262
  var pinCommand = define({
1158
1263
  name: "pin",
1159
1264
  tool: "kb_pin",
1160
1265
  usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
1161
1266
  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.",
1162
- input: import_zod12.z.object({
1267
+ input: import_zod13.z.object({
1163
1268
  bundlePath,
1164
- mode: import_zod12.z.enum(["full", "index"]).optional().describe(
1269
+ mode: import_zod13.z.enum(["full", "index"]).optional().describe(
1165
1270
  "full: always emit this base's records whole (still under the block budget); index: never upgrade. Absent: the profile's full-under threshold decides."
1166
1271
  ),
1167
- profiles: import_zod12.z.array(import_zod12.z.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
1168
- layer: import_zod12.z.enum(["project", "local", "user"]).optional().describe(
1272
+ profiles: import_zod13.z.array(import_zod13.z.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
1273
+ layer: import_zod13.z.enum(["project", "local", "user"]).optional().describe(
1169
1274
  "Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
1170
1275
  ),
1171
- frozen: import_zod12.z.boolean().optional().describe(
1276
+ frozen: import_zod13.z.boolean().optional().describe(
1172
1277
  "true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
1173
1278
  )
1174
1279
  }),
@@ -1197,29 +1302,29 @@ var pinCommand = define({
1197
1302
  });
1198
1303
 
1199
1304
  // src/commands/pins.ts
1200
- var import_zod13 = require("zod");
1305
+ var import_zod14 = require("zod");
1201
1306
  var pinsCommand = define({
1202
1307
  name: "pins",
1203
1308
  tool: "kb_pins",
1204
1309
  usage: "pins",
1205
1310
  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.",
1206
- input: import_zod13.z.object({}),
1311
+ input: import_zod14.z.object({}),
1207
1312
  fromArgv: () => ({}),
1208
1313
  run: ({ store }) => listPins(store, process.cwd())
1209
1314
  });
1210
1315
 
1211
1316
  // src/commands/query.ts
1212
- var import_zod14 = require("zod");
1317
+ var import_zod15 = require("zod");
1213
1318
  var queryCommand = define({
1214
1319
  name: "query",
1215
1320
  tool: "kb_query",
1216
1321
  usage: "query <text...>",
1217
1322
  description: "Search and return each match with its standing. Results are flagged, never filtered: a superseded record comes back alongside whatever replaced it, and a rejected one is marked as something explicitly not adopted. Prefer kb_load when the base fits its budget: on this package's measurements, a reader holding the whole base answered eight of nine questions whose wording appears in no record, where embedding search answered four. Never read record files directly \u2014 this tool (with kb_load and kb_trace) is the only supported way to read a base; a file read bypasses supersession resolution and returns replaced records as if current.",
1218
- input: import_zod14.z.object({
1323
+ input: import_zod15.z.object({
1219
1324
  bundlePath,
1220
- text: import_zod14.z.string().optional(),
1221
- type: import_zod14.z.enum(KB_RECORD_TYPES).optional(),
1222
- includeNonCurrent: import_zod14.z.boolean().optional()
1325
+ text: import_zod15.z.string().optional(),
1326
+ type: import_zod15.z.enum(KB_RECORD_TYPES).optional(),
1327
+ includeNonCurrent: import_zod15.z.boolean().optional()
1223
1328
  }),
1224
1329
  fromArgv: (argv, path) => ({
1225
1330
  bundlePath: path,
@@ -1241,33 +1346,33 @@ var queryCommand = define({
1241
1346
  });
1242
1347
 
1243
1348
  // src/commands/read-index.ts
1244
- var import_zod15 = require("zod");
1349
+ var import_zod16 = require("zod");
1245
1350
  var readIndexCommand = define({
1246
1351
  name: "index",
1247
1352
  tool: "kb_index",
1248
1353
  usage: "index",
1249
1354
  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.",
1250
- input: import_zod15.z.object({ bundlePath }),
1355
+ input: import_zod16.z.object({ bundlePath }),
1251
1356
  fromArgv: (_argv, path) => ({ bundlePath: path }),
1252
1357
  run: ({ store }, { bundlePath: path }) => store.readIndex(path)
1253
1358
  });
1254
1359
 
1255
1360
  // src/commands/schema.ts
1256
- var import_zod18 = require("zod");
1361
+ var import_zod19 = require("zod");
1257
1362
 
1258
1363
  // src/json-schema.ts
1259
- var import_zod17 = require("zod");
1364
+ var import_zod18 = require("zod");
1260
1365
 
1261
1366
  // src/kb-log.ts
1262
- var import_zod16 = require("zod");
1367
+ var import_zod17 = require("zod");
1263
1368
  var LOG_FILE = "log.jsonl";
1264
- var kbLogEntrySchema = import_zod16.z.object({
1265
- at: import_zod16.z.string().min(1),
1266
- by: import_zod16.z.string().min(1),
1267
- operation: import_zod16.z.string().min(1),
1268
- conceptId: import_zod16.z.string().min(1),
1369
+ var kbLogEntrySchema = import_zod17.z.object({
1370
+ at: import_zod17.z.string().min(1),
1371
+ by: import_zod17.z.string().min(1),
1372
+ operation: import_zod17.z.string().min(1),
1373
+ conceptId: import_zod17.z.string().min(1),
1269
1374
  /** Second concept id, where the operation relates two — supersession. */
1270
- target: import_zod16.z.string().min(1).optional()
1375
+ target: import_zod17.z.string().min(1).optional()
1271
1376
  }).strict();
1272
1377
  function renderLogEntry(entry) {
1273
1378
  return `${JSON.stringify(kbLogEntrySchema.parse(entry))}
@@ -1298,11 +1403,11 @@ function parseLog(raw) {
1298
1403
  // src/json-schema.ts
1299
1404
  function kbJsonSchemas() {
1300
1405
  return {
1301
- recordFrontmatter: import_zod17.z.toJSONSchema(kbRecordFrontmatterSchema, {
1406
+ recordFrontmatter: import_zod18.z.toJSONSchema(kbRecordFrontmatterSchema, {
1302
1407
  io: "input"
1303
1408
  }),
1304
- composeInput: import_zod17.z.toJSONSchema(composeInputSchema, { io: "input" }),
1305
- logEntry: import_zod17.z.toJSONSchema(kbLogEntrySchema, { io: "input" })
1409
+ composeInput: import_zod18.z.toJSONSchema(composeInputSchema, { io: "input" }),
1410
+ logEntry: import_zod18.z.toJSONSchema(kbLogEntrySchema, { io: "input" })
1306
1411
  };
1307
1412
  }
1308
1413
 
@@ -1312,22 +1417,22 @@ var schemaCommand = define({
1312
1417
  tool: "kb_schema",
1313
1418
  usage: "schema",
1314
1419
  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.",
1315
- input: import_zod18.z.object({}),
1420
+ input: import_zod19.z.object({}),
1316
1421
  fromArgv: () => ({}),
1317
1422
  run: () => Promise.resolve(kbJsonSchemas())
1318
1423
  });
1319
1424
 
1320
1425
  // src/commands/status.ts
1321
- var import_zod19 = require("zod");
1426
+ var import_zod20 = require("zod");
1322
1427
  var statusCommand = define({
1323
1428
  name: "status",
1324
1429
  tool: "kb_status",
1325
1430
  usage: "status <concept-id> <status>",
1326
1431
  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.",
1327
- input: import_zod19.z.object({
1432
+ input: import_zod20.z.object({
1328
1433
  bundlePath,
1329
1434
  conceptId,
1330
- status: import_zod19.z.enum(KB_RECORD_STATUSES)
1435
+ status: import_zod20.z.enum(KB_RECORD_STATUSES)
1331
1436
  }),
1332
1437
  fromArgv: (argv, path) => ({
1333
1438
  bundlePath: path,
@@ -1342,13 +1447,13 @@ var statusCommand = define({
1342
1447
  });
1343
1448
 
1344
1449
  // src/commands/supersede.ts
1345
- var import_zod20 = require("zod");
1450
+ var import_zod21 = require("zod");
1346
1451
  var supersedeCommand = define({
1347
1452
  name: "supersede",
1348
1453
  tool: "kb_supersede",
1349
1454
  usage: "supersede <concept-id> <replacement-id>",
1350
1455
  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.",
1351
- input: import_zod20.z.object({ bundlePath, conceptId, replacementId: conceptId }),
1456
+ input: import_zod21.z.object({ bundlePath, conceptId, replacementId: conceptId }),
1352
1457
  fromArgv: (argv, path) => ({
1353
1458
  bundlePath: path,
1354
1459
  conceptId: argv[1],
@@ -1362,16 +1467,16 @@ var supersedeCommand = define({
1362
1467
  });
1363
1468
 
1364
1469
  // src/commands/sync-instructions.ts
1365
- var import_zod21 = require("zod");
1470
+ var import_zod22 = require("zod");
1366
1471
  var syncInstructionsCommand = define({
1367
1472
  name: "sync-instructions",
1368
1473
  usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
1369
1474
  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.",
1370
- input: import_zod21.z.object({
1371
- file: import_zod21.z.string().min(1).describe("The instruction file to edit in place."),
1372
- budgetTokens: import_zod21.z.number().int().positive().optional(),
1373
- fullUnderTokens: import_zod21.z.number().int().positive().optional(),
1374
- profile: import_zod21.z.string().optional()
1475
+ input: import_zod22.z.object({
1476
+ file: import_zod22.z.string().min(1).describe("The instruction file to edit in place."),
1477
+ budgetTokens: import_zod22.z.number().int().positive().optional(),
1478
+ fullUnderTokens: import_zod22.z.number().int().positive().optional(),
1479
+ profile: import_zod22.z.string().optional()
1375
1480
  }),
1376
1481
  fromArgv: (argv) => {
1377
1482
  const budget = argvFlag(argv, "--budget");
@@ -1397,43 +1502,50 @@ var syncInstructionsCommand = define({
1397
1502
  });
1398
1503
 
1399
1504
  // src/commands/trace.ts
1400
- var import_zod22 = require("zod");
1505
+ var import_zod23 = require("zod");
1401
1506
 
1402
- // src/trace.ts
1403
- var TRACE_EDGES = ["supersession", "anchor", "source"];
1404
- function trace(seedId, bundle, options = {}) {
1405
- const edges = options.edges?.length ? options.edges : TRACE_EDGES;
1406
- const maxDepth = options.depth ?? 3;
1407
- const byId = new Map(bundle.map((record) => [record.conceptId, record]));
1408
- const seed = byId.get(seedId);
1409
- if (!seed) return [];
1410
- const reached = /* @__PURE__ */ new Map([
1411
- [seedId, { record: seed, depth: 0, via: [] }]
1412
- ]);
1413
- let frontier = [seed];
1414
- for (let depth = 1; depth <= maxDepth && frontier.length; depth += 1) {
1415
- const next = [];
1416
- for (const from of frontier) {
1417
- for (const edge of edges) {
1418
- for (const record of neighbours(from, bundle, edge)) {
1419
- const existing = reached.get(record.conceptId);
1420
- if (existing) {
1421
- if (existing.depth > 0 && !existing.via.includes(edge)) {
1422
- existing.via.push(edge);
1423
- }
1424
- continue;
1425
- }
1426
- reached.set(record.conceptId, { record, depth, via: [edge] });
1427
- next.push(record);
1428
- }
1507
+ // src/kb-edges.ts
1508
+ var KB_EDGE_KINDS = [
1509
+ "body-link",
1510
+ "supersession",
1511
+ "anchor",
1512
+ "source"
1513
+ ];
1514
+ var BODY_LINK_TARGET = new RegExp(
1515
+ `\\]\\((${KB_CONCEPT_ID_PATTERN.source.replace(/^\^|\$$/g, "")})\\.md\\)`,
1516
+ "g"
1517
+ );
1518
+ function neighbours(from, bundle, kinds = KB_EDGE_KINDS) {
1519
+ const found = /* @__PURE__ */ new Map();
1520
+ for (const kind of kinds) {
1521
+ for (const record of edgeNeighbours(from, bundle, kind)) {
1522
+ const existing = found.get(record.conceptId);
1523
+ if (existing) {
1524
+ if (!existing.via.includes(kind)) existing.via.push(kind);
1525
+ continue;
1429
1526
  }
1527
+ found.set(record.conceptId, { record, via: [kind] });
1430
1528
  }
1431
- frontier = next;
1432
1529
  }
1433
- return [...reached.values()].sort(byGeneratedAt);
1530
+ return [...found.values()];
1434
1531
  }
1435
- function neighbours(from, bundle, edge) {
1436
- switch (edge) {
1532
+ function edgeNeighbours(from, bundle, kind) {
1533
+ switch (kind) {
1534
+ // A link whose target is not in the bundle is legal per compose.ts —
1535
+ // records are routinely written before the ones they point at exist — so
1536
+ // missing targets are skipped, never an error.
1537
+ case "body-link": {
1538
+ const targets = new Set(
1539
+ [...from.body.matchAll(BODY_LINK_TARGET)].map((match) => match[1])
1540
+ );
1541
+ if (!targets.size) return [];
1542
+ return bundle.filter(
1543
+ (candidate) => candidate.conceptId !== from.conceptId && targets.has(candidate.conceptId)
1544
+ );
1545
+ }
1546
+ // Both directions and both pointers: `supersede()` writes the pair, but a
1547
+ // hand-edit can leave one side behind, and a walk trusting one pointer
1548
+ // would miss a replacement the bundle openly declares.
1437
1549
  case "supersession":
1438
1550
  return bundle.filter(
1439
1551
  (candidate) => candidate.conceptId !== from.conceptId && (candidate.conceptId === from.frontmatter.strauss_superseded_by || from.frontmatter.strauss_supersedes?.includes(
@@ -1467,6 +1579,40 @@ function anchorsTouch(left, right) {
1467
1579
  if (!left.symbol || !right.symbol) return true;
1468
1580
  return left.symbol === right.symbol;
1469
1581
  }
1582
+
1583
+ // src/trace.ts
1584
+ var TRACE_EDGES = ["supersession", "anchor", "source"];
1585
+ function trace(seedId, bundle, options = {}) {
1586
+ const edges = options.edges?.length ? options.edges : TRACE_EDGES;
1587
+ const maxDepth = options.depth ?? 3;
1588
+ const byId = new Map(bundle.map((record) => [record.conceptId, record]));
1589
+ const seed = byId.get(seedId);
1590
+ if (!seed) return [];
1591
+ const reached = /* @__PURE__ */ new Map([
1592
+ [seedId, { record: seed, depth: 0, via: [] }]
1593
+ ]);
1594
+ let frontier = [seed];
1595
+ for (let depth = 1; depth <= maxDepth && frontier.length; depth += 1) {
1596
+ const next = [];
1597
+ for (const from of frontier) {
1598
+ for (const edge of edges) {
1599
+ for (const record of edgeNeighbours(from, bundle, edge)) {
1600
+ const existing = reached.get(record.conceptId);
1601
+ if (existing) {
1602
+ if (existing.depth > 0 && !existing.via.includes(edge)) {
1603
+ existing.via.push(edge);
1604
+ }
1605
+ continue;
1606
+ }
1607
+ reached.set(record.conceptId, { record, depth, via: [edge] });
1608
+ next.push(record);
1609
+ }
1610
+ }
1611
+ }
1612
+ frontier = next;
1613
+ }
1614
+ return [...reached.values()].sort(byGeneratedAt);
1615
+ }
1470
1616
  function byGeneratedAt(left, right) {
1471
1617
  const at = (step) => step.record.frontmatter.generated?.at ?? "";
1472
1618
  return at(left).localeCompare(at(right)) || left.depth - right.depth;
@@ -1478,11 +1624,11 @@ var traceCommand = define({
1478
1624
  tool: "kb_trace",
1479
1625
  usage: "trace <concept-id> [edges...]",
1480
1626
  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.',
1481
- input: import_zod22.z.object({
1627
+ input: import_zod23.z.object({
1482
1628
  bundlePath,
1483
1629
  conceptId,
1484
- edges: import_zod22.z.array(import_zod22.z.enum(TRACE_EDGES)).optional(),
1485
- depth: import_zod22.z.number().int().positive().optional()
1630
+ edges: import_zod23.z.array(import_zod23.z.enum(TRACE_EDGES)).optional(),
1631
+ depth: import_zod23.z.number().int().positive().optional()
1486
1632
  }),
1487
1633
  fromArgv: (argv, path) => ({
1488
1634
  bundlePath: path,
@@ -1504,31 +1650,31 @@ var traceCommand = define({
1504
1650
  });
1505
1651
 
1506
1652
  // src/commands/types.ts
1507
- var import_zod23 = require("zod");
1653
+ var import_zod24 = require("zod");
1508
1654
  var typesCommand = define({
1509
1655
  name: "types",
1510
1656
  tool: "kb_types",
1511
1657
  usage: "types",
1512
1658
  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.",
1513
- input: import_zod23.z.object({}),
1659
+ input: import_zod24.z.object({}),
1514
1660
  fromArgv: () => ({}),
1515
1661
  run: () => Promise.resolve(RECORD_TYPES)
1516
1662
  });
1517
1663
 
1518
1664
  // src/commands/unpin.ts
1519
- var import_zod24 = require("zod");
1665
+ var import_zod25 = require("zod");
1520
1666
  var unpinCommand = define({
1521
1667
  name: "unpin",
1522
1668
  tool: "kb_unpin",
1523
1669
  usage: "unpin [bundle-path]",
1524
1670
  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.",
1525
- input: import_zod24.z.object({ bundlePath }),
1671
+ input: import_zod25.z.object({ bundlePath }),
1526
1672
  fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
1527
1673
  run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
1528
1674
  });
1529
1675
 
1530
1676
  // src/commands/validate.ts
1531
- var import_zod25 = require("zod");
1677
+ var import_zod26 = require("zod");
1532
1678
 
1533
1679
  // src/validate.ts
1534
1680
  function validateBundle(records) {
@@ -1571,14 +1717,43 @@ var validateCommand = define({
1571
1717
  tool: "kb_validate",
1572
1718
  usage: "validate",
1573
1719
  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.",
1574
- input: import_zod25.z.object({ bundlePath }),
1720
+ input: import_zod26.z.object({ bundlePath }),
1575
1721
  fromArgv: (_argv, path) => ({ bundlePath: path }),
1576
1722
  run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
1577
1723
  failsWhen: (result) => Array.isArray(result) && result.length > 0
1578
1724
  });
1579
1725
 
1726
+ // src/commands/verify.ts
1727
+ var import_zod27 = require("zod");
1728
+ var verifyCommand = define({
1729
+ name: "verify",
1730
+ tool: "kb_verify",
1731
+ usage: "verify <concept-id> --note <text>",
1732
+ 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.",
1733
+ input: import_zod27.z.object({
1734
+ bundlePath,
1735
+ conceptId,
1736
+ note: import_zod27.z.string().refine((s) => s.trim().length > 0, {
1737
+ message: "note must say what the check found"
1738
+ })
1739
+ }),
1740
+ fromArgv: (argv, path) => ({
1741
+ bundlePath: path,
1742
+ conceptId: argv[1],
1743
+ note: argvFlag(argv, "--note")
1744
+ }),
1745
+ run: async ({ store, actor, now }, { bundlePath: path, conceptId: id, note }) => {
1746
+ await assertBaseNotFrozen(process.cwd(), path);
1747
+ const record = await store.verify(path, id, note, actor, now());
1748
+ return {
1749
+ conceptId: record.conceptId,
1750
+ verified: record.frontmatter.verified?.length ?? 0
1751
+ };
1752
+ }
1753
+ });
1754
+
1580
1755
  // src/commands/write.ts
1581
- var import_zod26 = require("zod");
1756
+ var import_zod28 = require("zod");
1582
1757
  var writeCommand = define({
1583
1758
  name: "write",
1584
1759
  tool: "kb_write",
@@ -1592,9 +1767,9 @@ var writeCommand = define({
1592
1767
  "- Prefer a new record over overloading an existing one, and keep each short. A record nobody finishes reading is not durable memory.",
1593
1768
  "- Records are never deleted; supersede instead, so the earlier reasoning stays inspectable."
1594
1769
  ].join("\n"),
1595
- input: import_zod26.z.object({
1770
+ input: import_zod28.z.object({
1596
1771
  bundlePath,
1597
- type: import_zod26.z.enum(KB_RECORD_TYPES),
1772
+ type: import_zod28.z.enum(KB_RECORD_TYPES),
1598
1773
  input: composeInputSchema
1599
1774
  }),
1600
1775
  fromArgv: async (argv, path, stdin) => ({
@@ -1618,7 +1793,7 @@ var writeCommand = define({
1618
1793
  });
1619
1794
 
1620
1795
  // src/commands/write-decision.ts
1621
- var import_zod27 = require("zod");
1796
+ var import_zod29 = require("zod");
1622
1797
  var writeDecisionCommand = define({
1623
1798
  name: "write-decision",
1624
1799
  tool: "kb_write_decision",
@@ -1631,7 +1806,7 @@ var writeDecisionCommand = define({
1631
1806
  "- `alternative` is what you turned down and why, not a list of everything considered.",
1632
1807
  "- A reference to material you read goes in `sources`; a reference to code goes in `anchors`; a reference to another record goes in `relatedConceptIds`."
1633
1808
  ].join("\n"),
1634
- input: import_zod27.z.object({ bundlePath, input: decisionInputSchema }),
1809
+ input: import_zod29.z.object({ bundlePath, input: decisionInputSchema }),
1635
1810
  fromArgv: async (_argv, path, stdin) => ({
1636
1811
  bundlePath: path,
1637
1812
  input: JSON.parse(await stdin())
@@ -1659,7 +1834,9 @@ var KB_COMMANDS = [
1659
1834
  statusCommand,
1660
1835
  supersedeCommand,
1661
1836
  answerCommand,
1837
+ verifyCommand,
1662
1838
  loadCommand,
1839
+ packCommand,
1663
1840
  queryCommand,
1664
1841
  traceCommand,
1665
1842
  listCommand,
@@ -1774,6 +1951,46 @@ var KbWriteConflictError = class extends BaseError {
1774
1951
  }
1775
1952
  conceptId;
1776
1953
  };
1954
+ var KbSelfVerificationError = class extends BaseError {
1955
+ constructor(conceptId2, actor, generatedBy) {
1956
+ super({
1957
+ message: `kb: ${conceptId2} was generated by ${generatedBy}, and a record's generator cannot verify it \u2014 only a human or a different actor can`,
1958
+ errorType: "KbSelfVerification" /* KbSelfVerification */,
1959
+ code: 400,
1960
+ fault: "User" /* User */,
1961
+ retriable: false,
1962
+ reportToUser: true,
1963
+ details: { conceptId: conceptId2, actor, generatedBy, action: "refused" }
1964
+ });
1965
+ this.conceptId = conceptId2;
1966
+ this.actor = actor;
1967
+ this.generatedBy = generatedBy;
1968
+ }
1969
+ conceptId;
1970
+ actor;
1971
+ generatedBy;
1972
+ };
1973
+ var KbPackBudgetExceededError = class extends BaseError {
1974
+ constructor(recordCount, approxTokens2, budgetTokens, excluded) {
1975
+ super({
1976
+ message: `kb: a pack of ${recordCount} records is ~${approxTokens2} tokens against a budget of ${budgetTokens} \u2014 lower hops or maxNodes, or raise the budget`,
1977
+ errorType: "KbPackBudgetExceeded" /* KbPackBudgetExceeded */,
1978
+ code: 400,
1979
+ fault: "User" /* User */,
1980
+ retriable: false,
1981
+ reportToUser: true,
1982
+ details: { recordCount, approxTokens: approxTokens2, budgetTokens, excluded }
1983
+ });
1984
+ this.recordCount = recordCount;
1985
+ this.approxTokens = approxTokens2;
1986
+ this.budgetTokens = budgetTokens;
1987
+ this.excluded = excluded;
1988
+ }
1989
+ recordCount;
1990
+ approxTokens;
1991
+ budgetTokens;
1992
+ excluded;
1993
+ };
1777
1994
  var KbInvalidConceptIdError = class extends BaseError {
1778
1995
  constructor(message, details) {
1779
1996
  super({
@@ -1874,6 +2091,90 @@ async function loadQmd(logger) {
1874
2091
  }
1875
2092
  }
1876
2093
 
2094
+ // src/pack.ts
2095
+ var DEFAULT_PACK_HOPS = 2;
2096
+ var DEFAULT_PACK_MAX_NODES = 20;
2097
+ var TYPE_PRIORITY = [
2098
+ "decision",
2099
+ "constraint",
2100
+ "requirement",
2101
+ ...KB_RECORD_TYPES.filter(
2102
+ (type) => !["decision", "constraint", "requirement"].includes(type)
2103
+ )
2104
+ ];
2105
+ function pack(bundle, rootId, options = {}) {
2106
+ const hops = options.hops ?? DEFAULT_PACK_HOPS;
2107
+ const maxNodes = options.maxNodes ?? DEFAULT_PACK_MAX_NODES;
2108
+ const budgetTokens = options.budgetTokens ?? DEFAULT_LOAD_BUDGET;
2109
+ const byId = new Map(bundle.map((record) => [record.conceptId, record]));
2110
+ const root = byId.get(rootId);
2111
+ if (!root) throw new KbRecordNotFoundError(rootId);
2112
+ const reached = [{ record: root, depth: 0 }];
2113
+ const seen = /* @__PURE__ */ new Set([rootId]);
2114
+ let frontier = [root];
2115
+ for (let depth = 1; frontier.length; depth += 1) {
2116
+ const next = [];
2117
+ for (const from of frontier) {
2118
+ for (const { record } of neighbours(from, bundle)) {
2119
+ if (seen.has(record.conceptId)) continue;
2120
+ seen.add(record.conceptId);
2121
+ reached.push({ record, depth });
2122
+ next.push(record);
2123
+ }
2124
+ }
2125
+ frontier = next;
2126
+ }
2127
+ reached.sort(byRank);
2128
+ const within = reached.filter((entry) => entry.depth <= hops);
2129
+ const kept = within.slice(0, maxNodes);
2130
+ const excluded = [
2131
+ ...within.slice(maxNodes),
2132
+ ...reached.filter((entry) => entry.depth > hops)
2133
+ ].map((entry) => entry.record.conceptId).sort();
2134
+ const adjudicated = adjudicate(
2135
+ kept.map((entry) => entry.record),
2136
+ bundle
2137
+ );
2138
+ const superseded = adjudicated.filter((hit) => hit.standing === "superseded").map(stub);
2139
+ const whole = adjudicated.filter((hit) => hit.standing !== "superseded");
2140
+ const tokensLoaded = whole.reduce((total, hit) => total + estimateTokens(hit.record), 0) + superseded.reduce((total, entry) => total + estimateStubTokens(entry), 0);
2141
+ const recordCount = adjudicated.length;
2142
+ if (tokensLoaded > budgetTokens) {
2143
+ throw new KbPackBudgetExceededError(
2144
+ recordCount,
2145
+ tokensLoaded,
2146
+ budgetTokens,
2147
+ excluded
2148
+ );
2149
+ }
2150
+ return {
2151
+ root: rootId,
2152
+ records: whole.map((hit) => ({
2153
+ conceptId: hit.record.conceptId,
2154
+ title: hit.record.frontmatter.title ?? null,
2155
+ standing: hit.standing,
2156
+ supersededBy: hit.heads.map((head) => head.conceptId),
2157
+ warnings: hit.warnings,
2158
+ anchors: hit.record.frontmatter.strauss_anchors ?? [],
2159
+ body: hit.record.body
2160
+ })),
2161
+ superseded,
2162
+ excluded,
2163
+ recordCount,
2164
+ tokensLoaded,
2165
+ budgetTokens
2166
+ };
2167
+ }
2168
+ function byRank(left, right) {
2169
+ return left.depth - right.depth || typeRank(left.record) - typeRank(right.record) || (left.record.frontmatter.title ?? "").localeCompare(
2170
+ right.record.frontmatter.title ?? ""
2171
+ ) || left.record.conceptId.localeCompare(right.record.conceptId);
2172
+ }
2173
+ function typeRank(record) {
2174
+ const index = TYPE_PRIORITY.indexOf(record.frontmatter.type);
2175
+ return index === -1 ? TYPE_PRIORITY.length : index;
2176
+ }
2177
+
1877
2178
  // src/kb-store.ts
1878
2179
  var KB_DIR = (0, import_node_path6.join)(".strauss", "kb");
1879
2180
  var STORE_OWNED = /* @__PURE__ */ new Set([INDEX_FILE, LOG_FILE, SEARCH_INDEX_FILE]);
@@ -1992,6 +2293,40 @@ var KbStore = class {
1992
2293
  { operation: `status:${status}`, by: actor }
1993
2294
  );
1994
2295
  }
2296
+ /**
2297
+ * Appends one `verified[]` event: who checked the record, when, and what the
2298
+ * check found. Append-only — prior events are history, and are spread into
2299
+ * the new array untouched rather than reshaped through the write schema.
2300
+ *
2301
+ * A record's generator cannot verify its own record unless the actor is
2302
+ * human: the generator re-reading its own output is not an independent
2303
+ * check. The rule runs before the mutation so a refusal never publishes,
2304
+ * and the refusal is logged under its own operation name — `mutate` only
2305
+ * logs what it publishes.
2306
+ */
2307
+ async verify(bundlePath2, conceptId2, note, actor = "unknown", at = (/* @__PURE__ */ new Date()).toISOString()) {
2308
+ const event = kbVerifiedEventSchema.parse({ by: actor, at, note });
2309
+ const existing = await this.read(bundlePath2, conceptId2);
2310
+ if (!existing) throw new KbRecordNotFoundError(conceptId2);
2311
+ const generatedBy = existing.frontmatter.generated?.by;
2312
+ if (generatedBy !== void 0 && actor.toLowerCase() === generatedBy.toLowerCase() && !normalizeActor(actor).startsWith("human:")) {
2313
+ await this.record(this.root(bundlePath2), {
2314
+ operation: "verify:refused",
2315
+ conceptId: conceptId2,
2316
+ by: actor
2317
+ });
2318
+ throw new KbSelfVerificationError(conceptId2, actor, generatedBy);
2319
+ }
2320
+ return this.mutate(
2321
+ bundlePath2,
2322
+ conceptId2,
2323
+ (frontmatter) => ({
2324
+ ...frontmatter,
2325
+ verified: [...frontmatter.verified ?? [], event]
2326
+ }),
2327
+ { operation: "verify", by: actor }
2328
+ );
2329
+ }
1995
2330
  /**
1996
2331
  * Marks `conceptId` superseded by `replacementId`, and links both directions.
1997
2332
  *
@@ -2129,6 +2464,10 @@ ${answer}
2129
2464
  async trace(bundlePath2, seedId, options = {}) {
2130
2465
  return trace(seedId, await this.list(bundlePath2), options);
2131
2466
  }
2467
+ /** A bounded neighbourhood around one record. See `pack.ts`. */
2468
+ async pack(bundlePath2, rootId, options = {}) {
2469
+ return pack(await this.list(bundlePath2), rootId, options);
2470
+ }
2132
2471
  /**
2133
2472
  * The stored index, rebuilt if it disagrees with the records.
2134
2473
  *
@@ -2326,13 +2665,21 @@ function matches(record, needle) {
2326
2665
  (field) => field?.toLowerCase().includes(needle)
2327
2666
  );
2328
2667
  }
2668
+ function normalizeActor(id) {
2669
+ const colon = id.indexOf(":");
2670
+ if (colon === -1) return id.toLowerCase();
2671
+ return id.slice(0, colon + 1).toLowerCase() + id.slice(colon + 1);
2672
+ }
2329
2673
  function digest(contents) {
2330
2674
  return (0, import_node_crypto.createHash)("sha256").update(contents).digest("hex");
2331
2675
  }
2332
2676
 
2677
+ // src/version.ts
2678
+ var VERSION = true ? "0.1.7" : "0.0.0-dev";
2679
+
2333
2680
  // src/mcp.ts
2334
2681
  function createKbMcpServer() {
2335
- const server = new import_mcp.McpServer({ name: "strauss-kb", version: "0.1.0" });
2682
+ const server = new import_mcp.McpServer({ name: "strauss-kb", version: VERSION });
2336
2683
  const store = new KbStore({
2337
2684
  warn: (entry) => process.stderr.write(`${JSON.stringify(entry)}
2338
2685
  `)