@graview/core 0.1.3 → 0.1.5

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 (124) 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 +14 -2
  17. package/dist/document/compile.d.ts.map +1 -1
  18. package/dist/document/compile.js +50 -24
  19. package/dist/document/compile.js.map +1 -1
  20. package/dist/document/diff.d.ts.map +1 -1
  21. package/dist/document/diff.js +3 -1
  22. package/dist/document/diff.js.map +1 -1
  23. package/dist/document/edit.d.ts +7 -1
  24. package/dist/document/edit.d.ts.map +1 -1
  25. package/dist/document/edit.js +46 -2
  26. package/dist/document/edit.js.map +1 -1
  27. package/dist/document/graview-template.d.ts +2 -2
  28. package/dist/document/index.d.ts +2 -2
  29. package/dist/document/index.d.ts.map +1 -1
  30. package/dist/document/index.js +1 -1
  31. package/dist/document/index.js.map +1 -1
  32. package/dist/document/migrate.d.ts.map +1 -1
  33. package/dist/document/migrate.js +3 -2
  34. package/dist/document/migrate.js.map +1 -1
  35. package/dist/document/patch.d.ts.map +1 -1
  36. package/dist/document/patch.js +2 -5
  37. package/dist/document/patch.js.map +1 -1
  38. package/dist/document/schema.d.ts +298 -298
  39. package/dist/document/schema.d.ts.map +1 -1
  40. package/dist/document/schema.js +125 -140
  41. package/dist/document/schema.js.map +1 -1
  42. package/dist/document/to-document.d.ts.map +1 -1
  43. package/dist/document/to-document.js +2 -1
  44. package/dist/document/to-document.js.map +1 -1
  45. package/dist/index.d.ts +9 -6
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +12 -4
  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/mutations/define-mutation.d.ts +26 -3
  55. package/dist/mutations/define-mutation.d.ts.map +1 -1
  56. package/dist/mutations/define-mutation.js +18 -5
  57. package/dist/mutations/define-mutation.js.map +1 -1
  58. package/dist/mutations/derive-edits.js +5 -5
  59. package/dist/mutations/derive-edits.js.map +1 -1
  60. package/dist/mutations/form.d.ts.map +1 -1
  61. package/dist/mutations/form.js +6 -5
  62. package/dist/mutations/form.js.map +1 -1
  63. package/dist/mutations/node-ref.d.ts +16 -5
  64. package/dist/mutations/node-ref.d.ts.map +1 -1
  65. package/dist/mutations/node-ref.js +52 -16
  66. package/dist/mutations/node-ref.js.map +1 -1
  67. package/dist/mutations/types.d.ts +2 -2
  68. package/dist/mutations/types.d.ts.map +1 -1
  69. package/dist/ops/log.d.ts +11 -0
  70. package/dist/ops/log.d.ts.map +1 -1
  71. package/dist/ops/log.js.map +1 -1
  72. package/dist/ops/served.d.ts +80 -0
  73. package/dist/ops/served.d.ts.map +1 -0
  74. package/dist/ops/served.js +316 -0
  75. package/dist/ops/served.js.map +1 -0
  76. package/dist/ops/types.d.ts +8 -0
  77. package/dist/ops/types.d.ts.map +1 -1
  78. package/dist/ops/withheld.d.ts +63 -8
  79. package/dist/ops/withheld.d.ts.map +1 -1
  80. package/dist/ops/withheld.js +198 -22
  81. package/dist/ops/withheld.js.map +1 -1
  82. package/dist/ops/writers.d.ts +29 -0
  83. package/dist/ops/writers.d.ts.map +1 -0
  84. package/dist/ops/writers.js +98 -0
  85. package/dist/ops/writers.js.map +1 -0
  86. package/dist/permissions/sight.d.ts +15 -2
  87. package/dist/permissions/sight.d.ts.map +1 -1
  88. package/dist/permissions/sight.js +34 -5
  89. package/dist/permissions/sight.js.map +1 -1
  90. package/dist/presence.d.ts +24 -2
  91. package/dist/presence.d.ts.map +1 -1
  92. package/dist/presence.js +26 -1
  93. package/dist/presence.js.map +1 -1
  94. package/dist/refusal.d.ts +9 -5
  95. package/dist/refusal.d.ts.map +1 -1
  96. package/dist/refusal.js +9 -5
  97. package/dist/refusal.js.map +1 -1
  98. package/dist/scaffold/project.d.ts.map +1 -1
  99. package/dist/scaffold/project.js +5 -0
  100. package/dist/scaffold/project.js.map +1 -1
  101. package/dist/schema/json-schema.d.ts +2 -3
  102. package/dist/schema/json-schema.d.ts.map +1 -1
  103. package/dist/schema/json-schema.js +30 -10
  104. package/dist/schema/json-schema.js.map +1 -1
  105. package/dist/schema/schema.d.ts +19 -4
  106. package/dist/schema/schema.d.ts.map +1 -1
  107. package/dist/schema/schema.js +2 -2
  108. package/dist/schema/schema.js.map +1 -1
  109. package/dist/schema/zod.d.ts +18 -0
  110. package/dist/schema/zod.d.ts.map +1 -0
  111. package/dist/schema/zod.js +44 -0
  112. package/dist/schema/zod.js.map +1 -0
  113. package/dist/seen.d.ts +54 -1
  114. package/dist/seen.d.ts.map +1 -1
  115. package/dist/seen.js +457 -25
  116. package/dist/seen.js.map +1 -1
  117. package/dist/store.d.ts +141 -17
  118. package/dist/store.d.ts.map +1 -1
  119. package/dist/store.js +203 -84
  120. package/dist/store.js.map +1 -1
  121. package/dist/testing.js.map +1 -1
  122. package/dist/version.d.ts +1 -1
  123. package/dist/version.js +1 -1
  124. 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,22 @@ 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";
