@graview/core 0.1.0 → 0.1.3

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 (296) hide show
  1. package/README.md +7 -0
  2. package/dist/app.d.ts +17 -0
  3. package/dist/app.d.ts.map +1 -1
  4. package/dist/app.js.map +1 -1
  5. package/dist/arrange.d.ts +3 -2
  6. package/dist/arrange.d.ts.map +1 -1
  7. package/dist/arrange.js +19 -2
  8. package/dist/arrange.js.map +1 -1
  9. package/dist/beginning.d.ts +5 -2
  10. package/dist/beginning.d.ts.map +1 -1
  11. package/dist/beginning.js +28 -0
  12. package/dist/beginning.js.map +1 -1
  13. package/dist/capabilities.d.ts +32 -0
  14. package/dist/capabilities.d.ts.map +1 -0
  15. package/dist/capabilities.js +31 -0
  16. package/dist/capabilities.js.map +1 -0
  17. package/dist/cli/check/index.d.ts +1 -0
  18. package/dist/cli/check/index.d.ts.map +1 -1
  19. package/dist/cli/check/index.js +1 -0
  20. package/dist/cli/check/index.js.map +1 -1
  21. package/dist/cli/check/lenses.d.ts.map +1 -1
  22. package/dist/cli/check/lenses.js +25 -0
  23. package/dist/cli/check/lenses.js.map +1 -1
  24. package/dist/cli/check/policy.d.ts.map +1 -1
  25. package/dist/cli/check/policy.js +35 -0
  26. package/dist/cli/check/policy.js.map +1 -1
  27. package/dist/cli/check/relations.d.ts.map +1 -1
  28. package/dist/cli/check/relations.js +31 -0
  29. package/dist/cli/check/relations.js.map +1 -1
  30. package/dist/cli/check/views.d.ts +14 -0
  31. package/dist/cli/check/views.d.ts.map +1 -0
  32. package/dist/cli/check/views.js +27 -0
  33. package/dist/cli/check/views.js.map +1 -0
  34. package/dist/cli/check.d.ts.map +1 -1
  35. package/dist/cli/check.js +2 -1
  36. package/dist/cli/check.js.map +1 -1
  37. package/dist/cli/create.d.ts +1 -1
  38. package/dist/cli/create.d.ts.map +1 -1
  39. package/dist/cli/create.js +63 -1
  40. package/dist/cli/create.js.map +1 -1
  41. package/dist/cli/index.d.ts +18 -1
  42. package/dist/cli/index.d.ts.map +1 -1
  43. package/dist/cli/index.js +66 -1
  44. package/dist/cli/index.js.map +1 -1
  45. package/dist/conformance/fixtures.d.ts +41 -0
  46. package/dist/conformance/fixtures.d.ts.map +1 -0
  47. package/dist/conformance/fixtures.js +950 -0
  48. package/dist/conformance/fixtures.js.map +1 -0
  49. package/dist/conformance/index.d.ts +54 -0
  50. package/dist/conformance/index.d.ts.map +1 -0
  51. package/dist/conformance/index.js +81 -0
  52. package/dist/conformance/index.js.map +1 -0
  53. package/dist/document/canonical.d.ts +4 -0
  54. package/dist/document/canonical.d.ts.map +1 -0
  55. package/dist/document/canonical.js +29 -0
  56. package/dist/document/canonical.js.map +1 -0
  57. package/dist/document/compile.d.ts +43 -0
  58. package/dist/document/compile.d.ts.map +1 -0
  59. package/dist/document/compile.js +807 -0
  60. package/dist/document/compile.js.map +1 -0
  61. package/dist/document/diff.d.ts +32 -0
  62. package/dist/document/diff.d.ts.map +1 -0
  63. package/dist/document/diff.js +148 -0
  64. package/dist/document/diff.js.map +1 -0
  65. package/dist/document/edit.d.ts +63 -0
  66. package/dist/document/edit.d.ts.map +1 -0
  67. package/dist/document/edit.js +1432 -0
  68. package/dist/document/edit.js.map +1 -0
  69. package/dist/document/expr/analyze.d.ts +14 -0
  70. package/dist/document/expr/analyze.d.ts.map +1 -0
  71. package/dist/document/expr/analyze.js +52 -0
  72. package/dist/document/expr/analyze.js.map +1 -0
  73. package/dist/document/expr/evaluate.d.ts +36 -0
  74. package/dist/document/expr/evaluate.d.ts.map +1 -0
  75. package/dist/document/expr/evaluate.js +344 -0
  76. package/dist/document/expr/evaluate.js.map +1 -0
  77. package/dist/document/expr/index.d.ts +8 -0
  78. package/dist/document/expr/index.d.ts.map +1 -0
  79. package/dist/document/expr/index.js +5 -0
  80. package/dist/document/expr/index.js.map +1 -0
  81. package/dist/document/expr/parse.d.ts +50 -0
  82. package/dist/document/expr/parse.d.ts.map +1 -0
  83. package/dist/document/expr/parse.js +240 -0
  84. package/dist/document/expr/parse.js.map +1 -0
  85. package/dist/document/expr/print.d.ts +4 -0
  86. package/dist/document/expr/print.d.ts.map +1 -0
  87. package/dist/document/expr/print.js +81 -0
  88. package/dist/document/expr/print.js.map +1 -0
  89. package/dist/document/findings.d.ts +15 -0
  90. package/dist/document/findings.d.ts.map +1 -0
  91. package/dist/document/findings.js +30 -0
  92. package/dist/document/findings.js.map +1 -0
  93. package/dist/document/graview-template.d.ts +148 -0
  94. package/dist/document/graview-template.d.ts.map +1 -0
  95. package/dist/document/graview-template.js +450 -0
  96. package/dist/document/graview-template.js.map +1 -0
  97. package/dist/document/index.d.ts +27 -0
  98. package/dist/document/index.d.ts.map +1 -0
  99. package/dist/document/index.js +16 -0
  100. package/dist/document/index.js.map +1 -0
  101. package/dist/document/migrate.d.ts +41 -0
  102. package/dist/document/migrate.d.ts.map +1 -0
  103. package/dist/document/migrate.js +252 -0
  104. package/dist/document/migrate.js.map +1 -0
  105. package/dist/document/patch.d.ts +16 -0
  106. package/dist/document/patch.d.ts.map +1 -0
  107. package/dist/document/patch.js +79 -0
  108. package/dist/document/patch.js.map +1 -0
  109. package/dist/document/rules.d.ts +40 -0
  110. package/dist/document/rules.d.ts.map +1 -0
  111. package/dist/document/rules.js +88 -0
  112. package/dist/document/rules.js.map +1 -0
  113. package/dist/document/schema.d.ts +461 -0
  114. package/dist/document/schema.d.ts.map +1 -0
  115. package/dist/document/schema.js +199 -0
  116. package/dist/document/schema.js.map +1 -0
  117. package/dist/document/template.d.ts +28 -0
  118. package/dist/document/template.d.ts.map +1 -0
  119. package/dist/document/template.js +117 -0
  120. package/dist/document/template.js.map +1 -0
  121. package/dist/document/to-document.d.ts +34 -0
  122. package/dist/document/to-document.d.ts.map +1 -0
  123. package/dist/document/to-document.js +186 -0
  124. package/dist/document/to-document.js.map +1 -0
  125. package/dist/document/upgrade.d.ts +9 -0
  126. package/dist/document/upgrade.d.ts.map +1 -0
  127. package/dist/document/upgrade.js +19 -0
  128. package/dist/document/upgrade.js.map +1 -0
  129. package/dist/document/views.d.ts +65 -0
  130. package/dist/document/views.d.ts.map +1 -0
  131. package/dist/document/views.js +264 -0
  132. package/dist/document/views.js.map +1 -0
  133. package/dist/formats.d.ts +50 -0
  134. package/dist/formats.d.ts.map +1 -0
  135. package/dist/formats.js +75 -0
  136. package/dist/formats.js.map +1 -0
  137. package/dist/graph/graph.d.ts +61 -3
  138. package/dist/graph/graph.d.ts.map +1 -1
  139. package/dist/graph/graph.js +187 -20
  140. package/dist/graph/graph.js.map +1 -1
  141. package/dist/index.d.ts +43 -18
  142. package/dist/index.d.ts.map +1 -1
  143. package/dist/index.js +31 -11
  144. package/dist/index.js.map +1 -1
  145. package/dist/integrity.d.ts +46 -0
  146. package/dist/integrity.d.ts.map +1 -0
  147. package/dist/integrity.js +179 -0
  148. package/dist/integrity.js.map +1 -0
  149. package/dist/invariants/engine.d.ts +8 -0
  150. package/dist/invariants/engine.d.ts.map +1 -1
  151. package/dist/invariants/engine.js +40 -2
  152. package/dist/invariants/engine.js.map +1 -1
  153. package/dist/invariants/types.d.ts +19 -0
  154. package/dist/invariants/types.d.ts.map +1 -1
  155. package/dist/labels.d.ts +65 -0
  156. package/dist/labels.d.ts.map +1 -0
  157. package/dist/labels.js +123 -0
  158. package/dist/labels.js.map +1 -0
  159. package/dist/mutations/define-mutation.d.ts.map +1 -1
  160. package/dist/mutations/define-mutation.js +5 -6
  161. package/dist/mutations/define-mutation.js.map +1 -1
  162. package/dist/mutations/derive-edits.d.ts.map +1 -1
  163. package/dist/mutations/derive-edits.js +20 -1
  164. package/dist/mutations/derive-edits.js.map +1 -1
  165. package/dist/mutations/form.d.ts +9 -0
  166. package/dist/mutations/form.d.ts.map +1 -1
  167. package/dist/mutations/form.js +20 -0
  168. package/dist/mutations/form.js.map +1 -1
  169. package/dist/mutations/node-ref.d.ts +6 -0
  170. package/dist/mutations/node-ref.d.ts.map +1 -1
  171. package/dist/mutations/node-ref.js +22 -8
  172. package/dist/mutations/node-ref.js.map +1 -1
  173. package/dist/mutations/types.d.ts +8 -0
  174. package/dist/mutations/types.d.ts.map +1 -1
  175. package/dist/mutations/words.d.ts +49 -0
  176. package/dist/mutations/words.d.ts.map +1 -0
  177. package/dist/mutations/words.js +61 -0
  178. package/dist/mutations/words.js.map +1 -0
  179. package/dist/ops/log.d.ts +156 -4
  180. package/dist/ops/log.d.ts.map +1 -1
  181. package/dist/ops/log.js +234 -51
  182. package/dist/ops/log.js.map +1 -1
  183. package/dist/ops/revisions.d.ts +66 -0
  184. package/dist/ops/revisions.d.ts.map +1 -0
  185. package/dist/ops/revisions.js +97 -0
  186. package/dist/ops/revisions.js.map +1 -0
  187. package/dist/ops/types.d.ts +49 -1
  188. package/dist/ops/types.d.ts.map +1 -1
  189. package/dist/ops/undo.d.ts +29 -2
  190. package/dist/ops/undo.d.ts.map +1 -1
  191. package/dist/ops/undo.js +110 -1
  192. package/dist/ops/undo.js.map +1 -1
  193. package/dist/ops/withheld.d.ts +37 -0
  194. package/dist/ops/withheld.d.ts.map +1 -0
  195. package/dist/ops/withheld.js +84 -0
  196. package/dist/ops/withheld.js.map +1 -0
  197. package/dist/permissions/policy.d.ts +15 -0
  198. package/dist/permissions/policy.d.ts.map +1 -1
  199. package/dist/permissions/policy.js +28 -1
  200. package/dist/permissions/policy.js.map +1 -1
  201. package/dist/permissions/sight.d.ts +66 -0
  202. package/dist/permissions/sight.d.ts.map +1 -0
  203. package/dist/permissions/sight.js +131 -0
  204. package/dist/permissions/sight.js.map +1 -0
  205. package/dist/permissions/types.d.ts +26 -0
  206. package/dist/permissions/types.d.ts.map +1 -1
  207. package/dist/permissions/types.js.map +1 -1
  208. package/dist/persistence/memory.d.ts.map +1 -1
  209. package/dist/persistence/memory.js +25 -0
  210. package/dist/persistence/memory.js.map +1 -1
  211. package/dist/persistence/sql.d.ts +43 -0
  212. package/dist/persistence/sql.d.ts.map +1 -0
  213. package/dist/persistence/sql.js +154 -0
  214. package/dist/persistence/sql.js.map +1 -0
  215. package/dist/persistence/sqlite.d.ts +17 -3
  216. package/dist/persistence/sqlite.d.ts.map +1 -1
  217. package/dist/persistence/sqlite.js +32 -96
  218. package/dist/persistence/sqlite.js.map +1 -1
  219. package/dist/persistence/types.d.ts +19 -0
  220. package/dist/persistence/types.d.ts.map +1 -1
  221. package/dist/presence.d.ts +56 -0
  222. package/dist/presence.d.ts.map +1 -1
  223. package/dist/presence.js +62 -2
  224. package/dist/presence.js.map +1 -1
  225. package/dist/refusal.d.ts +34 -0
  226. package/dist/refusal.d.ts.map +1 -0
  227. package/dist/refusal.js +40 -0
  228. package/dist/refusal.js.map +1 -0
  229. package/dist/scaffold/from-template.d.ts +16 -0
  230. package/dist/scaffold/from-template.d.ts.map +1 -0
  231. package/dist/scaffold/from-template.js +157 -0
  232. package/dist/scaffold/from-template.js.map +1 -0
  233. package/dist/scaffold/index.d.ts +10 -1
  234. package/dist/scaffold/index.d.ts.map +1 -1
  235. package/dist/scaffold/index.js +35 -7
  236. package/dist/scaffold/index.js.map +1 -1
  237. package/dist/scaffold/names.d.ts +4 -0
  238. package/dist/scaffold/names.d.ts.map +1 -1
  239. package/dist/scaffold/names.js.map +1 -1
  240. package/dist/scaffold/project.d.ts +1 -1
  241. package/dist/scaffold/project.d.ts.map +1 -1
  242. package/dist/scaffold/project.js +44 -13
  243. package/dist/scaffold/project.js.map +1 -1
  244. package/dist/scaffold/ui.d.ts.map +1 -1
  245. package/dist/scaffold/ui.js +19 -10
  246. package/dist/scaffold/ui.js.map +1 -1
  247. package/dist/schema/define-node.d.ts +20 -0
  248. package/dist/schema/define-node.d.ts.map +1 -1
  249. package/dist/schema/define-node.js +27 -2
  250. package/dist/schema/define-node.js.map +1 -1
  251. package/dist/schema/path.d.ts +19 -0
  252. package/dist/schema/path.d.ts.map +1 -0
  253. package/dist/schema/path.js +34 -0
  254. package/dist/schema/path.js.map +1 -0
  255. package/dist/schema/types.d.ts +17 -0
  256. package/dist/schema/types.d.ts.map +1 -1
  257. package/dist/seen.d.ts +64 -0
  258. package/dist/seen.d.ts.map +1 -0
  259. package/dist/seen.js +200 -0
  260. package/dist/seen.js.map +1 -0
  261. package/dist/store.d.ts +378 -8
  262. package/dist/store.d.ts.map +1 -1
  263. package/dist/store.js +801 -41
  264. package/dist/store.js.map +1 -1
  265. package/dist/testing.d.ts +6 -0
  266. package/dist/testing.d.ts.map +1 -1
  267. package/dist/testing.js +3 -1
  268. package/dist/testing.js.map +1 -1
  269. package/dist/theme/palettes.d.ts.map +1 -1
  270. package/dist/theme/palettes.js +4 -0
  271. package/dist/theme/palettes.js.map +1 -1
  272. package/dist/theme/types.d.ts +9 -0
  273. package/dist/theme/types.d.ts.map +1 -1
  274. package/dist/theme/types.js +4 -0
  275. package/dist/theme/types.js.map +1 -1
  276. package/dist/validate-graph.d.ts +84 -0
  277. package/dist/validate-graph.d.ts.map +1 -0
  278. package/dist/validate-graph.js +295 -0
  279. package/dist/validate-graph.js.map +1 -0
  280. package/dist/version.d.ts +3 -0
  281. package/dist/version.d.ts.map +1 -0
  282. package/dist/version.js +3 -0
  283. package/dist/version.js.map +1 -0
  284. package/dist/views/types.d.ts +8 -0
  285. package/dist/views/types.d.ts.map +1 -1
  286. package/dist/views/types.js +5 -0
  287. package/dist/views/types.js.map +1 -1
  288. package/dist/watched.d.ts +39 -0
  289. package/dist/watched.d.ts.map +1 -1
  290. package/dist/watched.js +94 -1
  291. package/dist/watched.js.map +1 -1
  292. package/dist/who.d.ts +24 -1
  293. package/dist/who.d.ts.map +1 -1
  294. package/dist/who.js +49 -2
  295. package/dist/who.js.map +1 -1
  296. package/package.json +9 -1
