@saasontools/strauss-kb 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,6 +11,11 @@ var kbActorStampSchema = z.object({
11
11
  by: z.string().min(1),
12
12
  at: z.string().min(1)
13
13
  }).passthrough();
14
+ var kbVerifiedEventSchema = kbActorStampSchema.extend({
15
+ note: z.string().refine((s) => s.trim().length > 0, {
16
+ message: "note must say what the check found"
17
+ })
18
+ });
14
19
  var kbAnchorSchema = z.object({
15
20
  file: z.string().min(1),
16
21
  symbol: z.string().min(1).optional()
@@ -978,41 +983,48 @@ function kbJsonSchemas() {
978
983
  };
979
984
  }
980
985
 
981
- // src/trace.ts
982
- var TRACE_EDGES = ["supersession", "anchor", "source"];
983
- function trace(seedId, bundle, options = {}) {
984
- const edges = options.edges?.length ? options.edges : TRACE_EDGES;
985
- const maxDepth = options.depth ?? 3;
986
- const byId = new Map(bundle.map((record) => [record.conceptId, record]));
987
- const seed = byId.get(seedId);
988
- if (!seed) return [];
989
- const reached = /* @__PURE__ */ new Map([
990
- [seedId, { record: seed, depth: 0, via: [] }]
991
- ]);
992
- let frontier = [seed];
993
- for (let depth = 1; depth <= maxDepth && frontier.length; depth += 1) {
994
- const next = [];
995
- for (const from of frontier) {
996
- for (const edge of edges) {
997
- for (const record of neighbours(from, bundle, edge)) {
998
- const existing = reached.get(record.conceptId);
999
- if (existing) {
1000
- if (existing.depth > 0 && !existing.via.includes(edge)) {
1001
- existing.via.push(edge);
1002
- }
1003
- continue;
1004
- }
1005
- reached.set(record.conceptId, { record, depth, via: [edge] });
1006
- next.push(record);
1007
- }
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;
1008
1005
  }
1006
+ found.set(record.conceptId, { record, via: [kind] });
1009
1007
  }
1010
- frontier = next;
1011
1008
  }
1012
- return [...reached.values()].sort(byGeneratedAt);
1009
+ return [...found.values()];
1013
1010
  }
1014
- function neighbours(from, bundle, edge) {
1015
- 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.
1016
1028
  case "supersession":
1017
1029
  return bundle.filter(
1018
1030
  (candidate) => candidate.conceptId !== from.conceptId && (candidate.conceptId === from.frontmatter.strauss_superseded_by || from.frontmatter.strauss_supersedes?.includes(
@@ -1046,6 +1058,40 @@ function anchorsTouch(left, right) {
1046
1058
  if (!left.symbol || !right.symbol) return true;
1047
1059
  return left.symbol === right.symbol;
1048
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
+ }
1049
1095
  function byGeneratedAt(left, right) {
1050
1096
  const at = (step) => step.record.frontmatter.generated?.at ?? "";
1051
1097
  return at(left).localeCompare(at(right)) || left.depth - right.depth;
@@ -1276,23 +1322,123 @@ var noDecisionCommand = define({
1276
1322
  }
1277
1323
  });
1278
1324
 
1279
- // src/commands/pin.ts
1325
+ // src/commands/pack.ts
1280
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";
1281
1427
  var pinCommand = define({
1282
1428
  name: "pin",
1283
1429
  tool: "kb_pin",
1284
1430
  usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
1285
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.",
1286
- input: z14.object({
1432
+ input: z15.object({
1287
1433
  bundlePath,
1288
- mode: z14.enum(["full", "index"]).optional().describe(
1434
+ mode: z15.enum(["full", "index"]).optional().describe(
1289
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."
1290
1436
  ),
1291
- profiles: z14.array(z14.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
1292
- 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(
1293
1439
  "Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
1294
1440
  ),
1295
- frozen: z14.boolean().optional().describe(
1441
+ frozen: z15.boolean().optional().describe(
1296
1442
  "true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
1297
1443
  )
1298
1444
  }),
@@ -1321,29 +1467,29 @@ var pinCommand = define({
1321
1467
  });
1322
1468
 
1323
1469
  // src/commands/pins.ts
1324
- import { z as z15 } from "zod";
1470
+ import { z as z16 } from "zod";
1325
1471
  var pinsCommand = define({
1326
1472
  name: "pins",
1327
1473
  tool: "kb_pins",
1328
1474
  usage: "pins",
1329
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.",
1330
- input: z15.object({}),
1476
+ input: z16.object({}),
1331
1477
  fromArgv: () => ({}),
1332
1478
  run: ({ store }) => listPins(store, process.cwd())
1333
1479
  });
1334
1480
 
1335
1481
  // src/commands/query.ts
1336
- import { z as z16 } from "zod";
1482
+ import { z as z17 } from "zod";
1337
1483
  var queryCommand = define({
1338
1484
  name: "query",
1339
1485
  tool: "kb_query",
1340
1486
  usage: "query <text...>",
1341
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.",
1342
- input: z16.object({
1488
+ input: z17.object({
1343
1489
  bundlePath,
1344
- text: z16.string().optional(),
1345
- type: z16.enum(KB_RECORD_TYPES).optional(),
1346
- includeNonCurrent: z16.boolean().optional()
1490
+ text: z17.string().optional(),
1491
+ type: z17.enum(KB_RECORD_TYPES).optional(),
1492
+ includeNonCurrent: z17.boolean().optional()
1347
1493
  }),
1348
1494
  fromArgv: (argv, path) => ({
1349
1495
  bundlePath: path,
@@ -1365,40 +1511,40 @@ var queryCommand = define({
1365
1511
  });
1366
1512
 
1367
1513
  // src/commands/read-index.ts
1368
- import { z as z17 } from "zod";
1514
+ import { z as z18 } from "zod";
1369
1515
  var readIndexCommand = define({
1370
1516
  name: "index",
1371
1517
  tool: "kb_index",
1372
1518
  usage: "index",
1373
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.",
1374
- input: z17.object({ bundlePath }),
1520
+ input: z18.object({ bundlePath }),
1375
1521
  fromArgv: (_argv, path) => ({ bundlePath: path }),
1376
1522
  run: ({ store }, { bundlePath: path }) => store.readIndex(path)
1377
1523
  });
1378
1524
 
1379
1525
  // src/commands/schema.ts
1380
- import { z as z18 } from "zod";
1526
+ import { z as z19 } from "zod";
1381
1527
  var schemaCommand = define({
1382
1528
  name: "schema",
1383
1529
  tool: "kb_schema",
1384
1530
  usage: "schema",
1385
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.",
1386
- input: z18.object({}),
1532
+ input: z19.object({}),
1387
1533
  fromArgv: () => ({}),
1388
1534
  run: () => Promise.resolve(kbJsonSchemas())
1389
1535
  });
1390
1536
 
1391
1537
  // src/commands/status.ts
1392
- import { z as z19 } from "zod";
1538
+ import { z as z20 } from "zod";
1393
1539
  var statusCommand = define({
1394
1540
  name: "status",
1395
1541
  tool: "kb_status",
1396
1542
  usage: "status <concept-id> <status>",
1397
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.",
1398
- input: z19.object({
1544
+ input: z20.object({
1399
1545
  bundlePath,
1400
1546
  conceptId,
1401
- status: z19.enum(KB_RECORD_STATUSES)
1547
+ status: z20.enum(KB_RECORD_STATUSES)
1402
1548
  }),
1403
1549
  fromArgv: (argv, path) => ({
1404
1550
  bundlePath: path,
@@ -1413,13 +1559,13 @@ var statusCommand = define({
1413
1559
  });
1414
1560
 
1415
1561
  // src/commands/supersede.ts
1416
- import { z as z20 } from "zod";
1562
+ import { z as z21 } from "zod";
1417
1563
  var supersedeCommand = define({
1418
1564
  name: "supersede",
1419
1565
  tool: "kb_supersede",
1420
1566
  usage: "supersede <concept-id> <replacement-id>",
1421
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.",
1422
- input: z20.object({ bundlePath, conceptId, replacementId: conceptId }),
1568
+ input: z21.object({ bundlePath, conceptId, replacementId: conceptId }),
1423
1569
  fromArgv: (argv, path) => ({
1424
1570
  bundlePath: path,
1425
1571
  conceptId: argv[1],
@@ -1433,16 +1579,16 @@ var supersedeCommand = define({
1433
1579
  });
1434
1580
 
1435
1581
  // src/commands/sync-instructions.ts
1436
- import { z as z21 } from "zod";
1582
+ import { z as z22 } from "zod";
1437
1583
  var syncInstructionsCommand = define({
1438
1584
  name: "sync-instructions",
1439
1585
  usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
1440
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.",
1441
- input: z21.object({
1442
- file: z21.string().min(1).describe("The instruction file to edit in place."),
1443
- budgetTokens: z21.number().int().positive().optional(),
1444
- fullUnderTokens: z21.number().int().positive().optional(),
1445
- 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()
1446
1592
  }),
1447
1593
  fromArgv: (argv) => {
1448
1594
  const budget = argvFlag(argv, "--budget");
@@ -1468,17 +1614,17 @@ var syncInstructionsCommand = define({
1468
1614
  });
1469
1615
 
1470
1616
  // src/commands/trace.ts
1471
- import { z as z22 } from "zod";
1617
+ import { z as z23 } from "zod";
1472
1618
  var traceCommand = define({
1473
1619
  name: "trace",
1474
1620
  tool: "kb_trace",
1475
1621
  usage: "trace <concept-id> [edges...]",
1476
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.',
1477
- input: z22.object({
1623
+ input: z23.object({
1478
1624
  bundlePath,
1479
1625
  conceptId,
1480
- edges: z22.array(z22.enum(TRACE_EDGES)).optional(),
1481
- depth: z22.number().int().positive().optional()
1626
+ edges: z23.array(z23.enum(TRACE_EDGES)).optional(),
1627
+ depth: z23.number().int().positive().optional()
1482
1628
  }),
1483
1629
  fromArgv: (argv, path) => ({
1484
1630
  bundlePath: path,
@@ -1500,44 +1646,73 @@ var traceCommand = define({
1500
1646
  });
1501
1647
 
1502
1648
  // src/commands/types.ts
1503
- import { z as z23 } from "zod";
1649
+ import { z as z24 } from "zod";
1504
1650
  var typesCommand = define({
1505
1651
  name: "types",
1506
1652
  tool: "kb_types",
1507
1653
  usage: "types",
1508
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.",
1509
- input: z23.object({}),
1655
+ input: z24.object({}),
1510
1656
  fromArgv: () => ({}),
1511
1657
  run: () => Promise.resolve(RECORD_TYPES)
1512
1658
  });
1513
1659
 
1514
1660
  // src/commands/unpin.ts
1515
- import { z as z24 } from "zod";
1661
+ import { z as z25 } from "zod";
1516
1662
  var unpinCommand = define({
1517
1663
  name: "unpin",
1518
1664
  tool: "kb_unpin",
1519
1665
  usage: "unpin [bundle-path]",
1520
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.",
1521
- input: z24.object({ bundlePath }),
1667
+ input: z25.object({ bundlePath }),
1522
1668
  fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
1523
1669
  run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
1524
1670
  });
1525
1671
 
1526
1672
  // src/commands/validate.ts
1527
- import { z as z25 } from "zod";
1673
+ import { z as z26 } from "zod";
1528
1674
  var validateCommand = define({
1529
1675
  name: "validate",
1530
1676
  tool: "kb_validate",
1531
1677
  usage: "validate",
1532
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.",
1533
- input: z25.object({ bundlePath }),
1679
+ input: z26.object({ bundlePath }),
1534
1680
  fromArgv: (_argv, path) => ({ bundlePath: path }),
1535
1681
  run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
1536
1682
  failsWhen: (result) => Array.isArray(result) && result.length > 0
1537
1683
  });
1538
1684
 
1685
+ // src/commands/verify.ts
1686
+ import { z as z27 } from "zod";
1687
+ var verifyCommand = define({
1688
+ name: "verify",
1689
+ tool: "kb_verify",
1690
+ usage: "verify <concept-id> --note <text>",
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.",
1692
+ input: z27.object({
1693
+ bundlePath,
1694
+ conceptId,
1695
+ note: z27.string().refine((s) => s.trim().length > 0, {
1696
+ message: "note must say what the check found"
1697
+ })
1698
+ }),
1699
+ fromArgv: (argv, path) => ({
1700
+ bundlePath: path,
1701
+ conceptId: argv[1],
1702
+ note: argvFlag(argv, "--note")
1703
+ }),
1704
+ run: async ({ store, actor, now }, { bundlePath: path, conceptId: id, note }) => {
1705
+ await assertBaseNotFrozen(process.cwd(), path);
1706
+ const record = await store.verify(path, id, note, actor, now());
1707
+ return {
1708
+ conceptId: record.conceptId,
1709
+ verified: record.frontmatter.verified?.length ?? 0
1710
+ };
1711
+ }
1712
+ });
1713
+
1539
1714
  // src/commands/write.ts
1540
- import { z as z26 } from "zod";
1715
+ import { z as z28 } from "zod";
1541
1716
  var writeCommand = define({
1542
1717
  name: "write",
1543
1718
  tool: "kb_write",
@@ -1551,9 +1726,9 @@ var writeCommand = define({
1551
1726
  "- Prefer a new record over overloading an existing one, and keep each short. A record nobody finishes reading is not durable memory.",
1552
1727
  "- Records are never deleted; supersede instead, so the earlier reasoning stays inspectable."
1553
1728
  ].join("\n"),
1554
- input: z26.object({
1729
+ input: z28.object({
1555
1730
  bundlePath,
1556
- type: z26.enum(KB_RECORD_TYPES),
1731
+ type: z28.enum(KB_RECORD_TYPES),
1557
1732
  input: composeInputSchema
1558
1733
  }),
1559
1734
  fromArgv: async (argv, path, stdin) => ({
@@ -1577,7 +1752,7 @@ var writeCommand = define({
1577
1752
  });
1578
1753
 
1579
1754
  // src/commands/write-decision.ts
1580
- import { z as z27 } from "zod";
1755
+ import { z as z29 } from "zod";
1581
1756
  var writeDecisionCommand = define({
1582
1757
  name: "write-decision",
1583
1758
  tool: "kb_write_decision",
@@ -1590,7 +1765,7 @@ var writeDecisionCommand = define({
1590
1765
  "- `alternative` is what you turned down and why, not a list of everything considered.",
1591
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`."
1592
1767
  ].join("\n"),
1593
- input: z27.object({ bundlePath, input: decisionInputSchema }),
1768
+ input: z29.object({ bundlePath, input: decisionInputSchema }),
1594
1769
  fromArgv: async (_argv, path, stdin) => ({
1595
1770
  bundlePath: path,
1596
1771
  input: JSON.parse(await stdin())
@@ -1618,7 +1793,9 @@ var KB_COMMANDS = [
1618
1793
  statusCommand,
1619
1794
  supersedeCommand,
1620
1795
  answerCommand,
1796
+ verifyCommand,
1621
1797
  loadCommand,
1798
+ packCommand,
1622
1799
  queryCommand,
1623
1800
  traceCommand,
1624
1801
  listCommand,
@@ -1672,7 +1849,9 @@ var Fault = /* @__PURE__ */ ((Fault2) => {
1672
1849
  var ErrorTypes = /* @__PURE__ */ ((ErrorTypes2) => {
1673
1850
  ErrorTypes2["KbRecordAlreadyExists"] = "KbRecordAlreadyExists";
1674
1851
  ErrorTypes2["KbInvalidConceptId"] = "KbInvalidConceptId";
1852
+ ErrorTypes2["KbPackBudgetExceeded"] = "KbPackBudgetExceeded";
1675
1853
  ErrorTypes2["KbRecordNotFound"] = "KbRecordNotFound";
1854
+ ErrorTypes2["KbSelfVerification"] = "KbSelfVerification";
1676
1855
  ErrorTypes2["KbWriteConflict"] = "KbWriteConflict";
1677
1856
  return ErrorTypes2;
1678
1857
  })(ErrorTypes || {});
@@ -1741,6 +1920,46 @@ var KbWriteConflictError = class extends BaseError {
1741
1920
  }
1742
1921
  conceptId;
1743
1922
  };
1923
+ var KbSelfVerificationError = class extends BaseError {
1924
+ constructor(conceptId2, actor, generatedBy) {
1925
+ super({
1926
+ message: `kb: ${conceptId2} was generated by ${generatedBy}, and a record's generator cannot verify it \u2014 only a human or a different actor can`,
1927
+ errorType: "KbSelfVerification" /* KbSelfVerification */,
1928
+ code: 400,
1929
+ fault: "User" /* User */,
1930
+ retriable: false,
1931
+ reportToUser: true,
1932
+ details: { conceptId: conceptId2, actor, generatedBy, action: "refused" }
1933
+ });
1934
+ this.conceptId = conceptId2;
1935
+ this.actor = actor;
1936
+ this.generatedBy = generatedBy;
1937
+ }
1938
+ conceptId;
1939
+ actor;
1940
+ generatedBy;
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
+ };
1744
1963
  var KbInvalidConceptIdError = class extends BaseError {
1745
1964
  constructor(message, details) {
1746
1965
  super({
@@ -1971,6 +2190,40 @@ var KbStore = class {
1971
2190
  { operation: `status:${status}`, by: actor }
1972
2191
  );
1973
2192
  }
2193
+ /**
2194
+ * Appends one `verified[]` event: who checked the record, when, and what the
2195
+ * check found. Append-only — prior events are history, and are spread into
2196
+ * the new array untouched rather than reshaped through the write schema.
2197
+ *
2198
+ * A record's generator cannot verify its own record unless the actor is
2199
+ * human: the generator re-reading its own output is not an independent
2200
+ * check. The rule runs before the mutation so a refusal never publishes,
2201
+ * and the refusal is logged under its own operation name — `mutate` only
2202
+ * logs what it publishes.
2203
+ */
2204
+ async verify(bundlePath2, conceptId2, note, actor = "unknown", at = (/* @__PURE__ */ new Date()).toISOString()) {
2205
+ const event = kbVerifiedEventSchema.parse({ by: actor, at, note });
2206
+ const existing = await this.read(bundlePath2, conceptId2);
2207
+ if (!existing) throw new KbRecordNotFoundError(conceptId2);
2208
+ const generatedBy = existing.frontmatter.generated?.by;
2209
+ if (generatedBy !== void 0 && actor.toLowerCase() === generatedBy.toLowerCase() && !normalizeActor(actor).startsWith("human:")) {
2210
+ await this.record(this.root(bundlePath2), {
2211
+ operation: "verify:refused",
2212
+ conceptId: conceptId2,
2213
+ by: actor
2214
+ });
2215
+ throw new KbSelfVerificationError(conceptId2, actor, generatedBy);
2216
+ }
2217
+ return this.mutate(
2218
+ bundlePath2,
2219
+ conceptId2,
2220
+ (frontmatter) => ({
2221
+ ...frontmatter,
2222
+ verified: [...frontmatter.verified ?? [], event]
2223
+ }),
2224
+ { operation: "verify", by: actor }
2225
+ );
2226
+ }
1974
2227
  /**
1975
2228
  * Marks `conceptId` superseded by `replacementId`, and links both directions.
1976
2229
  *
@@ -2108,6 +2361,10 @@ ${answer}
2108
2361
  async trace(bundlePath2, seedId, options = {}) {
2109
2362
  return trace(seedId, await this.list(bundlePath2), options);
2110
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
+ }
2111
2368
  /**
2112
2369
  * The stored index, rebuilt if it disagrees with the records.
2113
2370
  *
@@ -2305,13 +2562,106 @@ function matches(record, needle) {
2305
2562
  (field) => field?.toLowerCase().includes(needle)
2306
2563
  );
2307
2564
  }
2565
+ function normalizeActor(id) {
2566
+ const colon = id.indexOf(":");
2567
+ if (colon === -1) return id.toLowerCase();
2568
+ return id.slice(0, colon + 1).toLowerCase() + id.slice(colon + 1);
2569
+ }
2308
2570
  function digest(contents) {
2309
2571
  return createHash("sha256").update(contents).digest("hex");
2310
2572
  }
2311
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
+
2312
2661
  export {
2313
2662
  kbSourceSchema,
2314
2663
  kbActorStampSchema,
2664
+ kbVerifiedEventSchema,
2315
2665
  kbAnchorSchema,
2316
2666
  KB_RECORD_TYPES,
2317
2667
  KB_SLUG_PATTERN,
@@ -2363,6 +2713,9 @@ export {
2363
2713
  renderLogEntry,
2364
2714
  parseLog,
2365
2715
  kbJsonSchemas,
2716
+ KB_EDGE_KINDS,
2717
+ neighbours,
2718
+ edgeNeighbours,
2366
2719
  TRACE_EDGES,
2367
2720
  trace,
2368
2721
  validateBundle,
@@ -2377,12 +2730,19 @@ export {
2377
2730
  KbRecordAlreadyExistsError,
2378
2731
  KbRecordNotFoundError,
2379
2732
  KbWriteConflictError,
2733
+ KbSelfVerificationError,
2734
+ KbPackBudgetExceededError,
2380
2735
  KbInvalidConceptIdError,
2381
2736
  SEARCH_INDEX_FILE,
2382
2737
  searchBase,
2383
2738
  resolveHits,
2384
2739
  loadQmd,
2740
+ DEFAULT_PACK_HOPS,
2741
+ DEFAULT_PACK_MAX_NODES,
2742
+ pack,
2385
2743
  KB_DIR,
2386
- KbStore
2744
+ DEFAULT_LOAD_BUDGET,
2745
+ KbStore,
2746
+ VERSION
2387
2747
  };
2388
- //# sourceMappingURL=chunk-FZIMFPGR.js.map
2748
+ //# sourceMappingURL=chunk-GKUQOJEK.js.map