@glossarist/concept-browser 0.7.85 → 0.7.87
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 +50 -27
- package/scripts/generate-data.mjs +59 -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 +98 -136
- package/src/__tests__/composables/use-concept-edges-v2.test.ts +123 -88
- package/src/__tests__/partitive-relation-styling.test.ts +12 -54
- package/src/__tests__/partitive-relation.test.ts +72 -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 +137 -0
- package/src/adapters/model-bridge.ts +48 -19
- package/src/adapters/non-verbal/glossarist-augment.d.ts +85 -54
- package/src/adapters/types.ts +3 -8
- package/src/components/PartitiveRelationDiagram.vue +90 -76
- package/src/components/PartitiveRelationList.vue +27 -34
- package/src/components/RelationSphere.vue +58 -73
- package/src/composables/use-concept-edges.ts +26 -21
- package/src/utils/partitive-multiplicity.ts +99 -0
- package/src/utils/partitive-relation-styling.ts +29 -38
- package/src/views/ConceptView.vue +8 -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
|
});
|
|
@@ -13,6 +13,14 @@ function extractPartitiveRelations(
|
|
|
13
13
|
const relations: PartitiveRelationWire[] = [];
|
|
14
14
|
const sourceUri = concept['@id'];
|
|
15
15
|
|
|
16
|
+
const LEGACY_MULTIPLICITY_MAP: Record<string, { presence: string; count: string }> = {
|
|
17
|
+
compulsory: { presence: 'required', count: 'exactly_one' },
|
|
18
|
+
optional: { presence: 'optional', count: 'exactly_one' },
|
|
19
|
+
compulsory_multiple: { presence: 'required', count: 'multiple' },
|
|
20
|
+
optional_multiple: { presence: 'optional', count: 'multiple' },
|
|
21
|
+
compulsory_at_least_one: { presence: 'required', count: 'at_least_one' },
|
|
22
|
+
};
|
|
23
|
+
|
|
16
24
|
const resolveConceptUri = (ref: any): string | null => {
|
|
17
25
|
const source = ref?.['gl:source'] || ref?.source;
|
|
18
26
|
const id = ref?.['gl:id'] || ref?.id;
|
|
@@ -21,89 +29,96 @@ function extractPartitiveRelations(
|
|
|
21
29
|
return `${uriBase}/${reg}/concept/${id}`;
|
|
22
30
|
};
|
|
23
31
|
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
const resolvePresence = (member: any): 'required' | 'optional' => {
|
|
33
|
+
const p = member['gl:presence'] ?? member.presence;
|
|
34
|
+
if (p === 'required' || p === 'optional') return p as 'required' | 'optional';
|
|
35
|
+
const multiplicity = member['gl:multiplicity'] ?? member.multiplicity;
|
|
36
|
+
if (typeof multiplicity === 'string' && LEGACY_MULTIPLICITY_MAP[multiplicity]) {
|
|
37
|
+
return LEGACY_MULTIPLICITY_MAP[multiplicity].presence as 'required' | 'optional';
|
|
38
|
+
}
|
|
39
|
+
const certainty = member['gl:certainty'] ?? member.certainty;
|
|
40
|
+
return certainty === 'possible' ? 'optional' : 'required';
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const resolveCount = (member: any): 'exactly_one' | 'at_least_one' | 'multiple' => {
|
|
44
|
+
const c = member['gl:count'] ?? member.count;
|
|
45
|
+
if (c === 'exactly_one' || c === 'at_least_one' || c === 'multiple') {
|
|
46
|
+
return c as 'exactly_one' | 'at_least_one' | 'multiple';
|
|
47
|
+
}
|
|
48
|
+
const multiplicity = member['gl:multiplicity'] ?? member.multiplicity;
|
|
49
|
+
if (typeof multiplicity === 'string' && LEGACY_MULTIPLICITY_MAP[multiplicity]) {
|
|
50
|
+
return LEGACY_MULTIPLICITY_MAP[multiplicity].count as 'exactly_one' | 'at_least_one' | 'multiple';
|
|
35
51
|
}
|
|
36
|
-
return
|
|
52
|
+
return 'exactly_one';
|
|
37
53
|
};
|
|
38
54
|
|
|
39
55
|
for (const rel of concept['gl:partitiveRelations'] || []) {
|
|
40
56
|
const comprehensive = resolveConceptUri(rel['gl:comprehensive']);
|
|
41
57
|
if (!comprehensive) continue;
|
|
58
|
+
|
|
42
59
|
const partitives = (rel['gl:hasPartitive'] || [])
|
|
43
60
|
.map((member: any) => {
|
|
44
|
-
const
|
|
45
|
-
const uri = resolveConceptUri(
|
|
61
|
+
const memberRef = member['gl:ref'] || member;
|
|
62
|
+
const uri = resolveConceptUri(memberRef);
|
|
46
63
|
if (!uri || uri === sourceUri) return null;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
return { uri, certainty };
|
|
64
|
+
return {
|
|
65
|
+
uri,
|
|
66
|
+
presence: resolvePresence(member),
|
|
67
|
+
count: resolveCount(member),
|
|
68
|
+
isDelimiting: member['gl:isDelimiting'] === true || member.isDelimiting === true,
|
|
69
|
+
};
|
|
54
70
|
})
|
|
55
|
-
.filter((p: any): p is { uri: string;
|
|
71
|
+
.filter((p: any): p is { uri: string; presence: 'required' | 'optional'; count: 'exactly_one' | 'at_least_one' | 'multiple'; isDelimiting: boolean } => p !== null);
|
|
72
|
+
|
|
56
73
|
if (partitives.length === 0) continue;
|
|
74
|
+
|
|
57
75
|
const completeness = rel['gl:completeness'] || 'complete';
|
|
58
76
|
if (completeness !== 'complete' && completeness !== 'partial') {
|
|
59
77
|
throw new Error(
|
|
60
78
|
`Invalid partitive relation completeness: "${completeness}". Allowed: complete, partial`,
|
|
61
79
|
);
|
|
62
80
|
}
|
|
81
|
+
|
|
63
82
|
relations.push({
|
|
64
83
|
source: sourceUri,
|
|
65
84
|
comprehensive,
|
|
66
85
|
partitives,
|
|
67
86
|
completeness,
|
|
68
|
-
plurality: resolvePlurality(rel['gl:hasPlurality']) as PartitiveRelationWire['plurality'],
|
|
69
|
-
criterion: rel['gl:criterion'],
|
|
70
87
|
register: registerId,
|
|
71
88
|
});
|
|
72
89
|
}
|
|
73
90
|
return relations;
|
|
74
91
|
}
|
|
75
92
|
|
|
76
|
-
describe('extractPartitiveRelations', () => {
|
|
93
|
+
describe('extractPartitiveRelations (MECE presence × count)', () => {
|
|
77
94
|
const uriBase = 'https://example.org';
|
|
78
95
|
const urnMap = new Map([['urn:vim:pub:v:2:2012', 'vim-2012']]);
|
|
79
96
|
const registerId = 'vim-2012';
|
|
80
|
-
const sourceUri = `${uriBase}/${registerId}/concept/
|
|
97
|
+
const sourceUri = `${uriBase}/${registerId}/concept/1.3`;
|
|
81
98
|
|
|
82
|
-
it('extracts a complete relation with two
|
|
99
|
+
it('extracts a complete relation with two required/exactly_one members', () => {
|
|
83
100
|
const concept = {
|
|
84
101
|
'@id': sourceUri,
|
|
85
102
|
'gl:partitiveRelations': [
|
|
86
103
|
{
|
|
87
|
-
'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' },
|
|
88
105
|
'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': '
|
|
106
|
+
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.4' } },
|
|
107
|
+
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.5' } },
|
|
91
108
|
],
|
|
92
109
|
'gl:completeness': 'complete',
|
|
93
|
-
'gl:criterion': { eng: 'measurement result composition' },
|
|
94
110
|
},
|
|
95
111
|
],
|
|
96
112
|
};
|
|
97
113
|
|
|
98
114
|
const result = extractPartitiveRelations(concept, registerId, uriBase, urnMap);
|
|
99
115
|
expect(result).toHaveLength(1);
|
|
100
|
-
expect(result[0].comprehensive).toBe(`${uriBase}/${registerId}/concept/
|
|
116
|
+
expect(result[0].comprehensive).toBe(`${uriBase}/${registerId}/concept/1.3`);
|
|
101
117
|
expect(result[0].partitives).toEqual([
|
|
102
|
-
{ uri: `${uriBase}/${registerId}/concept/
|
|
103
|
-
{ uri: `${uriBase}/${registerId}/concept/
|
|
118
|
+
{ uri: `${uriBase}/${registerId}/concept/1.4`, presence: 'required', count: 'exactly_one', isDelimiting: false },
|
|
119
|
+
{ uri: `${uriBase}/${registerId}/concept/1.5`, presence: 'required', count: 'exactly_one', isDelimiting: false },
|
|
104
120
|
]);
|
|
105
121
|
expect(result[0].completeness).toBe('complete');
|
|
106
|
-
expect(result[0].criterion).toEqual({ eng: 'measurement result composition' });
|
|
107
122
|
});
|
|
108
123
|
|
|
109
124
|
it('defaults completeness to complete when omitted', () => {
|
|
@@ -111,9 +126,9 @@ describe('extractPartitiveRelations', () => {
|
|
|
111
126
|
'@id': sourceUri,
|
|
112
127
|
'gl:partitiveRelations': [
|
|
113
128
|
{
|
|
114
|
-
'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '
|
|
129
|
+
'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.3' },
|
|
115
130
|
'gl:hasPartitive': [
|
|
116
|
-
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '
|
|
131
|
+
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.4' } },
|
|
117
132
|
],
|
|
118
133
|
},
|
|
119
134
|
],
|
|
@@ -139,7 +154,7 @@ describe('extractPartitiveRelations', () => {
|
|
|
139
154
|
expect(rel.completeness).toBe('partial');
|
|
140
155
|
});
|
|
141
156
|
|
|
142
|
-
it('
|
|
157
|
+
it('reads per-member presence + count + isDelimiting (v3 native)', () => {
|
|
143
158
|
const concept = {
|
|
144
159
|
'@id': sourceUri,
|
|
145
160
|
'gl:partitiveRelations': [
|
|
@@ -147,57 +162,39 @@ describe('extractPartitiveRelations', () => {
|
|
|
147
162
|
'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.3' },
|
|
148
163
|
'gl:hasPartitive': [
|
|
149
164
|
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.4' } },
|
|
165
|
+
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.5' }, 'gl:presence': 'optional' },
|
|
166
|
+
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.6' }, 'gl:count': 'multiple' },
|
|
167
|
+
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.7' }, 'gl:count': 'at_least_one', 'gl:isDelimiting': true },
|
|
150
168
|
],
|
|
151
|
-
'gl:hasPlurality': {
|
|
152
|
-
'gl:isShared': true,
|
|
153
|
-
'gl:isUncertain': false,
|
|
154
|
-
'gl:sharedType': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.99' },
|
|
155
|
-
},
|
|
156
169
|
},
|
|
157
170
|
],
|
|
158
171
|
};
|
|
159
172
|
const [rel] = extractPartitiveRelations(concept, registerId, uriBase, urnMap);
|
|
160
|
-
expect(rel.
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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();
|
|
173
|
+
expect(rel.partitives).toEqual([
|
|
174
|
+
{ uri: `${uriBase}/${registerId}/concept/1.4`, presence: 'required', count: 'exactly_one', isDelimiting: false },
|
|
175
|
+
{ uri: `${uriBase}/${registerId}/concept/1.5`, presence: 'optional', count: 'exactly_one', isDelimiting: false },
|
|
176
|
+
{ uri: `${uriBase}/${registerId}/concept/1.6`, presence: 'required', count: 'multiple', isDelimiting: false },
|
|
177
|
+
{ uri: `${uriBase}/${registerId}/concept/1.7`, presence: 'required', count: 'at_least_one', isDelimiting: true },
|
|
178
|
+
]);
|
|
182
179
|
});
|
|
183
180
|
|
|
184
|
-
it('
|
|
181
|
+
it('migrates legacy gl:multiplicity into presence + count (one-release compat)', () => {
|
|
185
182
|
const concept = {
|
|
186
183
|
'@id': sourceUri,
|
|
187
184
|
'gl:partitiveRelations': [
|
|
188
185
|
{
|
|
189
186
|
'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.3' },
|
|
190
187
|
'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:
|
|
188
|
+
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.4' }, 'gl:multiplicity': 'optional_multiple' },
|
|
189
|
+
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.5' }, 'gl:multiplicity': 'compulsory_at_least_one' },
|
|
193
190
|
],
|
|
194
191
|
},
|
|
195
192
|
],
|
|
196
193
|
};
|
|
197
194
|
const [rel] = extractPartitiveRelations(concept, registerId, uriBase, urnMap);
|
|
198
195
|
expect(rel.partitives).toEqual([
|
|
199
|
-
{ uri: `${uriBase}/${registerId}/concept/1.4`,
|
|
200
|
-
{ uri: `${uriBase}/${registerId}/concept/1.5`,
|
|
196
|
+
{ uri: `${uriBase}/${registerId}/concept/1.4`, presence: 'optional', count: 'multiple', isDelimiting: false },
|
|
197
|
+
{ uri: `${uriBase}/${registerId}/concept/1.5`, presence: 'required', count: 'at_least_one', isDelimiting: false },
|
|
201
198
|
]);
|
|
202
199
|
});
|
|
203
200
|
|
|
@@ -217,21 +214,6 @@ describe('extractPartitiveRelations', () => {
|
|
|
217
214
|
expect(() => extractPartitiveRelations(concept, registerId, uriBase, urnMap)).toThrow(/Invalid.*completeness.*closed/);
|
|
218
215
|
});
|
|
219
216
|
|
|
220
|
-
it('throws on invalid member certainty', () => {
|
|
221
|
-
const concept = {
|
|
222
|
-
'@id': sourceUri,
|
|
223
|
-
'gl:partitiveRelations': [
|
|
224
|
-
{
|
|
225
|
-
'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.3' },
|
|
226
|
-
'gl:hasPartitive': [
|
|
227
|
-
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.4' }, 'gl:certainty': 'maybe' },
|
|
228
|
-
],
|
|
229
|
-
},
|
|
230
|
-
],
|
|
231
|
-
};
|
|
232
|
-
expect(() => extractPartitiveRelations(concept, registerId, uriBase, urnMap)).toThrow(/Invalid.*certainty.*maybe/);
|
|
233
|
-
});
|
|
234
|
-
|
|
235
217
|
it('skips a relation whose comprehensive cannot be resolved', () => {
|
|
236
218
|
const concept = {
|
|
237
219
|
'@id': sourceUri,
|
|
@@ -252,7 +234,7 @@ describe('extractPartitiveRelations', () => {
|
|
|
252
234
|
'@id': sourceUri,
|
|
253
235
|
'gl:partitiveRelations': [
|
|
254
236
|
{
|
|
255
|
-
'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '
|
|
237
|
+
'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.3' },
|
|
256
238
|
'gl:hasPartitive': [{ 'gl:ref': { 'gl:id': '1.4' } }],
|
|
257
239
|
},
|
|
258
240
|
],
|
|
@@ -265,17 +247,17 @@ describe('extractPartitiveRelations', () => {
|
|
|
265
247
|
'@id': sourceUri,
|
|
266
248
|
'gl:partitiveRelations': [
|
|
267
249
|
{
|
|
268
|
-
'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '
|
|
250
|
+
'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.3' },
|
|
269
251
|
'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': '
|
|
252
|
+
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.4' } },
|
|
253
|
+
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.3' } },
|
|
272
254
|
],
|
|
273
255
|
},
|
|
274
256
|
],
|
|
275
257
|
};
|
|
276
258
|
const [rel] = extractPartitiveRelations(concept, registerId, uriBase, urnMap);
|
|
277
259
|
expect(rel.partitives).toHaveLength(1);
|
|
278
|
-
expect(rel.partitives[0].uri).toBe(`${uriBase}/${registerId}/concept/
|
|
260
|
+
expect(rel.partitives[0].uri).toBe(`${uriBase}/${registerId}/concept/1.4`);
|
|
279
261
|
});
|
|
280
262
|
|
|
281
263
|
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,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MECE refactor of partitive-multiplicity (ISO 704:2022).
|
|
3
|
+
*
|
|
4
|
+
* Types + enums come from glossarist-js (the SSOT for the model).
|
|
5
|
+
* The render-ready stroke style is the only browser-specific concern;
|
|
6
|
+
* its truth table is pinned here so future rendering edits stay
|
|
7
|
+
* aligned with the spec.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, it, expect } from 'vitest';
|
|
10
|
+
import {
|
|
11
|
+
PARTITIVE_PRESENCE,
|
|
12
|
+
PARTITIVE_COUNT,
|
|
13
|
+
isPartitivePresence,
|
|
14
|
+
isPartitiveCount,
|
|
15
|
+
presenceLabel,
|
|
16
|
+
countLabel,
|
|
17
|
+
rakeStrokeStyle,
|
|
18
|
+
partitiveMultiplicityName,
|
|
19
|
+
NORMAL_STROKE_WIDTH,
|
|
20
|
+
DELIMITING_STROKE_WIDTH,
|
|
21
|
+
type PartitivePresence,
|
|
22
|
+
type PartitiveCount,
|
|
23
|
+
} from '../../utils/partitive-multiplicity';
|
|
24
|
+
|
|
25
|
+
describe('presence axis (re-exported from glossarist)', () => {
|
|
26
|
+
it('has exactly 2 values: required, optional', () => {
|
|
27
|
+
expect([...PARTITIVE_PRESENCE.VALUES]).toEqual(['required', 'optional']);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('isPartitivePresence narrows known values', () => {
|
|
31
|
+
expect(isPartitivePresence('required')).toBe(true);
|
|
32
|
+
expect(isPartitivePresence('optional')).toBe(true);
|
|
33
|
+
expect(isPartitivePresence('unknown')).toBe(false);
|
|
34
|
+
expect(isPartitivePresence(null)).toBe(false);
|
|
35
|
+
expect(isPartitivePresence(42)).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('presenceLabel returns human-readable labels', () => {
|
|
39
|
+
expect(presenceLabel('required')).toBe('Required');
|
|
40
|
+
expect(presenceLabel('optional')).toBe('Optional');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('count axis (re-exported from glossarist)', () => {
|
|
45
|
+
it('has exactly 3 values: exactly_one, at_least_one, multiple', () => {
|
|
46
|
+
expect([...PARTITIVE_COUNT.VALUES]).toEqual(['exactly_one', 'at_least_one', 'multiple']);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('isPartitiveCount narrows known values', () => {
|
|
50
|
+
expect(isPartitiveCount('exactly_one')).toBe(true);
|
|
51
|
+
expect(isPartitiveCount('at_least_one')).toBe(true);
|
|
52
|
+
expect(isPartitiveCount('multiple')).toBe(true);
|
|
53
|
+
expect(isPartitiveCount('unknown')).toBe(false);
|
|
54
|
+
expect(isPartitiveCount(null)).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('countLabel returns human-readable labels', () => {
|
|
58
|
+
expect(countLabel('exactly_one')).toBe('Exactly one');
|
|
59
|
+
expect(countLabel('at_least_one')).toBe('At least one');
|
|
60
|
+
expect(countLabel('multiple')).toBe('Multiple');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('partitiveMultiplicityName (glossarist SSOT)', () => {
|
|
65
|
+
it('maps the 5 valid combinations to ISO 704 names', () => {
|
|
66
|
+
expect(partitiveMultiplicityName('required', 'exactly_one')).toBe('compulsory');
|
|
67
|
+
expect(partitiveMultiplicityName('optional', 'exactly_one')).toBe('optional');
|
|
68
|
+
expect(partitiveMultiplicityName('required', 'multiple')).toBe('compulsory_multiple');
|
|
69
|
+
expect(partitiveMultiplicityName('optional', 'multiple')).toBe('optional_multiple');
|
|
70
|
+
expect(partitiveMultiplicityName('required', 'at_least_one')).toBe('compulsory_at_least_one');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('rejects the vacuous (optional, at_least_one) combination', () => {
|
|
74
|
+
expect(() => partitiveMultiplicityName('optional', 'at_least_one'))
|
|
75
|
+
.toThrow(/collapses to optional \+ multiple/);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('rakeStrokeStyle (MECE rendering truth table)', () => {
|
|
80
|
+
const presenceCases: PartitivePresence[] = ['required', 'optional'];
|
|
81
|
+
const countCases: PartitiveCount[] = ['exactly_one', 'at_least_one', 'multiple'];
|
|
82
|
+
const delimitingCases = [false, true];
|
|
83
|
+
|
|
84
|
+
for (const presence of presenceCases) {
|
|
85
|
+
for (const count of countCases) {
|
|
86
|
+
for (const isDelimiting of delimitingCases) {
|
|
87
|
+
const expectedWidth = isDelimiting ? DELIMITING_STROKE_WIDTH : NORMAL_STROKE_WIDTH;
|
|
88
|
+
const expectedPrimaryDashed = presence === 'optional';
|
|
89
|
+
const expectedLineCount: 1 | 2 = count === 'exactly_one' ? 1 : 2;
|
|
90
|
+
// When lineCount=1, secondaryDashed is unused (no second line).
|
|
91
|
+
// Implementation choice: set it to false as the default.
|
|
92
|
+
// When lineCount=2:
|
|
93
|
+
// - at_least_one: secondary always dashed ("possibly more")
|
|
94
|
+
// - multiple: secondary mirrors primary
|
|
95
|
+
const expectedSecondaryDashed = expectedLineCount === 1
|
|
96
|
+
? false
|
|
97
|
+
: count === 'at_least_one'
|
|
98
|
+
? true
|
|
99
|
+
: expectedPrimaryDashed;
|
|
100
|
+
|
|
101
|
+
it(`${presence} × ${count}${isDelimiting ? ' / delimiting' : ''}`, () => {
|
|
102
|
+
expect(rakeStrokeStyle(presence, count, isDelimiting)).toEqual({
|
|
103
|
+
lineCount: expectedLineCount,
|
|
104
|
+
primaryDashed: expectedPrimaryDashed,
|
|
105
|
+
secondaryDashed: expectedSecondaryDashed,
|
|
106
|
+
strokeWidth: expectedWidth,
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
it('delimiting strokeWidth is 3× normal (per ISO 704 convention)', () => {
|
|
114
|
+
expect(DELIMITING_STROKE_WIDTH / NORMAL_STROKE_WIDTH).toBe(3);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('exactly_one always produces a single line, regardless of presence', () => {
|
|
118
|
+
for (const presence of presenceCases) {
|
|
119
|
+
const style = rakeStrokeStyle(presence, 'exactly_one', false);
|
|
120
|
+
expect(style.lineCount).toBe(1);
|
|
121
|
+
expect(style.primaryDashed).toBe(presence === 'optional');
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('at_least_one differs from multiple by secondary line dash pattern', () => {
|
|
126
|
+
for (const presence of presenceCases) {
|
|
127
|
+
const atleastOne = rakeStrokeStyle(presence, 'at_least_one', false);
|
|
128
|
+
const multiple = rakeStrokeStyle(presence, 'multiple', false);
|
|
129
|
+
expect(atleastOne.lineCount).toBe(2);
|
|
130
|
+
expect(multiple.lineCount).toBe(2);
|
|
131
|
+
// at_least_one: secondary always dashed (possibly more)
|
|
132
|
+
expect(atleastOne.secondaryDashed).toBe(true);
|
|
133
|
+
// multiple: secondary matches primary
|
|
134
|
+
expect(multiple.secondaryDashed).toBe(multiple.primaryDashed);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
});
|