@glossarist/concept-browser 0.3.7 → 0.4.0

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.
Files changed (54) hide show
  1. package/README.md +3 -2
  2. package/cli/index.mjs +2 -1
  3. package/env.d.ts +5 -0
  4. package/package.json +4 -3
  5. package/scripts/build-edges.js +78 -10
  6. package/scripts/generate-data.mjs +152 -20
  7. package/scripts/generate-ontology-data.mjs +184 -0
  8. package/scripts/generate-ontology-schema.mjs +315 -0
  9. package/src/__tests__/concept-card.test.ts +1 -1
  10. package/src/__tests__/concept-detail-interaction.test.ts +40 -18
  11. package/src/__tests__/concept-formats.test.ts +32 -30
  12. package/src/__tests__/concept-timeline.test.ts +108 -83
  13. package/src/__tests__/concept-view.test.ts +15 -2
  14. package/src/__tests__/dataset-adapter.test.ts +172 -23
  15. package/src/__tests__/dataset-view.test.ts +6 -5
  16. package/src/__tests__/designation-registry.test.ts +161 -0
  17. package/src/__tests__/graph.test.ts +62 -0
  18. package/src/__tests__/language-detail.test.ts +117 -60
  19. package/src/__tests__/ontology-registry.test.ts +109 -0
  20. package/src/__tests__/relationship-categories.test.ts +62 -0
  21. package/src/__tests__/test-helpers.ts +11 -8
  22. package/src/adapters/DatasetAdapter.ts +171 -48
  23. package/src/adapters/model-bridge.ts +277 -0
  24. package/src/adapters/ontology-registry.ts +75 -0
  25. package/src/adapters/ontology-schema.ts +100 -0
  26. package/src/adapters/types.ts +52 -77
  27. package/src/components/AppSidebar.vue +1 -1
  28. package/src/components/CitationDisplay.vue +35 -0
  29. package/src/components/ConceptDetail.vue +334 -93
  30. package/src/components/ConceptRdfView.vue +397 -0
  31. package/src/components/ConceptTimeline.vue +56 -52
  32. package/src/components/GraphPanel.vue +96 -31
  33. package/src/components/LanguageDetail.vue +45 -37
  34. package/src/components/NavIcon.vue +1 -0
  35. package/src/components/NonVerbalRepDisplay.vue +38 -0
  36. package/src/components/RelationshipList.vue +99 -0
  37. package/src/config/use-site-config.ts +3 -0
  38. package/src/data/ontology-schema.json +1551 -0
  39. package/src/data/taxonomies.json +543 -0
  40. package/src/graph/GraphEngine.ts +7 -4
  41. package/src/router/index.ts +5 -0
  42. package/src/shims/empty.ts +1 -0
  43. package/src/shims/node-crypto.ts +6 -0
  44. package/src/shims/node-path.ts +10 -0
  45. package/src/stores/vocabulary.ts +75 -25
  46. package/src/style.css +74 -20
  47. package/src/utils/concept-formats.ts +22 -20
  48. package/src/utils/concept-helpers.ts +43 -23
  49. package/src/utils/designation-registry.ts +124 -0
  50. package/src/utils/relationship-categories.ts +84 -0
  51. package/src/views/OntologySchemaView.vue +302 -0
  52. package/src/views/PageView.vue +28 -17
  53. package/src/views/StatsView.vue +34 -12
  54. package/vite.config.ts +8 -0
