@glossarist/concept-browser 0.7.51 → 0.7.53

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 (159) hide show
  1. package/cli/index.mjs +32 -0
  2. package/env.d.ts +15 -0
  3. package/package.json +12 -2
  4. package/scripts/__tests__/doctor.test.mjs +147 -0
  5. package/scripts/doctor.mjs +327 -0
  6. package/scripts/generate-data.mjs +136 -0
  7. package/scripts/generate-ontology-data.mjs +3 -3
  8. package/scripts/generate-ontology-schema.mjs +3 -3
  9. package/scripts/lib/agents-turtle.mjs +64 -0
  10. package/scripts/lib/bibliography-turtle.mjs +54 -0
  11. package/scripts/lib/build-activity-turtle.mjs +92 -0
  12. package/scripts/lib/build-cache.mjs +70 -0
  13. package/scripts/lib/dataset-turtle.mjs +79 -0
  14. package/scripts/lib/turtle-escape.mjs +0 -0
  15. package/scripts/lib/version-turtle.mjs +56 -0
  16. package/scripts/lib/vocab-turtle.mjs +64 -0
  17. package/scripts/normalize-yaml.mjs +99 -0
  18. package/scripts/sync-concept-model.mjs +86 -0
  19. package/scripts/validate-shacl.mjs +194 -0
  20. package/src/App.vue +2 -0
  21. package/src/__fixtures__/concept-shape.ttl +20 -0
  22. package/src/__fixtures__/shacl/bad/concept.ttl +7 -0
  23. package/src/__fixtures__/shacl/empty/.gitkeep +0 -0
  24. package/src/__fixtures__/shacl/good/concept.ttl +8 -0
  25. package/src/__tests__/__fixtures__/concepts.ts +221 -0
  26. package/src/__tests__/adapters/concept-identity.test.ts +76 -0
  27. package/src/__tests__/components/error-boundary.test.ts +109 -0
  28. package/src/__tests__/composables/use-dataset-series.test.ts +262 -0
  29. package/src/__tests__/concept-rdf/agents-emitter.test.ts +110 -0
  30. package/src/__tests__/concept-rdf/bibliography-emitter.test.ts +159 -0
  31. package/src/__tests__/concept-rdf/build-activity-emitter.test.ts +119 -0
  32. package/src/__tests__/concept-rdf/coerce-date.test.ts +97 -0
  33. package/src/__tests__/concept-rdf/concept-emitter.test.ts +258 -0
  34. package/src/__tests__/concept-rdf/dataset-emitter.test.ts +224 -0
  35. package/src/__tests__/concept-rdf/differential.test.ts +96 -0
  36. package/src/__tests__/concept-rdf/group-emitter.test.ts +109 -0
  37. package/src/__tests__/concept-rdf/image-variant-emitter.test.ts +135 -0
  38. package/src/__tests__/concept-rdf/jsonld-writer.test.ts +109 -0
  39. package/src/__tests__/concept-rdf/nonverbal-rep.test.ts +177 -0
  40. package/src/__tests__/concept-rdf/property-based.test.ts +179 -0
  41. package/src/__tests__/concept-rdf/provenance.test.ts +110 -0
  42. package/src/__tests__/concept-rdf/quad-isomorphism.test.ts +43 -0
  43. package/src/__tests__/concept-rdf/quad-isomorphism.ts +47 -0
  44. package/src/__tests__/concept-rdf/rdf-components.test.ts +145 -0
  45. package/src/__tests__/concept-rdf/rdf-graph.test.ts +115 -0
  46. package/src/__tests__/concept-rdf/round-trip.test.ts +243 -0
  47. package/src/__tests__/concept-rdf/scoped-examples.test.ts +126 -0
  48. package/src/__tests__/concept-rdf/sections-builder.test.ts +94 -0
  49. package/src/__tests__/concept-rdf/shacl-conformance.test.ts +110 -0
  50. package/src/__tests__/concept-rdf/shape-consistency.test.ts +138 -0
  51. package/src/__tests__/concept-rdf/snapshot-generation.test.ts +75 -0
  52. package/src/__tests__/concept-rdf/table-formula-emitter.test.ts +142 -0
  53. package/src/__tests__/concept-rdf/turtle-writer.test.ts +114 -0
  54. package/src/__tests__/concept-rdf/use-rdf-document.test.ts +246 -0
  55. package/src/__tests__/concept-rdf/version-emitter.test.ts +120 -0
  56. package/src/__tests__/concept-rdf/vocabulary-ssot.test.ts +100 -0
  57. package/src/__tests__/concept-rdf-view.test.ts +136 -0
  58. package/src/__tests__/config/group-renderers.test.ts +35 -0
  59. package/src/__tests__/config/group-types.test.ts +76 -0
  60. package/src/__tests__/dataset-style.test.ts +12 -7
  61. package/src/__tests__/errors/errors.test.ts +142 -0
  62. package/src/__tests__/format-downloads.test.ts +47 -65
  63. package/src/__tests__/markdown-lite.test.ts +19 -0
  64. package/src/__tests__/perf/bundle-layout.test.ts +50 -0
  65. package/src/__tests__/perf/serialization-perf.test.ts +121 -0
  66. package/src/__tests__/scripts/agents-turtle.test.ts +61 -0
  67. package/src/__tests__/scripts/bibliography-turtle.test.ts +59 -0
  68. package/src/__tests__/scripts/build-activity-turtle.test.ts +75 -0
  69. package/src/__tests__/scripts/build-cache.test.ts +78 -0
  70. package/src/__tests__/scripts/build-integration.test.ts +134 -0
  71. package/src/__tests__/scripts/dataset-turtle.test.ts +94 -0
  72. package/src/__tests__/scripts/normalize-yaml.test.ts +98 -0
  73. package/src/__tests__/scripts/stryker-config.test.ts +33 -0
  74. package/src/__tests__/scripts/turtle-escape.test.ts +63 -0
  75. package/src/__tests__/scripts/version-turtle.test.ts +72 -0
  76. package/src/__tests__/scripts/vocab-turtle.test.ts +63 -0
  77. package/src/__tests__/use-format-registry.test.ts +125 -0
  78. package/src/__tests__/utils/bcp47.test.ts +166 -0
  79. package/src/__tests__/utils/color-theme.test.ts +143 -0
  80. package/src/__tests__/utils/url-safety.test.ts +65 -0
  81. package/src/__tests__/validate-shacl.test.ts +100 -0
  82. package/src/adapters/DatasetAdapter.ts +11 -5
  83. package/src/adapters/GraphDataSource.ts +2 -1
  84. package/src/adapters/UriRouter.ts +2 -1
  85. package/src/adapters/concept-identity.ts +69 -0
  86. package/src/adapters/factory.ts +3 -2
  87. package/src/adapters/model-bridge.ts +2 -1
  88. package/src/adapters/non-verbal/glossarist-augment.d.ts +7 -0
  89. package/src/adapters/non-verbal-resolver.ts +2 -1
  90. package/src/components/AppSidebar.vue +189 -93
  91. package/src/components/ConceptDetail.vue +8 -0
  92. package/src/components/ConceptEditionRail.vue +222 -0
  93. package/src/components/ConceptRdfView.vue +37 -377
  94. package/src/components/DatasetSeriesCard.vue +270 -0
  95. package/src/components/ErrorBoundary.vue +95 -0
  96. package/src/components/FormatDownloads.vue +17 -13
  97. package/src/components/HomeSeriesSection.vue +277 -0
  98. package/src/components/RelationSphere.vue +1672 -0
  99. package/src/components/SidebarSeriesSection.vue +239 -0
  100. package/src/components/concept-rdf/RdfInstanceHeader.vue +47 -0
  101. package/src/components/concept-rdf/RdfInstanceSection.vue +54 -0
  102. package/src/components/concept-rdf/RdfPrefixLegend.vue +27 -0
  103. package/src/components/concept-rdf/RdfSourcePanel.vue +72 -0
  104. package/src/components/concept-rdf/agents-emitter.ts +82 -0
  105. package/src/components/concept-rdf/bibliography-emitter.ts +83 -0
  106. package/src/components/concept-rdf/build-activity-emitter.ts +89 -0
  107. package/src/components/concept-rdf/concept-emitter.ts +443 -0
  108. package/src/components/concept-rdf/dataset-emitter.ts +95 -0
  109. package/src/components/concept-rdf/group-emitter.ts +69 -0
  110. package/src/components/concept-rdf/image-variant-emitter.ts +46 -0
  111. package/src/components/concept-rdf/jsonld-writer.ts +82 -0
  112. package/src/components/concept-rdf/predicates.ts +261 -0
  113. package/src/components/concept-rdf/provenance.ts +80 -0
  114. package/src/components/concept-rdf/rdf-graph.ts +211 -0
  115. package/src/components/concept-rdf/rdf-prefixes.ts +23 -0
  116. package/src/components/concept-rdf/sections-builder.ts +62 -0
  117. package/src/components/concept-rdf/table-formula-emitter.ts +101 -0
  118. package/src/components/concept-rdf/turtle-writer.ts +116 -0
  119. package/src/components/concept-rdf/use-rdf-document.ts +72 -0
  120. package/src/components/concept-rdf/version-emitter.ts +65 -0
  121. package/src/components/concept-rdf/vocabulary-emitter.ts +62 -0
  122. package/src/components/groups/DatasetGroupRenderer.vue +32 -0
  123. package/src/components/groups/DefaultGroupSidebar.vue +50 -0
  124. package/src/components/groups/LineageGroupSidebar.vue +75 -0
  125. package/src/composables/use-color-theme.ts +82 -0
  126. package/src/composables/use-format-registry.ts +42 -0
  127. package/src/composables/useDatasetSeries.ts +258 -0
  128. package/src/composables/useSphereProjection.ts +125 -0
  129. package/src/config/group-renderers.ts +27 -0
  130. package/src/config/group-types.ts +92 -0
  131. package/src/config/types.ts +81 -2
  132. package/src/config/use-site-config.ts +2 -1
  133. package/src/errors.ts +136 -0
  134. package/src/i18n/locales/eng.yml +24 -0
  135. package/src/i18n/locales/fra.yml +24 -0
  136. package/src/stores/vocabulary.ts +3 -1
  137. package/src/style.css +17 -2
  138. package/src/types/agents-version-turtle.d.ts +27 -0
  139. package/src/types/bibliography-turtle.d.ts +12 -0
  140. package/src/types/build-activity-turtle.d.ts +16 -0
  141. package/src/types/build-cache.d.ts +20 -0
  142. package/src/types/dataset-turtle.d.ts +32 -0
  143. package/src/types/normalize-yaml.d.ts +16 -0
  144. package/src/types/turtle-escape.d.ts +6 -0
  145. package/src/types/vocab-turtle.d.ts +13 -0
  146. package/src/utils/asciidoc-lite.ts +11 -6
  147. package/src/utils/bcp47.ts +141 -0
  148. package/src/utils/color-theme-integration.ts +11 -0
  149. package/src/utils/color-theme.ts +129 -0
  150. package/src/utils/dataset-style.ts +31 -6
  151. package/src/utils/locale.ts +6 -14
  152. package/src/utils/markdown-lite.ts +6 -1
  153. package/src/utils/relation-sphere-styling.ts +63 -0
  154. package/src/utils/relationship-categories.ts +30 -0
  155. package/src/utils/url-safety.ts +30 -0
  156. package/src/views/ConceptView.vue +187 -9
  157. package/src/views/DatasetView.vue +6 -0
  158. package/src/views/HomeView.vue +5 -0
  159. package/vite.config.ts +7 -0
