@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glossarist/concept-browser",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.87",
|
|
4
4
|
"description": "Vue SPA for browsing Glossarist terminology datasets with cross-reference resolution, graph visualization, and multi-language support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"astro": "^7.0.7",
|
|
38
38
|
"d3": "^7.9.0",
|
|
39
39
|
"favicons": "^7.2.0",
|
|
40
|
-
"glossarist": "^0.4.
|
|
40
|
+
"glossarist": "^0.4.26",
|
|
41
41
|
"js-yaml": "^4.1.0",
|
|
42
42
|
"jszip": "^3.10.1",
|
|
43
43
|
"pinia": "^2.3.1",
|
package/scripts/build-edges.js
CHANGED
|
@@ -3,14 +3,6 @@ function buildConceptUri(uriBase, registerId, conceptId) {
|
|
|
3
3
|
}
|
|
4
4
|
|
|
5
5
|
const VALID_COMPLETENESS = new Set(['complete', 'partial']);
|
|
6
|
-
const VALID_CERTAINTY = new Set(['confirmed', 'possible']);
|
|
7
|
-
const VALID_MULTIPLICITY = new Set([
|
|
8
|
-
'compulsory',
|
|
9
|
-
'optional',
|
|
10
|
-
'compulsory_multiple',
|
|
11
|
-
'optional_multiple',
|
|
12
|
-
'compulsory_at_least_one',
|
|
13
|
-
]);
|
|
14
6
|
|
|
15
7
|
function validateCompleteness(value) {
|
|
16
8
|
if (value == null) return 'complete';
|
|
@@ -32,26 +24,40 @@ function validateCompleteness(value) {
|
|
|
32
24
|
* confirmed → compulsory
|
|
33
25
|
* possible → optional
|
|
34
26
|
*/
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
27
|
+
const VALID_PRESENCE = new Set(['required', 'optional']);
|
|
28
|
+
const VALID_COUNT = new Set(['exactly_one', 'at_least_one', 'multiple']);
|
|
29
|
+
const LEGACY_MULTIPLICITY_MAP = {
|
|
30
|
+
compulsory: { presence: 'required', count: 'exactly_one' },
|
|
31
|
+
optional: { presence: 'optional', count: 'exactly_one' },
|
|
32
|
+
compulsory_multiple: { presence: 'required', count: 'multiple' },
|
|
33
|
+
optional_multiple: { presence: 'optional', count: 'multiple' },
|
|
34
|
+
compulsory_at_least_one: { presence: 'required', count: 'at_least_one' },
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function resolvePresence(member) {
|
|
38
|
+
const p = member['gl:presence'] ?? member.presence;
|
|
39
|
+
if (p != null) {
|
|
40
|
+
if (!VALID_PRESENCE.has(p)) throw new Error(`Invalid presence: "${p}"`);
|
|
41
|
+
return p;
|
|
44
42
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
return
|
|
43
|
+
return migrateMultiplicity(member).presence;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function resolveCount(member) {
|
|
47
|
+
const c = member['gl:count'] ?? member.count;
|
|
48
|
+
if (c != null) {
|
|
49
|
+
if (!VALID_COUNT.has(c)) throw new Error(`Invalid count: "${c}"`);
|
|
50
|
+
return c;
|
|
53
51
|
}
|
|
54
|
-
return
|
|
52
|
+
return migrateMultiplicity(member).count;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function migrateMultiplicity(member) {
|
|
56
|
+
const m = member['gl:multiplicity'] ?? member.multiplicity;
|
|
57
|
+
if (m && LEGACY_MULTIPLICITY_MAP[m]) return LEGACY_MULTIPLICITY_MAP[m];
|
|
58
|
+
const certainty = member['gl:certainty'] ?? member.certainty;
|
|
59
|
+
if (certainty === 'possible') return { presence: 'optional', count: 'exactly_one' };
|
|
60
|
+
return { presence: 'required', count: 'exactly_one' };
|
|
55
61
|
}
|
|
56
62
|
|
|
57
63
|
function resolveIsDelimiting(member) {
|
|
@@ -216,7 +222,8 @@ function extractPartitiveRelations(concept, registerId, uriBase, urnMap) {
|
|
|
216
222
|
if (!uri || uri === sourceUri) return null;
|
|
217
223
|
return {
|
|
218
224
|
uri,
|
|
219
|
-
|
|
225
|
+
presence: resolvePresence(p),
|
|
226
|
+
count: resolveCount(p),
|
|
220
227
|
isDelimiting: resolveIsDelimiting(p),
|
|
221
228
|
};
|
|
222
229
|
})
|
|
@@ -15,20 +15,27 @@ import { buildBibliographyTurtle } from './lib/bibliography-turtle.mjs';
|
|
|
15
15
|
import { ttlLit } from './lib/turtle-escape.mjs';
|
|
16
16
|
import { firstNonEmpty } from './lib/first-non-empty.mjs';
|
|
17
17
|
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
// MECE partitive multiplicity: 2 independent axes (ISO 704:2022).
|
|
19
|
+
// presence × count
|
|
20
|
+
// Legacy 5-value `multiplicity` and 2-value `certainty` map into the
|
|
21
|
+
// 2-axis model — keep emitting them for one release cycle so consumers
|
|
22
|
+
// that still read the legacy fields keep working.
|
|
23
|
+
const LEGACY_MULTIPLICITY_MAP = {
|
|
24
|
+
compulsory: { presence: 'required', count: 'exactly_one' },
|
|
25
|
+
optional: { presence: 'optional', count: 'exactly_one' },
|
|
26
|
+
compulsory_multiple: { presence: 'required', count: 'multiple' },
|
|
27
|
+
optional_multiple: { presence: 'optional', count: 'multiple' },
|
|
28
|
+
compulsory_at_least_one: { presence: 'required', count: 'at_least_one' },
|
|
29
|
+
};
|
|
30
|
+
const LEGACY_CERTAINTY_FROM_AXIS = {
|
|
31
|
+
required: 'confirmed',
|
|
23
32
|
optional: 'possible',
|
|
24
|
-
compulsory_multiple: 'confirmed',
|
|
25
|
-
optional_multiple: 'possible',
|
|
26
|
-
compulsory_at_least_one: 'confirmed',
|
|
27
33
|
};
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
34
|
+
|
|
35
|
+
function splitMultiplicity(multiplicity) {
|
|
36
|
+
return multiplicity in LEGACY_MULTIPLICITY_MAP
|
|
37
|
+
? LEGACY_MULTIPLICITY_MAP[multiplicity]
|
|
38
|
+
: { presence: 'required', count: 'exactly_one' };
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
function buildConceptUri(uriBase, registerId, conceptId) {
|
|
@@ -710,17 +717,27 @@ function yamlToJsonLd(conceptYaml, register, refMaps) {
|
|
|
710
717
|
} else {
|
|
711
718
|
m['gl:ref'] = refToJsonLd(member, 'gl:ConceptRef');
|
|
712
719
|
}
|
|
713
|
-
// ISO 704:2022
|
|
714
|
-
//
|
|
715
|
-
//
|
|
716
|
-
|
|
720
|
+
// ISO 704:2022 MECE axes: presence × count.
|
|
721
|
+
// Prefer explicit YAML values; fall back to migrating from the
|
|
722
|
+
// legacy 5-value `multiplicity` enum for one release cycle.
|
|
723
|
+
const presence = member.presence
|
|
724
|
+
?? splitMultiplicity(member.multiplicity ?? '').presence;
|
|
725
|
+
const count = member.count
|
|
726
|
+
?? splitMultiplicity(member.multiplicity ?? '').count;
|
|
727
|
+
|
|
728
|
+
m['gl:presence'] = presence;
|
|
729
|
+
m['gl:count'] = count;
|
|
730
|
+
|
|
717
731
|
if (member.is_delimiting === true || member.isDelimiting === true) {
|
|
718
732
|
m['gl:isDelimiting'] = true;
|
|
719
733
|
}
|
|
720
|
-
|
|
721
|
-
//
|
|
734
|
+
|
|
735
|
+
// Legacy gl:multiplicity + gl:certainty — emitted for one
|
|
736
|
+
// release cycle. Remove after downstream consumers migrate to
|
|
737
|
+
// presence + count.
|
|
738
|
+
if (member.multiplicity) m['gl:multiplicity'] = member.multiplicity;
|
|
722
739
|
const legacyCertainty = member.certainty
|
|
723
|
-
??
|
|
740
|
+
?? LEGACY_CERTAINTY_FROM_AXIS[presence];
|
|
724
741
|
if (legacyCertainty) m['gl:certainty'] = legacyCertainty;
|
|
725
742
|
return m;
|
|
726
743
|
});
|
|
@@ -1005,13 +1022,8 @@ async function processDataset(dir, register, opts) {
|
|
|
1005
1022
|
partitiveRelations: {
|
|
1006
1023
|
count: 0,
|
|
1007
1024
|
byCompleteness: { complete: 0, partial: 0 },
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
optional: 0,
|
|
1011
|
-
compulsory_multiple: 0,
|
|
1012
|
-
optional_multiple: 0,
|
|
1013
|
-
compulsory_at_least_one: 0,
|
|
1014
|
-
},
|
|
1025
|
+
byPresence: { required: 0, optional: 0 },
|
|
1026
|
+
byCount: { exactly_one: 0, at_least_one: 0, multiple: 0 },
|
|
1015
1027
|
delimitingMembers: 0,
|
|
1016
1028
|
withCriterion: 0,
|
|
1017
1029
|
withoutCriterion: 0,
|
|
@@ -1048,11 +1060,13 @@ async function processDataset(dir, register, opts) {
|
|
|
1048
1060
|
(s.partitiveRelations.byCompleteness[completeness] || 0) + 1;
|
|
1049
1061
|
const partitives = rel.partitives || [];
|
|
1050
1062
|
for (const member of partitives) {
|
|
1051
|
-
|
|
1052
|
-
const
|
|
1053
|
-
|
|
1054
|
-
s.partitiveRelations.
|
|
1055
|
-
(s.partitiveRelations.
|
|
1063
|
+
const axes = splitMultiplicity(member.multiplicity ?? '');
|
|
1064
|
+
const presence = member.presence ?? axes.presence;
|
|
1065
|
+
const count = member.count ?? axes.count;
|
|
1066
|
+
s.partitiveRelations.byPresence[presence] =
|
|
1067
|
+
(s.partitiveRelations.byPresence[presence] || 0) + 1;
|
|
1068
|
+
s.partitiveRelations.byCount[count] =
|
|
1069
|
+
(s.partitiveRelations.byCount[count] || 0) + 1;
|
|
1056
1070
|
if (member.is_delimiting === true || member.isDelimiting === true) {
|
|
1057
1071
|
s.partitiveRelations.delimitingMembers += 1;
|
|
1058
1072
|
}
|
|
@@ -3,20 +3,25 @@ import { mount } from '@vue/test-utils';
|
|
|
3
3
|
import PartitiveRelationDiagram, {
|
|
4
4
|
type PartitiveMemberLabeled,
|
|
5
5
|
} from '../../components/PartitiveRelationDiagram.vue';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
rakeStrokeStyle,
|
|
8
|
+
type PartitivePresence,
|
|
9
|
+
type PartitiveCount,
|
|
10
|
+
} from '../../utils/partitive-multiplicity';
|
|
7
11
|
|
|
8
12
|
const members = (
|
|
9
13
|
n: number,
|
|
10
|
-
opts: {
|
|
14
|
+
opts: { presence?: PartitivePresence; count?: PartitiveCount; isDelimiting?: boolean } = {},
|
|
11
15
|
): PartitiveMemberLabeled[] =>
|
|
12
16
|
Array.from({ length: n }, (_, i) => ({
|
|
13
17
|
uri: `https://example.org/test/concept/1.${i + 1}`,
|
|
14
18
|
label: `1.${i + 1}`,
|
|
15
|
-
|
|
19
|
+
presence: opts.presence ?? 'required',
|
|
20
|
+
count: opts.count ?? 'exactly_one',
|
|
16
21
|
isDelimiting: opts.isDelimiting ?? false,
|
|
17
22
|
}));
|
|
18
23
|
|
|
19
|
-
describe('PartitiveRelationDiagram — ISO 704:2022 rake rendering', () => {
|
|
24
|
+
describe('PartitiveRelationDiagram — ISO 704:2022 rake rendering (MECE)', () => {
|
|
20
25
|
it('renders comprehensive + N partitive nodes as text', () => {
|
|
21
26
|
const w = mount(PartitiveRelationDiagram, {
|
|
22
27
|
props: {
|
|
@@ -44,8 +49,8 @@ describe('PartitiveRelationDiagram — ISO 704:2022 rake rendering', () => {
|
|
|
44
49
|
expect(w.findAll('text').map(t => t.text())).toContain('physical structure');
|
|
45
50
|
});
|
|
46
51
|
|
|
47
|
-
describe('per-member
|
|
48
|
-
it('
|
|
52
|
+
describe('per-member line rendering (presence × count × isDelimiting)', () => {
|
|
53
|
+
it('required/exactly_one: stem + spine + 1 solid line per drop', () => {
|
|
49
54
|
const w = mount(PartitiveRelationDiagram, {
|
|
50
55
|
props: {
|
|
51
56
|
comprehensiveLabel: '0',
|
|
@@ -57,11 +62,11 @@ describe('PartitiveRelationDiagram — ISO 704:2022 rake rendering', () => {
|
|
|
57
62
|
expect(w.findAll('line')).toHaveLength(4);
|
|
58
63
|
});
|
|
59
64
|
|
|
60
|
-
it('
|
|
65
|
+
it('required/multiple: each drop has 2 solid lines', () => {
|
|
61
66
|
const w = mount(PartitiveRelationDiagram, {
|
|
62
67
|
props: {
|
|
63
68
|
comprehensiveLabel: '0',
|
|
64
|
-
partitives: members(2, {
|
|
69
|
+
partitives: members(2, { count: 'multiple' }),
|
|
65
70
|
completeness: 'complete',
|
|
66
71
|
},
|
|
67
72
|
});
|
|
@@ -69,11 +74,11 @@ describe('PartitiveRelationDiagram — ISO 704:2022 rake rendering', () => {
|
|
|
69
74
|
expect(w.findAll('line')).toHaveLength(6);
|
|
70
75
|
});
|
|
71
76
|
|
|
72
|
-
it('optional: drop is dashed', () => {
|
|
77
|
+
it('optional/exactly_one: drop is dashed', () => {
|
|
73
78
|
const w = mount(PartitiveRelationDiagram, {
|
|
74
79
|
props: {
|
|
75
80
|
comprehensiveLabel: '0',
|
|
76
|
-
partitives: members(2, {
|
|
81
|
+
partitives: members(2, { presence: 'optional' }),
|
|
77
82
|
completeness: 'complete',
|
|
78
83
|
},
|
|
79
84
|
});
|
|
@@ -83,11 +88,11 @@ describe('PartitiveRelationDiagram — ISO 704:2022 rake rendering', () => {
|
|
|
83
88
|
expect(dashed).toHaveLength(2);
|
|
84
89
|
});
|
|
85
90
|
|
|
86
|
-
it('
|
|
91
|
+
it('optional/multiple: each drop has 2 dashed lines', () => {
|
|
87
92
|
const w = mount(PartitiveRelationDiagram, {
|
|
88
93
|
props: {
|
|
89
94
|
comprehensiveLabel: '0',
|
|
90
|
-
partitives: members(2, {
|
|
95
|
+
partitives: members(2, { presence: 'optional', count: 'multiple' }),
|
|
91
96
|
completeness: 'complete',
|
|
92
97
|
},
|
|
93
98
|
});
|
|
@@ -96,11 +101,11 @@ describe('PartitiveRelationDiagram — ISO 704:2022 rake rendering', () => {
|
|
|
96
101
|
expect(dashed).toHaveLength(4); // 2 members × 2 dashed lines each
|
|
97
102
|
});
|
|
98
103
|
|
|
99
|
-
it('
|
|
104
|
+
it('required/at_least_one: each drop has 1 solid + 1 dashed line', () => {
|
|
100
105
|
const w = mount(PartitiveRelationDiagram, {
|
|
101
106
|
props: {
|
|
102
107
|
comprehensiveLabel: '0',
|
|
103
|
-
partitives: members(2, {
|
|
108
|
+
partitives: members(2, { count: 'at_least_one' }),
|
|
104
109
|
completeness: 'complete',
|
|
105
110
|
},
|
|
106
111
|
});
|
|
@@ -161,9 +166,9 @@ describe('PartitiveRelationDiagram — ISO 704:2022 rake rendering', () => {
|
|
|
161
166
|
props: {
|
|
162
167
|
comprehensiveLabel: 'coordinate system of quantities',
|
|
163
168
|
partitives: [
|
|
164
|
-
{ uri: 'u1', label: 'system of quantities',
|
|
165
|
-
{ uri: 'u2', label: 'International System of Units',
|
|
166
|
-
{ uri: 'u3', label: 'base quantity dimension',
|
|
169
|
+
{ uri: 'u1', label: 'system of quantities', presence: 'required', count: 'exactly_one', isDelimiting: false },
|
|
170
|
+
{ uri: 'u2', label: 'International System of Units', presence: 'required', count: 'exactly_one', isDelimiting: false },
|
|
171
|
+
{ uri: 'u3', label: 'base quantity dimension', presence: 'required', count: 'exactly_one', isDelimiting: false },
|
|
167
172
|
],
|
|
168
173
|
completeness: 'complete',
|
|
169
174
|
},
|
|
@@ -173,20 +178,21 @@ describe('PartitiveRelationDiagram — ISO 704:2022 rake rendering', () => {
|
|
|
173
178
|
});
|
|
174
179
|
});
|
|
175
180
|
|
|
176
|
-
describe('consistency with rakeStrokeStyle helper', () => {
|
|
181
|
+
describe('consistency with rakeStrokeStyle helper (MECE truth table)', () => {
|
|
177
182
|
// Ensure the component's line rendering matches the pure helper
|
|
178
183
|
// (single source of truth — both must agree).
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
184
|
+
const testCases: Array<{ presence: PartitivePresence; count: PartitiveCount; isDelimiting: boolean }> = [
|
|
185
|
+
{ presence: 'required', count: 'exactly_one', isDelimiting: false },
|
|
186
|
+
{ presence: 'required', count: 'exactly_one', isDelimiting: true },
|
|
187
|
+
{ presence: 'optional', count: 'exactly_one', isDelimiting: false },
|
|
188
|
+
{ presence: 'required', count: 'multiple', isDelimiting: false },
|
|
189
|
+
{ presence: 'optional', count: 'multiple', isDelimiting: false },
|
|
190
|
+
{ presence: 'required', count: 'at_least_one', isDelimiting: false },
|
|
191
|
+
{ presence: 'optional', count: 'at_least_one', isDelimiting: false },
|
|
192
|
+
];
|
|
193
|
+
for (const tc of testCases) {
|
|
194
|
+
it(`${tc.presence} × ${tc.count}${tc.isDelimiting ? ' / delimiting' : ''}`, () => {
|
|
195
|
+
const expected = rakeStrokeStyle(tc.presence, tc.count, tc.isDelimiting);
|
|
190
196
|
const w = mount(PartitiveRelationDiagram, {
|
|
191
197
|
props: {
|
|
192
198
|
comprehensiveLabel: '0',
|
|
@@ -198,7 +204,7 @@ describe('PartitiveRelationDiagram — ISO 704:2022 rake rendering', () => {
|
|
|
198
204
|
// stem (1) + spine (1) + drop lineCount per member
|
|
199
205
|
const expectedLines = 2 + expected.lineCount;
|
|
200
206
|
expect(lines.length).toBe(expectedLines);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
207
|
+
});
|
|
208
|
+
}
|
|
203
209
|
});
|
|
204
210
|
});
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
// glossarist 0.4.
|
|
2
|
+
// glossarist-js 0.4.26's top-level d.ts doesn't re-export the MECE
|
|
3
3
|
// PartitiveRelation/PartitiveMember classes; import from glossarist/models
|
|
4
|
-
// (
|
|
5
|
-
import type {
|
|
4
|
+
// (extended by our local augmentation) so the types are correct.
|
|
5
|
+
import type {
|
|
6
|
+
PartitiveRelation,
|
|
7
|
+
PartitiveMember,
|
|
8
|
+
ConceptRef,
|
|
9
|
+
} from 'glossarist/models';
|
|
6
10
|
import {
|
|
7
11
|
PartitiveRelation as PartitiveRelationModel,
|
|
8
12
|
PartitiveMember as PartitiveMemberModel,
|
|
@@ -10,12 +14,12 @@ import {
|
|
|
10
14
|
} from 'glossarist/models';
|
|
11
15
|
|
|
12
16
|
/**
|
|
13
|
-
* Verifies glossarist-js 0.4.
|
|
14
|
-
* (ISO 704:2022
|
|
17
|
+
* Verifies glossarist-js 0.4.26's MECE-native PartitiveRelation model
|
|
18
|
+
* (ISO 704:2022 presence × count + is_delimiting).
|
|
15
19
|
*
|
|
16
|
-
* The concept-browser
|
|
17
|
-
*
|
|
18
|
-
*
|
|
20
|
+
* The concept-browser consumes these fields directly (no migration
|
|
21
|
+
* bridge). These specs pin the runtime contract so a regression in
|
|
22
|
+
* upstream is caught here.
|
|
19
23
|
*/
|
|
20
24
|
|
|
21
25
|
function makeRef(source: string, id: string): ConceptRef {
|
|
@@ -25,16 +29,17 @@ function makeRef(source: string, id: string): ConceptRef {
|
|
|
25
29
|
function makePartitive(
|
|
26
30
|
source: string,
|
|
27
31
|
id: string,
|
|
28
|
-
|
|
29
|
-
isDelimiting?: boolean,
|
|
32
|
+
fields: { presence?: 'required' | 'optional'; count?: 'exactly_one' | 'at_least_one' | 'multiple'; isDelimiting?: boolean } = {},
|
|
30
33
|
): PartitiveMember {
|
|
31
34
|
const data: {
|
|
32
35
|
ref: ConceptRef;
|
|
33
|
-
|
|
36
|
+
presence?: 'required' | 'optional';
|
|
37
|
+
count?: 'exactly_one' | 'at_least_one' | 'multiple';
|
|
34
38
|
is_delimiting?: boolean;
|
|
35
39
|
} = { ref: makeRef(source, id) };
|
|
36
|
-
if (
|
|
37
|
-
if (
|
|
40
|
+
if (fields.presence) data.presence = fields.presence;
|
|
41
|
+
if (fields.count) data.count = fields.count;
|
|
42
|
+
if (fields.isDelimiting) data.is_delimiting = fields.isDelimiting;
|
|
38
43
|
return PartitiveMemberModel.fromJSON(data as unknown as Record<string, unknown>) as PartitiveMember;
|
|
39
44
|
}
|
|
40
45
|
|
|
@@ -47,18 +52,35 @@ function makeRelation(opts: {
|
|
|
47
52
|
return PartitiveRelationModel.fromJSON(opts as unknown as Record<string, unknown>) as PartitiveRelation;
|
|
48
53
|
}
|
|
49
54
|
|
|
50
|
-
describe('useConceptEdges —
|
|
51
|
-
describe('PartitiveMember (ISO 704:2022
|
|
52
|
-
it('
|
|
55
|
+
describe('useConceptEdges — MECE PartitiveRelation (glossarist 0.4.26)', () => {
|
|
56
|
+
describe('PartitiveMember (ISO 704:2022 presence × count + delimiting)', () => {
|
|
57
|
+
it('presence defaults to required, count defaults to exactly_one', () => {
|
|
53
58
|
const m = makePartitive('test', '1.1');
|
|
54
|
-
expect(m.
|
|
55
|
-
expect(m.
|
|
59
|
+
expect(m.presence).toBe('required');
|
|
60
|
+
expect(m.isRequired).toBe(true);
|
|
61
|
+
expect(m.isOptional).toBe(false);
|
|
56
62
|
});
|
|
57
63
|
|
|
58
|
-
it('
|
|
59
|
-
const m = makePartitive('test', '1.1'
|
|
60
|
-
expect(m.
|
|
64
|
+
it('count defaults to exactly_one', () => {
|
|
65
|
+
const m = makePartitive('test', '1.1');
|
|
66
|
+
expect(m.count).toBe('exactly_one');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('presence=optional is preserved', () => {
|
|
70
|
+
const m = makePartitive('test', '1.1', { presence: 'optional' });
|
|
71
|
+
expect(m.presence).toBe('optional');
|
|
61
72
|
expect(m.isOptional).toBe(true);
|
|
73
|
+
expect(m.isRequired).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('count=multiple is preserved', () => {
|
|
77
|
+
const m = makePartitive('test', '1.1', { count: 'multiple' });
|
|
78
|
+
expect(m.count).toBe('multiple');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('count=at_least_one is preserved', () => {
|
|
82
|
+
const m = makePartitive('test', '1.1', { count: 'at_least_one' });
|
|
83
|
+
expect(m.count).toBe('at_least_one');
|
|
62
84
|
});
|
|
63
85
|
|
|
64
86
|
it('is_delimiting defaults to false', () => {
|
|
@@ -68,19 +90,20 @@ describe('useConceptEdges — native v3 PartitiveRelation (glossarist 0.4.24)',
|
|
|
68
90
|
});
|
|
69
91
|
|
|
70
92
|
it('is_delimiting is preserved when set to true', () => {
|
|
71
|
-
const m = makePartitive('test', '1.1',
|
|
93
|
+
const m = makePartitive('test', '1.1', { isDelimiting: true });
|
|
72
94
|
expect(m.is_delimiting).toBe(true);
|
|
95
|
+
expect(m.isDelimiting).toBe(true);
|
|
73
96
|
});
|
|
74
97
|
|
|
75
|
-
it('
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
98
|
+
it('rejects the vacuous (presence=optional, count=at_least_one)', () => {
|
|
99
|
+
expect(() =>
|
|
100
|
+
makePartitive('test', '1.1', { presence: 'optional', count: 'at_least_one' }),
|
|
101
|
+
).toThrow(/collapses to optional \+ multiple/);
|
|
79
102
|
});
|
|
80
103
|
});
|
|
81
104
|
|
|
82
105
|
describe('PartitiveRelation', () => {
|
|
83
|
-
it('accepts 2
|
|
106
|
+
it('accepts 2 members with default axes', () => {
|
|
84
107
|
const r = makeRelation({
|
|
85
108
|
comprehensive: makeRef('test', '1.3'),
|
|
86
109
|
partitives: [makePartitive('test', '1.4'), makePartitive('test', '1.5')],
|
|
@@ -101,21 +124,6 @@ describe('useConceptEdges — native v3 PartitiveRelation (glossarist 0.4.24)',
|
|
|
101
124
|
expect(r.isPartial).toBe(true);
|
|
102
125
|
});
|
|
103
126
|
|
|
104
|
-
it('member with compulsory_multiple has isCompulsory false', () => {
|
|
105
|
-
const m = makePartitive('test', '1.4', 'compulsory_multiple');
|
|
106
|
-
expect(m.isCompulsory).toBe(false);
|
|
107
|
-
expect(m.multiplicity).not.toBe('compulsory');
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('all compulsory-single members have isCompulsory true', () => {
|
|
111
|
-
const r = makeRelation({
|
|
112
|
-
comprehensive: makeRef('test', '1.3'),
|
|
113
|
-
partitives: [makePartitive('test', '1.4'), makePartitive('test', '1.5')],
|
|
114
|
-
});
|
|
115
|
-
expect(r.partitives[0].isCompulsory).toBe(true);
|
|
116
|
-
expect(r.partitives[1].isCompulsory).toBe(true);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
127
|
it('isCoordinate true when ≥2 partitives', () => {
|
|
120
128
|
const r = makeRelation({
|
|
121
129
|
comprehensive: makeRef('test', '1.3'),
|
|
@@ -131,27 +139,26 @@ describe('useConceptEdges — native v3 PartitiveRelation (glossarist 0.4.24)',
|
|
|
131
139
|
})).toThrow(/≥2 partitives/);
|
|
132
140
|
});
|
|
133
141
|
|
|
134
|
-
it('round-trips through toJSON with
|
|
142
|
+
it('round-trips through toJSON with MECE fields', () => {
|
|
135
143
|
const r = makeRelation({
|
|
136
144
|
comprehensive: makeRef('test', '1.3'),
|
|
137
145
|
partitives: [
|
|
138
|
-
makePartitive('test', '1.4', '
|
|
139
|
-
makePartitive('test', '1.5', '
|
|
146
|
+
makePartitive('test', '1.4', { presence: 'required', count: 'multiple' }),
|
|
147
|
+
makePartitive('test', '1.5', { presence: 'optional', isDelimiting: true }),
|
|
140
148
|
],
|
|
141
149
|
completeness: 'partial',
|
|
142
150
|
criterion: { eng: 'physical structure' },
|
|
143
151
|
});
|
|
144
152
|
const json = r.toJSON() as {
|
|
145
153
|
comprehensive: unknown;
|
|
146
|
-
partitives: Array<{
|
|
154
|
+
partitives: Array<{ presence?: string; count?: string; is_delimiting?: boolean; ref: unknown }>;
|
|
147
155
|
completeness: string;
|
|
148
156
|
criterion?: Record<string, string>;
|
|
149
157
|
};
|
|
150
158
|
expect(json.comprehensive).toBeDefined();
|
|
151
159
|
expect(json.partitives).toHaveLength(2);
|
|
152
|
-
|
|
153
|
-
expect(json.partitives[
|
|
154
|
-
expect(json.partitives[1].multiplicity).toBe('compulsory_multiple');
|
|
160
|
+
expect(json.partitives[0].count).toBe('multiple');
|
|
161
|
+
expect(json.partitives[1].presence).toBe('optional');
|
|
155
162
|
expect(json.partitives[1].is_delimiting).toBe(true);
|
|
156
163
|
expect(json.completeness).toBe('partial');
|
|
157
164
|
expect(json.criterion).toEqual({ eng: 'physical structure' });
|