@agentshouse/mdruntime 0.1.0-ce531b51ff088 → 0.1.0-ceb97106cc2fd

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