@saasontools/strauss-kb 0.1.9 → 0.1.10

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.
@@ -586,6 +586,157 @@ async function unpinBase(workspaceDir, bundlePath2) {
586
586
  };
587
587
  }
588
588
 
589
+ // src/errors.ts
590
+ var Fault = /* @__PURE__ */ ((Fault2) => {
591
+ Fault2["Configuration"] = "Configuration";
592
+ Fault2["System"] = "System";
593
+ Fault2["User"] = "User";
594
+ return Fault2;
595
+ })(Fault || {});
596
+ var ErrorTypes = /* @__PURE__ */ ((ErrorTypes2) => {
597
+ ErrorTypes2["KbRecordAlreadyExists"] = "KbRecordAlreadyExists";
598
+ ErrorTypes2["KbInvalidConceptId"] = "KbInvalidConceptId";
599
+ ErrorTypes2["KbMissingFlagValue"] = "KbMissingFlagValue";
600
+ ErrorTypes2["KbPackBudgetExceeded"] = "KbPackBudgetExceeded";
601
+ ErrorTypes2["KbRecordNotFound"] = "KbRecordNotFound";
602
+ ErrorTypes2["KbSelfVerification"] = "KbSelfVerification";
603
+ ErrorTypes2["KbWriteConflict"] = "KbWriteConflict";
604
+ return ErrorTypes2;
605
+ })(ErrorTypes || {});
606
+ var BaseError = class extends Error {
607
+ code;
608
+ errorType;
609
+ fault;
610
+ retriable;
611
+ reportToUser;
612
+ details;
613
+ constructor(props) {
614
+ super(props.message);
615
+ this.name = props.name ?? this.constructor.name;
616
+ this.code = props.code ?? 500;
617
+ this.errorType = props.errorType;
618
+ this.fault = props.fault;
619
+ this.retriable = props.retriable ?? true;
620
+ this.reportToUser = props.reportToUser ?? false;
621
+ this.details = props.details;
622
+ }
623
+ };
624
+
625
+ // src/kb-errors.ts
626
+ var KbRecordAlreadyExistsError = class extends BaseError {
627
+ constructor(conceptId2) {
628
+ super({
629
+ message: `kb: ${conceptId2} already exists \u2014 choose a more specific slug, or write with overwrite`,
630
+ errorType: "KbRecordAlreadyExists" /* KbRecordAlreadyExists */,
631
+ code: 409,
632
+ fault: "User" /* User */,
633
+ retriable: false,
634
+ reportToUser: true,
635
+ details: { conceptId: conceptId2, action: "refused" }
636
+ });
637
+ this.conceptId = conceptId2;
638
+ }
639
+ conceptId;
640
+ };
641
+ var KbRecordNotFoundError = class extends BaseError {
642
+ constructor(conceptId2) {
643
+ super({
644
+ message: `kb: ${conceptId2} does not exist`,
645
+ errorType: "KbRecordNotFound" /* KbRecordNotFound */,
646
+ code: 404,
647
+ fault: "User" /* User */,
648
+ retriable: false,
649
+ reportToUser: true,
650
+ details: { conceptId: conceptId2 }
651
+ });
652
+ this.conceptId = conceptId2;
653
+ }
654
+ conceptId;
655
+ };
656
+ var KbWriteConflictError = class extends BaseError {
657
+ constructor(conceptId2) {
658
+ super({
659
+ message: `kb: ${conceptId2} changed while it was being updated \u2014 re-read and retry`,
660
+ errorType: "KbWriteConflict" /* KbWriteConflict */,
661
+ code: 409,
662
+ fault: "System" /* System */,
663
+ retriable: true,
664
+ reportToUser: true,
665
+ details: { conceptId: conceptId2 }
666
+ });
667
+ this.conceptId = conceptId2;
668
+ }
669
+ conceptId;
670
+ };
671
+ var KbSelfVerificationError = class extends BaseError {
672
+ constructor(conceptId2, actor, generatedBy) {
673
+ super({
674
+ message: `kb: ${conceptId2} was generated by ${generatedBy}, and a record's generator cannot verify it \u2014 only a human or a different actor can`,
675
+ errorType: "KbSelfVerification" /* KbSelfVerification */,
676
+ code: 400,
677
+ fault: "User" /* User */,
678
+ retriable: false,
679
+ reportToUser: true,
680
+ details: { conceptId: conceptId2, actor, generatedBy, action: "refused" }
681
+ });
682
+ this.conceptId = conceptId2;
683
+ this.actor = actor;
684
+ this.generatedBy = generatedBy;
685
+ }
686
+ conceptId;
687
+ actor;
688
+ generatedBy;
689
+ };
690
+ var KbPackBudgetExceededError = class extends BaseError {
691
+ constructor(recordCount, approxTokens2, budgetTokens, excluded) {
692
+ super({
693
+ message: `kb: a pack of ${recordCount} records is ~${approxTokens2} tokens against a budget of ${budgetTokens} \u2014 lower hops or maxNodes, or raise the budget`,
694
+ errorType: "KbPackBudgetExceeded" /* KbPackBudgetExceeded */,
695
+ code: 400,
696
+ fault: "User" /* User */,
697
+ retriable: false,
698
+ reportToUser: true,
699
+ details: { recordCount, approxTokens: approxTokens2, budgetTokens, excluded }
700
+ });
701
+ this.recordCount = recordCount;
702
+ this.approxTokens = approxTokens2;
703
+ this.budgetTokens = budgetTokens;
704
+ this.excluded = excluded;
705
+ }
706
+ recordCount;
707
+ approxTokens;
708
+ budgetTokens;
709
+ excluded;
710
+ };
711
+ var KbMissingFlagValueError = class extends BaseError {
712
+ constructor(flag) {
713
+ super({
714
+ message: `kb: ${flag} needs a value \u2014 pass ${flag} <value> or ${flag}=<value>`,
715
+ errorType: "KbMissingFlagValue" /* KbMissingFlagValue */,
716
+ code: 400,
717
+ fault: "User" /* User */,
718
+ retriable: false,
719
+ reportToUser: true,
720
+ details: { flag }
721
+ });
722
+ this.flag = flag;
723
+ }
724
+ flag;
725
+ };
726
+ var KbInvalidConceptIdError = class extends BaseError {
727
+ constructor(message, details) {
728
+ super({
729
+ message: `kb: ${message}`,
730
+ errorType: "KbInvalidConceptId" /* KbInvalidConceptId */,
731
+ code: 400,
732
+ fault: "User" /* User */,
733
+ retriable: false,
734
+ reportToUser: true,
735
+ details
736
+ });
737
+ }
738
+ };
739
+
589
740
  // src/adjudicate.ts
