@graview/core 0.1.0 → 0.1.2

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 (292) 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 +787 -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 +143 -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 +1420 -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 +459 -0
  114. package/dist/document/schema.d.ts.map +1 -0
  115. package/dist/document/schema.js +197 -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 +182 -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 +52 -3
  138. package/dist/graph/graph.d.ts.map +1 -1
  139. package/dist/graph/graph.js +169 -16
  140. package/dist/graph/graph.js.map +1 -1
  141. package/dist/index.d.ts +40 -17
  142. package/dist/index.d.ts.map +1 -1
  143. package/dist/index.js +29 -10
  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 +2 -3
  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 +145 -4
  180. package/dist/ops/log.d.ts.map +1 -1
  181. package/dist/ops/log.js +220 -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 +23 -2
  190. package/dist/ops/undo.d.ts.map +1 -1
  191. package/dist/ops/undo.js +107 -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 +50 -0
  202. package/dist/permissions/sight.d.ts.map +1 -0
  203. package/dist/permissions/sight.js +76 -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 +31 -0
  222. package/dist/presence.d.ts.map +1 -1
  223. package/dist/presence.js +36 -0
  224. package/dist/presence.js.map +1 -1
  225. package/dist/scaffold/from-template.d.ts +16 -0
  226. package/dist/scaffold/from-template.d.ts.map +1 -0
  227. package/dist/scaffold/from-template.js +157 -0
  228. package/dist/scaffold/from-template.js.map +1 -0
  229. package/dist/scaffold/index.d.ts +10 -1
  230. package/dist/scaffold/index.d.ts.map +1 -1
  231. package/dist/scaffold/index.js +35 -7
  232. package/dist/scaffold/index.js.map +1 -1
  233. package/dist/scaffold/names.d.ts +4 -0
  234. package/dist/scaffold/names.d.ts.map +1 -1
  235. package/dist/scaffold/names.js.map +1 -1
  236. package/dist/scaffold/project.d.ts +1 -1
  237. package/dist/scaffold/project.d.ts.map +1 -1
  238. package/dist/scaffold/project.js +44 -13
  239. package/dist/scaffold/project.js.map +1 -1
  240. package/dist/scaffold/ui.d.ts.map +1 -1
  241. package/dist/scaffold/ui.js +19 -10
  242. package/dist/scaffold/ui.js.map +1 -1
  243. package/dist/schema/define-node.d.ts +20 -0
  244. package/dist/schema/define-node.d.ts.map +1 -1
  245. package/dist/schema/define-node.js +27 -2
  246. package/dist/schema/define-node.js.map +1 -1
  247. package/dist/schema/path.d.ts +19 -0
  248. package/dist/schema/path.d.ts.map +1 -0
  249. package/dist/schema/path.js +34 -0
  250. package/dist/schema/path.js.map +1 -0
  251. package/dist/schema/types.d.ts +6 -0
  252. package/dist/schema/types.d.ts.map +1 -1
  253. package/dist/seen.d.ts +58 -0
  254. package/dist/seen.d.ts.map +1 -0
  255. package/dist/seen.js +194 -0
  256. package/dist/seen.js.map +1 -0
  257. package/dist/store.d.ts +317 -8
  258. package/dist/store.d.ts.map +1 -1
  259. package/dist/store.js +720 -37
  260. package/dist/store.js.map +1 -1
  261. package/dist/testing.d.ts +6 -0
  262. package/dist/testing.d.ts.map +1 -1
  263. package/dist/testing.js +3 -1
  264. package/dist/testing.js.map +1 -1
  265. package/dist/theme/palettes.d.ts.map +1 -1
  266. package/dist/theme/palettes.js +4 -0
  267. package/dist/theme/palettes.js.map +1 -1
  268. package/dist/theme/types.d.ts +9 -0
  269. package/dist/theme/types.d.ts.map +1 -1
  270. package/dist/theme/types.js +4 -0
  271. package/dist/theme/types.js.map +1 -1
  272. package/dist/validate-graph.d.ts +83 -0
  273. package/dist/validate-graph.d.ts.map +1 -0
  274. package/dist/validate-graph.js +288 -0
  275. package/dist/validate-graph.js.map +1 -0
  276. package/dist/version.d.ts +3 -0
  277. package/dist/version.d.ts.map +1 -0
  278. package/dist/version.js +3 -0
  279. package/dist/version.js.map +1 -0
  280. package/dist/views/types.d.ts +8 -0
  281. package/dist/views/types.d.ts.map +1 -1
  282. package/dist/views/types.js +5 -0
  283. package/dist/views/types.js.map +1 -1
  284. package/dist/watched.d.ts +39 -0
  285. package/dist/watched.d.ts.map +1 -1
  286. package/dist/watched.js +94 -1
  287. package/dist/watched.js.map +1 -1
  288. package/dist/who.d.ts +24 -1
  289. package/dist/who.d.ts.map +1 -1
  290. package/dist/who.js +49 -2
  291. package/dist/who.js.map +1 -1
  292. 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,17 @@ 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
+ /** Above zero while a rebase applies its pending calls again: they are told as one change at the end. */
105
+ quiet = 0;
51
106
  listeners = new Set();