@@ -0,0 +1,161 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Expression, Abbreviation } from 'glossarist';
3
+ import {
4
+ designationTypeInfo,
5
+ normativeStatusInfo,
6
+ abbreviationDetails,
7
+ grammarBadges,
8
+ pronunciationLabel,
9
+ pronunciationTooltip,
10
+ termTypeInfo,
11
+ sourceStatusInfo,
12
+ sourceTypeInfo,
13
+ } from '../utils/designation-registry';
14
+
15
+ describe('designationTypeInfo', () => {
16
+ it('returns info for expression', () => {
17
+ const d = Expression.fromJSON({ type: 'expression', designation: 'test' });
18
+ const info = designationTypeInfo(d);
19
+ expect(info.label).toBe('expression');
20
+ expect(info.color).toContain('sky');
21
+ });
22
+
23
+ it('returns info for abbreviation', () => {
24
+ const d = Abbreviation.fromJSON({ type: 'abbreviation', designation: 'ISO' });
25
+ const info = designationTypeInfo(d);
26
+ expect(info.label).toBe('abbreviation');
27
+ expect(info.color).toContain('amber');
28
+ });
29
+
30
+ it('returns info for symbol with broader hierarchy', () => {
31
+ const d = { type: 'letter_symbol', designation: 'x' } as any;
32
+ const info = designationTypeInfo(d);
33
+ expect(info.label).toBe('letter symbol');
34
+ });
35
+
36
+ it('returns fallback for unknown type', () => {
37
+ const d = { type: 'custom', designation: 'x' } as any;
38
+ const info = designationTypeInfo(d);
39
+ expect(info.label).toBe('custom');
40
+ });
41
+ });
42
+
43
+ describe('normativeStatusInfo', () => {
44
+ it('returns preferred', () => {
45
+ const info = normativeStatusInfo('preferred');
46
+ expect(info.label).toBe('preferred');
47
+ expect(info.color).toContain('emerald');
48
+ });
49
+
50
+ it('returns deprecated', () => {
51
+ const info = normativeStatusInfo('deprecated');
52
+ expect(info.label).toBe('deprecated');
53
+ expect(info.color).toContain('red');
54
+ });
55
+
56
+ it('returns empty for null', () => {
57
+ const info = normativeStatusInfo(null);
58
+ expect(info.label).toBe('');
59
+ });
60
+ });
61
+
62
+ describe('sourceStatusInfo', () => {
63
+ it('returns identical status', () => {
64
+ const info = sourceStatusInfo('identical');
65
+ expect(info.label).toBe('identical');
66
+ });
67
+
68
+ it('returns modified status', () => {
69
+ const info = sourceStatusInfo('modified');
70
+ expect(info.label).toBe('modified');
71
+ expect(info.definition).toBeTruthy();
72
+ });
73
+
74
+ it('returns empty for null', () => {
75
+ const info = sourceStatusInfo(null);
76
+ expect(info.label).toBe('');
77
+ });
78
+ });
79
+
80
+ describe('sourceTypeInfo', () => {
81
+ it('returns authoritative', () => {
82
+ const info = sourceTypeInfo('authoritative');
83
+ expect(info.label).toBe('authoritative');
84
+ expect(info.color).toContain('purple');
85
+ });
86
+
87
+ it('returns lineage', () => {
88
+ const info = sourceTypeInfo('lineage');
89
+ expect(info.label).toBe('lineage');
90
+ expect(info.color).toContain('blue');
91
+ });
92
+ });
93
+
94
+ describe('termTypeInfo', () => {
95
+ it('returns term type with definition', () => {
96
+ const info = termTypeInfo('acronym');
97
+ expect(info.label).toBe('acronym');
98
+ expect(info.definition).toBeTruthy();
99
+ expect(info.category).toBe('abbreviation');
100
+ });
101
+
102
+ it('returns empty for null', () => {
103
+ const info = termTypeInfo(null);
104
+ expect(info.label).toBe('');
105
+ });
106
+ });
107
+
108
+ describe('abbreviationDetails', () => {
109
+ it('identifies acronym', () => {
110
+ const d = Abbreviation.fromJSON({ type: 'abbreviation', designation: 'ISO', acronym: true });
111
+ expect(abbreviationDetails(d)).toContain('acronym');
112
+ expect(abbreviationDetails(d)).not.toContain('initialism');
113
+ });
114
+
115
+ it('identifies initialism', () => {
116
+ const d = Abbreviation.fromJSON({ type: 'abbreviation', designation: 'UN', initialism: true });
117
+ expect(abbreviationDetails(d)).toContain('initialism');
118
+ });
119
+
120
+ it('identifies truncation', () => {
121
+ const d = Abbreviation.fromJSON({ type: 'abbreviation', designation: 'info', truncation: true });
122
+ expect(abbreviationDetails(d)).toContain('truncation');
123
+ });
124
+ });
125
+
126
+ describe('grammarBadges', () => {
127
+ it('returns gender badge with ontology label', () => {
128
+ const d = Expression.fromJSON({ type: 'expression', designation: 'test', grammar_info: [{ gender: 'f' }] });
129
+ const badges = grammarBadges((d as any).grammarInfo[0]);
130
+ expect(badges).toEqual([{ label: 'feminine', definition: 'Feminine grammatical gender.' }]);
131
+ });
132
+
133
+ it('returns gender and number badges', () => {
134
+ const d = Expression.fromJSON({ type: 'expression', designation: 'test', grammar_info: [{ gender: 'm', number: 'singular' }] });
135
+ const badges = grammarBadges((d as any).grammarInfo[0]);
136
+ expect(badges[0].label).toBe('masculine');
137
+ expect(badges[1].label).toBe('singular');
138
+ });
139
+ });
140
+
141
+ describe('pronunciationLabel', () => {
142
+ it('shows content with system', () => {
143
+ const p = { content: '/tɛst/', system: 'IPA', language: null, script: null, country: null } as any;
144
+ expect(pronunciationLabel(p)).toBe('/tɛst/ (IPA)');
145
+ });
146
+
147
+ it('shows content only', () => {
148
+ const p = { content: '/t/', system: null, language: null, script: null, country: null } as any;
149
+ expect(pronunciationLabel(p)).toBe('/t/');
150
+ });
151
+ });
152
+
153
+ describe('pronunciationTooltip', () => {
154
+ it('includes all metadata', () => {
155
+ const p = { content: '/t/', system: 'IPA', language: 'en', script: 'Latn', country: 'US' } as any;
156
+ const tip = pronunciationTooltip(p);
157
+ expect(tip).toContain('Language: en');
158
+ expect(tip).toContain('System: IPA');
159
+ expect(tip).toContain('Country: US');
160
+ });
161
+ });
@@ -91,6 +91,37 @@ describe('GraphEngine', () => {
91
91
  expect(g.edgeCount).toBe(1);
92
92
  });