590
741
  var STANDING = {
591
742
  accepted: "current",
@@ -678,6 +829,52 @@ function successors(record, byId) {
678
829
  return { records, missing };
679
830
  }
680
831
 
832
+ // src/catalog.ts
833
+ var EMPTY_STANDINGS = {
834
+ current: 0,
835
+ superseded: 0,
836
+ rejected: 0,
837
+ unsettled: 0,
838
+ open: 0
839
+ };
840
+ function catalog(bundle, options = {}) {
841
+ const wanted = options.type ? bundle.filter((record) => record.frontmatter.type === options.type) : bundle;
842
+ const entries = adjudicate(wanted, bundle, options.now ?? /* @__PURE__ */ new Date()).map((hit) => ({
843
+ conceptId: hit.record.conceptId,
844
+ type: hit.record.frontmatter.type,
845
+ title: hit.record.frontmatter.title ?? null,
846
+ standing: hit.standing,
847
+ supersededBy: hit.heads.map((head) => head.conceptId),
848
+ stale: hit.warnings.some((warning) => warning.kind === "stale")
849
+ })).sort(byTypeThenTitle);
850
+ const standings = { ...EMPTY_STANDINGS };
851
+ for (const entry of entries) standings[entry.standing] += 1;
852
+ return {
853
+ entries,
854
+ recordCount: entries.length,
855
+ standings,
856
+ currentCount: standings.current,
857
+ supersededCount: standings.superseded,
858
+ staleCount: entries.filter((entry) => entry.stale).length
859
+ };
860
+ }
861
+ function byTypeThenTitle(left, right) {
862
+ return byCodeUnit(left.type, right.type) || byCodeUnit(left.title ?? "", right.title ?? "") || byCodeUnit(left.conceptId, right.conceptId);
863
+ }
864
+ function byCodeUnit(left, right) {
865
+ return left < right ? -1 : left > right ? 1 : 0;
866
+ }
867
+ function renderCatalogLine(entry) {
868
+ const parts = [
869
+ entry.conceptId,
870
+ entry.type,
871
+ entry.title ?? "(untitled)",
872
+ entry.standing === "superseded" ? `superseded \u2192 ${entry.supersededBy.join(", ") || "(no surviving head)"}` : entry.standing
873
+ ];
874
+ if (entry.stale) parts.push("stale");
875
+ return `- ${parts.join(" \xB7 ")}`;
876
+ }
877
+
681
878
  // src/kb-index.ts
682
879
  var INDEX_FILE = "INDEX.md";
683
880
  var HEADING = "# KB Index";
@@ -1410,8 +1607,19 @@ function define(command) {
1410
1607
  return command;
1411
1608
  }
1412
1609
  function argvFlag(argv, name) {
1610
+ const joined = argv.find((arg) => arg.startsWith(`${name}=`));
1611
+ if (joined !== void 0) {
1612
+ const value2 = joined.slice(name.length + 1);
1613
+ if (!value2) throw new KbMissingFlagValueError(name);
1614
+ return value2;
1615
+ }
1413
1616
  const at = argv.indexOf(name);
1414
- return at !== -1 ? argv[at + 1] : void 0;
1617
+ if (at === -1) return void 0;
1618
+ const value = argv[at + 1];
1619
+ if (value === void 0 || value.startsWith("--")) {
1620
+ throw new KbMissingFlagValueError(name);
1621
+ }
1622
+ return value;
1415
1623
  }
1416
1624
 
1417
1625
  // src/commands/answer.ts
@@ -1433,27 +1641,90 @@ var answerCommand = define({
1433
1641
  }
1434
1642
  });
1435
1643
 
1436
- // src/commands/context.ts
1644
+ // src/commands/catalog.ts
1437
1645
  import { z as z9 } from "zod";
