@graview/core 0.1.2 → 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.
Files changed (123) hide show
  1. package/dist/app.d.ts +9 -1
  2. package/dist/app.d.ts.map +1 -1
  3. package/dist/app.js.map +1 -1
  4. package/dist/capabilities.js +1 -1
  5. package/dist/capabilities.js.map +1 -1
  6. package/dist/cli/check/policy.d.ts.map +1 -1
  7. package/dist/cli/check/policy.js +40 -0
  8. package/dist/cli/check/policy.js.map +1 -1
  9. package/dist/cli/index.d.ts +2 -1
  10. package/dist/cli/index.d.ts.map +1 -1
  11. package/dist/cli/index.js.map +1 -1
  12. package/dist/document/canonical.d.ts +2 -1
  13. package/dist/document/canonical.d.ts.map +1 -1
  14. package/dist/document/canonical.js +2 -1
  15. package/dist/document/canonical.js.map +1 -1
  16. package/dist/document/compile.d.ts.map +1 -1
  17. package/dist/document/compile.js +22 -2
  18. package/dist/document/compile.js.map +1 -1
  19. package/dist/document/diff.d.ts.map +1 -1
  20. package/dist/document/diff.js +8 -1
  21. package/dist/document/diff.js.map +1 -1
  22. package/dist/document/edit.d.ts +7 -1
  23. package/dist/document/edit.d.ts.map +1 -1
  24. package/dist/document/edit.js +58 -2
  25. package/dist/document/edit.js.map +1 -1
  26. package/dist/document/index.d.ts +1 -1
  27. package/dist/document/index.d.ts.map +1 -1
  28. package/dist/document/migrate.d.ts.map +1 -1
  29. package/dist/document/migrate.js +3 -2
  30. package/dist/document/migrate.js.map +1 -1
  31. package/dist/document/patch.d.ts.map +1 -1
  32. package/dist/document/patch.js +2 -5
  33. package/dist/document/patch.js.map +1 -1
  34. package/dist/document/schema.d.ts +12 -10
  35. package/dist/document/schema.d.ts.map +1 -1
  36. package/dist/document/schema.js +2 -0
  37. package/dist/document/schema.js.map +1 -1
  38. package/dist/document/to-document.d.ts.map +1 -1
  39. package/dist/document/to-document.js +5 -1
  40. package/dist/document/to-document.js.map +1 -1
  41. package/dist/graph/graph.d.ts +9 -0
  42. package/dist/graph/graph.d.ts.map +1 -1
  43. package/dist/graph/graph.js +18 -4
  44. package/dist/graph/graph.js.map +1 -1
  45. package/dist/index.d.ts +11 -8
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +7 -5
  48. package/dist/index.js.map +1 -1
  49. package/dist/integrity.d.ts.map +1 -1
  50. package/dist/integrity.js +35 -5
  51. package/dist/integrity.js.map +1 -1
  52. package/dist/invariants/types.d.ts +1 -1
  53. package/dist/invariants/types.d.ts.map +1 -1
  54. package/dist/labels.js +1 -1
  55. package/dist/labels.js.map +1 -1
  56. package/dist/mutations/define-mutation.d.ts +22 -2
  57. package/dist/mutations/define-mutation.d.ts.map +1 -1
  58. package/dist/mutations/define-mutation.js +21 -8
  59. package/dist/mutations/define-mutation.js.map +1 -1
  60. package/dist/mutations/node-ref.d.ts +5 -2
  61. package/dist/mutations/node-ref.d.ts.map +1 -1
  62. package/dist/mutations/node-ref.js +28 -7
  63. package/dist/mutations/node-ref.js.map +1 -1
  64. package/dist/mutations/types.d.ts +2 -2
  65. package/dist/mutations/types.d.ts.map +1 -1
  66. package/dist/ops/log.d.ts +22 -0
  67. package/dist/ops/log.d.ts.map +1 -1
  68. package/dist/ops/log.js +14 -0
  69. package/dist/ops/log.js.map +1 -1
  70. package/dist/ops/served.d.ts +80 -0
  71. package/dist/ops/served.d.ts.map +1 -0
  72. package/dist/ops/served.js +316 -0
  73. package/dist/ops/served.js.map +1 -0
  74. package/dist/ops/types.d.ts +8 -0
  75. package/dist/ops/types.d.ts.map +1 -1
  76. package/dist/ops/undo.d.ts +6 -0
  77. package/dist/ops/undo.d.ts.map +1 -1
  78. package/dist/ops/undo.js +3 -0
  79. package/dist/ops/undo.js.map +1 -1
  80. package/dist/ops/withheld.d.ts +63 -8
  81. package/dist/ops/withheld.d.ts.map +1 -1
  82. package/dist/ops/withheld.js +198 -22
  83. package/dist/ops/withheld.js.map +1 -1
  84. package/dist/ops/writers.d.ts +29 -0
  85. package/dist/ops/writers.d.ts.map +1 -0
  86. package/dist/ops/writers.js +98 -0
  87. package/dist/ops/writers.js.map +1 -0
  88. package/dist/permissions/sight.d.ts +33 -4
  89. package/dist/permissions/sight.d.ts.map +1 -1
  90. package/dist/permissions/sight.js +106 -22
  91. package/dist/permissions/sight.js.map +1 -1
  92. package/dist/presence.d.ts +48 -1
  93. package/dist/presence.d.ts.map +1 -1
  94. package/dist/presence.js +51 -2
  95. package/dist/presence.js.map +1 -1
  96. package/dist/refusal.d.ts +38 -0
  97. package/dist/refusal.d.ts.map +1 -0
  98. package/dist/refusal.js +44 -0
  99. package/dist/refusal.js.map +1 -0
  100. package/dist/schema/json-schema.d.ts.map +1 -1
  101. package/dist/schema/json-schema.js +27 -1
  102. package/dist/schema/json-schema.js.map +1 -1
  103. package/dist/schema/schema.d.ts +19 -4
  104. package/dist/schema/schema.d.ts.map +1 -1
  105. package/dist/schema/schema.js.map +1 -1
  106. package/dist/schema/types.d.ts +11 -0
  107. package/dist/schema/types.d.ts.map +1 -1
  108. package/dist/seen.d.ts +60 -1
  109. package/dist/seen.d.ts.map +1 -1
  110. package/dist/seen.js +467 -29
  111. package/dist/seen.js.map +1 -1
  112. package/dist/store.d.ts +151 -10
  113. package/dist/store.d.ts.map +1 -1
  114. package/dist/store.js +228 -40
  115. package/dist/store.js.map +1 -1
  116. package/dist/testing.js.map +1 -1
  117. package/dist/validate-graph.d.ts +2 -1
  118. package/dist/validate-graph.d.ts.map +1 -1
  119. package/dist/validate-graph.js +8 -1
  120. package/dist/validate-graph.js.map +1 -1
  121. package/dist/version.d.ts +1 -1
  122. package/dist/version.js +1 -1
  123. package/package.json +2 -2
