@glossarist/concept-browser 0.7.29 → 0.7.31
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 +3 -0
- package/src/adapters/DatasetAdapter.ts +4 -3
- package/src/adapters/factory.ts +1 -1
- package/src/adapters/types.ts +3 -0
- package/src/components/ConceptDetail.vue +12 -2
- package/src/graph/GraphEngine.ts +7 -0
- package/src/stores/vocabulary.ts +57 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glossarist/concept-browser",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.31",
|
|
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,
|
package/src/adapters/factory.ts
CHANGED
package/src/adapters/types.ts
CHANGED
|
@@ -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 ────────────────────────────────────────────────────────────
|
|
@@ -314,8 +314,18 @@ const conceptUriValue = computed(() =>
|
|
|
314
314
|
conceptUri(props.concept, props.registerId, props.manifest.uriBase)
|
|
315
315
|
);
|
|
316
316
|
|
|
317
|
-
const outgoingEdges = computed(() => props.edges.filter(e => e.source === conceptUriValue.value && e.type !== 'domain' && e.type !== 'section'));
|
|
318
|
-
const incomingEdges = computed(() => props.edges.filter(e => e.target === conceptUriValue.value && e.type !== 'domain' && e.type !== 'section'));
|
|
317
|
+
const outgoingEdges = computed(() => dedupeEdges(props.edges.filter(e => e.source === conceptUriValue.value && e.type !== 'domain' && e.type !== 'section'), 'target'));
|
|
318
|
+
const incomingEdges = computed(() => dedupeEdges(props.edges.filter(e => e.target === conceptUriValue.value && e.type !== 'domain' && e.type !== 'section'), 'source'));
|
|
319
|
+
|
|
320
|
+
function dedupeEdges(edges: GraphEdge[], direction: 'source' | 'target'): GraphEdge[] {
|
|
321
|
+
const seen = new Set<string>();
|
|
322
|
+
return edges.filter(e => {
|
|
323
|
+
const key = `${e[direction]}\0${e.type}`;
|
|
324
|
+
if (seen.has(key)) return false;
|
|
325
|
+
seen.add(key);
|
|
326
|
+
return true;
|
|
327
|
+
});
|
|
328
|
+
}
|
|
319
329
|
|
|
320
330
|
function inverseEdgeType(type: string): string {
|
|
321
331
|
return INVERSE_RELATIONSHIPS[type] || type;
|
package/src/graph/GraphEngine.ts
CHANGED
|
@@ -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
|
|
package/src/stores/vocabulary.ts
CHANGED
|
@@ -6,6 +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
10
|
import { deduplicateSearchHits } from '../utils/search';
|
|
10
11
|
|
|
11
12
|
export const useVocabularyStore = defineStore('vocabulary', () => {
|
|
@@ -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
|
-
|
|
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
|
|
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();
|