@glossarist/concept-browser 0.7.51 → 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 (152) 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/glossarist-augment.d.ts +7 -0
  86. package/src/adapters/non-verbal-resolver.ts +2 -1
  87. package/src/components/AppSidebar.vue +189 -93
  88. package/src/components/ConceptDetail.vue +8 -0
  89. package/src/components/ConceptEditionRail.vue +222 -0
  90. package/src/components/ConceptRdfView.vue +37 -377
  91. package/src/components/DatasetSeriesCard.vue +270 -0
  92. package/src/components/ErrorBoundary.vue +95 -0
  93. package/src/components/FormatDownloads.vue +17 -13
  94. package/src/components/HomeSeriesSection.vue +277 -0
  95. package/src/components/RelationSphere.vue +1672 -0
  96. package/src/components/SidebarSeriesSection.vue +239 -0
  97. package/src/components/concept-rdf/RdfInstanceHeader.vue +47 -0
  98. package/src/components/concept-rdf/RdfInstanceSection.vue +54 -0
  99. package/src/components/concept-rdf/RdfPrefixLegend.vue +27 -0
  100. package/src/components/concept-rdf/RdfSourcePanel.vue +72 -0
  101. package/src/components/concept-rdf/agents-emitter.ts +82 -0
  102. package/src/components/concept-rdf/bibliography-emitter.ts +83 -0
  103. package/src/components/concept-rdf/build-activity-emitter.ts +89 -0
  104. package/src/components/concept-rdf/concept-emitter.ts +443 -0
  105. package/src/components/concept-rdf/dataset-emitter.ts +95 -0
  106. package/src/components/concept-rdf/group-emitter.ts +69 -0
  107. package/src/components/concept-rdf/image-variant-emitter.ts +46 -0
  108. package/src/components/concept-rdf/jsonld-writer.ts +82 -0
  109. package/src/components/concept-rdf/predicates.ts +261 -0
  110. package/src/components/concept-rdf/provenance.ts +80 -0
  111. package/src/components/concept-rdf/rdf-graph.ts +211 -0
  112. package/src/components/concept-rdf/rdf-prefixes.ts +23 -0
  113. package/src/components/concept-rdf/sections-builder.ts +62 -0
  114. package/src/components/concept-rdf/table-formula-emitter.ts +101 -0
  115. package/src/components/concept-rdf/turtle-writer.ts +116 -0
  116. package/src/components/concept-rdf/use-rdf-document.ts +72 -0
  117. package/src/components/concept-rdf/version-emitter.ts +65 -0
  118. package/src/components/concept-rdf/vocabulary-emitter.ts +62 -0
  119. package/src/composables/use-color-theme.ts +82 -0
  120. package/src/composables/use-format-registry.ts +42 -0
  121. package/src/composables/useDatasetSeries.ts +258 -0
  122. package/src/composables/useSphereProjection.ts +125 -0
  123. package/src/config/group-types.ts +92 -0
  124. package/src/config/types.ts +81 -2
  125. package/src/config/use-site-config.ts +2 -1
  126. package/src/errors.ts +136 -0
  127. package/src/i18n/locales/eng.yml +24 -0
  128. package/src/i18n/locales/fra.yml +24 -0
  129. package/src/stores/vocabulary.ts +3 -1
  130. package/src/style.css +17 -2
  131. package/src/types/agents-version-turtle.d.ts +27 -0
  132. package/src/types/bibliography-turtle.d.ts +12 -0
  133. package/src/types/build-activity-turtle.d.ts +16 -0
  134. package/src/types/build-cache.d.ts +20 -0
  135. package/src/types/dataset-turtle.d.ts +32 -0
  136. package/src/types/normalize-yaml.d.ts +16 -0
  137. package/src/types/turtle-escape.d.ts +6 -0
  138. package/src/types/vocab-turtle.d.ts +13 -0
  139. package/src/utils/asciidoc-lite.ts +11 -6
  140. package/src/utils/bcp47.ts +141 -0
  141. package/src/utils/color-theme-integration.ts +11 -0
  142. package/src/utils/color-theme.ts +129 -0
  143. package/src/utils/dataset-style.ts +31 -6
  144. package/src/utils/locale.ts +6 -14
  145. package/src/utils/markdown-lite.ts +6 -1
  146. package/src/utils/relation-sphere-styling.ts +63 -0
  147. package/src/utils/relationship-categories.ts +30 -0
  148. package/src/utils/url-safety.ts +30 -0
  149. package/src/views/ConceptView.vue +183 -9
  150. package/src/views/DatasetView.vue +6 -0
  151. package/src/views/HomeView.vue +5 -0
  152. package/vite.config.ts +7 -0
