@saasontools/strauss-kb 0.1.3 → 0.1.4

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
@@ -334,6 +334,12 @@ type KbWriteInput = {
334
334
  /** Replace an existing record rather than failing on the collision. */
335
335
  overwrite?: boolean;
336
336
  };
337
+ type KbWriteResult = KbRecord & {
338
+ /** Whether this write also marked prior records superseded. */
339
+ action: "created" | "superseded-prior";
340
+ /** `frontmatter.strauss_supersedes` ids that were actually marked. */
341
+ supersededIds: string[];
342
+ };
337
343
  /**
338
344
  * Reads and writes a knowledge bundle.
339
345
  *
@@ -357,7 +363,7 @@ declare class KbStore {
357
363
  * concept id, so a caller cannot produce a file whose identity disagrees with
358
364
  * its contents.
359
365
  */
360
- write(bundlePath: string, input: KbWriteInput, actor?: string): Promise<KbRecord>;
366
+ write(bundlePath: string, input: KbWriteInput, actor?: string): Promise<KbWriteResult>;
361
367
  /** One record by concept id, or null when it does not exist. */
362
368
  read(bundlePath: string, conceptId: string): Promise<KbRecord | null>;
363
369
  /**
@@ -447,6 +453,19 @@ declare class KbStore {
447
453
  * knows which agent touched what. So a bad line is surfaced and left alone.
448
454
  */
449
455
  readLog(bundlePath: string): Promise<ReturnType<typeof parseLog>>;
456
+ /**
457
+ * `markSuperseded`, tolerant of the two ways it legitimately doesn't land:
458
+ * a missing target (a broken link, legal per compose.ts) or a CAS conflict
459
+ * from a concurrent writer touching the same target. A conflict is retried
460
+ * a bounded number of times — each attempt re-reads the target fresh — and
461
+ * on the last, `false` reports "not marked" rather than throwing: the
462
+ * caller's own record is already published, so failing here would leave
463
+ * that publish unreported instead of undone. kb_validate's existing
464
+ * "not marked superseded" check is what surfaces the residue.
465
+ */
466
+ private markSupersededRetrying;
467
+ /** The one-directional half of `supersede`: marks `conceptId` superseded. */
468
+ private markSuperseded;
450
469
  private mutate;
451
470
  /**
452
471
  * Two guarantees, both about writers running in parallel.
package/dist/index.d.ts CHANGED
@@ -334,6 +334,12 @@ type KbWriteInput = {
334
334
  /** Replace an existing record rather than failing on the collision. */
335
335
  overwrite?: boolean;
336
336
  };
337
+ type KbWriteResult = KbRecord & {
338
+ /** Whether this write also marked prior records superseded. */
339
+ action: "created" | "superseded-prior";
340
+ /** `frontmatter.strauss_supersedes` ids that were actually marked. */
341
+ supersededIds: string[];
342
+ };
337
343
  /**
338
344
  * Reads and writes a knowledge bundle.
339
345
  *
@@ -357,7 +363,7 @@ declare class KbStore {
357
363
  * concept id, so a caller cannot produce a file whose identity disagrees with
358
364
  * its contents.
359
365
  */
360
- write(bundlePath: string, input: KbWriteInput, actor?: string): Promise<KbRecord>;
366
+ write(bundlePath: string, input: KbWriteInput, actor?: string): Promise<KbWriteResult>;
361
367
  /** One record by concept id, or null when it does not exist. */
362
368
  read(bundlePath: string, conceptId: string): Promise<KbRecord | null>;
363
369
  /**
@@ -447,6 +453,19 @@ declare class KbStore {
447
453
  * knows which agent touched what. So a bad line is surfaced and left alone.
448
454
  */
449
455
  readLog(bundlePath: string): Promise<ReturnType<typeof parseLog>>;
456
+ /**
457
+ * `markSuperseded`, tolerant of the two ways it legitimately doesn't land:
458
+ * a missing target (a broken link, legal per compose.ts) or a CAS conflict
459
+ * from a concurrent writer touching the same target. A conflict is retried
460
+ * a bounded number of times — each attempt re-reads the target fresh — and
461
+ * on the last, `false` reports "not marked" rather than throwing: the
462
+ * caller's own record is already published, so failing here would leave
463
+ * that publish unreported instead of undone. kb_validate's existing
464
+ * "not marked superseded" check is what surfaces the residue.
465
+ */
466
+ private markSupersededRetrying;
467
+ /** The one-directional half of `supersede`: marks `conceptId` superseded. */
468
+ private markSuperseded;
450
469
  private mutate;
451
470
  /**
452
471
  * 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-Y5C7Z2HG.js";
4
4
  import {
5
5
  createKbMcpServer,
6
6
  runKbMcpServer
7
- } from "./chunk-FSI4Q2FD.js";
7
+ } from "./chunk-TS26G7TL.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-EDH43Z7J.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()
@@ -1602,7 +1602,11 @@ var writeCommand = define({
1602
1602
  composeRecord(type, input, actor, now()),
1603
1603
  actor
1604
1604
  );
1605
- return { conceptId: record.conceptId };
1605
+ return {
1606
+ conceptId: record.conceptId,
1607
+ action: record.action,
1608
+ supersededIds: record.supersededIds
1609
+ };
1606
1610
  }
1607
1611
  });
1608
1612
 
@@ -1632,7 +1636,11 @@ var writeDecisionCommand = define({
1632
1636
  composeDecisionRecord(input, actor, now()),
1633
1637
  actor
1634
1638
  );
1635
- return { conceptId: record.conceptId };
1639
+ return {
1640
+ conceptId: record.conceptId,
1641
+ action: record.action,
1642
+ supersededIds: record.supersededIds
1643
+ };
1636
1644
  }
1637
1645
  });
1638
1646
 
@@ -1723,7 +1731,7 @@ var KbRecordAlreadyExistsError = class extends BaseError {
1723
1731
  fault: "User" /* User */,
