@agentshouse/mdruntime 0.1.0-c969b8dee6cc8 → 0.1.0-c96e80d38e5f0

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 (231) hide show
  1. package/README.md +19 -16
  2. package/dist/adapter.d.ts +134 -76
  3. package/dist/adapter.d.ts.map +1 -1
  4. package/dist/attached-model.d.ts +5 -1
  5. package/dist/attached-model.d.ts.map +1 -1
  6. package/dist/attached-model.js +38 -4
  7. package/dist/binary.d.ts +0 -5
  8. package/dist/binary.d.ts.map +1 -1
  9. package/dist/binary.js +1 -1
  10. package/dist/bootstrap.d.ts +25 -10
  11. package/dist/bootstrap.d.ts.map +1 -1
  12. package/dist/bootstrap.js +180 -103
  13. package/dist/chain.d.ts +11 -0
  14. package/dist/chain.d.ts.map +1 -0
  15. package/dist/chain.js +64 -0
  16. package/dist/change-set.d.ts +9 -16
  17. package/dist/change-set.d.ts.map +1 -1
  18. package/dist/change-set.js +11 -89
  19. package/dist/change.d.ts +31 -1
  20. package/dist/change.d.ts.map +1 -1
  21. package/dist/change.js +10 -1
  22. package/dist/client.d.ts +13 -9
  23. package/dist/client.d.ts.map +1 -1
  24. package/dist/client.js +80 -7
  25. package/dist/collection.d.ts +10 -21
  26. package/dist/collection.d.ts.map +1 -1
  27. package/dist/collection.js +39 -291
  28. package/dist/compile.d.ts +16 -0
  29. package/dist/compile.d.ts.map +1 -0
  30. package/dist/compile.js +789 -0
  31. package/dist/connection.d.ts +19 -15
  32. package/dist/connection.d.ts.map +1 -1
  33. package/dist/connection.js +3020 -1417
  34. package/dist/conversation.d.ts +23 -14
  35. package/dist/conversation.d.ts.map +1 -1
  36. package/dist/conversation.js +337 -179
  37. package/dist/digest.d.ts +7 -0
  38. package/dist/digest.d.ts.map +1 -0
  39. package/dist/digest.js +113 -0
  40. package/dist/durability.d.ts +2 -5
  41. package/dist/durability.d.ts.map +1 -1
  42. package/dist/durability.js +5 -0
  43. package/dist/entry.d.ts +4 -1
  44. package/dist/entry.d.ts.map +1 -1
  45. package/dist/entry.js +19 -23
  46. package/dist/file.d.ts +2 -19
  47. package/dist/file.d.ts.map +1 -1
  48. package/dist/file.js +7 -122
  49. package/dist/host.d.ts +4 -10
  50. package/dist/host.d.ts.map +1 -1
  51. package/dist/host.js +49 -31
  52. package/dist/identity.d.ts +0 -1
  53. package/dist/identity.d.ts.map +1 -1
  54. package/dist/identity.js +0 -3
  55. package/dist/index.d.ts +14 -11
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +6 -2
  58. package/dist/live.d.ts +22 -0
  59. package/dist/live.d.ts.map +1 -0
  60. package/dist/live.js +123 -0
  61. package/dist/media.d.ts +8 -0
  62. package/dist/media.d.ts.map +1 -0
  63. package/dist/media.js +218 -0
  64. package/dist/memory.d.ts +11 -7
  65. package/dist/memory.d.ts.map +1 -1
  66. package/dist/memory.js +198 -107
  67. package/dist/messages.d.ts +28 -0
  68. package/dist/messages.d.ts.map +1 -0
  69. package/dist/messages.js +89 -0
  70. package/dist/model.d.ts +6 -1
  71. package/dist/model.d.ts.map +1 -1
  72. package/dist/model.js +7 -10
  73. package/dist/name.d.ts +4 -0
  74. package/dist/name.d.ts.map +1 -0
  75. package/dist/name.js +122 -0
  76. package/dist/observation.d.ts +13 -0
  77. package/dist/observation.d.ts.map +1 -1
  78. package/dist/observation.js +84 -0
  79. package/dist/opened.d.ts +22 -0
  80. package/dist/opened.d.ts.map +1 -0
  81. package/dist/opened.js +107 -0
  82. package/dist/outcome.d.ts +40 -8
  83. package/dist/outcome.d.ts.map +1 -1
  84. package/dist/outcome.js +38 -2
  85. package/dist/overlay.d.ts +2 -5
  86. package/dist/overlay.d.ts.map +1 -1
  87. package/dist/overlay.js +87 -172
  88. package/dist/paths.d.ts +1 -0
  89. package/dist/paths.d.ts.map +1 -1
  90. package/dist/paths.js +4 -0
  91. package/dist/phrase.d.ts +3 -0
  92. package/dist/phrase.d.ts.map +1 -0
  93. package/dist/phrase.js +8 -0
  94. package/dist/query.d.ts +15 -1
  95. package/dist/query.d.ts.map +1 -1
  96. package/dist/query.js +33 -2
  97. package/dist/reach.d.ts +13 -4
  98. package/dist/reach.d.ts.map +1 -1
  99. package/dist/reach.js +200 -81
  100. package/dist/react.d.ts +1 -0
  101. package/dist/react.d.ts.map +1 -1
  102. package/dist/readiness.d.ts +14 -17
  103. package/dist/readiness.d.ts.map +1 -1
  104. package/dist/readiness.js +2 -8
  105. package/dist/recovery.d.ts +3 -2
  106. package/dist/recovery.d.ts.map +1 -1
  107. package/dist/recovery.js +34 -7
  108. package/dist/refresh.d.ts +18 -15
  109. package/dist/refresh.d.ts.map +1 -1
  110. package/dist/refresh.js +155 -58
  111. package/dist/replica.d.ts +54 -36
  112. package/dist/replica.d.ts.map +1 -1
  113. package/dist/replica.js +282 -189
  114. package/dist/room-draft.d.ts +22 -4
  115. package/dist/room-draft.d.ts.map +1 -1
  116. package/dist/room-draft.js +154 -67
  117. package/dist/rows.d.ts +39 -31
  118. package/dist/rows.d.ts.map +1 -1
  119. package/dist/rows.js +178 -101
  120. package/dist/save.d.ts +52 -14
  121. package/dist/save.d.ts.map +1 -1
  122. package/dist/save.js +513 -185
  123. package/dist/search.d.ts +12 -95
  124. package/dist/search.d.ts.map +1 -1
  125. package/dist/search.js +10 -266
  126. package/dist/shared.d.ts +7 -3
  127. package/dist/shared.d.ts.map +1 -1
  128. package/dist/shared.js +119 -66
  129. package/dist/status.d.ts +4 -3
  130. package/dist/status.d.ts.map +1 -1
  131. package/dist/status.js +3 -1
  132. package/dist/store.d.ts +91 -103
  133. package/dist/store.d.ts.map +1 -1
  134. package/dist/store.js +330 -693
  135. package/dist/stored.d.ts +162 -0
  136. package/dist/stored.d.ts.map +1 -0
  137. package/dist/stored.js +549 -0
  138. package/dist/tables.d.ts +353 -0
  139. package/dist/tables.d.ts.map +1 -0
  140. package/dist/tables.js +585 -0
  141. package/dist/transfer.d.ts +0 -5
  142. package/dist/transfer.d.ts.map +1 -1
  143. package/dist/turn.d.ts +5 -3
  144. package/dist/turn.d.ts.map +1 -1
  145. package/dist/turn.js +51 -21
  146. package/dist/validation.d.ts +29 -0
  147. package/dist/validation.d.ts.map +1 -0
  148. package/dist/validation.js +101 -0
  149. package/dist/windows.d.ts +2 -6
  150. package/dist/windows.d.ts.map +1 -1
  151. package/dist/windows.js +30 -34
  152. package/dist/wire.d.ts +18 -5
  153. package/dist/wire.d.ts.map +1 -1
  154. package/dist/wire.js +6 -2
  155. package/package.json +18 -19
  156. package/AGENTS.md +0 -305
  157. package/dist/acquisition.d.ts +0 -14
  158. package/dist/acquisition.d.ts.map +0 -1
  159. package/dist/acquisition.js +0 -24
  160. package/dist/chat/acts.d.ts +0 -33
  161. package/dist/chat/acts.d.ts.map +0 -1
  162. package/dist/chat/acts.js +0 -36
  163. package/dist/chat/context.d.ts +0 -3
  164. package/dist/chat/context.d.ts.map +0 -1
  165. package/dist/chat/context.js +0 -10
  166. package/dist/chat/earlier.d.ts +0 -10
  167. package/dist/chat/earlier.d.ts.map +0 -1
  168. package/dist/chat/earlier.js +0 -54
  169. package/dist/chat/input.d.ts +0 -9
  170. package/dist/chat/input.d.ts.map +0 -1
  171. package/dist/chat/input.js +0 -99
  172. package/dist/chat/kit.d.ts +0 -61
  173. package/dist/chat/kit.d.ts.map +0 -1
  174. package/dist/chat/kit.js +0 -123
  175. package/dist/chat/lasting.d.ts +0 -6
  176. package/dist/chat/lasting.d.ts.map +0 -1
  177. package/dist/chat/lasting.js +0 -19
  178. package/dist/chat/list.d.ts +0 -27
  179. package/dist/chat/list.d.ts.map +0 -1
  180. package/dist/chat/list.js +0 -99
  181. package/dist/chat/markdown.d.ts +0 -20
  182. package/dist/chat/markdown.d.ts.map +0 -1
  183. package/dist/chat/markdown.js +0 -188
  184. package/dist/chat/message.d.ts +0 -6
  185. package/dist/chat/message.d.ts.map +0 -1
  186. package/dist/chat/message.js +0 -192
  187. package/dist/chat/messages.d.ts +0 -22
  188. package/dist/chat/messages.d.ts.map +0 -1
  189. package/dist/chat/messages.js +0 -155
  190. package/dist/chat/name.d.ts +0 -6
  191. package/dist/chat/name.d.ts.map +0 -1
  192. package/dist/chat/name.js +0 -37
  193. package/dist/chat/opened.d.ts +0 -11
  194. package/dist/chat/opened.d.ts.map +0 -1
  195. package/dist/chat/opened.js +0 -17
  196. package/dist/chat/overlay.d.ts +0 -31
  197. package/dist/chat/overlay.d.ts.map +0 -1
  198. package/dist/chat/overlay.js +0 -97
  199. package/dist/chat/parts.d.ts +0 -57
  200. package/dist/chat/parts.d.ts.map +0 -1
  201. package/dist/chat/parts.js +0 -119
  202. package/dist/chat/process.d.ts +0 -20
  203. package/dist/chat/process.d.ts.map +0 -1
  204. package/dist/chat/process.js +0 -32
  205. package/dist/chat/provider.d.ts +0 -63
  206. package/dist/chat/provider.d.ts.map +0 -1
  207. package/dist/chat/provider.js +0 -628
  208. package/dist/chat/question.d.ts +0 -9
  209. package/dist/chat/question.d.ts.map +0 -1
  210. package/dist/chat/question.js +0 -69
  211. package/dist/chat/questions.d.ts +0 -31
  212. package/dist/chat/questions.d.ts.map +0 -1
  213. package/dist/chat/questions.js +0 -111
  214. package/dist/chat/read.d.ts +0 -5
  215. package/dist/chat/read.d.ts.map +0 -1
  216. package/dist/chat/read.js +0 -13
  217. package/dist/chat/ready.d.ts +0 -34
  218. package/dist/chat/ready.d.ts.map +0 -1
  219. package/dist/chat/ready.js +0 -91
  220. package/dist/chat/style.d.ts +0 -4
  221. package/dist/chat/style.d.ts.map +0 -1
  222. package/dist/chat/style.js +0 -302
  223. package/dist/chat.d.ts +0 -14
  224. package/dist/chat.d.ts.map +0 -1
  225. package/dist/chat.js +0 -8
  226. package/dist/read-side.d.ts +0 -25
  227. package/dist/read-side.d.ts.map +0 -1
  228. package/dist/read-side.js +0 -767
  229. package/dist/reads.d.ts +0 -11
  230. package/dist/reads.d.ts.map +0 -1
  231. package/dist/reads.js +0 -98
