@agentshouse/mdruntime 0.1.0-c99e5271060cd → 0.1.0-c9b1e59e5baeb

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 (202) hide show
  1. package/AGENTS.md +274 -244
  2. package/README.md +18 -8
  3. package/dist/adapter.d.ts +98 -20
  4. package/dist/adapter.d.ts.map +1 -1
  5. package/dist/attempt.d.ts +16 -0
  6. package/dist/attempt.d.ts.map +1 -0
  7. package/dist/attempt.js +70 -0
  8. package/dist/binary.d.ts +23 -0
  9. package/dist/binary.d.ts.map +1 -0
  10. package/dist/binary.js +8 -0
  11. package/dist/bootstrap.d.ts +3 -2
  12. package/dist/bootstrap.d.ts.map +1 -1
  13. package/dist/bootstrap.js +38 -15
  14. package/dist/change-set.d.ts +7 -2
  15. package/dist/change-set.d.ts.map +1 -1
  16. package/dist/change-set.js +45 -13
  17. package/dist/change.d.ts +11 -0
  18. package/dist/change.d.ts.map +1 -1
  19. package/dist/change.js +26 -1
  20. package/dist/chat/acts.d.ts +33 -0
  21. package/dist/chat/acts.d.ts.map +1 -0
  22. package/dist/chat/acts.js +36 -0
  23. package/dist/chat/context.d.ts +3 -0
  24. package/dist/chat/context.d.ts.map +1 -0
  25. package/dist/chat/context.js +10 -0
  26. package/dist/chat/earlier.d.ts +10 -0
  27. package/dist/chat/earlier.d.ts.map +1 -0
  28. package/dist/chat/earlier.js +54 -0
  29. package/dist/chat/input.d.ts +9 -0
  30. package/dist/chat/input.d.ts.map +1 -0
  31. package/dist/chat/input.js +99 -0
  32. package/dist/chat/kit.d.ts +61 -0
  33. package/dist/chat/kit.d.ts.map +1 -0
  34. package/dist/chat/kit.js +123 -0
  35. package/dist/chat/lasting.d.ts +6 -0
  36. package/dist/chat/lasting.d.ts.map +1 -0
  37. package/dist/chat/lasting.js +19 -0
  38. package/dist/chat/list.d.ts +28 -0
  39. package/dist/chat/list.d.ts.map +1 -0
  40. package/dist/chat/list.js +99 -0
  41. package/dist/chat/markdown.d.ts +21 -0
  42. package/dist/chat/markdown.d.ts.map +1 -0
  43. package/dist/chat/markdown.js +246 -0
  44. package/dist/chat/message.d.ts +6 -0
  45. package/dist/chat/message.d.ts.map +1 -0
  46. package/dist/chat/message.js +199 -0
  47. package/dist/chat/messages.d.ts +22 -0
  48. package/dist/chat/messages.d.ts.map +1 -0
  49. package/dist/chat/messages.js +175 -0
  50. package/dist/chat/name.d.ts +6 -0
  51. package/dist/chat/name.d.ts.map +1 -0
  52. package/dist/chat/name.js +37 -0
  53. package/dist/chat/opened.d.ts +11 -0
  54. package/dist/chat/opened.d.ts.map +1 -0
  55. package/dist/chat/opened.js +17 -0
  56. package/dist/chat/overlay.d.ts +32 -0
  57. package/dist/chat/overlay.d.ts.map +1 -0
  58. package/dist/chat/overlay.js +107 -0
  59. package/dist/chat/parts.d.ts +57 -0
  60. package/dist/chat/parts.d.ts.map +1 -0
  61. package/dist/chat/parts.js +119 -0
  62. package/dist/chat/pieces.d.ts +62 -0
  63. package/dist/chat/pieces.d.ts.map +1 -0
  64. package/dist/chat/pieces.js +1110 -0
  65. package/dist/chat/process.d.ts +20 -0
  66. package/dist/chat/process.d.ts.map +1 -0
  67. package/dist/chat/process.js +32 -0
  68. package/dist/chat/provider.d.ts +64 -0
  69. package/dist/chat/provider.d.ts.map +1 -0
  70. package/dist/chat/provider.js +637 -0
  71. package/dist/chat/question.d.ts +9 -0
  72. package/dist/chat/question.d.ts.map +1 -0
  73. package/dist/chat/question.js +69 -0
  74. package/dist/chat/questions.d.ts +31 -0
  75. package/dist/chat/questions.d.ts.map +1 -0
  76. package/dist/chat/questions.js +111 -0
  77. package/dist/chat/read.d.ts +5 -0
  78. package/dist/chat/read.d.ts.map +1 -0
  79. package/dist/chat/read.js +13 -0
  80. package/dist/chat/ready.d.ts +34 -0
  81. package/dist/chat/ready.d.ts.map +1 -0
  82. package/dist/chat/ready.js +91 -0
  83. package/dist/chat/steady.d.ts +3 -0
  84. package/dist/chat/steady.d.ts.map +1 -0
  85. package/dist/chat/steady.js +26 -0
  86. package/dist/chat/style.d.ts +4 -0
  87. package/dist/chat/style.d.ts.map +1 -0
  88. package/dist/chat/style.js +302 -0
  89. package/dist/chat.d.ts +14 -0
  90. package/dist/chat.d.ts.map +1 -0
  91. package/dist/chat.js +8 -0
  92. package/dist/client.d.ts +26 -11
  93. package/dist/client.d.ts.map +1 -1
  94. package/dist/client.js +27 -4
  95. package/dist/collection.d.ts +20 -5
  96. package/dist/collection.d.ts.map +1 -1
  97. package/dist/collection.js +309 -33
  98. package/dist/connection.d.ts +41 -13
  99. package/dist/connection.d.ts.map +1 -1
  100. package/dist/connection.js +1819 -407
  101. package/dist/conversation.d.ts +34 -9
  102. package/dist/conversation.d.ts.map +1 -1
  103. package/dist/conversation.js +552 -168
  104. package/dist/durability.d.ts +7 -1
  105. package/dist/durability.d.ts.map +1 -1
  106. package/dist/entry.d.ts +17 -0
  107. package/dist/entry.d.ts.map +1 -0
  108. package/dist/entry.js +157 -0
  109. package/dist/expectation.d.ts +21 -0
  110. package/dist/expectation.d.ts.map +1 -0
  111. package/dist/expectation.js +70 -0
  112. package/dist/fence.d.ts +6 -0
  113. package/dist/fence.d.ts.map +1 -0
  114. package/dist/fence.js +10 -0
  115. package/dist/host.d.ts +15 -3
  116. package/dist/host.d.ts.map +1 -1
  117. package/dist/host.js +99 -13
  118. package/dist/identity.d.ts +4 -0
  119. package/dist/identity.d.ts.map +1 -1
  120. package/dist/identity.js +12 -0
  121. package/dist/index.d.ts +15 -8
  122. package/dist/index.d.ts.map +1 -1
  123. package/dist/index.js +5 -0
  124. package/dist/memory.d.ts +10 -4
  125. package/dist/memory.d.ts.map +1 -1
  126. package/dist/memory.js +64 -17
  127. package/dist/model.d.ts +4 -2
  128. package/dist/model.d.ts.map +1 -1
  129. package/dist/model.js +10 -1
  130. package/dist/observation.d.ts +7 -2
  131. package/dist/observation.d.ts.map +1 -1
  132. package/dist/observation.js +29 -12
  133. package/dist/outcome.d.ts +17 -1
  134. package/dist/outcome.d.ts.map +1 -1
  135. package/dist/outcome.js +10 -0
  136. package/dist/overlay.d.ts +18 -5
  137. package/dist/overlay.d.ts.map +1 -1
  138. package/dist/overlay.js +238 -160
  139. package/dist/query.d.ts +58 -4
  140. package/dist/query.d.ts.map +1 -1
  141. package/dist/query.js +36 -7
  142. package/dist/reach.d.ts +8 -3
  143. package/dist/reach.d.ts.map +1 -1
  144. package/dist/reach.js +55 -8
  145. package/dist/react.d.ts +10 -7
  146. package/dist/react.d.ts.map +1 -1
  147. package/dist/react.js +7 -17
  148. package/dist/read-side.d.ts +16 -7
  149. package/dist/read-side.d.ts.map +1 -1
  150. package/dist/read-side.js +495 -102
  151. package/dist/readiness.d.ts +53 -0
  152. package/dist/readiness.d.ts.map +1 -0
  153. package/dist/readiness.js +33 -0
  154. package/dist/reads.d.ts +11 -0
  155. package/dist/reads.d.ts.map +1 -0
  156. package/dist/reads.js +98 -0
  157. package/dist/recovery.d.ts +25 -0
  158. package/dist/recovery.d.ts.map +1 -0
  159. package/dist/recovery.js +197 -0
  160. package/dist/refresh.d.ts +4 -4
  161. package/dist/refresh.d.ts.map +1 -1
  162. package/dist/refresh.js +51 -30
  163. package/dist/replica.d.ts +15 -6
  164. package/dist/replica.d.ts.map +1 -1
  165. package/dist/replica.js +137 -66
  166. package/dist/room-draft.d.ts +32 -3
  167. package/dist/room-draft.d.ts.map +1 -1
  168. package/dist/room-draft.js +129 -12
  169. package/dist/rows.d.ts +8 -13
  170. package/dist/rows.d.ts.map +1 -1
  171. package/dist/rows.js +12 -13
  172. package/dist/save.d.ts +32 -2
  173. package/dist/save.d.ts.map +1 -1
  174. package/dist/save.js +495 -97
  175. package/dist/search.d.ts +49 -6
  176. package/dist/search.d.ts.map +1 -1
  177. package/dist/search.js +192 -47
  178. package/dist/seed.d.ts +16 -0
  179. package/dist/seed.d.ts.map +1 -0
  180. package/dist/seed.js +75 -0
  181. package/dist/shared.d.ts +15 -0
  182. package/dist/shared.d.ts.map +1 -0
  183. package/dist/shared.js +398 -0
  184. package/dist/status.d.ts +4 -1
  185. package/dist/status.d.ts.map +1 -1
  186. package/dist/status.js +0 -8
  187. package/dist/store.d.ts +71 -21
  188. package/dist/store.d.ts.map +1 -1
  189. package/dist/store.js +625 -320
  190. package/dist/turn.d.ts +9 -0
  191. package/dist/turn.d.ts.map +1 -0
  192. package/dist/turn.js +64 -0
  193. package/dist/windows.d.ts +26 -0
  194. package/dist/windows.d.ts.map +1 -0
  195. package/dist/windows.js +93 -0
  196. package/dist/wire.d.ts +153 -30
  197. package/dist/wire.d.ts.map +1 -1
  198. package/dist/wire.js +53 -29
  199. package/package.json +26 -14
  200. package/dist/attachment.d.ts +0 -3
  201. package/dist/attachment.d.ts.map +0 -1
  202. package/dist/attachment.js +0 -8
