@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.
@@ -983,41 +983,48 @@ function kbJsonSchemas() {
983
983
  };
984
984
  }
985
985
 
986
- // src/trace.ts
987
- var TRACE_EDGES = ["supersession", "anchor", "source"];
988
- function trace(seedId, bundle, options = {}) {
989
- const edges = options.edges?.length ? options.edges : TRACE_EDGES;
990
- const maxDepth = options.depth ?? 3;
991
- const byId = new Map(bundle.map((record) => [record.conceptId, record]));
992
- const seed = byId.get(seedId);
993
- if (!seed) return [];
994
- const reached = /* @__PURE__ */ new Map([
995
- [seedId, { record: seed, depth: 0, via: [] }]
996
- ]);
997
- let frontier = [seed];
998
- for (let depth = 1; depth <= maxDepth && frontier.length; depth += 1) {
999
- const next = [];
1000
- for (const from of frontier) {
1001
- for (const edge of edges) {
1002
- for (const record of neighbours(from, bundle, edge)) {
1003
- const existing = reached.get(record.conceptId);
1004
- if (existing) {
1005
- if (existing.depth > 0 && !existing.via.includes(edge)) {
1006
- existing.via.push(edge);
1007
- }
1008
- continue;
1009
- }
1010
- reached.set(record.conceptId, { record, depth, via: [edge] });
1011
- next.push(record);
1012
- }
986
+ // src/kb-edges.ts
987
+ var KB_EDGE_KINDS = [
988
+ "body-link",
989
+ "supersession",
990
+ "anchor",
991
+ "source"
992
+ ];
993
+ var BODY_LINK_TARGET = new RegExp(
994
+ `\\]\\((${KB_CONCEPT_ID_PATTERN.source.replace(/^\^|\$$/g, "")})\\.md\\)`,
995
+ "g"
996
+ );
997
+ function neighbours(from, bundle, kinds = KB_EDGE_KINDS) {
998
+ const found = /* @__PURE__ */ new Map();
999
+ for (const kind of kinds) {
1000
+ for (const record of edgeNeighbours(from, bundle, kind)) {
1001
+ const existing = found.get(record.conceptId);
1002
+ if (existing) {
1003
+ if (!existing.via.includes(kind)) existing.via.push(kind);
1004
+ continue;
1013
1005
  }
1006
+ found.set(record.conceptId, { record, via: [kind] });
1014
1007
  }
1015
- frontier = next;
1016
1008
  }
1017
- return [...reached.values()].sort(byGeneratedAt);
1009
+ return [...found.values()];
1018
1010
  }
1019
- function neighbours(from, bundle, edge) {
1020
- switch (edge) {
1011
+ function edgeNeighbours(from, bundle, kind) {
1012
+ switch (kind) {
1013
+ // A link whose target is not in the bundle is legal per compose.ts —
1014
+ // records are routinely written before the ones they point at exist — so
1015
+ // missing targets are skipped, never an error.
1016
+ case "body-link": {
1017
+ const targets = new Set(
1018
+ [...from.body.matchAll(BODY_LINK_TARGET)].map((match) => match[1])
1019
+ );
1020
+ if (!targets.size) return [];
1021
+ return bundle.filter(
1022
+ (candidate) => candidate.conceptId !== from.conceptId && targets.has(candidate.conceptId)
1023
+ );
1024
+ }
1025
+ // Both directions and both pointers: `supersede()` writes the pair, but a
1026
+ // hand-edit can leave one side behind, and a walk trusting one pointer
1027
+ // would miss a replacement the bundle openly declares.
1021
1028
  case "supersession":
1022
1029
  return bundle.filter(
1023
1030
  (candidate) => candidate.conceptId !== from.conceptId && (candidate.conceptId === from.frontmatter.strauss_superseded_by || from.frontmatter.strauss_supersedes?.includes(
@@ -1051,6 +1058,40 @@ function anchorsTouch(left, right) {
1051
1058
  if (!left.symbol || !right.symbol) return true;
1052
1059
  return left.symbol === right.symbol;
1053
1060
  }
1061
+
1062
+ // src/trace.ts
1063
+ var TRACE_EDGES = ["supersession", "anchor", "source"];
1064
+ function trace(seedId, bundle, options = {}) {
1065
+ const edges = options.edges?.length ? options.edges : TRACE_EDGES;
1066
+ const maxDepth = options.depth ?? 3;
1067
+ const byId = new Map(bundle.map((record) => [record.conceptId, record]));
1068
+ const seed = byId.get(seedId);
1069
+ if (!seed) return [];
1070
+ const reached = /* @__PURE__ */ new Map([
1071
+ [seedId, { record: seed, depth: 0, via: [] }]
1072
+ ]);
1073
+ let frontier = [seed];
1074
+ for (let depth = 1; depth <= maxDepth && frontier.length; depth += 1) {
1075
+ const next = [];
1076
+ for (const from of frontier) {
1077
+ for (const edge of edges) {
1078
+ for (const record of edgeNeighbours(from, bundle, edge)) {
1079
+ const existing = reached.get(record.conceptId);
1080
+ if (existing) {
1081
+ if (existing.depth > 0 && !existing.via.includes(edge)) {
1082
+ existing.via.push(edge);
1083
+ }
1084
+ continue;
1085
+ }
1086
+ reached.set(record.conceptId, { record, depth, via: [edge] });
1087
+ next.push(record);
1088
+ }
1089
+ }
1090
+ }
1091
+ frontier = next;
1092
+ }
1093
+ return [...reached.values()].sort(byGeneratedAt);
1094
+ }
1054
1095
  function byGeneratedAt(left, right) {
1055
1096
  const at = (step) => step.record.frontmatter.generated?.at ?? "";
1056
1097
  return at(left).localeCompare(at(right)) || left.depth - right.depth;
@@ -1281,23 +1322,123 @@ var noDecisionCommand = define({
1281
1322
  }
1282
1323
  });
1283
1324
 
1284
- // src/commands/pin.ts
1325
+ // src/commands/pack.ts
1285
1326
  import { z as z14 } from "zod";
1327
+ var packCommand = define({
1328
+ name: "pack",
1329
+ tool: "kb_pack",
1330
+ usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
1331
+ 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.",
1332
+ input: z14.object({
1333
+ bundlePath,
1334
+ conceptId,
1335
+ hops: z14.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
1336
+ maxNodes: z14.number().int().positive().optional().describe(
1337
+ "How many records the pack may hold, root included. Defaults to 20."
1338
+ ),
1339
+ budgetTokens: z14.number().int().positive().optional().describe(
1340
+ "Approximate token ceiling over what is actually emitted. Defaults to 25000."
1341
+ )
1342
+ }),
1343
+ fromArgv: (argv, path) => {
1344
+ const hops = argvFlag(argv, "--hops");
1345
+ const maxNodes = argvFlag(argv, "--max-nodes");
1346
+ const budget = argvFlag(argv, "--budget");
1347
+ return {
1348
+ bundlePath: path,
1349
+ conceptId: argv[1],
1350
+ ...hops ? { hops: Number(hops) } : {},
1351
+ ...maxNodes ? { maxNodes: Number(maxNodes) } : {},
1352
+ ...budget ? { budgetTokens: Number(budget) } : {}
1353
+ };
1354
+ },
1355
+ run: async ({ store, now }, { bundlePath: path, conceptId: root, hops, maxNodes, budgetTokens }) => {
1356
+ const result = await store.pack(path, root, {
1357
+ ...hops !== void 0 ? { hops } : {},
1358
+ ...maxNodes !== void 0 ? { maxNodes } : {},
1359
+ ...budgetTokens !== void 0 ? { budgetTokens } : {}
1360
+ });
1361
+ return render(result, path, now());
1362
+ }
1363
+ });
1364
+ function render(result, bundle, at) {
1365
+ const lines = [
1366
+ `# KB Pack \u2014 ${result.root}`,
1367
+ `bundle: ${bundle}`,
1368
+ `budget: ~${result.tokensLoaded} of ${result.budgetTokens} tokens, ${result.recordCount} records`,
1369
+ `packed: ${at}`,
1370
+ "",
1371
+ `## Records (${result.records.length})`
1372
+ ];
1373
+ for (const record of result.records) {
1374
+ lines.push(
1375
+ "",
1376
+ `### ${record.conceptId}${record.title ? ` \u2014 ${record.title}` : ""} [${record.standing}]`
1377
+ );
1378
+ if (record.warnings.length) {
1379
+ lines.push(`warnings: ${record.warnings.map(warningLabel).join("; ")}`);
1380
+ }
1381
+ if (record.anchors.length) {
1382
+ lines.push(
1383
+ `anchors: ${record.anchors.map(
1384
+ (anchor) => anchor.symbol ? `${anchor.file}#${anchor.symbol}` : anchor.file
1385
+ ).join(", ")}`
1386
+ );
1387
+ }
1388
+ lines.push("", record.body.trimEnd());
1389
+ }
1390
+ if (result.superseded.length) {
1391
+ lines.push("", `## Superseded (${result.superseded.length})`);
1392
+ for (const entry of result.superseded) {
1393
+ lines.push(
1394
+ `- ${entry.conceptId} \u2192 ${entry.supersededBy.join(", ") || "(no surviving head)"}${entry.at ? ` (${entry.at})` : ""}`
1395
+ );
1396
+ }
1397
+ }
1398
+ if (result.excluded.length) {
1399
+ lines.push("", `## Excluded (${result.excluded.length})`);
1400
+ for (const cut of result.excluded) lines.push(`- ${cut}`);
1401
+ }
1402
+ return lines.join("\n");
1403
+ }
1404
+ function warningLabel(warning) {
1405
+ switch (warning.kind) {
1406
+ case "superseded":
1407
+ return `superseded by ${warning.by.join(", ")}`;
1408
+ case "unsettled":
1409
+ return `unsettled (${warning.status})`;
1410
+ case "broken-chain":
1411
+ return `broken chain \u2014 ${warning.missing} is not in the bundle`;
1412
+ case "chain-cycle":
1413
+ return `chain cycle through ${warning.through.join(" \u2192 ")}`;
1414
+ case "forked-chain":
1415
+ return `forked chain \u2014 heads ${warning.heads.join(", ")}`;
1416
+ case "stale":
1417
+ return `stale since ${warning.staleAfter}`;
1418
+ case "unresolved-question":
1419
+ return "unresolved question";
1420
+ default:
1421
+ return warning.kind;
1422
+ }
1423
+ }
1424
+
1425
+ // src/commands/pin.ts
1426
+ import { z as z15 } from "zod";
1286
1427
  var pinCommand = define({
1287
1428
  name: "pin",
1288
1429
  tool: "kb_pin",
1289
1430
  usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
1290
1431
  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.",
1291
- input: z14.object({
1432
+ input: z15.object({
1292
1433
  bundlePath,
1293
- mode: z14.enum(["full", "index"]).optional().describe(
1434
+ mode: z15.enum(["full", "index"]).optional().describe(
1294
1435
  "full: always emit this base's records whole (still under the block budget); index: never upgrade. Absent: the profile's full-under threshold decides."
1295
1436
  ),
1296
- profiles: z14.array(z14.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
1297
- layer: z14.enum(["project", "local", "user"]).optional().describe(
1437
+ profiles: z15.array(z15.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
1438
+ layer: z15.enum(["project", "local", "user"]).optional().describe(
1298
1439
  "Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
1299
1440
  ),
1300
- frozen: z14.boolean().optional().describe(
1441
+ frozen: z15.boolean().optional().describe(
1301
1442
  "true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
1302
1443
  )
1303
1444
  }),
@@ -1326,29 +1467,29 @@ var pinCommand = define({
1326
1467
  });
1327
1468
 
1328
1469
  // src/commands/pins.ts
1329
- import { z as z15 } from "zod";
1470
+ import { z as z16 } from "zod";
1330
1471
  var pinsCommand = define({
1331
1472
  name: "pins",
1332
1473
  tool: "kb_pins",
1333
1474
  usage: "pins",
1334
1475
  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.",
1335
- input: z15.object({}),
1476
+ input: z16.object({}),
1336
1477
  fromArgv: () => ({}),
1337
1478
  run: ({ store }) => listPins(store, process.cwd())
1338
1479
  });
1339
1480
 
1340
1481
  // src/commands/query.ts
1341
- import { z as z16 } from "zod";
1482
+ import { z as z17 } from "zod";
1342
1483
  var queryCommand = define({
1343
1484
  name: "query",
1344
1485
  tool: "kb_query",
1345
1486
  usage: "query <text...>",
1346
1487
  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.",
1347
- input: z16.object({
1488
+ input: z17.object({
1348
1489
  bundlePath,
1349
- text: z16.string().optional(),
1350
- type: z16.enum(KB_RECORD_TYPES).optional(),
1351
- includeNonCurrent: z16.boolean().optional()
1490
+ text: z17.string().optional(),
1491
+ type: z17.enum(KB_RECORD_TYPES).optional(),
1492
+ includeNonCurrent: z17.boolean().optional()
1352
1493
  }),
1353
1494
  fromArgv: (argv, path) => ({
1354
1495
  bundlePath: path,
@@ -1370,40 +1511,40 @@ var queryCommand = define({
1370
1511
  });
1371
1512
 
1372
1513
  // src/commands/read-index.ts
1373
- import { z as z17 } from "zod";
1514
+ import { z as z18 } from "zod";
1374
1515
  var readIndexCommand = define({
1375
1516
  name: "index",
1376
1517
  tool: "kb_index",
1377
1518
  usage: "index",
1378
1519
  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.",
1379
- input: z17.object({ bundlePath }),
1520
+ input: z18.object({ bundlePath }),
1380
1521
  fromArgv: (_argv, path) => ({ bundlePath: path }),
1381
1522
  run: ({ store }, { bundlePath: path }) => store.readIndex(path)
1382
1523
  });
1383
1524
 
1384
1525
  // src/commands/schema.ts
1385
- import { z as z18 } from "zod";
1526
+ import { z as z19 } from "zod";
1386
1527
  var schemaCommand = define({
1387
1528
  name: "schema",
1388
1529
  tool: "kb_schema",
1389
1530
  usage: "schema",
1390
1531
  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.",
1391
- input: z18.object({}),
1532
+ input: z19.object({}),
1392
1533
  fromArgv: () => ({}),
1393
1534
  run: () => Promise.resolve(kbJsonSchemas())
1394
1535
  });
1395
1536
 
1396
1537
  // src/commands/status.ts
1397
- import { z as z19 } from "zod";
1538
+ import { z as z20 } from "zod";
1398
1539
  var statusCommand = define({
1399
1540
  name: "status",
1400
1541
  tool: "kb_status",
1401
1542
  usage: "status <concept-id> <status>",
1402
1543
  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.",
1403
- input: z19.object({
1544
+ input: z20.object({
1404
1545
  bundlePath,
1405
1546
  conceptId,
1406
- status: z19.enum(KB_RECORD_STATUSES)
1547
+ status: z20.enum(KB_RECORD_STATUSES)
1407
1548
  }),
1408
1549
  fromArgv: (argv, path) => ({
1409
1550
  bundlePath: path,
@@ -1418,13 +1559,13 @@ var statusCommand = define({
1418
1559
  });
1419
1560
 
1420
1561
  // src/commands/supersede.ts
1421
- import { z as z20 } from "zod";
1562
+ import { z as z21 } from "zod";
1422
1563
  var supersedeCommand = define({
1423
1564
  name: "supersede",
1424
1565
  tool: "kb_supersede",
1425
1566
  usage: "supersede <concept-id> <replacement-id>",
1426
1567
  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.",
1427
- input: z20.object({ bundlePath, conceptId, replacementId: conceptId }),
1568
+ input: z21.object({ bundlePath, conceptId, replacementId: conceptId }),
1428
1569
  fromArgv: (argv, path) => ({
1429
1570
  bundlePath: path,
1430
1571
  conceptId: argv[1],
@@ -1438,16 +1579,16 @@ var supersedeCommand = define({
1438
1579
  });
1439
1580
 
1440
1581
  // src/commands/sync-instructions.ts
1441
- import { z as z21 } from "zod";
1582
+ import { z as z22 } from "zod";
1442
1583
  var syncInstructionsCommand = define({
1443
1584
  name: "sync-instructions",
1444
1585
  usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
1445
1586
  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.",
1446
- input: z21.object({
1447
- file: z21.string().min(1).describe("The instruction file to edit in place."),
1448
- budgetTokens: z21.number().int().positive().optional(),
1449
- fullUnderTokens: z21.number().int().positive().optional(),
1450
- profile: z21.string().optional()
1587
+ input: z22.object({
1588
+ file: z22.string().min(1).describe("The instruction file to edit in place."),
1589
+ budgetTokens: z22.number().int().positive().optional(),
1590
+ fullUnderTokens: z22.number().int().positive().optional(),
1591
+ profile: z22.string().optional()
1451
1592
  }),
1452
1593
  fromArgv: (argv) => {
1453
1594
  const budget = argvFlag(argv, "--budget");
@@ -1473,17 +1614,17 @@ var syncInstructionsCommand = define({
1473
1614
  });
1474
1615
 
1475
1616
  // src/commands/trace.ts
1476
- import { z as z22 } from "zod";
1617
+ import { z as z23 } from "zod";
1477
1618
  var traceCommand = define({
1478
1619
  name: "trace",
1479
1620
  tool: "kb_trace",
1480
1621
  usage: "trace <concept-id> [edges...]",
1481
1622
  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.',
1482
- input: z22.object({
1623
+ input: z23.object({
1483
1624
  bundlePath,
1484
1625
  conceptId,
1485
- edges: z22.array(z22.enum(TRACE_EDGES)).optional(),
1486
- depth: z22.number().int().positive().optional()
1626
+ edges: z23.array(z23.enum(TRACE_EDGES)).optional(),
1627
+ depth: z23.number().int().positive().optional()
1487
1628
  }),
1488
1629
  fromArgv: (argv, path) => ({
1489
1630
  bundlePath: path,
@@ -1505,53 +1646,53 @@ var traceCommand = define({
1505
1646
  });
1506
1647
 
1507
1648
  // src/commands/types.ts
1508
- import { z as z23 } from "zod";
1649
+ import { z as z24 } from "zod";
1509
1650
  var typesCommand = define({
1510
1651
  name: "types",
1511
1652
  tool: "kb_types",
1512
1653
  usage: "types",
1513
1654
  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.",
1514
- input: z23.object({}),
1655
+ input: z24.object({}),
1515
1656
  fromArgv: () => ({}),
1516
1657
  run: () => Promise.resolve(RECORD_TYPES)
1517
1658
  });
1518
1659
 
1519
1660
  // src/commands/unpin.ts
1520
- import { z as z24 } from "zod";
1661
+ import { z as z25 } from "zod";
1521
1662
  var unpinCommand = define({
1522
1663
  name: "unpin",
1523
1664
  tool: "kb_unpin",
1524
1665
  usage: "unpin [bundle-path]",
1525
1666
  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.",
1526
- input: z24.object({ bundlePath }),
1667
+ input: z25.object({ bundlePath }),
1527
1668
  fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
1528
1669
  run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
1529
1670
  });
1530
1671
 
1531
1672
  // src/commands/validate.ts
1532
- import { z as z25 } from "zod";
1673
+ import { z as z26 } from "zod";
1533
1674
  var validateCommand = define({
1534
1675
  name: "validate",
1535
1676
  tool: "kb_validate",
1536
1677
  usage: "validate",
1537
1678
  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.",
1538
- input: z25.object({ bundlePath }),
1679
+ input: z26.object({ bundlePath }),
1539
1680
  fromArgv: (_argv, path) => ({ bundlePath: path }),
1540
1681
  run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
1541
1682
  failsWhen: (result) => Array.isArray(result) && result.length > 0
1542
1683
  });
1543
1684
 
1544
1685
  // src/commands/verify.ts
1545
- import { z as z26 } from "zod";
1686
+ import { z as z27 } from "zod";
1546
1687
  var verifyCommand = define({
1547
1688
  name: "verify",
1548
1689
  tool: "kb_verify",
1549
1690
  usage: "verify <concept-id> --note <text>",
1550
1691
  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.",
1551
- input: z26.object({
1692
+ input: z27.object({
1552
1693
  bundlePath,
1553
1694
  conceptId,
1554
- note: z26.string().refine((s) => s.trim().length > 0, {
1695
+ note: z27.string().refine((s) => s.trim().length > 0, {
1555
1696
  message: "note must say what the check found"
1556
1697
  })
1557
1698
  }),
@@ -1571,7 +1712,7 @@ var verifyCommand = define({
1571
1712
  });
1572
1713
 
1573
1714
  // src/commands/write.ts
1574
- import { z as z27 } from "zod";
1715
+ import { z as z28 } from "zod";
1575
1716
  var writeCommand = define({
1576
1717
  name: "write",
1577
1718
  tool: "kb_write",
@@ -1585,9 +1726,9 @@ var writeCommand = define({
1585
1726
  "- Prefer a new record over overloading an existing one, and keep each short. A record nobody finishes reading is not durable memory.",
1586
1727
  "- Records are never deleted; supersede instead, so the earlier reasoning stays inspectable."
1587
1728
  ].join("\n"),
1588
- input: z27.object({
1729
+ input: z28.object({
1589
1730
  bundlePath,
1590
- type: z27.enum(KB_RECORD_TYPES),
1731
+ type: z28.enum(KB_RECORD_TYPES),
1591
1732
  input: composeInputSchema
1592
1733
  }),
1593
1734
  fromArgv: async (argv, path, stdin) => ({
@@ -1611,7 +1752,7 @@ var writeCommand = define({
1611
1752
  });
1612
1753
 
1613
1754
  // src/commands/write-decision.ts
1614
- import { z as z28 } from "zod";
1755
+ import { z as z29 } from "zod";
1615
1756
  var writeDecisionCommand = define({
1616
1757
  name: "write-decision",
1617
1758
  tool: "kb_write_decision",
@@ -1624,7 +1765,7 @@ var writeDecisionCommand = define({
1624
1765
  "- `alternative` is what you turned down and why, not a list of everything considered.",
1625
1766
  "- A reference to material you read goes in `sources`; a reference to code goes in `anchors`; a reference to another record goes in `relatedConceptIds`."
1626
1767
  ].join("\n"),
1627
- input: z28.object({ bundlePath, input: decisionInputSchema }),
1768
+ input: z29.object({ bundlePath, input: decisionInputSchema }),
1628
1769
  fromArgv: async (_argv, path, stdin) => ({
1629
1770
  bundlePath: path,
1630
1771
  input: JSON.parse(await stdin())
@@ -1654,6 +1795,7 @@ var KB_COMMANDS = [
1654
1795
  answerCommand,
1655
1796
  verifyCommand,
1656
1797
  loadCommand,
1798
+ packCommand,
1657
1799
  queryCommand,
1658
1800
  traceCommand,
1659
1801
  listCommand,
@@ -1707,6 +1849,7 @@ var Fault = /* @__PURE__ */ ((Fault2) => {
1707
1849
  var ErrorTypes = /* @__PURE__ */ ((ErrorTypes2) => {
1708
1850
  ErrorTypes2["KbRecordAlreadyExists"] = "KbRecordAlreadyExists";
1709
1851
  ErrorTypes2["KbInvalidConceptId"] = "KbInvalidConceptId";
1852
+ ErrorTypes2["KbPackBudgetExceeded"] = "KbPackBudgetExceeded";
1710
1853
  ErrorTypes2["KbRecordNotFound"] = "KbRecordNotFound";
1711
1854
  ErrorTypes2["KbSelfVerification"] = "KbSelfVerification";
1712
1855
  ErrorTypes2["KbWriteConflict"] = "KbWriteConflict";
@@ -1796,6 +1939,27 @@ var KbSelfVerificationError = class extends BaseError {
1796
1939
  actor;
1797
1940
  generatedBy;
1798
1941
  };
1942
+ var KbPackBudgetExceededError = class extends BaseError {
1943
+ constructor(recordCount, approxTokens2, budgetTokens, excluded) {
1944
+ super({
1945
+ message: `kb: a pack of ${recordCount} records is ~${approxTokens2} tokens against a budget of ${budgetTokens} \u2014 lower hops or maxNodes, or raise the budget`,
1946
+ errorType: "KbPackBudgetExceeded" /* KbPackBudgetExceeded */,
1947
+ code: 400,
1948
+ fault: "User" /* User */,
1949
+ retriable: false,
1950
+ reportToUser: true,
1951
+ details: { recordCount, approxTokens: approxTokens2, budgetTokens, excluded }
1952
+ });
1953
+ this.recordCount = recordCount;
1954
+ this.approxTokens = approxTokens2;
1955
+ this.budgetTokens = budgetTokens;
1956
+ this.excluded = excluded;
1957
+ }
1958
+ recordCount;
1959
+ approxTokens;
1960
+ budgetTokens;
1961
+ excluded;
1962
+ };
1799
1963
  var KbInvalidConceptIdError = class extends BaseError {
1800
1964
  constructor(message, details) {
1801
1965
  super({
@@ -2197,6 +2361,10 @@ ${answer}
2197
2361
  async trace(bundlePath2, seedId, options = {}) {
2198
2362
  return trace(seedId, await this.list(bundlePath2), options);
2199
2363
  }
2364
+ /** A bounded neighbourhood around one record. See `pack.ts`. */
2365
+ async pack(bundlePath2, rootId, options = {}) {
2366
+ return pack(await this.list(bundlePath2), rootId, options);
2367
+ }
2200
2368
  /**
2201
2369
  * The stored index, rebuilt if it disagrees with the records.
2202
2370
  *
@@ -2403,6 +2571,93 @@ function digest(contents) {
2403
2571
  return createHash("sha256").update(contents).digest("hex");
2404
2572
  }
2405
2573
 
2574
+ // src/pack.ts
2575
+ var DEFAULT_PACK_HOPS = 2;
2576
+ var DEFAULT_PACK_MAX_NODES = 20;
2577
+ var TYPE_PRIORITY = [
2578
+ "decision",
2579
+ "constraint",
2580
+ "requirement",
2581
+ ...KB_RECORD_TYPES.filter(
2582
+ (type) => !["decision", "constraint", "requirement"].includes(type)
2583
+ )
2584
+ ];
2585
+ function pack(bundle, rootId, options = {}) {
2586
+ const hops = options.hops ?? DEFAULT_PACK_HOPS;
2587
+ const maxNodes = options.maxNodes ?? DEFAULT_PACK_MAX_NODES;
2588
+ const budgetTokens = options.budgetTokens ?? DEFAULT_LOAD_BUDGET;
2589
+ const byId = new Map(bundle.map((record) => [record.conceptId, record]));
2590
+ const root = byId.get(rootId);
2591
+ if (!root) throw new KbRecordNotFoundError(rootId);
2592
+ const reached = [{ record: root, depth: 0 }];
2593
+ const seen = /* @__PURE__ */ new Set([rootId]);
2594
+ let frontier = [root];
2595
+ for (let depth = 1; frontier.length; depth += 1) {
2596
+ const next = [];
2597
+ for (const from of frontier) {
2598
+ for (const { record } of neighbours(from, bundle)) {
2599
+ if (seen.has(record.conceptId)) continue;
2600
+ seen.add(record.conceptId);
2601
+ reached.push({ record, depth });
2602
+ next.push(record);
2603
+ }
2604
+ }
2605
+ frontier = next;
2606
+ }
2607
+ reached.sort(byRank);
2608
+ const within = reached.filter((entry) => entry.depth <= hops);
2609
+ const kept = within.slice(0, maxNodes);
2610
+ const excluded = [
2611
+ ...within.slice(maxNodes),
2612
+ ...reached.filter((entry) => entry.depth > hops)
2613
+ ].map((entry) => entry.record.conceptId).sort();
2614
+ const adjudicated = adjudicate(
2615
+ kept.map((entry) => entry.record),
2616
+ bundle
2617
+ );
2618
+ const superseded = adjudicated.filter((hit) => hit.standing === "superseded").map(stub);
2619
+ const whole = adjudicated.filter((hit) => hit.standing !== "superseded");
2620
+ const tokensLoaded = whole.reduce((total, hit) => total + estimateTokens(hit.record), 0) + superseded.reduce((total, entry) => total + estimateStubTokens(entry), 0);
2621
+ const recordCount = adjudicated.length;
2622
+ if (tokensLoaded > budgetTokens) {
2623
+ throw new KbPackBudgetExceededError(
2624
+ recordCount,
2625
+ tokensLoaded,
2626
+ budgetTokens,
2627
+ excluded
2628
+ );
2629
+ }
2630
+ return {
2631
+ root: rootId,
2632
+ records: whole.map((hit) => ({
2633
+ conceptId: hit.record.conceptId,
2634
+ title: hit.record.frontmatter.title ?? null,
2635
+ standing: hit.standing,
2636
+ supersededBy: hit.heads.map((head) => head.conceptId),
2637
+ warnings: hit.warnings,
2638
+ anchors: hit.record.frontmatter.strauss_anchors ?? [],
2639
+ body: hit.record.body
2640
+ })),
2641
+ superseded,
2642
+ excluded,
2643
+ recordCount,
2644
+ tokensLoaded,
2645
+ budgetTokens
2646
+ };
2647
+ }
2648
+ function byRank(left, right) {
2649
+ return left.depth - right.depth || typeRank(left.record) - typeRank(right.record) || (left.record.frontmatter.title ?? "").localeCompare(
2650
+ right.record.frontmatter.title ?? ""
2651
+ ) || left.record.conceptId.localeCompare(right.record.conceptId);
2652
+ }
2653
+ function typeRank(record) {
2654
+ const index = TYPE_PRIORITY.indexOf(record.frontmatter.type);
2655
+ return index === -1 ? TYPE_PRIORITY.length : index;
2656
+ }
2657
+
2658
+ // src/version.ts
2659
+ var VERSION = true ? "0.1.7" : "0.0.0-dev";
2660
+
2406
2661
  export {
2407
2662
  kbSourceSchema,
2408
2663
  kbActorStampSchema,
@@ -2458,6 +2713,9 @@ export {
2458
2713
  renderLogEntry,
2459
2714
  parseLog,
2460
2715
  kbJsonSchemas,
2716
+ KB_EDGE_KINDS,
2717
+ neighbours,
2718
+ edgeNeighbours,
2461
2719
  TRACE_EDGES,
2462
2720
  trace,
2463
2721
  validateBundle,
@@ -2473,12 +2731,18 @@ export {
2473
2731
  KbRecordNotFoundError,
2474
2732
  KbWriteConflictError,
2475
2733
  KbSelfVerificationError,
2734
+ KbPackBudgetExceededError,
2476
2735
  KbInvalidConceptIdError,
2477
2736
  SEARCH_INDEX_FILE,
2478
2737
  searchBase,
2479
2738
  resolveHits,
2480
2739
  loadQmd,
2740
+ DEFAULT_PACK_HOPS,
2741
+ DEFAULT_PACK_MAX_NODES,
2742
+ pack,
2481
2743
  KB_DIR,
2482
- KbStore
2744
+ DEFAULT_LOAD_BUDGET,
2745
+ KbStore,
2746
+ VERSION
2483
2747
  };
2484
- //# sourceMappingURL=chunk-PNSRTKYN.js.map
2748
+ //# sourceMappingURL=chunk-GKUQOJEK.js.map