package/dist/store.js CHANGED
@@ -1,17 +1,37 @@
1
+ import { readingOf, seenBy, seesId } from "./seen.js";
1
2
  import { Graph, GraphError } from "./graph/graph.js";
2
3
  import { resolveModules } from "./modules.js";
3
- import { diffSnapshots } from "./graph/diff.js";
4
- import { invert, normalise } from "./graph/primitives.js";
4
+ import { diffSnapshots, EMPTY_DIFF, isEmptyDiff } from "./graph/diff.js";
5
+ import { invert, normalise, writesOf } from "./graph/primitives.js";
6
+ import { LabelIndex, refusalFor } from "./labels.js";
7
+ import { nodeRefArgs } from "./mutations/node-ref.js";
8
+ import { verifyFold } from "./integrity.js";
5
9
  import { evaluate } from "./invariants/engine.js";
6
10
  import { compileMutation } from "./mutations/define-mutation.js";
7
11
  import { deriveMutations, derivedVia } from "./mutations/derive-edits.js";
8
12
  import { OperationLog } from "./ops/log.js";
9
- import { permits, permittedMutations } from "./permissions/policy.js";
13
+ import { actingAs, isSystem, permits, permittedMutations } from "./permissions/policy.js";
14
+ import { redact } from "./ops/withheld.js";
10
15
  import { nounOf } from "./schema/define-node.js";
