@saasontools/strauss-kb 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -318,8 +318,9 @@ type KbLoadResult = {
318
318
  /** Named only. Their bodies are reachable through `trace`. */
319
319
  superseded: KbSupersededStub[];
320
320
  recordCount: number;
321
- approxTokens: number;
322
- budgetTokens: number;
321
+ tokensLoaded: number;
322
+ /** `null` when loaded via `all`: no ceiling was applied. */
323
+ budgetTokens: number | null;
323
324
  } | {
324
325
  loaded: false;
325
326
  recordCount: number;
@@ -334,6 +335,12 @@ type KbWriteInput = {
334
335
  /** Replace an existing record rather than failing on the collision. */
335
336
  overwrite?: boolean;
336
337
  };
338
+ type KbWriteResult = KbRecord & {
339
+ /** Whether this write also marked prior records superseded. */
340
+ action: "created" | "superseded-prior";
341
+ /** `frontmatter.strauss_supersedes` ids that were actually marked. */
342
+ supersededIds: string[];
343
+ };
337
344
  /**
338
345
  * Reads and writes a knowledge bundle.
339
346
  *
@@ -357,7 +364,7 @@ declare class KbStore {
357
364
  * concept id, so a caller cannot produce a file whose identity disagrees with
358
365
  * its contents.
359
366
  */
360
- write(bundlePath: string, input: KbWriteInput, actor?: string): Promise<KbRecord>;
367
+ write(bundlePath: string, input: KbWriteInput, actor?: string): Promise<KbWriteResult>;
361
368
  /** One record by concept id, or null when it does not exist. */
362
369
  read(bundlePath: string, conceptId: string): Promise<KbRecord | null>;
363
370
  /**
@@ -424,10 +431,15 @@ declare class KbStore {
424
431
  * Refuses rather than truncates when the base is too large. A truncated base
425
432
  * is indistinguishable from a complete one, so a caller would answer "that
426
433
  * was never decided" from a slice it did not know was a slice.
434
+ *
435
+ * That refusal is the default guardrail. `all` bypasses it outright and
436
+ * always hands back the whole bundle: an explicit, never-accidental escape
437
+ * hatch for an operator who has the budget to spend, not a wider default.
427
438
  */
428
439
  load(bundlePath: string, options?: {
429
440
  budgetTokens?: number;
430
441
  type?: string;
442
+ all?: boolean;
431
443
  }): Promise<KbLoadResult>;
432
444
  /** How a position was arrived at, as a timeline. See `trace.ts`. */
433
445
  trace(bundlePath: string, seedId: string, options?: KbTraceOptions): Promise<KbTraceStep[]>;
@@ -447,6 +459,19 @@ declare class KbStore {
447
459
  * knows which agent touched what. So a bad line is surfaced and left alone.
448
460
  */
449
461
  readLog(bundlePath: string): Promise<ReturnType<typeof parseLog>>;
462
+ /**
463
+ * `markSuperseded`, tolerant of the two ways it legitimately doesn't land:
464
+ * a missing target (a broken link, legal per compose.ts) or a CAS conflict
465
+ * from a concurrent writer touching the same target. A conflict is retried
466
+ * a bounded number of times — each attempt re-reads the target fresh — and
467
+ * on the last, `false` reports "not marked" rather than throwing: the
468
+ * caller's own record is already published, so failing here would leave
469
+ * that publish unreported instead of undone. kb_validate's existing
470
+ * "not marked superseded" check is what surfaces the residue.
471
+ */
472
+ private markSupersededRetrying;
473
+ /** The one-directional half of `supersede`: marks `conceptId` superseded. */
474
+ private markSuperseded;
450
475
  private mutate;
451
476
  /**
452
477
  * Two guarantees, both about writers running in parallel.
package/dist/index.d.ts CHANGED
@@ -318,8 +318,9 @@ type KbLoadResult = {
318
318
  /** Named only. Their bodies are reachable through `trace`. */
319
319
  superseded: KbSupersededStub[];
320
320
  recordCount: number;
321
- approxTokens: number;
322
- budgetTokens: number;
321
+ tokensLoaded: number;
322
+ /** `null` when loaded via `all`: no ceiling was applied. */
323
+ budgetTokens: number | null;
323
324
  } | {
324
325
  loaded: false;
325
326
  recordCount: number;
@@ -334,6 +335,12 @@ type KbWriteInput = {
334
335
  /** Replace an existing record rather than failing on the collision. */
335
336
  overwrite?: boolean;
336
337
  };
338
+ type KbWriteResult = KbRecord & {
339
+ /** Whether this write also marked prior records superseded. */
340
+ action: "created" | "superseded-prior";
341
+ /** `frontmatter.strauss_supersedes` ids that were actually marked. */
342
+ supersededIds: string[];
343
+ };
337
344
  /**
338
345
  * Reads and writes a knowledge bundle.
339
346
  *
@@ -357,7 +364,7 @@ declare class KbStore {
357
364
  * concept id, so a caller cannot produce a file whose identity disagrees with
358
365
  * its contents.
359
366
  */
360
- write(bundlePath: string, input: KbWriteInput, actor?: string): Promise<KbRecord>;
367
+ write(bundlePath: string, input: KbWriteInput, actor?: string): Promise<KbWriteResult>;
361
368
  /** One record by concept id, or null when it does not exist. */
362
369
  read(bundlePath: string, conceptId: string): Promise<KbRecord | null>;
363
370
  /**
@@ -424,10 +431,15 @@ declare class KbStore {
424
431
  * Refuses rather than truncates when the base is too large. A truncated base
425
432
  * is indistinguishable from a complete one, so a caller would answer "that
426
433
  * was never decided" from a slice it did not know was a slice.
434
+ *
435
+ * That refusal is the default guardrail. `all` bypasses it outright and
436
+ * always hands back the whole bundle: an explicit, never-accidental escape
437
+ * hatch for an operator who has the budget to spend, not a wider default.
427
438
  */
428
439
  load(bundlePath: string, options?: {
429
440
  budgetTokens?: number;
430
441
  type?: string;
442
+ all?: boolean;
431
443
  }): Promise<KbLoadResult>;
432
444
  /** How a position was arrived at, as a timeline. See `trace.ts`. */
433
445
  trace(bundlePath: string, seedId: string, options?: KbTraceOptions): Promise<KbTraceStep[]>;
@@ -447,6 +459,19 @@ declare class KbStore {
447
459
  * knows which agent touched what. So a bad line is surfaced and left alone.
448
460
  */
449
461
  readLog(bundlePath: string): Promise<ReturnType<typeof parseLog>>;
462
+ /**
463
+ * `markSuperseded`, tolerant of the two ways it legitimately doesn't land:
464
+ * a missing target (a broken link, legal per compose.ts) or a CAS conflict
465
+ * from a concurrent writer touching the same target. A conflict is retried
466
+ * a bounded number of times — each attempt re-reads the target fresh — and
467
+ * on the last, `false` reports "not marked" rather than throwing: the
468
+ * caller's own record is already published, so failing here would leave
469
+ * that publish unreported instead of undone. kb_validate's existing
470
+ * "not marked superseded" check is what surfaces the residue.
471
+ */
472
+ private markSupersededRetrying;
473
+ /** The one-directional half of `supersede`: marks `conceptId` superseded. */
474
+ private markSuperseded;
450
475
  private mutate;
451
476
  /**
452
477
  * Two guarantees, both about writers running in parallel.
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  runKbCli
3
- } from "./chunk-QLTB77W4.js";
3
+ } from "./chunk-KQMGKSPZ.js";
4
4
  import {
5
5
  createKbMcpServer,
6
6
  runKbMcpServer
7
- } from "./chunk-FSI4Q2FD.js";
7
+ } from "./chunk-VOJ6D6OX.js";
8
8
  import {
9
9
  BaseError,
10
10
  CONTEXT_BEGIN,
@@ -80,7 +80,7 @@ import {
80
80
  trace,
81
81
  unpinBase,
82
82
  validateBundle
83
- } from "./chunk-HYNAEAPM.js";
83
+ } from "./chunk-FZIMFPGR.js";
84
84
 
85
85
  // src/match-diff.ts
86
86
  function matchToDiff(files, records, options = {}) {
package/dist/mcp-main.cjs CHANGED
@@ -214,7 +214,7 @@ var composeInputSchema = import_zod2.z.object({
214
214
  /** Concept ids this record relates to; rendered as body links. */
215
215
  relatedConceptIds: import_zod2.z.array(kbConceptIdSchema).optional(),
216
216
  /** Concept ids this record replaces. The store settles the backlinks. */
217
- supersedes: import_zod2.z.array(kbConceptIdSchema).optional(),
217
+ supersedes: import_zod2.z.array(kbConceptIdSchema).max(32).optional(),
218
218
  materiality: import_zod2.z.enum(KB_MATERIALITIES).optional(),
219
219
  confidence: import_zod2.z.enum(KB_CONFIDENCES).optional(),
220
220
  owner: import_zod2.z.string().min(1).optional()
@@ -1074,25 +1074,32 @@ var import_zod9 = require("zod");
1074
1074
  var loadCommand = define({
1075
1075
  name: "load",
1076
1076
  tool: "kb_load",
1077
- usage: "load [type] [--budget N]",
1078
- 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.",
1077
+ usage: "load [type] [--budget N | --all]",
1078
+ 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.",
1079
1079
  input: import_zod9.z.object({
1080
1080
  bundlePath,
1081
1081
  type: import_zod9.z.enum(KB_RECORD_TYPES).optional(),
1082
- budgetTokens: import_zod9.z.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000.")
1082
+ budgetTokens: import_zod9.z.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
1083
+ all: import_zod9.z.boolean().optional().describe(
1084
+ "Load the entire base regardless of size. The deliberate-operator escape hatch; mutually exclusive with budgetTokens."
1085
+ )
1086
+ }).refine((value) => !(value.all && value.budgetTokens !== void 0), {
1087
+ message: "all and budgetTokens are mutually exclusive: pass a ceiling or none, not both."
1083
1088
  }),
1084
1089
  fromArgv: (argv, path) => {
1085
1090
  const budget = argvFlag(argv, "--budget");
1086
1091
  return {
1087
1092
  bundlePath: path,
1088
- ...argv[1] && argv[1] !== "--budget" ? { type: argv[1] } : {},
1089
- ...budget ? { budgetTokens: Number(budget) } : {}
1093
+ ...argv[1] && !argv[1].startsWith("--") ? { type: argv[1] } : {},
1094
+ ...budget ? { budgetTokens: Number(budget) } : {},
1095
+ ...argv.includes("--all") ? { all: true } : {}
1090
1096
  };
1091
1097
  },
1092
- run: async ({ store }, { bundlePath: path, type, budgetTokens }) => {
1098
+ run: async ({ store }, { bundlePath: path, type, budgetTokens, all }) => {
1093
1099
  const result = await store.load(path, {
1094
1100
  ...type ? { type } : {},
1095
- ...budgetTokens ? { budgetTokens } : {}
1101
+ ...budgetTokens ? { budgetTokens } : {},
1102
+ ...all ? { all } : {}
1096
1103
  });
1097
1104
  if (!result.loaded) return result;
1098
1105
  return {
@@ -1602,7 +1609,11 @@ var writeCommand = define({
1602
1609
  composeRecord(type, input, actor, now()),
1603
1610
  actor
1604
1611
  );
1605
- return { conceptId: record.conceptId };
1612
+ return {
1613
+ conceptId: record.conceptId,
1614
+ action: record.action,
1615
+ supersededIds: record.supersededIds
1616
+ };
1606
1617
  }
1607
1618
  });
1608
1619
 
@@ -1632,7 +1643,11 @@ var writeDecisionCommand = define({
1632
1643
  composeDecisionRecord(input, actor, now()),
1633
1644
  actor
1634
1645
  );
1635
- return { conceptId: record.conceptId };
1646
+ return {
1647
+ conceptId: record.conceptId,
1648
+ action: record.action,
1649
+ supersededIds: record.supersededIds
1650
+ };
1636
1651
  }
1637
1652
  });
1638
1653
 
@@ -1723,7 +1738,7 @@ var KbRecordAlreadyExistsError = class extends BaseError {
1723
1738
  fault: "User" /* User */,
1724
1739
  retriable: false,
1725
1740
  reportToUser: true,
1726
- details: { conceptId: conceptId2 }
1741
+ details: { conceptId: conceptId2, action: "refused" }
1727
1742
  });
1728
1743
  this.conceptId = conceptId2;
1729
1744
  }
@@ -1903,13 +1918,27 @@ var KbStore = class {
1903
1918
  conceptId: conceptId2,
1904
1919
  by: actor
1905
1920
  });
1921
+ const targets = new Set(frontmatter.strauss_supersedes ?? []);
1922
+ targets.delete(conceptId2);
1923
+ const supersededIds = [];
1924
+ for (const old of targets) {
1925
+ if (await this.markSupersededRetrying(bundlePath2, old, conceptId2, actor)) {
1926
+ supersededIds.push(old);
1927
+ }
1928
+ }
1906
1929
  this.logger.info?.({
1907
1930
  operation: "kb.write",
1908
1931
  bundlePath: root,
1909
1932
  conceptId: conceptId2,
1910
1933
  anchors: frontmatter.strauss_anchors?.length ?? 0
1911
1934
  });
1912
- return { conceptId: conceptId2, frontmatter, body: input.body };
1935
+ return {
1936
+ conceptId: conceptId2,
1937
+ frontmatter,
1938
+ body: input.body,
1939
+ action: supersededIds.length ? "superseded-prior" : "created",
1940
+ supersededIds
1941
+ };
1913
1942
  }
