@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/cli-main.cjs CHANGED
@@ -213,7 +213,7 @@ var composeInputSchema = import_zod2.z.object({
213
213
  /** Concept ids this record relates to; rendered as body links. */
214
214
  relatedConceptIds: import_zod2.z.array(kbConceptIdSchema).optional(),
215
215
  /** Concept ids this record replaces. The store settles the backlinks. */
216
- supersedes: import_zod2.z.array(kbConceptIdSchema).optional(),
216
+ supersedes: import_zod2.z.array(kbConceptIdSchema).max(32).optional(),
217
217
  materiality: import_zod2.z.enum(KB_MATERIALITIES).optional(),
218
218
  confidence: import_zod2.z.enum(KB_CONFIDENCES).optional(),
219
219
  owner: import_zod2.z.string().min(1).optional()
@@ -1601,7 +1601,11 @@ var writeCommand = define({
1601
1601
  composeRecord(type, input, actor, now()),
1602
1602
  actor
1603
1603
  );
1604
- return { conceptId: record.conceptId };
1604
+ return {
1605
+ conceptId: record.conceptId,
1606
+ action: record.action,
1607
+ supersededIds: record.supersededIds
1608
+ };
1605
1609
  }
1606
1610
  });
1607
1611
 
@@ -1631,7 +1635,11 @@ var writeDecisionCommand = define({
1631
1635
  composeDecisionRecord(input, actor, now()),
1632
1636
  actor
1633
1637
  );
1634
- return { conceptId: record.conceptId };
1638
+ return {
1639
+ conceptId: record.conceptId,
1640
+ action: record.action,
1641
+ supersededIds: record.supersededIds
1642
+ };
1635
1643
  }
1636
1644
  });
1637
1645
 
@@ -1722,7 +1730,7 @@ var KbRecordAlreadyExistsError = class extends BaseError {
1722
1730
  fault: "User" /* User */,
1723
1731
  retriable: false,
1724
1732
  reportToUser: true,
1725
- details: { conceptId: conceptId2 }
1733
+ details: { conceptId: conceptId2, action: "refused" }
1726
1734
  });
1727
1735
  this.conceptId = conceptId2;
1728
1736
  }
@@ -1902,13 +1910,27 @@ var KbStore = class {
1902
1910
  conceptId: conceptId2,
1903
1911
  by: actor
1904
1912
  });
1913
+ const targets = new Set(frontmatter.strauss_supersedes ?? []);
1914
+ targets.delete(conceptId2);
1915
+ const supersededIds = [];
1916
+ for (const old of targets) {
1917
+ if (await this.markSupersededRetrying(bundlePath2, old, conceptId2, actor)) {
1918
+ supersededIds.push(old);
1919
+ }
1920
+ }
1905
1921
  this.logger.info?.({
1906
1922
  operation: "kb.write",
1907
1923
  bundlePath: root,
1908
1924
  conceptId: conceptId2,
1909
1925
  anchors: frontmatter.strauss_anchors?.length ?? 0
1910
1926
  });
1911
- return { conceptId: conceptId2, frontmatter, body: input.body };
1927
+ return {
1928
+ conceptId: conceptId2,
1929
+ frontmatter,
1930
+ body: input.body,
1931
+ action: supersededIds.length ? "superseded-prior" : "created",
1932
+ supersededIds
1933
+ };
1912
1934
  }
1913
1935
  /** One record by concept id, or null when it does not exist. */
1914
1936
  async read(bundlePath2, conceptId2) {
@@ -1972,15 +1994,11 @@ var KbStore = class {
1972
1994
  async supersede(bundlePath2, conceptId2, replacementId, actor = "unknown") {
1973
1995
  const replacement = await this.read(bundlePath2, replacementId);
1974
1996
  if (!replacement) throw new KbRecordNotFoundError(replacementId);
1975
- const superseded = await this.mutate(
1997
+ const superseded = await this.markSuperseded(
1976
1998
  bundlePath2,
1977
1999
  conceptId2,
1978
- (frontmatter) => ({
1979
- ...frontmatter,
1980
- strauss_status: "superseded",
1981
- strauss_superseded_by: replacementId
1982
- }),
1983
- { operation: "supersede", by: actor, target: replacementId }
2000
+ replacementId,
2001
+ actor
1984
2002
  );
1985
2003
  await this.mutate(
1986
2004
  bundlePath2,
@@ -2144,6 +2162,42 @@ ${answer}
2144
2162
  }
2145
2163
  return result;
2146
2164
  }
2165
+ /**
2166
+ * `markSuperseded`, tolerant of the two ways it legitimately doesn't land:
2167
+ * a missing target (a broken link, legal per compose.ts) or a CAS conflict
2168
+ * from a concurrent writer touching the same target. A conflict is retried
2169
+ * a bounded number of times — each attempt re-reads the target fresh — and
2170
+ * on the last, `false` reports "not marked" rather than throwing: the
2171
+ * caller's own record is already published, so failing here would leave
2172
+ * that publish unreported instead of undone. kb_validate's existing
2173
+ * "not marked superseded" check is what surfaces the residue.
2174
+ */
2175
+ async markSupersededRetrying(bundlePath2, conceptId2, replacementId, actor, retries = 3) {
2176
+ for (let attempt = 0; attempt <= retries; attempt++) {
2177
+ try {
2178
+ await this.markSuperseded(bundlePath2, conceptId2, replacementId, actor);
2179
+ return true;
2180
+ } catch (error) {
2181
+ if (error instanceof KbRecordNotFoundError) return false;
2182
+ if (!(error instanceof KbWriteConflictError)) throw error;
2183
+ if (attempt === retries) return false;
2184
+ }
2185
+ }
2186
+ return false;
2187
+ }
2188
+ /** The one-directional half of `supersede`: marks `conceptId` superseded. */
2189
+ async markSuperseded(bundlePath2, conceptId2, replacementId, actor) {
2190
+ return this.mutate(
2191
+ bundlePath2,
2192
+ conceptId2,
2193
+ (frontmatter) => ({
2194
+ ...frontmatter,
2195
+ strauss_status: "superseded",
2196
+ strauss_superseded_by: replacementId
2197
+ }),
2198
+ { operation: "supersede", by: actor, target: replacementId }
2199
+ );
2200
+ }
2147
2201
  async mutate(bundlePath2, conceptId2, change, entry, changeBody = (body) => body) {
2148
2202
  const target = this.recordPath(bundlePath2, conceptId2);
2149
2203
  const before = await (0, import_promises4.readFile)(target, "utf8").catch(() => null);