@glossarist/concept-browser 0.7.86 → 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 +34 -27
- package/scripts/generate-data.mjs +45 -31
- package/src/__tests__/components/PartitiveRelationDiagram.test.ts +37 -31
- package/src/__tests__/composables/use-concept-edges-v2.test.ts +56 -49
- package/src/__tests__/partitive-relation.test.ts +62 -32
- package/src/__tests__/utils/partitive-multiplicity.test.ts +105 -81
- package/src/adapters/model-bridge.ts +46 -13
- package/src/adapters/non-verbal/glossarist-augment.d.ts +64 -49
- package/src/adapters/types.ts +2 -1
- package/src/components/PartitiveRelationDiagram.vue +7 -5
- package/src/components/PartitiveRelationList.vue +6 -5
- package/src/components/RelationSphere.vue +4 -4
- package/src/composables/use-concept-edges.ts +15 -10
- package/src/utils/partitive-multiplicity.ts +69 -76
- package/src/views/ConceptView.vue +4 -3
|
@@ -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,6 +29,29 @@ function extractPartitiveRelations(
|
|
|
21
29
|
return `${uriBase}/${reg}/concept/${id}`;
|
|
22
30
|
};
|
|
23
31
|
|
|
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';
|
|
51
|
+
}
|
|
52
|
+
return 'exactly_one';
|
|
53
|
+
};
|
|
54
|
+
|
|
24
55
|
for (const rel of concept['gl:partitiveRelations'] || []) {
|
|
25
56
|
const comprehensive = resolveConceptUri(rel['gl:comprehensive']);
|
|
26
57
|
if (!comprehensive) continue;
|
|
@@ -30,16 +61,14 @@ function extractPartitiveRelations(
|
|
|
30
61
|
const memberRef = member['gl:ref'] || member;
|
|
31
62
|
const uri = resolveConceptUri(memberRef);
|
|
32
63
|
if (!uri || uri === sourceUri) return null;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
return { uri, multiplicity, isDelimiting };
|
|
64
|
+
return {
|
|
65
|
+
uri,
|
|
66
|
+
presence: resolvePresence(member),
|
|
67
|
+
count: resolveCount(member),
|
|
68
|
+
isDelimiting: member['gl:isDelimiting'] === true || member.isDelimiting === true,
|
|
69
|
+
};
|
|
41
70
|
})
|
|
42
|
-
.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);
|
|
43
72
|
|
|
44
73
|
if (partitives.length === 0) continue;
|
|
45
74
|
|
|
@@ -61,17 +90,13 @@ function extractPartitiveRelations(
|
|
|
61
90
|
return relations;
|
|
62
91
|
}
|
|
63
92
|
|
|
64
|
-
|
|
65
|
-
return ['compulsory', 'optional', 'compulsory_multiple', 'optional_multiple', 'compulsory_at_least_one'].includes(value as string);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
describe('extractPartitiveRelations', () => {
|
|
93
|
+
describe('extractPartitiveRelations (MECE presence × count)', () => {
|
|
69
94
|
const uriBase = 'https://example.org';
|
|
70
95
|
const urnMap = new Map([['urn:vim:pub:v:2:2012', 'vim-2012']]);
|
|
71
96
|
const registerId = 'vim-2012';
|
|
72
97
|
const sourceUri = `${uriBase}/${registerId}/concept/1.3`;
|
|
73
98
|
|
|
74
|
-
it('extracts a complete relation with two
|
|
99
|
+
it('extracts a complete relation with two required/exactly_one members', () => {
|
|
75
100
|
const concept = {
|
|
76
101
|
'@id': sourceUri,
|
|
77
102
|
'gl:partitiveRelations': [
|
|
@@ -90,8 +115,8 @@ describe('extractPartitiveRelations', () => {
|
|
|
90
115
|
expect(result).toHaveLength(1);
|
|
91
116
|
expect(result[0].comprehensive).toBe(`${uriBase}/${registerId}/concept/1.3`);
|
|
92
117
|
expect(result[0].partitives).toEqual([
|
|
93
|
-
{ uri: `${uriBase}/${registerId}/concept/1.4`,
|
|
94
|
-
{ uri: `${uriBase}/${registerId}/concept/1.5`,
|
|
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 },
|
|
95
120
|
]);
|
|
96
121
|
expect(result[0].completeness).toBe('complete');
|
|
97
122
|
});
|
|
@@ -129,7 +154,7 @@ describe('extractPartitiveRelations', () => {
|
|
|
129
154
|
expect(rel.completeness).toBe('partial');
|
|
130
155
|
});
|
|
131
156
|
|
|
132
|
-
it('reads per-member
|
|
157
|
+
it('reads per-member presence + count + isDelimiting (v3 native)', () => {
|
|
133
158
|
const concept = {
|
|
134
159
|
'@id': sourceUri,
|
|
135
160
|
'gl:partitiveRelations': [
|
|
@@ -137,51 +162,56 @@ describe('extractPartitiveRelations', () => {
|
|
|
137
162
|
'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.3' },
|
|
138
163
|
'gl:hasPartitive': [
|
|
139
164
|
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.4' } },
|
|
140
|
-
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.5' }, 'gl:
|
|
141
|
-
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.6' }, 'gl:
|
|
142
|
-
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.7' }, 'gl:
|
|
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 },
|
|
143
168
|
],
|
|
144
169
|
},
|
|
145
170
|
],
|
|
146
171
|
};
|
|
147
172
|
const [rel] = extractPartitiveRelations(concept, registerId, uriBase, urnMap);
|
|
148
173
|
expect(rel.partitives).toEqual([
|
|
149
|
-
{ uri: `${uriBase}/${registerId}/concept/1.4`,
|
|
150
|
-
{ uri: `${uriBase}/${registerId}/concept/1.5`,
|
|
151
|
-
{ uri: `${uriBase}/${registerId}/concept/1.6`,
|
|
152
|
-
{ uri: `${uriBase}/${registerId}/concept/1.7`,
|
|
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 },
|
|
153
178
|
]);
|
|
154
179
|
});
|
|
155
180
|
|
|
156
|
-
it('
|
|
181
|
+
it('migrates legacy gl:multiplicity into presence + count (one-release compat)', () => {
|
|
157
182
|
const concept = {
|
|
158
183
|
'@id': sourceUri,
|
|
159
184
|
'gl:partitiveRelations': [
|
|
160
185
|
{
|
|
161
186
|
'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.3' },
|
|
162
187
|
'gl:hasPartitive': [
|
|
163
|
-
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.4' } },
|
|
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' },
|
|
164
190
|
],
|
|
165
|
-
'gl:completeness': 'closed',
|
|
166
191
|
},
|
|
167
192
|
],
|
|
168
193
|
};
|
|
169
|
-
|
|
194
|
+
const [rel] = extractPartitiveRelations(concept, registerId, uriBase, urnMap);
|
|
195
|
+
expect(rel.partitives).toEqual([
|
|
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 },
|
|
198
|
+
]);
|
|
170
199
|
});
|
|
171
200
|
|
|
172
|
-
it('throws on invalid
|
|
201
|
+
it('throws on invalid completeness', () => {
|
|
173
202
|
const concept = {
|
|
174
203
|
'@id': sourceUri,
|
|
175
204
|
'gl:partitiveRelations': [
|
|
176
205
|
{
|
|
177
206
|
'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.3' },
|
|
178
207
|
'gl:hasPartitive': [
|
|
179
|
-
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.4' }
|
|
208
|
+
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.4' } },
|
|
180
209
|
],
|
|
210
|
+
'gl:completeness': 'closed',
|
|
181
211
|
},
|
|
182
212
|
],
|
|
183
213
|
};
|
|
184
|
-
expect(() => extractPartitiveRelations(concept, registerId, uriBase, urnMap)).toThrow(/Invalid.*
|
|
214
|
+
expect(() => extractPartitiveRelations(concept, registerId, uriBase, urnMap)).toThrow(/Invalid.*completeness.*closed/);
|
|
185
215
|
});
|
|
186
216
|
|
|
187
217
|
it('skips a relation whose comprehensive cannot be resolved', () => {
|
|
@@ -1,113 +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
|
+
*/
|
|
1
9
|
import { describe, it, expect } from 'vitest';
|
|
2
10
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
11
|
+
PARTITIVE_PRESENCE,
|
|
12
|
+
PARTITIVE_COUNT,
|
|
13
|
+
isPartitivePresence,
|
|
14
|
+
isPartitiveCount,
|
|
15
|
+
presenceLabel,
|
|
16
|
+
countLabel,
|
|
7
17
|
rakeStrokeStyle,
|
|
8
|
-
|
|
18
|
+
partitiveMultiplicityName,
|
|
9
19
|
NORMAL_STROKE_WIDTH,
|
|
10
20
|
DELIMITING_STROKE_WIDTH,
|
|
11
|
-
type
|
|
21
|
+
type PartitivePresence,
|
|
22
|
+
type PartitiveCount,
|
|
12
23
|
} from '../../utils/partitive-multiplicity';
|
|
13
24
|
|
|
14
|
-
describe('
|
|
15
|
-
it('has exactly
|
|
16
|
-
expect(
|
|
17
|
-
'compulsory',
|
|
18
|
-
'optional',
|
|
19
|
-
'compulsory_multiple',
|
|
20
|
-
'optional_multiple',
|
|
21
|
-
'compulsory_at_least_one',
|
|
22
|
-
]);
|
|
25
|
+
describe('presence axis (re-exported from glossarist)', () => {
|
|
26
|
+
it('has exactly 2 values: required, optional', () => {
|
|
27
|
+
expect([...PARTITIVE_PRESENCE.VALUES]).toEqual(['required', 'optional']);
|
|
23
28
|
});
|
|
24
29
|
|
|
25
|
-
it('
|
|
26
|
-
expect(
|
|
27
|
-
|
|
28
|
-
|
|
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);
|
|
29
36
|
});
|
|
30
37
|
|
|
31
|
-
it('
|
|
32
|
-
expect(
|
|
33
|
-
|
|
34
|
-
|
|
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']);
|
|
35
47
|
});
|
|
36
48
|
|
|
37
|
-
it('
|
|
38
|
-
expect(
|
|
39
|
-
|
|
40
|
-
|
|
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);
|
|
41
55
|
});
|
|
42
56
|
|
|
43
|
-
it('
|
|
44
|
-
expect(
|
|
45
|
-
|
|
46
|
-
|
|
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');
|
|
47
61
|
});
|
|
62
|
+
});
|
|
48
63
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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');
|
|
53
71
|
});
|
|
54
72
|
|
|
55
|
-
it('
|
|
56
|
-
expect(
|
|
57
|
-
|
|
58
|
-
expect(isPartitiveMultiplicity('unknown')).toBe(false);
|
|
59
|
-
expect(isPartitiveMultiplicity(null)).toBe(false);
|
|
60
|
-
expect(isPartitiveMultiplicity(42)).toBe(false);
|
|
73
|
+
it('rejects the vacuous (optional, at_least_one) combination', () => {
|
|
74
|
+
expect(() => partitiveMultiplicityName('optional', 'at_least_one'))
|
|
75
|
+
.toThrow(/collapses to optional \+ multiple/);
|
|
61
76
|
});
|
|
62
77
|
});
|
|
63
78
|
|
|
64
|
-
describe('rakeStrokeStyle', () => {
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
];
|
|
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];
|
|
88
83
|
|
|
89
|
-
for (const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
+
}
|
|
93
111
|
}
|
|
94
112
|
|
|
95
|
-
it('delimiting strokeWidth is
|
|
113
|
+
it('delimiting strokeWidth is 3× normal (per ISO 704 convention)', () => {
|
|
96
114
|
expect(DELIMITING_STROKE_WIDTH / NORMAL_STROKE_WIDTH).toBe(3);
|
|
97
115
|
});
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
describe('multiplicityFromCertainty (glossarist 0.4.20 migration)', () => {
|
|
101
|
-
it('confirmed → compulsory', () => {
|
|
102
|
-
expect(multiplicityFromCertainty('confirmed')).toBe('compulsory');
|
|
103
|
-
});
|
|
104
116
|
|
|
105
|
-
it('
|
|
106
|
-
|
|
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
|
+
}
|
|
107
123
|
});
|
|
108
124
|
|
|
109
|
-
it('
|
|
110
|
-
|
|
111
|
-
|
|
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
|
+
}
|
|
112
136
|
});
|
|
113
137
|
});
|
|
@@ -25,6 +25,10 @@ import {
|
|
|
25
25
|
DATE_TYPES,
|
|
26
26
|
ConceptRef,
|
|
27
27
|
} from 'glossarist';
|
|
28
|
+
import {
|
|
29
|
+
isPartitivePresence,
|
|
30
|
+
isPartitiveCount,
|
|
31
|
+
} from '../utils/partitive-multiplicity';
|
|
28
32
|
import {
|
|
29
33
|
LetterSymbol,
|
|
30
34
|
GrammarInfo,
|
|
@@ -166,11 +170,15 @@ interface JsonLdLocalizedConcept {
|
|
|
166
170
|
|
|
167
171
|
interface JsonLdPartitiveMember {
|
|
168
172
|
'gl:ref'?: JsonLdRef;
|
|
169
|
-
/** ISO 704:2022
|
|
170
|
-
'gl:
|
|
171
|
-
/** ISO 704:2022
|
|
173
|
+
/** ISO 704:2022 MECE: presence (required | optional) */
|
|
174
|
+
'gl:presence'?: string;
|
|
175
|
+
/** ISO 704:2022 MECE: count (exactly_one | at_least_one | multiple) */
|
|
176
|
+
'gl:count'?: string;
|
|
177
|
+
/** ISO 704:2022: delimiting part (3× stroke width in diagrams) */
|
|
172
178
|
'gl:isDelimiting'?: boolean;
|
|
173
|
-
/** Legacy
|
|
179
|
+
/** Legacy v3 field — one-string multiplicity, split into presence+count */
|
|
180
|
+
'gl:multiplicity'?: string;
|
|
181
|
+
/** Legacy v2 field — migrated to presence+count */
|
|
174
182
|
'gl:certainty'?: string;
|
|
175
183
|
}
|
|
176
184
|
|
|
@@ -590,16 +598,23 @@ function mapPartitiveRelationFromJsonLd(r: JsonLdPartitiveRelation): Record<stri
|
|
|
590
598
|
const ref = m['gl:ref'] ? mapRefFromJsonLd(m['gl:ref']) : null;
|
|
591
599
|
if (!ref) return null;
|
|
592
600
|
const out: Record<string, unknown> = { ref };
|
|
593
|
-
// ISO 704:2022
|
|
594
|
-
// legacy
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
601
|
+
// ISO 704:2022 MECE: prefer presence × count from JSON-LD. Fall back
|
|
602
|
+
// to migrating the legacy one-string `multiplicity` or v2 `certainty`
|
|
603
|
+
// so data in transit from older glossarist versions still loads.
|
|
604
|
+
const presence = m['gl:presence'];
|
|
605
|
+
const count = m['gl:count'];
|
|
606
|
+
if (isPartitivePresence(presence) && isPartitiveCount(count)) {
|
|
607
|
+
out.presence = presence;
|
|
608
|
+
out.count = count;
|
|
609
|
+
} else {
|
|
610
|
+
const raw = m['gl:multiplicity'] ?? splitLegacyCertainty(m['gl:certainty']);
|
|
611
|
+
if (raw) {
|
|
612
|
+
const parts = splitMultiplicity(raw);
|
|
613
|
+
out.presence = parts.presence;
|
|
614
|
+
out.count = parts.count;
|
|
615
|
+
}
|
|
602
616
|
}
|
|
617
|
+
if (m['gl:isDelimiting'] === true) out.is_delimiting = true;
|
|
603
618
|
return out;
|
|
604
619
|
})
|
|
605
620
|
.filter((m): m is Record<string, unknown> => m !== null);
|
|
@@ -624,6 +639,24 @@ function mapPartitiveRelationFromJsonLd(r: JsonLdPartitiveRelation): Record<stri
|
|
|
624
639
|
return out;
|
|
625
640
|
}
|
|
626
641
|
|
|
642
|
+
/** Migrate legacy v2 certainty → old v3 multiplicity string. */
|
|
643
|
+
function splitLegacyCertainty(certainty: string | undefined): string | null {
|
|
644
|
+
if (!certainty) return null;
|
|
645
|
+
return certainty === 'possible' ? 'optional' : 'compulsory';
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/** Split the legacy one-string multiplicity into MECE presence × count. */
|
|
649
|
+
function splitMultiplicity(m: string): { presence: 'required' | 'optional'; count: 'exactly_one' | 'at_least_one' | 'multiple' } {
|
|
650
|
+
const LEGACY_MAP: Record<string, { presence: 'required' | 'optional'; count: 'exactly_one' | 'at_least_one' | 'multiple' }> = {
|
|
651
|
+
compulsory: { presence: 'required', count: 'exactly_one' },
|
|
652
|
+
optional: { presence: 'optional', count: 'exactly_one' },
|
|
653
|
+
compulsory_multiple: { presence: 'required', count: 'multiple' },
|
|
654
|
+
optional_multiple: { presence: 'optional', count: 'multiple' },
|
|
655
|
+
compulsory_at_least_one: { presence: 'required', count: 'at_least_one' },
|
|
656
|
+
};
|
|
657
|
+
return LEGACY_MAP[m] ?? { presence: 'required', count: 'exactly_one' };
|
|
658
|
+
}
|
|
659
|
+
|
|
627
660
|
function conceptFromJsonLd(doc: JsonLdConcept): Concept {
|
|
628
661
|
const id = String(doc['gl:identifier'] ?? doc['@id']?.split('/').pop() ?? '');
|
|
629
662
|
const localizations: Record<string, unknown> = {};
|