@glossarist/concept-browser 0.7.78 → 0.7.80

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 (55) hide show
  1. package/package.json +2 -2
  2. package/scripts/build-edges.js +109 -39
  3. package/scripts/generate-data.mjs +63 -27
  4. package/src/__tests__/build-edges-redundancy.test.ts +101 -0
  5. package/src/__tests__/composables/use-concept-diff.test.ts +240 -0
  6. package/src/__tests__/composables/use-concept-edges-v2.test.ts +132 -0
  7. package/src/__tests__/composables/use-dataset-stats.test.ts +96 -0
  8. package/src/__tests__/config/page-types.test.ts +82 -0
  9. package/src/__tests__/model-bridge-bridges.test.ts +3 -2
  10. package/src/__tests__/model-bridge-citation.test.ts +1 -1
  11. package/src/__tests__/ontology-registry.test.ts +2 -2
  12. package/src/__tests__/partitive-relation-styling.test.ts +82 -0
  13. package/src/__tests__/partitive-relation.test.ts +285 -0
  14. package/src/adapters/GraphDataSource.ts +16 -6
  15. package/src/adapters/dataset-stats.ts +13 -0
  16. package/src/adapters/non-verbal/glossarist-augment.d.ts +155 -4
  17. package/src/adapters/types.ts +27 -17
  18. package/src/components/AppSidebar.vue +14 -57
  19. package/src/components/ConceptDetail.vue +29 -0
  20. package/src/components/ConceptDiffModal.vue +120 -0
  21. package/src/components/ConceptDiffView.vue +192 -0
  22. package/src/components/ConceptEditionRail.vue +61 -36
  23. package/src/components/PartitiveRelationList.vue +139 -0
  24. package/src/components/groups/DatasetGroupRenderer.vue +14 -4
  25. package/src/components/groups/DefaultGroupSidebar.vue +28 -11
  26. package/src/components/groups/LineageGroupSidebar.vue +17 -29
  27. package/src/composables/use-concept-diff.ts +231 -0
  28. package/src/composables/use-concept-edges.ts +66 -45
  29. package/src/composables/use-dataset-stats.ts +52 -0
  30. package/src/config/page-types.ts +67 -0
  31. package/src/config/types.ts +6 -1
  32. package/src/config/use-site-config.ts +13 -29
  33. package/src/data/taxonomies.json +24 -1
  34. package/src/i18n/locales/eng.yml +6 -0
  35. package/src/utils/partitive-relation-styling.ts +62 -0
  36. package/src/views/DatasetView.vue +5 -3
  37. package/src/views/StatsView.vue +9 -22
  38. package/src/__tests__/hyperedge-styling.test.ts +0 -57
  39. package/src/__tests__/partitive-hyperedge.test.ts +0 -199
  40. package/src/components/PartitiveHyperedgeList.vue +0 -112
  41. package/src/islands/BibliographyIsland.vue +0 -26
  42. package/src/islands/ConceptDetailIsland.vue +0 -27
  43. package/src/islands/ConceptDiffIsland.vue +0 -214
  44. package/src/islands/ConceptIsland.vue +0 -102
  45. package/src/islands/GraphIsland.vue +0 -15
  46. package/src/islands/GroupRendererIsland.vue +0 -15
  47. package/src/islands/HeaderIsland.vue +0 -10
  48. package/src/islands/NonVerbalIsland.vue +0 -75
  49. package/src/islands/RelationSphereIsland.vue +0 -20
  50. package/src/islands/SearchIsland.vue +0 -23
  51. package/src/islands/SeriesCardIsland.vue +0 -12
  52. package/src/islands/ShortcutsIsland.vue +0 -13
  53. package/src/islands/SidebarIsland.vue +0 -15
  54. package/src/islands/SupersessionDiffIsland.vue +0 -267
  55. package/src/utils/hyperedge-styling.ts +0 -48
