@semiont/make-meaning 0.5.32 → 0.5.33

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.ts CHANGED
@@ -230,10 +230,23 @@ interface KnowledgeBase {
230
230
  /** The lifecycle half of the working tree (GATEWAY.md D4a): the Archivist
231
231
  * accessions, moves, removes and resolves — it never serves bytes. */
232
232
  type ContentLifecycle = Pick<WorkingTreeStore, 'register' | 'move' | 'remove' | 'resolveUri'>;
233
- /** The record's single write seam. `Stower` is the only appendEvent caller
234
- * anywhere in make-meaning or the gateway (post-#1252): single-owner by
235
- * construction. A second caller is a design smell, not a wiring chore. */
236
- type EventAppends = Pick<EventStore, 'appendEvent'>;
233
+ /**
234
+ * The record's single write seam. `Stower` is the only appendEvent caller
235
+ * anywhere in make-meaning or the gateway (post-#1252): single-owner by
236
+ * construction. A second caller is a design smell, not a wiring chore.
237
+ *
238
+ * It carries a read — `viewStorage.get`, narrowed to `get` — because one write
239
+ * path is at-least-once and must not duplicate the log
240
+ * (COMMIT-ACK-FALSE-FAILURE F3): `mark:commit` diffs its batch against what the
241
+ * resource already holds. This does NOT reverse JOB-RESTART-SAFETY HD1, which
242
+ * rejected read-before-write for a WORKER reading a REMOTE store mid-recovery
243
+ * — "the thing it would read is exactly what is down". This read is inside the
244
+ * Archivist, against the store it is about to write, and cannot be down
245
+ * relative to itself.
246
+ */
247
+ type EventAppends = Pick<EventStore, 'appendEvent'> & {
248
+ readonly viewStorage: Pick<ViewStorage, 'get'>;
249
+ };
237
250
  /** Read-only reach into the event store: the log for queries, the
238
251
  * materializer for on-demand view assembly (`assembleResourceGraph`). */
239
252
  interface EventStoreReads {
@@ -350,10 +363,22 @@ declare class Stower {
350
363
  *
351
364
  * Appends are sequential, not concurrent: the event log is the system of
352
365
  * record and a batch that half-lands under concurrency is harder to reason
353
- * about than one that stops at the first failure. A partial batch is
354
- * reported as a failure and the worker retries the WHOLE unit, which is safe
355
- * because ids are deterministic (P3) and the annotation fold is idempotent
356
- * by id re-appending what already landed changes nothing.
366
+ * about than one that stops at the first failure.
367
+ *
368
+ * This channel is AT-LEAST-ONCE, and the log must not grow on a repeat
369
+ * (COMMIT-ACK-FALSE-FAILURE F3). Two paths re-send a batch that already
370
+ * landed: an acknowledgement lost after a successful append (the unit is
371
+ * never checkpointed, so the retry re-runs exactly the unit that landed), and
372
+ * a partial batch, reported as a failure and retried whole. Deterministic ids
373
+ * (JOB-RESTART-SAFETY P3) made those safe for the PROJECTIONS — the resource
374
+ * view and the graph both refuse a duplicate id — but a projection's guard
375
+ * says nothing about the log, which appends whatever it is handed. The result
376
+ * was a green graph over a doubled log: silent, and not undoable.
377
+ *
378
+ * So the batch is diffed against what the resource already holds. ONE view
379
+ * read per commit, never per annotation: the view for a 1,673-annotation
380
+ * resource is ~3 MB, and re-reading it per append would cost gigabytes of
381
+ * parsing for a single job.
357
382
  */
358
383
  private handleMarkCommit;
359
384
  private handleMarkDelete;
package/dist/index.js CHANGED
@@ -11574,36 +11574,56 @@ var Stower = class {
11574
11574
  *
11575
11575
  * Appends are sequential, not concurrent: the event log is the system of
11576
11576
  * record and a batch that half-lands under concurrency is harder to reason
11577
- * about than one that stops at the first failure. A partial batch is
11578
- * reported as a failure and the worker retries the WHOLE unit, which is safe
11579
- * because ids are deterministic (P3) and the annotation fold is idempotent
11580
- * by id re-appending what already landed changes nothing.
11577
+ * about than one that stops at the first failure.
11578
+ *
11579
+ * This channel is AT-LEAST-ONCE, and the log must not grow on a repeat
11580
+ * (COMMIT-ACK-FALSE-FAILURE F3). Two paths re-send a batch that already
11581
+ * landed: an acknowledgement lost after a successful append (the unit is
11582
+ * never checkpointed, so the retry re-runs exactly the unit that landed), and
11583
+ * a partial batch, reported as a failure and retried whole. Deterministic ids
11584
+ * (JOB-RESTART-SAFETY P3) made those safe for the PROJECTIONS — the resource
11585
+ * view and the graph both refuse a duplicate id — but a projection's guard
11586
+ * says nothing about the log, which appends whatever it is handed. The result
11587
+ * was a green graph over a doubled log: silent, and not undoable.
11588
+ *
11589
+ * So the batch is diffed against what the resource already holds. ONE view
11590
+ * read per commit, never per annotation: the view for a 1,673-annotation
11591
+ * resource is ~3 MB, and re-reading it per append would cost gigabytes of
11592
+ * parsing for a single job.
11581
11593
  */
11582
11594
  async handleMarkCommit(event) {
11583
11595
  if (!event._userId) {
11584
11596
  throw new Error("mark:commit missing _userId (gateway injection)");
11585
11597
  }
11586
11598
  const annotations = event.annotations ?? [];
11599
+ const rid = resourceId(event.resourceId);
11587
11600
  try {
11588
- let persisted = 0;
11601
+ const view = await this.stores.eventStore.viewStorage.get(rid);
11602
+ const present = new Set((view?.annotations.annotations ?? []).map((a) => String(a.id)));
11589
11603
  for (const annotation of annotations) {
11604
+ if (present.has(String(annotation.id))) continue;
11590
11605
  await this.stores.eventStore.appendEvent({
11591
11606
  type: "mark:added",
11592
- resourceId: resourceId(event.resourceId),
11607
+ resourceId: rid,
11593
11608
  userId: userId(event._userId),
11594
11609
  version: 1,
11595
11610
  payload: { annotation }
11596
11611
  });
11597
- persisted++;
11612
+ present.add(String(annotation.id));
11598
11613
  }
11599
11614
  this.logger.debug("Committed annotation batch", {
11600
11615
  correlationId: event.correlationId,
11601
11616
  resourceId: event.resourceId,
11602
- persisted
11617
+ persisted: annotations.length
11603
11618
  });
11604
11619
  this.eventBus.get("mark:commit-ok").next({
11605
11620
  correlationId: event.correlationId,
11606
- response: { persisted, annotationIds: annotations.map((a) => String(a.id)) }
11621
+ // The DURABLE count, which is what the acknowledgement means ("every
11622
+ // annotation named by the command is in the event log"). Not an append
11623
+ // tally: a retry whose annotations are all already present has
11624
+ // succeeded, and must be indistinguishable from the first commit or the
11625
+ // caller would have to interpret a 0 that means "all good".
11626
+ response: { persisted: annotations.length, annotationIds: annotations.map((a) => String(a.id)) }
11607
11627
  });
11608
11628
  } catch (error) {
11609
11629
  this.logger.error("Failed to commit annotation batch", {
@@ -11829,7 +11849,11 @@ var Stower = class {
11829
11849
  jobId: event.jobId,
11830
11850
  jobType: event.jobType,
11831
11851
  ...event.annotationId ? { annotationId: event.annotationId } : {},
11832
- result: event.result
11852
+ result: event.result,
11853
+ // How durability was ESTABLISHED (COMMIT-ACK-FALSE-FAILURE). An
11854
+ // acknowledged batch and one inferred from a probe are different
11855
+ // claims; absent means the question never arose.
11856
+ ...event.durability !== void 0 ? { durability: event.durability } : {}
11833
11857
  }
11834
11858
  });
11835
11859
  }
@@ -11846,7 +11870,19 @@ var Stower = class {
11846
11870
  jobId: event.jobId,
11847
11871
  jobType: event.jobType,
11848
11872
  ...event.annotationId ? { annotationId: event.annotationId } : {},
11849
- error: event.error
11873
+ error: event.error,
11874
+ // The worker's JUDGMENTS, not just its message. Both are computed where
11875
+ // the error is still typed and are unrecoverable here — the only other
11876
+ // witness in the log is `error`, a flattened English string. Spread
11877
+ // conditionally: absent `failureClass` means UNRECOGNISED, a different
11878
+ // claim from 'transient', and defaulting either would write a judgment
11879
+ // nobody made into a log nobody can rewrite.
11880
+ ...event.failureClass !== void 0 ? { failureClass: event.failureClass } : {},
11881
+ ...event.willRetry !== void 0 ? { willRetry: event.willRetry } : {},
11882
+ // How durability was ESTABLISHED (COMMIT-ACK-FALSE-FAILURE). An
11883
+ // acknowledged batch and one inferred from a probe are different
11884
+ // claims; absent means the question never arose.
11885
+ ...event.durability !== void 0 ? { durability: event.durability } : {}
11850
11886
  }
11851
11887
  });
11852
11888
  }