@glossarist/concept-browser 0.7.51 → 0.7.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/cli/index.mjs +32 -0
  2. package/env.d.ts +15 -0
  3. package/package.json +12 -2
  4. package/scripts/__tests__/doctor.test.mjs +147 -0
  5. package/scripts/doctor.mjs +327 -0
  6. package/scripts/generate-data.mjs +136 -0
  7. package/scripts/generate-ontology-data.mjs +3 -3
  8. package/scripts/generate-ontology-schema.mjs +3 -3
  9. package/scripts/lib/agents-turtle.mjs +64 -0
  10. package/scripts/lib/bibliography-turtle.mjs +54 -0
  11. package/scripts/lib/build-activity-turtle.mjs +92 -0
  12. package/scripts/lib/build-cache.mjs +70 -0
  13. package/scripts/lib/dataset-turtle.mjs +79 -0
  14. package/scripts/lib/turtle-escape.mjs +0 -0
  15. package/scripts/lib/version-turtle.mjs +56 -0
  16. package/scripts/lib/vocab-turtle.mjs +64 -0
  17. package/scripts/normalize-yaml.mjs +99 -0
  18. package/scripts/sync-concept-model.mjs +86 -0
  19. package/scripts/validate-shacl.mjs +194 -0
  20. package/src/App.vue +2 -0
  21. package/src/__fixtures__/concept-shape.ttl +20 -0
  22. package/src/__fixtures__/shacl/bad/concept.ttl +7 -0
  23. package/src/__fixtures__/shacl/empty/.gitkeep +0 -0
  24. package/src/__fixtures__/shacl/good/concept.ttl +8 -0
  25. package/src/__tests__/__fixtures__/concepts.ts +221 -0
  26. package/src/__tests__/adapters/concept-identity.test.ts +76 -0
  27. package/src/__tests__/components/error-boundary.test.ts +109 -0
  28. package/src/__tests__/composables/use-dataset-series.test.ts +262 -0
  29. package/src/__tests__/concept-rdf/agents-emitter.test.ts +110 -0
  30. package/src/__tests__/concept-rdf/bibliography-emitter.test.ts +159 -0
  31. package/src/__tests__/concept-rdf/build-activity-emitter.test.ts +119 -0
  32. package/src/__tests__/concept-rdf/coerce-date.test.ts +97 -0
  33. package/src/__tests__/concept-rdf/concept-emitter.test.ts +258 -0
  34. package/src/__tests__/concept-rdf/dataset-emitter.test.ts +224 -0
  35. package/src/__tests__/concept-rdf/differential.test.ts +96 -0
  36. package/src/__tests__/concept-rdf/group-emitter.test.ts +109 -0
  37. package/src/__tests__/concept-rdf/image-variant-emitter.test.ts +135 -0
  38. package/src/__tests__/concept-rdf/jsonld-writer.test.ts +109 -0
  39. package/src/__tests__/concept-rdf/nonverbal-rep.test.ts +177 -0
  40. package/src/__tests__/concept-rdf/property-based.test.ts +179 -0
  41. package/src/__tests__/concept-rdf/provenance.test.ts +110 -0
  42. package/src/__tests__/concept-rdf/quad-isomorphism.test.ts +43 -0
  43. package/src/__tests__/concept-rdf/quad-isomorphism.ts +47 -0
  44. package/src/__tests__/concept-rdf/rdf-components.test.ts +145 -0
  45. package/src/__tests__/concept-rdf/rdf-graph.test.ts +115 -0
  46. package/src/__tests__/concept-rdf/round-trip.test.ts +243 -0
  47. package/src/__tests__/concept-rdf/scoped-examples.test.ts +126 -0
  48. package/src/__tests__/concept-rdf/sections-builder.test.ts +94 -0
  49. package/src/__tests__/concept-rdf/shacl-conformance.test.ts +110 -0
  50. package/src/__tests__/concept-rdf/shape-consistency.test.ts +138 -0
  51. package/src/__tests__/concept-rdf/snapshot-generation.test.ts +75 -0
  52. package/src/__tests__/concept-rdf/table-formula-emitter.test.ts +142 -0
  53. package/src/__tests__/concept-rdf/turtle-writer.test.ts +114 -0
  54. package/src/__tests__/concept-rdf/use-rdf-document.test.ts +246 -0
  55. package/src/__tests__/concept-rdf/version-emitter.test.ts +120 -0
  56. package/src/__tests__/concept-rdf/vocabulary-ssot.test.ts +100 -0
  57. package/src/__tests__/concept-rdf-view.test.ts +136 -0
  58. package/src/__tests__/config/group-renderers.test.ts +35 -0
  59. package/src/__tests__/config/group-types.test.ts +76 -0
  60. package/src/__tests__/dataset-style.test.ts +12 -7
  61. package/src/__tests__/errors/errors.test.ts +142 -0
  62. package/src/__tests__/format-downloads.test.ts +47 -65
  63. package/src/__tests__/markdown-lite.test.ts +19 -0
  64. package/src/__tests__/perf/bundle-layout.test.ts +50 -0
  65. package/src/__tests__/perf/serialization-perf.test.ts +121 -0
  66. package/src/__tests__/scripts/agents-turtle.test.ts +61 -0
  67. package/src/__tests__/scripts/bibliography-turtle.test.ts +59 -0
  68. package/src/__tests__/scripts/build-activity-turtle.test.ts +75 -0
  69. package/src/__tests__/scripts/build-cache.test.ts +78 -0
  70. package/src/__tests__/scripts/build-integration.test.ts +134 -0
  71. package/src/__tests__/scripts/dataset-turtle.test.ts +94 -0
  72. package/src/__tests__/scripts/normalize-yaml.test.ts +98 -0
  73. package/src/__tests__/scripts/stryker-config.test.ts +33 -0
  74. package/src/__tests__/scripts/turtle-escape.test.ts +63 -0
  75. package/src/__tests__/scripts/version-turtle.test.ts +72 -0
  76. package/src/__tests__/scripts/vocab-turtle.test.ts +63 -0
  77. package/src/__tests__/use-format-registry.test.ts +125 -0
  78. package/src/__tests__/utils/bcp47.test.ts +166 -0
  79. package/src/__tests__/utils/color-theme.test.ts +143 -0
  80. package/src/__tests__/utils/url-safety.test.ts +65 -0
  81. package/src/__tests__/validate-shacl.test.ts +100 -0
  82. package/src/adapters/DatasetAdapter.ts +11 -5
  83. package/src/adapters/GraphDataSource.ts +2 -1
  84. package/src/adapters/UriRouter.ts +2 -1
  85. package/src/adapters/concept-identity.ts +69 -0
  86. package/src/adapters/factory.ts +3 -2
  87. package/src/adapters/model-bridge.ts +2 -1
  88. package/src/adapters/non-verbal/glossarist-augment.d.ts +7 -0
  89. package/src/adapters/non-verbal-resolver.ts +2 -1
  90. package/src/components/AppSidebar.vue +189 -93
  91. package/src/components/ConceptDetail.vue +8 -0
  92. package/src/components/ConceptEditionRail.vue +222 -0
  93. package/src/components/ConceptRdfView.vue +37 -377
  94. package/src/components/DatasetSeriesCard.vue +270 -0
  95. package/src/components/ErrorBoundary.vue +95 -0
  96. package/src/components/FormatDownloads.vue +17 -13
  97. package/src/components/HomeSeriesSection.vue +277 -0
  98. package/src/components/RelationSphere.vue +1672 -0
  99. package/src/components/SidebarSeriesSection.vue +239 -0
  100. package/src/components/concept-rdf/RdfInstanceHeader.vue +47 -0
  101. package/src/components/concept-rdf/RdfInstanceSection.vue +54 -0
  102. package/src/components/concept-rdf/RdfPrefixLegend.vue +27 -0
  103. package/src/components/concept-rdf/RdfSourcePanel.vue +72 -0
  104. package/src/components/concept-rdf/agents-emitter.ts +82 -0
  105. package/src/components/concept-rdf/bibliography-emitter.ts +83 -0
  106. package/src/components/concept-rdf/build-activity-emitter.ts +89 -0
  107. package/src/components/concept-rdf/concept-emitter.ts +443 -0
  108. package/src/components/concept-rdf/dataset-emitter.ts +95 -0
  109. package/src/components/concept-rdf/group-emitter.ts +69 -0
  110. package/src/components/concept-rdf/image-variant-emitter.ts +46 -0
  111. package/src/components/concept-rdf/jsonld-writer.ts +82 -0
  112. package/src/components/concept-rdf/predicates.ts +261 -0
  113. package/src/components/concept-rdf/provenance.ts +80 -0
  114. package/src/components/concept-rdf/rdf-graph.ts +211 -0
  115. package/src/components/concept-rdf/rdf-prefixes.ts +23 -0
  116. package/src/components/concept-rdf/sections-builder.ts +62 -0
  117. package/src/components/concept-rdf/table-formula-emitter.ts +101 -0
  118. package/src/components/concept-rdf/turtle-writer.ts +116 -0
  119. package/src/components/concept-rdf/use-rdf-document.ts +72 -0
  120. package/src/components/concept-rdf/version-emitter.ts +65 -0
  121. package/src/components/concept-rdf/vocabulary-emitter.ts +62 -0
  122. package/src/components/groups/DatasetGroupRenderer.vue +32 -0
  123. package/src/components/groups/DefaultGroupSidebar.vue +50 -0
  124. package/src/components/groups/LineageGroupSidebar.vue +75 -0
  125. package/src/composables/use-color-theme.ts +82 -0
  126. package/src/composables/use-format-registry.ts +42 -0
  127. package/src/composables/useDatasetSeries.ts +258 -0
  128. package/src/composables/useSphereProjection.ts +125 -0
  129. package/src/config/group-renderers.ts +27 -0
  130. package/src/config/group-types.ts +92 -0
  131. package/src/config/types.ts +81 -2
  132. package/src/config/use-site-config.ts +2 -1
  133. package/src/errors.ts +136 -0
  134. package/src/i18n/locales/eng.yml +24 -0
  135. package/src/i18n/locales/fra.yml +24 -0
  136. package/src/stores/vocabulary.ts +3 -1
  137. package/src/style.css +17 -2
  138. package/src/types/agents-version-turtle.d.ts +27 -0
  139. package/src/types/bibliography-turtle.d.ts +12 -0
  140. package/src/types/build-activity-turtle.d.ts +16 -0
  141. package/src/types/build-cache.d.ts +20 -0
  142. package/src/types/dataset-turtle.d.ts +32 -0
  143. package/src/types/normalize-yaml.d.ts +16 -0
  144. package/src/types/turtle-escape.d.ts +6 -0
  145. package/src/types/vocab-turtle.d.ts +13 -0
  146. package/src/utils/asciidoc-lite.ts +11 -6
  147. package/src/utils/bcp47.ts +141 -0
  148. package/src/utils/color-theme-integration.ts +11 -0
  149. package/src/utils/color-theme.ts +129 -0
  150. package/src/utils/dataset-style.ts +31 -6
  151. package/src/utils/locale.ts +6 -14
  152. package/src/utils/markdown-lite.ts +6 -1
  153. package/src/utils/relation-sphere-styling.ts +63 -0
  154. package/src/utils/relationship-categories.ts +30 -0
  155. package/src/utils/url-safety.ts +30 -0
  156. package/src/views/ConceptView.vue +187 -9
  157. package/src/views/DatasetView.vue +6 -0
  158. package/src/views/HomeView.vue +5 -0
  159. package/vite.config.ts +7 -0
