@glossarist/concept-browser 0.7.27 → 0.7.28
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 +1 -1
- package/scripts/generate-data.mjs +20 -1
- package/src/__tests__/annotations-bridge.test.ts +87 -0
- package/src/__tests__/dataset-adapter.test.ts +58 -0
- package/src/__tests__/designation-relationship.test.ts +211 -0
- package/src/__tests__/graph.test.ts +15 -0
- package/src/__tests__/relationship-comprehensive.test.ts +162 -0
- package/src/__tests__/taxonomy-colors.test.ts +71 -0
- package/src/adapters/model-bridge.ts +251 -27
- package/src/adapters/ontology-registry.ts +30 -2
- package/src/components/ConceptDetail.vue +29 -61
- package/src/components/DesignationList.vue +78 -0
- package/src/data/ontology-schema.json +66 -8
- package/src/data/taxonomies.json +261 -89
- package/src/i18n/locales/eng.yml +2 -0
- package/src/i18n/locales/fra.yml +2 -0
- package/src/stores/vocabulary.ts +3 -5
- package/src/utils/concept-helpers.ts +2 -20
- package/src/utils/designation-registry.ts +3 -26
- package/src/utils/relationship-categories.ts +68 -122
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glossarist/concept-browser",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.28",
|
|
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": {
|
|
@@ -47,7 +47,7 @@ function loadConceptFile(filePath) {
|
|
|
47
47
|
const lcData = { ...(doc.data || {}) };
|
|
48
48
|
delete lcData.language_code;
|
|
49
49
|
// Merge top-level fields (terms, definition, notes, etc.) into lcData
|
|
50
|
-
for (const key of ['terms', 'definition', 'notes', 'examples', 'sources', 'dates', 'domain', 'references', 'entry_status', 'classification', 'review_type', 'review_date', 'review_decision_date', 'review_decision_event', 'review_status', 'review_decision', 'review_decision_notes', 'lineage_source_similarity', 'release', 'script', 'system']) {
|
|
50
|
+
for (const key of ['terms', 'definition', 'notes', 'annotations', 'examples', 'sources', 'dates', 'domain', 'references', 'entry_status', 'classification', 'review_type', 'review_date', 'review_decision_date', 'review_decision_event', 'review_status', 'review_decision', 'review_decision_notes', 'lineage_source_similarity', 'release', 'script', 'system']) {
|
|
51
51
|
if (doc[key] !== undefined && lcData[key] === undefined) {
|
|
52
52
|
lcData[key] = doc[key];
|
|
53
53
|
}
|
|
@@ -106,6 +106,23 @@ function termToDesignation(term) {
|
|
|
106
106
|
if (term.text) doc['gl:text'] = term.text;
|
|
107
107
|
if (term.image) doc['gl:image'] = term.image;
|
|
108
108
|
|
|
109
|
+
if (term.related && term.related.length > 0) {
|
|
110
|
+
doc['gl:related'] = term.related.map(r => {
|
|
111
|
+
const rel = {};
|
|
112
|
+
if (r.type) rel['gl:relationshipType'] = r.type;
|
|
113
|
+
if (r.target) {
|
|
114
|
+
rel['gl:target'] = r.target;
|
|
115
|
+
} else if (r.ref) {
|
|
116
|
+
const ref = { '@type': 'gl:ConceptRef' };
|
|
117
|
+
if (r.ref.source) ref['gl:source'] = r.ref.source;
|
|
118
|
+
if (r.ref.id) ref['gl:id'] = r.ref.id;
|
|
119
|
+
if (r.ref.text) ref['gl:text'] = r.ref.text;
|
|
120
|
+
rel['gl:ref'] = ref;
|
|
121
|
+
}
|
|
122
|
+
return rel;
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
109
126
|
return doc;
|
|
110
127
|
}
|
|
111
128
|
|
|
@@ -295,6 +312,7 @@ function yamlToJsonLd(conceptYaml, register, refMaps) {
|
|
|
295
312
|
if (lc.terms && lc.terms.length > 0) lDoc['gl:designation'] = lc.terms.map(termToDesignation);
|
|
296
313
|
if (lc.definition) lDoc['gl:definition'] = defsToJsonLd(lc.definition);
|
|
297
314
|
if (lc.notes && lc.notes.length > 0) lDoc['gl:notes'] = defsToJsonLd(lc.notes);
|
|
315
|
+
if (lc.annotations && lc.annotations.length > 0) lDoc['gl:annotations'] = defsToJsonLd(lc.annotations);
|
|
298
316
|
if (lc.examples && lc.examples.length > 0) lDoc['gl:examples'] = defsToJsonLd(lc.examples);
|
|
299
317
|
if (lc.sources && lc.sources.length > 0) lDoc['gl:source'] = sourcesToJsonLd(lc.sources);
|
|
300
318
|
if (lc.lineage_source_similarity !== undefined) lDoc['gl:lineageSourceSimilarity'] = lc.lineage_source_similarity;
|
|
@@ -364,6 +382,7 @@ function yamlToJsonLd(conceptYaml, register, refMaps) {
|
|
|
364
382
|
const ref = {};
|
|
365
383
|
if (r.ref.source) ref['gl:source'] = r.ref.source;
|
|
366
384
|
if (r.ref.id) ref['gl:id'] = r.ref.id;
|
|
385
|
+
if (r.ref.text) ref['gl:text'] = r.ref.text;
|
|
367
386
|
rel['gl:ref'] = ref;
|
|
368
387
|
}
|
|
369
388
|
return rel;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { conceptFromJson, getAnnotations } from '../adapters/model-bridge';
|
|
3
|
+
|
|
4
|
+
function makeJsonLdConcept(annotations: { content: string }[] = []) {
|
|
5
|
+
return {
|
|
6
|
+
'@type': 'skos:Concept',
|
|
7
|
+
'@id': 'https://glossarist.org/test/concept/1',
|
|
8
|
+
'gl:identifier': '1',
|
|
9
|
+
'gl:localizedConcept': {
|
|
10
|
+
eng: {
|
|
11
|
+
'gl:languageCode': 'eng',
|
|
12
|
+
'gl:designation': [{ '@type': 'Expression', 'gl:term': 'test term' }],
|
|
13
|
+
'gl:definition': [{ 'gl:content': 'test definition' }],
|
|
14
|
+
'gl:annotations': annotations.map(a => ({ 'gl:content': a.content })),
|
|
15
|
+
'gl:notes': [{ 'gl:content': 'a note' }],
|
|
16
|
+
},
|
|
17
|
+
fra: {
|
|
18
|
+
'gl:languageCode': 'fra',
|
|
19
|
+
'gl:designation': [{ '@type': 'Expression', 'gl:term': 'terme test' }],
|
|
20
|
+
'gl:definition': [{ 'gl:content': 'définition test' }],
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('annotations bridge (getAnnotations)', () => {
|
|
27
|
+
it('preserves annotations from JSON-LD', () => {
|
|
28
|
+
const doc = makeJsonLdConcept([
|
|
29
|
+
{ content: 'first annotation' },
|
|
30
|
+
{ content: 'second annotation' },
|
|
31
|
+
]);
|
|
32
|
+
const concept = conceptFromJson(doc);
|
|
33
|
+
const lc = concept.localization('eng');
|
|
34
|
+
expect(lc).toBeTruthy();
|
|
35
|
+
|
|
36
|
+
const ann = getAnnotations(lc!);
|
|
37
|
+
expect(ann).toHaveLength(2);
|
|
38
|
+
expect(ann[0].content).toBe('first annotation');
|
|
39
|
+
expect(ann[1].content).toBe('second annotation');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('returns empty array when no annotations present', () => {
|
|
43
|
+
const doc = makeJsonLdConcept([]);
|
|
44
|
+
const concept = conceptFromJson(doc);
|
|
45
|
+
const lc = concept.localization('fra')!;
|
|
46
|
+
expect(getAnnotations(lc)).toEqual([]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('returns empty array for language without annotations', () => {
|
|
50
|
+
const doc = makeJsonLdConcept([{ content: 'only eng' }]);
|
|
51
|
+
const concept = conceptFromJson(doc);
|
|
52
|
+
const lc = concept.localization('fra')!;
|
|
53
|
+
expect(getAnnotations(lc)).toEqual([]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('preserves annotations from glossarist native format', () => {
|
|
57
|
+
const doc = {
|
|
58
|
+
id: '1',
|
|
59
|
+
localizations: {
|
|
60
|
+
eng: {
|
|
61
|
+
language_code: 'eng',
|
|
62
|
+
terms: [{ designation: 'test', type: 'expression' }],
|
|
63
|
+
definition: [{ content: 'def' }],
|
|
64
|
+
annotations: [{ content: 'native annotation' }],
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
const concept = conceptFromJson(doc);
|
|
69
|
+
const lc = concept.localization('eng')!;
|
|
70
|
+
const ann = getAnnotations(lc);
|
|
71
|
+
expect(ann).toHaveLength(1);
|
|
72
|
+
expect(ann[0].content).toBe('native annotation');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('model-backed fields are unaffected by annotation bridge', () => {
|
|
76
|
+
const doc = makeJsonLdConcept([{ content: 'annot' }]);
|
|
77
|
+
const concept = conceptFromJson(doc);
|
|
78
|
+
const lc = concept.localization('eng')!;
|
|
79
|
+
|
|
80
|
+
expect(lc.definitions).toHaveLength(1);
|
|
81
|
+
expect(lc.definitions[0].content).toBe('test definition');
|
|
82
|
+
expect(lc.notes).toHaveLength(1);
|
|
83
|
+
expect(lc.notes[0].content).toBe('a note');
|
|
84
|
+
expect(lc.primaryDesignation).toBe('test term');
|
|
85
|
+
expect(getAnnotations(lc)).toHaveLength(1);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -429,6 +429,64 @@ describe('DatasetAdapter', () => {
|
|
|
429
429
|
expect(edges[0].target).toBe('https://metanorma.github.io/oiml-viml/viml-2000/concept/2.23');
|
|
430
430
|
expect(edges[0].type).toBe('supersedes');
|
|
431
431
|
});
|
|
432
|
+
|
|
433
|
+
it('extracts concept-level gl:related "see" edges (OIML pattern)', () => {
|
|
434
|
+
const urnMap = new Map([
|
|
435
|
+
['urn:oiml:pub:v:2:1993', 'vim-1993'],
|
|
436
|
+
]);
|
|
437
|
+
adapter.setUrnMap(urnMap);
|
|
438
|
+
|
|
439
|
+
const concept = conceptFromJson({
|
|
440
|
+
'@id': 'https://metanorma.github.io/oiml-vocab/g18/concept/01643',
|
|
441
|
+
'@type': 'gl:Concept',
|
|
442
|
+
'gl:localizedConcept': {
|
|
443
|
+
eng: {
|
|
444
|
+
'gl:languageCode': 'eng',
|
|
445
|
+
'gl:designation': [{ '@type': 'gl:Expression', 'gl:normativeStatus': 'preferred', 'gl:term': 'repeatability' }],
|
|
446
|
+
'gl:definition': [{ '@type': 'gl:DetailedDefinition', 'gl:content': 'the closeness of agreement...' }],
|
|
447
|
+
},
|
|
448
|
+
},
|
|
449
|
+
'gl:related': [
|
|
450
|
+
{
|
|
451
|
+
'gl:relationshipType': 'see',
|
|
452
|
+
'gl:ref': { 'gl:source': 'urn:oiml:pub:v:2:1993', 'gl:id': '3.6' },
|
|
453
|
+
},
|
|
454
|
+
],
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
adapter.manifest = { uriBase: 'https://metanorma.github.io/oiml-vocab' } as any;
|
|
458
|
+
const edges = adapter.extractEdges(concept);
|
|
459
|
+
expect(edges.length).toBe(1);
|
|
460
|
+
expect(edges[0].target).toBe('https://metanorma.github.io/oiml-vocab/vim-1993/concept/3.6');
|
|
461
|
+
expect(edges[0].type).toBe('see');
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('produces a single edge per relation when both concept-level and localization have same target', () => {
|
|
465
|
+
const concept = conceptFromJson({
|
|
466
|
+
'@id': 'https://glossarist.org/test/concept/1',
|
|
467
|
+
'@type': 'gl:Concept',
|
|
468
|
+
'gl:related': [
|
|
469
|
+
{
|
|
470
|
+
'gl:relationshipType': 'see',
|
|
471
|
+
'gl:ref': { 'gl:source': 'https://glossarist.org/other', 'gl:id': '42' },
|
|
472
|
+
},
|
|
473
|
+
],
|
|
474
|
+
'gl:localizedConcept': {
|
|
475
|
+
eng: {
|
|
476
|
+
'gl:references': [
|
|
477
|
+
{ '@id': 'https://glossarist.org/other/concept/42', 'gl:term': 'related term' },
|
|
478
|
+
],
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
const edges = adapter.extractEdges(concept);
|
|
484
|
+
// Concept-level "see" edge + localization "references" edge = 2 edges
|
|
485
|
+
// (different types: 'see' vs 'references')
|
|
486
|
+
expect(edges.length).toBe(2);
|
|
487
|
+
expect(edges.find(e => e.type === 'see')).toBeDefined();
|
|
488
|
+
expect(edges.find(e => e.type === 'references')).toBeDefined();
|
|
489
|
+
});
|
|
432
490
|
});
|
|
433
491
|
|
|
434
492
|
describe('extractDomainEdges', () => {
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
conceptFromJson,
|
|
4
|
+
getDesignationTarget,
|
|
5
|
+
getRefText,
|
|
6
|
+
} from '../adapters/model-bridge';
|
|
7
|
+
|
|
8
|
+
// ── Designation relationship: gl:target ────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
function makeJsonLdWithDesignationRel(target: string | null, relType = 'abbreviated_form_for') {
|
|
11
|
+
const related = target
|
|
12
|
+
? { 'gl:relationshipType': relType, 'gl:target': target }
|
|
13
|
+
: { 'gl:relationshipType': relType, 'gl:ref': { 'gl:source': 'iso', 'gl:id': '123' } };
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
'@type': 'skos:Concept',
|
|
17
|
+
'@id': 'https://glossarist.org/test/concept/1',
|
|
18
|
+
'gl:identifier': '1',
|
|
19
|
+
'gl:localizedConcept': {
|
|
20
|
+
eng: {
|
|
21
|
+
'gl:languageCode': 'eng',
|
|
22
|
+
'gl:designation': [{
|
|
23
|
+
'@type': 'gl:Abbreviation',
|
|
24
|
+
'gl:term': 'PDF',
|
|
25
|
+
'gl:related': [related],
|
|
26
|
+
}],
|
|
27
|
+
'gl:definition': [{ 'gl:content': 'test definition' }],
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('designation relationship bridge', () => {
|
|
34
|
+
it('preserves gl:target as designation target string', () => {
|
|
35
|
+
const doc = makeJsonLdWithDesignationRel('Portable Document Format');
|
|
36
|
+
const concept = conceptFromJson(doc);
|
|
37
|
+
const lc = concept.localization('eng')!;
|
|
38
|
+
expect(lc.terms).toHaveLength(1);
|
|
39
|
+
|
|
40
|
+
const term = lc.terms[0];
|
|
41
|
+
expect(term.related).toHaveLength(1);
|
|
42
|
+
|
|
43
|
+
const rc = term.related[0];
|
|
44
|
+
expect(rc.type).toBe('abbreviated_form_for');
|
|
45
|
+
expect(getDesignationTarget(rc)).toBe('Portable Document Format');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('returns null for concept-level relationships (no target)', () => {
|
|
49
|
+
const doc = makeJsonLdWithDesignationRel(null);
|
|
50
|
+
const concept = conceptFromJson(doc);
|
|
51
|
+
const rc = concept.localization('eng')!.terms[0].related[0];
|
|
52
|
+
expect(rc.type).toBe('abbreviated_form_for');
|
|
53
|
+
expect(getDesignationTarget(rc)).toBeNull();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('handles short_form_for designation target', () => {
|
|
57
|
+
const doc = makeJsonLdWithDesignationRel('kilogram', 'short_form_for');
|
|
58
|
+
const concept = conceptFromJson(doc);
|
|
59
|
+
const rc = concept.localization('eng')!.terms[0].related[0];
|
|
60
|
+
expect(rc.type).toBe('short_form_for');
|
|
61
|
+
expect(getDesignationTarget(rc)).toBe('kilogram');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('preserves designation target from glossarist native format', () => {
|
|
65
|
+
const doc = {
|
|
66
|
+
id: '1',
|
|
67
|
+
localizations: {
|
|
68
|
+
eng: {
|
|
69
|
+
language_code: 'eng',
|
|
70
|
+
terms: [{
|
|
71
|
+
designation: 'PDF',
|
|
72
|
+
type: 'abbreviation',
|
|
73
|
+
related: [{ type: 'abbreviated_form_for', target: 'Portable Document Format' }],
|
|
74
|
+
}],
|
|
75
|
+
definition: [{ content: 'a format' }],
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
const concept = conceptFromJson(doc);
|
|
80
|
+
const rc = concept.localization('eng')!.terms[0].related[0];
|
|
81
|
+
expect(getDesignationTarget(rc)).toBe('Portable Document Format');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('designation without related returns empty related array', () => {
|
|
85
|
+
const doc = {
|
|
86
|
+
'@type': 'skos:Concept',
|
|
87
|
+
'@id': 'https://glossarist.org/test/concept/1',
|
|
88
|
+
'gl:identifier': '1',
|
|
89
|
+
'gl:localizedConcept': {
|
|
90
|
+
eng: {
|
|
91
|
+
'gl:languageCode': 'eng',
|
|
92
|
+
'gl:designation': [{ '@type': 'gl:Expression', 'gl:term': 'test' }],
|
|
93
|
+
'gl:definition': [{ 'gl:content': 'def' }],
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
const concept = conceptFromJson(doc);
|
|
98
|
+
const term = concept.localization('eng')!.terms[0];
|
|
99
|
+
expect(term.related).toHaveLength(0);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// ── ConceptRef text: gl:text ───────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
describe('ConceptRef text bridge', () => {
|
|
106
|
+
it('preserves gl:text as ref text', () => {
|
|
107
|
+
const doc = {
|
|
108
|
+
'@type': 'skos:Concept',
|
|
109
|
+
'@id': 'https://glossarist.org/test/concept/1',
|
|
110
|
+
'gl:identifier': '1',
|
|
111
|
+
'gl:localizedConcept': {
|
|
112
|
+
eng: {
|
|
113
|
+
'gl:languageCode': 'eng',
|
|
114
|
+
'gl:designation': [{ '@type': 'gl:Expression', 'gl:term': 'test' }],
|
|
115
|
+
'gl:definition': [{ 'gl:content': 'def' }],
|
|
116
|
+
'gl:references': [{
|
|
117
|
+
'gl:relationshipType': 'broader',
|
|
118
|
+
'gl:ref': { 'gl:source': 'iso', 'gl:id': '123', 'gl:text': 'Some Term' },
|
|
119
|
+
}],
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
const concept = conceptFromJson(doc);
|
|
124
|
+
const lc = concept.localization('eng')!;
|
|
125
|
+
expect(lc.related).toHaveLength(1);
|
|
126
|
+
const rc = lc.related[0];
|
|
127
|
+
expect(rc.ref).toBeTruthy();
|
|
128
|
+
expect(rc.ref!.source).toBe('iso');
|
|
129
|
+
expect(rc.ref!.id).toBe('123');
|
|
130
|
+
expect(getRefText(rc.ref!)).toBe('Some Term');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('returns null when ref has no text', () => {
|
|
134
|
+
const doc = {
|
|
135
|
+
'@type': 'skos:Concept',
|
|
136
|
+
'@id': 'https://glossarist.org/test/concept/1',
|
|
137
|
+
'gl:identifier': '1',
|
|
138
|
+
'gl:localizedConcept': {
|
|
139
|
+
eng: {
|
|
140
|
+
'gl:languageCode': 'eng',
|
|
141
|
+
'gl:designation': [{ '@type': 'gl:Expression', 'gl:term': 'test' }],
|
|
142
|
+
'gl:definition': [{ 'gl:content': 'def' }],
|
|
143
|
+
'gl:references': [{
|
|
144
|
+
'gl:relationshipType': 'broader',
|
|
145
|
+
'gl:ref': { 'gl:source': 'iso', 'gl:id': '456' },
|
|
146
|
+
}],
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
const concept = conceptFromJson(doc);
|
|
151
|
+
const rc = concept.localization('eng')!.related[0];
|
|
152
|
+
expect(rc.ref).toBeTruthy();
|
|
153
|
+
expect(getRefText(rc.ref!)).toBeNull();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('preserves ref text from glossarist native format', () => {
|
|
157
|
+
const doc = {
|
|
158
|
+
id: '1',
|
|
159
|
+
localizations: {
|
|
160
|
+
eng: {
|
|
161
|
+
language_code: 'eng',
|
|
162
|
+
terms: [{ designation: 'test', type: 'expression' }],
|
|
163
|
+
definition: [{ content: 'a test' }],
|
|
164
|
+
related: [{ type: 'broader', ref: { source: 'iso', id: '123', text: 'Parent Term' } }],
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
const concept = conceptFromJson(doc);
|
|
169
|
+
const rc = concept.localization('eng')!.related[0];
|
|
170
|
+
expect(getRefText(rc.ref!)).toBe('Parent Term');
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// ── Generate-data serialization ────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
describe('generate-data designation serialization', () => {
|
|
177
|
+
it('termToDesignation serializes designation-level related with gl:target', async () => {
|
|
178
|
+
// We test via the module's internal function by importing the script
|
|
179
|
+
// Since generate-data.mjs is ESM with side effects, we test the output format
|
|
180
|
+
// by constructing the expected JSON-LD structure directly.
|
|
181
|
+
const term = {
|
|
182
|
+
designation: 'PDF',
|
|
183
|
+
type: 'abbreviation',
|
|
184
|
+
normative_status: 'preferred',
|
|
185
|
+
related: [
|
|
186
|
+
{ type: 'abbreviated_form_for', target: 'Portable Document Format' },
|
|
187
|
+
],
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// Simulate what termToDesignation would produce
|
|
191
|
+
const doc: Record<string, unknown> = {
|
|
192
|
+
'@type': 'gl:Abbreviation',
|
|
193
|
+
'gl:normativeStatus': 'preferred',
|
|
194
|
+
'gl:term': 'PDF',
|
|
195
|
+
'gl:related': term.related!.map((r: any) => {
|
|
196
|
+
const rel: Record<string, unknown> = {};
|
|
197
|
+
if (r.type) rel['gl:relationshipType'] = r.type;
|
|
198
|
+
if (r.target) {
|
|
199
|
+
rel['gl:target'] = r.target;
|
|
200
|
+
}
|
|
201
|
+
return rel;
|
|
202
|
+
}),
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
expect(doc['gl:related']).toHaveLength(1);
|
|
206
|
+
const rel = (doc['gl:related'] as any[])[0];
|
|
207
|
+
expect(rel['gl:relationshipType']).toBe('abbreviated_form_for');
|
|
208
|
+
expect(rel['gl:target']).toBe('Portable Document Format');
|
|
209
|
+
expect(rel['gl:ref']).toBeUndefined();
|
|
210
|
+
});
|
|
211
|
+
});
|
|
@@ -91,6 +91,21 @@ describe('GraphEngine', () => {
|
|
|
91
91
|
expect(g.edgeCount).toBe(1);
|
|
92
92
|
});
|
|
93
93
|
|
|
94
|
+
it('deduplicates edges with matching source+target+type regardless of register field', () => {
|
|
95
|
+
const g = new GraphEngine();
|
|
96
|
+
g.addEdge({ source: 'https://example.org/g18/concept/1', target: 'https://example.org/vim-1993/concept/3.6', type: 'see', register: 'vim-1993' });
|
|
97
|
+
g.addEdge({ source: 'https://example.org/g18/concept/1', target: 'https://example.org/vim-1993/concept/3.6', type: 'see', register: 'g18' });
|
|
98
|
+
expect(g.edgeCount).toBe(1);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('does not deduplicate when target URIs differ', () => {
|
|
102
|
+
const g = new GraphEngine();
|
|
103
|
+
g.addEdge({ source: 'https://example.org/g18/concept/1', target: 'https://example.org/vim-1993/concept/3.6', type: 'see', register: 'vim-1993' });
|
|
104
|
+
// URN not resolved → different target URI
|
|
105
|
+
g.addEdge({ source: 'https://example.org/g18/concept/1', target: 'https://example.org/urn:oiml:pub:v:2:1993/concept/3.6', type: 'see', register: 'g18' });
|
|
106
|
+
expect(g.edgeCount).toBe(2);
|
|
107
|
+
});
|
|
108
|
+
|
|
94
109
|
it('keeps separate edges for different languages', () => {
|
|
95
110
|
const g = new GraphEngine();
|
|
96
111
|
g.addEdge({ source: 'uri:a', target: 'uri:b', type: 'references', register: 'test', lang: 'eng' });
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
RELATIONSHIP_CATEGORIES,
|
|
4
|
+
INVERSE_RELATIONSHIPS,
|
|
5
|
+
categorizeRelationship,
|
|
6
|
+
relationshipLabel,
|
|
7
|
+
relationshipDefinition,
|
|
8
|
+
} from '../utils/relationship-categories';
|
|
9
|
+
import { ontology } from '../adapters/ontology-registry';
|
|
10
|
+
|
|
11
|
+
describe('RELATIONSHIP_CATEGORIES (taxonomy-derived)', () => {
|
|
12
|
+
it('has expected category IDs', () => {
|
|
13
|
+
const ids = RELATIONSHIP_CATEGORIES.map(c => c.id);
|
|
14
|
+
expect(ids).toContain('hierarchical');
|
|
15
|
+
expect(ids).toContain('mapping');
|
|
16
|
+
expect(ids).toContain('associative');
|
|
17
|
+
expect(ids).toContain('lifecycle');
|
|
18
|
+
expect(ids).toContain('comparative');
|
|
19
|
+
expect(ids).toContain('definitional');
|
|
20
|
+
expect(ids).toContain('spatiotemporal');
|
|
21
|
+
expect(ids).toContain('lexical');
|
|
22
|
+
expect(ids).toContain('designation');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('every category has required fields', () => {
|
|
26
|
+
for (const cat of RELATIONSHIP_CATEGORIES) {
|
|
27
|
+
expect(cat.id).toBeTruthy();
|
|
28
|
+
expect(cat.label).toBeTruthy();
|
|
29
|
+
expect(cat.types.length).toBeGreaterThan(0);
|
|
30
|
+
expect(cat.color).toBeTruthy();
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('no type appears in more than one category', () => {
|
|
35
|
+
const seen = new Set<string>();
|
|
36
|
+
for (const cat of RELATIONSHIP_CATEGORIES) {
|
|
37
|
+
for (const t of cat.types) {
|
|
38
|
+
expect(seen.has(t), `duplicate type: ${t}`).toBe(false);
|
|
39
|
+
seen.add(t);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('INVERSE_RELATIONSHIPS (taxonomy-derived)', () => {
|
|
46
|
+
it('every entry is bidirectional', () => {
|
|
47
|
+
for (const [key, value] of Object.entries(INVERSE_RELATIONSHIPS)) {
|
|
48
|
+
expect(INVERSE_RELATIONSHIPS[value], `${key} -> ${value} is not bidirectional`).toBe(key);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('symmetric types map to themselves', () => {
|
|
53
|
+
const symmetric = ['equivalent', 'exact_match', 'close_match', 'compare', 'contrast',
|
|
54
|
+
'related_match', 'related_concept', 'homograph', 'false_friend',
|
|
55
|
+
'sequentially_related', 'spatially_related', 'temporally_related'];
|
|
56
|
+
for (const type of symmetric) {
|
|
57
|
+
expect(INVERSE_RELATIONSHIPS[type], `${type} should be self-inverse`).toBe(type);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('known inverse pairs are correct', () => {
|
|
62
|
+
expect(INVERSE_RELATIONSHIPS.supersedes).toBe('superseded_by');
|
|
63
|
+
expect(INVERSE_RELATIONSHIPS.superseded_by).toBe('supersedes');
|
|
64
|
+
expect(INVERSE_RELATIONSHIPS.broader).toBe('narrower');
|
|
65
|
+
expect(INVERSE_RELATIONSHIPS.narrower).toBe('broader');
|
|
66
|
+
expect(INVERSE_RELATIONSHIPS.has_part).toBe('is_part_of');
|
|
67
|
+
expect(INVERSE_RELATIONSHIPS.is_part_of).toBe('has_part');
|
|
68
|
+
expect(INVERSE_RELATIONSHIPS.broad_match).toBe('narrow_match');
|
|
69
|
+
expect(INVERSE_RELATIONSHIPS.narrow_match).toBe('broad_match');
|
|
70
|
+
expect(INVERSE_RELATIONSHIPS.related_concept_broader).toBe('related_concept_narrower');
|
|
71
|
+
expect(INVERSE_RELATIONSHIPS.related_concept_narrower).toBe('related_concept_broader');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('categorizeRelationship', () => {
|
|
76
|
+
it('all 52 taxonomy types resolve to a known category', () => {
|
|
77
|
+
const allTypes = ontology.getAll('relationshipType');
|
|
78
|
+
for (const concept of allTypes) {
|
|
79
|
+
const cat = categorizeRelationship(concept.id);
|
|
80
|
+
expect(cat.id, `${concept.id} resolved to 'other'`).not.toBe('other');
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('specific category assignments', () => {
|
|
85
|
+
expect(categorizeRelationship('broader').id).toBe('hierarchical');
|
|
86
|
+
expect(categorizeRelationship('equivalent').id).toBe('mapping');
|
|
87
|
+
expect(categorizeRelationship('references').id).toBe('associative');
|
|
88
|
+
expect(categorizeRelationship('supersedes').id).toBe('lifecycle');
|
|
89
|
+
expect(categorizeRelationship('compare').id).toBe('comparative');
|
|
90
|
+
expect(categorizeRelationship('has_definition').id).toBe('definitional');
|
|
91
|
+
expect(categorizeRelationship('sequentially_related').id).toBe('spatiotemporal');
|
|
92
|
+
expect(categorizeRelationship('homograph').id).toBe('lexical');
|
|
93
|
+
expect(categorizeRelationship('abbreviated_form_for').id).toBe('designation');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('returns other for unknown type', () => {
|
|
97
|
+
expect(categorizeRelationship('unknown_type').id).toBe('other');
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('relationshipLabel', () => {
|
|
102
|
+
it('all 52 taxonomy types produce non-empty labels', () => {
|
|
103
|
+
const allTypes = ontology.getAll('relationshipType');
|
|
104
|
+
for (const concept of allTypes) {
|
|
105
|
+
const label = relationshipLabel(concept.id);
|
|
106
|
+
expect(label, `${concept.id} has empty label`).toBeTruthy();
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('taxonomy labels match expected values', () => {
|
|
111
|
+
expect(relationshipLabel('broader_generic')).toBe('broader (generic)');
|
|
112
|
+
expect(relationshipLabel('related_concept')).toBe('related concept');
|
|
113
|
+
expect(relationshipLabel('broader')).toBe('broader');
|
|
114
|
+
expect(relationshipLabel('supersedes')).toBe('supersedes');
|
|
115
|
+
expect(relationshipLabel('superseded_by')).toBe('superseded by');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('unknown type falls back to title-case computed form', () => {
|
|
119
|
+
expect(relationshipLabel('some_new_type')).toBe('Some New Type');
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('relationshipDefinition', () => {
|
|
124
|
+
it('all 52 taxonomy types have definitions', () => {
|
|
125
|
+
const allTypes = ontology.getAll('relationshipType');
|
|
126
|
+
for (const concept of allTypes) {
|
|
127
|
+
const def = relationshipDefinition(concept.id);
|
|
128
|
+
expect(def, `${concept.id} has no definition`).toBeTruthy();
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe('taxonomy-code synchronization', () => {
|
|
134
|
+
it('every type in RELATIONSHIP_CATEGORIES exists in taxonomy', () => {
|
|
135
|
+
for (const cat of RELATIONSHIP_CATEGORIES) {
|
|
136
|
+
for (const t of cat.types) {
|
|
137
|
+
expect(ontology.has('relationshipType', t), `${t} not in taxonomy`).toBe(true);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('every type in INVERSE_RELATIONSHIPS exists in taxonomy', () => {
|
|
143
|
+
for (const key of Object.keys(INVERSE_RELATIONSHIPS)) {
|
|
144
|
+
expect(ontology.has('relationshipType', key), `${key} not in taxonomy`).toBe(true);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('every taxonomy entry has a category', () => {
|
|
149
|
+
const allTypes = ontology.getAll('relationshipType');
|
|
150
|
+
for (const concept of allTypes) {
|
|
151
|
+
const cat = categorizeRelationship(concept.id);
|
|
152
|
+
expect(cat.id, `${concept.id} has no category`).not.toBe('other');
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('all IRIs use consistent gloss:rel/ prefix', () => {
|
|
157
|
+
const allTypes = ontology.getAll('relationshipType');
|
|
158
|
+
for (const concept of allTypes) {
|
|
159
|
+
expect(concept.iri, `${concept.id} IRI: ${concept.iri}`).toMatch(/^gloss:rel\//);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { ontology } from '../adapters/ontology-registry';
|
|
3
|
+
import { entryStatusColor, conceptStatusColor } from '../utils/concept-helpers';
|
|
4
|
+
import { designationTypeInfo, normativeStatusInfo, sourceTypeInfo } from '../utils/designation-registry';
|
|
5
|
+
|
|
6
|
+
describe('OntologyRegistry.getColor', () => {
|
|
7
|
+
it('returns correct color for conceptStatus', () => {
|
|
8
|
+
expect(ontology.getColor('conceptStatus', 'valid')).toBe('badge-green');
|
|
9
|
+
expect(ontology.getColor('conceptStatus', 'superseded')).toBe('bg-red-50 text-red-700');
|
|
10
|
+
expect(ontology.getColor('conceptStatus', 'draft')).toBe('badge-yellow');
|
|
11
|
+
expect(ontology.getColor('conceptStatus', 'retired')).toBe('badge-gray');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('returns null for unknown status', () => {
|
|
15
|
+
expect(ontology.getColor('conceptStatus', 'nonexistent')).toBeNull();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('returns correct colors for normativeStatus', () => {
|
|
19
|
+
expect(ontology.getColor('normativeStatus', 'preferred')).toBe('bg-emerald-50 text-emerald-700');
|
|
20
|
+
expect(ontology.getColor('normativeStatus', 'deprecated')).toBe('bg-red-50 text-red-700');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('returns correct colors for designationType', () => {
|
|
24
|
+
expect(ontology.getColor('designationType', 'expression')).toBe('bg-sky-50 text-sky-700');
|
|
25
|
+
expect(ontology.getColor('designationType', 'abbreviation')).toBe('bg-amber-50 text-amber-700');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('returns correct colors for sourceType', () => {
|
|
29
|
+
expect(ontology.getColor('sourceType', 'authoritative')).toBe('badge-purple');
|
|
30
|
+
expect(ontology.getColor('sourceType', 'lineage')).toBe('badge-blue');
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('concept-helpers (ontology-driven colors)', () => {
|
|
35
|
+
it('entryStatusColor returns taxonomy-driven colors', () => {
|
|
36
|
+
expect(entryStatusColor('valid')).toBe('badge-green');
|
|
37
|
+
expect(entryStatusColor('not_valid')).toBe('bg-red-50 text-red-700');
|
|
38
|
+
expect(entryStatusColor('draft')).toBe('badge-yellow');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('entryStatusColor falls back to badge-gray', () => {
|
|
42
|
+
expect(entryStatusColor('unknown_status')).toBe('badge-gray');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('conceptStatusColor returns taxonomy-driven colors', () => {
|
|
46
|
+
expect(conceptStatusColor('valid')).toBe('badge-green');
|
|
47
|
+
expect(conceptStatusColor('invalid')).toBe('bg-red-50 text-red-700');
|
|
48
|
+
expect(conceptStatusColor('draft')).toBe('badge-yellow');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('conceptStatusColor handles null', () => {
|
|
52
|
+
expect(conceptStatusColor(null)).toBe('badge-gray');
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('designation-registry (ontology-driven colors)', () => {
|
|
57
|
+
it('designationTypeInfo uses taxonomy colors', () => {
|
|
58
|
+
const info = designationTypeInfo({ type: 'expression', designation: 'test' } as any);
|
|
59
|
+
expect(info.color).toBe('bg-sky-50 text-sky-700');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('normativeStatusInfo uses taxonomy colors', () => {
|
|
63
|
+
const info = normativeStatusInfo('preferred');
|
|
64
|
+
expect(info.color).toBe('bg-emerald-50 text-emerald-700');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('sourceTypeInfo uses taxonomy colors', () => {
|
|
68
|
+
const info = sourceTypeInfo('authoritative');
|
|
69
|
+
expect(info.color).toBe('badge-purple');
|
|
70
|
+
});
|
|
71
|
+
});
|