package/dist/store.js CHANGED
@@ -1,5 +1,5 @@
1
- import { readingOf, seenBy, seesId } from "./seen.js";
2
- import { Graph, GraphError } from "./graph/graph.js";
1
+ import { hidesFrom, readingOf, seatLens, seenBy, seesId } from "./seen.js";
2
+ import { Graph, GraphError, MissingRecordError } from "./graph/graph.js";
3
3
  import { resolveModules } from "./modules.js";
4
4
  import { diffSnapshots, EMPTY_DIFF, isEmptyDiff } from "./graph/diff.js";
5
5
  import { invert, normalise, writesOf } from "./graph/primitives.js";
@@ -7,17 +7,20 @@ import { LabelIndex, refusalFor } from "./labels.js";
7
7
  import { nodeRefArgs } from "./mutations/node-ref.js";
8
8
  import { verifyFold } from "./integrity.js";
9
9
  import { evaluate } from "./invariants/engine.js";
10
- import { compileMutation } from "./mutations/define-mutation.js";
10
+ import { compileMutation, READ_BEFORE_REFUSING } from "./mutations/define-mutation.js";
11
11
  import { deriveMutations, derivedVia } from "./mutations/derive-edits.js";
12
12
  import { OperationLog } from "./ops/log.js";
13
13
  import { actingAs, isSystem, permits, permittedMutations } from "./permissions/policy.js";
