@glossarist/concept-browser 0.7.66 → 0.7.68
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 +38 -33
- package/package.json +2 -2
- package/scripts/__tests__/cli-script-contract.test.mjs +39 -0
- package/scripts/__tests__/first-non-empty.test.mjs +51 -0
- package/scripts/__tests__/process-about-pages.test.mjs +106 -0
- package/scripts/build-edges.js +10 -2
- package/scripts/fetch-datasets.mjs +61 -54
- package/scripts/generate-data.mjs +40 -26
- package/scripts/lib/first-non-empty.mjs +19 -0
- package/scripts/process-about-pages.mjs +63 -15
- package/scripts/validate-shacl.mjs +1 -1
- package/src/__tests__/concept-rdf-view.test.ts +21 -13
- package/src/__tests__/perf/serialization-perf.test.ts +13 -30
- package/src/__tests__/tc204-deployment.test.ts +139 -0
- package/src/__tests__/use-rdf-document.test.ts +125 -0
- package/src/adapters/types.ts +7 -0
- package/src/components/ConceptRdfView.vue +4 -4
- package/src/components/use-rdf-document.ts +137 -0
- package/src/composables/useDatasetSeries.ts +1 -1
- package/src/__tests__/concept-rdf/agents-emitter.test.ts +0 -110
- package/src/__tests__/concept-rdf/bibliography-emitter.test.ts +0 -159
- package/src/__tests__/concept-rdf/build-activity-emitter.test.ts +0 -119
- package/src/__tests__/concept-rdf/coerce-date.test.ts +0 -97
- package/src/__tests__/concept-rdf/concept-emitter.test.ts +0 -258
- package/src/__tests__/concept-rdf/dataset-emitter.test.ts +0 -224
- package/src/__tests__/concept-rdf/differential.test.ts +0 -96
- package/src/__tests__/concept-rdf/group-emitter.test.ts +0 -109
- package/src/__tests__/concept-rdf/image-variant-emitter.test.ts +0 -135
- package/src/__tests__/concept-rdf/jsonld-writer.test.ts +0 -109
- package/src/__tests__/concept-rdf/nonverbal-rep.test.ts +0 -177
- package/src/__tests__/concept-rdf/property-based.test.ts +0 -179
- package/src/__tests__/concept-rdf/provenance.test.ts +0 -110
- package/src/__tests__/concept-rdf/quad-isomorphism.test.ts +0 -43
- package/src/__tests__/concept-rdf/quad-isomorphism.ts +0 -47
- package/src/__tests__/concept-rdf/rdf-components.test.ts +0 -145
- package/src/__tests__/concept-rdf/rdf-graph.test.ts +0 -115
- package/src/__tests__/concept-rdf/round-trip.test.ts +0 -243
- package/src/__tests__/concept-rdf/scoped-examples.test.ts +0 -126
- package/src/__tests__/concept-rdf/sections-builder.test.ts +0 -94
- package/src/__tests__/concept-rdf/shacl-conformance.test.ts +0 -110
- package/src/__tests__/concept-rdf/shape-consistency.test.ts +0 -138
- package/src/__tests__/concept-rdf/snapshot-generation.test.ts +0 -75
- package/src/__tests__/concept-rdf/table-formula-emitter.test.ts +0 -142
- package/src/__tests__/concept-rdf/turtle-writer.test.ts +0 -114
- package/src/__tests__/concept-rdf/use-rdf-document.test.ts +0 -246
- package/src/__tests__/concept-rdf/version-emitter.test.ts +0 -120
- package/src/__tests__/concept-rdf/vocabulary-ssot.test.ts +0 -100
- package/src/__tests__/perf/bundle-layout.test.ts +0 -50
- package/src/components/concept-rdf/agents-emitter.ts +0 -82
- package/src/components/concept-rdf/bibliography-emitter.ts +0 -83
- package/src/components/concept-rdf/build-activity-emitter.ts +0 -89
- package/src/components/concept-rdf/concept-emitter.ts +0 -443
- package/src/components/concept-rdf/dataset-emitter.ts +0 -95
- package/src/components/concept-rdf/group-emitter.ts +0 -69
- package/src/components/concept-rdf/image-variant-emitter.ts +0 -46
- package/src/components/concept-rdf/jsonld-writer.ts +0 -82
- package/src/components/concept-rdf/predicates.ts +0 -261
- package/src/components/concept-rdf/provenance.ts +0 -80
- package/src/components/concept-rdf/rdf-graph.ts +0 -211
- package/src/components/concept-rdf/sections-builder.ts +0 -62
- package/src/components/concept-rdf/table-formula-emitter.ts +0 -101
- package/src/components/concept-rdf/turtle-writer.ts +0 -116
- package/src/components/concept-rdf/use-rdf-document.ts +0 -72
- package/src/components/concept-rdf/version-emitter.ts +0 -65
- package/src/components/concept-rdf/vocabulary-emitter.ts +0 -62
- /package/src/components/{concept-rdf/RdfInstanceHeader.vue → RdfInstanceHeader.vue} +0 -0
- /package/src/components/{concept-rdf/RdfInstanceSection.vue → RdfInstanceSection.vue} +0 -0
- /package/src/components/{concept-rdf/RdfPrefixLegend.vue → RdfPrefixLegend.vue} +0 -0
- /package/src/components/{concept-rdf/RdfSourcePanel.vue → RdfSourcePanel.vue} +0 -0
- /package/src/components/{concept-rdf/rdf-prefixes.ts → rdf-prefixes.ts} +0 -0
|
@@ -14,16 +14,35 @@
|
|
|
14
14
|
//
|
|
15
15
|
// Output shape: { title: string, html: string }
|
|
16
16
|
|
|
17
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync } from 'node:fs';
|
|
18
|
-
import { join, basename, extname } from 'node:path';
|
|
17
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync, realpathSync } from 'node:fs';
|
|
18
|
+
import { join, basename, extname, resolve } from 'node:path';
|
|
19
19
|
import { cwd } from 'node:process';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
const PUBLIC_PAGES = join(ROOT, 'public', 'pages');
|
|
23
|
-
const DATASETS_DIR = join(ROOT, '.datasets');
|
|
24
|
-
const GROUPS_CONTENT_DIR = join(ROOT, 'site-content', 'groups');
|
|
22
|
+
import { loadSiteConfig } from './load-site-config.mjs';
|
|
25
23
|
|
|
26
|
-
function
|
|
24
|
+
export function loadSiteConfigForAbout() {
|
|
25
|
+
try {
|
|
26
|
+
const { config } = loadSiteConfig([]);
|
|
27
|
+
return config;
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Resolve roots at call time, not at module load — supports test
|
|
34
|
+
// environments that chdir between module import and main() call.
|
|
35
|
+
export function roots() {
|
|
36
|
+
const ROOT = cwd();
|
|
37
|
+
return {
|
|
38
|
+
ROOT,
|
|
39
|
+
PUBLIC_PAGES: join(ROOT, 'public', 'pages'),
|
|
40
|
+
DATASETS_DIR: join(ROOT, '.datasets'),
|
|
41
|
+
GROUPS_CONTENT_DIR: join(ROOT, 'site-content', 'groups'),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function renderMarkdown(text) {
|
|
27
46
|
// Minimal markdown → HTML. For production, use a real parser.
|
|
28
47
|
// This handles headings, paragraphs, bold, italic, links, lists.
|
|
29
48
|
const lines = text.split('\n');
|
|
@@ -65,7 +84,7 @@ function inline(text) {
|
|
|
65
84
|
.replace(/`(.+?)`/g, '<code>$1</code>');
|
|
66
85
|
}
|
|
67
86
|
|
|
68
|
-
function processAboutDir(sourceDir, outputPrefix) {
|
|
87
|
+
export function processAboutDir(sourceDir, outputPrefix, publicPagesDir) {
|
|
69
88
|
if (!existsSync(sourceDir)) return 0;
|
|
70
89
|
let count = 0;
|
|
71
90
|
|
|
@@ -104,8 +123,8 @@ function processAboutDir(sourceDir, outputPrefix) {
|
|
|
104
123
|
}
|
|
105
124
|
|
|
106
125
|
const output = { title, html };
|
|
107
|
-
mkdirSync(
|
|
108
|
-
writeFileSync(join(
|
|
126
|
+
mkdirSync(publicPagesDir, { recursive: true });
|
|
127
|
+
writeFileSync(join(publicPagesDir, outputName), JSON.stringify(output));
|
|
109
128
|
count++;
|
|
110
129
|
console.log(` Compiled: ${file} → public/pages/${outputName}`);
|
|
111
130
|
}
|
|
@@ -113,20 +132,41 @@ function processAboutDir(sourceDir, outputPrefix) {
|
|
|
113
132
|
return count;
|
|
114
133
|
}
|
|
115
134
|
|
|
116
|
-
function main() {
|
|
135
|
+
export function main() {
|
|
136
|
+
const { ROOT, PUBLIC_PAGES, DATASETS_DIR, GROUPS_CONTENT_DIR } = roots();
|
|
117
137
|
mkdirSync(PUBLIC_PAGES, { recursive: true });
|
|
118
138
|
let total = 0;
|
|
119
139
|
|
|
120
|
-
// Dataset about pages
|
|
140
|
+
// Dataset about pages — discover from .datasets/<id>/about/ (fetched
|
|
141
|
+
// or copied) OR from site-config localPath overrides for datasets that
|
|
142
|
+
// aren't materialized under .datasets/.
|
|
143
|
+
const siteConfig = loadSiteConfigForAbout();
|
|
144
|
+
const localPathMap = new Map();
|
|
145
|
+
for (const ds of siteConfig?.datasets || []) {
|
|
146
|
+
if (ds.id && ds.localPath) {
|
|
147
|
+
localPathMap.set(ds.id, resolve(ROOT, ds.localPath));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const seenIds = new Set();
|
|
121
152
|
if (existsSync(DATASETS_DIR)) {
|
|
122
153
|
for (const dsId of readdirSync(DATASETS_DIR)) {
|
|
154
|
+
seenIds.add(dsId);
|
|
123
155
|
const aboutDir = join(DATASETS_DIR, dsId, 'about');
|
|
124
156
|
if (existsSync(aboutDir) && statSync(aboutDir).isDirectory()) {
|
|
125
157
|
console.log(`Processing dataset: ${dsId}`);
|
|
126
|
-
total += processAboutDir(aboutDir, `dataset-${dsId}-about
|
|
158
|
+
total += processAboutDir(aboutDir, `dataset-${dsId}-about`, PUBLIC_PAGES);
|
|
127
159
|
}
|
|
128
160
|
}
|
|
129
161
|
}
|
|
162
|
+
for (const [dsId, dsPath] of localPathMap) {
|
|
163
|
+
if (seenIds.has(dsId)) continue;
|
|
164
|
+
const aboutDir = join(dsPath, 'about');
|
|
165
|
+
if (existsSync(aboutDir) && statSync(aboutDir).isDirectory()) {
|
|
166
|
+
console.log(`Processing dataset (localPath): ${dsId}`);
|
|
167
|
+
total += processAboutDir(aboutDir, `dataset-${dsId}-about`, PUBLIC_PAGES);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
130
170
|
|
|
131
171
|
// Group about pages
|
|
132
172
|
if (existsSync(GROUPS_CONTENT_DIR)) {
|
|
@@ -134,7 +174,7 @@ function main() {
|
|
|
134
174
|
const aboutDir = join(GROUPS_CONTENT_DIR, groupId, 'about');
|
|
135
175
|
if (existsSync(aboutDir) && statSync(aboutDir).isDirectory()) {
|
|
136
176
|
console.log(`Processing group: ${groupId}`);
|
|
137
|
-
total += processAboutDir(aboutDir, `group-${groupId}-about
|
|
177
|
+
total += processAboutDir(aboutDir, `group-${groupId}-about`, PUBLIC_PAGES);
|
|
138
178
|
}
|
|
139
179
|
}
|
|
140
180
|
}
|
|
@@ -142,4 +182,12 @@ function main() {
|
|
|
142
182
|
console.log(total > 0 ? `\nCompiled ${total} about page(s).` : '\nNo about pages found.');
|
|
143
183
|
}
|
|
144
184
|
|
|
145
|
-
|
|
185
|
+
// realpathSync dereferences symlinks and monorepo hoists so the
|
|
186
|
+
// comparison is stable across npx, symlinked binaries, and workspace
|
|
187
|
+
// installs. Without this, `process.argv[1]` may string-differ from
|
|
188
|
+
// `import.meta.url` even when they point to the same file.
|
|
189
|
+
const isDirectInvocation = process.argv[1]
|
|
190
|
+
&& realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
|
|
191
|
+
if (isDirectInvocation) {
|
|
192
|
+
main();
|
|
193
|
+
}
|
|
@@ -26,7 +26,7 @@ import { join, extname, dirname, resolve } from 'node:path';
|
|
|
26
26
|
import { fileURLToPath } from 'node:url';
|
|
27
27
|
import { Parser as N3Parser } from 'n3';
|
|
28
28
|
import rdfDataset from '@rdfjs/dataset';
|
|
29
|
-
import { validateShacl, quadsToDataset } from 'glossarist/rdf';
|
|
29
|
+
import { validateShacl, quadsToDataset } from 'glossarist/rdf/shacl';
|
|
30
30
|
|
|
31
31
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
32
32
|
const VENDORED_SHAPES = resolve(__dirname, '..', 'data', 'concept-model', 'shapes', 'glossarist.shacl.ttl');
|
|
@@ -64,14 +64,19 @@ async function mountFixture(uri: string, concept: Concept) {
|
|
|
64
64
|
return wrapper;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
// Contract tests for the migrated view. The view delegates RDF emission
|
|
68
|
+
// to glossarist-js via useRdfDocument. These tests verify the view still
|
|
69
|
+
// renders, the panel still opens, and RDF is produced — without coupling
|
|
70
|
+
// to the specific turtle format details (which are tested in glossarist-js's
|
|
71
|
+
// own suite).
|
|
72
|
+
describe('ConceptRdfView — Turtle emission contract', () => {
|
|
73
|
+
it('declares @prefix gloss: and skos: and skosxl:', async () => {
|
|
69
74
|
const wrapper = await mountRdfView();
|
|
70
75
|
await openRdfSourcePanel(wrapper);
|
|
71
|
-
const
|
|
72
|
-
|
|
76
|
+
const text = wrapper.find('pre').text();
|
|
77
|
+
expect(text).toContain('@prefix gloss: <https://www.glossarist.org/ontologies/>');
|
|
78
|
+
expect(text).toContain('@prefix skos: <http://www.w3.org/2004/02/skos/core#>');
|
|
73
79
|
expect(text).toContain('@prefix skosxl: <http://www.w3.org/2008/05/skos-xl#>');
|
|
74
|
-
expect(text).not.toContain('@prefix xl:');
|
|
75
80
|
});
|
|
76
81
|
|
|
77
82
|
it('uses skosxl: consistently (no stray xl: references)', async () => {
|
|
@@ -83,34 +88,37 @@ describe('ConceptRdfView — Turtle emission', () => {
|
|
|
83
88
|
expect(text).toContain('skosxl:literalForm');
|
|
84
89
|
});
|
|
85
90
|
|
|
86
|
-
it('emits
|
|
91
|
+
it('emits skosxl:prefLabel and skos:prefLabel both', async () => {
|
|
87
92
|
const wrapper = await mountRdfView();
|
|
88
93
|
await openRdfSourcePanel(wrapper);
|
|
89
94
|
const text = wrapper.find('pre').text();
|
|
90
|
-
|
|
95
|
+
// glossarist-js emits the skosxl:prefLabel link (object may be bnode or URI)
|
|
96
|
+
expect(text).toMatch(/skosxl:prefLabel\s+/);
|
|
97
|
+
// And the direct SKOS literal
|
|
91
98
|
expect(text).toMatch(/skos:prefLabel "atomic data unit"@eng/);
|
|
92
99
|
});
|
|
93
100
|
|
|
94
|
-
it('emits
|
|
101
|
+
it('emits skosxl:altLabel and skos:altLabel both for admitted', async () => {
|
|
95
102
|
const wrapper = await mountRdfView();
|
|
96
103
|
await openRdfSourcePanel(wrapper);
|
|
97
104
|
const text = wrapper.find('pre').text();
|
|
98
|
-
expect(text).toMatch(/skosxl:altLabel\s
|
|
105
|
+
expect(text).toMatch(/skosxl:altLabel\s+/);
|
|
99
106
|
expect(text).toMatch(/skos:altLabel "ADU"@eng/);
|
|
100
107
|
});
|
|
101
108
|
|
|
102
|
-
it('emits skos:definition
|
|
109
|
+
it('emits skos:definition with language tag', async () => {
|
|
103
110
|
const wrapper = await mountRdfView();
|
|
104
111
|
await openRdfSourcePanel(wrapper);
|
|
105
112
|
const text = wrapper.find('pre').text();
|
|
106
113
|
expect(text).toMatch(/skos:definition "A data unit that cannot be subdivided\."@eng/);
|
|
107
114
|
});
|
|
108
115
|
|
|
109
|
-
it('emits gloss:hasDefinition with
|
|
116
|
+
it('emits gloss:hasDefinition with DetailedDefinition typed resource', async () => {
|
|
110
117
|
const wrapper = await mountRdfView();
|
|
111
118
|
await openRdfSourcePanel(wrapper);
|
|
112
119
|
const text = wrapper.find('pre').text();
|
|
113
|
-
expect(text).
|
|
120
|
+
expect(text).toContain('gloss:DetailedDefinition');
|
|
121
|
+
expect(text).toMatch(/rdf:value "[^"]+"@eng/);
|
|
114
122
|
});
|
|
115
123
|
});
|
|
116
124
|
|
|
@@ -121,7 +129,7 @@ describe('ConceptRdfView — Layer 6 fixture corpus snapshots', () => {
|
|
|
121
129
|
const text = wrapper.find('pre').text();
|
|
122
130
|
expect(text).toContain('@prefix gloss:');
|
|
123
131
|
expect(text).toContain('@prefix skos:');
|
|
124
|
-
expect(text).toContain(
|
|
132
|
+
expect(text).toContain('@prefix skosxl:');
|
|
125
133
|
expect(text).toContain(fixture.uri);
|
|
126
134
|
});
|
|
127
135
|
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { Concept } from 'glossarist';
|
|
3
|
-
import {
|
|
4
|
-
import { writeTurtle } from '../../components/concept-rdf/turtle-writer';
|
|
5
|
-
import { writeJsonLd } from '../../components/concept-rdf/jsonld-writer';
|
|
3
|
+
import { conceptToQuads, collectQuads, writeTurtleSync, PREFIXES } from 'glossarist/rdf';
|
|
6
4
|
import type { ConceptFixture } from '../__fixtures__/concepts';
|
|
7
5
|
import { CONCEPT_FIXTURES } from '../__fixtures__/concepts';
|
|
8
6
|
|
|
@@ -41,9 +39,9 @@ describe('Layer 7 — serialization performance regression', () => {
|
|
|
41
39
|
const concepts = makeConcepts(TARGET_CONCEPT_COUNT);
|
|
42
40
|
|
|
43
41
|
const start = performance.now();
|
|
44
|
-
for (const { concept
|
|
45
|
-
const
|
|
46
|
-
|
|
42
|
+
for (const { concept } of concepts) {
|
|
43
|
+
const quads = collectQuads(conceptToQuads(concept, { registerId: 'perf', uriBase: BASE }));
|
|
44
|
+
writeTurtleSync(quads, { prefixes: PREFIXES });
|
|
47
45
|
}
|
|
48
46
|
const elapsed = performance.now() - start;
|
|
49
47
|
|
|
@@ -52,27 +50,12 @@ describe('Layer 7 — serialization performance regression', () => {
|
|
|
52
50
|
expect(elapsed).toBeLessThan(TIME_BUDGET_MS);
|
|
53
51
|
});
|
|
54
52
|
|
|
55
|
-
it(`emits JSON-LD for ${TARGET_CONCEPT_COUNT} concepts under ${TIME_BUDGET_MS}ms`, () => {
|
|
56
|
-
const concepts = makeConcepts(TARGET_CONCEPT_COUNT);
|
|
57
|
-
|
|
58
|
-
const start = performance.now();
|
|
59
|
-
for (const { concept, uri } of concepts) {
|
|
60
|
-
const { graph } = emitConceptGraph(concept, uri);
|
|
61
|
-
writeJsonLd(graph);
|
|
62
|
-
}
|
|
63
|
-
const elapsed = performance.now() - start;
|
|
64
|
-
|
|
65
|
-
// eslint-disable-next-line no-console
|
|
66
|
-
console.log(`Layer 7 perf: emitted ${TARGET_CONCEPT_COUNT} concepts to JSON-LD in ${elapsed.toFixed(0)}ms`);
|
|
67
|
-
expect(elapsed).toBeLessThan(TIME_BUDGET_MS);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
53
|
it('per-concept emit cost stays below 5ms on average', () => {
|
|
71
54
|
const concepts = makeConcepts(100);
|
|
72
55
|
const start = performance.now();
|
|
73
|
-
for (const { concept
|
|
74
|
-
const
|
|
75
|
-
|
|
56
|
+
for (const { concept } of concepts) {
|
|
57
|
+
const quads = collectQuads(conceptToQuads(concept, { registerId: 'perf', uriBase: BASE }));
|
|
58
|
+
writeTurtleSync(quads, { prefixes: PREFIXES });
|
|
76
59
|
}
|
|
77
60
|
const elapsed = performance.now() - start;
|
|
78
61
|
const perConcept = elapsed / 100;
|
|
@@ -84,9 +67,9 @@ describe('Layer 7 — scale stress (P4: 10 000 concepts)', () => {
|
|
|
84
67
|
it(`emits ${SCALE_CONCEPT_COUNT} concepts to Turtle under ${SCALE_TIME_BUDGET_MS}ms`, () => {
|
|
85
68
|
const concepts = makeConcepts(SCALE_CONCEPT_COUNT);
|
|
86
69
|
const start = performance.now();
|
|
87
|
-
for (const { concept
|
|
88
|
-
const
|
|
89
|
-
|
|
70
|
+
for (const { concept } of concepts) {
|
|
71
|
+
const quads = collectQuads(conceptToQuads(concept, { registerId: 'perf', uriBase: BASE }));
|
|
72
|
+
writeTurtleSync(quads, { prefixes: PREFIXES });
|
|
90
73
|
}
|
|
91
74
|
const elapsed = performance.now() - start;
|
|
92
75
|
// eslint-disable-next-line no-console
|
|
@@ -113,9 +96,9 @@ describe('Layer 7 — scale stress (P4: 10 000 concepts)', () => {
|
|
|
113
96
|
|
|
114
97
|
function timeTurtle(concepts: { concept: Concept; uri: string }[]): number {
|
|
115
98
|
const start = performance.now();
|
|
116
|
-
for (const { concept
|
|
117
|
-
const
|
|
118
|
-
|
|
99
|
+
for (const { concept } of concepts) {
|
|
100
|
+
const quads = collectQuads(conceptToQuads(concept, { registerId: 'perf', uriBase: BASE }));
|
|
101
|
+
writeTurtleSync(quads, { prefixes: PREFIXES });
|
|
119
102
|
}
|
|
120
103
|
return performance.now() - start;
|
|
121
104
|
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { groupManifestsIntoSeries } from '../composables/useDatasetSeries';
|
|
3
|
+
import type { Manifest } from '../adapters/types';
|
|
4
|
+
|
|
5
|
+
// Integration tests for the isotc204.geolexica.org deployment shape:
|
|
6
|
+
// three editions of ISO 14812 (2022 / 2025 / ed3-draft) wired as a
|
|
7
|
+
// lineage-series group with an explicit `current` pointing at the draft.
|
|
8
|
+
//
|
|
9
|
+
// These tests verify the exact config shape that tc204 PR #28 uses, so
|
|
10
|
+
// regressions specific to that deployment are caught here rather than
|
|
11
|
+
// in production.
|
|
12
|
+
|
|
13
|
+
function makeManifest(overrides: Partial<Manifest> & { id: string }): Manifest {
|
|
14
|
+
const base: Manifest = {
|
|
15
|
+
id: overrides.id,
|
|
16
|
+
title: overrides.title ?? overrides.id,
|
|
17
|
+
description: overrides.description ?? '',
|
|
18
|
+
owner: overrides.owner ?? '',
|
|
19
|
+
baseUrl: overrides.baseUrl ?? '/',
|
|
20
|
+
languages: overrides.languages ?? ['eng'],
|
|
21
|
+
conceptCount: overrides.conceptCount ?? 0,
|
|
22
|
+
conceptUrlTemplate: overrides.conceptUrlTemplate ?? '/concept/{id}',
|
|
23
|
+
indexUrl: overrides.indexUrl ?? '/index.json',
|
|
24
|
+
contextUrl: overrides.contextUrl ?? '/context.json',
|
|
25
|
+
uriBase: overrides.uriBase ?? 'https://isotc204.geolexica.org',
|
|
26
|
+
status: overrides.status ?? 'valid',
|
|
27
|
+
schemaVersion: overrides.schemaVersion ?? '3',
|
|
28
|
+
tags: overrides.tags ?? [],
|
|
29
|
+
lastUpdated: overrides.lastUpdated ?? '',
|
|
30
|
+
sourceRepo: overrides.sourceRepo ?? '',
|
|
31
|
+
chunkSize: overrides.chunkSize ?? 0,
|
|
32
|
+
datasetUri: overrides.datasetUri ?? '',
|
|
33
|
+
};
|
|
34
|
+
return { ...base, ...overrides } as Manifest;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const TC204_GROUP = {
|
|
38
|
+
id: 'isotc204',
|
|
39
|
+
datasets: ['isotc204-ed3', 'isotc204-2025', 'isotc204-2022'],
|
|
40
|
+
kind: 'lineage' as const,
|
|
41
|
+
current: 'isotc204-ed3',
|
|
42
|
+
label: 'ISO/TC 204 ITS Vocabulary',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
describe('tc204 deployment: multi-edition lineage series', () => {
|
|
46
|
+
it('groups all three editions into one series', () => {
|
|
47
|
+
const manifests = [
|
|
48
|
+
makeManifest({ id: 'isotc204-2022', status: 'valid', year: 2022, title: 'Edition 1' }),
|
|
49
|
+
makeManifest({ id: 'isotc204-2025', status: 'valid', year: 2025, title: 'Edition 2' }),
|
|
50
|
+
makeManifest({ id: 'isotc204-ed3', status: 'draft', year: 2026, title: 'Edition 3 draft' }),
|
|
51
|
+
];
|
|
52
|
+
const series = groupManifestsIntoSeries(manifests, undefined, [TC204_GROUP]);
|
|
53
|
+
expect(series.length).toBe(1);
|
|
54
|
+
expect(series[0].members.length).toBe(3);
|
|
55
|
+
expect(series[0].members.map(m => m.id)).toContain('isotc204-ed3');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('honors explicit current=isotc204-ed3 (the draft)', () => {
|
|
59
|
+
// tc204 marks ed3 (draft) as current because it's the active working
|
|
60
|
+
// edition, even though it's not yet published. The fallback logic
|
|
61
|
+
// would pick the newest *valid* member, which would be 2025. The
|
|
62
|
+
// explicit `current` override must win.
|
|
63
|
+
const manifests = [
|
|
64
|
+
makeManifest({ id: 'isotc204-2022', status: 'valid', year: 2022 }),
|
|
65
|
+
makeManifest({ id: 'isotc204-2025', status: 'valid', year: 2025 }),
|
|
66
|
+
makeManifest({ id: 'isotc204-ed3', status: 'draft', year: 2026 }),
|
|
67
|
+
];
|
|
68
|
+
const series = groupManifestsIntoSeries(manifests, undefined, [TC204_GROUP]);
|
|
69
|
+
expect(series[0].current?.id).toBe('isotc204-ed3');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('sorts members by year ascending — ed3 (2026) sorts LAST, not first', () => {
|
|
73
|
+
// Without manifest.year, extractYear('isotc204-ed3') returns undefined
|
|
74
|
+
// and the member sorts as year=0 (oldest). With register.yaml:year
|
|
75
|
+
// propagated to manifest.year, ed3 correctly sorts as the newest.
|
|
76
|
+
// This is the tc204-specific regression: drafts without a year in
|
|
77
|
+
// their id must still be positioned via the explicit year field.
|
|
78
|
+
const manifests = [
|
|
79
|
+
makeManifest({ id: 'isotc204-ed3', status: 'draft', year: 2026, title: 'E3' }),
|
|
80
|
+
makeManifest({ id: 'isotc204-2025', status: 'valid', year: 2025, title: 'E2' }),
|
|
81
|
+
makeManifest({ id: 'isotc204-2022', status: 'valid', year: 2022, title: 'E1' }),
|
|
82
|
+
];
|
|
83
|
+
const series = groupManifestsIntoSeries(manifests, undefined, [TC204_GROUP]);
|
|
84
|
+
const ids = series[0].members.map(m => m.id);
|
|
85
|
+
expect(ids).toEqual(['isotc204-2022', 'isotc204-2025', 'isotc204-ed3']);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('includes stub members for configured-but-unfetched editions', () => {
|
|
89
|
+
// tc204's ed3 GCR is generated from a draft ontology; it may be
|
|
90
|
+
// unavailable on some builds. The series deliberately includes
|
|
91
|
+
// stub members for every configured dataset so navigation links
|
|
92
|
+
// survive partial deployments.
|
|
93
|
+
const manifests = [
|
|
94
|
+
makeManifest({ id: 'isotc204-2022', status: 'valid', year: 2022 }),
|
|
95
|
+
makeManifest({ id: 'isotc204-2025', status: 'valid', year: 2025 }),
|
|
96
|
+
// isotc204-ed3 missing — GCR download may have failed
|
|
97
|
+
];
|
|
98
|
+
const series = groupManifestsIntoSeries(manifests, undefined, [TC204_GROUP]);
|
|
99
|
+
// All 3 configured members appear; the missing one is a stub
|
|
100
|
+
expect(series[0].members.length).toBe(3);
|
|
101
|
+
const ed3 = series[0].members.find(m => m.id === 'isotc204-ed3');
|
|
102
|
+
expect(ed3).toBeDefined();
|
|
103
|
+
expect(ed3?.status).toBe('unknown');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('honors configured current even when the member is a stub', () => {
|
|
107
|
+
// When ed3 fails to load AND the config says current=ed3, the stub
|
|
108
|
+
// member is still marked as current. This is deliberate: the config
|
|
109
|
+
// is the source of truth for which edition is "current". A stub
|
|
110
|
+
// current signals "this edition exists but isn't loaded" — the UI
|
|
111
|
+
// can surface that state.
|
|
112
|
+
//
|
|
113
|
+
// TODO: consider falling back to newest valid when the configured
|
|
114
|
+
// current has no manifest. Filed as a follow-up.
|
|
115
|
+
const manifests = [
|
|
116
|
+
makeManifest({ id: 'isotc204-2022', status: 'valid', year: 2022 }),
|
|
117
|
+
makeManifest({ id: 'isotc204-2025', status: 'valid', year: 2025 }),
|
|
118
|
+
];
|
|
119
|
+
const series = groupManifestsIntoSeries(manifests, undefined, [TC204_GROUP]);
|
|
120
|
+
expect(series[0].current?.id).toBe('isotc204-ed3');
|
|
121
|
+
expect(series[0].current?.status).toBe('unknown');
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('tc204 deployment: group about discovery', () => {
|
|
126
|
+
// tc204 ships group about content at:
|
|
127
|
+
// site-content/groups/isotc204/about/about.eng.md
|
|
128
|
+
// The build pipeline's ABOUT step must find and compile it.
|
|
129
|
+
// See scripts/__tests__/process-about-pages.test.mjs for the
|
|
130
|
+
// filesystem discovery contract.
|
|
131
|
+
it('site-content/groups/<id>/about/ layout is discoverable', async () => {
|
|
132
|
+
// Smoke check: the process-about-pages discovery logic handles
|
|
133
|
+
// the tc204 layout. The actual discovery test is in
|
|
134
|
+
// scripts/__tests__/process-about-pages.test.mjs (group case).
|
|
135
|
+
// Here we just verify the path convention matches.
|
|
136
|
+
const expectedPath = 'site-content/groups/isotc204/about/about.eng.md';
|
|
137
|
+
expect(expectedPath).toMatch(/^site-content\/groups\/[^/]+\/about\/about\.\w+\.(md|adoc|html)$/);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { ref } from 'vue';
|
|
3
|
+
import { Concept } from 'glossarist';
|
|
4
|
+
import { useRdfDocument } from '../components/use-rdf-document';
|
|
5
|
+
|
|
6
|
+
function makeConcept(): Concept {
|
|
7
|
+
return Concept.fromJSON({
|
|
8
|
+
id: '3.1.1',
|
|
9
|
+
uri: 'https://glossarist.org/test/concept/3.1.1',
|
|
10
|
+
status: 'valid',
|
|
11
|
+
localizations: {
|
|
12
|
+
eng: {
|
|
13
|
+
language_code: 'eng',
|
|
14
|
+
entry_status: 'valid',
|
|
15
|
+
terms: [
|
|
16
|
+
{ type: 'expression', designation: 'atomic data unit', normative_status: 'preferred' },
|
|
17
|
+
{ type: 'expression', designation: 'ADU', normative_status: 'admitted' },
|
|
18
|
+
],
|
|
19
|
+
definition: [{ content: 'A data unit that cannot be subdivided.' }],
|
|
20
|
+
notes: [{ content: 'Note here.' }],
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function makeConceptWithNonVerbal(): Concept {
|
|
27
|
+
return Concept.fromJSON({
|
|
28
|
+
id: '3.1.2',
|
|
29
|
+
uri: 'https://glossarist.org/test/concept/3.1.2',
|
|
30
|
+
status: 'valid',
|
|
31
|
+
localizations: {
|
|
32
|
+
eng: {
|
|
33
|
+
language_code: 'eng',
|
|
34
|
+
entry_status: 'valid',
|
|
35
|
+
terms: [{ type: 'expression', designation: 'angle of repose', normative_status: 'preferred' }],
|
|
36
|
+
definition: [{ content: 'Angle formed by a material at rest.' }],
|
|
37
|
+
non_verbal_rep: [
|
|
38
|
+
{ type: 'figure', caption: 'Angle of repose diagram', description: 'Schematic diagram showing the angle', images: [{ src: 'fig_A.23.svg' }] },
|
|
39
|
+
{ type: 'formula', caption: 'tan(θ) = μ' },
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Contract tests for the migrated composable. The composable now consumes
|
|
47
|
+
// glossarist-js's conceptToQuads + provenanceToQuads + quadSectionsToClassInstances
|
|
48
|
+
// + writeTurtleSync directly. These tests assert the wiring contract, not the
|
|
49
|
+
// detailed RDF emission (which is glossarist-js's responsibility and tested
|
|
50
|
+
// in its own suite).
|
|
51
|
+
describe('useRdfDocument — wiring contract', () => {
|
|
52
|
+
it('returns reactive sections, turtle, jsonld, and typeChain', () => {
|
|
53
|
+
const c = ref(makeConcept());
|
|
54
|
+
const doc = useRdfDocument(() => c.value, () => c.value.uri ?? '');
|
|
55
|
+
expect(doc.sections.value.length).toBeGreaterThan(0);
|
|
56
|
+
expect(typeof doc.turtle.value).toBe('string');
|
|
57
|
+
expect(doc.turtle.value.length).toBeGreaterThan(0);
|
|
58
|
+
expect(typeof doc.jsonld.value).toBe('string');
|
|
59
|
+
expect(doc.jsonld.value.length).toBeGreaterThan(0);
|
|
60
|
+
expect(Array.isArray(doc.typeChain.value)).toBe(true);
|
|
61
|
+
expect(doc.typeChain.value.length).toBeGreaterThan(0);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('emits a parseable JSON-LD document with @graph', () => {
|
|
65
|
+
const c = ref(makeConcept());
|
|
66
|
+
const { jsonld } = useRdfDocument(() => c.value, () => c.value.uri ?? '');
|
|
67
|
+
const parsed = JSON.parse(jsonld.value);
|
|
68
|
+
expect(parsed['@graph']).toBeDefined();
|
|
69
|
+
expect(Array.isArray(parsed['@graph'])).toBe(true);
|
|
70
|
+
expect(parsed['@graph'].length).toBeGreaterThan(0);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('emits turtle with @prefix declarations', () => {
|
|
74
|
+
const c = ref(makeConcept());
|
|
75
|
+
const { turtle } = useRdfDocument(() => c.value, () => c.value.uri ?? '');
|
|
76
|
+
expect(turtle.value).toMatch(/@prefix gloss: /);
|
|
77
|
+
expect(turtle.value).toMatch(/@prefix skos: /);
|
|
78
|
+
expect(turtle.value).toMatch(/@prefix skosxl: /);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('includes the concept resource in turtle output', () => {
|
|
82
|
+
const c = ref(makeConcept());
|
|
83
|
+
const { turtle } = useRdfDocument(() => c.value, () => c.value.uri ?? '');
|
|
84
|
+
expect(turtle.value).toContain('3.1.1');
|
|
85
|
+
expect(turtle.value).toMatch(/gloss:Concept/);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('attaches provenance (prov:wasGeneratedBy) to the concept resource', () => {
|
|
89
|
+
const c = ref(makeConcept());
|
|
90
|
+
const { turtle } = useRdfDocument(() => c.value, () => c.value.uri ?? '');
|
|
91
|
+
expect(turtle.value).toMatch(/prov:wasGeneratedBy/);
|
|
92
|
+
expect(turtle.value).toMatch(/activity\/serializers\/concept-browser\//);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('first section is the concept itself with classId gloss:Concept', () => {
|
|
96
|
+
const c = ref(makeConcept());
|
|
97
|
+
const { sections } = useRdfDocument(() => c.value, () => c.value.uri ?? '');
|
|
98
|
+
expect(sections.value[0].classId).toBe('gloss:Concept');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('recomputes when the concept changes', () => {
|
|
102
|
+
const c = ref(makeConcept());
|
|
103
|
+
const { sections } = useRdfDocument(() => c.value, () => c.value.uri ?? '');
|
|
104
|
+
const firstCount = sections.value.length;
|
|
105
|
+
c.value = makeConceptWithNonVerbal();
|
|
106
|
+
// Different concept should produce different section count or order
|
|
107
|
+
expect(sections.value).toBeDefined();
|
|
108
|
+
// Both should produce at least one section
|
|
109
|
+
expect(firstCount).toBeGreaterThan(0);
|
|
110
|
+
expect(sections.value.length).toBeGreaterThan(0);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('emits non-verbal rep quads when the concept has them', () => {
|
|
114
|
+
const c = ref(makeConceptWithNonVerbal());
|
|
115
|
+
const { turtle } = useRdfDocument(() => c.value, () => c.value.uri ?? '');
|
|
116
|
+
// glossarist-js emits hasNonVerbalRep for each NVR
|
|
117
|
+
expect(turtle.value).toMatch(/gloss:hasNonVerbalRep/);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('typeChain returns the canonical concept hierarchy', () => {
|
|
121
|
+
const c = ref(makeConcept());
|
|
122
|
+
const { typeChain } = useRdfDocument(() => c.value, () => c.value.uri ?? '');
|
|
123
|
+
expect(typeChain.value).toEqual(['owl:Thing', 'skos:Concept', 'gloss:Concept']);
|
|
124
|
+
});
|
|
125
|
+
});
|
package/src/adapters/types.ts
CHANGED
|
@@ -70,6 +70,13 @@ export interface Manifest {
|
|
|
70
70
|
ref?: string;
|
|
71
71
|
refAliases?: string[];
|
|
72
72
|
editionStatus?: string;
|
|
73
|
+
/**
|
|
74
|
+
* Edition year, sourced from register.yaml:year. When the dataset id
|
|
75
|
+
* doesn't contain a parseable year (e.g. "isotc204-ed3"), this field
|
|
76
|
+
* is the only way to position the edition correctly in a lineage
|
|
77
|
+
* timeline. Falls back to extractYear(id/ref/title) at the consumer.
|
|
78
|
+
*/
|
|
79
|
+
year?: number;
|
|
73
80
|
ordering?: string;
|
|
74
81
|
sections?: ManifestSection[];
|
|
75
82
|
languageStats?: Record<string, { terms: number; definitions: number }>;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed } from 'vue';
|
|
3
3
|
import type { Concept } from 'glossarist';
|
|
4
|
-
import RdfInstanceHeader from './
|
|
5
|
-
import RdfInstanceSection from './
|
|
6
|
-
import RdfSourcePanel from './
|
|
4
|
+
import RdfInstanceHeader from './RdfInstanceHeader.vue';
|
|
5
|
+
import RdfInstanceSection from './RdfInstanceSection.vue';
|
|
6
|
+
import RdfSourcePanel from './RdfSourcePanel.vue';
|
|
7
7
|
import ErrorBoundary from './ErrorBoundary.vue';
|
|
8
|
-
import { useRdfDocument } from './
|
|
8
|
+
import { useRdfDocument } from './use-rdf-document';
|
|
9
9
|
|
|
10
10
|
const props = defineProps<{
|
|
11
11
|
concept: Concept;
|