21
+ /** The batch a preview's ops are under when it was given none: no store mints it. */
22
+ export const PREVIEW_BATCH = "preview";
20
23
  const HUMAN = { kind: "human" };
24
+ /** The host itself, which sees what it keeps: the principal a question is asked as when no seat is named. */
25
+ const SYSTEM = { kind: "system" };
21
26
  /** Who turns a module off or on: the workspace itself, `system · modules` (FR-12). */
22
27
  export const MODULES_AUTHOR = { kind: "system", id: "modules", name: "Modules" };
23
28
  /**
@@ -58,6 +63,19 @@ export class ReceiveError extends Error {
58
63
  this.name = "ReceiveError";
59
64
  }
60
65
  }
66
+ /**
67
+ * A CALL NAMES AN ACT THE APP DOES NOT HAVE. Its message lists every act
68
+ * the store has, for the developer reading it; `mutation` is the name the
69
+ * call gave, so a surface that tells a person can say only that (FR-46).
70
+ */
71
+ export class UnknownMutationError extends GraphError {
72
+ mutation;
73
+ constructor(mutation, registered) {
74
+ super(`Unknown mutation "${mutation}"`, `Registered: ${registered.join(", ") || "(none)"}`);
75
+ this.mutation = mutation;
76
+ this.name = "UnknownMutationError";
77
+ }
78
+ }
61
79
  /**
62
80
  * A tag nobody else's store is using, for this store's batch ids. Drawn
63
81
  * from the platform's random source where there is one, which every page
@@ -100,11 +118,20 @@ export class Store {
100
118
  now;
101
119
  validate;
102
120
  mintBatch;
121
+ /**
122
+ * THE TAG THIS STORE MINTS ITS OWN BATCHES UNDER — `batch:<tag>:<n>` —
123
+ * when it mints them its default way; undefined when `batchIds` says
124
+ * otherwise. A server that takes batch ids from clients refuses this tag
125
+ * from one, so nothing the store lands later joins a client's batch.
126
+ */
127
+ batchTag;
103
128
  counter = 0;
