@glossarist/concept-browser 0.7.60 → 0.7.62

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/README.md CHANGED
@@ -432,7 +432,7 @@ Sites currently powered by `@glossarist/concept-browser`. When cutting a release
432
432
  | [`geolexica/osgeo.geolexica.org`](https://github.com/geolexica/osgeo.geolexica.org) | https://osgeo.geolexica.org |
433
433
  | [`oimlsmart/vocab`](https://github.com/oimlsmart/vocab) | https://www.oimlsmart.org/vocab/ (VIML)¹ |
434
434
  | [`metanorma/oiml-terms`](https://github.com/metanorma/oiml-terms) | https://metanorma.github.io/oiml-terms/ |
435
- | [`metanorma/iala-vocab`](https://github.com/metanorma/iala-vocab) | https://metanorma.github.io/iala-vocab/ |
435
+ | [`glossarist/iala-vocab`](https://github.com/glossarist/iala-vocab) | https://glossarist.github.io/iala-vocab/ |
436
436
  | [`metanorma/iso-10303-2-vocab`](https://github.com/metanorma/iso-10303-2-vocab) | https://metanorma.github.io/iso-10303-2-vocab/ |
437
437
 
438
438
  To add a new deployment here, open a PR against this README.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glossarist/concept-browser",
3
- "version": "0.7.60",
3
+ "version": "0.7.62",
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
+ }
@@ -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
+ });
@@ -33,7 +33,7 @@ export const GROUP_TYPES: Record<DatasetGroupKind, GroupTypeMeta> = {
33
33
  kind: 'lineage',
34
34
  label: 'Edition series',
35
35
  description: 'Same vocabulary, different editions over time',
36
- glyph: '⏳',
36
+ glyph: '⟶',
37
37
  ordered: true,
38
38
  supersession: true,
39
39
  sameVocabulary: true,
@@ -2,6 +2,7 @@
2
2
  import { computed, ref, watch, onMounted, onUnmounted, onBeforeUnmount } from 'vue';
3
3
  import { useRoute, useRouter } from 'vue-router';
4
4
  import { useVocabularyStore } from '../stores/vocabulary';
5
+ import { useUiStore } from '../stores/ui';
5
6
  import { useDsStyle } from '../utils/dataset-style';
6
7
  import { useDatasetLoader } from '../composables/use-dataset-loader';
7
8
  import { FORMAT_LABELS } from '../config/types';
@@ -17,6 +18,7 @@ import { formatSectionLabel } from '../utils/section-display';
17
18
  const props = defineProps<{ registerId: string }>();
18
19
 
19
20
  const store = useVocabularyStore();
21
+ const uiStore = useUiStore();
20
22
  const { getStyle } = useDsStyle();
21
23
  const { ensureLoaded, loading, localError } = useDatasetLoader(() => props.registerId);
22
24
  const { t } = useI18n();
@@ -30,6 +32,16 @@ const localizedDescription = computed(() => localizedDatasetField(props.register
30
32
  const adapter = computed(() => store.datasets.get(props.registerId));
31
33
  const chunkLoading = ref(false);
32
34
 
35
+ // Concept-count badge: tinted bg + always-readable text in either mode.
36
+ // Previously used `.light` for bg and `.dark` for text — but `.dark` is
37
+ // just `.light` with alpha, so text was invisible against the bg.
38
+ const conceptCountBadgeStyle = computed(() => {
39
+ const s = getStyle(props.registerId);
40
+ return uiStore.isDark
41
+ ? { backgroundColor: s.darkAlpha(0.25), color: s.light }
42
+ : { backgroundColor: s.light, color: '#ffffff' };
43
+ });
44
+
33
45
  // Background chunk preloading via requestIdleCallback
34
46
  let idlePreloadHandle: ReturnType<typeof requestIdleCallback> | ReturnType<typeof setTimeout> | null = null;
35
47
 
@@ -310,7 +322,7 @@ function clearSection() {
310
322
  <h1 class="font-serif text-3xl text-ink-800 mb-2">{{ localizedTitle }}</h1>
311
323
  <p class="text-ink-400 leading-relaxed max-w-2xl">{{ localizedDescription }}</p>
312
324
  <div class="flex flex-wrap gap-2 mt-4">
313
- <span class="badge" :style="{ backgroundColor: getStyle(registerId).light, color: getStyle(registerId).dark }">{{ manifest.conceptCount.toLocaleString() }} {{ t('dataset.concepts') }}</span>
325
+ <span class="badge" :style="conceptCountBadgeStyle">{{ manifest.conceptCount.toLocaleString() }} {{ t('dataset.concepts') }}</span>
314
326
  <span class="badge badge-gray">{{ manifest.languages.length }} {{ t('dataset.languages') }}</span>
315
327
  <span class="badge badge-green">{{ manifest.owner }}</span>
316
328
  <router-link :to="{ name: 'stats', params: { registerId } }" class="badge badge-blue hover:opacity-80 transition-opacity">
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, ref } from 'vue';
3
3
  import { useVocabularyStore } from '../stores/vocabulary';
4
+ import { useUiStore } from '../stores/ui';
4
5
  import { useRouter } from 'vue-router';
5
6
  import { useDsStyle } from '../utils/dataset-style';
6
7
  import { useSiteConfig } from '../config/use-site-config';
@@ -8,10 +9,18 @@ import { useI18n } from '../i18n';
8
9
  import HomeSeriesSection from '../components/HomeSeriesSection.vue';
9
10
 
10
11
  const store = useVocabularyStore();
12
+ const uiStore = useUiStore();
11
13
  const router = useRouter();
12
14
  const { getStyle } = useDsStyle();
13
15
  const { config: siteConfig, localizedTitle, localizedSubtitle, localizedDescription, localizedDatasetField } = useSiteConfig();
14
16
  const { t } = useI18n();
17
+
18
+ function tagBadgeStyle(dsId: string) {
19
+ const s = getStyle(dsId);
20
+ return uiStore.isDark
21
+ ? { backgroundColor: s.darkAlpha(0.25), color: s.light }
22
+ : { backgroundColor: s.light, color: '#ffffff' };
23
+ }
15
24
  const exploring = ref(false);
16
25
 
17
26
  async function exploreRandom() {
@@ -181,7 +190,7 @@ function goToGraph() { router.push({ name: 'graph' }); }
181
190
  </div>
182
191
 
183
192
  <div class="flex flex-wrap gap-1.5 pl-[22px] mb-3">
184
- <span v-for="tag in (ds.manifest.tags ?? []).slice(0, 3)" :key="tag" class="badge text-[10px]" :style="{ backgroundColor: getStyle(ds.id).light, color: getStyle(ds.id).dark }">
193
+ <span v-for="tag in (ds.manifest.tags ?? []).slice(0, 3)" :key="tag" class="badge text-[10px]" :style="tagBadgeStyle(ds.id)">
185
194
  {{ tag }}
186
195
  </span>
187
196
  </div>