@glossarist/concept-browser 0.7.77 → 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.
- package/astro.config.mjs +1 -0
- package/package.json +2 -2
- package/scripts/bridge-to-astro.mjs +6 -1
- package/scripts/build-edges.js +154 -1
- package/scripts/generate-data.mjs +138 -54
- package/src/__tests__/build-edges-redundancy.test.ts +101 -0
- package/src/__tests__/composables/use-concept-diff.test.ts +240 -0
- package/src/__tests__/composables/use-concept-edges-v2.test.ts +132 -0
- package/src/__tests__/composables/use-dataset-stats.test.ts +96 -0
- package/src/__tests__/config/page-types.test.ts +82 -0
- package/src/__tests__/model-bridge-bridges.test.ts +3 -2
- package/src/__tests__/model-bridge-citation.test.ts +1 -1
- package/src/__tests__/ontology-registry.test.ts +2 -2
- package/src/__tests__/partitive-relation-styling.test.ts +82 -0
- package/src/__tests__/partitive-relation.test.ts +285 -0
- package/src/__tests__/sphere-math.test.ts +56 -0
- package/src/adapters/GraphDataSource.ts +22 -5
- package/src/adapters/UriRouter.ts +5 -0
- package/src/adapters/dataset-stats.ts +13 -0
- package/src/adapters/model-bridge.ts +1 -1
- package/src/adapters/non-verbal/glossarist-augment.d.ts +155 -4
- package/src/adapters/types.ts +36 -0
- package/src/components/AppSidebar.vue +14 -57
- package/src/components/ConceptDetail.vue +29 -0
- package/src/components/ConceptDiffModal.vue +120 -0
- package/src/components/ConceptDiffView.vue +192 -0
- package/src/components/ConceptEditionRail.vue +61 -36
- package/src/components/PartitiveRelationList.vue +139 -0
- package/src/components/RelationSphere.vue +2 -36
- package/src/components/groups/DatasetGroupRenderer.vue +14 -4
- package/src/components/groups/DefaultGroupSidebar.vue +28 -11
- package/src/components/groups/LineageGroupSidebar.vue +17 -29
- package/src/composables/use-concept-diff.ts +231 -0
- package/src/composables/use-concept-edges.ts +98 -5
- package/src/composables/use-dataset-stats.ts +52 -0
- package/src/config/page-types.ts +67 -0
- package/src/config/types.ts +6 -1
- package/src/config/use-site-config.ts +13 -29
- package/src/data/taxonomies.json +24 -1
- package/src/i18n/locales/eng.yml +6 -0
- package/src/utils/partitive-relation-styling.ts +62 -0
- package/src/utils/sphere-math.ts +54 -0
- package/src/views/DatasetView.vue +5 -3
- package/src/views/StatsView.vue +9 -22
- package/src/islands/BibliographyIsland.vue +0 -26
- package/src/islands/ConceptDetailIsland.vue +0 -27
- package/src/islands/ConceptDiffIsland.vue +0 -214
- package/src/islands/ConceptIsland.vue +0 -102
- package/src/islands/GraphIsland.vue +0 -15
- package/src/islands/GroupRendererIsland.vue +0 -15
- package/src/islands/HeaderIsland.vue +0 -10
- package/src/islands/NonVerbalIsland.vue +0 -75
- package/src/islands/RelationSphereIsland.vue +0 -20
- package/src/islands/SearchIsland.vue +0 -23
- package/src/islands/SeriesCardIsland.vue +0 -12
- package/src/islands/ShortcutsIsland.vue +0 -13
- package/src/islands/SidebarIsland.vue +0 -15
- package/src/islands/SupersessionDiffIsland.vue +0 -267
|
@@ -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(
|
|
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(
|
|
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
|
+
});
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import type { PartitiveRelationWire } from '../adapters/types';
|
|
3
|
+
|
|
4
|
+
// Re-implements the extractor under test so we can test it in isolation
|
|
5
|
+
// without spinning up the full build-edges.js pipeline. The production
|
|
6
|
+
// extractor in scripts/build-edges.js uses the same logic.
|
|
7
|
+
function extractPartitiveRelations(
|
|
8
|
+
concept: { '@id': string; 'gl:partitiveRelations'?: any[] },
|
|
9
|
+
registerId: string,
|
|
10
|
+
uriBase: string,
|
|
11
|
+
urnMap: Map<string, string>,
|
|
12
|
+
): PartitiveRelationWire[] {
|
|
13
|
+
const relations: PartitiveRelationWire[] = [];
|
|
14
|
+
const sourceUri = concept['@id'];
|
|
15
|
+
|
|
16
|
+
const resolveConceptUri = (ref: any): string | null => {
|
|
17
|
+
const source = ref?.['gl:source'] || ref?.source;
|
|
18
|
+
const id = ref?.['gl:id'] || ref?.id;
|
|
19
|
+
if (!source || !id) return null;
|
|
20
|
+
const reg = urnMap.get(source) || source;
|
|
21
|
+
return `${uriBase}/${reg}/concept/${id}`;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const resolvePlurality = (plurality: any) => {
|
|
25
|
+
if (!plurality) return null;
|
|
26
|
+
const isShared = plurality['gl:isShared'] ?? plurality.is_shared;
|
|
27
|
+
if (typeof isShared !== 'boolean') return null;
|
|
28
|
+
const isUncertain = plurality['gl:isUncertain'] ?? plurality.is_uncertain ?? false;
|
|
29
|
+
const sharedTypeRef = plurality['gl:sharedType'] ?? plurality.shared_type;
|
|
30
|
+
let sharedType = null;
|
|
31
|
+
if (sharedTypeRef) {
|
|
32
|
+
const s = sharedTypeRef['gl:source'] ?? sharedTypeRef.source;
|
|
33
|
+
const i = sharedTypeRef['gl:id'] ?? sharedTypeRef.id;
|
|
34
|
+
if (s && i) sharedType = `${s}:${i}`;
|
|
35
|
+
}
|
|
36
|
+
return { isShared, isUncertain, sharedType };
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
for (const rel of concept['gl:partitiveRelations'] || []) {
|
|
40
|
+
const comprehensive = resolveConceptUri(rel['gl:comprehensive']);
|
|
41
|
+
if (!comprehensive) continue;
|
|
42
|
+
const partitives = (rel['gl:hasPartitive'] || [])
|
|
43
|
+
.map((member: any) => {
|
|
44
|
+
const ref = member['gl:ref'] || member;
|
|
45
|
+
const uri = resolveConceptUri(ref);
|
|
46
|
+
if (!uri || uri === sourceUri) return null;
|
|
47
|
+
const certainty = member['gl:certainty'] || 'confirmed';
|
|
48
|
+
if (certainty !== 'confirmed' && certainty !== 'possible') {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Invalid partitive member certainty: "${certainty}". Allowed: confirmed, possible`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
return { uri, certainty };
|
|
54
|
+
})
|
|
55
|
+
.filter((p: any): p is { uri: string; certainty: 'confirmed' | 'possible' } => p !== null);
|
|
56
|
+
if (partitives.length === 0) continue;
|
|
57
|
+
const completeness = rel['gl:completeness'] || 'complete';
|
|
58
|
+
if (completeness !== 'complete' && completeness !== 'partial') {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`Invalid partitive relation completeness: "${completeness}". Allowed: complete, partial`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
relations.push({
|
|
64
|
+
source: sourceUri,
|
|
65
|
+
comprehensive,
|
|
66
|
+
partitives,
|
|
67
|
+
completeness,
|
|
68
|
+
plurality: resolvePlurality(rel['gl:hasPlurality']) as PartitiveRelationWire['plurality'],
|
|
69
|
+
criterion: rel['gl:criterion'],
|
|
70
|
+
register: registerId,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return relations;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
describe('extractPartitiveRelations', () => {
|
|
77
|
+
const uriBase = 'https://example.org';
|
|
78
|
+
const urnMap = new Map([['urn:vim:pub:v:2:2012', 'vim-2012']]);
|
|
79
|
+
const registerId = 'vim-2012';
|
|
80
|
+
const sourceUri = `${uriBase}/${registerId}/concept/2.9`;
|
|
81
|
+
|
|
82
|
+
it('extracts a complete relation with two confirmed members', () => {
|
|
83
|
+
const concept = {
|
|
84
|
+
'@id': sourceUri,
|
|
85
|
+
'gl:partitiveRelations': [
|
|
86
|
+
{
|
|
87
|
+
'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '2.9' },
|
|
88
|
+
'gl:hasPartitive': [
|
|
89
|
+
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '2.10' } },
|
|
90
|
+
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '2.26' } },
|
|
91
|
+
],
|
|
92
|
+
'gl:completeness': 'complete',
|
|
93
|
+
'gl:criterion': { eng: 'measurement result composition' },
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const result = extractPartitiveRelations(concept, registerId, uriBase, urnMap);
|
|
99
|
+
expect(result).toHaveLength(1);
|
|
100
|
+
expect(result[0].comprehensive).toBe(`${uriBase}/${registerId}/concept/2.9`);
|
|
101
|
+
expect(result[0].partitives).toEqual([
|
|
102
|
+
{ uri: `${uriBase}/${registerId}/concept/2.10`, certainty: 'confirmed' },
|
|
103
|
+
{ uri: `${uriBase}/${registerId}/concept/2.26`, certainty: 'confirmed' },
|
|
104
|
+
]);
|
|
105
|
+
expect(result[0].completeness).toBe('complete');
|
|
106
|
+
expect(result[0].criterion).toEqual({ eng: 'measurement result composition' });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('defaults completeness to complete when omitted', () => {
|
|
110
|
+
const concept = {
|
|
111
|
+
'@id': sourceUri,
|
|
112
|
+
'gl:partitiveRelations': [
|
|
113
|
+
{
|
|
114
|
+
'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '2.9' },
|
|
115
|
+
'gl:hasPartitive': [
|
|
116
|
+
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '2.10' } },
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
};
|
|
121
|
+
const [rel] = extractPartitiveRelations(concept, registerId, uriBase, urnMap);
|
|
122
|
+
expect(rel.completeness).toBe('complete');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('preserves partial completeness', () => {
|
|
126
|
+
const concept = {
|
|
127
|
+
'@id': sourceUri,
|
|
128
|
+
'gl:partitiveRelations': [
|
|
129
|
+
{
|
|
130
|
+
'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.3' },
|
|
131
|
+
'gl:hasPartitive': [
|
|
132
|
+
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.4' } },
|
|
133
|
+
],
|
|
134
|
+
'gl:completeness': 'partial',
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
};
|
|
138
|
+
const [rel] = extractPartitiveRelations(concept, registerId, uriBase, urnMap);
|
|
139
|
+
expect(rel.completeness).toBe('partial');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('resolves plurality block (isShared + sharedType)', () => {
|
|
143
|
+
const concept = {
|
|
144
|
+
'@id': sourceUri,
|
|
145
|
+
'gl:partitiveRelations': [
|
|
146
|
+
{
|
|
147
|
+
'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.3' },
|
|
148
|
+
'gl:hasPartitive': [
|
|
149
|
+
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.4' } },
|
|
150
|
+
],
|
|
151
|
+
'gl:hasPlurality': {
|
|
152
|
+
'gl:isShared': true,
|
|
153
|
+
'gl:isUncertain': false,
|
|
154
|
+
'gl:sharedType': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.99' },
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
};
|
|
159
|
+
const [rel] = extractPartitiveRelations(concept, registerId, uriBase, urnMap);
|
|
160
|
+
expect(rel.plurality).toEqual({
|
|
161
|
+
isShared: true,
|
|
162
|
+
isUncertain: false,
|
|
163
|
+
sharedType: 'urn:vim:pub:v:2:2012:1.99',
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('returns null plurality when isShared missing', () => {
|
|
168
|
+
const concept = {
|
|
169
|
+
'@id': sourceUri,
|
|
170
|
+
'gl:partitiveRelations': [
|
|
171
|
+
{
|
|
172
|
+
'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.3' },
|
|
173
|
+
'gl:hasPartitive': [
|
|
174
|
+
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.4' } },
|
|
175
|
+
],
|
|
176
|
+
'gl:hasPlurality': { 'gl:isUncertain': true },
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
};
|
|
180
|
+
const [rel] = extractPartitiveRelations(concept, registerId, uriBase, urnMap);
|
|
181
|
+
expect(rel.plurality).toBeNull();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('preserves per-member certainty', () => {
|
|
185
|
+
const concept = {
|
|
186
|
+
'@id': sourceUri,
|
|
187
|
+
'gl:partitiveRelations': [
|
|
188
|
+
{
|
|
189
|
+
'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.3' },
|
|
190
|
+
'gl:hasPartitive': [
|
|
191
|
+
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.4' } },
|
|
192
|
+
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.5' }, 'gl:certainty': 'possible' },
|
|
193
|
+
],
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
};
|
|
197
|
+
const [rel] = extractPartitiveRelations(concept, registerId, uriBase, urnMap);
|
|
198
|
+
expect(rel.partitives).toEqual([
|
|
199
|
+
{ uri: `${uriBase}/${registerId}/concept/1.4`, certainty: 'confirmed' },
|
|
200
|
+
{ uri: `${uriBase}/${registerId}/concept/1.5`, certainty: 'possible' },
|
|
201
|
+
]);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('throws on invalid completeness', () => {
|
|
205
|
+
const concept = {
|
|
206
|
+
'@id': sourceUri,
|
|
207
|
+
'gl:partitiveRelations': [
|
|
208
|
+
{
|
|
209
|
+
'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.3' },
|
|
210
|
+
'gl:hasPartitive': [
|
|
211
|
+
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.4' } },
|
|
212
|
+
],
|
|
213
|
+
'gl:completeness': 'closed',
|
|
214
|
+
},
|
|
215
|
+
],
|
|
216
|
+
};
|
|
217
|
+
expect(() => extractPartitiveRelations(concept, registerId, uriBase, urnMap)).toThrow(/Invalid.*completeness.*closed/);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('throws on invalid member certainty', () => {
|
|
221
|
+
const concept = {
|
|
222
|
+
'@id': sourceUri,
|
|
223
|
+
'gl:partitiveRelations': [
|
|
224
|
+
{
|
|
225
|
+
'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.3' },
|
|
226
|
+
'gl:hasPartitive': [
|
|
227
|
+
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.4' }, 'gl:certainty': 'maybe' },
|
|
228
|
+
],
|
|
229
|
+
},
|
|
230
|
+
],
|
|
231
|
+
};
|
|
232
|
+
expect(() => extractPartitiveRelations(concept, registerId, uriBase, urnMap)).toThrow(/Invalid.*certainty.*maybe/);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('skips a relation whose comprehensive cannot be resolved', () => {
|
|
236
|
+
const concept = {
|
|
237
|
+
'@id': sourceUri,
|
|
238
|
+
'gl:partitiveRelations': [
|
|
239
|
+
{
|
|
240
|
+
'gl:comprehensive': { 'gl:id': '1.3' },
|
|
241
|
+
'gl:hasPartitive': [
|
|
242
|
+
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '1.4' } },
|
|
243
|
+
],
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
};
|
|
247
|
+
expect(extractPartitiveRelations(concept, registerId, uriBase, urnMap)).toHaveLength(0);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('skips a relation with no resolvable partitives', () => {
|
|
251
|
+
const concept = {
|
|
252
|
+
'@id': sourceUri,
|
|
253
|
+
'gl:partitiveRelations': [
|
|
254
|
+
{
|
|
255
|
+
'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '2.9' },
|
|
256
|
+
'gl:hasPartitive': [{ 'gl:ref': { 'gl:id': '1.4' } }],
|
|
257
|
+
},
|
|
258
|
+
],
|
|
259
|
+
};
|
|
260
|
+
expect(extractPartitiveRelations(concept, registerId, uriBase, urnMap)).toHaveLength(0);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('excludes the source concept from partitives (self-loop guard)', () => {
|
|
264
|
+
const concept = {
|
|
265
|
+
'@id': sourceUri,
|
|
266
|
+
'gl:partitiveRelations': [
|
|
267
|
+
{
|
|
268
|
+
'gl:comprehensive': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '2.9' },
|
|
269
|
+
'gl:hasPartitive': [
|
|
270
|
+
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '2.10' } },
|
|
271
|
+
{ 'gl:ref': { 'gl:source': 'urn:vim:pub:v:2:2012', 'gl:id': '2.9' } },
|
|
272
|
+
],
|
|
273
|
+
},
|
|
274
|
+
],
|
|
275
|
+
};
|
|
276
|
+
const [rel] = extractPartitiveRelations(concept, registerId, uriBase, urnMap);
|
|
277
|
+
expect(rel.partitives).toHaveLength(1);
|
|
278
|
+
expect(rel.partitives[0].uri).toBe(`${uriBase}/${registerId}/concept/2.10`);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('returns empty array when concept has no partitiveRelations', () => {
|
|
282
|
+
const concept = { '@id': sourceUri };
|
|
283
|
+
expect(extractPartitiveRelations(concept, registerId, uriBase, urnMap)).toEqual([]);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { hashSeed, expandParams, portSide, portPoint, idToUriGet } from '../utils/sphere-math';
|
|
3
|
+
|
|
4
|
+
describe('sphere-math', () => {
|
|
5
|
+
describe('hashSeed', () => {
|
|
6
|
+
it('produces deterministic positive integers', () => {
|
|
7
|
+
expect(hashSeed('abc')).toBe(hashSeed('abc'));
|
|
8
|
+
expect(hashSeed('abc')).not.toBe(hashSeed('abd'));
|
|
9
|
+
expect(hashSeed('')).toBeGreaterThanOrEqual(0);
|
|
10
|
+
expect(hashSeed('test')).toBeGreaterThan(0);
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe('expandParams', () => {
|
|
15
|
+
it('clamps to [0, 10] and maps to physical constants', () => {
|
|
16
|
+
const min = expandParams(0);
|
|
17
|
+
const max = expandParams(10);
|
|
18
|
+
expect(min.linkDist).toBeLessThan(max.linkDist);
|
|
19
|
+
expect(min.repMin).toBeLessThan(max.repMin);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('clamps out-of-range values', () => {
|
|
23
|
+
expect(expandParams(-5)).toEqual(expandParams(0));
|
|
24
|
+
expect(expandParams(20)).toEqual(expandParams(10));
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('portSide', () => {
|
|
29
|
+
it('determines the dominant axis direction', () => {
|
|
30
|
+
expect(portSide({ x: 0, y: 0 }, { x: 10, y: 0 })).toBe('right');
|
|
31
|
+
expect(portSide({ x: 0, y: 0 }, { x: -10, y: 0 })).toBe('left');
|
|
32
|
+
expect(portSide({ x: 0, y: 0 }, { x: 0, y: 10 })).toBe('bottom');
|
|
33
|
+
expect(portSide({ x: 0, y: 0 }, { x: 0, y: -10 })).toBe('top');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('portPoint', () => {
|
|
38
|
+
it('computes the connection point on each side', () => {
|
|
39
|
+
const c = { x: 100, y: 100 };
|
|
40
|
+
const w = 220, h = 60;
|
|
41
|
+
const right = portPoint(c, 'right', w, h, 0);
|
|
42
|
+
expect(right.x).toBe(210);
|
|
43
|
+
expect(right.y).toBe(100);
|
|
44
|
+
const top = portPoint(c, 'top', w, h, 0);
|
|
45
|
+
expect(top.y).toBe(70);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('idToUriGet', () => {
|
|
50
|
+
it('reverse-looks-up a URI in an ID→URI map', () => {
|
|
51
|
+
const map = new Map([['a', '/uri/a'], ['b', '/uri/b']]);
|
|
52
|
+
expect(idToUriGet(map, '/uri/b')).toBe('b');
|
|
53
|
+
expect(idToUriGet(map, '/uri/unknown')).toBeUndefined();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { GraphEdge, GraphNode, SectionNode } from './types';
|
|
1
|
+
import type { GraphEdge, GraphNode, PartitiveRelationWire, SectionNode } from './types';
|
|
2
2
|
import type { Concept, RelatedConcept } from 'glossarist';
|
|
3
3
|
import type { DatasetAdapter } from './DatasetAdapter';
|
|
4
4
|
import { UriRouter } from './UriRouter';
|
|
@@ -30,6 +30,16 @@ function resolveRefTarget(rc: RelatedConcept, uriBase: string, registerId: strin
|
|
|
30
30
|
return ref.source || '';
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
function contentLabel(content: unknown): string | undefined {
|
|
34
|
+
if (content == null) return undefined;
|
|
35
|
+
if (typeof content === 'string') return content;
|
|
36
|
+
if (typeof content === 'object' && content !== null) {
|
|
37
|
+
const obj = content as Record<string, string>;
|
|
38
|
+
return obj.default ?? obj.eng ?? Object.values(obj)[0];
|
|
39
|
+
}
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
33
43
|
export class GraphDataSource {
|
|
34
44
|
constructor(private adapter: DatasetAdapter) {}
|
|
35
45
|
|
|
@@ -58,6 +68,13 @@ export class GraphDataSource {
|
|
|
58
68
|
return data.edges ?? [];
|
|
59
69
|
}
|
|
60
70
|
|
|
71
|
+
async loadPartitiveRelations(): Promise<PartitiveRelationWire[]> {
|
|
72
|
+
const resp = await fetch(`${this.baseUrl}/partitive_relations.json`);
|
|
73
|
+
if (!resp.ok) return [];
|
|
74
|
+
const data = await resp.json();
|
|
75
|
+
return data.relations ?? data.hyperedges ?? [];
|
|
76
|
+
}
|
|
77
|
+
|
|
61
78
|
async loadGraphNodes(): Promise<{ uriPrefix: string; nodes: [string, Record<string, string>, string][] }> {
|
|
62
79
|
const resp = await fetch(`${this.baseUrl}/graph-nodes.json`);
|
|
63
80
|
if (!resp.ok) return { uriPrefix: '', nodes: [] };
|
|
@@ -73,7 +90,7 @@ export class GraphDataSource {
|
|
|
73
90
|
|
|
74
91
|
extractEdges(concept: Concept): GraphEdge[] {
|
|
75
92
|
const edges: GraphEdge[] = [];
|
|
76
|
-
const sourceUri = concept.uri ||
|
|
93
|
+
const sourceUri = concept.uri || UriRouter.buildConceptUri(this.uriBase, this.registerId, concept.id);
|
|
77
94
|
|
|
78
95
|
for (const rc of concept.relatedConcepts) {
|
|
79
96
|
const target = resolveRefTarget(rc, this.uriBase, this.registerId, this.urnMap);
|
|
@@ -83,7 +100,7 @@ export class GraphDataSource {
|
|
|
83
100
|
source: sourceUri,
|
|
84
101
|
target,
|
|
85
102
|
type: rc.type || 'references',
|
|
86
|
-
label: rc.content
|
|
103
|
+
label: contentLabel(rc.content),
|
|
87
104
|
register: parsed?.registerId ?? this.registerId,
|
|
88
105
|
});
|
|
89
106
|
}
|
|
@@ -100,7 +117,7 @@ export class GraphDataSource {
|
|
|
100
117
|
source: sourceUri,
|
|
101
118
|
target,
|
|
102
119
|
type: rc.type || 'references',
|
|
103
|
-
label: rc.content
|
|
120
|
+
label: contentLabel(rc.content),
|
|
104
121
|
register: parsed?.registerId ?? this.registerId,
|
|
105
122
|
lang,
|
|
106
123
|
});
|
|
@@ -113,7 +130,7 @@ export class GraphDataSource {
|
|
|
113
130
|
|
|
114
131
|
extractDomainEdges(concept: Concept): GraphEdge[] {
|
|
115
132
|
const edges: GraphEdge[] = [];
|
|
116
|
-
const sourceUri = concept.uri ||
|
|
133
|
+
const sourceUri = concept.uri || UriRouter.buildConceptUri(this.uriBase, this.registerId, concept.id);
|
|
117
134
|
|
|
118
135
|
for (const lang of concept.languages) {
|
|
119
136
|
const lc = concept.localization(lang);
|
|
@@ -101,6 +101,11 @@ export class UriRouter {
|
|
|
101
101
|
return m ? { registerId: m[1], conceptId: m[2] } : null;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
/** Construct a canonical concept URI from components. */
|
|
105
|
+
static buildConceptUri(uriBase: string, registerId: string, conceptId: string): string {
|
|
106
|
+
return `${uriBase}/${registerId}/concept/${conceptId}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
104
109
|
/** Construct a canonical URI for a concept. */
|
|
105
110
|
buildUri(registerId: string, conceptId: string): string {
|
|
106
111
|
const info = this.registerMap.get(registerId);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dataset-level stats shape. The data pipeline emits this as
|
|
3
|
+
* `public/data/{registerId}/stats.json`. Consumers fetch via HTTP.
|
|
4
|
+
*
|
|
5
|
+
* Kept in adapters/types.ts (the SSOT for wire shapes) so all views
|
|
6
|
+
* that need stats share the same definition.
|
|
7
|
+
*/
|
|
8
|
+
export interface DatasetStats {
|
|
9
|
+
sourceCount: number;
|
|
10
|
+
sources: Array<{ ref: string; types: string[]; conceptCount: number }>;
|
|
11
|
+
relationshipCount: number;
|
|
12
|
+
relationshipTypes: Record<string, number>;
|
|
13
|
+
}
|
|
@@ -287,7 +287,7 @@ function attachBridges(concept: Concept, localizations: Record<string, unknown>)
|
|
|
287
287
|
* instances, since RelatedConcept.fromJSON only reads type/content/ref.
|
|
288
288
|
*/
|
|
289
289
|
function attachRelatedBridges(
|
|
290
|
-
modelRelated: Array<{ type?: string | null; content?: string | null; ref?: any; related?: any[] }>,
|
|
290
|
+
modelRelated: Array<{ type?: string | null; content?: Record<string, string> | string | null; ref?: any; related?: any[] }>,
|
|
291
291
|
rawRelated: unknown[],
|
|
292
292
|
): void {
|
|
293
293
|
for (const rawRel of rawRelated) {
|