@saasontools/strauss-kb 0.1.6 → 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/cli-main.cjs CHANGED
@@ -1156,23 +1156,123 @@ var noDecisionCommand = define({
1156
1156
  }
1157
1157
  });
1158
1158
 
1159
- // src/commands/pin.ts
1159
+ // src/commands/pack.ts
1160
1160
  var import_zod12 = require("zod");
1161
+ var packCommand = define({
1162
+ name: "pack",
1163
+ tool: "kb_pack",
1164
+ usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
1165
+ 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.",
1166
+ input: import_zod12.z.object({
1167
+ bundlePath,
1168
+ conceptId,
1169
+ hops: import_zod12.z.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
1170
+ maxNodes: import_zod12.z.number().int().positive().optional().describe(
1171
+ "How many records the pack may hold, root included. Defaults to 20."
1172
+ ),
1173
+ budgetTokens: import_zod12.z.number().int().positive().optional().describe(
1174
+ "Approximate token ceiling over what is actually emitted. Defaults to 25000."
1175
+ )
1176
+ }),
1177
+ fromArgv: (argv, path) => {
1178
+ const hops = argvFlag(argv, "--hops");
1179
+ const maxNodes = argvFlag(argv, "--max-nodes");
1180
+ const budget = argvFlag(argv, "--budget");
1181
+ return {
1182
+ bundlePath: path,
1183
+ conceptId: argv[1],
1184
+ ...hops ? { hops: Number(hops) } : {},
1185
+ ...maxNodes ? { maxNodes: Number(maxNodes) } : {},
1186
+ ...budget ? { budgetTokens: Number(budget) } : {}
1187
+ };
1188
+ },
1189
+ run: async ({ store, now }, { bundlePath: path, conceptId: root, hops, maxNodes, budgetTokens }) => {
1190
+ const result = await store.pack(path, root, {
1191
+ ...hops !== void 0 ? { hops } : {},
1192
+ ...maxNodes !== void 0 ? { maxNodes } : {},
1193
+ ...budgetTokens !== void 0 ? { budgetTokens } : {}
1194
+ });
1195
+ return render(result, path, now());
1196
+ }
1197
+ });
1198
+ function render(result, bundle, at) {
1199
+ const lines = [
1200
+ `# KB Pack \u2014 ${result.root}`,
1201
+ `bundle: ${bundle}`,
1202
+ `budget: ~${result.tokensLoaded} of ${result.budgetTokens} tokens, ${result.recordCount} records`,
1203
+ `packed: ${at}`,
1204
+ "",
1205
+ `## Records (${result.records.length})`
1206
+ ];
1207
+ for (const record of result.records) {
1208
+ lines.push(
1209
+ "",
1210
+ `### ${record.conceptId}${record.title ? ` \u2014 ${record.title}` : ""} [${record.standing}]`
1211
+ );
1212
+ if (record.warnings.length) {
1213
+ lines.push(`warnings: ${record.warnings.map(warningLabel).join("; ")}`);
1214
+ }
1215
+ if (record.anchors.length) {
1216
+ lines.push(
1217
+ `anchors: ${record.anchors.map(
1218
+ (anchor) => anchor.symbol ? `${anchor.file}#${anchor.symbol}` : anchor.file
1219
+ ).join(", ")}`
1220
+ );
1221
+ }
1222
+ lines.push("", record.body.trimEnd());
1223
+ }
1224
+ if (result.superseded.length) {
1225
+ lines.push("", `## Superseded (${result.superseded.length})`);
1226
+ for (const entry of result.superseded) {
1227
+ lines.push(
1228
+ `- ${entry.conceptId} \u2192 ${entry.supersededBy.join(", ") || "(no surviving head)"}${entry.at ? ` (${entry.at})` : ""}`
1229
+ );
1230
+ }
1231
+ }
1232
+ if (result.excluded.length) {
1233
+ lines.push("", `## Excluded (${result.excluded.length})`);
1234
+ for (const cut of result.excluded) lines.push(`- ${cut}`);
1235
+ }
1236
+ return lines.join("\n");
1237
+ }
1238
+ function warningLabel(warning) {
1239
+ switch (warning.kind) {
1240
+ case "superseded":
1241
+ return `superseded by ${warning.by.join(", ")}`;
1242
+ case "unsettled":
1243
+ return `unsettled (${warning.status})`;
1244
+ case "broken-chain":
1245
+ return `broken chain \u2014 ${warning.missing} is not in the bundle`;
1246
+ case "chain-cycle":
1247
+ return `chain cycle through ${warning.through.join(" \u2192 ")}`;
1248
+ case "forked-chain":
1249
+ return `forked chain \u2014 heads ${warning.heads.join(", ")}`;
1250
+ case "stale":
1251
+ return `stale since ${warning.staleAfter}`;
1252
+ case "unresolved-question":
1253
+ return "unresolved question";
1254
+ default:
1255
+ return warning.kind;
1256
+ }
1257
+ }
1258
+
1259
+ // src/commands/pin.ts
1260
+ var import_zod13 = require("zod");
1161
1261
  var pinCommand = define({
1162
1262
  name: "pin",
1163
1263
  tool: "kb_pin",
1164
1264
  usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
1165
1265
  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.",
1166
- input: import_zod12.z.object({
1266
+ input: import_zod13.z.object({
1167
1267
  bundlePath,
1168
- mode: import_zod12.z.enum(["full", "index"]).optional().describe(
1268
+ mode: import_zod13.z.enum(["full", "index"]).optional().describe(
1169
1269
  "full: always emit this base's records whole (still under the block budget); index: never upgrade. Absent: the profile's full-under threshold decides."
1170
1270
  ),
1171
- profiles: import_zod12.z.array(import_zod12.z.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
1172
- layer: import_zod12.z.enum(["project", "local", "user"]).optional().describe(
1271
+ profiles: import_zod13.z.array(import_zod13.z.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
1272
+ layer: import_zod13.z.enum(["project", "local", "user"]).optional().describe(
1173
1273
  "Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
1174
1274
  ),
1175
- frozen: import_zod12.z.boolean().optional().describe(
1275
+ frozen: import_zod13.z.boolean().optional().describe(
1176
1276
  "true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
1177
1277
  )
1178
1278
  }),
@@ -1201,29 +1301,29 @@ var pinCommand = define({
1201
1301
  });
1202
1302
 
1203
1303
  // src/commands/pins.ts
1204
- var import_zod13 = require("zod");
1304
+ var import_zod14 = require("zod");
1205
1305
  var pinsCommand = define({
1206
1306
  name: "pins",
1207
1307
  tool: "kb_pins",
1208
1308
  usage: "pins",
1209
1309
  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.",
1210
- input: import_zod13.z.object({}),
1310
+ input: import_zod14.z.object({}),
1211
1311
  fromArgv: () => ({}),
1212
1312
  run: ({ store }) => listPins(store, process.cwd())
1213
1313
  });
1214
1314
 
1215
1315
  // src/commands/query.ts
1216
- var import_zod14 = require("zod");
1316
+ var import_zod15 = require("zod");
1217
1317
  var queryCommand = define({
1218
1318
  name: "query",
1219
1319
  tool: "kb_query",
1220
1320
  usage: "query <text...>",
1221
1321
  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.",
1222
- input: import_zod14.z.object({
1322
+ input: import_zod15.z.object({
1223
1323
  bundlePath,
1224
- text: import_zod14.z.string().optional(),
1225
- type: import_zod14.z.enum(KB_RECORD_TYPES).optional(),
1226
- includeNonCurrent: import_zod14.z.boolean().optional()
1324
+ text: import_zod15.z.string().optional(),
1325
+ type: import_zod15.z.enum(KB_RECORD_TYPES).optional(),
1326
+ includeNonCurrent: import_zod15.z.boolean().optional()
1227
1327
  }),
1228
1328
  fromArgv: (argv, path) => ({
1229
1329
  bundlePath: path,
@@ -1245,33 +1345,33 @@ var queryCommand = define({
1245
1345
  });
1246
1346
 
1247
1347
  // src/commands/read-index.ts
1248
- var import_zod15 = require("zod");
1348
+ var import_zod16 = require("zod");
1249
1349
  var readIndexCommand = define({
1250
1350
  name: "index",
1251
1351
  tool: "kb_index",
1252
1352
  usage: "index",
1253
1353
  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.",
1254
- input: import_zod15.z.object({ bundlePath }),
1354
+ input: import_zod16.z.object({ bundlePath }),
1255
1355
  fromArgv: (_argv, path) => ({ bundlePath: path }),
1256
1356
  run: ({ store }, { bundlePath: path }) => store.readIndex(path)
1257
1357
  });
1258
1358
 
1259
1359
  // src/commands/schema.ts
1260
- var import_zod18 = require("zod");
1360
+ var import_zod19 = require("zod");
1261
1361
 
1262
1362
  // src/json-schema.ts
1263
- var import_zod17 = require("zod");
1363
+ var import_zod18 = require("zod");
1264
1364
 
1265
1365
  // src/kb-log.ts
1266
- var import_zod16 = require("zod");
1366
+ var import_zod17 = require("zod");
1267
1367
  var LOG_FILE = "log.jsonl";
1268
- var kbLogEntrySchema = import_zod16.z.object({
1269
- at: import_zod16.z.string().min(1),
1270
- by: import_zod16.z.string().min(1),
1271
- operation: import_zod16.z.string().min(1),
1272
- conceptId: import_zod16.z.string().min(1),
1368
+ var kbLogEntrySchema = import_zod17.z.object({
1369
+ at: import_zod17.z.string().min(1),
1370
+ by: import_zod17.z.string().min(1),
1371
+ operation: import_zod17.z.string().min(1),
1372
+ conceptId: import_zod17.z.string().min(1),
1273
1373
  /** Second concept id, where the operation relates two — supersession. */
1274
- target: import_zod16.z.string().min(1).optional()
1374
+ target: import_zod17.z.string().min(1).optional()
1275
1375
  }).strict();
1276
1376
  function renderLogEntry(entry) {
1277
1377
  return `${JSON.stringify(kbLogEntrySchema.parse(entry))}
@@ -1302,11 +1402,11 @@ function parseLog(raw) {
1302
1402
  // src/json-schema.ts
1303
1403
  function kbJsonSchemas() {
1304
1404
  return {
1305
- recordFrontmatter: import_zod17.z.toJSONSchema(kbRecordFrontmatterSchema, {
1405
+ recordFrontmatter: import_zod18.z.toJSONSchema(kbRecordFrontmatterSchema, {
1306
1406
  io: "input"
1307
1407
  }),
1308
- composeInput: import_zod17.z.toJSONSchema(composeInputSchema, { io: "input" }),
1309
- logEntry: import_zod17.z.toJSONSchema(kbLogEntrySchema, { io: "input" })
1408
+ composeInput: import_zod18.z.toJSONSchema(composeInputSchema, { io: "input" }),
1409
+ logEntry: import_zod18.z.toJSONSchema(kbLogEntrySchema, { io: "input" })
1310
1410
  };
1311
1411
  }
1312
1412
 
@@ -1316,22 +1416,22 @@ var schemaCommand = define({
1316
1416
  tool: "kb_schema",
1317
1417
  usage: "schema",
1318
1418
  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.",
1319
- input: import_zod18.z.object({}),
1419
+ input: import_zod19.z.object({}),
1320
1420
  fromArgv: () => ({}),
1321
1421
  run: () => Promise.resolve(kbJsonSchemas())
1322
1422
  });
1323
1423
 
1324
1424
  // src/commands/status.ts
1325
- var import_zod19 = require("zod");
1425
+ var import_zod20 = require("zod");
1326
1426
  var statusCommand = define({
1327
1427
  name: "status",
1328
1428
  tool: "kb_status",
1329
1429
  usage: "status <concept-id> <status>",
1330
1430
  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.",
1331
- input: import_zod19.z.object({
1431
+ input: import_zod20.z.object({
1332
1432
  bundlePath,
1333
1433
  conceptId,
1334
- status: import_zod19.z.enum(KB_RECORD_STATUSES)
1434
+ status: import_zod20.z.enum(KB_RECORD_STATUSES)
1335
1435
  }),
1336
1436
  fromArgv: (argv, path) => ({
1337
1437
  bundlePath: path,
@@ -1346,13 +1446,13 @@ var statusCommand = define({
1346
1446
  });
1347
1447
 
1348
1448
  // src/commands/supersede.ts
1349
- var import_zod20 = require("zod");
1449
+ var import_zod21 = require("zod");
1350
1450
  var supersedeCommand = define({
1351
1451
  name: "supersede",
1352
1452
  tool: "kb_supersede",
1353
1453
  usage: "supersede <concept-id> <replacement-id>",
1354
1454
  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.",
1355
- input: import_zod20.z.object({ bundlePath, conceptId, replacementId: conceptId }),
1455
+ input: import_zod21.z.object({ bundlePath, conceptId, replacementId: conceptId }),
1356
1456
  fromArgv: (argv, path) => ({
1357
1457
  bundlePath: path,
1358
1458
  conceptId: argv[1],
@@ -1366,16 +1466,16 @@ var supersedeCommand = define({
1366
1466
  });
1367
1467
 
1368
1468
  // src/commands/sync-instructions.ts
1369
- var import_zod21 = require("zod");
1469
+ var import_zod22 = require("zod");
1370
1470
  var syncInstructionsCommand = define({
1371
1471
  name: "sync-instructions",
1372
1472
  usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
1373
1473
  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.",
1374
- input: import_zod21.z.object({
1375
- file: import_zod21.z.string().min(1).describe("The instruction file to edit in place."),
1376
- budgetTokens: import_zod21.z.number().int().positive().optional(),
1377
- fullUnderTokens: import_zod21.z.number().int().positive().optional(),
1378
- profile: import_zod21.z.string().optional()
1474
+ input: import_zod22.z.object({
1475
+ file: import_zod22.z.string().min(1).describe("The instruction file to edit in place."),
1476
+ budgetTokens: import_zod22.z.number().int().positive().optional(),
1477
+ fullUnderTokens: import_zod22.z.number().int().positive().optional(),
1478
+ profile: import_zod22.z.string().optional()
1379
1479
  }),
1380
1480
  fromArgv: (argv) => {
1381
1481
  const budget = argvFlag(argv, "--budget");
@@ -1401,43 +1501,50 @@ var syncInstructionsCommand = define({
1401
1501
  });
1402
1502
 
1403
1503
  // src/commands/trace.ts
1404
- var import_zod22 = require("zod");
1504
+ var import_zod23 = require("zod");
1405
1505
 
1406
- // src/trace.ts
1407
- var TRACE_EDGES = ["supersession", "anchor", "source"];
1408
- function trace(seedId, bundle, options = {}) {
1409
- const edges = options.edges?.length ? options.edges : TRACE_EDGES;
1410
- const maxDepth = options.depth ?? 3;
1411
- const byId = new Map(bundle.map((record) => [record.conceptId, record]));
1412
- const seed = byId.get(seedId);
1413
- if (!seed) return [];
1414
- const reached = /* @__PURE__ */ new Map([
1415
- [seedId, { record: seed, depth: 0, via: [] }]
1416
- ]);
1417
- let frontier = [seed];
1418
- for (let depth = 1; depth <= maxDepth && frontier.length; depth += 1) {
1419
- const next = [];
1420
- for (const from of frontier) {
1421
- for (const edge of edges) {
1422
- for (const record of neighbours(from, bundle, edge)) {
1423
- const existing = reached.get(record.conceptId);
1424
- if (existing) {
1425
- if (existing.depth > 0 && !existing.via.includes(edge)) {
1426
- existing.via.push(edge);
1427
- }
1428
- continue;
1429
- }
1430
- reached.set(record.conceptId, { record, depth, via: [edge] });
1431
- next.push(record);
1432
- }
1506
+ // src/kb-edges.ts
1507
+ var KB_EDGE_KINDS = [
1508
+ "body-link",
1509
+ "supersession",
1510
+ "anchor",
1511
+ "source"
1512
+ ];
1513
+ var BODY_LINK_TARGET = new RegExp(
1514
+ `\\]\\((${KB_CONCEPT_ID_PATTERN.source.replace(/^\^|\$$/g, "")})\\.md\\)`,
1515
+ "g"
1516
+ );
1517
+ function neighbours(from, bundle, kinds = KB_EDGE_KINDS) {
1518
+ const found = /* @__PURE__ */ new Map();
1519
+ for (const kind of kinds) {
1520
+ for (const record of edgeNeighbours(from, bundle, kind)) {
1521
+ const existing = found.get(record.conceptId);
1522
+ if (existing) {
1523
+ if (!existing.via.includes(kind)) existing.via.push(kind);
1524
+ continue;
1433
1525
  }
1526
+ found.set(record.conceptId, { record, via: [kind] });
1434
1527
  }
1435
- frontier = next;
1436
1528
  }
1437
- return [...reached.values()].sort(byGeneratedAt);
1529
+ return [...found.values()];
1438
1530
  }
1439
- function neighbours(from, bundle, edge) {
1440
- switch (edge) {
1531
+ function edgeNeighbours(from, bundle, kind) {
1532
+ switch (kind) {
1533
+ // A link whose target is not in the bundle is legal per compose.ts —
1534
+ // records are routinely written before the ones they point at exist — so
1535
+ // missing targets are skipped, never an error.
1536
+ case "body-link": {
1537
+ const targets = new Set(
1538
+ [...from.body.matchAll(BODY_LINK_TARGET)].map((match) => match[1])
1539
+ );
1540
+ if (!targets.size) return [];
1541
+ return bundle.filter(
1542
+ (candidate) => candidate.conceptId !== from.conceptId && targets.has(candidate.conceptId)
1543
+ );
1544
+ }
1545
+ // Both directions and both pointers: `supersede()` writes the pair, but a
1546
+ // hand-edit can leave one side behind, and a walk trusting one pointer
1547
+ // would miss a replacement the bundle openly declares.
1441
1548
  case "supersession":
1442
1549
  return bundle.filter(
1443
1550
  (candidate) => candidate.conceptId !== from.conceptId && (candidate.conceptId === from.frontmatter.strauss_superseded_by || from.frontmatter.strauss_supersedes?.includes(
@@ -1471,6 +1578,40 @@ function anchorsTouch(left, right) {
1471
1578
  if (!left.symbol || !right.symbol) return true;
1472
1579
  return left.symbol === right.symbol;
1473
1580
  }
1581
+
1582
+ // src/trace.ts
1583
+ var TRACE_EDGES = ["supersession", "anchor", "source"];
1584
+ function trace(seedId, bundle, options = {}) {
1585
+ const edges = options.edges?.length ? options.edges : TRACE_EDGES;
1586
+ const maxDepth = options.depth ?? 3;
1587
+ const byId = new Map(bundle.map((record) => [record.conceptId, record]));
1588
+ const seed = byId.get(seedId);
1589
+ if (!seed) return [];
1590
+ const reached = /* @__PURE__ */ new Map([
1591
+ [seedId, { record: seed, depth: 0, via: [] }]
1592
+ ]);
1593
+ let frontier = [seed];
1594
+ for (let depth = 1; depth <= maxDepth && frontier.length; depth += 1) {
1595
+ const next = [];
1596
+ for (const from of frontier) {
1597
+ for (const edge of edges) {
1598
+ for (const record of edgeNeighbours(from, bundle, edge)) {
1599
+ const existing = reached.get(record.conceptId);
1600
+ if (existing) {
1601
+ if (existing.depth > 0 && !existing.via.includes(edge)) {
1602
+ existing.via.push(edge);
1603
+ }
1604
+ continue;
1605
+ }
1606
+ reached.set(record.conceptId, { record, depth, via: [edge] });
1607
+ next.push(record);
1608
+ }
1609
+ }
1610
+ }
1611
+ frontier = next;
1612
+ }
1613
+ return [...reached.values()].sort(byGeneratedAt);
1614
+ }
1474
1615
  function byGeneratedAt(left, right) {
1475
1616
  const at = (step) => step.record.frontmatter.generated?.at ?? "";
1476
1617
  return at(left).localeCompare(at(right)) || left.depth - right.depth;
@@ -1482,11 +1623,11 @@ var traceCommand = define({
1482
1623
  tool: "kb_trace",
1483
1624
  usage: "trace <concept-id> [edges...]",
1484
1625
  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.',
1485
- input: import_zod22.z.object({
1626
+ input: import_zod23.z.object({
1486
1627
  bundlePath,
1487
1628
  conceptId,
1488
- edges: import_zod22.z.array(import_zod22.z.enum(TRACE_EDGES)).optional(),
1489
- depth: import_zod22.z.number().int().positive().optional()
1629
+ edges: import_zod23.z.array(import_zod23.z.enum(TRACE_EDGES)).optional(),
1630
+ depth: import_zod23.z.number().int().positive().optional()
1490
1631
  }),
1491
1632
  fromArgv: (argv, path) => ({
1492
1633
  bundlePath: path,
@@ -1508,31 +1649,31 @@ var traceCommand = define({
1508
1649
  });
1509
1650
 
1510
1651
  // src/commands/types.ts
1511
- var import_zod23 = require("zod");
1652
+ var import_zod24 = require("zod");
1512
1653
  var typesCommand = define({
1513
1654
  name: "types",
1514
1655
  tool: "kb_types",
1515
1656
  usage: "types",
1516
1657
  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.",
1517
- input: import_zod23.z.object({}),
1658
+ input: import_zod24.z.object({}),
1518
1659
  fromArgv: () => ({}),
1519
1660
  run: () => Promise.resolve(RECORD_TYPES)
1520
1661
  });
1521
1662
 
1522
1663
  // src/commands/unpin.ts
1523
- var import_zod24 = require("zod");
1664
+ var import_zod25 = require("zod");
1524
1665
  var unpinCommand = define({
1525
1666
  name: "unpin",
1526
1667
  tool: "kb_unpin",
1527
1668
  usage: "unpin [bundle-path]",
1528
1669
  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.",
1529
- input: import_zod24.z.object({ bundlePath }),
1670
+ input: import_zod25.z.object({ bundlePath }),
1530
1671
  fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
1531
1672
  run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
1532
1673
  });
1533
1674
 
1534
1675
  // src/commands/validate.ts
1535
- var import_zod25 = require("zod");
1676
+ var import_zod26 = require("zod");
1536
1677
 
1537
1678
  // src/validate.ts
1538
1679
  function validateBundle(records) {
@@ -1575,23 +1716,23 @@ var validateCommand = define({
1575
1716
  tool: "kb_validate",
1576
1717
  usage: "validate",
1577
1718
  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.",
1578
- input: import_zod25.z.object({ bundlePath }),
1719
+ input: import_zod26.z.object({ bundlePath }),
1579
1720
  fromArgv: (_argv, path) => ({ bundlePath: path }),
1580
1721
  run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
1581
1722
  failsWhen: (result) => Array.isArray(result) && result.length > 0
1582
1723
  });
1583
1724
 
1584
1725
  // src/commands/verify.ts
1585
- var import_zod26 = require("zod");
1726
+ var import_zod27 = require("zod");
1586
1727
  var verifyCommand = define({
1587
1728
  name: "verify",
1588
1729
  tool: "kb_verify",
1589
1730
  usage: "verify <concept-id> --note <text>",
1590
1731
  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.",
1591
- input: import_zod26.z.object({
1732
+ input: import_zod27.z.object({
1592
1733
  bundlePath,
1593
1734
  conceptId,
1594
- note: import_zod26.z.string().refine((s) => s.trim().length > 0, {
1735
+ note: import_zod27.z.string().refine((s) => s.trim().length > 0, {
1595
1736
  message: "note must say what the check found"
1596
1737
  })
1597
1738
  }),
@@ -1611,7 +1752,7 @@ var verifyCommand = define({
1611
1752
  });
1612
1753
 
1613
1754
  // src/commands/write.ts
1614
- var import_zod27 = require("zod");
1755
+ var import_zod28 = require("zod");
1615
1756
  var writeCommand = define({
1616
1757
  name: "write",
1617
1758
  tool: "kb_write",
@@ -1625,9 +1766,9 @@ var writeCommand = define({
1625
1766
  "- Prefer a new record over overloading an existing one, and keep each short. A record nobody finishes reading is not durable memory.",
1626
1767
  "- Records are never deleted; supersede instead, so the earlier reasoning stays inspectable."
1627
1768
  ].join("\n"),
1628
- input: import_zod27.z.object({
1769
+ input: import_zod28.z.object({
1629
1770
  bundlePath,
1630
- type: import_zod27.z.enum(KB_RECORD_TYPES),
1771
+ type: import_zod28.z.enum(KB_RECORD_TYPES),
1631
1772
  input: composeInputSchema
1632
1773
  }),
1633
1774
  fromArgv: async (argv, path, stdin) => ({
@@ -1651,7 +1792,7 @@ var writeCommand = define({
1651
1792
  });
1652
1793
 
1653
1794
  // src/commands/write-decision.ts
1654
- var import_zod28 = require("zod");
1795
+ var import_zod29 = require("zod");
1655
1796
  var writeDecisionCommand = define({
1656
1797
  name: "write-decision",
1657
1798
  tool: "kb_write_decision",
@@ -1664,7 +1805,7 @@ var writeDecisionCommand = define({
1664
1805
  "- `alternative` is what you turned down and why, not a list of everything considered.",
1665
1806
  "- A reference to material you read goes in `sources`; a reference to code goes in `anchors`; a reference to another record goes in `relatedConceptIds`."
1666
1807
  ].join("\n"),
1667
- input: import_zod28.z.object({ bundlePath, input: decisionInputSchema }),
1808
+ input: import_zod29.z.object({ bundlePath, input: decisionInputSchema }),
1668
1809
  fromArgv: async (_argv, path, stdin) => ({
1669
1810
  bundlePath: path,
1670
1811
  input: JSON.parse(await stdin())
@@ -1694,6 +1835,7 @@ var KB_COMMANDS = [
1694
1835
  answerCommand,
1695
1836
  verifyCommand,
1696
1837
  loadCommand,
1838
+ packCommand,
1697
1839
  queryCommand,
1698
1840
  traceCommand,
1699
1841
  listCommand,
@@ -1827,6 +1969,27 @@ var KbSelfVerificationError = class extends BaseError {
1827
1969
  actor;
1828
1970
  generatedBy;
1829
1971
  };
1972
+ var KbPackBudgetExceededError = class extends BaseError {
1973
+ constructor(recordCount, approxTokens2, budgetTokens, excluded) {
1974
+ super({
1975
+ message: `kb: a pack of ${recordCount} records is ~${approxTokens2} tokens against a budget of ${budgetTokens} \u2014 lower hops or maxNodes, or raise the budget`,
1976
+ errorType: "KbPackBudgetExceeded" /* KbPackBudgetExceeded */,
1977
+ code: 400,
1978
+ fault: "User" /* User */,
1979
+ retriable: false,
1980
+ reportToUser: true,
1981
+ details: { recordCount, approxTokens: approxTokens2, budgetTokens, excluded }
1982
+ });
1983
+ this.recordCount = recordCount;
1984
+ this.approxTokens = approxTokens2;
1985
+ this.budgetTokens = budgetTokens;
1986
+ this.excluded = excluded;
1987
+ }
1988
+ recordCount;
1989
+ approxTokens;
1990
+ budgetTokens;
1991
+ excluded;
1992
+ };
1830
1993
  var KbInvalidConceptIdError = class extends BaseError {
1831
1994
  constructor(message, details) {
1832
1995
  super({
@@ -1927,6 +2090,90 @@ async function loadQmd(logger) {
1927
2090
  }
1928
2091
  }
1929
2092
 
2093
+ // src/pack.ts
2094
+ var DEFAULT_PACK_HOPS = 2;
2095
+ var DEFAULT_PACK_MAX_NODES = 20;
2096
+ var TYPE_PRIORITY = [
2097
+ "decision",
2098
+ "constraint",
2099
+ "requirement",
2100
+ ...KB_RECORD_TYPES.filter(
2101
+ (type) => !["decision", "constraint", "requirement"].includes(type)
2102
+ )
2103
+ ];
2104
+ function pack(bundle, rootId, options = {}) {
2105
+ const hops = options.hops ?? DEFAULT_PACK_HOPS;
2106
+ const maxNodes = options.maxNodes ?? DEFAULT_PACK_MAX_NODES;
2107
+ const budgetTokens = options.budgetTokens ?? DEFAULT_LOAD_BUDGET;
2108
+ const byId = new Map(bundle.map((record) => [record.conceptId, record]));
2109
+ const root = byId.get(rootId);
2110
+ if (!root) throw new KbRecordNotFoundError(rootId);
2111
+ const reached = [{ record: root, depth: 0 }];
2112
+ const seen = /* @__PURE__ */ new Set([rootId]);
2113
+ let frontier = [root];
2114
+ for (let depth = 1; frontier.length; depth += 1) {
2115
+ const next = [];
2116
+ for (const from of frontier) {
2117
+ for (const { record } of neighbours(from, bundle)) {
2118
+ if (seen.has(record.conceptId)) continue;
2119
+ seen.add(record.conceptId);
2120
+ reached.push({ record, depth });
2121
+ next.push(record);
2122
+ }
2123
+ }
2124
+ frontier = next;
2125
+ }
2126
+ reached.sort(byRank);
2127
+ const within = reached.filter((entry) => entry.depth <= hops);
2128
+ const kept = within.slice(0, maxNodes);
2129
+ const excluded = [
2130
+ ...within.slice(maxNodes),
2131
+ ...reached.filter((entry) => entry.depth > hops)
2132
+ ].map((entry) => entry.record.conceptId).sort();
2133
+ const adjudicated = adjudicate(
2134
+ kept.map((entry) => entry.record),
2135
+ bundle
2136
+ );
2137
+ const superseded = adjudicated.filter((hit) => hit.standing === "superseded").map(stub);
2138
+ const whole = adjudicated.filter((hit) => hit.standing !== "superseded");
2139
+ const tokensLoaded = whole.reduce((total, hit) => total + estimateTokens(hit.record), 0) + superseded.reduce((total, entry) => total + estimateStubTokens(entry), 0);
2140
+ const recordCount = adjudicated.length;
2141
+ if (tokensLoaded > budgetTokens) {
2142
+ throw new KbPackBudgetExceededError(
2143
+ recordCount,
2144
+ tokensLoaded,
2145
+ budgetTokens,
2146
+ excluded
2147
+ );
2148
+ }
2149
+ return {
2150
+ root: rootId,
2151
+ records: whole.map((hit) => ({
2152
+ conceptId: hit.record.conceptId,
2153
+ title: hit.record.frontmatter.title ?? null,
2154
+ standing: hit.standing,
2155
+ supersededBy: hit.heads.map((head) => head.conceptId),
2156
+ warnings: hit.warnings,
2157
+ anchors: hit.record.frontmatter.strauss_anchors ?? [],
2158
+ body: hit.record.body
2159
+ })),
2160
+ superseded,
2161
+ excluded,
2162
+ recordCount,
2163
+ tokensLoaded,
2164
+ budgetTokens
2165
+ };
2166
+ }
2167
+ function byRank(left, right) {
2168
+ return left.depth - right.depth || typeRank(left.record) - typeRank(right.record) || (left.record.frontmatter.title ?? "").localeCompare(
2169
+ right.record.frontmatter.title ?? ""
2170
+ ) || left.record.conceptId.localeCompare(right.record.conceptId);
2171
+ }
2172
+ function typeRank(record) {
2173
+ const index = TYPE_PRIORITY.indexOf(record.frontmatter.type);
2174
+ return index === -1 ? TYPE_PRIORITY.length : index;
2175
+ }
2176
+
1930
2177
  // src/kb-store.ts
1931
2178
  var KB_DIR = (0, import_node_path6.join)(".strauss", "kb");
1932
2179
  var STORE_OWNED = /* @__PURE__ */ new Set([INDEX_FILE, LOG_FILE, SEARCH_INDEX_FILE]);
@@ -2216,6 +2463,10 @@ ${answer}
2216
2463
  async trace(bundlePath2, seedId, options = {}) {
2217
2464
  return trace(seedId, await this.list(bundlePath2), options);
2218
2465
  }
2466
+ /** A bounded neighbourhood around one record. See `pack.ts`. */
2467
+ async pack(bundlePath2, rootId, options = {}) {
2468
+ return pack(await this.list(bundlePath2), rootId, options);
2469
+ }
2219
2470
  /**
2220
2471
  * The stored index, rebuilt if it disagrees with the records.
2221
2472
  *
@@ -2422,6 +2673,9 @@ function digest(contents) {
2422
2673
  return (0, import_node_crypto.createHash)("sha256").update(contents).digest("hex");
2423
2674
  }
2424
2675
 
2676
+ // src/version.ts
2677
+ var VERSION = true ? "0.1.7" : "0.0.0-dev";
2678
+
2425
2679
  // src/cli.ts
2426
2680
  async function runKbCli(argv) {
2427
2681
  const { bundle, rest } = takeBundle(argv);
@@ -2430,6 +2684,11 @@ async function runKbCli(argv) {
2430
2684
  process.stdout.write(usage());
2431
2685
  return;
2432
2686
  }
2687
+ if (name === "--version" || name === "-v") {
2688
+ process.stdout.write(`${VERSION}
2689
+ `);
2690
+ return;
2691
+ }
2433
2692
  const command = KB_COMMANDS_BY_NAME.get(name);
2434
2693
  if (!command) die(`unknown command ${name}`);
2435
2694
  const raw = await command.fromArgv(rest, bundle, readStdin);
@@ -2499,6 +2758,7 @@ function usage() {
2499
2758
  ),
2500
2759
  "",
2501
2760
  ` --bundle PATH defaults to ./${KB_DIR}`,
2761
+ " --version the installed package version",
2502
2762
  " STRAUSS_KB_ACTOR names the writer in the log",
2503
2763
  ""
2504
2764
  ].join("\n");