@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/index.cjs CHANGED
@@ -35,6 +35,9 @@ __export(index_exports, {
35
35
  CONTEXT_END: () => CONTEXT_END,
36
36
  CONTEXT_PROFILES: () => CONTEXT_PROFILES,
37
37
  DECISION_TYPE: () => DECISION_TYPE,
38
+ DEFAULT_LOAD_BUDGET: () => DEFAULT_LOAD_BUDGET,
39
+ DEFAULT_PACK_HOPS: () => DEFAULT_PACK_HOPS,
40
+ DEFAULT_PACK_MAX_NODES: () => DEFAULT_PACK_MAX_NODES,
38
41
  ErrorTypes: () => ErrorTypes,
39
42
  Fault: () => Fault,
40
43
  INDEX_FILE: () => INDEX_FILE,
@@ -43,12 +46,14 @@ __export(index_exports, {
43
46
  KB_CONCEPT_ID_PATTERN: () => KB_CONCEPT_ID_PATTERN,
44
47
  KB_CONFIDENCES: () => KB_CONFIDENCES,
45
48
  KB_DIR: () => KB_DIR,
49
+ KB_EDGE_KINDS: () => KB_EDGE_KINDS,
46
50
  KB_MATERIALITIES: () => KB_MATERIALITIES,
47
51
  KB_RECORD_STATUSES: () => KB_RECORD_STATUSES,
48
52
  KB_RECORD_TYPES: () => KB_RECORD_TYPES,
49
53
  KB_SLUG_PATTERN: () => KB_SLUG_PATTERN,
50
54
  KbBaseFrozenError: () => KbBaseFrozenError,
51
55
  KbInvalidConceptIdError: () => KbInvalidConceptIdError,
56
+ KbPackBudgetExceededError: () => KbPackBudgetExceededError,
52
57
  KbPinsMalformedError: () => KbPinsMalformedError,
53
58
  KbRecordAlreadyExistsError: () => KbRecordAlreadyExistsError,
54
59
  KbRecordNotFoundError: () => KbRecordNotFoundError,
@@ -73,6 +78,7 @@ __export(index_exports, {
73
78
  contextProfileBudgets: () => contextProfileBudgets,
74
79
  createKbMcpServer: () => createKbMcpServer,
75
80
  decisionInputSchema: () => decisionInputSchema,
81
+ edgeNeighbours: () => edgeNeighbours,
76
82
  indexIsStale: () => indexIsStale,
77
83
  isKbRecordType: () => isKbRecordType,
78
84
  isNoDecisionRecord: () => isNoDecisionRecord,
@@ -88,6 +94,8 @@ __export(index_exports, {
88
94
  loadQmd: () => loadQmd,
89
95
  matchToDiff: () => matchToDiff,
90
96
  mergedContextBudgets: () => mergedContextBudgets,
97
+ neighbours: () => neighbours,
98
+ pack: () => pack,
91
99
  parseLog: () => parseLog,
92
100
  parseMarkdownWithFrontmatter: () => parseMarkdownWithFrontmatter,
93
101
  pinBase: () => pinBase,
@@ -244,6 +252,7 @@ var Fault = /* @__PURE__ */ ((Fault2) => {
244
252
  var ErrorTypes = /* @__PURE__ */ ((ErrorTypes2) => {
245
253
  ErrorTypes2["KbRecordAlreadyExists"] = "KbRecordAlreadyExists";
246
254
  ErrorTypes2["KbInvalidConceptId"] = "KbInvalidConceptId";
255
+ ErrorTypes2["KbPackBudgetExceeded"] = "KbPackBudgetExceeded";
247
256
  ErrorTypes2["KbRecordNotFound"] = "KbRecordNotFound";
248
257
  ErrorTypes2["KbSelfVerification"] = "KbSelfVerification";
249
258
  ErrorTypes2["KbWriteConflict"] = "KbWriteConflict";
@@ -333,6 +342,27 @@ var KbSelfVerificationError = class extends BaseError {
333
342
  actor;
334
343
  generatedBy;
335
344
  };
345
+ var KbPackBudgetExceededError = class extends BaseError {
346
+ constructor(recordCount, approxTokens2, budgetTokens, excluded) {
347
+ super({
348
+ message: `kb: a pack of ${recordCount} records is ~${approxTokens2} tokens against a budget of ${budgetTokens} \u2014 lower hops or maxNodes, or raise the budget`,
349
+ errorType: "KbPackBudgetExceeded" /* KbPackBudgetExceeded */,
350
+ code: 400,
351
+ fault: "User" /* User */,
352
+ retriable: false,
353
+ reportToUser: true,
354
+ details: { recordCount, approxTokens: approxTokens2, budgetTokens, excluded }
355
+ });
356
+ this.recordCount = recordCount;
357
+ this.approxTokens = approxTokens2;
358
+ this.budgetTokens = budgetTokens;
359
+ this.excluded = excluded;
360
+ }
361
+ recordCount;
362
+ approxTokens;
363
+ budgetTokens;
364
+ excluded;
365
+ };
336
366
  var KbInvalidConceptIdError = class extends BaseError {
337
367
  constructor(message, details) {
338
368
  super({
@@ -585,41 +615,48 @@ async function loadQmd(logger) {
585
615
  }
586
616
  }
587
617
 
588
- // src/trace.ts
589
- var TRACE_EDGES = ["supersession", "anchor", "source"];
590
- function trace(seedId, bundle, options = {}) {
591
- const edges = options.edges?.length ? options.edges : TRACE_EDGES;
592
- const maxDepth = options.depth ?? 3;
593
- const byId = new Map(bundle.map((record) => [record.conceptId, record]));
594
- const seed = byId.get(seedId);
595
- if (!seed) return [];
596
- const reached = /* @__PURE__ */ new Map([
597
- [seedId, { record: seed, depth: 0, via: [] }]
598
- ]);
599
- let frontier = [seed];
600
- for (let depth = 1; depth <= maxDepth && frontier.length; depth += 1) {
601
- const next = [];
602
- for (const from of frontier) {
603
- for (const edge of edges) {
604
- for (const record of neighbours(from, bundle, edge)) {
605
- const existing = reached.get(record.conceptId);
606
- if (existing) {
607
- if (existing.depth > 0 && !existing.via.includes(edge)) {
608
- existing.via.push(edge);
609
- }
610
- continue;
611
- }
612
- reached.set(record.conceptId, { record, depth, via: [edge] });
613
- next.push(record);
614
- }
618
+ // src/kb-edges.ts
619
+ var KB_EDGE_KINDS = [
620
+ "body-link",
621
+ "supersession",
622
+ "anchor",
623
+ "source"
624
+ ];
625
+ var BODY_LINK_TARGET = new RegExp(
626
+ `\\]\\((${KB_CONCEPT_ID_PATTERN.source.replace(/^\^|\$$/g, "")})\\.md\\)`,
627
+ "g"
628
+ );
629
+ function neighbours(from, bundle, kinds = KB_EDGE_KINDS) {
630
+ const found = /* @__PURE__ */ new Map();
631
+ for (const kind of kinds) {
632
+ for (const record of edgeNeighbours(from, bundle, kind)) {
633
+ const existing = found.get(record.conceptId);
634
+ if (existing) {
635
+ if (!existing.via.includes(kind)) existing.via.push(kind);
636
+ continue;
615
637
  }
638
+ found.set(record.conceptId, { record, via: [kind] });
616
639
  }
617
- frontier = next;
618
640
  }
619
- return [...reached.values()].sort(byGeneratedAt);
641
+ return [...found.values()];
620
642
  }
621
- function neighbours(from, bundle, edge) {
622
- switch (edge) {
643
+ function edgeNeighbours(from, bundle, kind) {
644
+ switch (kind) {
645
+ // A link whose target is not in the bundle is legal per compose.ts —
646
+ // records are routinely written before the ones they point at exist — so
647
+ // missing targets are skipped, never an error.
648
+ case "body-link": {
649
+ const targets = new Set(
650
+ [...from.body.matchAll(BODY_LINK_TARGET)].map((match) => match[1])
651
+ );
652
+ if (!targets.size) return [];
653
+ return bundle.filter(
654
+ (candidate) => candidate.conceptId !== from.conceptId && targets.has(candidate.conceptId)
655
+ );
656
+ }
657
+ // Both directions and both pointers: `supersede()` writes the pair, but a
658
+ // hand-edit can leave one side behind, and a walk trusting one pointer
659
+ // would miss a replacement the bundle openly declares.
623
660
  case "supersession":
624
661
  return bundle.filter(
625
662
  (candidate) => candidate.conceptId !== from.conceptId && (candidate.conceptId === from.frontmatter.strauss_superseded_by || from.frontmatter.strauss_supersedes?.includes(
@@ -653,11 +690,129 @@ function anchorsTouch(left, right) {
653
690
  if (!left.symbol || !right.symbol) return true;
654
691
  return left.symbol === right.symbol;
655
692
  }
693
+
694
+ // src/trace.ts
695
+ var TRACE_EDGES = ["supersession", "anchor", "source"];
696
+ function trace(seedId, bundle, options = {}) {
697
+ const edges = options.edges?.length ? options.edges : TRACE_EDGES;
698
+ const maxDepth = options.depth ?? 3;
699
+ const byId = new Map(bundle.map((record) => [record.conceptId, record]));
700
+ const seed = byId.get(seedId);
701
+ if (!seed) return [];
702
+ const reached = /* @__PURE__ */ new Map([
703
+ [seedId, { record: seed, depth: 0, via: [] }]
704
+ ]);
705
+ let frontier = [seed];
706
+ for (let depth = 1; depth <= maxDepth && frontier.length; depth += 1) {
707
+ const next = [];
708
+ for (const from of frontier) {
709
+ for (const edge of edges) {
710
+ for (const record of edgeNeighbours(from, bundle, edge)) {
711
+ const existing = reached.get(record.conceptId);
712
+ if (existing) {
713
+ if (existing.depth > 0 && !existing.via.includes(edge)) {
714
+ existing.via.push(edge);
715
+ }
716
+ continue;
717
+ }
718
+ reached.set(record.conceptId, { record, depth, via: [edge] });
719
+ next.push(record);
720
+ }
721
+ }
722
+ }
723
+ frontier = next;
724
+ }
725
+ return [...reached.values()].sort(byGeneratedAt);
726
+ }
656
727
  function byGeneratedAt(left, right) {
657
728
  const at = (step) => step.record.frontmatter.generated?.at ?? "";
658
729
  return at(left).localeCompare(at(right)) || left.depth - right.depth;
659
730
  }
660
731
 
732
+ // src/pack.ts
733
+ var DEFAULT_PACK_HOPS = 2;
734
+ var DEFAULT_PACK_MAX_NODES = 20;
735
+ var TYPE_PRIORITY = [
736
+ "decision",
737
+ "constraint",
738
+ "requirement",
739
+ ...KB_RECORD_TYPES.filter(
740
+ (type) => !["decision", "constraint", "requirement"].includes(type)
741
+ )
742
+ ];
743
+ function pack(bundle, rootId, options = {}) {
744
+ const hops = options.hops ?? DEFAULT_PACK_HOPS;
745
+ const maxNodes = options.maxNodes ?? DEFAULT_PACK_MAX_NODES;
746
+ const budgetTokens = options.budgetTokens ?? DEFAULT_LOAD_BUDGET;
747
+ const byId = new Map(bundle.map((record) => [record.conceptId, record]));
748
+ const root = byId.get(rootId);
749
+ if (!root) throw new KbRecordNotFoundError(rootId);
750
+ const reached = [{ record: root, depth: 0 }];
751
+ const seen = /* @__PURE__ */ new Set([rootId]);
752
+ let frontier = [root];
753
+ for (let depth = 1; frontier.length; depth += 1) {
754
+ const next = [];
755
+ for (const from of frontier) {
756
+ for (const { record } of neighbours(from, bundle)) {
757
+ if (seen.has(record.conceptId)) continue;
758
+ seen.add(record.conceptId);
759
+ reached.push({ record, depth });
760
+ next.push(record);
761
+ }
762
+ }
763
+ frontier = next;
764
+ }
765
+ reached.sort(byRank);
766
+ const within = reached.filter((entry) => entry.depth <= hops);
767
+ const kept = within.slice(0, maxNodes);
768
+ const excluded = [
769
+ ...within.slice(maxNodes),
770
+ ...reached.filter((entry) => entry.depth > hops)
771
+ ].map((entry) => entry.record.conceptId).sort();
772
+ const adjudicated = adjudicate(
773
+ kept.map((entry) => entry.record),
774
+ bundle
775
+ );
776
+ const superseded = adjudicated.filter((hit) => hit.standing === "superseded").map(stub);
777
+ const whole = adjudicated.filter((hit) => hit.standing !== "superseded");
778
+ const tokensLoaded = whole.reduce((total, hit) => total + estimateTokens(hit.record), 0) + superseded.reduce((total, entry) => total + estimateStubTokens(entry), 0);
779
+ const recordCount = adjudicated.length;
780
+ if (tokensLoaded > budgetTokens) {
781
+ throw new KbPackBudgetExceededError(
782
+ recordCount,
783
+ tokensLoaded,
784
+ budgetTokens,
785
+ excluded
786
+ );
787
+ }
788
+ return {
789
+ root: rootId,
790
+ records: whole.map((hit) => ({
791
+ conceptId: hit.record.conceptId,
792
+ title: hit.record.frontmatter.title ?? null,
793
+ standing: hit.standing,
794
+ supersededBy: hit.heads.map((head) => head.conceptId),
795
+ warnings: hit.warnings,
796
+ anchors: hit.record.frontmatter.strauss_anchors ?? [],
797
+ body: hit.record.body
798
+ })),
799
+ superseded,
800
+ excluded,
801
+ recordCount,
802
+ tokensLoaded,
803
+ budgetTokens
804
+ };
805
+ }
806
+ function byRank(left, right) {
807
+ return left.depth - right.depth || typeRank(left.record) - typeRank(right.record) || (left.record.frontmatter.title ?? "").localeCompare(
808
+ right.record.frontmatter.title ?? ""
809
+ ) || left.record.conceptId.localeCompare(right.record.conceptId);
810
+ }
811
+ function typeRank(record) {
812
+ const index = TYPE_PRIORITY.indexOf(record.frontmatter.type);
813
+ return index === -1 ? TYPE_PRIORITY.length : index;
814
+ }
815
+
661
816
  // src/kb-store.ts
662
817
  var KB_DIR = (0, import_node_path2.join)(".strauss", "kb");
663
818
  var STORE_OWNED = /* @__PURE__ */ new Set([INDEX_FILE, LOG_FILE, SEARCH_INDEX_FILE]);
@@ -947,6 +1102,10 @@ ${answer}
947
1102
  async trace(bundlePath2, seedId, options = {}) {
948
1103
  return trace(seedId, await this.list(bundlePath2), options);
949
1104
  }
1105
+ /** A bounded neighbourhood around one record. See `pack.ts`. */
1106
+ async pack(bundlePath2, rootId, options = {}) {
1107
+ return pack(await this.list(bundlePath2), rootId, options);
1108
+ }
950
1109
  /**
951
1110
  * The stored index, rebuilt if it disagrees with the records.
952
1111
  *
@@ -2206,23 +2365,123 @@ var noDecisionCommand = define({
2206
2365
  }
2207
2366
  });
2208
2367
 
2209
- // src/commands/pin.ts
2368
+ // src/commands/pack.ts
2210
2369
  var import_zod14 = require("zod");
2370
+ var packCommand = define({
2371
+ name: "pack",
2372
+ tool: "kb_pack",
2373
+ usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
2374
+ 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.",
2375
+ input: import_zod14.z.object({
2376
+ bundlePath,
2377
+ conceptId,
2378
+ hops: import_zod14.z.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
2379
+ maxNodes: import_zod14.z.number().int().positive().optional().describe(
2380
+ "How many records the pack may hold, root included. Defaults to 20."
2381
+ ),
2382
+ budgetTokens: import_zod14.z.number().int().positive().optional().describe(
2383
+ "Approximate token ceiling over what is actually emitted. Defaults to 25000."
2384
+ )
2385
+ }),
2386
+ fromArgv: (argv, path) => {
2387
+ const hops = argvFlag(argv, "--hops");
2388
+ const maxNodes = argvFlag(argv, "--max-nodes");
2389
+ const budget = argvFlag(argv, "--budget");
2390
+ return {
2391
+ bundlePath: path,
2392
+ conceptId: argv[1],
2393
+ ...hops ? { hops: Number(hops) } : {},
2394
+ ...maxNodes ? { maxNodes: Number(maxNodes) } : {},
2395
+ ...budget ? { budgetTokens: Number(budget) } : {}
2396
+ };
2397
+ },
2398
+ run: async ({ store, now }, { bundlePath: path, conceptId: root, hops, maxNodes, budgetTokens }) => {
2399
+ const result = await store.pack(path, root, {
2400
+ ...hops !== void 0 ? { hops } : {},
2401
+ ...maxNodes !== void 0 ? { maxNodes } : {},
2402
+ ...budgetTokens !== void 0 ? { budgetTokens } : {}
2403
+ });
2404
+ return render(result, path, now());
2405
+ }
2406
+ });
2407
+ function render(result, bundle, at) {
2408
+ const lines = [
2409
+ `# KB Pack \u2014 ${result.root}`,
2410
+ `bundle: ${bundle}`,
2411
+ `budget: ~${result.tokensLoaded} of ${result.budgetTokens} tokens, ${result.recordCount} records`,
2412
+ `packed: ${at}`,
2413
+ "",
2414
+ `## Records (${result.records.length})`
2415
+ ];
2416
+ for (const record of result.records) {
2417
+ lines.push(
2418
+ "",
2419
+ `### ${record.conceptId}${record.title ? ` \u2014 ${record.title}` : ""} [${record.standing}]`
2420
+ );
2421
+ if (record.warnings.length) {
2422
+ lines.push(`warnings: ${record.warnings.map(warningLabel).join("; ")}`);
2423
+ }
2424
+ if (record.anchors.length) {
2425
+ lines.push(
2426
+ `anchors: ${record.anchors.map(
2427
+ (anchor) => anchor.symbol ? `${anchor.file}#${anchor.symbol}` : anchor.file
2428
+ ).join(", ")}`
2429
+ );
2430
+ }
2431
+ lines.push("", record.body.trimEnd());
2432
+ }
2433
+ if (result.superseded.length) {
2434
+ lines.push("", `## Superseded (${result.superseded.length})`);
2435
+ for (const entry of result.superseded) {
2436
+ lines.push(
2437
+ `- ${entry.conceptId} \u2192 ${entry.supersededBy.join(", ") || "(no surviving head)"}${entry.at ? ` (${entry.at})` : ""}`
2438
+ );
2439
+ }
2440
+ }
2441
+ if (result.excluded.length) {
2442
+ lines.push("", `## Excluded (${result.excluded.length})`);
2443
+ for (const cut of result.excluded) lines.push(`- ${cut}`);
2444
+ }
2445
+ return lines.join("\n");
2446
+ }
2447
+ function warningLabel(warning) {
2448
+ switch (warning.kind) {
2449
+ case "superseded":
2450
+ return `superseded by ${warning.by.join(", ")}`;
2451
+ case "unsettled":
2452
+ return `unsettled (${warning.status})`;
2453
+ case "broken-chain":
2454
+ return `broken chain \u2014 ${warning.missing} is not in the bundle`;
2455
+ case "chain-cycle":
2456
+ return `chain cycle through ${warning.through.join(" \u2192 ")}`;
2457
+ case "forked-chain":
2458
+ return `forked chain \u2014 heads ${warning.heads.join(", ")}`;
2459
+ case "stale":
2460
+ return `stale since ${warning.staleAfter}`;
2461
+ case "unresolved-question":
2462
+ return "unresolved question";
2463
+ default:
2464
+ return warning.kind;
2465
+ }
2466
+ }
2467
+
2468
+ // src/commands/pin.ts
2469
+ var import_zod15 = require("zod");
2211
2470
  var pinCommand = define({
2212
2471
  name: "pin",
2213
2472
  tool: "kb_pin",
2214
2473
  usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
2215
2474
  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.",
2216
- input: import_zod14.z.object({
2475
+ input: import_zod15.z.object({
2217
2476
  bundlePath,
2218
- mode: import_zod14.z.enum(["full", "index"]).optional().describe(
2477
+ mode: import_zod15.z.enum(["full", "index"]).optional().describe(
2219
2478
  "full: always emit this base's records whole (still under the block budget); index: never upgrade. Absent: the profile's full-under threshold decides."
2220
2479
  ),
2221
- profiles: import_zod14.z.array(import_zod14.z.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
2222
- layer: import_zod14.z.enum(["project", "local", "user"]).optional().describe(
2480
+ profiles: import_zod15.z.array(import_zod15.z.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
2481
+ layer: import_zod15.z.enum(["project", "local", "user"]).optional().describe(
2223
2482
  "Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
2224
2483
  ),
2225
- frozen: import_zod14.z.boolean().optional().describe(
2484
+ frozen: import_zod15.z.boolean().optional().describe(
2226
2485
  "true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
2227
2486
  )
2228
2487
  }),
@@ -2251,29 +2510,29 @@ var pinCommand = define({
2251
2510
  });
2252
2511
 
2253
2512
  // src/commands/pins.ts
2254
- var import_zod15 = require("zod");
2513
+ var import_zod16 = require("zod");
2255
2514
  var pinsCommand = define({
2256
2515
  name: "pins",
2257
2516
  tool: "kb_pins",
2258
2517
  usage: "pins",
2259
2518
  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.",
2260
- input: import_zod15.z.object({}),
2519
+ input: import_zod16.z.object({}),
2261
2520
  fromArgv: () => ({}),
2262
2521
  run: ({ store }) => listPins(store, process.cwd())
2263
2522
  });
2264
2523
 
2265
2524
  // src/commands/query.ts
2266
- var import_zod16 = require("zod");
2525
+ var import_zod17 = require("zod");
2267
2526
  var queryCommand = define({
2268
2527
  name: "query",
2269
2528
  tool: "kb_query",
2270
2529
  usage: "query <text...>",
2271
2530
  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.",
2272
- input: import_zod16.z.object({
2531
+ input: import_zod17.z.object({
2273
2532
  bundlePath,
2274
- text: import_zod16.z.string().optional(),
2275
- type: import_zod16.z.enum(KB_RECORD_TYPES).optional(),
2276
- includeNonCurrent: import_zod16.z.boolean().optional()
2533
+ text: import_zod17.z.string().optional(),
2534
+ type: import_zod17.z.enum(KB_RECORD_TYPES).optional(),
2535
+ includeNonCurrent: import_zod17.z.boolean().optional()
2277
2536
  }),
2278
2537
  fromArgv: (argv, path) => ({
2279
2538
  bundlePath: path,
@@ -2295,40 +2554,40 @@ var queryCommand = define({
2295
2554
  });
2296
2555
 
2297
2556
  // src/commands/read-index.ts
2298
- var import_zod17 = require("zod");
2557
+ var import_zod18 = require("zod");
2299
2558
  var readIndexCommand = define({
2300
2559
  name: "index",
2301
2560
  tool: "kb_index",
2302
2561
  usage: "index",
2303
2562
  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.",
2304
- input: import_zod17.z.object({ bundlePath }),
2563
+ input: import_zod18.z.object({ bundlePath }),
2305
2564
  fromArgv: (_argv, path) => ({ bundlePath: path }),
2306
2565
  run: ({ store }, { bundlePath: path }) => store.readIndex(path)
2307
2566
  });
2308
2567
 
2309
2568
  // src/commands/schema.ts
2310
- var import_zod18 = require("zod");
2569
+ var import_zod19 = require("zod");
2311
2570
  var schemaCommand = define({
2312
2571
  name: "schema",
2313
2572
  tool: "kb_schema",
2314
2573
  usage: "schema",
2315
2574
  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.",
2316
- input: import_zod18.z.object({}),
2575
+ input: import_zod19.z.object({}),
2317
2576
  fromArgv: () => ({}),
2318
2577
  run: () => Promise.resolve(kbJsonSchemas())
2319
2578
  });
2320
2579
 
2321
2580
  // src/commands/status.ts
2322
- var import_zod19 = require("zod");
2581
+ var import_zod20 = require("zod");
2323
2582
  var statusCommand = define({
2324
2583
  name: "status",
2325
2584
  tool: "kb_status",
2326
2585
  usage: "status <concept-id> <status>",
2327
2586
  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.",
2328
- input: import_zod19.z.object({
2587
+ input: import_zod20.z.object({
2329
2588
  bundlePath,
2330
2589
  conceptId,
2331
- status: import_zod19.z.enum(KB_RECORD_STATUSES)
2590
+ status: import_zod20.z.enum(KB_RECORD_STATUSES)
2332
2591
  }),
2333
2592
  fromArgv: (argv, path) => ({
2334
2593
  bundlePath: path,
@@ -2343,13 +2602,13 @@ var statusCommand = define({
2343
2602
  });
2344
2603
 
2345
2604
  // src/commands/supersede.ts
2346
- var import_zod20 = require("zod");
2605
+ var import_zod21 = require("zod");
2347
2606
  var supersedeCommand = define({
2348
2607
  name: "supersede",
2349
2608
  tool: "kb_supersede",
2350
2609
  usage: "supersede <concept-id> <replacement-id>",
2351
2610
  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.",
2352
- input: import_zod20.z.object({ bundlePath, conceptId, replacementId: conceptId }),
2611
+ input: import_zod21.z.object({ bundlePath, conceptId, replacementId: conceptId }),
2353
2612
  fromArgv: (argv, path) => ({
2354
2613
  bundlePath: path,
2355
2614
  conceptId: argv[1],
@@ -2363,16 +2622,16 @@ var supersedeCommand = define({
2363
2622
  });
2364
2623
 
2365
2624
  // src/commands/sync-instructions.ts
2366
- var import_zod21 = require("zod");
2625
+ var import_zod22 = require("zod");
2367
2626
  var syncInstructionsCommand = define({
2368
2627
  name: "sync-instructions",
2369
2628
  usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
2370
2629
  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.",
2371
- input: import_zod21.z.object({
2372
- file: import_zod21.z.string().min(1).describe("The instruction file to edit in place."),
2373
- budgetTokens: import_zod21.z.number().int().positive().optional(),
2374
- fullUnderTokens: import_zod21.z.number().int().positive().optional(),
2375
- profile: import_zod21.z.string().optional()
2630
+ input: import_zod22.z.object({
2631
+ file: import_zod22.z.string().min(1).describe("The instruction file to edit in place."),
2632
+ budgetTokens: import_zod22.z.number().int().positive().optional(),
2633
+ fullUnderTokens: import_zod22.z.number().int().positive().optional(),
2634
+ profile: import_zod22.z.string().optional()
2376
2635
  }),
2377
2636
  fromArgv: (argv) => {
2378
2637
  const budget = argvFlag(argv, "--budget");
@@ -2398,17 +2657,17 @@ var syncInstructionsCommand = define({
2398
2657
  });
2399
2658
 
2400
2659
  // src/commands/trace.ts
2401
- var import_zod22 = require("zod");
2660
+ var import_zod23 = require("zod");
2402
2661
  var traceCommand = define({
2403
2662
  name: "trace",
2404
2663
  tool: "kb_trace",
2405
2664
  usage: "trace <concept-id> [edges...]",
2406
2665
  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.',
2407
- input: import_zod22.z.object({
2666
+ input: import_zod23.z.object({
2408
2667
  bundlePath,
2409
2668
  conceptId,
2410
- edges: import_zod22.z.array(import_zod22.z.enum(TRACE_EDGES)).optional(),
2411
- depth: import_zod22.z.number().int().positive().optional()
2669
+ edges: import_zod23.z.array(import_zod23.z.enum(TRACE_EDGES)).optional(),
2670
+ depth: import_zod23.z.number().int().positive().optional()
2412
2671
  }),
2413
2672
  fromArgv: (argv, path) => ({
2414
2673
  bundlePath: path,
@@ -2430,53 +2689,53 @@ var traceCommand = define({
2430
2689
  });
2431
2690
 
2432
2691
  // src/commands/types.ts
2433
- var import_zod23 = require("zod");
2692
+ var import_zod24 = require("zod");
2434
2693
  var typesCommand = define({
2435
2694
  name: "types",
2436
2695
  tool: "kb_types",
2437
2696
  usage: "types",
2438
2697
  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.",
2439
- input: import_zod23.z.object({}),
2698
+ input: import_zod24.z.object({}),
2440
2699
  fromArgv: () => ({}),
2441
2700
  run: () => Promise.resolve(RECORD_TYPES)
2442
2701
  });
2443
2702
 
2444
2703
  // src/commands/unpin.ts
2445
- var import_zod24 = require("zod");
2704
+ var import_zod25 = require("zod");
2446
2705
  var unpinCommand = define({
2447
2706
  name: "unpin",
2448
2707
  tool: "kb_unpin",
2449
2708
  usage: "unpin [bundle-path]",
2450
2709
  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.",
2451
- input: import_zod24.z.object({ bundlePath }),
2710
+ input: import_zod25.z.object({ bundlePath }),
2452
2711
  fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
2453
2712
  run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
2454
2713
  });
2455
2714
 
2456
2715
  // src/commands/validate.ts
2457
- var import_zod25 = require("zod");
2716
+ var import_zod26 = require("zod");
2458
2717
  var validateCommand = define({
2459
2718
  name: "validate",
2460
2719
  tool: "kb_validate",
2461
2720
  usage: "validate",
2462
2721
  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.",
2463
- input: import_zod25.z.object({ bundlePath }),
2722
+ input: import_zod26.z.object({ bundlePath }),
2464
2723
  fromArgv: (_argv, path) => ({ bundlePath: path }),
2465
2724
  run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
2466
2725
  failsWhen: (result) => Array.isArray(result) && result.length > 0
2467
2726
  });
2468
2727
 
2469
2728
  // src/commands/verify.ts
2470
- var import_zod26 = require("zod");
2729
+ var import_zod27 = require("zod");
2471
2730
  var verifyCommand = define({
2472
2731
  name: "verify",
2473
2732
  tool: "kb_verify",
2474
2733
  usage: "verify <concept-id> --note <text>",
2475
2734
  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.",
2476
- input: import_zod26.z.object({
2735
+ input: import_zod27.z.object({
2477
2736
  bundlePath,
2478
2737
  conceptId,
2479
- note: import_zod26.z.string().refine((s) => s.trim().length > 0, {
2738
+ note: import_zod27.z.string().refine((s) => s.trim().length > 0, {
2480
2739
  message: "note must say what the check found"
2481
2740
  })
2482
2741
  }),
@@ -2496,7 +2755,7 @@ var verifyCommand = define({
2496
2755
  });
2497
2756
 
2498
2757
  // src/commands/write.ts
2499
- var import_zod27 = require("zod");
2758
+ var import_zod28 = require("zod");
2500
2759
  var writeCommand = define({
2501
2760
  name: "write",
2502
2761
  tool: "kb_write",
@@ -2510,9 +2769,9 @@ var writeCommand = define({
2510
2769
  "- Prefer a new record over overloading an existing one, and keep each short. A record nobody finishes reading is not durable memory.",
2511
2770
  "- Records are never deleted; supersede instead, so the earlier reasoning stays inspectable."
2512
2771
  ].join("\n"),
2513
- input: import_zod27.z.object({
2772
+ input: import_zod28.z.object({
2514
2773
  bundlePath,
2515
- type: import_zod27.z.enum(KB_RECORD_TYPES),
2774
+ type: import_zod28.z.enum(KB_RECORD_TYPES),
2516
2775
  input: composeInputSchema
2517
2776
  }),
2518
2777
  fromArgv: async (argv, path, stdin) => ({
@@ -2536,7 +2795,7 @@ var writeCommand = define({
2536
2795
  });
2537
2796
 
2538
2797
  // src/commands/write-decision.ts
2539
- var import_zod28 = require("zod");
2798
+ var import_zod29 = require("zod");
2540
2799
  var writeDecisionCommand = define({
2541
2800
  name: "write-decision",
2542
2801
  tool: "kb_write_decision",
@@ -2549,7 +2808,7 @@ var writeDecisionCommand = define({
2549
2808
  "- `alternative` is what you turned down and why, not a list of everything considered.",
2550
2809
  "- A reference to material you read goes in `sources`; a reference to code goes in `anchors`; a reference to another record goes in `relatedConceptIds`."
2551
2810
  ].join("\n"),
2552
- input: import_zod28.z.object({ bundlePath, input: decisionInputSchema }),
2811
+ input: import_zod29.z.object({ bundlePath, input: decisionInputSchema }),
2553
2812
  fromArgv: async (_argv, path, stdin) => ({
2554
2813
  bundlePath: path,
2555
2814
  input: JSON.parse(await stdin())
@@ -2579,6 +2838,7 @@ var KB_COMMANDS = [
2579
2838
  answerCommand,
2580
2839
  verifyCommand,
2581
2840
  loadCommand,
2841
+ packCommand,
2582
2842
  queryCommand,
2583
2843
  traceCommand,
2584
2844
  listCommand,
@@ -2600,8 +2860,13 @@ var KB_COMMANDS_BY_NAME = new Map(
2600
2860
  // src/mcp.ts
2601
2861
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
2602
2862
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
2863
+
2864
+ // src/version.ts
2865
+ var VERSION = true ? "0.1.7" : "0.0.0-dev";
2866
+
2867
+ // src/mcp.ts
2603
2868
  function createKbMcpServer() {
2604
- const server = new import_mcp.McpServer({ name: "strauss-kb", version: "0.1.0" });
2869
+ const server = new import_mcp.McpServer({ name: "strauss-kb", version: VERSION });
2605
2870
  const store = new KbStore({
2606
2871
  warn: (entry) => process.stderr.write(`${JSON.stringify(entry)}
2607
2872
  `)
@@ -2644,6 +2909,11 @@ async function runKbCli(argv) {
2644
2909
  process.stdout.write(usage());
2645
2910
  return;
2646
2911
  }
2912
+ if (name === "--version" || name === "-v") {
2913
+ process.stdout.write(`${VERSION}
2914
+ `);
2915
+ return;
2916
+ }
2647
2917
  const command = KB_COMMANDS_BY_NAME.get(name);
2648
2918
  if (!command) die(`unknown command ${name}`);
2649
2919
  const raw = await command.fromArgv(rest, bundle, readStdin);
@@ -2713,6 +2983,7 @@ function usage() {
2713
2983
  ),
2714
2984
  "",
2715
2985
  ` --bundle PATH defaults to ./${KB_DIR}`,
2986
+ " --version the installed package version",
2716
2987
  " STRAUSS_KB_ACTOR names the writer in the log",
2717
2988
  ""
2718
2989
  ].join("\n");
@@ -2724,6 +2995,9 @@ function usage() {
2724
2995
  CONTEXT_END,
2725
2996
  CONTEXT_PROFILES,
2726
2997
  DECISION_TYPE,
2998
+ DEFAULT_LOAD_BUDGET,
2999
+ DEFAULT_PACK_HOPS,
3000
+ DEFAULT_PACK_MAX_NODES,
2727
3001
  ErrorTypes,
2728
3002
  Fault,
2729
3003
  INDEX_FILE,
@@ -2732,12 +3006,14 @@ function usage() {
2732
3006
  KB_CONCEPT_ID_PATTERN,
2733
3007
  KB_CONFIDENCES,
2734
3008
  KB_DIR,
3009
+ KB_EDGE_KINDS,
2735
3010
  KB_MATERIALITIES,
2736
3011
  KB_RECORD_STATUSES,
2737
3012
  KB_RECORD_TYPES,
2738
3013
  KB_SLUG_PATTERN,
2739
3014
  KbBaseFrozenError,
2740
3015
  KbInvalidConceptIdError,
3016
+ KbPackBudgetExceededError,
2741
3017
  KbPinsMalformedError,
2742
3018
  KbRecordAlreadyExistsError,
2743
3019
  KbRecordNotFoundError,
@@ -2762,6 +3038,7 @@ function usage() {
2762
3038
  contextProfileBudgets,
2763
3039
  createKbMcpServer,
2764
3040
  decisionInputSchema,
3041
+ edgeNeighbours,
2765
3042
  indexIsStale,
2766
3043
  isKbRecordType,
2767
3044
  isNoDecisionRecord,
@@ -2777,6 +3054,8 @@ function usage() {
2777
3054
  loadQmd,
2778
3055
  matchToDiff,
2779
3056
  mergedContextBudgets,
3057
+ neighbours,
3058
+ pack,
2780
3059
  parseLog,
2781
3060
  parseMarkdownWithFrontmatter,
2782
3061
  pinBase,