glossarist 0.4.15 → 0.4.16

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,609 @@
1
+ import { GlossaristModel } from '../models/base.js';
2
+ import { Added, Removed, Changed } from './change.js';
3
+ import { ListDiff, diffList, diffSet } from './list-diff.js';
4
+
5
+ const CONCEPT_METADATA_FIELDS = Object.freeze([
6
+ 'status', 'term', 'uri', 'schemaVersion',
7
+ ]);
8
+
9
+ const LOCALIZATION_METADATA_FIELDS = Object.freeze([
10
+ 'entryStatus',
11
+ 'classification',
12
+ 'reviewType',
13
+ 'domain',
14
+ 'release',
15
+ 'lineageSourceSimilarity',
16
+ 'script',
17
+ 'system',
18
+ 'reviewDate',
19
+ 'reviewDecisionDate',
20
+ 'reviewDecisionEvent',
21
+ 'reviewStatus',
22
+ 'reviewDecision',
23
+ 'reviewDecisionNotes',
24
+ ]);
25
+
26
+ export class DiffStats extends GlossaristModel {
27
+ constructor(data = {}) {
28
+ super();
29
+ this._added = data.added ?? 0;
30
+ this._removed = data.removed ?? 0;
31
+ this._changed = data.changed ?? 0;
32
+ }
33
+
34
+ get added() { return this._added; }
35
+ get removed() { return this._removed; }
36
+ get changed() { return this._changed; }
37
+ get total() { return this._added + this._removed + this._changed; }
38
+
39
+ toJSON() {
40
+ return { added: this._added, removed: this._removed, changed: this._changed };
41
+ }
42
+
43
+ static fromJSON(data) {
44
+ return new DiffStats(data ?? {});
45
+ }
46
+ }
47
+
48
+ export class MetadataDiff extends GlossaristModel {
49
+ constructor(data = {}) {
50
+ super();
51
+ this._changes = {};
52
+ const raw = data.changes ?? data ?? {};
53
+ for (const [field, change] of Object.entries(raw)) {
54
+ this._changes[field] = change instanceof Changed ? change : Changed.fromJSON(change);
55
+ }
56
+ }
57
+
58
+ get changes() {
59
+ return this._changes;
60
+ }
61
+
62
+ get hasChanges() {
63
+ return Object.keys(this._changes).length > 0;
64
+ }
65
+
66
+ get count() {
67
+ return Object.keys(this._changes).length;
68
+ }
69
+
70
+ *walk(section) {
71
+ for (const [field, change] of Object.entries(this._changes)) {
72
+ const path = section ? `${section}.${field}` : field;
73
+ yield { path, change };
74
+ }
75
+ }
76
+
77
+ toJSON() {
78
+ const obj = {};
79
+ for (const [field, change] of Object.entries(this._changes)) {
80
+ obj[field] = change.toJSON();
81
+ }
82
+ return obj;
83
+ }
84
+
85
+ static fromJSON(data) {
86
+ return new MetadataDiff({ changes: data ?? {} });
87
+ }
88
+ }
89
+
90
+ export class ConceptLevelDiff extends GlossaristModel {
91
+ constructor(data = {}) {
92
+ super();
93
+ this._sources = wrapListDiff(data.sources);
94
+ this._dates = wrapListDiff(data.dates);
95
+ this._relatedConcepts = wrapListDiff(data.relatedConcepts ?? data.related_concepts);
96
+ this._groups = wrapListDiff(data.groups);
97
+ this._sections = wrapListDiff(data.sections);
98
+ this._tags = wrapListDiff(data.tags);
99
+ this._metadata = data.metadata instanceof MetadataDiff
100
+ ? data.metadata
101
+ : MetadataDiff.fromJSON(data.metadata ?? {});
102
+ }
103
+
104
+ get sources() { return this._sources; }
105
+ get dates() { return this._dates; }
106
+ get relatedConcepts() { return this._relatedConcepts; }
107
+ get groups() { return this._groups; }
108
+ get sections() { return this._sections; }
109
+ get tags() { return this._tags; }
110
+ get metadata() { return this._metadata; }
111
+
112
+ get hasChanges() {
113
+ return this._sources.hasChanges
114
+ || this._dates.hasChanges
115
+ || this._relatedConcepts.hasChanges
116
+ || this._groups.hasChanges
117
+ || this._sections.hasChanges
118
+ || this._tags.hasChanges
119
+ || this._metadata.hasChanges;
120
+ }
121
+
122
+ *walk() {
123
+ yield* walkList('concept.sources', this._sources);
124
+ yield* walkList('concept.dates', this._dates);
125
+ yield* walkList('concept.relatedConcepts', this._relatedConcepts);
126
+ yield* walkList('concept.groups', this._groups);
127
+ yield* walkList('concept.sections', this._sections);
128
+ yield* walkList('concept.tags', this._tags);
129
+ yield* this._metadata.walk('concept.metadata');
130
+ }
131
+
132
+ toJSON() {
133
+ return {
134
+ sources: this._sources.toJSON(),
135
+ dates: this._dates.toJSON(),
136
+ related_concepts: this._relatedConcepts.toJSON(),
137
+ groups: this._groups.toJSON(),
138
+ sections: this._sections.toJSON(),
139
+ tags: this._tags.toJSON(),
140
+ metadata: this._metadata.toJSON(),
141
+ };
142
+ }
143
+
144
+ static fromJSON(data) {
145
+ return new ConceptLevelDiff(data);
146
+ }
147
+ }
148
+
149
+ export class LocalizedConceptDiff extends GlossaristModel {
150
+ constructor(data = {}) {
151
+ super();
152
+ this.languageCode = data.languageCode ?? data.language_code ?? null;
153
+ this._designations = wrapListDiff(data.designations);
154
+ this._definitions = wrapListDiff(data.definitions);
155
+ this._notes = wrapListDiff(data.notes);
156
+ this._examples = wrapListDiff(data.examples);
157
+ this._sources = wrapListDiff(data.sources);
158
+ this._dates = wrapListDiff(data.dates);
159
+ this._related = wrapListDiff(data.related);
160
+ this._metadata = data.metadata instanceof MetadataDiff
161
+ ? data.metadata
162
+ : MetadataDiff.fromJSON(data.metadata ?? {});
163
+ this._totalItems = data.totalItems ?? data.total_items ?? 0;
164
+ }
165
+
166
+ get designations() { return this._designations; }
167
+ get definitions() { return this._definitions; }
168
+ get notes() { return this._notes; }
169
+ get examples() { return this._examples; }
170
+ get sources() { return this._sources; }
171
+ get dates() { return this._dates; }
172
+ get related() { return this._related; }
173
+ get metadata() { return this._metadata; }
174
+
175
+ get hasChanges() {
176
+ return this._designations.hasChanges
177
+ || this._definitions.hasChanges
178
+ || this._notes.hasChanges
179
+ || this._examples.hasChanges
180
+ || this._sources.hasChanges
181
+ || this._dates.hasChanges
182
+ || this._related.hasChanges
183
+ || this._metadata.hasChanges;
184
+ }
185
+
186
+ get stats() {
187
+ return collectStats(this.walk());
188
+ }
189
+
190
+ get similarity() {
191
+ return computeSimilarity(this.stats.total, this._totalItems);
192
+ }
193
+
194
+ *walk(prefix) {
195
+ const base = prefix ?? '';
196
+ yield* walkList(`${base}.designations`, this._designations);
197
+ yield* walkList(`${base}.definitions`, this._definitions);
198
+ yield* walkList(`${base}.notes`, this._notes);
199
+ yield* walkList(`${base}.examples`, this._examples);
200
+ yield* walkList(`${base}.sources`, this._sources);
201
+ yield* walkList(`${base}.dates`, this._dates);
202
+ yield* walkList(`${base}.related`, this._related);
203
+ yield* this._metadata.walk(`${base}.metadata`);
204
+ }
205
+
206
+ toJSON() {
207
+ const obj = {};
208
+ if (this.languageCode != null) obj.language_code = this.languageCode;
209
+ obj.designations = this._designations.toJSON();
210
+ obj.definitions = this._definitions.toJSON();
211
+ obj.notes = this._notes.toJSON();
212
+ obj.examples = this._examples.toJSON();
213
+ obj.sources = this._sources.toJSON();
214
+ obj.dates = this._dates.toJSON();
215
+ obj.related = this._related.toJSON();
216
+ obj.metadata = this._metadata.toJSON();
217
+ obj.total_items = this._totalItems;
218
+ return obj;
219
+ }
220
+
221
+ static fromJSON(data) {
222
+ return new LocalizedConceptDiff(data);
223
+ }
224
+ }
225
+
226
+ export class ConceptDiff extends GlossaristModel {
227
+ constructor(data = {}) {
228
+ super();
229
+ this.oldId = data.oldId ?? data.old_id ?? null;
230
+ this.newId = data.newId ?? data.new_id ?? null;
231
+ this._concept = data.concept instanceof ConceptLevelDiff
232
+ ? data.concept
233
+ : ConceptLevelDiff.fromJSON(data.concept ?? {});
234
+ this._languages = wrapListDiff(data.languages);
235
+ this._localizations = {};
236
+ const raw = data.localizations ?? {};
237
+ for (const [lang, lcDiff] of Object.entries(raw)) {
238
+ this._localizations[lang] = lcDiff instanceof LocalizedConceptDiff
239
+ ? lcDiff
240
+ : LocalizedConceptDiff.fromJSON(lcDiff);
241
+ }
242
+ this._totalItems = data.totalItems ?? data.total_items ?? 0;
243
+ }
244
+
245
+ get oldId() { return this._oldId; }
246
+ get newId() { return this._newId; }
247
+ get concept() { return this._concept; }
248
+ get languages() { return this._languages; }
249
+ get localizations() { return this._localizations; }
250
+
251
+ set oldId(v) { this._oldId = v; }
252
+ set newId(v) { this._newId = v; }
253
+
254
+ get localizationLanguages() {
255
+ return Object.keys(this._localizations);
256
+ }
257
+
258
+ get hasChanges() {
259
+ return this._concept.hasChanges
260
+ || this._languages.hasChanges
261
+ || Object.values(this._localizations).some(lc => lc.hasChanges);
262
+ }
263
+
264
+ localization(lang) {
265
+ return this._localizations[lang] ?? null;
266
+ }
267
+
268
+ get stats() {
269
+ return collectStats(this.walk());
270
+ }
271
+
272
+ get similarity() {
273
+ return computeSimilarity(this.stats.total, this._totalItems);
274
+ }
275
+
276
+ *walk() {
277
+ yield* this._concept.walk();
278
+ yield* walkList('languages', this._languages);
279
+ for (const [lang, lc] of Object.entries(this._localizations)) {
280
+ for (const { path, change } of lc.walk(`localizations.${lang}`)) {
281
+ yield { path, change, language: lang };
282
+ }
283
+ }
284
+ }
285
+
286
+ toJSON() {
287
+ const obj = {};
288
+ if (this._oldId != null) obj.old_id = this._oldId;
289
+ if (this._newId != null) obj.new_id = this._newId;
290
+ obj.concept = this._concept.toJSON();
291
+ obj.languages = this._languages.toJSON();
292
+ obj.localizations = {};
293
+ for (const [lang, lc] of Object.entries(this._localizations)) {
294
+ obj.localizations[lang] = lc.toJSON();
295
+ }
296
+ obj.total_items = this._totalItems;
297
+ return obj;
298
+ }
299
+
300
+ static fromJSON(data) {
301
+ return new ConceptDiff(data);
302
+ }
303
+ }
304
+
305
+ export function diffConcepts(oldConcept, newConcept, language = 'eng') {
306
+ if (!oldConcept && !newConcept) {
307
+ return new ConceptDiff({});
308
+ }
309
+
310
+ const oldId = oldConcept?.id ?? null;
311
+ const newId = newConcept?.id ?? null;
312
+ const oldLangs = oldConcept?.languages ?? [];
313
+ const newLangs = newConcept?.languages ?? [];
314
+
315
+ let langs;
316
+ if (language === 'all') {
317
+ langs = union(oldLangs, newLangs);
318
+ } else if (oldLangs.includes(language) || newLangs.includes(language)) {
319
+ langs = [language];
320
+ } else if (oldLangs.length === 0 && newLangs.length === 0) {
321
+ langs = [];
322
+ } else {
323
+ langs = ['eng'];
324
+ }
325
+
326
+ const conceptDiff = diffConceptLevel(oldConcept ?? null, newConcept ?? null);
327
+ const languageDiff = diffLanguageSets(oldLangs, newLangs);
328
+ const localizations = {};
329
+ for (const lang of langs) {
330
+ const oldLoc = oldConcept?.localization(lang) ?? null;
331
+ const newLoc = newConcept?.localization(lang) ?? null;
332
+ const lcDiff = diffLocalizedConcepts(oldLoc, newLoc);
333
+ lcDiff._totalItems = countLocalizedItems(oldLoc, newLoc);
334
+ localizations[lang] = lcDiff;
335
+ }
336
+
337
+ const totalItems = countConceptItems(oldConcept ?? null, newConcept ?? null, langs);
338
+
339
+ return new ConceptDiff({
340
+ oldId,
341
+ newId,
342
+ concept: conceptDiff,
343
+ languages: languageDiff,
344
+ localizations,
345
+ totalItems,
346
+ });
347
+ }
348
+
349
+ export function diffLocalizedConcepts(oldLoc, newLoc) {
350
+ if (!oldLoc && !newLoc) {
351
+ return new LocalizedConceptDiff({ languageCode: null });
352
+ }
353
+
354
+ const lang = newLoc?.languageCode ?? oldLoc?.languageCode ?? null;
355
+
356
+ if (!oldLoc) {
357
+ return fullyDiff(newLoc, lang, 'added');
358
+ }
359
+ if (!newLoc) {
360
+ return fullyDiff(oldLoc, lang, 'removed');
361
+ }
362
+
363
+ return new LocalizedConceptDiff({
364
+ languageCode: lang,
365
+ designations: diffDesignations(oldLoc.terms ?? [], newLoc.terms ?? []),
366
+ definitions: diffTextList(oldLoc.definitions ?? [], newLoc.definitions ?? []),
367
+ notes: diffTextList(oldLoc.notes ?? [], newLoc.notes ?? []),
368
+ examples: diffTextList(oldLoc.examples ?? [], newLoc.examples ?? []),
369
+ sources: diffSources(oldLoc.sources ?? [], newLoc.sources ?? []),
370
+ dates: diffDates(oldLoc.dates ?? [], newLoc.dates ?? []),
371
+ related: diffRelated(oldLoc.related ?? [], newLoc.related ?? []),
372
+ metadata: diffMetadata(oldLoc, newLoc, LOCALIZATION_METADATA_FIELDS),
373
+ });
374
+ }
375
+
376
+ function diffConceptLevel(oldConcept, newConcept) {
377
+ if (!oldConcept && !newConcept) {
378
+ return new ConceptLevelDiff({});
379
+ }
380
+
381
+ const Direction = !oldConcept ? 'added' : !newConcept ? 'removed' : null;
382
+
383
+ if (Direction) {
384
+ const c = (oldConcept ?? newConcept);
385
+ return new ConceptLevelDiff({
386
+ sources: fullListDiff(c.sources ?? [], Direction),
387
+ dates: fullListDiff(c.dates ?? [], Direction),
388
+ relatedConcepts: fullListDiff(c.relatedConcepts ?? [], Direction),
389
+ groups: fullListDiff(c.groups ?? [], Direction),
390
+ sections: fullListDiff(c.sections ?? [], Direction),
391
+ tags: fullListDiff(c.tags ?? [], Direction),
392
+ metadata: fullMetadataDiff(c, CONCEPT_METADATA_FIELDS, Direction),
393
+ });
394
+ }
395
+
396
+ return new ConceptLevelDiff({
397
+ sources: diffSources(oldConcept.sources ?? [], newConcept.sources ?? []),
398
+ dates: diffDates(oldConcept.dates ?? [], newConcept.dates ?? []),
399
+ relatedConcepts: diffRelatedConcepts(oldConcept.relatedConcepts ?? [], newConcept.relatedConcepts ?? []),
400
+ groups: diffStringSet(oldConcept.groups ?? [], newConcept.groups ?? []),
401
+ sections: diffStringSet(oldConcept.sections ?? [], newConcept.sections ?? []),
402
+ tags: diffStringSet(oldConcept.tags ?? [], newConcept.tags ?? []),
403
+ metadata: diffMetadata(oldConcept, newConcept, CONCEPT_METADATA_FIELDS),
404
+ });
405
+ }
406
+
407
+ function diffLanguageSets(oldLangs, newLangs) {
408
+ const oldSet = new Set(oldLangs);
409
+ const newSet = new Set(newLangs);
410
+ const added = newLangs
411
+ .filter(l => !oldSet.has(l))
412
+ .sort()
413
+ .map(l => new Added({ value: l }));
414
+ const removed = oldLangs
415
+ .filter(l => !newSet.has(l))
416
+ .sort()
417
+ .map(l => new Removed({ value: l }));
418
+ return new ListDiff({ added, removed, changed: [] });
419
+ }
420
+
421
+ function fullyDiff(loc, lang, direction) {
422
+ const ChangeClass = direction === 'added' ? Added : Removed;
423
+ const key = direction;
424
+ return new LocalizedConceptDiff({
425
+ languageCode: lang,
426
+ designations: new ListDiff({ [key]: (loc.terms ?? []).map(v => new ChangeClass({ value: v })) }),
427
+ definitions: new ListDiff({ [key]: (loc.definitions ?? []).map(v => new ChangeClass({ value: v })) }),
428
+ notes: new ListDiff({ [key]: (loc.notes ?? []).map(v => new ChangeClass({ value: v })) }),
429
+ examples: new ListDiff({ [key]: (loc.examples ?? []).map(v => new ChangeClass({ value: v })) }),
430
+ sources: new ListDiff({ [key]: (loc.sources ?? []).map(v => new ChangeClass({ value: v })) }),
431
+ dates: new ListDiff({ [key]: (loc.dates ?? []).map(v => new ChangeClass({ value: v })) }),
432
+ related: new ListDiff({ [key]: (loc.related ?? []).map(v => new ChangeClass({ value: v })) }),
433
+ metadata: fullMetadataDiff(loc, LOCALIZATION_METADATA_FIELDS, direction),
434
+ });
435
+ }
436
+
437
+ function fullListDiff(items, direction) {
438
+ const ChangeClass = direction === 'added' ? Added : Removed;
439
+ return new ListDiff({ [direction]: items.map(v => new ChangeClass({ value: v })) });
440
+ }
441
+
442
+ function fullMetadataDiff(obj, fields, direction) {
443
+ const changes = {};
444
+ for (const field of fields) {
445
+ const val = obj[field];
446
+ if (val != null) {
447
+ changes[field] = new Changed({
448
+ oldValue: direction === 'added' ? null : val,
449
+ newValue: direction === 'added' ? val : null,
450
+ });
451
+ }
452
+ }
453
+ return new MetadataDiff({ changes });
454
+ }
455
+
456
+ function diffDesignations(oldTerms, newTerms) {
457
+ return diffSet(oldTerms, newTerms, {
458
+ identityKey: designationIdentity,
459
+ textKey: d => d?.designation ?? '',
460
+ });
461
+ }
462
+
463
+ function designationIdentity(d) {
464
+ const type = d?.type ?? 'expression';
465
+ const text = String(d?.designation ?? '').toLowerCase().trim();
466
+ return `${type}|${text}`;
467
+ }
468
+
469
+ function diffTextList(oldItems, newItems) {
470
+ return diffList(oldItems, newItems, {
471
+ textKey: d => d?.content ?? '',
472
+ });
473
+ }
474
+
475
+ function diffSources(oldSources, newSources) {
476
+ return diffSet(oldSources, newSources, {
477
+ identityKey: sourceIdentity,
478
+ });
479
+ }
480
+
481
+ function sourceIdentity(s) {
482
+ const ref = s?.origin?.ref;
483
+ const source = ref?.source ?? '';
484
+ const id = ref?.id ?? '';
485
+ const type = s?.type ?? '';
486
+ return `${type}|${source}|${id}`;
487
+ }
488
+
489
+ function diffDates(oldDates, newDates) {
490
+ return diffSet(oldDates, newDates, {
491
+ identityKey: d => d?.type ?? '',
492
+ textKey: d => d?.date ?? '',
493
+ });
494
+ }
495
+
496
+ function diffRelated(oldRelated, newRelated) {
497
+ return diffSet(oldRelated, newRelated, {
498
+ identityKey: relatedIdentity,
499
+ });
500
+ }
501
+
502
+ function diffRelatedConcepts(oldRC, newRC) {
503
+ return diffSet(oldRC, newRC, {
504
+ identityKey: relatedIdentity,
505
+ });
506
+ }
507
+
508
+ function relatedIdentity(r) {
509
+ const ref = r?.ref;
510
+ return `${r?.type ?? ''}|${ref?.source ?? ''}|${ref?.id ?? ''}`;
511
+ }
512
+
513
+ function diffStringSet(oldStrings, newStrings) {
514
+ return diffSet(oldStrings, newStrings, {
515
+ identityKey: s => String(s),
516
+ });
517
+ }
518
+
519
+ function diffMetadata(oldObj, newObj, fields) {
520
+ const changes = {};
521
+ for (const field of fields) {
522
+ const oldVal = oldObj[field];
523
+ const newVal = newObj[field];
524
+ if (oldVal !== newVal) {
525
+ changes[field] = new Changed({ oldValue: oldVal, newValue: newVal });
526
+ }
527
+ }
528
+ return new MetadataDiff({ changes });
529
+ }
530
+
531
+ function wrapListDiff(data) {
532
+ if (data instanceof ListDiff) return data;
533
+ return ListDiff.fromJSON(data ?? {});
534
+ }
535
+
536
+ function collectStats(walker) {
537
+ let added = 0;
538
+ let removed = 0;
539
+ let changed = 0;
540
+ for (const { change } of walker) {
541
+ if (change.type === 'added') added++;
542
+ else if (change.type === 'removed') removed++;
543
+ else if (change.type === 'changed') changed++;
544
+ }
545
+ return new DiffStats({ added, removed, changed });
546
+ }
547
+
548
+ function computeSimilarity(changeCount, totalItems) {
549
+ if (totalItems <= 0) return 1.0;
550
+ const ratio = Math.min(changeCount / totalItems, 1);
551
+ return Math.round((1 - ratio) * 10000) / 10000;
552
+ }
553
+
554
+ function maxOf(a, b) {
555
+ return Math.max(a ?? 0, b ?? 0);
556
+ }
557
+
558
+ function countConceptItems(oldConcept, newConcept, langs) {
559
+ let count = 0;
560
+
561
+ count += maxOf(oldConcept?.sources?.length, newConcept?.sources?.length);
562
+ count += maxOf(oldConcept?.dates?.length, newConcept?.dates?.length);
563
+ count += maxOf(oldConcept?.relatedConcepts?.length, newConcept?.relatedConcepts?.length);
564
+ count += maxOf(oldConcept?.groups?.length, newConcept?.groups?.length);
565
+ count += maxOf(oldConcept?.sections?.length, newConcept?.sections?.length);
566
+ count += maxOf(oldConcept?.tags?.length, newConcept?.tags?.length);
567
+ count += CONCEPT_METADATA_FIELDS.length;
568
+
569
+ const oldLangs = oldConcept?.languages ?? [];
570
+ const newLangs = newConcept?.languages ?? [];
571
+ count += maxOf(oldLangs.length, newLangs.length);
572
+
573
+ for (const lang of langs) {
574
+ const oldLoc = oldConcept?.localization(lang) ?? null;
575
+ const newLoc = newConcept?.localization(lang) ?? null;
576
+ count += countLocalizedItems(oldLoc, newLoc);
577
+ }
578
+
579
+ return count;
580
+ }
581
+
582
+ function countLocalizedItems(oldLoc, newLoc) {
583
+ let count = 0;
584
+ count += maxOf(oldLoc?.terms?.length, newLoc?.terms?.length);
585
+ count += maxOf(oldLoc?.definitions?.length, newLoc?.definitions?.length);
586
+ count += maxOf(oldLoc?.notes?.length, newLoc?.notes?.length);
587
+ count += maxOf(oldLoc?.examples?.length, newLoc?.examples?.length);
588
+ count += maxOf(oldLoc?.sources?.length, newLoc?.sources?.length);
589
+ count += maxOf(oldLoc?.dates?.length, newLoc?.dates?.length);
590
+ count += maxOf(oldLoc?.related?.length, newLoc?.related?.length);
591
+ count += LOCALIZATION_METADATA_FIELDS.length;
592
+ return count;
593
+ }
594
+
595
+ function* walkList(section, listDiff) {
596
+ for (let i = 0; i < listDiff.added.length; i++) {
597
+ yield { path: `${section}.added[${i}]`, change: listDiff.added[i] };
598
+ }
599
+ for (let i = 0; i < listDiff.removed.length; i++) {
600
+ yield { path: `${section}.removed[${i}]`, change: listDiff.removed[i] };
601
+ }
602
+ for (let i = 0; i < listDiff.changed.length; i++) {
603
+ yield { path: `${section}.changed[${i}]`, change: listDiff.changed[i] };
604
+ }
605
+ }
606
+
607
+ function union(a, b) {
608
+ return [...new Set([...a, ...b])].sort();
609
+ }