@glossarist/concept-browser 0.7.50 → 0.7.52

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 (170) 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/__fixtures__/concept-shape.ttl +20 -0
  21. package/src/__fixtures__/shacl/bad/concept.ttl +7 -0
  22. package/src/__fixtures__/shacl/empty/.gitkeep +0 -0
  23. package/src/__fixtures__/shacl/good/concept.ttl +8 -0
  24. package/src/__tests__/__fixtures__/concepts.ts +221 -0
  25. package/src/__tests__/adapters/concept-identity.test.ts +76 -0
  26. package/src/__tests__/components/error-boundary.test.ts +109 -0
  27. package/src/__tests__/composables/use-dataset-series.test.ts +262 -0
  28. package/src/__tests__/concept-rdf/agents-emitter.test.ts +110 -0
  29. package/src/__tests__/concept-rdf/bibliography-emitter.test.ts +159 -0
  30. package/src/__tests__/concept-rdf/build-activity-emitter.test.ts +119 -0
  31. package/src/__tests__/concept-rdf/coerce-date.test.ts +97 -0
  32. package/src/__tests__/concept-rdf/concept-emitter.test.ts +258 -0
  33. package/src/__tests__/concept-rdf/dataset-emitter.test.ts +224 -0
  34. package/src/__tests__/concept-rdf/differential.test.ts +96 -0
  35. package/src/__tests__/concept-rdf/group-emitter.test.ts +109 -0
  36. package/src/__tests__/concept-rdf/image-variant-emitter.test.ts +135 -0
  37. package/src/__tests__/concept-rdf/jsonld-writer.test.ts +109 -0
  38. package/src/__tests__/concept-rdf/nonverbal-rep.test.ts +177 -0
  39. package/src/__tests__/concept-rdf/property-based.test.ts +179 -0
  40. package/src/__tests__/concept-rdf/provenance.test.ts +110 -0
  41. package/src/__tests__/concept-rdf/quad-isomorphism.test.ts +43 -0
  42. package/src/__tests__/concept-rdf/quad-isomorphism.ts +47 -0
  43. package/src/__tests__/concept-rdf/rdf-components.test.ts +145 -0
  44. package/src/__tests__/concept-rdf/rdf-graph.test.ts +115 -0
  45. package/src/__tests__/concept-rdf/round-trip.test.ts +243 -0
  46. package/src/__tests__/concept-rdf/scoped-examples.test.ts +126 -0
  47. package/src/__tests__/concept-rdf/sections-builder.test.ts +94 -0
  48. package/src/__tests__/concept-rdf/shacl-conformance.test.ts +110 -0
  49. package/src/__tests__/concept-rdf/shape-consistency.test.ts +138 -0
  50. package/src/__tests__/concept-rdf/snapshot-generation.test.ts +75 -0
  51. package/src/__tests__/concept-rdf/table-formula-emitter.test.ts +142 -0
  52. package/src/__tests__/concept-rdf/turtle-writer.test.ts +114 -0
  53. package/src/__tests__/concept-rdf/use-rdf-document.test.ts +246 -0
  54. package/src/__tests__/concept-rdf/version-emitter.test.ts +120 -0
  55. package/src/__tests__/concept-rdf/vocabulary-ssot.test.ts +100 -0
  56. package/src/__tests__/concept-rdf-view.test.ts +136 -0
  57. package/src/__tests__/dataset-style.test.ts +12 -7
  58. package/src/__tests__/errors/errors.test.ts +142 -0
  59. package/src/__tests__/format-downloads.test.ts +47 -65
  60. package/src/__tests__/markdown-lite.test.ts +19 -0
  61. package/src/__tests__/perf/bundle-layout.test.ts +50 -0
  62. package/src/__tests__/perf/serialization-perf.test.ts +121 -0
  63. package/src/__tests__/scripts/agents-turtle.test.ts +61 -0
  64. package/src/__tests__/scripts/bibliography-turtle.test.ts +59 -0
  65. package/src/__tests__/scripts/build-activity-turtle.test.ts +75 -0
  66. package/src/__tests__/scripts/build-cache.test.ts +78 -0
  67. package/src/__tests__/scripts/build-integration.test.ts +134 -0
  68. package/src/__tests__/scripts/dataset-turtle.test.ts +94 -0
  69. package/src/__tests__/scripts/normalize-yaml.test.ts +98 -0
  70. package/src/__tests__/scripts/stryker-config.test.ts +33 -0
  71. package/src/__tests__/scripts/turtle-escape.test.ts +63 -0
  72. package/src/__tests__/scripts/version-turtle.test.ts +72 -0
  73. package/src/__tests__/scripts/vocab-turtle.test.ts +63 -0
  74. package/src/__tests__/use-format-registry.test.ts +125 -0
  75. package/src/__tests__/utils/bcp47.test.ts +166 -0
  76. package/src/__tests__/utils/color-theme.test.ts +143 -0
  77. package/src/__tests__/utils/url-safety.test.ts +65 -0
  78. package/src/__tests__/validate-shacl.test.ts +100 -0
  79. package/src/adapters/DatasetAdapter.ts +11 -5
  80. package/src/adapters/GraphDataSource.ts +2 -1
  81. package/src/adapters/UriRouter.ts +2 -1
  82. package/src/adapters/concept-identity.ts +69 -0
  83. package/src/adapters/factory.ts +3 -2
  84. package/src/adapters/model-bridge.ts +2 -1
  85. package/src/adapters/non-verbal/figure-bridge.ts +22 -23
  86. package/src/adapters/non-verbal/formula-bridge.ts +11 -9
  87. package/src/adapters/non-verbal/glossarist-augment.d.ts +133 -0
  88. package/src/adapters/non-verbal/index.ts +12 -9
  89. package/src/adapters/non-verbal/kind.ts +2 -1
  90. package/src/adapters/non-verbal/table-bridge.ts +12 -10
  91. package/src/adapters/non-verbal/types.ts +36 -54
  92. package/src/adapters/non-verbal-resolver.ts +6 -3
  93. package/src/components/AppSidebar.vue +189 -93
  94. package/src/components/ConceptDetail.vue +8 -0
  95. package/src/components/ConceptEditionRail.vue +222 -0
  96. package/src/components/ConceptRdfView.vue +37 -377
  97. package/src/components/DatasetSeriesCard.vue +270 -0
  98. package/src/components/ErrorBoundary.vue +95 -0
  99. package/src/components/FormatDownloads.vue +17 -13
  100. package/src/components/HomeSeriesSection.vue +277 -0
  101. package/src/components/NonVerbalRepDisplay.vue +2 -2
  102. package/src/components/RelationSphere.vue +1672 -0
  103. package/src/components/SidebarSeriesSection.vue +239 -0
  104. package/src/components/concept-rdf/RdfInstanceHeader.vue +47 -0
  105. package/src/components/concept-rdf/RdfInstanceSection.vue +54 -0
  106. package/src/components/concept-rdf/RdfPrefixLegend.vue +27 -0
  107. package/src/components/concept-rdf/RdfSourcePanel.vue +72 -0
  108. package/src/components/concept-rdf/agents-emitter.ts +82 -0
  109. package/src/components/concept-rdf/bibliography-emitter.ts +83 -0
  110. package/src/components/concept-rdf/build-activity-emitter.ts +89 -0
  111. package/src/components/concept-rdf/concept-emitter.ts +443 -0
  112. package/src/components/concept-rdf/dataset-emitter.ts +95 -0
  113. package/src/components/concept-rdf/group-emitter.ts +69 -0
  114. package/src/components/concept-rdf/image-variant-emitter.ts +46 -0
  115. package/src/components/concept-rdf/jsonld-writer.ts +82 -0
  116. package/src/components/concept-rdf/predicates.ts +261 -0
  117. package/src/components/concept-rdf/provenance.ts +80 -0
  118. package/src/components/concept-rdf/rdf-graph.ts +211 -0
  119. package/src/components/concept-rdf/rdf-prefixes.ts +23 -0
  120. package/src/components/concept-rdf/sections-builder.ts +62 -0
  121. package/src/components/concept-rdf/table-formula-emitter.ts +101 -0
  122. package/src/components/concept-rdf/turtle-writer.ts +116 -0
  123. package/src/components/concept-rdf/use-rdf-document.ts +72 -0
  124. package/src/components/concept-rdf/version-emitter.ts +65 -0
  125. package/src/components/concept-rdf/vocabulary-emitter.ts +62 -0
  126. package/src/components/figure/FigureDisplay.vue +16 -15
  127. package/src/components/figure/FigureImages.vue +38 -16
  128. package/src/components/figure/figure-image-pick.ts +1 -1
  129. package/src/components/figure/figure-layout.ts +1 -1
  130. package/src/components/formula/FormulaDisplay.vue +11 -9
  131. package/src/components/formula/FormulaExpression.vue +4 -4
  132. package/src/components/non-verbal/NonVerbalCaption.vue +5 -5
  133. package/src/components/non-verbal/NonVerbalSources.vue +3 -11
  134. package/src/components/table/TableDisplay.vue +6 -4
  135. package/src/components/table/TableMarkup.vue +1 -1
  136. package/src/composables/use-color-theme.ts +82 -0
  137. package/src/composables/use-format-registry.ts +42 -0
  138. package/src/composables/use-non-verbal-entity.ts +2 -1
  139. package/src/composables/useDatasetSeries.ts +258 -0
  140. package/src/composables/useSphereProjection.ts +125 -0
  141. package/src/config/group-types.ts +92 -0
  142. package/src/config/types.ts +81 -2
  143. package/src/config/use-site-config.ts +2 -1
  144. package/src/errors.ts +136 -0
  145. package/src/i18n/locales/eng.yml +24 -0
  146. package/src/i18n/locales/fra.yml +24 -0
  147. package/src/stores/vocabulary.ts +3 -1
  148. package/src/style.css +17 -2
  149. package/src/types/agents-version-turtle.d.ts +27 -0
  150. package/src/types/bibliography-turtle.d.ts +12 -0
  151. package/src/types/build-activity-turtle.d.ts +16 -0
  152. package/src/types/build-cache.d.ts +20 -0
  153. package/src/types/dataset-turtle.d.ts +32 -0
  154. package/src/types/normalize-yaml.d.ts +16 -0
  155. package/src/types/turtle-escape.d.ts +6 -0
  156. package/src/types/vocab-turtle.d.ts +13 -0
  157. package/src/utils/asciidoc-lite.ts +11 -6
  158. package/src/utils/bcp47.ts +141 -0
  159. package/src/utils/color-theme-integration.ts +11 -0
  160. package/src/utils/color-theme.ts +129 -0
  161. package/src/utils/dataset-style.ts +31 -6
  162. package/src/utils/locale.ts +6 -14
  163. package/src/utils/markdown-lite.ts +6 -1
  164. package/src/utils/relation-sphere-styling.ts +63 -0
  165. package/src/utils/relationship-categories.ts +30 -0
  166. package/src/utils/url-safety.ts +30 -0
  167. package/src/views/ConceptView.vue +183 -9
  168. package/src/views/DatasetView.vue +6 -0
  169. package/src/views/HomeView.vue +5 -0
  170. package/vite.config.ts +7 -0