1724
1732
  retriable: false,
1725
1733
  reportToUser: true,
1726
- details: { conceptId: conceptId2 }
1734
+ details: { conceptId: conceptId2, action: "refused" }
1727
1735
  });
1728
1736
  this.conceptId = conceptId2;
1729
1737
  }
@@ -1903,13 +1911,27 @@ var KbStore = class {
1903
1911
  conceptId: conceptId2,
1904
1912
  by: actor
1905
1913
  });
1914
+ const targets = new Set(frontmatter.strauss_supersedes ?? []);
1915
+ targets.delete(conceptId2);
1916
+ const supersededIds = [];
1917
+ for (const old of targets) {
1918
+ if (await this.markSupersededRetrying(bundlePath2, old, conceptId2, actor)) {
1919
+ supersededIds.push(old);
1920
+ }
1921
+ }
1906
1922
  this.logger.info?.({
1907
1923
  operation: "kb.write",
1908
1924
  bundlePath: root,
1909
1925
  conceptId: conceptId2,
1910
1926
  anchors: frontmatter.strauss_anchors?.length ?? 0
1911
1927
  });
1912
- return { conceptId: conceptId2, frontmatter, body: input.body };
1928
+ return {
1929
+ conceptId: conceptId2,
1930
+ frontmatter,
1931
+ body: input.body,
1932
+ action: supersededIds.length ? "superseded-prior" : "created",
1933
+ supersededIds
1934
+ };
1913
1935
  }
1914
1936
  /** One record by concept id, or null when it does not exist. */
1915
1937
  async read(bundlePath2, conceptId2) {
@@ -1973,15 +1995,11 @@ var KbStore = class {
1973
1995
  async supersede(bundlePath2, conceptId2, replacementId, actor = "unknown") {
1974
1996
  const replacement = await this.read(bundlePath2, replacementId);
1975
1997
  if (!replacement) throw new KbRecordNotFoundError(replacementId);
1976
- const superseded = await this.mutate(
1998
+ const superseded = await this.markSuperseded(
1977
1999
  bundlePath2,
1978
2000
  conceptId2,
1979
- (frontmatter) => ({
1980
- ...frontmatter,
1981
- strauss_status: "superseded",
1982
- strauss_superseded_by: replacementId
1983
- }),
1984
- { operation: "supersede", by: actor, target: replacementId }
2001
+ replacementId,
2002
+ actor
1985
2003
  );
1986
2004
  await this.mutate(
1987
2005
  bundlePath2,
@@ -2145,6 +2163,42 @@ ${answer}
2145
2163
  }
2146
2164
  return result;
2147
2165
  }
2166
+ /**
2167
+ * `markSuperseded`, tolerant of the two ways it legitimately doesn't land:
2168
+ * a missing target (a broken link, legal per compose.ts) or a CAS conflict
2169
+ * from a concurrent writer touching the same target. A conflict is retried
2170
+ * a bounded number of times — each attempt re-reads the target fresh — and
2171
+ * on the last, `false` reports "not marked" rather than throwing: the
2172
+ * caller's own record is already published, so failing here would leave
2173
+ * that publish unreported instead of undone. kb_validate's existing
2174
+ * "not marked superseded" check is what surfaces the residue.
2175
+ */
2176
+ async markSupersededRetrying(bundlePath2, conceptId2, replacementId, actor, retries = 3) {
2177
+ for (let attempt = 0; attempt <= retries; attempt++) {
2178
+ try {
2179
+ await this.markSuperseded(bundlePath2, conceptId2, replacementId, actor);
2180
+ return true;
2181
+ } catch (error) {
2182
+ if (error instanceof KbRecordNotFoundError) return false;
2183
+ if (!(error instanceof KbWriteConflictError)) throw error;
2184
+ if (attempt === retries) return false;
2185
+ }
2186
+ }
2187
+ return false;
2188
+ }
2189
+ /** The one-directional half of `supersede`: marks `conceptId` superseded. */
2190
+ async markSuperseded(bundlePath2, conceptId2, replacementId, actor) {
2191
+ return this.mutate(
2192
+ bundlePath2,
2193
+ conceptId2,
2194
+ (frontmatter) => ({
2195
+ ...frontmatter,
2196
+ strauss_status: "superseded",
2197
+ strauss_superseded_by: replacementId
2198
+ }),
2199
+ { operation: "supersede", by: actor, target: replacementId }
2200
+ );
2201
+ }
2148
2202
  async mutate(bundlePath2, conceptId2, change, entry, changeBody = (body) => body) {
2149
2203
  const target = this.recordPath(bundlePath2, conceptId2);
2150
2204
  const before = await (0, import_promises4.readFile)(target, "utf8").catch(() => null);