@glossarist/concept-browser 0.7.114 → 0.7.116
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 +10 -13
- package/cli/index.ts +9 -11
- package/data/concept-model/shapes/glossarist.concept.yaml.schema.json +255 -0
- package/package.json +18 -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.js +132 -0
- package/scripts/{bridge-to-astro.mjs → bridge-to-astro.ts} +2 -4
- package/scripts/build-edges.js +242 -336
- package/scripts/build-edges.ts +572 -0
- package/scripts/build-scripts.mjs +22 -0
- package/scripts/check-syntax.js +81 -0
- package/scripts/{check-syntax.mjs → check-syntax.ts} +20 -13
- package/scripts/doctor.js +278 -0
- package/scripts/extract-source-refs.js +16 -24
- package/scripts/extract-source-refs.ts +32 -0
- package/scripts/fetch-datasets.js +278 -0
- package/scripts/{fetch-datasets.mjs → fetch-datasets.ts} +2 -2
- package/scripts/generate-404.js +10 -11
- package/scripts/generate-404.ts +15 -0
- package/scripts/generate-data.js +2220 -0
- package/scripts/{generate-data.mjs → generate-data.ts} +73 -460
- package/scripts/generate-ontology-data.js +139 -0
- package/scripts/generate-ontology-schema.js +472 -0
- 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/load-site-config.js +83 -0
- package/scripts/migrate-v1-to-v3.js +113 -0
- package/scripts/normalize-yaml.js +96 -0
- package/scripts/process-about-pages.js +241 -0
- package/scripts/{process-about-pages.mjs → process-about-pages.ts} +1 -1
- package/scripts/smoke.js +211 -0
- package/scripts/{smoke.mjs → smoke.ts} +4 -3
- package/scripts/sync-concept-model.js +63 -0
- package/scripts/validate-shacl.js +152 -0
- package/scripts/{validate-shacl.mjs → validate-shacl.ts} +5 -5
- package/scripts/validate-yaml.js +132 -0
- 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__/published-package-contract.test.ts +84 -0
- 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 +4 -4
- 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/data/ontology-schema.json +1336 -183
- 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/{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,152 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// scripts/validate-shacl.ts
|
|
4
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
5
|
+
import { join, extname, dirname, resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { Parser as N3Parser } from "n3";
|
|
8
|
+
import rdfDataset from "@rdfjs/dataset";
|
|
9
|
+
import { validateShacl } from "glossarist/rdf/shacl";
|
|
10
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
var VENDORED_SHAPES = resolve(__dirname, "..", "data", "concept-model", "shapes", "glossarist.shacl.ttl");
|
|
12
|
+
var createDataset = rdfDataset.dataset.bind(rdfDataset);
|
|
13
|
+
function parseArgs(argv) {
|
|
14
|
+
const out = { dataRoot: "public/data", shapesPath: null };
|
|
15
|
+
for (let i = 2; i < argv.length; i++) {
|
|
16
|
+
const arg = argv[i];
|
|
17
|
+
if (arg === "--shapes") {
|
|
18
|
+
out.shapesPath = argv[++i];
|
|
19
|
+
if (!out.shapesPath) throw new Error("--shapes requires a path argument");
|
|
20
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
21
|
+
console.log(USAGE);
|
|
22
|
+
process.exit(0);
|
|
23
|
+
} else {
|
|
24
|
+
out.dataRoot = arg;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (process.env.SHAPES_PATH && !out.shapesPath) {
|
|
28
|
+
out.shapesPath = process.env.SHAPES_PATH;
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
var USAGE = `Usage: validate-shacl.mjs [options] [data-root]
|
|
33
|
+
|
|
34
|
+
Options:
|
|
35
|
+
--shapes <path> Path to a SHACL shapes .ttl file (overrides the default
|
|
36
|
+
vendored shapes under data/concept-model/shapes/).
|
|
37
|
+
--help, -h Show this help.
|
|
38
|
+
|
|
39
|
+
Environment:
|
|
40
|
+
SHAPES_PATH Same as --shapes. Lower priority than the CLI flag.
|
|
41
|
+
|
|
42
|
+
Default data-root is public/data.
|
|
43
|
+
`;
|
|
44
|
+
function resolveShapesPath(cliPath) {
|
|
45
|
+
if (cliPath) return cliPath;
|
|
46
|
+
if (!statSync(VENDORED_SHAPES, { throwIfNoEntry: false })) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Vendored SHACL shapes not found at ${VENDORED_SHAPES}.
|
|
49
|
+
Run \`npm run sync:model\` to fetch them from glossarist/concept-model, or pass --shapes <path>, or set SHAPES_PATH=<path>.`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return VENDORED_SHAPES;
|
|
53
|
+
}
|
|
54
|
+
function parseTurtle(text, baseIri) {
|
|
55
|
+
const parser = new N3Parser({ baseIRI: baseIri });
|
|
56
|
+
const out = createDataset();
|
|
57
|
+
return new Promise((resolve2, reject) => {
|
|
58
|
+
parser.parse(text, (err, quad) => {
|
|
59
|
+
if (err) reject(err);
|
|
60
|
+
else if (quad) out.add(quad);
|
|
61
|
+
else resolve2(out);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
function* walkTtl(dir) {
|
|
66
|
+
for (const entry of readdirSync(dir)) {
|
|
67
|
+
const full = join(dir, entry);
|
|
68
|
+
if (statSync(full).isDirectory()) yield* walkTtl(full);
|
|
69
|
+
else if (extname(full) === ".ttl") yield full;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function formatViolation(v) {
|
|
73
|
+
const focus = v.focusNode?.value ?? "(unknown)";
|
|
74
|
+
const shape = v.shape?.value ?? "(unknown)";
|
|
75
|
+
const path = v.path?.value ?? "";
|
|
76
|
+
const message = v.message && v.message.length > 0 ? v.message.map((m) => m.value).join("; ") : "(no message)";
|
|
77
|
+
const pathPart = path ? `
|
|
78
|
+
path: ${path}` : "";
|
|
79
|
+
return ` shape: ${shape}${pathPart}
|
|
80
|
+
node: ${focus}
|
|
81
|
+
message: ${message}`;
|
|
82
|
+
}
|
|
83
|
+
async function main() {
|
|
84
|
+
const args = parseArgs(process.argv);
|
|
85
|
+
const shapesPath = resolveShapesPath(args.shapesPath);
|
|
86
|
+
let shapesDataset;
|
|
87
|
+
try {
|
|
88
|
+
const shapesText = readFileSync(shapesPath, "utf8");
|
|
89
|
+
shapesDataset = await parseTurtle(shapesText, `file://${shapesPath}`);
|
|
90
|
+
} catch (e) {
|
|
91
|
+
console.error(`Failed to load SHACL shapes from ${shapesPath}: ${e.message}`);
|
|
92
|
+
process.exit(2);
|
|
93
|
+
}
|
|
94
|
+
let statDir;
|
|
95
|
+
try {
|
|
96
|
+
statDir = statSync(args.dataRoot);
|
|
97
|
+
} catch (e) {
|
|
98
|
+
console.error(`Data root not found: ${args.dataRoot}`);
|
|
99
|
+
process.exit(2);
|
|
100
|
+
}
|
|
101
|
+
if (!statDir.isDirectory()) {
|
|
102
|
+
console.error(`Data root is not a directory: ${args.dataRoot}`);
|
|
103
|
+
process.exit(2);
|
|
104
|
+
}
|
|
105
|
+
const files = [...walkTtl(args.dataRoot)];
|
|
106
|
+
if (files.length === 0) {
|
|
107
|
+
console.log(`No .ttl files under ${args.dataRoot} \u2014 SHACL gate skipped.`);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const violations = [];
|
|
111
|
+
for (const path of files) {
|
|
112
|
+
let graph;
|
|
113
|
+
try {
|
|
114
|
+
const text = readFileSync(path, "utf8");
|
|
115
|
+
graph = await parseTurtle(text, `file://${path}`);
|
|
116
|
+
} catch (e) {
|
|
117
|
+
violations.push({ path, parseError: e.message });
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const report = await validateShacl(graph, { shapes: shapesDataset });
|
|
121
|
+
if (!report.conforms) {
|
|
122
|
+
for (const v of report.results) {
|
|
123
|
+
violations.push({ path, result: v });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (violations.length === 0) {
|
|
128
|
+
console.log(`SHACL validation passed \u2014 ${files.length} file(s) conform.`);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
console.error(`SHACL validation FAILED \u2014 ${violations.length} violation(s) in ${files.length} file(s):
|
|
132
|
+
`);
|
|
133
|
+
let currentPath = null;
|
|
134
|
+
for (const v of violations) {
|
|
135
|
+
if (v.path !== currentPath) {
|
|
136
|
+
currentPath = v.path;
|
|
137
|
+
console.error(`
|
|
138
|
+
${v.path}`);
|
|
139
|
+
}
|
|
140
|
+
if (v.parseError) {
|
|
141
|
+
console.error(` PARSE ERROR: ${v.parseError}`);
|
|
142
|
+
} else {
|
|
143
|
+
console.error(formatViolation(v.result));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
console.error("");
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
main().catch((e) => {
|
|
150
|
+
console.error(`validate-shacl: unexpected error: ${e.stack ?? e}`);
|
|
151
|
+
process.exit(2);
|
|
152
|
+
});
|
|
@@ -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,132 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// scripts/validate-yaml.ts
|
|
4
|
+
import { readFileSync, readdirSync, statSync, existsSync } from "node:fs";
|
|
5
|
+
import { join, extname, resolve } from "node:path";
|
|
6
|
+
import { cwd } from "node:process";
|
|
7
|
+
import yaml from "js-yaml";
|
|
8
|
+
var SCHEMA_PATH = join(cwd(), "data", "concept-model", "shapes", "glossarist.concept.yaml.schema.json");
|
|
9
|
+
function loadSchema() {
|
|
10
|
+
if (!existsSync(SCHEMA_PATH)) {
|
|
11
|
+
throw new Error(`Schema not found: ${SCHEMA_PATH}`);
|
|
12
|
+
}
|
|
13
|
+
return JSON.parse(readFileSync(SCHEMA_PATH, "utf8"));
|
|
14
|
+
}
|
|
15
|
+
function validateValue(value, schema, path, rootSchema, errors) {
|
|
16
|
+
if (schema.$ref) {
|
|
17
|
+
const ref = schema.$ref.replace(/^#\/\$defs\//, "");
|
|
18
|
+
validateValue(value, rootSchema.$defs?.[ref] ?? {}, path, rootSchema, errors);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (schema.type === "array") {
|
|
22
|
+
if (!Array.isArray(value)) {
|
|
23
|
+
errors.push({ path, message: `Expected array, got ${typeof value}` });
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
for (let i = 0; i < value.length; i++) {
|
|
27
|
+
validateValue(value[i], schema.items ?? {}, `${path}[${i}]`, rootSchema, errors);
|
|
28
|
+
}
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (schema.type && typeof value !== schema.type) {
|
|
32
|
+
errors.push({ path, message: `Expected ${schema.type}, got ${typeof value}` });
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (schema.enum && !schema.enum.includes(value)) {
|
|
36
|
+
errors.push({ path, message: `"${value}" is not one of [${schema.enum.join(", ")}]` });
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (schema.type === "object" && schema.properties) {
|
|
40
|
+
for (const [key, subSchema] of Object.entries(schema.properties)) {
|
|
41
|
+
if (value[key] !== void 0) {
|
|
42
|
+
validateValue(value[key], subSchema, `${path}.${key}`, rootSchema, errors);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (schema.required) {
|
|
46
|
+
for (const req of schema.required) {
|
|
47
|
+
if (!(req in value)) {
|
|
48
|
+
errors.push({ path: `${path}.${req}`, message: `Missing required field: ${req}` });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (schema.patternProperties) {
|
|
53
|
+
for (const [pattern, subSchema] of Object.entries(schema.patternProperties)) {
|
|
54
|
+
const re = new RegExp(pattern);
|
|
55
|
+
for (const key of Object.keys(value)) {
|
|
56
|
+
if (re.test(key) && !(key in (schema.properties ?? {}))) {
|
|
57
|
+
validateValue(value[key], subSchema, `${path}.${key}`, rootSchema, errors);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (schema.type === "array" && Array.isArray(value)) {
|
|
64
|
+
for (let i = 0; i < value.length; i++) {
|
|
65
|
+
validateValue(value[i], schema.items ?? {}, `${path}[${i}]`, rootSchema, errors);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function validateYaml(yamlContent, schema) {
|
|
70
|
+
const data = yaml.load(yamlContent);
|
|
71
|
+
const errors = [];
|
|
72
|
+
if (Array.isArray(data)) {
|
|
73
|
+
for (let i = 0; i < data.length; i++) {
|
|
74
|
+
validateValue(data[i], schema, `doc[${i}]`, schema, errors);
|
|
75
|
+
}
|
|
76
|
+
} else if (data && typeof data === "object") {
|
|
77
|
+
validateValue(data, schema, "root", schema, errors);
|
|
78
|
+
} else {
|
|
79
|
+
errors.push({ path: "root", message: "YAML must be an object or array of objects" });
|
|
80
|
+
}
|
|
81
|
+
return { valid: errors.length === 0, errors };
|
|
82
|
+
}
|
|
83
|
+
function walkYaml(dir) {
|
|
84
|
+
const out = [];
|
|
85
|
+
for (const entry of readdirSync(dir)) {
|
|
86
|
+
const full = join(dir, entry);
|
|
87
|
+
const st = statSync(full);
|
|
88
|
+
if (st.isDirectory()) out.push(...walkYaml(full));
|
|
89
|
+
else if ([".yaml", ".yml"].includes(extname(entry))) out.push(full);
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
function main() {
|
|
94
|
+
const target = process.argv[2];
|
|
95
|
+
if (!target) {
|
|
96
|
+
console.error("Usage: validate-yaml <path-to-yaml-or-dir>");
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
const schema = loadSchema();
|
|
100
|
+
const resolvedTarget = resolve(target);
|
|
101
|
+
if (!existsSync(resolvedTarget)) {
|
|
102
|
+
console.error(`Path not found: ${resolvedTarget}`);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
const files = statSync(resolvedTarget).isDirectory() ? walkYaml(resolvedTarget) : [resolvedTarget];
|
|
106
|
+
let totalValid = 0;
|
|
107
|
+
let totalErrors = 0;
|
|
108
|
+
for (const file of files) {
|
|
109
|
+
const content = readFileSync(file, "utf8");
|
|
110
|
+
const result = validateYaml(content, schema);
|
|
111
|
+
if (result.valid) {
|
|
112
|
+
totalValid++;
|
|
113
|
+
} else {
|
|
114
|
+
totalErrors += result.errors.length;
|
|
115
|
+
console.error(`FAIL: ${file}`);
|
|
116
|
+
for (const err of result.errors) console.error(` ${err.path}: ${err.message}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
console.log(`
|
|
120
|
+
${totalValid}/${files.length} files valid, ${totalErrors} errors`);
|
|
121
|
+
process.exit(totalErrors > 0 ? 1 : 0);
|
|
122
|
+
}
|
|
123
|
+
var isDirect = process.argv[1] && resolve(process.argv[1]) === fileURLToPathPath();
|
|
124
|
+
function fileURLToPathPath() {
|
|
125
|
+
return resolve(new URL(import.meta.url).pathname);
|
|
126
|
+
}
|
|
127
|
+
if (isDirect) main();
|
|
128
|
+
export {
|
|
129
|
+
loadSchema,
|
|
130
|
+
validateValue,
|
|
131
|
+
validateYaml
|
|
132
|
+
};
|
|
@@ -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;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { resolve, join, dirname } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const ROOT = resolve(__dirname, '..', '..');
|
|
9
|
+
const SCRIPTS_DIR = join(ROOT, 'scripts');
|
|
10
|
+
|
|
11
|
+
const LIBRARY_SCRIPTS = [
|
|
12
|
+
'load-site-config',
|
|
13
|
+
'validate-yaml',
|
|
14
|
+
'normalize-yaml',
|
|
15
|
+
'extract-source-refs',
|
|
16
|
+
'migrate-v1-to-v3',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const PIPELINE_SCRIPTS = [
|
|
20
|
+
'fetch-datasets',
|
|
21
|
+
'generate-data',
|
|
22
|
+
'build-edges',
|
|
23
|
+
'process-about-pages',
|
|
24
|
+
'smoke',
|
|
25
|
+
'generate-ontology-data',
|
|
26
|
+
'generate-ontology-schema',
|
|
27
|
+
'doctor',
|
|
28
|
+
'validate-shacl',
|
|
29
|
+
'bridge-to-astro',
|
|
30
|
+
'sync-concept-model',
|
|
31
|
+
'generate-404',
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
function runPlain(args: string[], opts?: Parameters<typeof spawnSync>[2]): { status: number | null; stdout: string; stderr: string } {
|
|
35
|
+
const result = spawnSync('node', args, { encoding: 'utf8', cwd: ROOT, ...opts });
|
|
36
|
+
return {
|
|
37
|
+
status: result.status,
|
|
38
|
+
stdout: typeof result.stdout === 'string' ? result.stdout : result.stdout?.toString('utf8') ?? '',
|
|
39
|
+
stderr: typeof result.stderr === 'string' ? result.stderr : result.stderr?.toString('utf8') ?? '',
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe('published-package contract — compiled .js scripts load under plain node (no tsx)', () => {
|
|
44
|
+
it('npm run build:scripts produces .js for every script', () => {
|
|
45
|
+
const build = spawnSync('node', [join(ROOT, 'scripts', 'build-scripts.mjs')], {
|
|
46
|
+
encoding: 'utf8',
|
|
47
|
+
cwd: ROOT,
|
|
48
|
+
});
|
|
49
|
+
expect(build.status).toBe(0);
|
|
50
|
+
|
|
51
|
+
for (const name of [...LIBRARY_SCRIPTS, ...PIPELINE_SCRIPTS]) {
|
|
52
|
+
const jsPath = join(SCRIPTS_DIR, `${name}.js`);
|
|
53
|
+
expect(existsSync(jsPath)).toBe(true);
|
|
54
|
+
}
|
|
55
|
+
}, 30000);
|
|
56
|
+
|
|
57
|
+
it('library scripts export functions under plain node (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING regression)', () => {
|
|
58
|
+
for (const name of LIBRARY_SCRIPTS) {
|
|
59
|
+
const jsPath = `./scripts/${name}.js`;
|
|
60
|
+
const result = runPlain(['--input-type=module', '-e', `import(${JSON.stringify(jsPath)}).then(m => console.log('OK', Object.keys(m).length)).catch(e => { console.error('FAIL', e.message); process.exit(1); })`]);
|
|
61
|
+
expect(result.stderr).not.toContain('ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING');
|
|
62
|
+
expect(result.status).toBe(0);
|
|
63
|
+
expect(result.stdout).toContain('OK');
|
|
64
|
+
}
|
|
65
|
+
}, 60000);
|
|
66
|
+
|
|
67
|
+
it('pipeline scripts do not throw ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING (may fail on missing config, that is OK)', () => {
|
|
68
|
+
for (const name of PIPELINE_SCRIPTS) {
|
|
69
|
+
const result = runPlain(['--input-type=module', '-e', `import(${JSON.stringify(`./scripts/${name}.js`)}).then(() => console.log('OK')).catch(e => { console.error(e.message); })`]);
|
|
70
|
+
expect(result.stderr).not.toContain('ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING');
|
|
71
|
+
}
|
|
72
|
+
}, 60000);
|
|
73
|
+
|
|
74
|
+
it('validate-yaml.js exports validateYaml function', () => {
|
|
75
|
+
const result = runPlain(['--input-type=module', '-e', `
|
|
76
|
+
import { validateYaml } from './scripts/validate-yaml.js';
|
|
77
|
+
const schema = { type: 'object', required: ['termid'], properties: { termid: { type: 'string' } } };
|
|
78
|
+
const r = validateYaml('termid: test', schema);
|
|
79
|
+
console.log('valid=' + r.valid);
|
|
80
|
+
`]);
|
|
81
|
+
expect(result.status).toBe(0);
|
|
82
|
+
expect(result.stdout).toContain('valid=true');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -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
|
|