@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.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runKbCli
4
- } from "./chunk-QLTB77W4.js";
5
- import "./chunk-HYNAEAPM.js";
4
+ } from "./chunk-Y5C7Z2HG.js";
5
+ import "./chunk-EDH43Z7J.js";
6
6
 
7
7
  // src/cli-main.ts
8
8
  runKbCli(process.argv.slice(2)).catch((error) => {
package/dist/index.cjs CHANGED
@@ -270,7 +270,7 @@ var KbRecordAlreadyExistsError = class extends BaseError {
270
270
  fault: "User" /* User */,
271
271
  retriable: false,
272
272
  reportToUser: true,
273
- details: { conceptId: conceptId2 }
273
+ details: { conceptId: conceptId2, action: "refused" }
274
274
  });
275
275
  this.conceptId = conceptId2;
276
276
  }
@@ -675,13 +675,27 @@ var KbStore = class {
675
675
  conceptId: conceptId2,
676
676
  by: actor
677
677
  });
678
+ const targets = new Set(frontmatter.strauss_supersedes ?? []);
679
+ targets.delete(conceptId2);
680
+ const supersededIds = [];
681
+ for (const old of targets) {
682
+ if (await this.markSupersededRetrying(bundlePath2, old, conceptId2, actor)) {
683
+ supersededIds.push(old);
684
+ }
685
+ }
678
686
  this.logger.info?.({
679
687
  operation: "kb.write",
680
688
  bundlePath: root,
681
689
  conceptId: conceptId2,
682
690
  anchors: frontmatter.strauss_anchors?.length ?? 0
683
691
  });
684
- return { conceptId: conceptId2, frontmatter, body: input.body };
692
+ return {
693
+ conceptId: conceptId2,
694
+ frontmatter,
695
+ body: input.body,
696
+ action: supersededIds.length ? "superseded-prior" : "created",
697
+ supersededIds
698
+ };
685
699
  }
686
700
  /** One record by concept id, or null when it does not exist. */
687
701
  async read(bundlePath2, conceptId2) {
@@ -745,15 +759,11 @@ var KbStore = class {
745
759
  async supersede(bundlePath2, conceptId2, replacementId, actor = "unknown") {
746
760
  const replacement = await this.read(bundlePath2, replacementId);
747
761
  if (!replacement) throw new KbRecordNotFoundError(replacementId);
748
- const superseded = await this.mutate(
762
+ const superseded = await this.markSuperseded(
749
763
  bundlePath2,
750
764
  conceptId2,
751
- (frontmatter) => ({
752
- ...frontmatter,
753
- strauss_status: "superseded",
754
- strauss_superseded_by: replacementId
755
- }),
756
- { operation: "supersede", by: actor, target: replacementId }
765
+ replacementId,
766
+ actor
757
767
  );
758
768
  await this.mutate(
759
769
  bundlePath2,
@@ -917,6 +927,42 @@ ${answer}
917
927
  }
918
928
  return result;
919
929
  }
930
+ /**
931
+ * `markSuperseded`, tolerant of the two ways it legitimately doesn't land:
932
+ * a missing target (a broken link, legal per compose.ts) or a CAS conflict
933
+ * from a concurrent writer touching the same target. A conflict is retried
934
+ * a bounded number of times — each attempt re-reads the target fresh — and
935
+ * on the last, `false` reports "not marked" rather than throwing: the
936
+ * caller's own record is already published, so failing here would leave
937
+ * that publish unreported instead of undone. kb_validate's existing
938
+ * "not marked superseded" check is what surfaces the residue.
939
+ */
940
+ async markSupersededRetrying(bundlePath2, conceptId2, replacementId, actor, retries = 3) {
941
+ for (let attempt = 0; attempt <= retries; attempt++) {
942
+ try {
943
+ await this.markSuperseded(bundlePath2, conceptId2, replacementId, actor);
944
+ return true;
945
+ } catch (error) {
946
+ if (error instanceof KbRecordNotFoundError) return false;
947
+ if (!(error instanceof KbWriteConflictError)) throw error;
948
+ if (attempt === retries) return false;
949
+ }
950
+ }
951
+ return false;
952
+ }
953
+ /** The one-directional half of `supersede`: marks `conceptId` superseded. */
954
+ async markSuperseded(bundlePath2, conceptId2, replacementId, actor) {
955
+ return this.mutate(
956
+ bundlePath2,
957
+ conceptId2,
958
+ (frontmatter) => ({
959
+ ...frontmatter,
960
+ strauss_status: "superseded",
961
+ strauss_superseded_by: replacementId
962
+ }),
963
+ { operation: "supersede", by: actor, target: replacementId }
964
+ );
965
+ }
920
966
  async mutate(bundlePath2, conceptId2, change, entry, changeBody = (body) => body) {
921
967
  const target = this.recordPath(bundlePath2, conceptId2);
922
968
  const before = await (0, import_promises2.readFile)(target, "utf8").catch(() => null);
@@ -1133,7 +1179,7 @@ var composeInputSchema = import_zod3.z.object({
1133
1179
  /** Concept ids this record relates to; rendered as body links. */
1134
1180
  relatedConceptIds: import_zod3.z.array(kbConceptIdSchema).optional(),
1135
1181
  /** Concept ids this record replaces. The store settles the backlinks. */
1136
- supersedes: import_zod3.z.array(kbConceptIdSchema).optional(),
1182
+ supersedes: import_zod3.z.array(kbConceptIdSchema).max(32).optional(),
1137
1183
  materiality: import_zod3.z.enum(KB_MATERIALITIES).optional(),
1138
1184
  confidence: import_zod3.z.enum(KB_CONFIDENCES).optional(),
1139
1185
  owner: import_zod3.z.string().min(1).optional()
@@ -2375,7 +2421,11 @@ var writeCommand = define({
2375
2421
  composeRecord(type, input, actor, now()),
2376
2422
  actor
2377
2423
  );
2378
- return { conceptId: record.conceptId };
2424
+ return {
2425
+ conceptId: record.conceptId,
2426
+ action: record.action,
2427
+ supersededIds: record.supersededIds
2428
+ };
2379
2429
  }
2380
2430
  });
2381
2431
 
@@ -2405,7 +2455,11 @@ var writeDecisionCommand = define({
2405
2455
  composeDecisionRecord(input, actor, now()),
2406
2456
  actor
2407
2457
  );
2408
- return { conceptId: record.conceptId };
2458
+ return {
2459
+ conceptId: record.conceptId,
2460
+ action: record.action,
2461
+ supersededIds: record.supersededIds
2462
+ };
2409
2463
  }
2410
2464
  });
2411
2465