@glossarist/concept-browser 0.7.86 → 0.7.88

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glossarist/concept-browser",
3
- "version": "0.7.86",
3
+ "version": "0.7.88",
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.24",
40
+ "glossarist": "^0.4.26",
41
41
  "js-yaml": "^4.1.0",
42
42
  "jszip": "^3.10.1",
43
43
  "pinia": "^2.3.1",
@@ -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
- function resolveMultiplicity(member) {
36
- const m = member['gl:multiplicity'] ?? member.multiplicity;
37
- if (m != null) {
38
- if (!VALID_MULTIPLICITY.has(m)) {
39
- throw new Error(
40
- `Invalid partitive member multiplicity: "${m}". Allowed: ${[...VALID_MULTIPLICITY].join(', ')}`,
41
- );
42
- }
43
- return m;
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
- const certainty = member['gl:certainty'] ?? member.certainty;
46
- if (certainty != null) {
47
- if (!VALID_CERTAINTY.has(certainty)) {
48
- throw new Error(
49
- `Invalid partitive member certainty: "${certainty}". Allowed: confirmed, possible`,
50
- );
51
- }
52
- return certainty === 'possible' ? 'optional' : 'compulsory';
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 'compulsory';
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
- multiplicity: resolveMultiplicity(p),
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
- // Legacy v2 → v3 migration helper: derive a gl:certainty value from the
19
- // new ISO 704:2022 multiplicity so consumers that still read certainty
20
- // keep working during the one-release-cycle deprecation window.
21
- const CERTAINTY_FROM_MULTIPLICITY = {
22
- compulsory: 'confirmed',
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
- function multiplicityToCertainty(multiplicity) {
29
- return multiplicity in CERTAINTY_FROM_MULTIPLICITY
30
- ? CERTAINTY_FROM_MULTIPLICITY[multiplicity]
31
- : null;
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 multiplicity + is_delimiting (v3 wire shape).
714
- // Emit both new and legacy fields for one release cycle so
715
- // consumers that still read gl:certainty keep working.
716
- if (member.multiplicity) m['gl:multiplicity'] = member.multiplicity;
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
- // Legacy gl:certainty — derived from multiplicity when not set
721
- // explicitly. Remove after consumers migrate to multiplicity.
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
- ?? multiplicityToCertainty(member.multiplicity);
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
- byMemberMultiplicity: {
1009
- compulsory: 0,
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
- // Prefer multiplicity; fall back to legacy certainty for one release.
1052
- const multiplicity = member.multiplicity
1053
- ?? (member.certainty === 'possible' ? 'optional' : 'compulsory');
1054
- s.partitiveRelations.byMemberMultiplicity[multiplicity] =
1055
- (s.partitiveRelations.byMemberMultiplicity[multiplicity] || 0) + 1;
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 { rakeStrokeStyle, type PartitiveMultiplicity } from '../../utils/partitive-multiplicity';
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: { multiplicity?: PartitiveMultiplicity; isDelimiting?: boolean } = {},
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
- multiplicity: opts.multiplicity ?? 'compulsory',
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 multiplicity line rendering', () => {
48
- it('compulsory: stem + spine + 1 solid line per drop', () => {
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('compulsory_multiple: each drop has 2 lines', () => {
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, { multiplicity: 'compulsory_multiple' }),
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, { multiplicity: 'optional' }),
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('optional_multiple: each drop has 2 dashed lines', () => {
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, { multiplicity: 'optional_multiple' }),
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('compulsory_at_least_one: each drop has 1 solid + 1 dashed line', () => {
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, { multiplicity: 'compulsory_at_least_one' }),
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', multiplicity: 'compulsory', isDelimiting: false },
165
- { uri: 'u2', label: 'International System of Units', multiplicity: 'compulsory', isDelimiting: false },
166
- { uri: 'u3', label: 'base quantity dimension', multiplicity: 'compulsory', isDelimiting: false },
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
- it('renders exactly the line count + dash pattern the helper predicts', () => {
180
- const testCases: Array<{ multiplicity: PartitiveMultiplicity; isDelimiting: boolean }> = [
181
- { multiplicity: 'compulsory', isDelimiting: false },
182
- { multiplicity: 'compulsory', isDelimiting: true },
183
- { multiplicity: 'optional', isDelimiting: false },
184
- { multiplicity: 'compulsory_multiple', isDelimiting: false },
185
- { multiplicity: 'optional_multiple', isDelimiting: false },
186
- { multiplicity: 'compulsory_at_least_one', isDelimiting: false },
187
- ];
188
- for (const tc of testCases) {
189
- const expected = rakeStrokeStyle(tc.multiplicity, tc.isDelimiting);
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.24's top-level d.ts doesn't re-export the v3
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
- // (which our local augmentation extends) so the types are correct.
5
- import type { PartitiveRelation, PartitiveMember, ConceptRef } 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';
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.24's native v3 PartitiveRelation model
14
- * (ISO 704:2022 multiplicity + is_delimiting).
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 now consumes this directly (no migration adapter,
17
- * no `as any` bridge). These specs lock in the contract so an upstream
18
- * regression is caught here.
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
- multiplicity?: 'compulsory' | 'optional' | 'compulsory_multiple' | 'optional_multiple' | 'compulsory_at_least_one',
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
- multiplicity?: typeof multiplicity;
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 (multiplicity) data.multiplicity = multiplicity;
37
- if (isDelimiting) data.is_delimiting = isDelimiting;
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 — native v3 PartitiveRelation (glossarist 0.4.24)', () => {
51
- describe('PartitiveMember (ISO 704:2022 multiplicity + delimiting)', () => {
52
- it('multiplicity defaults to compulsory when omitted', () => {
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.multiplicity).toBe('compulsory');
55
- expect(m.isCompulsory).toBe(true);
59
+ expect(m.presence).toBe('required');
60
+ expect(m.isRequired).toBe(true);
61
+ expect(m.isOptional).toBe(false);
56
62
  });
57
63
 
58
- it('multiplicity is preserved when set to optional', () => {
59
- const m = makePartitive('test', '1.1', 'optional');
60
- expect(m.multiplicity).toBe('optional');
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', 'compulsory', true);
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('multiplicity + is_delimiting are orthogonal axes', () => {
76
- const m = makePartitive('test', '1.1', 'compulsory_multiple', true);
77
- expect(m.multiplicity).toBe('compulsory_multiple');
78
- expect(m.is_delimiting).toBe(true);
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 compulsory members with completeness=complete', () => {
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 v3 fields', () => {
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', 'compulsory', false),
139
- makePartitive('test', '1.5', 'compulsory_multiple', true),
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<{ multiplicity?: string; is_delimiting?: boolean; ref: unknown }>;
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
- // compulsory is the default multiplicity — not emitted in toJSON
153
- expect(json.partitives[0].multiplicity).toBeUndefined();
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' });