@glossarist/concept-browser 0.7.115 → 0.7.117

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.
@@ -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,100 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { spawnSync } from 'node:child_process';
3
+ import { existsSync, readFileSync } 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
+ 'generate-ontology-data',
25
+ 'generate-ontology-schema',
26
+ 'doctor',
27
+ 'validate-shacl',
28
+ 'bridge-to-astro',
29
+ 'sync-concept-model',
30
+ 'generate-404',
31
+ ];
32
+
33
+ function runPlain(args: string[], opts?: Parameters<typeof spawnSync>[2]): { status: number | null; stdout: string; stderr: string } {
34
+ const result = spawnSync('node', args, { encoding: 'utf8', cwd: ROOT, ...opts });
35
+ return {
36
+ status: result.status,
37
+ stdout: typeof result.stdout === 'string' ? result.stdout : result.stdout?.toString('utf8') ?? '',
38
+ stderr: typeof result.stderr === 'string' ? result.stderr : result.stderr?.toString('utf8') ?? '',
39
+ };
40
+ }
41
+
42
+ describe('published-package contract — compiled .js scripts load under plain node (no tsx)', () => {
43
+ it('npm run build:scripts produces .js for every script', () => {
44
+ const build = spawnSync('node', [join(ROOT, 'scripts', 'build-scripts.mjs')], {
45
+ encoding: 'utf8',
46
+ cwd: ROOT,
47
+ });
48
+ expect(build.status).toBe(0);
49
+
50
+ for (const name of [...LIBRARY_SCRIPTS, ...PIPELINE_SCRIPTS]) {
51
+ const jsPath = join(SCRIPTS_DIR, `${name}.js`);
52
+ expect(existsSync(jsPath)).toBe(true);
53
+ }
54
+ }, 30000);
55
+
56
+ it('library scripts export functions under plain node (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING regression)', () => {
57
+ for (const name of LIBRARY_SCRIPTS) {
58
+ const jsPath = `./scripts/${name}.js`;
59
+ 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); })`]);
60
+ expect(result.stderr).not.toContain('ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING');
61
+ expect(result.status).toBe(0);
62
+ expect(result.stdout).toContain('OK');
63
+ }
64
+ }, 60000);
65
+
66
+ it('pipeline scripts do not throw ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING (may fail on missing config, that is OK)', () => {
67
+ for (const name of PIPELINE_SCRIPTS) {
68
+ const result = runPlain(['--input-type=module', '-e', `import(${JSON.stringify(`./scripts/${name}.js`)}).then(() => console.log('OK')).catch(e => { console.error(e.message); })`]);
69
+ expect(result.stderr).not.toContain('ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING');
70
+ }
71
+ }, 60000);
72
+
73
+ it('validate-yaml.js exports validateYaml function', () => {
74
+ const result = runPlain(['--input-type=module', '-e', `
75
+ import { validateYaml } from './scripts/validate-yaml.js';
76
+ const schema = { type: 'object', required: ['termid'], properties: { termid: { type: 'string' } } };
77
+ const r = validateYaml('termid: test', schema);
78
+ console.log('valid=' + r.valid);
79
+ `]);
80
+ expect(result.status).toBe(0);
81
+ expect(result.stdout).toContain('valid=true');
82
+ });
83
+
84
+ it('compiled scripts do not import devDependencies (runtime packaging regression)', () => {
85
+ const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8'));
86
+ const deps = new Set(Object.keys(pkg.dependencies || {}));
87
+ const devDeps = new Set(Object.keys(pkg.devDependencies || {}));
88
+ for (const name of LIBRARY_SCRIPTS) {
89
+ const jsPath = join(SCRIPTS_DIR, `${name}.js`);
90
+ if (!existsSync(jsPath)) continue;
91
+ const src = readFileSync(jsPath, 'utf8');
92
+ for (const devDep of devDeps) {
93
+ if (deps.has(devDep)) continue;
94
+ expect(src).not.toContain(`from "${devDep}"`);
95
+ expect(src).not.toContain(`from '${devDep}'`);
96
+ expect(src).not.toMatch(new RegExp(`import\\s*\\(\\s*["']${devDep}["']`));
97
+ }
98
+ }
99
+ });
100
+ });
@@ -13,7 +13,7 @@ describe('scripts syntax gate', () => {
13
13
  const result = spawnSync(
14
14
  process.execPath,
15
15
  ['--import', 'tsx', path.join(repoRoot, 'scripts', 'check-syntax.ts')],
16
- { encoding: 'utf8' },
16
+ { encoding: 'utf8', timeout: 60000 },
17
17
  );
18
18
 
19
19
  if (result.status !== 0) {
@@ -24,5 +24,5 @@ describe('scripts syntax gate', () => {
24
24
 
25
25
  expect(result.status).toBe(0);
26
26
  expect(result.stdout).toMatch(/syntax OK:/);
27
- });
27
+ }, 120000);
28
28
  });