@glossarist/concept-browser 0.7.61 → 0.7.63

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.61",
3
+ "version": "0.7.63",
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": {
@@ -31,7 +31,7 @@
31
31
  "autoprefixer": "^10.4.21",
32
32
  "d3": "^7.9.0",
33
33
  "favicons": "^7.2.0",
34
- "glossarist": "^0.4.9",
34
+ "glossarist": "^0.4.10",
35
35
  "js-yaml": "^4.1.0",
36
36
  "jszip": "^3.10.1",
37
37
  "pinia": "^2.3.1",
@@ -91,7 +91,7 @@ function writeJson(filePath, data) {
91
91
  fs.writeFileSync(filePath, JSON.stringify(data));
92
92
  }
93
93
 
94
- function writeDatasetRdf(register, manifest, concepts, refMaps, opts) {
94
+ async function writeDatasetRdf(register, manifest, concepts, refMaps, opts) {
95
95
  const uriBase = refMaps?.uriBase ?? 'https://glossarist.org';
96
96
  const datasetIri = `${uriBase}/${register}/`;
97
97
  const topConceptUris = concepts
@@ -122,7 +122,7 @@ function writeDatasetRdf(register, manifest, concepts, refMaps, opts) {
122
122
  },
123
123
  ];
124
124
 
125
- const ttl = buildDatasetTurtle({
125
+ const ttl = await buildDatasetTurtle({
126
126
  datasetIri,
127
127
  registerId: register,
128
128
  title: manifest.title ?? register,
@@ -139,7 +139,7 @@ function writeDatasetRdf(register, manifest, concepts, refMaps, opts) {
139
139
  fs.writeFileSync(path.join(DATA, register, `${register}.ttl`), ttl);
140
140
  }
141
141
 
142
- function writeBuildActivity(conceptCount, datasetRegisters) {
142
+ async function writeBuildActivity(conceptCount, datasetRegisters) {
143
143
  const runId = process.env.GITHUB_RUN_ID
144
144
  ? `${process.env.GITHUB_RUN_ID}-${process.env.GITHUB_RUN_ATTEMPT ?? '1'}`
145
145
  : `local-${new Date().toISOString().replace(/[:.]/g, '-')}`;
@@ -150,7 +150,7 @@ function writeBuildActivity(conceptCount, datasetRegisters) {
150
150
  const pkgVersion = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')).version;
151
151
  const agentIri = process.env.CI_BOT_AGENT_IRI ?? null;
152
152
 
153
- const ttl = buildActivityTurtle({
153
+ const ttl = await buildActivityTurtle({
154
154
  runId,
155
155
  startedAt,
156
156
  endedAt,
@@ -1092,7 +1092,7 @@ async function processDataset(dir, register, opts) {
1092
1092
  writeJson(path.join(DATA, register, 'manifest.json'), manifest);
1093
1093
 
1094
1094
  // Dataset-level RDF (WS J2/J5): dcat:Dataset + skos:ConceptScheme + skos:Collection per section.
1095
- writeDatasetRdf(register, manifest, concepts, refMaps, opts);
1095
+ await writeDatasetRdf(register, manifest, concepts, refMaps, opts);
1096
1096
 
1097
1097
  // Copy bibliography.yaml → bibliography.json
1098
1098
  const bibPath = path.join(sourceRoot, 'bibliography.yaml');
@@ -1610,14 +1610,14 @@ for (const [id, count] of Object.entries(counts)) {
1610
1610
  console.log(` ${id}: ${count} concepts`);
1611
1611
  }
1612
1612
 
1613
- writeBuildActivity(total, registry.map(r => r.id));
1613
+ await writeBuildActivity(total, registry.map(r => r.id));
1614
1614
 
1615
1615
  fs.writeFileSync(path.join(DATA, '_vocab.ttl'), await buildVocabularyTurtle());
1616
1616
  console.log('Emitted vocabulary graph: data/_vocab.ttl');
1617
1617
 
1618
1618
  const contributors = config.contributors ?? [];
1619
1619
  if (contributors.length > 0) {
1620
- fs.writeFileSync(path.join(DATA, 'agents.ttl'), buildAgentsTurtle(contributors));
1620
+ fs.writeFileSync(path.join(DATA, 'agents.ttl'), await buildAgentsTurtle(contributors));
1621
1621
  console.log(`Emitted agents graph: data/agents.ttl (${contributors.length} contributors)`);
1622
1622
  }
1623
1623
 
@@ -1626,7 +1626,7 @@ for (const ds of registry) {
1626
1626
  const bibPath = path.join(DATA, ds.id, 'bibliography.json');
1627
1627
  if (fs.existsSync(bibPath)) {
1628
1628
  const bibJson = JSON.parse(fs.readFileSync(bibPath, 'utf8'));
1629
- const bibTtl = buildBibliographyTurtle(ds.id, bibJson);
1629
+ const bibTtl = await buildBibliographyTurtle(ds.id, bibJson);
1630
1630
  fs.writeFileSync(path.join(DATA, ds.id, 'bib.ttl'), bibTtl);
1631
1631
  }
1632
1632
  }
@@ -1644,14 +1644,14 @@ const datasetVersions = registry.map(ds => ({
1644
1644
  }));
1645
1645
 
1646
1646
  if (datasetVersions.length > 0) {
1647
- const versionTtl = datasetVersions.map(v =>
1647
+ const versionTtl = (await Promise.all(datasetVersions.map(v =>
1648
1648
  buildVersionHistoryTurtle({
1649
1649
  registerId: v.registerId,
1650
1650
  datasetIri: v.datasetIri,
1651
1651
  versions: v.versions,
1652
1652
  associatedAgentIri: process.env.CI_BOT_AGENT_IRI ?? null,
1653
1653
  }),
1654
- ).join('\n');
1654
+ ))).join('\n');
1655
1655
  fs.writeFileSync(path.join(DATA, 'versions.ttl'), versionTtl);
1656
1656
  console.log(`Emitted versions graph: data/versions.ttl (${datasetVersions.length} datasets)`);
1657
1657
  }
@@ -1,64 +1,26 @@
1
- function slugify(input) {
2
- return String(input)
3
- .toLowerCase()
4
- .normalize('NFKD')
5
- .replace(/[̀-ͯ]/g, '')
6
- .replace(/[^a-z0-9]+/g, '-')
7
- .replace(/^-+|-+$/g, '');
8
- }
1
+ // Agents turtle builder — emits foaf:Person / prov:Organization from
2
+ // contributor declarations.
3
+ //
4
+ // Delegates to glossarist-js's agents emitter + writeTurtle.
9
5
 
10
- function ttlLit(s) {
11
- if (s == null) return '""';
12
- const escaped = String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
13
- return `"${escaped}"`;
14
- }
6
+ import {
7
+ agentsFromContributors,
8
+ agentsToQuads,
9
+ collectQuads,
10
+ writeTurtle,
11
+ } from 'glossarist/rdf';
15
12
 
16
- function ttlPrefixed(qname) {
17
- const colonIdx = qname.indexOf(':');
18
- if (colonIdx < 0) return qname;
19
- const local = qname.slice(colonIdx + 1);
20
- const escaped = local.replace(/([/])/g, '\\$1');
21
- return `${qname.slice(0, colonIdx + 1)}${escaped}`;
13
+ function writerPrefixes() {
14
+ return {
15
+ foaf: 'http://xmlns.com/foaf/0.1/',
16
+ prov: 'http://www.w3.org/ns/prov#',
17
+ dcterms: 'http://purl.org/dc/terms/',
18
+ rdfs: 'http://www.w3.org/2000/01/rdf-schema#',
19
+ };
22
20
  }
23
21
 
24
- export function buildAgentsTurtle(contributors, agentBase = 'https://glossarist.org/agent') {
25
- const lines = [];
26
- lines.push('@prefix foaf: <http://xmlns.com/foaf/0.1/> .');
27
- lines.push('@prefix prov: <http://www.w3.org/ns/prov#> .');
28
- lines.push('@prefix dcterms: <http://purl.org/dc/terms/> .');
29
- lines.push('@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .');
30
- lines.push('');
31
-
32
- const orgSlugs = new Set();
33
- for (const c of contributors ?? []) {
34
- const slug = slugify(c.name);
35
- const iri = `${agentBase}/${slug}`;
36
- const personLines = [
37
- `<${iri}> a foaf:Person, prov:Person, prov:Agent ;`,
38
- ` foaf:name ${ttlLit(c.name)} ;`,
39
- ];
40
- if (c.email) personLines.push(` foaf:mbox <mailto:${c.email}> ;`);
41
- if (c.url) personLines.push(` rdfs:seeAlso <${c.url}> ;`);
42
- if (c.role) personLines.push(` dcterms:description ${ttlLit(c.role)} ;`);
43
- personLines[personLines.length - 1] = personLines[personLines.length - 1].replace(/ ;$/, ' .');
44
- lines.push(...personLines);
45
-
46
- if (c.organization) {
47
- const orgSlug = slugify(c.organization);
48
- const orgIri = `https://glossarist.org/org/${orgSlug}`;
49
- lines[lines.length - 1] = lines[lines.length - 1].replace(/\.$/, ` ;\n prov:actedOnBehalfOf <${orgIri}> .`);
50
-
51
- if (!orgSlugs.has(orgSlug)) {
52
- orgSlugs.add(orgSlug);
53
- const orgLines = [
54
- ``,
55
- `<${orgIri}> a foaf:Organization, prov:Organization, prov:Agent ;`,
56
- ` foaf:name ${ttlLit(c.organization)} .`,
57
- ];
58
- lines.push(...orgLines);
59
- }
60
- }
61
- lines.push('');
62
- }
63
- return lines.join('\n');
64
- }
22
+ export async function buildAgentsTurtle(contributors, agentBase = 'https://glossarist.org/agent') {
23
+ const agents = agentsFromContributors(contributors ?? [], agentBase);
24
+ const quads = collectQuads(agentsToQuads(agents));
25
+ return writeTurtle(quads, { prefixes: writerPrefixes() });
26
+ }
@@ -1,54 +1,29 @@
1
- import { ttlLit, ttlIri } from './turtle-escape.mjs';
1
+ // Bibliography turtle builder — emits dcterms:BibliographicResource
2
+ // per bibliography entry.
3
+ //
4
+ // Delegates to glossarist-js's bibliography emitter + writeTurtle.
2
5
 
3
- function entryFromV3(e, fallbackId) {
6
+ import {
7
+ bibliographyToQuads,
8
+ normalizeBibliographyData,
9
+ collectQuads,
10
+ writeTurtle,
11
+ } from 'glossarist/rdf';
12
+
13
+ const DEFAULT_BASE_URI = 'https://glossarist.org';
14
+
15
+ function writerPrefixes() {
4
16
  return {
5
- id: e.id ?? fallbackId ?? '',
6
- reference: e.reference ?? '',
7
- title: e.title,
8
- link: e.link,
9
- type: e.type,
17
+ dcterms: 'http://purl.org/dc/terms/',
18
+ foaf: 'http://xmlns.com/foaf/0.1/',
19
+ gloss: 'https://www.glossarist.org/ontologies/',
10
20
  };
11
21
  }
12
22
 
13
- export function normalizeBibliographyData(raw) {
14
- if (!raw || typeof raw !== 'object') return [];
15
- if (Array.isArray(raw.bibliography)) {
16
- return raw.bibliography.map(e => entryFromV3(e));
17
- }
18
- const entries = [];
19
- for (const [id, value] of Object.entries(raw)) {
20
- if (!value || typeof value !== 'object') continue;
21
- entries.push(entryFromV3(value, id));
22
- }
23
- return entries;
24
- }
25
-
26
- export function buildBibliographyTurtle(register, bibliographyJson, baseUri = 'https://glossarist.org') {
27
- const lines = [
28
- '@prefix dcterms: <http://purl.org/dc/terms/> .',
29
- '@prefix foaf: <http://xmlns.com/foaf/0.1/> .',
30
- '@prefix gloss: <https://www.glossarist.org/ontologies/> .',
31
- '',
32
- ];
23
+ export { normalizeBibliographyData };
33
24
 
34
- const datasetIri = `${baseUri}/${register}/`;
25
+ export async function buildBibliographyTurtle(register, bibliographyJson, baseUri = DEFAULT_BASE_URI) {
35
26
  const entries = normalizeBibliographyData(bibliographyJson);
36
-
37
- for (const entry of entries) {
38
- if (!entry.id || !entry.reference) continue;
39
- /* Percent-encode the bib id when embedding it in an IRI — many
40
- bibliography ids contain spaces or other reserved chars
41
- (e.g. "ISO/IEC 17000:2020") which are forbidden in raw IRI form. */
42
- const bibIri = `${datasetIri}bib/${encodeURIComponent(entry.id)}`;
43
- lines.push(`${ttlIri(bibIri)} a dcterms:BibliographicResource ;`);
44
- lines.push(` dcterms:identifier ${ttlLit(entry.id)} ;`);
45
- lines.push(` dcterms:bibliographicCitation ${ttlLit(entry.reference)} ;`);
46
- if (entry.title) lines.push(` dcterms:title ${ttlLit(entry.title)} ;`);
47
- if (entry.link) lines.push(` foaf:page ${ttlIri(entry.link)} ;`);
48
- if (entry.type) lines.push(` dcterms:type ${ttlIri(`${baseUri}/${register}/bibtype/${entry.type}`)} ;`);
49
- lines.push(` dcterms:isPartOf ${ttlIri(datasetIri)} .`);
50
- lines.push('');
51
- }
52
-
53
- return lines.join('\n');
54
- }
27
+ const quads = collectQuads(bibliographyToQuads({ registerId: register, entries, baseUri }));
28
+ return writeTurtle(quads, { prefixes: writerPrefixes() });
29
+ }
@@ -1,92 +1,26 @@
1
- const PREFIXES = [
2
- ['prov', 'http://www.w3.org/ns/prov#'],
3
- ['dcterms', 'http://purl.org/dc/terms/'],
4
- ['foaf', 'http://xmlns.com/foaf/0.1/'],
5
- ['rdf', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'],
6
- ['rdfs', 'http://www.w3.org/2000/01/rdf-schema#'],
7
- ['xsd', 'http://www.w3.org/2001/XMLSchema#'],
8
- ['gloss', 'https://www.glossarist.org/ontologies/'],
9
- ];
10
-
11
- function ttlLit(s) {
12
- if (s == null) return '""';
13
- const escaped = String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
14
- return `"${escaped}"`;
1
+ // Build activity turtle builder — emits prov:Activity per CI build run.
2
+ //
3
+ // Delegates to glossarist-js's build-activity emitter + writeTurtle.
4
+
5
+ import {
6
+ buildActivityToQuads,
7
+ collectQuads,
8
+ writeTurtle,
9
+ } from 'glossarist/rdf';
10
+
11
+ function writerPrefixes() {
12
+ return {
13
+ prov: 'http://www.w3.org/ns/prov#',
14
+ dcterms: 'http://purl.org/dc/terms/',
15
+ foaf: 'http://xmlns.com/foaf/0.1/',
16
+ rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
17
+ rdfs: 'http://www.w3.org/2000/01/rdf-schema#',
18
+ xsd: 'http://www.w3.org/2001/XMLSchema#',
19
+ gloss: 'https://www.glossarist.org/ontologies/',
20
+ };
15
21
  }
16
22
 
17
- export function buildActivityTurtle(input) {
18
- const lines = [];
19
- for (const [prefix, iri] of PREFIXES) {
20
- lines.push(`@prefix ${prefix}: <${iri}> .`);
21
- }
22
- lines.push('');
23
-
24
- const activityIri = `https://glossarist.org/activity/build/${input.runId}`;
25
- const usedEntities = [];
26
-
27
- if (input.gitSha) {
28
- usedEntities.push({
29
- iri: `https://glossarist.org/commit/${input.gitSha}`,
30
- types: ['prov:Entity'],
31
- label: input.gitSha,
32
- extras: input.gitBranch ? [`dcterms:description ${ttlLit(`branch: ${input.gitBranch}`)}`] : [],
33
- });
34
- }
35
-
36
- usedEntities.push({
37
- iri: `https://glossarist.org/tool/${input.toolId}/${input.toolVersion}`,
38
- types: ['prov:Entity', 'prov:SoftwareAgent'],
39
- label: `${input.toolId} ${input.toolVersion}`,
40
- extras: [
41
- `dcterms:identifier ${ttlLit(input.toolVersion)}`,
42
- `prov:version ${ttlLit(input.toolVersion)}`,
43
- ],
44
- });
45
-
46
- for (const register of input.datasetRegisters ?? []) {
47
- usedEntities.push({
48
- iri: `https://glossarist.org/${register}/`,
49
- types: ['prov:Entity'],
50
- label: register,
51
- extras: [],
52
- });
53
- }
54
-
55
- const activityLines = [
56
- `<${activityIri}> a prov:Activity ;`,
57
- ` rdfs:label ${ttlLit(`build ${input.runId}`)} ;`,
58
- ` prov:generatedAtTime "${input.endedAt}"^^xsd:dateTime ;`,
59
- ];
60
- for (const ent of usedEntities) {
61
- activityLines.push(` prov:used <${ent.iri}> ;`);
62
- }
63
- activityLines.push(` gloss:conceptCount "${input.conceptCount}"^^xsd:integer ;`);
64
- if (input.associatedAgentIri) {
65
- activityLines.push(` prov:wasAssociatedWith <${input.associatedAgentIri}> ;`);
66
- }
67
- activityLines[activityLines.length - 1] = activityLines[activityLines.length - 1].replace(/ ;$/, ' .');
68
- lines.push(...activityLines);
69
- lines.push('');
70
-
71
- for (const ent of usedEntities) {
72
- const entLines = [
73
- `<${ent.iri}> a ${ent.types.join(', ')} ;`,
74
- ` rdfs:label ${ttlLit(ent.label)}${ent.extras.length > 0 ? ' ;' : ' .'}`,
75
- ];
76
- for (let i = 0; i < ent.extras.length; i++) {
77
- const last = i === ent.extras.length - 1;
78
- entLines.push(` ${ent.extras[i]}${last ? ' .' : ' ;'}`);
79
- }
80
- lines.push(...entLines);
81
- lines.push('');
82
- }
83
-
84
- if (input.associatedAgentIri) {
85
- const label = input.associatedAgentIri.split('/').pop() ?? 'agent';
86
- lines.push(`<${input.associatedAgentIri}> a prov:Agent, foaf:Person ;`);
87
- lines.push(` rdfs:label ${ttlLit(label)} .`);
88
- lines.push('');
89
- }
90
-
91
- return lines.join('\n') + '\n';
23
+ export async function buildActivityTurtle(input) {
24
+ const quads = collectQuads(buildActivityToQuads(input));
25
+ return writeTurtle(quads, { prefixes: writerPrefixes() });
92
26
  }
@@ -1,79 +1,33 @@
1
- const PREFIXES = [
2
- ['dcat', 'http://www.w3.org/ns/dcat#'],
3
- ['skos', 'http://www.w3.org/2004/02/skos/core#'],
4
- ['dcterms', 'http://purl.org/dc/terms/'],
5
- ['rdf', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'],
6
- ['rdfs', 'http://www.w3.org/2000/01/rdf-schema#'],
7
- ['prov', 'http://www.w3.org/ns/prov#'],
8
- ['xsd', 'http://www.w3.org/2001/XMLSchema#'],
9
- ];
10
-
11
- function ttlLit(s) {
12
- if (s == null) return '""';
13
- const escaped = String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
14
- return `"${escaped}"`;
1
+ // Dataset turtle builder — emits dcat:Dataset + skos:ConceptScheme per
2
+ // register.
3
+ //
4
+ // Delegates to glossarist-js's dataset emitter + writeTurtle so the
5
+ // canonical prefix list, CURIE handling, and serialization stay in one
6
+ // place.
7
+
8
+ import {
9
+ datasetToQuads,
10
+ collectQuads,
11
+ writeTurtle,
12
+ PREFIXES,
13
+ } from 'glossarist/rdf';
14
+
15
+ function writerPrefixes() {
16
+ const out = {};
17
+ // Only the prefixes this emitter actually uses — keeps the output
18
+ // header focused.
19
+ for (const k of ['dcat', 'skos', 'dcterms', 'prov', 'rdf', 'rdfs', 'xsd']) {
20
+ if (PREFIXES[k]) out[k] = PREFIXES[k];
21
+ }
22
+ // dcat/prov are absent from glossarist-js's PREFIXES (which is
23
+ // generated from the JSON-LD context). Add them locally — they're
24
+ // in concept-model/prefixes.ttl.
25
+ out.dcat ??= 'http://www.w3.org/ns/dcat#';
26
+ out.prov ??= 'http://www.w3.org/ns/prov#';
27
+ return out;
15
28
  }
16
29
 
17
- export function buildDatasetTurtle(input) {
18
- const lines = [];
19
- for (const [prefix, iri] of PREFIXES) {
20
- lines.push(`@prefix ${prefix}: <${iri}> .`);
21
- }
22
- lines.push('');
23
-
24
- lines.push(`<${input.datasetIri}> a dcat:Dataset, skos:ConceptScheme ;`);
25
- lines.push(` dcterms:title ${ttlLit(input.title)} ;`);
26
- if (input.description) {
27
- lines.push(` dcterms:description ${ttlLit(input.description)} ;`);
28
- }
29
- lines.push(` dcterms:modified "${input.modified}"^^xsd:date ;`);
30
- lines.push(` dcterms:identifier ${ttlLit(input.registerId)} ;`);
31
-
32
- for (const lang of input.languages ?? []) {
33
- lines.push(` dcterms:language <http://id.loc.gov/vocabulary/iso639-1/${lang}> ;`);
34
- }
35
-
36
- for (const dist of input.distributions ?? []) {
37
- lines.push(' dcat:distribution [');
38
- lines.push(` a dcat:Distribution ;`);
39
- lines.push(` dcterms:title ${ttlLit(dist.title)} ;`);
40
- lines.push(` dcat:mediaType ${ttlLit(dist.mediaType)} ;`);
41
- lines.push(` dcat:downloadURL <${dist.downloadUrl}> ;`);
42
- if (dist.byteSize != null) {
43
- lines.push(` dcat:byteSize "${dist.byteSize}"^^xsd:integer ;`);
44
- }
45
- lines.push(' ] ;');
46
- }
47
-
48
- for (const concept of input.topConceptUris ?? []) {
49
- lines.push(` skos:hasTopConcept <${concept}> ;`);
50
- }
51
-
52
- if (input.sourceRepoUrl) {
53
- lines.push(` prov:wasDerivedFrom <${input.sourceRepoUrl}> ;`);
54
- }
55
- if (input.publisherIri) {
56
- lines.push(` dcterms:publisher <${input.publisherIri}> ;`);
57
- }
58
- if (input.contactIri) {
59
- lines.push(` dcat:contactPoint <${input.contactIri}> ;`);
60
- }
61
-
62
- lines[lines.length - 1] = lines[lines.length - 1].replace(/ ;$/, ' .');
63
-
64
- for (const section of input.sections ?? []) {
65
- lines.push('');
66
- lines.push(`<${section.collectionIri}> a skos:Collection ;`);
67
- lines.push(` dcterms:title ${ttlLit(section.title)} ;`);
68
- for (const member of section.memberUris) {
69
- lines.push(` skos:member <${member}> ;`);
70
- }
71
- if (section.memberUris.length === 0) {
72
- lines[lines.length - 1] = lines[lines.length - 1].replace(/ ;$/, ' .');
73
- } else {
74
- lines[lines.length - 1] = lines[lines.length - 1].replace(/ ;$/, ' .');
75
- }
76
- }
77
-
78
- return lines.join('\n') + '\n';
30
+ export async function buildDatasetTurtle(input) {
31
+ const quads = collectQuads(datasetToQuads(input));
32
+ return writeTurtle(quads, { prefixes: writerPrefixes() });
79
33
  }
@@ -1,56 +1,29 @@
1
- const PREFIXES = [
2
- ['prov', 'http://www.w3.org/ns/prov#'],
3
- ['dcterms', 'http://purl.org/dc/terms/'],
4
- ['rdfs', 'http://www.w3.org/2000/01/rdf-schema#'],
5
- ['xsd', 'http://www.w3.org/2001/XMLSchema#'],
6
- ];
1
+ // Version turtle builder — emits prov:Entity version chain per dataset.
2
+ //
3
+ // Delegates to glossarist-js's version emitter + writeTurtle.
7
4
 
8
- function ttlLit(s) {
9
- if (s == null) return '""';
10
- const escaped = String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
11
- return `"${escaped}"`;
12
- }
5
+ import {
6
+ versionToQuads,
7
+ versionHistoryToQuads,
8
+ collectQuads,
9
+ writeTurtle,
10
+ } from 'glossarist/rdf';
13
11
 
14
- export function buildVersionTurtle(input) {
15
- const lines = [];
16
- for (const [prefix, iri] of PREFIXES) {
17
- lines.push(`@prefix ${prefix}: <${iri}> .`);
18
- }
19
- lines.push('');
12
+ function writerPrefixes() {
13
+ return {
14
+ prov: 'http://www.w3.org/ns/prov#',
15
+ dcterms: 'http://purl.org/dc/terms/',
16
+ rdfs: 'http://www.w3.org/2000/01/rdf-schema#',
17
+ xsd: 'http://www.w3.org/2001/XMLSchema#',
18
+ };
19
+ }
20
20
 
21
- lines.push(`<${input.versionIri}> a prov:Entity ;`);
22
- lines.push(` rdfs:label ${ttlLit(`${input.registerId} version ${input.version}`)} ;`);
23
- lines.push(` dcterms:isVersionOf <${input.datasetIri}> ;`);
24
- if (input.previousVersionIri) {
25
- lines.push(` prov:wasRevisionOf <${input.previousVersionIri}> ;`);
26
- }
27
- lines.push(` prov:generatedAtTime "${input.generatedAt}"^^xsd:dateTime ;`);
28
- if (input.changeSummary) {
29
- lines.push(` dcterms:description ${ttlLit(input.changeSummary)} ;`);
30
- }
31
- if (input.associatedAgentIri) {
32
- lines.push(` prov:wasAssociatedWith <${input.associatedAgentIri}> ;`);
33
- }
34
- lines[lines.length - 1] = lines[lines.length - 1].replace(/ ;$/, ' .');
35
- return lines.join('\n') + '\n';
21
+ export async function buildVersionTurtle(input) {
22
+ const quads = collectQuads(versionToQuads(input));
23
+ return writeTurtle(quads, { prefixes: writerPrefixes() });
36
24
  }
37
25
 
38
- export function buildVersionHistoryTurtle(input) {
39
- let previousIri;
40
- const blocks = [];
41
- for (const v of input.versions ?? []) {
42
- const versionIri = `${input.datasetIri}versions/${v.version}`;
43
- blocks.push(buildVersionTurtle({
44
- registerId: input.registerId,
45
- version: v.version,
46
- versionIri,
47
- datasetIri: input.datasetIri,
48
- generatedAt: v.generatedAt,
49
- previousVersionIri: previousIri,
50
- changeSummary: v.changeSummary,
51
- associatedAgentIri: input.associatedAgentIri,
52
- }));
53
- previousIri = versionIri;
54
- }
55
- return blocks.join('\n');
56
- }
26
+ export async function buildVersionHistoryTurtle(input) {
27
+ const quads = collectQuads(versionHistoryToQuads(input));
28
+ return writeTurtle(quads, { prefixes: writerPrefixes() });
29
+ }
@@ -0,0 +1,45 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { UriRouter } from '../adapters/UriRouter';
3
+
4
+ describe('UriRouter.parseUri — deployment-agnostic concept URI parsing', () => {
5
+ // The old parseNeighborUri in RelationSphere.vue hardcoded specific
6
+ // hostnames. UriRouter.parseUri must work for ANY deployment.
7
+ const cases: Array<[string, string, string]> = [
8
+ // [input URI, expected registerId, expected conceptId]
9
+ ['https://www.glossarist.org/iala-vocab/iala-2022/concept/1-1-000', 'iala-2022', '1-1-000'],
10
+ ['https://glossarist.org/iala-2022/concept/1-1-000', 'iala-2022', '1-1-000'],
11
+ ['https://glossarist.org/iso/concept/3.1.1', 'iso', '3.1.1'],
12
+ ['https://oimlsmart.github.io/vocab/viml-2022/concept/1.1', 'viml-2022', '1.1'],
13
+ ['https://www.oimlsmart.org/vocab/viml-2022/concept/1.1', 'viml-2022', '1.1'],
14
+ ['https://metanorma.github.io/iala-vocab/iala-2023/concept/2-1-245', 'iala-2023', '2-1-245'],
15
+ ['https://isotc204.geolexica.org/isotc204-2022/concept/3.5.8.8', 'isotc204-2022', '3.5.8.8'],
16
+ ['https://vocab.example.org/dataset-x/concept/foo', 'dataset-x', 'foo'],
17
+ ['https://custom.host/path/to/register/concept/abc-123', 'register', 'abc-123'],
18
+ ];
19
+
20
+ for (const [uri, expectedReg, expectedId] of cases) {
21
+ it(`parses ${uri}`, () => {
22
+ const parsed = UriRouter.parseUri(uri);
23
+ expect(parsed).toEqual({ registerId: expectedReg, conceptId: expectedId });
24
+ });
25
+ }
26
+
27
+ it('returns null for URIs without /concept/', () => {
28
+ expect(UriRouter.parseUri('https://example.org/foo/bar')).toBeNull();
29
+ expect(UriRouter.parseUri('urn:iso:std:iso:14812:2025:3.5.8.8')).toBeNull();
30
+ expect(UriRouter.parseUri('')).toBeNull();
31
+ });
32
+
33
+ it('does NOT hardcode any specific hostname', () => {
34
+ // Every deployment domain should work. If the regex hardcodes
35
+ // a specific host, this test would fail for others.
36
+ const diverse = [
37
+ 'https://a.io/r/concept/1',
38
+ 'https://b.com/x/y/z/concept/2',
39
+ 'https://1.2.3.4/reg/concept/3',
40
+ ];
41
+ for (const uri of diverse) {
42
+ expect(UriRouter.parseUri(uri)).not.toBeNull();
43
+ }
44
+ });
45
+ });
@@ -15,14 +15,14 @@ const DCTERMS = 'http://purl.org/dc/terms/';
15
15
  const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
16
16
 
17
17
  describe('buildAgentsTurtle (mjs)', () => {
18
- it('parses without errors', () => {
19
- const ttl = buildAgentsTurtle([{ name: 'Ada Lovelace' }]);
18
+ it('parses without errors', async () => {
19
+ const ttl = await buildAgentsTurtle([{ name: 'Ada Lovelace' }]);
20
20
  const store = parse(ttl);
21
21
  expect(store.size).toBeGreaterThan(0);
22
22
  });
23
23
 
24
- it('types each person as foaf:Person, prov:Person, prov:Agent', () => {
25
- const ttl = buildAgentsTurtle([{ name: 'Ada Lovelace' }]);
24
+ it('types each person as foaf:Person, prov:Person, prov:Agent', async () => {
25
+ const ttl = await buildAgentsTurtle([{ name: 'Ada Lovelace' }]);
26
26
  const store = parse(ttl);
27
27
  const types = store.getObjects('https://glossarist.org/agent/ada-lovelace', RDF_TYPE, null).map(q => q.value);
28
28
  expect(types).toContain(`${FOAF}Person`);
@@ -30,8 +30,8 @@ describe('buildAgentsTurtle (mjs)', () => {
30
30
  expect(types).toContain(`${PROV}Agent`);
31
31
  });
32
32
 
33
- it('records name, mailto mbox, role, and seeAlso', () => {
34
- const ttl = buildAgentsTurtle([
33
+ it('records name, mailto mbox, role, and seeAlso', async () => {
34
+ const ttl = await buildAgentsTurtle([
35
35
  { name: 'Ada Lovelace', email: 'ada@example.org', role: 'Editor', url: 'https://example.org/ada' },
36
36
  ]);
37
37
  const store = parse(ttl);
@@ -42,8 +42,8 @@ describe('buildAgentsTurtle (mjs)', () => {
42
42
  expect(store.getObjects(iri, 'http://www.w3.org/2000/01/rdf-schema#seeAlso', null).map(q => q.value)).toContain('https://example.org/ada');
43
43
  });
44
44
 
45
- it('creates a single prov:Organization per unique organization', () => {
46
- const ttl = buildAgentsTurtle([
45
+ it('creates a single prov:Organization per unique organization', async () => {
46
+ const ttl = await buildAgentsTurtle([
47
47
  { name: 'Alice', organization: 'ISO' },
48
48
  { name: 'Bob', organization: 'ISO' },
49
49
  ]);
@@ -53,9 +53,9 @@ describe('buildAgentsTurtle (mjs)', () => {
53
53
  expect(types).toContain(`${PROV}Organization`);
54
54
  });
55
55
 
56
- it('emits only prefix declarations for an empty input', () => {
57
- const ttl = buildAgentsTurtle([]);
56
+ it('emits only prefix declarations for an empty input', async () => {
57
+ const ttl = await buildAgentsTurtle([]);
58
58
  expect(ttl).toContain('@prefix foaf:');
59
59
  expect(ttl).not.toContain('<https://glossarist.org/agent/');
60
60
  });
61
- });
61
+ });
@@ -14,21 +14,21 @@ function parse(turtle: string): Store {
14
14
  }
15
15
 
16
16
  describe('buildBibliographyTurtle (mjs)', () => {
17
- it('parses without errors', () => {
18
- const ttl = buildBibliographyTurtle('iso-geodetic', { iso704: { reference: 'ISO 704' } });
17
+ it('parses without errors', async () => {
18
+ const ttl = await buildBibliographyTurtle('iso-geodetic', { iso704: { reference: 'ISO 704' } });
19
19
  const store = parse(ttl);
20
20
  expect(store.size).toBeGreaterThan(0);
21
21
  });
22
22
 
23
- it('types each entry as dcterms:BibliographicResource', () => {
24
- const ttl = buildBibliographyTurtle('iso-geodetic', { iso704: { reference: 'ISO 704' } });
23
+ it('types each entry as dcterms:BibliographicResource', async () => {
24
+ const ttl = await buildBibliographyTurtle('iso-geodetic', { iso704: { reference: 'ISO 704' } });
25
25
  const store = parse(ttl);
26
26
  const types = store.getObjects('https://glossarist.org/iso-geodetic/bib/iso704', RDF_TYPE, null).map(q => q.value);
27
27
  expect(types).toContain(`${DCTERMS}BibliographicResource`);
28
28
  });
29
29
 
30
- it('emits identifier, bibliographicCitation, and title', () => {
31
- const ttl = buildBibliographyTurtle('iso-geodetic', {
30
+ it('emits identifier, bibliographicCitation, and title', async () => {
31
+ const ttl = await buildBibliographyTurtle('iso-geodetic', {
32
32
  iso704: { reference: 'ISO 704', title: 'Terminology work — Principles and methods' },
33
33
  });
34
34
  const store = parse(ttl);
@@ -38,8 +38,8 @@ describe('buildBibliographyTurtle (mjs)', () => {
38
38
  expect(store.getObjects(iri, `${DCTERMS}title`, null).map(q => q.value)).toContain('Terminology work — Principles and methods');
39
39
  });
40
40
 
41
- it('emits foaf:page when link is provided', () => {
42
- const ttl = buildBibliographyTurtle('iso-geodetic', {
41
+ it('emits foaf:page when link is provided', async () => {
42
+ const ttl = await buildBibliographyTurtle('iso-geodetic', {
43
43
  ref: { reference: 'X', link: 'https://example.org/x' },
44
44
  });
45
45
  const store = parse(ttl);
@@ -47,8 +47,8 @@ describe('buildBibliographyTurtle (mjs)', () => {
47
47
  expect(store.getObjects(iri, `${FOAF}page`, null).map(q => q.value)).toContain('https://example.org/x');
48
48
  });
49
49
 
50
- it('emits one resource per bibliography entry', () => {
51
- const ttl = buildBibliographyTurtle('iso-geodetic', {
50
+ it('emits one resource per bibliography entry', async () => {
51
+ const ttl = await buildBibliographyTurtle('iso-geodetic', {
52
52
  a: { reference: 'A' },
53
53
  b: { reference: 'B' },
54
54
  });
@@ -56,4 +56,4 @@ describe('buildBibliographyTurtle (mjs)', () => {
56
56
  expect(store.getObjects('https://glossarist.org/iso-geodetic/bib/a', `${DCTERMS}bibliographicCitation`, null).map(q => q.value)).toContain('A');
57
57
  expect(store.getObjects('https://glossarist.org/iso-geodetic/bib/b', `${DCTERMS}bibliographicCitation`, null).map(q => q.value)).toContain('B');
58
58
  });
59
- });
59
+ });
@@ -36,39 +36,39 @@ const expand = (v: string): string => {
36
36
  };
37
37
 
38
38
  describe('buildActivityTurtle (mjs)', () => {
39
- it('parses without errors', () => {
40
- const ttl = buildActivityTurtle(makeInput());
39
+ it('parses without errors', async () => {
40
+ const ttl = await buildActivityTurtle(makeInput());
41
41
  const store = parse(ttl);
42
42
  expect(store.size).toBeGreaterThan(0);
43
43
  });
44
44
 
45
- it('types the activity as prov:Activity', () => {
45
+ it('types the activity as prov:Activity', async () => {
46
46
  const input = makeInput();
47
- const store = parse(buildActivityTurtle(input));
47
+ const store = parse(await buildActivityTurtle(input));
48
48
  const types = store.getObjects(ACTIVITY_IRI(input.runId), expand('rdf:type'), null).map(q => q.value);
49
49
  expect(types).toContain(expand('prov:Activity'));
50
50
  });
51
51
 
52
- it('records git commit and tool as prov:used', () => {
52
+ it('records git commit and tool as prov:used', async () => {
53
53
  const input = makeInput();
54
- const store = parse(buildActivityTurtle(input));
54
+ const store = parse(await buildActivityTurtle(input));
55
55
  const used = store.getObjects(ACTIVITY_IRI(input.runId), expand('prov:used'), null).map(q => q.value);
56
56
  expect(used).toContain(`https://glossarist.org/commit/${input.gitSha}`);
57
57
  expect(used).toContain(`https://glossarist.org/tool/${input.toolId}/${input.toolVersion}`);
58
58
  });
59
59
 
60
- it('references every dataset register via prov:used', () => {
60
+ it('references every dataset register via prov:used', async () => {
61
61
  const input = makeInput({ datasetRegisters: ['a', 'b', 'c'] });
62
- const store = parse(buildActivityTurtle(input));
62
+ const store = parse(await buildActivityTurtle(input));
63
63
  const used = store.getObjects(ACTIVITY_IRI(input.runId), expand('prov:used'), null).map(q => q.value);
64
64
  for (const r of input.datasetRegisters) {
65
65
  expect(used).toContain(`https://glossarist.org/${r}/`);
66
66
  }
67
67
  });
68
68
 
69
- it('associates the CI agent when provided', () => {
69
+ it('associates the CI agent when provided', async () => {
70
70
  const input = makeInput();
71
- const store = parse(buildActivityTurtle(input));
71
+ const store = parse(await buildActivityTurtle(input));
72
72
  const agents = store.getObjects(ACTIVITY_IRI(input.runId), expand('prov:wasAssociatedWith'), null).map(q => q.value);
73
73
  expect(agents).toContain(input.associatedAgentIri);
74
74
  });
@@ -27,7 +27,7 @@ describe('WS F Layer 8 — build pipeline integration', () => {
27
27
  it('dataset-turtle.mjs produces parseable dcat:Dataset', () => {
28
28
  const out = runInline(`
29
29
  import { buildDatasetTurtle } from './scripts/lib/dataset-turtle.mjs';
30
- console.log(buildDatasetTurtle({
30
+ console.log(await buildDatasetTurtle({
31
31
  datasetIri: 'https://glossarist.org/test/',
32
32
  registerId: 'test', title: 'Test', modified: '2026-06-28',
33
33
  languages: ['eng'], distributions: [], topConceptUris: [], sections: [],
@@ -45,7 +45,7 @@ describe('WS F Layer 8 — build pipeline integration', () => {
45
45
  it('build-activity-turtle.mjs produces parseable prov:Activity', () => {
46
46
  const out = runInline(`
47
47
  import { buildActivityTurtle } from './scripts/lib/build-activity-turtle.mjs';
48
- console.log(buildActivityTurtle({
48
+ console.log(await buildActivityTurtle({
49
49
  runId: 'test', startedAt: '2026-01-01T00:00:00Z', endedAt: '2026-01-01T00:05:00Z',
50
50
  toolId: 'cb', toolVersion: '0.7.52', datasetRegisters: [], conceptCount: 0,
51
51
  }));
@@ -61,7 +61,7 @@ describe('WS F Layer 8 — build pipeline integration', () => {
61
61
  it('agents-turtle.mjs produces parseable foaf:Person', () => {
62
62
  const out = runInline(`
63
63
  import { buildAgentsTurtle } from './scripts/lib/agents-turtle.mjs';
64
- console.log(buildAgentsTurtle([{ name: 'Ada Lovelace', role: 'Editor', organization: 'Royal Society' }]));
64
+ console.log(await buildAgentsTurtle([{ name: 'Ada Lovelace', role: 'Editor', organization: 'Royal Society' }]));
65
65
  `);
66
66
  const store = parseTurtle(out);
67
67
  const persons = [...store].filter(q =>
@@ -74,7 +74,7 @@ describe('WS F Layer 8 — build pipeline integration', () => {
74
74
  it('bibliography-turtle.mjs produces parseable dcterms:BibliographicResource', () => {
75
75
  const out = runInline(`
76
76
  import { buildBibliographyTurtle } from './scripts/lib/bibliography-turtle.mjs';
77
- console.log(buildBibliographyTurtle('test', { iso704: { reference: 'ISO 704' } }));
77
+ console.log(await buildBibliographyTurtle('test', { iso704: { reference: 'ISO 704' } }));
78
78
  `);
79
79
  const store = parseTurtle(out);
80
80
  const bibs = [...store].filter(q =>
@@ -87,7 +87,7 @@ describe('WS F Layer 8 — build pipeline integration', () => {
87
87
  it('version-turtle.mjs produces parseable prov:Entity chain', () => {
88
88
  const out = runInline(`
89
89
  import { buildVersionHistoryTurtle } from './scripts/lib/version-turtle.mjs';
90
- console.log(buildVersionHistoryTurtle({
90
+ console.log(await buildVersionHistoryTurtle({
91
91
  registerId: 'test', datasetIri: 'https://glossarist.org/test/',
92
92
  versions: [
93
93
  { version: '1.0', generatedAt: '2024-01-01T00:00:00Z' },
@@ -120,11 +120,11 @@ describe('WS F Layer 8 — build pipeline integration', () => {
120
120
  import { buildVersionHistoryTurtle } from './scripts/lib/version-turtle.mjs';
121
121
  const parts = [
122
122
  await buildVocabularyTurtle(),
123
- buildDatasetTurtle({ datasetIri: 'https://glossarist.org/test/', registerId: 'test', title: 't', modified: '2026-01-01', languages: ['eng'], distributions: [], topConceptUris: [], sections: [] }),
124
- buildActivityTurtle({ runId: 'r', startedAt: '2026-01-01T00:00:00Z', endedAt: '2026-01-01T00:05:00Z', toolId: 'cb', toolVersion: '0', datasetRegisters: [], conceptCount: 0 }),
125
- buildAgentsTurtle([{ name: 'Ada' }]),
126
- buildBibliographyTurtle('test', { x: { reference: 'X' } }),
127
- buildVersionHistoryTurtle({ registerId: 't', datasetIri: 'https://glossarist.org/t/', versions: [{ version: '1', generatedAt: '2026-01-01' }] }),
123
+ await buildDatasetTurtle({ datasetIri: 'https://glossarist.org/test/', registerId: 'test', title: 't', modified: '2026-01-01', languages: ['eng'], distributions: [], topConceptUris: [], sections: [] }),
124
+ await buildActivityTurtle({ runId: 'r', startedAt: '2026-01-01T00:00:00Z', endedAt: '2026-01-01T00:05:00Z', toolId: 'cb', toolVersion: '0', datasetRegisters: [], conceptCount: 0 }),
125
+ await buildAgentsTurtle([{ name: 'Ada' }]),
126
+ await buildBibliographyTurtle('test', { x: { reference: 'X' } }),
127
+ await buildVersionHistoryTurtle({ registerId: 't', datasetIri: 'https://glossarist.org/t/', versions: [{ version: '1', generatedAt: '2026-01-01' }] }),
128
128
  ];
129
129
  console.log(parts.join('\\n'));
130
130
  `);
@@ -50,23 +50,23 @@ const expand = (v: string): string => {
50
50
  };
51
51
 
52
52
  describe('buildDatasetTurtle (mjs)', () => {
53
- it('parses without errors and produces a non-empty graph', () => {
54
- const ttl = buildDatasetTurtle(makeInput());
53
+ it('parses without errors and produces a non-empty graph', async () => {
54
+ const ttl = await buildDatasetTurtle(makeInput());
55
55
  const store = parse(ttl);
56
56
  expect(store.size).toBeGreaterThan(0);
57
57
  });
58
58
 
59
- it('types the dataset as dcat:Dataset and skos:ConceptScheme', () => {
59
+ it('types the dataset as dcat:Dataset and skos:ConceptScheme', async () => {
60
60
  const input = makeInput();
61
- const store = parse(buildDatasetTurtle(input));
61
+ const store = parse(await buildDatasetTurtle(input));
62
62
  const types = store.getObjects(input.datasetIri, expand('rdf:type'), null).map(q => q.value);
63
63
  expect(types).toContain(expand('dcat:Dataset'));
64
64
  expect(types).toContain(expand('skos:ConceptScheme'));
65
65
  });
66
66
 
67
- it('emits a dcat:Distribution blank per distribution', () => {
67
+ it('emits a dcat:Distribution blank per distribution', async () => {
68
68
  const input = makeInput();
69
- const store = parse(buildDatasetTurtle(input));
69
+ const store = parse(await buildDatasetTurtle(input));
70
70
  const dists = store.getObjects(input.datasetIri, expand('dcat:distribution'), null);
71
71
  expect(dists.length).toBe(1);
72
72
  const mediaTypes = store.getObjects(dists[0], expand('dcat:mediaType'), null).map(q => q.value);
@@ -75,9 +75,9 @@ describe('buildDatasetTurtle (mjs)', () => {
75
75
  expect(byteSizes).toContain('12345');
76
76
  });
77
77
 
78
- it('emits skos:Collection per section with skos:member entries', () => {
78
+ it('emits skos:Collection per section with skos:member entries', async () => {
79
79
  const input = makeInput();
80
- const store = parse(buildDatasetTurtle(input));
80
+ const store = parse(await buildDatasetTurtle(input));
81
81
  const section = input.sections[0];
82
82
  const types = store.getObjects(section.collectionIri, expand('rdf:type'), null).map(q => q.value);
83
83
  expect(types).toContain(expand('skos:Collection'));
@@ -85,9 +85,9 @@ describe('buildDatasetTurtle (mjs)', () => {
85
85
  expect(members).toContain(`${BASE}/test/concept/1`);
86
86
  });
87
87
 
88
- it('escapes double-quotes and backslashes in titles', () => {
88
+ it('escapes double-quotes and backslashes in titles', async () => {
89
89
  const input = makeInput({ title: 'has "quotes" and back\\slash' });
90
- const store = parse(buildDatasetTurtle(input));
90
+ const store = parse(await buildDatasetTurtle(input));
91
91
  const titles = store.getObjects(input.datasetIri, expand('dcterms:title'), null).map(q => q.value);
92
92
  expect(titles).toContain('has "quotes" and back\\slash');
93
93
  });
@@ -14,8 +14,8 @@ const DCTERMS = 'http://purl.org/dc/terms/';
14
14
  const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
15
15
 
16
16
  describe('buildVersionTurtle (mjs)', () => {
17
- it('parses without errors', () => {
18
- const ttl = buildVersionTurtle({
17
+ it('parses without errors', async () => {
18
+ const ttl = await buildVersionTurtle({
19
19
  registerId: 'test',
20
20
  version: '1.0',
21
21
  versionIri: 'https://glossarist.org/test/versions/1.0',
@@ -26,8 +26,8 @@ describe('buildVersionTurtle (mjs)', () => {
26
26
  expect(store.size).toBeGreaterThan(0);
27
27
  });
28
28
 
29
- it('types the version as prov:Entity', () => {
30
- const ttl = buildVersionTurtle({
29
+ it('types the version as prov:Entity', async () => {
30
+ const ttl = await buildVersionTurtle({
31
31
  registerId: 'test',
32
32
  version: '1.0',
33
33
  versionIri: 'https://glossarist.org/test/versions/1.0',
@@ -39,8 +39,8 @@ describe('buildVersionTurtle (mjs)', () => {
39
39
  expect(types).toContain(`${PROV}Entity`);
40
40
  });
41
41
 
42
- it('emits prov:wasRevisionOf when a previous version is provided', () => {
43
- const ttl = buildVersionTurtle({
42
+ it('emits prov:wasRevisionOf when a previous version is provided', async () => {
43
+ const ttl = await buildVersionTurtle({
44
44
  registerId: 'test',
45
45
  version: '1.0',
46
46
  versionIri: 'https://glossarist.org/test/versions/1.0',
@@ -55,8 +55,8 @@ describe('buildVersionTurtle (mjs)', () => {
55
55
  });
56
56
 
57
57
  describe('buildVersionHistoryTurtle (mjs)', () => {
58
- it('emits a chain with revision links', () => {
59
- const ttl = buildVersionHistoryTurtle({
58
+ it('emits a chain with revision links', async () => {
59
+ const ttl = await buildVersionHistoryTurtle({
60
60
  registerId: 'test',
61
61
  datasetIri: 'https://glossarist.org/test/',
62
62
  versions: [
@@ -69,4 +69,4 @@ describe('buildVersionHistoryTurtle (mjs)', () => {
69
69
  const prev = store.getObjects(iri11, `${PROV}wasRevisionOf`, null).map(q => q.value);
70
70
  expect(prev).toContain('https://glossarist.org/test/versions/1.0');
71
71
  });
72
- });
72
+ });
@@ -18,6 +18,7 @@ import { useDsStyle } from '../utils/dataset-style';
18
18
  import { getFactory } from '../adapters/factory';
19
19
  import { SPHERE_RELATION_CATEGORIES as RELATION_CATEGORIES, sphereCategoryForType as categoryForType, categorizeRelationForSphere as categorizeRelation, colorForTypeInMode as colorForTypeRaw, relationLabel as relationTypeLabel } from '../utils/relation-sphere-styling';
20
20
  import { easeInOutCubic, slerp, fibonacciSpherePosition, project, cardEdge, type Vec3 } from '../composables/useSphereProjection';
21
+ import { UriRouter } from '../adapters/UriRouter';
21
22
  import { getPreferredTerm } from '../utils/concept-helpers';
22
23
  import { renderContent } from '../utils/content-renderer';
23
24
  import { useI18n } from '../i18n';
@@ -226,14 +227,6 @@ function updateNodeTerm(n: SNode) {
226
227
  }
227
228
 
228
229
  /* ── Build internal graph from concept + edges (BFS) ────── */
229
- function parseNeighborUri(uri: string, fallbackReg: string): { register: string; conceptId: string } | null {
230
- const m = uri.match(/(?:glossarist\.org|oimlsmart\.github\.io\/vocab)\/([^/]+)\/concept\/([^/?#]+)/);
231
- if (m) return { register: m[1], conceptId: m[2] };
232
- /* URN or other shape — use fallback register, last path segment as id */
233
- const parts = uri.split('/').filter(Boolean);
234
- if (parts.length === 0) return null;
235
- return { register: fallbackReg, conceptId: parts[parts.length - 1] };
236
- }
237
230
 
238
231
  function hashSeed(s: string): number {
239
232
  let h = 0;
@@ -280,12 +273,12 @@ function buildGraph() {
280
273
 
281
274
  /* First pass: collect depth-1 neighbors (deduped) so we know N for even
282
275
  ring placement. */
283
- const depth1List: Array<{ uri: string; parsed: { register: string; conceptId: string }; isOutgoing: boolean; edge: GraphEdge }> = [];
276
+ const depth1List: Array<{ uri: string; parsed: { registerId: string; conceptId: string }; isOutgoing: boolean; edge: GraphEdge }> = [];
284
277
  for (const edge of props.edges) {
285
278
  const isOutgoing = edge.source === focusUri || edge.source === focusId;
286
279
  const otherUri = isOutgoing ? edge.target : edge.source;
287
280
  if (!otherUri || visited.has(otherUri)) continue;
288
- const parsed = parseNeighborUri(otherUri, edge.register || props.registerId);
281
+ const parsed = UriRouter.parseUri(otherUri);
289
282
  if (!parsed || parsed.conceptId === focusId) continue;
290
283
  visited.set(otherUri, 1);
291
284
  depth1List.push({ uri: otherUri, parsed, isOutgoing, edge });
@@ -298,8 +291,8 @@ function buildGraph() {
298
291
  nodeMap.set(parsed.conceptId, {
299
292
  id: parsed.conceptId,
300
293
  term: edge.label || parsed.conceptId,
301
- ref: parsed.register,
302
- register: parsed.register,
294
+ ref: parsed.registerId,
295
+ register: parsed.registerId,
303
296
  conceptId: parsed.conceptId,
304
297
  depth: 1,
305
298
  x: pos.x, y: pos.y, z: pos.z,
@@ -331,13 +324,13 @@ function buildGraph() {
331
324
  const inEdges = store.graph.getIncomingEdges(uri);
332
325
  /* Collect this node's children first so we can place them as an even
333
326
  sub-ring around their parent. */
334
- const children: Array<{ uri: string; parsed: { register: string; conceptId: string }; isOutgoing: boolean; edge: GraphEdge }> = [];
327
+ const children: Array<{ uri: string; parsed: { registerId: string; conceptId: string }; isOutgoing: boolean; edge: GraphEdge }> = [];
335
328
  for (const edge of [...outEdges, ...inEdges]) {
336
329
  if (nodeMap.size + children.length >= MAX_NODES) break;
337
330
  const isOutgoing = edge.source === uri;
338
331
  const otherUri = isOutgoing ? edge.target : edge.source;
339
332
  if (!otherUri || visited.has(otherUri)) continue;
340
- const parsed = parseNeighborUri(otherUri, edge.register || props.registerId);
333
+ const parsed = UriRouter.parseUri(otherUri);
341
334
  if (!parsed || parsed.conceptId === focusId || nodeMap.has(parsed.conceptId)) continue;
342
335
  visited.set(otherUri, depth + 1);
343
336
  children.push({ uri: otherUri, parsed, isOutgoing, edge });
@@ -347,8 +340,8 @@ function buildGraph() {
347
340
  nodeMap.set(parsed.conceptId, {
348
341
  id: parsed.conceptId,
349
342
  term: parsed.conceptId,
350
- ref: parsed.register,
351
- register: parsed.register,
343
+ ref: parsed.registerId,
344
+ register: parsed.registerId,
352
345
  conceptId: parsed.conceptId,
353
346
  depth: depth + 1,
354
347
  x: pos.x, y: pos.y, z: pos.z,