rapier-markdown-kit 1.1.98 → 1.1.101

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.
@@ -21,33 +21,27 @@ const K = new Uint32Array([
21
21
  0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,
22
22
  0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2,
23
23
  ]);
24
- // The rounds run on plain 32-bit locals: no array is made per block, so a megabyte (a painting's text) hashes about five times
25
- // faster than through destructured arrays, with the same digest.
26
- const W = new Int32Array(64);
24
+ const rotate = (x, n) => (x >>> n) | (x << (32 - n));
27
25
  export function sha256(text) {
28
- const bytes = new TextEncoder().encode(text), length = bytes.length;
29
- const padded = new Uint8Array(Math.ceil((length + 9) / 64) * 64), end = padded.length;
30
- padded.set(bytes); padded[length] = 0x80;
31
- const high = Math.floor(length * 8 / 0x100000000), low = (length * 8) >>> 0;
32
- padded[end - 8] = high >>> 24; padded[end - 7] = high >>> 16; padded[end - 6] = high >>> 8; padded[end - 5] = high;
33
- padded[end - 4] = low >>> 24; padded[end - 3] = low >>> 16; padded[end - 2] = low >>> 8; padded[end - 1] = low;
34
- let h0 = 0x6a09e667 | 0, h1 = 0xbb67ae85 | 0, h2 = 0x3c6ef372 | 0, h3 = 0xa54ff53a | 0, h4 = 0x510e527f | 0, h5 = 0x9b05688c | 0, h6 = 0x1f83d9ab | 0, h7 = 0x5be0cd19 | 0;
35
- const w = W;
36
- for (let offset = 0; offset < end; offset += 64) {
37
- for (let i = 0, j = offset; i < 16; i++, j += 4) w[i] = padded[j] << 24 | padded[j + 1] << 16 | padded[j + 2] << 8 | padded[j + 3];
26
+ const bytes = new TextEncoder().encode(text);
27
+ const padded = new Uint8Array(Math.ceil((bytes.length + 9) / 64) * 64);
28
+ padded.set(bytes); padded[bytes.length] = 0x80;
29
+ const data = new DataView(padded.buffer), bits = bytes.length * 8;
30
+ data.setUint32(padded.length - 8, Math.floor(bits / 0x100000000)); data.setUint32(padded.length - 4, bits >>> 0);
31
+ const h = new Uint32Array([0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19]), w = new Uint32Array(64);
32
+ for (let offset = 0; offset < padded.length; offset += 64) {
33
+ for (let i = 0; i < 16; i++) w[i] = data.getUint32(offset + i * 4);
38
34
  for (let i = 16; i < 64; i++) {
39
35
  const x = w[i - 15], y = w[i - 2];
40
- w[i] = (w[i - 16] + ((x >>> 7 | x << 25) ^ (x >>> 18 | x << 14) ^ x >>> 3) + w[i - 7] + ((y >>> 17 | y << 15) ^ (y >>> 19 | y << 13) ^ y >>> 10)) | 0;
36
+ w[i] = w[i - 16] + (rotate(x, 7) ^ rotate(x, 18) ^ (x >>> 3)) + w[i - 7] + (rotate(y, 17) ^ rotate(y, 19) ^ (y >>> 10));
41
37
  }
42
- let a = h0, b = h1, c = h2, d = h3, e = h4, f = h5, g = h6, k = h7;
38
+ let [a,b,c,d,e,f,g,k] = h;
43
39
  for (let i = 0; i < 64; i++) {
44
- const t1 = (k + ((e >>> 6 | e << 26) ^ (e >>> 11 | e << 21) ^ (e >>> 25 | e << 7)) + (e & f ^ ~e & g) + K[i] + w[i]) | 0;
45
- const t2 = (((a >>> 2 | a << 30) ^ (a >>> 13 | a << 19) ^ (a >>> 22 | a << 10)) + (a & b ^ a & c ^ b & c)) | 0;
46
- k = g; g = f; f = e; e = (d + t1) | 0; d = c; c = b; b = a; a = (t1 + t2) | 0;
40
+ const t1 = (k + (rotate(e, 6) ^ rotate(e, 11) ^ rotate(e, 25)) + ((e & f) ^ (~e & g)) + K[i] + w[i]) >>> 0;
41
+ const t2 = ((rotate(a, 2) ^ rotate(a, 13) ^ rotate(a, 22)) + ((a & b) ^ (a & c) ^ (b & c))) >>> 0;
42
+ k = g; g = f; f = e; e = (d + t1) >>> 0; d = c; c = b; b = a; a = (t1 + t2) >>> 0;
47
43
  }
48
- h0 = (h0 + a) | 0; h1 = (h1 + b) | 0; h2 = (h2 + c) | 0; h3 = (h3 + d) | 0; h4 = (h4 + e) | 0; h5 = (h5 + f) | 0; h6 = (h6 + g) | 0; h7 = (h7 + k) | 0;
44
+ for (const [i, value] of [a,b,c,d,e,f,g,k].entries()) h[i] += value;
49
45
  }
50
- let out = '';
51
- for (const x of [h0, h1, h2, h3, h4, h5, h6, h7]) out += (x >>> 0).toString(16).padStart(8, '0');
52
- return out;
46
+ return [...h].map(x => x.toString(16).padStart(8, '0')).join('');
53
47
  }
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  // Transient projections and selective inverses of the existing canonical journal.
3
3
  import {_rapierTransformSplices, _rapierRecordSplices, _rapierRecordMetadata, _rapierValidMetadata, _rapierTransformMetadata, _rapierMetadataDelta, _rapierHistoryEffects, _rapierMetadataState, _rapierReplayMetadata} from './journal-records.mjs';
4
- import {diffChars, diffLines} from '../../agent/diff.mjs';
4
+ import {projectInverseSource} from './inverse-source.mjs';
5
5
  import {textRoot, rootAfter} from './hash.mjs';
6
6
  import {authoredHistory} from './transport.mjs';
7
7
  import {plainData} from './data.mjs';
@@ -41,6 +41,39 @@ export function groupHistoryActs(acts) {
41
41
  return groups;
42
42
  }
43
43
 
44
+ // A picture's codec replacement travels with its original navigation step.
45
+ // Match every complete replacement exactly before moving any source bytes.
46
+ export function pictureHistorySplices({source, records}, target, redo) {
47
+ const id = target?.transaction?.id, at = records.indexOf(target);
48
+ if (!id || at < 0 || target.transaction.operation === 'document.finish-picture' || !Array.isArray(target.splices)) return null;
49
+ const later = records.slice(at + 1).filter(row => row?.transaction?.operation === 'document.finish-picture' && Array.isArray(row.splices) && row.splices.length);
50
+ if (!later.length) return null;
51
+ const own = new Set(later.filter(row => row.transaction.sourceTransactionId === id));
52
+ const step = redo ? target.splices : target.splices.slice().reverse().map(row => ({pos: row.pos, removed: row.inserted, inserted: row.removed}));
53
+ const plan = moveOthers => {
54
+ let text = source;
55
+ const rows = [], moved = new Set();
56
+ const only = needle => { const found = text.indexOf(needle); return found >= 0 && text.indexOf(needle, found + 1) < 0 ? found : -1; };
57
+ const apply = row => { const next = _rapierTransformSplices(text, [row]); if (next === null) throw Error('picture_target'); text = next; rows.push(row); };
58
+ const swap = (record, back) => {
59
+ const found = record.splices.map(splice => { const from = back ? splice.inserted : splice.removed; return from ? only(from) : -1; });
60
+ if (found.every(position => position < 0)) return false;
61
+ if (found.some(position => position < 0)) throw Error('partial_picture');
62
+ for (const splice of record.splices) {
63
+ const from = back ? splice.inserted : splice.removed, to = back ? splice.removed : splice.inserted;
64
+ apply({pos: only(from), removed: from, inserted: to});
65
+ }
66
+ return true;
67
+ };
68
+ for (const record of later.slice().reverse()) if ((moveOthers || own.has(record)) && swap(record, true)) moved.add(record);
69
+ for (const splice of step) apply(splice);
70
+ for (const record of later) if (own.has(record) ? redo : moved.has(record)) swap(record, false);
71
+ return rows;
72
+ };
73
+ try { const rows = plan(false); return !own.size && rows.length === step.length ? null : rows; } catch (_) {}
74
+ try { return plan(true); } catch (_) { return null; }
75
+ }
76
+
44
77
  // Reading an admitted repair can recover pre-existing lone UTF-16 units. This is
45
78
  // not a document mutation; the forward replay and roots still validate every row.
46
79
  function historyPreviousSource(source, splices) {
@@ -118,7 +151,7 @@ export function replayHistory(input) {
118
151
  const prepared = prepareHistory(input);
119
152
  if (!prepared.ok) return prepared;
120
153
  const {source, initialSource, metadata, initialMetadata, revision, earliestRevision, complete, rows, effects} = prepared;
121
- const acts = rows.map(({record}) => historyAct(record, effects));
154
+ const acts = rows.filter(({record}) => !effects.pictures.has(record.transaction.id)).map(({record}) => historyAct(record, effects));
122
155
  return {ok: true, source, initialSource, metadata, initialMetadata, revision, earliestRevision, complete, acts,
123
156
  effectiveActIds: effects.effectiveActIds.slice(), groups: groupHistoryActs(acts)};
124
157
  }
@@ -151,9 +184,7 @@ function historyRangeBefore(range, splice) {
151
184
  touched: true};
152
185
  }
153
186
 
154
- export function historyProjection(input, {from = 0, to = input?.source?.length, actId} = {}) {
155
- const prepared = prepareHistory(input);
156
- if (!prepared.ok) return prepared;
187
+ function projectHistoryRange(prepared, from, to, actId) {
157
188
  if (!historyBoundary(prepared.source, from) || !historyBoundary(prepared.source, to) || to < from) return historyFailure('range_invalid');
158
189
  let text = prepared.source, range = {from, to}, before = null;
159
190
  const acts = [];
@@ -168,7 +199,7 @@ export function historyProjection(input, {from = 0, to = input?.source?.length,
168
199
  if (text === null) return historyFailure('history_invalid');
169
200
  const previous = {...range, source: text.slice(range.from, range.to)};
170
201
  if (record.transaction.id === actId) before = previous;
171
- if (touched || record.metadata || prepared.effects.references.get(record.transaction.id)?.length)
202
+ if (!prepared.effects.pictures.has(record.transaction.id) && (touched || record.metadata || prepared.effects.references.get(record.transaction.id)?.length))
172
203
  acts.push({...historyAct(record, prepared.effects), before: previous, after});
173
204
  }
174
205
  if (actId != null && !before) return historyFailure('act_unavailable');
@@ -177,6 +208,21 @@ export function historyProjection(input, {from = 0, to = input?.source?.length,
177
208
  earliestRevision: prepared.earliestRevision, complete: prepared.complete, acts, groups: groupHistoryActs(acts), ...(before ? {before} : {})};
178
209
  }
179
210
 
211
+ export function historyProjection(input, {from = 0, to = input?.source?.length, actId} = {}) {
212
+ const prepared = prepareHistory(input);
213
+ if (!prepared.ok) return prepared;
214
+ return projectHistoryRange(prepared, from, to, actId);
215
+ }
216
+
217
+ // Several ranges share one proved source snapshot for this call.
218
+ export function historyProjections(input, ranges) {
219
+ const prepared = prepareHistory(input);
220
+ if (!prepared.ok) return prepared;
221
+ if (!Array.isArray(ranges)) return historyFailure('range_invalid');
222
+ return {ok: true, projections: ranges.map(({from = 0, to = prepared.source.length, actId} = {}) =>
223
+ structuredClone(projectHistoryRange(prepared, from, to, actId)))};
224
+ }
225
+
180
226
  // Project all reading places in one replay. Typing only invalidates this transient view.
181
227
  export function historyPlaces(input, blocks) {
182
228
  const prepared = prepareHistory(input);
@@ -198,11 +244,11 @@ export function historyPlaces(input, blocks) {
198
244
  block.range = {from: mapped.from, to: mapped.to}; touched ||= mapped.touched;
199
245
  }
200
246
  const previous = {...block.range, source: before.slice(block.range.from, block.range.to)};
201
- if (touched && previous.source !== after.source) block.acts.push({...historyAct(record), before: previous, after});
247
+ if (touched && previous.source !== after.source && !prepared.effects.pictures.has(record.transaction.id)) block.acts.push({...historyAct(record), before: previous, after});
202
248
  }
203
249
  text = before;
204
250
  }
205
- const acts = prepared.rows.map(({record}) => historyAct(record));
251
+ const acts = prepared.rows.filter(({record}) => !prepared.effects.pictures.has(record.transaction.id)).map(({record}) => historyAct(record));
206
252
  const present = new Map(), effects = new Map();
207
253
  for (const act of acts) {
208
254
  const changes = [];
@@ -228,37 +274,6 @@ export function historyPlaces(input, blocks) {
228
274
  reversed: acts.filter(act => present.get(act.id) !== true).map(act => act.id)};
229
275
  }
230
276
 
231
- function minimalHistorySplice(before, after, pos = 0) {
232
- let first = 0, a = before.length, b = after.length;
233
- while (first < a && first < b && before[first] === after[first]) first++;
234
- if (!historyBoundary(before, first) || !historyBoundary(after, first)) first--;
235
- while (a > first && b > first && before[a - 1] === after[b - 1]) { a--; b--; }
236
- if (!historyBoundary(before, a) || !historyBoundary(after, b)) { a++; b++; }
237
- return {pos: pos + first, removed: before.slice(first, a), inserted: after.slice(first, b)};
238
- }
239
-
240
- // Split only at byte-identical runs inside the recorded replacement, never by proportional character positions.
241
- function historyHunks(row) {
242
- if (row.removed === row.inserted) return [];
243
- if (!row.removed || !row.inserted) return [{...row}];
244
- const size = row.removed.length + row.inserted.length;
245
- const parts = size <= 16384 ? diffChars(row.removed, row.inserted, {maxEditLength: 256})
246
- : size <= 1024 * 1024 ? diffLines(row.removed, row.inserted, {maxEditLength: 128}) : null;
247
- if (!parts) return [minimalHistorySplice(row.removed, row.inserted, row.pos)];
248
- const hunks = [];
249
- let offset = 0, start = 0, removed = '', inserted = '';
250
- const flush = () => { if (removed || inserted) hunks.push({pos: row.pos + start, removed, inserted}); removed = ''; inserted = ''; };
251
- for (const part of parts) {
252
- if (part.added || part.removed) {
253
- if (!removed && !inserted) start = offset;
254
- if (part.added) inserted += part.value;
255
- else { removed += part.value; offset += part.value.length; }
256
- } else { flush(); offset += part.value.length; }
257
- }
258
- flush();
259
- return hunks.length <= 64 ? hunks.reverse() : [minimalHistorySplice(row.removed, row.inserted, row.pos)];
260
- }
261
-
262
277
  // Runs retain only exact replacement ancestry. A later replacement owns an ambiguous hunk whole.
263
278
  export function selectiveUndo(input, actIds, options = {}) {
264
279
  const prepared = prepareHistory(input);
@@ -283,244 +298,12 @@ export function selectiveUndo(input, actIds, options = {}) {
283
298
  sourceTransactionIds: [...selected], metadata, afterMetadata, effectChanged, unchanged: !metadata && !effectChanged,
284
299
  superseded: [], overlaps: []};
285
300
  try {
286
- let pieces = prepared.initialSource ? [{text: prepared.initialSource, owner: null, lineage: []}] : [];
287
- const all = [], byId = new Map(), byAct = new Map(), processed = [], keptInverseAncestors = new Set();
288
- const textOf = rows => rows.map(row => row.text || '').join('');
289
- const boundary = pos => {
290
- let offset = 0;
291
- for (let index = 0; index < pieces.length; index++) {
292
- const row = pieces[index];
293
- if (!row.text) continue;
294
- const end = offset + row.text.length;
295
- if (pos > offset && pos < end) {
296
- const at = pos - offset;
297
- pieces.splice(index, 1, {...row, text: row.text.slice(0, at)}, {...row, text: row.text.slice(at)});
298
- break;
299
- }
300
- offset = end;
301
- }
302
- offset = 0;
303
- let first = pieces.length, last = pieces.length;
304
- for (let index = 0; index < pieces.length; index++) {
305
- if (offset === pos) { first = index; last = index; while (last < pieces.length && !pieces[last].text) last++; return {first, last}; }
306
- offset += pieces[index].text?.length || 0;
307
- }
308
- if (offset !== pos) throw Error('range');
309
- return {first, last};
310
- };
311
- const owns = (piece, hunk) => piece === hunk.marker || piece.owner === hunk.id ||
312
- piece.restoredBy?.includes(hunk.id) || hunk.restoredMarkers?.has(piece);
313
- const effectiveBefore = (hunk, effects, visiting = new Set()) => {
314
- if (visiting.has(hunk.id)) return null;
315
- visiting.add(hunk.id); let text = '';
316
- for (const piece of hunk.before) {
317
- if (!piece.text) continue;
318
- const owner = byId.get(piece.owner);
319
- if (owner && !effects.active.has(owner.actId) && !owner.kept) {
320
- if (piece.text !== owner.inserted) return null;
321
- const prior = effectiveBefore(owner, effects, new Set(visiting));
322
- if (prior === null) return null;
323
- text += prior;
324
- } else text += piece.text;
325
- }
326
- return text;
327
- };
328
- const reactivate = (actIds, effects) => {
329
- for (const hunk of all) {
330
- if (!actIds.has(hunk.actId) || hunk.derived || prepared.effects.references.get(hunk.actId)?.length) continue;
331
- if (keptInverseAncestors.has(hunk.id)) continue;
332
- const later = [...hunk.supersededBy].some(id => {
333
- const next = byId.get(id);
334
- return next && effects.active.has(next.actId) && !prepared.effects.references.get(next.actId)?.length;
335
- });
336
- if (later || pieces.some(piece => piece.text && piece.owner === hunk.id) || (!hunk.inserted && hunk.active)) continue;
337
- const marker = pieces.indexOf(hunk.marker);
338
- if (marker < 0) throw Error('reactivation');
339
- const pos = textOf(pieces.slice(0, marker)).length, current = textOf(pieces);
340
- const removed = effectiveBefore(hunk, effects);
341
- if (removed === null || current.slice(pos, pos + removed.length) !== removed) throw Error('reactivation');
342
- boundary(pos + removed.length);
343
- const start = boundary(pos), end = boundary(pos + removed.length);
344
- const at = start.last, count = Math.max(0, end.first - at);
345
- if (textOf(pieces.slice(at, at + count)) !== removed) throw Error('reactivation');
346
- const replacedOwners = new Set(pieces.slice(at, at + count).map(piece => piece.owner).filter(Boolean));
347
- pieces.splice(at, count, ...(hunk.inserted ? [{text: hunk.inserted, owner: hunk.id, lineage: hunk.lineage.slice()}] : []));
348
- for (const owner of replacedOwners) if (owner !== hunk.id && !pieces.some(piece => piece.text && piece.owner === owner)) {
349
- const prior = byId.get(owner); if (prior) prior.active = false;
350
- }
351
- hunk.active = true; delete hunk.undoneBy;
352
- }
353
- };
354
301
  const transported = records.some(row => row.authored) ? authoredHistory(prepared.initialSource, records, selected) : null;
355
- const projectedRows = transported ? transported.records.map(record => ({record, splices: record.splices,
302
+ const rows = transported ? transported.records.map(record => ({record, splices: record.splices,
356
303
  sourceIndices: transported.sourceIndices.get(record.transaction.id)})) : prepared.rows;
357
- for (const {record, splices, sourceIndices} of projectedRows) {
358
- const tx = record.transaction, references = new Set(prepared.effects.references.get(tx.id) || []);
359
- const actSource = options.keepHunk && selected.has(tx.id) ? textOf(pieces) : null;
360
- processed.push(record);
361
- if (references.size) {
362
- // Exact inverse rows restore the previous run identities. Retain the inverse's
363
- // own transient marker too, so reversing it restores that same ancestry again.
364
- const targets = all.filter(hunk => hunk.active && references.has(hunk.actId));
365
- let restored = targets.length ? pieces.slice() : null;
366
- const inverses = [], priorSplices = options.keepHunk ? [] : null;
367
- for (let index = targets.length - 1; restored && index >= 0; index--) {
368
- const target = targets[index], first = restored.indexOf(target.marker);
369
- if (first < 0) { restored = null; break; }
370
- let last = first;
371
- for (let at = first; at < restored.length; at++) if (owns(restored[at], target)) last = at;
372
- const pos = textOf(restored.slice(0, first)).length;
373
- const before = restored.slice(first, last + 1), replacement = [];
374
- for (const piece of before) {
375
- if (piece === target.marker) replacement.push(...target.before);
376
- else if (!owns(piece, target)) replacement.push(piece);
377
- }
378
- const id = tx.id + '#inverse:' + index, lineage = new Set([id]);
379
- for (const piece of before) for (const parent of piece.lineage || piece.mark?.lineage || []) lineage.add(parent);
380
- const hunk = {id, actId: tx.id, removed: textOf(before), inserted: textOf(replacement), before, reversedHunk: target.id,
381
- chosen: selected.has(tx.id), lineage: [...lineage], supersededBy: new Set(), active: true,
382
- restoredMarkers: new Set(replacement.filter(piece => !piece.text))};
383
- if (hunk.chosen && options.keepHunk) {
384
- const spliceIndex = splices.findIndex(row => row.pos <= pos && row.pos + row.removed.length >= pos);
385
- const keep = options.keepHunk({actId: tx.id, spliceIndex: Math.max(0, spliceIndex), hunkIndex: index,
386
- splice: {pos, removed: hunk.removed, inserted: hunk.inserted}, beforeSource: textOf(restored), actSource, priorSplices});
387
- if (typeof keep !== 'boolean') throw Error('hunk_selection');
388
- hunk.chosen = !keep;
389
- }
390
- hunk.kept = selected.has(tx.id) && !hunk.chosen;
391
- // A semantic owner retaining this inverse also retains the exact ancestors
392
- // it restored; effect reactivation must not change them behind that owner.
393
- if (hunk.kept) for (let prior = target; prior; prior = byId.get(prior.reversedHunk)) keptInverseAncestors.add(prior.id);
394
- hunk.marker = {mark: hunk};
395
- const insertion = replacement.map(piece => piece.text ? {...piece,
396
- lineage: [...new Set([...(piece.lineage || []), id])],
397
- restoredBy: [...(piece.restoredBy || []), id]} : piece);
398
- restored.splice(first, last + 1 - first, hunk.marker, ...insertion);
399
- inverses.push(hunk); priorSplices?.push({pos, removed: hunk.removed, inserted: hunk.inserted});
400
- }
401
- if (restored && textOf(restored) === _rapierTransformSplices(textOf(pieces), splices)) {
402
- pieces = restored;
403
- for (const target of targets) { target.active = false; target.undoneBy = tx.id; }
404
- for (const piece of pieces) if (piece.mark) {
405
- piece.mark.active = true; delete piece.mark.undoneBy;
406
- }
407
- for (const hunk of inverses) { all.push(hunk); byId.set(hunk.id, hunk); }
408
- byAct.set(tx.id, inverses);
409
- continue;
410
- }
411
- }
412
-
413
- if (references.size) {
414
- const previous = _rapierHistoryEffects(processed.slice(0, -1)), current = _rapierHistoryEffects(processed);
415
- const activated = new Set(current.effectiveActIds.filter(id => !previous.active.has(id) &&
416
- id !== tx.id && !current.references.get(id)?.length));
417
- if (activated.size) {
418
- const before = pieces, priorStates = all.map(hunk => [hunk, hunk.active, hunk.undoneBy]); pieces = pieces.slice();
419
- try {
420
- reactivate(activated, current);
421
- if (textOf(pieces) === _rapierTransformSplices(textOf(before), splices)) continue;
422
- } catch (_) {}
423
- pieces = before;
424
- for (const [hunk, active, undoneBy] of priorStates) { hunk.active = active; hunk.undoneBy = undoneBy; }
425
- }
426
- }
427
- const priorSplices = options.keepHunk ? [] : null;
428
- for (let rowIndex = 0; rowIndex < splices.length; rowIndex++) {
429
- const original = splices[rowIndex];
430
- let rows;
431
- if (options.splitSplice) {
432
- const beforeSource = textOf(pieces), parts = options.splitSplice({actId: tx.id, beforeSource, splice: original});
433
- // Semantic boundaries only refine this act's exact bytes, never its history.
434
- if (!Array.isArray(parts) || _rapierTransformSplices(beforeSource, parts) !== _rapierTransformSplices(beforeSource, [original]))
435
- throw Error('hunk_projection');
436
- rows = parts.flatMap(historyHunks);
437
- } else rows = historyHunks(original);
438
- for (let index = 0; index < rows.length; index++) {
439
- const row = rows[index];
440
- const derived = (sourceIndices?.[rowIndex] ?? rowIndex) === record.derivedCommentIndex;
441
- let chosen = selected.has(tx.id) && !derived;
442
- boundary(row.pos + row.removed.length);
443
- const start = boundary(row.pos), end = boundary(row.pos + row.removed.length);
444
- let at = start.last, count = Math.max(0, end.first - at);
445
- if (!row.removed.length) {
446
- // A new insertion at a replacement's left boundary stays before that replacement.
447
- const marker = pieces.slice(start.first, start.last).findIndex(item => item.mark?.inserted.length);
448
- at = marker < 0 ? start.last : start.first + marker; count = 0;
449
- }
450
- const removed = pieces.slice(at, at + count);
451
- if (textOf(removed) !== row.removed) throw Error('replay');
452
- // A semantic owner may retain a drawing hunk while prose in that same act undoes.
453
- // The predicate only selects transient hunks; every original row still replays.
454
- if (chosen && options.keepHunk) {
455
- const keep = options.keepHunk({actId: tx.id, spliceIndex: rowIndex, hunkIndex: index,
456
- splice: {pos: row.pos, removed: row.removed, inserted: row.inserted}, beforeSource: textOf(pieces), actSource, priorSplices});
457
- if (typeof keep !== 'boolean') throw Error('hunk_selection');
458
- chosen = !keep;
459
- }
460
- const id = tx.id + '#' + rowIndex + ':' + index;
461
- const lineage = new Set([id]);
462
- for (const piece of removed) for (const parent of piece.lineage || piece.mark?.lineage || []) lineage.add(parent);
463
- const hunk = {id, actId: tx.id, removed: row.removed, inserted: row.inserted, before: removed, derived,
464
- chosen, kept: selected.has(tx.id) && !chosen, lineage: [...lineage], supersededBy: new Set(), active: true};
465
- hunk.marker = {mark: hunk};
466
- all.push(hunk); byId.set(id, hunk);
467
- if (!byAct.has(tx.id)) byAct.set(tx.id, []);
468
- byAct.get(tx.id).push(hunk);
469
- for (const parent of lineage) {
470
- const earlier = byId.get(parent);
471
- if (earlier && earlier !== hunk) earlier.supersededBy.add(id);
472
- }
473
- pieces.splice(at, count, hunk.marker, ...(row.inserted ? [{text: row.inserted, owner: id, lineage: [...lineage]}] : []));
474
- priorSplices?.push({pos: row.pos, removed: row.removed, inserted: row.inserted});
475
- }
476
- }
477
- }
478
- if (textOf(pieces) !== prepared.source) throw Error('head');
479
- const origins = new Map();
480
- let offset = 0;
481
- for (const piece of pieces) if (piece.text) { origins.set(piece, offset); offset += piece.text.length; }
482
- const superseded = [];
483
- for (let index = all.length - 1; index >= 0; index--) {
484
- const hunk = all[index];
485
- const inheritedInactive = !hunk.derived && !hunk.kept && !keptInverseAncestors.has(hunk.id) && !projectedEffects.active.has(hunk.actId) &&
486
- !prepared.effects.references.get(hunk.actId)?.length;
487
- if (!hunk.chosen && !inheritedInactive) continue;
488
- hunk.active = prepared.effects.active.has(hunk.actId);
489
- const laterActIds = [...new Set([...hunk.supersededBy].filter(id =>
490
- (byId.get(id)?.kept || byId.get(id)?.active && projectedEffects.active.has(byId.get(id)?.actId))).map(id => byId.get(id).actId))];
491
- const marker = pieces.indexOf(hunk.marker);
492
- if ((!hunk.active && !inheritedInactive) || laterActIds.length || marker < 0) {
493
- if (hunk.chosen) superseded.push({actId: hunk.actId, hunk: hunk.id, laterActIds: hunk.undoneBy ? [hunk.undoneBy] : laterActIds,
494
- reason: hunk.active ? 'later_edit' : 'already_undone'});
495
- continue;
496
- }
497
- for (let at = pieces.length - 1; at >= 0; at--) if (pieces[at] !== hunk.marker && owns(pieces[at], hunk)) pieces.splice(at, 1);
498
- pieces.splice(pieces.indexOf(hunk.marker), 1, ...hunk.before);
499
- // Restoring an inverse can restore a deletion marker without any owned
500
- // text. That exact marker already represents the active deletion.
501
- for (const piece of hunk.before) if (piece.mark) { piece.mark.active = true; delete piece.mark.undoneBy; }
502
- }
503
- // A hidden withdrawal can have no splice of its own. Reversing that withdrawal
504
- // reactivates the original hunk at its retained marker, never at a text search.
505
- reactivate(new Set(records.filter(record => !prepared.effects.active.has(record.transaction.id) &&
506
- projectedEffects.active.has(record.transaction.id)).map(record => record.transaction.id)), projectedEffects);
507
- const after = textOf(pieces), changes = [];
508
- let cursor = 0, inserted = '';
509
- for (const piece of pieces) {
510
- if (!piece.text) continue;
511
- const origin = origins.get(piece);
512
- if (origin == null) { inserted += piece.text; continue; }
513
- if (origin < cursor) throw Error('source_order');
514
- const removed = prepared.source.slice(cursor, origin);
515
- if (removed !== inserted) changes.push(minimalHistorySplice(removed, inserted, cursor));
516
- inserted = ''; cursor = origin + piece.text.length;
517
- }
518
- const removed = prepared.source.slice(cursor);
519
- if (removed !== inserted) changes.push(minimalHistorySplice(removed, inserted, cursor));
520
- const splices = changes.length <= 64 ? changes.reverse() : [minimalHistorySplice(prepared.source, after)];
521
- if (_rapierTransformSplices(prepared.source, splices) !== after) throw Error('plan');
522
- return {ok: true, splices, after, actIds: [...requested], sourceTransactionIds: [...selected], metadata, afterMetadata,
523
- effectChanged, unchanged: after === prepared.source && !metadata && !effectChanged, superseded,
524
- overlaps: superseded.filter(row => row.reason === 'later_edit')};
304
+ const result = projectInverseSource({...prepared, rows}, selected, projectedEffects, options);
305
+ if (!result.ok) return result;
306
+ return {...result, actIds: [...requested], sourceTransactionIds: [...selected], metadata, afterMetadata,
307
+ effectChanged, unchanged: result.after === prepared.source && !metadata && !effectChanged};
525
308
  } catch (_) { return historyFailure('history_invalid'); }
526
309
  }