rapier-html 1.1.88 → 1.1.91

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.
@@ -31,19 +31,203 @@ function _rapierTransformSplices(text, splices, inverse = false) {
31
31
  } catch (_) { return null; }
32
32
  }
33
33
 
34
+ function _rapierPlainLedgerData(record) {
35
+ try {
36
+ const active = new Set(), pending = [[record, false]];
37
+ while (pending.length) {
38
+ const [value, leaving] = pending.pop();
39
+ if (leaving) { active.delete(value); continue; }
40
+ if (value === null || typeof value === 'boolean') continue;
41
+ if (typeof value === 'string') { if (/[\uD800-\uDFFF]/u.test(value)) return false; continue; }
42
+ if (typeof value === 'number') { if (!Number.isFinite(value) || Object.is(value, -0)) return false; continue; }
43
+ if (!value || typeof value !== 'object' || active.has(value)) return false;
44
+ const array = Array.isArray(value), proto = Object.getPrototypeOf(value);
45
+ if (!array && proto !== null && (Object.getPrototypeOf(proto) !== null ||
46
+ Object.getOwnPropertyDescriptor(proto, 'constructor')?.value?.name !== 'Object')) return false;
47
+ const keys = Reflect.ownKeys(value);
48
+ if (array && keys.length !== value.length + 1) return false;
49
+ active.add(value); pending.push([value, true]);
50
+ for (const key of keys) {
51
+ if (array && key === 'length') continue;
52
+ if (typeof key !== 'string' || ['__proto__', 'constructor', 'prototype'].includes(key) ||
53
+ (array && (!Number.isSafeInteger(Number(key)) || Number(key) < 0 || Number(key) >= value.length || String(Number(key)) !== key))) return false;
54
+ const field = Object.getOwnPropertyDescriptor(value, key);
55
+ if (!field || !Object.hasOwn(field, 'value') || !field.enumerable) return false;
56
+ pending.push([field.value, false]);
57
+ }
58
+ }
59
+ return true;
60
+ } catch (_) { return false; }
61
+ }
62
+
63
+ const _rapierMetadataValue = (key, value) => key === 'filename'
64
+ ? typeof value === 'string' && value.trim().length > 0 && value.length <= 512 &&
65
+ !/[<>"\x00-\x1f\x7f/\\\u200b-\u200f\u2028-\u202e\u2066-\u2069\ufeff\uD800-\uDFFF]/u.test(value) && value !== '.' && value !== '..'
66
+ : key === 'docKind' && ['markdown', 'text', 'code'].includes(value);
67
+
68
+ function _rapierValidMetadata(value) {
69
+ return _rapierPlainLedgerData(value) && value && !Array.isArray(value) &&
70
+ Object.keys(value).length === 2 && ['filename', 'docKind'].every(key => Object.hasOwn(value, key) && _rapierMetadataValue(key, value[key]));
71
+ }
72
+
73
+ function _rapierValidMetadataEffect(effect) {
74
+ return _rapierPlainLedgerData(effect) && effect && !Array.isArray(effect) &&
75
+ Object.keys(effect).length > 0 && Object.keys(effect).every(key => {
76
+ const pair = effect[key];
77
+ return pair && !Array.isArray(pair) && Object.keys(pair).length === 2 &&
78
+ Object.hasOwn(pair, 'before') && Object.hasOwn(pair, 'after') &&
79
+ _rapierMetadataValue(key, pair.before) && _rapierMetadataValue(key, pair.after);
80
+ });
81
+ }
82
+
83
+ function _rapierMetadataDelta(before, after, {assign = []} = {}) {
84
+ if (!_rapierValidMetadata(before) || !_rapierValidMetadata(after)) return null;
85
+ const effect = {};
86
+ for (const key of ['filename', 'docKind']) if (before[key] !== after[key] || assign.includes(key)) effect[key] = {before: before[key], after: after[key]};
87
+ return Object.keys(effect).length ? effect : null;
88
+ }
89
+
90
+ function _rapierTransformMetadata(value, effect, inverse = false) {
91
+ if (!_rapierValidMetadata(value) || (effect != null && !_rapierValidMetadataEffect(effect))) return null;
92
+ const next = {...value};
93
+ for (const [key, pair] of Object.entries(effect || {})) {
94
+ if (next[key] !== pair[inverse ? 'after' : 'before']) return null;
95
+ next[key] = pair[inverse ? 'before' : 'after'];
96
+ }
97
+ return next;
98
+ }
99
+
100
+ // An origin binds transported placement to exact authored rows and their causal basis.
101
+ function _rapierValidAuthored(origin) {
102
+ if (!_rapierPlainLedgerData(origin) || !origin || Array.isArray(origin) ||
103
+ Object.keys(origin).length !== 3 || !Array.isArray(origin.basis) ||
104
+ new Set(origin.basis).size !== origin.basis.length || origin.basis.some((id, index) => index && origin.basis[index - 1] >= id) ||
105
+ !origin.basis.every(id => typeof id === 'string' && id.length > 0 && id.length <= 256) ||
106
+ typeof origin.source !== 'string' || !/^[a-f0-9]{64}$/.test(origin.source) ||
107
+ !Array.isArray(origin.splices) || origin.splices.length > 64) return false;
108
+ return origin.splices.every(row => row && !Array.isArray(row) && Object.keys(row).length === 3 &&
109
+ Number.isSafeInteger(row.pos) && row.pos >= 0 && !Object.is(row.pos, -0) &&
110
+ typeof row.removed === 'string' && typeof row.inserted === 'string' &&
111
+ !/[\uD800-\uDFFF]/u.test(row.removed) && !/[\uD800-\uDFFF]/u.test(row.inserted));
112
+ }
113
+
34
114
  function _rapierRecordSplices(entry, ledger) {
35
115
  try {
36
- if (entry && Array.isArray(entry.splices) && entry.splices.length) return entry.splices;
37
- const transaction = entry && entry.transaction;
38
- const targetId = String(transaction?.reverts || transaction?.reapplies || '');
39
- const target = targetId && (ledger || []).find(record => record?.transaction?.id === targetId);
40
- if (!target || !Array.isArray(target.splices) || !target.splices.length) return null;
41
- return transaction.reverts ? target.splices.slice().reverse().map(row => ({
42
- pos: row.pos, removed: row.inserted, inserted: row.removed,
43
- })) : target.splices;
116
+ if (entry && Array.isArray(entry.splices)) return entry.splices;
117
+ const tx = entry?.transaction, targetId = tx?.reverts || tx?.reapplies;
118
+ const prior = (ledger || []).filter(record => record?.transaction?.revision < tx?.revision);
119
+ const target = targetId && prior.find(record => record?.transaction?.id === targetId);
120
+ if (!target) return null;
121
+ const rows = _rapierRecordSplices(target, prior);
122
+ if (!rows) return null;
123
+ return tx.reverts ? rows.slice().reverse().map(row => ({pos: row.pos, removed: row.inserted, inserted: row.removed})) : rows;
44
124
  } catch (_) { return null; }
45
125
  }
46
126
 
127
+ function _rapierRecordMetadata(entry, ledger) {
128
+ try {
129
+ if (Object.hasOwn(entry, 'metadata')) return _rapierValidMetadataEffect(entry.metadata) ? entry.metadata : undefined;
130
+ // An expanded record owns its complete effect; only compact navigation inherits one.
131
+ if (Array.isArray(entry.splices)) return null;
132
+ const tx = entry.transaction, targetId = tx.reverts || tx.reapplies;
133
+ const prior = (ledger || []).filter(record => record?.transaction?.revision < tx.revision);
134
+ const target = targetId && prior.find(record => record?.transaction?.id === targetId);
135
+ if (!target) return undefined;
136
+ const effect = _rapierRecordMetadata(target, prior);
137
+ if (effect == null) return effect;
138
+ return tx.reverts ? Object.fromEntries(Object.entries(effect).map(([key, pair]) =>
139
+ [key, {before: pair.after, after: pair.before}])) : effect;
140
+ } catch (_) { return undefined; }
141
+ }
142
+
143
+ // Active inverses suppress their targets. Reversing an inverse restores its targets,
144
+ // including assignments hidden by later choices. Nothing outside these records is retained.
145
+ function _rapierHistoryEffects(records, visit = null) {
146
+ try {
147
+ if (!Array.isArray(records)) return {ok: false};
148
+ const nodes = new Map(), references = new Map();
149
+ const update = (target, delta) => {
150
+ const pending = [[target, delta]];
151
+ while (pending.length) {
152
+ const [id, change] = pending.pop(), node = nodes.get(id), was = node.active;
153
+ node.blockers += change;
154
+ if (node.blockers < 0) throw Error('effect');
155
+ node.active = node.blockers === 0;
156
+ if (was !== node.active) for (const ref of node.references) pending.push([ref, node.active ? 1 : -1]);
157
+ }
158
+ };
159
+ const lastUndo = new Map();
160
+ for (const record of records) {
161
+ const tx = record?.transaction;
162
+ if (!tx || typeof tx.id !== 'string' || !tx.id || nodes.has(tx.id) || (tx.reverts && tx.reapplies)) return {ok: false};
163
+ const supplied = tx.sourceTransactionIds || (tx.sourceTransactionId ? [tx.sourceTransactionId] : []);
164
+ if (!Array.isArray(supplied) || new Set(supplied).size !== supplied.length ||
165
+ (tx.sourceTransactionIds && tx.sourceTransactionId && (supplied.length !== 1 || supplied[0] !== tx.sourceTransactionId))) return {ok: false};
166
+ let refs = supplied;
167
+ if (tx.reverts || tx.reapplies) {
168
+ const target = tx.reverts || tx.reapplies;
169
+ if (!nodes.has(target) || (supplied.length && (supplied.length !== 1 || supplied[0] !== target))) return {ok: false};
170
+ if (tx.reverts) refs = [target];
171
+ else {
172
+ const undo = lastUndo.get(target);
173
+ if (!undo || !nodes.get(undo).active) return {ok: false};
174
+ refs = [undo];
175
+ }
176
+ }
177
+ if (refs.some(id => typeof id !== 'string' || !nodes.get(id)?.active)) return {ok: false};
178
+ nodes.set(tx.id, {active: true, blockers: 0, references: refs}); references.set(tx.id, refs.slice());
179
+ for (const ref of refs) update(ref, 1);
180
+ if (tx.reverts) lastUndo.set(tx.reverts, tx.id);
181
+ if (visit) visit(record, nodes, refs);
182
+ }
183
+ const effectiveActIds = [...nodes].filter(([, node]) => node.active).map(([id]) => id);
184
+ return {ok: true, effectiveActIds, active: new Set(effectiveActIds), references};
185
+ } catch (_) { return {ok: false}; }
186
+ }
187
+
188
+ function _rapierMetadataState(initial, records, effects = _rapierHistoryEffects(records)) {
189
+ if (!_rapierValidMetadata(initial) || !effects.ok) return null;
190
+ const state = {...initial};
191
+ for (const record of records) if (effects.active.has(record.transaction.id) && !effects.references.get(record.transaction.id)?.length) {
192
+ const effect = _rapierRecordMetadata(record, records);
193
+ if (effect === undefined) return null;
194
+ for (const [key, pair] of Object.entries(effect || {})) state[key] = pair.after;
195
+ }
196
+ return state;
197
+ }
198
+
199
+ // Prove each recorded transition and each inverse's semantic result, not only the final values.
200
+ function _rapierReplayMetadata(initial, records) {
201
+ if (!_rapierValidMetadata(initial)) return null;
202
+ let state = {...initial};
203
+ const earlier = [], assignments = {filename: [], docKind: []};
204
+ const effects = _rapierHistoryEffects(records, (record, nodes, refs) => {
205
+ const effect = _rapierRecordMetadata(record, earlier);
206
+ if (effect === undefined) throw Error('metadata');
207
+ state = _rapierTransformMetadata(state, effect);
208
+ if (!state) throw Error('metadata');
209
+ if (!refs.length) for (const [key, pair] of Object.entries(effect || {}))
210
+ assignments[key].push({id: record.transaction.id, value: pair.after});
211
+ for (const key of ['filename', 'docKind']) {
212
+ let expected = initial[key];
213
+ for (let i = assignments[key].length - 1; i >= 0; i--) if (nodes.get(assignments[key][i].id).active) {
214
+ expected = assignments[key][i].value; break;
215
+ }
216
+ if (state[key] !== expected) throw Error('metadata_effect');
217
+ }
218
+ earlier.push(record);
219
+ });
220
+ return effects.ok ? state : null;
221
+ }
222
+
223
+ function _rapierHasHistoryEffect(record, earlier = []) {
224
+ const tx = record?.transaction;
225
+ if (!tx || !(tx.reverts || tx.reapplies || tx.sourceTransactionId || tx.sourceTransactionIds?.length)) return false;
226
+ let id = tx.id;
227
+ if (!id) { id = 'candidate'; while (earlier.some(row => row?.transaction?.id === id)) id += ':'; }
228
+ return _rapierHistoryEffects([...earlier, {...record, transaction: {...tx, id}}]).ok;
229
+ }
230
+
47
231
  // A ledger record as the journal and the Notes return validate it below: plain data, nothing
48
232
  // undefined anywhere in it. A live record could carry a row whose type or order is not yet known (a
49
233
  // block just inserted, before its first render: `type: undefined`), and the validator would refuse
@@ -65,48 +249,30 @@ function _rapierValidLedgerRecord(record, prior, earlier, documentAuthority, max
65
249
  // Iterative traversal refuses cycles without exhausting the call stack. Proxies/counters that
66
250
  // throw are refused by the same gate rather than escaping into document recovery.
67
251
  try {
68
- const active = new Set(), pending = [[record, false]];
69
- while (pending.length) {
70
- const [value, leaving] = pending.pop();
71
- if (leaving) { active.delete(value); continue; }
72
- if (value === null || typeof value === 'string' || typeof value === 'boolean') continue;
73
- if (typeof value === 'number') { if (!Number.isFinite(value)) return false; continue; }
74
- if (!value || typeof value !== 'object' || active.has(value)) return false;
75
- const array = Array.isArray(value), proto = Object.getPrototypeOf(value);
76
- if (!array && proto !== null && (Object.getPrototypeOf(proto) !== null ||
77
- Object.getOwnPropertyDescriptor(proto, 'constructor')?.value?.name !== 'Object')) return false;
78
- const keys = Reflect.ownKeys(value);
79
- if (array && keys.length !== value.length + 1) return false;
80
- active.add(value); pending.push([value, true]);
81
- for (const key of keys) {
82
- if (array && key === 'length') continue;
83
- if (typeof key !== 'string' || key === '__proto__' || key === 'constructor' || key === 'prototype' ||
84
- (array && (!Number.isSafeInteger(Number(key)) || Number(key) < 0 ||
85
- Number(key) >= value.length || String(Number(key)) !== key))) return false;
86
- const field = Object.getOwnPropertyDescriptor(value, key);
87
- if (!field || !Object.hasOwn(field, 'value')) return false;
88
- pending.push([field.value, false]);
89
- }
90
- }
252
+ if (!_rapierPlainLedgerData(record) || (Object.hasOwn(record, 'authored') && !_rapierValidAuthored(record.authored))) return false;
91
253
  if (!record || typeof record !== 'object' ||
92
254
  typeof record.beforeHash !== 'string' || !record.beforeHash.length ||
93
- typeof record.afterHash !== 'string' || !record.afterHash.length || record.beforeHash === record.afterHash) return false;
94
- const own = Array.isArray(record.splices) && record.splices.length;
95
- const navigation = !!(record.transaction?.reverts || record.transaction?.reapplies);
255
+ typeof record.afterHash !== 'string' || !record.afterHash.length) return false;
256
+ const own = Array.isArray(record.splices), navigation = !!(record.transaction?.reverts || record.transaction?.reapplies);
96
257
  if ((!own && !navigation) || (own && record.splices.length > 64) ||
97
- (record.transaction?.reverts && record.transaction?.reapplies)) return false;
98
- const splices = _rapierRecordSplices(record, earlier);
258
+ (record.transaction?.reverts && record.transaction?.reapplies)) return false;
259
+ const splices = _rapierRecordSplices(record, earlier), metadata = _rapierRecordMetadata(record, earlier);
260
+ const semantic = _rapierHasHistoryEffect(record, earlier);
261
+ if (metadata === undefined || (!splices?.length && !Object.values(metadata || {}).some(pair => pair.before !== pair.after) && !semantic) ||
262
+ (record.beforeHash === record.afterHash && splices?.length) ||
263
+ (record.beforeHash !== record.afterHash && !splices?.length) ||
264
+ ((record.transaction?.sourceTransactionId || record.transaction?.sourceTransactionIds?.length || navigation) && !semantic)) return false;
99
265
  if (!splices || splices.length > 64 || !splices.every(row =>
100
266
  row && Number.isSafeInteger(row.pos) && row.pos >= 0 && !Object.is(row.pos, -0) &&
101
267
  typeof row.removed === 'string' && typeof row.inserted === 'string' &&
102
268
  !/[\uD800-\uDFFF]/u.test(row.removed) && !/[\uD800-\uDFFF]/u.test(row.inserted) &&
103
269
  utf8Length(row.removed) <= maxDocumentBytes &&
104
270
  utf8Length(row.inserted) <= maxDocumentBytes)) return false;
271
+ if (record.authored && record.authored.splices.some(row => utf8Length(row.removed) > maxDocumentBytes || utf8Length(row.inserted) > maxDocumentBytes)) return false;
105
272
  const transaction = record.transaction;
106
273
  const nullableText = (value, limit, empty = true) => value === null ||
107
274
  (typeof value === 'string' && value.length <= limit && (empty || value.length > 0));
108
- // A grouped agent contribution's optional fields, bounded like the identities beside them: a name of 1 to 120 characters, a
109
- // revision, at most 500 source identities (the journal the kernel retains) of 1 to 160, and a hosted row's identity of 1 to 256.
275
+ // Identity fields are bounded individually; durable history has no record-count limit.
110
276
  const optional = (value, valid) => value === undefined || valid(value);
111
277
  const identity = (value, limit) => typeof value === 'string' && value.length > 0 && value.length <= limit;
112
278
  if (!transaction || typeof transaction.id !== 'string' || !transaction.id.length ||
@@ -118,17 +284,21 @@ function _rapierValidLedgerRecord(record, prior, earlier, documentAuthority, max
118
284
  typeof transaction.operation !== 'string' || !transaction.operation.length ||
119
285
  transaction.operation.length > _RAPIER_TRANSACTION_OPERATION_LIMIT ||
120
286
  !nullableText(transaction.requestId, _RAPIER_TRANSACTION_REQUEST_LIMIT) ||
121
- !nullableText(transaction.sourceTransactionId, _RAPIER_TRANSACTION_REQUEST_LIMIT) ||
287
+ !nullableText(transaction.sourceTransactionId, 256, false) ||
288
+ !optional(transaction.label, value => nullableText(value, 120)) ||
289
+ !optional(transaction.turnBaseRevision, value => Number.isSafeInteger(value) && value >= 0) ||
290
+ !optional(transaction.turnId, value => value === null || identity(value, 160)) ||
291
+ !optional(transaction.turnLabel, value => value === null || identity(value, 120)) ||
122
292
  !optional(transaction.contribution, value => identity(value, 120)) ||
123
293
  !optional(transaction.contributionBaseRevision, value => Number.isSafeInteger(value) && value >= 0 && !Object.is(value, -0)) ||
124
- !optional(transaction.sourceTransactionIds, value => Array.isArray(value) && value.length <= 500 &&
125
- value.every(id => identity(id, _RAPIER_TRANSACTION_REQUEST_LIMIT))) ||
294
+ !optional(transaction.sourceTransactionIds, value => Array.isArray(value) && value.length > 0 &&
295
+ new Set(value).size === value.length && value.every(id => identity(id, 256))) ||
126
296
  !optional(transaction.remoteTransactionId, value => identity(value, 256)) ||
127
297
  !nullableText(transaction.parent, 256, false) ||
128
298
  !nullableText(transaction.reverts, 256, false) ||
129
299
  !nullableText(transaction.reapplies, 256, false) ||
130
- !Number.isSafeInteger(transaction.createdAt) || transaction.createdAt < 0 ||
131
- !Array.isArray(transaction.affectedBlockIds) || transaction.affectedBlockIds.length > 64 ||
300
+ !(transaction.createdAt === null || (Number.isSafeInteger(transaction.createdAt) && transaction.createdAt >= 0)) ||
301
+ !Array.isArray(transaction.affectedBlockIds) ||
132
302
  !transaction.affectedBlockIds.every(id => Number.isSafeInteger(id) && id >= 0) ||
133
303
  !Number.isSafeInteger(transaction.revision) || transaction.revision < 1 ||
134
304
  !Number.isSafeInteger(transaction.baseRevision) ||
@@ -140,8 +310,9 @@ function _rapierValidLedgerRecord(record, prior, earlier, documentAuthority, max
140
310
  return !!transaction.actor && typeof transaction.actor.kind === 'string' &&
141
311
  /^(?:human|agent|system)$/.test(transaction.actor.kind) &&
142
312
  typeof transaction.actor.id === 'string' && transaction.actor.id.length > 0 &&
143
- transaction.actor.id.length <= _RAPIER_TRANSACTION_ACTOR_LIMIT;
313
+ transaction.actor.id.length <= _RAPIER_TRANSACTION_ACTOR_LIMIT &&
314
+ optional(transaction.actor.name, value => nullableText(value, 120, false));
144
315
  } catch (_) { return false; }
145
316
  }
146
317
 
147
- export { _RAPIER_TRANSACTION_ACTOR_LIMIT, _RAPIER_TRANSACTION_OPERATION_LIMIT, _RAPIER_TRANSACTION_REQUEST_LIMIT, _rapierTransformSplices, _rapierRecordSplices, _rapierValidLedgerRecord, _rapierJournalRecord};
318
+ export { _rapierValidAuthored, _RAPIER_TRANSACTION_ACTOR_LIMIT, _RAPIER_TRANSACTION_OPERATION_LIMIT, _RAPIER_TRANSACTION_REQUEST_LIMIT, _rapierTransformSplices, _rapierRecordSplices, _rapierValidLedgerRecord, _rapierJournalRecord, _rapierValidMetadata, _rapierValidMetadataEffect, _rapierMetadataDelta, _rapierTransformMetadata, _rapierRecordMetadata, _rapierHistoryEffects, _rapierMetadataState, _rapierReplayMetadata, _rapierHasHistoryEffect};
@@ -0,0 +1,318 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Paired source placement, shared by live editing and canonical-history proof.
3
+ import {_rapierTransformSplices, _rapierValidAuthored, _rapierHistoryEffects} from './journal-records.mjs';
4
+ import {sha256} from './hash.mjs';
5
+ import {canonicalJSON} from './data.mjs';
6
+
7
+ const whole = text => typeof text === 'string' && !/[\uD800-\uDFFF]/u.test(text);
8
+ function admitted(rows) {
9
+ if (!Array.isArray(rows)) throw new RangeError('source_edits_invalid');
10
+ let end = 0;
11
+ for (const row of rows) {
12
+ if (!row || !Number.isSafeInteger(row.at) || row.at < end || Object.is(row.at, -0) ||
13
+ !Number.isSafeInteger(row.remove) || row.remove < 0 || Object.is(row.remove, -0) ||
14
+ !Number.isSafeInteger(row.at + row.remove) || !whole(row.insert)) throw new RangeError('source_edits_invalid');
15
+ end = row.at + row.remove;
16
+ }
17
+ }
18
+ function emit(rows, at, remove, insert = '') {
19
+ if (!remove && !insert) return;
20
+ const prior = rows.at(-1);
21
+ if (prior && prior.at + prior.remove === at && (!insert || !prior.remove)) {
22
+ prior.remove += remove; prior.insert += insert;
23
+ } else rows.push({at, remove, insert});
24
+ }
25
+
26
+ // Sweep original-source boundaries. Each counter names a coordinate in one input's
27
+ // resulting document; concurrent insertions never enter the other input's removals.
28
+ export function transformPair(left, right, leftFirst) {
29
+ admitted(left); admitted(right);
30
+ const points = [...new Set([0, ...left.flatMap(row => [row.at, row.at + row.remove]),
31
+ ...right.flatMap(row => [row.at, row.at + row.remove])])].sort((a, b) => a - b);
32
+ const out = [[], []], rows = [left, right], index = [0, 0], position = [0, 0], deletingUntil = [0, 0];
33
+ for (let point = 0; point < points.length; point++) {
34
+ const at = points[point], insertions = ['', ''];
35
+ for (let side = 0; side < 2; side++) while (rows[side][index[side]]?.at === at) {
36
+ const row = rows[side][index[side]++];
37
+ insertions[side] += row.insert;
38
+ deletingUntil[side] = Math.max(deletingUntil[side], row.at + row.remove);
39
+ }
40
+ for (const side of leftFirst ? [0, 1] : [1, 0]) if (insertions[side]) {
41
+ emit(out[side], position[1 - side], 0, insertions[side]);
42
+ position[side] += insertions[side].length;
43
+ }
44
+ const length = (points[point + 1] ?? at) - at;
45
+ for (let side = 0; side < 2; side++) {
46
+ if (deletingUntil[side] > at && deletingUntil[1 - side] <= at)
47
+ emit(out[side], position[1 - side], length);
48
+ }
49
+ for (let side = 0; side < 2; side++) if (deletingUntil[side] <= at) position[side] += length;
50
+ }
51
+ return {left: out[0], right: out[1]};
52
+ }
53
+
54
+ function applyBatch(text, rows) {
55
+ const exact = rows.slice().reverse().map(row => ({pos: row.at,
56
+ removed: text.slice(row.at, row.at + row.remove), inserted: row.insert}));
57
+ if (rows.some(row => row.at + row.remove > text.length)) throw new RangeError('source_edits_invalid');
58
+ const after = _rapierTransformSplices(text, exact);
59
+ if (after === null) throw new RangeError('source_edits_invalid');
60
+ return {text: after, splices: exact};
61
+ }
62
+
63
+ // Transport one sequential canonical act and every crossed record together. The
64
+ // reciprocal path is checked against exact source, rather than only equal end text.
65
+ // Canonical transport refuses deleting any authored unit already removed elsewhere.
66
+ export function transposeRecord(base, splices, id, records) {
67
+ if (typeof base !== 'string' || !Array.isArray(splices) || typeof id !== 'string' || !Array.isArray(records))
68
+ throw new RangeError('canonical_transport_invalid');
69
+ let source = base, current = base;
70
+ let crossed = records.map(record => ({...record, splices: record.splices.map(row => ({...row}))}));
71
+ for (const record of crossed) {
72
+ const next = _rapierTransformSplices(current, record.splices);
73
+ if (next === null) throw new RangeError('canonical_transport_invalid');
74
+ current = next;
75
+ }
76
+ const moved = [], sourceIndices = [];
77
+ for (const [sourceIndex, row] of splices.entries()) {
78
+ const submitted = _rapierTransformSplices(source, [row]);
79
+ if (submitted === null) throw new RangeError('canonical_transport_invalid');
80
+ let left = [{at: row.pos, remove: row.removed.length, insert: row.inserted}];
81
+ const reciprocal = [];
82
+ for (const record of crossed) {
83
+ const steps = [];
84
+ for (const other of record.splices) {
85
+ const pair = transformPair(left, [{at: other.pos, remove: other.removed.length, insert: other.inserted}], id < record.transaction.id);
86
+ left = pair.left; steps.push(pair.right);
87
+ }
88
+ reciprocal.push({record, steps});
89
+ }
90
+ if (left.reduce((sum, part) => sum + part.remove, 0) !== row.removed.length ||
91
+ left.map(part => part.insert).join('') !== row.inserted) throw new RangeError('canonical_act_rebase_conflict');
92
+ const applied = applyBatch(current, left);
93
+ let replay = submitted;
94
+ crossed = reciprocal.map(({record, steps}) => {
95
+ const transported = [];
96
+ for (const step of steps) {
97
+ const value = applyBatch(replay, step); replay = value.text; transported.push(...value.splices);
98
+ }
99
+ const before = record.splices.reduce((sum, part) => sum + part.removed.length, 0);
100
+ if (transported.reduce((sum, part) => sum + part.removed.length, 0) !== before ||
101
+ transported.map(part => part.inserted).join('') !== record.splices.map(part => part.inserted).join(''))
102
+ throw new RangeError('canonical_act_rebase_conflict');
103
+ return {...record, splices: transported};
104
+ });
105
+ if (replay !== applied.text) throw new RangeError('canonical_transport_invalid');
106
+ moved.push(...applied.splices); sourceIndices.push(...applied.splices.map(() => sourceIndex)); current = applied.text; source = submitted;
107
+ }
108
+ return {splices: moved, sourceIndices, against: crossed, text: current};
109
+ }
110
+
111
+ // Hosted and editor callers already own a reciprocal transport log. Convert only
112
+ // its admitted exact source to records, then prove canonical-unit preservation.
113
+ export function transposeSource(base, splices, client, log) {
114
+ let source = base;
115
+ const records = log.map(entry => {
116
+ const applied = applyBatch(source, entry.splices); source = applied.text;
117
+ return {transaction: {id: entry.client}, splices: applied.splices};
118
+ });
119
+ return transposeRecord(base, splices, client, records);
120
+ }
121
+
122
+ // The frontier names only causal maxima. Linear history needs one predecessor,
123
+ // and concurrent predecessors remain separate until an act observes both.
124
+ function authoredFrontier(records, visit = null) {
125
+ const dependencies = new Map(), frontier = new Set();
126
+ for (const record of records) {
127
+ const id = record.transaction.id, basis = record.authored?.basis;
128
+ if (visit) visit(record, basis ?? [...frontier].sort());
129
+ if (!basis) { dependencies.set(id, [...frontier]); frontier.clear(); }
130
+ else {
131
+ const pending = [...basis], seen = new Set();
132
+ while (pending.length) {
133
+ const prior = pending.pop();
134
+ if (seen.has(prior)) continue;
135
+ if (!dependencies.has(prior)) throw new RangeError('canonical_origin_invalid');
136
+ seen.add(prior); frontier.delete(prior); pending.push(...dependencies.get(prior));
137
+ }
138
+ dependencies.set(id, basis);
139
+ }
140
+ frontier.add(id);
141
+ }
142
+ return [...frontier].sort();
143
+ }
144
+
145
+ export function authoredBasis(records) { return authoredFrontier(records); }
146
+
147
+ export function authoredPlacements(base, records) {
148
+ const origins = new Map(); let source = base;
149
+ authoredFrontier(records, (record, basis) => {
150
+ origins.set(record.transaction.id, record.authored ?? authoredPlacement(source, record.splices, basis));
151
+ source = _rapierTransformSplices(source, record.splices);
152
+ if (source === null) throw new RangeError('canonical_origin_invalid');
153
+ });
154
+ return origins;
155
+ }
156
+
157
+ // Immutable authored placement stays on its own act. Basis IDs refer only to this
158
+ // canonical history; no source snapshot or duplicate history is stored.
159
+ export function authoredPlacement(source, splices, basis) {
160
+ const origin = {basis: basis.slice().sort(), source: sha256(source), splices: splices.map(row => ({...row}))};
161
+ if (!_rapierValidAuthored(origin) || _rapierTransformSplices(source, splices) === null)
162
+ throw new RangeError('canonical_origin_invalid');
163
+ return origin;
164
+ }
165
+
166
+ // Reconstruct an authored view from the same records, then prove its exact physical
167
+ // placement. Equality of resulting text alone cannot prove a repeated-text anchor.
168
+ function authoredReplay(base, records, append = null) {
169
+ const origins = new Map(), byId = new Map(), originalOrder = [], cache = new Map(), frontier = new Set();
170
+ const effects = _rapierHistoryEffects(append ? [...records, append] : records);
171
+ if (!effects.ok) throw new RangeError('canonical_origin_invalid');
172
+ let source = base;
173
+ for (const record of append ? [...records, append] : records) {
174
+ const id = record.transaction.id;
175
+ if (byId.has(id)) throw new RangeError('canonical_origin_invalid');
176
+ const origin = Object.hasOwn(record, 'authored') ? record.authored : authoredPlacement(source, record.splices, [...frontier]);
177
+ if (!_rapierValidAuthored(origin) || origin.basis.some(prior => !byId.has(prior)))
178
+ throw new RangeError('canonical_origin_invalid');
179
+ const seen = new Set(), pending = [...origin.basis];
180
+ while (pending.length) {
181
+ const prior = pending.pop(); if (seen.has(prior)) continue;
182
+ seen.add(prior); pending.push(...origins.get(prior).basis);
183
+ }
184
+ if (origin.basis.some(id => origin.basis.some(other => other !== id && (() => {
185
+ const pending = [...origins.get(other).basis], visited = new Set();
186
+ while (pending.length) { const next = pending.pop(); if (next === id) return true;
187
+ if (!visited.has(next)) { visited.add(next); pending.push(...origins.get(next).basis); } }
188
+ return false;
189
+ })()))) throw new RangeError('canonical_origin_invalid');
190
+ const refs = effects.references.get(id) || [];
191
+ if (refs.some(id => !seen.has(id))) throw new RangeError('canonical_origin_invalid');
192
+ for (const prior of seen) frontier.delete(prior); frontier.add(id);
193
+ origins.set(id, origin); byId.set(id, record); originalOrder.push(id);
194
+ if (record !== append) {
195
+ source = _rapierTransformSplices(source, record.splices);
196
+ if (source === null) throw new RangeError('canonical_origin_invalid');
197
+ }
198
+ }
199
+ const same = (a, b) => canonicalJSON(a) === canonicalJSON(b);
200
+ const ordered = (ids, selected = null) => {
201
+ const order = [], known = new Set(ids), waiting = new Map(), dependents = new Map(), ready = [];
202
+ const rank = id => selected ? Number(selected.has(id)) : Number(origins.get(id).splices.some(row => row.removed.length));
203
+ const compare = (a, b) => rank(a) - rank(b) || (a < b ? -1 : a > b ? 1 : 0);
204
+ const push = id => { let at = ready.length; ready.push(id);
205
+ while (at) { const parent = (at - 1) >> 1; if (compare(ready[parent], id) <= 0) break;
206
+ ready[at] = ready[parent]; at = parent; } ready[at] = id; };
207
+ const pop = () => { const first = ready[0], last = ready.pop(); if (ready.length) {
208
+ let at = 0; while (at * 2 + 1 < ready.length) { let child = at * 2 + 1;
209
+ if (child + 1 < ready.length && compare(ready[child + 1], ready[child]) < 0) child++;
210
+ if (compare(last, ready[child]) <= 0) break; ready[at] = ready[child]; at = child;
211
+ } ready[at] = last; } return first; };
212
+ for (const id of ids) {
213
+ const basis = origins.get(id)?.basis;
214
+ if (!basis || basis.some(prior => !known.has(prior))) throw new RangeError('canonical_origin_invalid');
215
+ waiting.set(id, basis.length);
216
+ for (const prior of basis) { if (!dependents.has(prior)) dependents.set(prior, []); dependents.get(prior).push(id); }
217
+ if (!basis.length) push(id);
218
+ }
219
+ while (ready.length) {
220
+ const id = pop(); order.push(id);
221
+ for (const next of dependents.get(id) || []) { waiting.set(next, waiting.get(next) - 1); if (!waiting.get(next)) push(next); }
222
+ }
223
+ if (order.length !== ids.length) throw new RangeError('canonical_origin_invalid');
224
+ return order;
225
+ };
226
+ const closureOf = basis => {
227
+ const closure = new Set(), pending = [...basis];
228
+ while (pending.length) { const prior = pending.pop(); if (closure.has(prior)) continue;
229
+ closure.add(prior); pending.push(...origins.get(prior).basis); }
230
+ return closure;
231
+ };
232
+ const canonicalOrder = ordered(originalOrder), firstExplicit = (append ? [...records, append] : records).findIndex(record => Object.hasOwn(record, 'authored'));
233
+ const implicitPrefix = firstExplicit < 0 ? records.length : firstExplicit;
234
+ const materialize = order => {
235
+ const key = canonicalJSON(order);
236
+ if (cache.has(key)) return cache.get(key);
237
+ const placed = [], ids = new Set(), sourceIndices = new Map(); let text = base;
238
+ if (!order.length) { const empty = {text, records: [], sourceIndices}; cache.set(key, empty); return empty; }
239
+ // An implicit linear prefix already is its authored view. Replay it once;
240
+ // reconstructing every growing prefix adds no evidence.
241
+ let prefix = 0;
242
+ while (prefix < order.length && prefix < implicitPrefix && order[prefix] === originalOrder[prefix]) {
243
+ const record = byId.get(order[prefix]); text = _rapierTransformSplices(text, record.splices);
244
+ if (text === null) throw new RangeError('canonical_origin_invalid');
245
+ placed.push(record); ids.add(order[prefix++]);
246
+ if (record.derivedCommentIndex != null) sourceIndices.set(record.transaction.id, record.splices.map((_, index) => index));
247
+ }
248
+ for (const id of order.slice(prefix)) {
249
+ const original = byId.get(id), origin = origins.get(id);
250
+ if (!original || ids.has(id) || origin.basis.some(prior => !ids.has(prior)))
251
+ throw new RangeError('canonical_origin_invalid');
252
+ const closure = closureOf(origin.basis), basisOrder = canonicalOrder.filter(prior => closure.has(prior)), basis = materialize(basisOrder);
253
+ if (sha256(basis.text) !== origin.source) throw new RangeError('canonical_origin_invalid');
254
+ // Reconstruct causal context before crossing it. Physical gaps may already
255
+ // have collapsed under a deletion; they cannot choose concurrent ordering.
256
+ const contextOrder = canonicalOrder.filter(prior => ids.has(prior)), context = same(contextOrder, [...ids]) ? {text, records: placed} : materialize(contextOrder);
257
+ if (context.text !== text) throw new RangeError('canonical_origin_invalid');
258
+ const alignedOrder = [...basisOrder, ...contextOrder.filter(prior => !closure.has(prior))];
259
+ const aligned = same(alignedOrder, [...ids]) ? {text, records: placed} : materialize(alignedOrder);
260
+ if (aligned.text !== text) throw new RangeError('canonical_origin_invalid');
261
+ const crossed = aligned.records.slice(basisOrder.length);
262
+ const moved = transposeRecord(basis.text, origin.splices, id, crossed);
263
+ if (_rapierTransformSplices(text, moved.splices) !== moved.text) throw new RangeError('canonical_origin_invalid');
264
+ placed.push({...original, splices: moved.splices}); text = moved.text; ids.add(id);
265
+ if (original.derivedCommentIndex != null) sourceIndices.set(id, moved.sourceIndices);
266
+ cache.set(canonicalJSON(order.slice(0, placed.length)), {text, records: placed.slice(), sourceIndices: new Map(sourceIndices)});
267
+ }
268
+ const value = {text, records: placed, sourceIndices}; cache.set(key, value); return value;
269
+ };
270
+ const physicalOrder = append ? originalOrder.slice(0, -1) : originalOrder, physical = materialize(physicalOrder);
271
+ if (physical.text !== source || physical.records.some((row, index) => !same(row.splices, records[index].splices)))
272
+ throw new RangeError('canonical_origin_invalid');
273
+ return {source, origins, originalOrder, materialize, ordered};
274
+ }
275
+
276
+ export function authoredHistory(base, records, selected = null) {
277
+ const replay = authoredReplay(base, records);
278
+ if (!selected) return {text: replay.source, records, origins: replay.origins};
279
+ // Targets follow independent work only in this transient Undo projection.
280
+ const projected = replay.materialize(replay.ordered(replay.originalOrder, selected));
281
+ if (projected.text !== replay.source) throw new RangeError('canonical_origin_invalid');
282
+ return {...projected, origins: replay.origins};
283
+ }
284
+
285
+ // Semantic annotations use the original splice coordinates, even when an accepted
286
+ // physical placement split those rows. The same replay proves the causal basis first.
287
+ export function authoredSourceReader(base, records) {
288
+ const replay = authoredReplay(base, records);
289
+ return actId => {
290
+ const origin = replay.origins.get(actId);
291
+ if (!origin) throw new RangeError('canonical_origin_invalid');
292
+ const ids = new Set(), pending = [...origin.basis];
293
+ while (pending.length) {
294
+ const id = pending.pop();
295
+ if (ids.has(id)) continue;
296
+ ids.add(id); pending.push(...replay.origins.get(id).basis);
297
+ }
298
+ const basis = replay.materialize(replay.ordered([...ids]));
299
+ if (sha256(basis.text) !== origin.source) throw new RangeError('canonical_origin_invalid');
300
+ return {source: basis.text, splices: origin.splices};
301
+ };
302
+ }
303
+
304
+ // The original view and the accepted view are both reconstructed from the same
305
+ // immutable acts. The reciprocal rows are an exact ACK, not another history.
306
+ export function transposeAuthored(base, records, incoming, submitted) {
307
+ if (!incoming.authored || !Array.isArray(submitted)) throw new RangeError('canonical_origin_invalid');
308
+ const replay = authoredReplay(base, records, incoming), id = incoming.transaction.id;
309
+ const authored = replay.materialize([...submitted, id]);
310
+ if (canonicalJSON(authored.records.at(-1).splices) !== canonicalJSON(incoming.splices))
311
+ throw new RangeError('canonical_origin_invalid');
312
+ const accepted = replay.materialize(replay.originalOrder), known = new Set([...submitted, id]);
313
+ const reciprocal = replay.materialize([...submitted, id, ...replay.ordered(replay.originalOrder).filter(prior => !known.has(prior))]);
314
+ if (reciprocal.text !== accepted.text) throw new RangeError('canonical_origin_invalid');
315
+ return {text: accepted.text, splices: accepted.records.at(-1).splices,
316
+ submittedBefore: replay.materialize(submitted).text, submitted: authored.text,
317
+ against: reciprocal.records.slice(submitted.length + 1)};
318
+ }