@@ -0,0 +1,100 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { execFileSync } from 'node:child_process';
3
+ import { join, dirname } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const SCRIPT = join(__dirname, '..', '..', 'scripts', 'validate-shacl.mjs');
8
+ const FIXTURES = join(__dirname, '..', '__fixtures__', 'shacl');
9
+ const SHAPES = join(__dirname, '..', '__fixtures__', 'concept-shape.ttl');
10
+
11
+ interface RunResult { code: number; stdout: string; stderr: string }
12
+ interface RunOptions { env?: NodeJS.ProcessEnv; shapes?: string }
13
+ interface ExecException extends Error {
14
+ status?: number;
15
+ stdout?: Buffer | string;
16
+ stderr?: Buffer | string;
17
+ }
18
+
19
+ function execCaught(cmd: string, args: string[], opts: Parameters<typeof execFileSync>[2] = {}): RunResult {
20
+ try {
21
+ const stdout = execFileSync(cmd, args, { encoding: 'utf8', ...opts }) as string;
22
+ return { code: 0, stdout, stderr: '' };
23
+ } catch (e) {
24
+ const err = e as ExecException;
25
+ return {
26
+ code: err.status ?? 1,
27
+ stdout: typeof err.stdout === 'string' ? err.stdout : (err.stdout?.toString() ?? ''),
28
+ stderr: typeof err.stderr === 'string' ? err.stderr : (err.stderr?.toString() ?? ''),
29
+ };
30
+ }
31
+ }
32
+
33
+ function runValidate(dataDir: string, { env, shapes }: RunOptions = {}): RunResult {
34
+ return execCaught('node', [SCRIPT, '--shapes', shapes ?? SHAPES, dataDir], {
35
+ env: { ...process.env, ...env },
36
+ });
37
+ }
38
+
39
+ function runValidateRaw(args: string[], { env }: RunOptions = {}): RunResult {
40
+ return execCaught('node', [SCRIPT, ...args], {
41
+ env: { ...process.env, ...env },
42
+ stdio: ['ignore', 'pipe', 'pipe'],
43
+ });
44
+ }
45
+
46
+ describe('validate-shacl.mjs', () => {
47
+ it('passes when a fixture conforms to the shapes', () => {
48
+ const goodDir = join(FIXTURES, 'good');
49
+ const result = runValidate(goodDir);
50
+ expect(result.code).toBe(0);
51
+ expect(result.stdout).toContain('SHACL validation passed');
52
+ });
53
+
54
+ it('fails when a fixture has missing language tags', () => {
55
+ const badDir = join(FIXTURES, 'bad');
56
+ const result = runValidate(badDir);
57
+ expect(result.code).not.toBe(0);
58
+ expect(result.stderr).toContain('SHACL validation FAILED');
59
+ expect(result.stderr).toContain('concept.ttl');
60
+ expect(result.stderr.toLowerCase()).toContain('langstring');
61
+ });
62
+
63
+ it('aggregates violations across multiple files', () => {
64
+ const result = runValidate(FIXTURES);
65
+ expect(result.code).not.toBe(0);
66
+ expect(result.stderr).not.toContain('good/concept.ttl');
67
+ expect(result.stderr).toContain('bad/concept.ttl');
68
+ });
69
+
70
+ it('exits cleanly when no .ttl files are found', () => {
71
+ const emptyDir = join(FIXTURES, 'empty');
72
+ const result = runValidate(emptyDir);
73
+ expect(result.code).toBe(0);
74
+ expect(result.stdout).toContain('No .ttl files');
75
+ });
76
+
77
+ it('errors clearly when --shapes path does not exist', () => {
78
+ const result = runValidate(FIXTURES, { shapes: '/does/not/exist.ttl' });
79
+ expect(result.code).not.toBe(0);
80
+ expect(result.stderr).toContain('Failed to load SHACL shapes');
81
+ });
82
+
83
+ it('accepts SHAPES_PATH env var as fallback when --shapes is omitted', () => {
84
+ const result = runValidate(join(FIXTURES, 'good'), {
85
+ env: { SHAPES_PATH: SHAPES },
86
+ });
87
+ expect(result.code).toBe(0);
88
+ expect(result.stdout).toContain('SHACL validation passed');
89
+ });
90
+
91
+ it('uses the vendored shapes by default when no override is given', () => {
92
+ const result = runValidateRaw([join(FIXTURES, 'good')], {
93
+ env: { SHAPES_PATH: '' },
94
+ });
95
+ // The vendored shapes live at data/concept-model/shapes/glossarist.shacl.ttl
96
+ // and ship with the repo. The good fixture conforms to them.
97
+ expect(result.code).toBe(0);
98
+ expect(result.stdout).toContain('SHACL validation passed');
99
+ });
100
+ });
@@ -12,6 +12,12 @@ import type {
12
12
  import type { Concept } from 'glossarist';
13
13
  import { conceptFromJson } from './model-bridge';
14
14
  import { GraphDataSource } from './GraphDataSource';
15
+ import {
16
+ ChunkLoadError,
17
+ ConceptNotFoundError,
18
+ IndexLoadError,
19
+ ManifestLoadError,
20
+ } from '../errors';
15
21
 
16
22
  // ── Wire-format types for JSON responses ────────────────────────────────────
17
23
 
@@ -82,7 +88,7 @@ export class DatasetAdapter {
82
88
  async loadManifest(): Promise<Manifest> {
83
89
  if (this.manifestComplete && this.manifest) return this.manifest;
84
90
  const resp = await fetch(`${this.baseUrl}/manifest.json`);
85
- if (!resp.ok) throw new Error(`Failed to load manifest for ${this.registerId}: ${resp.status}`);
91
+ if (!resp.ok) throw ManifestLoadError.make(this.registerId, resp.status);
86
92
  this.manifest = (await resp.json()) as Manifest;
87
93
  this.manifestComplete = true;
88
94
  return this.manifest;
@@ -97,7 +103,7 @@ export class DatasetAdapter {
97
103
  }
98
104
 
99
105
  const resp = await fetch(`${this.baseUrl}/index.json`);
100
- if (!resp.ok) throw new Error(`Failed to load index for ${this.registerId}: ${resp.status}`);
106
+ if (!resp.ok) throw IndexLoadError.make(this.registerId, resp.status);
101
107
  const data = await resp.json();
102
108
 
103
109
  // Handle both old format (with eng/status fields) and new format (with designations map)
@@ -150,7 +156,7 @@ export class DatasetAdapter {
150
156
  meta = await metaResp.json();
151
157
  } else {
152
158
  const resp = await fetch(`${this.baseUrl}/index.json`);
153
- if (!resp.ok) throw new Error(`Failed to load index for ${this.registerId}`);
159
+ if (!resp.ok) throw IndexLoadError.make(this.registerId, resp.status);
154
160
  const data = await resp.json();
155
161
  this.index = this.normalizeIndex(data as IndexJson);
156
162
  this.buildSummaryIndex();
@@ -180,7 +186,7 @@ export class DatasetAdapter {
180
186
  if (this.loadedChunks.has(chunkIndex)) return [];
181
187
  const chunkFile = `index-${String(chunkIndex).padStart(4, '0')}.json`;
182
188
  const resp = await fetch(`${this.baseUrl}/chunks/${chunkFile}`);
183
- if (!resp.ok) throw new Error(`Failed to load chunk ${chunkIndex} for ${this.registerId}`);
189
+ if (!resp.ok) throw ChunkLoadError.make(this.registerId, chunkIndex, resp.status);
184
190
  const data = await resp.json();
185
191
  this.loadedChunks.add(chunkIndex);
186
192
  return data.concepts as ConceptEntry[];
@@ -255,7 +261,7 @@ export class DatasetAdapter {
255
261
  }
256
262
 
257
263
  const resp = await fetch(`${this.baseUrl}/concepts/${conceptId}.json`);
258
- if (!resp.ok) throw new Error(`Concept ${conceptId} not found in ${this.registerId}`);
264
+ if (!resp.ok) throw ConceptNotFoundError.make(this.registerId, conceptId);
259
265
  const json = await resp.json();
260
266
  const concept = conceptFromJson(json);
261
267
  this.conceptCache.set(conceptId, concept);
@@ -4,6 +4,7 @@ import type { DatasetAdapter } from './DatasetAdapter';
4
4
  import { UriRouter } from './UriRouter';
5
5
  import { slugify } from '../utils/slugify';
6
6
  import { toSectionNode, toSectionTree } from '../utils/section-tree';
7
+ import { ConceptIdentity } from './concept-identity';
7
8
 
8
9
  interface DomainNodeJson {
9
10
  uri?: string;
@@ -23,7 +24,7 @@ function resolveRefTarget(rc: RelatedConcept, uriBase: string, registerId: strin
23
24
  if (ref.source && !ref.source.startsWith('http')) {
24
25
  reg = urnMap?.get(ref.source) ?? ref.source;
25
26
  }
26
- return `${uriBase}/${reg}/concept/${ref.id}`;
27
+ return new ConceptIdentity(ref.id, reg, uriBase).uri;
27
28
  }
28
29
  if (ref.source && ref.source.startsWith('http')) return ref.source;
29
30
  return ref.source || '';
@@ -1,4 +1,5 @@
1
1
  import type { Manifest } from './types';
2
+ import { ConceptIdentity } from './concept-identity';
2
3
 
3
4
  // ── URI pattern matching ────────────────────────────────────────────────────
4
5
 
@@ -104,7 +105,7 @@ export class UriRouter {
104
105
  buildUri(registerId: string, conceptId: string): string {
105
106
  const info = this.registerMap.get(registerId);
106
107
  const uriBase = info?.uriBase ?? '';
107
- return `${uriBase}/${registerId}/concept/${conceptId}`;
108
+ return new ConceptIdentity(conceptId, registerId, uriBase).uri;
108
109
  }
109
110
 
110
111
  getRegisteredIds(): string[] {
@@ -0,0 +1,69 @@
1
+ import { InvalidConceptIdentityError, InvalidConceptUriError } from '../errors';
2
+
3
+ export interface ConceptIdentityParts {
4
+ readonly localId: string;
5
+ readonly registerId: string;
6
+ readonly uriBase: string;
7
+ }
8
+
9
+ const CONCEPT_URI_RE = /^(.+)\/([^/]+)\/concept\/(.+)$/;
10
+
11
+ export class ConceptIdentity implements ConceptIdentityParts {
12
+ private readonly _uri: string;
13
+ private readonly _slug: string;
14
+ private readonly _path: string;
15
+
16
+ constructor(
17
+ public readonly localId: string,
18
+ public readonly registerId: string,
19
+ public readonly uriBase: string,
20
+ ) {
21
+ if (!localId || !registerId || !uriBase) {
22
+ throw new InvalidConceptIdentityError(
23
+ 'ConceptIdentity requires non-empty localId, registerId, and uriBase',
24
+ { localId, registerId, uriBase },
25
+ );
26
+ }
27
+ this._uri = `${uriBase}/${registerId}/concept/${localId}`;
28
+ this._slug = localId;
29
+ this._path = `${registerId}/concepts/${localId}`;
30
+ }
31
+
32
+ get uri(): string { return this._uri; }
33
+ get slug(): string { return this._slug; }
34
+ get path(): string { return this._path; }
35
+
36
+ equals(other: ConceptIdentity): boolean {
37
+ return this._uri === other._uri;
38
+ }
39
+
40
+ toString(): string { return this._uri; }
41
+
42
+ toJSON(): ConceptIdentityParts {
43
+ return { localId: this.localId, registerId: this.registerId, uriBase: this.uriBase };
44
+ }
45
+
46
+ localizationUri(lang: string): string {
47
+ return `${this._uri}/${lang}`;
48
+ }
49
+
50
+ designationUri(lang: string, slug: string): string {
51
+ return `${this._uri}/${lang}/desig/${slug}`;
52
+ }
53
+
54
+ domainUri(domainSlug: string): string {
55
+ return `${this.uriBase}/${this.registerId}/domain/${domainSlug}`;
56
+ }
57
+
58
+ static fromUri(uri: string): ConceptIdentity {
59
+ const m = uri.match(CONCEPT_URI_RE);
60
+ if (!m) {
61
+ throw InvalidConceptUriError.make(uri);
62
+ }
63
+ return new ConceptIdentity(m[3], m[2], m[1]);
64
+ }
65
+
66
+ static isConceptUri(uri: string): boolean {
67
+ return CONCEPT_URI_RE.test(uri);
68
+ }
69
+ }
@@ -5,6 +5,7 @@ import { ReferenceResolver } from './ReferenceResolver';
5
5
  import { UriRouter } from './UriRouter';
6
6
  import { NonVerbalEntityResolver } from './non-verbal-resolver';
7
7
  import { BibliographyAdapter } from './bibliography-adapter';
8
+ import { DatasetRegistryLoadError, UnknownDatasetError } from '../errors';
8
9
 
9
10
  export class AdapterFactory {
10
11
  private adapters = new Map<string, DatasetAdapter>();
@@ -36,7 +37,7 @@ export class AdapterFactory {
36
37
  registry = JSON.parse(inline.textContent) as DatasetRegistry[];
37
38
  } else {
38
39
  const resp = await fetch(datasetsUrl);
39
- if (!resp.ok) throw new Error(`Failed to load dataset registry: ${resp.status}`);
40
+ if (!resp.ok) throw DatasetRegistryLoadError.make(resp.status, datasetsUrl);
40
41
  registry = await resp.json() as DatasetRegistry[];
41
42
  }
42
43
 
@@ -132,7 +133,7 @@ export class AdapterFactory {
132
133
 
133
134
  async loadDataset(registerId: string): Promise<DatasetAdapter> {
134
135
  const adapter = this.adapters.get(registerId);
135
- if (!adapter) throw new Error(`Unknown dataset: ${registerId}`);
136
+ if (!adapter) throw UnknownDatasetError.make(registerId);
136
137
 
137
138
  const manifest = await adapter.loadManifest();
138
139
  await adapter.loadIndex();
@@ -36,6 +36,7 @@ import {
36
36
  GRAMMAR_PARTS_OF_SPEECH,
37
37
  } from 'glossarist/models';
38
38
  import type { ConceptSummary } from './types';
39
+ import { ConceptIdentity } from './concept-identity';
39
40
 
40
41
  // ── JSON-LD wire-format types ─────────────────────────────────────────────
41
42
 
@@ -634,5 +635,5 @@ export function conceptToSummary(concept: Concept): ConceptSummary {
634
635
 
635
636
  export function conceptUri(concept: Concept, registerId: string, uriBase: string): string {
636
637
  if (concept.uri) return concept.uri;
637
- return `${uriBase}/${registerId}/concept/${concept.id}`;
638
+ return new ConceptIdentity(concept.id, registerId, uriBase).uri;
638
639
  }
@@ -91,6 +91,13 @@ declare module 'glossarist' {
91
91
  static register(type: string, cls: typeof NonVerbalReference): void;
92
92
  }
93
93
 
94
+ interface NonVerbRep {
95
+ readonly caption: string | null;
96
+ readonly description: string | null;
97
+ readonly alt: string | null;
98
+ readonly images: FigureImage[];
99
+ }
100
+
94
101
  class FigureReference extends NonVerbalReference {
95
102
  static fromJSON(data: Record<string, unknown> | string): FigureReference;
96
103
  }
@@ -19,6 +19,7 @@ import type { NonVerbalKind } from './non-verbal/types';
19
19
  import type { NonVerbalEntity } from 'glossarist';
20
20
  import { KIND_TO_DIR, KIND_TO_BRIDGE } from './non-verbal/kind';
21
21
  import { anchorId } from '../utils/non-verbal-anchor';
22
+ import { NonVerbalEntityNotFoundError } from '../errors';
22
23
 
23
24
  export type { NonVerbalKind } from './non-verbal/types';
24
25
  export type { NonVerbalEntity } from 'glossarist';
@@ -54,7 +55,7 @@ export class NonVerbalEntityResolver {
54
55
  const resp = await this.fetcher(url);
55
56
  if (resp.status === 404) return null;
56
57
  if (!resp.ok) {
57
- throw new Error(`Failed to load ${kind} ${entityId} from ${datasetId}: ${resp.status}`);
58
+ throw NonVerbalEntityNotFoundError.make(datasetId, kind, entityId, resp.status);
58
59
  }
59
60
  const doc = (await resp.json()) as Record<string, unknown>;
60
61
  const entity = KIND_TO_BRIDGE[kind](doc);