@@ -6,6 +6,12 @@ import { loadSiteConfig } from './load-site-config.mjs';
6
6
  import { getGroups } from './lib/concept-groups.mjs';
7
7
  import { consumeDatasetEntities } from './lib/build/non-verbal-consumer.mjs';
8
8
  import { copyImageAssets } from './lib/build/image-assets.mjs';
9
+ import { buildDatasetTurtle } from './lib/dataset-turtle.mjs';
10
+ import { buildActivityTurtle } from './lib/build-activity-turtle.mjs';
11
+ import { buildVocabularyTurtle } from './lib/vocab-turtle.mjs';
12
+ import { buildAgentsTurtle } from './lib/agents-turtle.mjs';
13
+ import { buildVersionHistoryTurtle } from './lib/version-turtle.mjs';
14
+ import { buildBibliographyTurtle } from './lib/bibliography-turtle.mjs';
9
15
  const __dirname = path.dirname(new URL(import.meta.url).pathname);
10
16
  const ROOT = process.cwd();
11
17
  const PUBLIC = path.join(ROOT, 'public');
@@ -84,6 +90,83 @@ function writeJson(filePath, data) {
84
90
  fs.writeFileSync(filePath, JSON.stringify(data));
85
91
  }
86
92
 
93
+ function writeDatasetRdf(register, manifest, concepts, refMaps, opts) {
94
+ const uriBase = refMaps?.uriBase ?? 'https://glossarist.org';
95
+ const datasetIri = `${uriBase}/${register}/`;
96
+ const topConceptUris = concepts
97
+ .slice(0, 32)
98
+ .map(c => `${uriBase}/${register}/concept/${c.id}`);
99
+
100
+ const sections = (manifest.sections ?? []).map(section => {
101
+ const sectionId = section.id ?? section.slug ?? section.title;
102
+ return {
103
+ collectionIri: `${uriBase}/${register}/section/${sectionId}`,
104
+ title: section.title ?? section.name ?? sectionId,
105
+ memberUris: (section.members ?? []).map(id => `${uriBase}/${register}/concept/${id}`),
106
+ };
107
+ });
108
+
109
+ const distributions = [
110
+ {
111
+ id: `${register}-ttl`,
112
+ title: 'Turtle distribution',
113
+ mediaType: 'text/turtle',
114
+ downloadUrl: `${uriBase}/data/${register}/${register}.ttl`,
115
+ },
116
+ {
117
+ id: `${register}-jsonld`,
118
+ title: 'JSON-LD distribution',
119
+ mediaType: 'application/ld+json',
120
+ downloadUrl: `${uriBase}/data/${register}/${register}.jsonld`,
121
+ },
122
+ ];
123
+
124
+ const ttl = buildDatasetTurtle({
125
+ datasetIri,
126
+ registerId: register,
127
+ title: manifest.title ?? register,
128
+ description: manifest.description,
129
+ modified: manifest.lastUpdated ?? new Date().toISOString().slice(0, 10),
130
+ languages: opts.languages ?? ['eng'],
131
+ distributions,
132
+ topConceptUris,
133
+ sections,
134
+ sourceRepoUrl: manifest.sourceRepoUrl,
135
+ publisherIri: manifest.publisher,
136
+ contactIri: manifest.contactPoint,
137
+ });
138
+ fs.writeFileSync(path.join(DATA, register, `${register}.ttl`), ttl);
139
+ }
140
+
141
+ function writeBuildActivity(conceptCount, datasetRegisters) {
142
+ const runId = process.env.GITHUB_RUN_ID
143
+ ? `${process.env.GITHUB_RUN_ID}-${process.env.GITHUB_RUN_ATTEMPT ?? '1'}`
144
+ : `local-${new Date().toISOString().replace(/[:.]/g, '-')}`;
145
+ const startedAt = process.env.BUILD_STARTED_AT ?? new Date(Date.now() - 60_000).toISOString();
146
+ const endedAt = new Date().toISOString();
147
+ const gitSha = process.env.GITHUB_SHA ?? null;
148
+ const gitBranch = process.env.GITHUB_REF_NAME ?? null;
149
+ const pkgVersion = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')).version;
150
+ const agentIri = process.env.CI_BOT_AGENT_IRI ?? null;
151
+
152
+ const ttl = buildActivityTurtle({
153
+ runId,
154
+ startedAt,
155
+ endedAt,
156
+ gitSha,
157
+ gitBranch,
158
+ toolId: 'concept-browser',
159
+ toolVersion: pkgVersion,
160
+ datasetRegisters,
161
+ conceptCount,
162
+ associatedAgentIri: agentIri,
163
+ });
164
+ const activityDir = path.join(DATA, 'activity');
165
+ fs.mkdirSync(activityDir, { recursive: true });
166
+ fs.writeFileSync(path.join(activityDir, `${runId}.ttl`), ttl);
167
+ console.log(`Emitted build activity record: data/activity/${runId}.ttl`);
168
+ }
169
+
87
170
  function termToDesignation(term) {
88
171
  const typeMap = {
89
172
  expression: 'gl:Expression',
@@ -1007,6 +1090,9 @@ async function processDataset(dir, register, opts) {
1007
1090
  if (opts.sections && opts.sections.length > 0) manifest.sections = opts.sections;
1008
1091
  writeJson(path.join(DATA, register, 'manifest.json'), manifest);
1009
1092
 
1093
+ // Dataset-level RDF (WS J2/J5): dcat:Dataset + skos:ConceptScheme + skos:Collection per section.
1094
+ writeDatasetRdf(register, manifest, concepts, refMaps, opts);
1095
+
1010
1096
  // Copy bibliography.yaml → bibliography.json
1011
1097
  const bibPath = path.join(sourceRoot, 'bibliography.yaml');
1012
1098
  if (fs.existsSync(bibPath)) {
@@ -1522,3 +1608,53 @@ console.log(`\nDone! Generated data for ${total} concepts across ${registry.leng
1522
1608
  for (const [id, count] of Object.entries(counts)) {
1523
1609
  console.log(` ${id}: ${count} concepts`);
1524
1610
  }
1611
+
1612
+ writeBuildActivity(total, registry.map(r => r.id));
1613
+
1614
+ fs.writeFileSync(path.join(DATA, '_vocab.ttl'), buildVocabularyTurtle());
1615
+ console.log('Emitted vocabulary graph: data/_vocab.ttl');
1616
+
1617
+ const contributors = config.contributors ?? [];
1618
+ if (contributors.length > 0) {
1619
+ fs.writeFileSync(path.join(DATA, 'agents.ttl'), buildAgentsTurtle(contributors));
1620
+ console.log(`Emitted agents graph: data/agents.ttl (${contributors.length} contributors)`);
1621
+ }
1622
+
1623
+ // Bibliography aggregation (K5): one bib.ttl per register from bibliography.json
1624
+ for (const ds of registry) {
1625
+ const bibPath = path.join(DATA, ds.id, 'bibliography.json');
1626
+ if (fs.existsSync(bibPath)) {
1627
+ const bibJson = JSON.parse(fs.readFileSync(bibPath, 'utf8'));
1628
+ const bibTtl = buildBibliographyTurtle(ds.id, bibJson);
1629
+ fs.writeFileSync(path.join(DATA, ds.id, 'bib.ttl'), bibTtl);
1630
+ }
1631
+ }
1632
+
1633
+ const datasetVersions = registry.map(ds => ({
1634
+ registerId: ds.id,
1635
+ datasetIri: `${refMaps.uriBase}/${ds.id}/`,
1636
+ versions: [
1637
+ {
1638
+ version: pkgVersionForVersions(),
1639
+ generatedAt: new Date().toISOString(),
1640
+ changeSummary: `Build ${new Date().toISOString().slice(0, 10)}`,
1641
+ },
1642
+ ],
1643
+ }));
1644
+
1645
+ if (datasetVersions.length > 0) {
1646
+ const versionTtl = datasetVersions.map(v =>
1647
+ buildVersionHistoryTurtle({
1648
+ registerId: v.registerId,
1649
+ datasetIri: v.datasetIri,
1650
+ versions: v.versions,
1651
+ associatedAgentIri: process.env.CI_BOT_AGENT_IRI ?? null,
1652
+ }),
1653
+ ).join('\n');
1654
+ fs.writeFileSync(path.join(DATA, 'versions.ttl'), versionTtl);
1655
+ console.log(`Emitted versions graph: data/versions.ttl (${datasetVersions.length} datasets)`);
1656
+ }
1657
+
1658
+ function pkgVersionForVersions() {
1659
+ return JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')).version;
1660
+ }
@@ -3,7 +3,7 @@
3
3
  * Extract SKOS taxonomy data from concept-model TTL files into JSON
4
4
  * for browser consumption via the OntologyRegistry.
5
5
  *
6
- * Reads from: ../concept-model/ontologies/taxonomies/*.ttl
6
+ * Reads from: data/concept-model/taxonomies/*.ttl (vendored from glossarist/concept-model)
7
7
  * Writes to: src/data/taxonomies.json
8
8
  */
9
9
 
@@ -13,7 +13,7 @@ import { fileURLToPath } from 'url';
13
13
 
14
14
  const __dirname = dirname(fileURLToPath(import.meta.url));
15
15
  const ROOT = resolve(__dirname, '..');
16
- const TAXONOMY_DIR = resolve(ROOT, '..', 'concept-model', 'ontologies', 'taxonomies');
16
+ const TAXONOMY_DIR = resolve(ROOT, 'data', 'concept-model', 'taxonomies');
17
17
  const OUTPUT = resolve(ROOT, 'src', 'data', 'taxonomies.json');
18
18
 
19
19
  /**
@@ -151,7 +151,7 @@ const TAXONOMY_MAP = {
151
151
  function main() {
152
152
  if (!existsSync(TAXONOMY_DIR)) {
153
153
  console.error(`Taxonomy directory not found: ${TAXONOMY_DIR}`);
154
- console.error('Ensure concept-model is available at ../concept-model/');
154
+ console.error('Run `npm run sync:model` to vendor concept-model data.');
155
155
  process.exit(1);
156
156
  }
157
157
 
@@ -3,7 +3,7 @@
3
3
  * Parse the glossarist OWL ontology (TTL) into a structured JSON schema
4
4
  * for the Ontospy-style browser view.
5
5
  *
6
- * Reads: ../concept-model/ontologies/glossarist.ttl
6
+ * Reads: data/concept-model/glossarist.ttl (vendored from glossarist/concept-model)
7
7
  * Writes: src/data/ontology-schema.json
8
8
  */
9
9
 
@@ -13,7 +13,7 @@ import { fileURLToPath } from 'url';
13
13
 
14
14
  const __dirname = dirname(fileURLToPath(import.meta.url));
15
15
  const ROOT = resolve(__dirname, '..');
16
- const CONCEPT_MODEL = resolve(ROOT, '..', 'concept-model', 'ontologies');
16
+ const CONCEPT_MODEL = resolve(ROOT, 'data', 'concept-model');
17
17
  const ONTOLOGY_TTL = resolve(CONCEPT_MODEL, 'glossarist.ttl');
18
18
  const SHACL_TTL = resolve(CONCEPT_MODEL, 'shapes', 'glossarist.shacl.ttl');
19
19
  const OUTPUT = resolve(ROOT, 'src', 'data', 'ontology-schema.json');
@@ -537,7 +537,7 @@ function parseAnnotationProperties(ttlText) {
537
537
  function main() {
538
538
  if (!existsSync(ONTOLOGY_TTL)) {
539
539
  console.error(`Ontology file not found: ${ONTOLOGY_TTL}`);
540
- console.error('Ensure concept-model is available at ../concept-model/');
540
+ console.error('Run `npm run sync:model` to vendor from glossarist/concept-model.');
541
541
  process.exit(1);
542
542
  }
543
543
 
@@ -0,0 +1,64 @@
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
+ }
9
+
10
+ function ttlLit(s) {
11
+ if (s == null) return '""';
12
+ const escaped = String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
13
+ return `"${escaped}"`;
14
+ }
15
+
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}`;
22
+ }
23
+
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
+ }
@@ -0,0 +1,54 @@
1
+ import { ttlLit, ttlIri } from './turtle-escape.mjs';
2
+
3
+ function entryFromV3(e, fallbackId) {
4
+ return {
5
+ id: e.id ?? fallbackId ?? '',
6
+ reference: e.reference ?? '',
7
+ title: e.title,
8
+ link: e.link,
9
+ type: e.type,
10
+ };
11
+ }
12
+
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
+ ];
33
+
34
+ const datasetIri = `${baseUri}/${register}/`;
35
+ 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
+ }
@@ -0,0 +1,92 @@
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}"`;
15
+ }
16
+
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';
92
+ }
@@ -0,0 +1,70 @@
1
+ // @ts-check
2
+ import { createHash } from 'node:crypto';
3
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
4
+ import { dirname } from 'node:path';
5
+
6
+ /**
7
+ * @typedef {Object} BuildCacheEntry
8
+ * @property {string} hash
9
+ * @property {unknown} value
10
+ * @property {string} storedAt
11
+ */
12
+
13
+ export class BuildCache {
14
+ /** @param {string} cacheDir */
15
+ constructor(cacheDir) {
16
+ this.cacheDir = cacheDir;
17
+ }
18
+
19
+ /** @template T @param {string} key @returns {Promise<BuildCacheEntry<T> | null>} */
20
+ async get(key) {
21
+ const path = this.pathFor(key);
22
+ try {
23
+ const text = await readFile(path, 'utf8');
24
+ return JSON.parse(text);
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * @template T
32
+ * @param {string} key
33
+ * @param {T} value
34
+ * @param {string} hash
35
+ */
36
+ async set(key, value, hash) {
37
+ const path = this.pathFor(key);
38
+ await mkdir(dirname(path), { recursive: true });
39
+ const entry = { hash, value, storedAt: new Date().toISOString() };
40
+ await writeFile(path, JSON.stringify(entry));
41
+ }
42
+
43
+ /**
44
+ * @template T
45
+ * @param {string} key
46
+ * @param {string} hash
47
+ * @param {() => Promise<T>} producer
48
+ * @returns {Promise<{ value: T; hit: boolean }>}
49
+ */
50
+ async readThrough(key, hash, producer) {
51
+ const cached = await this.get(key);
52
+ if (cached && cached.hash === hash) {
53
+ return { value: cached.value, hit: true };
54
+ }
55
+ const value = await producer();
56
+ await this.set(key, value, hash);
57
+ return { value, hit: false };
58
+ }
59
+
60
+ /** @param {string} key @returns {string} */
61
+ pathFor(key) {
62
+ const safe = key.replace(/[^a-zA-Z0-9._-]/g, '_');
63
+ return `${this.cacheDir}/${safe}.json`;
64
+ }
65
+
66
+ /** @param {string} input @returns {string} */
67
+ static hash(input) {
68
+ return createHash('sha256').update(input).digest('hex').slice(0, 16);
69
+ }
70
+ }
@@ -0,0 +1,79 @@
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}"`;
15
+ }
16
+
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';
79
+ }
Binary file
@@ -0,0 +1,56 @@
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
+ ];
7
+
8
+ function ttlLit(s) {
9
+ if (s == null) return '""';
10
+ const escaped = String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
11
+ return `"${escaped}"`;
12
+ }
13
+
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('');
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';
36
+ }
37
+
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
+ }