@@ -0,0 +1,243 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Parser, Store } from 'n3';
3
+ import { writeTurtle } from '../../components/concept-rdf/turtle-writer';
4
+ import { writeJsonLd } from '../../components/concept-rdf/jsonld-writer';
5
+ import { emitConceptGraph } from '../../components/concept-rdf/concept-emitter';
6
+ import { GLOSS, SKOS, SKOSXL, DCTERMS, RDF, PROV, XSD } from '../../components/concept-rdf/predicates';
7
+ import { CONCEPT_FIXTURES } from '../__fixtures__/concepts';
8
+
9
+ function expandPrefixed(value: string): string {
10
+ if (value.startsWith('gloss:')) return `https://www.glossarist.org/ontologies/${value.slice(6)}`;
11
+ if (value.startsWith('skos:')) return `http://www.w3.org/2004/02/skos/core#${value.slice(5)}`;
12
+ if (value.startsWith('skosxl:')) return `http://www.w3.org/2008/05/skos-xl#${value.slice(7)}`;
13
+ if (value.startsWith('dcterms:')) return `http://purl.org/dc/terms/${value.slice(8)}`;
14
+ if (value.startsWith('rdf:')) return `http://www.w3.org/1999/02/22-rdf-syntax-ns#${value.slice(4)}`;
15
+ if (value.startsWith('rdfs:')) return `http://www.w3.org/2000/01/rdf-schema#${value.slice(5)}`;
16
+ if (value.startsWith('prov:')) return `http://www.w3.org/ns/prov#${value.slice(5)}`;
17
+ if (value.startsWith('xsd:')) return `http://www.w3.org/2001/XMLSchema#${value.slice(4)}`;
18
+ return value;
19
+ }
20
+
21
+ function parseTurtle(turtle: string): Store {
22
+ const parser = new Parser({ format: 'Turtle' });
23
+ const store = new Store();
24
+ store.addQuads(parser.parse(turtle));
25
+ return store;
26
+ }
27
+
28
+ const RDF_TYPE = expandPrefixed(RDF.type);
29
+
30
+ describe('RDF round-trip — Layer 2 fixture corpus (Turtle)', () => {
31
+ for (const fixture of CONCEPT_FIXTURES) {
32
+ describe(`fixture: ${fixture.name}`, () => {
33
+ it('parses without errors and yields a non-empty graph', () => {
34
+ const { graph } = emitConceptGraph(fixture.concept, fixture.uri);
35
+ const store = parseTurtle(writeTurtle(graph));
36
+ expect(store.size).toBeGreaterThan(0);
37
+ });
38
+
39
+ it('preserves the concept type and identifier', () => {
40
+ const { graph } = emitConceptGraph(fixture.concept, fixture.uri);
41
+ const store = parseTurtle(writeTurtle(graph));
42
+
43
+ const types = store.getObjects(fixture.uri, RDF_TYPE, null).map(q => q.value);
44
+ expect(types).toContain(expandPrefixed(GLOSS.Concept));
45
+ expect(types).toContain(expandPrefixed(SKOS.Concept));
46
+
47
+ const ids = store.getObjects(fixture.uri, expandPrefixed(GLOSS.identifier), null).map(q => q.value);
48
+ expect(ids).toContain(fixture.concept.id);
49
+ });
50
+
51
+ it('emits a skos:prefLabel and a skosxl:literalForm for every localization', () => {
52
+ const { graph } = emitConceptGraph(fixture.concept, fixture.uri);
53
+ const store = parseTurtle(writeTurtle(graph));
54
+
55
+ for (const lang of fixture.concept.languages) {
56
+ const lcUri = `${fixture.uri}/${lang}`;
57
+ const pref = store.getObjects(lcUri, expandPrefixed(SKOS.prefLabel), null);
58
+ expect(pref.length).toBeGreaterThan(0);
59
+
60
+ const lcTypes = store.getObjects(lcUri, RDF_TYPE, null).map(q => q.value);
61
+ expect(lcTypes).toContain(expandPrefixed(GLOSS.LocalizedConcept));
62
+
63
+ const xlLabels = store.getObjects(lcUri, expandPrefixed(SKOSXL.prefLabel), null);
64
+ expect(xlLabels.length).toBeGreaterThan(0);
65
+
66
+ const literalForms = xlLabels.flatMap(label =>
67
+ store.getObjects(label, expandPrefixed(SKOSXL.literalForm), null),
68
+ );
69
+ expect(literalForms.length).toBeGreaterThan(0);
70
+ }
71
+ });
72
+
73
+ it('preserves isLocalizationOf back-references', () => {
74
+ const { graph } = emitConceptGraph(fixture.concept, fixture.uri);
75
+ const store = parseTurtle(writeTurtle(graph));
76
+
77
+ for (const lang of fixture.concept.languages) {
78
+ const lcUri = `${fixture.uri}/${lang}`;
79
+ const parents = store.getObjects(lcUri, expandPrefixed(GLOSS.isLocalizationOf), null).map(q => q.value);
80
+ expect(parents).toContain(fixture.uri);
81
+ }
82
+ });
83
+ });
84
+ }
85
+ });
86
+
87
+ describe('RDF round-trip — Layer 2 fixture corpus (JSON-LD syntactic)', () => {
88
+ for (const fixture of CONCEPT_FIXTURES) {
89
+ it(`${fixture.name}: emits a parseable JSON-LD document with a non-empty @graph`, () => {
90
+ const { graph } = emitConceptGraph(fixture.concept, fixture.uri);
91
+ const doc = JSON.parse(writeJsonLd(graph));
92
+ expect(Array.isArray(doc['@graph'])).toBe(true);
93
+ expect(doc['@graph'].length).toBeGreaterThan(0);
94
+ });
95
+
96
+ it(`${fixture.name}: includes the concept and at least one localized concept node`, () => {
97
+ const { graph } = emitConceptGraph(fixture.concept, fixture.uri);
98
+ const doc = JSON.parse(writeJsonLd(graph));
99
+
100
+ const conceptNode = doc['@graph'].find((n: any) =>
101
+ Array.isArray(n['@type']) && n['@type'].includes('gloss:Concept'),
102
+ );
103
+ expect(conceptNode).toBeDefined();
104
+ expect(conceptNode['@id']).toBe(fixture.uri);
105
+
106
+ const lcNode = doc['@graph'].find((n: any) =>
107
+ Array.isArray(n['@type']) && n['@type'].includes('gloss:LocalizedConcept'),
108
+ );
109
+ expect(lcNode).toBeDefined();
110
+ });
111
+ }
112
+ });
113
+
114
+ describe('RDF round-trip — Turtle and JSON-LD agree on prefLabel values', () => {
115
+ for (const fixture of CONCEPT_FIXTURES) {
116
+ it(`${fixture.name}: prefLabel values are identical across formats`, () => {
117
+ const { graph } = emitConceptGraph(fixture.concept, fixture.uri);
118
+
119
+ const turtleStore = parseTurtle(writeTurtle(graph));
120
+ const doc = JSON.parse(writeJsonLd(graph));
121
+
122
+ for (const lang of fixture.concept.languages) {
123
+ const lcUri = `${fixture.uri}/${lang}`;
124
+ const turtleLabels = turtleStore.getObjects(lcUri, expandPrefixed(SKOS.prefLabel), null).map(q => q.value);
125
+
126
+ const lcNode = doc['@graph'].find((n: any) => n['@id'] === lcUri);
127
+ expect(lcNode).toBeDefined();
128
+
129
+ const raw = lcNode['skos:prefLabel'];
130
+ const arr = Array.isArray(raw) ? raw : [raw];
131
+ const jsonLabels = arr.map((v: any) => typeof v === 'string' ? v : v['@value']);
132
+
133
+ expect(jsonLabels.sort()).toEqual(turtleLabels.slice().sort());
134
+ }
135
+ });
136
+ }
137
+ });
138
+
139
+ describe('RDF round-trip — fixture-specific invariants', () => {
140
+ it('multilingual: emits three distinct localizations with matching language codes', () => {
141
+ const fixture = CONCEPT_FIXTURES.find(f => f.name === 'multilingual')!;
142
+ const { graph } = emitConceptGraph(fixture.concept, fixture.uri);
143
+ const store = parseTurtle(writeTurtle(graph));
144
+
145
+ const langs = ['eng', 'fra', 'jpn'];
146
+ for (const lang of langs) {
147
+ const lcUri = `${fixture.uri}/${lang}`;
148
+ const langsOnLc = store.getObjects(lcUri, expandPrefixed(GLOSS.language), null).map(q => q.value);
149
+ expect(langsOnLc).toContain(lang);
150
+ }
151
+ });
152
+
153
+ it('with-sources: every concept-level source is typed as gloss:ConceptSource', () => {
154
+ const fixture = CONCEPT_FIXTURES.find(f => f.name === 'with-sources')!;
155
+ const { graph } = emitConceptGraph(fixture.concept, fixture.uri);
156
+ const store = parseTurtle(writeTurtle(graph));
157
+
158
+ const sources = store.getObjects(fixture.uri, expandPrefixed(GLOSS.hasSource), null);
159
+ expect(sources.length).toBeGreaterThanOrEqual(3);
160
+ for (const src of sources) {
161
+ const types = store.getObjects(src, RDF_TYPE, null).map(q => q.value);
162
+ expect(types).toContain(expandPrefixed(GLOSS.ConceptSource));
163
+ }
164
+ });
165
+
166
+ it('with-non-verbal: emits at least three non-verbal representations (URI form, K4)', () => {
167
+ const fixture = CONCEPT_FIXTURES.find(f => f.name === 'with-non-verbal')!;
168
+ const { graph } = emitConceptGraph(fixture.concept, fixture.uri);
169
+ const store = parseTurtle(writeTurtle(graph));
170
+
171
+ const lcUri = `${fixture.uri}/eng`;
172
+ const uriLinks = store.getObjects(lcUri, expandPrefixed(GLOSS.hasNonVerbalRepresentation), null);
173
+ expect(uriLinks.length).toBeGreaterThanOrEqual(3);
174
+ });
175
+
176
+ it('with-dates: emits one hasDate blank node per date entry', () => {
177
+ const fixture = CONCEPT_FIXTURES.find(f => f.name === 'with-dates')!;
178
+ const { graph } = emitConceptGraph(fixture.concept, fixture.uri);
179
+ const store = parseTurtle(writeTurtle(graph));
180
+
181
+ const dates = store.getObjects(fixture.uri, expandPrefixed(GLOSS.hasDate), null);
182
+ expect(dates.length).toBe(3);
183
+ });
184
+
185
+ it('full-relationships: emits hasRelatedConcept for each related entry', () => {
186
+ const fixture = CONCEPT_FIXTURES.find(f => f.name === 'full-relationships')!;
187
+ const { graph } = emitConceptGraph(fixture.concept, fixture.uri);
188
+ const store = parseTurtle(writeTurtle(graph));
189
+
190
+ const rels = store.getObjects(fixture.uri, expandPrefixed(GLOSS.hasRelatedConcept), null);
191
+ expect(rels.length).toBe(5);
192
+
193
+ for (const blank of rels) {
194
+ const typeQuads = store.getObjects(blank, expandPrefixed(GLOSS.relationshipType), null);
195
+ expect(typeQuads.length).toBe(1);
196
+ }
197
+ });
198
+
199
+ it('full-relationships: supersedes / superseded_by emit dcterms:replaces / dcterms:isReplacedBy (URN form)', () => {
200
+ const fixture = CONCEPT_FIXTURES.find(f => f.name === 'full-relationships')!;
201
+ const { graph } = emitConceptGraph(fixture.concept, fixture.uri);
202
+ const store = parseTurtle(writeTurtle(graph));
203
+
204
+ const replaces = store.getObjects(fixture.uri, expandPrefixed(DCTERMS.replaces), null).map(q => q.value);
205
+ const isReplacedBy = store.getObjects(fixture.uri, expandPrefixed(DCTERMS.isReplacedBy), null).map(q => q.value);
206
+
207
+ expect(replaces.some(u => u.startsWith('urn:'))).toBe(true);
208
+ expect(isReplacedBy.some(u => u.startsWith('urn:'))).toBe(true);
209
+ });
210
+
211
+ it('full-relationships: when a resolveRef callback is provided, dcterms:replaces uses the resolved URI', () => {
212
+ const fixture = CONCEPT_FIXTURES.find(f => f.name === 'full-relationships')!;
213
+ const resolver = (ref: { source?: string | null; id?: string | null }): string | undefined => {
214
+ if (ref.source === 'IEC' && ref.id === '60050-3.1.1') return 'https://glossarist.org/iec/concept/60050-3.1.1';
215
+ return undefined;
216
+ };
217
+ const { graph } = emitConceptGraph(fixture.concept, fixture.uri, { resolveRef: resolver });
218
+ const store = parseTurtle(writeTurtle(graph));
219
+
220
+ const replaces = store.getObjects(fixture.uri, expandPrefixed(DCTERMS.replaces), null).map(q => q.value);
221
+ expect(replaces).toContain('https://glossarist.org/iec/concept/60050-3.1.1');
222
+ });
223
+ });
224
+
225
+ describe('RDF round-trip — concept lifecycle (WS J3)', () => {
226
+ it('withdrawn concept with retired date emits prov:invalidatedAtTime', () => {
227
+ const fixture = CONCEPT_FIXTURES.find(f => f.name === 'withdrawn')!;
228
+ const { graph } = emitConceptGraph(fixture.concept, fixture.uri);
229
+ const store = parseTurtle(writeTurtle(graph));
230
+
231
+ const invalidated = store.getObjects(fixture.uri, expandPrefixed(PROV.invalidatedAtTime), null).map(q => q.value);
232
+ expect(invalidated).toContain('2024-09-15T00:00:00Z');
233
+ });
234
+
235
+ it('valid concept with retired date does not emit prov:invalidatedAtTime', () => {
236
+ const fixture = CONCEPT_FIXTURES.find(f => f.name === 'with-dates')!;
237
+ const { graph } = emitConceptGraph(fixture.concept, fixture.uri);
238
+ const store = parseTurtle(writeTurtle(graph));
239
+
240
+ const invalidated = store.getObjects(fixture.uri, expandPrefixed(PROV.invalidatedAtTime), null);
241
+ expect(invalidated.length).toBe(0);
242
+ });
243
+ });
@@ -0,0 +1,126 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Parser, Store } from 'n3';
3
+ import { Concept } from 'glossarist';
4
+ import { writeTurtle } from '../../components/concept-rdf/turtle-writer';
5
+ import { emitConceptGraph } from '../../components/concept-rdf/concept-emitter';
6
+
7
+ const G = 'https://www.glossarist.org/ontologies/';
8
+ const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
9
+
10
+ function parse(turtle: string): Store {
11
+ const parser = new Parser({ format: 'Turtle' });
12
+ const store = new Store();
13
+ store.addQuads(parser.parse(turtle));
14
+ return store;
15
+ }
16
+
17
+ const BASE = 'https://glossarist.org/fixtures/scoped';
18
+
19
+ describe('A3/B2 — DetailedDefinition helper with scoped examples recursion', () => {
20
+ it('emits a gloss:hasExample blank inside gloss:hasNote when the note has examples', () => {
21
+ const concept = Concept.fromJSON({
22
+ id: 'scoped.1',
23
+ uri: `${BASE}/scoped.1`,
24
+ status: 'valid',
25
+ localizations: {
26
+ eng: {
27
+ language_code: 'eng',
28
+ entry_status: 'valid',
29
+ terms: [{ type: 'expression', designation: 'scoped concept', normative_status: 'preferred' }],
30
+ definition: [{ content: 'Outer definition.' }],
31
+ notes: [{
32
+ content: 'NOTE — Outer note text.',
33
+ examples: [{ content: 'EXAMPLE — Nested scoped example.' }],
34
+ }],
35
+ },
36
+ },
37
+ });
38
+
39
+ const { graph } = emitConceptGraph(concept, concept.uri ?? "");
40
+ const store = parse(writeTurtle(graph));
41
+
42
+ const lcUri = `${BASE}/scoped.1/eng`;
43
+ const noteBlanks = store.getObjects(lcUri, `${G}hasNote`, null);
44
+ expect(noteBlanks.length).toBe(1);
45
+
46
+ const noteBlank = noteBlanks[0];
47
+ const noteTypes = store.getObjects(noteBlank, RDF_TYPE, null).map(q => q.value);
48
+ expect(noteTypes).toContain(`${G}DetailedDefinition`);
49
+
50
+ const innerExamples = store.getObjects(noteBlank, `${G}hasExample`, null);
51
+ expect(innerExamples.length).toBe(1);
52
+
53
+ const innerEx = innerExamples[0];
54
+ const innerExTypes = store.getObjects(innerEx, RDF_TYPE, null).map(q => q.value);
55
+ expect(innerExTypes).toContain(`${G}DetailedDefinition`);
56
+
57
+ const innerValues = store.getObjects(innerEx, 'http://www.w3.org/1999/02/22-rdf-syntax-ns#value', null);
58
+ expect(innerValues.length).toBe(1);
59
+ expect((innerValues[0] as any).value).toBe('EXAMPLE — Nested scoped example.');
60
+ });
61
+
62
+ it('recurses through arbitrarily deep scoped example chains', () => {
63
+ const concept = Concept.fromJSON({
64
+ id: 'scoped.2',
65
+ uri: `${BASE}/scoped.2`,
66
+ status: 'valid',
67
+ localizations: {
68
+ eng: {
69
+ language_code: 'eng',
70
+ entry_status: 'valid',
71
+ terms: [{ type: 'expression', designation: 'deep', normative_status: 'preferred' }],
72
+ definition: [{
73
+ content: 'Outer def.',
74
+ examples: [{
75
+ content: 'EXAMPLE level 1',
76
+ examples: [{
77
+ content: 'EXAMPLE level 2 (nested inside example)',
78
+ }],
79
+ }],
80
+ }],
81
+ },
82
+ },
83
+ });
84
+
85
+ const { graph } = emitConceptGraph(concept, concept.uri ?? "");
86
+ const store = parse(writeTurtle(graph));
87
+
88
+ const lcUri = `${BASE}/scoped.2/eng`;
89
+ const defBlanks = store.getObjects(lcUri, `${G}hasDefinition`, null);
90
+ expect(defBlanks.length).toBe(1);
91
+
92
+ const inner1 = store.getObjects(defBlanks[0], `${G}hasExample`, null);
93
+ expect(inner1.length).toBe(1);
94
+
95
+ const inner2 = store.getObjects(inner1[0], `${G}hasExample`, null);
96
+ expect(inner2.length).toBe(1);
97
+
98
+ const inner2Values = store.getObjects(inner2[0], 'http://www.w3.org/1999/02/22-rdf-syntax-ns#value', null);
99
+ expect((inner2Values[0] as any).value).toBe('EXAMPLE level 2 (nested inside example)');
100
+ });
101
+
102
+ it('does not emit gloss:hasExample when no scoped examples are present', () => {
103
+ const concept = Concept.fromJSON({
104
+ id: 'scoped.3',
105
+ uri: `${BASE}/scoped.3`,
106
+ status: 'valid',
107
+ localizations: {
108
+ eng: {
109
+ language_code: 'eng',
110
+ entry_status: 'valid',
111
+ terms: [{ type: 'expression', designation: 'flat', normative_status: 'preferred' }],
112
+ definition: [{ content: 'Flat def.' }],
113
+ notes: [{ content: 'Flat note.' }],
114
+ },
115
+ },
116
+ });
117
+
118
+ const { graph } = emitConceptGraph(concept, concept.uri ?? "");
119
+ const store = parse(writeTurtle(graph));
120
+
121
+ const lcUri = `${BASE}/scoped.3/eng`;
122
+ const noteBlanks = store.getObjects(lcUri, `${G}hasNote`, null);
123
+ const innerExamples = store.getObjects(noteBlanks[0], `${G}hasExample`, null);
124
+ expect(innerExamples.length).toBe(0);
125
+ });
126
+ });
@@ -0,0 +1,94 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { buildSections } from '../../components/concept-rdf/sections-builder';
3
+ import { RdfGraph } from '../../components/concept-rdf/rdf-graph';
4
+ import type { RdfResource } from '../../components/concept-rdf/rdf-graph';
5
+ import { lit, iri, blank, triple } from '../../components/concept-rdf/rdf-graph';
6
+
7
+ function freshGraph(): RdfGraph {
8
+ return new RdfGraph();
9
+ }
10
+
11
+ describe('buildSections', () => {
12
+ it('produces one ClassInstance per resource', () => {
13
+ const g = freshGraph();
14
+ g.declare('https://ex/a', { classId: 'gloss:Concept', classLabel: 'Concept', label: 'A' });
15
+ g.declare('https://ex/b', { classId: 'gloss:LocalizedConcept', classLabel: 'Localized', label: 'B' });
16
+
17
+ const sections = buildSections(g);
18
+ expect(sections).toHaveLength(2);
19
+ expect(sections[0].classId).toBe('gloss:Concept');
20
+ expect(sections[1].classId).toBe('gloss:LocalizedConcept');
21
+ });
22
+
23
+ it('carries the resource label as the section label', () => {
24
+ const g = freshGraph();
25
+ g.declare('https://ex/a', { label: '3.1.1', classId: 'gloss:Concept', classLabel: 'Concept' });
26
+ const sections = buildSections(g);
27
+ expect(sections[0].label).toBe('3.1.1');
28
+ });
29
+
30
+ it('produces one PropValue per triple', () => {
31
+ const g = freshGraph();
32
+ g.declare('https://ex/a', { classId: 'x', classLabel: 'X', label: 'a' })
33
+ .literal('gloss:identifier', '1')
34
+ .literal('gloss:note', 'one')
35
+ .literal('gloss:note', 'two');
36
+
37
+ const props = buildSections(g)[0].props;
38
+ expect(props).toHaveLength(3);
39
+ });
40
+
41
+ it('marks blank node triples as nested', () => {
42
+ const g = freshGraph();
43
+ g.declare('https://ex/a', { classId: 'x', classLabel: 'X', label: 'a' })
44
+ .blank('gloss:hasSource', [triple('rdf:value', lit('ISO 1'))])
45
+ .literal('gloss:identifier', '1');
46
+
47
+ const props = buildSections(g)[0].props;
48
+ const sourceProp = props.find(p => p.predicate === 'gloss:hasSource')!;
49
+ expect(sourceProp.nested).toBe(true);
50
+ expect(sourceProp.values[0]).toContain('rdf:value');
51
+ expect(sourceProp.values[0]).toContain('ISO 1');
52
+ });
53
+
54
+ it('dedupes identical predicate+value pairs within a section', () => {
55
+ const g = freshGraph();
56
+ g.declare('https://ex/a', { classId: 'x', classLabel: 'X', label: 'a' })
57
+ .literal('gloss:note', 'one')
58
+ .literal('gloss:note', 'one');
59
+
60
+ const props = buildSections(g)[0].props;
61
+ expect(props).toHaveLength(1);
62
+ });
63
+
64
+ it('keeps separate nested and non-nested entries for the same predicate', () => {
65
+ const g = freshGraph();
66
+ g.declare('https://ex/a', { classId: 'x', classLabel: 'X', label: 'a' })
67
+ .literal('gloss:hasSource', 'plain string')
68
+ .blank('gloss:hasSource', [triple('rdf:value', lit('structured'))]);
69
+
70
+ const props = buildSections(g)[0].props;
71
+ const sources = props.filter(p => p.predicate === 'gloss:hasSource');
72
+ expect(sources).toHaveLength(2);
73
+ expect(sources.some(p => p.nested === true)).toBe(true);
74
+ expect(sources.some(p => !p.nested)).toBe(true);
75
+ });
76
+
77
+ it('formats IRI objects as the raw value', () => {
78
+ const g = freshGraph();
79
+ g.declare('https://ex/a', { classId: 'x', classLabel: 'X', label: 'a' })
80
+ .iri('gloss:related', 'https://ex/other');
81
+
82
+ const props = buildSections(g)[0].props;
83
+ expect(props[0].values[0]).toBe('https://ex/other');
84
+ });
85
+
86
+ it('formats language-tagged literals as the bare value', () => {
87
+ const g = freshGraph();
88
+ g.declare('https://ex/a', { classId: 'x', classLabel: 'X', label: 'a' })
89
+ .literal('skos:prefLabel', 'term', { lang: 'eng' });
90
+
91
+ const props = buildSections(g)[0].props;
92
+ expect(props[0].values[0]).toBe('term');
93
+ });
94
+ });
@@ -0,0 +1,110 @@
1
+ import { describe, it, expect, beforeAll } from 'vitest';
2
+ import { readFileSync } from 'node:fs';
3
+ import { join, dirname } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { Parser as N3Parser, DataFactory } from 'n3';
6
+ import rdfDataset from '@rdfjs/dataset';
7
+ import ShaclValidator from 'rdf-validate-shacl';
8
+ import { writeTurtle } from '../../components/concept-rdf/turtle-writer';
9
+ import { emitConceptGraph } from '../../components/concept-rdf/concept-emitter';
10
+ import { emitVocabularyGraph } from '../../components/concept-rdf/vocabulary-emitter';
11
+ import { CONCEPT_FIXTURES } from '../__fixtures__/concepts';
12
+
13
+ const __dirname = dirname(fileURLToPath(import.meta.url));
14
+ const SHAPES_PATH = join(__dirname, '..', '..', '..', 'data', 'concept-model', 'shapes', 'glossarist.shacl.ttl');
15
+
16
+ const FACTORY = {
17
+ namedNode: DataFactory.namedNode,
18
+ blankNode: DataFactory.blankNode,
19
+ literal: DataFactory.literal,
20
+ defaultGraph: DataFactory.defaultGraph,
21
+ quad: DataFactory.quad,
22
+ fromTerm: DataFactory.fromTerm,
23
+ fromQuad: DataFactory.fromQuad,
24
+ dataset: rdfDataset.dataset.bind(rdfDataset),
25
+ };
26
+
27
+ const ShaclValidatorCtor = (ShaclValidator as any).default ?? ShaclValidator;
28
+
29
+ async function parseTurtle(text: string, baseIri: string) {
30
+ const parser = new N3Parser({ baseIRI: baseIri });
31
+ const out = FACTORY.dataset();
32
+ return new Promise((resolve, reject) => {
33
+ parser.parse(text, (err: Error | null, quad: any) => {
34
+ if (err) reject(err);
35
+ else if (quad) out.add(quad);
36
+ else resolve(out);
37
+ });
38
+ });
39
+ }
40
+
41
+ let validator: InstanceType<typeof ShaclValidatorCtor>;
42
+ let vocabDataset: any;
43
+
44
+ beforeAll(async () => {
45
+ const shapesText = readFileSync(SHAPES_PATH, 'utf8');
46
+ const shapes = await parseTurtle(shapesText, `file://${SHAPES_PATH}`);
47
+ validator = new ShaclValidatorCtor(shapes, { factory: FACTORY });
48
+
49
+ const vocabTtl = writeTurtle(emitVocabularyGraph());
50
+ vocabDataset = await parseTurtle(vocabTtl, 'https://glossarist.org/vocab');
51
+ });
52
+
53
+ interface Violation {
54
+ readonly shape: string;
55
+ readonly path: string;
56
+ readonly focus: string;
57
+ readonly message: string;
58
+ }
59
+
60
+ function violationsFor(report: any): Violation[] {
61
+ if (report.conforms) return [];
62
+ return report.results.map((r: any) => ({
63
+ shape: r.shape?.value ?? '(unknown)',
64
+ path: r.path?.value ?? '(unknown)',
65
+ focus: r.focusNode?.value ?? '(unknown)',
66
+ message: r.message?.length ? r.message.map((m: any) => m.value).join('; ') : '',
67
+ }));
68
+ }
69
+
70
+ function uniquePaths(violations: readonly Violation[]): readonly string[] {
71
+ return [...new Set(violations.map(v => v.path))].sort();
72
+ }
73
+
74
+ describe('Layer 4 — SHACL conformance for every emitted fixture', () => {
75
+ for (const fixture of CONCEPT_FIXTURES) {
76
+ it(`${fixture.name}: emitted Turtle + vocab conforms to glossarist SHACL shapes`, async () => {
77
+ const { graph } = emitConceptGraph(fixture.concept, fixture.uri);
78
+ const ttl = writeTurtle(graph);
79
+ const data = await parseTurtle(ttl, fixture.uri);
80
+
81
+ const combined = FACTORY.dataset();
82
+ for (const q of (vocabDataset as any)) combined.add(q);
83
+ for (const q of (data as any)) combined.add(q);
84
+
85
+ const report = validator.validate(combined);
86
+ const violations = violationsFor(report);
87
+
88
+ if (!report.conforms) {
89
+ const detail = violations.map(v => ` path=${v.path}\n focus=${v.focus}`).join('\n');
90
+ expect.fail(`SHACL violations for ${fixture.name}:\n${detail}`);
91
+ }
92
+ expect(report.conforms).toBe(true);
93
+ });
94
+ }
95
+
96
+ it('vocabulary graph is a valid skos:ConceptScheme set', async () => {
97
+ const vocabTtl = writeTurtle(emitVocabularyGraph());
98
+ const ds = await parseTurtle(vocabTtl, 'https://glossarist.org/vocab');
99
+ const SKOS = 'http://www.w3.org/2004/02/skos/core#';
100
+ const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
101
+ const concepts = [...(ds as any)].filter((q: any) =>
102
+ q.predicate.value === RDF_TYPE && q.object.value === `${SKOS}Concept`,
103
+ );
104
+ const schemes = [...(ds as any)].filter((q: any) =>
105
+ q.predicate.value === RDF_TYPE && q.object.value === `${SKOS}ConceptScheme`,
106
+ );
107
+ expect(schemes.length).toBeGreaterThan(0);
108
+ expect(concepts.length).toBeGreaterThan(0);
109
+ });
110
+ });