@@ -0,0 +1,64 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join, dirname } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ const VOCAB_PATH = join(__dirname, '..', '..', 'data', 'glossarist-vocab.json');
7
+
8
+ function loadVocab(path = VOCAB_PATH) {
9
+ return JSON.parse(readFileSync(path, 'utf8'));
10
+ }
11
+
12
+ function ttlLit(s) {
13
+ if (s == null) return '""';
14
+ const escaped = String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"')
15
+ .replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/\t/g, '\\t');
16
+ return `"${escaped}"`;
17
+ }
18
+
19
+ function ttlPrefixed(qname) {
20
+ const colonIdx = qname.indexOf(':');
21
+ if (colonIdx < 0) return qname;
22
+ const local = qname.slice(colonIdx + 1);
23
+ const escaped = local.replace(/([/])/g, '\\$1');
24
+ return `${qname.slice(0, colonIdx + 1)}${escaped}`;
25
+ }
26
+
27
+ export function buildVocabularyTurtle() {
28
+ const vocab = loadVocab();
29
+ const lines = [
30
+ '@prefix skos: <http://www.w3.org/2004/02/skos/core#> .',
31
+ '@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .',
32
+ '@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .',
33
+ '@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .',
34
+ '@prefix dcterms: <http://purl.org/dc/terms/> .',
35
+ '@prefix gloss: <https://www.glossarist.org/ontologies/> .',
36
+ '',
37
+ ];
38
+
39
+ for (const scheme of vocab.schemes) {
40
+ const schemeLines = [
41
+ `${ttlPrefixed(scheme.schemeIri)} a skos:ConceptScheme ;`,
42
+ ` rdfs:label ${ttlLit(scheme.label)} ;`,
43
+ ];
44
+ for (const term of scheme.terms) {
45
+ schemeLines.push(` skos:hasTopConcept ${ttlPrefixed(term.iri)} ;`);
46
+ }
47
+ schemeLines[schemeLines.length - 1] = schemeLines[schemeLines.length - 1].replace(/ ;$/, ' .');
48
+ lines.push(...schemeLines);
49
+
50
+ for (const term of scheme.terms) {
51
+ lines.push(`${ttlPrefixed(term.iri)} a skos:Concept ;`);
52
+ lines.push(` rdfs:label ${ttlLit(term.label)} ;`);
53
+ lines.push(` skos:inScheme ${ttlPrefixed(scheme.schemeIri)} .`);
54
+ lines.push('');
55
+ }
56
+ lines.push('');
57
+ }
58
+
59
+ return lines.join('\n') + '\n';
60
+ }
61
+
62
+ export function listVocabSchemes() {
63
+ return loadVocab().schemes;
64
+ }
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, writeFileSync, readdirSync, statSync } from 'node:fs';
3
+ import { join, relative, extname, dirname } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
7
+
8
+ const YAML_EXTS = new Set(['.yaml', '.yml']);
9
+ const SKIP_DIRS = new Set(['node_modules', '.git', 'dist']);
10
+
11
+ function walk(dir) {
12
+ const out = [];
13
+ for (const entry of readdirSync(dir)) {
14
+ if (SKIP_DIRS.has(entry)) continue;
15
+ const full = join(dir, entry);
16
+ const st = statSync(full);
17
+ if (st.isDirectory()) {
18
+ out.push(...walk(full));
19
+ } else if (YAML_EXTS.has(extname(entry))) {
20
+ out.push(full);
21
+ }
22
+ }
23
+ return out;
24
+ }
25
+
26
+ export function normalizeYaml({ root = ROOT, check = false, paths = [] } = {}) {
27
+ const searchDirs = paths.length > 0 ? paths.map(p => join(root, p)) : [join(root, '.datasets')];
28
+
29
+ const files = [];
30
+ for (const dir of searchDirs) {
31
+ try {
32
+ statSync(dir);
33
+ } catch {
34
+ continue;
35
+ }
36
+ files.push(...walk(dir));
37
+ }
38
+ files.sort();
39
+
40
+ let checked = 0;
41
+ let nonNfc = 0;
42
+ const fixed = [];
43
+
44
+ for (const file of files) {
45
+ const original = readFileSync(file, 'utf8');
46
+ checked++;
47
+ if (!isNfc(original)) {
48
+ nonNfc++;
49
+ if (!check) {
50
+ writeFileSync(file, original.normalize('NFC'), 'utf8');
51
+ fixed.push(relative(root, file));
52
+ } else {
53
+ fixed.push(relative(root, file));
54
+ }
55
+ }
56
+ }
57
+
58
+ return { checked, nonNfc, fixed, check };
59
+ }
60
+
61
+ function isNfc(s) {
62
+ return s === s.normalize('NFC');
63
+ }
64
+
65
+ function main() {
66
+ const args = process.argv.slice(2);
67
+ const check = args.includes('--check');
68
+ const paths = args.filter(a => !a.startsWith('-') && a !== 'normalize');
69
+
70
+ const { checked, nonNfc, fixed } = normalizeYaml({ check, paths });
71
+
72
+ if (check) {
73
+ if (nonNfc === 0) {
74
+ process.stdout.write(`NFC OK: ${checked} file(s) checked, all normalized\n`);
75
+ return;
76
+ }
77
+ process.stderr.write(`NFC check failed: ${nonNfc} of ${checked} file(s) are not NFC-normalized\n\n`);
78
+ for (const f of fixed) {
79
+ process.stderr.write(` ${f}\n`);
80
+ }
81
+ process.exit(1);
82
+ } else {
83
+ if (nonNfc === 0) {
84
+ process.stdout.write(`NFC OK: ${checked} file(s) checked, all already normalized\n`);
85
+ return;
86
+ }
87
+ process.stdout.write(`Normalized ${nonNfc} of ${checked} file(s)\n`);
88
+ for (const f of fixed) {
89
+ process.stdout.write(` ${f}\n`);
90
+ }
91
+ }
92
+ }
93
+
94
+ const isDirectEntry = process.argv[1] &&
95
+ fileURLToPath(import.meta.url) === process.argv[1];
96
+
97
+ if (isDirectEntry) {
98
+ main();
99
+ }
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+ // Sync vendored concept-model data artifacts from the upstream git repo.
3
+ //
4
+ // concept-model is a data-only repo. It is not an npm package. We vendor
5
+ // the small set of artifacts we actually need (glossarist.context.jsonld,
6
+ // glossarist.ttl, shapes/glossarist.shacl.ttl) so this repo's build scripts
7
+ // can run without any runtime dependency on concept-model.
8
+ //
9
+ // Usage:
10
+ // npm run sync:model # latest release tag
11
+ // npm run sync:model -- v3.0.0 # specific tag
12
+ // node scripts/sync-concept-model.mjs --ref main # any git ref
13
+ //
14
+ // Output: data/concept-model/* updated in place. Review the diff and commit.
15
+
16
+ import { mkdirSync, writeFileSync } from 'node:fs';
17
+ import { dirname, resolve } from 'node:path';
18
+ import { fileURLToPath } from 'node:url';
19
+ import { execSync } from 'node:child_process';
20
+
21
+ const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
22
+ const OUT_DIR = resolve(ROOT, 'data', 'concept-model');
23
+ const REPO = 'glossarist/concept-model';
24
+
25
+ function parseArgs(argv) {
26
+ const out = { ref: null };
27
+ for (let i = 2; i < argv.length; i++) {
28
+ const a = argv[i];
29
+ if (a === '--ref') out.ref = argv[++i];
30
+ else if (!a.startsWith('--')) out.ref = a;
31
+ }
32
+ return out;
33
+ }
34
+
35
+ function latestTag() {
36
+ return execSync(`gh api repos/${REPO}/releases/latest --jq .tag_name`, { encoding: 'utf8' }).trim();
37
+ }
38
+
39
+ function fetchFile(ref, path) {
40
+ const url = `https://raw.githubusercontent.com/${REPO}/${ref}/${path}`;
41
+ return execSync(`curl -fsSL ${url}`, { encoding: 'utf8' });
42
+ }
43
+
44
+ const args = parseArgs(process.argv);
45
+ const ref = args.ref || latestTag();
46
+ if (!ref) {
47
+ console.error('Could not determine concept-model ref to sync.');
48
+ process.exit(1);
49
+ }
50
+
51
+ console.log(`Syncing from ${REPO}@${ref}`);
52
+
53
+ mkdirSync(resolve(OUT_DIR, 'shapes'), { recursive: true });
54
+
55
+ function fetchAny(ref, candidates) {
56
+ for (const p of candidates) {
57
+ try {
58
+ return fetchFile(ref, p);
59
+ } catch (e) {
60
+ // try next
61
+ }
62
+ }
63
+ throw new Error(`Could not fetch any of: ${candidates.join(', ')}`);
64
+ }
65
+
66
+ const targets = {
67
+ 'glossarist.context.jsonld': ['ontologies/glossarist.context.jsonld', 'glossarist.context.jsonld'],
68
+ 'glossarist.ttl': ['ontologies/glossarist.ttl', 'glossarist.ttl'],
69
+ 'shapes/glossarist.shacl.ttl': ['ontologies/shapes/glossarist.shacl.ttl', 'shapes/glossarist.shacl.ttl'],
70
+ };
71
+
72
+ let updated = 0;
73
+ for (const [outPath, candidates] of Object.entries(targets)) {
74
+ const content = fetchAny(ref, candidates);
75
+ writeFileSync(resolve(OUT_DIR, outPath), content);
76
+ console.log(` ✓ ${outPath} (${content.length} bytes)`);
77
+ updated++;
78
+ }
79
+
80
+ writeFileSync(
81
+ resolve(OUT_DIR, 'SOURCE.json'),
82
+ JSON.stringify({ repo: REPO, ref, syncedAt: new Date().toISOString() }, null, 2) + '\n',
83
+ );
84
+
85
+ console.log(`\nSynced ${updated} file(s) from ${REPO}@${ref}.`);
86
+ console.log('Review the diff, then commit.');
@@ -0,0 +1,194 @@
1
+ #!/usr/bin/env node
2
+ // SHACL validation gate for concept-browser's data pipeline.
3
+ //
4
+ // Walks `public/data/` (or any directory passed as argv[2]), parses every
5
+ // `.ttl` file, and validates it against the vendored SHACL shapes. Fails
6
+ // the build on any violation.
7
+ //
8
+ // Usage:
9
+ // node scripts/validate-shacl.mjs # validates public/data
10
+ // node scripts/validate-shacl.mjs path/to/dir # validates a custom dir
11
+ // SHAPES_PATH=/path/to/shapes.ttl node scripts/validate-shacl.mjs
12
+ // # uses a custom shapes file
13
+ //
14
+ // The shapes path defaults to the vendored file at
15
+ // `data/concept-model/shapes/glossarist.shacl.ttl` (synced from
16
+ // glossarist/concept-model via `npm run sync:model`). Pass --shapes <path>
17
+ // or set SHAPES_PATH to override.
18
+
19
+ import { readFileSync, readdirSync, statSync } from 'node:fs';
20
+ import { join, extname, dirname, resolve } from 'node:path';
21
+ import { fileURLToPath } from 'node:url';
22
+ import { Parser as N3Parser, DataFactory } from 'n3';
23
+ import rdfDataset from '@rdfjs/dataset';
24
+ import ShaclValidator from 'rdf-validate-shacl';
25
+
26
+ const __dirname = dirname(fileURLToPath(import.meta.url));
27
+ const VENDORED_SHAPES = resolve(__dirname, '..', 'data', 'concept-model', 'shapes', 'glossarist.shacl.ttl');
28
+
29
+ const COMBINED_FACTORY = {
30
+ namedNode: DataFactory.namedNode,
31
+ blankNode: DataFactory.blankNode,
32
+ literal: DataFactory.literal,
33
+ defaultGraph: DataFactory.defaultGraph,
34
+ quad: DataFactory.quad,
35
+ fromTerm: DataFactory.fromTerm,
36
+ fromQuad: DataFactory.fromQuad,
37
+ dataset: rdfDataset.dataset.bind(rdfDataset),
38
+ };
39
+ const createDataset = COMBINED_FACTORY.dataset;
40
+ const ShaclValidatorCtor = ShaclValidator.default ?? ShaclValidator;
41
+
42
+ function parseArgs(argv) {
43
+ const out = { dataRoot: 'public/data', shapesPath: null };
44
+ for (let i = 2; i < argv.length; i++) {
45
+ const arg = argv[i];
46
+ if (arg === '--shapes') {
47
+ out.shapesPath = argv[++i];
48
+ if (!out.shapesPath) throw new Error('--shapes requires a path argument');
49
+ } else if (arg === '--help' || arg === '-h') {
50
+ console.log(USAGE);
51
+ process.exit(0);
52
+ } else {
53
+ out.dataRoot = arg;
54
+ }
55
+ }
56
+ if (process.env.SHAPES_PATH && !out.shapesPath) {
57
+ out.shapesPath = process.env.SHAPES_PATH;
58
+ }
59
+ return out;
60
+ }
61
+
62
+ const USAGE = `Usage: validate-shacl.mjs [options] [data-root]
63
+
64
+ Options:
65
+ --shapes <path> Path to a SHACL shapes .ttl file (overrides the default
66
+ vendored shapes under data/concept-model/shapes/).
67
+ --help, -h Show this help.
68
+
69
+ Environment:
70
+ SHAPES_PATH Same as --shapes. Lower priority than the CLI flag.
71
+
72
+ Default data-root is public/data.
73
+ `;
74
+
75
+ function resolveShapesPath(cliPath) {
76
+ if (cliPath) return cliPath;
77
+ if (!statSync(VENDORED_SHAPES, { throwIfNoEntry: false })) {
78
+ throw new Error(
79
+ `Vendored SHACL shapes not found at ${VENDORED_SHAPES}.\n` +
80
+ `Run \`npm run sync:model\` to fetch them from glossarist/concept-model, ` +
81
+ `or pass --shapes <path>, or set SHAPES_PATH=<path>.`,
82
+ );
83
+ }
84
+ return VENDORED_SHAPES;
85
+ }
86
+
87
+ function parseTurtle(text, baseIri) {
88
+ const parser = new N3Parser({ baseIRI: baseIri });
89
+ const out = createDataset();
90
+ return new Promise((resolve, reject) => {
91
+ parser.parse(text, (err, quad) => {
92
+ if (err) reject(err);
93
+ else if (quad) out.add(quad);
94
+ else resolve(out);
95
+ });
96
+ });
97
+ }
98
+
99
+ function* walkTtl(dir) {
100
+ for (const entry of readdirSync(dir)) {
101
+ const full = join(dir, entry);
102
+ if (statSync(full).isDirectory()) yield* walkTtl(full);
103
+ else if (extname(full) === '.ttl') yield full;
104
+ }
105
+ }
106
+
107
+ function formatViolation(v) {
108
+ const focus = v.focusNode?.value ?? '(unknown)';
109
+ const shape = v.shape?.value ?? '(unknown)';
110
+ const path = v.path?.value ?? '';
111
+ const message = (v.message && v.message.length > 0)
112
+ ? v.message.map(m => m.value).join('; ')
113
+ : '(no message)';
114
+ const pathPart = path ? `\n path: ${path}` : '';
115
+ return ` shape: ${shape}${pathPart}\n node: ${focus}\n message: ${message}`;
116
+ }
117
+
118
+ async function main() {
119
+ const args = parseArgs(process.argv);
120
+ const shapesPath = resolveShapesPath(args.shapesPath);
121
+
122
+ let shapesDataset;
123
+ try {
124
+ const shapesText = readFileSync(shapesPath, 'utf8');
125
+ shapesDataset = await parseTurtle(shapesText, `file://${shapesPath}`);
126
+ } catch (e) {
127
+ console.error(`Failed to load SHACL shapes from ${shapesPath}: ${e.message}`);
128
+ process.exit(2);
129
+ }
130
+
131
+ const validator = new ShaclValidatorCtor(shapesDataset, { factory: COMBINED_FACTORY });
132
+
133
+ let statDir;
134
+ try {
135
+ statDir = statSync(args.dataRoot);
136
+ } catch (e) {
137
+ console.error(`Data root not found: ${args.dataRoot}`);
138
+ process.exit(2);
139
+ }
140
+ if (!statDir.isDirectory()) {
141
+ console.error(`Data root is not a directory: ${args.dataRoot}`);
142
+ process.exit(2);
143
+ }
144
+
145
+ const files = [...walkTtl(args.dataRoot)];
146
+ if (files.length === 0) {
147
+ console.log(`No .ttl files under ${args.dataRoot} — SHACL gate skipped.`);
148
+ return;
149
+ }
150
+
151
+ const violations = [];
152
+ for (const path of files) {
153
+ let graph;
154
+ try {
155
+ const text = readFileSync(path, 'utf8');
156
+ graph = await parseTurtle(text, `file://${path}`);
157
+ } catch (e) {
158
+ violations.push({ path, parseError: e.message });
159
+ continue;
160
+ }
161
+ const report = validator.validate(graph);
162
+ if (!report.conforms) {
163
+ for (const v of report.results) {
164
+ violations.push({ path, result: v });
165
+ }
166
+ }
167
+ }
168
+
169
+ if (violations.length === 0) {
170
+ console.log(`SHACL validation passed — ${files.length} file(s) conform.`);
171
+ return;
172
+ }
173
+
174
+ console.error(`SHACL validation FAILED — ${violations.length} violation(s) in ${files.length} file(s):\n`);
175
+ let currentPath = null;
176
+ for (const v of violations) {
177
+ if (v.path !== currentPath) {
178
+ currentPath = v.path;
179
+ console.error(`\n ${v.path}`);
180
+ }
181
+ if (v.parseError) {
182
+ console.error(` PARSE ERROR: ${v.parseError}`);
183
+ } else {
184
+ console.error(formatViolation(v.result));
185
+ }
186
+ }
187
+ console.error('');
188
+ process.exit(1);
189
+ }
190
+
191
+ main().catch((e) => {
192
+ console.error(`validate-shacl: unexpected error: ${e.stack ?? e}`);
193
+ process.exit(2);
194
+ });
@@ -0,0 +1,20 @@
1
+ @prefix sh: <http://www.w3.org/ns/shacl#> .
2
+ @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
3
+ @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
4
+ @prefix skos: <http://www.w3.org/2004/02/skos/core#> .
5
+ @prefix ex: <http://example.org/> .
6
+
7
+ ex:ConceptShape
8
+ a sh:NodeShape ;
9
+ sh:targetClass skos:Concept ;
10
+ sh:property [
11
+ sh:path skos:prefLabel ;
12
+ sh:datatype rdf:langString ;
13
+ sh:minCount 1 ;
14
+ sh:message "skos:prefLabel must be an rdf:langString (language-tagged literal)." ;
15
+ ] ;
16
+ sh:property [
17
+ sh:path skos:definition ;
18
+ sh:datatype rdf:langString ;
19
+ sh:message "skos:definition must be an rdf:langString (language-tagged literal)." ;
20
+ ] .
@@ -0,0 +1,7 @@
1
+ @prefix skos: <http://www.w3.org/2004/02/skos/core#> .
2
+
3
+ # Missing language tag on skos:prefLabel — should violate the shape.
4
+ <https://glossarist.org/test/concept/bad>
5
+ a skos:Concept ;
6
+ skos:prefLabel "no lang tag" ;
7
+ skos:definition "Also no lang tag." .
File without changes
@@ -0,0 +1,8 @@
1
+ @prefix skos: <http://www.w3.org/2004/02/skos/core#> .
2
+ @prefix dcterms: <http://purl.org/dc/terms/> .
3
+
4
+ <https://glossarist.org/test/concept/good>
5
+ a skos:Concept ;
6
+ skos:prefLabel "atomic data unit"@eng ;
7
+ skos:definition "A data unit that cannot be subdivided."@eng ;
8
+ dcterms:language "eng" .