@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
@@ -0,0 +1,246 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { computed, ref, nextTick } from 'vue';
3
+ import { Concept } from 'glossarist';
4
+ import { useRdfDocument } from '../../components/concept-rdf/use-rdf-document';
5
+ import { RDF_PREFIXES, findPrefix } from '../../components/concept-rdf/rdf-prefixes';
6
+
7
+ function makeConcept(): Concept {
8
+ return Concept.fromJSON({
9
+ id: '3.1.1',
10
+ uri: 'https://glossarist.org/test/concept/3.1.1',
11
+ status: 'valid',
12
+ localizations: {
13
+ eng: {
14
+ language_code: 'eng',
15
+ entry_status: 'valid',
16
+ terms: [
17
+ { type: 'expression', designation: 'atomic data unit', normative_status: 'preferred' },
18
+ { type: 'expression', designation: 'ADU', normative_status: 'admitted' },
19
+ ],
20
+ definition: [{ content: 'A data unit that cannot be subdivided.' }],
21
+ notes: [{ content: 'Note here.' }],
22
+ },
23
+ },
24
+ });
25
+ }
26
+
27
+ function makeConceptWithNonVerbal(): Concept {
28
+ return Concept.fromJSON({
29
+ id: '3.1.2',
30
+ uri: 'https://glossarist.org/test/concept/3.1.2',
31
+ status: 'valid',
32
+ localizations: {
33
+ eng: {
34
+ language_code: 'eng',
35
+ entry_status: 'valid',
36
+ terms: [{ type: 'expression', designation: 'angle of repose', normative_status: 'preferred' }],
37
+ definition: [{ content: 'Angle formed by a material at rest.' }],
38
+ non_verbal_rep: [
39
+ { type: 'figure', caption: 'Angle of repose diagram', description: 'Schematic diagram showing the angle', images: [{ src: 'fig_A.23.svg' }] },
40
+ { type: 'formula', caption: 'tan(θ) = μ' },
41
+ ],
42
+ },
43
+ },
44
+ });
45
+ }
46
+
47
+ describe('useRdfDocument — Turtle emission contract', () => {
48
+ it('declares skosxl: prefix and never xl:', () => {
49
+ const c = ref(makeConcept());
50
+ const { turtle } = useRdfDocument(() => c.value, () => c.value.uri ?? '');
51
+ expect(turtle.value).toContain('@prefix skosxl: <http://www.w3.org/2008/05/skos-xl#>');
52
+ expect(turtle.value).not.toContain('@prefix xl:');
53
+ expect(turtle.value).not.toMatch(/\bxl:/);
54
+ });
55
+
56
+ it('emits BOTH skosxl:prefLabel AND skos:prefLabel', () => {
57
+ const c = ref(makeConcept());
58
+ const { turtle } = useRdfDocument(() => c.value, () => c.value.uri ?? '');
59
+ expect(turtle.value).toMatch(/skosxl:prefLabel\s+<[^>]+\/eng\/desig\//);
60
+ expect(turtle.value).toMatch(/skos:prefLabel "atomic data unit"@eng/);
61
+ });
62
+
63
+ it('emits BOTH skosxl:altLabel AND skos:altLabel for admitted designations', () => {
64
+ const c = ref(makeConcept());
65
+ const { turtle } = useRdfDocument(() => c.value, () => c.value.uri ?? '');
66
+ expect(turtle.value).toMatch(/skosxl:altLabel\s+<[^>]+\/eng\/desig\//);
67
+ expect(turtle.value).toMatch(/skos:altLabel "ADU"@eng/);
68
+ });
69
+
70
+ it('emits skos:definition with language tag', () => {
71
+ const c = ref(makeConcept());
72
+ const { turtle } = useRdfDocument(() => c.value, () => c.value.uri ?? '');
73
+ expect(turtle.value).toMatch(/skos:definition "A data unit that cannot be subdivided\."@eng/);
74
+ });
75
+
76
+ it('emits gloss:hasDefinition as a typed gloss:DetailedDefinition with rdf:value', () => {
77
+ const c = ref(makeConcept());
78
+ const { turtle } = useRdfDocument(() => c.value, () => c.value.uri ?? '');
79
+ expect(turtle.value).toMatch(/gloss:hasDefinition \[ (a|rdf:type) gloss:DetailedDefinition ; rdf:value "[^"]+"@eng \]/);
80
+ });
81
+
82
+ it('terminates the concept resource with a full stop (not a trailing semicolon)', () => {
83
+ const c = ref(makeConcept());
84
+ const { turtle } = useRdfDocument(() => c.value, () => c.value.uri ?? '');
85
+ const block = turtle.value.split('\n\n')[1] ?? '';
86
+ expect(block).toMatch(/<[^>]+> a gloss:Concept, skos:Concept ;[\s\S]+gloss:hasLocalization <[^>]+>/);
87
+ expect(block).toMatch(/\.$/);
88
+ expect(block).not.toMatch(/;\s*$/);
89
+ });
90
+
91
+ it('attaches provenance triples to the concept resource', () => {
92
+ const c = ref(makeConcept());
93
+ const { turtle } = useRdfDocument(() => c.value, () => c.value.uri ?? '');
94
+ expect(turtle.value).toMatch(/prov:wasGeneratedBy <activity\/serializers\/concept-browser\//);
95
+ expect(turtle.value).toMatch(/prov:generatedAtTime "[^"]+"\^\^xsd:dateTime/);
96
+ });
97
+ });
98
+
99
+ describe('useRdfDocument — JSON-LD emission contract', () => {
100
+ it('produces a parseable JSON-LD document with @context and @graph', () => {
101
+ const c = ref(makeConcept());
102
+ const { jsonld } = useRdfDocument(() => c.value, () => c.value.uri ?? '');
103
+ const doc = JSON.parse(jsonld.value);
104
+ expect(doc['@context']).toBeDefined();
105
+ expect(Array.isArray(doc['@graph'])).toBe(true);
106
+ expect(doc['@context'].skosxl).toBe('http://www.w3.org/2008/05/skos-xl#');
107
+ });
108
+
109
+ it('emits the concept node and at least one localized concept', () => {
110
+ const c = ref(makeConcept());
111
+ const { jsonld } = useRdfDocument(() => c.value, () => c.value.uri ?? '');
112
+ const doc = JSON.parse(jsonld.value);
113
+ const types = doc['@graph'].map((n: any) => n['@type']);
114
+ expect(types.some((t: any) => Array.isArray(t) && t.includes('gloss:Concept'))).toBe(true);
115
+ expect(types.some((t: any) => Array.isArray(t) && t.includes('gloss:LocalizedConcept'))).toBe(true);
116
+ });
117
+ });
118
+
119
+ describe('useRdfDocument — non-verbal representation emission (WS K)', () => {
120
+ it('emits gloss:hasNonVerbalRepresentation for NVR with caption (K4 URI form)', () => {
121
+ const c = ref(makeConceptWithNonVerbal());
122
+ const { turtle } = useRdfDocument(() => c.value, () => c.value.uri ?? '');
123
+ expect(turtle.value).toMatch(/gloss:hasNonVerbalRepresentation <[^>]+\/nvr\/[^>]+>/);
124
+ expect(turtle.value).toMatch(/(a|rdf:type) gloss:NonVerbalRep/);
125
+ expect(turtle.value).toMatch(/skosxl:prefLabel \[/);
126
+ });
127
+
128
+ it('emits gloss:image as xsd:anyURI on the NVR resource', () => {
129
+ const c = ref(makeConceptWithNonVerbal());
130
+ const { turtle } = useRdfDocument(() => c.value, () => c.value.uri ?? '');
131
+ expect(turtle.value).toMatch(/gloss:image "fig_A\.23\.svg"\^\^xsd:anyURI/);
132
+ });
133
+
134
+ it('emits NVR URIs in JSON-LD with NonVerbalRep type', () => {
135
+ const c = ref(makeConceptWithNonVerbal());
136
+ const { jsonld } = useRdfDocument(() => c.value, () => c.value.uri ?? '');
137
+ const doc = JSON.parse(jsonld.value);
138
+ const nvrNodes = doc['@graph'].filter((n: any) => {
139
+ const t = n['@type'];
140
+ return Array.isArray(t) ? t.includes('gloss:NonVerbalRep') : t === 'gloss:NonVerbalRep';
141
+ });
142
+ expect(nvrNodes.length).toBeGreaterThanOrEqual(1);
143
+ const lcNode = doc['@graph'].find((n: any) => Array.isArray(n['@type']) && n['@type'].includes('gloss:LocalizedConcept'));
144
+ expect(lcNode).toBeDefined();
145
+ const refRaw = lcNode['gloss:hasNonVerbalRepresentation'];
146
+ const refArr = Array.isArray(refRaw) ? refRaw : [refRaw];
147
+ expect(refArr.length).toBeGreaterThanOrEqual(1);
148
+ expect(refArr[0]['@id']).toMatch(/\/nvr\/[^/]+$/);
149
+ });
150
+
151
+ it('includes non-verbal reps in the sections view', () => {
152
+ const c = ref(makeConceptWithNonVerbal());
153
+ const { sections } = useRdfDocument(() => c.value, () => c.value.uri ?? '');
154
+ const lcSection = sections.value.find(s => s.classId === 'gloss:LocalizedConcept');
155
+ expect(lcSection).toBeDefined();
156
+ const nvrProps = lcSection!.props.filter(p =>
157
+ p.predicate === 'gloss:hasNonVerbalRepresentation' || p.predicate === 'gloss:hasNonVerbalRep',
158
+ );
159
+ expect(nvrProps.length).toBeGreaterThanOrEqual(1);
160
+ });
161
+
162
+ it('omits non-verbal emission when the concept has no non-verbal reps', () => {
163
+ const c = ref(makeConcept());
164
+ const { turtle, jsonld } = useRdfDocument(() => c.value, () => c.value.uri ?? '');
165
+ expect(turtle.value).not.toMatch(/gloss:hasNonVerbalRep/);
166
+ expect(turtle.value).not.toMatch(/gloss:hasNonVerbalRepresentation/);
167
+ const doc = JSON.parse(jsonld.value);
168
+ const lcNode = doc['@graph'].find((n: any) => Array.isArray(n['@type']) && n['@type'].includes('gloss:LocalizedConcept'));
169
+ expect(lcNode['gloss:hasNonVerbalRep']).toBeUndefined();
170
+ });
171
+ });
172
+
173
+ describe('useRdfDocument — reactivity', () => {
174
+ it('recomputes turtle when the concept changes', async () => {
175
+ const c = ref(makeConcept());
176
+ const { turtle } = useRdfDocument(() => c.value, () => c.value.uri ?? '');
177
+ const before = turtle.value;
178
+ expect(before).toContain('atomic data unit');
179
+
180
+ const next = Concept.fromJSON({
181
+ id: '4.5.6',
182
+ uri: 'https://glossarist.org/test/concept/4.5.6',
183
+ status: 'valid',
184
+ localizations: {
185
+ eng: {
186
+ language_code: 'eng',
187
+ entry_status: 'valid',
188
+ terms: [{ type: 'expression', designation: 'different designation', normative_status: 'preferred' }],
189
+ definition: [{ content: 'Different definition.' }],
190
+ },
191
+ },
192
+ });
193
+ c.value = next;
194
+ await nextTick();
195
+ expect(turtle.value).not.toBe(before);
196
+ expect(turtle.value).toContain('different designation');
197
+ });
198
+
199
+ it('returns one section per concept + localized concept + designation', () => {
200
+ const c = ref(makeConcept());
201
+ const { sections } = useRdfDocument(() => c.value, () => c.value.uri ?? '');
202
+ const labels = sections.value.map(s => s.classId);
203
+ expect(labels).toContain('gloss:Concept');
204
+ expect(labels).toContain('gloss:LocalizedConcept');
205
+ expect(labels.filter(l => l === 'gloss:Expression').length).toBe(2);
206
+ });
207
+
208
+ it('emits a type chain ending in gloss:Concept', () => {
209
+ const c = ref(makeConcept());
210
+ const { typeChain } = useRdfDocument(() => c.value, () => c.value.uri ?? '');
211
+ expect(typeChain.value[typeChain.value.length - 1]).toBe('gloss:Concept');
212
+ });
213
+
214
+ it('can be consumed inside another computed without re-triggering the emitter', () => {
215
+ const c = ref(makeConcept());
216
+ const { turtle } = useRdfDocument(() => c.value, () => c.value.uri ?? '');
217
+ const upper = computed(() => turtle.value.split('\n').length);
218
+ const initial = upper.value;
219
+ expect(initial).toBeGreaterThan(5);
220
+ // No mutation: reading upper twice doesn't recompute turtle.
221
+ expect(upper.value).toBe(initial);
222
+ });
223
+ });
224
+
225
+ describe('rdf-prefixes legend', () => {
226
+ it('declares all canonical prefixes used by the emitter', () => {
227
+ const declared = new Set(RDF_PREFIXES.map(p => p.prefix));
228
+ for (const required of ['gloss', 'skos', 'skosxl', 'rdf', 'dcterms']) {
229
+ expect(declared.has(required)).toBe(true);
230
+ }
231
+ });
232
+
233
+ it('lookup returns the matching prefix entry', () => {
234
+ expect(findPrefix('skosxl')?.iri).toBe('http://www.w3.org/2008/05/skos-xl#');
235
+ });
236
+
237
+ it('returns undefined for unknown prefixes', () => {
238
+ expect(findPrefix('does-not-exist')).toBeUndefined();
239
+ });
240
+
241
+ it('skosxl prefix resolves to the canonical IRI (settlement decision)', () => {
242
+ const skosxl = findPrefix('skosxl');
243
+ expect(skosxl).toBeDefined();
244
+ expect(skosxl!.iri).toBe('http://www.w3.org/2008/05/skos-xl#');
245
+ });
246
+ });
@@ -0,0 +1,120 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Parser, Store } from 'n3';
3
+ import { writeTurtle } from '../../components/concept-rdf/turtle-writer';
4
+ import { emitVersionGraph, emitVersionHistory } from '../../components/concept-rdf/version-emitter';
5
+
6
+ const PROV = 'http://www.w3.org/ns/prov#';
7
+ const DCTERMS = 'http://purl.org/dc/terms/';
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 VERSION_IRI = 'https://glossarist.org/test/versions/1.0';
18
+
19
+ function makeInput() {
20
+ return {
21
+ registerId: 'test',
22
+ version: '1.0',
23
+ versionIri: VERSION_IRI,
24
+ datasetIri: 'https://glossarist.org/test/',
25
+ generatedAt: '2026-06-28T12:00:00Z',
26
+ };
27
+ }
28
+
29
+ describe('emitVersionGraph — J6 dataset versioning', () => {
30
+ it('types the version as prov:Entity', () => {
31
+ const graph = emitVersionGraph(makeInput());
32
+ const store = parse(writeTurtle(graph));
33
+ const types = store.getObjects(VERSION_IRI, RDF_TYPE, null).map(q => q.value);
34
+ expect(types).toContain(`${PROV}Entity`);
35
+ });
36
+
37
+ it('emits dcterms:isVersionOf pointing at the canonical dataset IRI', () => {
38
+ const graph = emitVersionGraph(makeInput());
39
+ const store = parse(writeTurtle(graph));
40
+ const targets = store.getObjects(VERSION_IRI, `${DCTERMS}isVersionOf`, null).map(q => q.value);
41
+ expect(targets).toContain('https://glossarist.org/test/');
42
+ });
43
+
44
+ it('emits prov:generatedAtTime as xsd:dateTime', () => {
45
+ const graph = emitVersionGraph(makeInput());
46
+ const store = parse(writeTurtle(graph));
47
+ const ts = store.getObjects(VERSION_IRI, `${PROV}generatedAtTime`, null).map(q => q.value);
48
+ expect(ts).toContain('2026-06-28T12:00:00Z');
49
+ });
50
+
51
+ it('emits prov:wasRevisionOf when a previous version is provided', () => {
52
+ const graph = emitVersionGraph({ ...makeInput(), previousVersionIri: 'https://glossarist.org/test/versions/0.9' });
53
+ const store = parse(writeTurtle(graph));
54
+ const prev = store.getObjects(VERSION_IRI, `${PROV}wasRevisionOf`, null).map(q => q.value);
55
+ expect(prev).toContain('https://glossarist.org/test/versions/0.9');
56
+ });
57
+
58
+ it('omits prov:wasRevisionOf when no previous version', () => {
59
+ const graph = emitVersionGraph(makeInput());
60
+ const store = parse(writeTurtle(graph));
61
+ const prev = store.getObjects(VERSION_IRI, `${PROV}wasRevisionOf`, null);
62
+ expect(prev).toHaveLength(0);
63
+ });
64
+
65
+ it('records change summary and associated agent when provided', () => {
66
+ const graph = emitVersionGraph({
67
+ ...makeInput(),
68
+ changeSummary: 'Initial release',
69
+ associatedAgentIri: 'https://glossarist.org/agent/ci-bot',
70
+ });
71
+ const store = parse(writeTurtle(graph));
72
+ const summaries = store.getObjects(VERSION_IRI, `${DCTERMS}description`, null).map(q => q.value);
73
+ expect(summaries).toContain('Initial release');
74
+ const agents = store.getObjects(VERSION_IRI, `${PROV}wasAssociatedWith`, null).map(q => q.value);
75
+ expect(agents).toContain('https://glossarist.org/agent/ci-bot');
76
+ });
77
+ });
78
+
79
+ describe('emitVersionHistory', () => {
80
+ it('emits a chain of versions with prov:wasRevisionOf links', () => {
81
+ const graph = emitVersionHistory({
82
+ registerId: 'test',
83
+ datasetIri: 'https://glossarist.org/test/',
84
+ versions: [
85
+ { version: '1.0', generatedAt: '2024-01-01T00:00:00Z' },
86
+ { version: '1.1', generatedAt: '2024-06-01T00:00:00Z', changeSummary: 'Add 50 terms' },
87
+ { version: '2.0', generatedAt: '2025-01-01T00:00:00Z', changeSummary: 'Major release' },
88
+ ],
89
+ associatedAgentIri: 'https://glossarist.org/agent/ci-bot',
90
+ });
91
+
92
+ const store = parse(writeTurtle(graph));
93
+ const iri10 = 'https://glossarist.org/test/versions/1.0';
94
+ const iri11 = 'https://glossarist.org/test/versions/1.1';
95
+ const iri20 = 'https://glossarist.org/test/versions/2.0';
96
+
97
+ expect(store.getObjects(iri10, RDF_TYPE, null).length).toBeGreaterThan(0);
98
+ expect(store.getObjects(iri11, RDF_TYPE, null).length).toBeGreaterThan(0);
99
+ expect(store.getObjects(iri20, RDF_TYPE, null).length).toBeGreaterThan(0);
100
+
101
+ const v11Prev = store.getObjects(iri11, `${PROV}wasRevisionOf`, null).map(q => q.value);
102
+ expect(v11Prev).toContain(iri10);
103
+
104
+ const v20Prev = store.getObjects(iri20, `${PROV}wasRevisionOf`, null).map(q => q.value);
105
+ expect(v20Prev).toContain(iri11);
106
+
107
+ const v10Prev = store.getObjects(iri10, `${PROV}wasRevisionOf`, null);
108
+ expect(v10Prev).toHaveLength(0);
109
+ });
110
+
111
+ it('emits an empty graph when versions array is empty', () => {
112
+ const graph = emitVersionHistory({
113
+ registerId: 'test',
114
+ datasetIri: 'https://glossarist.org/test/',
115
+ versions: [],
116
+ });
117
+ const resources = Array.from(graph.resources());
118
+ expect(resources.length).toBe(0);
119
+ });
120
+ });
@@ -0,0 +1,100 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { Parser, Store } from 'n3';
5
+ import { writeTurtle } from '../../components/concept-rdf/turtle-writer';
6
+ import { emitVocabularyGraph, VOCAB_SCHEMES } from '../../components/concept-rdf/vocabulary-emitter';
7
+
8
+ const VOCAB_JSON = JSON.parse(readFileSync(join(process.cwd(), 'data', 'glossarist-vocab.json'), 'utf8'));
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 SKOS = 'http://www.w3.org/2004/02/skos/core#';
18
+ const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
19
+
20
+ const EXPECTED_REL_TYPES = [
21
+ 'deprecates', 'deprecated_by', 'supersedes', 'superseded_by', 'replaces', 'replaced_by',
22
+ 'invalidates', 'invalidated_by', 'retires', 'retired_by',
23
+ 'narrower', 'broader', 'broader_generic', 'narrower_generic',
24
+ 'broader_partitive', 'narrower_partitive', 'has_part', 'is_part_of',
25
+ 'broader_instantial', 'narrower_instantial', 'instance_of', 'has_instance',
26
+ 'equivalent', 'exact_match', 'close_match', 'broad_match', 'narrow_match', 'related_match',
27
+ 'compare', 'contrast',
28
+ 'see', 'references', 'related_concept', 'related_concept_broader', 'related_concept_narrower',
29
+ 'sequentially_related', 'spatially_related', 'temporally_related',
30
+ 'homograph', 'false_friend',
31
+ 'has_concept', 'is_concept_of', 'has_definition', 'definition_of',
32
+ 'inherits', 'inherited_by',
33
+ 'has_version', 'version_of', 'current_version', 'current_version_of',
34
+ 'derived',
35
+ ];
36
+
37
+ describe('A1/A2/C1 — vocabulary SSOT', () => {
38
+ it('VOCAB_SCHEMES loads from data/glossarist-vocab.json at module init', () => {
39
+ expect(VOCAB_SCHEMES.length).toBe(VOCAB_JSON.schemes.length);
40
+ });
41
+
42
+ it('every scheme entry in JSON appears in VOCAB_SCHEMES', () => {
43
+ const expected = new Set(VOCAB_JSON.schemes.map((s: any) => s.schemeIri));
44
+ const actual = new Set(VOCAB_SCHEMES.map(s => s.schemeIri));
45
+ expect(actual).toEqual(expected);
46
+ });
47
+
48
+ it('TS and mjs consumers read the same JSON file (single source of truth)', () => {
49
+ const tsCount = VOCAB_SCHEMES.reduce((acc, s) => acc + s.terms.length, 0);
50
+ const jsonCount = VOCAB_JSON.schemes.reduce(
51
+ (acc: number, s: any) => acc + s.terms.length, 0,
52
+ );
53
+ expect(tsCount).toBe(jsonCount);
54
+ });
55
+ });
56
+
57
+ describe('C1 — all 52 relationship types are declared', () => {
58
+ it('relationship-type scheme declares every type in EXPECTED_REL_TYPES', () => {
59
+ const relScheme = VOCAB_SCHEMES.find(s => s.schemeIri === 'gloss:rel-scheme');
60
+ expect(relScheme).toBeDefined();
61
+ const declared = new Set(relScheme!.terms.map(t => t.label));
62
+ for (const expected of EXPECTED_REL_TYPES) {
63
+ expect(declared.has(expected)).toBe(true);
64
+ }
65
+ });
66
+
67
+ it('relationship-type count matches glossarist-ruby config.yml (50 canonical + legacy)', () => {
68
+ const relScheme = VOCAB_SCHEMES.find(s => s.schemeIri === 'gloss:rel-scheme');
69
+ // glossarist-ruby config.yml related_concept.type lists 50 types. We add 'derived' as a legacy alias.
70
+ expect(relScheme!.terms.length).toBeGreaterThanOrEqual(50);
71
+ });
72
+
73
+ it('every relationship type IRI follows the gloss:rel/{type} pattern', () => {
74
+ const relScheme = VOCAB_SCHEMES.find(s => s.schemeIri === 'gloss:rel-scheme');
75
+ for (const term of relScheme!.terms) {
76
+ expect(term.iri).toMatch(/^gloss:rel\/[a-z_]+$/);
77
+ }
78
+ });
79
+
80
+ it('every relationship type is a skos:Concept in the emitted graph', () => {
81
+ const ttl = writeTurtle(emitVocabularyGraph());
82
+ const store = parse(ttl);
83
+ for (const term of VOCAB_SCHEMES.find(s => s.schemeIri === 'gloss:rel-scheme')!.terms) {
84
+ const iri = `https://www.glossarist.org/ontologies/rel/${term.label}`;
85
+ const types = store.getObjects(iri, RDF_TYPE, null).map(q => q.value);
86
+ expect(types).toContain(`${SKOS}Concept`);
87
+ }
88
+ });
89
+ });
90
+
91
+ describe('C1 — vocabulary emits without drift between TS and mjs', () => {
92
+ it('emitted Turtle contains every scheme and every term', () => {
93
+ const ttl = writeTurtle(emitVocabularyGraph());
94
+ for (const scheme of VOCAB_SCHEMES) {
95
+ for (const term of scheme.terms) {
96
+ expect(ttl).toContain(term.label);
97
+ }
98
+ }
99
+ });
100
+ });
@@ -0,0 +1,136 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { mount } from '@vue/test-utils';
3
+ import { createPinia } from 'pinia';
4
+ import { createRouter, createMemoryHistory } from 'vue-router';
5
+ import ConceptRdfView from '../components/ConceptRdfView.vue';
6
+ import { Concept } from 'glossarist';
7
+ import { CONCEPT_FIXTURES } from './__fixtures__/concepts';
8
+
9
+ function makeConcept(): Concept {
10
+ return Concept.fromJSON({
11
+ id: '3.1.1',
12
+ uri: 'https://glossarist.org/test/concept/3.1.1',
13
+ status: 'valid',
14
+ localizations: {
15
+ eng: {
16
+ language_code: 'eng',
17
+ entry_status: 'valid',
18
+ terms: [
19
+ { type: 'expression', designation: 'atomic data unit', normative_status: 'preferred' },
20
+ { type: 'expression', designation: 'ADU', normative_status: 'admitted' },
21
+ ],
22
+ definition: [{ content: 'A data unit that cannot be subdivided.' }],
23
+ notes: [{ content: 'Note here.' }],
24
+ },
25
+ },
26
+ });
27
+ }
28
+
29
+ async function mountRdfView() {
30
+ const router = createRouter({
31
+ history: createMemoryHistory(),
32
+ routes: [{ path: '/', name: 'home', component: { template: '<div/>' } }],
33
+ });
34
+ const wrapper = mount(ConceptRdfView, {
35
+ global: {
36
+ plugins: [createPinia(), router],
37
+ },
38
+ props: {
39
+ concept: makeConcept(),
40
+ registerId: 'test',
41
+ conceptUriValue: 'https://glossarist.org/test/concept/3.1.1',
42
+ },
43
+ });
44
+ await router.isReady();
45
+ return wrapper;
46
+ }
47
+
48
+ async function openRdfSourcePanel(wrapper: ReturnType<typeof mount>) {
49
+ const toggle = wrapper.find('button.w-full');
50
+ await toggle.trigger('click');
51
+ }
52
+
53
+ async function mountFixture(uri: string, concept: Concept) {
54
+ const router = createRouter({
55
+ history: createMemoryHistory(),
56
+ routes: [{ path: '/', name: 'home', component: { template: '<div/>' } }],
57
+ });
58
+ const wrapper = mount(ConceptRdfView, {
59
+ global: { plugins: [createPinia(), router] },
60
+ props: { concept, registerId: 'fixtures', conceptUriValue: uri },
61
+ });
62
+ await router.isReady();
63
+ await openRdfSourcePanel(wrapper);
64
+ return wrapper;
65
+ }
66
+
67
+ describe('ConceptRdfView — Turtle emission', () => {
68
+ it('declares the skosxl: prefix (not xl:)', async () => {
69
+ const wrapper = await mountRdfView();
70
+ await openRdfSourcePanel(wrapper);
71
+ const pre = wrapper.find('pre');
72
+ const text = pre.text();
73
+ expect(text).toContain('@prefix skosxl: <http://www.w3.org/2008/05/skos-xl#>');
74
+ expect(text).not.toContain('@prefix xl:');
75
+ });
76
+
77
+ it('uses skosxl: consistently (no stray xl: references)', async () => {
78
+ const wrapper = await mountRdfView();
79
+ await openRdfSourcePanel(wrapper);
80
+ const text = wrapper.find('pre').text();
81
+ expect(text).not.toMatch(/\bxl:/);
82
+ expect(text).toContain('skosxl:Label');
83
+ expect(text).toContain('skosxl:literalForm');
84
+ });
85
+
86
+ it('emits BOTH skosxl:prefLabel (reified) AND skos:prefLabel (direct literal)', async () => {
87
+ const wrapper = await mountRdfView();
88
+ await openRdfSourcePanel(wrapper);
89
+ const text = wrapper.find('pre').text();
90
+ expect(text).toMatch(/skosxl:prefLabel\s+<[^>]+\/eng\/desig\//);
91
+ expect(text).toMatch(/skos:prefLabel "atomic data unit"@eng/);
92
+ });
93
+
94
+ it('emits BOTH skosxl:altLabel AND skos:altLabel for non-preferred designations', async () => {
95
+ const wrapper = await mountRdfView();
96
+ await openRdfSourcePanel(wrapper);
97
+ const text = wrapper.find('pre').text();
98
+ expect(text).toMatch(/skosxl:altLabel\s+<[^>]+\/eng\/desig\//);
99
+ expect(text).toMatch(/skos:altLabel "ADU"@eng/);
100
+ });
101
+
102
+ it('emits skos:definition as a direct language-tagged literal', async () => {
103
+ const wrapper = await mountRdfView();
104
+ await openRdfSourcePanel(wrapper);
105
+ const text = wrapper.find('pre').text();
106
+ expect(text).toMatch(/skos:definition "A data unit that cannot be subdivided\."@eng/);
107
+ });
108
+
109
+ it('emits gloss:hasDefinition with a typed DetailedDefinition and language-tagged rdf:value', async () => {
110
+ const wrapper = await mountRdfView();
111
+ await openRdfSourcePanel(wrapper);
112
+ const text = wrapper.find('pre').text();
113
+ expect(text).toMatch(/gloss:hasDefinition \[ (a|rdf:type) gloss:DetailedDefinition ; rdf:value "[^"]+"@eng \]/);
114
+ });
115
+ });
116
+
117
+ describe('ConceptRdfView — Layer 6 fixture corpus snapshots', () => {
118
+ for (const fixture of CONCEPT_FIXTURES) {
119
+ it(`${fixture.name}: mounts and renders RDF for the fixture concept`, async () => {
120
+ const wrapper = await mountFixture(fixture.uri, fixture.concept);
121
+ const text = wrapper.find('pre').text();
122
+ expect(text).toContain('@prefix gloss:');
123
+ expect(text).toContain('@prefix skos:');
124
+ expect(text).toContain(`@prefix skosxl:`);
125
+ expect(text).toContain(fixture.uri);
126
+ });
127
+
128
+ it(`${fixture.name}: contains at least one skos:prefLabel per localization`, async () => {
129
+ const wrapper = await mountFixture(fixture.uri, fixture.concept);
130
+ const text = wrapper.find('pre').text();
131
+ for (const lang of fixture.concept.languages) {
132
+ expect(text).toMatch(new RegExp(`skos:prefLabel "[^"]+"@${lang}`));
133
+ }
134
+ });
135
+ }
136
+ });
@@ -0,0 +1,35 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { GROUP_RENDERERS, groupRendererFor } from '../../config/group-renderers';
3
+ import type { DatasetGroupKind } from '../../config/types';
4
+
5
+ describe('GROUP_RENDERERS registry', () => {
6
+ it('has an entry for every DatasetGroupKind', () => {
7
+ const expected: DatasetGroupKind[] = ['lineage', 'topic', 'family', 'collection', 'default'];
8
+ for (const kind of expected) {
9
+ expect(GROUP_RENDERERS[kind], `missing ${kind}`).toBeDefined();
10
+ expect(GROUP_RENDERERS[kind].sidebar, `missing sidebar for ${kind}`).toBeDefined();
11
+ }
12
+ });
13
+
14
+ it('lineage uses a distinct sidebar component', () => {
15
+ expect(GROUP_RENDERERS.lineage.sidebar).not.toBe(GROUP_RENDERERS.default.sidebar);
16
+ });
17
+
18
+ it('topic, family, collection, default share the DefaultGroupSidebar', () => {
19
+ const defaultSidebar = GROUP_RENDERERS.default.sidebar;
20
+ expect(GROUP_RENDERERS.topic.sidebar).toBe(defaultSidebar);
21
+ expect(GROUP_RENDERERS.family.sidebar).toBe(defaultSidebar);
22
+ expect(GROUP_RENDERERS.collection.sidebar).toBe(defaultSidebar);
23
+ });
24
+ });
25
+
26
+ describe('groupRendererFor', () => {
27
+ it('returns the correct renderer for each kind', () => {
28
+ expect(groupRendererFor('lineage').kind).toBe('lineage');
29
+ expect(groupRendererFor('topic').kind).toBe('topic');
30
+ });
31
+
32
+ it('falls back to default for unknown kinds', () => {
33
+ expect(groupRendererFor('nonexistent' as DatasetGroupKind)).toBe(GROUP_RENDERERS.default);
34
+ });
35
+ });