11
16
  import { PermissionDeniedError } from "./permissions/types.js";
12
- import { checkUndo, undoPrimitives } from "./ops/undo.js";
17
+ import { checkUndo, UndoBlockedError, undoPrimitives } from "./ops/undo.js";
18
+ import { validateGraph } from "./validate-graph.js";
13
19
  import { tellTheWatchItsNames, tellTheWatchOfAStore, tellTheWatchOfAnAuthor, tellTheWatchOfARefusal } from "./watched.js";
14
20
  const HUMAN = { kind: "human" };
21
+ /** Who turns a module off or on: the workspace itself, `system · modules` (FR-12). */
22
+ export const MODULES_AUTHOR = { kind: "system", id: "modules", name: "Modules" };
23
+ /**
24
+ * WHETHER UNDOING AN OP PUTS BACK EXACTLY WHAT IT TOOK (FR-28).
25
+ *
26
+ * An op that was not an act — a repair from `repairPlan`, a migration, a
27
+ * host's own change through `applyPrimitives` — is undone by putting the
28
+ * records back as they were, even a record that no longer fits: undoing a
29
+ * repair means the misfit returns, and `validateGraph` says so again. The
30
+ * undo of an ACT is itself a change within the declaration, and is held to
31
+ * it like any write: one whose inverse the current declaration refuses is
32
+ * refused, and says why.
33
+ */
34
+ const putsBack = (op) => op.mutation === null && op.undoes === undefined;
15
35
  /** A violation's identity across judgements: the rule, what it is about, and what it says. */