14
- import { redact } from "./ops/withheld.js";
14
+ import { namesUnseen, redact } from "./ops/withheld.js";
15
15
  import { nounOf } from "./schema/define-node.js";
16
+ import { creatorsBefore } from "./permissions/sight.js";
16
17
  import { PermissionDeniedError } from "./permissions/types.js";
17
18
  import { checkUndo, UndoBlockedError, undoPrimitives } from "./ops/undo.js";
18
19
  import { validateGraph } from "./validate-graph.js";
19
20
  import { tellTheWatchItsNames, tellTheWatchOfAStore, tellTheWatchOfAnAuthor, tellTheWatchOfARefusal } from "./watched.js";
20
21
  const HUMAN = { kind: "human" };
22
+ /** The host itself, which sees what it keeps: the principal a question is asked as when no seat is named. */
23
+ const SYSTEM = { kind: "system" };
21
24
  /** Who turns a module off or on: the workspace itself, `system · modules` (FR-12). */
22
25
  export const MODULES_AUTHOR = { kind: "system", id: "modules", name: "Modules" };
23
26
  /**
@@ -58,6 +61,19 @@ export class ReceiveError extends Error {
58
61
  this.name = "ReceiveError";
59
62
  }
60
63
  }
64
+ /**
65
+ * A CALL NAMES AN ACT THE APP DOES NOT HAVE. Its message lists every act
66
+ * the store has, for the developer reading it; `mutation` is the name the
67
+ * call gave, so a surface that tells a person can say only that (FR-46).
68
+ */
69
+ export class UnknownMutationError extends GraphError {
70
+ mutation;
71
+ constructor(mutation, registered) {
72
+ super(`Unknown mutation "${mutation}"`, `Registered: ${registered.join(", ") || "(none)"}`);
73
+ this.mutation = mutation;
74
+ this.name = "UnknownMutationError";
75
+ }
76
+ }
61
77
  /**
62
78
  * A tag nobody else's store is using, for this store's batch ids. Drawn
63
79
  * from the platform's random source where there is one, which every page
@@ -100,7 +116,16 @@ export class Store {
100
116
  now;
101
117
  validate;
102
118
  mintBatch;
119
+ /**
120
+ * THE TAG THIS STORE MINTS ITS OWN BATCHES UNDER — `batch:<tag>:<n>` —
121
+ * when it mints them its default way; undefined when `batchIds` says
122
+ * otherwise. A server that takes batch ids from clients refuses this tag
123
+ * from one, so nothing the store lands later joins a client's batch.
124
+ */
125
+ batchTag;
103
126
  counter = 0;
127
+ /** The default op id generator's count. */
128
+ opCount = 0;
104
129
  /** Above zero while a rebase applies its pending calls again: they are told as one change at the end. */
105
130
  quiet = 0;
106
131
  listeners = new Set();
@@ -116,11 +141,11 @@ export class Store {
116
141
  }
117
142
  this.declaredModules = options.modules;
118
143
  this.startingModules = options.enabledModules;
119
- let n = 0;
120
- this.nextId = options.ids ?? (() => `op${++n}`);
144
+ this.nextId = options.ids ?? (() => `op${++this.opCount}`);
121
145
  this.now = options.now ?? (() => new Date().toISOString());
122
146
  this.validate = options.validate ?? true;
123
147
  const tag = storeTag();
148
+ this.batchTag = options.batchIds ? undefined : tag;
124
149
  this.mintBatch = options.batchIds ?? ((kind) => `${kind}:${tag}:${++this.counter}`);