1914
1943
  /** One record by concept id, or null when it does not exist. */
1915
1944
  async read(bundlePath2, conceptId2) {
@@ -1973,15 +2002,11 @@ var KbStore = class {
1973
2002
  async supersede(bundlePath2, conceptId2, replacementId, actor = "unknown") {
1974
2003
  const replacement = await this.read(bundlePath2, replacementId);
1975
2004
  if (!replacement) throw new KbRecordNotFoundError(replacementId);
1976
- const superseded = await this.mutate(
2005
+ const superseded = await this.markSuperseded(
1977
2006
  bundlePath2,
1978
2007
  conceptId2,
1979
- (frontmatter) => ({
1980
- ...frontmatter,
1981
- strauss_status: "superseded",
1982
- strauss_superseded_by: replacementId
1983
- }),
1984
- { operation: "supersede", by: actor, target: replacementId }
2008
+ replacementId,
2009
+ actor
1985
2010
  );
1986
2011
  await this.mutate(
1987
2012
  bundlePath2,
@@ -2070,6 +2095,10 @@ ${answer}
2070
2095
  * Refuses rather than truncates when the base is too large. A truncated base
2071
2096
  * is indistinguishable from a complete one, so a caller would answer "that
2072
2097
  * was never decided" from a slice it did not know was a slice.
2098
+ *
2099
+ * That refusal is the default guardrail. `all` bypasses it outright and
2100
+ * always hands back the whole bundle: an explicit, never-accidental escape
2101
+ * hatch for an operator who has the budget to spend, not a wider default.
2073
2102
  */
2074
2103
  async load(bundlePath2, options = {}) {
2075
2104
  const budgetTokens = options.budgetTokens ?? DEFAULT_LOAD_BUDGET;
@@ -2079,7 +2108,7 @@ ${answer}
2079
2108
  const records = adjudicated.filter((hit) => hit.standing !== "superseded");
2080
2109
  const superseded = adjudicated.filter((hit) => hit.standing === "superseded").map(stub);
2081
2110
  const approxTokens2 = records.reduce((total, hit) => total + estimateTokens(hit.record), 0) + superseded.reduce((total, entry) => total + estimateStubTokens(entry), 0);
2082
- if (approxTokens2 > budgetTokens) {
2111
+ if (!options.all && approxTokens2 > budgetTokens) {
2083
2112
  return {
2084
2113
  loaded: false,
2085
2114
  recordCount: wanted.length,
@@ -2090,8 +2119,8 @@ ${answer}
2090
2119
  return {
2091
2120
  loaded: true,
2092
2121
  recordCount: wanted.length,
2093
- approxTokens: approxTokens2,
2094
- budgetTokens,
2122
+ tokensLoaded: approxTokens2,
2123
+ budgetTokens: options.all ? null : budgetTokens,
2095
2124
  records,
2096
2125
  superseded
2097
2126
  };
@@ -2145,6 +2174,42 @@ ${answer}
2145
2174
  }
2146
2175
  return result;
2147
2176
  }
2177
+ /**
2178
+ * `markSuperseded`, tolerant of the two ways it legitimately doesn't land:
2179
+ * a missing target (a broken link, legal per compose.ts) or a CAS conflict
2180
+ * from a concurrent writer touching the same target. A conflict is retried
2181
+ * a bounded number of times — each attempt re-reads the target fresh — and
2182
+ * on the last, `false` reports "not marked" rather than throwing: the
2183
+ * caller's own record is already published, so failing here would leave
2184
+ * that publish unreported instead of undone. kb_validate's existing
2185
+ * "not marked superseded" check is what surfaces the residue.
2186
+ */
2187
+ async markSupersededRetrying(bundlePath2, conceptId2, replacementId, actor, retries = 3) {
2188
+ for (let attempt = 0; attempt <= retries; attempt++) {
2189
+ try {
2190
+ await this.markSuperseded(bundlePath2, conceptId2, replacementId, actor);
2191
+ return true;
2192
+ } catch (error) {
2193
+ if (error instanceof KbRecordNotFoundError) return false;
2194
+ if (!(error instanceof KbWriteConflictError)) throw error;
2195
+ if (attempt === retries) return false;
2196
+ }
2197
+ }
2198
+ return false;
2199
+ }
2200
+ /** The one-directional half of `supersede`: marks `conceptId` superseded. */
2201
+ async markSuperseded(bundlePath2, conceptId2, replacementId, actor) {
2202
+ return this.mutate(
2203
+ bundlePath2,
2204
+ conceptId2,
2205
+ (frontmatter) => ({
2206
+ ...frontmatter,
2207
+ strauss_status: "superseded",
2208
+ strauss_superseded_by: replacementId
2209
+ }),
2210
+ { operation: "supersede", by: actor, target: replacementId }
2211
+ );
2212
+ }
2148
2213
  async mutate(bundlePath2, conceptId2, change, entry, changeBody = (body) => body) {
2149
2214
  const target = this.recordPath(bundlePath2, conceptId2);
2150
2215
  const before = await (0, import_promises4.readFile)(target, "utf8").catch(() => null);