52
107
  constructor(options) {
53
108
  this.schema = options.schema;
@@ -59,10 +114,14 @@ export class Store {
59
114
  if (provider.may)
60
115
  this.may.set(provider.name, new Set(provider.may));
61
116
  }
62
- this.modules = resolveModules(options.modules, options.enabledModules);
117
+ this.declaredModules = options.modules;
118
+ this.startingModules = options.enabledModules;
63
119
  let n = 0;
64
120
  this.nextId = options.ids ?? (() => `op${++n}`);
65
- this.now = options.now ?? (() => new Date(0).toISOString());
121
+ this.now = options.now ?? (() => new Date().toISOString());
122
+ this.validate = options.validate ?? true;
123
+ const tag = storeTag();
124
+ this.mintBatch = options.batchIds ?? ((kind) => `${kind}:${tag}:${++this.counter}`);
66
125
  for (const mutation of options.mutations ?? []) {
67
126
  if (this.mutations.has(mutation.name)) {
68
127
  throw new GraphError(`Duplicate mutation "${mutation.name}"`);
@@ -81,19 +140,21 @@ export class Store {
81
140
  this.mutations.set(mutation.name, mutation);
82
141
  }
83
142
  tellTheWatchItsNames(options.schema, this.mutations.values(), options.policy);
143
+ const horizon = options.horizon !== undefined ? { horizon: options.horizon } : {};
84
144
  if (options.log && options.snapshot) {
85
145
  // Hydrate: the graph as stored, the history as recorded.
86
- this.log = OperationLog.from(options.log);
146
+ this.log = OperationLog.from(options.log, options.epochs, horizon);
87
147
  this.graph = Graph.from(options.schema, options.snapshot, {
88
148
  validate: options.validate ?? true,
89
149
  });
90
150
  }
91
151
  else if (options.log) {
92
- this.log = OperationLog.from(options.log);
93
- this.graph = this.log.fold(options.schema, { validate: options.validate ?? true });
152
+ this.log = OperationLog.from(options.log, options.epochs, horizon);
153
+ const last = this.log.lastEpoch();
154
+ this.graph = this.log.fold(options.schema, { validate: options.validate ?? true, ...(last ? { from: last } : {}) });
94
155
  }
95
156
  else {
96
- this.log = new OperationLog();
157
+ this.log = OperationLog.from([], options.epochs ?? (options.snapshot ? [{ seq: options.horizon ?? 0, base: options.snapshot }] : []), horizon);
97
158
  this.graph = Graph.from(options.schema, options.snapshot ?? { nodes: [], edges: [] }, {
98
159
  validate: options.validate ?? true,
99
160
  });
@@ -121,8 +182,87 @@ export class Store {
121
182
  this.counter = Math.max(this.counter, trailing(op.batch));
122
183
  n = Math.max(n, trailing(op.id));
123
184
  }
185
+ // Behind an undo horizon (FR-23) are ids the log no longer holds: count on past them all.
186
+ if (this.log.horizon > 0)
187
+ n = Math.max(n, this.log.length);
124
188
  tellTheWatchOfAStore(this);
125
189
  }
190
+ /**
191
+ * What the enabled modules work out to; every surface reads this one answer.
192
+ *
193
+ * READ FROM THE LOG (FR-12): the set the last `system · modules` op says,
194
+ * or the one the store was opened with when no op has said one. Kept
195
+ * between reads and re-read only past what it has read — a log only ever
196
+ * grows at its end, and a rebase that cut it is read again from the start.
197
+ */
198
+ get modules() {
199
+ const all = this.log.all();
200
+ const last = all.at(-1)?.id;
201
+ const held = this.projected;
202
+ if (held && held.length === all.length && held.last === last)
203
+ return held.projection;
204
+ let said;
205
+ let from = 0;
206
+ if (held && held.length <= all.length && (held.length === 0 || all[held.length - 1]?.id === held.last)) {
207
+ said = held.said;
208
+ from = held.length;
209
+ }
210
+ for (let at = all.length - 1; at >= from; at--) {
211
+ const stated = all[at].enabledModules;
212
+ if (stated !== undefined) {
213
+ said = stated;
214
+ break;
215
+ }
216
+ }
217
+ const projection = held && said === held.said ? held.projection : resolveModules(this.declaredModules, said ?? this.startingModules);
218
+ this.projected = { projection, length: all.length, last, said };
219
+ return projection;
220
+ }
221
+ /**
222
+ * TURN MODULES OFF OR ON, AS AN OP (FR-12).
223
+ *
224
+ * The enabled set is a fact about the workspace — usually what it pays
225
+ * for — and changing it is history: one op, authored `system · modules`,
226
+ * saying the set it leaves (`enabledModules`) in a sentence a person can
227
+ * read. It touches no record, so it folds to nothing and takes nothing
228
+ * away: a module off is a horizon, and turning it on again brings every
229
+ * record back as it was. Not undone: the set changes by turning a module
230
+ * the other way. `undefined` is every declared module. Nothing is written
231
+ * when the set is the one the store already has.
232
+ */
233
+ setEnabledModules(enabled, options = {}) {
234
+ const before = this.modules.enabled;
235
+ const after = resolveModules(this.declaredModules, enabled).enabled;
236
+ const off = [...before].filter((name) => !after.has(name));
237
+ const on = [...after].filter((name) => !before.has(name));
238
+ if (off.length === 0 && on.length === 0)
239
+ return undefined;
240
+ const named = (names) => names
241
+ .map((name) => {
242
+ const kinds = this.declaredModules?.[name]?.kinds ?? [];
243
+ return kinds.length > 0 ? kinds.map((kind) => this.schema.tryDefinition(kind)?.plural ?? kind).join(", ") : name;
244
+ })
245
+ .join(", ");
246
+ const said = [off.length > 0 ? `Turn off ${named(off)}` : "", on.length > 0 ? `turn on ${named(on)}` : ""].filter(Boolean).join("; ");
247
+ const op = {
248
+ id: this.nextId(),
249
+ seq: this.log.length,
250
+ batch: options.batch ?? this.mintBatch("batch"),
251
+ author: MODULES_AUTHOR,
252
+ intent: said.charAt(0).toUpperCase() + said.slice(1),
253
+ mutation: null,
254
+ primitives: [],
255
+ inverse: [],
256
+ reads: [],
257
+ writes: [],
258
+ at: this.now(),
259
+ ...(options.via !== undefined ? { via: options.via } : {}),
260
+ enabledModules: [...after].sort(),
261
+ };
262
+ this.log.append(op);
263
+ this.notify(EMPTY_DIFF, [op]);
264
+ return op;
265
+ }
126
266
  mutation(name) {
127
267
  const found = this.mutations.get(name);
128
268
  if (!found) {
@@ -183,7 +323,7 @@ export class Store {
183
323
  * repair as the starter seat, which may only add, and the press met
184
324
  * "starter may not take-off here" (W-110).
185
325
  */
186
- const narrowed = this.refusesAgent(call, principal);
326
+ const narrowed = this.refusesAgent(call, principal) ?? this.namesTurnedOff(call) ?? this.namesUnseen(call, principal);
187
327
  if (narrowed)
188
328
  return { ok: false, refusal: narrowed };
189
329
  return permits(this.policy, principal, call.name, this.subjectKindOf(call), this.viaOf(this.mutations.get(call.name)), this.subjectIdOf(call), this.words);
@@ -216,6 +356,69 @@ export class Store {
216
356
  wouldNeed: [],
217
357
  };
218
358
  }
359
+ /**
360
+ * A CALL THAT NAMES A RECORD OF A MODULE THAT IS OFF IS REFUSED (FR-12),
361
+ * whoever makes it: the module's own acts are already off, and an act
362
+ * the core owns — a rename that takes any kind — must not reach into a
363
+ * district the workspace does not have. The sentence names the act.
364
+ */
365
+ namesTurnedOff(call) {
366
+ const off = this.modules.disabledKinds;
367
+ if (off.size === 0)
368
+ return undefined;
369
+ const mutation = this.mutations.get(call.name);
370
+ if (!mutation)
371
+ return undefined;
372
+ const named = [];
373
+ if (mutation.subject)
374
+ named.push(call.args[mutation.subject.arg]);
375
+ for (const ref of nodeRefArgs(mutation.input))
376
+ named.push(call.args[ref.name]);
377
+ const ids = named.flatMap((value) => (typeof value === "string" ? [value] : Array.isArray(value) ? value.filter((inner) => typeof inner === "string") : []));
378
+ if (!ids.some((id) => off.has(this.graph.getNode(id)?.kind)))
379
+ return undefined;
380
+ return {
381
+ mutation: call.name,
382
+ message: `“${mutation.title ?? call.name}” names a record of a module this workspace has turned off.`,
383
+ wouldNeed: [],
384
+ };
385
+ }
386
+ /**
387
+ * A CALL THAT NAMES A RECORD THE CALLER MAY NOT SEE IS REFUSED (FR-02),
388
+ * before any grant is read: an act on a record is a way of reading it —
389
+ * its preview, its refusal, what its rules say after — and a seat that
390
+ * could act on what it may not see could learn it that way. Only a
391
+ * record that is there is judged; an id that names nothing is the act's
392
+ * own to refuse. The sentence names the act, never the record or its kind.
393
+ */
394
+ namesUnseen(call, principal) {
395
+ if (!this.policy?.sees?.length || isSystem(principal))
396
+ return undefined;
397
+ const mutation = this.mutations.get(call.name);
398
+ if (!mutation)
399
+ return undefined;
400
+ const named = new Set();
401
+ const add = (value) => {
402
+ if (typeof value === "string")
403
+ named.add(value);
404
+ else if (Array.isArray(value))
405
+ for (const inner of value)
406
+ if (typeof inner === "string")
407
+ named.add(inner);
408
+ };
409
+ if (mutation.subject)
410
+ add(call.args[mutation.subject.arg]);
411
+ for (const ref of nodeRefArgs(mutation.input))
412
+ add(call.args[ref.name]);
413
+ const visible = seesId(this, principal);
414
+ if (![...named].some((id) => this.graph.has(id) && !visible(id)))
415
+ return undefined;
416
+ return {
417
+ mutation: call.name,
418
+ message: `Not permitted: “${mutation.title ?? call.name}” names a record you may not see.`,
419
+ wouldNeed: [],
420
+ };
421
+ }
219
422
  /**
220
423
  * Whether a principal may run ANY act of a module drawn only for those
221
424
  * who administer it. This is the one question the interface asks before
@@ -236,6 +439,46 @@ export class Store {
236
439
  return kinds.some((kind) => permits(this.policy, principal, name, kind, this.viaOf(mutation)).ok);
237
440
  });
238
441
  }
442
+ /**
443
+ * The store as one principal may see it: its graph, log, history and
444
+ * problems hold only what the policy's `sees` lets them see, and every
445
+ * act still comes here. With no `sees`, this store itself (see `seen.ts`).
446
+ */
447
+ seenBy(principal) {
448
+ return seenBy(this, principal);
449
+ }
450
+ /** Whether a principal may see one record (the policy's `sees`). */
451
+ sees(principal, id) {
452
+ return this.graph.has(id) && seesId(this, principal)(id);
453
+ }
454
+ labels;
455
+ /**
456
+ * WHAT A NAME GIVEN FOR A NODE ARGUMENT MEANS, to this principal (FR-33).
457
+ *
458
+ * An id of a record the principal may see is that record, whatever it is
459
+ * called. Otherwise a label, case and accents aside, among the records of
460
+ * the kinds the argument accepts; failing that, the one label the name
461
+ * starts. Only records the principal may see are ever candidates — a
462
+ * name never tells a seat that something it may not see exists. Several
463
+ * matches are refused with every candidate; none says so.
464
+ */
465
+ resolveRef(arg, given, principal = HUMAN) {
466
+ const sighted = (this.policy?.sees?.length ?? 0) > 0;
467
+ const visible = (id) => !sighted || this.sees(principal, id);
468
+ const named = this.graph.getNode(given);
469
+ this.labels ??= new LabelIndex(this.graph);
470
+ if (named && visible(given))
471
+ return { ok: true, id: given, label: this.labels.labelOf(given) ?? given, by: "id" };
472
+ const found = this.labels.lookup(arg.kinds, given);
473
+ const exact = found.exact.filter(visible);
474
+ const exactly = exact.length > 0;
475
+ const candidates = (exactly ? exact : found.prefix.filter(visible))
476
+ .map((id) => ({ id, kind: this.graph.getNode(id).kind, label: this.labels.labelOf(id) ?? id }))
477
+ .sort((a, b) => (a.label < b.label ? -1 : a.label > b.label ? 1 : a.id < b.id ? -1 : 1));
478
+ if (candidates.length === 1)
479
+ return { ok: true, id: candidates[0].id, label: candidates[0].label, by: exactly ? "label" : "prefix" };
480
+ return refusalFor(this.schema, arg, given, candidates);
481
+ }
239
482
  /** The kinds of administered modules this principal may not see at all. */
240
483
  kindsKeptFrom(principal = HUMAN) {
241
484
  const kept = new Set();
@@ -245,6 +488,22 @@ export class Store {
245
488
  for (const kind of module.kinds ?? [])
246
489
  kept.add(kind);
247
490
  }
491
+ /*
492
+ * A KIND THE POLICY SAYS THIS SEAT SEES NONE OF, and may not begin: no
493
+ * district, no tab, no "none yet". Somebody browsing a showroom has no
494
+ * test drives to see and none to book, and a Test drives page that said
495
+ * "none yet — waiting for shoppers" was a page about other people.
496
+ */
497
+ const sights = this.policy?.sees ?? [];
498
+ if (sights.length > 0 && !isSystem(principal)) {
499
+ const creatable = new Set(this.permittedMutations(principal).flatMap((mutation) => mutation.creates ?? []));
500
+ // Every kind: once a policy says who sees what, a kind no sight names is seen by nobody.
501
+ for (const kind of this.schema.kinds) {
502
+ const seeing = sights.some((sight) => sight.kinds.includes(kind) && (sight.roles === "*" || (principal.roles ?? []).some((role) => sight.roles.includes(role))) && (!sight.own || principal.id !== undefined));
503
+ if (!seeing && !creatable.has(kind))
504
+ kept.add(kind);
505
+ }
506
+ }
248
507
  return kept;
249
508
  }
250
509
  subjectIdOf(call) {
@@ -266,7 +525,7 @@ export class Store {
266
525
  const declared = permittedMutations(this.policy, principal, all.filter((mutation) => !mutation.derived));
267
526
  // A derived act is offered when the kind's own acts it rides are.
268
527
  const derived = all.filter((mutation) => mutation.derived !== undefined &&
269
- permits(this.policy, principal, mutation.name, mutation.derived.kind, this.viaOf(mutation)).ok);
528
+ permits(this.policy, principal, mutation.name, mutation.derived.kind, this.viaOf(mutation), actingAs(principal).id).ok);
270
529
  return [...declared, ...derived];
271
530
  }
272
531
  /**
@@ -283,8 +542,14 @@ export class Store {
283
542
  if (!subject)
284
543
  return undefined;
285
544
  const id = call.args[subject.arg];
545
+ /*
546
+ * NOT CHOSEN YET — offered from the far end, the subject is still to be
547
+ * asked for. Its kind is the act's to say when it names one: asked as
548
+ * "no kind", a grant drawn by kind ("a reviewer, on talks") refused the
549
+ * reviewer on every topic with "a chair or a reviewer can".
550
+ */
286
551
  if (typeof id !== "string")
287
- return undefined;
552
+ return subject.kinds !== "*" && subject.kinds.length === 1 ? subject.kinds[0] : undefined;
288
553
  return this.graph.getNode(id)?.kind;
289
554
  }
290
555
  /** Current violations, evaluated fresh — nothing is cached or stale. */
@@ -330,10 +595,57 @@ export class Store {
330
595
  context,
331
596
  });
332
597
  }
598
+ /**
599
+ * What several calls would do as one gesture, without doing any of it
600
+ * (FR-18): each compiles on the graph the one before it left, as
601
+ * `applyAll` would run them, on a copy. The diff is the net of them all,
602
+ * and `introduces` and `resolves` judge the end state against today's.
603
+ * A call that cannot compile throws, as it would on applying; nothing is
604
+ * written, logged or heard either way.
605
+ */
606
+ previewAll(calls, context) {
607
+ const before = this.violations(context);
608
+ const start = this.graph.snapshot();
609
+ const trial = Graph.from(this.schema, start, { validate: this.validate });
610
+ const primitives = [];
611
+ const reads = new Set();
612
+ const writes = new Set();
613
+ const intents = [];
614
+ for (const call of calls) {
615
+ const compiled = compileMutation(trial, this.mutation(call.name), call.args);
616
+ // An act that does nothing leaves no trace when applied, nor here.
617
+ if (compiled.primitives.length === 0)
618
+ continue;
619
+ const recorded = compiled.primitives.map(normalise);
620
+ trial.applyPrimitives(recorded);
621
+ primitives.push(...recorded);
622
+ for (const id of compiled.reads)
623
+ reads.add(id);
624
+ for (const id of compiled.writes)
625
+ writes.add(id);
626
+ intents.push(compiled.intent);
627
+ }
628
+ const after = evaluate(trial, this.allInvariants(), {
629
+ ...this.invariantOptions,
630
+ ...(context === undefined ? {} : { context }),
631
+ });
632
+ const beforeKeys = new Set(before.map(violationKey));
633
+ const afterKeys = new Set(after.map(violationKey));
634
+ return {
635
+ diff: diffSnapshots(start, trial.snapshot()),
636
+ primitives,
637
+ reads: [...reads],
638
+ writes: [...writes],
639
+ intent: intents.join("; "),
640
+ introduces: after.filter((v) => !beforeKeys.has(violationKey(v))),
641
+ resolves: before.filter((v) => !afterKeys.has(violationKey(v))),
642
+ violationsAfter: after,
643
+ };
644
+ }
333
645
  previewPrimitives(primitives, meta) {
334
646
  const before = this.violations(meta.context);
335
647
  const trial = Graph.from(this.schema, this.graph.snapshot());
336
- const diff = trial.applyPrimitives(primitives);
648
+ const diff = trial.applyPrimitives(primitives, { restoring: meta.restoring === true });
337
649
  const after = evaluate(trial, this.allInvariants(), {
338
650
  ...this.invariantOptions,
339
651
  ...(meta.context === undefined ? {} : { context: meta.context }),
@@ -357,7 +669,15 @@ export class Store {
357
669
  }
358
670
  /** Applies several mutations as one gesture — one batch, one undo unit. */
359
671
  applyAll(calls, options = {}) {
360
- const batch = options.batch ?? `batch:${++this.counter}`;
672
+ return this.applying(calls, options);
673
+ }
674
+ /**
675
+ * `applyAll` itself. Kept apart so a rebase applies pending calls again
676
+ * through the store's own path, whatever a host has wrapped `applyAll` in
677
+ * (`openRemote` sends each one down the wire).
678
+ */
679
+ applying(calls, options) {
680
+ const batch = options.batch ?? this.mintBatch("batch");
361
681
  const author = options.author ?? HUMAN;
362
682
  const ops = [];
363
683
  const allPrimitives = [];
@@ -395,7 +715,7 @@ export class Store {
395
715
  * was brought in for, and the narrower of the two wins.
396
716
  */
397
717
  for (const call of calls) {
398
- const refusal = this.refusesAgent(call, author);
718
+ const refusal = this.refusesAgent(call, author) ?? this.namesTurnedOff(call);
399
719
  if (refusal) {
400
720
  tellTheWatchOfARefusal(refusal, author.id);
401
721
  throw new PermissionDeniedError(refusal);
@@ -424,7 +744,14 @@ export class Store {
424
744
  seq: this.log.length,
425
745
  batch,
426
746
  author,
427
- intent: options.intent ?? compiled.intent,
747
+ /*
748
+ * THE ACT'S OWN SENTENCE, kept; what the gesture was for goes
749
+ * beside it (FR-18). An act with no `describe` has no sentence of
750
+ * its own, only `close-item(id="deposit")`, and there the caller's
751
+ * words ("Close it", the button's) are the better one.
752
+ */
753
+ intent: definition.describe !== undefined ? compiled.intent : (options.intent ?? compiled.intent),
754
+ ...(options.intent !== undefined ? { batchIntent: options.intent } : {}),
428
755
  mutation: call,
429
756
  /*
430
757
  * NORMALISED on the way into the record: a patch that clears a
@@ -437,6 +764,7 @@ export class Store {
437
764
  reads: compiled.reads,
438
765
  writes: compiled.writes,
439
766
  at: this.now(),
767
+ ...(options.via !== undefined ? { via: options.via } : {}),
440
768
  };
441
769
  this.graph.applyPrimitives(op.primitives);
442
770
  this.log.append(op);
@@ -506,23 +834,169 @@ export class Store {
506
834
  return [];
507
835
  const before = this.graph.snapshot();
508
836
  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);
837
+ /*
838
+ * ALL OR NOTHING, ACROSS THE OPS TOO (FR-26). Every op's primitives go
839
+ * on first; one that fails puts the graph back as it was — `load`, so a
840
+ * listener that heard the ops before it hears them reversed — and
841
+ * throws a ReceiveError naming that op, before any of them joins the
842
+ * log. A store handed a bad op is the store it was.
843
+ *
844
+ * ALREADY IN EFFECT HERE (`applied`). A client that applied a call
845
+ * provisionally and is now handed the server's op for it has the graph
846
+ * the op describes; applying the primitives again would add the node
847
+ * twice. The op still joins the log — it is the one everybody else has
848
+ * — so undo names it and the activity shows it, and the graph stays put.
849
+ */
850
+ if (!options.applied) {
851
+ for (const op of fresh) {
852
+ try {
853
+ // Somebody's undo puts back what was there, here as there (FR-28).
854
+ this.graph.applyPrimitives(op.primitives, { restoring: op.undoes !== undefined });
855
+ }
856
+ catch (error) {
857
+ this.graph.load(before);
858
+ throw new ReceiveError(op, error);
859
+ }
860
+ }
861
+ }
862
+ const seq = this.log.length;
863
+ for (const [index, op] of fresh.entries()) {
864
+ const here = { ...op, seq: seq + index };
520
865
  this.log.append(here);
521
866
  landed.push(here);
522
867
  }
523
868
  this.notify(diffSnapshots(before, this.graph.snapshot()), landed);
524
869
  return landed;
525
870
  }
871
+ /**
872
+ * OPS A HOST MADE, landed as ordinary history (FR-18): a template's
873
+ * example content, a seeded beginning, a repair or an import the host
874
+ * built itself. Unlike `receive`, which takes ops somebody else's store
875
+ * already made and skips the ones it has, these are new here, so the
876
+ * store fills in what the host left out the way it fills its own: the
877
+ * seq, an id, one batch for the call (`options.batch`, else a fresh one),
878
+ * the time, the inverse (from the primitives), what they write, and no
879
+ * mutation. What the host did say is kept.
880
+ *
881
+ * Each op lands as a write (a default is filled in on the way in, as a
882
+ * fold would), or, when it `undoes` another, as a putting-back. No policy
883
+ * is asked: there is no act to judge, and whoever calls this is the
884
+ * authority, as with `applyPrimitives`. All or nothing: an op that does
885
+ * not fit puts the store back as it was and throws a `ReceiveError`
886
+ * naming it, and an id the log already holds is refused before anything
887
+ * lands. Subscribers hear one change, and every op undoes like any other.
888
+ */
889
+ append(ops, options = {}) {
890
+ if (ops.length === 0)
891
+ return [];
892
+ const taken = new Set(this.log.all().map((op) => op.id));
893
+ let batch;
894
+ const built = ops.map((made, index) => {
895
+ const primitives = made.primitives.map(normalise);
896
+ const op = {
897
+ id: made.id ?? this.nextId(),
898
+ seq: this.log.length + index,
899
+ batch: made.batch ?? options.batch ?? (batch ??= this.mintBatch("batch")),
900
+ author: made.author,
901
+ intent: made.intent,
902
+ ...(made.batchIntent !== undefined ? { batchIntent: made.batchIntent } : {}),
903
+ mutation: made.mutation ?? null,
904
+ primitives,
905
+ inverse: made.inverse ?? [...primitives].reverse().map(invert),
906
+ reads: made.reads ?? [],
907
+ writes: made.writes ?? [...new Set(primitives.flatMap(writesOf))],
908
+ at: made.at ?? this.now(),
909
+ ...(made.undoes !== undefined ? { undoes: made.undoes } : {}),
910
+ ...(made.via !== undefined ? { via: made.via } : {}),
911
+ };
912
+ if (taken.has(op.id))
913
+ 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.");
914
+ taken.add(op.id);
915
+ return op;
916
+ });
917
+ const before = this.graph.snapshot();
918
+ for (const op of built) {
919
+ try {
920
+ this.graph.applyPrimitives(op.primitives, { restoring: op.undoes !== undefined });
921
+ }
922
+ catch (error) {
923
+ this.graph.load(before);
924
+ throw new ReceiveError(op, error);
925
+ }
926
+ }
927
+ for (const op of built)
928
+ this.log.append(op);
929
+ for (const op of built)
930
+ tellTheWatchOfAnAuthor(op.author.id);
931
+ this.notify(diffSnapshots(before, this.graph.snapshot()), built);
932
+ return built;
933
+ }
934
+ /**
935
+ * PRIMITIVES AS AN ORDINARY CHANGE (FR-21): one batch, one op, logged,
936
+ * attributed to `author` and undoable like any act. For changes that are
937
+ * not a declared mutation — a repair from `repairPlan`, a host's own fix —
938
+ * so they land in the history rather than beside it.
939
+ *
940
+ * No policy is asked: there is no act to judge, so whoever calls this is
941
+ * the authority, the way a migration engine is. All or nothing (FR-26): a
942
+ * primitive that does not fit leaves the store as it was and throws.
943
+ */
944
+ applyPrimitives(primitives, options = {}) {
945
+ const batch = options.batch ?? this.mintBatch("batch");
946
+ const author = options.author ?? HUMAN;
947
+ const before = this.violations();
948
+ const rollback = this.graph.snapshot();
949
+ const intent = options.intent ?? "Apply a change";
950
+ const recorded = primitives.map(normalise);
951
+ const ops = [];
952
+ if (recorded.length > 0) {
953
+ const op = {
954
+ id: this.nextId(),
955
+ seq: this.log.length,
956
+ batch,
957
+ author,
958
+ intent,
959
+ mutation: null,
960
+ primitives: recorded,
961
+ inverse: [...recorded].reverse().map(invert),
962
+ reads: [],
963
+ writes: [...new Set(recorded.flatMap(writesOf))],
964
+ at: this.now(),
965
+ ...(options.via !== undefined ? { via: options.via } : {}),
966
+ };
967
+ this.graph.applyPrimitives(op.primitives);
968
+ this.log.append(op);
969
+ ops.push(op);
970
+ tellTheWatchOfAnAuthor(author.id);
971
+ }
972
+ const after = this.violations();
973
+ const beforeKeys = new Set(before.map(violationKey));
974
+ const afterKeys = new Set(after.map(violationKey));
975
+ const diff = diffSnapshots(rollback, this.graph.snapshot());
976
+ this.notify(diff, ops);
977
+ return {
978
+ batch,
979
+ ops,
980
+ diff,
981
+ primitives: recorded,
982
+ reads: [],
983
+ writes: ops[0]?.writes ?? [],
984
+ intent,
985
+ introduces: after.filter((v) => !beforeKeys.has(violationKey(v))),
986
+ resolves: before.filter((v) => !afterKeys.has(violationKey(v))),
987
+ violationsAfter: after,
988
+ };
989
+ }
990
+ /**
991
+ * What in this store no longer fits its declaration (FR-21): the graph
992
+ * as held, judged by `validateGraph` against the schema and the rules
993
+ * this workspace has on. A clean store has none.
994
+ */
995
+ findings() {
996
+ return validateGraph({ schema: this.schema, invariants: this.allInvariants() }, this.graph.snapshot(), {
997
+ invariantOptions: this.invariantOptions,
998
+ });
999
+ }
526
1000
  batches() {
527
1001
  return this.log.batches();
528
1002
  }
@@ -550,6 +1024,7 @@ export class Store {
550
1024
  reads: [...new Set(check.ops.flatMap((op) => [...op.reads]))],
551
1025
  writes: [...writes],
552
1026
  intent: `Undo: ${check.ops.map((op) => op.intent).join("; ")}`,
1027
+ restoring: check.ops.every(putsBack),
553
1028
  ...(context === undefined ? {} : { context }),
554
1029
  }),
555
1030
  };
@@ -561,12 +1036,32 @@ export class Store {
561
1036
  * than a stack pop.
562
1037
  */
563
1038
  undo(batchIds, options = {}) {
1039
+ return this.undoing(batchIds, options);
1040
+ }
1041
+ /** `undo` itself, kept apart for the same reason as `applying`. */
1042
+ undoing(batchIds, options) {
564
1043
  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
1044
  const author = options.author ?? HUMAN;
1045
+ /*
1046
+ * JUDGED FIRST AS THIS SEAT SEES THE LOG (FR-16, FR-02). A change it
1047
+ * may not see is not its to take back, and when one stands in the way
1048
+ * the refusal says a change you cannot see does — the full check's
1049
+ * sentence would quote it. The two agree on what blocks: a withheld op
1050
+ * keeps the ids this seat sees, and those are all it can overlap.
1051
+ */
1052
+ const sighted = (this.policy?.sees?.length ?? 0) > 0 && !isSystem(author);
1053
+ if (sighted) {
1054
+ const seen = checkUndo(readingOf(redact(this.log.all(), seesId(this, author)), () => this.log.epochs()), ids);
1055
+ if (!seen.ok)
1056
+ throw new UndoBlockedError(seen);
1057
+ }
1058
+ const check = this.canUndo(ids);
1059
+ if (!check.ok) {
1060
+ throw new UndoBlockedError(sighted
1061
+ ? { 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." }
1062
+ : check);
1063
+ }
1064
+ const batch = options.batch ?? this.mintBatch("undo");
570
1065
  /*
571
1066
  * Undo is a CHANGE, and it is judged like one.
572
1067
  *
@@ -600,7 +1095,8 @@ export class Store {
600
1095
  seq: this.log.length,
601
1096
  batch,
602
1097
  author,
603
- intent: options.intent ?? `Undo: ${target.intent}`,
1098
+ intent: `Undo: ${target.intent}`,
1099
+ ...(options.intent !== undefined ? { batchIntent: options.intent } : {}),
604
1100
  mutation: null,
605
1101
  primitives: target.inverse,
606
1102
  inverse: [...target.inverse].reverse().map(invert),
@@ -608,8 +1104,9 @@ export class Store {
608
1104
  writes: target.writes,
609
1105
  at: this.now(),
610
1106
  undoes: target.id,
1107
+ ...(options.via !== undefined ? { via: options.via } : {}),
611
1108
  };
612
- this.graph.applyPrimitives(op.primitives);
1109
+ this.graph.applyPrimitives(op.primitives, { restoring: putsBack(target) });
613
1110
  this.log.append(op);
614
1111
  ops.push(op);
615
1112
  }
@@ -647,14 +1144,200 @@ export class Store {
647
1144
  this.listeners.add(listener);
648
1145
  return () => this.listeners.delete(listener);
649
1146
  }
1147
+ /**
1148
+ * TELLS EVERY SUBSCRIBER OF A CHANGE: the diff, and the ops that made it.
1149
+ * The store calls this for each change it makes; it is public for a host
1150
+ * that changes the graph some other way and owes its subscribers the same
1151
+ * news (the rebase item of FR-05). A change with no ops and an empty diff
1152
+ * is nothing to tell.
1153
+ */
650
1154
  notify(diff, ops) {
651
- if (ops.length === 0)
1155
+ if (this.quiet > 0)
1156
+ return;
1157
+ if (ops.length === 0 && isEmptyDiff(diff))
652
1158
  return;
653
1159
  for (const listener of this.listeners)
654
1160
  listener(diff, ops);
655
1161
  }
1162
+ /**
1163
+ * AN OPTIMISTIC CLIENT'S ROLLBACK (the rebase item of FR-05).
1164
+ *
1165
+ * A client applies a press at once and sends it; the server's op is the
1166
+ * one that stays. When anything arrives from the server — somebody else's
1167
+ * ops, or the verdict on one of this client's — the client's log must
1168
+ * read `[confirmed…, pending…]` again. So, in one step:
1169
+ *
1170
+ * 1. every op of the `pending` and `drop` batches is rolled back, newest
1171
+ * first, and cut from the log (they must be its tail: an op after
1172
+ * them that is neither is refused, before anything moves);
1173
+ * 2. the `confirmed` ops land in the server's order, skipped by id when
1174
+ * the log already holds them, all or nothing (`ReceiveError`);
1175
+ * 3. the `pending` batches are applied again on top, in the order given,
1176
+ * under the same batch ids, author, intent and channel: each call
1177
+ * judged and compiled afresh against the graph the server's ops left.
1178
+ * One that no longer applies there is left off and said in `refused`;
1179
+ * the server's verdict on it will say the rest.
1180
+ *
1181
+ * `drop` is the batches the server has answered (its own op for them is
1182
+ * among `confirmed`) or refused. Subscribers hear ONE change, the net
1183
+ * diff, with the confirmed ops and the re-applied ones, so an interface
1184
+ * never flickers through the states in between.
1185
+ */
1186
+ rebase(change) {
1187
+ const settling = new Set([...change.pending, ...(change.drop ?? [])]);
1188
+ const all = this.log.all();
1189
+ const first = all.findIndex((op) => settling.has(op.batch));
1190
+ const tail = all.slice(first < 0 ? all.length : first);
1191
+ // The seq the log is cut back to: indexes into `all()` count from the horizon (FR-23).
1192
+ const cut = this.log.horizon + (first < 0 ? all.length : first);
1193
+ const stray = tail.find((op) => !settling.has(op.batch));
1194
+ if (stray) {
1195
+ 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.");
1196
+ }
1197
+ const epoch = this.log.lastEpoch();
1198
+ if (epoch && epoch.seq > cut)
1199
+ throw new GraphError(`Cannot rebase across an epoch: one begins at seq ${epoch.seq}, after pending op "${tail[0].id}"`);
1200
+ const before = this.graph.snapshot();
1201
+ const landed = [];
1202
+ const replayed = [];
1203
+ const refused = [];
1204
+ this.quiet++;
1205
+ try {
1206
+ // 1. Back to the confirmed prefix, putting back exactly what each op took.
1207
+ // In place: what a pending op took out goes back where it stood, or this copy and the server's disagree on order.
1208
+ for (const op of [...tail].reverse())
1209
+ this.graph.applyPrimitives(op.inverse, { restoring: true, inPlace: true });
1210
+ this.log.truncate(cut);
1211
+ // 2. What the server says, in its order.
1212
+ const known = new Set(this.log.all().map((op) => op.id));
1213
+ for (const op of change.confirmed) {
1214
+ if (known.has(op.id))
1215
+ continue;
1216
+ try {
1217
+ this.graph.applyPrimitives(op.primitives, { restoring: op.undoes !== undefined });
1218
+ }
1219
+ catch (error) {
1220
+ throw new ReceiveError(op, error);
1221
+ }
1222
+ const here = { ...op, seq: this.log.length };
1223
+ this.log.append(here);
1224
+ known.add(op.id);
1225
+ landed.push(here);
1226
+ }
1227
+ // 3. What is still pending, again, on top.
1228
+ for (const batch of change.pending) {
1229
+ const ops = tail.filter((op) => op.batch === batch);
1230
+ if (ops.length === 0)
1231
+ continue;
1232
+ try {
1233
+ replayed.push(...this.replay(batch, ops));
1234
+ }
1235
+ catch (error) {
1236
+ refused.push({ batch, error });
1237
+ }
1238
+ }
1239
+ }
1240
+ catch (error) {
1241
+ // A store handed a bad op is the store it was.
1242
+ this.log.truncate(cut);
1243
+ for (const op of tail)
1244
+ this.log.append(op);
1245
+ this.graph.load(before);
1246
+ throw error;
1247
+ }
1248
+ finally {
1249
+ this.quiet--;
1250
+ }
1251
+ const diff = diffSnapshots(before, this.graph.snapshot());
1252
+ const ops = [...landed, ...replayed];
1253
+ this.notify(diff, ops);
1254
+ return { diff, confirmed: landed, pending: replayed, refused };
1255
+ }
1256
+ /** One pending batch, applied again as it was made: its calls, its undo, or its primitives. */
1257
+ replay(batch, ops) {
1258
+ const head = ops[0];
1259
+ const options = {
1260
+ author: head.author,
1261
+ batch,
1262
+ ...(head.batchIntent !== undefined ? { intent: head.batchIntent } : {}),
1263
+ ...(head.via !== undefined ? { via: head.via } : {}),
1264
+ };
1265
+ if (head.undoes !== undefined) {
1266
+ const targets = ops.map((op) => this.log.get(op.undoes ?? ""));
1267
+ const missing = ops.find((_op, index) => targets[index] === undefined);
1268
+ if (missing)
1269
+ throw new GraphError(`What "${missing.intent}" took back is no longer in the log`);
1270
+ return this.undoing([...new Set(targets.map((op) => op.batch))], options).ops;
1271
+ }
1272
+ if (ops.every((op) => op.mutation !== null)) {
1273
+ return this.applying(ops.map((op) => op.mutation), options).ops;
1274
+ }
1275
+ return this.append(ops.map(({ seq: _seq, ...op }) => op));
1276
+ }
656
1277
  snapshot() {
657
1278
  return this.graph.snapshot();
658
1279
  }
1280
+ /**
1281
+ * WHETHER THE GRAPH IS WHAT ITS LOG SAYS (FR-20).
1282
+ *
1283
+ * Refolds the log from its last epoch (FR-27), or from empty when it has
1284
+ * none, and compares the result with the graph held, by `snapshotHash`. Agreement returns the hash; disagreement returns both
1285
+ * hashes and the op after which they part, so a host that finds a store
1286
+ * drifted knows where to look rather than only that it should.
1287
+ */
1288
+ verify() {
1289
+ const from = this.log.lastEpoch();
1290
+ return verifyFold(this.schema, this.log.opsFrom(from?.seq ?? 0), this.graph.snapshot(), {
1291
+ validate: this.validate,
1292
+ ...(from ? { base: from.base } : {}),
1293
+ });
1294
+ }
1295
+ /**
1296
+ * THE CHECKPOINT A COMPACTION WOULD MAKE (FR-23), or `undefined` when
1297
+ * nothing is old enough to archive. The horizon is `seq` when given, or
1298
+ * else whichever of two keeps more: the ops made in the last `keepDays`
1299
+ * days before `now` (default 90), or the last `keepOps` ops (default
1300
+ * 1000). It is moved back to the start of the gesture it would split, so
1301
+ * a batch is never half behind it.
1302
+ *
1303
+ * Nothing moves until `compact` is handed the checkpoint, so a host can
1304
+ * archive what is behind it first. A store whose graph does not verify
1305
+ * against its log is not compacted: the checkpoint would vouch for a
1306
+ * history that does not lead to the graph held.
1307
+ */
1308
+ checkpoint(options = {}) {
1309
+ const ops = this.log.all();
1310
+ let seq;
1311
+ if (options.seq !== undefined)
1312
+ seq = options.seq;
1313
+ else {
1314
+ const byOps = this.log.length - (options.keepOps ?? 1000);
1315
+ const now = Date.parse(options.now ?? this.now());
1316
+ const cutoff = now - (options.keepDays ?? 90) * 86_400_000;
1317
+ const kept = ops.find((op) => Date.parse(op.at) > cutoff);
1318
+ const byDays = kept ? kept.seq : this.log.length;
1319
+ seq = Math.min(byOps, byDays);
1320
+ }
1321
+ seq = Math.min(Math.max(seq, this.log.horizon), this.log.length);
1322
+ // Never inside a gesture: back to where the batch it would split began.
1323
+ while (seq > this.log.horizon && seq < this.log.length && ops[seq - this.log.horizon].batch === ops[seq - 1 - this.log.horizon].batch)
1324
+ seq--;
1325
+ if (seq <= this.log.horizon)
1326
+ return undefined;
1327
+ const verified = this.verify();
1328
+ if (!verified.ok)
1329
+ throw new GraphError(`Cannot compact a store that does not verify: ${verified.reason}`);
1330
+ return this.log.checkpointAt(this.schema, seq, { validate: this.validate });
1331
+ }
1332
+ /**
1333
+ * MOVES THE UNDO HORIZON TO `checkpoint` (FR-23): the ops and epochs
1334
+ * before it leave the log, which begins at its seq from now on, and are
1335
+ * returned for an adapter to archive. The graph is unchanged and still
1336
+ * verifies; undo of anything behind the horizon is refused, naming it.
1337
+ * Ship's `opened.compact()` archives first and calls this after.
1338
+ */
1339
+ compact(checkpoint) {
1340
+ return this.log.compact(checkpoint);
1341
+ }
659
1342
  }
660
1343
  //# sourceMappingURL=store.js.map