package/dist/save.js CHANGED
@@ -1,10 +1,13 @@
1
1
  import { definitionOf } from './model.js';
2
- import { apply, ATTACHMENT, links, parse, pointerSegments, references, relative, relativize, retarget, serialize, target, } from '@agentshouse/mdmodel';
3
- import { immutable, queuedFile, departure, } from './change.js';
4
- import { available, changedCleanly, collectionOf, documentsOf, governing, referring, validateCollection, } from './collection.js';
2
+ import { apply, ARRAY_INDEX, ATTACHMENT, links, parse, pointerSegments, references, relative, relativize, retarget, roomReferences, serialize, target, } from '@agentshouse/mdmodel';
3
+ import { immutable, leaving, queuedFile, departure, } from './change.js';
4
+ import { collectionOf, documentsOf, governing, } from './collection.js';
5
5
  import { changeBatchIdentity } from './identity.js';
6
- import { overlay } from './overlay.js';
7
- import { covered, normalize } from './paths.js';
6
+ import { overlaying } from './overlay.js';
7
+ import { ancestors, covered, normalize } from './paths.js';
8
+ import { sorting } from './shared.js';
9
+ import { paced, ROW_WEIGHT, stepped } from './turn.js';
10
+ import { validatedCollection } from './validation.js';
8
11
  function refuse(code, file, location) {
9
12
  return {
10
13
  kind: 'refused',
@@ -20,7 +23,7 @@ function repointed(document, definition, from, to, moves, settled) {
20
23
  const parsed = parse(document.content, from);
21
24
  if (!parsed.ok)
22
25
  return [];
23
- let next = parsed.document;
26
+ const changes = [];
24
27
  const names = new Set();
25
28
  for (const { pointer, value } of definition === undefined
26
29
  ? []
@@ -32,38 +35,39 @@ function repointed(document, definition, from, to, moves, settled) {
32
35
  const destination = moves.get(named) ?? named;
33
36
  if (target(to, value) === destination)
34
37
  continue;
35
- const applied = apply(next, {
38
+ changes.push({
36
39
  kind: 'reference',
37
40
  pointer,
38
41
  value: retarget(value, relative(to, destination)),
39
42
  prior: value,
40
43
  });
41
- if (applied.outcome !== 'applied')
42
- continue;
43
- next = applied.document;
44
44
  names.add(name);
45
45
  }
46
+ if (changes.length === 0)
47
+ return [];
48
+ const applied = apply(parsed.document, changes);
49
+ if (applied.outcome !== 'applied')
50
+ return [];
51
+ const next = applied.document;
46
52
  return [...names].map((name) => ({ kind: 'field', path: from, name, value: next.fields[name] }));
47
53
  }
48
- function edited(document, path, changes) {
49
- let next = document;
54
+ function edited(document, changes) {
55
+ const parts = new Map();
50
56
  for (const change of changes) {
51
- if (change.path !== path)
52
- continue;
53
- if (change.kind !== 'section' && change.kind !== 'preamble')
54
- continue;
55
- const applied = apply(next, change.kind === 'preamble'
56
- ? { kind: 'preamble', value: change.value, prior: next.preamble }
57
- : {
57
+ if (change.kind === 'preamble')
58
+ parts.set('preamble', { kind: 'preamble', value: change.value, prior: document.preamble });
59
+ if (change.kind === 'section')
60
+ parts.set(`section ${change.name}`, {
58
61
  kind: 'section',
59
62
  name: change.name,
60
63
  value: change.value,
61
- prior: next.sections[change.name],
64
+ prior: document.sections[change.name] ?? '',
62
65
  });
63
- if (applied.outcome === 'applied')
64
- next = applied.document;
65
66
  }
66
- return next;
67
+ if (parts.size === 0)
68
+ return document;
69
+ const applied = apply(document, [...parts.values()]);
70
+ return applied.outcome === 'applied' ? applied.document : document;
67
71
  }
68
72
  function bodies(path, before, after) {
69
73
  const changes = [];
@@ -78,7 +82,7 @@ function bodies(path, before, after) {
78
82
  return changes;
79
83
  }
80
84
  function relinks(path, moved, before, moves) {
81
- let after = before;
85
+ const changes = [];
82
86
  for (const link of links(before, path)) {
83
87
  const destination = moves.get(link.target) ?? link.target;
84
88
  if (destination === link.target && moved === path)
@@ -86,20 +90,23 @@ function relinks(path, moved, before, moves) {
86
90
  const to = relative(moved, destination);
87
91
  if (to === relative(path, link.target))
88
92
  continue;
89
- const applied = apply(after, { kind: 'link', href: link.href, to });
90
- if (applied.outcome === 'refuse') {
91
- return {
92
- kind: 'refused',
93
- refusal: {
94
- code: applied.refusal.rule,
95
- retryable: false,
96
- file: path,
97
- ...(applied.refusal.location === undefined ? {} : { location: applied.refusal.location }),
98
- },
99
- };
100
- }
101
- after = applied.document;
93
+ changes.push({ kind: 'link', href: link.href, to });
102
94
  }
95
+ if (changes.length === 0)
96
+ return { kind: 'changes', changes: [] };
97
+ const applied = apply(before, changes);
98
+ if (applied.outcome === 'refuse') {
99
+ return {
100
+ kind: 'refused',
101
+ refusal: {
102
+ code: applied.refusal.rule,
103
+ retryable: false,
104
+ file: path,
105
+ ...(applied.refusal.location === undefined ? {} : { location: applied.refusal.location }),
106
+ },
107
+ };
108
+ }
109
+ const after = applied.document;
103
110
  if (after === before)
104
111
  return { kind: 'changes', changes: [] };
105
112
  if (before.type === undefined) {
@@ -107,19 +114,14 @@ function relinks(path, moved, before, moves) {
107
114
  }
108
115
  return { kind: 'changes', changes: bodies(path, before, after) };
109
116
  }
110
- function subsumed(merged, path) {
111
- for (let index = merged.length - 1; index >= 0; index -= 1) {
112
- const item = merged[index];
113
- if (item.path !== path)
114
- continue;
115
- if (item.kind === 'section' || item.kind === 'preamble')
116
- merged.splice(index, 1);
117
- }
117
+ function partKey(change) {
118
+ return change.kind === 'section' ? `section ${change.name}` : change.kind;
118
119
  }
119
- function rewrites(changes, visible) {
120
+ function* rewrites(changes, visible) {
120
121
  const moves = new Map();
121
122
  const settled = new Set();
122
- for (const change of changes) {
123
+ const parts = new Map();
124
+ for (const [index, change] of changes.entries()) {
123
125
  if (change.kind === 'move')
124
126
  moves.set(change.path, change.to);
125
127
  if (change.kind === 'remove' ||
@@ -130,54 +132,143 @@ function rewrites(changes, visible) {
130
132
  }
131
133
  if (change.kind === 'field')
132
134
  settled.add(`${change.path} ${change.name}`);
135
+ if (isPart(change)) {
136
+ const held = parts.get(change.path);
137
+ if (held === undefined)
138
+ parts.set(change.path, [index]);
139
+ else
140
+ held.push(index);
141
+ }
133
142
  }
134
143
  if (moves.size === 0)
135
144
  return { kind: 'changes', changes };
136
145
  const merged = [...changes];
137
146
  const added = [];
138
- for (const [path, document] of documentsOf(visible.collection)) {
147
+ for (const [path, document] of yield* documentsOf(visible.collection)) {
139
148
  if (settled.has(path))
140
149
  continue;
141
150
  const moved = moves.get(path) ?? path;
142
151
  const definition = definitionOf(governing(visible.collection.composition, path), document.type);
143
- added.push(...repointed(document, definition, path, moved, moves, settled));
144
- const relinked = relinks(path, moved, edited(document, path, merged), moves);
152
+ const repointing = repointed(document, definition, path, moved, moves, settled);
153
+ added.push(...repointing);
154
+ const own = parts.get(path) ?? [];
155
+ const relinked = relinks(path, moved, edited(document, own.map((index) => merged[index])), moves);
145
156
  if (relinked.kind === 'refused')
146
157
  return relinked;
158
+ const keyed = new Map();
159
+ for (const index of own) {
160
+ const key = partKey(merged[index]);
161
+ if (!keyed.has(key))
162
+ keyed.set(key, index);
163
+ }
147
164
  for (const change of relinked.changes) {
148
165
  if (change.kind === 'replace') {
149
- subsumed(merged, path);
166
+ for (const index of own) {
167
+ const kind = merged[index]?.kind;
168
+ if (kind === 'section' || kind === 'preamble')
169
+ merged[index] = null;
170
+ }
150
171
  added.push(change);
151
172
  continue;
152
173
  }
153
- const index = merged.findIndex((item) => item.kind === change.kind &&
154
- item.path === path &&
155
- (change.kind !== 'section' || (item.kind === 'section' && item.name === change.name)));
156
- if (index === -1)
174
+ const index = keyed.get(partKey(change));
175
+ if (index === undefined)
157
176
  added.push(change);
158
177
  else
159
178
  merged[index] = change;
160
179
  }
180
+ const text = visible.collection.text[path].length;
181
+ const rewritten = repointing.length > 0 || relinked.changes.length > 0;
182
+ yield ROW_WEIGHT + text + (rewritten ? text : 0);
161
183
  }
162
- return { kind: 'changes', changes: [...added, ...merged] };
184
+ return {
185
+ kind: 'changes',
186
+ changes: [...added, ...merged.filter((change) => change !== null)],
187
+ };
163
188
  }
164
- function anticipated(changes, preconditions, rewritten, visible) {
165
- const derived = rewritten.filter((change) => !changes.includes(change));
189
+ function* anticipated(changes, preconditions, rewritten, visible) {
190
+ const given = new Set(changes);
191
+ const derived = rewritten.filter((change) => !given.has(change));
166
192
  return {
167
193
  changes: rewritten,
168
- preconditions: [...preconditionsOf(derived, visible, []), ...preconditions],
194
+ preconditions: [...(yield* preconditionsOf(derived, visible, [])), ...preconditions],
169
195
  };
170
196
  }
171
- export function providerRewritten(batch, visible) {
172
- const rewritten = rewrites(batch.changes, visible);
197
+ export function* providerRewritten(batch, visible) {
198
+ const rewritten = yield* rewrites(batch.changes, visible);
173
199
  if (rewritten.kind === 'refused')
174
200
  return batch;
175
201
  return {
176
202
  ...batch,
177
- ...anticipated(batch.changes, batch.preconditions, rewritten.changes, visible),
203
+ ...(yield* anticipated(batch.changes, batch.preconditions, rewritten.changes, visible)),
178
204
  };
179
205
  }
180
- function incoming(collection, removed, candidates) {
206
+ function crossed(document, definition, path, to) {
207
+ const changes = [];
208
+ for (const link of links(document, path)) {
209
+ const written = to(link.target, link.href);
210
+ if (written !== undefined)
211
+ changes.push({ kind: 'link', href: link.href, to: written });
212
+ }
213
+ for (const { pointer, value } of definition === undefined
214
+ ? []
215
+ : references(document, definition)) {
216
+ const named = target(path, value);
217
+ const written = named === null ? undefined : to(named, value);
218
+ if (written !== undefined) {
219
+ changes.push({ kind: 'reference', pointer, value: retarget(value, written), prior: value });
220
+ }
221
+ }
222
+ return changes;
223
+ }
224
+ function rewritten(path, document, changes) {
225
+ if (changes.length === 0)
226
+ return [];
227
+ const applied = apply(document, changes);
228
+ if (applied.outcome !== 'applied')
229
+ return [];
230
+ const after = applied.document;
231
+ if (document.type === undefined)
232
+ return [{ kind: 'replace', path, content: serialize(after) }];
233
+ const names = new Set(changes.flatMap((change) => change.kind === 'reference' ? [pointerSegments(change.pointer)[0] ?? ''] : []));
234
+ return [
235
+ ...[...names].map((name) => ({ kind: 'field', path, name, value: after.fields[name] })),
236
+ ...bodies(path, document, after),
237
+ ];
238
+ }
239
+ function roomPathOf(handle, path) {
240
+ return `/rooms/${handle}/${path}`;
241
+ }
242
+ export function* leavingRewritten(batch, visible) {
243
+ const between = batch.between;
244
+ if (between === undefined)
245
+ return batch;
246
+ const moved = new Map(between.moves.map(({ path, to }) => [path, roomPathOf(between.destination.handle, to)]));
247
+ const derived = [];
248
+ for (const [path, document] of yield* documentsOf(visible.collection)) {
249
+ if (moved.has(path))
250
+ continue;
251
+ const definition = definitionOf(governing(visible.collection.composition, path), document.type);
252
+ derived.push(...rewritten(path, document, crossed(document, definition, path, (named) => moved.get(named))));
253
+ yield ROW_WEIGHT + visible.collection.text[path].length;
254
+ }
255
+ if (derived.length === 0)
256
+ return batch;
257
+ return {
258
+ ...batch,
259
+ changes: [...derived, ...batch.changes],
260
+ preconditions: [...(yield* preconditionsOf(derived, visible, [])), ...batch.preconditions],
261
+ };
262
+ }
263
+ export function rewriting(batch, locator) {
264
+ return leaving(batch, locator) || batch.changes.some((change) => change.kind === 'move');
265
+ }
266
+ export function* effectiveIn(batch, locator, visible) {
267
+ return leaving(batch, locator)
268
+ ? yield* leavingRewritten(batch, visible)
269
+ : yield* providerRewritten(batch, visible);
270
+ }
271
+ function* incoming(collection, removed, candidates) {
181
272
  const found = new Map();
182
273
  const note = (target, file) => {
183
274
  if (!removed.has(target))
@@ -186,13 +277,20 @@ function incoming(collection, removed, candidates) {
186
277
  referrers.add(file);
187
278
  found.set(target, referrers);
188
279
  };
189
- const read = candidates === null
190
- ? documentsOf(collection)
191
- : [...new Set(candidates)].sort().flatMap((path) => {
280
+ const read = function* (paths) {
281
+ const seen = new Set();
282
+ for (const path of paths) {
283
+ if (seen.has(path))
284
+ continue;
285
+ seen.add(path);
192
286
  const document = collection.document(path);
193
- return document === undefined ? [] : [[path, document]];
194
- });
195
- for (const [path, document] of read) {
287
+ if (document !== undefined)
288
+ yield [path, document];
289
+ }
290
+ };
291
+ for (const [path, document] of candidates === null
292
+ ? yield* documentsOf(collection)
293
+ : read(candidates)) {
196
294
  const definition = definitionOf(governing(collection.composition, path), document.type);
197
295
  for (const { value, to } of definition === undefined ? [] : references(document, definition)) {
198
296
  if (to.includes(ATTACHMENT))
@@ -201,8 +299,9 @@ function incoming(collection, removed, candidates) {
201
299
  if (named !== null)
202
300
  note(named, path);
203
301
  }
302
+ yield ROW_WEIGHT + collection.text[path].length;
204
303
  }
205
- const [first] = [...found.keys()].sort();
304
+ const [first] = yield* sorting(found.keys());
206
305
  if (first === undefined)
207
306
  return null;
208
307
  const every = new Set();
@@ -215,14 +314,14 @@ function incoming(collection, removed, candidates) {
215
314
  code: 'referenced-missing-file',
216
315
  retryable: false,
217
316
  file: first,
218
- referrers: [...every].sort(),
317
+ referrers: yield* sorting(every),
219
318
  },
220
319
  };
221
320
  }
222
321
  function affected(change) {
223
322
  return change.kind === 'move' ? [change.path, change.to] : [change.path];
224
323
  }
225
- function guard(changes, input) {
324
+ function guard(changes, input, removing) {
226
325
  const overridden = new Map();
227
326
  const existing = {
228
327
  has: (path) => overridden.get(path) ??
@@ -230,6 +329,7 @@ function guard(changes, input) {
230
329
  add: (path) => void overridden.set(path, true),
231
330
  delete: (path) => void overridden.set(path, false),
232
331
  };
332
+ const occupied = (path) => input.folders.has(path) ? path : (ancestors(path).find((one) => existing.has(one)) ?? null);
233
333
  for (const [index, change] of changes.entries()) {
234
334
  for (const path of affected(change)) {
235
335
  const normalized = normalize(path);
@@ -238,13 +338,14 @@ function guard(changes, input) {
238
338
  if (covered(input.providerOwnedPaths, normalized)) {
239
339
  return refuse('provider-owned-path', normalized);
240
340
  }
241
- const operation = change.kind === 'remove' ? 'delete' : 'write';
341
+ const operation = change.kind === 'remove' ? removing : 'write';
242
342
  if (!input.local && !covered(input.operations[operation], normalized))
243
343
  return refuse(`${operation}-grant`, normalized);
244
344
  }
245
- if (change.kind === 'create') {
246
- if (existing.has(change.path))
247
- return refuse('path-collision', change.path);
345
+ if (change.kind === 'create' || change.kind === 'arrival') {
346
+ const taken = existing.has(change.path) ? change.path : occupied(change.path);
347
+ if (taken !== null)
348
+ return refuse('path-collision', taken);
248
349
  existing.add(change.path);
249
350
  continue;
250
351
  }
@@ -254,6 +355,9 @@ function guard(changes, input) {
254
355
  input.bases.some((base) => base.kind === 'absent' && base.path === change.path))) {
255
356
  return refuse('path-collision', change.path);
256
357
  }
358
+ const taken = occupied(change.path);
359
+ if (taken !== null)
360
+ return refuse('path-collision', taken);
257
361
  if (!input.local &&
258
362
  existing.has(change.path) &&
259
363
  input.visible.revisions[departure(changes, index)] === undefined) {
@@ -265,8 +369,9 @@ function guard(changes, input) {
265
369
  if (change.kind === 'move') {
266
370
  if (!existing.has(change.path))
267
371
  return refuse('path-absent', change.path);
268
- if (existing.has(change.to))
269
- return refuse('path-collision', change.to);
372
+ const taken = existing.has(change.to) ? change.to : occupied(change.to);
373
+ if (taken !== null)
374
+ return refuse('path-collision', taken);
270
375
  if (unrevised(input, change.path)) {
271
376
  return refuse('unknown-revision', change.path);
272
377
  }
@@ -302,7 +407,7 @@ export function queuedDocuments(queue) {
302
407
  const paths = new Set();
303
408
  for (const batch of queue) {
304
409
  for (const change of batch.changes) {
305
- if (change.kind === 'binary')
410
+ if (change.kind === 'binary' || (change.kind === 'arrival' && !('content' in change)))
306
411
  continue;
307
412
  if (change.kind === 'remove')
308
413
  paths.delete(change.path);
@@ -331,7 +436,7 @@ function sameTarget(precondition, change) {
331
436
  return ((precondition.kind !== 'field' && precondition.kind !== 'section') ||
332
437
  ((change.kind === 'field' || change.kind === 'section') && precondition.name === change.name));
333
438
  }
334
- function preconditionsOf(changes, visible, bases) {
439
+ function* preconditionsOf(changes, visible, bases) {
335
440
  const preconditions = [];
336
441
  const seen = new Set();
337
442
  const add = (precondition) => {
@@ -342,6 +447,7 @@ function preconditionsOf(changes, visible, bases) {
342
447
  preconditions.push(precondition);
343
448
  };
344
449
  for (const change of changes) {
450
+ yield ROW_WEIGHT;
345
451
  if (change.kind === 'field' || change.kind === 'section' || change.kind === 'preamble') {
346
452
  const based = bases.find((base) => sameTarget(base, change));
347
453
  if (based !== undefined) {
@@ -389,10 +495,13 @@ function preconditionsOf(changes, visible, bases) {
389
495
  function depth(path) {
390
496
  return path.split('/').length;
391
497
  }
392
- function exceeded(limits, changes, effective, resulting) {
498
+ function* exceeding(limits, changes, effective, resulting) {
393
499
  const written = effective.map((change) => (change.kind === 'move' ? change.to : change.path));
394
500
  const created = changes
395
- .filter((change) => change.kind === 'create' || change.kind === 'move' || change.kind === 'binary')
501
+ .filter((change) => change.kind === 'create' ||
502
+ change.kind === 'move' ||
503
+ change.kind === 'binary' ||
504
+ change.kind === 'arrival')
396
505
  .map((change) => (change.kind === 'move' ? change.to : change.path));
397
506
  for (const limit of limits) {
398
507
  const value = limit.value;
@@ -406,9 +515,13 @@ function exceeded(limits, changes, effective, resulting) {
406
515
  return refuse('provider-limit', undefined, limit.name);
407
516
  }
408
517
  if (limit.name === 'document-size') {
409
- const large = written.find((path) => new TextEncoder().encode(resulting.documents[path] ?? '').byteLength > value);
410
- if (large !== undefined)
411
- return refuse('provider-limit', large, limit.name);
518
+ for (const path of written) {
519
+ const text = resulting.documents[path] ?? '';
520
+ if (new TextEncoder().encode(text).byteLength > value) {
521
+ return refuse('provider-limit', path, limit.name);
522
+ }
523
+ yield ROW_WEIGHT + text.length;
524
+ }
412
525
  }
413
526
  if (limit.name === 'path-length') {
414
527
  const long = created.find((path) => path.length > value);
@@ -438,7 +551,7 @@ function relativeField(path, name, value, handle, definition) {
438
551
  !opened.ok ||
439
552
  !JSON.stringify(value ?? null).includes(`/rooms/${handle}/`))
440
553
  return value;
441
- const held = apply(opened.document, { kind: 'field', name, value, prior: undefined });
554
+ const held = apply(opened.document, [{ kind: 'field', name, value, prior: undefined }]);
442
555
  if (held.outcome !== 'applied')
443
556
  return value;
444
557
  const result = relativize(held.document, path, handle, definition);
@@ -474,17 +587,6 @@ function relativeChanges(input) {
474
587
  return { ...change, fields, sections };
475
588
  });
476
589
  }
477
- function unmodeledType(collection, composition) {
478
- if (available(composition))
479
- return false;
480
- for (const [path, document] of documentsOf(collection)) {
481
- if (document.type !== undefined &&
482
- document.type !== 'any' &&
483
- governing(composition, path).status === 'unavailable')
484
- return true;
485
- }
486
- return false;
487
- }
488
590
  function unqueueable(file) {
489
591
  return { kind: 'refused', refusal: { code: 'unknown-revision', retryable: false, file } };
490
592
  }
@@ -494,29 +596,44 @@ function isPart(change) {
494
596
  function touched(change) {
495
597
  return change.kind === 'move' ? [change.path, change.to] : [change.path];
496
598
  }
497
- function partApplied(content, path, change) {
599
+ function partsApplied(content, path, changes) {
498
600
  const parsed = parse(content, path);
499
601
  if (!parsed.ok)
500
602
  return null;
501
603
  const document = parsed.document;
502
- const applied = apply(document, change.kind === 'field'
503
- ? {
504
- kind: 'field',
505
- name: change.name,
506
- value: change.value,
507
- prior: document.fields[change.name],
508
- }
509
- : change.kind === 'section'
510
- ? {
604
+ const parts = new Map();
605
+ for (const change of changes) {
606
+ if (change.kind === 'field')
607
+ parts.set(`field ${change.name}`, {
608
+ kind: 'field',
609
+ name: change.name,
610
+ value: change.value,
611
+ prior: document.fields[change.name],
612
+ });
613
+ else if (change.kind === 'section')
614
+ parts.set(`section ${change.name}`, {
511
615
  kind: 'section',
512
616
  name: change.name,
513
617
  value: change.value,
514
- prior: document.sections[change.name],
515
- }
516
- : { kind: 'preamble', value: change.value, prior: document.preamble });
618
+ prior: document.sections[change.name] ?? '',
619
+ });
620
+ else
621
+ parts.set('preamble', { kind: 'preamble', value: change.value, prior: document.preamble });
622
+ }
623
+ const applied = apply(document, [...parts.values()]);
517
624
  return applied.outcome === 'applied' ? serialize(applied.document) : null;
518
625
  }
519
- function folded(changes, change, appending) {
626
+ function settledParts(changes, deferred) {
627
+ for (const [held, parts] of deferred) {
628
+ const content = partsApplied(held.content, held.path, parts);
629
+ if (content === null)
630
+ return held.path;
631
+ changes[changes.indexOf(held)] = { ...held, content };
632
+ }
633
+ deferred.clear();
634
+ return null;
635
+ }
636
+ function folded(changes, change, appending, deferred) {
520
637
  const held = changes.find((item) => item.path === change.path && (item.kind === 'create' || item.kind === 'replace'));
521
638
  const whole = held === undefined ? -1 : changes.indexOf(held);
522
639
  const occupied = changes.some((item) => !isPart(item) && item !== held && touched(item).includes(change.path));
@@ -537,10 +654,11 @@ function folded(changes, change, appending) {
537
654
  return 'appended';
538
655
  }
539
656
  if ('content' in held) {
540
- const content = partApplied(held.content, held.path, change);
541
- if (content === null)
542
- return 'refused';
543
- changes[whole] = { ...held, content };
657
+ const parts = deferred.get(held);
658
+ if (parts === undefined)
659
+ deferred.set(held, [change]);
660
+ else
661
+ parts.push(change);
544
662
  return 'folded';
545
663
  }
546
664
  if (change.kind === 'preamble')
@@ -571,17 +689,21 @@ function folded(changes, change, appending) {
571
689
  changes.push(change);
572
690
  return 'appended';
573
691
  }
574
- function relinked(batch, move, before, after, appending) {
575
- const derived = rewrites([move], after);
692
+ function* relinked(batch, move, before, after, appending) {
693
+ const derived = yield* rewrites([move], after);
576
694
  if (derived.kind === 'refused')
577
695
  return derived;
578
696
  const changes = [...batch.changes];
579
697
  const appended = [];
580
698
  let changed = false;
699
+ const deferred = new Map();
581
700
  for (const change of derived.changes) {
582
701
  if (change === move)
583
702
  continue;
584
- const outcome = folded(changes, change, appending);
703
+ const unsettled = isPart(change) ? null : settledParts(changes, deferred);
704
+ if (unsettled !== null)
705
+ return unqueueable(unsettled);
706
+ const outcome = folded(changes, change, appending, deferred);
585
707
  if (outcome === 'refused')
586
708
  return unqueueable(change.path);
587
709
  if (outcome === 'appended')
@@ -589,9 +711,13 @@ function relinked(batch, move, before, after, appending) {
589
711
  if (outcome !== 'skipped')
590
712
  changed = true;
591
713
  }
714
+ const unsettled = settledParts(changes, deferred);
715
+ if (unsettled !== null)
716
+ return unqueueable(unsettled);
592
717
  if (!changed)
593
718
  return { kind: 'relinked', batch };
594
719
  const held = new Set(batch.preconditions.map(identity));
720
+ const added = yield* preconditionsOf(appended, before, []);
595
721
  return {
596
722
  kind: 'relinked',
597
723
  batch: immutable({
@@ -600,7 +726,7 @@ function relinked(batch, move, before, after, appending) {
600
726
  changes,
601
727
  preconditions: [
602
728
  ...batch.preconditions,
603
- ...preconditionsOf(appended, before, []).filter((precondition) => !held.has(identity(precondition))),
729
+ ...added.filter((precondition) => !held.has(identity(precondition))),
604
730
  ],
605
731
  }),
606
732
  };
@@ -646,7 +772,7 @@ function withdrawnFile(batch, change, at) {
646
772
  ];
647
773
  return immutable({ ...batch, id: changeBatchIdentity(), changes, preconditions });
648
774
  }
649
- export function requeued(queue, changes, visible, changeable, state) {
775
+ export function* requeuing(queue, changes, visible, changeable, state) {
650
776
  const next = [...queue];
651
777
  const origins = new Map();
652
778
  const withdrawn = new Map();
@@ -669,19 +795,19 @@ export function requeued(queue, changes, visible, changeable, state) {
669
795
  return unqueueable(change.path);
670
796
  const following = next.slice(index);
671
797
  const rebuilt = [];
672
- let before = change.kind === 'move' ? state(next.slice(0, index)) : visible;
798
+ let before = change.kind === 'move' ? yield* state(next.slice(0, index)) : visible;
673
799
  for (const [offset, batch] of following.entries()) {
674
800
  let current = batch;
675
801
  if (change.kind === 'move') {
676
- const after = state(next.slice(0, index + offset + 1));
802
+ const after = yield* state(next.slice(0, index + offset + 1));
677
803
  const appending = offset === 0 && created(batch, queuedFile(batch, change.path, held.localId));
678
- const relinking = relinked(batch, change, before, after, appending);
804
+ const relinking = yield* relinked(batch, change, before, after, appending);
679
805
  if (relinking.kind === 'refused')
680
806
  return relinking;
681
807
  current = relinking.batch;
682
808
  before = after;
683
809
  }
684
- if (current !== batch && !open(batch))
810
+ if (current !== batch && (!open(batch) || batch.between !== undefined))
685
811
  return unqueueable(change.path);
686
812
  rebuilt.push(offset === 0
687
813
  ? withdrawnFile(current, change, queuedFile(current, change.path, held.localId))
@@ -722,10 +848,17 @@ export function joined(queue, owner, extra) {
722
848
  if (clash !== undefined)
723
849
  return unqueueable(clash);
724
850
  const changes = [...owner.changes];
851
+ const deferred = new Map();
725
852
  for (const change of extra.changes) {
726
- if (folded(changes, change, true) === 'refused')
853
+ const unsettled = isPart(change) ? null : settledParts(changes, deferred);
854
+ if (unsettled !== null)
855
+ return unqueueable(unsettled);
856
+ if (folded(changes, change, true, deferred) === 'refused')
727
857
  return unqueueable(change.path);
728
858
  }
859
+ const unsettled = settledParts(changes, deferred);
860
+ if (unsettled !== null)
861
+ return unqueueable(unsettled);
729
862
  const held = new Set(owner.preconditions.map(covering));
730
863
  const written = new Set(owner.changes.flatMap((change) => change.kind === 'create' || change.kind === 'replace' ? [change.path] : []));
731
864
  const preconditions = [
@@ -744,62 +877,66 @@ export function joined(queue, owner, extra) {
744
877
  });
745
878
  return { kind: 'joined', batch, queue: queue.map((item) => (item === owner ? batch : item)) };
746
879
  }
747
- export function limitedIn(queue, batch, limits, state) {
880
+ export function* limiting(queue, batch, limits, state) {
748
881
  const at = queue.indexOf(batch);
749
- const limited = exceeded(limits, batch.changes, providerRewritten(batch, state(queue.slice(0, at))).changes, state(queue.slice(0, at + 1)));
882
+ const rewritten = yield* providerRewritten(batch, yield* state(queue.slice(0, at)));
883
+ const limited = yield* exceeding(limits, batch.changes, rewritten.changes, yield* state(queue.slice(0, at + 1)));
750
884
  return limited?.kind === 'refused' ? limited.refusal : null;
751
885
  }
752
- export function buildBatch(input) {
753
- const changes = relativeChanges(input);
754
- const structural = changes.find((change) => change.kind === 'move' || change.kind === 'remove');
755
- if (structural !== undefined && unmodeledType(input.visible.collection, input.composition))
756
- return refuse('model-unavailable', structural.path);
757
- if (changes.length === 0)
758
- return refuse('no-change');
759
- const rewritten = rewrites(changes, input.visible);
760
- if (rewritten.kind === 'refused')
761
- return rewritten;
762
- const refusal = guard(changes, input);
763
- if (refusal !== null)
764
- return refusal;
765
- const preconditions = preconditionsOf(changes, input.visible, input.bases);
766
- const predicted = anticipated(changes, preconditions, rewritten.changes, input.visible);
767
- const resulting = overlay(input.visible, [
768
- { id: '', baseRoomVersion: input.baseRoomVersion, ...predicted },
769
- ]);
770
- const clean = input.clean !== undefined &&
771
- input.clean.text === input.visible.documents &&
772
- input.clean.binaries === input.visible.binaries &&
773
- available(input.composition)
774
- ? input.clean
775
- : null;
776
- const removed = new Set(changes.filter((change) => change.kind === 'remove').map((change) => change.path));
886
+ function typedText(content) {
887
+ if (content === undefined)
888
+ return false;
889
+ const parsed = parse(content);
890
+ return parsed.ok && parsed.document.type !== undefined && parsed.document.type !== 'any';
891
+ }
892
+ function touchedLibrary(resulting, touched, composition) {
893
+ const entries = touched
894
+ .flatMap((path) => {
895
+ const content = resulting.documents[path];
896
+ const binary = resulting.binaries[path] !== undefined;
897
+ return content === undefined && !binary ? [] : [{ path, content: content ?? null }];
898
+ })
899
+ .sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
900
+ const contents = new Map(entries.map(({ path, content }) => [path, content]));
901
+ const binaries = new Set(Object.keys(resulting.binaries).filter((path) => resulting.binaries[path] !== undefined));
902
+ return validatedCollection(() => Promise.resolve({ entries, next: null }), async (paths) => {
903
+ const known = new Map();
904
+ const pace = paced();
905
+ for (const path of paths) {
906
+ const content = contents.get(path);
907
+ if (binaries.has(path))
908
+ known.set(path, { type: undefined, binary: true });
909
+ else if (content !== undefined && content !== null) {
910
+ const read = parse(content, path);
911
+ if (read.ok)
912
+ known.set(path, { type: read.document.type ?? 'any', binary: false });
913
+ await pace(content.length + ROW_WEIGHT);
914
+ }
915
+ }
916
+ return known;
917
+ }, composition);
918
+ }
919
+ async function judging(visible, resulting, composition, validate, removed, touched) {
777
920
  if (removed.size > 0) {
778
- const referrers = clean === null ? null : referring(clean, input.composition, removed);
779
- const referenced = incoming(collectionOf(resulting.documents, input.composition), removed, referrers === null ? null : [...referrers, ...predicted.changes.flatMap(affected)]);
921
+ const referenced = await stepped(incoming(collectionOf(resulting.documents, composition), removed, null));
780
922
  if (referenced !== null)
781
923
  return referenced;
782
924
  }
783
- const typed = (content) => {
784
- if (content === undefined)
785
- return false;
786
- const parsed = parse(content);
787
- return parsed.ok && parsed.document.type !== undefined && parsed.document.type !== 'any';
788
- };
789
- const touched = [...new Set(predicted.changes.flatMap(affected))];
790
- const typedPaths = touched.filter((path) => typed(input.visible.documents[path]) || typed(resulting.documents[path]));
791
- const needsModel = typedPaths.length > 0;
792
- const withoutModel = typedPaths.find((path) => governing(input.composition, path).status === 'unavailable');
925
+ const typedPaths = [];
926
+ const pace = paced();
927
+ for (const path of touched) {
928
+ const before = visible.documents[path];
929
+ const after = resulting.documents[path];
930
+ if (typedText(before) || typedText(after))
931
+ typedPaths.push(path);
932
+ await pace((before?.length ?? 0) + (after?.length ?? 0) + ROW_WEIGHT);
933
+ }
934
+ const withoutModel = typedPaths.find((path) => governing(composition, path).status === 'unavailable');
793
935
  if (withoutModel !== undefined)
794
936
  return refuse('model-unavailable', withoutModel);
795
- const verdict = clean !== null &&
796
- changedCleanly({ text: resulting.documents, binaries: resulting.binaries }, clean, input.composition)
797
- ? { verdict: 'accept' }
798
- : validateCollection(needsModel
799
- ? resulting.documents
800
- : Object.fromEntries(touched.flatMap((path) => resulting.documents[path] === undefined
801
- ? []
802
- : [[path, resulting.documents[path]]])), Object.keys(resulting.binaries), input.composition);
937
+ const verdict = typedPaths.length > 0
938
+ ? await validate(resulting, touched)
939
+ : await touchedLibrary(resulting, touched, composition);
803
940
  if (verdict.verdict === 'refuse') {
804
941
  return {
805
942
  kind: 'refused',
@@ -812,12 +949,92 @@ export function buildBatch(input) {
812
949
  },
813
950
  };
814
951
  }
815
- const limited = exceeded(input.providerLimits, changes, predicted.changes, resulting);
952
+ return {
953
+ kind: 'judged',
954
+ validated: typedPaths
955
+ .map((path) => governing(composition, path))
956
+ .find((model) => model.status !== 'unavailable'),
957
+ };
958
+ }
959
+ function placed(reference) {
960
+ const key = roomPathOf(reference.handle, reference.path);
961
+ if (reference.kind === 'link')
962
+ return `link ${key}`;
963
+ if (reference.kind === 'item')
964
+ return `section ${reference.section} ${key}`;
965
+ const pointer = reference.pointer
966
+ .split('/')
967
+ .map((segment) => (ARRAY_INDEX.test(segment) ? '-' : segment))
968
+ .join('/');
969
+ return `field ${pointer} ${key}`;
970
+ }
971
+ function roomReferencesIn(path, content, composition) {
972
+ if (content === undefined)
973
+ return [];
974
+ const parsed = parse(content, path);
975
+ if (!parsed.ok)
976
+ return [];
977
+ return roomReferences(path, content, definitionOf(governing(composition, path), parsed.document.type));
978
+ }
979
+ function* admittedRooms(changes, visible, resulting, composition) {
980
+ const from = new Map();
981
+ for (const [index, change] of changes.entries()) {
982
+ if (change.kind === 'move')
983
+ from.set(change.to, departure(changes, index));
984
+ }
985
+ for (const path of [...new Set(changes.flatMap(affected))].sort()) {
986
+ const now = resulting.documents[path];
987
+ if (now === undefined)
988
+ continue;
989
+ const held = new Set(roomReferencesIn(path, visible.documents[from.get(path) ?? path], composition).map(placed));
990
+ for (const reference of roomReferencesIn(path, now, composition)) {
991
+ if (reference.kind === 'link' || reference.crossRoom === true)
992
+ continue;
993
+ if (held.has(placed(reference)))
994
+ continue;
995
+ return refuse('unresolved-reference', path, reference.kind === 'field' ? reference.pointer : reference.section);
996
+ }
997
+ yield ROW_WEIGHT + now.length;
998
+ }
999
+ return null;
1000
+ }
1001
+ function validationOf(validated) {
1002
+ return validated === undefined
1003
+ ? {}
1004
+ : {
1005
+ validation: {
1006
+ identity: validated.snapshot.identity,
1007
+ version: validated.snapshot.version,
1008
+ source: validated.status,
1009
+ },
1010
+ };
1011
+ }
1012
+ async function batching(input) {
1013
+ const changes = relativeChanges(input);
1014
+ const structural = changes.find((change) => change.kind === 'move' || change.kind === 'remove');
1015
+ if (structural !== undefined && input.unmodeled) {
1016
+ return refuse('model-unavailable', structural.path);
1017
+ }
1018
+ if (changes.length === 0)
1019
+ return refuse('no-change');
1020
+ const rewritten = await stepped(rewrites(changes, input.visible));
1021
+ if (rewritten.kind === 'refused')
1022
+ return rewritten;
1023
+ const refusal = guard(changes, input, 'delete');
1024
+ if (refusal !== null)
1025
+ return refusal;
1026
+ const preconditions = await stepped(preconditionsOf(changes, input.visible, input.bases));
1027
+ const predicted = await stepped(anticipated(changes, preconditions, rewritten.changes, input.visible));
1028
+ const resulting = await stepped(overlaying(input.visible, [{ id: '', baseRoomVersion: input.baseRoomVersion, ...predicted }]));
1029
+ const judged = await judging(input.visible, resulting, input.composition, input.validate, new Set(changes.filter((change) => change.kind === 'remove').map((change) => change.path)), [...new Set(predicted.changes.flatMap(affected))]);
1030
+ if (judged.kind === 'refused')
1031
+ return judged;
1032
+ const crossing = await stepped(admittedRooms(changes, input.visible, resulting, input.composition));
1033
+ if (crossing !== null)
1034
+ return crossing;
1035
+ const limited = await stepped(exceeding(input.providerLimits, changes, predicted.changes, resulting));
816
1036
  if (limited !== null)
817
1037
  return limited;
818
- const validated = typedPaths
819
- .map((path) => governing(input.composition, path))
820
- .find((model) => model.status !== 'unavailable');
821
1038
  return {
822
1039
  kind: 'batch',
823
1040
  batch: immutable({
@@ -826,15 +1043,126 @@ export function buildBatch(input) {
826
1043
  changes,
827
1044
  preconditions,
828
1045
  ...(input.writtenBy === undefined ? {} : { writtenBy: input.writtenBy }),
829
- ...(validated === undefined
830
- ? {}
831
- : {
832
- validation: {
833
- identity: validated.snapshot.identity,
834
- version: validated.snapshot.version,
835
- source: validated.status,
836
- },
837
- }),
1046
+ ...validationOf(judged.validated),
838
1047
  }),
839
1048
  };
840
1049
  }
1050
+ export const MOVE_DOCUMENTS = 'move-documents';
1051
+ function heldAt(visible, path) {
1052
+ return visible.documents[path] !== undefined || visible.binaries[path] !== undefined;
1053
+ }
1054
+ function arrivingText(path, to, source, moved) {
1055
+ const content = source.visible.documents[path];
1056
+ const document = source.visible.collection.document(path);
1057
+ if (document === undefined)
1058
+ return content;
1059
+ const definition = definitionOf(governing(source.composition, path), document.type);
1060
+ const changes = crossed(document, definition, path, (named, href) => {
1061
+ const at = moved.get(named);
1062
+ if (at !== undefined)
1063
+ return target(to, href) === at ? undefined : relative(to, at);
1064
+ return heldAt(source.visible, named) ? roomPathOf(source.handle, named) : undefined;
1065
+ });
1066
+ if (changes.length === 0)
1067
+ return content;
1068
+ const applied = apply(document, changes);
1069
+ return applied.outcome === 'applied' ? serialize(applied.document) : content;
1070
+ }
1071
+ function arrivalOf({ path, to }, source, moved) {
1072
+ const { documents, revisions, fileIds, changes, binaries } = source.visible;
1073
+ const kept = {
1074
+ ...(fileIds[path] === undefined ? {} : { fileId: fileIds[path] }),
1075
+ ...(changes[path] === undefined ? {} : { change: changes[path] }),
1076
+ };
1077
+ const heads = {
1078
+ ...(revisions[path] === undefined ? {} : { revision: revisions[path] }),
1079
+ ...kept,
1080
+ };
1081
+ const binary = binaries[path];
1082
+ if (binary?.state === 'accepted') {
1083
+ const { byteLength, mediaType, sha256 } = binary;
1084
+ return {
1085
+ kind: 'arrival',
1086
+ path: to,
1087
+ binary: { byteLength, mediaType, sha256 },
1088
+ ...heads,
1089
+ };
1090
+ }
1091
+ const content = arrivingText(path, to, source, moved);
1092
+ return {
1093
+ kind: 'arrival',
1094
+ path: to,
1095
+ content,
1096
+ ...(content === documents[path] ? heads : kept),
1097
+ };
1098
+ }
1099
+ async function moving(input) {
1100
+ const { source, destination, moves } = input;
1101
+ if (moves.length === 0)
1102
+ return refuse('no-change');
1103
+ const removals = moves.map(({ path }) => ({ kind: 'remove', path }));
1104
+ const leaves = guard(removals, { ...source, local: input.local, bases: [] }, 'write');
1105
+ if (leaves !== null)
1106
+ return leaves;
1107
+ const uploading = moves.find(({ path }) => source.visible.binaries[path]?.state === 'pending');
1108
+ if (uploading !== undefined)
1109
+ return refuse('unknown-revision', uploading.path);
1110
+ const moved = new Map(moves.map(({ path, to }) => [path, to]));
1111
+ const arrivals = moves.map((move) => arrivalOf(move, source, moved));
1112
+ const arrives = guard(arrivals, { ...destination, local: input.local, bases: [] }, 'write');
1113
+ if (arrives !== null)
1114
+ return arrives;
1115
+ const bound = input.providerLimits.find((limit) => limit.name === MOVE_DOCUMENTS)?.value;
1116
+ if (typeof bound === 'number' && moves.length > bound) {
1117
+ return refuse('provider-limit', undefined, MOVE_DOCUMENTS);
1118
+ }
1119
+ if (source.unmodeled)
1120
+ return refuse('model-unavailable', moves[0].path);
1121
+ const id = changeBatchIdentity();
1122
+ const between = {
1123
+ source: { room: source.room, baseRoomVersion: source.baseRoomVersion },
1124
+ destination: {
1125
+ room: destination.room,
1126
+ baseRoomVersion: destination.baseRoomVersion,
1127
+ handle: destination.handle,
1128
+ },
1129
+ moves,
1130
+ };
1131
+ const written = input.writtenBy === undefined ? {} : { writtenBy: input.writtenBy };
1132
+ const leavingBatch = {
1133
+ id,
1134
+ baseRoomVersion: source.baseRoomVersion,
1135
+ changes: removals,
1136
+ preconditions: moves.map(({ path }) => existence(source.visible, path)),
1137
+ ...written,
1138
+ between,
1139
+ };
1140
+ const effective = await stepped(leavingRewritten(leavingBatch, source.visible));
1141
+ const left = await stepped(overlaying(source.visible, [effective]));
1142
+ const sourceJudged = await judging(source.visible, left, source.composition, source.validate, new Set(moves.map(({ path }) => path)), [...new Set(effective.changes.map((change) => change.path))]);
1143
+ if (sourceJudged.kind === 'refused')
1144
+ return sourceJudged;
1145
+ const arrivingBatch = {
1146
+ id,
1147
+ baseRoomVersion: destination.baseRoomVersion,
1148
+ changes: arrivals,
1149
+ preconditions: moves.map(({ to }) => ({ kind: 'absent', path: to })),
1150
+ ...written,
1151
+ between,
1152
+ };
1153
+ const arrived = await stepped(overlaying(destination.visible, [arrivingBatch]));
1154
+ const destinationJudged = await judging(destination.visible, arrived, destination.composition, destination.validate, new Set(), moves.map(({ to }) => to));
1155
+ if (destinationJudged.kind === 'refused')
1156
+ return destinationJudged;
1157
+ const limited = (await stepped(exceeding(input.providerLimits, removals, effective.changes, left))) ??
1158
+ (await stepped(exceeding(input.providerLimits, arrivals, arrivals, arrived)));
1159
+ if (limited !== null)
1160
+ return limited;
1161
+ return { kind: 'moved', leaving: immutable(leavingBatch), arriving: immutable(arrivingBatch) };
1162
+ }
1163
+ export function buildMove(input) {
1164
+ return moving(input);
1165
+ }
1166
+ export function buildBatch(input) {
1167
+ return batching(input);
1168
+ }