rapier-markdown-kit 1.1.90 → 1.1.92

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.
@@ -0,0 +1,526 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Transient projections and selective inverses of the existing canonical journal.
3
+ import {_rapierTransformSplices, _rapierRecordSplices, _rapierRecordMetadata, _rapierValidMetadata, _rapierTransformMetadata, _rapierMetadataDelta, _rapierHistoryEffects, _rapierMetadataState, _rapierReplayMetadata} from './journal-records.mjs';
4
+ import {diffChars, diffLines} from '../../agent/diff.mjs';
5
+ import {textRoot, rootAfter} from './hash.mjs';
6
+ import {authoredHistory} from './transport.mjs';
7
+ import {plainData} from './data.mjs';
8
+
9
+ const historyFailure = reason => ({ok: false, reason});
10
+ const historyInteger = value => Number.isSafeInteger(value) && value >= 0 && !Object.is(value, -0);
11
+ const historyBoundary = (source, at) => historyInteger(at) && at <= source.length && !(at > 0 && at < source.length &&
12
+ (source.charCodeAt(at - 1) & 0xFC00) === 0xD800 && (source.charCodeAt(at) & 0xFC00) === 0xDC00);
13
+
14
+ function historyAct(record, effects = null) {
15
+ const tx = record.transaction;
16
+ return {id: tx.id, actor: {...tx.actor}, createdAt: tx.createdAt, baseRevision: tx.baseRevision, revision: tx.revision,
17
+ operation: tx.operation || 'document.change',
18
+ ...(tx.turnId ? {turnId: tx.turnId} : {}),
19
+ ...(tx.contribution ? {contribution: tx.contribution} : {}),
20
+ ...(tx.sourceTransactionId ? {sourceTransactionId: tx.sourceTransactionId} : {}),
21
+ ...(tx.sourceTransactionIds?.length ? {sourceTransactionIds: tx.sourceTransactionIds.slice()} : {}),
22
+ ...(tx.reverts ? {reverts: tx.reverts} : {}), ...(tx.reapplies ? {reapplies: tx.reapplies} : {}),
23
+ ...(record.changeSet && Object.hasOwn(record.changeSet, 'label') ? {label: record.changeSet.label} : {}),
24
+ ...(Array.isArray(tx.affectedBlockIds) ? {affectedBlockIds: tx.affectedBlockIds.slice()} : {}),
25
+ ...(record.metadata ? {metadata: plainData(record.metadata)} : {}),
26
+ ...(effects ? {active: effects.active.has(tx.id)} : {})};
27
+ }
28
+
29
+ export function groupHistoryActs(acts) {
30
+ const groups = [], grouped = new Map();
31
+ for (const act of acts) {
32
+ const key = JSON.stringify([act.actor.kind, act.actor.id, act.turnId || null, act.turnId ? null : act.id]);
33
+ let group = grouped.get(key);
34
+ if (!group) {
35
+ group = {id: act.turnId || act.id, actor: {...act.actor}, actIds: [], createdAt: act.createdAt,
36
+ ...(act.turnId ? {turnId: act.turnId} : {})};
37
+ groups.push(group); grouped.set(key, group);
38
+ }
39
+ group.actIds.push(act.id);
40
+ }
41
+ return groups;
42
+ }
43
+
44
+ // Reading an admitted repair can recover pre-existing lone UTF-16 units. This is
45
+ // not a document mutation; the forward replay and roots still validate every row.
46
+ function historyPreviousSource(source, splices) {
47
+ let value = source;
48
+ for (let index = splices.length - 1; index >= 0; index--) {
49
+ const row = splices[index];
50
+ if (!row || !historyInteger(row.pos) || typeof row.removed !== 'string' || typeof row.inserted !== 'string' ||
51
+ row.pos > value.length || row.inserted.length > value.length - row.pos ||
52
+ value.slice(row.pos, row.pos + row.inserted.length) !== row.inserted) return null;
53
+ value = value.slice(0, row.pos) + row.removed + value.slice(row.pos + row.inserted.length);
54
+ }
55
+ return value;
56
+ }
57
+
58
+ // This is a projection of the existing records. No source snapshots or provenance survive the call.
59
+ function prepareHistory(input) {
60
+ try {
61
+ if (!input || typeof input.source !== 'string' || !Array.isArray(input.records) || !_rapierValidMetadata(input.metadata)) return historyFailure('history_invalid');
62
+ const source = input.source, records = plainData(input.records), ids = new Set(), rows = [];
63
+ const effects = _rapierHistoryEffects(records);
64
+ if (!effects.ok) return historyFailure('history_invalid');
65
+ const revision = input.revision ?? records.at(-1)?.transaction?.revision ?? 0;
66
+ if (!historyInteger(revision)) return historyFailure('history_invalid');
67
+ let prior = input.earliestRevision ?? records[0]?.transaction?.baseRevision ?? revision, complete = prior === 0;
68
+ if (!historyInteger(prior)) return historyFailure('history_invalid');
69
+ for (const record of records) {
70
+ const tx = record?.transaction, actor = tx?.actor;
71
+ if (!tx || typeof tx.id !== 'string' || !tx.id || ids.has(tx.id) || !historyInteger(tx.baseRevision) ||
72
+ !historyInteger(tx.revision) || tx.revision !== tx.baseRevision + 1 || tx.baseRevision < prior ||
73
+ !actor || !['human', 'agent', 'system'].includes(actor.kind) || typeof actor.id !== 'string' || !actor.id ||
74
+ !(tx.createdAt === null || historyInteger(tx.createdAt)) ||
75
+ (tx.turnId != null && (typeof tx.turnId !== 'string' || !tx.turnId || tx.turnId.length > 160))) return historyFailure('history_invalid');
76
+ if (tx.baseRevision !== prior) complete = false;
77
+ const splices = _rapierRecordSplices(record, records), metadata = _rapierRecordMetadata(record, records);
78
+ if (!splices || metadata === undefined || (!splices.length &&
79
+ !Object.values(metadata || {}).some(pair => pair.before !== pair.after) && !effects.references.get(tx.id)?.length)) return historyFailure('history_unavailable');
80
+ rows.push({record, splices, metadata}); ids.add(tx.id); prior = tx.revision;
81
+ }
82
+ if (prior !== revision) return historyFailure('history_unavailable');
83
+ let initialSource = source, initialMetadata = {...input.metadata};
84
+ for (let index = rows.length - 1; index >= 0; index--) {
85
+ initialSource = historyPreviousSource(initialSource, rows[index].splices);
86
+ initialMetadata = _rapierTransformMetadata(initialMetadata, rows[index].metadata, true);
87
+ if (initialSource === null || !initialMetadata) return historyFailure('history_invalid');
88
+ }
89
+ const earliestRevision = rows[0]?.record.transaction.baseRevision ?? revision;
90
+ let replay = initialSource, root = earliestRevision === 0 ? textRoot(initialSource) : null, rootRevision = earliestRevision;
91
+ for (const {record, splices} of rows) {
92
+ const hashed = Object.hasOwn(record, 'beforeHash') || Object.hasOwn(record, 'afterHash');
93
+ if (hashed && (![record.beforeHash, record.afterHash].every(value =>
94
+ typeof value === 'string' && /^\d+:[a-z0-9]+:[a-z0-9]+$/.test(value)) ||
95
+ (splices.length ? record.beforeHash === record.afterHash : record.beforeHash !== record.afterHash)))
96
+ return historyFailure('history_invalid');
97
+ // Roots bind revision zero to the full source. A retained suffix has only a path root;
98
+ // omitted neutral pairs change that root, so a stated revision gap begins a new chain.
99
+ if (record.transaction.baseRevision !== rootRevision) root = null;
100
+ if (hashed) {
101
+ if (root !== null && record.beforeHash !== root) return historyFailure('history_invalid');
102
+ root = record.beforeHash;
103
+ }
104
+ replay = _rapierTransformSplices(replay, splices);
105
+ if (replay === null) return historyFailure('history_invalid');
106
+ if (root !== null) for (const row of splices) root = rootAfter(root, row);
107
+ if (hashed && root !== record.afterHash) return historyFailure('history_invalid');
108
+ rootRevision = record.transaction.revision;
109
+ }
110
+ if (records.some(row => Object.hasOwn(row, 'authored'))) authoredHistory(initialSource, rows.map(({record, splices}) => ({...record, splices})));
111
+ const metadata = _rapierReplayMetadata(initialMetadata, records);
112
+ if (replay !== source || !metadata || ['filename', 'docKind'].some(key => metadata[key] !== input.metadata[key])) return historyFailure('history_invalid');
113
+ return {ok: true, source, initialSource, metadata, initialMetadata, effects, rows, revision, earliestRevision, complete: input.complete !== false && complete && earliestRevision === 0};
114
+ } catch (_) { return historyFailure('history_invalid'); }
115
+ }
116
+
117
+ export function replayHistory(input) {
118
+ const prepared = prepareHistory(input);
119
+ if (!prepared.ok) return prepared;
120
+ const {source, initialSource, metadata, initialMetadata, revision, earliestRevision, complete, rows, effects} = prepared;
121
+ const acts = rows.map(({record}) => historyAct(record, effects));
122
+ return {ok: true, source, initialSource, metadata, initialMetadata, revision, earliestRevision, complete, acts,
123
+ effectiveActIds: effects.effectiveActIds.slice(), groups: groupHistoryActs(acts)};
124
+ }
125
+
126
+ export function sourceBefore(input, actId) {
127
+ const prepared = prepareHistory(input);
128
+ if (!prepared.ok) return prepared;
129
+ let source = prepared.source, metadata = {...prepared.metadata};
130
+ for (let index = prepared.rows.length - 1; index >= 0; index--) {
131
+ const {record, splices} = prepared.rows[index];
132
+ source = historyPreviousSource(source, splices);
133
+ if (source === null) return historyFailure('history_invalid');
134
+ metadata = _rapierTransformMetadata(metadata, prepared.rows[index].metadata, true);
135
+ if (record.transaction.id === actId) return {ok: true, source, metadata, revision: record.transaction.baseRevision,
136
+ act: historyAct(record, prepared.effects), complete: prepared.complete, earliestRevision: prepared.earliestRevision};
137
+ }
138
+ return historyFailure('act_unavailable');
139
+ }
140
+
141
+ function historyRangeBefore(range, splice) {
142
+ const start = splice.pos, end = start + splice.inserted.length, replacement = splice.removed.length;
143
+ const touched = range.from === range.to ? start <= range.from && end >= range.to
144
+ : start === end ? range.from <= start && start <= range.to : start < range.to && end > range.from;
145
+ if (!touched) {
146
+ const shift = end <= range.from ? replacement - splice.inserted.length : 0;
147
+ return {from: range.from + shift, to: range.to + shift, touched: false};
148
+ }
149
+ return {from: range.from <= start ? range.from : range.from >= end ? range.from + replacement - splice.inserted.length : start,
150
+ to: range.to >= end ? range.to + replacement - splice.inserted.length : range.to <= start ? range.to : start + replacement,
151
+ touched: true};
152
+ }
153
+
154
+ export function historyProjection(input, {from = 0, to = input?.source?.length, actId} = {}) {
155
+ const prepared = prepareHistory(input);
156
+ if (!prepared.ok) return prepared;
157
+ if (!historyBoundary(prepared.source, from) || !historyBoundary(prepared.source, to) || to < from) return historyFailure('range_invalid');
158
+ let text = prepared.source, range = {from, to}, before = null;
159
+ const acts = [];
160
+ for (let index = prepared.rows.length - 1; index >= 0; index--) {
161
+ const {record, splices} = prepared.rows[index], after = {...range, source: text.slice(range.from, range.to)};
162
+ let touched = false;
163
+ for (let at = splices.length - 1; at >= 0; at--) {
164
+ const mapped = historyRangeBefore(range, splices[at]); touched ||= mapped.touched;
165
+ range = {from: mapped.from, to: mapped.to};
166
+ }
167
+ text = historyPreviousSource(text, splices);
168
+ if (text === null) return historyFailure('history_invalid');
169
+ const previous = {...range, source: text.slice(range.from, range.to)};
170
+ if (record.transaction.id === actId) before = previous;
171
+ if (touched || record.metadata || prepared.effects.references.get(record.transaction.id)?.length)
172
+ acts.push({...historyAct(record, prepared.effects), before: previous, after});
173
+ }
174
+ if (actId != null && !before) return historyFailure('act_unavailable');
175
+ acts.reverse();
176
+ return {ok: true, from, to, source: prepared.source.slice(from, to), revision: prepared.revision,
177
+ earliestRevision: prepared.earliestRevision, complete: prepared.complete, acts, groups: groupHistoryActs(acts), ...(before ? {before} : {})};
178
+ }
179
+
180
+ // Project all reading places in one replay. Typing only invalidates this transient view.
181
+ export function historyPlaces(input, blocks) {
182
+ const prepared = prepareHistory(input);
183
+ if (!prepared.ok) return prepared;
184
+ if (!Array.isArray(blocks) || blocks.some(block => !block ||
185
+ !historyBoundary(prepared.source, block.from) || !historyBoundary(prepared.source, block.to) || block.to < block.from))
186
+ return historyFailure('range_invalid');
187
+ const projected = blocks.map(block => ({id: block.id, from: block.from, to: block.to,
188
+ source: prepared.source.slice(block.from, block.to), range: {from: block.from, to: block.to}, acts: []}));
189
+ let text = prepared.source;
190
+ for (let index = prepared.rows.length - 1; index >= 0; index--) {
191
+ const {record, splices} = prepared.rows[index], before = historyPreviousSource(text, splices);
192
+ if (before === null) return historyFailure('history_invalid');
193
+ for (const block of projected) {
194
+ const after = {...block.range, source: text.slice(block.range.from, block.range.to)};
195
+ let touched = false;
196
+ for (let at = splices.length - 1; at >= 0; at--) {
197
+ const mapped = historyRangeBefore(block.range, splices[at]);
198
+ block.range = {from: mapped.from, to: mapped.to}; touched ||= mapped.touched;
199
+ }
200
+ 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});
202
+ }
203
+ text = before;
204
+ }
205
+ const acts = prepared.rows.map(({record}) => historyAct(record));
206
+ const present = new Map(), effects = new Map();
207
+ for (const act of acts) {
208
+ const changes = [];
209
+ const set = (id, value) => {
210
+ const prior = present.get(id) === true;
211
+ present.set(id, value); changes.push({id, before: prior, after: value});
212
+ };
213
+ if (act.reapplies) {
214
+ for (const change of effects.get(act.reapplies) || [{id: act.reapplies, after: true}]) set(change.id, change.after);
215
+ } else {
216
+ const ids = act.sourceTransactionIds || (act.sourceTransactionId ? [act.sourceTransactionId] : act.reverts ? [act.reverts] : []);
217
+ for (const id of ids) {
218
+ for (const change of (effects.get(id) || [{id, before: false, after: true}]).slice().reverse()) {
219
+ if (present.get(change.id) === change.after) set(change.id, change.before);
220
+ }
221
+ }
222
+ }
223
+ set(act.id, true); effects.set(act.id, changes);
224
+ }
225
+ for (const block of projected) { block.acts.reverse(); delete block.range; }
226
+ return {ok: true, source: prepared.source, initialSource: prepared.initialSource, revision: prepared.revision,
227
+ earliestRevision: prepared.earliestRevision, complete: prepared.complete, acts, blocks: projected,
228
+ reversed: acts.filter(act => present.get(act.id) !== true).map(act => act.id)};
229
+ }
230
+
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
+ // Runs retain only exact replacement ancestry. A later replacement owns an ambiguous hunk whole.
263
+ export function selectiveUndo(input, actIds, options = {}) {
264
+ const prepared = prepareHistory(input);
265
+ if (!prepared.ok) return prepared;
266
+ if (!Array.isArray(actIds) || !actIds.length || actIds.some(id => typeof id !== 'string' || !id)) return historyFailure('act_invalid');
267
+ if (!options || typeof options !== 'object' || Array.isArray(options) ||
268
+ (options.keepHunk !== undefined && typeof options.keepHunk !== 'function') ||
269
+ (options.splitSplice !== undefined && typeof options.splitSplice !== 'function')) return historyFailure('act_invalid');
270
+ const requested = new Set(actIds), available = new Set(prepared.rows.map(({record}) => record.transaction.id));
271
+ if ([...requested].some(id => !available.has(id))) return historyFailure('act_unavailable');
272
+ const selected = new Set([...requested].filter(id => prepared.effects.active.has(id)));
273
+ const records = prepared.rows.map(({record, splices}) => ({...record, splices}));
274
+ let inverseId = 'planned-inverse'; while (available.has(inverseId)) inverseId += ':';
275
+ const inverse = {splices: [], transaction: {id: inverseId, sourceTransactionIds: [...selected]}};
276
+ const projectedEffects = selected.size ? _rapierHistoryEffects([...records, inverse]) : prepared.effects;
277
+ const afterMetadata = _rapierMetadataState(prepared.initialMetadata, records, projectedEffects);
278
+ if (!afterMetadata) return historyFailure('history_invalid');
279
+ const metadata = _rapierMetadataDelta(prepared.metadata, afterMetadata), effectChanged = selected.size > 0;
280
+ const authoredEffectChanged = records.some(record => !prepared.effects.references.get(record.transaction.id)?.length &&
281
+ prepared.effects.active.has(record.transaction.id) !== projectedEffects.active.has(record.transaction.id));
282
+ if (!authoredEffectChanged) return {ok: true, splices: [], after: prepared.source, actIds: [...requested],
283
+ sourceTransactionIds: [...selected], metadata, afterMetadata, effectChanged, unchanged: !metadata && !effectChanged,
284
+ superseded: [], overlaps: []};
285
+ 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
+ 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,
356
+ 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')};
525
+ } catch (_) { return historyFailure('history_invalid'); }
526
+ }