@glossarist/concept-browser 0.7.117 → 0.7.119
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 +2 -2
- package/scripts/build-edges.js +13 -0
- package/scripts/build-edges.ts +19 -0
- package/scripts/generate-data.js +26 -4
- package/scripts/generate-data.ts +20 -0
- package/scripts/lib/vocab-turtle.ts +19 -4
- package/src/__tests__/adapters/cross-dataset-related.test.ts +45 -0
- package/src/__tests__/consumer-e2e-simulation.test.ts +98 -0
- package/src/__tests__/published-package-contract.test.ts +10 -0
- package/src/adapters/model-bridge.ts +7 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glossarist/concept-browser",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.119",
|
|
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": {
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"astro": "^7.0.7",
|
|
43
43
|
"d3": "^7.9.0",
|
|
44
44
|
"favicons": "^7.2.0",
|
|
45
|
-
"glossarist": "^0.4.
|
|
45
|
+
"glossarist": "^0.4.54",
|
|
46
46
|
"js-yaml": "^4.1.0",
|
|
47
47
|
"jszip": "^3.10.1",
|
|
48
48
|
"n3": "^1.26.0",
|
package/scripts/build-edges.js
CHANGED
|
@@ -157,6 +157,16 @@ function extractRelated(concept, registerId, uriBase, urnMap) {
|
|
|
157
157
|
const edges = [];
|
|
158
158
|
const sourceUri = concept["@id"];
|
|
159
159
|
for (const r of concept["gl:related"] || []) {
|
|
160
|
+
if (r["gl:target"]) {
|
|
161
|
+
edges.push({
|
|
162
|
+
source: sourceUri,
|
|
163
|
+
target: r["gl:target"],
|
|
164
|
+
type: r["gl:relationshipType"] || "references",
|
|
165
|
+
label: r["gl:term"] || void 0,
|
|
166
|
+
register: r["gl:target"].match(/\/([^/]+)\/concept\//)?.[1] || registerId
|
|
167
|
+
});
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
160
170
|
const ref = r["gl:ref"];
|
|
161
171
|
if (!ref) continue;
|
|
162
172
|
const source = ref["gl:source"] || ref["source"];
|
|
@@ -382,6 +392,9 @@ function main() {
|
|
|
382
392
|
const base = alias.endsWith("*") ? alias.slice(0, -1) : alias;
|
|
383
393
|
if (base) urnMap.set(base, ds);
|
|
384
394
|
}
|
|
395
|
+
if (manifest.ref) {
|
|
396
|
+
urnMap.set(manifest.ref, ds);
|
|
397
|
+
}
|
|
385
398
|
} catch {
|
|
386
399
|
}
|
|
387
400
|
}
|
package/scripts/build-edges.ts
CHANGED
|
@@ -173,6 +173,19 @@ function extractRelated(concept, registerId, uriBase, urnMap) {
|
|
|
173
173
|
const edges = [];
|
|
174
174
|
const sourceUri = concept['@id'];
|
|
175
175
|
for (const r of concept['gl:related'] || []) {
|
|
176
|
+
// Prefer gl:target — already resolved at generate-data build time
|
|
177
|
+
// via refPrefixMap (maps dataset ref labels to dataset IDs).
|
|
178
|
+
if (r['gl:target']) {
|
|
179
|
+
edges.push({
|
|
180
|
+
source: sourceUri,
|
|
181
|
+
target: r['gl:target'],
|
|
182
|
+
type: r['gl:relationshipType'] || 'references',
|
|
183
|
+
label: r['gl:term'] || undefined,
|
|
184
|
+
register: r['gl:target'].match(/\/([^/]+)\/concept\//)?.[1] || registerId,
|
|
185
|
+
});
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
// Fallback: resolve from gl:ref using urnMap
|
|
176
189
|
const ref = r['gl:ref'];
|
|
177
190
|
if (!ref) continue;
|
|
178
191
|
const source = ref['gl:source'] || ref['source'];
|
|
@@ -458,6 +471,12 @@ for (const ds of datasets) {
|
|
|
458
471
|
const base = alias.endsWith('*') ? alias.slice(0, -1) : alias;
|
|
459
472
|
if (base) urnMap.set(base, ds);
|
|
460
473
|
}
|
|
474
|
+
// Map the dataset's ref label (e.g. "CIE S 017:2020") to its dataset ID.
|
|
475
|
+
// This enables cross-dataset resolution for gl:ref entries that use
|
|
476
|
+
// source labels instead of URIs.
|
|
477
|
+
if (manifest.ref) {
|
|
478
|
+
urnMap.set(manifest.ref, ds);
|
|
479
|
+
}
|
|
461
480
|
} catch {}
|
|
462
481
|
}
|
|
463
482
|
console.log(`URI resolution map: ${[...urnMap.entries()].map(([k,v]) => `${k}→${v}`).join(', ')}\n`);
|
package/scripts/generate-data.js
CHANGED
|
@@ -420,7 +420,7 @@ async function buildActivityTurtle(input) {
|
|
|
420
420
|
}
|
|
421
421
|
|
|
422
422
|
// scripts/lib/vocab-turtle.ts
|
|
423
|
-
import { readFileSync as readFileSync2 } from "node:fs";
|
|
423
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "node:fs";
|
|
424
424
|
import { join, dirname as dirname2 } from "node:path";
|
|
425
425
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
426
426
|
import {
|
|
@@ -430,9 +430,21 @@ import {
|
|
|
430
430
|
PREFIXES as PREFIXES2
|
|
431
431
|
} from "glossarist/rdf";
|
|
432
432
|
var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
433
|
+
function findVocabPath() {
|
|
434
|
+
const candidates = [
|
|
435
|
+
join(__dirname2, "..", "..", "data", "glossarist-vocab.json"),
|
|
436
|
+
join(__dirname2, "..", "data", "glossarist-vocab.json")
|
|
437
|
+
];
|
|
438
|
+
for (const p of candidates) {
|
|
439
|
+
if (existsSync2(p)) return p;
|
|
440
|
+
}
|
|
441
|
+
throw new Error(
|
|
442
|
+
`glossarist-vocab.json not found. Tried:
|
|
443
|
+
${candidates.map((p) => " " + p).join("\n")}`
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
function loadVocab() {
|
|
447
|
+
return JSON.parse(readFileSync2(findVocabPath(), "utf8"));
|
|
436
448
|
}
|
|
437
449
|
function writerPrefixes3() {
|
|
438
450
|
const out = {};
|
|
@@ -1090,6 +1102,9 @@ function buildPatternIndex(datasets, registerCache2) {
|
|
|
1090
1102
|
function buildRefMaps(config2, registerCache2) {
|
|
1091
1103
|
const refPrefixMap = {};
|
|
1092
1104
|
const patternIndex = buildPatternIndex(config2.datasets, registerCache2);
|
|
1105
|
+
for (const ds of config2.datasets) {
|
|
1106
|
+
if (ds.ref) refPrefixMap[ds.ref] = ds.id;
|
|
1107
|
+
}
|
|
1093
1108
|
for (const route of config2.routing || []) {
|
|
1094
1109
|
if (route.uri && route.uri.includes("iec") && route.uri.includes("60050")) {
|
|
1095
1110
|
const mapped = route.targetDataset;
|
|
@@ -1316,6 +1331,13 @@ function yamlToJsonLd(conceptYaml, register, refMaps2) {
|
|
|
1316
1331
|
if (r.ref.id) ref["gl:id"] = r.ref.id;
|
|
1317
1332
|
if (r.ref.text) ref["gl:text"] = r.ref.text;
|
|
1318
1333
|
rel["gl:ref"] = ref;
|
|
1334
|
+
if (r.ref.source && r.ref.id && refMaps2?.refPrefixMap?.[r.ref.source]) {
|
|
1335
|
+
const datasetId = refMaps2.refPrefixMap[r.ref.source];
|
|
1336
|
+
rel["gl:target"] = buildConceptUri(refMaps2.uriBase, datasetId, r.ref.id);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
if (r.target) {
|
|
1340
|
+
rel["gl:target"] = r.target;
|
|
1319
1341
|
}
|
|
1320
1342
|
return rel;
|
|
1321
1343
|
});
|
package/scripts/generate-data.ts
CHANGED
|
@@ -221,6 +221,14 @@ function buildRefMaps(config, registerCache) {
|
|
|
221
221
|
const refPrefixMap = {};
|
|
222
222
|
const patternIndex = buildPatternIndex(config.datasets, registerCache);
|
|
223
223
|
|
|
224
|
+
// Auto-map each dataset's `ref` field to its dataset ID.
|
|
225
|
+
// This enables cross-dataset link resolution: a concept in cie-2020
|
|
226
|
+
// with ref: { source: "CIE S 017:2011", id: "17-245" } resolves to
|
|
227
|
+
// cie-2011/concept/17-245 because cie-2011 has ref: "CIE S 017:2011".
|
|
228
|
+
for (const ds of config.datasets) {
|
|
229
|
+
if (ds.ref) refPrefixMap[ds.ref] = ds.id;
|
|
230
|
+
}
|
|
231
|
+
|
|
224
232
|
for (const route of config.routing || []) {
|
|
225
233
|
if (route.uri && route.uri.includes('iec') && route.uri.includes('60050')) {
|
|
226
234
|
const mapped = route.targetDataset;
|
|
@@ -508,6 +516,18 @@ function yamlToJsonLd(conceptYaml, register, refMaps) {
|
|
|
508
516
|
if (r.ref.id) ref['gl:id'] = r.ref.id;
|
|
509
517
|
if (r.ref.text) ref['gl:text'] = r.ref.text;
|
|
510
518
|
rel['gl:ref'] = ref;
|
|
519
|
+
|
|
520
|
+
// Resolve cross-dataset refs to gl:target URIs.
|
|
521
|
+
// refPrefixMap maps source labels (e.g. "CIE S 017:2011") to
|
|
522
|
+
// dataset IDs (e.g. "cie-2011"). When the source matches a
|
|
523
|
+
// co-deployed dataset, we can build a navigable concept URI.
|
|
524
|
+
if (r.ref.source && r.ref.id && refMaps?.refPrefixMap?.[r.ref.source]) {
|
|
525
|
+
const datasetId = refMaps.refPrefixMap[r.ref.source];
|
|
526
|
+
rel['gl:target'] = buildConceptUri(refMaps.uriBase, datasetId, r.ref.id);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
if (r.target) {
|
|
530
|
+
rel['gl:target'] = r.target;
|
|
511
531
|
}
|
|
512
532
|
return rel;
|
|
513
533
|
});
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// to absolute IRIs, then re-rendered via glossarist-js's n3 writer with
|
|
8
8
|
// the canonical prefix map ).
|
|
9
9
|
|
|
10
|
-
import { readFileSync } from 'node:fs';
|
|
10
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
11
11
|
import { join, dirname } from 'node:path';
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
13
|
import {
|
|
@@ -18,10 +18,25 @@ import {
|
|
|
18
18
|
} from 'glossarist/rdf';
|
|
19
19
|
|
|
20
20
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
|
-
const VOCAB_PATH = join(__dirname, '..', '..', 'data', 'glossarist-vocab.json');
|
|
22
21
|
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
// When running from source (tsx), this file is at scripts/lib/ and
|
|
23
|
+
// the data dir is two levels up. When bundled by esbuild into
|
|
24
|
+
// scripts/*.js, __dirname is scripts/ and the data dir is one level up.
|
|
25
|
+
function findVocabPath(): string {
|
|
26
|
+
const candidates = [
|
|
27
|
+
join(__dirname, '..', '..', 'data', 'glossarist-vocab.json'),
|
|
28
|
+
join(__dirname, '..', 'data', 'glossarist-vocab.json'),
|
|
29
|
+
];
|
|
30
|
+
for (const p of candidates) {
|
|
31
|
+
if (existsSync(p)) return p;
|
|
32
|
+
}
|
|
33
|
+
throw new Error(
|
|
34
|
+
`glossarist-vocab.json not found. Tried:\n${candidates.map(p => ' ' + p).join('\n')}`,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function loadVocab() {
|
|
39
|
+
return JSON.parse(readFileSync(findVocabPath(), 'utf8'));
|
|
25
40
|
}
|
|
26
41
|
|
|
27
42
|
// n3's writer prefixes must omit the trailing # or / — those go in
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { conceptFromJson } from '../../adapters/model-bridge';
|
|
3
|
+
|
|
4
|
+
function makeJsonLdWithRelated(target: string | null) {
|
|
5
|
+
const related = target
|
|
6
|
+
? {
|
|
7
|
+
'gl:relationshipType': 'superseded_by',
|
|
8
|
+
'gl:target': target,
|
|
9
|
+
'gl:ref': { 'gl:source': 'CIE S 017:2020', 'gl:id': '17-21-097' },
|
|
10
|
+
}
|
|
11
|
+
: {
|
|
12
|
+
'gl:relationshipType': 'superseded_by',
|
|
13
|
+
'gl:ref': { 'gl:source': 'CIE S 017:2020', 'gl:id': '17-21-097' },
|
|
14
|
+
};
|
|
15
|
+
return {
|
|
16
|
+
'@type': 'skos:Concept',
|
|
17
|
+
'@id': 'https://example.com/cie-2011/concept/17-50',
|
|
18
|
+
'gl:identifier': '17-50',
|
|
19
|
+
'gl:localizedConcept': {
|
|
20
|
+
eng: {
|
|
21
|
+
'gl:languageCode': 'eng',
|
|
22
|
+
'gl:designation': [{ '@type': 'gl:Expression', 'gl:term': 'test term', 'gl:normativeStatus': 'preferred' }],
|
|
23
|
+
'gl:definition': [{ 'gl:content': 'test definition' }],
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
'gl:related': [related],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('cross-dataset related concept resolution — gl:target flows through model-bridge natively', () => {
|
|
31
|
+
it('concept.relatedConcepts entries with gl:target have native target field', () => {
|
|
32
|
+
const json = makeJsonLdWithRelated('https://example.com/cie-2020/concept/17-21-097');
|
|
33
|
+
const concept = conceptFromJson(json as any);
|
|
34
|
+
expect(concept.relatedConcepts).toHaveLength(1);
|
|
35
|
+
expect(concept.relatedConcepts[0].type).toBe('superseded_by');
|
|
36
|
+
expect(concept.relatedConcepts[0].target).toBe('https://example.com/cie-2020/concept/17-21-097');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('concept.relatedConcepts entries without gl:target have null target', () => {
|
|
40
|
+
const json = makeJsonLdWithRelated(null);
|
|
41
|
+
const concept = conceptFromJson(json as any);
|
|
42
|
+
expect(concept.relatedConcepts).toHaveLength(1);
|
|
43
|
+
expect(concept.relatedConcepts[0].target).toBeNull();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* End-to-end consumer simulation test.
|
|
3
|
+
*
|
|
4
|
+
* Simulates what a real consumer experiences: creates a temp "consumer
|
|
5
|
+
* project" with the compiled package in node_modules, then runs scripts
|
|
6
|
+
* with plain node. Catches packaging bugs that dev-mode tests miss.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
10
|
+
import { spawnSync } from 'node:child_process';
|
|
11
|
+
import {
|
|
12
|
+
mkdirSync, writeFileSync, cpSync, rmSync, existsSync, readFileSync, readdirSync,
|
|
13
|
+
} from 'node:fs';
|
|
14
|
+
import { resolve, join, dirname } from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
|
|
17
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const ROOT = resolve(__dirname, '..', '..');
|
|
19
|
+
const SCRIPTS_DIR = join(ROOT, 'scripts');
|
|
20
|
+
const CONSUMER_DIR = join(ROOT, '.tmp-consumer-test');
|
|
21
|
+
|
|
22
|
+
describe('end-to-end consumer simulation — compiled package works under plain node', () => {
|
|
23
|
+
beforeAll(() => {
|
|
24
|
+
const build = spawnSync('node', [join(ROOT, 'scripts', 'build-scripts.mjs')], {
|
|
25
|
+
encoding: 'utf8', cwd: ROOT,
|
|
26
|
+
});
|
|
27
|
+
expect(build.status).toBe(0);
|
|
28
|
+
|
|
29
|
+
rmSync(CONSUMER_DIR, { recursive: true, force: true });
|
|
30
|
+
const pkgDest = join(CONSUMER_DIR, 'node_modules', '@glossarist', 'concept-browser');
|
|
31
|
+
mkdirSync(pkgDest, { recursive: true });
|
|
32
|
+
cpSync(join(ROOT, 'scripts'), join(pkgDest, 'scripts'), { recursive: true });
|
|
33
|
+
cpSync(join(ROOT, 'data'), join(pkgDest, 'data'), { recursive: true });
|
|
34
|
+
cpSync(join(ROOT, 'cli'), join(pkgDest, 'cli'), { recursive: true });
|
|
35
|
+
writeFileSync(join(pkgDest, 'package.json'), JSON.stringify({
|
|
36
|
+
name: '@glossarist/concept-browser', version: '0.0.0-test', type: 'module',
|
|
37
|
+
bin: { 'concept-browser': './cli/index.mjs' },
|
|
38
|
+
}));
|
|
39
|
+
}, 60000);
|
|
40
|
+
|
|
41
|
+
afterAll(() => {
|
|
42
|
+
rmSync(CONSUMER_DIR, { recursive: true, force: true });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('compiled scripts can find package data files from node_modules location', () => {
|
|
46
|
+
const scriptDir = join(CONSUMER_DIR, 'node_modules', '@glossarist', 'concept-browser', 'scripts');
|
|
47
|
+
const pathFromBundled = join(scriptDir, '..', 'data', 'glossarist-vocab.json');
|
|
48
|
+
expect(existsSync(pathFromBundled)).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('compiled scripts list matches source scripts (no missing outputs)', () => {
|
|
52
|
+
const sourceTs = readdirSync(SCRIPTS_DIR)
|
|
53
|
+
.filter(f => f.endsWith('.ts') && !f.includes('__tests__') && f !== 'smoke.ts')
|
|
54
|
+
.map(f => f.replace(/\.ts$/, ''));
|
|
55
|
+
const compiledJs = readdirSync(SCRIPTS_DIR)
|
|
56
|
+
.filter(f => f.endsWith('.js'))
|
|
57
|
+
.map(f => f.replace(/\.js$/, ''));
|
|
58
|
+
for (const name of sourceTs) {
|
|
59
|
+
expect(compiledJs).toContain(name);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('validate-yaml.js validates against the shipped schema from node_modules', () => {
|
|
64
|
+
const consumerPkg = join(CONSUMER_DIR, 'node_modules', '@glossarist', 'concept-browser');
|
|
65
|
+
const scriptPath = join(consumerPkg, 'scripts', 'validate-yaml.js');
|
|
66
|
+
const schemaPath = join(consumerPkg, 'data', 'concept-model', 'shapes', 'glossarist.concept.yaml.schema.json');
|
|
67
|
+
const code = [
|
|
68
|
+
'import { validateYaml } from ' + JSON.stringify(scriptPath) + ';',
|
|
69
|
+
'import { readFileSync } from "fs";',
|
|
70
|
+
'const schema = JSON.parse(readFileSync(' + JSON.stringify(schemaPath) + ', "utf8"));',
|
|
71
|
+
'const r = validateYaml("termid: test", schema);',
|
|
72
|
+
'console.log("valid=" + r.valid);',
|
|
73
|
+
].join('\n');
|
|
74
|
+
const result = spawnSync('node', ['--input-type=module', '-e', code], {
|
|
75
|
+
encoding: 'utf8', cwd: ROOT,
|
|
76
|
+
});
|
|
77
|
+
expect(result.status).toBe(0);
|
|
78
|
+
expect(result.stdout).toContain('valid=true');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('no compiled script references a devDependency-only package', () => {
|
|
82
|
+
const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8'));
|
|
83
|
+
const deps = new Set(Object.keys(pkg.dependencies || {}));
|
|
84
|
+
const devDeps = new Set(Object.keys(pkg.devDependencies || {}));
|
|
85
|
+
const violations: string[] = [];
|
|
86
|
+
for (const file of readdirSync(SCRIPTS_DIR).filter(f => f.endsWith('.js'))) {
|
|
87
|
+
const src = readFileSync(join(SCRIPTS_DIR, file), 'utf8');
|
|
88
|
+
const importRe = new RegExp('(?:from\\s+|import\\s*\\(\\s*)["\']([^"\'./@][^"\']*)["\']', 'g');
|
|
89
|
+
let m;
|
|
90
|
+
while ((m = importRe.exec(src)) !== null) {
|
|
91
|
+
const name = m[1].startsWith('@') ? m[1].split('/').slice(0, 2).join('/') : m[1].split('/')[0];
|
|
92
|
+
if (name.startsWith('node:') || name === 'node') continue;
|
|
93
|
+
if (devDeps.has(name) && !deps.has(name)) violations.push(file + ' -> ' + name);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
expect(violations).toEqual([]);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -97,4 +97,14 @@ describe('published-package contract — compiled .js scripts load under plain n
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
});
|
|
100
|
+
|
|
101
|
+
it('compiled scripts can find package data files (glossarist-vocab.json path regression)', () => {
|
|
102
|
+
// When esbuild bundles scripts/lib/vocab-turtle.ts into scripts/generate-data.js,
|
|
103
|
+
// __dirname changes from scripts/lib/ to scripts/. The path to data/ must
|
|
104
|
+
// resolve correctly in both modes. This test verifies the fallback logic.
|
|
105
|
+
const dataDir = join(ROOT, 'data');
|
|
106
|
+
expect(existsSync(join(dataDir, 'glossarist-vocab.json'))).toBe(true);
|
|
107
|
+
expect(existsSync(join(dataDir, 'colors.json'))).toBe(true);
|
|
108
|
+
expect(existsSync(join(dataDir, 'concept-model', 'shapes', 'glossarist.shacl.ttl'))).toBe(true);
|
|
109
|
+
});
|
|
100
110
|
});
|
|
@@ -367,6 +367,13 @@ function mapRelatedFromJsonLd(r: JsonLdRelated): Record<string, unknown> {
|
|
|
367
367
|
result.type = r[GL.RELATIONSHIP_TYPE];
|
|
368
368
|
}
|
|
369
369
|
|
|
370
|
+
// gl:target is the resolved concept URI for cross-dataset relations
|
|
371
|
+
// (e.g., superseded_by → cie-2020 concept). Without this, the UI
|
|
372
|
+
// renders related concepts as plain text with no clickable link.
|
|
373
|
+
if (r[GL.TARGET]) {
|
|
374
|
+
result.target = r[GL.TARGET];
|
|
375
|
+
}
|
|
376
|
+
|
|
370
377
|
if (r[GL.REF]) {
|
|
371
378
|
const ref = mapRefFromJsonLd(r[GL.REF]);
|
|
372
379
|
if (ref) result.ref = ref;
|