@glossarist/concept-browser 0.1.0

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.
Files changed (68) hide show
  1. package/README.md +319 -0
  2. package/cli/index.mjs +119 -0
  3. package/env.d.ts +7 -0
  4. package/index.html +16 -0
  5. package/package.json +78 -0
  6. package/postcss.config.js +6 -0
  7. package/scripts/build-edges.js +112 -0
  8. package/scripts/fetch-datasets.mjs +195 -0
  9. package/scripts/generate-404.js +15 -0
  10. package/scripts/generate-data.mjs +606 -0
  11. package/scripts/load-site-config.mjs +56 -0
  12. package/src/App.vue +98 -0
  13. package/src/__tests__/data-integration.test.ts +135 -0
  14. package/src/__tests__/data-integrity.test.ts +101 -0
  15. package/src/__tests__/dataset-adapter.test.ts +336 -0
  16. package/src/__tests__/dataset-style.test.ts +37 -0
  17. package/src/__tests__/graph.test.ts +187 -0
  18. package/src/__tests__/lang.test.ts +29 -0
  19. package/src/__tests__/math.test.ts +113 -0
  20. package/src/__tests__/reference-resolver.test.ts +122 -0
  21. package/src/__tests__/site-config.test.ts +52 -0
  22. package/src/__tests__/uri-router.test.ts +76 -0
  23. package/src/adapters/DatasetAdapter.ts +270 -0
  24. package/src/adapters/ReferenceResolver.ts +95 -0
  25. package/src/adapters/UriRouter.ts +41 -0
  26. package/src/adapters/factory.ts +78 -0
  27. package/src/adapters/types.ts +162 -0
  28. package/src/components/AppHeader.vue +99 -0
  29. package/src/components/AppSidebar.vue +133 -0
  30. package/src/components/ConceptCard.vue +65 -0
  31. package/src/components/ConceptDetail.vue +540 -0
  32. package/src/components/ConceptTimeline.vue +410 -0
  33. package/src/components/FormatDownloads.vue +46 -0
  34. package/src/components/GraphPanel.vue +499 -0
  35. package/src/components/LanguageDetail.vue +211 -0
  36. package/src/components/NavIcon.vue +20 -0
  37. package/src/components/SearchBar.vue +241 -0
  38. package/src/composables/use-dataset-loader.ts +27 -0
  39. package/src/config/types.ts +130 -0
  40. package/src/config/use-site-config.ts +144 -0
  41. package/src/graph/GraphEngine.ts +137 -0
  42. package/src/graph/index.ts +1 -0
  43. package/src/main.ts +11 -0
  44. package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  45. package/src/router/index.ts +43 -0
  46. package/src/router/page-routes.ts +35 -0
  47. package/src/stores/ui.ts +59 -0
  48. package/src/stores/vocabulary.ts +309 -0
  49. package/src/style.css +314 -0
  50. package/src/utils/asciidoc-lite.ts +123 -0
  51. package/src/utils/concept-formats.ts +157 -0
  52. package/src/utils/dataset-style.ts +54 -0
  53. package/src/utils/index.ts +1 -0
  54. package/src/utils/lang.ts +32 -0
  55. package/src/utils/math.ts +100 -0
  56. package/src/views/AboutView.vue +122 -0
  57. package/src/views/ConceptView.vue +119 -0
  58. package/src/views/ContributorsView.vue +110 -0
  59. package/src/views/DatasetView.vue +249 -0
  60. package/src/views/GraphView.vue +65 -0
  61. package/src/views/HomeView.vue +168 -0
  62. package/src/views/NewsView.vue +146 -0
  63. package/src/views/ResolveView.vue +63 -0
  64. package/src/views/SearchView.vue +33 -0
  65. package/src/views/StatsView.vue +121 -0
  66. package/tailwind.config.js +43 -0
  67. package/tsconfig.json +24 -0
  68. package/vite.config.ts +27 -0
