@glossarist/concept-browser 0.7.65 → 0.7.67
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 +39 -27
- package/scripts/lib/agents-turtle.mjs +8 -2
- 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__/scripts/agents-turtle.test.ts +18 -6
- package/src/__tests__/scripts/build-activity-turtle.test.ts +6 -0
- package/src/__tests__/scripts/build-integration.test.ts +4 -3
- package/src/__tests__/use-rdf-document.test.ts +125 -0
- package/src/components/ConceptRdfView.vue +4 -4
- package/src/components/use-rdf-document.ts +137 -0
- 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
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { Parser, Store } from 'n3';
|
|
3
|
-
import { buildAgentsTurtle } from '../../../scripts/lib/agents-turtle.mjs';
|
|
3
|
+
import { buildAgentsTurtle as _buildAgentsTurtle } from '../../../scripts/lib/agents-turtle.mjs';
|
|
4
|
+
|
|
5
|
+
const buildAgentsTurtle = _buildAgentsTurtle as unknown as (...args: any[]) => Promise<string>;
|
|
4
6
|
|
|
5
7
|
function parse(turtle: string): Store {
|
|
6
8
|
const parser = new Parser({ format: 'Turtle' });
|
|
@@ -16,13 +18,13 @@ const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
|
|
|
16
18
|
|
|
17
19
|
describe('buildAgentsTurtle (mjs)', () => {
|
|
18
20
|
it('parses without errors', async () => {
|
|
19
|
-
const ttl = await buildAgentsTurtle([{ name: 'Ada Lovelace' }], 'https://glossarist.org/agent');
|
|
21
|
+
const ttl = await buildAgentsTurtle([{ name: 'Ada Lovelace' }], 'https://glossarist.org/agent', 'https://glossarist.org/org');
|
|
20
22
|
const store = parse(ttl);
|
|
21
23
|
expect(store.size).toBeGreaterThan(0);
|
|
22
24
|
});
|
|
23
25
|
|
|
24
26
|
it('types each person as foaf:Person, prov:Person, prov:Agent', async () => {
|
|
25
|
-
const ttl = await buildAgentsTurtle([{ name: 'Ada Lovelace' }], 'https://glossarist.org/agent');
|
|
27
|
+
const ttl = await buildAgentsTurtle([{ name: 'Ada Lovelace' }], 'https://glossarist.org/agent', 'https://glossarist.org/org');
|
|
26
28
|
const store = parse(ttl);
|
|
27
29
|
const types = store.getObjects('https://glossarist.org/agent/ada-lovelace', RDF_TYPE, null).map(q => q.value);
|
|
28
30
|
expect(types).toContain(`${FOAF}Person`);
|
|
@@ -33,7 +35,7 @@ describe('buildAgentsTurtle (mjs)', () => {
|
|
|
33
35
|
it('records name, mailto mbox, role, and seeAlso', async () => {
|
|
34
36
|
const ttl = await buildAgentsTurtle([
|
|
35
37
|
{ name: 'Ada Lovelace', email: 'ada@example.org', role: 'Editor', url: 'https://example.org/ada' },
|
|
36
|
-
], 'https://glossarist.org/agent');
|
|
38
|
+
], 'https://glossarist.org/agent', 'https://glossarist.org/org');
|
|
37
39
|
const store = parse(ttl);
|
|
38
40
|
const iri = 'https://glossarist.org/agent/ada-lovelace';
|
|
39
41
|
expect(store.getObjects(iri, `${FOAF}name`, null).map(q => q.value)).toContain('Ada Lovelace');
|
|
@@ -46,7 +48,7 @@ describe('buildAgentsTurtle (mjs)', () => {
|
|
|
46
48
|
const ttl = await buildAgentsTurtle([
|
|
47
49
|
{ name: 'Alice', organization: 'ISO' },
|
|
48
50
|
{ name: 'Bob', organization: 'ISO' },
|
|
49
|
-
], 'https://glossarist.org/agent');
|
|
51
|
+
], 'https://glossarist.org/agent', 'https://glossarist.org/org');
|
|
50
52
|
const store = parse(ttl);
|
|
51
53
|
const types = store.getObjects('https://glossarist.org/org/iso', RDF_TYPE, null).map(q => q.value);
|
|
52
54
|
expect(types).toContain(`${FOAF}Organization`);
|
|
@@ -54,8 +56,18 @@ describe('buildAgentsTurtle (mjs)', () => {
|
|
|
54
56
|
});
|
|
55
57
|
|
|
56
58
|
it('emits only prefix declarations for an empty input', async () => {
|
|
57
|
-
const ttl = await buildAgentsTurtle([], 'https://glossarist.org/agent');
|
|
59
|
+
const ttl = await buildAgentsTurtle([], 'https://glossarist.org/agent', 'https://glossarist.org/org');
|
|
58
60
|
expect(ttl).toContain('@prefix foaf:');
|
|
59
61
|
expect(ttl).not.toContain('<https://glossarist.org/agent/');
|
|
60
62
|
});
|
|
63
|
+
|
|
64
|
+
it('throws when agentBase is missing (no hardcoded default)', async () => {
|
|
65
|
+
await expect(buildAgentsTurtle([{ name: 'X' }])).rejects.toThrow('agentBase');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('throws when orgBase is missing (no hardcoded default)', async () => {
|
|
69
|
+
await expect(
|
|
70
|
+
buildAgentsTurtle([{ name: 'X' }], 'https://example.org/agent'),
|
|
71
|
+
).rejects.toThrow('orgBase');
|
|
72
|
+
});
|
|
61
73
|
});
|
|
@@ -14,6 +14,7 @@ function makeInput(overrides: any = {}): any {
|
|
|
14
14
|
datasetRegisters: ['iso-geodetic'],
|
|
15
15
|
conceptCount: 1234,
|
|
16
16
|
associatedAgentIri: 'https://glossarist.org/agent/ci-bot',
|
|
17
|
+
baseUri: 'https://glossarist.org',
|
|
17
18
|
...overrides,
|
|
18
19
|
};
|
|
19
20
|
}
|
|
@@ -72,4 +73,9 @@ describe('buildActivityTurtle (mjs)', () => {
|
|
|
72
73
|
const agents = store.getObjects(ACTIVITY_IRI(input.runId), expand('prov:wasAssociatedWith'), null).map(q => q.value);
|
|
73
74
|
expect(agents).toContain(input.associatedAgentIri);
|
|
74
75
|
});
|
|
76
|
+
|
|
77
|
+
it('throws when baseUri is missing (no hardcoded default)', async () => {
|
|
78
|
+
const { baseUri, ...inputWithoutBaseUri } = makeInput();
|
|
79
|
+
await expect(buildActivityTurtle(inputWithoutBaseUri)).rejects.toThrow('baseUri');
|
|
80
|
+
});
|
|
75
81
|
});
|
|
@@ -46,6 +46,7 @@ describe('WS F Layer 8 — build pipeline integration', () => {
|
|
|
46
46
|
const out = runInline(`
|
|
47
47
|
import { buildActivityTurtle } from './scripts/lib/build-activity-turtle.mjs';
|
|
48
48
|
console.log(await buildActivityTurtle({
|
|
49
|
+
baseUri: 'https://glossarist.org',
|
|
49
50
|
runId: 'test', startedAt: '2026-01-01T00:00:00Z', endedAt: '2026-01-01T00:05:00Z',
|
|
50
51
|
toolId: 'cb', toolVersion: '0.7.52', datasetRegisters: [], conceptCount: 0,
|
|
51
52
|
}));
|
|
@@ -61,7 +62,7 @@ describe('WS F Layer 8 — build pipeline integration', () => {
|
|
|
61
62
|
it('agents-turtle.mjs produces parseable foaf:Person', () => {
|
|
62
63
|
const out = runInline(`
|
|
63
64
|
import { buildAgentsTurtle } from './scripts/lib/agents-turtle.mjs';
|
|
64
|
-
console.log(await buildAgentsTurtle([{ name: 'Ada Lovelace', role: 'Editor', organization: 'Royal Society' }], 'https://glossarist.org/agent'));
|
|
65
|
+
console.log(await buildAgentsTurtle([{ name: 'Ada Lovelace', role: 'Editor', organization: 'Royal Society' }], 'https://glossarist.org/agent', 'https://glossarist.org/org'));
|
|
65
66
|
`);
|
|
66
67
|
const store = parseTurtle(out);
|
|
67
68
|
const persons = [...store].filter(q =>
|
|
@@ -121,8 +122,8 @@ describe('WS F Layer 8 — build pipeline integration', () => {
|
|
|
121
122
|
const parts = [
|
|
122
123
|
await buildVocabularyTurtle(),
|
|
123
124
|
await buildDatasetTurtle({ datasetIri: 'https://glossarist.org/test/', registerId: 'test', title: 't', modified: '2026-01-01', languages: ['eng'], distributions: [], topConceptUris: [], sections: [] }),
|
|
124
|
-
await buildActivityTurtle({ runId: 'r', startedAt: '2026-01-01T00:00:00Z', endedAt: '2026-01-01T00:05:00Z', toolId: 'cb', toolVersion: '0', datasetRegisters: [], conceptCount: 0 }),
|
|
125
|
-
await buildAgentsTurtle([{ name: 'Ada' }], 'https://glossarist.org/agent'),
|
|
125
|
+
await buildActivityTurtle({ baseUri: 'https://glossarist.org', runId: 'r', startedAt: '2026-01-01T00:00:00Z', endedAt: '2026-01-01T00:05:00Z', toolId: 'cb', toolVersion: '0', datasetRegisters: [], conceptCount: 0 }),
|
|
126
|
+
await buildAgentsTurtle([{ name: 'Ada' }], 'https://glossarist.org/agent', 'https://glossarist.org/org'),
|
|
126
127
|
await buildBibliographyTurtle('test', { x: { reference: 'X' } }, 'https://glossarist.org'),
|
|
127
128
|
await buildVersionHistoryTurtle({ registerId: 't', datasetIri: 'https://glossarist.org/t/', versions: [{ version: '1', generatedAt: '2026-01-01' }] }),
|
|
128
129
|
];
|
|
@@ -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
|
+
});
|
|
@@ -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;
|