@glossarist/concept-browser 0.7.34 → 0.7.35

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.34",
3
+ "version": "0.7.35",
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": {
@@ -242,10 +242,6 @@ describe('ConceptDetail interactions', () => {
242
242
  // Register the URI pattern via factory so it resolves as internal
243
243
  const { getFactory } = await import('../adapters/factory');
244
244
  const factory = getFactory();
245
- factory.router.registerDataset('test', '/data/test', {
246
- ...makeManifest(),
247
- uriBase: 'https://glossarist.org',
248
- });
249
245
  factory.resolver.registerDataset('test', ['https://glossarist.org/test/concept/*']);
250
246
 
251
247
  const wrapper = mountDetail(json);
@@ -9,10 +9,9 @@ import type {
9
9
  SectionNode,
10
10
  DatasetSummary,
11
11
  } from './types';
12
- import type { Concept, LocalizedConcept, Designation, RelatedConcept } from 'glossarist';
13
- import { conceptFromJson, conceptUri } from './model-bridge';
14
- import { UriRouter } from './UriRouter';
15
- import { slugify } from '../utils/slugify';
12
+ import type { Concept } from 'glossarist';
13
+ import { conceptFromJson } from './model-bridge';
14
+ import { GraphDataSource } from './GraphDataSource';
16
15
 
17
16
  // ── Wire-format types for JSON responses ────────────────────────────────────
18
17
 
@@ -33,36 +32,6 @@ interface IndexConceptJson {
33
32
  groups?: string[];
34
33
  }
35
34
 
36
- interface DomainNodeJson {
37
- uri?: string;
38
- id?: string;
39
- registerId?: string;
40
- label?: string;
41
- names?: Record<string, string>;
42
- conceptCount?: number;
43
- children?: DomainNodeJson[];
44
- }
45
-
46
- interface SectionJson {
47
- id: string;
48
- names?: Record<string, string>;
49
- children?: SectionJson[];
50
- }
51
-
52
- function resolveRefTarget(rc: RelatedConcept, uriBase: string, registerId: string, urnMap?: ReadonlyMap<string, string>): string {
53
- if (!rc.ref) return '';
54
- const ref = rc.ref;
55
- if (ref.id) {
56
- let reg = registerId;
57
- if (ref.source && !ref.source.startsWith('http')) {
58
- reg = urnMap?.get(ref.source) ?? ref.source;
59
- }
60
- return `${uriBase}/${reg}/concept/${ref.id}`;
61
- }
62
- if (ref.source && ref.source.startsWith('http')) return ref.source;
63
- return ref.source || '';
64
- }
65
-
66
35
  export class DatasetAdapter {
67
36
  private positionIndex = new Map<string, number>();
68
37
  private _urnMap: ReadonlyMap<string, string> = new Map();
@@ -376,131 +345,42 @@ export class DatasetAdapter {
376
345
  this._urnMap = map;
377
346
  }
378
347
 
379
- extractEdges(concept: Concept): GraphEdge[] {
380
- const edges: GraphEdge[] = [];
381
- const uriBase = this.manifest?.uriBase || 'https://glossarist.org';
382
- const sourceUri = concept.uri || `${uriBase}/${this.registerId}/concept/${concept.id}`;
383
-
384
- // Managed concept level relationships
385
- for (const rc of concept.relatedConcepts) {
386
- const target = resolveRefTarget(rc, uriBase, this.registerId, this._urnMap);
387
- if (target && target !== sourceUri) {
388
- const parsed = UriRouter.parseUri(target);
389
- edges.push({
390
- source: sourceUri,
391
- target,
392
- type: rc.type || 'references',
393
- label: rc.content || undefined,
394
- register: parsed?.registerId ?? this.registerId,
395
- });
396
- }
397
- }
398
-
399
- // Per-localization references (from inline extraction in generate-data)
400
- for (const lang of concept.languages) {
401
- const lc = concept.localization(lang);
402
- if (!lc) continue;
403
- for (const rc of lc.related) {
404
- const target = resolveRefTarget(rc, uriBase, this.registerId, this._urnMap);
405
- if (target && target !== sourceUri) {
406
- const parsed = UriRouter.parseUri(target);
407
- edges.push({
408
- source: sourceUri,
409
- target,
410
- type: rc.type || 'references',
411
- label: rc.content || undefined,
412
- register: parsed?.registerId ?? this.registerId,
413
- lang,
414
- });
415
- }
416
- }
417
- }
418
-
419
- return edges;
420
- }
421
-
422
- extractDomainEdges(concept: Concept): GraphEdge[] {
423
- const edges: GraphEdge[] = [];
424
- const uriBase = this.manifest?.uriBase || 'https://glossarist.org';
425
- const sourceUri = concept.uri || `${uriBase}/${this.registerId}/concept/${concept.id}`;
426
-
427
- for (const lang of concept.languages) {
428
- const lc = concept.localization(lang);
429
- if (lc?.domain) {
430
- edges.push({
431
- source: sourceUri,
432
- target: `${uriBase}/${this.registerId}/domain/${slugify(lc.domain)}`,
433
- type: 'domain',
434
- label: lc.domain,
435
- register: this.registerId,
436
- lang,
437
- });
438
- }
439
- }
440
- return edges;
348
+ get dataUrl(): string {
349
+ return this.baseUrl;
441
350
  }
442
351
 
443
- async loadDomainNodes(): Promise<GraphNode[]> {
444
- const resp = await fetch(`${this.baseUrl}/domain-nodes.json`);
445
- if (!resp.ok) return [];
446
- const data = await resp.json();
447
- return (data.domainNodes || []).map((dn: DomainNodeJson) => this.mapDomainNode(dn));
352
+ get urnMap(): ReadonlyMap<string, string> {
353
+ return this._urnMap;
448
354
  }
449
355
 
450
- private mapDomainNode(dn: DomainNodeJson): GraphNode {
451
- const node: GraphNode = {
452
- uri: dn.uri ?? '',
453
- register: dn.registerId ?? '',
454
- conceptId: dn.uri?.split('/domain/')[1] || dn.id || '',
455
- designations: dn.names || (dn.label ? { eng: dn.label } : {}),
456
- status: 'domain',
457
- loaded: true,
458
- nodeType: 'domain' as const,
459
- conceptCount: dn.conceptCount || 0,
460
- };
461
- if (dn.children && dn.children.length > 0) {
462
- node.children = dn.children.map((c) => this.mapSectionNode(c));
463
- }
464
- return node;
356
+ private _graphDataSource: GraphDataSource | null = null;
357
+ get graphDataSource(): GraphDataSource {
358
+ if (!this._graphDataSource) this._graphDataSource = new GraphDataSource(this);
359
+ return this._graphDataSource;
465
360
  }
466
361
 
467
- private mapSectionNode(dn: DomainNodeJson): SectionNode {
468
- const node: SectionNode = {
469
- id: dn.id ?? '',
470
- names: dn.names || (dn.label ? { eng: dn.label } : {}),
471
- conceptCount: dn.conceptCount || 0,
472
- };
473
- if (dn.children && dn.children.length > 0) {
474
- node.children = dn.children.map((c) => this.mapSectionNode(c));
475
- }
476
- return node;
362
+ extractEdges(concept: Concept): GraphEdge[] {
363
+ return this.graphDataSource.extractEdges(concept);
477
364
  }
478
365
 
479
- getSectionTree(): SectionNode[] {
480
- const nodes = this.manifest?.sections;
481
- if (!nodes || nodes.length === 0) return [];
482
- return nodes.map(s => this.mapManifestSection(s));
366
+ extractDomainEdges(concept: Concept): GraphEdge[] {
367
+ return this.graphDataSource.extractDomainEdges(concept);
483
368
  }
484
369
 
485
- private mapManifestSection(s: SectionJson): SectionNode {
486
- const node: SectionNode = { id: s.id, names: s.names || {}, conceptCount: 0 };
487
- if (s.children && s.children.length > 0) {
488
- node.children = s.children.map(c => this.mapManifestSection(c));
489
- }
490
- return node;
370
+ async loadDomainNodes(): Promise<GraphNode[]> {
371
+ return this.graphDataSource.loadDomainNodes();
491
372
  }
492
373
 
493
374
  async loadEdgeIndex(): Promise<GraphEdge[]> {
494
- const resp = await fetch(`${this.baseUrl}/edges.json`);
495
- if (!resp.ok) return [];
496
- const data = await resp.json();
497
- return data.edges ?? [];
375
+ return this.graphDataSource.loadEdgeIndex();
498
376
  }
499
377
 
500
378
  async loadGraphNodes(): Promise<{ uriPrefix: string; nodes: [string, Record<string, string>, string][] }> {
501
- const resp = await fetch(`${this.baseUrl}/graph-nodes.json`);
502
- if (!resp.ok) return { uriPrefix: '', nodes: [] };
503
- return await resp.json();
379
+ return this.graphDataSource.loadGraphNodes();
380
+ }
381
+
382
+ getSectionTree(): SectionNode[] {
383
+ return this.graphDataSource.getSectionTree();
504
384
  }
505
385
 
506
386
  getLanguages(): string[] {
@@ -0,0 +1,178 @@
1
+ import type { GraphEdge, GraphNode, SectionNode } from './types';
2
+ import type { Concept, RelatedConcept } from 'glossarist';
3
+ import type { DatasetAdapter } from './DatasetAdapter';
4
+ import { ReferenceResolver } from './ReferenceResolver';
5
+ import { slugify } from '../utils/slugify';
6
+
7
+ interface DomainNodeJson {
8
+ uri?: string;
9
+ id?: string;
10
+ registerId?: string;
11
+ label?: string;
12
+ names?: Record<string, string>;
13
+ conceptCount?: number;
14
+ children?: DomainNodeJson[];
15
+ }
16
+
17
+ interface SectionJson {
18
+ id: string;
19
+ names?: Record<string, string>;
20
+ children?: SectionJson[];
21
+ }
22
+
23
+ function resolveRefTarget(rc: RelatedConcept, uriBase: string, registerId: string, urnMap?: ReadonlyMap<string, string>): string {
24
+ if (!rc.ref) return '';
25
+ const ref = rc.ref;
26
+ if (ref.id) {
27
+ let reg = registerId;
28
+ if (ref.source && !ref.source.startsWith('http')) {
29
+ reg = urnMap?.get(ref.source) ?? ref.source;
30
+ }
31
+ return `${uriBase}/${reg}/concept/${ref.id}`;
32
+ }
33
+ if (ref.source && ref.source.startsWith('http')) return ref.source;
34
+ return ref.source || '';
35
+ }
36
+
37
+ export class GraphDataSource {
38
+ constructor(private adapter: DatasetAdapter) {}
39
+
40
+ private get baseUrl(): string {
41
+ return this.adapter.dataUrl;
42
+ }
43
+
44
+ private get registerId(): string {
45
+ return this.adapter.registerId;
46
+ }
47
+
48
+ private get uriBase(): string {
49
+ return this.adapter.manifest?.uriBase || 'https://glossarist.org';
50
+ }
51
+
52
+ private get urnMap(): ReadonlyMap<string, string> {
53
+ return this.adapter.urnMap;
54
+ }
55
+
56
+ async loadEdgeIndex(): Promise<GraphEdge[]> {
57
+ const resp = await fetch(`${this.baseUrl}/edges.json`);
58
+ if (!resp.ok) return [];
59
+ const data = await resp.json();
60
+ return data.edges ?? [];
61
+ }
62
+
63
+ async loadGraphNodes(): Promise<{ uriPrefix: string; nodes: [string, Record<string, string>, string][] }> {
64
+ const resp = await fetch(`${this.baseUrl}/graph-nodes.json`);
65
+ if (!resp.ok) return { uriPrefix: '', nodes: [] };
66
+ return await resp.json();
67
+ }
68
+
69
+ async loadDomainNodes(): Promise<GraphNode[]> {
70
+ const resp = await fetch(`${this.baseUrl}/domain-nodes.json`);
71
+ if (!resp.ok) return [];
72
+ const data = await resp.json();
73
+ return (data.domainNodes || []).map((dn: DomainNodeJson) => this.mapDomainNode(dn));
74
+ }
75
+
76
+ extractEdges(concept: Concept): GraphEdge[] {
77
+ const edges: GraphEdge[] = [];
78
+ const sourceUri = concept.uri || `${this.uriBase}/${this.registerId}/concept/${concept.id}`;
79
+
80
+ for (const rc of concept.relatedConcepts) {
81
+ const target = resolveRefTarget(rc, this.uriBase, this.registerId, this.urnMap);
82
+ if (target && target !== sourceUri) {
83
+ const parsed = ReferenceResolver.parseUri(target);
84
+ edges.push({
85
+ source: sourceUri,
86
+ target,
87
+ type: rc.type || 'references',
88
+ label: rc.content || undefined,
89
+ register: parsed?.registerId ?? this.registerId,
90
+ });
91
+ }
92
+ }
93
+
94
+ for (const lang of concept.languages) {
95
+ const lc = concept.localization(lang);
96
+ if (!lc) continue;
97
+ for (const rc of lc.related) {
98
+ const target = resolveRefTarget(rc, this.uriBase, this.registerId, this.urnMap);
99
+ if (target && target !== sourceUri) {
100
+ const parsed = ReferenceResolver.parseUri(target);
101
+ edges.push({
102
+ source: sourceUri,
103
+ target,
104
+ type: rc.type || 'references',
105
+ label: rc.content || undefined,
106
+ register: parsed?.registerId ?? this.registerId,
107
+ lang,
108
+ });
109
+ }
110
+ }
111
+ }
112
+
113
+ return edges;
114
+ }
115
+
116
+ extractDomainEdges(concept: Concept): GraphEdge[] {
117
+ const edges: GraphEdge[] = [];
118
+ const sourceUri = concept.uri || `${this.uriBase}/${this.registerId}/concept/${concept.id}`;
119
+
120
+ for (const lang of concept.languages) {
121
+ const lc = concept.localization(lang);
122
+ if (lc?.domain) {
123
+ edges.push({
124
+ source: sourceUri,
125
+ target: `${this.uriBase}/${this.registerId}/domain/${slugify(lc.domain)}`,
126
+ type: 'domain',
127
+ label: lc.domain,
128
+ register: this.registerId,
129
+ lang,
130
+ });
131
+ }
132
+ }
133
+ return edges;
134
+ }
135
+
136
+ getSectionTree(): SectionNode[] {
137
+ const nodes = this.adapter.manifest?.sections;
138
+ if (!nodes || nodes.length === 0) return [];
139
+ return nodes.map(s => this.mapManifestSection(s));
140
+ }
141
+
142
+ private mapDomainNode(dn: DomainNodeJson): GraphNode {
143
+ const node: GraphNode = {
144
+ uri: dn.uri ?? '',
145
+ register: dn.registerId ?? '',
146
+ conceptId: dn.uri?.split('/domain/')[1] || dn.id || '',
147
+ designations: dn.names || (dn.label ? { eng: dn.label } : {}),
148
+ status: 'domain',
149
+ loaded: true,
150
+ nodeType: 'domain' as const,
151
+ conceptCount: dn.conceptCount || 0,
152
+ };
153
+ if (dn.children && dn.children.length > 0) {
154
+ node.children = dn.children.map((c) => this.mapSectionNode(c));
155
+ }
156
+ return node;
157
+ }
158
+
159
+ private mapSectionNode(dn: DomainNodeJson): SectionNode {
160
+ const node: SectionNode = {
161
+ id: dn.id ?? '',
162
+ names: dn.names || (dn.label ? { eng: dn.label } : {}),
163
+ conceptCount: dn.conceptCount || 0,
164
+ };
165
+ if (dn.children && dn.children.length > 0) {
166
+ node.children = dn.children.map((c) => this.mapSectionNode(c));
167
+ }
168
+ return node;
169
+ }
170
+
171
+ private mapManifestSection(s: SectionJson): SectionNode {
172
+ const node: SectionNode = { id: s.id, names: s.names || {}, conceptCount: 0 };
173
+ if (s.children && s.children.length > 0) {
174
+ node.children = s.children.map(c => this.mapManifestSection(c));
175
+ }
176
+ return node;
177
+ }
178
+ }
@@ -32,6 +32,13 @@ export class ReferenceResolver {
32
32
  private routing: RoutingEntry[] = [];
33
33
  private sourceRefs = new Map<string, { datasetId: string; uriPrefix: string }>();
34
34
 
35
+ private static readonly URI_REGISTER_RE = /\/([^/]+)\/concept\/([^/]+)$/;
36
+
37
+ static parseUri(uri: string): { registerId: string; conceptId: string } | null {
38
+ const m = uri.match(ReferenceResolver.URI_REGISTER_RE);
39
+ return m ? { registerId: m[1], conceptId: m[2] } : null;
40
+ }
41
+
35
42
  registerDataset(id: string, uriPatterns: string[]): void {
36
43
  this.datasets.push({ id, uriPatterns });
37
44
  }
@@ -115,6 +122,23 @@ export class ReferenceResolver {
115
122
  return null;
116
123
  }
117
124
 
125
+ resolveRelatedRef(ref: { source: string | null; id: string | null } | null, sourceDatasetId?: string): { registerId: string; conceptId: string } | null {
126
+ if (!ref?.source || !ref?.id) return null;
127
+ const uri = `${ref.source}/${ref.id}`;
128
+ const resolution = this.resolveReference(uri, sourceDatasetId);
129
+ if (resolution.type === 'internal') {
130
+ return { registerId: resolution.registerId, conceptId: resolution.conceptId.replace(/^\//, '') };
131
+ }
132
+ if (ref.source.startsWith('urn:')) {
133
+ const directUri = ref.source + ref.id;
134
+ const directRes = this.resolveReference(directUri, sourceDatasetId);
135
+ if (directRes.type === 'internal') {
136
+ return { registerId: directRes.registerId, conceptId: directRes.conceptId.replace(/^\//, '') };
137
+ }
138
+ }
139
+ return null;
140
+ }
141
+
118
142
  private extractConceptIdFromRouting(uri: string, pattern: string): string | null {
119
143
  for (const ds of this.datasets) {
120
144
  for (const dsPattern of ds.uriPatterns) {
@@ -1,13 +1,11 @@
1
1
  import type { DatasetRegistry, Manifest, Resolution } from './types';
2
2
  import type { RoutingEntry as ConfigRoutingEntry } from '../config/types';
3
3
  import { DatasetAdapter } from './DatasetAdapter';
4
- import { UriRouter } from './UriRouter';
5
4
  import { ReferenceResolver } from './ReferenceResolver';
6
5
 
7
6
  export class AdapterFactory {
8
7
  private adapters = new Map<string, DatasetAdapter>();
9
8
  private urnMap = new Map<string, string>();
10
- readonly router = new UriRouter();
11
9
  readonly resolver: ReferenceResolver;
12
10
  private crossRefIndex: Record<string, string[]> | null = null;
13
11
 
@@ -76,8 +74,6 @@ export class AdapterFactory {
76
74
  }
77
75
 
78
76
  private registerDataset(registerId: string, manifest: Manifest): void {
79
- this.router.registerDataset(registerId, `${import.meta.env.BASE_URL}data/${registerId}`, manifest);
80
-
81
77
  const uriPatterns = [
82
78
  manifest.datasetUri,
83
79
  ...(manifest.uriAliases ?? []),
@@ -117,20 +113,7 @@ export class AdapterFactory {
117
113
  }
118
114
 
119
115
  resolveRelatedRef(ref: { source: string | null; id: string | null } | null, sourceDatasetId?: string): { registerId: string; conceptId: string } | null {
120
- if (!ref?.source || !ref?.id) return null;
121
- const uri = `${ref.source}/${ref.id}`;
122
- const resolution = this.resolve(uri, sourceDatasetId);
123
- if (resolution.type === 'internal') {
124
- return { registerId: resolution.registerId, conceptId: resolution.conceptId.replace(/^\//, '') };
125
- }
126
- if (ref.source.startsWith('urn:')) {
127
- const directUri = ref.source + ref.id;
128
- const directRes = this.resolve(directUri, sourceDatasetId);
129
- if (directRes.type === 'internal') {
130
- return { registerId: directRes.registerId, conceptId: directRes.conceptId.replace(/^\//, '') };
131
- }
132
- }
133
- return null;
116
+ return this.resolver.resolveRelatedRef(ref, sourceDatasetId);
134
117
  }
135
118
 
136
119
  resolveCitation(source: string, referenceFrom: string, sourceDatasetId?: string): Resolution | null {
@@ -1,21 +1,21 @@
1
1
  <script setup lang="ts">
2
- import type { Concept, LocalizedConcept, Designation, ConceptSource } from 'glossarist';
2
+ import type { Concept, LocalizedConcept, Designation } from 'glossarist';
3
3
  import type { Manifest, GraphEdge } from '../adapters/types';
4
4
  import { computed, ref, nextTick, watch } from 'vue';
5
- import { langName, langLabel, sortLanguages } from '../utils/lang';
6
- import { renderMath, cleanContent } from '../utils/math';
5
+ import { langName } from '../utils/lang';
6
+ import { renderMath } from '../utils/math';
7
7
  import type { RenderOptions } from '../utils/math';
8
8
  import { escapeAttr } from '../utils/escape';
9
9
  import { entryStatusColor, conceptStatusColor, conceptStatusLabel, conceptStatusDefinition, entryStatusLabel, entryStatusDefinition, getPreferredTerm } from '../utils/concept-helpers';
10
10
  import { sourceTypeInfo, sourceStatusInfo } from '../utils/designation-registry';
11
- import { conceptUri, getAnnotations } from '../adapters/model-bridge';
11
+ import { conceptUri } from '../adapters/model-bridge';
12
12
  import { useRouter } from 'vue-router';
13
13
  import { useVocabularyStore } from '../stores/vocabulary';
14
14
  import { useDsStyle } from '../utils/dataset-style';
15
15
  import { getFactory } from '../adapters/factory';
16
16
  import { useRenderOptions } from '../composables/use-render-options';
17
17
  import { useConceptEdges } from '../composables/use-concept-edges';
18
- import { useConceptContent, type LangContent } from '../composables/use-concept-content';
18
+ import { useConceptContent } from '../composables/use-concept-content';
19
19
  import { relationshipLabel, INVERSE_RELATIONSHIPS } from '../utils/relationship-categories';
20
20
  import { slugify } from '../utils/slugify';
21
21
  import { useSiteConfig } from '../config/use-site-config';
@@ -83,21 +83,6 @@ function copyUri() {
83
83
  });
84
84
  }
85
85
 
86
- const languages = computed(() => {
87
- const sorted = sortLanguages(props.concept.languages, props.manifest.languageOrder);
88
- // Put current UI locale first
89
- const current = locale.value;
90
- const idx = sorted.indexOf(current);
91
- if (idx > 0) {
92
- sorted.splice(idx, 1);
93
- sorted.unshift(current);
94
- }
95
- return sorted;
96
- });
97
-
98
- // Collapsible language sections — expand all with content, collapse those without
99
- const collapsedLangs = ref(new Set<string>());
100
-
101
86
  const engConcept = computed((): LocalizedConcept | null => {
102
87
  return props.concept.localization('eng') ?? null;
103
88
  });
@@ -115,12 +100,10 @@ const conceptSources = computed(() => props.concept.sources);
115
100
 
116
101
  const conceptTags = computed(() => props.concept.tags ?? []);
117
102
 
118
- // Cross-reference resolver: generates clickable links for inline refs
119
-
120
103
  const factory = getFactory();
121
104
  const { ensureBibLoaded, bibResolver, figResolver } = useRenderOptions(() => props.registerId);
122
105
 
123
- const renderOpts: RenderOptions = {
106
+ const renderOpts = computed<RenderOptions>(() => ({
124
107
  xrefResolver: (uri, term) => {
125
108
  const resolution = factory.resolve(uri, props.registerId);
126
109
  if (resolution.type === 'internal') {
@@ -139,11 +122,10 @@ const renderOpts: RenderOptions = {
139
122
  },
140
123
  bibResolver,
141
124
  figResolver,
142
- };
125
+ }));
143
126
 
144
127
  watch(() => props.registerId, () => { ensureBibLoaded(); }, { immediate: true });
145
128
 
146
- // Handle clicks on cross-reference links via event delegation
147
129
  function handleContentClick(e: MouseEvent) {
148
130
  const target = (e.target as HTMLElement).closest('.xref-link') as HTMLElement | null;
149
131
  if (!target) return;
@@ -155,86 +137,18 @@ function handleContentClick(e: MouseEvent) {
155
137
  }
156
138
  }
157
139
 
158
- // LangContent type is imported from the composable
159
-
160
- const allLangContent = computed(() => {
161
- const result: LangContent[] = [];
162
- for (const lang of languages.value) {
163
- const lc = props.concept.localization(lang);
164
- if (!lc) continue;
165
-
166
- const definition = lc.definitions
167
- .map(d => d.content).filter(Boolean).join('\n\n');
168
- const annotations = getAnnotations(lc).map(a => a.content).filter(Boolean);
169
- const notes = lc.notes.map(n => n.content).filter(Boolean);
170
- const examples = lc.examples.map(e => e.content).filter(Boolean);
171
-
172
- result.push({
173
- lang,
174
- lc,
175
- renderedTerm: renderMath(getPreferredTerm(lc, '')),
176
- definition,
177
- renderedDefinition: renderMath(definition, renderOpts),
178
- annotations,
179
- renderedAnnotations: annotations.map((a: string) => renderMath(a, renderOpts)),
180
- notes,
181
- renderedNotes: notes.map(n => renderMath(n, renderOpts)),
182
- examples,
183
- renderedExamples: examples.map(e => renderMath(e, renderOpts)),
184
- sources: lc.sources,
185
- designations: lc.terms,
186
- renderedDesignations: new Map(lc.terms.map(d => [d.designation, renderMath(d.designation)])),
187
- entryStatus: lc.entryStatus ?? '',
188
- classification: lc.classification,
189
- reviewType: lc.reviewType,
190
- release: lc.release,
191
- lineageSourceSimilarity: lc.lineageSourceSimilarity,
192
- lcScript: lc.script,
193
- lcSystem: lc.system,
194
- });
195
- }
196
- return result;
197
- });
198
-
199
- const langContentMap = computed(() => {
200
- const map = new Map<string, LangContent>();
201
- for (const lc of allLangContent.value) map.set(lc.lang, lc);
202
- return map;
203
- });
204
-
205
- function hasContent(lc: LangContent): boolean {
206
- return !!(lc.definition || lc.annotations.length || lc.notes.length || lc.examples.length || lc.sources.length);
207
- }
208
-
209
- function initCollapsed() {
210
- const mainLangs = siteConfig.value?.defaults?.mainLanguages || [];
211
- const mainSet = new Set(mainLangs.length ? mainLangs : ['eng']);
212
- const collapsed = new Set<string>();
213
- for (const lc of allLangContent.value) {
214
- if (!hasContent(lc) && !mainSet.has(lc.lang)) {
215
- collapsed.add(lc.lang);
216
- }
217
- }
218
- collapsedLangs.value = collapsed;
219
- }
220
-
221
- watch(languages, () => { initCollapsed(); }, { immediate: true });
222
-
223
- const allCollapsed = computed(() => collapsedLangs.value.size === allLangContent.value.length);
224
-
225
- function toggleLang(lang: string) {
226
- const s = new Set(collapsedLangs.value);
227
- if (s.has(lang)) s.delete(lang); else s.add(lang);
228
- collapsedLangs.value = s;
229
- }
230
-
231
- function toggleAll() {
232
- if (allCollapsed.value) {
233
- collapsedLangs.value = new Set();
234
- } else {
235
- collapsedLangs.value = new Set(allLangContent.value.map(lc => lc.lang));
236
- }
237
- }
140
+ const {
141
+ languages,
142
+ allLangContent,
143
+ langContentMap,
144
+ hasContent,
145
+ collapsedLangs,
146
+ allCollapsed,
147
+ toggleLang,
148
+ toggleAll,
149
+ plainTruncate,
150
+ orderedDesignations,
151
+ } = useConceptContent(conceptComputed, manifestComputed, renderOpts);
238
152
 
239
153
  function scrollToLang(lang: string) {
240
154
  if (collapsedLangs.value.has(lang)) {
@@ -263,14 +177,6 @@ function getDesignationsForLang(lang: string): Designation[] {
263
177
  return lc?.terms ?? [];
264
178
  }
265
179
 
266
- function orderedDesignations(lang: string): Designation[] {
267
- const desigs = getDesignationsForLang(lang);
268
- const preferred = desigs.filter(d => d.normativeStatus === 'preferred');
269
- const admitted = desigs.filter(d => d.normativeStatus === 'admitted' || d.normativeStatus === 'deprecated');
270
- const rest = desigs.filter(d => d.normativeStatus !== 'preferred' && d.normativeStatus !== 'admitted' && d.normativeStatus !== 'deprecated');
271
- return [...preferred, ...admitted, ...rest];
272
- }
273
-
274
180
  function hasDefinition(lang: string): boolean {
275
181
  const lc = props.concept.localization(lang);
276
182
  if (!lc) return false;
@@ -282,16 +188,9 @@ function goAdjacent(id: string) {
282
188
  window.scrollTo({ top: 0, behavior: 'smooth' });
283
189
  }
284
190
 
285
- function plainTruncate(html: string, max: number = 120): string {
286
- const text = cleanContent(html).replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();
287
- return text.length <= max ? text : text.slice(0, max).trimEnd() + '…';
288
- }
289
-
290
- // Domain rendering: merge ConceptReference domains and per-localization domain strings
291
191
  const conceptDomains = computed(() => {
292
192
  const domainMap = new Map<string, { slug: string; label: string; langs: string[]; conceptId?: string }>();
293
193
 
294
- // Managed concept level ConceptReference domains (authoritative)
295
194
  for (const ref of conceptRefDomains.value) {
296
195
  const id = ref.conceptId ?? '';
297
196
  const label = id || ref.urn || '';
@@ -301,7 +200,6 @@ const conceptDomains = computed(() => {
301
200
  }
302
201
  }
303
202
 
304
- // Per-localization domain strings
305
203
  for (const lang of props.concept.languages) {
306
204
  const lc = props.concept.localization(lang);
307
205
  const domain = lc?.domain;
@@ -318,7 +216,6 @@ const conceptDomains = computed(() => {
318
216
  return [...domainMap.values()].sort((a, b) => b.langs.length - a.langs.length);
319
217
  });
320
218
 
321
- // Non-verbal reps: aggregate across all localizations
322
219
  const nonVerbalReps = computed(() => {
323
220
  const reps: typeof import('glossarist').NonVerbRep.prototype[] = [];
324
221
  for (const lang of props.concept.languages) {
@@ -329,7 +226,6 @@ const nonVerbalReps = computed(() => {
329
226
  }
330
227
  return reps;
331
228
  });
332
-
333
229
  </script>
334
230
 
335
231
  <template>
@@ -26,51 +26,6 @@ export function compactToSlug(compact: string): string {
26
26
  return compact.replace(/:/g, '-');
27
27
  }
28
28
 
29
- const expandedClasses = ref(new Set<string>(['gloss:Designation']));
30
-
31
- const collapsedSections = ref(new Set<string>([
32
- 'objectProperty',
33
- 'datatypeProperty',
34
- 'shape',
35
- 'taxonomy',
36
- 'namedIndividual',
37
- 'annotationProperty',
38
- ]));
39
-
40
- const searchQuery = ref('');
41
-
42
- function toggleExpand(cls: OwlClass) {
43
- const s = new Set(expandedClasses.value);
44
- if (s.has(cls.compact)) s.delete(cls.compact);
45
- else s.add(cls.compact);
46
- expandedClasses.value = s;
47
- }
48
-
49
- function toggleSection(key: string) {
50
- const s = new Set(collapsedSections.value);
51
- if (s.has(key)) s.delete(key);
52
- else s.add(key);
53
- collapsedSections.value = s;
54
- }
55
-
56
- function expandAllSections() {
57
- collapsedSections.value = new Set();
58
- }
59
-
60
- function collapseAllSections() {
61
- collapsedSections.value = new Set(['objectProperty', 'datatypeProperty', 'shape', 'taxonomy', 'class', 'namedIndividual', 'annotationProperty']);
62
- }
63
-
64
- function childClasses(parentId: string): OwlClass[] {
65
- const cls = getClass(parentId);
66
- if (!cls) return [];
67
- return cls.children.map(id => getClass(id)).filter((c): c is OwlClass => !!c);
68
- }
69
-
70
- function hasChildren(cls: OwlClass): boolean {
71
- return cls.children.length > 0;
72
- }
73
-
74
29
  const treeRoots = getClassTree();
75
30
  const allShapes = getAllShapes();
76
31
  const objectProperties = getObjectProperties();
@@ -97,28 +52,6 @@ const taxonomyLabels: Record<string, string> = {
97
52
  grammarNumber: 'Grammar Number',
98
53
  };
99
54
 
100
- interface IndividualGroup {
101
- key: string;
102
- label: string;
103
- concepts: { id: string; prefLabel: string }[];
104
- }
105
-
106
- const groupedIndividuals = computed<IndividualGroup[]>(() => {
107
- return taxonomyKeys.map(key => {
108
- const tax = (taxonomyData as Record<string, any>)[key];
109
- if (!tax) return { key, label: taxonomyLabels[key] || key, concepts: [] };
110
- const concepts = Object.values(tax.concepts as Record<string, any>).map((c: any) => ({
111
- id: c.id,
112
- prefLabel: c.prefLabel,
113
- }));
114
- return { key, label: tax.schemeLabel || taxonomyLabels[key] || key, concepts };
115
- });
116
- });
117
-
118
- const totalIndividuals = computed(() =>
119
- groupedIndividuals.value.reduce((sum, g) => sum + g.concepts.length, 0),
120
- );
121
-
122
55
  const valuesToTaxonomy: Record<string, string> = {
123
56
  'gloss:status': 'conceptStatus',
124
57
  'gloss:entstatus': 'entryStatus',
@@ -132,6 +65,20 @@ const valuesToTaxonomy: Record<string, string> = {
132
65
  'gloss:number': 'grammarNumber',
133
66
  };
134
67
 
68
+ function childClasses(parentId: string): OwlClass[] {
69
+ const cls = getClass(parentId);
70
+ if (!cls) return [];
71
+ return cls.children.map(id => getClass(id)).filter((c): c is OwlClass => !!c);
72
+ }
73
+
74
+ function hasChildren(cls: OwlClass): boolean {
75
+ return cls.children.length > 0;
76
+ }
77
+
78
+ function matchesSearch(text: string, query: string): boolean {
79
+ return text.toLowerCase().includes(query.toLowerCase());
80
+ }
81
+
135
82
  function taxonomyKeyForValuesFrom(valuesFrom: string | null): string | null {
136
83
  if (!valuesFrom) return null;
137
84
  return valuesToTaxonomy[valuesFrom] ?? null;
@@ -145,80 +92,126 @@ function getShapesForTaxonomy(taxonomyKey: string): OwlShape[] {
145
92
  );
146
93
  }
147
94
 
148
- const allNavItems = computed(() => {
149
- const items: { id: string; label: string; depth: number }[] = [];
150
- function walk(classes: OwlClass[], depth: number) {
151
- for (const cls of classes) {
152
- items.push({ id: cls.compact, label: cls.label, depth });
153
- if (expandedClasses.value.has(cls.compact)) {
154
- walk(childClasses(cls.compact), depth + 1);
155
- }
156
- }
95
+ export function useOntologyNav() {
96
+ const expandedClasses = ref(new Set<string>(['gloss:Designation']));
97
+
98
+ const collapsedSections = ref(new Set<string>([
99
+ 'objectProperty',
100
+ 'datatypeProperty',
101
+ 'shape',
102
+ 'taxonomy',
103
+ 'namedIndividual',
104
+ 'annotationProperty',
105
+ ]));
106
+
107
+ const searchQuery = ref('');
108
+
109
+ function toggleExpand(cls: OwlClass) {
110
+ const s = new Set(expandedClasses.value);
111
+ if (s.has(cls.compact)) s.delete(cls.compact);
112
+ else s.add(cls.compact);
113
+ expandedClasses.value = s;
157
114
  }
158
- walk(treeRoots, 0);
159
- return items;
160
- });
161
-
162
- function matchesSearch(text: string, query: string): boolean {
163
- return text.toLowerCase().includes(query.toLowerCase());
164
- }
165
115
 
166
- const searchResults = computed(() => {
167
- const q = searchQuery.value.trim();
168
- if (!q) return null;
169
-
170
- const matchedClasses: OwlClass[] = [];
171
- const matchedObjectProps: OwlProperty[] = [];
172
- const matchedDatatypeProps: OwlProperty[] = [];
173
- const matchedShapes: OwlShape[] = [];
174
- const matchedIndividuals: { group: string; id: string; prefLabel: string }[] = [];
175
- const matchedAnnotationProps: AnnotationProperty[] = [];
176
-
177
- // Walk all classes (tree + leaves)
178
- function walkAll(classes: OwlClass[]) {
179
- for (const cls of classes) {
180
- if (matchesSearch(cls.label, q) || matchesSearch(cls.compact, q)) {
181
- matchedClasses.push(cls);
182
- }
183
- walkAll(childClasses(cls.compact));
184
- }
116
+ function toggleSection(key: string) {
117
+ const s = new Set(collapsedSections.value);
118
+ if (s.has(key)) s.delete(key);
119
+ else s.add(key);
120
+ collapsedSections.value = s;
185
121
  }
186
- walkAll(treeRoots);
187
122
 
188
- for (const p of objectProperties) {
189
- if (matchesSearch(p.label, q) || matchesSearch(p.compact, q)) matchedObjectProps.push(p);
123
+ function expandAllSections() {
124
+ collapsedSections.value = new Set();
190
125
  }
191
- for (const p of datatypeProperties) {
192
- if (matchesSearch(p.label, q) || matchesSearch(p.compact, q)) matchedDatatypeProps.push(p);
193
- }
194
- for (const s of allShapes) {
195
- if (matchesSearch(s.label, q) || matchesSearch(s.compact, q)) matchedShapes.push(s);
126
+
127
+ function collapseAllSections() {
128
+ collapsedSections.value = new Set(['objectProperty', 'datatypeProperty', 'shape', 'taxonomy', 'class', 'namedIndividual', 'annotationProperty']);
196
129
  }
197
- for (const g of groupedIndividuals.value) {
198
- for (const c of g.concepts) {
199
- if (matchesSearch(c.prefLabel, q) || matchesSearch(c.id, q)) {
200
- matchedIndividuals.push({ group: g.key, id: c.id, prefLabel: c.prefLabel });
130
+
131
+ const allNavItems = computed(() => {
132
+ const items: { id: string; label: string; depth: number }[] = [];
133
+ function walk(classes: OwlClass[], depth: number) {
134
+ for (const cls of classes) {
135
+ items.push({ id: cls.compact, label: cls.label, depth });
136
+ if (expandedClasses.value.has(cls.compact)) {
137
+ walk(childClasses(cls.compact), depth + 1);
138
+ }
201
139
  }
202
140
  }
203
- }
204
- for (const ap of annotationProperties) {
205
- if (matchesSearch(ap.label, q) || matchesSearch(ap.compact, q)) matchedAnnotationProps.push(ap);
206
- }
141
+ walk(treeRoots, 0);
142
+ return items;
143
+ });
207
144
 
208
- const total = matchedClasses.length + matchedObjectProps.length + matchedDatatypeProps.length + matchedShapes.length + matchedIndividuals.length + matchedAnnotationProps.length;
145
+ const searchResults = computed(() => {
146
+ const q = searchQuery.value.trim();
147
+ if (!q) return null;
148
+
149
+ const matchedClasses: OwlClass[] = [];
150
+ const matchedObjectProps: OwlProperty[] = [];
151
+ const matchedDatatypeProps: OwlProperty[] = [];
152
+ const matchedShapes: OwlShape[] = [];
153
+ const matchedIndividuals: { group: string; id: string; prefLabel: string }[] = [];
154
+ const matchedAnnotationProps: AnnotationProperty[] = [];
155
+
156
+ function walkAll(classes: OwlClass[]) {
157
+ for (const cls of classes) {
158
+ if (matchesSearch(cls.label, q) || matchesSearch(cls.compact, q)) {
159
+ matchedClasses.push(cls);
160
+ }
161
+ walkAll(childClasses(cls.compact));
162
+ }
163
+ }
164
+ walkAll(treeRoots);
209
165
 
210
- return {
211
- total,
212
- classes: matchedClasses,
213
- objectProperties: matchedObjectProps,
214
- datatypeProperties: matchedDatatypeProps,
215
- shapes: matchedShapes,
216
- individuals: matchedIndividuals,
217
- annotationProperties: matchedAnnotationProps,
218
- };
219
- });
166
+ for (const p of objectProperties) {
167
+ if (matchesSearch(p.label, q) || matchesSearch(p.compact, q)) matchedObjectProps.push(p);
168
+ }
169
+ for (const p of datatypeProperties) {
170
+ if (matchesSearch(p.label, q) || matchesSearch(p.compact, q)) matchedDatatypeProps.push(p);
171
+ }
172
+ for (const s of allShapes) {
173
+ if (matchesSearch(s.label, q) || matchesSearch(s.compact, q)) matchedShapes.push(s);
174
+ }
175
+ for (const g of groupedIndividuals.value) {
176
+ for (const c of g.concepts) {
177
+ if (matchesSearch(c.prefLabel, q) || matchesSearch(c.id, q)) {
178
+ matchedIndividuals.push({ group: g.key, id: c.id, prefLabel: c.prefLabel });
179
+ }
180
+ }
181
+ }
182
+ for (const ap of annotationProperties) {
183
+ if (matchesSearch(ap.label, q) || matchesSearch(ap.compact, q)) matchedAnnotationProps.push(ap);
184
+ }
185
+
186
+ const total = matchedClasses.length + matchedObjectProps.length + matchedDatatypeProps.length + matchedShapes.length + matchedIndividuals.length + matchedAnnotationProps.length;
187
+
188
+ return {
189
+ total,
190
+ classes: matchedClasses,
191
+ objectProperties: matchedObjectProps,
192
+ datatypeProperties: matchedDatatypeProps,
193
+ shapes: matchedShapes,
194
+ individuals: matchedIndividuals,
195
+ annotationProperties: matchedAnnotationProps,
196
+ };
197
+ });
198
+
199
+ const groupedIndividuals = computed<IndividualGroup[]>(() => {
200
+ return taxonomyKeys.map(key => {
201
+ const tax = (taxonomyData as Record<string, any>)[key];
202
+ if (!tax) return { key, label: taxonomyLabels[key] || key, concepts: [] };
203
+ const concepts = Object.values(tax.concepts as Record<string, any>).map((c: any) => ({
204
+ id: c.id,
205
+ prefLabel: c.prefLabel,
206
+ }));
207
+ return { key, label: tax.schemeLabel || taxonomyLabels[key] || key, concepts };
208
+ });
209
+ });
210
+
211
+ const totalIndividuals = computed(() =>
212
+ groupedIndividuals.value.reduce((sum, g) => sum + g.concepts.length, 0),
213
+ );
220
214
 
221
- export function useOntologyNav() {
222
215
  return {
223
216
  expandedClasses,
224
217
  collapsedSections,
@@ -248,3 +241,9 @@ export function useOntologyNav() {
248
241
  ENTITY_TYPE_META,
249
242
  };
250
243
  }
244
+
245
+ interface IndividualGroup {
246
+ key: string;
247
+ label: string;
248
+ concepts: { id: string; prefLabel: string }[];
249
+ }
@@ -1,5 +1,5 @@
1
1
  import type { GraphNode, GraphEdge } from '../adapters/types';
2
- import { UriRouter } from '../adapters/UriRouter';
2
+ import { ReferenceResolver } from '../adapters/ReferenceResolver';
3
3
 
4
4
  function hasDesignations(node: GraphNode): boolean {
5
5
  const d = node.designations;
@@ -33,9 +33,9 @@ export class GraphEngine {
33
33
  if (this.edgeKeys.has(key)) return;
34
34
  this.edgeKeys.add(key);
35
35
 
36
- const parsed = UriRouter.parseUri(edge.target);
36
+ const parsed = ReferenceResolver.parseUri(edge.target);
37
37
  if (!this.nodes.has(edge.source)) {
38
- const sourceParsed = UriRouter.parseUri(edge.source);
38
+ const sourceParsed = ReferenceResolver.parseUri(edge.source);
39
39
  this.nodes.set(edge.source, {
40
40
  uri: edge.source,
41
41
  register: sourceParsed?.registerId ?? edge.register,
@@ -161,6 +161,71 @@ export class GraphEngine {
161
161
  return [...this.nodes.values()];
162
162
  }
163
163
 
164
+ // ── Bulk seeding: accept domain-level data, construct nodes internally ──────
165
+
166
+ addGraphNodes(uriPrefix: string, registerId: string, nodes: [string, Record<string, string>, string][]): void {
167
+ for (const [id, designations, status] of nodes) {
168
+ this.addNode({
169
+ uri: uriPrefix + id,
170
+ register: registerId,
171
+ conceptId: id,
172
+ designations: designations || {},
173
+ status: status || 'unknown',
174
+ loaded: false,
175
+ });
176
+ }
177
+ }
178
+
179
+ addEdges(edges: GraphEdge[]): void {
180
+ for (const edge of edges) {
181
+ this.addEdge(edge);
182
+ }
183
+ }
184
+
185
+ addDomainNodes(nodes: GraphNode[]): void {
186
+ for (const node of nodes) {
187
+ this.addNode(node);
188
+ }
189
+ }
190
+
191
+ seedConceptNode(uri: string, registerId: string, conceptId: string, designations: Record<string, string>, status: string): void {
192
+ this.addNode({
193
+ uri,
194
+ register: registerId,
195
+ conceptId,
196
+ designations,
197
+ status,
198
+ loaded: true,
199
+ });
200
+ }
201
+
202
+ addDomainEdgesWithNodes(edges: GraphEdge[], registerId: string): void {
203
+ for (const edge of edges) {
204
+ // Create domain target node before addEdge so addEdge finds it and skips stub creation
205
+ if (!this.nodes.has(edge.target)) {
206
+ this.nodes.set(edge.target, {
207
+ uri: edge.target,
208
+ register: registerId,
209
+ conceptId: '',
210
+ designations: edge.label ? { eng: edge.label } : {},
211
+ status: 'domain',
212
+ loaded: true,
213
+ nodeType: 'domain',
214
+ });
215
+ }
216
+ this.addEdge(edge);
217
+ }
218
+ }
219
+
220
+ getRelated(uri: string): { outgoing: GraphEdge[]; incoming: GraphEdge[] } {
221
+ return {
222
+ outgoing: this.getUniqueEdges(uri, 'outgoing', 'target')
223
+ .filter(e => e.type !== 'domain' && e.type !== 'section'),
224
+ incoming: this.getUniqueEdges(uri, 'incoming', 'source')
225
+ .filter(e => e.type !== 'domain' && e.type !== 'section'),
226
+ };
227
+ }
228
+
164
229
  get nodeCount(): number {
165
230
  return this.nodes.size;
166
231
  }
@@ -6,7 +6,7 @@ import type { Manifest, SearchHit, GraphEdge } from '../adapters/types';
6
6
  import type { Concept } from 'glossarist';
7
7
  import { conceptUri } from '../adapters/model-bridge';
8
8
  import { GraphEngine } from '../graph';
9
- import { UriRouter } from '../adapters/UriRouter';
9
+ import { ReferenceResolver } from '../adapters/ReferenceResolver';
10
10
  import { deduplicateSearchHits } from '../utils/search';
11
11
 
12
12
  export const useVocabularyStore = defineStore('vocabulary', () => {
@@ -107,31 +107,17 @@ export const useVocabularyStore = defineStore('vocabulary', () => {
107
107
  adapter.loadDomainNodes(),
108
108
  ]);
109
109
 
110
- if (nodeResult.status === 'fulfilled') {
111
- const { uriPrefix, nodes } = nodeResult.value;
112
- for (const [id, designations, status] of nodes) {
113
- engine.addNode({
114
- uri: uriPrefix + id,
115
- register: adapter.registerId,
116
- conceptId: id,
117
- designations: designations || {},
118
- status: status || 'unknown',
119
- loaded: false,
120
- });
121
- }
110
+ if (nodeResult.status === 'fulfilled' && nodeResult.value.uriPrefix) {
111
+ engine.addGraphNodes(nodeResult.value.uriPrefix, adapter.registerId, nodeResult.value.nodes);
122
112
  }
123
113
 
124
114
  if (edgeResult.status === 'fulfilled' && Array.isArray(edgeResult.value)) {
125
- for (const edge of edgeResult.value) {
126
- engine.addEdge(edge);
127
- }
115
+ engine.addEdges(edgeResult.value);
128
116
  edgeStatus.value[adapter.registerId] = { loaded: true, count: edgeResult.value.length };
129
117
  }
130
118
 
131
119
  if (domainResult.status === 'fulfilled') {
132
- for (const dn of domainResult.value) {
133
- engine.addNode(dn);
134
- }
120
+ engine.addDomainNodes(domainResult.value);
135
121
  }
136
122
  } catch {
137
123
  // Individual adapter failures are non-critical for graph view
@@ -149,24 +135,11 @@ export const useVocabularyStore = defineStore('vocabulary', () => {
149
135
  adapter.loadGraphNodes(),
150
136
  ]);
151
137
  const engine = graph.value;
152
- for (const dn of domainNodes) {
153
- engine.addNode(dn);
154
- }
138
+ engine.addDomainNodes(domainNodes);
155
139
  if (graphNodes.uriPrefix) {
156
- for (const [id, designations, status] of graphNodes.nodes) {
157
- engine.addNode({
158
- uri: graphNodes.uriPrefix + id,
159
- register: adapter.registerId,
160
- conceptId: id,
161
- designations: designations || {},
162
- status: status || 'unknown',
163
- loaded: false,
164
- });
165
- }
166
- }
167
- for (const edge of edges) {
168
- engine.addEdge(edge);
140
+ engine.addGraphNodes(graphNodes.uriPrefix, adapter.registerId, graphNodes.nodes);
169
141
  }
142
+ engine.addEdges(edges);
170
143
  edgeStatus.value[adapter.registerId] = { loaded: true, count: edges.length };
171
144
  return edges;
172
145
  } catch {
@@ -186,7 +159,7 @@ export const useVocabularyStore = defineStore('vocabulary', () => {
186
159
  if (adapter && loadedEdges.length > 0) {
187
160
  const targetRegisters = new Set<string>();
188
161
  for (const edge of loadedEdges) {
189
- const parsed = UriRouter.parseUri(edge.target);
162
+ const parsed = ReferenceResolver.parseUri(edge.target);
190
163
  if (parsed?.registerId && parsed.registerId !== registerId) {
191
164
  targetRegisters.add(parsed.registerId);
192
165
  }
@@ -197,17 +170,7 @@ export const useVocabularyStore = defineStore('vocabulary', () => {
197
170
  try {
198
171
  const gn = await targetAdapter.loadGraphNodes();
199
172
  if (gn.uriPrefix) {
200
- const engine = graph.value;
201
- for (const [id, designations, status] of gn.nodes) {
202
- engine.addNode({
203
- uri: gn.uriPrefix + id,
204
- register: targetId,
205
- conceptId: id,
206
- designations: designations || {},
207
- status: status || 'unknown',
208
- loaded: false,
209
- });
210
- }
173
+ graph.value.addGraphNodes(gn.uriPrefix, targetId, gn.nodes);
211
174
  }
212
175
  } catch { /* non-critical */ }
213
176
  }));
@@ -261,36 +224,12 @@ export const useVocabularyStore = defineStore('vocabulary', () => {
261
224
  }
262
225
 
263
226
  const engine = graph.value;
264
- engine.addNode({
265
- uri,
266
- register: registerId,
267
- conceptId,
268
- designations,
269
- status: indexEntry?.status ?? 'unknown',
270
- loaded: true,
271
- });
272
-
273
- for (const edge of domainEdges) {
274
- engine.addEdge(edge);
275
- const existing = engine.getNode(edge.target);
276
- if (!existing || !existing.loaded) {
277
- engine.addNode({
278
- uri: edge.target,
279
- register: registerId,
280
- conceptId: '',
281
- designations: edge.label ? { eng: edge.label } : {},
282
- status: 'domain',
283
- loaded: true,
284
- nodeType: 'domain',
285
- });
286
- }
287
- }
227
+ engine.seedConceptNode(uri, registerId, conceptId, designations, indexEntry?.status ?? 'unknown');
228
+ engine.addDomainEdgesWithNodes(domainEdges, registerId);
288
229
 
289
230
  touchGraph();
290
- conceptEdges.value = [
291
- ...engine.getEdges(uri),
292
- ...engine.getIncomingEdges(uri),
293
- ];
231
+ const related = engine.getRelated(uri);
232
+ conceptEdges.value = [...related.outgoing, ...related.incoming];
294
233
  } catch (e: unknown) {
295
234
  error.value = `Failed to load concept ${conceptId}: ${e instanceof Error ? e.message : String(e)}`;
296
235
  currentConcept.value = null;