@glossarist/concept-browser 0.7.14 → 0.7.16

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glossarist/concept-browser",
3
- "version": "0.7.14",
3
+ "version": "0.7.16",
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": {
@@ -43,7 +43,8 @@ function extractReferences(concept, registerId) {
43
43
  return edges;
44
44
  }
45
45
 
46
- function extractDomains(concept, registerId) {
46
+ function extractDomains(concept, registerId, uriBase) {
47
+ const base = uriBase || 'https://glossarist.org';
47
48
  const edges = [];
48
49
  const sourceUri = concept['@id'];
49
50
  const lcs = concept['gl:localizedConcept'] || {};
@@ -55,7 +56,7 @@ function extractDomains(concept, registerId) {
55
56
  seen.add(domain);
56
57
  edges.push({
57
58
  source: sourceUri,
58
- target: `https://glossarist.org/${registerId}/domain/${slugify(domain)}`,
59
+ target: `${base}/${registerId}/domain/${slugify(domain)}`,
59
60
  type: 'domain',
60
61
  label: domain,
61
62
  register: registerId,
@@ -65,15 +66,38 @@ function extractDomains(concept, registerId) {
65
66
  return edges;
66
67
  }
67
68
 
68
- const EXTRACTORS = [extractReferences, extractDomains];
69
+ function extractRelated(concept, registerId, uriBase, urnMap) {
70
+ const edges = [];
71
+ const sourceUri = concept['@id'];
72
+ for (const r of concept['gl:related'] || []) {
73
+ const ref = r['gl:ref'];
74
+ if (!ref) continue;
75
+ const source = ref['gl:source'] || ref['source'];
76
+ const id = ref['gl:id'] || ref['id'];
77
+ if (!source || !id) continue;
78
+ const reg = urnMap.get(source) || source;
79
+ const target = `${uriBase}/${reg}/concept/${id}`;
80
+ if (target === sourceUri) continue;
81
+ edges.push({
82
+ source: sourceUri,
83
+ target,
84
+ type: r['gl:relationshipType'] || 'references',
85
+ label: r['gl:term'] || undefined,
86
+ register: reg,
87
+ });
88
+ }
89
+ return edges;
90
+ }
91
+
92
+ const EXTRACTORS = [extractReferences, extractRelated, extractDomains];
69
93
 
70
- function extractAllEdges(concept, registerId) {
71
- return EXTRACTORS.flatMap(fn => fn(concept, registerId));
94
+ function extractAllEdges(concept, registerId, uriBase, urnMap) {
95
+ return EXTRACTORS.flatMap(fn => fn(concept, registerId, uriBase, urnMap));
72
96
  }
73
97
 
74
98
  // --- Build ---
75
99
 
76
- function buildEdgesForDataset(datasetDir, registerId) {
100
+ function buildEdgesForDataset(datasetDir, registerId, uriBase, urnMap) {
77
101
  const conceptsDir = join(datasetDir, 'concepts');
78
102
  if (!existsSync(conceptsDir)) {
79
103
  console.log(` Skipping ${registerId}: no concepts directory`);
@@ -90,7 +114,7 @@ function buildEdgesForDataset(datasetDir, registerId) {
90
114
  for (const file of files) {
91
115
  try {
92
116
  const data = JSON.parse(readFileSync(join(conceptsDir, file), 'utf-8'));
93
- const edges = extractAllEdges(data, registerId);
117
+ const edges = extractAllEdges(data, registerId, uriBase, urnMap);
94
118
  allEdges.push(...edges);
95
119
 
96
120
  for (const edge of edges) {
@@ -170,12 +194,30 @@ const datasets = readdirSync(DATA_DIR).filter(f => {
170
194
  }
171
195
  });
172
196
 
197
+ // Build URN→datasetId map from all manifests
198
+ const urnMap = new Map();
199
+ const manifestCache = new Map();
173
200
  for (const ds of datasets) {
174
201
  const manifestPath = join(DATA_DIR, ds, 'manifest.json');
175
202
  try {
176
203
  const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
204
+ manifestCache.set(ds, manifest);
205
+ if (manifest.datasetUri) urnMap.set(manifest.datasetUri, ds);
206
+ for (const alias of manifest.uriAliases ?? []) {
207
+ const base = alias.endsWith('*') ? alias.slice(0, -1) : alias;
208
+ if (base.startsWith('urn:')) urnMap.set(base, ds);
209
+ }
210
+ } catch {}
211
+ }
212
+ console.log(`URN resolution map: ${[...urnMap.entries()].map(([k,v]) => `${k}→${v}`).join(', ')}\n`);
213
+
214
+ for (const ds of datasets) {
215
+ const manifest = manifestCache.get(ds);
216
+ if (!manifest) continue;
217
+ try {
177
218
  console.log(`${manifest.title} (${ds}):`);
178
- buildEdgesForDataset(join(DATA_DIR, ds), ds);
219
+ const uriBase = manifest.uriBase || 'https://glossarist.org';
220
+ buildEdgesForDataset(join(DATA_DIR, ds), ds, uriBase, urnMap);
179
221
  } catch (e) {
180
222
  console.error(`Error reading manifest for ${ds}: ${e.message}`);
181
223
  }
@@ -851,6 +851,18 @@ for (let i = 0; i < config.datasets.length; i++) {
851
851
  }
852
852
  writeJson(path.join(PUBLIC, 'datasets.json'), registry);
853
853
 
854
+ // Clean stale dataset directories not referenced in config
855
+ const configuredIds = new Set(config.datasets.map(d => d.id));
856
+ if (fs.existsSync(DATA)) {
857
+ for (const entry of fs.readdirSync(DATA)) {
858
+ if (!configuredIds.has(entry)) {
859
+ const stalePath = path.join(DATA, entry);
860
+ fs.rmSync(stalePath, { recursive: true, force: true });
861
+ console.log(` Removed stale data directory: ${entry}`);
862
+ }
863
+ }
864
+ }
865
+
854
866
  // Generate routing.json from site config
855
867
  writeJson(path.join(PUBLIC, 'routing.json'), config.routing || []);
856
868
  console.log('Generated routing.json');
@@ -386,6 +386,37 @@ describe('DatasetAdapter', () => {
386
386
  expect(edges[0].target).toBe('https://glossarist.org/isotc204/concept/3.1.1.6');
387
387
  expect(edges[0].label).toBe('entity');
388
388
  });
389
+
390
+ it('resolves URN-based ref.source to dataset ID via urnMap', () => {
391
+ const urnMap = new Map([
392
+ ['urn:oiml:pub:v:1:2000', 'viml-2000'],
393
+ ]);
394
+ adapter.setUrnMap(urnMap);
395
+
396
+ const concept = conceptFromJson({
397
+ '@id': 'https://metanorma.github.io/oiml-viml/viml-2022/concept/2.23',
398
+ '@type': 'gl:Concept',
399
+ 'gl:localizedConcept': {
400
+ eng: {
401
+ 'gl:languageCode': 'eng',
402
+ 'gl:designation': [{ '@type': 'gl:Expression', 'gl:term': 'measurement' }],
403
+ },
404
+ },
405
+ 'gl:related': [
406
+ {
407
+ 'gl:relationshipType': 'supersedes',
408
+ 'gl:ref': { 'gl:source': 'urn:oiml:pub:v:1:2000', 'gl:id': '2.23' },
409
+ 'gl:term': 'measurement',
410
+ },
411
+ ],
412
+ });
413
+
414
+ adapter.manifest = { uriBase: 'https://metanorma.github.io/oiml-viml' } as any;
415
+ const edges = adapter.extractEdges(concept);
416
+ expect(edges.length).toBe(1);
417
+ expect(edges[0].target).toBe('https://metanorma.github.io/oiml-viml/viml-2000/concept/2.23');
418
+ expect(edges[0].type).toBe('supersedes');
419
+ });
389
420
  });
390
421
 
391
422
  describe('extractDomainEdges', () => {
@@ -15,11 +15,14 @@ function slugify(text: string): string {
15
15
  return text.toLowerCase().replace(/[^\w\s-]/g, '').replace(/[\s/]+/g, '-');
16
16
  }
17
17
 
18
- function resolveRefTarget(rc: any, uriBase: string, registerId: string): string {
18
+ function resolveRefTarget(rc: any, uriBase: string, registerId: string, urnMap?: ReadonlyMap<string, string>): string {
19
19
  if (!rc.ref) return '';
20
20
  const ref = rc.ref;
21
21
  if (ref.id) {
22
- const reg = (ref.source && !ref.source.startsWith('http')) ? ref.source : registerId;
22
+ let reg = registerId;
23
+ if (ref.source && !ref.source.startsWith('http')) {
24
+ reg = urnMap?.get(ref.source) ?? ref.source;
25
+ }
23
26
  return `${uriBase}/${reg}/concept/${ref.id}`;
24
27
  }
25
28
  if (ref.source && ref.source.startsWith('http')) return ref.source;
@@ -28,6 +31,7 @@ function resolveRefTarget(rc: any, uriBase: string, registerId: string): string
28
31
 
29
32
  export class DatasetAdapter {
30
33
  private positionIndex = new Map<string, number>();
34
+ private _urnMap: ReadonlyMap<string, string> = new Map();
31
35
  readonly registerId: string;
32
36
  private baseUrl: string;
33
37
  manifest: Manifest | null = null;
@@ -294,6 +298,10 @@ export class DatasetAdapter {
294
298
  return scored;
295
299
  }
296
300
 
301
+ setUrnMap(map: ReadonlyMap<string, string>): void {
302
+ this._urnMap = map;
303
+ }
304
+
297
305
  extractEdges(concept: Concept): GraphEdge[] {
298
306
  const edges: GraphEdge[] = [];
299
307
  const uriBase = this.manifest?.uriBase || 'https://glossarist.org';
@@ -301,7 +309,7 @@ export class DatasetAdapter {
301
309
 
302
310
  // Managed concept level relationships
303
311
  for (const rc of concept.relatedConcepts) {
304
- const target = resolveRefTarget(rc, uriBase, this.registerId);
312
+ const target = resolveRefTarget(rc, uriBase, this.registerId, this._urnMap);
305
313
  if (target && target !== sourceUri) {
306
314
  const parsed = UriRouter.parseUri(target);
307
315
  edges.push({
@@ -319,7 +327,7 @@ export class DatasetAdapter {
319
327
  const lc = concept.localization(lang);
320
328
  if (!lc) continue;
321
329
  for (const rc of lc.related) {
322
- const target = resolveRefTarget(rc, uriBase, this.registerId);
330
+ const target = resolveRefTarget(rc, uriBase, this.registerId, this._urnMap);
323
331
  if (target && target !== sourceUri) {
324
332
  const parsed = UriRouter.parseUri(target);
325
333
  edges.push({
@@ -6,6 +6,7 @@ import { ReferenceResolver } from './ReferenceResolver';
6
6
 
7
7
  export class AdapterFactory {
8
8
  private adapters = new Map<string, DatasetAdapter>();
9
+ private urnMap = new Map<string, string>();
9
10
  readonly router = new UriRouter();
10
11
  readonly resolver: ReferenceResolver;
11
12
 
@@ -62,6 +63,18 @@ export class AdapterFactory {
62
63
  this.resolver.registerSourceRef(alias, registerId, manifest.datasetUri);
63
64
  }
64
65
 
66
+ // Build URN→datasetId map from manifest
67
+ if (manifest.datasetUri) this.urnMap.set(manifest.datasetUri, registerId);
68
+ for (const alias of manifest.uriAliases ?? []) {
69
+ const base = alias.endsWith('*') ? alias.slice(0, -1) : alias;
70
+ if (base.startsWith('urn:')) this.urnMap.set(base, registerId);
71
+ }
72
+
73
+ // Distribute the updated URN map to all loaded adapters
74
+ for (const adapter of this.adapters.values()) {
75
+ adapter.setUrnMap(this.urnMap);
76
+ }
77
+
65
78
  return adapter;
66
79
  }
67
80
 
@@ -156,8 +156,22 @@ function mapSourceFromJsonLd(s: any): Record<string, unknown> {
156
156
 
157
157
  function mapRelatedFromJsonLd(r: any): Record<string, unknown> {
158
158
  const result: Record<string, unknown> = { type: 'references' };
159
- if (r['@id']) {
160
- // Extract concept ID from URI: ".../concept/3.1.1.6" → source=register, id=3.1.1.6
159
+
160
+ if (r['gl:relationshipType']) {
161
+ result.type = r['gl:relationshipType'];
162
+ }
163
+
164
+ if (r['gl:ref']) {
165
+ const ref = r['gl:ref'];
166
+ const refObj: Record<string, unknown> = {};
167
+ if (ref['gl:source']) refObj.source = ref['gl:source'];
168
+ if (ref['gl:id']) refObj.id = ref['gl:id'];
169
+ if (ref['source']) refObj.source = ref['source'];
170
+ if (ref['id']) refObj.id = ref['id'];
171
+ if (Object.keys(refObj).length > 0) result.ref = refObj;
172
+ }
173
+
174
+ if (!result.ref && r['@id']) {
161
175
  const uri = r['@id'] as string;
162
176
  const idMatch = uri.match(/\/concept\/([^/]+)$/);
163
177
  result.ref = idMatch
@@ -237,7 +251,7 @@ function conceptFromJsonLd(doc: Record<string, any>): Concept {
237
251
  }
238
252
  }
239
253
 
240
- const related = (doc['gl:references'] ?? []).map(mapRelatedFromJsonLd);
254
+ const related = (doc['gl:related'] ?? []).map(mapRelatedFromJsonLd);
241
255
  const tags = Array.isArray(doc['gl:tags']) ? [...doc['gl:tags']] : [];
242
256
 
243
257
  return Concept.fromJSON({
@@ -98,7 +98,25 @@ const conceptSources = computed(() => props.concept.sources);
98
98
  const conceptTags = computed(() => props.concept.tags ?? []);
99
99
 
100
100
  // Managed concept related (concept-level cross-references)
101
- const conceptRelated = computed(() => props.concept.relatedConcepts ?? []);
101
+ // Derives superseded_by from incoming supersedes edges instead of storing it.
102
+ const conceptRelated = computed(() => {
103
+ const direct = props.concept.relatedConcepts?.filter(rc => rc.type !== 'superseded_by') ?? [];
104
+ const derived = incomingEdges.value
105
+ .filter(e => e.type === 'supersedes')
106
+ .map(e => {
107
+ const parsed = factory.resolve(e.source, props.registerId);
108
+ const sourceUrn = parsed.type === 'internal'
109
+ ? store.manifests.get(parsed.registerId)?.datasetUri
110
+ : null;
111
+ const conceptId = e.source.match(/\/concept\/([^/]+)$/)?.[1];
112
+ return {
113
+ type: 'superseded_by' as const,
114
+ ref: sourceUrn && conceptId ? { source: sourceUrn, id: conceptId } : null,
115
+ content: '',
116
+ };
117
+ });
118
+ return [...direct, ...derived];
119
+ });
102
120
 
103
121
  function resolveRelatedRef(ref: { source: string | null; id: string | null } | null): { registerId: string; conceptId: string } | null {
104
122
  if (!ref?.source || !ref?.id) return null;