125
150
  for (const mutation of options.mutations ?? []) {
126
151
  if (this.mutations.has(mutation.name)) {
@@ -174,18 +199,22 @@ export class Store {
174
199
  * Only the DEFAULT generators are wound forward; an app that supplies
175
200
  * its own `ids` owns their uniqueness.
176
201
  */
202
+ this.countPastTheLog();
203
+ tellTheWatchOfAStore(this);
204
+ }
205
+ /** Winds the default op and batch counters past every id the log holds, and past the horizon (FR-23). */
206
+ countPastTheLog() {
177
207
  const trailing = (value) => {
178
208
  const digits = /(\d+)$/.exec(value);
179
209
  return digits ? Number(digits[1]) : 0;
180
210
  };
181
211
  for (const op of this.log.all()) {
182
212
  this.counter = Math.max(this.counter, trailing(op.batch));
183
- n = Math.max(n, trailing(op.id));
213
+ this.opCount = Math.max(this.opCount, trailing(op.id));
184
214
  }
185
215
  // Behind an undo horizon (FR-23) are ids the log no longer holds: count on past them all.
186
216
  if (this.log.horizon > 0)
187
- n = Math.max(n, this.log.length);
188
- tellTheWatchOfAStore(this);
217
+ this.opCount = Math.max(this.opCount, this.log.length);
189
218
  }
190
219
  /**
191
220
  * What the enabled modules work out to; every surface reads this one answer.
@@ -266,7 +295,7 @@ export class Store {
266
295
  mutation(name) {
267
296
  const found = this.mutations.get(name);
268
297
  if (!found) {
269
- throw new GraphError(`Unknown mutation "${name}"`, `Registered: ${[...this.mutations.keys()].join(", ") || "(none)"}`);
298
+ throw new UnknownMutationError(name, [...this.mutations.keys()]);
270
299
  }
271
300
  // A refusal is a result: the mutation exists, and this workspace has its
272
301
  // module off — which is a different sentence from "unknown".
@@ -323,10 +352,10 @@ export class Store {
323
352
  * repair as the starter seat, which may only add, and the press met
324
353
  * "starter may not take-off here" (W-110).
325
354
  */
326
- const narrowed = this.refusesAgent(call, principal) ?? this.namesTurnedOff(call) ?? this.namesUnseen(call, principal);
355
+ const narrowed = this.refusesAgent(call, principal) ?? this.namesTurnedOff(call, principal);
327
356
  if (narrowed)
328
357
  return { ok: false, refusal: narrowed };
329
- return permits(this.policy, principal, call.name, this.subjectKindOf(call), this.viaOf(this.mutations.get(call.name)), this.subjectIdOf(call), this.words);
358
+ return permits(this.policy, principal, call.name, this.subjectKindOf(call, principal), this.viaOf(this.mutations.get(call.name)), this.subjectIdOf(call), this.words);
330
359
  }
331
360
  /** How the policy's refusals name an act and a kind: by title and noun, never by id. */
332
361
  get words() {
@@ -362,10 +391,13 @@ export class Store {
362
391
  * the core owns — a rename that takes any kind — must not reach into a
363
392
  * district the workspace does not have. The sentence names the act.
364
393
  */
365
- namesTurnedOff(call) {
394
+ namesTurnedOff(call, principal = SYSTEM) {
366
395
  const off = this.modules.disabledKinds;
367
396
  if (off.size === 0)
368
397
  return undefined;
398
+ // A seat the module's records are kept from is not told they are there: naming one is naming nothing (FR-55).
399
+ if (!isSystem(principal))
400
+ return undefined;
369
401
  const mutation = this.mutations.get(call.name);
370
402
  if (!mutation)
371
403
  return undefined;
@@ -384,16 +416,53 @@ export class Store {
384
416
  };
385
417
  }
386
418
  /**
387
- * A CALL THAT NAMES A RECORD THE CALLER MAY NOT SEE IS REFUSED (FR-02),
388
- * before any grant is read: an act on a record is a way of reading it —
389
- * its preview, its refusal, what its rules say after — and a seat that
390
- * could act on what it may not see could learn it that way. Only a
391
- * record that is there is judged; an id that names nothing is the act's
392
- * own to refuse. The sentence names the act, never the record or its kind.
419
+ * AN ACT'S SENTENCE IS WORDED FROM ITS AUTHOR'S VIEW (FR-55). `describe`
420
+ * reads the graph to name what the act touched ("Point at Venue"); read
421
+ * from the whole store, it named a record the author may not see by its
422
+ * label, and so told the author whether a guessed id was real. Read from
423
+ * the store as the author is served it, such a record is named only by
424
+ * what the author wrote, as one that does not exist is. A reader who sees
425
+ * more reads the author's sentence as the author would have. With nothing
426
+ * kept from the author, the store's own graph.
393
427
  */
394
- namesUnseen(call, principal) {
395
- if (!this.policy?.sees?.length || isSystem(principal))
396
- return undefined;
428
+ wordedFor(author) {
429
+ if (author === undefined || !hidesFrom(this, author))
430
+ return {};
431
+ // And a seat that writes a value naming what it may not see has written its own words, though the store held them already.
432
+ const hidden = seesId(this, author);
433
+ return { describeWith: seenBy(this, author).graph, keepUnchanged: (value) => namesUnseen(value, hidden) };
434
+ }
435
+ /**
436
+ * AN ACT'S OWN REFUSAL, AS ITS CALLER MAY READ IT (FR-55). An act's body
437
+ * may refuse in its own words, worded from what it read — "Freya Davies
438
+ * is already booked" — and what it read may be a record its caller may
439
+ * not see. Such a refusal is said without its words: the act, and that it
440
+ * could not be done. The store's own refusals already name nothing.
441
+ */
442
+ refusalWordedFor(error, author, calls) {
443
+ if (!hidesFrom(this, author) || error instanceof PermissionDeniedError || error instanceof MissingRecordError || error === null || typeof error !== "object")
444
+ return error;
445
+ const read = error[READ_BEFORE_REFUSING] ?? [];
446
+ const visible = seesId(this, author);
447
+ if (!read.some((id) => this.graph.has(id) && !visible(id)))
448
+ return error;
449
+ const title = calls.length === 1 ? (this.mutations.get(calls[0].name)?.title ?? calls[0].name) : undefined;
450
+ return new GraphError(title ? `“${title}” could not be done as asked.` : "This change could not be done as asked.");
451
+ }
452
+ /**
453
+ * A CALL THAT NAMES A RECORD THAT IS NOT THERE FOR ITS CALLER IS REFUSED
454
+ * AS MISSING (FR-02, FR-55) — one that does not exist, and one the caller
455
+ * may not see, alike: the same `MissingRecordError`, the same sentence,
456
+ * reason `missing`. Ids are minted from labels, so a seat can guess one,
457
+ * and a refusal that told a hidden record from an absent one would tell
458
+ * it which guesses were real. An act on a record is also a way of reading
459
+ * it — its preview, what its rules say after — so a hidden one is never
460
+ * acted on by a seat that may not see it. Asked of each call as it is
461
+ * about to run, so a record an earlier call in the same gesture made is
462
+ * there. The sentence names the act, never the id: it says the same of a
463
+ * hidden record and an absent one, and says back nothing the seat sent.
464
+ */
465
+ missingFor(call, principal = HUMAN) {
397
466
  const mutation = this.mutations.get(call.name);
398
467
  if (!mutation)
399
468
  return undefined;
@@ -410,14 +479,15 @@ export class Store {
410
479
  add(call.args[mutation.subject.arg]);
411
480
  for (const ref of nodeRefArgs(mutation.input))
412
481
  add(call.args[ref.name]);
413
- const visible = seesId(this, principal);
414
- if (![...named].some((id) => this.graph.has(id) && !visible(id)))
482
+ if (named.size === 0)
415
483
  return undefined;
416
- return {
417
- mutation: call.name,
418
- message: `Not permitted: “${mutation.title ?? call.name}” names a record you may not see.`,
419
- wouldNeed: [],
420
- };
484
+ const visible = seesId(this, principal);
485
+ for (const id of named) {
486
+ if (this.graph.has(id) && visible(id))
487
+ continue;
488
+ return new MissingRecordError(id, `“${mutation.title ?? call.name}” names a record that is not there.`);
489
+ }
490
+ return undefined;
421
491
  }
422
492
  /**
423
493
  * Whether a principal may run ANY act of a module drawn only for those
@@ -536,7 +606,7 @@ export class Store {
536
606
  * refuses that case rather than allowing it: the safe reading of "I could
537
607
  * not tell what this acts on" is no.
538
608
  */
539
- subjectKindOf(call) {
609
+ subjectKindOf(call, principal = SYSTEM) {
540
610
  const definition = this.mutations.get(call.name);
541
611
  const subject = definition?.subject;
542
612
  if (!subject)
@@ -550,6 +620,9 @@ export class Store {
550
620
  */
551
621
  if (typeof id !== "string")
552
622
  return subject.kinds !== "*" && subject.kinds.length === 1 ? subject.kinds[0] : undefined;
623
+ // A record the principal may not see is judged as one that is not there: its kind is not the policy's to read aloud (FR-55).
624
+ if (!isSystem(principal) && !seesId(this, principal)(id))
625
+ return undefined;
553
626
  return this.graph.getNode(id)?.kind;
554
627
  }
555
628
  /** Current violations, evaluated fresh — nothing is cached or stale. */
@@ -585,9 +658,9 @@ export class Store {
585
658
  return true;
586
659
  }
587
660
  }
588
- preview(call, context) {
661
+ preview(call, context, options = {}) {
589
662
  const definition = this.mutation(call.name);
590
- const compiled = compileMutation(this.graph, definition, call.args);
663
+ const compiled = compileMutation(this.graph, definition, call.args, this.wordedFor(options.author));
591
664
  return this.previewPrimitives(compiled.primitives, {
592
665
  reads: compiled.reads,
593
666
  writes: compiled.writes,
@@ -686,6 +759,7 @@ export class Store {
686
759
  const intents = [];
687
760
  const before = this.violations();
688
761
  const rollback = this.graph.snapshot();
762
+ const kept = this.log.length;
689
763
  /*
690
764
  * Permission is checked for the WHOLE gesture before any of it runs.
691
765
  *
@@ -715,7 +789,7 @@ export class Store {
715
789
  * was brought in for, and the narrower of the two wins.
716
790
  */
717
791
  for (const call of calls) {
718
- const refusal = this.refusesAgent(call, author) ?? this.namesTurnedOff(call);
792
+ const refusal = this.refusesAgent(call, author) ?? this.namesTurnedOff(call, author);
719
793
  if (refusal) {
720
794
  tellTheWatchOfARefusal(refusal, author.id);
721
795
  throw new PermissionDeniedError(refusal);
@@ -725,7 +799,10 @@ export class Store {
725
799
  try {
726
800
  for (const call of calls) {
727
801
  const definition = this.mutation(call.name);
728
- const compiled = compileMutation(this.graph, definition, call.args);
802
+ const missing = this.missingFor(call, author);
803
+ if (missing)
804
+ throw missing;
805
+ const compiled = compileMutation(this.graph, definition, call.args, this.wordedFor(author));
729
806
  /*
730
807
  * AN ACT THAT DID NOTHING DOES NOT GO IN THE HISTORY.
731
808
  *
@@ -763,6 +840,8 @@ export class Store {
763
840
  inverse: [...compiled.primitives].reverse().map(normalise).map(invert),
764
841
  reads: compiled.reads,
765
842
  writes: compiled.writes,
843
+ // Which records its sentence read, so a reader who may not see one is not handed the sentence (FR-55).
844
+ ...(definition.describe !== undefined && compiled.described && compiled.described.length > 0 ? { described: compiled.described } : {}),
766
845
  at: this.now(),
767
846
  ...(options.via !== undefined ? { via: options.via } : {}),
768
847
  };
@@ -778,13 +857,15 @@ export class Store {
778
857
  }
779
858
  }
780
859
  catch (error) {
781
- this.graph.load(rollback);
782
- throw error;
860
+ // A batch refused part way keeps none of it: the graph, and the ops it had logged before the refusal.
861
+ this.putBack(rollback, kept);
862
+ throw this.refusalWordedFor(error, author, calls);
783
863
  }
864
+ const diff = diffSnapshots(rollback, this.graph.snapshot());
865
+ this.admitted(options, ops, allPrimitives, diff, rollback, kept);
784
866
  const after = this.violations();
785
867
  const beforeKeys = new Set(before.map(violationKey));
786
868
  const afterKeys = new Set(after.map(violationKey));
787
- const diff = diffSnapshots(rollback, this.graph.snapshot());
788
869
  this.notify(diff, ops);
789
870
  return {
790
871
  batch,
@@ -1051,7 +1132,7 @@ export class Store {
1051
1132
  */
1052
1133
  const sighted = (this.policy?.sees?.length ?? 0) > 0 && !isSystem(author);
1053
1134
  if (sighted) {
1054
- const seen = checkUndo(readingOf(redact(this.log.all(), seesId(this, author)), () => this.log.epochs()), ids);
1135
+ const seen = checkUndo(readingOf(redact(this.log.all(), seatLens(this, author)), () => this.log.epochs()), ids);
1055
1136
  if (!seen.ok)
1056
1137
  throw new UndoBlockedError(seen);
1057
1138
  }
@@ -1089,6 +1170,7 @@ export class Store {
1089
1170
  const rollback = this.graph.snapshot();
1090
1171
  const before = this.violations();
1091
1172
  const ops = [];
1173
+ const kept = this.log.length;
1092
1174
  for (const target of [...check.ops].sort((a, b) => b.seq - a.seq)) {
1093
1175
  const op = {
1094
1176
  id: this.nextId(),
@@ -1110,10 +1192,11 @@ export class Store {
1110
1192
  this.log.append(op);
1111
1193
  ops.push(op);
1112
1194
  }
1195
+ const diff = diffSnapshots(rollback, this.graph.snapshot());
1196
+ this.admitted(options, ops, ops.flatMap((op) => [...op.primitives]), diff, rollback, kept);
1113
1197
  const after = this.violations();
1114
1198
  const beforeKeys = new Set(before.map(violationKey));
1115
1199
  const afterKeys = new Set(after.map(violationKey));
1116
- const diff = diffSnapshots(rollback, this.graph.snapshot());
1117
1200
  this.notify(diff, ops);
1118
1201
  return {
1119
1202
  batch,
@@ -1128,6 +1211,33 @@ export class Store {
1128
1211
  violationsAfter: after,
1129
1212
  };
1130
1213
  }
1214
+ /** A change not kept: the graph as it was, and the log cut back to where it stood. */
1215
+ putBack(rollback, kept) {
1216
+ this.graph.load(rollback);
1217
+ if (this.log.length > kept)
1218
+ this.log.truncate(kept);
1219
+ }
1220
+ /** Asks the host's `admit`, when there is one, with the change as planned; one it refuses is put back, and its word goes on up. */
1221
+ admitted(options, ops, primitives, diff, rollback, kept) {
1222
+ if (!options.admit || ops.length === 0)
1223
+ return;
1224
+ const size = this.graph.size;
1225
+ try {
1226
+ options.admit({
1227
+ ops,
1228
+ primitives,
1229
+ added: { nodes: diff.addedNodes.length, edges: diff.addedEdges.length },
1230
+ removed: { nodes: diff.removedNodes.length, edges: diff.removedEdges.length },
1231
+ changed: { nodes: diff.changedNodes.length },
1232
+ nodesAfter: size.nodes,
1233
+ edgesAfter: size.edges,
1234
+ });
1235
+ }
1236
+ catch (error) {
1237
+ this.putBack(rollback, kept);
1238
+ throw error;
1239
+ }
1240
+ }
1131
1241
  /** Redo is the undo of an undo — no separate stack exists. */
1132
1242
  redo(undoBatchId, options = {}) {
1133
1243
  return this.undo(undoBatchId, options);
@@ -1253,6 +1363,78 @@ export class Store {
1253
1363
  this.notify(diff, ops);
1254
1364
  return { diff, confirmed: landed, pending: replayed, refused };
1255
1365
  }
1366
+ /**
1367
+ * A CLIENT THAT RESYNCED TAKES THE SERVER'S WHOLE STATE (FR-53).
1368
+ *
1369
+ * `rebase` lands the server's ops on the confirmed prefix this store
1370
+ * holds. When that prefix cannot be trusted — this copy drifted, and an
1371
+ * op of the server's no longer fits it, or the server compacted past
1372
+ * where this client left off, so `/graview/since` cannot bring the ops in
1373
+ * between — the client takes the server's graph and log as they are:
1374
+ *
1375
+ * 1. the ops of the `pending` batches are read off the log as it stands;
1376
+ * 2. the log becomes `log` (with its `epochs` and `horizon`, checked as
1377
+ * `OperationLog.from` checks them; one that is not intact is refused
1378
+ * before anything moves) and the graph becomes `snapshot`, in the
1379
+ * server's order — not put back op by op, as `rebase` puts it;
1380
+ * 3. the `pending` batches are applied again on top, in the order given,
1381
+ * exactly as `rebase` applies them: under the same batch ids, judged
1382
+ * and compiled afresh. One that no longer applies is said in `refused`.
1383
+ *
1384
+ * `drop` names the batches the server has answered or refused: they are
1385
+ * not applied again (nor is any batch left unnamed: the log is the
1386
+ * server's). Subscribers hear ONE change, the net diff, with the ops this
1387
+ * store did not hold before and the re-applied ones.
1388
+ */
1389
+ adopt(state) {
1390
+ const dropped = new Set(state.drop ?? []);
1391
+ const pending = (state.pending ?? []).filter((batch) => !dropped.has(batch));
1392
+ const held = this.log.all();
1393
+ const before = this.graph.snapshot();
1394
+ const was = { ops: [...held], epochs: [...this.log.epochs()], horizon: this.log.horizon };
1395
+ const known = new Set(held.map((op) => op.id));
1396
+ const tails = new Map(pending.map((batch) => [batch, held.filter((op) => op.batch === batch)]));
1397
+ // A log that is not intact is refused here, before the graph moves.
1398
+ if (state.log.length > 0)
1399
+ this.log.replace(state.log, state.epochs, state.horizon !== undefined ? { horizon: state.horizon } : {});
1400
+ else {
1401
+ const at = state.horizon ?? 0;
1402
+ this.log.replace([], state.epochs ?? [{ seq: at, base: state.snapshot }], { horizon: at });
1403
+ }
1404
+ const replayed = [];
1405
+ const refused = [];
1406
+ this.quiet++;
1407
+ try {
1408
+ this.graph.load(state.snapshot);
1409
+ this.projected = undefined;
1410
+ this.countPastTheLog();
1411
+ for (const batch of pending) {
1412
+ const ops = tails.get(batch) ?? [];
1413
+ if (ops.length === 0)
1414
+ continue;
1415
+ try {
1416
+ replayed.push(...this.replay(batch, ops));
1417
+ }
1418
+ catch (error) {
1419
+ refused.push({ batch, error });
1420
+ }
1421
+ }
1422
+ }
1423
+ catch (error) {
1424
+ // A store that could not adopt is the store it was.
1425
+ this.log.replace(was.ops, was.epochs, { horizon: was.horizon });
1426
+ this.graph.load(before);
1427
+ this.projected = undefined;
1428
+ throw error;
1429
+ }
1430
+ finally {
1431
+ this.quiet--;
1432
+ }
1433
+ const adopted = this.log.all().filter((op) => !known.has(op.id) && !replayed.includes(op));
1434
+ const diff = diffSnapshots(before, this.graph.snapshot());
1435
+ this.notify(diff, [...adopted, ...replayed]);
1436
+ return { diff, adopted, pending: replayed, refused };
1437
+ }
1256
1438
  /** One pending batch, applied again as it was made: its calls, its undo, or its primitives. */
1257
1439
  replay(batch, ops) {
1258
1440
  const head = ops[0];
@@ -1300,6 +1482,9 @@ export class Store {
1300
1482
  * 1000). It is moved back to the start of the gesture it would split, so
1301
1483
  * a batch is never half behind it.
1302
1484
  *
1485
+ * The checkpoint keeps who made each record behind it (`creators`), so a
1486
+ * store opened on it knows a record's maker without the archived ops.
1487
+ *
1303
1488
  * Nothing moves until `compact` is handed the checkpoint, so a host can
1304
1489
  * archive what is behind it first. A store whose graph does not verify
1305
1490
  * against its log is not compacted: the checkpoint would vouch for a
@@ -1327,7 +1512,10 @@ export class Store {
1327
1512
  const verified = this.verify();
1328
1513
  if (!verified.ok)
1329
1514
  throw new GraphError(`Cannot compact a store that does not verify: ${verified.reason}`);
1330
- return this.log.checkpointAt(this.schema, seq, { validate: this.validate });
1515
+ const checkpoint = this.log.checkpointAt(this.schema, seq, { validate: this.validate });
1516
+ // Who made each record behind it, so a store opened on it still knows (an `own` sight reads the log for it).
1517
+ const creators = creatorsBefore(this.log, seq);
1518
+ return Object.keys(creators).length > 0 ? { ...checkpoint, creators } : checkpoint;
1331
1519
  }
1332
1520
  /**
1333
1521
  * MOVES THE UNDO HORIZON TO `checkpoint` (FR-23): the ops and epochs