1646
+ var catalogCommand = define({
1647
+ name: "catalog",
1648
+ tool: "kb_catalog",
1649
+ usage: "catalog [type]",
1650
+ description: "Lists every record as one line \u2014 concept id, type, title, standing, and a stale flag \u2014 at roughly thirty tokens each. Pick this over kb_load once kb_load refuses: kb_catalog never refuses. Superseded records show only their replacement; fetch bodies with kb_load, kb_pack, kb_query, or kb_trace.",
1651
+ input: z9.object({
1652
+ bundlePath,
1653
+ type: z9.enum(KB_RECORD_TYPES).optional()
1654
+ }),
1655
+ fromArgv: (argv, path) => ({
1656
+ bundlePath: path,
1657
+ ...argv[1] && !argv[1].startsWith("--") ? { type: argv[1] } : {}
1658
+ }),
1659
+ run: async ({ store }, { bundlePath: path, type }) => render(
1660
+ await store.catalog(path, { ...type ? { type } : {} }),
1661
+ path,
1662
+ type
1663
+ )
1664
+ });
1665
+ function render(result, bundle, type) {
1666
+ const lines = [
1667
+ `# KB Catalog${type ? ` \u2014 ${type}` : ""}`,
1668
+ `bundle: ${bundle}`,
1669
+ `${count(result.recordCount, "record")}: ${standingCounts(result)}`
1670
+ ];
1671
+ if (result.staleCount) {
1672
+ lines.push(
1673
+ `${result.staleCount} stale \u2014 a flag over the standings above, not one of them`
1674
+ );
1675
+ }
1676
+ lines.push("");
1677
+ if (!result.entries.length) {
1678
+ lines.push(
1679
+ type ? `(no records of type ${type})` : "(no records \u2014 this base is empty)"
1680
+ );
1681
+ } else {
1682
+ for (const entry of result.entries) lines.push(renderCatalogLine(entry));
1683
+ }
1684
+ lines.push(
1685
+ "",
1686
+ "Bodies are not here: kb_pack <conceptId> for the neighbourhood around one record, kb_load for the whole base when it fits the budget, kb_query for a lookup by wording, kb_trace <conceptId> for how a position was arrived at."
1687
+ );
1688
+ return lines.join("\n");
1689
+ }
1690
+ function standingCounts(result) {
1691
+ const ORDER = [
1692
+ "current",
1693
+ "open",
1694
+ "unsettled",
1695
+ "rejected",
1696
+ "superseded"
1697
+ ];
1698
+ const parts = ORDER.filter((standing) => result.standings[standing]).map(
1699
+ (standing) => `${result.standings[standing]} ${standing}`
1700
+ );
1701
+ return parts.length ? parts.join(" \xB7 ") : "none";
1702
+ }
1703
+ function count(value, noun) {
1704
+ return `${value} ${value === 1 ? noun : `${noun}s`}`;
1705
+ }
1706
+
1707
+ // src/commands/context.ts
1708
+ import { z as z10 } from "zod";
1438
1709
  var contextCommand = define({
1439
1710
  name: "context",
1440
1711
  tool: "kb_context",
1441
1712
  usage: "context [--profile NAME] [--budget N] [--full-under N] [--format json] [--event NAME]",
1442
1713
  description: "The pinned-base index block, for injection at every context birth \u2014 startup, clear, resume, and after compaction. An index, not the content: concept ids, titles and standing, with the bodies left behind kb_load at the point of use. Emits nothing when nothing is pinned. Refuses with the list of bases and their sizes rather than truncating past its budget. Budgets resolve most-specific-first: explicit flags, then the workspace manifests' `context` tables (per profile, over their `default`), then the built-in profile (session-start, compact, turn), then package defaults \u2014 so a repo tunes its own numbers in .strauss/kb-pins.json without touching hook commands. Like kb_schema and kb_types this takes no bundlePath \u2014 it reads the workspace pin manifests, because which bases a session should see is workspace state, not a property of one base.",
1443
- input: z9.object({
1444
- budgetTokens: z9.number().int().positive().optional().describe(
1714
+ input: z10.object({
1715
+ budgetTokens: z10.number().int().positive().optional().describe(
1445
1716
  "Ceiling on the whole emitted block; past it the command refuses with a list of bases rather than truncating. Defaults to 4000."
1446
1717
  ),
1447
- fullUnderTokens: z9.number().int().positive().optional().describe(
1718
+ fullUnderTokens: z10.number().int().positive().optional().describe(
1448
1719
  "Per-base rendering threshold, applied before the budget: a base whose complete load fits under this arrives as full records instead of index lines, and the whole block still answers to budgetTokens. Off by default \u2014 index-only is the safe default at a context birth, because injected bodies outlive the qualifiers on them; the session-start profile opts tiny bases in at 1500."
1449
1720
  ),
1450
- profile: z9.string().optional().describe(
1721
+ profile: z10.string().optional().describe(
1451
1722
  "Named budget set: built-ins are session-start (full-under 1500), compact and turn (budget 2500); the manifests' `context` tables override per repo. Unknown names fall through to defaults rather than failing."
1452
1723
  ),
1453
- format: z9.enum(["markdown", "json"]).optional().describe(
1724
+ format: z10.enum(["markdown", "json"]).optional().describe(
1454
1725
  "CLI envelope for hook protocols that require strict JSON on stdout. MCP callers omit this \u2014 the block itself is identical."
1455
1726
  ),
1456
- event: z9.string().optional().describe(
1727
+ event: z10.string().optional().describe(
1457
1728
  "hookEventName stamped into the JSON envelope. Only meaningful with format=json."
1458
1729
  )
1459
1730
  }),
@@ -1489,14 +1760,14 @@ var contextCommand = define({
1489
1760
  });
1490
1761
 
1491
1762
  // src/commands/doctor.ts
1492
- import { z as z10 } from "zod";
1493
- var days = (what, fallback) => z10.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
1763
+ import { z as z11 } from "zod";
1764
+ var days = (what, fallback) => z11.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
1494
1765
  var doctorCommand = define({
1495
1766
  name: "doctor",
1496
1767
  tool: "kb_doctor",
1497
1768
  usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--strict]",
1498
1769
  description: "A health sweep over a whole base: what the calendar has already retired, what nobody ever confirmed, what has been open or proposed long enough that the status is now the answer, and what the graph has dropped on the floor. Read-only \u2014 it never writes, never supersedes, and never re-dates anything; every finding names a record for a person to repair. Seven checks, grouped and counted: expired (past `stale_after`), expiring (inside the window), unverified (an empty `verified[]` on a record old enough to matter), aging (still `open` or `proposed`), orphaned (no other record links to it), broken supersession (a chain that does not resolve), and superseded-but-cited (a live record whose body links to a record that no longer holds). Every group is reported even when empty, because a check that found nothing and a check that never ran look identical in a report that only lists findings.\n\nThis is the question no reader thinks to ask, which is why it needs a command: decay is invisible from inside a single record \u2014 a stale one reads exactly like a live one, and a question nobody answered reads exactly like one nobody asked. Reach for it when picking up a base someone else kept, before trusting a base you have not touched in months, or on a schedule; kb_validate is the narrower neighbour, checking only whether pointers between records agree.",
1499
- input: z10.object({
1770
+ input: z11.object({
1500
1771
  bundlePath,
1501
1772
  expiringDays: days(
1502
1773
  "How far ahead `expiring` looks, in days.",
@@ -1510,7 +1781,7 @@ var doctorCommand = define({
1510
1781
  "How long a record may stay `open` or `proposed` before `aging` reports it, in days.",
1511
1782
  DEFAULT_AGING_DAYS
1512
1783
  ),
1513
- strict: z10.boolean().optional().describe(
1784
+ strict: z11.boolean().optional().describe(
1514
1785
  "Turn an expired record into a non-zero exit for the CLI. No effect on the report itself."
1515
1786
  )
1516
1787
  }),
@@ -1540,14 +1811,14 @@ var doctorCommand = define({
1540
1811
  });
1541
1812
  return { bundlePath: path, checkedAt, ...report };
1542
1813
  },
1543
- render: (result) => render(result),
1814
+ render: (result) => render2(result),
1544
1815
  // Only expiry, and only under --strict. The other six checks report debt a
1545
1816
  // reader decides about; an expired record is the base asserting something it
1546
1817
  // already said it would stop standing behind, which is the one finding a
1547
1818
  // pipeline can act on without a judgment call.
1548
1819
  failsWhen: (result, input) => input.strict === true && result.counts.expired > 0
1549
1820
  });
1550
- function render(result) {
1821
+ function render2(result) {
1551
1822
  const { thresholds } = result;
1552
1823
  const lines = [
1553
1824
  `# KB Doctor \u2014 ${result.bundlePath}`,
@@ -1579,13 +1850,13 @@ function render(result) {
1579
1850
  }
1580
1851
 
1581
1852
  // src/commands/list.ts
1582
- import { z as z11 } from "zod";
1853
+ import { z as z12 } from "zod";
1583
1854
  var listCommand = define({
1584
1855
  name: "list",
1585
1856
  tool: "kb_list",
1586
1857
  usage: "list [type]",
1587
1858
  description: "Every record, optionally narrowed to one type. Use kb_query when you have a question; this is for enumerating.",
1588
- input: z11.object({ bundlePath, type: z11.enum(KB_RECORD_TYPES).optional() }),
1859
+ input: z12.object({ bundlePath, type: z12.enum(KB_RECORD_TYPES).optional() }),
1589
1860
  fromArgv: (argv, path) => ({ bundlePath: path, type: argv[1] }),
1590
1861
  run: async ({ store }, { bundlePath: path, type }) => (await store.list(path, type)).map((record) => ({
1591
1862
  conceptId: record.conceptId,
@@ -1597,21 +1868,21 @@ var listCommand = define({
1597
1868
  });
1598
1869
 
1599
1870
  // src/commands/load.ts
1600
- import { z as z12 } from "zod";
1871
+ import { z as z13 } from "zod";
1601
1872
  var loadCommand = define({
1602
1873
  name: "load",
1603
1874
  tool: "kb_load",
1604
- usage: "load [type] [--budget N | --all]",
1605
- description: "Load the whole knowledge base at once, each record with its standing. Prefer this over searching: these bases run to a few thousand tokens, and a reader holding all of it has perfect recall and knows why it is asking, which no ranker does. Superseded records arrive under `superseded` as name, replacement and date only \u2014 their bodies no longer hold, and reading one later in a long session is the mistake this prevents; pass the id to kb_trace when you need the history. Rejected and unresolved records arrive whole: what was turned down, and what is still open, is the part a diff cannot show you. Refuses with a count rather than truncating when the base is too large \u2014 a truncated base is indistinguishable from a complete one, and would have you conclude something was never decided from a slice you did not know was a slice. Call at the point of use, not once per session: a base loaded early is summarised away by compaction, so if the visible context holds no records from this base and the question at hand is one it might govern, load before answering \u2014 never conclude nothing was decided from a context with no KB content in it. This tool (with 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.\n\nThat refusal is the default guardrail, meant for an agent that would otherwise burn its whole context on one call. `all` bypasses it and loads everything regardless of size: a deliberate operator with the budget to spend, not something to reach for automatically. It is mutually exclusive with `budgetTokens`. When the reader does not need everything, kb_query or a narrower `type` filter is the better fit than either.",
1606
- input: z12.object({
1875
+ usage: "load [type] [--budget N] [--all]",
1876
+ description: "Loads the whole knowledge base at once, each record with its standing. Superseded records arrive as stubs (name, replacement, date); rejected and open records arrive whole. Refuses past the token budget rather than truncating \u2014 call kb_catalog, then kb_pack on the record that matters, or narrow with `type`; kb_query for a lookup by wording. `all` bypasses the budget.",
1877
+ input: z13.object({
1607
1878
  bundlePath,
1608
- type: z12.enum(KB_RECORD_TYPES).optional(),
1609
- budgetTokens: z12.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
1610
- all: z12.boolean().optional().describe(
1611
- "Load the entire base regardless of size. The deliberate-operator escape hatch; mutually exclusive with budgetTokens."
1879
+ type: z13.enum(KB_RECORD_TYPES).optional(),
1880
+ budgetTokens: z13.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
1881
+ all: z13.boolean().optional().describe(
1882
+ "Loads the entire base regardless of size, bypassing the token budget; mutually exclusive with budgetTokens."
1612
1883
  )
1613
1884
  }).refine((value) => !(value.all && value.budgetTokens !== void 0), {
1614
- message: "all and budgetTokens are mutually exclusive: pass a ceiling or none, not both."
1885
+ message: "all is mutually exclusive with budgetTokens: pass a ceiling or none, not both."
1615
1886
  }),
1616
1887
  fromArgv: (argv, path) => {
1617
1888
  const budget = argvFlag(argv, "--budget");
@@ -1645,25 +1916,25 @@ var loadCommand = define({
1645
1916
  });
1646
1917
 
1647
1918
  // src/commands/log.ts
1648
- import { z as z13 } from "zod";
1919
+ import { z as z14 } from "zod";
1649
1920
  var logCommand = define({
1650
1921
  name: "log",
1651
1922
  tool: "kb_log",
1652
1923
  usage: "log",
1653
1924
  description: "What touched what, and when. The only artifact here that cannot be reconstructed from the records, so malformed lines are reported rather than repaired.",
1654
- input: z13.object({ bundlePath }),
1925
+ input: z14.object({ bundlePath }),
1655
1926
  fromArgv: (_argv, path) => ({ bundlePath: path }),
1656
1927
  run: ({ store }, { bundlePath: path }) => store.readLog(path)
1657
1928
  });
1658
1929
 
1659
1930
  // src/commands/no-decision.ts
1660
- import { z as z14 } from "zod";
1931
+ import { z as z15 } from "zod";
1661
1932
  var noDecisionCommand = define({
1662
1933
  name: "no-decision",
1663
1934
  tool: "kb_no_decision",
1664
1935
  usage: "no-decision <reason...>",
1665
1936
  description: 'Claim in one sentence that there was nothing to decide. Gating on "did you write a decision?" rewards writing a junk one; gating on "did you answer?" does not, so silence has to be expressible. Idempotent \u2014 restating it is not a collision.',
1666
- input: z14.object({ bundlePath, reason: z14.string().min(1) }),
1937
+ input: z15.object({ bundlePath, reason: z15.string().min(1) }),
1667
1938
  fromArgv: (argv, path) => ({
1668
1939
  bundlePath: path,
1669
1940
  reason: argv.slice(1).join(" ").trim()
@@ -1680,20 +1951,20 @@ var noDecisionCommand = define({
1680
1951
  });
1681
1952
 
1682
1953
  // src/commands/pack.ts
1683
- import { z as z15 } from "zod";
1954
+ import { z as z16 } from "zod";
1684
1955
  var packCommand = define({
1685
1956
  name: "pack",
1686
1957
  tool: "kb_pack",
1687
1958
  usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
1688
1959
  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.",
1689
- input: z15.object({
1960
+ input: z16.object({
1690
1961
  bundlePath,
1691
1962
  conceptId,
1692
- hops: z15.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
1693
- maxNodes: z15.number().int().positive().optional().describe(
1963
+ hops: z16.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
1964
+ maxNodes: z16.number().int().positive().optional().describe(
1694
1965
  "How many records the pack may hold, root included. Defaults to 20."
1695
1966
  ),
1696
- budgetTokens: z15.number().int().positive().optional().describe(
1967
+ budgetTokens: z16.number().int().positive().optional().describe(
1697
1968
  "Approximate token ceiling over what is actually emitted. Defaults to 25000."
1698
1969
  )
1699
1970
  }),
@@ -1715,10 +1986,10 @@ var packCommand = define({
1715
1986
  ...maxNodes !== void 0 ? { maxNodes } : {},
1716
1987
  ...budgetTokens !== void 0 ? { budgetTokens } : {}
1717
1988
  });
1718
- return render2(result, path, now());
1989
+ return render3(result, path, now());
1719
1990
  }
1720
1991
  });
1721
- function render2(result, bundle, at) {
1992
+ function render3(result, bundle, at) {
1722
1993
  const lines = [
1723
1994
  `# KB Pack \u2014 ${result.root}`,
1724
1995
  `bundle: ${bundle}`,
@@ -1780,22 +2051,22 @@ function warningLabel(warning) {
1780
2051
  }
1781
2052
 
1782
2053
  // src/commands/pin.ts
1783
- import { z as z16 } from "zod";
2054
+ import { z as z17 } from "zod";
1784
2055
  var pinCommand = define({
1785
2056
  name: "pin",
1786
2057
  tool: "kb_pin",
1787
2058
  usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
1788
2059
  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.",
1789
- input: z16.object({
2060
+ input: z17.object({
1790
2061
  bundlePath,
1791
- mode: z16.enum(["full", "index"]).optional().describe(
2062
+ mode: z17.enum(["full", "index"]).optional().describe(
1792
2063
  "full: always emit this base's records whole (still under the block budget); index: never upgrade. Absent: the profile's full-under threshold decides."
1793
2064
  ),
1794
- profiles: z16.array(z16.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
1795
- layer: z16.enum(["project", "local", "user"]).optional().describe(
2065
+ profiles: z17.array(z17.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
2066
+ layer: z17.enum(["project", "local", "user"]).optional().describe(
1796
2067
  "Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
1797
2068
  ),
1798
- frozen: z16.boolean().optional().describe(
2069
+ frozen: z17.boolean().optional().describe(
1799
2070
  "true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
1800
2071
  )
1801
2072
  }),
@@ -1824,29 +2095,29 @@ var pinCommand = define({
1824
2095
  });
1825
2096
 
1826
2097
  // src/commands/pins.ts
1827
- import { z as z17 } from "zod";
2098
+ import { z as z18 } from "zod";
1828
2099
  var pinsCommand = define({
1829
2100
  name: "pins",
1830
2101
  tool: "kb_pins",
1831
2102
  usage: "pins",
1832
2103
  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.",
1833
- input: z17.object({}),
2104
+ input: z18.object({}),
1834
2105
  fromArgv: () => ({}),
1835
2106
  run: ({ store }) => listPins(store, process.cwd())
1836
2107
  });
1837
2108
 
1838
2109
  // src/commands/query.ts
1839
- import { z as z18 } from "zod";
2110
+ import { z as z19 } from "zod";
1840
2111
  var queryCommand = define({
1841
2112
  name: "query",
1842
2113
  tool: "kb_query",
1843
2114
  usage: "query <text...>",
1844
- 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.",
1845
- input: z18.object({
2115
+ 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. This is the lookup-by-wording rung, and the narrowest of the three: use it when you know roughly what the record says. The decision rule around it \u2014 while the base fits kb_load's token budget, kb_load it whole, because 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; once kb_load refuses, kb_catalog for one line per record and then kb_pack on the record the work centres on; and kb_query when the question is a point lookup rather than a neighbourhood. A query cannot tell you that nothing was decided \u2014 it returns its nearest hit whatever the distance \u2014 so reach for kb_catalog when the question is what exists. Never read record files directly: this tool (with kb_load, kb_catalog, kb_pack 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.",
2116
+ input: z19.object({
1846
2117
  bundlePath,
1847
- text: z18.string().optional(),
1848
- type: z18.enum(KB_RECORD_TYPES).optional(),
1849
- includeNonCurrent: z18.boolean().optional()
2118
+ text: z19.string().optional(),
2119
+ type: z19.enum(KB_RECORD_TYPES).optional(),
2120
+ includeNonCurrent: z19.boolean().optional()
1850
2121
  }),
1851
2122
  fromArgv: (argv, path) => ({
1852
2123
  bundlePath: path,
@@ -1868,40 +2139,40 @@ var queryCommand = define({
1868
2139
  });
1869
2140
 
1870
2141
  // src/commands/read-index.ts
1871
- import { z as z19 } from "zod";
2142
+ import { z as z20 } from "zod";
1872
2143
  var readIndexCommand = define({
1873
2144
  name: "index",
1874
2145
  tool: "kb_index",
1875
2146
  usage: "index",
1876
2147
  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.",
1877
- input: z19.object({ bundlePath }),
2148
+ input: z20.object({ bundlePath }),
1878
2149
  fromArgv: (_argv, path) => ({ bundlePath: path }),
1879
2150
  run: ({ store }, { bundlePath: path }) => store.readIndex(path)
1880
2151
  });
1881
2152
 
1882
2153
  // src/commands/schema.ts
1883
- import { z as z20 } from "zod";
2154
+ import { z as z21 } from "zod";
1884
2155
  var schemaCommand = define({
1885
2156
  name: "schema",
1886
2157
  tool: "kb_schema",
1887
2158
  usage: "schema",
1888
2159
  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.",
1889
- input: z20.object({}),
2160
+ input: z21.object({}),
1890
2161
  fromArgv: () => ({}),
1891
2162
  run: () => Promise.resolve(kbJsonSchemas())
1892
2163
  });
1893
2164
 
1894
2165
  // src/commands/status.ts
1895
- import { z as z21 } from "zod";
2166
+ import { z as z22 } from "zod";
1896
2167
  var statusCommand = define({
1897
2168
  name: "status",
1898
2169
  tool: "kb_status",
1899
2170
  usage: "status <concept-id> <status>",
1900
2171
  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.",
1901
- input: z21.object({
2172
+ input: z22.object({
1902
2173
  bundlePath,
1903
2174
  conceptId,
1904
- status: z21.enum(KB_RECORD_STATUSES)
2175
+ status: z22.enum(KB_RECORD_STATUSES)
1905
2176
  }),
1906
2177
  fromArgv: (argv, path) => ({
1907
2178
  bundlePath: path,
@@ -1916,13 +2187,13 @@ var statusCommand = define({
1916
2187
  });
1917
2188
 
1918
2189
  // src/commands/supersede.ts
1919
- import { z as z22 } from "zod";
2190
+ import { z as z23 } from "zod";
1920
2191
  var supersedeCommand = define({
1921
2192
  name: "supersede",
1922
2193
  tool: "kb_supersede",
1923
2194
  usage: "supersede <concept-id> <replacement-id>",
1924
2195
  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.",
1925
- input: z22.object({ bundlePath, conceptId, replacementId: conceptId }),
2196
+ input: z23.object({ bundlePath, conceptId, replacementId: conceptId }),
1926
2197
  fromArgv: (argv, path) => ({
1927
2198
  bundlePath: path,
1928
2199
  conceptId: argv[1],
@@ -1936,16 +2207,16 @@ var supersedeCommand = define({
1936
2207
  });
1937
2208
 
1938
2209
  // src/commands/sync-instructions.ts
1939
- import { z as z23 } from "zod";
2210
+ import { z as z24 } from "zod";
1940
2211
  var syncInstructionsCommand = define({
1941
2212
  name: "sync-instructions",
1942
2213
  usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
1943
2214
  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.",
1944
- input: z23.object({
1945
- file: z23.string().min(1).describe("The instruction file to edit in place."),
1946
- budgetTokens: z23.number().int().positive().optional(),
1947
- fullUnderTokens: z23.number().int().positive().optional(),
1948
- profile: z23.string().optional()
2215
+ input: z24.object({
2216
+ file: z24.string().min(1).describe("The instruction file to edit in place."),
2217
+ budgetTokens: z24.number().int().positive().optional(),
2218
+ fullUnderTokens: z24.number().int().positive().optional(),
2219
+ profile: z24.string().optional()
1949
2220
  }),
1950
2221
  fromArgv: (argv) => {
1951
2222
  const budget = argvFlag(argv, "--budget");
@@ -1971,17 +2242,17 @@ var syncInstructionsCommand = define({
1971
2242
  });
1972
2243
 
1973
2244
  // src/commands/trace.ts
1974
- import { z as z24 } from "zod";
2245
+ import { z as z25 } from "zod";
1975
2246
  var traceCommand = define({
1976
2247
  name: "trace",
1977
2248
  tool: "kb_trace",
1978
2249
  usage: "trace <concept-id> [edges...]",
1979
2250
  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.',
1980
- input: z24.object({
2251
+ input: z25.object({
1981
2252
  bundlePath,
1982
2253
  conceptId,
1983
- edges: z24.array(z24.enum(TRACE_EDGES)).optional(),
1984
- depth: z24.number().int().positive().optional()
2254
+ edges: z25.array(z25.enum(TRACE_EDGES)).optional(),
2255
+ depth: z25.number().int().positive().optional()
1985
2256
  }),
1986
2257
  fromArgv: (argv, path) => ({
1987
2258
  bundlePath: path,
@@ -2003,53 +2274,53 @@ var traceCommand = define({
2003
2274
  });
2004
2275
 
2005
2276
  // src/commands/types.ts
2006
- import { z as z25 } from "zod";
2277
+ import { z as z26 } from "zod";
2007
2278
  var typesCommand = define({
2008
2279
  name: "types",
2009
2280
  tool: "kb_types",
2010
2281
  usage: "types",
2011
2282
  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.",
2012
- input: z25.object({}),
2283
+ input: z26.object({}),
2013
2284
  fromArgv: () => ({}),
2014
2285
  run: () => Promise.resolve(RECORD_TYPES)
2015
2286
  });
2016
2287
 
2017
2288
  // src/commands/unpin.ts
2018
- import { z as z26 } from "zod";
2289
+ import { z as z27 } from "zod";
2019
2290
  var unpinCommand = define({
2020
2291
  name: "unpin",
2021
2292
  tool: "kb_unpin",
2022
2293
  usage: "unpin [bundle-path]",
2023
2294
  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.",
2024
- input: z26.object({ bundlePath }),
2295
+ input: z27.object({ bundlePath }),
2025
2296
  fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
2026
2297
  run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
2027
2298
  });
2028
2299
 
2029
2300
  // src/commands/validate.ts
2030
- import { z as z27 } from "zod";
2301
+ import { z as z28 } from "zod";
2031
2302
  var validateCommand = define({
2032
2303
  name: "validate",
2033
2304
  tool: "kb_validate",
2034
2305
  usage: "validate",
2035
2306
  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.",
2036
- input: z27.object({ bundlePath }),
2307
+ input: z28.object({ bundlePath }),
2037
2308
  fromArgv: (_argv, path) => ({ bundlePath: path }),
2038
2309
  run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
2039
2310
  failsWhen: (result) => Array.isArray(result) && result.length > 0
2040
2311
  });
2041
2312
 
2042
2313
  // src/commands/verify.ts
2043
- import { z as z28 } from "zod";
2314
+ import { z as z29 } from "zod";
2044
2315
  var verifyCommand = define({
2045
2316
  name: "verify",
2046
2317
  tool: "kb_verify",
2047
2318
  usage: "verify <concept-id> --note <text>",
2048
2319
  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.",
2049
- input: z28.object({
2320
+ input: z29.object({
2050
2321
  bundlePath,
2051
2322
  conceptId,
2052
- note: z28.string().refine((s) => s.trim().length > 0, {
2323
+ note: z29.string().refine((s) => s.trim().length > 0, {
2053
2324
  message: "note must say what the check found"
2054
2325
  })
2055
2326
  }),
@@ -2069,7 +2340,7 @@ var verifyCommand = define({
2069
2340
  });
2070
2341
 
2071
2342
  // src/commands/write.ts
2072
- import { z as z29 } from "zod";
2343
+ import { z as z30 } from "zod";
2073
2344
  var writeCommand = define({
2074
2345
  name: "write",
2075
2346
  tool: "kb_write",
@@ -2083,9 +2354,9 @@ var writeCommand = define({
2083
2354
  "- Prefer a new record over overloading an existing one, and keep each short. A record nobody finishes reading is not durable memory.",
2084
2355
  "- Records are never deleted; supersede instead, so the earlier reasoning stays inspectable."
2085
2356
  ].join("\n"),
2086
- input: z29.object({
2357
+ input: z30.object({
2087
2358
  bundlePath,
2088
- type: z29.enum(KB_RECORD_TYPES),
2359
+ type: z30.enum(KB_RECORD_TYPES),
2089
2360
  input: composeInputSchema
2090
2361
  }),
2091
2362
  fromArgv: async (argv, path, stdin) => ({
@@ -2109,7 +2380,7 @@ var writeCommand = define({
2109
2380
  });
2110
2381
 
2111
2382
  // src/commands/write-decision.ts
2112
- import { z as z30 } from "zod";
2383
+ import { z as z31 } from "zod";
2113
2384
  var writeDecisionCommand = define({
2114
2385
  name: "write-decision",
2115
2386
  tool: "kb_write_decision",
@@ -2122,7 +2393,7 @@ var writeDecisionCommand = define({
2122
2393
  "- `alternative` is what you turned down and why, not a list of everything considered.",
2123
2394
  "- A reference to material you read goes in `sources`; a reference to code goes in `anchors`; a reference to another record goes in `relatedConceptIds`."
2124
2395
  ].join("\n"),
2125
- input: z30.object({ bundlePath, input: decisionInputSchema }),
2396
+ input: z31.object({ bundlePath, input: decisionInputSchema }),
2126
2397
  fromArgv: async (_argv, path, stdin) => ({
2127
2398
  bundlePath: path,
2128
2399
  input: JSON.parse(await stdin())
@@ -2152,6 +2423,7 @@ var KB_COMMANDS = [
2152
2423
  answerCommand,
2153
2424
  verifyCommand,
2154
2425
  loadCommand,
2426
+ catalogCommand,
2155
2427
  packCommand,
2156
2428
  queryCommand,
2157
2429
  traceCommand,
@@ -2197,141 +2469,6 @@ function parseMarkdownWithFrontmatter(text, schema) {
2197
2469
  };
2198
2470
  }
2199
2471
 
2200
- // src/errors.ts
2201
- var Fault = /* @__PURE__ */ ((Fault2) => {
2202
- Fault2["Configuration"] = "Configuration";
2203
- Fault2["System"] = "System";
2204
- Fault2["User"] = "User";
2205
- return Fault2;
2206
- })(Fault || {});
2207
- var ErrorTypes = /* @__PURE__ */ ((ErrorTypes2) => {
2208
- ErrorTypes2["KbRecordAlreadyExists"] = "KbRecordAlreadyExists";
2209
- ErrorTypes2["KbInvalidConceptId"] = "KbInvalidConceptId";
2210
- ErrorTypes2["KbPackBudgetExceeded"] = "KbPackBudgetExceeded";
2211
- ErrorTypes2["KbRecordNotFound"] = "KbRecordNotFound";
2212
- ErrorTypes2["KbSelfVerification"] = "KbSelfVerification";
2213
- ErrorTypes2["KbWriteConflict"] = "KbWriteConflict";
2214
- return ErrorTypes2;
2215
- })(ErrorTypes || {});
2216
- var BaseError = class extends Error {
2217
- code;
2218
- errorType;
2219
- fault;
2220
- retriable;
2221
- reportToUser;
2222
- details;
2223
- constructor(props) {
2224
- super(props.message);
2225
- this.name = props.name ?? this.constructor.name;
2226
- this.code = props.code ?? 500;
2227
- this.errorType = props.errorType;
2228
- this.fault = props.fault;
2229
- this.retriable = props.retriable ?? true;
2230
- this.reportToUser = props.reportToUser ?? false;
2231
- this.details = props.details;
2232
- }
2233
- };
2234
-
2235
- // src/kb-errors.ts
2236
- var KbRecordAlreadyExistsError = class extends BaseError {
2237
- constructor(conceptId2) {
2238
- super({
2239
- message: `kb: ${conceptId2} already exists \u2014 choose a more specific slug, or write with overwrite`,
2240
- errorType: "KbRecordAlreadyExists" /* KbRecordAlreadyExists */,
2241
- code: 409,
2242
- fault: "User" /* User */,
2243
- retriable: false,
2244
- reportToUser: true,
2245
- details: { conceptId: conceptId2, action: "refused" }
2246
- });
2247
- this.conceptId = conceptId2;
2248
- }
2249
- conceptId;
2250
- };
2251
- var KbRecordNotFoundError = class extends BaseError {
2252
- constructor(conceptId2) {
2253
- super({
2254
- message: `kb: ${conceptId2} does not exist`,
2255
- errorType: "KbRecordNotFound" /* KbRecordNotFound */,
2256
- code: 404,
2257
- fault: "User" /* User */,
2258
- retriable: false,
2259
- reportToUser: true,
2260
- details: { conceptId: conceptId2 }
2261
- });
2262
- this.conceptId = conceptId2;
2263
- }
2264
- conceptId;
2265
- };
2266
- var KbWriteConflictError = class extends BaseError {
2267
- constructor(conceptId2) {
2268
- super({
2269
- message: `kb: ${conceptId2} changed while it was being updated \u2014 re-read and retry`,
2270
- errorType: "KbWriteConflict" /* KbWriteConflict */,
2271
- code: 409,
2272
- fault: "System" /* System */,
2273
- retriable: true,
2274
- reportToUser: true,
2275
- details: { conceptId: conceptId2 }
2276
- });
2277
- this.conceptId = conceptId2;
2278
- }
2279
- conceptId;
2280
- };
2281
- var KbSelfVerificationError = class extends BaseError {
2282
- constructor(conceptId2, actor, generatedBy) {
2283
- super({
2284
- message: `kb: ${conceptId2} was generated by ${generatedBy}, and a record's generator cannot verify it \u2014 only a human or a different actor can`,
2285
- errorType: "KbSelfVerification" /* KbSelfVerification */,
2286
- code: 400,
2287
- fault: "User" /* User */,
2288
- retriable: false,
2289
- reportToUser: true,
2290
- details: { conceptId: conceptId2, actor, generatedBy, action: "refused" }
2291
- });
2292
- this.conceptId = conceptId2;
2293
- this.actor = actor;
2294
- this.generatedBy = generatedBy;
2295
- }
2296
- conceptId;
2297
- actor;
2298
- generatedBy;
2299
- };
2300
- var KbPackBudgetExceededError = class extends BaseError {
2301
- constructor(recordCount, approxTokens2, budgetTokens, excluded) {
2302
- super({
2303
- message: `kb: a pack of ${recordCount} records is ~${approxTokens2} tokens against a budget of ${budgetTokens} \u2014 lower hops or maxNodes, or raise the budget`,
2304
- errorType: "KbPackBudgetExceeded" /* KbPackBudgetExceeded */,
2305
- code: 400,
2306
- fault: "User" /* User */,
2307
- retriable: false,
2308
- reportToUser: true,
2309
- details: { recordCount, approxTokens: approxTokens2, budgetTokens, excluded }
2310
- });
2311
- this.recordCount = recordCount;
2312
- this.approxTokens = approxTokens2;
2313
- this.budgetTokens = budgetTokens;
2314
- this.excluded = excluded;
2315
- }
2316
- recordCount;
2317
- approxTokens;
2318
- budgetTokens;
2319
- excluded;
2320
- };
2321
- var KbInvalidConceptIdError = class extends BaseError {
2322
- constructor(message, details) {
2323
- super({
2324
- message: `kb: ${message}`,
2325
- errorType: "KbInvalidConceptId" /* KbInvalidConceptId */,
2326
- code: 400,
2327
- fault: "User" /* User */,
2328
- retriable: false,
2329
- reportToUser: true,
2330
- details
2331
- });
2332
- }
2333
- };
2334
-
2335
2472
  // src/search-index.ts
2336
2473
  import { stat } from "fs/promises";
2337
2474
  import { join as join3 } from "path";
@@ -2712,9 +2849,16 @@ ${answer}
2712
2849
  * is indistinguishable from a complete one, so a caller would answer "that
2713
2850
  * was never decided" from a slice it did not know was a slice.
2714
2851
  *
2715
- * That refusal is the default guardrail. `all` bypasses it outright and
2716
- * always hands back the whole bundle: an explicit, never-accidental escape
2717
- * hatch for an operator who has the budget to spend, not a wider default.
2852
+ * A token budget decides that, measured over what is actually handed back.
2853
+ * The refusal names the estimate and the budget, because a caller told only
2854
+ * "too big" cannot tell whether to narrow the type filter, raise the budget,
2855
+ * or stop loading the base whole altogether. Past the budget the answer is
2856
+ * the catalog and then a pack, which is what the refusal says.
2857
+ *
2858
+ * That refusal is the default guardrail. `all` bypasses the budget outright
2859
+ * and always hands back the whole bundle: an explicit, never-accidental
2860
+ * escape hatch for an operator who has the budget to spend, not a wider
2861
+ * default.
2718
2862
  */
2719
2863
  async load(bundlePath2, options = {}) {
2720
2864
  const budgetTokens = options.budgetTokens ?? DEFAULT_LOAD_BUDGET;
@@ -2729,7 +2873,12 @@ ${answer}
2729
2873
  loaded: false,
2730
2874
  recordCount: wanted.length,
2731
2875
  approxTokens: approxTokens2,
2732
- budgetTokens
2876
+ budgetTokens,
2877
+ message: refusalMessage({
2878
+ approxTokens: approxTokens2,
2879
+ budgetTokens,
2880
+ type: options.type
2881
+ })
2733
2882
  };
2734
2883
  }
2735
2884
  return {
@@ -2745,6 +2894,10 @@ ${answer}
2745
2894
  async trace(bundlePath2, seedId, options = {}) {
2746
2895
  return trace(seedId, await this.list(bundlePath2), options);
2747
2896
  }
2897
+ /** Every record named in one line each. See `catalog.ts`. */
2898
+ async catalog(bundlePath2, options = {}) {
2899
+ return catalog(await this.list(bundlePath2), options);
2900
+ }
2748
2901
  /** A bounded neighbourhood around one record. See `pack.ts`. */
2749
2902
  async pack(bundlePath2, rootId, options = {}) {
2750
2903
  return pack(await this.list(bundlePath2), rootId, options);
@@ -3011,6 +3164,14 @@ function estimateTokens(record) {
3011
3164
  function estimateStubTokens(entry) {
3012
3165
  return Math.ceil(JSON.stringify(entry).length / 4);
3013
3166
  }
3167
+ function refusalMessage(refusal) {
3168
+ const scope = refusal.type ? ` of type ${refusal.type}` : "";
3169
+ return [
3170
+ `Refusing to load this base whole: ~${refusal.approxTokens} tokens is past the ${refusal.budgetTokens}-token budget.`,
3171
+ `Call kb_catalog for one line per record${scope} (id, type, title, standing), then kb_pack on the record that matters; kb_query works for a lookup by wording.`,
3172
+ `To load anyway: raise budgetTokens (currently ${refusal.budgetTokens}), or all=true to bypass the budget.`
3173
+ ].join(" ");
3174
+ }
3014
3175
  function stub(hit) {
3015
3176
  return {
3016
3177
  conceptId: hit.record.conceptId,
@@ -3119,7 +3280,7 @@ function typeRank(record) {
3119
3280
  }
3120
3281
 
3121
3282
  // src/version.ts
3122
- var VERSION = true ? "0.1.9" : "0.0.0-dev";
3283
+ var VERSION = true ? "0.1.10" : "0.0.0-dev";
3123
3284
 
3124
3285
  export {
3125
3286
  kbSourceSchema,
@@ -3159,8 +3320,20 @@ export {
3159
3320
  listPins,
3160
3321
  pinBase,
3161
3322
  unpinBase,
3323
+ Fault,
3324
+ ErrorTypes,
3325
+ BaseError,
3326
+ KbRecordAlreadyExistsError,
3327
+ KbRecordNotFoundError,
3328
+ KbWriteConflictError,
3329
+ KbSelfVerificationError,
3330
+ KbPackBudgetExceededError,
3331
+ KbMissingFlagValueError,
3332
+ KbInvalidConceptIdError,
3162
3333
  adjudicate,
3163
3334
  resolveHeads,
3335
+ catalog,
3336
+ renderCatalogLine,
3164
3337
  INDEX_FILE,
3165
3338
  renderIndex,
3166
3339
  renderIndexLine,
@@ -3192,15 +3365,6 @@ export {
3192
3365
  stringifyMarkdownWithFrontmatter,
3193
3366
  splitMarkdownFrontmatter,
3194
3367
  parseMarkdownWithFrontmatter,
3195
- Fault,
3196
- ErrorTypes,
3197
- BaseError,
3198
- KbRecordAlreadyExistsError,
3199
- KbRecordNotFoundError,
3200
- KbWriteConflictError,
3201
- KbSelfVerificationError,
3202
- KbPackBudgetExceededError,
3203
- KbInvalidConceptIdError,
3204
3368
  SEARCH_INDEX_FILE,
3205
3369
  searchBase,
3206
3370
  resolveHits,
@@ -3213,4 +3377,4 @@ export {
3213
3377
  KbStore,
3214
3378
  VERSION
3215
3379
  };
3216
- //# sourceMappingURL=chunk-OFDWRMY6.js.map
3380
+ //# sourceMappingURL=chunk-EJQPZWN5.js.map