glossarist 0.4.14 → 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.
- package/package.json +11 -1
- package/src/concept-diff.js +19 -0
- package/src/diff/change.js +111 -0
- package/src/diff/collection-diff.js +164 -0
- package/src/diff/concept-diff.js +609 -0
- package/src/diff/diff-patch.js +234 -0
- package/src/diff/diff-renderer.js +220 -0
- package/src/diff/index.d.ts +179 -0
- package/src/diff/index.js +48 -0
- package/src/diff/list-diff.js +227 -0
- package/src/diff/text-diff.js +128 -0
- package/src/index.d.ts +21 -0
- package/src/index.js +19 -0
- package/src/models/concept.js +5 -0
- package/src/rdf/bnode-id.js +31 -11
- package/src/rdf/deterministic-id.js +60 -6
- package/src/rdf/index.d.ts +2 -4
- package/src/rdf/index.js +3 -1
- package/src/rdf/shacl-entry.d.ts +6 -0
- package/src/rdf/shacl-entry.js +9 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { Concept } from '../models/concept.js';
|
|
2
|
+
import { Added, Removed, Changed } from './change.js';
|
|
3
|
+
import { ListDiff } from './list-diff.js';
|
|
4
|
+
import { TextDiff, TextHunk } from './text-diff.js';
|
|
5
|
+
import {
|
|
6
|
+
ConceptDiff,
|
|
7
|
+
ConceptLevelDiff,
|
|
8
|
+
LocalizedConceptDiff,
|
|
9
|
+
MetadataDiff,
|
|
10
|
+
} from './concept-diff.js';
|
|
11
|
+
|
|
12
|
+
const CONCEPT_METADATA_JSON_KEYS = {
|
|
13
|
+
status: 'status',
|
|
14
|
+
term: 'term',
|
|
15
|
+
uri: 'uri',
|
|
16
|
+
schemaVersion: 'schema_version',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const LOC_METADATA_JSON_KEYS = {
|
|
20
|
+
entryStatus: 'entry_status',
|
|
21
|
+
classification: 'classification',
|
|
22
|
+
reviewType: 'review_type',
|
|
23
|
+
domain: 'domain',
|
|
24
|
+
release: 'release',
|
|
25
|
+
lineageSourceSimilarity: 'lineage_source_similarity',
|
|
26
|
+
script: 'script',
|
|
27
|
+
system: 'system',
|
|
28
|
+
reviewDate: 'review_date',
|
|
29
|
+
reviewDecisionDate: 'review_decision_date',
|
|
30
|
+
reviewDecisionEvent: 'review_decision_event',
|
|
31
|
+
reviewStatus: 'review_status',
|
|
32
|
+
reviewDecision: 'review_decision',
|
|
33
|
+
reviewDecisionNotes: 'review_decision_notes',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export function applyDiff(oldConcept, diff) {
|
|
37
|
+
const json = oldConcept.toJSON();
|
|
38
|
+
|
|
39
|
+
applyConceptLevelPatch(json, diff.concept);
|
|
40
|
+
applyLanguagePatch(json, diff);
|
|
41
|
+
|
|
42
|
+
for (const [lang, lcDiff] of Object.entries(diff.localizations)) {
|
|
43
|
+
applyLocalizedPatch(json, lang, lcDiff);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return Concept.fromJSON(json);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function reverseDiff(diff) {
|
|
50
|
+
const concept = new ConceptLevelDiff({
|
|
51
|
+
sources: reverseListDiff(diff.concept.sources),
|
|
52
|
+
dates: reverseListDiff(diff.concept.dates),
|
|
53
|
+
relatedConcepts: reverseListDiff(diff.concept.relatedConcepts),
|
|
54
|
+
groups: reverseListDiff(diff.concept.groups),
|
|
55
|
+
sections: reverseListDiff(diff.concept.sections),
|
|
56
|
+
tags: reverseListDiff(diff.concept.tags),
|
|
57
|
+
metadata: reverseMetadataDiff(diff.concept.metadata),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const languages = reverseListDiff(diff.languages);
|
|
61
|
+
|
|
62
|
+
const localizations = {};
|
|
63
|
+
for (const [lang, lcDiff] of Object.entries(diff.localizations)) {
|
|
64
|
+
localizations[lang] = new LocalizedConceptDiff({
|
|
65
|
+
languageCode: lcDiff.languageCode,
|
|
66
|
+
designations: reverseListDiff(lcDiff.designations),
|
|
67
|
+
definitions: reverseListDiff(lcDiff.definitions),
|
|
68
|
+
notes: reverseListDiff(lcDiff.notes),
|
|
69
|
+
examples: reverseListDiff(lcDiff.examples),
|
|
70
|
+
sources: reverseListDiff(lcDiff.sources),
|
|
71
|
+
dates: reverseListDiff(lcDiff.dates),
|
|
72
|
+
related: reverseListDiff(lcDiff.related),
|
|
73
|
+
metadata: reverseMetadataDiff(lcDiff.metadata),
|
|
74
|
+
totalItems: lcDiff._totalItems,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return new ConceptDiff({
|
|
79
|
+
oldId: diff.newId,
|
|
80
|
+
newId: diff.oldId,
|
|
81
|
+
concept,
|
|
82
|
+
languages,
|
|
83
|
+
localizations,
|
|
84
|
+
totalItems: diff._totalItems,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function applyConceptLevelPatch(json, conceptDiff) {
|
|
89
|
+
json.sources = applyListPatch(json.sources ?? [], conceptDiff.sources, sourceIdentity);
|
|
90
|
+
json.dates = applyListPatch(json.dates ?? [], conceptDiff.dates, dateIdentity);
|
|
91
|
+
json.related = applyListPatch(json.related ?? [], conceptDiff.relatedConcepts, relatedIdentity);
|
|
92
|
+
|
|
93
|
+
json.tags = applyListPatch(json.tags ?? [], conceptDiff.tags, s => String(s));
|
|
94
|
+
|
|
95
|
+
applyMetadataPatch(json, conceptDiff.metadata, CONCEPT_METADATA_JSON_KEYS);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function applyLanguagePatch(json, conceptDiff) {
|
|
99
|
+
if (!json.localizations) json.localizations = {};
|
|
100
|
+
for (const entry of conceptDiff.languages.added) {
|
|
101
|
+
if (!(entry.value in json.localizations)) {
|
|
102
|
+
json.localizations[entry.value] = {};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
for (const entry of conceptDiff.languages.removed) {
|
|
106
|
+
delete json.localizations[entry.value];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function applyLocalizedPatch(json, lang, lcDiff) {
|
|
111
|
+
if (!json.localizations) json.localizations = {};
|
|
112
|
+
if (!(lang in json.localizations)) {
|
|
113
|
+
json.localizations[lang] = {};
|
|
114
|
+
}
|
|
115
|
+
const loc = json.localizations[lang];
|
|
116
|
+
|
|
117
|
+
loc.terms = applyListPatch(loc.terms ?? [], lcDiff.designations, designationIdentity);
|
|
118
|
+
loc.definition = applyListPatch(loc.definition ?? [], lcDiff.definitions, definitionIdentity);
|
|
119
|
+
loc.notes = applyListPatch(loc.notes ?? [], lcDiff.notes, definitionIdentity);
|
|
120
|
+
loc.examples = applyListPatch(loc.examples ?? [], lcDiff.examples, definitionIdentity);
|
|
121
|
+
loc.sources = applyListPatch(loc.sources ?? [], lcDiff.sources, sourceIdentity);
|
|
122
|
+
loc.dates = applyListPatch(loc.dates ?? [], lcDiff.dates, dateIdentity);
|
|
123
|
+
loc.related = applyListPatch(loc.related ?? [], lcDiff.related, relatedIdentity);
|
|
124
|
+
|
|
125
|
+
applyMetadataPatch(loc, lcDiff.metadata, LOC_METADATA_JSON_KEYS);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function applyListPatch(existingItems, listDiff, identityFn) {
|
|
129
|
+
let result = [...existingItems];
|
|
130
|
+
|
|
131
|
+
for (const entry of listDiff.added) {
|
|
132
|
+
result.push(toJsonValue(entry.value));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for (const entry of listDiff.removed) {
|
|
136
|
+
const targetKey = identityFn(entry.value);
|
|
137
|
+
result = result.filter(item => identityFn(item) !== targetKey);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
for (const entry of listDiff.changed) {
|
|
141
|
+
const oldKey = identityFn(entry.oldValue);
|
|
142
|
+
result = result.map(item =>
|
|
143
|
+
identityFn(item) === oldKey ? toJsonValue(entry.newValue) : item,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function applyMetadataPatch(target, metadataDiff, keyMap) {
|
|
151
|
+
for (const [field, change] of Object.entries(metadataDiff.changes)) {
|
|
152
|
+
const jsonKey = keyMap[field];
|
|
153
|
+
if (!jsonKey) continue;
|
|
154
|
+
if (change.newValue == null) {
|
|
155
|
+
delete target[jsonKey];
|
|
156
|
+
} else {
|
|
157
|
+
target[jsonKey] = change.newValue;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function reverseListDiff(listDiff) {
|
|
163
|
+
const added = listDiff.removed.map(r => new Added({ value: r.value, path: r.path }));
|
|
164
|
+
const removed = listDiff.added.map(a => new Removed({ value: a.value, path: a.path }));
|
|
165
|
+
const changed = listDiff.changed.map(c => new Changed({
|
|
166
|
+
oldValue: c.newValue,
|
|
167
|
+
newValue: c.oldValue,
|
|
168
|
+
textDiff: c.textDiff ? reverseTextDiff(c.textDiff) : null,
|
|
169
|
+
path: c.path,
|
|
170
|
+
}));
|
|
171
|
+
return new ListDiff({ added, removed, changed });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function reverseMetadataDiff(metadataDiff) {
|
|
175
|
+
const changes = {};
|
|
176
|
+
for (const [field, change] of Object.entries(metadataDiff.changes)) {
|
|
177
|
+
changes[field] = new Changed({
|
|
178
|
+
oldValue: change.newValue,
|
|
179
|
+
newValue: change.oldValue,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return new MetadataDiff({ changes });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function reverseTextDiff(textDiff) {
|
|
186
|
+
const hunks = textDiff.hunks.map(h => {
|
|
187
|
+
if (h.type === 'added') return new TextHunk({ type: 'removed', text: h.text });
|
|
188
|
+
if (h.type === 'removed') return new TextHunk({ type: 'added', text: h.text });
|
|
189
|
+
return new TextHunk({ type: 'equal', text: h.text });
|
|
190
|
+
});
|
|
191
|
+
return new TextDiff({
|
|
192
|
+
oldText: textDiff.newText,
|
|
193
|
+
newText: textDiff.oldText,
|
|
194
|
+
hunks,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function toJsonValue(value) {
|
|
199
|
+
if (value == null) return null;
|
|
200
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
201
|
+
return value;
|
|
202
|
+
}
|
|
203
|
+
if (typeof value.toJSON === 'function') return value.toJSON();
|
|
204
|
+
return value;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function designationIdentity(d) {
|
|
208
|
+
const data = typeof d?.toJSON === 'function' ? d.toJSON() : d ?? {};
|
|
209
|
+
const type = data.type ?? 'expression';
|
|
210
|
+
const text = String(data.designation ?? '').toLowerCase().trim();
|
|
211
|
+
return `${type}|${text}`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function definitionIdentity(d) {
|
|
215
|
+
const data = typeof d?.toJSON === 'function' ? d.toJSON() : d ?? {};
|
|
216
|
+
return JSON.stringify(data);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function sourceIdentity(s) {
|
|
220
|
+
const data = typeof s?.toJSON === 'function' ? s.toJSON() : s ?? {};
|
|
221
|
+
const ref = data.origin?.ref;
|
|
222
|
+
return `${data.type ?? ''}|${ref?.source ?? ''}|${ref?.id ?? ''}`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function dateIdentity(d) {
|
|
226
|
+
const data = typeof d?.toJSON === 'function' ? d.toJSON() : d ?? {};
|
|
227
|
+
return data.type ?? '';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function relatedIdentity(r) {
|
|
231
|
+
const data = typeof r?.toJSON === 'function' ? r.toJSON() : r ?? {};
|
|
232
|
+
const ref = data.ref;
|
|
233
|
+
return `${data.type ?? ''}|${ref?.source ?? ''}|${ref?.id ?? ''}`;
|
|
234
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { TextDiff } from './text-diff.js';
|
|
2
|
+
|
|
3
|
+
const COLORS = {
|
|
4
|
+
red: s => `\x1b[31m${s}\x1b[0m`,
|
|
5
|
+
green: s => `\x1b[32m${s}\x1b[0m`,
|
|
6
|
+
yellow: s => `\x1b[33m${s}\x1b[0m`,
|
|
7
|
+
cyan: s => `\x1b[36m${s}\x1b[0m`,
|
|
8
|
+
dim: s => `\x1b[2m${s}\x1b[0m`,
|
|
9
|
+
bold: s => `\x1b[1m${s}\x1b[0m`,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function renderConceptDiff(diff, options = {}) {
|
|
13
|
+
const colors = options.colors ?? false;
|
|
14
|
+
const showUnchanged = options.showUnchanged ?? false;
|
|
15
|
+
const lines = [];
|
|
16
|
+
|
|
17
|
+
const pct = Math.round(diff.similarity * 100);
|
|
18
|
+
const idPart = diff.oldId && diff.newId && diff.oldId !== diff.newId
|
|
19
|
+
? `${diff.oldId} → ${diff.newId}`
|
|
20
|
+
: (diff.newId ?? diff.oldId ?? '?');
|
|
21
|
+
|
|
22
|
+
const similarityStr = colors ? colorizeSimilarity(pct) : `${pct}%`;
|
|
23
|
+
lines.push(`Concept "${idPart}" — ${similarityStr} similar`);
|
|
24
|
+
|
|
25
|
+
if (!diff.hasChanges && !showUnchanged) {
|
|
26
|
+
lines.push(' (no changes)');
|
|
27
|
+
return lines.join('\n');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (diff.concept.hasChanges || showUnchanged) {
|
|
31
|
+
lines.push('');
|
|
32
|
+
lines.push('Concept-level:');
|
|
33
|
+
lines.push(renderConceptLevel(diff.concept, colors));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (diff.languages.hasChanges || showUnchanged) {
|
|
37
|
+
lines.push('');
|
|
38
|
+
lines.push('Languages:');
|
|
39
|
+
lines.push(renderLanguageSet(diff.languages, colors));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
for (const lang of diff.localizationLanguages) {
|
|
43
|
+
const lcDiff = diff.localization(lang);
|
|
44
|
+
if (!lcDiff) continue;
|
|
45
|
+
if (lcDiff.hasChanges || showUnchanged) {
|
|
46
|
+
lines.push('');
|
|
47
|
+
lines.push(`Localization (${lang}):`);
|
|
48
|
+
lines.push(renderLocalized(lcDiff, colors));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return lines.join('\n');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function renderCollectionDiff(diff, options = {}) {
|
|
56
|
+
const colors = options.colors ?? false;
|
|
57
|
+
const lines = [];
|
|
58
|
+
|
|
59
|
+
const pct = Math.round(diff.similarity * 100);
|
|
60
|
+
const similarityStr = colors ? colorizeSimilarity(pct) : `${pct}%`;
|
|
61
|
+
|
|
62
|
+
lines.push(`Collection comparison — ${similarityStr} similar overall`);
|
|
63
|
+
lines.push(` Old: ${diff.oldCount} concepts`);
|
|
64
|
+
lines.push(` New: ${diff.newCount} concepts`);
|
|
65
|
+
lines.push(` Matched: ${diff.matched.length}`);
|
|
66
|
+
lines.push(` Added: ${diff.added.length}`);
|
|
67
|
+
lines.push(` Removed: ${diff.removed.length}`);
|
|
68
|
+
|
|
69
|
+
if (diff.added.length > 0) {
|
|
70
|
+
lines.push('');
|
|
71
|
+
lines.push(colors ? COLORS.green('Added concepts:') : 'Added concepts:');
|
|
72
|
+
for (const entry of diff.added) {
|
|
73
|
+
lines.push(` + ${entry.value}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (diff.removed.length > 0) {
|
|
78
|
+
lines.push('');
|
|
79
|
+
lines.push(colors ? COLORS.red('Removed concepts:') : 'Removed concepts:');
|
|
80
|
+
for (const entry of diff.removed) {
|
|
81
|
+
lines.push(` - ${entry.value}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const changedDiffs = Object.entries(diff.conceptDiffs)
|
|
86
|
+
.filter(([, d]) => d.hasChanges)
|
|
87
|
+
.sort(([, a], [, b]) => a.similarity - b.similarity);
|
|
88
|
+
|
|
89
|
+
if (changedDiffs.length > 0) {
|
|
90
|
+
lines.push('');
|
|
91
|
+
lines.push('Changed concepts:');
|
|
92
|
+
for (const [id, conceptDiff] of changedDiffs) {
|
|
93
|
+
const conceptPct = Math.round(conceptDiff.similarity * 100);
|
|
94
|
+
const pctStr = colors ? colorizeSimilarity(conceptPct) : `${conceptPct}%`;
|
|
95
|
+
lines.push(` ${id}: ${pctStr}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return lines.join('\n');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function renderTextDiff(textDiff, options = {}) {
|
|
103
|
+
if (!(textDiff instanceof TextDiff)) {
|
|
104
|
+
textDiff = TextDiff.fromJSON(textDiff);
|
|
105
|
+
}
|
|
106
|
+
const colors = options.colors ?? false;
|
|
107
|
+
const lines = [];
|
|
108
|
+
|
|
109
|
+
for (const hunk of textDiff.hunks) {
|
|
110
|
+
if (hunk.type === 'equal') {
|
|
111
|
+
lines.push(` ${hunk.text}`);
|
|
112
|
+
} else if (hunk.type === 'added') {
|
|
113
|
+
lines.push(colors ? `+ ${COLORS.green(hunk.text)}` : `+ ${hunk.text}`);
|
|
114
|
+
} else if (hunk.type === 'removed') {
|
|
115
|
+
lines.push(colors ? `- ${COLORS.red(hunk.text)}` : `- ${hunk.text}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return lines.join('\n');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function renderConceptLevel(diff, colors) {
|
|
123
|
+
const lines = [];
|
|
124
|
+
lines.push(...renderListDiff('Sources', diff.sources, colors, itemLabel));
|
|
125
|
+
lines.push(...renderListDiff('Dates', diff.dates, colors, itemLabel));
|
|
126
|
+
lines.push(...renderListDiff('Related', diff.relatedConcepts, colors, itemLabel));
|
|
127
|
+
lines.push(...renderListDiff('Groups', diff.groups, colors, itemLabel));
|
|
128
|
+
lines.push(...renderListDiff('Sections', diff.sections, colors, itemLabel));
|
|
129
|
+
lines.push(...renderListDiff('Tags', diff.tags, colors, itemLabel));
|
|
130
|
+
lines.push(...renderMetadataDiff(diff.metadata));
|
|
131
|
+
return lines.filter(Boolean).join('\n');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function renderLanguageSet(diff, colors) {
|
|
135
|
+
const lines = [];
|
|
136
|
+
for (const entry of diff.added) {
|
|
137
|
+
lines.push(colors ? ` + ${COLORS.green(entry.value)}` : ` + ${entry.value}`);
|
|
138
|
+
}
|
|
139
|
+
for (const entry of diff.removed) {
|
|
140
|
+
lines.push(colors ? ` - ${COLORS.red(entry.value)}` : ` - ${entry.value}`);
|
|
141
|
+
}
|
|
142
|
+
return lines.join('\n');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function renderLocalized(diff, colors) {
|
|
146
|
+
const lines = [];
|
|
147
|
+
lines.push(...renderListDiff('Designations', diff.designations, colors, designationLabel));
|
|
148
|
+
lines.push(...renderListDiff('Definitions', diff.definitions, colors, definitionLabel));
|
|
149
|
+
lines.push(...renderListDiff('Notes', diff.notes, colors, definitionLabel));
|
|
150
|
+
lines.push(...renderListDiff('Examples', diff.examples, colors, definitionLabel));
|
|
151
|
+
lines.push(...renderListDiff('Sources', diff.sources, colors, itemLabel));
|
|
152
|
+
lines.push(...renderListDiff('Dates', diff.dates, colors, itemLabel));
|
|
153
|
+
lines.push(...renderListDiff('Related', diff.related, colors, itemLabel));
|
|
154
|
+
lines.push(...renderMetadataDiff(diff.metadata));
|
|
155
|
+
return lines.filter(Boolean).join('\n');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function renderListDiff(label, listDiff, colors, labelFn) {
|
|
159
|
+
if (!listDiff.hasChanges) return [];
|
|
160
|
+
const lines = [` ${label}:`];
|
|
161
|
+
for (const entry of listDiff.added) {
|
|
162
|
+
const text = ` + ${labelFn(entry.value)}`;
|
|
163
|
+
lines.push(colors ? COLORS.green(text) : text);
|
|
164
|
+
}
|
|
165
|
+
for (const entry of listDiff.removed) {
|
|
166
|
+
const text = ` - ${labelFn(entry.value)}`;
|
|
167
|
+
lines.push(colors ? COLORS.red(text) : text);
|
|
168
|
+
}
|
|
169
|
+
for (const entry of listDiff.changed) {
|
|
170
|
+
const oldLabel = labelFn(entry.oldValue);
|
|
171
|
+
const newLabel = labelFn(entry.newValue);
|
|
172
|
+
lines.push(` ~ ${oldLabel} → ${newLabel}`);
|
|
173
|
+
}
|
|
174
|
+
return lines;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function renderMetadataDiff(metadataDiff) {
|
|
178
|
+
if (!metadataDiff.hasChanges) return [];
|
|
179
|
+
const lines = [` Metadata:`];
|
|
180
|
+
for (const [field, change] of Object.entries(metadataDiff.changes)) {
|
|
181
|
+
const oldVal = formatScalar(change.oldValue);
|
|
182
|
+
const newVal = formatScalar(change.newValue);
|
|
183
|
+
lines.push(` ~ ${field}: ${oldVal} → ${newVal}`);
|
|
184
|
+
}
|
|
185
|
+
return lines;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function designationLabel(d) {
|
|
189
|
+
if (!d) return '?';
|
|
190
|
+
const text = d.designation ?? '?';
|
|
191
|
+
const status = d.normativeStatus ? ` (${d.normativeStatus})` : '';
|
|
192
|
+
return `${text}${status}`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function definitionLabel(d) {
|
|
196
|
+
if (!d) return '?';
|
|
197
|
+
return d.content ?? '?';
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function itemLabel(item) {
|
|
201
|
+
if (item == null) return '?';
|
|
202
|
+
if (typeof item === 'string') return item;
|
|
203
|
+
if (typeof item === 'number') return String(item);
|
|
204
|
+
if (typeof item.toJSON === 'function') {
|
|
205
|
+
return JSON.stringify(item.toJSON());
|
|
206
|
+
}
|
|
207
|
+
return String(item);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function formatScalar(val) {
|
|
211
|
+
if (val == null) return '∅';
|
|
212
|
+
if (typeof val === 'string') return val;
|
|
213
|
+
return String(val);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function colorizeSimilarity(pct) {
|
|
217
|
+
if (pct >= 95) return COLORS.green(`${pct}%`);
|
|
218
|
+
if (pct >= 80) return COLORS.yellow(`${pct}%`);
|
|
219
|
+
return COLORS.red(`${pct}%`);
|
|
220
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type { GlossaristModel } from '../models/base';
|
|
2
|
+
|
|
3
|
+
// ── Text diff ──────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
export type HunkType = 'equal' | 'added' | 'removed';
|
|
6
|
+
|
|
7
|
+
export class TextHunk extends GlossaristModel {
|
|
8
|
+
readonly type: HunkType;
|
|
9
|
+
readonly text: string;
|
|
10
|
+
toJSON(): { type: HunkType; text: string };
|
|
11
|
+
static fromJSON(data: { type: HunkType; text: string }): TextHunk;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class TextDiff extends GlossaristModel {
|
|
15
|
+
readonly oldText: string;
|
|
16
|
+
readonly newText: string;
|
|
17
|
+
readonly hunks: TextHunk[];
|
|
18
|
+
readonly hasChanges: boolean;
|
|
19
|
+
readonly addedText: string;
|
|
20
|
+
readonly removedText: string;
|
|
21
|
+
toJSON(): { old_text: string; new_text: string; hunks: Array<{ type: HunkType; text: string }> };
|
|
22
|
+
static fromJSON(data: Record<string, unknown>): TextDiff;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function diffText(oldText: string | null | undefined, newText: string | null | undefined): TextDiff;
|
|
26
|
+
|
|
27
|
+
// ── Change types ───────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
export const CHANGE_ADDED: 'added';
|
|
30
|
+
export const CHANGE_REMOVED: 'removed';
|
|
31
|
+
export const CHANGE_CHANGED: 'changed';
|
|
32
|
+
|
|
33
|
+
export type ChangeType = typeof CHANGE_ADDED | typeof CHANGE_REMOVED | typeof CHANGE_CHANGED;
|
|
34
|
+
|
|
35
|
+
export class Change extends GlossaristModel {
|
|
36
|
+
readonly type: ChangeType;
|
|
37
|
+
readonly path: string | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class Added extends Change {
|
|
41
|
+
readonly type: 'added';
|
|
42
|
+
readonly value: unknown;
|
|
43
|
+
toJSON(): { type: 'added'; path?: string; value: unknown };
|
|
44
|
+
static fromJSON(data: Record<string, unknown>): Added;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class Removed extends Change {
|
|
48
|
+
readonly type: 'removed';
|
|
49
|
+
readonly value: unknown;
|
|
50
|
+
toJSON(): { type: 'removed'; path?: string; value: unknown };
|
|
51
|
+
static fromJSON(data: Record<string, unknown>): Removed;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class Changed extends Change {
|
|
55
|
+
readonly type: 'changed';
|
|
56
|
+
readonly oldValue: unknown;
|
|
57
|
+
readonly newValue: unknown;
|
|
58
|
+
readonly textDiff: TextDiff | null;
|
|
59
|
+
toJSON(): {
|
|
60
|
+
type: 'changed';
|
|
61
|
+
path?: string;
|
|
62
|
+
old_value: unknown;
|
|
63
|
+
new_value: unknown;
|
|
64
|
+
text_diff?: ReturnType<TextDiff['toJSON']>;
|
|
65
|
+
};
|
|
66
|
+
static fromJSON(data: Record<string, unknown>): Changed;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function deserializeChange(data: Record<string, unknown>): Change;
|
|
70
|
+
|
|
71
|
+
// ── List diff ──────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
export interface DiffListOptions {
|
|
74
|
+
identityKey?: (item: any) => string;
|
|
75
|
+
textKey?: (item: any) => string | null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export class ListDiff extends GlossaristModel {
|
|
79
|
+
readonly added: Added[];
|
|
80
|
+
readonly removed: Removed[];
|
|
81
|
+
readonly changed: Changed[];
|
|
82
|
+
readonly hasChanges: boolean;
|
|
83
|
+
readonly count: number;
|
|
84
|
+
entries(): Generator<Change, void, unknown>;
|
|
85
|
+
toJSON(): {
|
|
86
|
+
added: ReturnType<Added['toJSON']>[];
|
|
87
|
+
removed: ReturnType<Removed['toJSON']>[];
|
|
88
|
+
changed: ReturnType<Changed['toJSON']>[];
|
|
89
|
+
};
|
|
90
|
+
static fromJSON(data: Record<string, unknown>): ListDiff;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function diffList(oldList: unknown[], newList: unknown[], options?: DiffListOptions): ListDiff;
|
|
94
|
+
export function diffSet(oldList: unknown[], newList: unknown[], options: DiffListOptions): ListDiff;
|
|
95
|
+
|
|
96
|
+
// ── Concept diff ───────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
export interface DiffWalkEntry {
|
|
99
|
+
path: string;
|
|
100
|
+
change: Change;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface ConceptDiffWalkEntry extends DiffWalkEntry {
|
|
104
|
+
language: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export class DiffStats extends GlossaristModel {
|
|
108
|
+
readonly added: number;
|
|
109
|
+
readonly removed: number;
|
|
110
|
+
readonly changed: number;
|
|
111
|
+
readonly total: number;
|
|
112
|
+
toJSON(): { added: number; removed: number; changed: number };
|
|
113
|
+
static fromJSON(data: Record<string, unknown>): DiffStats;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export class MetadataDiff extends GlossaristModel {
|
|
117
|
+
readonly changes: Record<string, Changed>;
|
|
118
|
+
readonly hasChanges: boolean;
|
|
119
|
+
readonly count: number;
|
|
120
|
+
walk(section?: string): Generator<DiffWalkEntry, void, unknown>;
|
|
121
|
+
toJSON(): Record<string, ReturnType<Changed['toJSON']>>;
|
|
122
|
+
static fromJSON(data: Record<string, unknown>): MetadataDiff;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export class ConceptLevelDiff extends GlossaristModel {
|
|
126
|
+
readonly sources: ListDiff;
|
|
127
|
+
readonly dates: ListDiff;
|
|
128
|
+
readonly relatedConcepts: ListDiff;
|
|
129
|
+
readonly groups: ListDiff;
|
|
130
|
+
readonly sections: ListDiff;
|
|
131
|
+
readonly tags: ListDiff;
|
|
132
|
+
readonly metadata: MetadataDiff;
|
|
133
|
+
readonly hasChanges: boolean;
|
|
134
|
+
walk(): Generator<DiffWalkEntry, void, unknown>;
|
|
135
|
+
toJSON(): Record<string, unknown>;
|
|
136
|
+
static fromJSON(data: Record<string, unknown>): ConceptLevelDiff;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export class LocalizedConceptDiff extends GlossaristModel {
|
|
140
|
+
readonly languageCode: string | null;
|
|
141
|
+
readonly designations: ListDiff;
|
|
142
|
+
readonly definitions: ListDiff;
|
|
143
|
+
readonly notes: ListDiff;
|
|
144
|
+
readonly examples: ListDiff;
|
|
145
|
+
readonly sources: ListDiff;
|
|
146
|
+
readonly dates: ListDiff;
|
|
147
|
+
readonly related: ListDiff;
|
|
148
|
+
readonly metadata: MetadataDiff;
|
|
149
|
+
readonly hasChanges: boolean;
|
|
150
|
+
walk(prefix?: string): Generator<DiffWalkEntry, void, unknown>;
|
|
151
|
+
toJSON(): Record<string, unknown>;
|
|
152
|
+
static fromJSON(data: Record<string, unknown>): LocalizedConceptDiff;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export class ConceptDiff extends GlossaristModel {
|
|
156
|
+
readonly oldId: string | null;
|
|
157
|
+
readonly newId: string | null;
|
|
158
|
+
readonly concept: ConceptLevelDiff;
|
|
159
|
+
readonly languages: ListDiff;
|
|
160
|
+
readonly localizations: Record<string, LocalizedConceptDiff>;
|
|
161
|
+
readonly hasChanges: boolean;
|
|
162
|
+
readonly localizationLanguages: string[];
|
|
163
|
+
readonly stats: DiffStats;
|
|
164
|
+
localization(lang: string): LocalizedConceptDiff | null;
|
|
165
|
+
walk(): Generator<ConceptDiffWalkEntry, void, unknown>;
|
|
166
|
+
toJSON(): Record<string, unknown>;
|
|
167
|
+
static fromJSON(data: Record<string, unknown>): ConceptDiff;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function diffConcepts(
|
|
171
|
+
oldConcept: any | null,
|
|
172
|
+
newConcept: any | null,
|
|
173
|
+
language?: string,
|
|
174
|
+
): ConceptDiff;
|
|
175
|
+
|
|
176
|
+
export function diffLocalizedConcepts(
|
|
177
|
+
oldLoc: any | null,
|
|
178
|
+
newLoc: any | null,
|
|
179
|
+
): LocalizedConceptDiff;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export {
|
|
2
|
+
TextHunk,
|
|
3
|
+
TextDiff,
|
|
4
|
+
diffText,
|
|
5
|
+
} from './text-diff.js';
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
Change,
|
|
9
|
+
Added,
|
|
10
|
+
Removed,
|
|
11
|
+
Changed,
|
|
12
|
+
CHANGE_ADDED,
|
|
13
|
+
CHANGE_REMOVED,
|
|
14
|
+
CHANGE_CHANGED,
|
|
15
|
+
deserializeChange,
|
|
16
|
+
} from './change.js';
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
ListDiff,
|
|
20
|
+
diffList,
|
|
21
|
+
diffSet,
|
|
22
|
+
} from './list-diff.js';
|
|
23
|
+
|
|
24
|
+
export {
|
|
25
|
+
DiffStats,
|
|
26
|
+
MetadataDiff,
|
|
27
|
+
ConceptLevelDiff,
|
|
28
|
+
LocalizedConceptDiff,
|
|
29
|
+
ConceptDiff,
|
|
30
|
+
diffConcepts,
|
|
31
|
+
diffLocalizedConcepts,
|
|
32
|
+
} from './concept-diff.js';
|
|
33
|
+
|
|
34
|
+
export {
|
|
35
|
+
ConceptCollectionDiff,
|
|
36
|
+
diffConceptCollections,
|
|
37
|
+
} from './collection-diff.js';
|
|
38
|
+
|
|
39
|
+
export {
|
|
40
|
+
renderConceptDiff,
|
|
41
|
+
renderCollectionDiff,
|
|
42
|
+
renderTextDiff,
|
|
43
|
+
} from './diff-renderer.js';
|
|
44
|
+
|
|
45
|
+
export {
|
|
46
|
+
applyDiff,
|
|
47
|
+
reverseDiff,
|
|
48
|
+
} from './diff-patch.js';
|