16
36
  export function violationKey(v) {
17
37
  return `${v.invariant}|${v.subjectId ?? ""}|${v.message}`;
@@ -24,6 +44,35 @@ export function violationKey(v) {
24
44
  * agent's work needs no bespoke observability layer — it produces the same
25
45
  * diff a human edit does, carrying the same attribution.
26
46
  */
47
+ /**
48
+ * An op a store could not take: its primitives do not fit the graph (a node
49
+ * it patches is not there, an edge to nowhere, a field the schema refuses).
50
+ * Nothing it was handed has landed — the store is as it was — and `op` says
51
+ * which one, with the graph's own error as `cause`.
52
+ */
53
+ export class ReceiveError extends Error {
54
+ op;
55
+ constructor(op, cause) {
56
+ super(`Op "${op.id}" (${op.intent}) could not be applied: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
57
+ this.op = op;
58
+ this.name = "ReceiveError";
59
+ }
60
+ }
61
+ /**
62
+ * A tag nobody else's store is using, for this store's batch ids. Drawn
63
+ * from the platform's random source where there is one, which every page
64
+ * and Node 22 has.
65
+ */
66
+ function storeTag() {
67
+ const random = globalThis.crypto;
68
+ const bytes = new Uint8Array(6);
69
+ if (random?.getRandomValues)
70
+ random.getRandomValues(bytes);
71
+ else
72
+ for (let i = 0; i < bytes.length; i++)
73
+ bytes[i] = Math.floor(Math.random() * 256);
74
+ return [...bytes].map((byte) => byte.toString(36).padStart(2, "0")).join("");
75
+ }
27
76
  export class Store {
28
77
  schema;
29
78
  graph;
@@ -43,11 +92,19 @@ export class Store {
43
92
  intelligence;
44
93
  /** What each declared agent may do, by provider name. */
45
94
  may = new Map();
46
- /** What the enabled modules work out to; every surface reads this one answer. */
47
- modules;
95
+ declaredModules;
96
+ startingModules;
97
+ /** The projection, and the log it was read from: its length and its last op. */
98
+ projected;
48
99
  nextId;
49
100
  now;
101
+ validate;
102
+ mintBatch;
50
103
  counter = 0;
104
+ /** The default op id generator's count. */
105
+ opCount = 0;
106
+ /** Above zero while a rebase applies its pending calls again: they are told as one change at the end. */
107
+ quiet = 0;
51
108
  listeners = new Set();
52
109
  constructor(options) {
53
110
  this.schema = options.schema;
@@ -59,10 +116,13 @@ export class Store {
59
116
  if (provider.may)
60
117
  this.may.set(provider.name, new Set(provider.may));
61
118
  }
62
- this.modules = resolveModules(options.modules, options.enabledModules);
63
- let n = 0;
64
- this.nextId = options.ids ?? (() => `op${++n}`);
65
- this.now = options.now ?? (() => new Date(0).toISOString());
119
+ this.declaredModules = options.modules;
120
+ this.startingModules = options.enabledModules;
121
+ this.nextId = options.ids ?? (() => `op${++this.opCount}`);
122
+ this.now = options.now ?? (() => new Date().toISOString());
123
+ this.validate = options.validate ?? true;
124
+ const tag = storeTag();
125
+ this.mintBatch = options.batchIds ?? ((kind) => `${kind}:${tag}:${++this.counter}`);
66
126
  for (const mutation of options.mutations ?? []) {
67
127
  if (this.mutations.has(mutation.name)) {
68
128
  throw new GraphError(`Duplicate mutation "${mutation.name}"`);
@@ -81,19 +141,21 @@ export class Store {
81
141
  this.mutations.set(mutation.name, mutation);
82
142
  }
83
143
  tellTheWatchItsNames(options.schema, this.mutations.values(), options.policy);
144
+ const horizon = options.horizon !== undefined ? { horizon: options.horizon } : {};
84
145
  if (options.log && options.snapshot) {
85
146
  // Hydrate: the graph as stored, the history as recorded.
86
- this.log = OperationLog.from(options.log);
147
+ this.log = OperationLog.from(options.log, options.epochs, horizon);
87
148
  this.graph = Graph.from(options.schema, options.snapshot, {
88
149
  validate: options.validate ?? true,
89
150
  });
90
151
  }
91
152
  else if (options.log) {
92
- this.log = OperationLog.from(options.log);
93
- this.graph = this.log.fold(options.schema, { validate: options.validate ?? true });
153
+ this.log = OperationLog.from(options.log, options.epochs, horizon);
154
+ const last = this.log.lastEpoch();
155
+ this.graph = this.log.fold(options.schema, { validate: options.validate ?? true, ...(last ? { from: last } : {}) });
94
156
  }
95
157
  else {
96
- this.log = new OperationLog();
158
+ this.log = OperationLog.from([], options.epochs ?? (options.snapshot ? [{ seq: options.horizon ?? 0, base: options.snapshot }] : []), horizon);
97
159
  this.graph = Graph.from(options.schema, options.snapshot ?? { nodes: [], edges: [] }, {
98
160
  validate: options.validate ?? true,
99
161
  });
@@ -113,15 +175,98 @@ export class Store {
113
175
  * Only the DEFAULT generators are wound forward; an app that supplies
114
176
  * its own `ids` owns their uniqueness.
115
177
  */
178
+ this.countPastTheLog();
179
+ tellTheWatchOfAStore(this);
180
+ }
181
+ /** Winds the default op and batch counters past every id the log holds, and past the horizon (FR-23). */
182
+ countPastTheLog() {
116
183
  const trailing = (value) => {
117
184
  const digits = /(\d+)$/.exec(value);
118
185
  return digits ? Number(digits[1]) : 0;
119
186
  };
120
187
  for (const op of this.log.all()) {
121
188
  this.counter = Math.max(this.counter, trailing(op.batch));
122
- n = Math.max(n, trailing(op.id));
189
+ this.opCount = Math.max(this.opCount, trailing(op.id));
123
190
  }
124
- tellTheWatchOfAStore(this);
191
+ // Behind an undo horizon (FR-23) are ids the log no longer holds: count on past them all.
192
+ if (this.log.horizon > 0)
193
+ this.opCount = Math.max(this.opCount, this.log.length);
194
+ }
195
+ /**
196
+ * What the enabled modules work out to; every surface reads this one answer.
197
+ *
198
+ * READ FROM THE LOG (FR-12): the set the last `system · modules` op says,
199
+ * or the one the store was opened with when no op has said one. Kept
200
+ * between reads and re-read only past what it has read — a log only ever
201
+ * grows at its end, and a rebase that cut it is read again from the start.
202
+ */
203
+ get modules() {
204
+ const all = this.log.all();
205
+ const last = all.at(-1)?.id;
206
+ const held = this.projected;
207
+ if (held && held.length === all.length && held.last === last)
208
+ return held.projection;
209
+ let said;
210
+ let from = 0;
211
+ if (held && held.length <= all.length && (held.length === 0 || all[held.length - 1]?.id === held.last)) {
212
+ said = held.said;
213
+ from = held.length;
214
+ }
215
+ for (let at = all.length - 1; at >= from; at--) {
216
+ const stated = all[at].enabledModules;
217
+ if (stated !== undefined) {
218
+ said = stated;
219
+ break;
220
+ }
221
+ }
222
+ const projection = held && said === held.said ? held.projection : resolveModules(this.declaredModules, said ?? this.startingModules);
223
+ this.projected = { projection, length: all.length, last, said };
224
+ return projection;
225
+ }
226
+ /**
227
+ * TURN MODULES OFF OR ON, AS AN OP (FR-12).
228
+ *
229
+ * The enabled set is a fact about the workspace — usually what it pays
230
+ * for — and changing it is history: one op, authored `system · modules`,
231
+ * saying the set it leaves (`enabledModules`) in a sentence a person can
232
+ * read. It touches no record, so it folds to nothing and takes nothing
233
+ * away: a module off is a horizon, and turning it on again brings every
234
+ * record back as it was. Not undone: the set changes by turning a module
235
+ * the other way. `undefined` is every declared module. Nothing is written
236
+ * when the set is the one the store already has.
237
+ */
238
+ setEnabledModules(enabled, options = {}) {
239
+ const before = this.modules.enabled;
240
+ const after = resolveModules(this.declaredModules, enabled).enabled;
241
+ const off = [...before].filter((name) => !after.has(name));
242
+ const on = [...after].filter((name) => !before.has(name));
243
+ if (off.length === 0 && on.length === 0)
244
+ return undefined;
245
+ const named = (names) => names
246
+ .map((name) => {
247
+ const kinds = this.declaredModules?.[name]?.kinds ?? [];
248
+ return kinds.length > 0 ? kinds.map((kind) => this.schema.tryDefinition(kind)?.plural ?? kind).join(", ") : name;
249
+ })
250
+ .join(", ");
251
+ const said = [off.length > 0 ? `Turn off ${named(off)}` : "", on.length > 0 ? `turn on ${named(on)}` : ""].filter(Boolean).join("; ");
252
+ const op = {
253
+ id: this.nextId(),
254
+ seq: this.log.length,
255
+ batch: options.batch ?? this.mintBatch("batch"),
256
+ author: MODULES_AUTHOR,
257
+ intent: said.charAt(0).toUpperCase() + said.slice(1),
258
+ mutation: null,
259
+ primitives: [],
260
+ inverse: [],
261
+ reads: [],
262
+ writes: [],
263
+ at: this.now(),
264
+ ...(options.via !== undefined ? { via: options.via } : {}),
265
+ enabledModules: [...after].sort(),
266
+ };
267
+ this.log.append(op);
268
+ this.notify(EMPTY_DIFF, [op]);
269
+ return op;
125
270
  }
126
271
  mutation(name) {
127
272
  const found = this.mutations.get(name);
@@ -183,7 +328,7 @@ export class Store {
183
328
  * repair as the starter seat, which may only add, and the press met
184
329
  * "starter may not take-off here" (W-110).
185
330
  */
186
- const narrowed = this.refusesAgent(call, principal);
331
+ const narrowed = this.refusesAgent(call, principal) ?? this.namesTurnedOff(call) ?? this.namesUnseen(call, principal);
187
332
  if (narrowed)
188
333
  return { ok: false, refusal: narrowed };
189
334
  return permits(this.policy, principal, call.name, this.subjectKindOf(call), this.viaOf(this.mutations.get(call.name)), this.subjectIdOf(call), this.words);
@@ -216,6 +361,69 @@ export class Store {
216
361
  wouldNeed: [],
217
362
  };
218
363
  }
364
+ /**
365
+ * A CALL THAT NAMES A RECORD OF A MODULE THAT IS OFF IS REFUSED (FR-12),
366
+ * whoever makes it: the module's own acts are already off, and an act
367
+ * the core owns — a rename that takes any kind — must not reach into a
368
+ * district the workspace does not have. The sentence names the act.
369
+ */
370
+ namesTurnedOff(call) {
371
+ const off = this.modules.disabledKinds;
372
+ if (off.size === 0)
373
+ return undefined;
374
+ const mutation = this.mutations.get(call.name);
375
+ if (!mutation)
376
+ return undefined;
377
+ const named = [];
378
+ if (mutation.subject)
379
+ named.push(call.args[mutation.subject.arg]);
380
+ for (const ref of nodeRefArgs(mutation.input))
381
+ named.push(call.args[ref.name]);
382
+ const ids = named.flatMap((value) => (typeof value === "string" ? [value] : Array.isArray(value) ? value.filter((inner) => typeof inner === "string") : []));
383
+ if (!ids.some((id) => off.has(this.graph.getNode(id)?.kind)))
384
+ return undefined;
385
+ return {
386
+ mutation: call.name,
387
+ message: `“${mutation.title ?? call.name}” names a record of a module this workspace has turned off.`,
388
+ wouldNeed: [],
389
+ };
390
+ }
391
+ /**
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.
398
+ */
399
+ namesUnseen(call, principal) {
400
+ if (!this.policy?.sees?.length || isSystem(principal))
401
+ return undefined;
402
+ const mutation = this.mutations.get(call.name);
403
+ if (!mutation)
404
+ return undefined;
405
+ const named = new Set();
406
+ const add = (value) => {
407
+ if (typeof value === "string")
408
+ named.add(value);
409
+ else if (Array.isArray(value))
410
+ for (const inner of value)
411
+ if (typeof inner === "string")
412
+ named.add(inner);
413
+ };
414
+ if (mutation.subject)
415
+ add(call.args[mutation.subject.arg]);
416
+ for (const ref of nodeRefArgs(mutation.input))
417
+ add(call.args[ref.name]);
418
+ const visible = seesId(this, principal);
419
+ if (![...named].some((id) => this.graph.has(id) && !visible(id)))
420
+ 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
+ };
426
+ }
219
427
  /**
220
428
  * Whether a principal may run ANY act of a module drawn only for those
221
429
  * who administer it. This is the one question the interface asks before
@@ -236,6 +444,46 @@ export class Store {
236
444
  return kinds.some((kind) => permits(this.policy, principal, name, kind, this.viaOf(mutation)).ok);
237
445
  });
238
446
  }
447
+ /**
448
+ * The store as one principal may see it: its graph, log, history and
449
+ * problems hold only what the policy's `sees` lets them see, and every
450
+ * act still comes here. With no `sees`, this store itself (see `seen.ts`).
451
+ */
452
+ seenBy(principal) {
453
+ return seenBy(this, principal);
454
+ }
455
+ /** Whether a principal may see one record (the policy's `sees`). */
456
+ sees(principal, id) {
457
+ return this.graph.has(id) && seesId(this, principal)(id);
458
+ }
459
+ labels;
460
+ /**
461
+ * WHAT A NAME GIVEN FOR A NODE ARGUMENT MEANS, to this principal (FR-33).
462
+ *
463
+ * An id of a record the principal may see is that record, whatever it is
464
+ * called. Otherwise a label, case and accents aside, among the records of
465
+ * the kinds the argument accepts; failing that, the one label the name
466
+ * starts. Only records the principal may see are ever candidates — a
467
+ * name never tells a seat that something it may not see exists. Several
468
+ * matches are refused with every candidate; none says so.
469
+ */
470
+ resolveRef(arg, given, principal = HUMAN) {
471
+ const sighted = (this.policy?.sees?.length ?? 0) > 0;
472
+ const visible = (id) => !sighted || this.sees(principal, id);
473
+ const named = this.graph.getNode(given);
474
+ this.labels ??= new LabelIndex(this.graph);
475
+ if (named && visible(given))
476
+ return { ok: true, id: given, label: this.labels.labelOf(given) ?? given, by: "id" };
477
+ const found = this.labels.lookup(arg.kinds, given);
478
+ const exact = found.exact.filter(visible);
479
+ const exactly = exact.length > 0;
480
+ const candidates = (exactly ? exact : found.prefix.filter(visible))
481
+ .map((id) => ({ id, kind: this.graph.getNode(id).kind, label: this.labels.labelOf(id) ?? id }))
482
+ .sort((a, b) => (a.label < b.label ? -1 : a.label > b.label ? 1 : a.id < b.id ? -1 : 1));
483
+ if (candidates.length === 1)
484
+ return { ok: true, id: candidates[0].id, label: candidates[0].label, by: exactly ? "label" : "prefix" };
485
+ return refusalFor(this.schema, arg, given, candidates);
486
+ }
239
487
  /** The kinds of administered modules this principal may not see at all. */
240
488
  kindsKeptFrom(principal = HUMAN) {
241
489
  const kept = new Set();
@@ -245,6 +493,22 @@ export class Store {
245
493
  for (const kind of module.kinds ?? [])
246
494
  kept.add(kind);
247
495
  }
496
+ /*
497
+ * A KIND THE POLICY SAYS THIS SEAT SEES NONE OF, and may not begin: no
498
+ * district, no tab, no "none yet". Somebody browsing a showroom has no
499
+ * test drives to see and none to book, and a Test drives page that said
500
+ * "none yet — waiting for shoppers" was a page about other people.
501
+ */
502
+ const sights = this.policy?.sees ?? [];
503
+ if (sights.length > 0 && !isSystem(principal)) {
504
+ const creatable = new Set(this.permittedMutations(principal).flatMap((mutation) => mutation.creates ?? []));
505
+ // Every kind: once a policy says who sees what, a kind no sight names is seen by nobody.
506
+ for (const kind of this.schema.kinds) {
507
+ const seeing = sights.some((sight) => sight.kinds.includes(kind) && (sight.roles === "*" || (principal.roles ?? []).some((role) => sight.roles.includes(role))) && (!sight.own || principal.id !== undefined));
508
+ if (!seeing && !creatable.has(kind))
509
+ kept.add(kind);
510
+ }
511
+ }
248
512
  return kept;
249
513
  }
250
514
  subjectIdOf(call) {
@@ -266,7 +530,7 @@ export class Store {
266
530
  const declared = permittedMutations(this.policy, principal, all.filter((mutation) => !mutation.derived));
267
531
  // A derived act is offered when the kind's own acts it rides are.
268
532
  const derived = all.filter((mutation) => mutation.derived !== undefined &&
269
- permits(this.policy, principal, mutation.name, mutation.derived.kind, this.viaOf(mutation)).ok);
533
+ permits(this.policy, principal, mutation.name, mutation.derived.kind, this.viaOf(mutation), actingAs(principal).id).ok);
270
534
  return [...declared, ...derived];
271
535
  }
272
536
  /**
@@ -283,8 +547,14 @@ export class Store {
283
547
  if (!subject)
284
548
  return undefined;
285
549
  const id = call.args[subject.arg];
550
+ /*
551
+ * NOT CHOSEN YET — offered from the far end, the subject is still to be
552
+ * asked for. Its kind is the act's to say when it names one: asked as
553
+ * "no kind", a grant drawn by kind ("a reviewer, on talks") refused the
554
+ * reviewer on every topic with "a chair or a reviewer can".
555
+ */
286
556
  if (typeof id !== "string")
287
- return undefined;
557
+ return subject.kinds !== "*" && subject.kinds.length === 1 ? subject.kinds[0] : undefined;
288
558
  return this.graph.getNode(id)?.kind;
289
559
  }
290
560
  /** Current violations, evaluated fresh — nothing is cached or stale. */
@@ -330,10 +600,57 @@ export class Store {
330
600
  context,
331
601
  });
332
602
  }
603
+ /**
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.
610
+ */
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 }),
636
+ });
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
+ };
649
+ }
333
650
  previewPrimitives(primitives, meta) {
334
651
  const before = this.violations(meta.context);
335
652
  const trial = Graph.from(this.schema, this.graph.snapshot());
336
- const diff = trial.applyPrimitives(primitives);
653
+ const diff = trial.applyPrimitives(primitives, { restoring: meta.restoring === true });
337
654
  const after = evaluate(trial, this.allInvariants(), {
338
655
  ...this.invariantOptions,
339
656
  ...(meta.context === undefined ? {} : { context: meta.context }),
@@ -357,7 +674,15 @@ export class Store {
357
674
  }
358
675
  /** Applies several mutations as one gesture — one batch, one undo unit. */
359
676
  applyAll(calls, options = {}) {
360
- const batch = options.batch ?? `batch:${++this.counter}`;
677
+ return this.applying(calls, options);
678
+ }
679
+ /**
680
+ * `applyAll` itself. Kept apart so a rebase applies pending calls again
681
+ * through the store's own path, whatever a host has wrapped `applyAll` in
682
+ * (`openRemote` sends each one down the wire).
683
+ */
684
+ applying(calls, options) {
685
+ const batch = options.batch ?? this.mintBatch("batch");
361
686
  const author = options.author ?? HUMAN;
362
687
  const ops = [];
363
688
  const allPrimitives = [];
@@ -395,7 +720,7 @@ export class Store {
395
720
  * was brought in for, and the narrower of the two wins.
396
721
  */
397
722
  for (const call of calls) {
398
- const refusal = this.refusesAgent(call, author);
723
+ const refusal = this.refusesAgent(call, author) ?? this.namesTurnedOff(call);
399
724
  if (refusal) {
400
725
  tellTheWatchOfARefusal(refusal, author.id);
401
726
  throw new PermissionDeniedError(refusal);
@@ -424,7 +749,14 @@ export class Store {
424
749
  seq: this.log.length,
425
750
  batch,
426
751
  author,
427
- intent: options.intent ?? compiled.intent,
752
+ /*
753
+ * THE ACT'S OWN SENTENCE, kept; what the gesture was for goes
754
+ * beside it (FR-18). An act with no `describe` has no sentence of
755
+ * its own, only `close-item(id="deposit")`, and there the caller's
756
+ * words ("Close it", the button's) are the better one.
757
+ */
758
+ intent: definition.describe !== undefined ? compiled.intent : (options.intent ?? compiled.intent),
759
+ ...(options.intent !== undefined ? { batchIntent: options.intent } : {}),
428
760
  mutation: call,
429
761
  /*
430
762
  * NORMALISED on the way into the record: a patch that clears a
@@ -437,6 +769,7 @@ export class Store {
437
769
  reads: compiled.reads,
438
770
  writes: compiled.writes,
439
771
  at: this.now(),
772
+ ...(options.via !== undefined ? { via: options.via } : {}),
440
773
  };
441
774
  this.graph.applyPrimitives(op.primitives);
442
775
  this.log.append(op);
@@ -506,23 +839,169 @@ export class Store {
506
839
  return [];
507
840
  const before = this.graph.snapshot();
508
841
  const landed = [];
509
- for (const op of fresh) {
510
- const here = { ...op, seq: this.log.all().length };
511
- /*
512
- * ALREADY IN EFFECT HERE. A client that applied a call provisionally
513
- * and is now handed the server's op for it has the graph the op
514
- * describes; applying the primitives again would add the node twice.
515
- * The op still joins the log — it is the one everybody else has — so
516
- * undo names it and the activity shows it, and the graph stays put.
517
- */
518
- if (!options.applied)
519
- this.graph.applyPrimitives(here.primitives);
842
+ /*
843
+ * ALL OR NOTHING, ACROSS THE OPS TOO (FR-26). Every op's primitives go
844
+ * on first; one that fails puts the graph back as it was — `load`, so a
845
+ * listener that heard the ops before it hears them reversed — and
846
+ * throws a ReceiveError naming that op, before any of them joins the
847
+ * log. A store handed a bad op is the store it was.
848
+ *
849
+ * ALREADY IN EFFECT HERE (`applied`). A client that applied a call
850
+ * provisionally and is now handed the server's op for it has the graph
851
+ * the op describes; applying the primitives again would add the node
852
+ * twice. The op still joins the log — it is the one everybody else has
853
+ * — so undo names it and the activity shows it, and the graph stays put.
854
+ */
855
+ if (!options.applied) {
856
+ for (const op of fresh) {
857
+ try {
858
+ // Somebody's undo puts back what was there, here as there (FR-28).
859
+ this.graph.applyPrimitives(op.primitives, { restoring: op.undoes !== undefined });
860
+ }
861
+ catch (error) {
862
+ this.graph.load(before);
863
+ throw new ReceiveError(op, error);
864
+ }
865
+ }
866
+ }
867
+ const seq = this.log.length;
868
+ for (const [index, op] of fresh.entries()) {
869
+ const here = { ...op, seq: seq + index };
520
870
  this.log.append(here);
521
871
  landed.push(here);
522
872
  }
523
873
  this.notify(diffSnapshots(before, this.graph.snapshot()), landed);
524
874
  return landed;
525
875
  }
876
+ /**
877
+ * OPS A HOST MADE, landed as ordinary history (FR-18): a template's
878
+ * example content, a seeded beginning, a repair or an import the host
879
+ * built itself. Unlike `receive`, which takes ops somebody else's store
880
+ * already made and skips the ones it has, these are new here, so the
881
+ * store fills in what the host left out the way it fills its own: the
882
+ * seq, an id, one batch for the call (`options.batch`, else a fresh one),
883
+ * the time, the inverse (from the primitives), what they write, and no
884
+ * mutation. What the host did say is kept.
885
+ *
886
+ * Each op lands as a write (a default is filled in on the way in, as a
887
+ * fold would), or, when it `undoes` another, as a putting-back. No policy
888
+ * is asked: there is no act to judge, and whoever calls this is the
889
+ * authority, as with `applyPrimitives`. All or nothing: an op that does
890
+ * not fit puts the store back as it was and throws a `ReceiveError`
891
+ * naming it, and an id the log already holds is refused before anything
892
+ * lands. Subscribers hear one change, and every op undoes like any other.
893
+ */
894
+ append(ops, options = {}) {
895
+ if (ops.length === 0)
896
+ return [];
897
+ const taken = new Set(this.log.all().map((op) => op.id));
898
+ let batch;
899
+ const built = ops.map((made, index) => {
900
+ const primitives = made.primitives.map(normalise);
901
+ const op = {
902
+ id: made.id ?? this.nextId(),
903
+ seq: this.log.length + index,
904
+ batch: made.batch ?? options.batch ?? (batch ??= this.mintBatch("batch")),
905
+ author: made.author,
906
+ intent: made.intent,
907
+ ...(made.batchIntent !== undefined ? { batchIntent: made.batchIntent } : {}),
908
+ mutation: made.mutation ?? null,
909
+ primitives,
910
+ inverse: made.inverse ?? [...primitives].reverse().map(invert),
911
+ reads: made.reads ?? [],
912
+ writes: made.writes ?? [...new Set(primitives.flatMap(writesOf))],
913
+ at: made.at ?? this.now(),
914
+ ...(made.undoes !== undefined ? { undoes: made.undoes } : {}),
915
+ ...(made.via !== undefined ? { via: made.via } : {}),
916
+ };
917
+ if (taken.has(op.id))
918
+ throw new GraphError(`Op "${op.id}" (${op.intent}) is already in the log`, "A host-made op needs an id of its own; leave `id` out and the store mints one.");
919
+ taken.add(op.id);
920
+ return op;
921
+ });
922
+ const before = this.graph.snapshot();
923
+ for (const op of built) {
924
+ try {
925
+ this.graph.applyPrimitives(op.primitives, { restoring: op.undoes !== undefined });
926
+ }
927
+ catch (error) {
928
+ this.graph.load(before);
929
+ throw new ReceiveError(op, error);
930
+ }
931
+ }
932
+ for (const op of built)
933
+ this.log.append(op);
934
+ for (const op of built)
935
+ tellTheWatchOfAnAuthor(op.author.id);
936
+ this.notify(diffSnapshots(before, this.graph.snapshot()), built);
937
+ return built;
938
+ }
939
+ /**
940
+ * PRIMITIVES AS AN ORDINARY CHANGE (FR-21): one batch, one op, logged,
941
+ * attributed to `author` and undoable like any act. For changes that are
942
+ * not a declared mutation — a repair from `repairPlan`, a host's own fix —
943
+ * so they land in the history rather than beside it.
944
+ *
945
+ * No policy is asked: there is no act to judge, so whoever calls this is
946
+ * the authority, the way a migration engine is. All or nothing (FR-26): a
947
+ * primitive that does not fit leaves the store as it was and throws.
948
+ */
949
+ applyPrimitives(primitives, options = {}) {
950
+ const batch = options.batch ?? this.mintBatch("batch");
951
+ const author = options.author ?? HUMAN;
952
+ const before = this.violations();
953
+ const rollback = this.graph.snapshot();
954
+ const intent = options.intent ?? "Apply a change";
955
+ const recorded = primitives.map(normalise);
956
+ const ops = [];
957
+ if (recorded.length > 0) {
958
+ const op = {
959
+ id: this.nextId(),
960
+ seq: this.log.length,
961
+ batch,
962
+ author,
963
+ intent,
964
+ mutation: null,
965
+ primitives: recorded,
966
+ inverse: [...recorded].reverse().map(invert),
967
+ reads: [],
968
+ writes: [...new Set(recorded.flatMap(writesOf))],
969
+ at: this.now(),
970
+ ...(options.via !== undefined ? { via: options.via } : {}),
971
+ };
972
+ this.graph.applyPrimitives(op.primitives);
973
+ this.log.append(op);
974
+ ops.push(op);
975
+ tellTheWatchOfAnAuthor(author.id);
976
+ }
977
+ const after = this.violations();
978
+ const beforeKeys = new Set(before.map(violationKey));
979
+ const afterKeys = new Set(after.map(violationKey));
980
+ const diff = diffSnapshots(rollback, this.graph.snapshot());
981
+ this.notify(diff, ops);
982
+ return {
983
+ batch,
984
+ ops,
985
+ diff,
986
+ primitives: recorded,
987
+ reads: [],
988
+ writes: ops[0]?.writes ?? [],
989
+ intent,
990
+ introduces: after.filter((v) => !beforeKeys.has(violationKey(v))),
991
+ resolves: before.filter((v) => !afterKeys.has(violationKey(v))),
992
+ violationsAfter: after,
993
+ };
994
+ }
995
+ /**
996
+ * What in this store no longer fits its declaration (FR-21): the graph
997
+ * as held, judged by `validateGraph` against the schema and the rules
998
+ * this workspace has on. A clean store has none.
999
+ */
1000
+ findings() {
1001
+ return validateGraph({ schema: this.schema, invariants: this.allInvariants() }, this.graph.snapshot(), {
1002
+ invariantOptions: this.invariantOptions,
1003
+ });
1004
+ }
526
1005
  batches() {
527
1006
  return this.log.batches();
528
1007
  }
@@ -550,6 +1029,7 @@ export class Store {
550
1029
  reads: [...new Set(check.ops.flatMap((op) => [...op.reads]))],
551
1030
  writes: [...writes],
552
1031
  intent: `Undo: ${check.ops.map((op) => op.intent).join("; ")}`,
1032
+ restoring: check.ops.every(putsBack),
553
1033
  ...(context === undefined ? {} : { context }),
554
1034
  }),
555
1035
  };
@@ -561,12 +1041,32 @@ export class Store {
561
1041
  * than a stack pop.
562
1042
  */
563
1043
  undo(batchIds, options = {}) {
1044
+ return this.undoing(batchIds, options);
1045
+ }
1046
+ /** `undo` itself, kept apart for the same reason as `applying`. */
1047
+ undoing(batchIds, options) {
564
1048
  const ids = typeof batchIds === "string" ? [batchIds] : batchIds;
565
- const check = this.canUndo(ids);
566
- if (!check.ok)
567
- throw new GraphError(check.message);
568
- const batch = options.batch ?? `undo:${++this.counter}`;
569
1049
  const author = options.author ?? HUMAN;
1050
+ /*
1051
+ * JUDGED FIRST AS THIS SEAT SEES THE LOG (FR-16, FR-02). A change it
1052
+ * may not see is not its to take back, and when one stands in the way
1053
+ * the refusal says a change you cannot see does — the full check's
1054
+ * sentence would quote it. The two agree on what blocks: a withheld op
1055
+ * keeps the ids this seat sees, and those are all it can overlap.
1056
+ */
1057
+ const sighted = (this.policy?.sees?.length ?? 0) > 0 && !isSystem(author);
1058
+ if (sighted) {
1059
+ const seen = checkUndo(readingOf(redact(this.log.all(), seesId(this, author)), () => this.log.epochs()), ids);
1060
+ if (!seen.ok)
1061
+ throw new UndoBlockedError(seen);
1062
+ }
1063
+ const check = this.canUndo(ids);
1064
+ if (!check.ok) {
1065
+ throw new UndoBlockedError(sighted
1066
+ ? { ok: false, ops: [], blockedBy: [], includeBatches: [], message: "Cannot undo on its own — a later change you cannot see depends on it, which only somebody who can see it can take back." }
1067
+ : check);
1068
+ }
1069
+ const batch = options.batch ?? this.mintBatch("undo");
570
1070
  /*
571
1071
  * Undo is a CHANGE, and it is judged like one.
572
1072
  *
@@ -600,7 +1100,8 @@ export class Store {
600
1100
  seq: this.log.length,
601
1101
  batch,
602
1102
  author,
603
- intent: options.intent ?? `Undo: ${target.intent}`,
1103
+ intent: `Undo: ${target.intent}`,
1104
+ ...(options.intent !== undefined ? { batchIntent: options.intent } : {}),
604
1105
  mutation: null,
605
1106
  primitives: target.inverse,
606
1107
  inverse: [...target.inverse].reverse().map(invert),
@@ -608,8 +1109,9 @@ export class Store {
608
1109
  writes: target.writes,
609
1110
  at: this.now(),
610
1111
  undoes: target.id,
1112
+ ...(options.via !== undefined ? { via: options.via } : {}),
611
1113
  };
612
- this.graph.applyPrimitives(op.primitives);
1114
+ this.graph.applyPrimitives(op.primitives, { restoring: putsBack(target) });
613
1115
  this.log.append(op);
614
1116
  ops.push(op);
615
1117
  }
@@ -647,14 +1149,272 @@ export class Store {
647
1149
  this.listeners.add(listener);
648
1150
  return () => this.listeners.delete(listener);
649
1151
  }
1152
+ /**
1153
+ * TELLS EVERY SUBSCRIBER OF A CHANGE: the diff, and the ops that made it.
1154
+ * The store calls this for each change it makes; it is public for a host
1155
+ * that changes the graph some other way and owes its subscribers the same
1156
+ * news (the rebase item of FR-05). A change with no ops and an empty diff
1157
+ * is nothing to tell.
1158
+ */
650
1159
  notify(diff, ops) {
651
- if (ops.length === 0)
1160
+ if (this.quiet > 0)
1161
+ return;
1162
+ if (ops.length === 0 && isEmptyDiff(diff))
652
1163
  return;
653
1164
  for (const listener of this.listeners)
654
1165
  listener(diff, ops);
655
1166
  }
1167
+ /**
1168
+ * AN OPTIMISTIC CLIENT'S ROLLBACK (the rebase item of FR-05).
1169
+ *
1170
+ * A client applies a press at once and sends it; the server's op is the
1171
+ * one that stays. When anything arrives from the server — somebody else's
1172
+ * ops, or the verdict on one of this client's — the client's log must
1173
+ * read `[confirmed…, pending…]` again. So, in one step:
1174
+ *
1175
+ * 1. every op of the `pending` and `drop` batches is rolled back, newest
1176
+ * first, and cut from the log (they must be its tail: an op after
1177
+ * them that is neither is refused, before anything moves);
1178
+ * 2. the `confirmed` ops land in the server's order, skipped by id when
1179
+ * the log already holds them, all or nothing (`ReceiveError`);
1180
+ * 3. the `pending` batches are applied again on top, in the order given,
1181
+ * under the same batch ids, author, intent and channel: each call
1182
+ * judged and compiled afresh against the graph the server's ops left.
1183
+ * One that no longer applies there is left off and said in `refused`;
1184
+ * the server's verdict on it will say the rest.
1185
+ *
1186
+ * `drop` is the batches the server has answered (its own op for them is
1187
+ * among `confirmed`) or refused. Subscribers hear ONE change, the net
1188
+ * diff, with the confirmed ops and the re-applied ones, so an interface
1189
+ * never flickers through the states in between.
1190
+ */
1191
+ rebase(change) {
1192
+ const settling = new Set([...change.pending, ...(change.drop ?? [])]);
1193
+ const all = this.log.all();
1194
+ const first = all.findIndex((op) => settling.has(op.batch));
1195
+ const tail = all.slice(first < 0 ? all.length : first);
1196
+ // The seq the log is cut back to: indexes into `all()` count from the horizon (FR-23).
1197
+ const cut = this.log.horizon + (first < 0 ? all.length : first);
1198
+ const stray = tail.find((op) => !settling.has(op.batch));
1199
+ if (stray) {
1200
+ throw new GraphError(`Cannot rebase: op "${stray.id}" (${stray.intent}) comes after a pending batch and is neither pending nor dropped`, "While anything is pending, land the server's ops through rebase, so the log stays confirmed ops then pending ones.");
1201
+ }
1202
+ const epoch = this.log.lastEpoch();
1203
+ if (epoch && epoch.seq > cut)
1204
+ throw new GraphError(`Cannot rebase across an epoch: one begins at seq ${epoch.seq}, after pending op "${tail[0].id}"`);
1205
+ const before = this.graph.snapshot();
1206
+ const landed = [];
1207
+ const replayed = [];
1208
+ const refused = [];
1209
+ this.quiet++;
1210
+ try {
1211
+ // 1. Back to the confirmed prefix, putting back exactly what each op took.
1212
+ // In place: what a pending op took out goes back where it stood, or this copy and the server's disagree on order.
1213
+ for (const op of [...tail].reverse())
1214
+ this.graph.applyPrimitives(op.inverse, { restoring: true, inPlace: true });
1215
+ this.log.truncate(cut);
1216
+ // 2. What the server says, in its order.
1217
+ const known = new Set(this.log.all().map((op) => op.id));
1218
+ for (const op of change.confirmed) {
1219
+ if (known.has(op.id))
1220
+ continue;
1221
+ try {
1222
+ this.graph.applyPrimitives(op.primitives, { restoring: op.undoes !== undefined });
1223
+ }
1224
+ catch (error) {
1225
+ throw new ReceiveError(op, error);
1226
+ }
1227
+ const here = { ...op, seq: this.log.length };
1228
+ this.log.append(here);
1229
+ known.add(op.id);
1230
+ landed.push(here);
1231
+ }
1232
+ // 3. What is still pending, again, on top.
1233
+ for (const batch of change.pending) {
1234
+ const ops = tail.filter((op) => op.batch === batch);
1235
+ if (ops.length === 0)
1236
+ continue;
1237
+ try {
1238
+ replayed.push(...this.replay(batch, ops));
1239
+ }
1240
+ catch (error) {
1241
+ refused.push({ batch, error });
1242
+ }
1243
+ }
1244
+ }
1245
+ catch (error) {
1246
+ // A store handed a bad op is the store it was.
1247
+ this.log.truncate(cut);
1248
+ for (const op of tail)
1249
+ this.log.append(op);
1250
+ this.graph.load(before);
1251
+ throw error;
1252
+ }
1253
+ finally {
1254
+ this.quiet--;
1255
+ }
1256
+ const diff = diffSnapshots(before, this.graph.snapshot());
1257
+ const ops = [...landed, ...replayed];
1258
+ this.notify(diff, ops);
1259
+ return { diff, confirmed: landed, pending: replayed, refused };
1260
+ }
1261
+ /**
1262
+ * A CLIENT THAT RESYNCED TAKES THE SERVER'S WHOLE STATE (FR-53).
1263
+ *
1264
+ * `rebase` lands the server's ops on the confirmed prefix this store
1265
+ * holds. When that prefix cannot be trusted — this copy drifted, and an
1266
+ * op of the server's no longer fits it, or the server compacted past
1267
+ * where this client left off, so `/graview/since` cannot bring the ops in
1268
+ * between — the client takes the server's graph and log as they are:
1269
+ *
1270
+ * 1. the ops of the `pending` batches are read off the log as it stands;
1271
+ * 2. the log becomes `log` (with its `epochs` and `horizon`, checked as
1272
+ * `OperationLog.from` checks them; one that is not intact is refused
1273
+ * before anything moves) and the graph becomes `snapshot`, in the
1274
+ * server's order — not put back op by op, as `rebase` puts it;
1275
+ * 3. the `pending` batches are applied again on top, in the order given,
1276
+ * exactly as `rebase` applies them: under the same batch ids, judged
1277
+ * and compiled afresh. One that no longer applies is said in `refused`.
1278
+ *
1279
+ * `drop` names the batches the server has answered or refused: they are
1280
+ * not applied again (nor is any batch left unnamed: the log is the
1281
+ * server's). Subscribers hear ONE change, the net diff, with the ops this
1282
+ * store did not hold before and the re-applied ones.
1283
+ */
1284
+ adopt(state) {
1285
+ const dropped = new Set(state.drop ?? []);
1286
+ const pending = (state.pending ?? []).filter((batch) => !dropped.has(batch));
1287
+ const held = this.log.all();
1288
+ const before = this.graph.snapshot();
1289
+ const was = { ops: [...held], epochs: [...this.log.epochs()], horizon: this.log.horizon };
1290
+ const known = new Set(held.map((op) => op.id));
1291
+ const tails = new Map(pending.map((batch) => [batch, held.filter((op) => op.batch === batch)]));
1292
+ // A log that is not intact is refused here, before the graph moves.
1293
+ if (state.log.length > 0)
1294
+ this.log.replace(state.log, state.epochs, state.horizon !== undefined ? { horizon: state.horizon } : {});
1295
+ else {
1296
+ const at = state.horizon ?? 0;
1297
+ this.log.replace([], state.epochs ?? [{ seq: at, base: state.snapshot }], { horizon: at });
1298
+ }
1299
+ const replayed = [];
1300
+ const refused = [];
1301
+ this.quiet++;
1302
+ try {
1303
+ this.graph.load(state.snapshot);
1304
+ this.projected = undefined;
1305
+ this.countPastTheLog();
1306
+ for (const batch of pending) {
1307
+ const ops = tails.get(batch) ?? [];
1308
+ if (ops.length === 0)
1309
+ continue;
1310
+ try {
1311
+ replayed.push(...this.replay(batch, ops));
1312
+ }
1313
+ catch (error) {
1314
+ refused.push({ batch, error });
1315
+ }
1316
+ }
1317
+ }
1318
+ catch (error) {
1319
+ // A store that could not adopt is the store it was.
1320
+ this.log.replace(was.ops, was.epochs, { horizon: was.horizon });
1321
+ this.graph.load(before);
1322
+ this.projected = undefined;
1323
+ throw error;
1324
+ }
1325
+ finally {
1326
+ this.quiet--;
1327
+ }
1328
+ const adopted = this.log.all().filter((op) => !known.has(op.id) && !replayed.includes(op));
1329
+ const diff = diffSnapshots(before, this.graph.snapshot());
1330
+ this.notify(diff, [...adopted, ...replayed]);
1331
+ return { diff, adopted, pending: replayed, refused };
1332
+ }
1333
+ /** One pending batch, applied again as it was made: its calls, its undo, or its primitives. */
1334
+ replay(batch, ops) {
1335
+ const head = ops[0];
1336
+ const options = {
1337
+ author: head.author,
1338
+ batch,
1339
+ ...(head.batchIntent !== undefined ? { intent: head.batchIntent } : {}),
1340
+ ...(head.via !== undefined ? { via: head.via } : {}),
1341
+ };
1342
+ if (head.undoes !== undefined) {
1343
+ const targets = ops.map((op) => this.log.get(op.undoes ?? ""));
1344
+ const missing = ops.find((_op, index) => targets[index] === undefined);
1345
+ if (missing)
1346
+ throw new GraphError(`What "${missing.intent}" took back is no longer in the log`);
1347
+ return this.undoing([...new Set(targets.map((op) => op.batch))], options).ops;
1348
+ }
1349
+ if (ops.every((op) => op.mutation !== null)) {
1350
+ return this.applying(ops.map((op) => op.mutation), options).ops;
1351
+ }
1352
+ return this.append(ops.map(({ seq: _seq, ...op }) => op));
1353
+ }
656
1354
  snapshot() {
657
1355
  return this.graph.snapshot();
658
1356
  }
1357
+ /**
1358
+ * WHETHER THE GRAPH IS WHAT ITS LOG SAYS (FR-20).
1359
+ *
1360
+ * Refolds the log from its last epoch (FR-27), or from empty when it has
1361
+ * none, and compares the result with the graph held, by `snapshotHash`. Agreement returns the hash; disagreement returns both
1362
+ * hashes and the op after which they part, so a host that finds a store
1363
+ * drifted knows where to look rather than only that it should.
1364
+ */
1365
+ verify() {
1366
+ const from = this.log.lastEpoch();
1367
+ return verifyFold(this.schema, this.log.opsFrom(from?.seq ?? 0), this.graph.snapshot(), {
1368
+ validate: this.validate,
1369
+ ...(from ? { base: from.base } : {}),
1370
+ });
1371
+ }
1372
+ /**
1373
+ * THE CHECKPOINT A COMPACTION WOULD MAKE (FR-23), or `undefined` when
1374
+ * nothing is old enough to archive. The horizon is `seq` when given, or
1375
+ * else whichever of two keeps more: the ops made in the last `keepDays`
1376
+ * days before `now` (default 90), or the last `keepOps` ops (default
1377
+ * 1000). It is moved back to the start of the gesture it would split, so
1378
+ * a batch is never half behind it.
1379
+ *
1380
+ * Nothing moves until `compact` is handed the checkpoint, so a host can
1381
+ * archive what is behind it first. A store whose graph does not verify
1382
+ * against its log is not compacted: the checkpoint would vouch for a
1383
+ * history that does not lead to the graph held.
1384
+ */
1385
+ checkpoint(options = {}) {
1386
+ const ops = this.log.all();
1387
+ let seq;
1388
+ if (options.seq !== undefined)
1389
+ seq = options.seq;
1390
+ else {
1391
+ const byOps = this.log.length - (options.keepOps ?? 1000);
1392
+ const now = Date.parse(options.now ?? this.now());
1393
+ const cutoff = now - (options.keepDays ?? 90) * 86_400_000;
1394
+ const kept = ops.find((op) => Date.parse(op.at) > cutoff);
1395
+ const byDays = kept ? kept.seq : this.log.length;
1396
+ seq = Math.min(byOps, byDays);
1397
+ }
1398
+ seq = Math.min(Math.max(seq, this.log.horizon), this.log.length);
1399
+ // Never inside a gesture: back to where the batch it would split began.
1400
+ while (seq > this.log.horizon && seq < this.log.length && ops[seq - this.log.horizon].batch === ops[seq - 1 - this.log.horizon].batch)
1401
+ seq--;
1402
+ if (seq <= this.log.horizon)
1403
+ return undefined;
1404
+ const verified = this.verify();
1405
+ if (!verified.ok)
1406
+ throw new GraphError(`Cannot compact a store that does not verify: ${verified.reason}`);
1407
+ return this.log.checkpointAt(this.schema, seq, { validate: this.validate });
1408
+ }
1409
+ /**
1410
+ * MOVES THE UNDO HORIZON TO `checkpoint` (FR-23): the ops and epochs
1411
+ * before it leave the log, which begins at its seq from now on, and are
1412
+ * returned for an adapter to archive. The graph is unchanged and still
1413
+ * verifies; undo of anything behind the horizon is refused, naming it.
1414
+ * Ship's `opened.compact()` archives first and calls this after.
1415
+ */
1416
+ compact(checkpoint) {
1417
+ return this.log.compact(checkpoint);
1418
+ }
659
1419
  }
660
1420
  //# sourceMappingURL=store.js.map