@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.
Files changed (103) hide show
  1. package/README.md +45 -2
  2. package/cli/index.mjs +10 -13
  3. package/cli/index.ts +9 -11
  4. package/data/concept-model/shapes/glossarist.concept.yaml.schema.json +255 -0
  5. package/package.json +18 -12
  6. package/scripts/__tests__/{bibliography.test.mjs → bibliography.test.ts} +1 -1
  7. package/scripts/__tests__/{cli-script-contract.test.mjs → cli-script-contract.test.ts} +3 -3
  8. package/scripts/__tests__/{concept-groups.test.mjs → concept-groups.test.ts} +1 -1
  9. package/scripts/__tests__/{doctor.test.mjs → doctor.test.ts} +1 -1
  10. package/scripts/__tests__/{fetch-datasets.test.mjs → fetch-datasets.test.ts} +1 -1
  11. package/scripts/__tests__/{first-non-empty.test.mjs → first-non-empty.test.ts} +1 -1
  12. package/scripts/__tests__/{process-about-pages.test.mjs → process-about-pages.test.ts} +1 -1
  13. package/scripts/bridge-to-astro.js +132 -0
  14. package/scripts/{bridge-to-astro.mjs → bridge-to-astro.ts} +2 -4
  15. package/scripts/build-edges.js +242 -336
  16. package/scripts/build-edges.ts +572 -0
  17. package/scripts/build-scripts.mjs +22 -0
  18. package/scripts/check-syntax.js +81 -0
  19. package/scripts/{check-syntax.mjs → check-syntax.ts} +20 -13
  20. package/scripts/doctor.js +278 -0
  21. package/scripts/extract-source-refs.js +16 -24
  22. package/scripts/extract-source-refs.ts +32 -0
  23. package/scripts/fetch-datasets.js +278 -0
  24. package/scripts/{fetch-datasets.mjs → fetch-datasets.ts} +2 -2
  25. package/scripts/generate-404.js +10 -11
  26. package/scripts/generate-404.ts +15 -0
  27. package/scripts/generate-data.js +2220 -0
  28. package/scripts/{generate-data.mjs → generate-data.ts} +73 -460
  29. package/scripts/generate-ontology-data.js +139 -0
  30. package/scripts/generate-ontology-schema.js +472 -0
  31. package/scripts/{generate-ontology-schema.mjs → generate-ontology-schema.ts} +2 -2
  32. package/scripts/lib/{build-cache.mjs → build-cache.ts} +3 -2
  33. package/scripts/lib/concept-formats.ts +206 -0
  34. package/scripts/lib/concept-uri.ts +14 -0
  35. package/scripts/lib/{dataset-turtle.mjs → dataset-turtle.ts} +4 -3
  36. package/scripts/lib/harmonize-concept.ts +86 -0
  37. package/scripts/lib/jsonld-emitter.ts +169 -0
  38. package/scripts/lib/{local-path-safety.mjs → local-path-safety.ts} +6 -2
  39. package/scripts/lib/yaml-types.ts +262 -0
  40. package/scripts/load-site-config.js +83 -0
  41. package/scripts/migrate-v1-to-v3.js +113 -0
  42. package/scripts/normalize-yaml.js +96 -0
  43. package/scripts/process-about-pages.js +241 -0
  44. package/scripts/{process-about-pages.mjs → process-about-pages.ts} +1 -1
  45. package/scripts/smoke.js +211 -0
  46. package/scripts/{smoke.mjs → smoke.ts} +4 -3
  47. package/scripts/sync-concept-model.js +63 -0
  48. package/scripts/validate-shacl.js +152 -0
  49. package/scripts/{validate-shacl.mjs → validate-shacl.ts} +5 -5
  50. package/scripts/validate-yaml.js +132 -0
  51. package/scripts/validate-yaml.ts +144 -0
  52. package/src/__tests__/extract-source-refs.test.ts +2 -3
  53. package/src/__tests__/perf/serialization-perf.test.ts +5 -5
  54. package/src/__tests__/published-package-contract.test.ts +84 -0
  55. package/src/__tests__/scripts/agents-turtle.test.ts +1 -1
  56. package/src/__tests__/scripts/bibliography-turtle.test.ts +1 -1
  57. package/src/__tests__/scripts/build-activity-turtle.test.ts +1 -1
  58. package/src/__tests__/scripts/build-cache.test.ts +1 -1
  59. package/src/__tests__/scripts/build-integration.test.ts +19 -19
  60. package/src/__tests__/scripts/dataset-turtle.test.ts +1 -1
  61. package/src/__tests__/scripts/migrate-v1-to-v3.test.ts +1 -1
  62. package/src/__tests__/scripts/normalize-yaml.test.ts +1 -1
  63. package/src/__tests__/scripts/turtle-escape.test.ts +1 -1
  64. package/src/__tests__/scripts/validate-yaml.test.ts +76 -0
  65. package/src/__tests__/scripts/version-turtle.test.ts +1 -1
  66. package/src/__tests__/scripts/vocab-turtle.test.ts +1 -1
  67. package/src/__tests__/scripts-syntax.test.ts +4 -4
  68. package/src/__tests__/use-rdf-document.test.ts +2 -3
  69. package/src/__tests__/utils/content-renderer-xss.test.ts +57 -0
  70. package/src/__tests__/validate-shacl.test.ts +3 -3
  71. package/src/adapters/wire/index.ts +69 -0
  72. package/src/data/ontology-schema.json +1336 -183
  73. package/src/utils/content-renderer.ts +7 -3
  74. package/src/utils/url-safety.ts +1 -5
  75. package/tsconfig.json +2 -2
  76. package/vite.config.ts +1 -1
  77. package/src/__tests__/composables/use-concept-edges-v2.test.ts +0 -167
  78. package/src/__tests__/composables/use-concept-edges-v2.test.ts.bak +0 -166
  79. package/src/types/agents-version-turtle.d.ts +0 -27
  80. package/src/types/bibliography-turtle.d.ts +0 -12
  81. package/src/types/build-activity-turtle.d.ts +0 -16
  82. package/src/types/build-cache.d.ts +0 -20
  83. package/src/types/dataset-turtle.d.ts +0 -32
  84. package/src/types/normalize-yaml.d.ts +0 -16
  85. package/src/types/turtle-escape.d.ts +0 -6
  86. package/src/types/vocab-turtle.d.ts +0 -13
  87. /package/scripts/{doctor.mjs → doctor.ts} +0 -0
  88. /package/scripts/{generate-ontology-data.mjs → generate-ontology-data.ts} +0 -0
  89. /package/scripts/lib/{agents-turtle.mjs → agents-turtle.ts} +0 -0
  90. /package/scripts/lib/{bibliography-turtle.mjs → bibliography-turtle.ts} +0 -0
  91. /package/scripts/lib/{bibliography.mjs → bibliography.ts} +0 -0
  92. /package/scripts/lib/build/{image-assets.mjs → image-assets.ts} +0 -0
  93. /package/scripts/lib/build/{non-verbal-consumer.mjs → non-verbal-consumer.ts} +0 -0
  94. /package/scripts/lib/{build-activity-turtle.mjs → build-activity-turtle.ts} +0 -0
  95. /package/scripts/lib/{concept-groups.mjs → concept-groups.ts} +0 -0
  96. /package/scripts/lib/{first-non-empty.mjs → first-non-empty.ts} +0 -0
  97. /package/scripts/lib/{turtle-escape.mjs → turtle-escape.ts} +0 -0
  98. /package/scripts/lib/{version-turtle.mjs → version-turtle.ts} +0 -0
  99. /package/scripts/lib/{vocab-turtle.mjs → vocab-turtle.ts} +0 -0
  100. /package/scripts/{load-site-config.mjs → load-site-config.ts} +0 -0
  101. /package/scripts/{migrate-v1-to-v3.mjs → migrate-v1-to-v3.ts} +0 -0
  102. /package/scripts/{normalize-yaml.mjs → normalize-yaml.ts} +0 -0
  103. /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.js';
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.mjs';
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.mjs';
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.mjs';
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.mjs';
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