@saasontools/strauss-kb 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli-main.cjs CHANGED
@@ -45,6 +45,11 @@ var kbActorStampSchema = import_zod.z.object({
45
45
  by: import_zod.z.string().min(1),
46
46
  at: import_zod.z.string().min(1)
47
47
  }).passthrough();
48
+ var kbVerifiedEventSchema = kbActorStampSchema.extend({
49
+ note: import_zod.z.string().refine((s) => s.trim().length > 0, {
50
+ message: "note must say what the check found"
51
+ })
52
+ });
48
53
  var kbAnchorSchema = import_zod.z.object({
49
54
  file: import_zod.z.string().min(1),
50
55
  symbol: import_zod.z.string().min(1).optional()
@@ -1073,25 +1078,32 @@ var import_zod9 = require("zod");
1073
1078
  var loadCommand = define({
1074
1079
  name: "load",
1075
1080
  tool: "kb_load",
1076
- usage: "load [type] [--budget N]",
1077
- 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.",
1081
+ usage: "load [type] [--budget N | --all]",
1082
+ 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.",
1078
1083
  input: import_zod9.z.object({
1079
1084
  bundlePath,
1080
1085
  type: import_zod9.z.enum(KB_RECORD_TYPES).optional(),
1081
- budgetTokens: import_zod9.z.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000.")
1086
+ budgetTokens: import_zod9.z.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
1087
+ all: import_zod9.z.boolean().optional().describe(
1088
+ "Load the entire base regardless of size. The deliberate-operator escape hatch; mutually exclusive with budgetTokens."
1089
+ )
1090
+ }).refine((value) => !(value.all && value.budgetTokens !== void 0), {
1091
+ message: "all and budgetTokens are mutually exclusive: pass a ceiling or none, not both."
1082
1092
  }),
1083
1093
  fromArgv: (argv, path) => {
1084
1094
  const budget = argvFlag(argv, "--budget");
1085
1095
  return {
1086
1096
  bundlePath: path,
1087
- ...argv[1] && argv[1] !== "--budget" ? { type: argv[1] } : {},
1088
- ...budget ? { budgetTokens: Number(budget) } : {}
1097
+ ...argv[1] && !argv[1].startsWith("--") ? { type: argv[1] } : {},
1098
+ ...budget ? { budgetTokens: Number(budget) } : {},
1099
+ ...argv.includes("--all") ? { all: true } : {}
1089
1100
  };
1090
1101
  },
1091
- run: async ({ store }, { bundlePath: path, type, budgetTokens }) => {
1102
+ run: async ({ store }, { bundlePath: path, type, budgetTokens, all }) => {
1092
1103
  const result = await store.load(path, {
1093
1104
  ...type ? { type } : {},
1094
- ...budgetTokens ? { budgetTokens } : {}
1105
+ ...budgetTokens ? { budgetTokens } : {},
1106
+ ...all ? { all } : {}
1095
1107
  });
1096
1108
  if (!result.loaded) return result;
1097
1109
  return {
@@ -1569,8 +1581,37 @@ var validateCommand = define({
1569
1581
  failsWhen: (result) => Array.isArray(result) && result.length > 0
1570
1582
  });
1571
1583
 
1572
- // src/commands/write.ts
1584
+ // src/commands/verify.ts
1573
1585
  var import_zod26 = require("zod");
1586
+ var verifyCommand = define({
1587
+ name: "verify",
1588
+ tool: "kb_verify",
1589
+ usage: "verify <concept-id> --note <text>",
1590
+ description: "Append one verified[] event \u2014 who checked the record, when, and what the check found. Appends only; prior events are never rewritten. A record's own generator is refused unless the actor is human: re-reading your own output is not an independent check.",
1591
+ input: import_zod26.z.object({
1592
+ bundlePath,
1593
+ conceptId,
1594
+ note: import_zod26.z.string().refine((s) => s.trim().length > 0, {
1595
+ message: "note must say what the check found"
1596
+ })
1597
+ }),
1598
+ fromArgv: (argv, path) => ({
1599
+ bundlePath: path,
1600
+ conceptId: argv[1],
1601
+ note: argvFlag(argv, "--note")
1602
+ }),
1603
+ run: async ({ store, actor, now }, { bundlePath: path, conceptId: id, note }) => {
1604
+ await assertBaseNotFrozen(process.cwd(), path);
1605
+ const record = await store.verify(path, id, note, actor, now());
1606
+ return {
1607
+ conceptId: record.conceptId,
1608
+ verified: record.frontmatter.verified?.length ?? 0
1609
+ };
1610
+ }
1611
+ });
1612
+
1613
+ // src/commands/write.ts
1614
+ var import_zod27 = require("zod");
1574
1615
  var writeCommand = define({
1575
1616
  name: "write",
1576
1617
  tool: "kb_write",
@@ -1584,9 +1625,9 @@ var writeCommand = define({
1584
1625
  "- Prefer a new record over overloading an existing one, and keep each short. A record nobody finishes reading is not durable memory.",
1585
1626
  "- Records are never deleted; supersede instead, so the earlier reasoning stays inspectable."
1586
1627
  ].join("\n"),
1587
- input: import_zod26.z.object({
1628
+ input: import_zod27.z.object({
1588
1629
  bundlePath,
1589
- type: import_zod26.z.enum(KB_RECORD_TYPES),
1630
+ type: import_zod27.z.enum(KB_RECORD_TYPES),
1590
1631
  input: composeInputSchema
1591
1632
  }),
1592
1633
  fromArgv: async (argv, path, stdin) => ({
@@ -1610,7 +1651,7 @@ var writeCommand = define({
1610
1651
  });
1611
1652
 
1612
1653
  // src/commands/write-decision.ts
1613
- var import_zod27 = require("zod");
1654
+ var import_zod28 = require("zod");
1614
1655
  var writeDecisionCommand = define({
1615
1656
  name: "write-decision",
1616
1657
  tool: "kb_write_decision",
@@ -1623,7 +1664,7 @@ var writeDecisionCommand = define({
1623
1664
  "- `alternative` is what you turned down and why, not a list of everything considered.",
1624
1665
  "- A reference to material you read goes in `sources`; a reference to code goes in `anchors`; a reference to another record goes in `relatedConceptIds`."
1625
1666
  ].join("\n"),
1626
- input: import_zod27.z.object({ bundlePath, input: decisionInputSchema }),
1667
+ input: import_zod28.z.object({ bundlePath, input: decisionInputSchema }),
1627
1668
  fromArgv: async (_argv, path, stdin) => ({
1628
1669
  bundlePath: path,
1629
1670
  input: JSON.parse(await stdin())
@@ -1651,6 +1692,7 @@ var KB_COMMANDS = [
1651
1692
  statusCommand,
1652
1693
  supersedeCommand,
1653
1694
  answerCommand,
1695
+ verifyCommand,
1654
1696
  loadCommand,
1655
1697
  queryCommand,
1656
1698
  traceCommand,
@@ -1766,6 +1808,25 @@ var KbWriteConflictError = class extends BaseError {
1766
1808
  }
1767
1809
  conceptId;
1768
1810
  };
1811
+ var KbSelfVerificationError = class extends BaseError {
1812
+ constructor(conceptId2, actor, generatedBy) {
1813
+ super({
1814
+ message: `kb: ${conceptId2} was generated by ${generatedBy}, and a record's generator cannot verify it \u2014 only a human or a different actor can`,
1815
+ errorType: "KbSelfVerification" /* KbSelfVerification */,
1816
+ code: 400,
1817
+ fault: "User" /* User */,
1818
+ retriable: false,
1819
+ reportToUser: true,
1820
+ details: { conceptId: conceptId2, actor, generatedBy, action: "refused" }
1821
+ });
1822
+ this.conceptId = conceptId2;
1823
+ this.actor = actor;
1824
+ this.generatedBy = generatedBy;
1825
+ }
1826
+ conceptId;
1827
+ actor;
1828
+ generatedBy;
1829
+ };
1769
1830
  var KbInvalidConceptIdError = class extends BaseError {
1770
1831
  constructor(message, details) {
1771
1832
  super({
@@ -1984,6 +2045,40 @@ var KbStore = class {
1984
2045
  { operation: `status:${status}`, by: actor }
1985
2046
  );
1986
2047
  }
2048
+ /**
2049
+ * Appends one `verified[]` event: who checked the record, when, and what the
2050
+ * check found. Append-only — prior events are history, and are spread into
2051
+ * the new array untouched rather than reshaped through the write schema.
2052
+ *
2053
+ * A record's generator cannot verify its own record unless the actor is
2054
+ * human: the generator re-reading its own output is not an independent
2055
+ * check. The rule runs before the mutation so a refusal never publishes,
2056
+ * and the refusal is logged under its own operation name — `mutate` only
2057
+ * logs what it publishes.
2058
+ */
2059
+ async verify(bundlePath2, conceptId2, note, actor = "unknown", at = (/* @__PURE__ */ new Date()).toISOString()) {
2060
+ const event = kbVerifiedEventSchema.parse({ by: actor, at, note });
2061
+ const existing = await this.read(bundlePath2, conceptId2);
2062
+ if (!existing) throw new KbRecordNotFoundError(conceptId2);
2063
+ const generatedBy = existing.frontmatter.generated?.by;
2064
+ if (generatedBy !== void 0 && actor.toLowerCase() === generatedBy.toLowerCase() && !normalizeActor(actor).startsWith("human:")) {
2065
+ await this.record(this.root(bundlePath2), {
2066
+ operation: "verify:refused",
2067
+ conceptId: conceptId2,
2068
+ by: actor
2069
+ });
2070
+ throw new KbSelfVerificationError(conceptId2, actor, generatedBy);
2071
+ }
2072
+ return this.mutate(
2073
+ bundlePath2,
2074
+ conceptId2,
2075
+ (frontmatter) => ({
2076
+ ...frontmatter,
2077
+ verified: [...frontmatter.verified ?? [], event]
2078
+ }),
2079
+ { operation: "verify", by: actor }
2080
+ );
2081
+ }
1987
2082
  /**
1988
2083
  * Marks `conceptId` superseded by `replacementId`, and links both directions.
1989
2084
  *
@@ -2087,6 +2182,10 @@ ${answer}
2087
2182
  * Refuses rather than truncates when the base is too large. A truncated base
2088
2183
  * is indistinguishable from a complete one, so a caller would answer "that
2089
2184
  * was never decided" from a slice it did not know was a slice.
2185
+ *
2186
+ * That refusal is the default guardrail. `all` bypasses it outright and
2187
+ * always hands back the whole bundle: an explicit, never-accidental escape
2188
+ * hatch for an operator who has the budget to spend, not a wider default.
2090
2189
  */
2091
2190
  async load(bundlePath2, options = {}) {
2092
2191
  const budgetTokens = options.budgetTokens ?? DEFAULT_LOAD_BUDGET;
@@ -2096,7 +2195,7 @@ ${answer}
2096
2195
  const records = adjudicated.filter((hit) => hit.standing !== "superseded");
2097
2196
  const superseded = adjudicated.filter((hit) => hit.standing === "superseded").map(stub);
2098
2197
  const approxTokens2 = records.reduce((total, hit) => total + estimateTokens(hit.record), 0) + superseded.reduce((total, entry) => total + estimateStubTokens(entry), 0);
2099
- if (approxTokens2 > budgetTokens) {
2198
+ if (!options.all && approxTokens2 > budgetTokens) {
2100
2199
  return {
2101
2200
  loaded: false,
2102
2201
  recordCount: wanted.length,
@@ -2107,8 +2206,8 @@ ${answer}
2107
2206
  return {
2108
2207
  loaded: true,
2109
2208
  recordCount: wanted.length,
2110
- approxTokens: approxTokens2,
2111
- budgetTokens,
2209
+ tokensLoaded: approxTokens2,
2210
+ budgetTokens: options.all ? null : budgetTokens,
2112
2211
  records,
2113
2212
  superseded
2114
2213
  };
@@ -2314,6 +2413,11 @@ function matches(record, needle) {
2314
2413
  (field) => field?.toLowerCase().includes(needle)
2315
2414
  );
2316
2415
  }
2416
+ function normalizeActor(id) {
2417
+ const colon = id.indexOf(":");
2418
+ if (colon === -1) return id.toLowerCase();
2419
+ return id.slice(0, colon + 1).toLowerCase() + id.slice(colon + 1);
2420
+ }
2317
2421
  function digest(contents) {
2318
2422
  return (0, import_node_crypto.createHash)("sha256").update(contents).digest("hex");
2319
2423
  }