@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.
- package/cli/index.mjs +32 -0
- package/env.d.ts +15 -0
- package/package.json +12 -2
- package/scripts/__tests__/doctor.test.mjs +147 -0
- package/scripts/doctor.mjs +327 -0
- package/scripts/generate-data.mjs +136 -0
- package/scripts/generate-ontology-data.mjs +3 -3
- package/scripts/generate-ontology-schema.mjs +3 -3
- package/scripts/lib/agents-turtle.mjs +64 -0
- package/scripts/lib/bibliography-turtle.mjs +54 -0
- package/scripts/lib/build-activity-turtle.mjs +92 -0
- package/scripts/lib/build-cache.mjs +70 -0
- package/scripts/lib/dataset-turtle.mjs +79 -0
- package/scripts/lib/turtle-escape.mjs +0 -0
- package/scripts/lib/version-turtle.mjs +56 -0
- package/scripts/lib/vocab-turtle.mjs +64 -0
- package/scripts/normalize-yaml.mjs +99 -0
- package/scripts/sync-concept-model.mjs +86 -0
- package/scripts/validate-shacl.mjs +194 -0
- package/src/__fixtures__/concept-shape.ttl +20 -0
- package/src/__fixtures__/shacl/bad/concept.ttl +7 -0
- package/src/__fixtures__/shacl/empty/.gitkeep +0 -0
- package/src/__fixtures__/shacl/good/concept.ttl +8 -0
- package/src/__tests__/__fixtures__/concepts.ts +221 -0
- package/src/__tests__/adapters/concept-identity.test.ts +76 -0
- package/src/__tests__/components/error-boundary.test.ts +109 -0
- package/src/__tests__/composables/use-dataset-series.test.ts +262 -0
- package/src/__tests__/concept-rdf/agents-emitter.test.ts +110 -0
- package/src/__tests__/concept-rdf/bibliography-emitter.test.ts +159 -0
- package/src/__tests__/concept-rdf/build-activity-emitter.test.ts +119 -0
- package/src/__tests__/concept-rdf/coerce-date.test.ts +97 -0
- package/src/__tests__/concept-rdf/concept-emitter.test.ts +258 -0
- package/src/__tests__/concept-rdf/dataset-emitter.test.ts +224 -0
- package/src/__tests__/concept-rdf/differential.test.ts +96 -0
- package/src/__tests__/concept-rdf/group-emitter.test.ts +109 -0
- package/src/__tests__/concept-rdf/image-variant-emitter.test.ts +135 -0
- package/src/__tests__/concept-rdf/jsonld-writer.test.ts +109 -0
- package/src/__tests__/concept-rdf/nonverbal-rep.test.ts +177 -0
- package/src/__tests__/concept-rdf/property-based.test.ts +179 -0
- package/src/__tests__/concept-rdf/provenance.test.ts +110 -0
- package/src/__tests__/concept-rdf/quad-isomorphism.test.ts +43 -0
- package/src/__tests__/concept-rdf/quad-isomorphism.ts +47 -0
- package/src/__tests__/concept-rdf/rdf-components.test.ts +145 -0
- package/src/__tests__/concept-rdf/rdf-graph.test.ts +115 -0
- package/src/__tests__/concept-rdf/round-trip.test.ts +243 -0
- package/src/__tests__/concept-rdf/scoped-examples.test.ts +126 -0
- package/src/__tests__/concept-rdf/sections-builder.test.ts +94 -0
- package/src/__tests__/concept-rdf/shacl-conformance.test.ts +110 -0
- package/src/__tests__/concept-rdf/shape-consistency.test.ts +138 -0
- package/src/__tests__/concept-rdf/snapshot-generation.test.ts +75 -0
- package/src/__tests__/concept-rdf/table-formula-emitter.test.ts +142 -0
- package/src/__tests__/concept-rdf/turtle-writer.test.ts +114 -0
- package/src/__tests__/concept-rdf/use-rdf-document.test.ts +246 -0
- package/src/__tests__/concept-rdf/version-emitter.test.ts +120 -0
- package/src/__tests__/concept-rdf/vocabulary-ssot.test.ts +100 -0
- package/src/__tests__/concept-rdf-view.test.ts +136 -0
- package/src/__tests__/dataset-style.test.ts +12 -7
- package/src/__tests__/errors/errors.test.ts +142 -0
- package/src/__tests__/format-downloads.test.ts +47 -65
- package/src/__tests__/markdown-lite.test.ts +19 -0
- package/src/__tests__/perf/bundle-layout.test.ts +50 -0
- package/src/__tests__/perf/serialization-perf.test.ts +121 -0
- package/src/__tests__/scripts/agents-turtle.test.ts +61 -0
- package/src/__tests__/scripts/bibliography-turtle.test.ts +59 -0
- package/src/__tests__/scripts/build-activity-turtle.test.ts +75 -0
- package/src/__tests__/scripts/build-cache.test.ts +78 -0
- package/src/__tests__/scripts/build-integration.test.ts +134 -0
- package/src/__tests__/scripts/dataset-turtle.test.ts +94 -0
- package/src/__tests__/scripts/normalize-yaml.test.ts +98 -0
- package/src/__tests__/scripts/stryker-config.test.ts +33 -0
- package/src/__tests__/scripts/turtle-escape.test.ts +63 -0
- package/src/__tests__/scripts/version-turtle.test.ts +72 -0
- package/src/__tests__/scripts/vocab-turtle.test.ts +63 -0
- package/src/__tests__/use-format-registry.test.ts +125 -0
- package/src/__tests__/utils/bcp47.test.ts +166 -0
- package/src/__tests__/utils/color-theme.test.ts +143 -0
- package/src/__tests__/utils/url-safety.test.ts +65 -0
- package/src/__tests__/validate-shacl.test.ts +100 -0
- package/src/adapters/DatasetAdapter.ts +11 -5
- package/src/adapters/GraphDataSource.ts +2 -1
- package/src/adapters/UriRouter.ts +2 -1
- package/src/adapters/concept-identity.ts +69 -0
- package/src/adapters/factory.ts +3 -2
- package/src/adapters/model-bridge.ts +2 -1
- package/src/adapters/non-verbal/glossarist-augment.d.ts +7 -0
- package/src/adapters/non-verbal-resolver.ts +2 -1
- package/src/components/AppSidebar.vue +189 -93
- package/src/components/ConceptDetail.vue +8 -0
- package/src/components/ConceptEditionRail.vue +222 -0
- package/src/components/ConceptRdfView.vue +37 -377
- package/src/components/DatasetSeriesCard.vue +270 -0
- package/src/components/ErrorBoundary.vue +95 -0
- package/src/components/FormatDownloads.vue +17 -13
- package/src/components/HomeSeriesSection.vue +277 -0
- package/src/components/RelationSphere.vue +1672 -0
- package/src/components/SidebarSeriesSection.vue +239 -0
- package/src/components/concept-rdf/RdfInstanceHeader.vue +47 -0
- package/src/components/concept-rdf/RdfInstanceSection.vue +54 -0
- package/src/components/concept-rdf/RdfPrefixLegend.vue +27 -0
- package/src/components/concept-rdf/RdfSourcePanel.vue +72 -0
- package/src/components/concept-rdf/agents-emitter.ts +82 -0
- package/src/components/concept-rdf/bibliography-emitter.ts +83 -0
- package/src/components/concept-rdf/build-activity-emitter.ts +89 -0
- package/src/components/concept-rdf/concept-emitter.ts +443 -0
- package/src/components/concept-rdf/dataset-emitter.ts +95 -0
- package/src/components/concept-rdf/group-emitter.ts +69 -0
- package/src/components/concept-rdf/image-variant-emitter.ts +46 -0
- package/src/components/concept-rdf/jsonld-writer.ts +82 -0
- package/src/components/concept-rdf/predicates.ts +261 -0
- package/src/components/concept-rdf/provenance.ts +80 -0
- package/src/components/concept-rdf/rdf-graph.ts +211 -0
- package/src/components/concept-rdf/rdf-prefixes.ts +23 -0
- package/src/components/concept-rdf/sections-builder.ts +62 -0
- package/src/components/concept-rdf/table-formula-emitter.ts +101 -0
- package/src/components/concept-rdf/turtle-writer.ts +116 -0
- package/src/components/concept-rdf/use-rdf-document.ts +72 -0
- package/src/components/concept-rdf/version-emitter.ts +65 -0
- package/src/components/concept-rdf/vocabulary-emitter.ts +62 -0
- package/src/composables/use-color-theme.ts +82 -0
- package/src/composables/use-format-registry.ts +42 -0
- package/src/composables/useDatasetSeries.ts +258 -0
- package/src/composables/useSphereProjection.ts +125 -0
- package/src/config/group-types.ts +92 -0
- package/src/config/types.ts +81 -2
- package/src/config/use-site-config.ts +2 -1
- package/src/errors.ts +136 -0
- package/src/i18n/locales/eng.yml +24 -0
- package/src/i18n/locales/fra.yml +24 -0
- package/src/stores/vocabulary.ts +3 -1
- package/src/style.css +17 -2
- package/src/types/agents-version-turtle.d.ts +27 -0
- package/src/types/bibliography-turtle.d.ts +12 -0
- package/src/types/build-activity-turtle.d.ts +16 -0
- package/src/types/build-cache.d.ts +20 -0
- package/src/types/dataset-turtle.d.ts +32 -0
- package/src/types/normalize-yaml.d.ts +16 -0
- package/src/types/turtle-escape.d.ts +6 -0
- package/src/types/vocab-turtle.d.ts +13 -0
- package/src/utils/asciidoc-lite.ts +11 -6
- package/src/utils/bcp47.ts +141 -0
- package/src/utils/color-theme-integration.ts +11 -0
- package/src/utils/color-theme.ts +129 -0
- package/src/utils/dataset-style.ts +31 -6
- package/src/utils/locale.ts +6 -14
- package/src/utils/markdown-lite.ts +6 -1
- package/src/utils/relation-sphere-styling.ts +63 -0
- package/src/utils/relationship-categories.ts +30 -0
- package/src/utils/url-safety.ts +30 -0
- package/src/views/ConceptView.vue +183 -9
- package/src/views/DatasetView.vue +6 -0
- package/src/views/HomeView.vue +5 -0
- package/vite.config.ts +7 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { Concept } from 'glossarist';
|
|
3
|
+
import { emitConceptGraph } from '../../components/concept-rdf/concept-emitter';
|
|
4
|
+
import { GLOSS, SKOS, SKOSXL, DCTERMS, RDF, RDFS } from '../../components/concept-rdf/predicates';
|
|
5
|
+
import type { RdfBlankNode, RdfLiteral, RdfIri } from '../../components/concept-rdf/rdf-graph';
|
|
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
|
+
examples: [{ content: 'Example here.' }],
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('emitConceptGraph', () => {
|
|
29
|
+
it('produces one resource per concept + localized + designation', () => {
|
|
30
|
+
const { graph } = emitConceptGraph(makeConcept(), 'https://glossarist.org/test/concept/3.1.1');
|
|
31
|
+
const classIds = Array.from(graph.resources()).map(r => r.classId);
|
|
32
|
+
expect(classIds).toContain(GLOSS.Concept);
|
|
33
|
+
expect(classIds).toContain(GLOSS.LocalizedConcept);
|
|
34
|
+
expect(classIds.filter(c => c === GLOSS.Expression).length).toBe(2);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('declares the concept resource with both gloss:Concept and skos:Concept types', () => {
|
|
38
|
+
const uri = 'https://glossarist.org/test/concept/3.1.1';
|
|
39
|
+
const { graph } = emitConceptGraph(makeConcept(), uri);
|
|
40
|
+
const concept = graph.get(uri)!;
|
|
41
|
+
expect(concept.types).toContain(GLOSS.Concept);
|
|
42
|
+
expect(concept.types).toContain(SKOS.Concept);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('emits gloss:hasLocalization as the LAST triple on the concept (UI ordering contract)', () => {
|
|
46
|
+
const uri = 'https://glossarist.org/test/concept/3.1.1';
|
|
47
|
+
const { graph } = emitConceptGraph(makeConcept(), uri);
|
|
48
|
+
const concept = graph.get(uri)!;
|
|
49
|
+
const last = concept.triples[concept.triples.length - 1];
|
|
50
|
+
expect(last.predicate).toBe(GLOSS.hasLocalization);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('emits identifiers, status, and localization on the concept', () => {
|
|
54
|
+
const uri = 'https://glossarist.org/test/concept/3.1.1';
|
|
55
|
+
const { graph } = emitConceptGraph(makeConcept(), uri);
|
|
56
|
+
const concept = graph.get(uri)!;
|
|
57
|
+
const preds = concept.triples.map(t => t.predicate);
|
|
58
|
+
expect(preds).toContain(GLOSS.identifier);
|
|
59
|
+
expect(preds).toContain(GLOSS.hasStatus);
|
|
60
|
+
expect(preds).toContain(GLOSS.hasLocalization);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('emits both skosxl and skos predicates for each designation', () => {
|
|
64
|
+
const { graph } = emitConceptGraph(makeConcept(), 'https://glossarist.org/test/concept/3.1.1');
|
|
65
|
+
const lc = graph.get('https://glossarist.org/test/concept/3.1.1/eng')!;
|
|
66
|
+
const preds = lc.triples.map(t => t.predicate);
|
|
67
|
+
expect(preds).toContain(SKOSXL.prefLabel);
|
|
68
|
+
expect(preds).toContain(SKOS.prefLabel);
|
|
69
|
+
expect(preds).toContain(SKOSXL.altLabel);
|
|
70
|
+
expect(preds).toContain(SKOS.altLabel);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('emits skos:definition with a language tag', () => {
|
|
74
|
+
const { graph } = emitConceptGraph(makeConcept(), 'https://glossarist.org/test/concept/3.1.1');
|
|
75
|
+
const lc = graph.get('https://glossarist.org/test/concept/3.1.1/eng')!;
|
|
76
|
+
const def = lc.triples.find(t => t.predicate === SKOS.definition);
|
|
77
|
+
expect(def).toBeDefined();
|
|
78
|
+
expect((def!.object as any).lang).toBe('eng');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('emits a gloss:hasDefinition blank node reifying the definition', () => {
|
|
82
|
+
const { graph } = emitConceptGraph(makeConcept(), 'https://glossarist.org/test/concept/3.1.1');
|
|
83
|
+
const lc = graph.get('https://glossarist.org/test/concept/3.1.1/eng')!;
|
|
84
|
+
const reified = lc.triples.find(t => t.predicate === GLOSS.hasDefinition);
|
|
85
|
+
expect(reified).toBeDefined();
|
|
86
|
+
expect(reified!.object.kind).toBe('blank');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('emits notes and examples as typed gloss:DetailedDefinition blanks with language tag', () => {
|
|
90
|
+
const { graph } = emitConceptGraph(makeConcept(), 'https://glossarist.org/test/concept/3.1.1');
|
|
91
|
+
const lc = graph.get('https://glossarist.org/test/concept/3.1.1/eng')!;
|
|
92
|
+
const note = lc.triples.find(t => t.predicate === GLOSS.hasNote);
|
|
93
|
+
const example = lc.triples.find(t => t.predicate === GLOSS.hasExample);
|
|
94
|
+
expect(note).toBeDefined();
|
|
95
|
+
expect(note!.object.kind).toBe('blank');
|
|
96
|
+
const noteBlank = note!.object as any;
|
|
97
|
+
expect(noteBlank.triples.some((t: any) => t.predicate === RDF.type
|
|
98
|
+
&& t.object.kind === 'iri' && t.object.value === GLOSS.DetailedDefinition)).toBe(true);
|
|
99
|
+
expect(noteBlank.triples.some((t: any) => t.predicate === RDF.value
|
|
100
|
+
&& t.object.lang === 'eng')).toBe(true);
|
|
101
|
+
expect(example).toBeDefined();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('emits a designation resource per term with the right class', () => {
|
|
105
|
+
const { graph } = emitConceptGraph(makeConcept(), 'https://glossarist.org/test/concept/3.1.1');
|
|
106
|
+
const desig1 = graph.get('https://glossarist.org/test/concept/3.1.1/eng/desig/atomic_data_unit');
|
|
107
|
+
const desig2 = graph.get('https://glossarist.org/test/concept/3.1.1/eng/desig/ADU');
|
|
108
|
+
expect(desig1).toBeDefined();
|
|
109
|
+
expect(desig2).toBeDefined();
|
|
110
|
+
expect(desig1!.classId).toBe(GLOSS.Expression);
|
|
111
|
+
expect(desig1!.types).toContain(SKOSXL.Label);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('returns the designation URI map keyed by lang#index', () => {
|
|
115
|
+
const { designationUris } = emitConceptGraph(makeConcept(), 'https://glossarist.org/test/concept/3.1.1');
|
|
116
|
+
expect(designationUris.get('eng#0')).toBe('https://glossarist.org/test/concept/3.1.1/eng/desig/atomic_data_unit');
|
|
117
|
+
expect(designationUris.get('eng#1')).toBe('https://glossarist.org/test/concept/3.1.1/eng/desig/ADU');
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('emitConceptGraph — structured source/citation (WS K6)', () => {
|
|
122
|
+
function makeConceptWithSource() {
|
|
123
|
+
return Concept.fromJSON({
|
|
124
|
+
id: '3.1.2',
|
|
125
|
+
uri: 'https://glossarist.org/test/concept/3.1.2',
|
|
126
|
+
status: 'valid',
|
|
127
|
+
sources: [
|
|
128
|
+
{
|
|
129
|
+
status: 'identical',
|
|
130
|
+
type: 'authoritative',
|
|
131
|
+
modification: 'revised 2024',
|
|
132
|
+
origin: {
|
|
133
|
+
ref: { source: 'ISO 704', id: '3.1', version: '2020' },
|
|
134
|
+
locality: { type: 'clause', referenceFrom: '3.1' },
|
|
135
|
+
link: 'https://example.org/iso-704',
|
|
136
|
+
original: 'Original wording',
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
localizations: {
|
|
141
|
+
eng: {
|
|
142
|
+
language_code: 'eng',
|
|
143
|
+
entry_status: 'valid',
|
|
144
|
+
terms: [{ type: 'expression', designation: 'cited term', normative_status: 'preferred' }],
|
|
145
|
+
definition: [{ content: 'Definition with citation.' }],
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function firstSourceBlank(graph: any, uri: string): RdfBlankNode | undefined {
|
|
152
|
+
const r = graph.get(uri)!;
|
|
153
|
+
const t = r.triples.find((x: any) => x.predicate === GLOSS.hasSource);
|
|
154
|
+
return t?.object?.kind === 'blank' ? (t.object as RdfBlankNode) : undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
it('types every source blank node as gloss:ConceptSource', () => {
|
|
158
|
+
const uri = 'https://glossarist.org/test/concept/3.1.2';
|
|
159
|
+
const { graph } = emitConceptGraph(makeConceptWithSource(), uri);
|
|
160
|
+
const src = firstSourceBlank(graph, uri)!;
|
|
161
|
+
expect(src).toBeDefined();
|
|
162
|
+
const typeTriple = src.triples.find(t => t.predicate === RDF.type);
|
|
163
|
+
expect((typeTriple!.object as RdfIri).value).toBe(GLOSS.ConceptSource);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('emits the formatted bibliographic string on the nested gloss:Citation', () => {
|
|
167
|
+
const uri = 'https://glossarist.org/test/concept/3.1.2';
|
|
168
|
+
const { graph } = emitConceptGraph(makeConceptWithSource(), uri);
|
|
169
|
+
const src = firstSourceBlank(graph, uri)!;
|
|
170
|
+
const originSlot = src.triples.find(t => t.predicate === GLOSS.sourceOrigin);
|
|
171
|
+
const cite = (originSlot!.object as RdfBlankNode).triples.find(t => t.predicate === DCTERMS.bibliographicCitation);
|
|
172
|
+
expect(cite).toBeDefined();
|
|
173
|
+
expect((cite!.object as RdfLiteral).value).toBe('ISO 704 3.1');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('emits gloss:sourceStatus and gloss:sourceType as IRIs when present', () => {
|
|
177
|
+
const uri = 'https://glossarist.org/test/concept/3.1.2';
|
|
178
|
+
const { graph } = emitConceptGraph(makeConceptWithSource(), uri);
|
|
179
|
+
const src = firstSourceBlank(graph, uri)!;
|
|
180
|
+
const status = src.triples.find(t => t.predicate === GLOSS.sourceStatus);
|
|
181
|
+
const type = src.triples.find(t => t.predicate === GLOSS.sourceType);
|
|
182
|
+
expect((status!.object as RdfIri).value).toBe('gloss:srcstatus/identical');
|
|
183
|
+
expect((type!.object as RdfIri).value).toBe('gloss:srctype/authoritative');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('emits gloss:modification as a literal on the ConceptSource when modification is present', () => {
|
|
187
|
+
const uri = 'https://glossarist.org/test/concept/3.1.2';
|
|
188
|
+
const { graph } = emitConceptGraph(makeConceptWithSource(), uri);
|
|
189
|
+
const src = firstSourceBlank(graph, uri)!;
|
|
190
|
+
const mod = src.triples.find(t => t.predicate === GLOSS.modification);
|
|
191
|
+
expect((mod!.object as RdfLiteral).value).toBe('revised 2024');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('embeds a typed gloss:CitationRef with citationRefSource, citationRefId, citationRefVersion', () => {
|
|
195
|
+
const uri = 'https://glossarist.org/test/concept/3.1.2';
|
|
196
|
+
const { graph } = emitConceptGraph(makeConceptWithSource(), uri);
|
|
197
|
+
const src = firstSourceBlank(graph, uri)!;
|
|
198
|
+
const originSlot = src.triples.find(t => t.predicate === GLOSS.sourceOrigin)!;
|
|
199
|
+
const cite = originSlot.object as RdfBlankNode;
|
|
200
|
+
const refSlot = cite.triples.find(t => t.predicate === GLOSS.hasCitationRef)!;
|
|
201
|
+
const ref = refSlot.object as RdfBlankNode;
|
|
202
|
+
expect(ref.triples.find(t => t.predicate === RDF.type)?.object).toMatchObject({ kind: 'iri', value: GLOSS.CitationRef });
|
|
203
|
+
expect(ref.triples.find(t => t.predicate === GLOSS.citationRefSource)?.object).toMatchObject({ kind: 'literal', value: 'ISO 704' });
|
|
204
|
+
expect(ref.triples.find(t => t.predicate === GLOSS.citationRefId)?.object).toMatchObject({ kind: 'literal', value: '3.1' });
|
|
205
|
+
expect(ref.triples.find(t => t.predicate === GLOSS.citationRefVersion)?.object).toMatchObject({ kind: 'literal', value: '2020' });
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('embeds a typed gloss:Locality with localityType and referenceFrom', () => {
|
|
209
|
+
const uri = 'https://glossarist.org/test/concept/3.1.2';
|
|
210
|
+
const { graph } = emitConceptGraph(makeConceptWithSource(), uri);
|
|
211
|
+
const src = firstSourceBlank(graph, uri)!;
|
|
212
|
+
const originSlot = src.triples.find(t => t.predicate === GLOSS.sourceOrigin)!;
|
|
213
|
+
const cite = originSlot.object as RdfBlankNode;
|
|
214
|
+
const locSlot = cite.triples.find(t => t.predicate === GLOSS.hasCitationLocality)!;
|
|
215
|
+
const loc = locSlot.object as RdfBlankNode;
|
|
216
|
+
expect(loc.triples.find(t => t.predicate === RDF.type)?.object).toMatchObject({ kind: 'iri', value: GLOSS.Locality });
|
|
217
|
+
expect(loc.triples.find(t => t.predicate === GLOSS.localityType)?.object).toMatchObject({ kind: 'literal', value: 'clause' });
|
|
218
|
+
expect(loc.triples.find(t => t.predicate === GLOSS.referenceFrom)?.object).toMatchObject({ kind: 'literal', value: '3.1' });
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('emits gloss:citationLink and gloss:citationOriginal on the nested Citation', () => {
|
|
222
|
+
const uri = 'https://glossarist.org/test/concept/3.1.2';
|
|
223
|
+
const { graph } = emitConceptGraph(makeConceptWithSource(), uri);
|
|
224
|
+
const src = firstSourceBlank(graph, uri)!;
|
|
225
|
+
const originSlot = src.triples.find(t => t.predicate === GLOSS.sourceOrigin)!;
|
|
226
|
+
const cite = originSlot.object as RdfBlankNode;
|
|
227
|
+
const link = cite.triples.find(t => t.predicate === GLOSS.citationLink);
|
|
228
|
+
const original = cite.triples.find(t => t.predicate === GLOSS.citationOriginal);
|
|
229
|
+
expect((link!.object as RdfLiteral).value).toBe('https://example.org/iso-704');
|
|
230
|
+
expect((link!.object as any).datatype).toBe('xsd:anyURI');
|
|
231
|
+
expect((original!.object as RdfLiteral).value).toBe('Original wording');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('skips the source block entirely when the origin is empty', () => {
|
|
235
|
+
const concept = Concept.fromJSON({
|
|
236
|
+
id: '3.1.3',
|
|
237
|
+
uri: 'https://glossarist.org/test/concept/3.1.3',
|
|
238
|
+
status: 'valid',
|
|
239
|
+
sources: [{ status: null, type: null, origin: null }],
|
|
240
|
+
localizations: {
|
|
241
|
+
eng: {
|
|
242
|
+
language_code: 'eng',
|
|
243
|
+
terms: [{ type: 'expression', designation: 'x', normative_status: 'preferred' }],
|
|
244
|
+
definition: [{ content: 'def' }],
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
const uri = 'https://glossarist.org/test/concept/3.1.3';
|
|
249
|
+
const { graph } = emitConceptGraph(concept, uri);
|
|
250
|
+
// The type triple is always emitted, so the blank exists — but it has no citation/ref/locality.
|
|
251
|
+
const r = graph.get(uri)!;
|
|
252
|
+
const sourceTriples = r.triples.filter(t => t.predicate === GLOSS.hasSource);
|
|
253
|
+
expect(sourceTriples).toHaveLength(1);
|
|
254
|
+
const blank = sourceTriples[0].object as RdfBlankNode;
|
|
255
|
+
expect(blank.triples).toHaveLength(1); // only the type triple
|
|
256
|
+
expect(blank.triples[0].predicate).toBe(RDF.type);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
@@ -0,0 +1,224 @@
|
|
|
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 { emitDatasetGraph } from '../../components/concept-rdf/dataset-emitter';
|
|
6
|
+
import type { DatasetEmitterInput } from '../../components/concept-rdf/dataset-emitter';
|
|
7
|
+
|
|
8
|
+
const DATASET_BASE = 'https://glossarist.org/dataset';
|
|
9
|
+
|
|
10
|
+
function makeInput(overrides: Partial<DatasetEmitterInput> = {}): DatasetEmitterInput {
|
|
11
|
+
return {
|
|
12
|
+
datasetIri: `${DATASET_BASE}/test`,
|
|
13
|
+
registerId: 'test',
|
|
14
|
+
title: 'Test Glossary',
|
|
15
|
+
description: 'A test dataset.',
|
|
16
|
+
modified: '2026-06-28',
|
|
17
|
+
languages: ['eng', 'fra'],
|
|
18
|
+
distributions: [
|
|
19
|
+
{
|
|
20
|
+
id: 'test-ttl',
|
|
21
|
+
title: 'Turtle distribution',
|
|
22
|
+
mediaType: 'text/turtle',
|
|
23
|
+
downloadUrl: 'https://glossarist.org/data/test.ttl',
|
|
24
|
+
byteSize: 12345,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: 'test-tbx',
|
|
28
|
+
title: 'TBX distribution',
|
|
29
|
+
mediaType: 'application/x-tbx',
|
|
30
|
+
downloadUrl: 'https://glossarist.org/data/test.tbx.xml',
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
topConceptUris: [
|
|
34
|
+
`${DATASET_BASE}/test/concept/1`,
|
|
35
|
+
`${DATASET_BASE}/test/concept/2`,
|
|
36
|
+
],
|
|
37
|
+
sections: [
|
|
38
|
+
{
|
|
39
|
+
collectionIri: `${DATASET_BASE}/test/section/3-1`,
|
|
40
|
+
title: 'Geodetic concepts',
|
|
41
|
+
memberUris: [`${DATASET_BASE}/test/concept/1`, `${DATASET_BASE}/test/concept/2`],
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
sourceRepoUrl: 'https://github.com/example/test',
|
|
45
|
+
publisherIri: 'https://glossarist.org/agent/test-publisher',
|
|
46
|
+
contactIri: 'https://glossarist.org/agent/test-contact',
|
|
47
|
+
...overrides,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseTurtle(turtle: string): Store {
|
|
52
|
+
const parser = new Parser({ format: 'Turtle' });
|
|
53
|
+
const store = new Store();
|
|
54
|
+
store.addQuads(parser.parse(turtle));
|
|
55
|
+
return store;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const expand = (v: string): string => {
|
|
59
|
+
if (v.startsWith('dcat:')) return `http://www.w3.org/ns/dcat#${v.slice(5)}`;
|
|
60
|
+
if (v.startsWith('skos:')) return `http://www.w3.org/2004/02/skos/core#${v.slice(5)}`;
|
|
61
|
+
if (v.startsWith('dcterms:')) return `http://purl.org/dc/terms/${v.slice(8)}`;
|
|
62
|
+
if (v.startsWith('rdf:')) return `http://www.w3.org/1999/02/22-rdf-syntax-ns#${v.slice(4)}`;
|
|
63
|
+
if (v.startsWith('prov:')) return `http://www.w3.org/ns/prov#${v.slice(5)}`;
|
|
64
|
+
if (v.startsWith('xsd:')) return `http://www.w3.org/2001/XMLSchema#${v.slice(4)}`;
|
|
65
|
+
return v;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
describe('emitDatasetGraph — J2 dcat:Dataset / skos:ConceptScheme', () => {
|
|
69
|
+
it('types the dataset as dcat:Dataset and skos:ConceptScheme', () => {
|
|
70
|
+
const input = makeInput();
|
|
71
|
+
const { graph } = { graph: emitDatasetGraph(input) };
|
|
72
|
+
const store = parseTurtle(writeTurtle(graph));
|
|
73
|
+
const types = store.getObjects(input.datasetIri, expand('rdf:type'), null).map(q => q.value);
|
|
74
|
+
expect(types).toContain(expand('dcat:Dataset'));
|
|
75
|
+
expect(types).toContain(expand('skos:ConceptScheme'));
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('emits title, description, modified, and identifier', () => {
|
|
79
|
+
const input = makeInput();
|
|
80
|
+
const graph = emitDatasetGraph(input);
|
|
81
|
+
const store = parseTurtle(writeTurtle(graph));
|
|
82
|
+
const titles = store.getObjects(input.datasetIri, expand('dcterms:title'), null).map(q => q.value);
|
|
83
|
+
expect(titles).toContain('Test Glossary');
|
|
84
|
+
const descriptions = store.getObjects(input.datasetIri, expand('dcterms:description'), null).map(q => q.value);
|
|
85
|
+
expect(descriptions).toContain('A test dataset.');
|
|
86
|
+
const modified = store.getObjects(input.datasetIri, expand('dcterms:modified'), null).map(q => q.value);
|
|
87
|
+
expect(modified).toContain('2026-06-28');
|
|
88
|
+
const ids = store.getObjects(input.datasetIri, expand('dcterms:identifier'), null).map(q => q.value);
|
|
89
|
+
expect(ids).toContain('test');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('emits a dcat:Distribution blank per distribution with mediaType and downloadURL', () => {
|
|
93
|
+
const input = makeInput();
|
|
94
|
+
const graph = emitDatasetGraph(input);
|
|
95
|
+
const store = parseTurtle(writeTurtle(graph));
|
|
96
|
+
const dists = store.getObjects(input.datasetIri, expand('dcat:distribution'), null);
|
|
97
|
+
expect(dists.length).toBe(2);
|
|
98
|
+
|
|
99
|
+
const ttlDist = dists.find(d => {
|
|
100
|
+
const types = store.getObjects(d, expand('rdf:type'), null).map(q => q.value);
|
|
101
|
+
const mediaTypes = store.getObjects(d, expand('dcat:mediaType'), null).map(q => q.value);
|
|
102
|
+
return types.includes(expand('dcat:Distribution')) && mediaTypes.includes('text/turtle');
|
|
103
|
+
});
|
|
104
|
+
expect(ttlDist).toBeDefined();
|
|
105
|
+
|
|
106
|
+
const downloadUrls = store.getObjects(ttlDist!, expand('dcat:downloadURL'), null).map(q => q.value);
|
|
107
|
+
expect(downloadUrls).toContain('https://glossarist.org/data/test.ttl');
|
|
108
|
+
|
|
109
|
+
const byteSizes = store.getObjects(ttlDist!, expand('dcat:byteSize'), null).map(q => q.value);
|
|
110
|
+
expect(byteSizes).toContain('12345');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('emits skos:hasTopConcept for every top-level concept', () => {
|
|
114
|
+
const input = makeInput();
|
|
115
|
+
const graph = emitDatasetGraph(input);
|
|
116
|
+
const store = parseTurtle(writeTurtle(graph));
|
|
117
|
+
const tops = store.getObjects(input.datasetIri, expand('skos:hasTopConcept'), null).map(q => q.value);
|
|
118
|
+
expect(tops).toContain(`${DATASET_BASE}/test/concept/1`);
|
|
119
|
+
expect(tops).toContain(`${DATASET_BASE}/test/concept/2`);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('emits dcterms:language IRIs for every language', () => {
|
|
123
|
+
const input = makeInput();
|
|
124
|
+
const graph = emitDatasetGraph(input);
|
|
125
|
+
const store = parseTurtle(writeTurtle(graph));
|
|
126
|
+
const langs = store.getObjects(input.datasetIri, expand('dcterms:language'), null).map(q => q.value);
|
|
127
|
+
expect(langs).toContain('http://id.loc.gov/vocabulary/iso639-1/eng');
|
|
128
|
+
expect(langs).toContain('http://id.loc.gov/vocabulary/iso639-1/fra');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('emits prov:wasDerivedFrom, publisher, and contactPoint when provided', () => {
|
|
132
|
+
const input = makeInput();
|
|
133
|
+
const graph = emitDatasetGraph(input);
|
|
134
|
+
const store = parseTurtle(writeTurtle(graph));
|
|
135
|
+
const derived = store.getObjects(input.datasetIri, expand('prov:wasDerivedFrom'), null).map(q => q.value);
|
|
136
|
+
const publishers = store.getObjects(input.datasetIri, expand('dcterms:publisher'), null).map(q => q.value);
|
|
137
|
+
const contacts = store.getObjects(input.datasetIri, expand('dcat:contactPoint'), null).map(q => q.value);
|
|
138
|
+
expect(derived).toContain('https://github.com/example/test');
|
|
139
|
+
expect(publishers).toContain('https://glossarist.org/agent/test-publisher');
|
|
140
|
+
expect(contacts).toContain('https://glossarist.org/agent/test-contact');
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('emitDatasetGraph — J5 skos:Collection per section', () => {
|
|
145
|
+
it('emits a skos:Collection per section with skos:member entries', () => {
|
|
146
|
+
const input = makeInput({
|
|
147
|
+
sections: [
|
|
148
|
+
{
|
|
149
|
+
collectionIri: `${DATASET_BASE}/test/section/1-1`,
|
|
150
|
+
title: 'Section 1.1',
|
|
151
|
+
memberUris: [`${DATASET_BASE}/test/concept/1`],
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
collectionIri: `${DATASET_BASE}/test/section/1-2`,
|
|
155
|
+
title: 'Section 1.2',
|
|
156
|
+
memberUris: [`${DATASET_BASE}/test/concept/2`, `${DATASET_BASE}/test/concept/3`],
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
});
|
|
160
|
+
const graph = emitDatasetGraph(input);
|
|
161
|
+
const store = parseTurtle(writeTurtle(graph));
|
|
162
|
+
|
|
163
|
+
for (const section of input.sections) {
|
|
164
|
+
const types = store.getObjects(section.collectionIri, expand('rdf:type'), null).map(q => q.value);
|
|
165
|
+
expect(types).toContain(expand('skos:Collection'));
|
|
166
|
+
const titles = store.getObjects(section.collectionIri, expand('dcterms:title'), null).map(q => q.value);
|
|
167
|
+
expect(titles).toContain(section.title);
|
|
168
|
+
const members = store.getObjects(section.collectionIri, expand('skos:member'), null).map(q => q.value);
|
|
169
|
+
for (const member of section.memberUris) {
|
|
170
|
+
expect(members).toContain(member);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('omits collections when sections array is empty', () => {
|
|
176
|
+
const input = makeInput({ sections: [] });
|
|
177
|
+
const graph = emitDatasetGraph(input);
|
|
178
|
+
const ttl = writeTurtle(graph);
|
|
179
|
+
expect(ttl).not.toMatch(/skos:Collection/);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('emits gloss:hasParentSection and gloss:hasChildSection for hierarchical sections', () => {
|
|
183
|
+
const root = `${DATASET_BASE}/test/section/3`;
|
|
184
|
+
const mid = `${DATASET_BASE}/test/section/3-1`;
|
|
185
|
+
const leaf = `${DATASET_BASE}/test/section/3-1-1`;
|
|
186
|
+
const input = makeInput({
|
|
187
|
+
sections: [
|
|
188
|
+
{ collectionIri: root, title: 'Section 3', memberUris: [], childCollectionIris: [mid] },
|
|
189
|
+
{ collectionIri: mid, title: 'Section 3.1', memberUris: [], parentCollectionIri: root, childCollectionIris: [leaf] },
|
|
190
|
+
{ collectionIri: leaf, title: 'Section 3.1.1', memberUris: [], parentCollectionIri: mid },
|
|
191
|
+
],
|
|
192
|
+
});
|
|
193
|
+
const graph = emitDatasetGraph(input);
|
|
194
|
+
const store = parseTurtle(writeTurtle(graph));
|
|
195
|
+
|
|
196
|
+
const midChildren = store.getObjects(mid, 'https://www.glossarist.org/ontologies/hasChildSection', null).map(q => q.value);
|
|
197
|
+
expect(midChildren).toContain(leaf);
|
|
198
|
+
|
|
199
|
+
const midParent = store.getObjects(mid, 'https://www.glossarist.org/ontologies/hasParentSection', null).map(q => q.value);
|
|
200
|
+
expect(midParent).toContain(root);
|
|
201
|
+
|
|
202
|
+
const leafParent = store.getObjects(leaf, 'https://www.glossarist.org/ontologies/hasParentSection', null).map(q => q.value);
|
|
203
|
+
expect(leafParent).toContain(mid);
|
|
204
|
+
|
|
205
|
+
const rootParent = store.getObjects(root, 'https://www.glossarist.org/ontologies/hasParentSection', null);
|
|
206
|
+
expect(rootParent.length).toBe(0);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe('emitDatasetGraph — JSON-LD output', () => {
|
|
211
|
+
it('produces a parseable JSON-LD document with the dataset node', () => {
|
|
212
|
+
const input = makeInput();
|
|
213
|
+
const graph = emitDatasetGraph(input);
|
|
214
|
+
const doc = JSON.parse(writeJsonLd(graph));
|
|
215
|
+
const datasetNode = doc['@graph'].find((n: any) =>
|
|
216
|
+
Array.isArray(n['@type']) && n['@type'].includes('dcat:Dataset'),
|
|
217
|
+
);
|
|
218
|
+
expect(datasetNode).toBeDefined();
|
|
219
|
+
expect(datasetNode['@id']).toBe(input.datasetIri);
|
|
220
|
+
expect(datasetNode['dcterms:title']).toBe('Test Glossary');
|
|
221
|
+
const types = datasetNode['@type'];
|
|
222
|
+
expect(types).toContain('skos:ConceptScheme');
|
|
223
|
+
});
|
|
224
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll } from 'vitest';
|
|
2
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
3
|
+
import { join, dirname, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { Parser, Store, DataFactory } from 'n3';
|
|
6
|
+
import { Concept } from 'glossarist';
|
|
7
|
+
import { emitConceptGraph } from '../../components/concept-rdf/concept-emitter';
|
|
8
|
+
import { writeTurtle } from '../../components/concept-rdf/turtle-writer';
|
|
9
|
+
import { CONCEPT_FIXTURES } from '../__fixtures__/concepts';
|
|
10
|
+
|
|
11
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
|
|
13
|
+
const RUBY_SNAPSHOTS_PATH = resolve(
|
|
14
|
+
__dirname,
|
|
15
|
+
'..',
|
|
16
|
+
'..',
|
|
17
|
+
'..',
|
|
18
|
+
'node_modules',
|
|
19
|
+
'@glossarist',
|
|
20
|
+
'concept-model',
|
|
21
|
+
'test',
|
|
22
|
+
'snapshots',
|
|
23
|
+
'ruby',
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const HAVE_SNAPSHOTS = existsSync(RUBY_SNAPSHOTS_PATH);
|
|
27
|
+
|
|
28
|
+
function parseTurtle(turtle: string): Store {
|
|
29
|
+
const parser = new Parser({ format: 'Turtle' });
|
|
30
|
+
const store = new Store();
|
|
31
|
+
store.addQuads(parser.parse(turtle));
|
|
32
|
+
return store;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface QuadSet {
|
|
36
|
+
readonly size: number;
|
|
37
|
+
readonly quads: ReadonlySet<string>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function canonicalizeQuadSet(store: Store): QuadSet {
|
|
41
|
+
const quads = new Set<string>();
|
|
42
|
+
store.forEach(q => {
|
|
43
|
+
quads.add(`${q.subject.value}|${q.predicate.value}|${q.object.value}|${q.object.termType}`);
|
|
44
|
+
});
|
|
45
|
+
return { size: quads.size, quads };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe.skipIf(!HAVE_SNAPSHOTS)('WS P1 — differential testing (JS vs Ruby snapshots)', () => {
|
|
49
|
+
for (const fixture of CONCEPT_FIXTURES) {
|
|
50
|
+
it(`${fixture.name}: JS Turtle is graph-isomorphic to the Ruby snapshot`, () => {
|
|
51
|
+
const jsTtl = writeTurtle(emitConceptGraph(fixture.concept, fixture.uri).graph);
|
|
52
|
+
const rubyPath = join(RUBY_SNAPSHOTS_PATH, `${fixture.name}.ttl`);
|
|
53
|
+
const rubyTtl = readFileSync(rubyPath, 'utf8');
|
|
54
|
+
|
|
55
|
+
const jsStore = parseTurtle(jsTtl);
|
|
56
|
+
const rubyStore = parseTurtle(rubyTtl);
|
|
57
|
+
|
|
58
|
+
const jsSet = canonicalizeQuadSet(jsStore);
|
|
59
|
+
const rubySet = canonicalizeQuadSet(rubyStore);
|
|
60
|
+
|
|
61
|
+
if (jsSet.size !== rubySet.size) {
|
|
62
|
+
const jsOnly = [...jsSet.quads].filter(q => !rubySet.quads.has(q));
|
|
63
|
+
const rubyOnly = [...rubySet.quads].filter(q => !jsSet.quads.has(q));
|
|
64
|
+
expect.fail(
|
|
65
|
+
`Quad count mismatch for ${fixture.name}.\n` +
|
|
66
|
+
`JS-only (${jsOnly.length}):\n${jsOnly.slice(0, 20).join('\n')}\n` +
|
|
67
|
+
`Ruby-only (${rubyOnly.length}):\n${rubyOnly.slice(0, 20).join('\n')}`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
expect(jsSet.size).toBe(rubySet.size);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('WS P1 — differential test scaffolding', () => {
|
|
76
|
+
it('reports whether Ruby snapshots are present', () => {
|
|
77
|
+
if (!HAVE_SNAPSHOTS) {
|
|
78
|
+
// eslint-disable-next-line no-console
|
|
79
|
+
console.log(
|
|
80
|
+
`P1: Ruby snapshots not found at ${RUBY_SNAPSHOTS_PATH}.\n` +
|
|
81
|
+
`Once concept-model publishes @glossarist/concept-model/test-fixtures with snapshots/ruby/,\n` +
|
|
82
|
+
`this test will run byte-equivalence / graph-isomorphism assertions against them.\n` +
|
|
83
|
+
`Until then, the suite passes conditionally and the scaffolding is ready.`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
expect(typeof HAVE_SNAPSHOTS).toBe('boolean');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('emits a parseable Turtle for every fixture (smoke check)', () => {
|
|
90
|
+
for (const fixture of CONCEPT_FIXTURES) {
|
|
91
|
+
const jsTtl = writeTurtle(emitConceptGraph(fixture.concept, fixture.uri).graph);
|
|
92
|
+
const store = parseTurtle(jsTtl);
|
|
93
|
+
expect(store.size).toBeGreaterThan(0);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
});
|