104
129
  /** The default op id generator's count. */
105
130
  opCount = 0;
106
131
  /** Above zero while a rebase applies its pending calls again: they are told as one change at the end. */
107
132
  quiet = 0;
133
+ /** True of a rehearsal (`previewAll`): what it does is told to nobody. */
134
+ rehearsing = false;
108
135
  listeners = new Set();
109
136
  constructor(options) {
110
137
  this.schema = options.schema;
@@ -122,6 +149,7 @@ export class Store {
122
149
  this.now = options.now ?? (() => new Date().toISOString());
123
150
  this.validate = options.validate ?? true;
124
151
  const tag = storeTag();
152
+ this.batchTag = options.batchIds ? undefined : tag;
125
153
  this.mintBatch = options.batchIds ?? ((kind) => `${kind}:${tag}:${++this.counter}`);
126
154
  for (const mutation of options.mutations ?? []) {
127
155
  if (this.mutations.has(mutation.name)) {
@@ -271,7 +299,7 @@ export class Store {
271
299
  mutation(name) {
272
300
  const found = this.mutations.get(name);
273
301
  if (!found) {
274
- throw new GraphError(`Unknown mutation "${name}"`, `Registered: ${[...this.mutations.keys()].join(", ") || "(none)"}`);
302
+ throw new UnknownMutationError(name, [...this.mutations.keys()]);
275
303
  }
276
304
  // A refusal is a result: the mutation exists, and this workspace has its
277
305
  // module off — which is a different sentence from "unknown".
@@ -328,10 +356,10 @@ export class Store {
328
356
  * repair as the starter seat, which may only add, and the press met
329
357
  * "starter may not take-off here" (W-110).
330
358
  */
331
- const narrowed = this.refusesAgent(call, principal) ?? this.namesTurnedOff(call) ?? this.namesUnseen(call, principal);
359
+ const narrowed = this.refusesAgent(call, principal) ?? this.namesTurnedOff(call, principal);
332
360
  if (narrowed)
333
361
  return { ok: false, refusal: narrowed };
334
- return permits(this.policy, principal, call.name, this.subjectKindOf(call), this.viaOf(this.mutations.get(call.name)), this.subjectIdOf(call), this.words);
362
+ return permits(this.policy, principal, call.name, this.subjectKindOf(call, principal), this.viaOf(this.mutations.get(call.name)), this.subjectIdOf(call), this.words);
335
363
  }
336
364
  /** How the policy's refusals name an act and a kind: by title and noun, never by id. */
337
365
  get words() {
@@ -367,10 +395,13 @@ export class Store {
367
395
  * the core owns — a rename that takes any kind — must not reach into a
368
396
  * district the workspace does not have. The sentence names the act.
369
397
  */
370
- namesTurnedOff(call) {
398
+ namesTurnedOff(call, principal = SYSTEM) {
371
399
  const off = this.modules.disabledKinds;
372
400
  if (off.size === 0)
373
401
  return undefined;
402
+ // A seat the module's records are kept from is not told they are there: naming one is naming nothing (FR-55).
403
+ if (!isSystem(principal))
404
+ return undefined;
374
405
  const mutation = this.mutations.get(call.name);
375
406
  if (!mutation)
376
407
  return undefined;
@@ -389,16 +420,53 @@ export class Store {
389
420
  };
390
421
  }
391
422
  /**
392
- * A CALL THAT NAMES A RECORD THE CALLER MAY NOT SEE IS REFUSED (FR-02),
393
- * before any grant is read: an act on a record is a way of reading it —
394
- * its preview, its refusal, what its rules say after — and a seat that
395
- * could act on what it may not see could learn it that way. Only a
396
- * record that is there is judged; an id that names nothing is the act's
397
- * own to refuse. The sentence names the act, never the record or its kind.
423
+ * AN ACT'S SENTENCE IS WORDED FROM ITS AUTHOR'S VIEW (FR-55). `describe`
424
+ * reads the graph to name what the act touched ("Point at Venue"); read
425
+ * from the whole store, it named a record the author may not see by its
426
+ * label, and so told the author whether a guessed id was real. Read from
427
+ * the store as the author is served it, such a record is named only by
428
+ * what the author wrote, as one that does not exist is. A reader who sees
429
+ * more reads the author's sentence as the author would have. With nothing
430
+ * kept from the author, the store's own graph.
398
431
  */
399
- namesUnseen(call, principal) {
400
- if (!this.policy?.sees?.length || isSystem(principal))
401
- return undefined;
432
+ wordedFor(author) {
433
+ if (author === undefined || !hidesFrom(this, author))
434
+ return {};
435
+ // And a seat that writes a value naming what it may not see has written its own words, though the store held them already.
436
+ const hidden = seesId(this, author);
437
+ return { describeWith: seenBy(this, author).graph, keepUnchanged: (value) => namesUnseen(value, hidden) };
438
+ }
439
+ /**
440
+ * AN ACT'S OWN REFUSAL, AS ITS CALLER MAY READ IT (FR-55). An act's body
441
+ * may refuse in its own words, worded from what it read — "Freya Davies
442
+ * is already booked" — and what it read may be a record its caller may
443
+ * not see. Such a refusal is said without its words: the act, and that it
444
+ * could not be done. The store's own refusals already name nothing.
445
+ */
446
+ refusalWordedFor(error, author, calls) {
447
+ if (!hidesFrom(this, author) || error instanceof PermissionDeniedError || error instanceof MissingRecordError || error === null || typeof error !== "object")
448
+ return error;
449
+ const read = error[READ_BEFORE_REFUSING] ?? [];
450
+ const visible = seesId(this, author);
451
+ if (!read.some((id) => this.graph.has(id) && !visible(id)))
452
+ return error;
453
+ const title = calls.length === 1 ? (this.mutations.get(calls[0].name)?.title ?? calls[0].name) : undefined;
454
+ return new GraphError(title ? `“${title}” could not be done as asked.` : "This change could not be done as asked.");
455
+ }
456
+ /**
457
+ * A CALL THAT NAMES A RECORD THAT IS NOT THERE FOR ITS CALLER IS REFUSED
458
+ * AS MISSING (FR-02, FR-55) — one that does not exist, and one the caller
459
+ * may not see, alike: the same `MissingRecordError`, the same sentence,
460
+ * reason `missing`. Ids are minted from labels, so a seat can guess one,
461
+ * and a refusal that told a hidden record from an absent one would tell
462
+ * it which guesses were real. An act on a record is also a way of reading
463
+ * it — its preview, what its rules say after — so a hidden one is never
464
+ * acted on by a seat that may not see it. Asked of each call as it is
465
+ * about to run, so a record an earlier call in the same gesture made is
466
+ * there. The sentence names the act, never the id: it says the same of a
467
+ * hidden record and an absent one, and says back nothing the seat sent.
468
+ */
469
+ missingFor(call, principal = HUMAN) {
402
470
  const mutation = this.mutations.get(call.name);
403
471
  if (!mutation)
404
472
  return undefined;
@@ -415,14 +483,15 @@ export class Store {
415
483
  add(call.args[mutation.subject.arg]);
416
484
  for (const ref of nodeRefArgs(mutation.input))
417
485
  add(call.args[ref.name]);
418
- const visible = seesId(this, principal);
419
- if (![...named].some((id) => this.graph.has(id) && !visible(id)))
486
+ if (named.size === 0)
420
487
  return undefined;
421
- return {
422
- mutation: call.name,
423
- message: `Not permitted: “${mutation.title ?? call.name}” names a record you may not see.`,
424
- wouldNeed: [],
425
- };
488
+ const visible = seesId(this, principal);
489
+ for (const id of named) {
490
+ if (this.graph.has(id) && visible(id))
491
+ continue;
492
+ return new MissingRecordError(id, `“${mutation.title ?? call.name}” names a record that is not there.`);
493
+ }
494
+ return undefined;
426
495
  }
427
496
  /**
428
497
  * Whether a principal may run ANY act of a module drawn only for those
@@ -541,7 +610,7 @@ export class Store {
541
610
  * refuses that case rather than allowing it: the safe reading of "I could
542
611
  * not tell what this acts on" is no.
543
612
  */
544
- subjectKindOf(call) {
613
+ subjectKindOf(call, principal = SYSTEM) {
545
614
  const definition = this.mutations.get(call.name);
546
615
  const subject = definition?.subject;
547
616
  if (!subject)
@@ -555,6 +624,9 @@ export class Store {
555
624
  */
556
625
  if (typeof id !== "string")
557
626
  return subject.kinds !== "*" && subject.kinds.length === 1 ? subject.kinds[0] : undefined;
627
+ // 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).
628
+ if (!isSystem(principal) && !seesId(this, principal)(id))
629
+ return undefined;
558
630
  return this.graph.getNode(id)?.kind;
559
631
  }
560
632
  /** Current violations, evaluated fresh — nothing is cached or stale. */
@@ -590,9 +662,9 @@ export class Store {
590
662
  return true;
591
663
  }
592
664
  }
593
- preview(call, context) {
665
+ preview(call, context, options = {}) {
594
666
  const definition = this.mutation(call.name);
595
- const compiled = compileMutation(this.graph, definition, call.args);
667
+ const compiled = compileMutation(this.graph, definition, call.args, this.wordedFor(options.author));
596
668
  return this.previewPrimitives(compiled.primitives, {
597
669
  reads: compiled.reads,
598
670
  writes: compiled.writes,
@@ -601,51 +673,52 @@ export class Store {
601
673
  });
602
674
  }
603
675
  /**
604
- * What several calls would do as one gesture, without doing any of it
605
- * (FR-18): each compiles on the graph the one before it left, as
606
- * `applyAll` would run them, on a copy. The diff is the net of them all,
607
- * and `introduces` and `resolves` judge the end state against today's.
608
- * A call that cannot compile throws, as it would on applying; nothing is
609
- * written, logged or heard either way.
676
+ * WHAT SEVERAL CALLS WOULD DO AS ONE GESTURE, JUDGED AS IT WOULD BE, WITHOUT
677
+ * DOING ANY OF IT (FR-18, FR-56).
678
+ *
679
+ * Not a second path beside `applyAll` but `applyAll`'s own, run on a
680
+ * rehearsal of this store — its graph and its log copied, nobody
681
+ * listening — so it cannot drift from it: it refuses exactly when the
682
+ * apply would, in the same words, for the same author, channel and batch
683
+ * — the policy, the seat's sight (a record the batch made earlier is its
684
+ * maker's for the calls after, read off the rehearsal's log as it goes),
685
+ * a declared agent's `may`, an act's own guard, and whatever `admit`
686
+ * throws, asked with the change as planned. Otherwise it answers what the
687
+ * apply would: the ops as the log would hold them, marked not kept (ids
688
+ * `preview:<n>`, `kept: false`), the net diff, and the problems after,
689
+ * judged in `context`. Nothing is written, logged, told to a subscriber
690
+ * of the store or its graph, or told to a watching harness; no id or
691
+ * batch this store would mint next is spent.
692
+ *
693
+ * Why not `applyAll(calls, { dryRun: true })`: a host wraps `applyAll` —
694
+ * `openRemote` sends every call down the wire — and a flag on it would
695
+ * travel with the call; and an answer that says `kept: false` in its type
696
+ * cannot be mistaken for one that was.
610
697
  */
611
- previewAll(calls, context) {
612
- const before = this.violations(context);
613
- const start = this.graph.snapshot();
614
- const trial = Graph.from(this.schema, start, { validate: this.validate });
615
- const primitives = [];
616
- const reads = new Set();
617
- const writes = new Set();
618
- const intents = [];
619
- for (const call of calls) {
620
- const compiled = compileMutation(trial, this.mutation(call.name), call.args);
621
- // An act that does nothing leaves no trace when applied, nor here.
622
- if (compiled.primitives.length === 0)
623
- continue;
624
- const recorded = compiled.primitives.map(normalise);
625
- trial.applyPrimitives(recorded);
626
- primitives.push(...recorded);
627
- for (const id of compiled.reads)
628
- reads.add(id);
629
- for (const id of compiled.writes)
630
- writes.add(id);
631
- intents.push(compiled.intent);
632
- }
633
- const after = evaluate(trial, this.allInvariants(), {
634
- ...this.invariantOptions,
635
- ...(context === undefined ? {} : { context }),
698
+ previewAll(calls, options = {}) {
699
+ const { context, ...applying } = options;
700
+ const result = this.rehearsal().applying(calls, applying, context);
701
+ return { ...result, kept: false };
702
+ }
703
+ /**
704
+ * THIS STORE, TO REHEARSE ON (FR-56): its declaration, policy, modules and
705
+ * clock as they are, its graph and log copied, and nobody listening. Its
706
+ * op ids are `preview:<n>` and its batch `preview`, so nothing this store
707
+ * would mint next is spent; it tells a watching harness nothing.
708
+ */
709
+ rehearsal() {
710
+ const twin = Object.assign(Object.create(Store.prototype), this);
711
+ let made = 0;
712
+ Object.assign(twin, {
713
+ graph: Graph.from(this.schema, this.graph.snapshot(), { validate: this.validate }),
714
+ log: OperationLog.from(this.log.all(), this.log.epochs(), { horizon: this.log.horizon }),
715
+ listeners: new Set(),
716
+ labels: undefined,
717
+ nextId: () => `${PREVIEW_BATCH}:${++made}`,
718
+ mintBatch: () => PREVIEW_BATCH,
719
+ rehearsing: true,
636
720
  });
637
- const beforeKeys = new Set(before.map(violationKey));
638
- const afterKeys = new Set(after.map(violationKey));
639
- return {
640
- diff: diffSnapshots(start, trial.snapshot()),
641
- primitives,
642
- reads: [...reads],
643
- writes: [...writes],
644
- intent: intents.join("; "),
645
- introduces: after.filter((v) => !beforeKeys.has(violationKey(v))),
646
- resolves: before.filter((v) => !afterKeys.has(violationKey(v))),
647
- violationsAfter: after,
648
- };
721
+ return twin;
649
722
  }
650
723
  previewPrimitives(primitives, meta) {
651
724
  const before = this.violations(meta.context);
@@ -681,7 +754,7 @@ export class Store {
681
754
  * through the store's own path, whatever a host has wrapped `applyAll` in
682
755
  * (`openRemote` sends each one down the wire).
683
756
  */
684
- applying(calls, options) {
757
+ applying(calls, options, context) {
685
758
  const batch = options.batch ?? this.mintBatch("batch");
686
759
  const author = options.author ?? HUMAN;
687
760
  const ops = [];
@@ -689,8 +762,9 @@ export class Store {
689
762
  const reads = new Set();
690
763
  const writes = new Set();
691
764
  const intents = [];
692
- const before = this.violations();
765
+ const before = this.violations(context);
693
766
  const rollback = this.graph.snapshot();
767
+ const kept = this.log.length;
694
768
  /*
695
769
  * Permission is checked for the WHOLE gesture before any of it runs.
696
770
  *
@@ -704,7 +778,8 @@ export class Store {
704
778
  for (const call of calls) {
705
779
  const verdict = this.permits(call, author);
706
780
  if (!verdict.ok) {
707
- tellTheWatchOfARefusal(verdict.refusal, author.id);
781
+ if (!this.rehearsing)
782
+ tellTheWatchOfARefusal(verdict.refusal, author.id);
708
783
  throw new PermissionDeniedError(verdict.refusal);
709
784
  }
710
785
  }
@@ -720,17 +795,22 @@ export class Store {
720
795
  * was brought in for, and the narrower of the two wins.
721
796
  */
722
797
  for (const call of calls) {
723
- const refusal = this.refusesAgent(call, author) ?? this.namesTurnedOff(call);
798
+ const refusal = this.refusesAgent(call, author) ?? this.namesTurnedOff(call, author);
724
799
  if (refusal) {
725
- tellTheWatchOfARefusal(refusal, author.id);
800
+ if (!this.rehearsing)
801
+ tellTheWatchOfARefusal(refusal, author.id);
726
802
  throw new PermissionDeniedError(refusal);
727
803
  }
728
804
  }
729
- tellTheWatchOfAnAuthor(author.id);
805
+ if (!this.rehearsing)
806
+ tellTheWatchOfAnAuthor(author.id);
730
807
  try {
731
808
  for (const call of calls) {
732
809
  const definition = this.mutation(call.name);
733
- const compiled = compileMutation(this.graph, definition, call.args);
810
+ const missing = this.missingFor(call, author);
811
+ if (missing)
812
+ throw missing;
813
+ const compiled = compileMutation(this.graph, definition, call.args, this.wordedFor(author));
734
814
  /*
735
815
  * AN ACT THAT DID NOTHING DOES NOT GO IN THE HISTORY.
736
816
  *
@@ -768,6 +848,8 @@ export class Store {
768
848
  inverse: [...compiled.primitives].reverse().map(normalise).map(invert),
769
849
  reads: compiled.reads,
770
850
  writes: compiled.writes,
851
+ // Which records its sentence read, so a reader who may not see one is not handed the sentence (FR-55).
852
+ ...(definition.describe !== undefined && compiled.described && compiled.described.length > 0 ? { described: compiled.described } : {}),
771
853
  at: this.now(),
772
854
  ...(options.via !== undefined ? { via: options.via } : {}),
773
855
  };
@@ -783,13 +865,15 @@ export class Store {
783
865
  }
784
866
  }
785
867
  catch (error) {
786
- this.graph.load(rollback);
787
- throw error;
868
+ // A batch refused part way keeps none of it: the graph, and the ops it had logged before the refusal.
869
+ this.putBack(rollback, kept);
870
+ throw this.refusalWordedFor(error, author, calls);
788
871
  }
789
- const after = this.violations();
872
+ const diff = diffSnapshots(rollback, this.graph.snapshot());
873
+ this.admitted(options, ops, allPrimitives, diff, rollback, kept);
874
+ const after = this.violations(context);
790
875
  const beforeKeys = new Set(before.map(violationKey));
791
876
  const afterKeys = new Set(after.map(violationKey));
792
- const diff = diffSnapshots(rollback, this.graph.snapshot());
793
877
  this.notify(diff, ops);
794
878
  return {
795
879
  batch,
@@ -1056,7 +1140,7 @@ export class Store {
1056
1140
  */
1057
1141
  const sighted = (this.policy?.sees?.length ?? 0) > 0 && !isSystem(author);
1058
1142
  if (sighted) {
1059
- const seen = checkUndo(readingOf(redact(this.log.all(), seesId(this, author)), () => this.log.epochs()), ids);
1143
+ const seen = checkUndo(readingOf(redact(this.log.all(), seatLens(this, author)), () => this.log.epochs()), ids);
1060
1144
  if (!seen.ok)
1061
1145
  throw new UndoBlockedError(seen);
1062
1146
  }
@@ -1094,6 +1178,7 @@ export class Store {
1094
1178
  const rollback = this.graph.snapshot();
1095
1179
  const before = this.violations();
1096
1180
  const ops = [];
1181
+ const kept = this.log.length;
1097
1182
  for (const target of [...check.ops].sort((a, b) => b.seq - a.seq)) {
1098
1183
  const op = {
1099
1184
  id: this.nextId(),
@@ -1115,10 +1200,11 @@ export class Store {
1115
1200
  this.log.append(op);
1116
1201
  ops.push(op);
1117
1202
  }
1203
+ const diff = diffSnapshots(rollback, this.graph.snapshot());
1204
+ this.admitted(options, ops, ops.flatMap((op) => [...op.primitives]), diff, rollback, kept);
1118
1205
  const after = this.violations();
1119
1206
  const beforeKeys = new Set(before.map(violationKey));
1120
1207
  const afterKeys = new Set(after.map(violationKey));
1121
- const diff = diffSnapshots(rollback, this.graph.snapshot());
1122
1208
  this.notify(diff, ops);
1123
1209
  return {
1124
1210
  batch,
@@ -1133,6 +1219,33 @@ export class Store {
1133
1219
  violationsAfter: after,
1134
1220
  };
1135
1221
  }
1222
+ /** A change not kept: the graph as it was, and the log cut back to where it stood. */
1223
+ putBack(rollback, kept) {
1224
+ this.graph.load(rollback);
1225
+ if (this.log.length > kept)
1226
+ this.log.truncate(kept);
1227
+ }
1228
+ /** 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. */
1229
+ admitted(options, ops, primitives, diff, rollback, kept) {
1230
+ if (!options.admit || ops.length === 0)
1231
+ return;
1232
+ const size = this.graph.size;
1233
+ try {
1234
+ options.admit({
1235
+ ops,
1236
+ primitives,
1237
+ added: { nodes: diff.addedNodes.length, edges: diff.addedEdges.length },
1238
+ removed: { nodes: diff.removedNodes.length, edges: diff.removedEdges.length },
1239
+ changed: { nodes: diff.changedNodes.length },
1240
+ nodesAfter: size.nodes,
1241
+ edgesAfter: size.edges,
1242
+ });
1243
+ }
1244
+ catch (error) {
1245
+ this.putBack(rollback, kept);
1246
+ throw error;
1247
+ }
1248
+ }
1136
1249
  /** Redo is the undo of an undo — no separate stack exists. */
1137
1250
  redo(undoBatchId, options = {}) {
1138
1251
  return this.undo(undoBatchId, options);
@@ -1377,6 +1490,9 @@ export class Store {
1377
1490
  * 1000). It is moved back to the start of the gesture it would split, so
1378
1491
  * a batch is never half behind it.
1379
1492
  *
1493
+ * The checkpoint keeps who made each record behind it (`creators`), so a
1494
+ * store opened on it knows a record's maker without the archived ops.
1495
+ *
1380
1496
  * Nothing moves until `compact` is handed the checkpoint, so a host can
1381
1497
  * archive what is behind it first. A store whose graph does not verify
1382
1498
  * against its log is not compacted: the checkpoint would vouch for a
@@ -1404,7 +1520,10 @@ export class Store {
1404
1520
  const verified = this.verify();
1405
1521
  if (!verified.ok)
1406
1522
  throw new GraphError(`Cannot compact a store that does not verify: ${verified.reason}`);
1407
- return this.log.checkpointAt(this.schema, seq, { validate: this.validate });
1523
+ const checkpoint = this.log.checkpointAt(this.schema, seq, { validate: this.validate });
1524
+ // Who made each record behind it, so a store opened on it still knows (an `own` sight reads the log for it).
1525
+ const creators = creatorsBefore(this.log, seq);
1526
+ return Object.keys(creators).length > 0 ? { ...checkpoint, creators } : checkpoint;
1408
1527
  }
1409
1528
  /**
1410
1529
  * MOVES THE UNDO HORIZON TO `checkpoint` (FR-23): the ops and epochs