@ibgib/core-gib 0.1.40 → 0.1.42

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,592 @@
1
+ /**
2
+ * @module sync-conflict-text-merge.respec
3
+ *
4
+ * Verifies text merge (LCS algorithm) in SyncSagaCoordinator.
5
+ *
6
+ * Tests ibgib.data.text conflicts with various text patterns, which with
7
+ * optimistic merging. This should trigger the automatic text merging which is
8
+ * different than the replay of dna for other types of conflict grafts.
9
+ */
10
+
11
+ import {
12
+ respecfully, lastOfAll, ifWe, iReckon,
13
+ ifWeMight
14
+ } from '@ibgib/helper-gib/dist/respec-gib/respec-gib.mjs';
15
+ const maam = `[${import.meta.url}]`, sir = maam;
16
+ import { clone, delay, extractErrorMsg, pretty } from '@ibgib/helper-gib/dist/helpers/utils-helper.mjs';
17
+ import { getIbGibAddr } from '@ibgib/ts-gib/dist/helper.mjs';
18
+ import { IbGibData_V1, IbGib_V1 } from '@ibgib/ts-gib/dist/V1/types.mjs';
19
+
20
+ import { GLOBAL_LOG_A_LOT } from '../core-constants.mjs';
21
+ import { TestTransformer, getTestKeystoneServiceHelper } from '../test-helpers.mjs';
22
+ import { TestMut8Info, } from '../test-types.mjs';
23
+ import { SyncSagaCoordinator } from './sync-saga-coordinator.mjs';
24
+ import { getFromSpace } from '../witness/space/space-helper.mjs';
25
+ import { Metaspace_Innerspace } from '../witness/space/metaspace/metaspace-innerspace/metaspace-innerspace.mjs';
26
+ import { InnerSpace_V1 } from '../witness/space/inner-space/inner-space-v1.mjs';
27
+ import { SyncPeerInnerspace_V1 } from './sync-peer/sync-peer-innerspace/sync-peer-innerspace-v1.mjs';
28
+ import { DEFAULT_INNER_SPACE_DATA_V1 } from '../witness/space/inner-space/inner-space-types.mjs';
29
+ import { getIbGibsFromCache_fallbackToSpaces, getTjpAddr, } from '../common/other/ibgib-helper.mjs';
30
+ import { SyncSagaInfo } from './sync-types.mjs';
31
+ import { getDependencyGraph, graphsAreEquivalent } from '../common/other/graph-helper.mjs';
32
+ import { SYNC_PEER_INNERSPACE_DEFAULT_DATA_V1 } from './sync-peer/sync-peer-innerspace/sync-peer-innerspace-constants.mjs';
33
+ import { SyncConflictStrategy } from './sync-constants.mjs';
34
+
35
+ const logalot = GLOBAL_LOG_A_LOT;
36
+ const lc = sir;
37
+
38
+ interface TestData extends IbGibData_V1 {
39
+ /**
40
+ * text field that will be used for LCS merge testing
41
+ */
42
+ text?: string;
43
+ /**
44
+ * additional text field for multi-field testing
45
+ */
46
+ description?: string;
47
+ }
48
+
49
+ await respecfully(sir, `Text merge (LCS) conflict resolution`, async () => {
50
+
51
+ // #region Init/Setup
52
+
53
+ const metaspace = new Metaspace_Innerspace(undefined);
54
+ await metaspace.initialize({
55
+ getFnAlert: () => async ({ title, msg }) => { console.log(`[Alert] ${title}: ${msg}`); },
56
+ getFnPrompt: () => async ({ title, msg }) => { console.log(`[Prompt] ${title}: ${msg}`); return ''; },
57
+ getFnPromptPassword: () => async (title, msg) => { console.log(`[PromptPwd] ${title}: ${msg}`); return null; },
58
+ });
59
+ while (!metaspace.initialized) { await delay(10); }
60
+
61
+ const defaultLocalUserSpace = await metaspace.getLocalUserSpace({ lock: false });
62
+ await defaultLocalUserSpace!.initialized;
63
+
64
+ const sourceSpace = new InnerSpace_V1({
65
+ ...DEFAULT_INNER_SPACE_DATA_V1,
66
+ name: 'source',
67
+ uuid: 'source_uuid',
68
+ description: 'source test space',
69
+ });
70
+ await sourceSpace.initialized;
71
+
72
+ const destSpace = new InnerSpace_V1({
73
+ ...DEFAULT_INNER_SPACE_DATA_V1,
74
+ name: 'dest',
75
+ uuid: 'dest_uuid',
76
+ description: 'dest test space',
77
+ });
78
+ await destSpace.initialized;
79
+
80
+ const testTransformer = new TestTransformer(metaspace, sourceSpace, destSpace);
81
+
82
+ if (logalot) { console.log(`${lc} Setting up Coordinators...`); }
83
+ const mockKeystone = await getTestKeystoneServiceHelper();
84
+ const senderCoordinator = new SyncSagaCoordinator(mockKeystone);
85
+ const receiverCoordinator = new SyncSagaCoordinator(mockKeystone);
86
+
87
+ async function newTestPeer(): Promise<SyncPeerInnerspace_V1> {
88
+ const peer = new SyncPeerInnerspace_V1(clone(SYNC_PEER_INNERSPACE_DEFAULT_DATA_V1));
89
+ await peer.initialized;
90
+ await peer.initializeSender({
91
+ senderSpace: sourceSpace,
92
+ receiverSpace: destSpace,
93
+ receiverCoordinator: receiverCoordinator,
94
+ receiverMetaspace: metaspace,
95
+ });
96
+ return peer;
97
+ }
98
+
99
+ // #endregion Init/Setup
100
+
101
+ // #region Round 1: Seed common history & initial text
102
+
103
+ const _r1 = testTransformer.newRound({
104
+ name: 'r1_seed_common_text',
105
+ description: 'Create timeline with initial text, sync to establish common history'
106
+ });
107
+
108
+ if (logalot) { console.log(`${lc} R1: Creating initial alpha with text...`); }
109
+
110
+ // Create alpha with initial text on source
111
+ const INITIAL_TEXT = "This is the initial quiz question.\nIt has multiple lines.\nStudents will answer this.";
112
+
113
+ const r1_alpha_v0_source = await testTransformer.create({
114
+ atom: 'alpha',
115
+ in: 'source',
116
+ data: { text: INITIAL_TEXT },
117
+ name: 'r1_alpha_v0_source'
118
+ });
119
+ const alpha_tjpAddr = getTjpAddr({ ibGib: r1_alpha_v0_source.ibGib, defaultIfNone: 'incomingAddr' })!;
120
+
121
+ if (logalot) { console.log(`${lc} R1: Syncing initial alpha to dest...`); }
122
+
123
+ let r1_syncSaga: SyncSagaInfo | undefined;
124
+ try {
125
+ r1_syncSaga = await senderCoordinator.sync({
126
+ peer: await newTestPeer(),
127
+ localSpace: sourceSpace,
128
+ metaspace,
129
+ domainIbGibs: [r1_alpha_v0_source.ibGib],
130
+ conflictStrategy: SyncConflictStrategy.optimisticWithLCS,
131
+ useSessionIdentity: false,
132
+ });
133
+ await r1_syncSaga.done;
134
+ if (logalot) { console.log(`${lc} R1 Sync Complete.`); }
135
+ } catch (e) {
136
+ console.error(`${lc} R1 Sync Failed:`, e);
137
+ iReckon(sir, false).asTo(`R1 Sync failed: ${e}`).isGonnaBeTruthy();
138
+ return;
139
+ }
140
+
141
+ // Verify both sides have same text
142
+ await respecfully(sir, `r1 verify post`, async () => {
143
+ try {
144
+ const r1_alpha_sourceKV = await senderCoordinator.getKnowledgeMap({
145
+ space: sourceSpace,
146
+ metaspace,
147
+ domainIbGibs: [r1_alpha_v0_source.ibGib]
148
+ });
149
+ const r1_alpha_source_tipAddr = r1_alpha_sourceKV[alpha_tjpAddr];
150
+ if (!r1_alpha_source_tipAddr) {
151
+ ifWeMight(sir, 'r1_alpha_source_tipAddr is falsy?', async () => {
152
+ iReckon(sir, true).asTo('fail').isGonnaBeFalse();
153
+ });
154
+ return; /* <<<< returns early */
155
+ }
156
+
157
+ const r1_alpha_destKV = await senderCoordinator.getKnowledgeMap({
158
+ space: destSpace,
159
+ metaspace,
160
+ domainIbGibs: [r1_alpha_v0_source.ibGib]
161
+ });
162
+ const r1_alpha_dest_tipAddr = r1_alpha_destKV[alpha_tjpAddr];
163
+ if (!r1_alpha_dest_tipAddr) {
164
+ ifWeMight(sir, 'r1_alpha_dest_tipAddr is falsy?', async () => {
165
+ iReckon(sir, true).asTo('fail').isGonnaBeFalse();
166
+ });
167
+ return; /* <<<< returns early */
168
+ }
169
+
170
+ await ifWeMight(sir, 'r1 tip addrs match', async () => {
171
+ iReckon(sir, r1_alpha_source_tipAddr).asTo('R1 source/dest have same tip').isGonnaBe(r1_alpha_dest_tipAddr);
172
+ });
173
+
174
+ await ifWeMight(sir, 'r1 text synced correctly', async () => {
175
+ if (!r1_alpha_dest_tipAddr) {
176
+ throw new Error(`r1_dest_tipAddr is null/undefined (E: 1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d)`);
177
+ }
178
+ const resGet_destTip = await getFromSpace({ space: destSpace, addr: r1_alpha_dest_tipAddr });
179
+ const destTipIbGib = resGet_destTip.ibGibs![0] as IbGib_V1<TestData>;
180
+
181
+ iReckon(sir, destTipIbGib.data!.text).asTo('Dest has initial text').isGonnaBe(INITIAL_TEXT);
182
+ });
183
+
184
+ await ifWeMight(sir, 'r1 dep graphs synced', async () => {
185
+ const [r1_alpha_source_tip] = await getIbGibsFromCache_fallbackToSpaces({
186
+ addrs: [r1_alpha_source_tipAddr],
187
+ space: sourceSpace,
188
+ });
189
+ const [r1_alpha_dest_tip] = await getIbGibsFromCache_fallbackToSpaces({
190
+ addrs: [r1_alpha_dest_tipAddr],
191
+ space: destSpace,
192
+ });
193
+ iReckon(sir, r1_alpha_dest_tip).asTo('alpha tip exists in dest').isGonnaBeTruthy();
194
+ if (r1_alpha_dest_tip) {
195
+ const r1_alpha_source_depGraph = await getDependencyGraph({
196
+ ibGib: r1_alpha_source_tip,
197
+ live: true,
198
+ space: sourceSpace
199
+ });
200
+ const r1_alpha_dest_depGraph = await getDependencyGraph({
201
+ ibGib: r1_alpha_dest_tip,
202
+ live: true,
203
+ space: destSpace
204
+ });
205
+ const graphsEqual = graphsAreEquivalent({
206
+ graphA: r1_alpha_source_depGraph,
207
+ graphB: r1_alpha_dest_depGraph,
208
+ slowButThorough: true,
209
+ });
210
+ iReckon(sir, graphsEqual).asTo('R1 dep graphs equal').isGonnaBeTrue();
211
+ }
212
+ });
213
+ } catch (error) {
214
+ console.error(`${lc} ${extractErrorMsg(error)}`);
215
+ iReckon(sir, true).asTo('R1 verify errored').isGonnaBeFalse();
216
+ }
217
+ });
218
+
219
+ // #endregion Round 1: Seed common history & initial text
220
+
221
+ // #region Round 2: Simple append - different parts of text
222
+
223
+ const _r2 = testTransformer.newRound({
224
+ name: 'r2_simple_append',
225
+ description: 'Source appends to end, dest appends to beginning - should merge both'
226
+ });
227
+
228
+ // #region r2 source edits
229
+
230
+ if (logalot) { console.log(`${lc} R2: Source appending to end of text...`); }
231
+
232
+ // const resGet_r2_source = await getFromSpace({ space: sourceSpace, addr: alpha_tjpAddr });
233
+ // const r2_v0_graft = resGet_r2_source.ibGibs![0] as IbGib_V1<TestData>;
234
+
235
+ const SOURCE_APPEND = "\nInstructor added hint.";
236
+ const r2_alpha_v1_source_appendedText = await testTransformer.mut8({
237
+ ibGib: r1_alpha_v0_source.ibGib,
238
+ in: 'source',
239
+ strField: { name: 'text', value: INITIAL_TEXT + SOURCE_APPEND },
240
+ name: 'r2_alpha_v1_source_appendedText',
241
+ });
242
+
243
+ // #endregion r2 source edits
244
+
245
+ // #region r2 dest edits
246
+
247
+ if (logalot) { console.log(`${lc} R2: Dest prepending to beginning of text...`); }
248
+
249
+ const resGet_r2_dest = await getFromSpace({ space: destSpace, addr: r1_alpha_v0_source.addr });
250
+ const r2_v0_alpha_dest = resGet_r2_dest.ibGibs![0] as IbGib_V1<TestData>;
251
+
252
+ const DEST_PREPEND = "Student note: Confusing!\n";
253
+ const r2_alpha_v1_dest_prependedText = await testTransformer.mut8({
254
+ ibGib: r2_v0_alpha_dest,
255
+ in: 'dest',
256
+ strField: { name: 'text', value: DEST_PREPEND + INITIAL_TEXT },
257
+ name: 'r2_alpha_v1_dest_prependedText',
258
+ });
259
+
260
+ // #endregion r2 dest edits
261
+
262
+ await respecfully(sir, `r2 verify pre`, async () => {
263
+ await ifWeMight(sir, 'texts as expected', async () => {
264
+ // before the sync, each side only has their edit. after the sync,
265
+ // both sides should have both prepended and appended text
266
+ iReckon(sir, r2_alpha_v1_source_appendedText.ibGib.data?.text.includes(INITIAL_TEXT)).asTo('alpha source has initial text').isGonnaBeTrue();
267
+ iReckon(sir, r2_alpha_v1_source_appendedText.ibGib.data?.text.includes(SOURCE_APPEND)).asTo('alpha source has appended text').isGonnaBeTrue();
268
+ iReckon(sir, r2_alpha_v1_source_appendedText.ibGib.data?.text.includes(DEST_PREPEND)).asTo('alpha source does NOT have prepended text').isGonnaBeFalse();
269
+ iReckon(sir, r2_alpha_v1_dest_prependedText.ibGib.data?.text.includes(INITIAL_TEXT)).asTo('alpha dest has initial text').isGonnaBeTrue();
270
+ iReckon(sir, r2_alpha_v1_dest_prependedText.ibGib.data?.text.includes(SOURCE_APPEND)).asTo('alpha dest does NOT have appended text').isGonnaBeFalse();
271
+ iReckon(sir, r2_alpha_v1_dest_prependedText.ibGib.data?.text.includes(DEST_PREPEND)).asTo('alpha dest has prepended text').isGonnaBeTrue();
272
+ });
273
+ });
274
+
275
+ if (logalot) { console.log(`${lc} Running r2 Sync (simple data.text conflict)...`); }
276
+
277
+ let r2_syncSaga: SyncSagaInfo | undefined;
278
+ try {
279
+ r2_syncSaga = await senderCoordinator.sync({
280
+ peer: await newTestPeer(),
281
+ localSpace: sourceSpace,
282
+ metaspace,
283
+ domainIbGibs: [r2_alpha_v1_source_appendedText.ibGib],
284
+ conflictStrategy: SyncConflictStrategy.optimisticWithLCS,
285
+ useSessionIdentity: false,
286
+ });
287
+ await r2_syncSaga.done;
288
+ } catch (e) {
289
+ console.error(`${lc} R2 Sync Failed:`, e);
290
+ iReckon(sir, false).asTo(`R2 failed: ${e}`).isGonnaBeTruthy();
291
+ return;
292
+ }
293
+
294
+ await respecfully(sir, `r2 verify post`, async () => {
295
+ const kv_source = await senderCoordinator.getKnowledgeMap({ space: sourceSpace, metaspace, domainIbGibs: [r1_alpha_v0_source.ibGib] });
296
+ const kv_dest = await senderCoordinator.getKnowledgeMap({ space: destSpace, metaspace, domainIbGibs: [r1_alpha_v0_source.ibGib] });
297
+ const r2_alpha_source_tipAddr = kv_source[alpha_tjpAddr];
298
+ if (!r2_alpha_source_tipAddr) {
299
+ await ifWeMight(sir, 'r2_alpha_source_tipAddr falsy?', async () => {
300
+ iReckon(sir, true).asTo('fails').isGonnaBe(false);
301
+ });
302
+ return; /* <<<< returns early */
303
+ }
304
+ const r2_alpha_dest_tipAddr = kv_dest[alpha_tjpAddr];
305
+ if (!r2_alpha_dest_tipAddr) {
306
+ await ifWeMight(sir, 'r2_alpha_dest_tipAddr falsy?', async () => {
307
+ iReckon(sir, true).asTo('fails').isGonnaBe(false);
308
+ });
309
+ return; /* <<<< returns early */
310
+ }
311
+ const [r2_alpha_source_tip] = await getIbGibsFromCache_fallbackToSpaces({
312
+ addrs: [r2_alpha_source_tipAddr],
313
+ space: sourceSpace,
314
+ });
315
+ const [r2_alpha_dest_tip] = await getIbGibsFromCache_fallbackToSpaces({
316
+ addrs: [r2_alpha_dest_tipAddr],
317
+ space: sourceSpace,
318
+ });
319
+
320
+ await ifWeMight(sir, 'r2 tip addrs match', async () => {
321
+ iReckon(sir, r2_alpha_source_tipAddr).asTo('alpha').isGonnaBe(r2_alpha_dest_tipAddr);
322
+ });
323
+
324
+ await ifWeMight(sir, 'r2 text merged correctly', async () => {
325
+ // before the sync, each side only has their edit. after the sync,
326
+ // both sides should have both prepended and appended text
327
+ iReckon(sir, r2_alpha_v1_source_appendedText.ibGib.data?.text.includes(INITIAL_TEXT)).asTo('alpha source has initial text').isGonnaBeTrue();
328
+ iReckon(sir, r2_alpha_v1_source_appendedText.ibGib.data?.text.includes(SOURCE_APPEND)).asTo('alpha source has appended text').isGonnaBeTrue();
329
+ iReckon(sir, r2_alpha_v1_source_appendedText.ibGib.data?.text.includes(DEST_PREPEND)).asTo('alpha source has prepended text').isGonnaBeFalse();
330
+ iReckon(sir, r2_alpha_v1_dest_prependedText.ibGib.data?.text.includes(INITIAL_TEXT)).asTo('alpha dest has initial text').isGonnaBeTrue();
331
+ iReckon(sir, r2_alpha_v1_dest_prependedText.ibGib.data?.text.includes(SOURCE_APPEND)).asTo('alpha dest has appended text').isGonnaBeFalse();
332
+ iReckon(sir, r2_alpha_v1_dest_prependedText.ibGib.data?.text.includes(DEST_PREPEND)).asTo('alpha dest has prepended text').isGonnaBeTrue();
333
+
334
+ // actually both should be the same text now of course
335
+ iReckon(sir, r2_alpha_source_tip.data?.text).asTo('source/dest same text').isGonnaBe(r2_alpha_dest_tip.data?.text);
336
+
337
+ const text = r2_alpha_source_tip.data?.text;
338
+
339
+ // we'll go one step further and assume the final output is exactly this
340
+ iReckon(sir, text).asTo('R2 has both prepend and append').isGonnaBe(DEST_PREPEND + INITIAL_TEXT + SOURCE_APPEND);
341
+ });
342
+
343
+ await ifWeMight(sir, 'r2 dep graphs synced', async () => {
344
+ // alpha's full dep graph should exist on dest
345
+ const depGraph_alpha_source = await getDependencyGraph({
346
+ ibGib: r2_alpha_source_tip,
347
+ live: true,
348
+ space: sourceSpace
349
+ });
350
+ const depGraph_alpha_dest = await getDependencyGraph({
351
+ ibGib: r2_alpha_dest_tip,
352
+ live: true,
353
+ space: destSpace
354
+ });
355
+ const alphaDepsAreEqual = graphsAreEquivalent({
356
+ graphA: depGraph_alpha_source,
357
+ graphB: depGraph_alpha_dest,
358
+ slowButThorough: true,
359
+ });
360
+ iReckon(sir, alphaDepsAreEqual).asTo('alpha got synced and graphs are equal').isGonnaBeTrue();
361
+ });
362
+ });
363
+
364
+ // #endregion Round 2: Simple append - different parts of text
365
+
366
+ // #region Round 3: Interleaved edits - different paragraphs
367
+
368
+ // const _r3 = testTransformer.newRound({
369
+ // name: 'r3_interleaved_paragraphs',
370
+ // description: 'Source edits paragraph 1, dest edits paragraph 2 - should merge both changes'
371
+ // });
372
+
373
+ // // #region r3 source edits
374
+
375
+ // if (logalot) { console.log(`${lc} R3: Source editing first paragraph...`); }
376
+
377
+ // const resGet_r3_source = await getFromSpace({ space: sourceSpace, addr: alpha_tjpAddr });
378
+ // const r3_v0_graft = resGet_r3_source.ibGibs![0] as IbGib_V1<TestData>;
379
+ // const r3_currentText = r3_v0_graft.data!.text!;
380
+
381
+ // // Split into lines, modify first paragraph (line 1)
382
+ // const lines = r3_currentText.split('\n');
383
+ // lines[0] = "This is the UPDATED quiz question."; // Source edits line 1
384
+ // const r3_sourceModifiedText = lines.join('\n');
385
+
386
+ // const r3_v1_source = await testTransformer.mut8({
387
+ // ibGib: r3_v0_graft,
388
+ // in: 'source',
389
+ // strField: { name: 'text', value: r3_sourceModifiedText },
390
+ // name: 'r3_v1_source',
391
+ // });
392
+
393
+ // // #endregion r3 source edits
394
+
395
+ // // #region r3 dest edits
396
+
397
+ // if (logalot) { console.log(`${lc} R3: Dest editing second paragraph...`); }
398
+
399
+ // const resGet_r3_dest = await getFromSpace({ space: destSpace, addr: alpha_tjpAddr });
400
+ // const r3_v0_graft_fromDest = resGet_r3_dest.ibGibs![0] as IbGib_V1<TestData>;
401
+
402
+ // // Split and modify third line
403
+ // const destLines = r3_v0_graft_fromDest.data!.text!.split('\n');
404
+ // destLines[2] = "Students will DEFINITELY answer this."; // Dest edits line 3
405
+ // const r3_destModifiedText = destLines.join('\n');
406
+
407
+ // const r3_v1_dest = await testTransformer.mut8({
408
+ // ibGib: r3_v0_graft_fromDest,
409
+ // in: 'dest',
410
+ // strField: { name: 'text', value: r3_destModifiedText },
411
+ // name: 'r3_v1_dest',
412
+ // });
413
+
414
+ // // #endregion r3 dest edits
415
+
416
+ // await respecfully(sir, `r3 verify pre`, async () => {
417
+ // await ifWeMight(sir, 'dest has alpha after R2 graft', async () => {
418
+ // const kv = await receiverCoordinator.getKnowledgeMap({ space: destSpace, metaspace, domainIbGibs: [r3_v0_graft_fromDest] });
419
+ // iReckon(sir, !!kv[alpha_tjpAddr]).asTo('dest has tip').isGonnaBeTrue();
420
+ // });
421
+ // });
422
+
423
+ // if (logalot) { console.log(`${lc} Running r3 Sync (interleaved paragraphs)...`); }
424
+
425
+ // let r3_syncSaga: SyncSagaInfo | undefined;
426
+ // try {
427
+ // r3_syncSaga = await senderCoordinator.sync({
428
+ // peer: await newTestPeer(),
429
+ // localSpace: sourceSpace,
430
+ // metaspace,
431
+ // domainIbGibs: [r3_v1_source.ibGib],
432
+ // conflictStrategy: SyncConflictStrategy.optimisticWithLCS,
433
+ // useSessionIdentity: false,
434
+ // });
435
+ // await r3_syncSaga.done;
436
+ // } catch (e) {
437
+ // console.error(`${lc} R3 Sync Failed:`, e);
438
+ // iReckon(sir, false).asTo(`R3 failed: ${e}`).isGonnaBeTruthy();
439
+ // return;
440
+ // }
441
+
442
+ // await respecfully(sir, `r3 verify post`, async () => {
443
+ // const kv_s = await senderCoordinator.getKnowledgeMap({ space: sourceSpace, metaspace, domainIbGibs: [r1_alpha_v0_source.ibGib] });
444
+ // const kv_d = await senderCoordinator.getKnowledgeMap({ space: destSpace, metaspace, domainIbGibs: [r1_alpha_v0_source.ibGib] });
445
+ // const r3_tip_s = kv_s[alpha_tjpAddr];
446
+ // const r3_tip_d = kv_d[alpha_tjpAddr];
447
+
448
+ // await ifWeMight(sir, 'r3 tip addrs match', async () => {
449
+ // iReckon(sir, r3_tip_s).asTo('R3 tips match').isGonnaBe(r3_tip_d);
450
+ // });
451
+
452
+ // await ifWeMight(sir, 'r3 both paragraph edits merged', async () => {
453
+ // const res = await getFromSpace({ space: sourceSpace, addr: r3_tip_s! });
454
+ // const tip = res.ibGibs![0] as IbGib_V1<TestData>;
455
+ // const mergedText = tip.data!.text!;
456
+
457
+ // // Should have BOTH source's line 1 change AND dest's line 3 change
458
+ // iReckon(sir, mergedText.includes("UPDATED quiz question"))
459
+ // .asTo('has source paragraph 1 edit')
460
+ // .isGonnaBeTrue();
461
+ // iReckon(sir, mergedText.includes("DEFINITELY answer this"))
462
+ // .asTo('has dest paragraph 2 edit')
463
+ // .isGonnaBeTrue();
464
+
465
+ // // Verify middle line unchanged
466
+ // iReckon(sir, mergedText.includes("It has multiple lines"))
467
+ // .asTo('middle line preserved')
468
+ // .isGonnaBeTrue();
469
+ // });
470
+
471
+ // await ifWeMight(sir, 'r3 dep graphs synced', async () => {
472
+ // const [d] = await getIbGibsFromCache_fallbackToSpaces({ addrs: [r3_tip_s!], space: destSpace });
473
+ // iReckon(sir, d).asTo('exists dest').isGonnaBeTruthy();
474
+ // });
475
+ // });
476
+
477
+ // #endregion Round 3: Interleaved edits - different paragraphs
478
+
479
+ // #region Round 4: Same paragraph conflict
480
+
481
+ // const _r4 = testTransformer.newRound({
482
+ // name: 'r4_same_paragraph_conflict',
483
+ // description: 'Both edit same paragraph - LCS should merge at word/character level'
484
+ // });
485
+
486
+ // // #region r4 source edits
487
+
488
+ // if (logalot) { console.log(`${lc} R4: Source editing same paragraph...`); }
489
+
490
+ // // TODO: Retrieve from source
491
+ // // TODO: Mutate - edit specific words in paragraph
492
+
493
+ // // #endregion r4 source edits
494
+
495
+ // // #region r4 dest edits
496
+
497
+ // if (logalot) { console.log(`${lc} R4: Dest editing same paragraph (different words)...`); }
498
+
499
+ // // TODO: Retrieve from dest
500
+ // // TODO: Mutate - edit different words in same paragraph
501
+
502
+ // // #endregion r4 dest edits
503
+
504
+ // await respecfully(sir, `r4 verify pre`, async () => {
505
+ // await ifWeMight(sir, 'dest has alpha after R3 graft', async () => {
506
+ // // TODO: Verify pre-sync state
507
+ // });
508
+ // });
509
+
510
+ // if (logalot) { console.log(`${lc} Running r4 Sync (same paragraph conflict)...`); }
511
+
512
+ // // TODO: Add sync operation
513
+
514
+ // await respecfully(sir, `r4 verify post`, async () => {
515
+ // await ifWeMight(sir, 'r4 tip addrs match', async () => {
516
+ // // TODO: Verify tips match
517
+ // });
518
+
519
+ // await ifWeMight(sir, 'r4 LCS merged both word changes', async () => {
520
+ // // TODO: Verify text has both source and dest word changes
521
+ // // TODO: Verify LCS algorithm preserved both edits correctly
522
+ // });
523
+
524
+ // await ifWeMight(sir, 'r4 dep graphs synced', async () => {
525
+ // // TODO: Verify dep graphs
526
+ // });
527
+ // });
528
+
529
+ // #endregion Round 4: Same paragraph conflict
530
+
531
+ // #region Round 5: Multi-field text merge
532
+
533
+ // const _r5 = testTransformer.newRound({
534
+ // name: 'r5_multi_field_text',
535
+ // description: 'Conflicting text edits in BOTH text and description fields simultaneously'
536
+ // });
537
+
538
+ // // #region r5 source edits
539
+
540
+ // if (logalot) { console.log(`${lc} R5: Source editing both text and description...`); }
541
+
542
+ // // TODO: Retrieve from source
543
+ // // TODO: Mutate text field
544
+ // // TODO: Mutate description field (in same mut8 or separate)
545
+
546
+ // // #endregion r5 source edits
547
+
548
+ // // #region r5 dest edits
549
+
550
+ // if (logalot) { console.log(`${lc} R5: Dest editing both text and description differently...`); }
551
+
552
+ // // TODO: Retrieve from dest
553
+ // // TODO: Mutate text field (different changes)
554
+ // // TODO: Mutate description field (different changes)
555
+
556
+ // // #endregion r5 dest edits
557
+
558
+ // await respecfully(sir, `r5 verify pre`, async () => {
559
+ // await ifWeMight(sir, 'dest has alpha after R4 graft', async () => {
560
+ // // TODO: Verify pre-sync state
561
+ // });
562
+ // });
563
+
564
+ // if (logalot) { console.log(`${lc} Running r5 Sync (multi-field text merge)...`); }
565
+
566
+ // // TODO: Add sync operation
567
+
568
+ // await respecfully(sir, `r5 verify post`, async () => {
569
+ // await ifWeMight(sir, 'r5 tip addrs match', async () => {
570
+ // // TODO: Verify tips match
571
+ // });
572
+
573
+ // await ifWeMight(sir, 'r5 text field merged', async () => {
574
+ // // TODO: Verify text field has both source and dest changes
575
+ // });
576
+
577
+ // await ifWeMight(sir, 'r5 description field merged', async () => {
578
+ // // TODO: Verify description field has both source and dest changes
579
+ // });
580
+
581
+ // await ifWeMight(sir, 'r5 both fields independently LCS-merged', async () => {
582
+ // // TODO: Verify each field was merged independently
583
+ // // TODO: Ensure one field's merge didn't affect the other
584
+ // });
585
+
586
+ // await ifWeMight(sir, 'r5 dep graphs synced', async () => {
587
+ // // TODO: Verify dep graphs
588
+ // });
589
+ // });
590
+
591
+ // #endregion Round 5: Multi-field text merge
592
+ });