@@ -0,0 +1,270 @@
1
+ import type {
2
+ Manifest,
3
+ ConceptIndex,
4
+ ConceptSummary,
5
+ ConceptEntry,
6
+ ConceptDocument,
7
+ SearchHit,
8
+ GraphEdge,
9
+ } from './types';
10
+ import { UriRouter } from './UriRouter';
11
+
12
+ export class DatasetAdapter {
13
+ private positionIndex = new Map<string, number>();
14
+ readonly registerId: string;
15
+ private baseUrl: string;
16
+ manifest: Manifest | null = null;
17
+ index: ConceptIndex | null = null;
18
+
19
+ private conceptCache = new Map<string, ConceptDocument>();
20
+ private summaryMap = new Map<string, ConceptSummary>();
21
+ private loadedChunks = new Set<number>();
22
+ private indexMeta: { conceptCount: number; chunkSize: number; chunks: { file: string; count: number }[] } | null = null;
23
+
24
+ constructor(registerId: string, baseUrl: string) {
25
+ this.registerId = registerId;
26
+ this.baseUrl = baseUrl;
27
+ }
28
+
29
+ async loadManifest(): Promise<Manifest> {
30
+ const resp = await fetch(`${this.baseUrl}/manifest.json`);
31
+ if (!resp.ok) throw new Error(`Failed to load manifest for ${this.registerId}: ${resp.status}`);
32
+ this.manifest = (await resp.json()) as Manifest;
33
+ return this.manifest;
34
+ }
35
+
36
+ async loadIndex(): Promise<ConceptIndex> {
37
+ const manifest = this.manifest;
38
+ const isLarge = manifest && manifest.conceptCount > 1000;
39
+
40
+ if (isLarge) {
41
+ return this.loadIndexChunked();
42
+ }
43
+
44
+ const resp = await fetch(`${this.baseUrl}/index.json`);
45
+ if (!resp.ok) throw new Error(`Failed to load index for ${this.registerId}: ${resp.status}`);
46
+ this.index = (await resp.json()) as ConceptIndex;
47
+ this.summaryMap.clear();
48
+ this.positionIndex.clear();
49
+ for (let i = 0; i < this.index.concepts.length; i++) {
50
+ this.summaryMap.set(this.index.concepts[i].id, this.index.concepts[i]);
51
+ this.positionIndex.set(this.index.concepts[i].id, i);
52
+ }
53
+ return this.index;
54
+ }
55
+
56
+ private async loadIndexChunked(): Promise<ConceptIndex> {
57
+ const metaResp = await fetch(`${this.baseUrl}/index-meta.json`);
58
+ let meta: { registerId: string; schemaVersion: string; conceptCount: number; chunkSize: number; chunks: { file: string; count: number }[] };
59
+ if (metaResp.ok) {
60
+ meta = await metaResp.json();
61
+ } else {
62
+ const resp = await fetch(`${this.baseUrl}/index.json`);
63
+ if (!resp.ok) throw new Error(`Failed to load index for ${this.registerId}`);
64
+ this.index = (await resp.json()) as ConceptIndex;
65
+ this.summaryMap.clear();
66
+ this.positionIndex.clear();
67
+ for (let i = 0; i < this.index.concepts.length; i++) {
68
+ this.summaryMap.set(this.index.concepts[i].id, this.index.concepts[i]);
69
+ this.positionIndex.set(this.index.concepts[i].id, i);
70
+ }
71
+ return this.index;
72
+ }
73
+
74
+ this.indexMeta = {
75
+ conceptCount: meta.conceptCount,
76
+ chunkSize: meta.chunkSize,
77
+ chunks: meta.chunks,
78
+ };
79
+
80
+ // Pre-allocate array so positions match concept order — undefined = not loaded yet
81
+ this.index = {
82
+ registerId: meta.registerId,
83
+ schemaVersion: meta.schemaVersion,
84
+ conceptCount: meta.conceptCount,
85
+ chunkSize: meta.chunkSize,
86
+ chunks: meta.chunks,
87
+ concepts: new Array(meta.conceptCount),
88
+ };
89
+
90
+ await this.loadChunkAsSummaries(0);
91
+
92
+ return this.index;
93
+ }
94
+
95
+ async loadChunk(chunkIndex: number): Promise<ConceptEntry[]> {
96
+ if (this.loadedChunks.has(chunkIndex)) return [];
97
+ const chunkFile = `index-${String(chunkIndex).padStart(4, '0')}.json`;
98
+ const resp = await fetch(`${this.baseUrl}/chunks/${chunkFile}`);
99
+ if (!resp.ok) throw new Error(`Failed to load chunk ${chunkIndex} for ${this.registerId}`);
100
+ const data = await resp.json();
101
+ this.loadedChunks.add(chunkIndex);
102
+ return data.concepts as ConceptEntry[];
103
+ }
104
+
105
+ private async loadChunkAsSummaries(chunkIndex: number): Promise<void> {
106
+ if (this.loadedChunks.has(chunkIndex)) return;
107
+
108
+ const chunkFile = `index-${String(chunkIndex).padStart(4, '0')}.json`;
109
+ const resp = await fetch(`${this.baseUrl}/chunks/${chunkFile}`);
110
+ if (!resp.ok) return;
111
+ const data = await resp.json();
112
+ this.loadedChunks.add(chunkIndex);
113
+
114
+ const entries = data.concepts as ConceptEntry[];
115
+ const startPos = chunkIndex * (this.indexMeta?.chunkSize ?? 500);
116
+
117
+ for (let i = 0; i < entries.length; i++) {
118
+ const entry = entries[i];
119
+ const summary: ConceptSummary = {
120
+ id: entry.id,
121
+ eng: entry.designations?.eng || Object.values(entry.designations || {})[0] || '',
122
+ status: entry.status,
123
+ };
124
+ (this.index!.concepts as (ConceptSummary | undefined)[])[startPos + i] = summary;
125
+ this.summaryMap.set(entry.id, summary);
126
+ this.positionIndex.set(entry.id, startPos + i);
127
+ }
128
+ }
129
+
130
+ async ensureChunksForRange(offset: number, limit: number): Promise<void> {
131
+ if (!this.indexMeta) return;
132
+ const { chunkSize, chunks } = this.indexMeta;
133
+ const firstChunk = Math.floor(offset / chunkSize);
134
+ const lastChunk = Math.floor((offset + limit - 1) / chunkSize);
135
+ const toLoad: number[] = [];
136
+ for (let c = firstChunk; c <= Math.min(lastChunk, chunks.length - 1); c++) {
137
+ if (!this.loadedChunks.has(c)) toLoad.push(c);
138
+ }
139
+ if (toLoad.length === 0) return;
140
+ await Promise.all(toLoad.map(c => this.loadChunkAsSummaries(c)));
141
+ }
142
+
143
+ async ensureAllChunksLoaded(): Promise<void> {
144
+ if (!this.indexMeta) return;
145
+ const { chunks } = this.indexMeta;
146
+ const toLoad = chunks.map((_, i) => i).filter(i => !this.loadedChunks.has(i));
147
+ // Load in parallel batches of 5 to avoid overwhelming the browser
148
+ for (let i = 0; i < toLoad.length; i += 5) {
149
+ const batch = toLoad.slice(i, i + 5);
150
+ await Promise.all(batch.map(c => this.loadChunkAsSummaries(c)));
151
+ }
152
+ }
153
+
154
+ isRangeLoaded(offset: number, limit: number): boolean {
155
+ if (!this.index?.concepts) return false;
156
+ const arr = this.index.concepts as (ConceptSummary | undefined)[];
157
+ for (let i = offset; i < Math.min(offset + limit, arr.length); i++) {
158
+ if (arr[i] === undefined) return false;
159
+ }
160
+ return true;
161
+ }
162
+
163
+ async fetchConcept(conceptId: string): Promise<ConceptDocument> {
164
+ const cached = this.conceptCache.get(conceptId);
165
+ if (cached) return cached;
166
+
167
+ const resp = await fetch(`${this.baseUrl}/concepts/${conceptId}.json`);
168
+ if (!resp.ok) throw new Error(`Concept ${conceptId} not found in ${this.registerId}`);
169
+ const doc = (await resp.json()) as ConceptDocument;
170
+ this.conceptCache.set(conceptId, doc);
171
+ return doc;
172
+ }
173
+
174
+ getIndexEntry(conceptId: string): ConceptSummary | undefined {
175
+ return this.summaryMap.get(conceptId);
176
+ }
177
+
178
+ getConcepts(): ConceptSummary[] {
179
+ return this.index?.concepts ?? [];
180
+ }
181
+
182
+ getConceptCount(): number {
183
+ return this.index?.conceptCount ?? this.indexMeta?.conceptCount ?? 0;
184
+ }
185
+
186
+ getConceptPosition(conceptId: string): number {
187
+ return this.positionIndex.get(conceptId) ?? -1;
188
+ }
189
+
190
+ getAdjacentConcepts(conceptId: string): { prev: string | null; next: string | null } {
191
+ const concepts = this.index?.concepts as (ConceptSummary | undefined)[] | undefined;
192
+ if (!concepts) return { prev: null, next: null };
193
+ const idx = this.getConceptPosition(conceptId);
194
+ if (idx === -1) return { prev: null, next: null };
195
+ // Scan backward for prev (skip undefined)
196
+ let prev: string | null = null;
197
+ for (let i = idx - 1; i >= 0; i--) {
198
+ if (concepts[i]) { prev = concepts[i]!.id; break; }
199
+ }
200
+ // Scan forward for next (skip undefined)
201
+ let next: string | null = null;
202
+ for (let i = idx + 1; i < concepts.length; i++) {
203
+ if (concepts[i]) { next = concepts[i]!.id; break; }
204
+ }
205
+ return { prev, next };
206
+ }
207
+
208
+ search(query: string, lang: string = 'eng'): SearchHit[] {
209
+ const q = query.toLowerCase();
210
+ const hits: SearchHit[] = [];
211
+ const arr = this.index?.concepts as (ConceptSummary | undefined)[] | undefined;
212
+ if (!arr) return hits;
213
+
214
+ for (const entry of arr) {
215
+ if (!entry) continue;
216
+ const term = entry.eng || '';
217
+ if (term.toLowerCase().includes(q) || entry.id.toLowerCase().includes(q)) {
218
+ hits.push({
219
+ conceptId: entry.id,
220
+ registerId: this.registerId,
221
+ designation: term,
222
+ language: lang,
223
+ matchField: 'designation',
224
+ });
225
+ }
226
+ }
227
+ return hits;
228
+ }
229
+
230
+ extractEdges(concept: ConceptDocument): GraphEdge[] {
231
+ const edges: GraphEdge[] = [];
232
+ const sourceUri = concept['@id'];
233
+
234
+ for (const [_lang, lc] of Object.entries(concept['gl:localizedConcept'] || {})) {
235
+ if (lc['gl:references']) {
236
+ for (const ref of lc['gl:references']) {
237
+ if (ref['@id'] && ref['@id'] !== sourceUri) {
238
+ const parsed = UriRouter.parseUri(ref['@id']);
239
+ edges.push({
240
+ source: sourceUri,
241
+ target: ref['@id'],
242
+ type: 'references',
243
+ label: ref['gl:term'],
244
+ register: parsed?.registerId ?? this.registerId,
245
+ });
246
+ }
247
+ }
248
+ }
249
+ }
250
+
251
+ return edges;
252
+ }
253
+
254
+ async loadEdgeIndex(): Promise<GraphEdge[]> {
255
+ const resp = await fetch(`${this.baseUrl}/edges.json`);
256
+ if (!resp.ok) return [];
257
+ const data = await resp.json();
258
+ return data.edges ?? [];
259
+ }
260
+
261
+ async loadGraphNodes(): Promise<{ uriPrefix: string; nodes: [string, string, string, string][] }> {
262
+ const resp = await fetch(`${this.baseUrl}/graph-nodes.json`);
263
+ if (!resp.ok) return { uriPrefix: '', nodes: [] };
264
+ return await resp.json();
265
+ }
266
+
267
+ getLanguages(): string[] {
268
+ return this.manifest?.languages ?? [];
269
+ }
270
+ }
@@ -0,0 +1,95 @@
1
+ import type { Resolution } from './types';
2
+ import type { RoutingEntry } from '../config/types';
3
+
4
+ interface DatasetEntry {
5
+ id: string;
6
+ uriPatterns: string[];
7
+ }
8
+
9
+ function extractConceptId(uri: string, pattern: string): string | null {
10
+ if (!pattern.endsWith('*')) return null;
11
+ const base = pattern.slice(0, -1);
12
+ if (!uri.startsWith(base)) return null;
13
+ const remainder = uri.slice(base.length);
14
+
15
+ if (uri.startsWith('https://') || uri.startsWith('http://')) {
16
+ const match = remainder.match(/^\/?concept\/([^/?#]+)/);
17
+ return match ? match[1] : null;
18
+ }
19
+ if (uri.startsWith('urn:')) {
20
+ return remainder || null;
21
+ }
22
+ return null;
23
+ }
24
+
25
+ function matchUriPattern(uri: string, pattern: string): boolean {
26
+ if (!pattern.endsWith('*')) return uri === pattern;
27
+ return uri.startsWith(pattern.slice(0, -1));
28
+ }
29
+
30
+ export class ReferenceResolver {
31
+ private datasets: DatasetEntry[] = [];
32
+ private routing: RoutingEntry[] = [];
33
+
34
+ registerDataset(id: string, uriPatterns: string[]): void {
35
+ this.datasets.push({ id, uriPatterns });
36
+ }
37
+
38
+ loadRouting(entries: RoutingEntry[]): void {
39
+ this.routing = entries;
40
+ }
41
+
42
+ resolveReference(uri: string, sourceDatasetId?: string): Resolution {
43
+ // Step 1: Check provided datasets
44
+ for (const ds of this.datasets) {
45
+ for (const pattern of ds.uriPatterns) {
46
+ if (matchUriPattern(uri, pattern)) {
47
+ const conceptId = extractConceptId(uri, pattern);
48
+ if (conceptId) {
49
+ return {
50
+ type: 'internal',
51
+ registerId: ds.id,
52
+ conceptId,
53
+ crossDataset: sourceDatasetId != null && sourceDatasetId !== ds.id,
54
+ };
55
+ }
56
+ }
57
+ }
58
+ }
59
+
60
+ // Step 2: Check routing table
61
+ for (const entry of this.routing) {
62
+ if (matchUriPattern(uri, entry.uri)) {
63
+ if (entry.type === 'site') {
64
+ return {
65
+ type: 'site',
66
+ baseUrl: entry.baseUrl!,
67
+ conceptUri: uri,
68
+ label: entry.label,
69
+ };
70
+ }
71
+ if (entry.type === 'url') {
72
+ const template = entry.url!;
73
+ const conceptId = this.extractConceptIdFromRouting(uri, entry.uri);
74
+ const url = template.includes('{conceptId}') && conceptId
75
+ ? template.replace('{conceptId}', conceptId)
76
+ : template;
77
+ return { type: 'url', url, label: entry.label };
78
+ }
79
+ }
80
+ }
81
+
82
+ return { type: 'unresolved', uri };
83
+ }
84
+
85
+ private extractConceptIdFromRouting(uri: string, pattern: string): string | null {
86
+ for (const ds of this.datasets) {
87
+ for (const dsPattern of ds.uriPatterns) {
88
+ if (matchUriPattern(uri, dsPattern)) {
89
+ return extractConceptId(uri, dsPattern);
90
+ }
91
+ }
92
+ }
93
+ return extractConceptId(uri, pattern);
94
+ }
95
+ }
@@ -0,0 +1,41 @@
1
+ import type { Manifest } from './types';
2
+
3
+ const URI_REGISTER_RE = /\/([^/]+)\/concept\/([^/]+)$/;
4
+
5
+ export class UriRouter {
6
+ private registerMap = new Map<string, { baseUrl: string; manifest: Manifest | null; uriBase: string }>();
7
+
8
+ registerDataset(registerId: string, baseUrl: string, manifest?: Manifest) {
9
+ this.registerMap.set(registerId, {
10
+ baseUrl,
11
+ manifest: manifest ?? null,
12
+ uriBase: manifest?.uriBase ?? 'https://glossarist.org',
13
+ });
14
+ }
15
+
16
+ resolveUri(uri: string): { registerId: string; conceptId: string } | null {
17
+ for (const [registerId, info] of this.registerMap) {
18
+ const prefix = `${info.uriBase}/${registerId}/concept/`;
19
+ if (uri.startsWith(prefix)) {
20
+ return { registerId, conceptId: uri.slice(prefix.length) };
21
+ }
22
+ }
23
+ return null;
24
+ }
25
+
26
+ /** Extract registerId and conceptId from any glossarist URI (no registration needed). */
27
+ static parseUri(uri: string): { registerId: string; conceptId: string } | null {
28
+ const m = uri.match(URI_REGISTER_RE);
29
+ return m ? { registerId: m[1], conceptId: m[2] } : null;
30
+ }
31
+
32
+ buildUri(registerId: string, conceptId: string): string {
33
+ const info = this.registerMap.get(registerId);
34
+ const uriBase = info?.uriBase ?? 'https://glossarist.org';
35
+ return `${uriBase}/${registerId}/concept/${conceptId}`;
36
+ }
37
+
38
+ getRegisteredIds(): string[] {
39
+ return [...this.registerMap.keys()];
40
+ }
41
+ }
@@ -0,0 +1,78 @@
1
+ import type { DatasetRegistry, Manifest, Resolution } from './types';
2
+ import type { RoutingEntry as ConfigRoutingEntry } from '../config/types';
3
+ import { DatasetAdapter } from './DatasetAdapter';
4
+ import { UriRouter } from './UriRouter';
5
+ import { ReferenceResolver } from './ReferenceResolver';
6
+
7
+ export class AdapterFactory {
8
+ private adapters = new Map<string, DatasetAdapter>();
9
+ readonly router = new UriRouter();
10
+ readonly resolver: ReferenceResolver;
11
+
12
+ constructor() {
13
+ this.resolver = new ReferenceResolver();
14
+ }
15
+
16
+ async discoverDatasets(datasetsUrl: string): Promise<DatasetAdapter[]> {
17
+ const resp = await fetch(datasetsUrl);
18
+ if (!resp.ok) throw new Error(`Failed to load dataset registry: ${resp.status}`);
19
+ const registry = (await resp.json()) as DatasetRegistry[];
20
+
21
+ const adapters: DatasetAdapter[] = [];
22
+ for (const reg of registry) {
23
+ const adapter = new DatasetAdapter(reg.id, `/data/${reg.id}`);
24
+ this.adapters.set(reg.id, adapter);
25
+ adapters.push(adapter);
26
+ }
27
+
28
+ await Promise.all(adapters.map(a => a.loadManifest().catch(() => {})));
29
+
30
+ return adapters;
31
+ }
32
+
33
+ getAdapter(registerId: string): DatasetAdapter | undefined {
34
+ return this.adapters.get(registerId);
35
+ }
36
+
37
+ getAdapters(): DatasetAdapter[] {
38
+ return [...this.adapters.values()];
39
+ }
40
+
41
+ async loadDataset(registerId: string): Promise<DatasetAdapter> {
42
+ const adapter = this.adapters.get(registerId);
43
+ if (!adapter) throw new Error(`Unknown dataset: ${registerId}`);
44
+
45
+ const manifest = await adapter.loadManifest();
46
+ await adapter.loadIndex();
47
+
48
+ this.router.registerDataset(registerId, `/data/${registerId}`, manifest);
49
+
50
+ const uriPatterns = [
51
+ manifest.datasetUri,
52
+ ...(manifest.uriAliases ?? []),
53
+ `https://glossarist.org/${registerId}/*`,
54
+ ];
55
+ this.resolver.registerDataset(registerId, uriPatterns);
56
+
57
+ return adapter;
58
+ }
59
+
60
+ loadRouting(entries: ConfigRoutingEntry[]): void {
61
+ this.resolver.loadRouting(entries);
62
+ }
63
+
64
+ resolve(uri: string, sourceDatasetId?: string): Resolution {
65
+ return this.resolver.resolveReference(uri, sourceDatasetId);
66
+ }
67
+ }
68
+
69
+ let _instance: AdapterFactory | null = null;
70
+
71
+ export function getFactory(): AdapterFactory {
72
+ if (!_instance) _instance = new AdapterFactory();
73
+ return _instance;
74
+ }
75
+
76
+ export function resetFactory(): void {
77
+ _instance = null;
78
+ }
@@ -0,0 +1,162 @@
1
+ /** Core types for the vocabulary browser data model. */
2
+
3
+ export interface Manifest {
4
+ id: string;
5
+ datasetUri: string;
6
+ uriAliases?: string[];
7
+ title: string;
8
+ description: string;
9
+ owner: string;
10
+ baseUrl: string;
11
+ languages: string[];
12
+ conceptCount: number;
13
+ conceptUrlTemplate: string;
14
+ indexUrl: string;
15
+ contextUrl: string;
16
+ uriBase: string;
17
+ status: string;
18
+ schemaVersion: string;
19
+ tags: string[];
20
+ lastUpdated: string;
21
+ sourceRepo: string;
22
+ chunkSize: number;
23
+ color?: string;
24
+ shortname?: string;
25
+ languageOrder?: string[];
26
+ languageStats?: Record<string, { terms: number; definitions: number }>;
27
+ availableFormats?: string[];
28
+ }
29
+
30
+ export interface ConceptIndex {
31
+ registerId: string;
32
+ schemaVersion: string;
33
+ conceptCount: number;
34
+ chunkSize: number;
35
+ chunks: { file: string; count: number }[];
36
+ concepts: ConceptSummary[];
37
+ }
38
+
39
+ export interface ConceptSummary {
40
+ id: string;
41
+ eng: string;
42
+ status: string;
43
+ }
44
+
45
+ export interface ConceptEntry {
46
+ id: string;
47
+ designations: Record<string, string>;
48
+ groups: string[];
49
+ status: string;
50
+ }
51
+
52
+ export interface ConceptDocument {
53
+ '@context': string;
54
+ '@id': string;
55
+ '@type': string;
56
+ 'gl:identifier': string;
57
+ 'gl:localizedConcept': Record<string, LocalizedConcept>;
58
+ }
59
+
60
+ export interface LocalizedConcept {
61
+ '@id': string;
62
+ '@type': string;
63
+ 'gl:languageCode': string;
64
+ 'gl:entryStatus'?: string;
65
+ 'gl:designation'?: Designation[];
66
+ 'gl:definition'?: DetailedDefinition[];
67
+ 'gl:notes'?: DetailedDefinition[];
68
+ 'gl:examples'?: DetailedDefinition[];
69
+ 'gl:source'?: ConceptSource[];
70
+ 'gl:release'?: number;
71
+ 'gl:reviewDate'?: string;
72
+ 'gl:reviewDecisionDate'?: string;
73
+ 'gl:reviewDecisionEvent'?: string;
74
+ 'gl:reviewStatus'?: string;
75
+ 'gl:reviewDecision'?: string;
76
+ 'gl:reviewDecisionNotes'?: string;
77
+ 'gl:dates'?: ConceptDate[];
78
+ 'gl:references'?: CrossReference[];
79
+ }
80
+
81
+ export interface Designation {
82
+ '@type': string;
83
+ 'gl:normativeStatus': string;
84
+ 'gl:term': string;
85
+ 'gl:gender'?: string;
86
+ 'gl:plurality'?: string;
87
+ 'gl:international'?: boolean;
88
+ }
89
+
90
+ export interface DetailedDefinition {
91
+ '@type': string;
92
+ 'gl:content': string;
93
+ }
94
+
95
+ export interface ConceptSource {
96
+ '@type': string;
97
+ 'gl:sourceType'?: string;
98
+ 'gl:sourceStatus'?: string;
99
+ 'gl:modification'?: string;
100
+ 'gl:origin'?: {
101
+ '@type': string;
102
+ 'gl:ref'?: string;
103
+ 'gl:clause'?: string;
104
+ 'gl:link'?: string;
105
+ };
106
+ }
107
+
108
+ export interface ConceptDate {
109
+ 'gl:dateType': string;
110
+ 'gl:date': string;
111
+ }
112
+
113
+ export interface CrossReference {
114
+ '@id': string;
115
+ 'gl:term': string;
116
+ }
117
+
118
+ export interface DatasetRegistry {
119
+ id: string;
120
+ manifestUrl: string;
121
+ }
122
+
123
+ export interface GraphEdge {
124
+ source: string; // concept URI
125
+ target: string; // concept URI
126
+ type: string;
127
+ label?: string;
128
+ register: string;
129
+ }
130
+
131
+ export interface GraphNode {
132
+ uri: string;
133
+ register: string;
134
+ conceptId: string;
135
+ designations: Record<string, string>;
136
+ status: string;
137
+ loaded: boolean;
138
+ }
139
+
140
+ export interface SearchHit {
141
+ conceptId: string;
142
+ registerId: string;
143
+ designation: string;
144
+ language: string;
145
+ matchField: 'designation' | 'definition';
146
+ snippet?: string;
147
+ }
148
+
149
+ export type RelationType =
150
+ | 'related'
151
+ | 'narrower'
152
+ | 'broader'
153
+ | 'see'
154
+ | 'references'
155
+ | 'replaces'
156
+ | 'superseded';
157
+
158
+ export type Resolution =
159
+ | { type: 'internal'; registerId: string; conceptId: string; crossDataset: boolean }
160
+ | { type: 'site'; baseUrl: string; conceptUri: string; label: string }
161
+ | { type: 'url'; url: string; label: string }
162
+ | { type: 'unresolved'; uri: string };