@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
|
@@ -3,26 +3,31 @@ import { mount } from '@vue/test-utils';
|
|
|
3
3
|
import PartitiveRelationDiagram, {
|
|
4
4
|
type PartitiveMemberLabeled,
|
|
5
5
|
} from '../../components/PartitiveRelationDiagram.vue';
|
|
6
|
+
import {
|
|
7
|
+
rakeStrokeStyle,
|
|
8
|
+
type PartitivePresence,
|
|
9
|
+
type PartitiveCount,
|
|
10
|
+
} from '../../utils/partitive-multiplicity';
|
|
6
11
|
|
|
7
|
-
const members = (
|
|
12
|
+
const members = (
|
|
13
|
+
n: number,
|
|
14
|
+
opts: { presence?: PartitivePresence; count?: PartitiveCount; isDelimiting?: boolean } = {},
|
|
15
|
+
): PartitiveMemberLabeled[] =>
|
|
8
16
|
Array.from({ length: n }, (_, i) => ({
|
|
9
17
|
uri: `https://example.org/test/concept/1.${i + 1}`,
|
|
10
18
|
label: `1.${i + 1}`,
|
|
11
|
-
|
|
19
|
+
presence: opts.presence ?? 'required',
|
|
20
|
+
count: opts.count ?? 'exactly_one',
|
|
21
|
+
isDelimiting: opts.isDelimiting ?? false,
|
|
12
22
|
}));
|
|
13
23
|
|
|
14
|
-
|
|
15
|
-
return wrapper.findAll('line');
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
describe('PartitiveRelationDiagram — ISO 704 rake rendering', () => {
|
|
24
|
+
describe('PartitiveRelationDiagram — ISO 704:2022 rake rendering (MECE)', () => {
|
|
19
25
|
it('renders comprehensive + N partitive nodes as text', () => {
|
|
20
26
|
const w = mount(PartitiveRelationDiagram, {
|
|
21
27
|
props: {
|
|
22
28
|
comprehensiveLabel: '1.3',
|
|
23
29
|
partitives: members(3),
|
|
24
30
|
completeness: 'complete',
|
|
25
|
-
plurality: null,
|
|
26
31
|
},
|
|
27
32
|
});
|
|
28
33
|
const texts = w.findAll('text').map(t => t.text());
|
|
@@ -38,135 +43,110 @@ describe('PartitiveRelationDiagram — ISO 704 rake rendering', () => {
|
|
|
38
43
|
comprehensiveLabel: '1.3',
|
|
39
44
|
partitives: members(2),
|
|
40
45
|
completeness: 'complete',
|
|
41
|
-
plurality: null,
|
|
42
46
|
criterion: 'physical structure',
|
|
43
47
|
},
|
|
44
48
|
});
|
|
45
49
|
expect(w.findAll('text').map(t => t.text())).toContain('physical structure');
|
|
46
50
|
});
|
|
47
51
|
|
|
48
|
-
describe('line
|
|
49
|
-
it('
|
|
52
|
+
describe('per-member line rendering (presence × count × isDelimiting)', () => {
|
|
53
|
+
it('required/exactly_one: stem + spine + 1 solid line per drop', () => {
|
|
50
54
|
const w = mount(PartitiveRelationDiagram, {
|
|
51
55
|
props: {
|
|
52
|
-
comprehensiveLabel: '
|
|
56
|
+
comprehensiveLabel: '0',
|
|
53
57
|
partitives: members(2),
|
|
54
58
|
completeness: 'complete',
|
|
55
|
-
plurality: null,
|
|
56
59
|
},
|
|
57
60
|
});
|
|
58
|
-
// stem (1) + spine (1) +
|
|
59
|
-
expect(
|
|
60
|
-
// none should be dashed
|
|
61
|
-
const dashed = svgLinesOf(w).filter(l => l.attributes('stroke-dasharray'));
|
|
62
|
-
expect(dashed).toHaveLength(0);
|
|
61
|
+
// stem (1) + spine (1) + 1 drop line per member × 2 members = 4
|
|
62
|
+
expect(w.findAll('line')).toHaveLength(4);
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
-
it('
|
|
65
|
+
it('required/multiple: each drop has 2 solid lines', () => {
|
|
66
66
|
const w = mount(PartitiveRelationDiagram, {
|
|
67
67
|
props: {
|
|
68
|
-
comprehensiveLabel: '
|
|
69
|
-
partitives: members(2),
|
|
68
|
+
comprehensiveLabel: '0',
|
|
69
|
+
partitives: members(2, { count: 'multiple' }),
|
|
70
70
|
completeness: 'complete',
|
|
71
|
-
plurality: { isShared: true, isUncertain: false },
|
|
72
71
|
},
|
|
73
72
|
});
|
|
74
|
-
// stem
|
|
75
|
-
expect(
|
|
76
|
-
const dashed = svgLinesOf(w).filter(l => l.attributes('stroke-dasharray'));
|
|
77
|
-
expect(dashed).toHaveLength(0);
|
|
73
|
+
// stem + spine + 2 drop lines per member × 2 = 6
|
|
74
|
+
expect(w.findAll('line')).toHaveLength(6);
|
|
78
75
|
});
|
|
79
76
|
|
|
80
|
-
it('
|
|
77
|
+
it('optional/exactly_one: drop is dashed', () => {
|
|
81
78
|
const w = mount(PartitiveRelationDiagram, {
|
|
82
79
|
props: {
|
|
83
|
-
comprehensiveLabel: '
|
|
84
|
-
partitives: members(2),
|
|
80
|
+
comprehensiveLabel: '0',
|
|
81
|
+
partitives: members(2, { presence: 'optional' }),
|
|
85
82
|
completeness: 'complete',
|
|
86
|
-
plurality: { isShared: true, isUncertain: true },
|
|
87
83
|
},
|
|
88
84
|
});
|
|
89
|
-
// stem
|
|
90
|
-
const lines =
|
|
91
|
-
expect(lines).toHaveLength(6);
|
|
85
|
+
// stem + spine (frame, not dashed) + 2 drops (dashed)
|
|
86
|
+
const lines = w.findAll('line');
|
|
92
87
|
const dashed = lines.filter(l => l.attributes('stroke-dasharray'));
|
|
93
|
-
// exactly 2 dashed lines (the second stem + the second spine)
|
|
94
88
|
expect(dashed).toHaveLength(2);
|
|
95
89
|
});
|
|
96
|
-
});
|
|
97
90
|
|
|
98
|
-
|
|
99
|
-
it('extends spine past last tooth when completeness=partial', () => {
|
|
91
|
+
it('optional/multiple: each drop has 2 dashed lines', () => {
|
|
100
92
|
const w = mount(PartitiveRelationDiagram, {
|
|
101
93
|
props: {
|
|
102
|
-
comprehensiveLabel: '
|
|
103
|
-
partitives: members(2),
|
|
104
|
-
completeness: '
|
|
105
|
-
plurality: null,
|
|
94
|
+
comprehensiveLabel: '0',
|
|
95
|
+
partitives: members(2, { presence: 'optional', count: 'multiple' }),
|
|
96
|
+
completeness: 'complete',
|
|
106
97
|
},
|
|
107
98
|
});
|
|
108
|
-
const lines =
|
|
109
|
-
|
|
110
|
-
expect(
|
|
111
|
-
// The spine line's x2 should be > the last partitive's x
|
|
112
|
-
const lastMember = w.props('partitives')[1];
|
|
113
|
-
// visual assertion: the spine line's x2 is greater than the second
|
|
114
|
-
// partitive's slot x. We can't compute slot exactly without internals,
|
|
115
|
-
// but we know PARTIAL_TAIL (32) is added.
|
|
116
|
-
const spineLine = lines.find(l => {
|
|
117
|
-
const y1 = l.attributes('y1');
|
|
118
|
-
const y2 = l.attributes('y2');
|
|
119
|
-
return y1 && y2 && y1 === y2; // horizontal
|
|
120
|
-
});
|
|
121
|
-
expect(spineLine).toBeDefined();
|
|
122
|
-
const x2 = Number(spineLine!.attributes('x2'));
|
|
123
|
-
// last member x ≈ PADDING_X + NODE_W * 1.5 + GAP_X = 24 + 180 + 16 = 220
|
|
124
|
-
// spine end with PARTIAL_TAIL = 220 + 32 = 252
|
|
125
|
-
expect(x2).toBeGreaterThan(220);
|
|
126
|
-
void lastMember;
|
|
99
|
+
const lines = w.findAll('line');
|
|
100
|
+
const dashed = lines.filter(l => l.attributes('stroke-dasharray'));
|
|
101
|
+
expect(dashed).toHaveLength(4); // 2 members × 2 dashed lines each
|
|
127
102
|
});
|
|
128
|
-
});
|
|
129
103
|
|
|
130
|
-
|
|
131
|
-
it('renders possible members with dashed border', () => {
|
|
132
|
-
const possibleMembers: PartitiveMemberLabeled[] = [
|
|
133
|
-
{ uri: 'u1', label: '1', certainty: 'confirmed' },
|
|
134
|
-
{ uri: 'u2', label: '2', certainty: 'possible' },
|
|
135
|
-
];
|
|
104
|
+
it('required/at_least_one: each drop has 1 solid + 1 dashed line', () => {
|
|
136
105
|
const w = mount(PartitiveRelationDiagram, {
|
|
137
106
|
props: {
|
|
138
107
|
comprehensiveLabel: '0',
|
|
139
|
-
partitives:
|
|
108
|
+
partitives: members(2, { count: 'at_least_one' }),
|
|
140
109
|
completeness: 'complete',
|
|
141
|
-
plurality: null,
|
|
142
110
|
},
|
|
143
111
|
});
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
//
|
|
149
|
-
const possibleRect = rects[2];
|
|
150
|
-
expect(possibleRect.attributes('stroke-dasharray')).toBeTruthy();
|
|
112
|
+
const lines = w.findAll('line');
|
|
113
|
+
// stem + spine (solid) + 1 solid + 1 dashed per member × 2 = 6
|
|
114
|
+
expect(lines).toHaveLength(6);
|
|
115
|
+
const dashed = lines.filter(l => l.attributes('stroke-dasharray'));
|
|
116
|
+
expect(dashed).toHaveLength(2); // 1 dashed per member
|
|
151
117
|
});
|
|
152
118
|
|
|
153
|
-
it('
|
|
119
|
+
it('delimiting: drops use 3× stroke width', () => {
|
|
154
120
|
const w = mount(PartitiveRelationDiagram, {
|
|
155
121
|
props: {
|
|
156
122
|
comprehensiveLabel: '0',
|
|
157
|
-
partitives: members(2),
|
|
123
|
+
partitives: members(2, { isDelimiting: true }),
|
|
158
124
|
completeness: 'complete',
|
|
159
|
-
plurality: null,
|
|
160
125
|
},
|
|
161
126
|
});
|
|
162
|
-
//
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
127
|
+
// The drop lines should have stroke-width = 4.5 (vs frame's 1.4)
|
|
128
|
+
const lines = w.findAll('line');
|
|
129
|
+
const dropWidths = lines
|
|
130
|
+
.map(l => Number(l.attributes('stroke-width') || '0'))
|
|
131
|
+
.filter(w => w > 2); // delimiting width is 4.5
|
|
132
|
+
expect(dropWidths).toHaveLength(2); // 1 drop per member × 2 members
|
|
167
133
|
});
|
|
168
134
|
});
|
|
169
135
|
|
|
136
|
+
it('emits navigate when a partitive is clicked', async () => {
|
|
137
|
+
const w = mount(PartitiveRelationDiagram, {
|
|
138
|
+
props: {
|
|
139
|
+
comprehensiveLabel: '0',
|
|
140
|
+
partitives: members(2),
|
|
141
|
+
completeness: 'complete',
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
const groups = w.findAll('g.cursor-pointer');
|
|
145
|
+
expect(groups.length).toBeGreaterThan(0);
|
|
146
|
+
await groups[0].trigger('click');
|
|
147
|
+
expect(w.emitted('navigate')).toBeTruthy();
|
|
148
|
+
});
|
|
149
|
+
|
|
170
150
|
describe('designation-aware sizing', () => {
|
|
171
151
|
it('uses MIN_NODE_W when labels are short', () => {
|
|
172
152
|
const w = mount(PartitiveRelationDiagram, {
|
|
@@ -174,75 +154,57 @@ describe('PartitiveRelationDiagram — ISO 704 rake rendering', () => {
|
|
|
174
154
|
comprehensiveLabel: '1.3',
|
|
175
155
|
partitives: members(2),
|
|
176
156
|
completeness: 'complete',
|
|
177
|
-
plurality: null,
|
|
178
157
|
},
|
|
179
158
|
});
|
|
180
159
|
const svg = w.find('svg');
|
|
181
|
-
|
|
182
|
-
const viewBox = svg.attributes('viewBox') || '';
|
|
183
|
-
const width = Number(viewBox.split(' ')[2]);
|
|
160
|
+
const width = Number((svg.attributes('viewBox') || '').split(' ')[2]);
|
|
184
161
|
expect(width).toBeLessThan(300);
|
|
185
162
|
});
|
|
186
163
|
|
|
187
164
|
it('grows nodes for long designations', () => {
|
|
188
|
-
const longMembers: PartitiveMemberLabeled[] = [
|
|
189
|
-
{ uri: 'u1', label: 'system of quantities', certainty: 'confirmed' },
|
|
190
|
-
{ uri: 'u2', label: 'International System of Units', certainty: 'confirmed' },
|
|
191
|
-
{ uri: 'u3', label: 'base quantity dimension', certainty: 'confirmed' },
|
|
192
|
-
];
|
|
193
165
|
const w = mount(PartitiveRelationDiagram, {
|
|
194
166
|
props: {
|
|
195
167
|
comprehensiveLabel: 'coordinate system of quantities',
|
|
196
|
-
partitives: longMembers,
|
|
197
|
-
completeness: 'complete',
|
|
198
|
-
plurality: null,
|
|
199
|
-
},
|
|
200
|
-
});
|
|
201
|
-
const svg = w.find('svg');
|
|
202
|
-
const viewBox = svg.attributes('viewBox') || '';
|
|
203
|
-
const width = Number(viewBox.split(' ')[2]);
|
|
204
|
-
// Each node wider than MIN_NODE_W → total > 3*110 = 330
|
|
205
|
-
expect(width).toBeGreaterThan(330);
|
|
206
|
-
// Capped: node width ≤ MAX_NODE_W=220 → total ≤ 3*220 + 2*16 + 40 = 732
|
|
207
|
-
expect(width).toBeLessThanOrEqual(732);
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it('wraps long labels to 2 lines via tspan when needed', () => {
|
|
211
|
-
const longLabel = 'International System of Quantities and Units';
|
|
212
|
-
const w = mount(PartitiveRelationDiagram, {
|
|
213
|
-
props: {
|
|
214
|
-
comprehensiveLabel: longLabel,
|
|
215
168
|
partitives: [
|
|
216
|
-
{ uri: 'u1', label: '
|
|
217
|
-
{ uri: 'u2', label: '
|
|
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 },
|
|
218
172
|
],
|
|
219
173
|
completeness: 'complete',
|
|
220
|
-
plurality: null,
|
|
221
174
|
},
|
|
222
175
|
});
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const compText = texts[0]; // first text is the comprehensive
|
|
226
|
-
const tspans = compText.findAll('tspan');
|
|
227
|
-
expect(tspans.length).toBeGreaterThanOrEqual(2);
|
|
176
|
+
const width = Number((w.find('svg').attributes('viewBox') || '').split(' ')[2]);
|
|
177
|
+
expect(width).toBeGreaterThan(330);
|
|
228
178
|
});
|
|
179
|
+
});
|
|
229
180
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
181
|
+
describe('consistency with rakeStrokeStyle helper (MECE truth table)', () => {
|
|
182
|
+
// Ensure the component's line rendering matches the pure helper
|
|
183
|
+
// (single source of truth — both must agree).
|
|
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);
|
|
196
|
+
const w = mount(PartitiveRelationDiagram, {
|
|
197
|
+
props: {
|
|
198
|
+
comprehensiveLabel: '0',
|
|
199
|
+
partitives: members(1, tc),
|
|
200
|
+
completeness: 'complete',
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
const lines = w.findAll('line');
|
|
204
|
+
// stem (1) + spine (1) + drop lineCount per member
|
|
205
|
+
const expectedLines = 2 + expected.lineCount;
|
|
206
|
+
expect(lines.length).toBe(expectedLines);
|
|
241
207
|
});
|
|
242
|
-
|
|
243
|
-
expect(allText).toContain('system of quantities');
|
|
244
|
-
expect(allText).toContain('base quantity');
|
|
245
|
-
expect(allText).toContain('derived quantity');
|
|
246
|
-
});
|
|
208
|
+
}
|
|
247
209
|
});
|
|
248
210
|
});
|
|
@@ -1,132 +1,167 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
|
|
2
|
+
// glossarist-js 0.4.26's top-level d.ts doesn't re-export the MECE
|
|
3
|
+
// PartitiveRelation/PartitiveMember classes; import from glossarist/models
|
|
4
|
+
// (extended by our local augmentation) so the types are correct.
|
|
5
|
+
import type {
|
|
6
|
+
PartitiveRelation,
|
|
7
|
+
PartitiveMember,
|
|
8
|
+
ConceptRef,
|
|
9
|
+
} from 'glossarist/models';
|
|
3
10
|
import {
|
|
4
11
|
PartitiveRelation as PartitiveRelationModel,
|
|
5
12
|
PartitiveMember as PartitiveMemberModel,
|
|
6
|
-
TypeSharedPlurality as TypeSharedPluralityModel,
|
|
7
13
|
ConceptRef as ConceptRefModel,
|
|
8
14
|
} from 'glossarist/models';
|
|
9
15
|
|
|
10
16
|
/**
|
|
11
|
-
* Verifies glossarist-js's native
|
|
17
|
+
* Verifies glossarist-js 0.4.26's MECE-native PartitiveRelation model
|
|
18
|
+
* (ISO 704:2022 presence × count + is_delimiting).
|
|
12
19
|
*
|
|
13
|
-
* The concept-browser
|
|
14
|
-
* bridge). These specs
|
|
15
|
-
*
|
|
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.
|
|
16
23
|
*/
|
|
17
24
|
|
|
18
25
|
function makeRef(source: string, id: string): ConceptRef {
|
|
19
26
|
return ConceptRefModel.fromJSON({ source, id }) as ConceptRef;
|
|
20
27
|
}
|
|
21
28
|
|
|
22
|
-
function makePartitive(
|
|
23
|
-
|
|
24
|
-
|
|
29
|
+
function makePartitive(
|
|
30
|
+
source: string,
|
|
31
|
+
id: string,
|
|
32
|
+
fields: { presence?: 'required' | 'optional'; count?: 'exactly_one' | 'at_least_one' | 'multiple'; isDelimiting?: boolean } = {},
|
|
33
|
+
): PartitiveMember {
|
|
34
|
+
const data: {
|
|
35
|
+
ref: ConceptRef;
|
|
36
|
+
presence?: 'required' | 'optional';
|
|
37
|
+
count?: 'exactly_one' | 'at_least_one' | 'multiple';
|
|
38
|
+
is_delimiting?: boolean;
|
|
39
|
+
} = { ref: makeRef(source, id) };
|
|
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;
|
|
25
43
|
return PartitiveMemberModel.fromJSON(data as unknown as Record<string, unknown>) as PartitiveMember;
|
|
26
44
|
}
|
|
27
45
|
|
|
28
|
-
function makePlurality(isShared: boolean, isUncertain = false): TypeSharedPlurality {
|
|
29
|
-
return TypeSharedPluralityModel.fromJSON({ isShared, isUncertain }) as TypeSharedPlurality;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
46
|
function makeRelation(opts: {
|
|
33
47
|
comprehensive: ConceptRef;
|
|
34
48
|
partitives: PartitiveMember[];
|
|
35
49
|
completeness?: 'complete' | 'partial';
|
|
36
|
-
plurality?: TypeSharedPlurality | null;
|
|
37
50
|
criterion?: Record<string, string>;
|
|
38
51
|
}): PartitiveRelation {
|
|
39
52
|
return PartitiveRelationModel.fromJSON(opts as unknown as Record<string, unknown>) as PartitiveRelation;
|
|
40
53
|
}
|
|
41
54
|
|
|
42
|
-
describe('
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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', () => {
|
|
58
|
+
const m = makePartitive('test', '1.1');
|
|
59
|
+
expect(m.presence).toBe('required');
|
|
60
|
+
expect(m.isRequired).toBe(true);
|
|
61
|
+
expect(m.isOptional).toBe(false);
|
|
62
|
+
});
|
|
47
63
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
});
|
|
64
|
+
it('count defaults to exactly_one', () => {
|
|
65
|
+
const m = makePartitive('test', '1.1');
|
|
66
|
+
expect(m.count).toBe('exactly_one');
|
|
67
|
+
});
|
|
53
68
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
69
|
+
it('presence=optional is preserved', () => {
|
|
70
|
+
const m = makePartitive('test', '1.1', { presence: 'optional' });
|
|
71
|
+
expect(m.presence).toBe('optional');
|
|
72
|
+
expect(m.isOptional).toBe(true);
|
|
73
|
+
expect(m.isRequired).toBe(false);
|
|
74
|
+
});
|
|
59
75
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
partitives: [makePartitive('test', '1.4'), makePartitive('test', '1.5')],
|
|
76
|
+
it('count=multiple is preserved', () => {
|
|
77
|
+
const m = makePartitive('test', '1.1', { count: 'multiple' });
|
|
78
|
+
expect(m.count).toBe('multiple');
|
|
64
79
|
});
|
|
65
|
-
expect(r.completeness).toBe('complete');
|
|
66
|
-
expect(r.isComplete).toBe(true);
|
|
67
|
-
});
|
|
68
80
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
partitives: [makePartitive('test', '1.4'), makePartitive('test', '1.5')],
|
|
73
|
-
completeness: 'partial',
|
|
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');
|
|
74
84
|
});
|
|
75
|
-
expect(r.completeness).toBe('partial');
|
|
76
|
-
expect(r.isPartial).toBe(true);
|
|
77
|
-
});
|
|
78
85
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
plurality: makePlurality(true, true),
|
|
86
|
+
it('is_delimiting defaults to false', () => {
|
|
87
|
+
const m = makePartitive('test', '1.1');
|
|
88
|
+
expect(m.is_delimiting).toBe(false);
|
|
89
|
+
expect(m.isDelimiting).toBe(false);
|
|
84
90
|
});
|
|
85
|
-
expect(r.hasPlurality()).toBe(true);
|
|
86
|
-
expect(r.plurality?.isShared).toBe(true);
|
|
87
|
-
expect(r.plurality?.isUncertain).toBe(true);
|
|
88
|
-
});
|
|
89
91
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
criterion: { eng: 'physical structure', fra: 'structure physique' },
|
|
92
|
+
it('is_delimiting is preserved when set to true', () => {
|
|
93
|
+
const m = makePartitive('test', '1.1', { isDelimiting: true });
|
|
94
|
+
expect(m.is_delimiting).toBe(true);
|
|
95
|
+
expect(m.isDelimiting).toBe(true);
|
|
95
96
|
});
|
|
96
|
-
expect(r.hasCriterion()).toBe(true);
|
|
97
|
-
expect(r.criterion?.eng).toBe('physical structure');
|
|
98
|
-
});
|
|
99
97
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
})
|
|
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/);
|
|
102
|
+
});
|
|
105
103
|
});
|
|
106
104
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
makePartitive('test', '1.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
105
|
+
describe('PartitiveRelation', () => {
|
|
106
|
+
it('accepts 2 members with default axes', () => {
|
|
107
|
+
const r = makeRelation({
|
|
108
|
+
comprehensive: makeRef('test', '1.3'),
|
|
109
|
+
partitives: [makePartitive('test', '1.4'), makePartitive('test', '1.5')],
|
|
110
|
+
});
|
|
111
|
+
expect(r.partitives).toHaveLength(2);
|
|
112
|
+
expect(r.completeness).toBe('complete');
|
|
113
|
+
expect(r.isComplete).toBe(true);
|
|
114
|
+
expect(r.isPartial).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('completeness=partial is preserved', () => {
|
|
118
|
+
const r = makeRelation({
|
|
119
|
+
comprehensive: makeRef('test', '1.3'),
|
|
120
|
+
partitives: [makePartitive('test', '1.4'), makePartitive('test', '1.5')],
|
|
121
|
+
completeness: 'partial',
|
|
122
|
+
});
|
|
123
|
+
expect(r.completeness).toBe('partial');
|
|
124
|
+
expect(r.isPartial).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('isCoordinate true when ≥2 partitives', () => {
|
|
128
|
+
const r = makeRelation({
|
|
129
|
+
comprehensive: makeRef('test', '1.3'),
|
|
130
|
+
partitives: [makePartitive('test', '1.4'), makePartitive('test', '1.5')],
|
|
131
|
+
});
|
|
132
|
+
expect(r.isCoordinate).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('rejects < 2 partitives (ISO 704 requirement)', () => {
|
|
136
|
+
expect(() => makeRelation({
|
|
137
|
+
comprehensive: makeRef('test', '1.3'),
|
|
138
|
+
partitives: [makePartitive('test', '1.4')],
|
|
139
|
+
})).toThrow(/≥2 partitives/);
|
|
140
|
+
});
|
|
116
141
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
142
|
+
it('round-trips through toJSON with MECE fields', () => {
|
|
143
|
+
const r = makeRelation({
|
|
144
|
+
comprehensive: makeRef('test', '1.3'),
|
|
145
|
+
partitives: [
|
|
146
|
+
makePartitive('test', '1.4', { presence: 'required', count: 'multiple' }),
|
|
147
|
+
makePartitive('test', '1.5', { presence: 'optional', isDelimiting: true }),
|
|
148
|
+
],
|
|
149
|
+
completeness: 'partial',
|
|
150
|
+
criterion: { eng: 'physical structure' },
|
|
151
|
+
});
|
|
152
|
+
const json = r.toJSON() as {
|
|
153
|
+
comprehensive: unknown;
|
|
154
|
+
partitives: Array<{ presence?: string; count?: string; is_delimiting?: boolean; ref: unknown }>;
|
|
155
|
+
completeness: string;
|
|
156
|
+
criterion?: Record<string, string>;
|
|
157
|
+
};
|
|
158
|
+
expect(json.comprehensive).toBeDefined();
|
|
159
|
+
expect(json.partitives).toHaveLength(2);
|
|
160
|
+
expect(json.partitives[0].count).toBe('multiple');
|
|
161
|
+
expect(json.partitives[1].presence).toBe('optional');
|
|
162
|
+
expect(json.partitives[1].is_delimiting).toBe(true);
|
|
163
|
+
expect(json.completeness).toBe('partial');
|
|
164
|
+
expect(json.criterion).toEqual({ eng: 'physical structure' });
|
|
124
165
|
});
|
|
125
|
-
const json = r.toJSON();
|
|
126
|
-
expect(json.completeness).toBe('partial');
|
|
127
|
-
expect(json.partitives).toHaveLength(2);
|
|
128
|
-
expect(json.partitives[1].certainty).toBe('possible');
|
|
129
|
-
expect(json.plurality?.is_shared).toBe(true);
|
|
130
|
-
expect(json.criterion?.eng).toBe('criterion text');
|
|
131
166
|
});
|
|
132
167
|
});
|