@glossarist/concept-browser 0.7.114 → 0.7.115
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/README.md +45 -2
- package/cli/index.mjs +15 -15
- package/cli/index.ts +9 -11
- package/data/concept-model/shapes/glossarist.concept.yaml.schema.json +255 -0
- package/package.json +14 -12
- package/scripts/__tests__/{bibliography.test.mjs → bibliography.test.ts} +1 -1
- package/scripts/__tests__/{cli-script-contract.test.mjs → cli-script-contract.test.ts} +3 -3
- package/scripts/__tests__/{concept-groups.test.mjs → concept-groups.test.ts} +1 -1
- package/scripts/__tests__/{doctor.test.mjs → doctor.test.ts} +1 -1
- package/scripts/__tests__/{fetch-datasets.test.mjs → fetch-datasets.test.ts} +1 -1
- package/scripts/__tests__/{first-non-empty.test.mjs → first-non-empty.test.ts} +1 -1
- package/scripts/__tests__/{process-about-pages.test.mjs → process-about-pages.test.ts} +1 -1
- package/scripts/{bridge-to-astro.mjs → bridge-to-astro.ts} +2 -4
- package/scripts/{build-edges.js → build-edges.ts} +11 -13
- package/scripts/{check-syntax.mjs → check-syntax.ts} +20 -13
- package/scripts/{fetch-datasets.mjs → fetch-datasets.ts} +2 -2
- package/scripts/{generate-data.mjs → generate-data.ts} +73 -460
- package/scripts/{generate-ontology-schema.mjs → generate-ontology-schema.ts} +2 -2
- package/scripts/lib/{build-cache.mjs → build-cache.ts} +3 -2
- package/scripts/lib/concept-formats.ts +206 -0
- package/scripts/lib/concept-uri.ts +14 -0
- package/scripts/lib/{dataset-turtle.mjs → dataset-turtle.ts} +4 -3
- package/scripts/lib/harmonize-concept.ts +86 -0
- package/scripts/lib/jsonld-emitter.ts +169 -0
- package/scripts/lib/{local-path-safety.mjs → local-path-safety.ts} +6 -2
- package/scripts/lib/yaml-types.ts +262 -0
- package/scripts/{process-about-pages.mjs → process-about-pages.ts} +1 -1
- package/scripts/{smoke.mjs → smoke.ts} +4 -3
- package/scripts/{validate-shacl.mjs → validate-shacl.ts} +5 -5
- package/scripts/validate-yaml.ts +144 -0
- package/src/__tests__/extract-source-refs.test.ts +2 -3
- package/src/__tests__/perf/serialization-perf.test.ts +5 -5
- package/src/__tests__/scripts/agents-turtle.test.ts +1 -1
- package/src/__tests__/scripts/bibliography-turtle.test.ts +1 -1
- package/src/__tests__/scripts/build-activity-turtle.test.ts +1 -1
- package/src/__tests__/scripts/build-cache.test.ts +1 -1
- package/src/__tests__/scripts/build-integration.test.ts +19 -19
- package/src/__tests__/scripts/dataset-turtle.test.ts +1 -1
- package/src/__tests__/scripts/migrate-v1-to-v3.test.ts +1 -1
- package/src/__tests__/scripts/normalize-yaml.test.ts +1 -1
- package/src/__tests__/scripts/turtle-escape.test.ts +1 -1
- package/src/__tests__/scripts/validate-yaml.test.ts +76 -0
- package/src/__tests__/scripts/version-turtle.test.ts +1 -1
- package/src/__tests__/scripts/vocab-turtle.test.ts +1 -1
- package/src/__tests__/scripts-syntax.test.ts +2 -2
- package/src/__tests__/use-rdf-document.test.ts +2 -3
- package/src/__tests__/utils/content-renderer-xss.test.ts +57 -0
- package/src/__tests__/validate-shacl.test.ts +3 -3
- package/src/adapters/wire/index.ts +69 -0
- package/src/utils/content-renderer.ts +7 -3
- package/src/utils/url-safety.ts +1 -5
- package/tsconfig.json +2 -2
- package/vite.config.ts +1 -1
- package/src/__tests__/composables/use-concept-edges-v2.test.ts +0 -167
- package/src/__tests__/composables/use-concept-edges-v2.test.ts.bak +0 -166
- package/src/types/agents-version-turtle.d.ts +0 -27
- package/src/types/bibliography-turtle.d.ts +0 -12
- package/src/types/build-activity-turtle.d.ts +0 -16
- package/src/types/build-cache.d.ts +0 -20
- package/src/types/dataset-turtle.d.ts +0 -32
- package/src/types/normalize-yaml.d.ts +0 -16
- package/src/types/turtle-escape.d.ts +0 -6
- package/src/types/vocab-turtle.d.ts +0 -13
- /package/scripts/{doctor.mjs → doctor.ts} +0 -0
- /package/scripts/{extract-source-refs.js → extract-source-refs.ts} +0 -0
- /package/scripts/{generate-404.js → generate-404.ts} +0 -0
- /package/scripts/{generate-ontology-data.mjs → generate-ontology-data.ts} +0 -0
- /package/scripts/lib/{agents-turtle.mjs → agents-turtle.ts} +0 -0
- /package/scripts/lib/{bibliography-turtle.mjs → bibliography-turtle.ts} +0 -0
- /package/scripts/lib/{bibliography.mjs → bibliography.ts} +0 -0
- /package/scripts/lib/build/{image-assets.mjs → image-assets.ts} +0 -0
- /package/scripts/lib/build/{non-verbal-consumer.mjs → non-verbal-consumer.ts} +0 -0
- /package/scripts/lib/{build-activity-turtle.mjs → build-activity-turtle.ts} +0 -0
- /package/scripts/lib/{concept-groups.mjs → concept-groups.ts} +0 -0
- /package/scripts/lib/{first-non-empty.mjs → first-non-empty.ts} +0 -0
- /package/scripts/lib/{turtle-escape.mjs → turtle-escape.ts} +0 -0
- /package/scripts/lib/{version-turtle.mjs → version-turtle.ts} +0 -0
- /package/scripts/lib/{vocab-turtle.mjs → vocab-turtle.ts} +0 -0
- /package/scripts/{load-site-config.mjs → load-site-config.ts} +0 -0
- /package/scripts/{migrate-v1-to-v3.mjs → migrate-v1-to-v3.ts} +0 -0
- /package/scripts/{normalize-yaml.mjs → normalize-yaml.ts} +0 -0
- /package/scripts/{sync-concept-model.mjs → sync-concept-model.ts} +0 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YAML author-format TypeScript interfaces.
|
|
3
|
+
*
|
|
4
|
+
* These types describe the YAML wire format that dataset authors write
|
|
5
|
+
* and that `scripts/generate-data.ts` reads. They are the author-facing
|
|
6
|
+
* contract — the complement to `src/adapters/jsonld-types.ts` (the
|
|
7
|
+
* browser-facing JSON-LD wire format).
|
|
8
|
+
*
|
|
9
|
+
* MECE boundary:
|
|
10
|
+
* - yaml-types.ts → INPUT to the build pipeline (what authors write)
|
|
11
|
+
* - jsonld-types.ts → OUTPUT of the build pipeline (what the browser reads)
|
|
12
|
+
* - model-bridge.ts → converts JSON-LD → glossarist-js Concept instances
|
|
13
|
+
*
|
|
14
|
+
* Model-driven: field names match the YAML keys authors actually write
|
|
15
|
+
* (snake_case for author-facing, camelCase for internal normalized form).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// ── Content blocks ─────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
export interface YamlContent {
|
|
21
|
+
content?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ── Terms / designations ──────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
export interface YamlTerm {
|
|
27
|
+
type?: string;
|
|
28
|
+
designation?: string;
|
|
29
|
+
normative_status?: string;
|
|
30
|
+
origin?: YamlOrigin;
|
|
31
|
+
grammar_info?: YamlGrammarInfo[];
|
|
32
|
+
english?: string[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface YamlOrigin {
|
|
36
|
+
source?: string;
|
|
37
|
+
ref?: string;
|
|
38
|
+
version?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface YamlGrammarInfo {
|
|
42
|
+
gender?: string;
|
|
43
|
+
number?: string;
|
|
44
|
+
partOfSpeech?: string;
|
|
45
|
+
noun?: boolean;
|
|
46
|
+
verb?: boolean;
|
|
47
|
+
adj?: boolean;
|
|
48
|
+
adverb?: boolean;
|
|
49
|
+
preposition?: boolean;
|
|
50
|
+
participle?: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── Sources ───────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
export interface YamlSource {
|
|
56
|
+
origin?: YamlOrigin;
|
|
57
|
+
ref?: string;
|
|
58
|
+
link?: string;
|
|
59
|
+
relationship_type?: string;
|
|
60
|
+
status?: string;
|
|
61
|
+
title?: string;
|
|
62
|
+
type?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── Dates ─────────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
export interface YamlDate {
|
|
68
|
+
type?: string;
|
|
69
|
+
date?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── Relations ─────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
export interface YamlRelation {
|
|
75
|
+
type?: string;
|
|
76
|
+
target?: string | { ref?: string; uri?: string };
|
|
77
|
+
content?: string;
|
|
78
|
+
label?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface YamlPartitiveMember {
|
|
82
|
+
ref?: string;
|
|
83
|
+
uri?: string;
|
|
84
|
+
presence?: string;
|
|
85
|
+
count?: string;
|
|
86
|
+
delimiting?: boolean;
|
|
87
|
+
multiplicity?: string;
|
|
88
|
+
certainty?: string;
|
|
89
|
+
designation?: string;
|
|
90
|
+
content?: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface YamlPartitiveRelation {
|
|
94
|
+
type?: string;
|
|
95
|
+
target?: string;
|
|
96
|
+
members?: YamlPartitiveMember[];
|
|
97
|
+
criterion?: string;
|
|
98
|
+
multiplicity?: string;
|
|
99
|
+
certainty?: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface YamlGenericRelation {
|
|
103
|
+
type?: string;
|
|
104
|
+
target?: string;
|
|
105
|
+
members?: (string | { ref?: string; uri?: string })[];
|
|
106
|
+
criterion?: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── Localization (per-language concept data) ──────────────────────────────
|
|
110
|
+
|
|
111
|
+
export interface YamlLocalization {
|
|
112
|
+
language_code?: string;
|
|
113
|
+
terms?: YamlTerm[];
|
|
114
|
+
definition?: YamlContent[];
|
|
115
|
+
notes?: YamlContent[];
|
|
116
|
+
examples?: YamlContent[];
|
|
117
|
+
annotations?: YamlContent[];
|
|
118
|
+
sources?: YamlSource[];
|
|
119
|
+
dates?: YamlDate[];
|
|
120
|
+
domain?: string[];
|
|
121
|
+
references?: YamlSource[];
|
|
122
|
+
entry_status?: string;
|
|
123
|
+
classification?: string;
|
|
124
|
+
review_type?: string;
|
|
125
|
+
review_date?: string;
|
|
126
|
+
review_decision_date?: string;
|
|
127
|
+
review_decision_event?: string;
|
|
128
|
+
review_status?: string;
|
|
129
|
+
review_decision?: string;
|
|
130
|
+
review_decision_notes?: string;
|
|
131
|
+
lineage_source_similarity?: number;
|
|
132
|
+
release?: string;
|
|
133
|
+
script?: string;
|
|
134
|
+
system?: string;
|
|
135
|
+
[key: string]: unknown;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── Harmonized concept (output of loadConceptFile) ────────────────────────
|
|
139
|
+
//
|
|
140
|
+
// After loadConceptFile(), the concept is normalized to this shape
|
|
141
|
+
// regardless of whether the source was simple or managed format.
|
|
142
|
+
// The `_`-prefixed fields are managed-concept metadata hoisted to top level.
|
|
143
|
+
|
|
144
|
+
export interface HarmonizedConcept {
|
|
145
|
+
termid: string;
|
|
146
|
+
id?: string;
|
|
147
|
+
uri?: string;
|
|
148
|
+
status?: string;
|
|
149
|
+
_related?: YamlRelation[];
|
|
150
|
+
_partitiveRelations?: YamlPartitiveRelation[];
|
|
151
|
+
_genericRelations?: YamlGenericRelation[];
|
|
152
|
+
_domains?: string[];
|
|
153
|
+
_dates?: YamlDate[];
|
|
154
|
+
_sources?: YamlSource[];
|
|
155
|
+
_status?: string;
|
|
156
|
+
_schemaVersion?: string;
|
|
157
|
+
_dateAccepted?: string;
|
|
158
|
+
[lang: string]: unknown;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ── Managed concept YAML doc (multi-doc format) ───────────────────────────
|
|
162
|
+
|
|
163
|
+
export interface YamlManagedConceptDoc {
|
|
164
|
+
data?: {
|
|
165
|
+
identifier?: string | number;
|
|
166
|
+
language_code?: string;
|
|
167
|
+
domains?: string[];
|
|
168
|
+
[key: string]: unknown;
|
|
169
|
+
};
|
|
170
|
+
related?: YamlRelation[];
|
|
171
|
+
partitive_relations?: YamlPartitiveRelation[];
|
|
172
|
+
generic_relations?: YamlGenericRelation[];
|
|
173
|
+
dates?: YamlDate[];
|
|
174
|
+
sources?: YamlSource[];
|
|
175
|
+
status?: string;
|
|
176
|
+
schema_version?: string;
|
|
177
|
+
date_accepted?: string;
|
|
178
|
+
terms?: YamlTerm[];
|
|
179
|
+
definition?: YamlContent[];
|
|
180
|
+
notes?: YamlContent[];
|
|
181
|
+
examples?: YamlContent[];
|
|
182
|
+
[key: string]: unknown;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── Dataset manifest ──────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
export interface YamlManifestSection {
|
|
188
|
+
id?: string;
|
|
189
|
+
slug?: string;
|
|
190
|
+
title?: string;
|
|
191
|
+
name?: string;
|
|
192
|
+
members?: string[];
|
|
193
|
+
children?: YamlManifestSection[];
|
|
194
|
+
ordering?: string;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export interface YamlManifest {
|
|
198
|
+
id?: string;
|
|
199
|
+
title?: string;
|
|
200
|
+
description?: string;
|
|
201
|
+
lastUpdated?: string;
|
|
202
|
+
sourceRepoUrl?: string;
|
|
203
|
+
publisher?: string;
|
|
204
|
+
contactPoint?: string;
|
|
205
|
+
sections?: YamlManifestSection[];
|
|
206
|
+
languageOrder?: string[];
|
|
207
|
+
ref?: Record<string, string>;
|
|
208
|
+
refAliases?: string[];
|
|
209
|
+
editionStatus?: string;
|
|
210
|
+
ordering?: string;
|
|
211
|
+
[key: string]: unknown;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ── Bibliography ──────────────────────────────────────────────────────────
|
|
215
|
+
|
|
216
|
+
export interface YamlBibliographyEntry {
|
|
217
|
+
reference?: string;
|
|
218
|
+
title?: string;
|
|
219
|
+
link?: string;
|
|
220
|
+
author?: string;
|
|
221
|
+
date?: string;
|
|
222
|
+
publisher?: string;
|
|
223
|
+
type?: string;
|
|
224
|
+
[key: string]: unknown;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ── News/content frontmatter ──────────────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
export interface YamlNewsFrontmatter {
|
|
230
|
+
title?: string;
|
|
231
|
+
date?: string;
|
|
232
|
+
categories?: string;
|
|
233
|
+
[key: string]: unknown;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ── Content page config ───────────────────────────────────────────────────
|
|
237
|
+
|
|
238
|
+
export interface YamlContentPage {
|
|
239
|
+
route?: string;
|
|
240
|
+
source?: string;
|
|
241
|
+
title?: string;
|
|
242
|
+
translations?: Record<string, { source?: string; title?: string }>;
|
|
243
|
+
[key: string]: unknown;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ── SHACL constraint (ontology schema) ────────────────────────────────────
|
|
247
|
+
|
|
248
|
+
export interface ShaclConstraint {
|
|
249
|
+
path?: string | null;
|
|
250
|
+
datatype?: string | null;
|
|
251
|
+
class?: string | null;
|
|
252
|
+
valuesFrom?: string | null;
|
|
253
|
+
nodeKind?: string | null;
|
|
254
|
+
minCount?: number | null;
|
|
255
|
+
maxCount?: number | null;
|
|
256
|
+
in?: string[];
|
|
257
|
+
[key: string]: unknown;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ── Prefix map (Turtle emission) ──────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
export type PrefixMap = Record<string, string>;
|
|
@@ -19,7 +19,7 @@ import { join, basename, extname, resolve } from 'node:path';
|
|
|
19
19
|
import { cwd } from 'node:process';
|
|
20
20
|
import { fileURLToPath } from 'node:url';
|
|
21
21
|
|
|
22
|
-
import { loadSiteConfig } from './load-site-config
|
|
22
|
+
import { loadSiteConfig } from './load-site-config';
|
|
23
23
|
|
|
24
24
|
export function loadSiteConfigForAbout() {
|
|
25
25
|
try {
|
|
@@ -36,7 +36,7 @@ const DIST = pathResolve(__dirname, '..', 'dist');
|
|
|
36
36
|
const MIME = {
|
|
37
37
|
'.html': 'text/html; charset=utf-8',
|
|
38
38
|
'.js': 'application/javascript; charset=utf-8',
|
|
39
|
-
'
|
|
39
|
+
'': 'application/javascript; charset=utf-8',
|
|
40
40
|
'.css': 'text/css; charset=utf-8',
|
|
41
41
|
'.json': 'application/json; charset=utf-8',
|
|
42
42
|
'.svg': 'image/svg+xml',
|
|
@@ -53,7 +53,7 @@ const MIME = {
|
|
|
53
53
|
'.webmanifest': 'application/manifest+json; charset=utf-8',
|
|
54
54
|
};
|
|
55
55
|
|
|
56
|
-
function serveDist(root) {
|
|
56
|
+
function serveDist(root: string): Promise<{ server: ReturnType<typeof createServer>; origin: string }> {
|
|
57
57
|
return new Promise((resolve) => {
|
|
58
58
|
const server = createServer((req, res) => {
|
|
59
59
|
try {
|
|
@@ -91,7 +91,8 @@ function serveDist(root) {
|
|
|
91
91
|
});
|
|
92
92
|
// Use port 0 to let the OS pick a free port; retrieve it after listen.
|
|
93
93
|
server.listen(0, '127.0.0.1', () => {
|
|
94
|
-
const
|
|
94
|
+
const addr = server.address();
|
|
95
|
+
const port = typeof addr === 'object' && addr ? addr.port : 0;
|
|
95
96
|
resolve({ server, origin: `http://127.0.0.1:${port}` });
|
|
96
97
|
});
|
|
97
98
|
});
|
|
@@ -90,7 +90,7 @@ function parseTurtle(text, baseIri) {
|
|
|
90
90
|
});
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
function* walkTtl(dir) {
|
|
93
|
+
function* walkTtl(dir: string): Generator<string> {
|
|
94
94
|
for (const entry of readdirSync(dir)) {
|
|
95
95
|
const full = join(dir, entry);
|
|
96
96
|
if (statSync(full).isDirectory()) yield* walkTtl(full);
|
|
@@ -98,12 +98,12 @@ function* walkTtl(dir) {
|
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
function formatViolation(v) {
|
|
101
|
+
function formatViolation(v: any): string {
|
|
102
102
|
const focus = v.focusNode?.value ?? '(unknown)';
|
|
103
103
|
const shape = v.shape?.value ?? '(unknown)';
|
|
104
104
|
const path = v.path?.value ?? '';
|
|
105
105
|
const message = (v.message && v.message.length > 0)
|
|
106
|
-
? v.message.map(m => m.value).join('; ')
|
|
106
|
+
? v.message.map((m: any) => m.value).join('; ')
|
|
107
107
|
: '(no message)';
|
|
108
108
|
const pathPart = path ? `\n path: ${path}` : '';
|
|
109
109
|
return ` shape: ${shape}${pathPart}\n node: ${focus}\n message: ${message}`;
|
|
@@ -121,7 +121,7 @@ async function main() {
|
|
|
121
121
|
try {
|
|
122
122
|
const shapesText = readFileSync(shapesPath, 'utf8');
|
|
123
123
|
shapesDataset = await parseTurtle(shapesText, `file://${shapesPath}`);
|
|
124
|
-
} catch (e) {
|
|
124
|
+
} catch (e: any) {
|
|
125
125
|
console.error(`Failed to load SHACL shapes from ${shapesPath}: ${e.message}`);
|
|
126
126
|
process.exit(2);
|
|
127
127
|
}
|
|
@@ -150,7 +150,7 @@ async function main() {
|
|
|
150
150
|
try {
|
|
151
151
|
const text = readFileSync(path, 'utf8');
|
|
152
152
|
graph = await parseTurtle(text, `file://${path}`);
|
|
153
|
-
} catch (e) {
|
|
153
|
+
} catch (e: any) {
|
|
154
154
|
violations.push({ path, parseError: e.message });
|
|
155
155
|
continue;
|
|
156
156
|
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* validate-yaml — validate dataset author YAML files against the
|
|
4
|
+
* Glossarist concept JSON Schema.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* npx tsx scripts/validate-yaml.ts <path-to-yaml-or-dir>
|
|
8
|
+
* npx tsx scripts/validate-yaml.ts .datasets/my-vocab/concepts/
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
|
|
12
|
+
import { join, extname, resolve } from 'node:path';
|
|
13
|
+
import { cwd } from 'node:process';
|
|
14
|
+
import yaml from 'js-yaml';
|
|
15
|
+
|
|
16
|
+
const SCHEMA_PATH = join(cwd(), 'data', 'concept-model', 'shapes', 'glossarist.concept.yaml.schema.json');
|
|
17
|
+
|
|
18
|
+
export interface ValidationError { path: string; message: string; }
|
|
19
|
+
export interface ValidationResult { valid: boolean; errors: ValidationError[]; }
|
|
20
|
+
|
|
21
|
+
export function loadSchema(): any {
|
|
22
|
+
if (!existsSync(SCHEMA_PATH)) {
|
|
23
|
+
throw new Error(`Schema not found: ${SCHEMA_PATH}`);
|
|
24
|
+
}
|
|
25
|
+
return JSON.parse(readFileSync(SCHEMA_PATH, 'utf8'));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function validateValue(value: any, schema: any, path: string, rootSchema: any, errors: ValidationError[]): void {
|
|
29
|
+
if (schema.$ref) {
|
|
30
|
+
const ref = schema.$ref.replace(/^#\/\$defs\//, '');
|
|
31
|
+
validateValue(value, rootSchema.$defs?.[ref] ?? {}, path, rootSchema, errors);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (schema.type === 'array') {
|
|
35
|
+
if (!Array.isArray(value)) {
|
|
36
|
+
errors.push({ path, message: `Expected array, got ${typeof value}` });
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
for (let i = 0; i < value.length; i++) {
|
|
40
|
+
validateValue(value[i], schema.items ?? {}, `${path}[${i}]`, rootSchema, errors);
|
|
41
|
+
}
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (schema.type && typeof value !== schema.type) {
|
|
45
|
+
errors.push({ path, message: `Expected ${schema.type}, got ${typeof value}` });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (schema.enum && !schema.enum.includes(value)) {
|
|
49
|
+
errors.push({ path, message: `"${value}" is not one of [${schema.enum.join(', ')}]` });
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (schema.type === 'object' && schema.properties) {
|
|
53
|
+
for (const [key, subSchema] of Object.entries(schema.properties) as [string, any][]) {
|
|
54
|
+
if (value[key] !== undefined) {
|
|
55
|
+
validateValue(value[key], subSchema, `${path}.${key}`, rootSchema, errors);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (schema.required) {
|
|
59
|
+
for (const req of schema.required) {
|
|
60
|
+
if (!(req in value)) {
|
|
61
|
+
errors.push({ path: `${path}.${req}`, message: `Missing required field: ${req}` });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (schema.patternProperties) {
|
|
66
|
+
for (const [pattern, subSchema] of Object.entries(schema.patternProperties) as [string, any][]) {
|
|
67
|
+
const re = new RegExp(pattern);
|
|
68
|
+
for (const key of Object.keys(value)) {
|
|
69
|
+
if (re.test(key) && !(key in (schema.properties ?? {}))) {
|
|
70
|
+
validateValue(value[key], subSchema, `${path}.${key}`, rootSchema, errors);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (schema.type === 'array' && Array.isArray(value)) {
|
|
77
|
+
for (let i = 0; i < value.length; i++) {
|
|
78
|
+
validateValue(value[i], schema.items ?? {}, `${path}[${i}]`, rootSchema, errors);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function validateYaml(yamlContent: string, schema: any): ValidationResult {
|
|
84
|
+
const data = yaml.load(yamlContent);
|
|
85
|
+
const errors: ValidationError[] = [];
|
|
86
|
+
if (Array.isArray(data)) {
|
|
87
|
+
for (let i = 0; i < data.length; i++) {
|
|
88
|
+
validateValue(data[i], schema, `doc[${i}]`, schema, errors);
|
|
89
|
+
}
|
|
90
|
+
} else if (data && typeof data === 'object') {
|
|
91
|
+
validateValue(data, schema, 'root', schema, errors);
|
|
92
|
+
} else {
|
|
93
|
+
errors.push({ path: 'root', message: 'YAML must be an object or array of objects' });
|
|
94
|
+
}
|
|
95
|
+
return { valid: errors.length === 0, errors };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function walkYaml(dir: string): string[] {
|
|
99
|
+
const out: string[] = [];
|
|
100
|
+
for (const entry of readdirSync(dir)) {
|
|
101
|
+
const full = join(dir, entry);
|
|
102
|
+
const st = statSync(full);
|
|
103
|
+
if (st.isDirectory()) out.push(...walkYaml(full));
|
|
104
|
+
else if (['.yaml', '.yml'].includes(extname(entry))) out.push(full);
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function main(): void {
|
|
110
|
+
const target = process.argv[2];
|
|
111
|
+
if (!target) {
|
|
112
|
+
console.error('Usage: validate-yaml <path-to-yaml-or-dir>');
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
const schema = loadSchema();
|
|
116
|
+
const resolvedTarget = resolve(target);
|
|
117
|
+
if (!existsSync(resolvedTarget)) {
|
|
118
|
+
console.error(`Path not found: ${resolvedTarget}`);
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
const files = statSync(resolvedTarget).isDirectory() ? walkYaml(resolvedTarget) : [resolvedTarget];
|
|
122
|
+
let totalValid = 0;
|
|
123
|
+
let totalErrors = 0;
|
|
124
|
+
for (const file of files) {
|
|
125
|
+
const content = readFileSync(file, 'utf8');
|
|
126
|
+
const result = validateYaml(content, schema);
|
|
127
|
+
if (result.valid) {
|
|
128
|
+
totalValid++;
|
|
129
|
+
} else {
|
|
130
|
+
totalErrors += result.errors.length;
|
|
131
|
+
console.error(`FAIL: ${file}`);
|
|
132
|
+
for (const err of result.errors) console.error(` ${err.path}: ${err.message}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
console.log(`\n${totalValid}/${files.length} files valid, ${totalErrors} errors`);
|
|
136
|
+
process.exit(totalErrors > 0 ? 1 : 0);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const isDirect = process.argv[1] && resolve(process.argv[1]) === fileURLToPathPath();
|
|
140
|
+
function fileURLToPathPath(): string {
|
|
141
|
+
return resolve(new URL(import.meta.url).pathname);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (isDirect) main();
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
// @ts-nocheck — ESM .js import without type declarations
|
|
2
1
|
import { describe, it, expect } from 'vitest';
|
|
3
|
-
import { extractSourceRefs } from '../../scripts/extract-source-refs
|
|
2
|
+
import { extractSourceRefs } from '../../scripts/extract-source-refs';
|
|
4
3
|
|
|
5
|
-
function makeConcept(sources: any[], localizedSources?: any) {
|
|
4
|
+
function makeConcept(sources: any[] = [], localizedSources?: any) {
|
|
6
5
|
return {
|
|
7
6
|
'@id': 'https://glossarist.org/ds1/concept/1.1',
|
|
8
7
|
'gl:source': sources || [],
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// @ts-nocheck — TODO.typescript/12: remove after glossarist TS migration
|
|
2
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import type { Quad } from 'n3';
|
|
3
3
|
import { Concept } from 'glossarist';
|
|
4
4
|
import { conceptToQuads, collectQuads, writeTurtleSync, PREFIXES } from 'glossarist/rdf';
|
|
5
5
|
import type { ConceptFixture } from '../__fixtures__/concepts';
|
|
@@ -41,7 +41,7 @@ describe('Layer 7 — serialization performance regression', () => {
|
|
|
41
41
|
|
|
42
42
|
const start = performance.now();
|
|
43
43
|
for (const { concept } of concepts) {
|
|
44
|
-
const quads = collectQuads(conceptToQuads(concept, { registerId: 'perf', uriBase: BASE }));
|
|
44
|
+
const quads = collectQuads(conceptToQuads(concept, { registerId: 'perf', uriBase: BASE }) as Generator<Quad>);
|
|
45
45
|
writeTurtleSync(quads, { prefixes: PREFIXES });
|
|
46
46
|
}
|
|
47
47
|
const elapsed = performance.now() - start;
|
|
@@ -55,7 +55,7 @@ describe('Layer 7 — serialization performance regression', () => {
|
|
|
55
55
|
const concepts = makeConcepts(100);
|
|
56
56
|
const start = performance.now();
|
|
57
57
|
for (const { concept } of concepts) {
|
|
58
|
-
const quads = collectQuads(conceptToQuads(concept, { registerId: 'perf', uriBase: BASE }));
|
|
58
|
+
const quads = collectQuads(conceptToQuads(concept, { registerId: 'perf', uriBase: BASE }) as Generator<Quad>);
|
|
59
59
|
writeTurtleSync(quads, { prefixes: PREFIXES });
|
|
60
60
|
}
|
|
61
61
|
const elapsed = performance.now() - start;
|
|
@@ -69,7 +69,7 @@ describe('Layer 7 — scale stress (P4: 10 000 concepts)', () => {
|
|
|
69
69
|
const concepts = makeConcepts(SCALE_CONCEPT_COUNT);
|
|
70
70
|
const start = performance.now();
|
|
71
71
|
for (const { concept } of concepts) {
|
|
72
|
-
const quads = collectQuads(conceptToQuads(concept, { registerId: 'perf', uriBase: BASE }));
|
|
72
|
+
const quads = collectQuads(conceptToQuads(concept, { registerId: 'perf', uriBase: BASE }) as Generator<Quad>);
|
|
73
73
|
writeTurtleSync(quads, { prefixes: PREFIXES });
|
|
74
74
|
}
|
|
75
75
|
const elapsed = performance.now() - start;
|
|
@@ -98,7 +98,7 @@ describe('Layer 7 — scale stress (P4: 10 000 concepts)', () => {
|
|
|
98
98
|
function timeTurtle(concepts: { concept: Concept; uri: string }[]): number {
|
|
99
99
|
const start = performance.now();
|
|
100
100
|
for (const { concept } of concepts) {
|
|
101
|
-
const quads = collectQuads(conceptToQuads(concept, { registerId: 'perf', uriBase: BASE }));
|
|
101
|
+
const quads = collectQuads(conceptToQuads(concept, { registerId: 'perf', uriBase: BASE }) as Generator<Quad>);
|
|
102
102
|
writeTurtleSync(quads, { prefixes: PREFIXES });
|
|
103
103
|
}
|
|
104
104
|
return performance.now() - start;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { Parser, Store } from 'n3';
|
|
3
|
-
import { buildAgentsTurtle as _buildAgentsTurtle } from '../../../scripts/lib/agents-turtle
|
|
3
|
+
import { buildAgentsTurtle as _buildAgentsTurtle } from '../../../scripts/lib/agents-turtle';
|
|
4
4
|
|
|
5
5
|
const buildAgentsTurtle = _buildAgentsTurtle as unknown as (...args: any[]) => Promise<string>;
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { Parser, Store } from 'n3';
|
|
3
|
-
import { buildBibliographyTurtle } from '../../../scripts/lib/bibliography-turtle
|
|
3
|
+
import { buildBibliographyTurtle } from '../../../scripts/lib/bibliography-turtle';
|
|
4
4
|
|
|
5
5
|
const FOAF = 'http://xmlns.com/foaf/0.1/';
|
|
6
6
|
const DCTERMS = 'http://purl.org/dc/terms/';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { Parser, Store } from 'n3';
|
|
3
|
-
import { buildActivityTurtle } from '../../../scripts/lib/build-activity-turtle
|
|
3
|
+
import { buildActivityTurtle } from '../../../scripts/lib/build-activity-turtle';
|
|
4
4
|
|
|
5
5
|
function makeInput(overrides: any = {}): any {
|
|
6
6
|
return {
|
|
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
|
2
2
|
import { rmSync, mkdirSync } from 'node:fs';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
-
import { BuildCache } from '../../../scripts/lib/build-cache
|
|
5
|
+
import { BuildCache } from '../../../scripts/lib/build-cache';
|
|
6
6
|
|
|
7
7
|
const SUITE_DIR = join(tmpdir(), `build-cache-${process.pid}-${Date.now()}`);
|
|
8
8
|
|
|
@@ -10,12 +10,12 @@ function parseTurtle(text: string): Store {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
function runInline(code: string): string {
|
|
13
|
-
return execFileSync('node', ['--input-type=module', '-e', code], { encoding: 'utf8' }).toString();
|
|
13
|
+
return execFileSync('node', ['--import', 'tsx', '--input-type=module', '-e', code], { encoding: 'utf8' }).toString();
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
describe('WS F Layer 8 — build pipeline integration', () => {
|
|
17
|
-
it('vocab-turtle.
|
|
18
|
-
const out = runInline(`import { buildVocabularyTurtle } from './scripts/lib/vocab-turtle
|
|
17
|
+
it('vocab-turtle.ts produces parseable Turtle with 7 schemes', () => {
|
|
18
|
+
const out = runInline(`import { buildVocabularyTurtle } from './scripts/lib/vocab-turtle'; console.log(await buildVocabularyTurtle());`);
|
|
19
19
|
const store = parseTurtle(out);
|
|
20
20
|
const schemes = [...store].filter(q =>
|
|
21
21
|
q.predicate.value === 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type' &&
|
|
@@ -24,9 +24,9 @@ describe('WS F Layer 8 — build pipeline integration', () => {
|
|
|
24
24
|
expect(schemes.length).toBe(7);
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
-
it('dataset-turtle.
|
|
27
|
+
it('dataset-turtle.ts produces parseable dcat:Dataset', () => {
|
|
28
28
|
const out = runInline(`
|
|
29
|
-
import { buildDatasetTurtle } from './scripts/lib/dataset-turtle
|
|
29
|
+
import { buildDatasetTurtle } from './scripts/lib/dataset-turtle';
|
|
30
30
|
console.log(await buildDatasetTurtle({
|
|
31
31
|
datasetIri: 'https://glossarist.org/test/',
|
|
32
32
|
registerId: 'test', title: 'Test', modified: '2026-06-28',
|
|
@@ -42,9 +42,9 @@ describe('WS F Layer 8 — build pipeline integration', () => {
|
|
|
42
42
|
expect(types).toContain('http://www.w3.org/2004/02/skos/core#ConceptScheme');
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
-
it('build-activity-turtle.
|
|
45
|
+
it('build-activity-turtle.ts produces parseable prov:Activity', () => {
|
|
46
46
|
const out = runInline(`
|
|
47
|
-
import { buildActivityTurtle } from './scripts/lib/build-activity-turtle
|
|
47
|
+
import { buildActivityTurtle } from './scripts/lib/build-activity-turtle';
|
|
48
48
|
console.log(await buildActivityTurtle({
|
|
49
49
|
baseUri: 'https://glossarist.org',
|
|
50
50
|
runId: 'test', startedAt: '2026-01-01T00:00:00Z', endedAt: '2026-01-01T00:05:00Z',
|
|
@@ -59,9 +59,9 @@ describe('WS F Layer 8 — build pipeline integration', () => {
|
|
|
59
59
|
expect(activities.length).toBe(1);
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
-
it('agents-turtle.
|
|
62
|
+
it('agents-turtle.ts produces parseable foaf:Person', () => {
|
|
63
63
|
const out = runInline(`
|
|
64
|
-
import { buildAgentsTurtle } from './scripts/lib/agents-turtle
|
|
64
|
+
import { buildAgentsTurtle } from './scripts/lib/agents-turtle';
|
|
65
65
|
console.log(await buildAgentsTurtle([{ name: 'Ada Lovelace', role: 'Editor', organization: 'Royal Society' }], 'https://glossarist.org/agent', 'https://glossarist.org/org'));
|
|
66
66
|
`);
|
|
67
67
|
const store = parseTurtle(out);
|
|
@@ -72,9 +72,9 @@ describe('WS F Layer 8 — build pipeline integration', () => {
|
|
|
72
72
|
expect(persons.length).toBe(1);
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
-
it('bibliography-turtle.
|
|
75
|
+
it('bibliography-turtle.ts produces parseable dcterms:BibliographicResource', () => {
|
|
76
76
|
const out = runInline(`
|
|
77
|
-
import { buildBibliographyTurtle } from './scripts/lib/bibliography-turtle
|
|
77
|
+
import { buildBibliographyTurtle } from './scripts/lib/bibliography-turtle';
|
|
78
78
|
console.log(await buildBibliographyTurtle('test', { iso704: { reference: 'ISO 704' } }, 'https://glossarist.org'));
|
|
79
79
|
`);
|
|
80
80
|
const store = parseTurtle(out);
|
|
@@ -85,9 +85,9 @@ describe('WS F Layer 8 — build pipeline integration', () => {
|
|
|
85
85
|
expect(bibs.length).toBe(1);
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
-
it('version-turtle.
|
|
88
|
+
it('version-turtle.ts produces parseable prov:Entity chain', () => {
|
|
89
89
|
const out = runInline(`
|
|
90
|
-
import { buildVersionHistoryTurtle } from './scripts/lib/version-turtle
|
|
90
|
+
import { buildVersionHistoryTurtle } from './scripts/lib/version-turtle';
|
|
91
91
|
console.log(await buildVersionHistoryTurtle({
|
|
92
92
|
registerId: 'test', datasetIri: 'https://glossarist.org/test/',
|
|
93
93
|
versions: [
|
|
@@ -113,12 +113,12 @@ describe('WS F Layer 8 — build pipeline integration', () => {
|
|
|
113
113
|
|
|
114
114
|
it('all six emitters produce parseable output in a single subprocess', () => {
|
|
115
115
|
const out = runInline(`
|
|
116
|
-
import { buildVocabularyTurtle } from './scripts/lib/vocab-turtle
|
|
117
|
-
import { buildDatasetTurtle } from './scripts/lib/dataset-turtle
|
|
118
|
-
import { buildActivityTurtle } from './scripts/lib/build-activity-turtle
|
|
119
|
-
import { buildAgentsTurtle } from './scripts/lib/agents-turtle
|
|
120
|
-
import { buildBibliographyTurtle } from './scripts/lib/bibliography-turtle
|
|
121
|
-
import { buildVersionHistoryTurtle } from './scripts/lib/version-turtle
|
|
116
|
+
import { buildVocabularyTurtle } from './scripts/lib/vocab-turtle';
|
|
117
|
+
import { buildDatasetTurtle } from './scripts/lib/dataset-turtle';
|
|
118
|
+
import { buildActivityTurtle } from './scripts/lib/build-activity-turtle';
|
|
119
|
+
import { buildAgentsTurtle } from './scripts/lib/agents-turtle';
|
|
120
|
+
import { buildBibliographyTurtle } from './scripts/lib/bibliography-turtle';
|
|
121
|
+
import { buildVersionHistoryTurtle } from './scripts/lib/version-turtle';
|
|
122
122
|
const parts = [
|
|
123
123
|
await buildVocabularyTurtle(),
|
|
124
124
|
await buildDatasetTurtle({ datasetIri: 'https://glossarist.org/test/', registerId: 'test', title: 't', modified: '2026-01-01', languages: ['eng'], distributions: [], topConceptUris: [], sections: [] }),
|