93
93
 
94
+ it('keeps separate edges for different languages', () => {
95
+ const g = new GraphEngine();
96
+ g.addEdge({ source: 'uri:a', target: 'uri:b', type: 'references', register: 'test', lang: 'eng' });
97
+ g.addEdge({ source: 'uri:a', target: 'uri:b', type: 'references', register: 'test', lang: 'fra' });
98
+ expect(g.edgeCount).toBe(2);
99
+ });
100
+
101
+ it('deduplicates edges with same source+target+type+lang', () => {
102
+ const g = new GraphEngine();
103
+ g.addEdge({ source: 'uri:a', target: 'uri:b', type: 'references', register: 'test', lang: 'eng' });
104
+ g.addEdge({ source: 'uri:a', target: 'uri:b', type: 'references', register: 'test', lang: 'eng' });
105
+ expect(g.edgeCount).toBe(1);
106
+ });
107
+
108
+ it('creates domain stub with correct fields', () => {
109
+ const g = new GraphEngine();
110
+ g.addEdge({
111
+ source: 'https://glossarist.org/isotc211/concept/3',
112
+ target: 'https://glossarist.org/isotc211/domain/iso-19105',
113
+ type: 'domain',
114
+ label: 'ISO 19105',
115
+ register: 'isotc211',
116
+ lang: 'eng',
117
+ });
118
+ const domainNode = g.getNode('https://glossarist.org/isotc211/domain/iso-19105');
119
+ expect(domainNode?.register).toBe('isotc211');
120
+ expect(domainNode?.nodeType).toBe('domain');
121
+ expect(domainNode?.status).toBe('domain');
122
+ expect(domainNode?.loaded).toBe(false);
123
+ });
124
+
94
125
  it('extracts register from URI for stub nodes', () => {
95
126
  const g = new GraphEngine();
96
127
  g.addEdge({
@@ -172,6 +203,37 @@ describe('GraphEngine', () => {
172
203
  const sub = g.getSubgraph('uri:a', 5);
173
204
  expect(sub.nodes.length).toBe(2);
174
205
  });
206
+
207
+ it('does not traverse past domain nodes in getSubgraph', () => {
208
+ const g = new GraphEngine();
209
+ g.addNode(makeNode('https://glossarist.org/test/concept/a', 'a'));
210
+ g.addNode(makeNode('https://glossarist.org/test/concept/b', 'b'));
211
+ g.addNode(makeNode('https://glossarist.org/test/concept/c', 'c'));
212
+ g.addNode(makeNode('https://glossarist.org/test/concept/d', 'd'));
213
+
214
+ g.addEdge({
215
+ source: 'https://glossarist.org/test/concept/a',
216
+ target: 'https://glossarist.org/test/domain/iso-12345',
217
+ type: 'domain', register: 'test', label: 'ISO 12345', lang: 'eng',
218
+ });
219
+ g.addEdge({
220
+ source: 'https://glossarist.org/test/concept/b',
221
+ target: 'https://glossarist.org/test/domain/iso-12345',
222
+ type: 'domain', register: 'test', label: 'ISO 12345', lang: 'eng',
223
+ });
224
+ g.addEdge({
225
+ source: 'https://glossarist.org/test/concept/c',
226
+ target: 'https://glossarist.org/test/domain/iso-12345',
227
+ type: 'domain', register: 'test', label: 'ISO 12345', lang: 'eng',
228
+ });
229
+
230
+ const sub = g.getSubgraph('https://glossarist.org/test/concept/a', 3);
231
+ const nodeUris = sub.nodes.map(n => n.uri);
232
+ expect(nodeUris).toContain('https://glossarist.org/test/concept/a');
233
+ expect(nodeUris).toContain('https://glossarist.org/test/domain/iso-12345');
234
+ expect(nodeUris).not.toContain('https://glossarist.org/test/concept/b');
235
+ expect(nodeUris).not.toContain('https://glossarist.org/test/concept/c');
236
+ });
175
237
  });
176
238
 
177
239
  describe('getAllNodes', () => {
@@ -1,9 +1,26 @@
1
1
  import { describe, it, expect, beforeEach } from 'vitest';
2
- import { mount, flushPromises } from '@vue/test-utils';
2
+ import { mount } from '@vue/test-utils';
3
3
  import LanguageDetail from '../components/LanguageDetail.vue';
4
4
  import { useVocabularyStore } from '../stores/vocabulary';
5
- import type { LocalizedConcept } from '../adapters/types';
6
- import { createTestRouter, setupPinia, makeManifest, makeLocalizedConcept, makeAdapterStub } from './test-helpers';
5
+ import { conceptFromJson } from '../adapters/model-bridge';
6
+ import { createTestRouter, setupPinia, makeManifest, makeAdapterStub } from './test-helpers';
7
+
8
+ function makeConceptJson(overrides: Record<string, any> = {}) {
9
+ return {
10
+ '@id': 'https://glossarist.org/test/concept/1',
11
+ '@type': 'gl:Concept',
12
+ 'gl:identifier': '1',
13
+ 'gl:localizedConcept': {
14
+ eng: {
15
+ '@type': 'gl:LocalizedConcept',
16
+ 'gl:languageCode': 'eng',
17
+ 'gl:entryStatus': 'valid',
18
+ ...overrides.eng,
19
+ },
20
+ ...(overrides.otherLangs || {}),
21
+ },
22
+ };
23
+ }
7
24
 
8
25
  describe('LanguageDetail', () => {
9
26
  let pinia: ReturnType<typeof setupPinia>;
@@ -17,33 +34,58 @@ describe('LanguageDetail', () => {
17
34
  store.datasets.set('test', makeAdapterStub());
18
35
  });
19
36
 
20
- function mountDetail(lcs: Record<string, LocalizedConcept>, activeLang = 'eng') {
37
+ function mountDetail(conceptJson: Record<string, any>, activeLang = 'eng') {
38
+ const concept = conceptFromJson(conceptJson);
21
39
  return mount(LanguageDetail, {
22
40
  global: { plugins: [pinia, router], directives: { math: () => {} } },
23
- props: { localizedConcepts: lcs, activeLang },
41
+ props: { concept, activeLang },
24
42
  });
25
43
  }
26
44
 
27
45
  it('renders language selector buttons', () => {
28
- const eng = makeLocalizedConcept();
29
- const fra = makeLocalizedConcept({ '@id': '.../fra', 'gl:languageCode': 'fra' });
30
- const wrapper = mountDetail({ eng, fra });
46
+ const json = makeConceptJson({
47
+ eng: {},
48
+ otherLangs: {
49
+ fra: {
50
+ '@type': 'gl:LocalizedConcept',
51
+ 'gl:languageCode': 'fra',
52
+ 'gl:entryStatus': 'valid',
53
+ },
54
+ },
55
+ });
56
+ const wrapper = mountDetail(json);
31
57
  expect(wrapper.text()).toContain('English');
32
58
  expect(wrapper.text()).toContain('French');
33
59
  });
34
60
 
35
61
  it('highlights active language button', () => {
36
- const eng = makeLocalizedConcept();
37
- const fra = makeLocalizedConcept({ '@id': '.../fra', 'gl:languageCode': 'fra' });
38
- const wrapper = mountDetail({ eng, fra }, 'eng');
62
+ const json = makeConceptJson({
63
+ eng: {},
64
+ otherLangs: {
65
+ fra: {
66
+ '@type': 'gl:LocalizedConcept',
67
+ 'gl:languageCode': 'fra',
68
+ 'gl:entryStatus': 'valid',
69
+ },
70
+ },
71
+ });
72
+ const wrapper = mountDetail(json, 'eng');
39
73
  const buttons = wrapper.findAll('button').filter(b => b.text().includes('English'));
40
74
  expect(buttons[0].classes()).toContain('bg-ink-800');
41
75
  });
42
76
 
43
77
  it('emits update:activeLang on language click', async () => {
44
- const eng = makeLocalizedConcept();
45
- const fra = makeLocalizedConcept({ '@id': '.../fra', 'gl:languageCode': 'fra' });
46
- const wrapper = mountDetail({ eng, fra }, 'eng');
78
+ const json = makeConceptJson({
79
+ eng: {},
80
+ otherLangs: {
81
+ fra: {
82
+ '@type': 'gl:LocalizedConcept',
83
+ 'gl:languageCode': 'fra',
84
+ 'gl:entryStatus': 'valid',
85
+ },
86
+ },
87
+ });
88
+ const wrapper = mountDetail(json, 'eng');
47
89
  const fraBtn = wrapper.findAll('button').find(b => b.text().includes('French'));
48
90
  expect(fraBtn).toBeDefined();
49
91
  await fraBtn!.trigger('click');
@@ -51,96 +93,111 @@ describe('LanguageDetail', () => {
51
93
  });
52
94
 
53
95
  it('shows entry status badge', () => {
54
- const eng = makeLocalizedConcept({ 'gl:entryStatus': 'valid' });
55
- const wrapper = mountDetail({ eng });
96
+ const json = makeConceptJson({
97
+ eng: { 'gl:entryStatus': 'valid' },
98
+ });
99
+ const wrapper = mountDetail(json);
56
100
  expect(wrapper.text()).toContain('valid');
57
101
  });
58
102
 
59
- it('shows designations', () => {
60
- const eng = makeLocalizedConcept({
61
- 'gl:designation': [
62
- { '@type': 'gl:Expression', 'gl:term': 'road', 'gl:normativeStatus': 'preferred' },
63
- ],
103
+ it('shows designations with ontology labels', () => {
104
+ const json = makeConceptJson({
105
+ eng: {
106
+ 'gl:designation': [
107
+ { '@type': 'gl:Expression', 'gl:term': 'road', 'gl:normativeStatus': 'preferred' },
108
+ ],
109
+ },
64
110
  });
65
- const wrapper = mountDetail({ eng });
111
+ const wrapper = mountDetail(json);
66
112
  expect(wrapper.text()).toContain('road');
67
- expect(wrapper.text()).toContain('Expression');
68
- expect(wrapper.text()).toContain('Preferred');
113
+ expect(wrapper.text()).toContain('expression');
114
+ expect(wrapper.text()).toContain('preferred');
69
115
  });
70
116
 
71
117
  it('shows definition', () => {
72
- const eng = makeLocalizedConcept({
73
- 'gl:definition': [{ '@type': 'gl:Definition', 'gl:content': 'A paved surface for vehicles.' }],
118
+ const json = makeConceptJson({
119
+ eng: {
120
+ 'gl:definition': [{ '@type': 'gl:Definition', 'gl:content': 'A paved surface for vehicles.' }],
121
+ },
74
122
  });
75
- const wrapper = mountDetail({ eng });
123
+ const wrapper = mountDetail(json);
76
124
  expect(wrapper.text()).toContain('Definition');
77
125
  expect(wrapper.text()).toContain('paved surface');
78
126
  });
79
127
 
80
128
  it('shows notes', () => {
81
- const eng = makeLocalizedConcept({
82
- 'gl:notes': [{ '@type': 'gl:Note', 'gl:content': 'This is a note.' }],
129
+ const json = makeConceptJson({
130
+ eng: {
131
+ 'gl:notes': [{ '@type': 'gl:Note', 'gl:content': 'This is a note.' }],
132
+ },
83
133
  });
84
- const wrapper = mountDetail({ eng });
134
+ const wrapper = mountDetail(json);
85
135
  expect(wrapper.text()).toContain('Notes');
86
136
  expect(wrapper.text()).toContain('This is a note');
87
137
  });
88
138
 
89
139
  it('shows examples', () => {
90
- const eng = makeLocalizedConcept({
91
- 'gl:examples': [{ '@type': 'gl:Example', 'gl:content': 'A highway is a road.' }],
140
+ const json = makeConceptJson({
141
+ eng: {
142
+ 'gl:examples': [{ '@type': 'gl:Example', 'gl:content': 'A highway is a road.' }],
143
+ },
92
144
  });
93
- const wrapper = mountDetail({ eng });
145
+ const wrapper = mountDetail(json);
94
146
  expect(wrapper.text()).toContain('Examples');
95
147
  expect(wrapper.text()).toContain('A highway is a road');
96
148
  });
97
149
 
98
150
  it('shows sources', () => {
99
- const eng = makeLocalizedConcept({
100
- 'gl:source': [{ '@type': 'gl:Source', 'gl:sourceType': 'authoritative', 'gl:origin': { '@type': 'gl:Origin', 'gl:ref': 'ISO 7010' } }],
151
+ const json = makeConceptJson({
152
+ eng: {
153
+ 'gl:source': [{ '@type': 'gl:Source', 'gl:sourceType': 'authoritative', 'gl:origin': { '@type': 'gl:Origin', 'gl:ref': { '@type': 'gl:Ref', 'gl:source': 'ISO 7010' } } }],
154
+ },
101
155
  });
102
- const wrapper = mountDetail({ eng });
156
+ const wrapper = mountDetail(json);
103
157
  expect(wrapper.text()).toContain('Sources');
104
158
  expect(wrapper.text()).toContain('ISO 7010');
105
159
  });
106
160
 
107
161
  it('shows term-only state for language without definition', () => {
108
- const eng = makeLocalizedConcept({
109
- 'gl:designation': [{ '@type': 'gl:Expression', 'gl:term': 'test', 'gl:normativeStatus': 'preferred' }],
162
+ const json = makeConceptJson({
163
+ eng: {
164
+ 'gl:designation': [{ '@type': 'gl:Expression', 'gl:term': 'test', 'gl:normativeStatus': 'preferred' }],
165
+ },
110
166
  });
111
- delete (eng as any)['gl:definition'];
112
- delete (eng as any)['gl:notes'];
113
- delete (eng as any)['gl:examples'];
114
- const wrapper = mountDetail({ eng });
167
+ const wrapper = mountDetail(json);
115
168
  expect(wrapper.text()).toContain('Term only in English');
116
169
  });
117
170
 
118
171
  it('shows no data message for missing language', () => {
119
- const eng = makeLocalizedConcept();
120
- const wrapper = mountDetail({ eng }, 'zho');
172
+ const json = makeConceptJson({ eng: {} });
173
+ const wrapper = mountDetail(json, 'zho');
121
174
  expect(wrapper.text()).toContain('No data available');
122
175
  });
123
176
 
124
- it('shows designation type badges', () => {
125
- const eng = makeLocalizedConcept({
126
- 'gl:designation': [
127
- { '@type': 'gl:Symbol', 'gl:term': 'H₂O', 'gl:normativeStatus': 'preferred' },
128
- { '@type': 'gl:Abbreviation', 'gl:term': 'abbr', 'gl:normativeStatus': 'admitted' },
129
- ],
177
+ it('shows designation type badges with ontology labels', () => {
178
+ const json = makeConceptJson({
179
+ eng: {
180
+ 'gl:designation': [
181
+ { '@type': 'gl:Symbol', 'gl:term': 'H₂O', 'gl:normativeStatus': 'preferred' },
182
+ { '@type': 'gl:Abbreviation', 'gl:term': 'abbr', 'gl:normativeStatus': 'admitted' },
183
+ ],
184
+ },
130
185
  });
131
- const wrapper = mountDetail({ eng });
132
- expect(wrapper.text()).toContain('Symbol');
133
- expect(wrapper.text()).toContain('Abbreviation');
186
+ const wrapper = mountDetail(json);
187
+ expect(wrapper.text()).toContain('symbol');
188
+ expect(wrapper.text()).toContain('abbreviation');
134
189
  });
135
190
 
136
- it('shows gender and plurality when present', () => {
137
- const eng = makeLocalizedConcept({
138
- 'gl:designation': [
139
- { '@type': 'gl:Expression', 'gl:term': 'route', 'gl:normativeStatus': 'preferred', 'gl:gender': 'f', 'gl:plurality': 'singular' },
140
- ],
191
+ it('shows grammar info with ontology labels when present', () => {
192
+ const json = makeConceptJson({
193
+ eng: {
194
+ 'gl:designation': [
195
+ { '@type': 'gl:Expression', 'gl:term': 'route', 'gl:normativeStatus': 'preferred', 'gl:grammarInfo': [{ 'gl:gender': 'f', 'gl:number': 'singular' }] },
196
+ ],
197
+ },
141
198
  });
142
- const wrapper = mountDetail({ eng });
143
- expect(wrapper.text()).toContain('f');
199
+ const wrapper = mountDetail(json);
200
+ expect(wrapper.text()).toContain('feminine');
144
201
  expect(wrapper.text()).toContain('singular');
145
202
  });
146
203
  });
@@ -0,0 +1,109 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { ontology } from '../adapters/ontology-registry';
3
+
4
+ describe('OntologyRegistry', () => {
5
+ it('loads all 10 taxonomies', () => {
6
+ expect(ontology.getAll('conceptStatus').length).toBe(7);
7
+ expect(ontology.getAll('entryStatus').length).toBe(4);
8
+ expect(ontology.getAll('normativeStatus').length).toBe(4);
9
+ expect(ontology.getAll('sourceType').length).toBe(2);
10
+ expect(ontology.getAll('sourceStatus').length).toBe(10);
11
+ expect(ontology.getAll('relationshipType').length).toBe(14);
12
+ expect(ontology.getAll('designationType').length).toBe(5);
13
+ expect(ontology.getAll('termType').length).toBe(24);
14
+ expect(ontology.getAll('grammarGender').length).toBe(4);
15
+ expect(ontology.getAll('grammarNumber').length).toBe(3);
16
+ });
17
+
18
+ it('returns correct labels for conceptStatus', () => {
19
+ expect(ontology.getLabel('conceptStatus', 'valid')).toBe('valid');
20
+ expect(ontology.getLabel('conceptStatus', 'superseded')).toBe('superseded');
21
+ expect(ontology.getLabel('conceptStatus', 'not_valid')).toBe('not valid');
22
+ });
23
+
24
+ it('returns correct labels for grammarGender', () => {
25
+ expect(ontology.getLabel('grammarGender', 'm')).toBe('masculine');
26
+ expect(ontology.getLabel('grammarGender', 'f')).toBe('feminine');
27
+ expect(ontology.getLabel('grammarGender', 'n')).toBe('neuter');
28
+ expect(ontology.getLabel('grammarGender', 'c')).toBe('common');
29
+ });
30
+
31
+ it('returns altLabel for grammarGender', () => {
32
+ expect(ontology.getAltLabel('grammarGender', 'm')).toBe('m');
33
+ expect(ontology.getAltLabel('grammarGender', 'f')).toBe('f');
34
+ });
35
+
36
+ it('returns definitions for every taxonomy value', () => {
37
+ for (const status of ['draft', 'submitted', 'not_valid', 'invalid', 'valid', 'superseded', 'retired']) {
38
+ expect(ontology.getDefinition('conceptStatus', status)).toBeTruthy();
39
+ }
40
+ for (const gender of ['m', 'f', 'n', 'c']) {
41
+ expect(ontology.getDefinition('grammarGender', gender)).toBeTruthy();
42
+ }
43
+ for (const status of ['identical', 'similar', 'modified', 'restyle', 'context_added', 'generalisation', 'specialisation', 'unspecified', 'related', 'not_equal']) {
44
+ expect(ontology.getDefinition('sourceStatus', status)).toBeTruthy();
45
+ }
46
+ });
47
+
48
+ it('returns correct hierarchy for designationType', () => {
49
+ expect(ontology.getBroader('designationType', 'abbreviation')).toBe('expression');
50
+ expect(ontology.getBroader('designationType', 'letter_symbol')).toBe('symbol');
51
+ expect(ontology.getBroader('designationType', 'graphical_symbol')).toBe('symbol');
52
+ expect(ontology.getBroader('designationType', 'expression')).toBeNull();
53
+ expect(ontology.getBroader('designationType', 'symbol')).toBeNull();
54
+ });
55
+
56
+ it('returns narrower for designationType', () => {
57
+ const childrenOfExpression = ontology.getNarrower('designationType', 'expression');
58
+ expect(childrenOfExpression.map(c => c.id)).toContain('abbreviation');
59
+
60
+ const childrenOfSymbol = ontology.getNarrower('designationType', 'symbol');
61
+ expect(childrenOfSymbol.map(c => c.id)).toEqual(expect.arrayContaining(['letter_symbol', 'graphical_symbol']));
62
+ });
63
+
64
+ it('returns broader for termType', () => {
65
+ expect(ontology.getBroader('termType', 'acronym')).toBe('abbreviation');
66
+ expect(ontology.getBroader('termType', 'initialism')).toBe('abbreviation');
67
+ });
68
+
69
+ it('returns null for unknown taxonomy/value', () => {
70
+ expect(ontology.getConcept('conceptStatus', 'nonexistent')).toBeNull();
71
+ expect(ontology.getLabel('conceptStatus', 'nonexistent')).toBe('nonexistent');
72
+ expect(ontology.getDefinition('conceptStatus', 'nonexistent')).toBeNull();
73
+ });
74
+
75
+ it('returns scheme IRIs', () => {
76
+ expect(ontology.getScheme('conceptStatus')).toContain('status');
77
+ expect(ontology.getScheme('grammarGender')).toContain('gender');
78
+ });
79
+
80
+ it('has() checks work', () => {
81
+ expect(ontology.has('conceptStatus', 'valid')).toBe(true);
82
+ expect(ontology.has('conceptStatus', 'nonexistent')).toBe(false);
83
+ });
84
+
85
+ it('normativeStatus includes correct labels', () => {
86
+ expect(ontology.getLabel('normativeStatus', 'preferred')).toBe('preferred');
87
+ expect(ontology.getLabel('normativeStatus', 'admitted')).toBe('admitted');
88
+ expect(ontology.getLabel('normativeStatus', 'deprecated')).toBe('deprecated');
89
+ expect(ontology.getLabel('normativeStatus', 'superseded')).toBe('superseded');
90
+ });
91
+
92
+ it('relationshipType has all glossarist-specific types', () => {
93
+ const types = ontology.getAll('relationshipType').map(c => c.id);
94
+ expect(types).toContain('deprecates');
95
+ expect(types).toContain('supersedes');
96
+ expect(types).toContain('superseded_by');
97
+ expect(types).toContain('compare');
98
+ expect(types).toContain('contrast');
99
+ expect(types).toContain('homograph');
100
+ expect(types).toContain('false_friend');
101
+ expect(types).toContain('abbreviated_form_for');
102
+ expect(types).toContain('short_form_for');
103
+ expect(types).toContain('sequentially_related');
104
+ expect(types).toContain('spatially_related');
105
+ expect(types).toContain('temporally_related');
106
+ expect(types).toContain('related_concept_broader');
107
+ expect(types).toContain('related_concept_narrower');
108
+ });
109
+ });
@@ -0,0 +1,62 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { categorizeRelationship, relationshipLabel, RELATIONSHIP_CATEGORIES } from '../utils/relationship-categories';
3
+
4
+ describe('RELATIONSHIP_CATEGORIES', () => {
5
+ it('has expected categories', () => {
6
+ const ids = RELATIONSHIP_CATEGORIES.map(c => c.id);
7
+ expect(ids).toContain('hierarchical');
8
+ expect(ids).toContain('mapping');
9
+ expect(ids).toContain('associative');
10
+ expect(ids).toContain('lifecycle');
11
+ expect(ids).toContain('comparative');
12
+ expect(ids).toContain('spatiotemporal');
13
+ expect(ids).toContain('lexical');
14
+ expect(ids).toContain('designation');
15
+ });
16
+
17
+ it('each category has required fields', () => {
18
+ for (const cat of RELATIONSHIP_CATEGORIES) {
19
+ expect(cat.id).toBeTruthy();
20
+ expect(cat.label).toBeTruthy();
21
+ expect(cat.types.length).toBeGreaterThan(0);
22
+ expect(cat.color).toBeTruthy();
23
+ }
24
+ });
25
+ });
26
+
27
+ describe('categorizeRelationship', () => {
28
+ it('categorizes broader as hierarchical', () => {
29
+ expect(categorizeRelationship('broader').id).toBe('hierarchical');
30
+ });
31
+
32
+ it('categorizes equivalent as mapping', () => {
33
+ expect(categorizeRelationship('equivalent').id).toBe('mapping');
34
+ });
35
+
36
+ it('categorizes references as associative', () => {
37
+ expect(categorizeRelationship('references').id).toBe('associative');
38
+ });
39
+
40
+ it('categorizes supersedes as lifecycle', () => {
41
+ expect(categorizeRelationship('supersedes').id).toBe('lifecycle');
42
+ });
43
+
44
+ it('categorizes compare as comparative', () => {
45
+ expect(categorizeRelationship('compare').id).toBe('comparative');
46
+ });
47
+
48
+ it('returns other for unknown type', () => {
49
+ expect(categorizeRelationship('unknown_type').id).toBe('other');
50
+ });
51
+ });
52
+
53
+ describe('relationshipLabel', () => {
54
+ it('formats snake_case as title case', () => {
55
+ expect(relationshipLabel('broader_generic')).toBe('Broader Generic');
56
+ expect(relationshipLabel('related_concept')).toBe('Related Concept');
57
+ });
58
+
59
+ it('handles single word', () => {
60
+ expect(relationshipLabel('broader')).toBe('Broader');
61
+ });
62
+ });