package/dist/save.js CHANGED
@@ -1,10 +1,12 @@
1
1
  import { definitionOf } from './model.js';
2
2
  import { apply, ATTACHMENT, links, parse, pointerSegments, references, relative, relativize, retarget, serialize, target, } from '@agentshouse/mdmodel';
3
- import { immutable, } from './change.js';
4
- import { collectionOf, documentsOf, governing, validateCollection, } from './collection.js';
3
+ import { immutable, queuedFile, departure, } from './change.js';
4
+ import { available, changedCleanly, collectionOf, documentsOf, governing, referring, validateCollection, } from './collection.js';
5
5
  import { changeBatchIdentity } from './identity.js';
6
- import { overlay } from './overlay.js';
6
+ import { overlaying } from './overlay.js';
7
7
  import { covered, normalize } from './paths.js';
8
+ import { differences } from './shared.js';
9
+ import { finished, ROW_WEIGHT, stepped } from './turn.js';
8
10
  function refuse(code, file, location) {
9
11
  return {
10
12
  kind: 'refused',
@@ -20,7 +22,7 @@ function repointed(document, definition, from, to, moves, settled) {
20
22
  const parsed = parse(document.content, from);
21
23
  if (!parsed.ok)
22
24
  return [];
23
- let next = parsed.document;
25
+ const changes = [];
24
26
  const names = new Set();
25
27
  for (const { pointer, value } of definition === undefined
26
28
  ? []
@@ -32,38 +34,39 @@ function repointed(document, definition, from, to, moves, settled) {
32
34
  const destination = moves.get(named) ?? named;
33
35
  if (target(to, value) === destination)
34
36
  continue;
35
- const applied = apply(next, {
37
+ changes.push({
36
38
  kind: 'reference',
37
39
  pointer,
38
40
  value: retarget(value, relative(to, destination)),
39
41
  prior: value,
40
42
  });
41
- if (applied.outcome !== 'applied')
42
- continue;
43
- next = applied.document;
44
43
  names.add(name);
45
44
  }
45
+ if (changes.length === 0)
46
+ return [];
47
+ const applied = apply(parsed.document, changes);
48
+ if (applied.outcome !== 'applied')
49
+ return [];
50
+ const next = applied.document;
46
51
  return [...names].map((name) => ({ kind: 'field', path: from, name, value: next.fields[name] }));
47
52
  }
48
- function edited(document, path, changes) {
49
- let next = document;
53
+ function edited(document, changes) {
54
+ const parts = new Map();
50
55
  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
- : {
56
+ if (change.kind === 'preamble')
57
+ parts.set('preamble', { kind: 'preamble', value: change.value, prior: document.preamble });
58
+ if (change.kind === 'section')
59
+ parts.set(`section ${change.name}`, {
58
60
  kind: 'section',
59
61
  name: change.name,
60
62
  value: change.value,
61
- prior: next.sections[change.name],
63
+ prior: document.sections[change.name] ?? '',
62
64
  });
63
- if (applied.outcome === 'applied')
64
- next = applied.document;
65
65
  }
66
- return next;
66
+ if (parts.size === 0)
67
+ return document;
68
+ const applied = apply(document, [...parts.values()]);
69
+ return applied.outcome === 'applied' ? applied.document : document;
67
70
  }
68
71
  function bodies(path, before, after) {
69
72
  const changes = [];
@@ -78,7 +81,7 @@ function bodies(path, before, after) {
78
81
  return changes;
79
82
  }
80
83
  function relinks(path, moved, before, moves) {
81
- let after = before;
84
+ const changes = [];
82
85
  for (const link of links(before, path)) {
83
86
  const destination = moves.get(link.target) ?? link.target;
84
87
  if (destination === link.target && moved === path)
@@ -86,20 +89,23 @@ function relinks(path, moved, before, moves) {
86
89
  const to = relative(moved, destination);
87
90
  if (to === relative(path, link.target))
88
91
  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;
92
+ changes.push({ kind: 'link', href: link.href, to });
93
+ }
94
+ if (changes.length === 0)
95
+ return { kind: 'changes', changes: [] };
96
+ const applied = apply(before, changes);
97
+ if (applied.outcome === 'refuse') {
98
+ return {
99
+ kind: 'refused',
100
+ refusal: {
101
+ code: applied.refusal.rule,
102
+ retryable: false,
103
+ file: path,
104
+ ...(applied.refusal.location === undefined ? {} : { location: applied.refusal.location }),
105
+ },
106
+ };
102
107
  }
108
+ const after = applied.document;
103
109
  if (after === before)
104
110
  return { kind: 'changes', changes: [] };
105
111
  if (before.type === undefined) {
@@ -107,26 +113,31 @@ function relinks(path, moved, before, moves) {
107
113
  }
108
114
  return { kind: 'changes', changes: bodies(path, before, after) };
109
115
  }
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
- }
116
+ function partKey(change) {
117
+ return change.kind === 'section' ? `section ${change.name}` : change.kind;
118
118
  }
119
- function rewrites(changes, visible) {
119
+ function* rewrites(changes, visible) {
120
120
  const moves = new Map();
121
121
  const settled = new Set();
122
- for (const change of changes) {
122
+ const parts = new Map();
123
+ for (const [index, change] of changes.entries()) {
123
124
  if (change.kind === 'move')
124
125
  moves.set(change.path, change.to);
125
- if (change.kind === 'remove' || change.kind === 'replace' || change.kind === 'create') {
126
+ if (change.kind === 'remove' ||
127
+ change.kind === 'replace' ||
128
+ change.kind === 'create' ||
129
+ change.kind === 'binary') {
126
130
  settled.add(change.path);
127
131
  }
128
132
  if (change.kind === 'field')
129
133
  settled.add(`${change.path} ${change.name}`);
134
+ if (isPart(change)) {
135
+ const held = parts.get(change.path);
136
+ if (held === undefined)
137
+ parts.set(change.path, [index]);
138
+ else
139
+ held.push(index);
140
+ }
130
141
  }
131
142
  if (moves.size === 0)
132
143
  return { kind: 'changes', changes };
@@ -138,43 +149,57 @@ function rewrites(changes, visible) {
138
149
  const moved = moves.get(path) ?? path;
139
150
  const definition = definitionOf(governing(visible.collection.composition, path), document.type);
140
151
  added.push(...repointed(document, definition, path, moved, moves, settled));
141
- const relinked = relinks(path, moved, edited(document, path, merged), moves);
152
+ const own = parts.get(path) ?? [];
153
+ const relinked = relinks(path, moved, edited(document, own.map((index) => merged[index])), moves);
142
154
  if (relinked.kind === 'refused')
143
155
  return relinked;
156
+ const keyed = new Map();
157
+ for (const index of own) {
158
+ const key = partKey(merged[index]);
159
+ if (!keyed.has(key))
160
+ keyed.set(key, index);
161
+ }
144
162
  for (const change of relinked.changes) {
145
163
  if (change.kind === 'replace') {
146
- subsumed(merged, path);
164
+ for (const index of own) {
165
+ const kind = merged[index]?.kind;
166
+ if (kind === 'section' || kind === 'preamble')
167
+ merged[index] = null;
168
+ }
147
169
  added.push(change);
148
170
  continue;
149
171
  }
150
- const index = merged.findIndex((item) => item.kind === change.kind &&
151
- item.path === path &&
152
- (change.kind !== 'section' || (item.kind === 'section' && item.name === change.name)));
153
- if (index === -1)
172
+ const index = keyed.get(partKey(change));
173
+ if (index === undefined)
154
174
  added.push(change);
155
175
  else
156
176
  merged[index] = change;
157
177
  }
178
+ yield ROW_WEIGHT + visible.collection.text[path].length;
158
179
  }
159
- return { kind: 'changes', changes: [...added, ...merged] };
180
+ return {
181
+ kind: 'changes',
182
+ changes: [...added, ...merged.filter((change) => change !== null)],
183
+ };
160
184
  }
161
- function anticipated(changes, preconditions, rewritten, visible) {
162
- const derived = rewritten.filter((change) => !changes.includes(change));
185
+ function* anticipated(changes, preconditions, rewritten, visible) {
186
+ const given = new Set(changes);
187
+ const derived = rewritten.filter((change) => !given.has(change));
163
188
  return {
164
189
  changes: rewritten,
165
- preconditions: [...preconditionsOf(derived, visible, []), ...preconditions],
190
+ preconditions: [...(yield* preconditionsOf(derived, visible, [])), ...preconditions],
166
191
  };
167
192
  }
168
- export function providerRewritten(batch, visible) {
169
- const rewritten = rewrites(batch.changes, visible);
193
+ export function* providerRewritten(batch, visible) {
194
+ const rewritten = yield* rewrites(batch.changes, visible);
170
195
  if (rewritten.kind === 'refused')
171
196
  return batch;
172
197
  return {
173
198
  ...batch,
174
- ...anticipated(batch.changes, batch.preconditions, rewritten.changes, visible),
199
+ ...(yield* anticipated(batch.changes, batch.preconditions, rewritten.changes, visible)),
175
200
  };
176
201
  }
177
- function incoming(collection, removed) {
202
+ function* incoming(collection, removed, candidates) {
178
203
  const found = new Map();
179
204
  const note = (target, file) => {
180
205
  if (!removed.has(target))
@@ -183,7 +208,14 @@ function incoming(collection, removed) {
183
208
  referrers.add(file);
184
209
  found.set(target, referrers);
185
210
  };
186
- for (const [path, document] of documentsOf(collection)) {
211
+ const read = function* (paths) {
212
+ for (const path of [...new Set(paths)].sort()) {
213
+ const document = collection.document(path);
214
+ if (document !== undefined)
215
+ yield [path, document];
216
+ }
217
+ };
218
+ for (const [path, document] of candidates === null ? documentsOf(collection) : read(candidates)) {
187
219
  const definition = definitionOf(governing(collection.composition, path), document.type);
188
220
  for (const { value, to } of definition === undefined ? [] : references(document, definition)) {
189
221
  if (to.includes(ATTACHMENT))
@@ -192,6 +224,7 @@ function incoming(collection, removed) {
192
224
  if (named !== null)
193
225
  note(named, path);
194
226
  }
227
+ yield ROW_WEIGHT + collection.text[path].length;
195
228
  }
196
229
  const [first] = [...found.keys()].sort();
197
230
  if (first === undefined)
@@ -214,11 +247,14 @@ function affected(change) {
214
247
  return change.kind === 'move' ? [change.path, change.to] : [change.path];
215
248
  }
216
249
  function guard(changes, input) {
217
- const existing = new Set([
218
- ...Object.keys(input.visible.documents),
219
- ...input.visible.binaryTargets,
220
- ]);
221
- for (const change of changes) {
250
+ const overridden = new Map();
251
+ const existing = {
252
+ has: (path) => overridden.get(path) ??
253
+ (Object.hasOwn(input.visible.documents, path) || Object.hasOwn(input.visible.binaries, path)),
254
+ add: (path) => void overridden.set(path, true),
255
+ delete: (path) => void overridden.set(path, false),
256
+ };
257
+ for (const [index, change] of changes.entries()) {
222
258
  for (const path of affected(change)) {
223
259
  const normalized = normalize(path);
224
260
  if (normalized === null || normalized === '')
@@ -236,12 +272,26 @@ function guard(changes, input) {
236
272
  existing.add(change.path);
237
273
  continue;
238
274
  }
275
+ if (change.kind === 'binary') {
276
+ if (input.visible.documents[change.path] !== undefined ||
277
+ (existing.has(change.path) &&
278
+ input.bases.some((base) => base.kind === 'absent' && base.path === change.path))) {
279
+ return refuse('path-collision', change.path);
280
+ }
281
+ if (!input.local &&
282
+ existing.has(change.path) &&
283
+ input.visible.revisions[departure(changes, index)] === undefined) {
284
+ return refuse('unknown-revision', change.path);
285
+ }
286
+ existing.add(change.path);
287
+ continue;
288
+ }
239
289
  if (change.kind === 'move') {
240
290
  if (!existing.has(change.path))
241
291
  return refuse('path-absent', change.path);
242
292
  if (existing.has(change.to))
243
293
  return refuse('path-collision', change.to);
244
- if (!input.local && unrevisionedDocument(input.visible, change.path)) {
294
+ if (unrevised(input, change.path)) {
245
295
  return refuse('unknown-revision', change.path);
246
296
  }
247
297
  existing.delete(change.path);
@@ -251,14 +301,14 @@ function guard(changes, input) {
251
301
  if (!existing.has(change.path))
252
302
  return refuse('path-absent', change.path);
253
303
  if (change.kind === 'remove') {
254
- if (!input.local && unrevisionedDocument(input.visible, change.path)) {
304
+ if (unrevised(input, change.path)) {
255
305
  return refuse('unknown-revision', change.path);
256
306
  }
257
307
  existing.delete(change.path);
258
308
  continue;
259
309
  }
260
310
  if (change.kind === 'replace') {
261
- if (!input.local && input.visible.revisions[change.path] === undefined) {
311
+ if (unrevised(input, change.path)) {
262
312
  return refuse('unknown-revision', change.path);
263
313
  }
264
314
  continue;
@@ -269,8 +319,28 @@ function guard(changes, input) {
269
319
  }
270
320
  return null;
271
321
  }
272
- function unrevisionedDocument(visible, path) {
273
- return visible.revisions[path] === undefined && !visible.binaryTargets.includes(path);
322
+ function unrevised(input, path) {
323
+ return (!input.local && input.queued?.has(path) !== true && input.visible.revisions[path] === undefined);
324
+ }
325
+ export function queuedDocuments(queue) {
326
+ const paths = new Set();
327
+ for (const batch of queue) {
328
+ for (const change of batch.changes) {
329
+ if (change.kind === 'binary')
330
+ continue;
331
+ if (change.kind === 'remove')
332
+ paths.delete(change.path);
333
+ else if (change.kind !== 'move')
334
+ paths.add(change.path);
335
+ else if (paths.delete(change.path))
336
+ paths.add(change.to);
337
+ }
338
+ }
339
+ return paths;
340
+ }
341
+ function identity(precondition) {
342
+ const part = precondition.kind === 'field' || precondition.kind === 'section' ? precondition.name : '';
343
+ return `${precondition.kind} ${precondition.path} ${part}`;
274
344
  }
275
345
  function existence(visible, path) {
276
346
  const revision = visible.revisions[path];
@@ -285,18 +355,18 @@ function sameTarget(precondition, change) {
285
355
  return ((precondition.kind !== 'field' && precondition.kind !== 'section') ||
286
356
  ((change.kind === 'field' || change.kind === 'section') && precondition.name === change.name));
287
357
  }
288
- function preconditionsOf(changes, visible, bases) {
358
+ function* preconditionsOf(changes, visible, bases) {
289
359
  const preconditions = [];
290
360
  const seen = new Set();
291
361
  const add = (precondition) => {
292
- const part = precondition.kind === 'field' || precondition.kind === 'section' ? precondition.name : '';
293
- const identity = `${precondition.kind} ${precondition.path} ${part}`;
294
- if (seen.has(identity))
362
+ const held = identity(precondition);
363
+ if (seen.has(held))
295
364
  return;
296
- seen.add(identity);
365
+ seen.add(held);
297
366
  preconditions.push(precondition);
298
367
  };
299
368
  for (const change of changes) {
369
+ yield ROW_WEIGHT;
300
370
  if (change.kind === 'field' || change.kind === 'section' || change.kind === 'preamble') {
301
371
  const based = bases.find((base) => sameTarget(base, change));
302
372
  if (based !== undefined) {
@@ -328,6 +398,13 @@ function preconditionsOf(changes, visible, bases) {
328
398
  add({ kind: 'absent', path: change.path });
329
399
  continue;
330
400
  }
401
+ if (change.kind === 'binary') {
402
+ const revision = visible.revisions[change.path];
403
+ add(visible.binaries[change.path] === undefined || revision === undefined
404
+ ? { kind: 'absent', path: change.path }
405
+ : { kind: 'revision', path: change.path, revision });
406
+ continue;
407
+ }
331
408
  add(existence(visible, change.path));
332
409
  if (change.kind === 'move')
333
410
  add({ kind: 'absent', path: change.to });
@@ -337,10 +414,10 @@ function preconditionsOf(changes, visible, bases) {
337
414
  function depth(path) {
338
415
  return path.split('/').length;
339
416
  }
340
- function exceeded(limits, changes, resulting) {
341
- const written = changes.map((change) => (change.kind === 'move' ? change.to : change.path));
417
+ function* exceeding(limits, changes, effective, resulting) {
418
+ const written = effective.map((change) => (change.kind === 'move' ? change.to : change.path));
342
419
  const created = changes
343
- .filter((change) => change.kind === 'create' || change.kind === 'move')
420
+ .filter((change) => change.kind === 'create' || change.kind === 'move' || change.kind === 'binary')
344
421
  .map((change) => (change.kind === 'move' ? change.to : change.path));
345
422
  for (const limit of limits) {
346
423
  const value = limit.value;
@@ -354,9 +431,13 @@ function exceeded(limits, changes, resulting) {
354
431
  return refuse('provider-limit', undefined, limit.name);
355
432
  }
356
433
  if (limit.name === 'document-size') {
357
- const large = written.find((path) => (resulting.documents[path] ?? '').length > value);
358
- if (large !== undefined)
359
- return refuse('provider-limit', large, limit.name);
434
+ for (const path of written) {
435
+ const text = resulting.documents[path] ?? '';
436
+ if (new TextEncoder().encode(text).byteLength > value) {
437
+ return refuse('provider-limit', path, limit.name);
438
+ }
439
+ yield ROW_WEIGHT + text.length;
440
+ }
360
441
  }
361
442
  if (limit.name === 'path-length') {
362
443
  const long = created.find((path) => path.length > value);
@@ -386,7 +467,7 @@ function relativeField(path, name, value, handle, definition) {
386
467
  !opened.ok ||
387
468
  !JSON.stringify(value ?? null).includes(`/rooms/${handle}/`))
388
469
  return value;
389
- const held = apply(opened.document, { kind: 'field', name, value, prior: undefined });
470
+ const held = apply(opened.document, [{ kind: 'field', name, value, prior: undefined }]);
390
471
  if (held.outcome !== 'applied')
391
472
  return value;
392
473
  const result = relativize(held.document, path, handle, definition);
@@ -422,36 +503,338 @@ function relativeChanges(input) {
422
503
  return { ...change, fields, sections };
423
504
  });
424
505
  }
425
- function unmodeledType(collection, composition) {
506
+ function* unmodeledType(collection, composition) {
507
+ if (available(composition))
508
+ return false;
426
509
  for (const [path, document] of documentsOf(collection)) {
427
510
  if (document.type !== undefined &&
428
511
  document.type !== 'any' &&
429
512
  governing(composition, path).status === 'unavailable')
430
513
  return true;
514
+ yield ROW_WEIGHT + collection.text[path].length;
431
515
  }
432
516
  return false;
433
517
  }
434
- export function buildBatch(input) {
518
+ function unqueueable(file) {
519
+ return { kind: 'refused', refusal: { code: 'unknown-revision', retryable: false, file } };
520
+ }
521
+ function isPart(change) {
522
+ return change.kind === 'field' || change.kind === 'section' || change.kind === 'preamble';
523
+ }
524
+ function touched(change) {
525
+ return change.kind === 'move' ? [change.path, change.to] : [change.path];
526
+ }
527
+ function partsApplied(content, path, changes) {
528
+ const parsed = parse(content, path);
529
+ if (!parsed.ok)
530
+ return null;
531
+ const document = parsed.document;
532
+ const parts = new Map();
533
+ for (const change of changes) {
534
+ if (change.kind === 'field')
535
+ parts.set(`field ${change.name}`, {
536
+ kind: 'field',
537
+ name: change.name,
538
+ value: change.value,
539
+ prior: document.fields[change.name],
540
+ });
541
+ else if (change.kind === 'section')
542
+ parts.set(`section ${change.name}`, {
543
+ kind: 'section',
544
+ name: change.name,
545
+ value: change.value,
546
+ prior: document.sections[change.name] ?? '',
547
+ });
548
+ else
549
+ parts.set('preamble', { kind: 'preamble', value: change.value, prior: document.preamble });
550
+ }
551
+ const applied = apply(document, [...parts.values()]);
552
+ return applied.outcome === 'applied' ? serialize(applied.document) : null;
553
+ }
554
+ function settledParts(changes, deferred) {
555
+ for (const [held, parts] of deferred) {
556
+ const content = partsApplied(held.content, held.path, parts);
557
+ if (content === null)
558
+ return held.path;
559
+ changes[changes.indexOf(held)] = { ...held, content };
560
+ }
561
+ deferred.clear();
562
+ return null;
563
+ }
564
+ function folded(changes, change, appending, deferred) {
565
+ const held = changes.find((item) => item.path === change.path && (item.kind === 'create' || item.kind === 'replace'));
566
+ const whole = held === undefined ? -1 : changes.indexOf(held);
567
+ const occupied = changes.some((item) => !isPart(item) && item !== held && touched(item).includes(change.path));
568
+ if (isPart(change)) {
569
+ const same = changes.findIndex((item) => item.kind === change.kind &&
570
+ item.path === change.path &&
571
+ (change.kind === 'preamble' || ('name' in item && item.name === change.name)));
572
+ if (same !== -1) {
573
+ changes[same] = change;
574
+ return 'folded';
575
+ }
576
+ if (held === undefined) {
577
+ if (!appending)
578
+ return 'skipped';
579
+ if (occupied)
580
+ return 'refused';
581
+ changes.push(change);
582
+ return 'appended';
583
+ }
584
+ if ('content' in held) {
585
+ const parts = deferred.get(held);
586
+ if (parts === undefined)
587
+ deferred.set(held, [change]);
588
+ else
589
+ parts.push(change);
590
+ return 'folded';
591
+ }
592
+ if (change.kind === 'preamble')
593
+ return 'refused';
594
+ changes[whole] =
595
+ change.kind === 'field'
596
+ ? { ...held, fields: { ...held.fields, [change.name]: change.value } }
597
+ : { ...held, sections: { ...held.sections, [change.name]: change.value } };
598
+ return 'folded';
599
+ }
600
+ if (change.kind === 'replace') {
601
+ if (held !== undefined) {
602
+ if (!('content' in held))
603
+ return 'refused';
604
+ changes[whole] = { ...held, content: change.content };
605
+ return 'folded';
606
+ }
607
+ const parts = changes.filter((item) => isPart(item) && item.path === change.path);
608
+ if (parts.length === 0 && !appending)
609
+ return 'skipped';
610
+ if (occupied)
611
+ return 'refused';
612
+ changes.splice(0, changes.length, ...changes.filter((item) => !parts.includes(item)), change);
613
+ return 'appended';
614
+ }
615
+ if (changes.some((item) => touched(change).some((path) => touched(item).includes(path))))
616
+ return 'refused';
617
+ changes.push(change);
618
+ return 'appended';
619
+ }
620
+ function relinked(batch, move, before, after, appending) {
621
+ const derived = finished(rewrites([move], after));
622
+ if (derived.kind === 'refused')
623
+ return derived;
624
+ const changes = [...batch.changes];
625
+ const appended = [];
626
+ let changed = false;
627
+ const deferred = new Map();
628
+ for (const change of derived.changes) {
629
+ if (change === move)
630
+ continue;
631
+ const unsettled = isPart(change) ? null : settledParts(changes, deferred);
632
+ if (unsettled !== null)
633
+ return unqueueable(unsettled);
634
+ const outcome = folded(changes, change, appending, deferred);
635
+ if (outcome === 'refused')
636
+ return unqueueable(change.path);
637
+ if (outcome === 'appended')
638
+ appended.push(change);
639
+ if (outcome !== 'skipped')
640
+ changed = true;
641
+ }
642
+ const unsettled = settledParts(changes, deferred);
643
+ if (unsettled !== null)
644
+ return unqueueable(unsettled);
645
+ if (!changed)
646
+ return { kind: 'relinked', batch };
647
+ const held = new Set(batch.preconditions.map(identity));
648
+ return {
649
+ kind: 'relinked',
650
+ batch: immutable({
651
+ ...batch,
652
+ id: changeBatchIdentity(),
653
+ changes,
654
+ preconditions: [
655
+ ...batch.preconditions,
656
+ ...finished(preconditionsOf(appended, before, [])).filter((precondition) => !held.has(identity(precondition))),
657
+ ],
658
+ }),
659
+ };
660
+ }
661
+ function leading(changes, at) {
662
+ const from = departure(changes, at);
663
+ return new Set(changes.filter((change, index) => index < at && change.kind === 'move' && departure(changes, index) === from));
664
+ }
665
+ function created(batch, at) {
666
+ const file = batch.changes[at];
667
+ return (leading(batch.changes, at).size === 0 &&
668
+ batch.preconditions.some((precondition) => precondition.kind === 'absent' && precondition.path === file.path));
669
+ }
670
+ function withdrawnFile(batch, change, at) {
671
+ const file = batch.changes[at];
672
+ const from = departure(batch.changes, at);
673
+ const moves = leading(batch.changes, at);
674
+ const fresh = created(batch, at);
675
+ const vacated = new Set([...moves].map((move) => move.to));
676
+ if (fresh)
677
+ vacated.add(file.path);
678
+ const replaced = fresh
679
+ ? change.kind === 'remove'
680
+ ? []
681
+ : [{ ...file, path: change.to }]
682
+ : change.kind === 'remove'
683
+ ? [{ kind: 'remove', path: from }]
684
+ : change.to === from
685
+ ? [{ ...file, path: from }]
686
+ : [
687
+ { kind: 'move', path: from, to: change.to },
688
+ { ...file, path: change.to },
689
+ ];
690
+ const added = change.kind === 'move' && (fresh || change.to !== from)
691
+ ? [{ kind: 'absent', path: change.to }]
692
+ : [];
693
+ const changes = batch.changes.flatMap((item) => item === file ? replaced : item.kind === 'move' && moves.has(item) ? [] : [item]);
694
+ if (changes.length === 0)
695
+ return null;
696
+ const preconditions = [
697
+ ...added,
698
+ ...batch.preconditions.filter((precondition) => precondition.kind !== 'absent' || !vacated.has(precondition.path)),
699
+ ];
700
+ return immutable({ ...batch, id: changeBatchIdentity(), changes, preconditions });
701
+ }
702
+ export function requeued(queue, changes, visible, changeable, state) {
703
+ const next = [...queue];
704
+ const origins = new Map();
705
+ const withdrawn = new Map();
706
+ const owners = new Set();
707
+ const queued = new Set();
708
+ const kept = [];
709
+ const originOf = (batch) => origins.get(batch) ?? batch.id;
710
+ const open = (batch) => origins.has(batch) || changeable(batch);
711
+ for (const change of changes) {
712
+ const held = visible.binaries[change.path];
713
+ if ((change.kind !== 'move' && change.kind !== 'remove') || held?.state !== 'pending') {
714
+ kept.push(change);
715
+ continue;
716
+ }
717
+ let index = next.length - 1;
718
+ while (index >= 0 && queuedFile(next[index], change.path, held.localId) === -1)
719
+ index -= 1;
720
+ const owner = next[index];
721
+ if (owner === undefined || !open(owner))
722
+ return unqueueable(change.path);
723
+ const following = next.slice(index);
724
+ const rebuilt = [];
725
+ let before = change.kind === 'move' ? state(next.slice(0, index)) : visible;
726
+ for (const [offset, batch] of following.entries()) {
727
+ let current = batch;
728
+ if (change.kind === 'move') {
729
+ const after = state(next.slice(0, index + offset + 1));
730
+ const appending = offset === 0 && created(batch, queuedFile(batch, change.path, held.localId));
731
+ const relinking = relinked(batch, change, before, after, appending);
732
+ if (relinking.kind === 'refused')
733
+ return relinking;
734
+ current = relinking.batch;
735
+ before = after;
736
+ }
737
+ if (current !== batch && !open(batch))
738
+ return unqueueable(change.path);
739
+ rebuilt.push(offset === 0
740
+ ? withdrawnFile(current, change, queuedFile(current, change.path, held.localId))
741
+ : current);
742
+ }
743
+ for (const [offset, batch] of following.entries()) {
744
+ const replaced = rebuilt[offset];
745
+ if (replaced === batch)
746
+ continue;
747
+ const origin = originOf(batch);
748
+ withdrawn.set(origin, replaced);
749
+ if (replaced !== null)
750
+ origins.set(replaced, origin);
751
+ }
752
+ owners.add(originOf(owner));
753
+ next.splice(index, following.length, ...rebuilt.filter((batch) => batch !== null));
754
+ queued.add(change.path);
755
+ }
756
+ return {
757
+ kind: 'requeued',
758
+ queue: withdrawn.size === 0 ? queue : next,
759
+ changes: kept,
760
+ queued,
761
+ withdrawn,
762
+ owners,
763
+ };
764
+ }
765
+ function covering(precondition) {
766
+ return precondition.kind === 'field' ||
767
+ precondition.kind === 'section' ||
768
+ precondition.kind === 'preamble'
769
+ ? identity(precondition)
770
+ : `path ${precondition.path}`;
771
+ }
772
+ export function joined(queue, owner, extra) {
773
+ const later = new Set(queue.slice(queue.indexOf(owner) + 1).flatMap((batch) => batch.changes.flatMap(touched)));
774
+ const clash = extra.changes.flatMap(touched).find((path) => later.has(path));
775
+ if (clash !== undefined)
776
+ return unqueueable(clash);
777
+ const changes = [...owner.changes];
778
+ const deferred = new Map();
779
+ for (const change of extra.changes) {
780
+ const unsettled = isPart(change) ? null : settledParts(changes, deferred);
781
+ if (unsettled !== null)
782
+ return unqueueable(unsettled);
783
+ if (folded(changes, change, true, deferred) === 'refused')
784
+ return unqueueable(change.path);
785
+ }
786
+ const unsettled = settledParts(changes, deferred);
787
+ if (unsettled !== null)
788
+ return unqueueable(unsettled);
789
+ const held = new Set(owner.preconditions.map(covering));
790
+ const written = new Set(owner.changes.flatMap((change) => change.kind === 'create' || change.kind === 'replace' ? [change.path] : []));
791
+ const preconditions = [
792
+ ...owner.preconditions,
793
+ ...extra.preconditions.filter((precondition) => {
794
+ const key = covering(precondition);
795
+ return (!held.has(key) && (key === `path ${precondition.path}` || !written.has(precondition.path)));
796
+ }),
797
+ ];
798
+ const batch = immutable({
799
+ ...owner,
800
+ ...(extra.validation === undefined ? {} : { validation: extra.validation }),
801
+ id: changeBatchIdentity(),
802
+ changes,
803
+ preconditions,
804
+ });
805
+ return { kind: 'joined', batch, queue: queue.map((item) => (item === owner ? batch : item)) };
806
+ }
807
+ export function limitedIn(queue, batch, limits, state) {
808
+ const at = queue.indexOf(batch);
809
+ const limited = finished(exceeding(limits, batch.changes, finished(providerRewritten(batch, state(queue.slice(0, at)))).changes, state(queue.slice(0, at + 1))));
810
+ return limited?.kind === 'refused' ? limited.refusal : null;
811
+ }
812
+ function* batching(input) {
435
813
  const changes = relativeChanges(input);
436
814
  const structural = changes.find((change) => change.kind === 'move' || change.kind === 'remove');
437
- if (structural !== undefined && unmodeledType(input.visible.collection, input.composition))
815
+ if (structural !== undefined &&
816
+ (yield* unmodeledType(input.visible.collection, input.composition)))
438
817
  return refuse('model-unavailable', structural.path);
439
818
  if (changes.length === 0)
440
819
  return refuse('no-change');
441
- const rewritten = rewrites(changes, input.visible);
820
+ const rewritten = yield* rewrites(changes, input.visible);
442
821
  if (rewritten.kind === 'refused')
443
822
  return rewritten;
444
823
  const refusal = guard(changes, input);
445
824
  if (refusal !== null)
446
825
  return refusal;
447
- const preconditions = preconditionsOf(changes, input.visible, input.bases);
448
- const predicted = anticipated(changes, preconditions, rewritten.changes, input.visible);
449
- const resulting = overlay(input.visible, [
826
+ const preconditions = yield* preconditionsOf(changes, input.visible, input.bases);
827
+ const predicted = yield* anticipated(changes, preconditions, rewritten.changes, input.visible);
828
+ const resulting = yield* overlaying(input.visible, [
450
829
  { id: '', baseRoomVersion: input.baseRoomVersion, ...predicted },
451
830
  ]);
831
+ const clean = input.clean !== undefined && available(input.composition) ? input.clean : null;
452
832
  const removed = new Set(changes.filter((change) => change.kind === 'remove').map((change) => change.path));
453
833
  if (removed.size > 0) {
454
- const referenced = incoming(collectionOf(resulting.documents, input.composition), removed);
834
+ const referrers = clean === null ? null : referring(clean, input.composition, removed);
835
+ const referenced = yield* incoming(collectionOf(resulting.documents, input.composition), removed, clean === null || referrers === null
836
+ ? null
837
+ : [...referrers, ...(yield* differences(clean.text, resulting.documents))]);
455
838
  if (referenced !== null)
456
839
  return referenced;
457
840
  }
@@ -461,15 +844,27 @@ export function buildBatch(input) {
461
844
  const parsed = parse(content);
462
845
  return parsed.ok && parsed.document.type !== undefined && parsed.document.type !== 'any';
463
846
  };
464
- const typedPaths = [...new Set(predicted.changes.flatMap(affected))].filter((path) => typed(input.visible.documents[path]) || typed(resulting.documents[path]));
847
+ const touched = [...new Set(predicted.changes.flatMap(affected))];
848
+ const typedPaths = [];
849
+ for (const path of touched) {
850
+ const before = input.visible.documents[path];
851
+ const after = resulting.documents[path];
852
+ if (typed(before) || typed(after))
853
+ typedPaths.push(path);
854
+ yield ROW_WEIGHT + (before?.length ?? 0) + (after?.length ?? 0);
855
+ }
465
856
  const needsModel = typedPaths.length > 0;
466
857
  const withoutModel = typedPaths.find((path) => governing(input.composition, path).status === 'unavailable');
467
858
  if (withoutModel !== undefined)
468
859
  return refuse('model-unavailable', withoutModel);
469
- const validationText = needsModel
470
- ? resulting.documents
471
- : Object.fromEntries(Object.entries(resulting.documents).filter(([path]) => predicted.changes.some((change) => affected(change).includes(path))));
472
- const verdict = validateCollection(validationText, resulting.binaryTargets, input.composition);
860
+ const verdict = clean !== null &&
861
+ (yield* changedCleanly({ text: resulting.documents, binaries: resulting.binaries }, clean, input.composition))
862
+ ? { verdict: 'accept' }
863
+ : yield* validateCollection(needsModel
864
+ ? resulting.documents
865
+ : Object.fromEntries(touched.flatMap((path) => resulting.documents[path] === undefined
866
+ ? []
867
+ : [[path, resulting.documents[path]]])), Object.keys(resulting.binaries), input.composition);
473
868
  if (verdict.verdict === 'refuse') {
474
869
  return {
475
870
  kind: 'refused',
@@ -482,7 +877,7 @@ export function buildBatch(input) {
482
877
  },
483
878
  };
484
879
  }
485
- const limited = exceeded(input.providerLimits, changes, resulting);
880
+ const limited = yield* exceeding(input.providerLimits, changes, predicted.changes, resulting);
486
881
  if (limited !== null)
487
882
  return limited;
488
883
  const validated = typedPaths
@@ -508,3 +903,6 @@ export function buildBatch(input) {
508
903
  }),
509
904
  };
510
905
  }
906
+ export function buildBatch(input) {
907
+ return stepped(batching(input));
908
+ }