@@ -0,0 +1,240 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ toDiffableConcept,
4
+ deriveDiffSections,
5
+ computeSimilarity,
6
+ useConceptDiff,
7
+ } from '../../composables/use-concept-diff';
8
+ import type { ConceptDiff } from 'glossarist/diff';
9
+
10
+ describe('useConceptDiff', () => {
11
+ describe('toDiffableConcept', () => {
12
+ it('reads id from conceptId', () => {
13
+ const c = toDiffableConcept({ conceptId: '1.2.3' });
14
+ expect(c.id).toBe('1.2.3');
15
+ expect(c.termid).toBe('1.2.3');
16
+ });
17
+
18
+ it('falls back to id', () => {
19
+ const c = toDiffableConcept({ id: 'abc' });
20
+ expect(c.id).toBe('abc');
21
+ });
22
+
23
+ it('collects languages from localizations', () => {
24
+ const c = toDiffableConcept({
25
+ id: 'x',
26
+ localizations: { eng: {}, fra: {} },
27
+ });
28
+ expect(c.languages).toEqual(['eng', 'fra']);
29
+ });
30
+
31
+ it('returns null from localization() for unknown lang', () => {
32
+ const c = toDiffableConcept({ id: 'x', localizations: { eng: {} } });
33
+ expect(c.localization('deu')).toBeNull();
34
+ });
35
+
36
+ it('localization() returns terms/definitions/notes/examples with defaults', () => {
37
+ const c = toDiffableConcept({
38
+ id: 'x',
39
+ localizations: { eng: { terms: ['t'] } },
40
+ });
41
+ const lc = c.localization('eng');
42
+ expect(lc).not.toBeNull();
43
+ expect(lc?.terms).toEqual(['t']);
44
+ expect(lc?.definitions).toEqual([]);
45
+ expect(lc?.languageCode).toBe('eng');
46
+ });
47
+ });
48
+
49
+ describe('deriveDiffSections', () => {
50
+ it('returns empty for null', () => {
51
+ expect(deriveDiffSections(null, 'eng')).toEqual({});
52
+ });
53
+
54
+ it('returns empty when no localization for lang', () => {
55
+ const diff = {
56
+ localization: () => null,
57
+ localizations: {},
58
+ } as unknown as ConceptDiff;
59
+ expect(deriveDiffSections(diff, 'eng')).toEqual({});
60
+ });
61
+
62
+ it('extracts added definition', () => {
63
+ const diff = {
64
+ localization: () => ({
65
+ definitions: { added: [{ value: { content: 'new def' } }] },
66
+ }),
67
+ } as unknown as ConceptDiff;
68
+ const s = deriveDiffSections(diff, 'eng');
69
+ expect(s.definition?.type).toBe('added');
70
+ expect(s.definition?.value).toBe('new def');
71
+ });
72
+
73
+ it('extracts removed definition', () => {
74
+ const diff = {
75
+ localization: () => ({
76
+ definitions: { removed: [{ value: { content: 'old def' } }] },
77
+ }),
78
+ } as unknown as ConceptDiff;
79
+ const s = deriveDiffSections(diff, 'eng');
80
+ expect(s.definition?.type).toBe('removed');
81
+ expect(s.definition?.value).toBe('old def');
82
+ });
83
+
84
+ it('extracts changed definition hunks', () => {
85
+ const hunks = [
86
+ { type: 'equal', text: 'a' },
87
+ { type: 'added', text: 'b' },
88
+ ];
89
+ const diff = {
90
+ localization: () => ({
91
+ definitions: { changed: [{ textDiff: { hunks } }] },
92
+ }),
93
+ } as unknown as ConceptDiff;
94
+ const s = deriveDiffSections(diff, 'eng');
95
+ expect(s.definition?.hunks).toEqual(hunks);
96
+ });
97
+
98
+ it('extracts designations (added + removed + changed)', () => {
99
+ const diff = {
100
+ localization: () => ({
101
+ designations: {
102
+ added: [{ value: { designation: 'new' } }],
103
+ removed: [{ value: { designation: 'old' } }],
104
+ changed: [{ oldValue: { designation: 'a' }, newValue: { designation: 'b' } }],
105
+ },
106
+ }),
107
+ } as unknown as ConceptDiff;
108
+ const s = deriveDiffSections(diff, 'eng');
109
+ expect(s.designations?.items).toEqual([
110
+ { type: 'added', text: 'new' },
111
+ { type: 'removed', text: 'old' },
112
+ { type: 'removed', text: 'a' },
113
+ { type: 'added', text: 'b' },
114
+ ]);
115
+ });
116
+
117
+ it('extracts notes and examples', () => {
118
+ const diff = {
119
+ localization: () => ({
120
+ notes: { added: [{ value: { content: 'n1' } }] },
121
+ examples: { removed: [{ value: { content: 'e1' } }] },
122
+ }),
123
+ } as unknown as ConceptDiff;
124
+ const s = deriveDiffSections(diff, 'eng');
125
+ expect(s.notes?.items).toEqual([{ type: 'added', text: 'n1' }]);
126
+ expect(s.examples?.items).toEqual([{ type: 'removed', text: 'e1' }]);
127
+ });
128
+
129
+ it('prefers changed hunks over added/removed when both present', () => {
130
+ const diff = {
131
+ localization: () => ({
132
+ definitions: {
133
+ changed: [{ textDiff: { hunks: [{ type: 'equal', text: 'x' }] } }],
134
+ added: [{ value: 'should-be-ignored' }],
135
+ },
136
+ }),
137
+ } as unknown as ConceptDiff;
138
+ const s = deriveDiffSections(diff, 'eng');
139
+ expect(s.definition?.hunks).toHaveLength(1);
140
+ expect(s.definition?.type).toBeUndefined();
141
+ });
142
+ });
143
+
144
+ describe('computeSimilarity', () => {
145
+ it('returns null for null diff', () => {
146
+ expect(computeSimilarity(null)).toBeNull();
147
+ });
148
+
149
+ it('returns null when no stats', () => {
150
+ const diff = {} as ConceptDiff;
151
+ expect(computeSimilarity(diff)).toBeNull();
152
+ });
153
+
154
+ it('returns null when total is 0', () => {
155
+ const diff = { stats: { added: 0, removed: 0, changed: 0, total: 0 } } as unknown as ConceptDiff;
156
+ expect(computeSimilarity(diff)).toBeNull();
157
+ });
158
+
159
+ it('returns 1.0 when nothing changed', () => {
160
+ const diff = { stats: { added: 0, removed: 0, changed: 0, total: 10 } } as unknown as ConceptDiff;
161
+ expect(computeSimilarity(diff)).toBe(1);
162
+ });
163
+
164
+ it('returns 0 when everything changed', () => {
165
+ const diff = { stats: { added: 5, removed: 5, changed: 0, total: 10 } } as unknown as ConceptDiff;
166
+ expect(computeSimilarity(diff)).toBe(0);
167
+ });
168
+
169
+ it('computes partial similarity', () => {
170
+ const diff = { stats: { added: 1, removed: 1, changed: 0, total: 10 } } as unknown as ConceptDiff;
171
+ expect(computeSimilarity(diff)).toBe(0.8);
172
+ });
173
+
174
+ it('clamps to [0, 1]', () => {
175
+ const over = { stats: { added: 100, removed: 100, changed: 100, total: 10 } } as unknown as ConceptDiff;
176
+ expect(computeSimilarity(over)).toBe(0);
177
+ });
178
+ });
179
+
180
+ describe('useConceptDiff composable', () => {
181
+ it('starts empty', () => {
182
+ const { diffResult, loading, error } = useConceptDiff();
183
+ expect(diffResult.value).toBeNull();
184
+ expect(loading.value).toBe(false);
185
+ expect(error.value).toBeNull();
186
+ });
187
+
188
+ it('clear() resets state', () => {
189
+ const { diffResult, error, clear } = useConceptDiff();
190
+ diffResult.value = {} as ConceptDiff;
191
+ error.value = 'oops';
192
+ clear();
193
+ expect(diffResult.value).toBeNull();
194
+ expect(error.value).toBeNull();
195
+ });
196
+
197
+ it('diff() sets a ConceptDiff for two real concepts', () => {
198
+ const { diffResult, diff } = useConceptDiff();
199
+ const oldData = {
200
+ id: '1',
201
+ localizations: {
202
+ eng: { terms: [{ designation: 'old term', type: 'expression', normative_status: 'preferred' }] },
203
+ },
204
+ };
205
+ const newData = {
206
+ id: '1',
207
+ localizations: {
208
+ eng: { terms: [{ designation: 'new term', type: 'expression', normative_status: 'preferred' }] },
209
+ },
210
+ };
211
+ diff(oldData, newData);
212
+ expect(diffResult.value).not.toBeNull();
213
+ expect(diffResult.value?.oldId).toBe('1');
214
+ expect(diffResult.value?.newId).toBe('1');
215
+ });
216
+
217
+ it('diff() against identical concepts reports hasChanges=false', () => {
218
+ const { diffResult, diff } = useConceptDiff();
219
+ const data = {
220
+ id: '1',
221
+ localizations: {
222
+ eng: { terms: [{ designation: 'same', type: 'expression', normative_status: 'preferred' }] },
223
+ },
224
+ };
225
+ diff(data, data);
226
+ expect(diffResult.value?.hasChanges).toBe(false);
227
+ });
228
+
229
+ it('diff() catches errors and exposes them', () => {
230
+ const { error, diffResult, diff } = useConceptDiff();
231
+ // A concept whose `localization` throws — simulates malformed input
232
+ const bad = {
233
+ get localizations() { throw new Error('boom'); },
234
+ } as any;
235
+ diff(bad, bad);
236
+ expect(error.value).toBeTruthy();
237
+ expect(diffResult.value).toBeNull();
238
+ });
239
+ });
240
+ });
@@ -0,0 +1,132 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import type { PartitiveRelation, PartitiveMember, TypeSharedPlurality, ConceptRef } from 'glossarist';
3
+ import {
4
+ PartitiveRelation as PartitiveRelationModel,
5
+ PartitiveMember as PartitiveMemberModel,
6
+ TypeSharedPlurality as TypeSharedPluralityModel,
7
+ ConceptRef as ConceptRefModel,
8
+ } from 'glossarist/models';
9
+
10
+ /**
11
+ * Verifies glossarist-js's native v2 PartitiveRelation model.
12
+ *
13
+ * The concept-browser now consumes this directly (no `as any`
14
+ * bridge). These specs lock in the contract so an upstream
15
+ * regression is caught here.
16
+ */
17
+
18
+ function makeRef(source: string, id: string): ConceptRef {
19
+ return ConceptRefModel.fromJSON({ source, id }) as ConceptRef;
20
+ }
21
+
22
+ function makePartitive(source: string, id: string, certainty?: 'confirmed' | 'possible'): PartitiveMember {
23
+ const data: { ref: ConceptRef; certainty?: 'confirmed' | 'possible' } = { ref: makeRef(source, id) };
24
+ if (certainty) data.certainty = certainty;
25
+ return PartitiveMemberModel.fromJSON(data as unknown as Record<string, unknown>) as PartitiveMember;
26
+ }
27
+
28
+ function makePlurality(isShared: boolean, isUncertain = false): TypeSharedPlurality {
29
+ return TypeSharedPluralityModel.fromJSON({ isShared, isUncertain }) as TypeSharedPlurality;
30
+ }
31
+
32
+ function makeRelation(opts: {
33
+ comprehensive: ConceptRef;
34
+ partitives: PartitiveMember[];
35
+ completeness?: 'complete' | 'partial';
36
+ plurality?: TypeSharedPlurality | null;
37
+ criterion?: Record<string, string>;
38
+ }): PartitiveRelation {
39
+ return PartitiveRelationModel.fromJSON(opts as unknown as Record<string, unknown>) as PartitiveRelation;
40
+ }
41
+
42
+ describe('native v2 PartitiveRelation model (glossarist 0.4.20)', () => {
43
+ it('PartitiveMember.certainty defaults to confirmed when omitted', () => {
44
+ const m = makePartitive('test', '1.4');
45
+ expect(m.certainty).toBe('confirmed');
46
+ });
47
+
48
+ it('PartitiveMember.certainty is preserved when set to possible', () => {
49
+ const m = makePartitive('test', '1.4', 'possible');
50
+ expect(m.certainty).toBe('possible');
51
+ expect(m.isPossible).toBe(true);
52
+ });
53
+
54
+ it('PartitiveMember.ref exposes the typed ConceptRef', () => {
55
+ const m = makePartitive('test', '1.4');
56
+ expect(m.ref.source).toBe('test');
57
+ expect(m.ref.id).toBe('1.4');
58
+ });
59
+
60
+ it('PartitiveRelation has completeness default of complete', () => {
61
+ const r = makeRelation({
62
+ comprehensive: makeRef('test', '1.3'),
63
+ partitives: [makePartitive('test', '1.4'), makePartitive('test', '1.5')],
64
+ });
65
+ expect(r.completeness).toBe('complete');
66
+ expect(r.isComplete).toBe(true);
67
+ });
68
+
69
+ it('PartitiveRelation preserves explicit partial completeness', () => {
70
+ const r = makeRelation({
71
+ comprehensive: makeRef('test', '1.3'),
72
+ partitives: [makePartitive('test', '1.4'), makePartitive('test', '1.5')],
73
+ completeness: 'partial',
74
+ });
75
+ expect(r.completeness).toBe('partial');
76
+ expect(r.isPartial).toBe(true);
77
+ });
78
+
79
+ it('PartitiveRelation accepts plurality', () => {
80
+ const r = makeRelation({
81
+ comprehensive: makeRef('test', '1.3'),
82
+ partitives: [makePartitive('test', '1.4'), makePartitive('test', '1.5')],
83
+ plurality: makePlurality(true, true),
84
+ });
85
+ expect(r.hasPlurality()).toBe(true);
86
+ expect(r.plurality?.isShared).toBe(true);
87
+ expect(r.plurality?.isUncertain).toBe(true);
88
+ });
89
+
90
+ it('PartitiveRelation accepts criterion as localized hash', () => {
91
+ const r = makeRelation({
92
+ comprehensive: makeRef('test', '1.3'),
93
+ partitives: [makePartitive('test', '1.4'), makePartitive('test', '1.5')],
94
+ criterion: { eng: 'physical structure', fra: 'structure physique' },
95
+ });
96
+ expect(r.hasCriterion()).toBe(true);
97
+ expect(r.criterion?.eng).toBe('physical structure');
98
+ });
99
+
100
+ it('rejects < 2 partitives (ISO 704 requirement)', () => {
101
+ expect(() => makeRelation({
102
+ comprehensive: makeRef('test', '1.3'),
103
+ partitives: [makePartitive('test', '1.4')],
104
+ })).toThrow(/≥2 partitives/);
105
+ });
106
+
107
+ it('rejects self-loops', () => {
108
+ expect(() => makeRelation({
109
+ comprehensive: makeRef('test', '1.3'),
110
+ partitives: [
111
+ makePartitive('test', '1.3'),
112
+ makePartitive('test', '1.4'),
113
+ ],
114
+ })).toThrow(/self-loop/);
115
+ });
116
+
117
+ it('round-trips through toJSON with v2 shape', () => {
118
+ const r = makeRelation({
119
+ comprehensive: makeRef('test', '1.3'),
120
+ partitives: [makePartitive('test', '1.4'), makePartitive('test', '1.5', 'possible')],
121
+ completeness: 'partial',
122
+ plurality: makePlurality(true),
123
+ criterion: { eng: 'criterion text' },
124
+ });
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
+ });
132
+ });
@@ -0,0 +1,96 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { ref, nextTick } from 'vue';
3
+ import { useDatasetStats } from '../../composables/use-dataset-stats';
4
+
5
+ const okResponse = (body: unknown) => ({
6
+ ok: true,
7
+ json: async () => body,
8
+ });
9
+ const notFoundResponse = () => ({ ok: false, json: async () => null });
10
+
11
+ describe('useDatasetStats', () => {
12
+ let originalFetch: typeof globalThis.fetch;
13
+
14
+ beforeEach(() => {
15
+ originalFetch = globalThis.fetch;
16
+ });
17
+
18
+ afterEach(() => {
19
+ globalThis.fetch = originalFetch;
20
+ vi.restoreAllMocks();
21
+ });
22
+
23
+ it('starts with null stats', async () => {
24
+ vi.spyOn(globalThis, 'fetch').mockResolvedValue(okResponse(null) as any);
25
+ const { stats, loading } = useDatasetStats(() => 'reg-1', '/');
26
+ expect(stats.value).toBeNull();
27
+ await nextTick();
28
+ await nextTick();
29
+ await nextTick();
30
+ expect(loading.value).toBe(false);
31
+ });
32
+
33
+ it('loads stats.json for the resolved registerId', async () => {
34
+ const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
35
+ okResponse({
36
+ sourceCount: 5,
37
+ sources: [],
38
+ relationshipCount: 12,
39
+ relationshipTypes: { supersedes: 3 },
40
+ }) as any,
41
+ );
42
+ const { stats } = useDatasetStats(() => 'reg-1', '/');
43
+ await nextTick();
44
+ await nextTick();
45
+ expect(fetchMock).toHaveBeenCalledWith('/data/reg-1/stats.json');
46
+ expect(stats.value?.sourceCount).toBe(5);
47
+ expect(stats.value?.relationshipCount).toBe(12);
48
+ });
49
+
50
+ it('handles 404 gracefully (stats stays null)', async () => {
51
+ vi.spyOn(globalThis, 'fetch').mockResolvedValue(notFoundResponse() as any);
52
+ const { stats, error } = useDatasetStats(() => 'reg-1', '/');
53
+ await nextTick();
54
+ await nextTick();
55
+ expect(stats.value).toBeNull();
56
+ expect(error.value).toBeNull();
57
+ });
58
+
59
+ it('handles network errors and exposes error message', async () => {
60
+ vi.spyOn(globalThis, 'fetch').mockRejectedValue(new Error('network down'));
61
+ const { stats, error } = useDatasetStats(() => 'reg-1', '/');
62
+ await nextTick();
63
+ await nextTick();
64
+ await nextTick();
65
+ expect(stats.value).toBeNull();
66
+ expect(error.value).toBe('network down');
67
+ });
68
+
69
+ it('reacts to registerId change (Ref form)', async () => {
70
+ const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
71
+ okResponse({ sourceCount: 1, sources: [], relationshipCount: 0, relationshipTypes: {} }) as any,
72
+ );
73
+ const registerId = ref('reg-a');
74
+ useDatasetStats(registerId, '/');
75
+ await nextTick();
76
+ expect(fetchMock).toHaveBeenLastCalledWith('/data/reg-a/stats.json');
77
+
78
+ registerId.value = 'reg-b';
79
+ await nextTick();
80
+ await nextTick();
81
+ expect(fetchMock).toHaveBeenLastCalledWith('/data/reg-b/stats.json');
82
+ });
83
+
84
+ it('reload() re-fetches the same id', async () => {
85
+ const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
86
+ okResponse({ sourceCount: 7, sources: [], relationshipCount: 0, relationshipTypes: {} }) as any,
87
+ );
88
+ const { reload } = useDatasetStats(() => 'reg-1', '/');
89
+ await nextTick();
90
+ await nextTick();
91
+ expect(fetchMock).toHaveBeenCalledTimes(1);
92
+ reload();
93
+ await nextTick();
94
+ expect(fetchMock).toHaveBeenCalledTimes(2);
95
+ });
96
+ });
@@ -0,0 +1,82 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ PAGE_TYPES,
4
+ pageTypeOf,
5
+ synthesizePages,
6
+ } from '../../config/page-types';
7
+
8
+ describe('page-types', () => {
9
+ describe('PAGE_TYPES registry', () => {
10
+ it('includes home (global index)', () => {
11
+ expect(pageTypeOf('home')?.scope).toBe('global');
12
+ expect(pageTypeOf('home')?.route).toBe('');
13
+ });
14
+
15
+ it('includes concepts (dataset index)', () => {
16
+ expect(pageTypeOf('concepts')?.scope).toBe('dataset');
17
+ expect(pageTypeOf('concepts')?.route).toBe('');
18
+ });
19
+
20
+ it('includes sources (always synthesized for datasets)', () => {
21
+ const sources = pageTypeOf('sources');
22
+ expect(sources?.scope).toBe('dataset');
23
+ expect(sources?.featureFlag).toBeUndefined();
24
+ expect(sources?.autoSynthesize).toBe(true);
25
+ });
26
+
27
+ it('each entry has unique type', () => {
28
+ const types = PAGE_TYPES.map(p => p.type);
29
+ expect(new Set(types).size).toBe(types.length);
30
+ });
31
+
32
+ it('each entry has unique (scope, route) for autoSynthesized', () => {
33
+ const auto = PAGE_TYPES.filter(p => p.autoSynthesize);
34
+ const keys = auto.map(p => `${p.scope}:${p.route}`);
35
+ expect(new Set(keys).size).toBe(keys.length);
36
+ });
37
+ });
38
+
39
+ describe('synthesizePages', () => {
40
+ it('synthesizes global pages respecting default-on features', () => {
41
+ const pages = synthesizePages('global', {}, new Set());
42
+ const routes = pages.map(p => p.route);
43
+ expect(routes).toContain('search');
44
+ expect(routes).toContain('graph');
45
+ expect(routes).toContain('ontology');
46
+ expect(routes).not.toContain('news');
47
+ });
48
+
49
+ it('synthesizes news only when feature flag is truthy', () => {
50
+ const without = synthesizePages('global', {}, new Set());
51
+ const withFlag = synthesizePages('global', { news: true }, new Set());
52
+ expect(without.find(p => p.type === 'news')).toBeUndefined();
53
+ expect(withFlag.find(p => p.type === 'news')).toBeDefined();
54
+ });
55
+
56
+ it('respects feature off (search=false)', () => {
57
+ const pages = synthesizePages('global', { search: false }, new Set());
58
+ expect(pages.find(p => p.type === 'search')).toBeUndefined();
59
+ });
60
+
61
+ it('synthesizes dataset pages', () => {
62
+ const pages = synthesizePages('dataset', {}, new Set());
63
+ const types = pages.map(p => p.type);
64
+ expect(types).toContain('concepts');
65
+ expect(types).toContain('stats');
66
+ expect(types).toContain('sources');
67
+ expect(types).toContain('about');
68
+ });
69
+
70
+ it('skips routes already declared', () => {
71
+ const pages = synthesizePages('dataset', {}, new Set(['stats']));
72
+ expect(pages.find(p => p.type === 'stats')).toBeUndefined();
73
+ });
74
+
75
+ it('does not cross-contaminate scopes', () => {
76
+ const global = synthesizePages('global', {}, new Set());
77
+ const dataset = synthesizePages('dataset', {}, new Set());
78
+ expect(global.find(p => p.scope === 'dataset')).toBeUndefined();
79
+ expect(dataset.find(p => p.scope === 'global')).toBeUndefined();
80
+ });
81
+ });
82
+ });
@@ -135,8 +135,9 @@ describe('model-bridge — citation bridge round-trip', () => {
135
135
  const lc = concept.localization('eng')!;
136
136
  const rc = lc.related[0];
137
137
 
138
- // All fields preserved through the bridge
139
- expect(rc.content).toBe('entity');
138
+ // All fields preserved through the bridge (glossarist 0.4.20+
139
+ // normalizes content to a localized hash)
140
+ expect(rc.content).toEqual({ default: 'entity' });
140
141
  expect(getRelatedSourceId(rc)).toBe('vim-2.2');
141
142
 
142
143
  const citation = getRelatedCitation(rc)!;
@@ -60,7 +60,7 @@ describe('model-bridge — reference citation mapping via WeakMap bridges', () =
60
60
  const lc = concept.localization('eng')!;
61
61
  const related = lc.related;
62
62
  expect(related).toHaveLength(1);
63
- expect(related[0].content).toBe('entity data type');
63
+ expect(related[0].content).toEqual({ default: 'entity data type' });
64
64
  expect(getRelatedSourceId(related[0])).toBe('iso-10303-2-def');
65
65
  });
66
66
 
@@ -3,12 +3,12 @@ import { ontology } from '../adapters/ontology-registry';
3
3
 
4
4
  describe('OntologyRegistry', () => {
5
5
  it('loads all 10 taxonomies', () => {
6
- expect(ontology.getAll('conceptStatus').length).toBe(7);
6
+ expect(ontology.getAll('conceptStatus').length).toBe(8);
7
7
  expect(ontology.getAll('entryStatus').length).toBe(4);
8
8
  expect(ontology.getAll('normativeStatus').length).toBe(4);
9
9
  expect(ontology.getAll('sourceType').length).toBe(2);
10
10
  expect(ontology.getAll('sourceStatus').length).toBe(10);
11
- expect(ontology.getAll('relationshipType').length).toBe(52);
11
+ expect(ontology.getAll('relationshipType').length).toBe(54);
12
12
  expect(ontology.getAll('designationType').length).toBe(5);
13
13
  expect(ontology.getAll('termType').length).toBe(24);
14
14
  expect(ontology.getAll('grammarGender').length).toBe(4);
@@ -0,0 +1,82 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ partitiveRelationStyle,
4
+ pluralityColor,
5
+ completenessLabel,
6
+ certaintyLabel,
7
+ } from '../utils/partitive-relation-styling';
8
+ import type { TypeSharedPluralityWire } from '../adapters/types';
9
+
10
+ describe('partitive-relation-styling', () => {
11
+ describe('partitiveRelationStyle', () => {
12
+ it('returns full opacity for complete', () => {
13
+ expect(partitiveRelationStyle('complete', null, false).opacity).toBe(1.0);
14
+ });
15
+
16
+ it('returns reduced opacity for partial', () => {
17
+ expect(partitiveRelationStyle('partial', null, false).opacity).toBe(0.6);
18
+ });
19
+
20
+ it('uses teal badge class for complete with no plurality', () => {
21
+ expect(partitiveRelationStyle('complete', null, false).badgeClass).toBe('badge-teal');
22
+ });
23
+
24
+ it('uses blue badge class for isShared plurality', () => {
25
+ const p: TypeSharedPluralityWire = { isShared: true, isUncertain: false };
26
+ expect(partitiveRelationStyle('complete', p, false).badgeClass).toBe('badge-blue');
27
+ });
28
+
29
+ it('uses yellow badge class when isShared + isUncertain', () => {
30
+ const p: TypeSharedPluralityWire = { isShared: true, isUncertain: true };
31
+ expect(partitiveRelationStyle('complete', p, false).badgeClass).toBe('badge-yellow');
32
+ });
33
+
34
+ it('returns different colors in dark mode', () => {
35
+ const light = partitiveRelationStyle('complete', null, false);
36
+ const dark = partitiveRelationStyle('complete', null, true);
37
+ expect(light.color).not.toBe(dark.color);
38
+ });
39
+
40
+ it('ignores plurality when isShared is false', () => {
41
+ const p: TypeSharedPluralityWire = { isShared: false, isUncertain: true };
42
+ expect(partitiveRelationStyle('complete', p, false).badgeClass).toBe('badge-teal');
43
+ });
44
+ });
45
+
46
+ describe('pluralityColor', () => {
47
+ it('returns blue for isShared', () => {
48
+ const p: TypeSharedPluralityWire = { isShared: true, isUncertain: false };
49
+ expect(pluralityColor(p, false)).toBe('#3b82f6');
50
+ });
51
+
52
+ it('returns amber when isUncertain', () => {
53
+ const p: TypeSharedPluralityWire = { isShared: true, isUncertain: true };
54
+ expect(pluralityColor(p, false)).toBe('#f59e0b');
55
+ });
56
+
57
+ it('returns dark-mode variants', () => {
58
+ expect(pluralityColor({ isShared: true, isUncertain: false }, true)).toBe('#60a5fa');
59
+ expect(pluralityColor({ isShared: true, isUncertain: true }, true)).toBe('#fbbf24');
60
+ });
61
+ });
62
+
63
+ describe('completenessLabel', () => {
64
+ it('labels complete', () => {
65
+ expect(completenessLabel('complete')).toBe('Complete');
66
+ });
67
+
68
+ it('labels partial', () => {
69
+ expect(completenessLabel('partial')).toBe('Partial');
70
+ });
71
+ });
72
+
73
+ describe('certaintyLabel', () => {
74
+ it('labels confirmed', () => {
75
+ expect(certaintyLabel('confirmed')).toBe('Confirmed');
76
+ });
77
+
78
+ it('labels possible', () => {
79
+ expect(certaintyLabel('possible')).toBe('Possible');
80
+ });
81
+ });
82
+ });