@glossarist/concept-browser 0.7.84 → 0.7.86
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 +2 -2
- package/scripts/build-edges.js +42 -26
- package/scripts/generate-data.mjs +45 -5
- package/scripts/migrate-v1-to-v3.d.mts +6 -0
- package/scripts/migrate-v1-to-v3.mjs +176 -0
- package/src/__tests__/components/PartitiveRelationDiagram.test.ts +92 -136
- package/src/__tests__/composables/use-concept-edges-v2.test.ts +115 -87
- package/src/__tests__/partitive-relation-styling.test.ts +12 -54
- package/src/__tests__/partitive-relation.test.ts +42 -90
- package/src/__tests__/scripts/migrate-v1-to-v3.d.mts +8 -0
- package/src/__tests__/scripts/migrate-v1-to-v3.test.ts +115 -0
- package/src/__tests__/utils/partitive-multiplicity.test.ts +113 -0
- package/src/adapters/model-bridge.ts +15 -19
- package/src/adapters/non-verbal/glossarist-augment.d.ts +34 -18
- package/src/adapters/types.ts +2 -8
- package/src/components/PartitiveRelationDiagram.vue +88 -76
- package/src/components/PartitiveRelationList.vue +26 -34
- package/src/components/RelationSphere.vue +173 -106
- package/src/composables/use-concept-edges.ts +21 -21
- package/src/utils/partitive-multiplicity.ts +106 -0
- package/src/utils/partitive-relation-styling.ts +29 -38
- package/src/views/ConceptView.vue +7 -13
- package/src/__tests__/model-bridge-partitive.test.ts +0 -146
|
@@ -1,62 +1,30 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import {
|
|
3
3
|
partitiveRelationStyle,
|
|
4
|
-
pluralityColor,
|
|
5
4
|
completenessLabel,
|
|
6
|
-
certaintyLabel,
|
|
7
5
|
} from '../utils/partitive-relation-styling';
|
|
8
|
-
import type { TypeSharedPluralityWire } from '../adapters/types';
|
|
9
6
|
|
|
10
7
|
describe('partitive-relation-styling', () => {
|
|
11
|
-
describe('partitiveRelationStyle', () => {
|
|
12
|
-
it('returns
|
|
13
|
-
expect(partitiveRelationStyle('complete',
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it('returns reduced opacity for partial', () => {
|
|
17
|
-
expect(partitiveRelationStyle('partial', null, false).opacity).toBe(0.6);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it('uses teal badge class for complete with no plurality', () => {
|
|
21
|
-
expect(partitiveRelationStyle('complete', null, false).badgeClass).toBe('badge-teal');
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('uses blue badge class for isShared plurality', () => {
|
|
25
|
-
const p: TypeSharedPluralityWire = { isShared: true, isUncertain: false };
|
|
26
|
-
expect(partitiveRelationStyle('complete', p, false).badgeClass).toBe('badge-blue');
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('uses yellow badge class when isShared + isUncertain', () => {
|
|
30
|
-
const p: TypeSharedPluralityWire = { isShared: true, isUncertain: true };
|
|
31
|
-
expect(partitiveRelationStyle('complete', p, false).badgeClass).toBe('badge-yellow');
|
|
8
|
+
describe('partitiveRelationStyle (relation-level frame)', () => {
|
|
9
|
+
it('returns teal color in light mode', () => {
|
|
10
|
+
expect(partitiveRelationStyle('complete', false).color).toBe('#0d9488');
|
|
32
11
|
});
|
|
33
12
|
|
|
34
|
-
it('returns
|
|
35
|
-
|
|
36
|
-
const dark = partitiveRelationStyle('complete', null, true);
|
|
37
|
-
expect(light.color).not.toBe(dark.color);
|
|
13
|
+
it('returns teal color in dark mode', () => {
|
|
14
|
+
expect(partitiveRelationStyle('complete', true).color).toBe('#2dd4bf');
|
|
38
15
|
});
|
|
39
16
|
|
|
40
|
-
it('
|
|
41
|
-
|
|
42
|
-
expect(partitiveRelationStyle('complete', p, false).badgeClass).toBe('badge-teal');
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
describe('pluralityColor', () => {
|
|
47
|
-
it('returns blue for isShared', () => {
|
|
48
|
-
const p: TypeSharedPluralityWire = { isShared: true, isUncertain: false };
|
|
49
|
-
expect(pluralityColor(p, false)).toBe('#3b82f6');
|
|
17
|
+
it('returns full opacity for complete', () => {
|
|
18
|
+
expect(partitiveRelationStyle('complete', false).opacity).toBe(1.0);
|
|
50
19
|
});
|
|
51
20
|
|
|
52
|
-
it('returns
|
|
53
|
-
|
|
54
|
-
expect(pluralityColor(p, false)).toBe('#f59e0b');
|
|
21
|
+
it('returns reduced opacity for partial', () => {
|
|
22
|
+
expect(partitiveRelationStyle('partial', false).opacity).toBe(0.6);
|
|
55
23
|
});
|
|
56
24
|
|
|
57
|
-
it('
|
|
58
|
-
expect(
|
|
59
|
-
expect(
|
|
25
|
+
it('uses teal badge class regardless of completeness', () => {
|
|
26
|
+
expect(partitiveRelationStyle('complete', false).badgeClass).toBe('badge-teal');
|
|
27
|
+
expect(partitiveRelationStyle('partial', false).badgeClass).toBe('badge-teal');
|
|
60
28
|
});
|
|
61
29
|
});
|
|
62
30
|
|
|
@@ -69,14 +37,4 @@ describe('partitive-relation-styling', () => {
|
|
|
69
37
|
expect(completenessLabel('partial')).toBe('Partial');
|
|
70
38
|
});
|
|
71
39
|
});
|
|
72
|
-
|
|
73
|
-
describe('certaintyLabel', () => {
|
|
74
|
-
it('labels confirmed', () => {
|
|
75
|
-
expect(certaintyLabel('confirmed')).toBe('Confirmed');
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('labels possible', () => {
|
|
79
|
-
expect(certaintyLabel('possible')).toBe('Possible');
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
40
|
});
|
|
@@ -21,89 +21,79 @@ function extractPartitiveRelations(
|
|
|
21
21
|
return `${uriBase}/${reg}/concept/${id}`;
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
-
const resolvePlurality = (plurality: any) => {
|
|
25
|
-
if (!plurality) return null;
|
|
26
|
-
const isShared = plurality['gl:isShared'] ?? plurality.is_shared;
|
|
27
|
-
if (typeof isShared !== 'boolean') return null;
|
|
28
|
-
const isUncertain = plurality['gl:isUncertain'] ?? plurality.is_uncertain ?? false;
|
|
29
|
-
const sharedTypeRef = plurality['gl:sharedType'] ?? plurality.shared_type;
|
|
30
|
-
let sharedType = null;
|
|
31
|
-
if (sharedTypeRef) {
|
|
32
|
-
const s = sharedTypeRef['gl:source'] ?? sharedTypeRef.source;
|
|
33
|
-
const i = sharedTypeRef['gl:id'] ?? sharedTypeRef.id;
|
|
34
|
-
if (s && i) sharedType = `${s}:${i}`;
|
|
35
|
-
}
|
|
36
|
-
return { isShared, isUncertain, sharedType };
|
|
37
|
-
};
|
|
38
|
-
|
|
39
24
|
for (const rel of concept['gl:partitiveRelations'] || []) {
|
|
40
25
|
const comprehensive = resolveConceptUri(rel['gl:comprehensive']);
|
|
41
26
|
if (!comprehensive) continue;
|
|
27
|
+
|
|
42
28
|
const partitives = (rel['gl:hasPartitive'] || [])
|
|
43
29
|
.map((member: any) => {
|
|
44
|
-
const
|
|
45
|
-
const uri = resolveConceptUri(
|
|
30
|
+
const memberRef = member['gl:ref'] || member;
|
|
31
|
+
const uri = resolveConceptUri(memberRef);
|
|
46
32
|
if (!uri || uri === sourceUri) return null;
|
|
47
|
-
const
|
|
48
|
-
|
|
33
|
+
const multiplicity = member['gl:multiplicity'] || 'compulsory';
|
|
34
|
+
const isDelimiting = member['gl:isDelimiting'] === true;
|
|
35
|
+
if (!isValidMultiplicity(multiplicity)) {
|
|
49
36
|
throw new Error(
|
|
50
|
-
`Invalid partitive member
|
|
37
|
+
`Invalid partitive member multiplicity: "${multiplicity}". Allowed: compulsory, optional, compulsory_multiple, optional_multiple, compulsory_at_least_one`,
|
|
51
38
|
);
|
|
52
39
|
}
|
|
53
|
-
return { uri,
|
|
40
|
+
return { uri, multiplicity, isDelimiting };
|
|
54
41
|
})
|
|
55
|
-
.filter((p: any): p is { uri: string;
|
|
42
|
+
.filter((p: any): p is { uri: string; multiplicity: 'compulsory' | 'optional' | 'compulsory_multiple' | 'optional_multiple' | 'compulsory_at_least_one'; isDelimiting: boolean } => p !== null);
|
|
43
|
+
|
|
56
44
|
if (partitives.length === 0) continue;
|
|
45
|
+
|
|
57
46
|
const completeness = rel['gl:completeness'] || 'complete';
|
|
58
47
|
if (completeness !== 'complete' && completeness !== 'partial') {
|
|
59
48
|
throw new Error(
|
|
60
49
|
`Invalid partitive relation completeness: "${completeness}". Allowed: complete, partial`,
|
|
61
50
|
);
|
|
62
51
|
}
|
|
52
|
+
|
|
63
53
|
relations.push({
|
|
64
54
|
source: sourceUri,
|
|
65
55
|
comprehensive,
|
|
66
56
|
partitives,
|
|
67
57
|
completeness,
|
|
68
|
-
plurality: resolvePlurality(rel['gl:hasPlurality']) as PartitiveRelationWire['plurality'],
|
|
69
|
-
criterion: rel['gl:criterion'],
|
|
70
58
|
register: registerId,
|
|
71
59
|
});
|
|
72
60
|
}
|
|
73
61
|
return relations;
|
|
74
62
|
}
|
|
75
63
|
|
|
64
|
+
function isValidMultiplicity(value: unknown): value is 'compulsory' | 'optional' | 'compulsory_multiple' | 'optional_multiple' | 'compulsory_at_least_one' {
|
|
65
|
+
return ['compulsory', 'optional', 'compulsory_multiple', 'optional_multiple', 'compulsory_at_least_one'].includes(value as string);
|
|
66
|
+
}
|
|
67
|
+
|
|
76
68
|
describe('extractPartitiveRelations', () => {
|
|
77
69
|
const uriBase = 'https://example.org';
|
|
78
70
|
const urnMap = new Map([['urn:vim:pub:v:2:2012', 'vim-2012']]);
|
|
79
71
|
const registerId = 'vim-2012';
|
|
80
|
-
const sourceUri = `${uriBase}/${registerId}/concept/
|
|
72
|
+
const sourceUri = `${uriBase}/${registerId}/concept/1.3`;
|
|
81
73
|
|
|
82
|
-
it('extracts a complete relation with two
|
|
74
|
+
it('extracts a complete relation with two compulsory members', () => {
|
|
83
75
|
const concept = {
|
|
84
76
|
'@id': sourceUri,
|
|
85
77
|
'gl:partitiveRelations': [
|
|
86
78
|
{
|
|
87
|
-
'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '
|
|
79
|
+
'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.3' },
|
|
88
80
|
'gl:hasPartitive': [
|
|
89
|
-
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '
|
|
90
|
-
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '
|
|
81
|
+
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.4' } },
|
|
82
|
+
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.5' } },
|
|
91
83
|
],
|
|
92
84
|
'gl:completeness': 'complete',
|
|
93
|
-
'gl:criterion': { eng: 'measurement result composition' },
|
|
94
85
|
},
|
|
95
86
|
],
|
|
96
87
|
};
|
|
97
88
|
|
|
98
89
|
const result = extractPartitiveRelations(concept, registerId, uriBase, urnMap);
|
|
99
90
|
expect(result).toHaveLength(1);
|
|
100
|
-
expect(result[0].comprehensive).toBe(`${uriBase}/${registerId}/concept/
|
|
91
|
+
expect(result[0].comprehensive).toBe(`${uriBase}/${registerId}/concept/1.3`);
|
|
101
92
|
expect(result[0].partitives).toEqual([
|
|
102
|
-
{ uri: `${uriBase}/${registerId}/concept/
|
|
103
|
-
{ uri: `${uriBase}/${registerId}/concept/
|
|
93
|
+
{ uri: `${uriBase}/${registerId}/concept/1.4`, multiplicity: 'compulsory', isDelimiting: false },
|
|
94
|
+
{ uri: `${uriBase}/${registerId}/concept/1.5`, multiplicity: 'compulsory', isDelimiting: false },
|
|
104
95
|
]);
|
|
105
96
|
expect(result[0].completeness).toBe('complete');
|
|
106
|
-
expect(result[0].criterion).toEqual({ eng: 'measurement result composition' });
|
|
107
97
|
});
|
|
108
98
|
|
|
109
99
|
it('defaults completeness to complete when omitted', () => {
|
|
@@ -111,9 +101,9 @@ describe('extractPartitiveRelations', () => {
|
|
|
111
101
|
'@id': sourceUri,
|
|
112
102
|
'gl:partitiveRelations': [
|
|
113
103
|
{
|
|
114
|
-
'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '
|
|
104
|
+
'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.3' },
|
|
115
105
|
'gl:hasPartitive': [
|
|
116
|
-
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '
|
|
106
|
+
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.4' } },
|
|
117
107
|
],
|
|
118
108
|
},
|
|
119
109
|
],
|
|
@@ -139,7 +129,7 @@ describe('extractPartitiveRelations', () => {
|
|
|
139
129
|
expect(rel.completeness).toBe('partial');
|
|
140
130
|
});
|
|
141
131
|
|
|
142
|
-
it('
|
|
132
|
+
it('reads per-member multiplicity + isDelimiting', () => {
|
|
143
133
|
const concept = {
|
|
144
134
|
'@id': sourceUri,
|
|
145
135
|
'gl:partitiveRelations': [
|
|
@@ -147,57 +137,19 @@ describe('extractPartitiveRelations', () => {
|
|
|
147
137
|
'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.3' },
|
|
148
138
|
'gl:hasPartitive': [
|
|
149
139
|
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.4' } },
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
'gl:
|
|
153
|
-
'gl:isUncertain': false,
|
|
154
|
-
'gl:sharedType': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.99' },
|
|
155
|
-
},
|
|
156
|
-
},
|
|
157
|
-
],
|
|
158
|
-
};
|
|
159
|
-
const [rel] = extractPartitiveRelations(concept, registerId, uriBase, urnMap);
|
|
160
|
-
expect(rel.plurality).toEqual({
|
|
161
|
-
isShared: true,
|
|
162
|
-
isUncertain: false,
|
|
163
|
-
sharedType: 'urn:vim:pub:v:2:2012:1.99',
|
|
164
|
-
});
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it('returns null plurality when isShared missing', () => {
|
|
168
|
-
const concept = {
|
|
169
|
-
'@id': sourceUri,
|
|
170
|
-
'gl:partitiveRelations': [
|
|
171
|
-
{
|
|
172
|
-
'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.3' },
|
|
173
|
-
'gl:hasPartitive': [
|
|
174
|
-
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.4' } },
|
|
175
|
-
],
|
|
176
|
-
'gl:hasPlurality': { 'gl:isUncertain': true },
|
|
177
|
-
},
|
|
178
|
-
],
|
|
179
|
-
};
|
|
180
|
-
const [rel] = extractPartitiveRelations(concept, registerId, uriBase, urnMap);
|
|
181
|
-
expect(rel.plurality).toBeNull();
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it('preserves per-member certainty', () => {
|
|
185
|
-
const concept = {
|
|
186
|
-
'@id': sourceUri,
|
|
187
|
-
'gl:partitiveRelations': [
|
|
188
|
-
{
|
|
189
|
-
'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.3' },
|
|
190
|
-
'gl:hasPartitive': [
|
|
191
|
-
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.4' } },
|
|
192
|
-
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.5' }, 'gl:certainty': 'possible' },
|
|
140
|
+
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.5' }, 'gl:multiplicity': 'optional' },
|
|
141
|
+
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.6' }, 'gl:multiplicity': 'compulsory_multiple' },
|
|
142
|
+
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.7' }, 'gl:multiplicity': 'compulsory_at_least_one', 'gl:isDelimiting': true },
|
|
193
143
|
],
|
|
194
144
|
},
|
|
195
145
|
],
|
|
196
146
|
};
|
|
197
147
|
const [rel] = extractPartitiveRelations(concept, registerId, uriBase, urnMap);
|
|
198
148
|
expect(rel.partitives).toEqual([
|
|
199
|
-
{ uri: `${uriBase}/${registerId}/concept/1.4`,
|
|
200
|
-
{ uri: `${uriBase}/${registerId}/concept/1.5`,
|
|
149
|
+
{ uri: `${uriBase}/${registerId}/concept/1.4`, multiplicity: 'compulsory', isDelimiting: false },
|
|
150
|
+
{ uri: `${uriBase}/${registerId}/concept/1.5`, multiplicity: 'optional', isDelimiting: false },
|
|
151
|
+
{ uri: `${uriBase}/${registerId}/concept/1.6`, multiplicity: 'compulsory_multiple', isDelimiting: false },
|
|
152
|
+
{ uri: `${uriBase}/${registerId}/concept/1.7`, multiplicity: 'compulsory_at_least_one', isDelimiting: true },
|
|
201
153
|
]);
|
|
202
154
|
});
|
|
203
155
|
|
|
@@ -217,19 +169,19 @@ describe('extractPartitiveRelations', () => {
|
|
|
217
169
|
expect(() => extractPartitiveRelations(concept, registerId, uriBase, urnMap)).toThrow(/Invalid.*completeness.*closed/);
|
|
218
170
|
});
|
|
219
171
|
|
|
220
|
-
it('throws on invalid
|
|
172
|
+
it('throws on invalid multiplicity', () => {
|
|
221
173
|
const concept = {
|
|
222
174
|
'@id': sourceUri,
|
|
223
175
|
'gl:partitiveRelations': [
|
|
224
176
|
{
|
|
225
177
|
'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.3' },
|
|
226
178
|
'gl:hasPartitive': [
|
|
227
|
-
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.4' }, 'gl:
|
|
179
|
+
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.4' }, 'gl:multiplicity': 'maybe' },
|
|
228
180
|
],
|
|
229
181
|
},
|
|
230
182
|
],
|
|
231
183
|
};
|
|
232
|
-
expect(() => extractPartitiveRelations(concept, registerId, uriBase, urnMap)).toThrow(/Invalid.*
|
|
184
|
+
expect(() => extractPartitiveRelations(concept, registerId, uriBase, urnMap)).toThrow(/Invalid.*multiplicity.*maybe/);
|
|
233
185
|
});
|
|
234
186
|
|
|
235
187
|
it('skips a relation whose comprehensive cannot be resolved', () => {
|
|
@@ -252,7 +204,7 @@ describe('extractPartitiveRelations', () => {
|
|
|
252
204
|
'@id': sourceUri,
|
|
253
205
|
'gl:partitiveRelations': [
|
|
254
206
|
{
|
|
255
|
-
'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '
|
|
207
|
+
'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.3' },
|
|
256
208
|
'gl:hasPartitive': [{ 'gl:ref': { 'gl:id': '1.4' } }],
|
|
257
209
|
},
|
|
258
210
|
],
|
|
@@ -265,17 +217,17 @@ describe('extractPartitiveRelations', () => {
|
|
|
265
217
|
'@id': sourceUri,
|
|
266
218
|
'gl:partitiveRelations': [
|
|
267
219
|
{
|
|
268
|
-
'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '
|
|
220
|
+
'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.3' },
|
|
269
221
|
'gl:hasPartitive': [
|
|
270
|
-
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '
|
|
271
|
-
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '
|
|
222
|
+
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.4' } },
|
|
223
|
+
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.3' } },
|
|
272
224
|
],
|
|
273
225
|
},
|
|
274
226
|
],
|
|
275
227
|
};
|
|
276
228
|
const [rel] = extractPartitiveRelations(concept, registerId, uriBase, urnMap);
|
|
277
229
|
expect(rel.partitives).toHaveLength(1);
|
|
278
|
-
expect(rel.partitives[0].uri).toBe(`${uriBase}/${registerId}/concept/
|
|
230
|
+
expect(rel.partitives[0].uri).toBe(`${uriBase}/${registerId}/concept/1.4`);
|
|
279
231
|
});
|
|
280
232
|
|
|
281
233
|
it('returns empty array when concept has no partitiveRelations', () => {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Type shim for the .mjs script — vitest needs declared types.
|
|
2
|
+
// The actual functions are imported by value (see below).
|
|
3
|
+
declare module '../../../scripts/migrate-v1-to-v3.mjs' {
|
|
4
|
+
export function isV1PartitiveRelation(rel: unknown): boolean;
|
|
5
|
+
export function migrateRelation(rel: unknown): unknown;
|
|
6
|
+
export function migrateConceptData(data: unknown): unknown;
|
|
7
|
+
export function isAlreadyMigrated(content: unknown): boolean;
|
|
8
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
// .mjs scripts have no type declarations. The .d.mts shim provides them.
|
|
3
|
+
import {
|
|
4
|
+
isV1PartitiveRelation,
|
|
5
|
+
migrateRelation,
|
|
6
|
+
migrateConceptData,
|
|
7
|
+
isAlreadyMigrated,
|
|
8
|
+
} from '../../../scripts/migrate-v1-to-v3.mjs';
|
|
9
|
+
|
|
10
|
+
describe('migrate-v1-to-v3 pure conversion', () => {
|
|
11
|
+
describe('isV1PartitiveRelation', () => {
|
|
12
|
+
it('returns true for v1 shape (has parts array)', () => {
|
|
13
|
+
expect(isV1PartitiveRelation({ comprehensive: {}, parts: [] })).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('returns false for v3 shape (has partitives array)', () => {
|
|
17
|
+
expect(isV1PartitiveRelation({ comprehensive: {}, partitives: [] })).toBe(false);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('returns falsy for null/undefined', () => {
|
|
21
|
+
expect(isV1PartitiveRelation(null)).toBeFalsy();
|
|
22
|
+
expect(isV1PartitiveRelation(undefined)).toBeFalsy();
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('migrateRelation', () => {
|
|
27
|
+
it('converts v1 → v3 fields', () => {
|
|
28
|
+
const v1 = {
|
|
29
|
+
comprehensive: { source: 'urn:test', id: '1.3' },
|
|
30
|
+
parts: [
|
|
31
|
+
{ source: 'urn:test', id: '1.4' },
|
|
32
|
+
{ source: 'urn:test', id: '1.5' },
|
|
33
|
+
],
|
|
34
|
+
enumeration: 'closed',
|
|
35
|
+
};
|
|
36
|
+
const v3 = migrateRelation(v1) as null | { comprehensive: { source: string; id: string }; completeness: string; partitives: Array<{ ref: { source: string; id: string }; certainty: string }> };
|
|
37
|
+
expect(v3).not.toBeNull();
|
|
38
|
+
expect(v3!.comprehensive).toEqual({ source: 'urn:test', id: '1.3' });
|
|
39
|
+
expect(v3!.completeness).toBe('complete');
|
|
40
|
+
expect(v3!.partitives).toHaveLength(2);
|
|
41
|
+
expect(v3!.partitives[0].ref).toEqual({ source: 'urn:test', id: '1.4' });
|
|
42
|
+
expect(v3!.partitives[1].ref).toEqual({ source: 'urn:test', id: '1.5' });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('maps v1 enumeration: open → v3 completeness: partial', () => {
|
|
46
|
+
const v3 = migrateRelation({
|
|
47
|
+
comprehensive: { source: 'urn:test', id: '1.3' },
|
|
48
|
+
parts: [{ source: 'urn:test', id: '1.4' }, { source: 'urn:test', id: '1.5' }],
|
|
49
|
+
enumeration: 'open',
|
|
50
|
+
}) as null | { completeness: string };
|
|
51
|
+
expect(v3).not.toBeNull();
|
|
52
|
+
expect(v3!.completeness).toBe('partial');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('drops v1 markers (multiplicity carries that info now)', () => {
|
|
56
|
+
const v3 = migrateRelation({
|
|
57
|
+
comprehensive: {}, parts: [{ source: 'x', id: 'a' }, { source: 'x', id: 'b' }],
|
|
58
|
+
markers: ['double', 'dashed'],
|
|
59
|
+
});
|
|
60
|
+
expect(v3).not.toHaveProperty('markers');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('preserves v1 content if present (forward-compat)', () => {
|
|
64
|
+
const v3 = migrateRelation({
|
|
65
|
+
comprehensive: {}, parts: [{ source: 'x', id: 'a' }, { source: 'x', id: 'b' }],
|
|
66
|
+
content: 'some prose',
|
|
67
|
+
}) as null | { content?: string };
|
|
68
|
+
expect(v3).not.toBeNull();
|
|
69
|
+
expect(v3!.content).toBe('some prose');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('returns null for already-migrated relation (v3 shape)', () => {
|
|
73
|
+
expect(migrateRelation({
|
|
74
|
+
comprehensive: {},
|
|
75
|
+
partitives: [{ ref: {}, certainty: 'confirmed' }],
|
|
76
|
+
})).toBeNull();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('migrateConceptData', () => {
|
|
81
|
+
it('returns null for empty partitive_relations', () => {
|
|
82
|
+
expect(migrateConceptData({ partitive_relations: [] })).toBeNull();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('migrates a concept with multiple relations', () => {
|
|
86
|
+
const v1Data = {
|
|
87
|
+
other_field: 'preserved',
|
|
88
|
+
partitive_relations: [
|
|
89
|
+
{ comprehensive: {}, parts: [{ source: 'x', id: 'a' }, { source: 'x', id: 'b' }] },
|
|
90
|
+
{ comprehensive: {}, parts: [{ source: 'x', id: 'c' }, { source: 'x', id: 'd' }] },
|
|
91
|
+
],
|
|
92
|
+
};
|
|
93
|
+
const v3 = migrateConceptData(v1Data) as null | { other_field: string; partitive_relations: Array<{ partitives: Array<{ ref: { source: string; id: string } }> }> };
|
|
94
|
+
expect(v3!.other_field).toBe('preserved');
|
|
95
|
+
expect(v3!.partitive_relations).toHaveLength(2);
|
|
96
|
+
expect(v3!.partitive_relations[0].partitives[0].ref).toEqual({ source: 'x', id: 'a' });
|
|
97
|
+
expect(v3!.partitive_relations[1].partitives[1].ref).toEqual({ source: 'x', id: 'd' });
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('isAlreadyMigrated', () => {
|
|
102
|
+
it('detects v3 (partitives array present)', () => {
|
|
103
|
+
expect(isAlreadyMigrated({ data: { partitive_relations: [{ partitives: [] }] } })).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('returns false for v1 (parts but no partitives)', () => {
|
|
107
|
+
expect(isAlreadyMigrated({ data: { partitive_relations: [{ parts: [] }] } })).toBe(false);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('returns false for empty data', () => {
|
|
111
|
+
expect(isAlreadyMigrated({ data: {} })).toBe(false);
|
|
112
|
+
expect(isAlreadyMigrated(null)).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
PARTITIVE_MULTIPLICITY,
|
|
4
|
+
PARTITIVE_MULTIPLICITY_VALUES,
|
|
5
|
+
isPartitiveMultiplicity,
|
|
6
|
+
multiplicityDefinition,
|
|
7
|
+
rakeStrokeStyle,
|
|
8
|
+
multiplicityFromCertainty,
|
|
9
|
+
NORMAL_STROKE_WIDTH,
|
|
10
|
+
DELIMITING_STROKE_WIDTH,
|
|
11
|
+
type PartitiveMultiplicity,
|
|
12
|
+
} from '../../utils/partitive-multiplicity';
|
|
13
|
+
|
|
14
|
+
describe('partitive-multiplicity registry', () => {
|
|
15
|
+
it('has exactly 5 ISO 704:2022 multiplicity values', () => {
|
|
16
|
+
expect(PARTITIVE_MULTIPLICITY_VALUES).toEqual([
|
|
17
|
+
'compulsory',
|
|
18
|
+
'optional',
|
|
19
|
+
'compulsory_multiple',
|
|
20
|
+
'optional_multiple',
|
|
21
|
+
'compulsory_at_least_one',
|
|
22
|
+
]);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('compulsory: 1 line, solid', () => {
|
|
26
|
+
expect(multiplicityDefinition('compulsory')).toEqual({
|
|
27
|
+
lines: 1, pattern: 'solid', label: 'Compulsory',
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('optional: 1 line, dashed', () => {
|
|
32
|
+
expect(multiplicityDefinition('optional')).toEqual({
|
|
33
|
+
lines: 1, pattern: 'dashed', label: 'Optional',
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('compulsory_multiple: 2 lines, solid', () => {
|
|
38
|
+
expect(multiplicityDefinition('compulsory_multiple')).toEqual({
|
|
39
|
+
lines: 2, pattern: 'solid', label: 'Compulsory multiple',
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('optional_multiple: 2 lines, dashed', () => {
|
|
44
|
+
expect(multiplicityDefinition('optional_multiple')).toEqual({
|
|
45
|
+
lines: 2, pattern: 'dashed', label: 'Optional multiple',
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('compulsory_at_least_one: 2 lines, mixed (one solid + one dashed)', () => {
|
|
50
|
+
expect(multiplicityDefinition('compulsory_at_least_one')).toEqual({
|
|
51
|
+
lines: 2, pattern: 'mixed', label: 'At least one',
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('isPartitiveMultiplicity narrows known values', () => {
|
|
56
|
+
expect(isPartitiveMultiplicity('compulsory')).toBe(true);
|
|
57
|
+
expect(isPartitiveMultiplicity('compulsory_at_least_one')).toBe(true);
|
|
58
|
+
expect(isPartitiveMultiplicity('unknown')).toBe(false);
|
|
59
|
+
expect(isPartitiveMultiplicity(null)).toBe(false);
|
|
60
|
+
expect(isPartitiveMultiplicity(42)).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('rakeStrokeStyle', () => {
|
|
65
|
+
const cases: Array<{
|
|
66
|
+
name: string;
|
|
67
|
+
multiplicity: PartitiveMultiplicity;
|
|
68
|
+
isDelimiting: boolean;
|
|
69
|
+
expected: {
|
|
70
|
+
lineCount: 1 | 2;
|
|
71
|
+
primaryDashed: boolean;
|
|
72
|
+
secondaryDashed: boolean;
|
|
73
|
+
strokeWidth: number;
|
|
74
|
+
};
|
|
75
|
+
}> = [
|
|
76
|
+
// 5 multiplicity × 2 delimiting = 10 variants
|
|
77
|
+
{ name: 'compulsory / non-delimiting', multiplicity: 'compulsory', isDelimiting: false, expected: { lineCount: 1, primaryDashed: false, secondaryDashed: false, strokeWidth: NORMAL_STROKE_WIDTH } },
|
|
78
|
+
{ name: 'compulsory / delimiting', multiplicity: 'compulsory', isDelimiting: true, expected: { lineCount: 1, primaryDashed: false, secondaryDashed: false, strokeWidth: DELIMITING_STROKE_WIDTH } },
|
|
79
|
+
{ name: 'optional / non-delimiting', multiplicity: 'optional', isDelimiting: false, expected: { lineCount: 1, primaryDashed: true, secondaryDashed: true, strokeWidth: NORMAL_STROKE_WIDTH } },
|
|
80
|
+
{ name: 'optional / delimiting', multiplicity: 'optional', isDelimiting: true, expected: { lineCount: 1, primaryDashed: true, secondaryDashed: true, strokeWidth: DELIMITING_STROKE_WIDTH } },
|
|
81
|
+
{ name: 'compulsory_multiple / non-delimiting', multiplicity: 'compulsory_multiple', isDelimiting: false, expected: { lineCount: 2, primaryDashed: false, secondaryDashed: false, strokeWidth: NORMAL_STROKE_WIDTH } },
|
|
82
|
+
{ name: 'compulsory_multiple / delimiting', multiplicity: 'compulsory_multiple', isDelimiting: true, expected: { lineCount: 2, primaryDashed: false, secondaryDashed: false, strokeWidth: DELIMITING_STROKE_WIDTH } },
|
|
83
|
+
{ name: 'optional_multiple / non-delimiting', multiplicity: 'optional_multiple', isDelimiting: false, expected: { lineCount: 2, primaryDashed: true, secondaryDashed: true, strokeWidth: NORMAL_STROKE_WIDTH } },
|
|
84
|
+
{ name: 'optional_multiple / delimiting', multiplicity: 'optional_multiple', isDelimiting: true, expected: { lineCount: 2, primaryDashed: true, secondaryDashed: true, strokeWidth: DELIMITING_STROKE_WIDTH } },
|
|
85
|
+
{ name: 'compulsory_at_least_one / non-delimiting', multiplicity: 'compulsory_at_least_one', isDelimiting: false, expected: { lineCount: 2, primaryDashed: false, secondaryDashed: true, strokeWidth: NORMAL_STROKE_WIDTH } },
|
|
86
|
+
{ name: 'compulsory_at_least_one / delimiting', multiplicity: 'compulsory_at_least_one', isDelimiting: true, expected: { lineCount: 2, primaryDashed: false, secondaryDashed: true, strokeWidth: DELIMITING_STROKE_WIDTH } },
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
for (const c of cases) {
|
|
90
|
+
it(`${c.name} → ${JSON.stringify(c.expected)}`, () => {
|
|
91
|
+
expect(rakeStrokeStyle(c.multiplicity, c.isDelimiting)).toEqual(c.expected);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
it('delimiting strokeWidth is exactly 3× normal (per ISO 704)', () => {
|
|
96
|
+
expect(DELIMITING_STROKE_WIDTH / NORMAL_STROKE_WIDTH).toBe(3);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('multiplicityFromCertainty (glossarist 0.4.20 migration)', () => {
|
|
101
|
+
it('confirmed → compulsory', () => {
|
|
102
|
+
expect(multiplicityFromCertainty('confirmed')).toBe('compulsory');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('possible → optional', () => {
|
|
106
|
+
expect(multiplicityFromCertainty('possible')).toBe('optional');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('null / undefined → compulsory (default)', () => {
|
|
110
|
+
expect(multiplicityFromCertainty(null)).toBe('compulsory');
|
|
111
|
+
expect(multiplicityFromCertainty(undefined)).toBe('compulsory');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -166,20 +166,18 @@ interface JsonLdLocalizedConcept {
|
|
|
166
166
|
|
|
167
167
|
interface JsonLdPartitiveMember {
|
|
168
168
|
'gl:ref'?: JsonLdRef;
|
|
169
|
+
/** ISO 704:2022 v3 field (native in glossarist 0.4.24). */
|
|
170
|
+
'gl:multiplicity'?: string;
|
|
171
|
+
/** ISO 704:2022 v3 field (native in glossarist 0.4.24). */
|
|
172
|
+
'gl:isDelimiting'?: boolean;
|
|
173
|
+
/** Legacy v2 field — still read for backward compat with older datasets. */
|
|
169
174
|
'gl:certainty'?: string;
|
|
170
175
|
}
|
|
171
176
|
|
|
172
|
-
interface JsonLdTypeSharedPlurality {
|
|
173
|
-
'gl:isShared'?: boolean;
|
|
174
|
-
'gl:isUncertain'?: boolean;
|
|
175
|
-
'gl:sharedType'?: JsonLdRef;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
177
|
interface JsonLdPartitiveRelation {
|
|
179
178
|
'gl:comprehensive'?: JsonLdRef;
|
|
180
179
|
'gl:hasPartitive'?: JsonLdPartitiveMember[];
|
|
181
180
|
'gl:completeness'?: string;
|
|
182
|
-
'gl:hasPlurality'?: JsonLdTypeSharedPlurality;
|
|
183
181
|
'gl:criterion'?: Record<string, string> | string;
|
|
184
182
|
}
|
|
185
183
|
|
|
@@ -592,7 +590,16 @@ function mapPartitiveRelationFromJsonLd(r: JsonLdPartitiveRelation): Record<stri
|
|
|
592
590
|
const ref = m['gl:ref'] ? mapRefFromJsonLd(m['gl:ref']) : null;
|
|
593
591
|
if (!ref) return null;
|
|
594
592
|
const out: Record<string, unknown> = { ref };
|
|
595
|
-
|
|
593
|
+
// ISO 704:2022 multiplicity + is_delimiting (v3). Fall back to
|
|
594
|
+
// legacy gl:certainty for backward compat.
|
|
595
|
+
if (m['gl:multiplicity']) {
|
|
596
|
+
out.multiplicity = m['gl:multiplicity'];
|
|
597
|
+
} else if (m['gl:certainty']) {
|
|
598
|
+
out.multiplicity = m['gl:certainty'] === 'possible' ? 'optional' : 'compulsory';
|
|
599
|
+
}
|
|
600
|
+
if (m['gl:isDelimiting'] === true) {
|
|
601
|
+
out.is_delimiting = true;
|
|
602
|
+
}
|
|
596
603
|
return out;
|
|
597
604
|
})
|
|
598
605
|
.filter((m): m is Record<string, unknown> => m !== null);
|
|
@@ -608,17 +615,6 @@ function mapPartitiveRelationFromJsonLd(r: JsonLdPartitiveRelation): Record<stri
|
|
|
608
615
|
|
|
609
616
|
if (r['gl:completeness']) out.completeness = r['gl:completeness'];
|
|
610
617
|
|
|
611
|
-
const plurality = r['gl:hasPlurality'];
|
|
612
|
-
if (plurality && typeof plurality['gl:isShared'] === 'boolean') {
|
|
613
|
-
const p: Record<string, unknown> = { isShared: plurality['gl:isShared'] };
|
|
614
|
-
if (typeof plurality['gl:isUncertain'] === 'boolean') {
|
|
615
|
-
p.isUncertain = plurality['gl:isUncertain'];
|
|
616
|
-
}
|
|
617
|
-
const sharedType = plurality['gl:sharedType'] ? mapRefFromJsonLd(plurality['gl:sharedType']) : null;
|
|
618
|
-
if (sharedType) p.sharedType = sharedType;
|
|
619
|
-
out.plurality = p;
|
|
620
|
-
}
|
|
621
|
-
|
|
622
618
|
if (r['gl:criterion']) {
|
|
623
619
|
out.criterion = typeof r['gl:criterion'] === 'string'
|
|
624
620
|
? { default: r['gl:criterion'] }
|