@glossarist/concept-browser 0.7.28 → 0.7.30

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.28",
3
+ "version": "0.7.30",
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": {
@@ -924,6 +924,9 @@ for (let i = 0; i < config.datasets.length; i++) {
924
924
  tags: ds.tags || reg?.tags || [],
925
925
  color: ds.color || DS_PALETTE[i % DS_PALETTE.length],
926
926
  },
927
+ datasetUri: ds.uri || reg?.urn || undefined,
928
+ uriBase: config.uriBase || undefined,
929
+ uriAliases: ds.uriAliases || reg?.urnAliases || undefined,
927
930
  });
928
931
  }
929
932
  writeJson(path.join(PUBLIC, 'datasets.json'), registry);
@@ -51,10 +51,10 @@ export class DatasetAdapter {
51
51
  this.baseUrl = baseUrl;
52
52
  }
53
53
 
54
- setSummaryManifest(summary: DatasetSummary): void {
54
+ setSummaryManifest(summary: DatasetSummary, registry?: { datasetUri?: string; uriBase?: string; uriAliases?: string[] }): void {
55
55
  this.manifest = {
56
56
  id: this.registerId,
57
- datasetUri: '',
57
+ datasetUri: registry?.datasetUri || '',
58
58
  title: summary.title,
59
59
  description: summary.description,
60
60
  owner: summary.owner,
@@ -64,7 +64,8 @@ export class DatasetAdapter {
64
64
  conceptUrlTemplate: '',
65
65
  indexUrl: '',
66
66
  contextUrl: '',
67
- uriBase: '',
67
+ uriBase: registry?.uriBase || '',
68
+ uriAliases: registry?.uriAliases,
68
69
  status: '',
69
70
  schemaVersion: '',
70
71
  tags: summary.tags,
@@ -36,7 +36,7 @@ export class AdapterFactory {
36
36
  adapters.push(adapter);
37
37
 
38
38
  if (reg.summary) {
39
- adapter.setSummaryManifest(reg.summary);
39
+ adapter.setSummaryManifest(reg.summary, reg);
40
40
  } else {
41
41
  needManifest.push(adapter);
42
42
  }
@@ -113,6 +113,9 @@ export interface DatasetRegistry {
113
113
  id: string;
114
114
  manifestUrl: string;
115
115
  summary?: DatasetSummary;
116
+ datasetUri?: string;
117
+ uriBase?: string;
118
+ uriAliases?: string[];
116
119
  }
117
120
 
118
121
  // ── Graph types ────────────────────────────────────────────────────────────
@@ -1,6 +1,11 @@
1
1
  import type { GraphNode, GraphEdge } from '../adapters/types';
2
2
  import { UriRouter } from '../adapters/UriRouter';
3
3
 
4
+ function hasDesignations(node: GraphNode): boolean {
5
+ const d = node.designations;
6
+ return d != null && typeof d === 'object' && Object.keys(d).length > 0;
7
+ }
8
+
4
9
  /**
5
10
  * Directed multigraph engine for concept relationships.
6
11
  * Supports cross-register edges with stub nodes for unresolved targets.
@@ -18,6 +23,8 @@ export class GraphEngine {
18
23
  this.nodes.set(node.uri, node);
19
24
  } else if (node.loaded && !existing.loaded) {
20
25
  this.nodes.set(node.uri, node);
26
+ } else if (!existing.loaded && hasDesignations(node) && !hasDesignations(existing)) {
27
+ this.nodes.set(node.uri, node);
21
28
  }
22
29
  }
23
30
 
@@ -6,11 +6,12 @@ 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
10
  import { deduplicateSearchHits } from '../utils/search';
10
11
 
11
12
  export const useVocabularyStore = defineStore('vocabulary', () => {
12
13
  // State
13
- const datasets = ref<Map<string, DatasetAdapter>>(new Map());
14
+ const datasets = shallowRef<Map<string, DatasetAdapter>>(new Map());
14
15
  const manifests = ref<Map<string, Manifest>>(new Map());
15
16
  const currentConcept = ref<Concept | null>(null);
16
17
  const currentRegisterId = ref<string>('');
@@ -54,12 +55,14 @@ export const useVocabularyStore = defineStore('vocabulary', () => {
54
55
  error.value = null;
55
56
  try {
56
57
  const adapters = await factory.discoverDatasets(`${import.meta.env.BASE_URL}datasets.json`);
58
+ const newMap = new Map(datasets.value);
57
59
  for (const adapter of adapters) {
58
- datasets.value.set(adapter.registerId, adapter);
60
+ newMap.set(adapter.registerId, adapter);
59
61
  if (adapter.manifest) {
60
62
  manifests.value.set(adapter.registerId, adapter.manifest);
61
63
  }
62
64
  }
65
+ datasets.value = newMap;
63
66
  initialized.value = true;
64
67
  } catch (e: unknown) {
65
68
  error.value = `Failed to discover datasets: ${e instanceof Error ? e.message : String(e)}`;
@@ -72,7 +75,9 @@ export const useVocabularyStore = defineStore('vocabulary', () => {
72
75
  error.value = null;
73
76
  try {
74
77
  const adapter = await factory.loadDataset(registerId);
75
- datasets.value.set(registerId, adapter);
78
+ const newMap = new Map(datasets.value);
79
+ newMap.set(registerId, adapter);
80
+ datasets.value = newMap;
76
81
  if (adapter.manifest) {
77
82
  manifests.value.set(registerId, adapter.manifest);
78
83
  }
@@ -136,29 +141,76 @@ export const useVocabularyStore = defineStore('vocabulary', () => {
136
141
  touchGraph();
137
142
  }
138
143
 
139
- async function loadEdges(adapter: DatasetAdapter) {
144
+ async function loadEdges(adapter: DatasetAdapter): Promise<GraphEdge[]> {
140
145
  try {
141
- const [edges, domainNodes] = await Promise.all([
146
+ const [edges, domainNodes, graphNodes] = await Promise.all([
142
147
  adapter.loadEdgeIndex(),
143
148
  adapter.loadDomainNodes(),
149
+ adapter.loadGraphNodes(),
144
150
  ]);
145
151
  const engine = graph.value;
146
152
  for (const dn of domainNodes) {
147
153
  engine.addNode(dn);
148
154
  }
155
+ 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
+ }
149
167
  for (const edge of edges) {
150
168
  engine.addEdge(edge);
151
169
  }
152
170
  edgeStatus.value[adapter.registerId] = { loaded: true, count: edges.length };
171
+ return edges;
153
172
  } catch {
154
173
  edgeStatus.value[adapter.registerId] = { loaded: false, count: 0 };
174
+ return [];
155
175
  }
156
176
  }
157
177
 
158
178
  async function ensureEdgesForDataset(registerId: string) {
159
179
  const adapter = datasets.value.get(registerId);
180
+ let loadedEdges: GraphEdge[] = [];
160
181
  if (adapter && !edgeStatus.value[registerId]?.loaded) {
161
- await loadEdges(adapter);
182
+ loadedEdges = await loadEdges(adapter);
183
+ }
184
+
185
+ // Load graph nodes for any target datasets referenced by this dataset's edges
186
+ if (adapter && loadedEdges.length > 0) {
187
+ const targetRegisters = new Set<string>();
188
+ for (const edge of loadedEdges) {
189
+ const parsed = UriRouter.parseUri(edge.target);
190
+ if (parsed?.registerId && parsed.registerId !== registerId) {
191
+ targetRegisters.add(parsed.registerId);
192
+ }
193
+ }
194
+ await Promise.all([...targetRegisters].map(async (targetId) => {
195
+ const targetAdapter = datasets.value.get(targetId);
196
+ if (!targetAdapter) return;
197
+ try {
198
+ const gn = await targetAdapter.loadGraphNodes();
199
+ 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
+ }
211
+ }
212
+ } catch { /* non-critical */ }
213
+ }));
162
214
  }
163
215
 
164
216
  const index = await factory.loadCrossRefIndex();
@@ -34,9 +34,9 @@ watch(adapter, (a) => {
34
34
  if (idlePreloadHandle !== null) return;
35
35
  if (!a || !a.index) return;
36
36
 
37
- const schedule = typeof requestIdleCallback !== 'undefined'
38
- ? requestIdleCallback
39
- : (cb: () => void) => setTimeout(cb, 0);
37
+ const schedule: (cb: () => void) => number = typeof requestIdleCallback !== 'undefined'
38
+ ? (cb) => requestIdleCallback(cb, { timeout: 2000 })
39
+ : (cb) => window.setTimeout(cb, 0);
40
40
 
41
41
  idlePreloadHandle = schedule(() => {
42
42
  if (allChunksLoaded.value || !a.index) {
@@ -52,7 +52,7 @@ watch(adapter, (a) => {
52
52
  a.ensureChunksForRange(0, 100).catch(() => {});
53
53
  }
54
54
  idlePreloadHandle = null;
55
- }, { timeout: 2000 } as any);
55
+ });
56
56
  });
57
57
 
58
58
  onBeforeUnmount(() => {