@glossarist/concept-browser 0.7.66 → 0.7.67
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/index.mjs +38 -33
- package/package.json +2 -2
- package/scripts/__tests__/cli-script-contract.test.mjs +39 -0
- package/scripts/__tests__/first-non-empty.test.mjs +51 -0
- package/scripts/__tests__/process-about-pages.test.mjs +106 -0
- package/scripts/build-edges.js +10 -2
- package/scripts/fetch-datasets.mjs +61 -54
- package/scripts/generate-data.mjs +38 -26
- package/scripts/lib/first-non-empty.mjs +19 -0
- package/scripts/process-about-pages.mjs +63 -15
- package/scripts/validate-shacl.mjs +1 -1
- package/src/__tests__/concept-rdf-view.test.ts +21 -13
- package/src/__tests__/perf/serialization-perf.test.ts +13 -30
- package/src/__tests__/use-rdf-document.test.ts +125 -0
- package/src/components/ConceptRdfView.vue +4 -4
- package/src/components/use-rdf-document.ts +137 -0
- package/src/__tests__/concept-rdf/agents-emitter.test.ts +0 -110
- package/src/__tests__/concept-rdf/bibliography-emitter.test.ts +0 -159
- package/src/__tests__/concept-rdf/build-activity-emitter.test.ts +0 -119
- package/src/__tests__/concept-rdf/coerce-date.test.ts +0 -97
- package/src/__tests__/concept-rdf/concept-emitter.test.ts +0 -258
- package/src/__tests__/concept-rdf/dataset-emitter.test.ts +0 -224
- package/src/__tests__/concept-rdf/differential.test.ts +0 -96
- package/src/__tests__/concept-rdf/group-emitter.test.ts +0 -109
- package/src/__tests__/concept-rdf/image-variant-emitter.test.ts +0 -135
- package/src/__tests__/concept-rdf/jsonld-writer.test.ts +0 -109
- package/src/__tests__/concept-rdf/nonverbal-rep.test.ts +0 -177
- package/src/__tests__/concept-rdf/property-based.test.ts +0 -179
- package/src/__tests__/concept-rdf/provenance.test.ts +0 -110
- package/src/__tests__/concept-rdf/quad-isomorphism.test.ts +0 -43
- package/src/__tests__/concept-rdf/quad-isomorphism.ts +0 -47
- package/src/__tests__/concept-rdf/rdf-components.test.ts +0 -145
- package/src/__tests__/concept-rdf/rdf-graph.test.ts +0 -115
- package/src/__tests__/concept-rdf/round-trip.test.ts +0 -243
- package/src/__tests__/concept-rdf/scoped-examples.test.ts +0 -126
- package/src/__tests__/concept-rdf/sections-builder.test.ts +0 -94
- package/src/__tests__/concept-rdf/shacl-conformance.test.ts +0 -110
- package/src/__tests__/concept-rdf/shape-consistency.test.ts +0 -138
- package/src/__tests__/concept-rdf/snapshot-generation.test.ts +0 -75
- package/src/__tests__/concept-rdf/table-formula-emitter.test.ts +0 -142
- package/src/__tests__/concept-rdf/turtle-writer.test.ts +0 -114
- package/src/__tests__/concept-rdf/use-rdf-document.test.ts +0 -246
- package/src/__tests__/concept-rdf/version-emitter.test.ts +0 -120
- package/src/__tests__/concept-rdf/vocabulary-ssot.test.ts +0 -100
- package/src/__tests__/perf/bundle-layout.test.ts +0 -50
- package/src/components/concept-rdf/agents-emitter.ts +0 -82
- package/src/components/concept-rdf/bibliography-emitter.ts +0 -83
- package/src/components/concept-rdf/build-activity-emitter.ts +0 -89
- package/src/components/concept-rdf/concept-emitter.ts +0 -443
- package/src/components/concept-rdf/dataset-emitter.ts +0 -95
- package/src/components/concept-rdf/group-emitter.ts +0 -69
- package/src/components/concept-rdf/image-variant-emitter.ts +0 -46
- package/src/components/concept-rdf/jsonld-writer.ts +0 -82
- package/src/components/concept-rdf/predicates.ts +0 -261
- package/src/components/concept-rdf/provenance.ts +0 -80
- package/src/components/concept-rdf/rdf-graph.ts +0 -211
- package/src/components/concept-rdf/sections-builder.ts +0 -62
- package/src/components/concept-rdf/table-formula-emitter.ts +0 -101
- package/src/components/concept-rdf/turtle-writer.ts +0 -116
- package/src/components/concept-rdf/use-rdf-document.ts +0 -72
- package/src/components/concept-rdf/version-emitter.ts +0 -65
- package/src/components/concept-rdf/vocabulary-emitter.ts +0 -62
- /package/src/components/{concept-rdf/RdfInstanceHeader.vue → RdfInstanceHeader.vue} +0 -0
- /package/src/components/{concept-rdf/RdfInstanceSection.vue → RdfInstanceSection.vue} +0 -0
- /package/src/components/{concept-rdf/RdfPrefixLegend.vue → RdfPrefixLegend.vue} +0 -0
- /package/src/components/{concept-rdf/RdfSourcePanel.vue → RdfSourcePanel.vue} +0 -0
- /package/src/components/{concept-rdf/rdf-prefixes.ts → rdf-prefixes.ts} +0 -0
package/cli/index.mjs
CHANGED
|
@@ -30,9 +30,10 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
30
30
|
const pkgRoot = resolve(__dirname, '..');
|
|
31
31
|
|
|
32
32
|
const commands = {
|
|
33
|
-
fetch: () => import('../scripts/fetch-datasets.mjs'),
|
|
34
|
-
generate: () => import('../scripts/generate-data.mjs'),
|
|
35
|
-
edges: () => import('../scripts/build-edges.js'),
|
|
33
|
+
fetch: async () => (await import('../scripts/fetch-datasets.mjs')).main(),
|
|
34
|
+
generate: async () => { await import('../scripts/generate-data.mjs'); },
|
|
35
|
+
edges: async () => (await import('../scripts/build-edges.js')).main(),
|
|
36
|
+
about: async () => (await import('../scripts/process-about-pages.mjs')).main(),
|
|
36
37
|
};
|
|
37
38
|
|
|
38
39
|
function parseArgs(argv) {
|
|
@@ -61,7 +62,8 @@ Commands:
|
|
|
61
62
|
fetch Fetch/update datasets from GCR packages or source repos
|
|
62
63
|
generate Convert YAML concepts to JSON-LD static data
|
|
63
64
|
edges Build cross-reference edges from generated concepts
|
|
64
|
-
|
|
65
|
+
about Compile per-dataset and per-group about pages
|
|
66
|
+
build Full pipeline (fetch + generate + edges + about + vite build)
|
|
65
67
|
site Same as build
|
|
66
68
|
normalize NFC-normalize YAML files in .datasets/
|
|
67
69
|
doctor Diagnose the local environment (deps, datasets, shapes, context)
|
|
@@ -87,7 +89,7 @@ Environment:
|
|
|
87
89
|
if (!process.env.BASE_PATH && config?.basePath) {
|
|
88
90
|
process.env.BASE_PATH = config.basePath;
|
|
89
91
|
}
|
|
90
|
-
for (const step of ['fetch', 'generate', 'edges']) {
|
|
92
|
+
for (const step of ['fetch', 'generate', 'edges', 'about']) {
|
|
91
93
|
console.log(`\n=== ${step.toUpperCase()} ===\n`);
|
|
92
94
|
await commands[step]();
|
|
93
95
|
}
|
|
@@ -191,40 +193,43 @@ Environment:
|
|
|
191
193
|
}
|
|
192
194
|
|
|
193
195
|
const runner = commands[cmd];
|
|
194
|
-
if (
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
console.error(`NFC check failed: ${nonNfc} of ${checked} file(s) are not NFC-normalized\n`);
|
|
206
|
-
for (const f of fixed) console.error(` ${f}`);
|
|
207
|
-
process.exit(1);
|
|
208
|
-
}
|
|
196
|
+
if (runner) {
|
|
197
|
+
await runner();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (cmd === 'normalize') {
|
|
202
|
+
const { normalizeYaml } = await import('../scripts/normalize-yaml.mjs');
|
|
203
|
+
const check = process.argv.includes('--check');
|
|
204
|
+
const paths = process.argv.slice(2).filter(a => !a.startsWith('-') && a !== 'normalize');
|
|
205
|
+
const { checked, nonNfc, fixed } = normalizeYaml({ check, paths });
|
|
206
|
+
if (check) {
|
|
209
207
|
if (nonNfc === 0) {
|
|
210
|
-
console.log(`NFC OK: ${checked} file(s) checked, all
|
|
211
|
-
|
|
212
|
-
console.log(`Normalized ${nonNfc} of ${checked} file(s)`);
|
|
213
|
-
for (const f of fixed) console.log(` ${f}`);
|
|
208
|
+
console.log(`NFC OK: ${checked} file(s) checked, all normalized`);
|
|
209
|
+
return;
|
|
214
210
|
}
|
|
215
|
-
|
|
211
|
+
console.error(`NFC check failed: ${nonNfc} of ${checked} file(s) are not NFC-normalized\n`);
|
|
212
|
+
for (const f of fixed) console.error(` ${f}`);
|
|
213
|
+
process.exit(1);
|
|
216
214
|
}
|
|
217
|
-
if (
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
215
|
+
if (nonNfc === 0) {
|
|
216
|
+
console.log(`NFC OK: ${checked} file(s) checked, all already normalized`);
|
|
217
|
+
} else {
|
|
218
|
+
console.log(`Normalized ${nonNfc} of ${checked} file(s)`);
|
|
219
|
+
for (const f of fixed) console.log(` ${f}`);
|
|
221
220
|
}
|
|
222
|
-
|
|
223
|
-
console.error('Run `concept-browser help` for usage.');
|
|
224
|
-
process.exit(1);
|
|
221
|
+
return;
|
|
225
222
|
}
|
|
226
223
|
|
|
227
|
-
|
|
224
|
+
if (cmd === 'doctor') {
|
|
225
|
+
const { main: doctorMain } = await import('../scripts/doctor.mjs');
|
|
226
|
+
await doctorMain();
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
console.error(`Unknown command: ${cmd}`);
|
|
231
|
+
console.error('Run `concept-browser help` for usage.');
|
|
232
|
+
process.exit(1);
|
|
228
233
|
}
|
|
229
234
|
|
|
230
235
|
main().catch(e => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glossarist/concept-browser",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.67",
|
|
4
4
|
"description": "Vue SPA for browsing Glossarist terminology datasets with cross-reference resolution, graph visualization, and multi-language support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"autoprefixer": "^10.4.21",
|
|
32
32
|
"d3": "^7.9.0",
|
|
33
33
|
"favicons": "^7.2.0",
|
|
34
|
-
"glossarist": "^0.4.
|
|
34
|
+
"glossarist": "^0.4.15",
|
|
35
35
|
"js-yaml": "^4.1.0",
|
|
36
36
|
"jszip": "^3.10.1",
|
|
37
37
|
"pinia": "^2.3.1",
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { resolve, dirname } from 'node:path';
|
|
5
|
+
|
|
6
|
+
// Verifies every CLI-driven script exports main() and uses a
|
|
7
|
+
// direct-invocation guard. This pins the contract documented in
|
|
8
|
+
// TODO.bugs/03 and TODO.bugs/05: scripts are testable AND
|
|
9
|
+
// auto-run when invoked directly. Without this contract, the CLI
|
|
10
|
+
// would need a separate code path per script (some auto-run via
|
|
11
|
+
// import side-effect, others need explicit m.main()).
|
|
12
|
+
|
|
13
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
|
|
15
|
+
const SCRIPT_PATHS = [
|
|
16
|
+
'../../scripts/fetch-datasets.mjs',
|
|
17
|
+
'../../scripts/build-edges.js',
|
|
18
|
+
'../../scripts/process-about-pages.mjs',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
describe('CLI script contract: every script exports main()', () => {
|
|
22
|
+
for (const scriptPath of SCRIPT_PATHS) {
|
|
23
|
+
it(`${scriptPath} exports main as a function`, async () => {
|
|
24
|
+
const mod = await import(scriptPath);
|
|
25
|
+
expect(typeof mod.main).toBe('function');
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('CLI script contract: direct-invocation guard', () => {
|
|
31
|
+
for (const scriptPath of SCRIPT_PATHS) {
|
|
32
|
+
it(`${scriptPath} source uses realpathSync direct-invocation guard`, () => {
|
|
33
|
+
const abs = resolve(__dirname, scriptPath);
|
|
34
|
+
const src = readFileSync(abs, 'utf8');
|
|
35
|
+
expect(src).toMatch(/realpathSync\s*\(\s*process\.argv\[1\]/);
|
|
36
|
+
expect(src).toMatch(/realpathSync\s*\(\s*fileURLToPath\s*\(\s*import\.meta\.url\s*\)\s*\)/);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { firstNonEmpty } from '../lib/first-non-empty.mjs';
|
|
3
|
+
|
|
4
|
+
describe('firstNonEmpty — register-wins resolution', () => {
|
|
5
|
+
it('returns the first value when both are present (register wins)', () => {
|
|
6
|
+
expect(firstNonEmpty('reg-value', 'ds-value')).toBe('reg-value');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('falls through to site-config when register value is null', () => {
|
|
10
|
+
expect(firstNonEmpty(null, 'ds-value')).toBe('ds-value');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('falls through to site-config when register value is undefined', () => {
|
|
14
|
+
expect(firstNonEmpty(undefined, 'ds-value')).toBe('ds-value');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('falls through to site-config when register value is empty string', () => {
|
|
18
|
+
expect(firstNonEmpty('', 'ds-value')).toBe('ds-value');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('falls through to site-config when register value is empty array', () => {
|
|
22
|
+
expect(firstNonEmpty([], ['ds-tag'])).toEqual(['ds-tag']);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('returns the first value when site-config is empty', () => {
|
|
26
|
+
expect(firstNonEmpty('reg-value', '')).toBe('reg-value');
|
|
27
|
+
expect(firstNonEmpty('reg-value', null)).toBe('reg-value');
|
|
28
|
+
expect(firstNonEmpty('reg-value', undefined)).toBe('reg-value');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('returns the first value when register is array and site-config is empty', () => {
|
|
32
|
+
expect(firstNonEmpty(['reg'], [])).toEqual(['reg']);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('returns the default when both are absent', () => {
|
|
36
|
+
expect(firstNonEmpty(null, undefined, 'default')).toBe('default');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('returns undefined when no arguments are non-empty', () => {
|
|
40
|
+
expect(firstNonEmpty(null, undefined, '', [])).toBeUndefined();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('returns undefined when called with no arguments', () => {
|
|
44
|
+
expect(firstNonEmpty()).toBeUndefined();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('does not skip falsy but meaningful values (e.g. 0, false)', () => {
|
|
48
|
+
expect(firstNonEmpty(0, 1)).toBe(0);
|
|
49
|
+
expect(firstNonEmpty(false, true)).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { writeFileSync, mkdirSync, rmSync, existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join, dirname } from 'node:path';
|
|
4
|
+
import { mkdtempSync } from 'node:fs';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
|
|
7
|
+
const scriptPath = join(dirname(new URL(import.meta.url).pathname), '..', 'process-about-pages.mjs');
|
|
8
|
+
|
|
9
|
+
// Process-about-pages discovers per-dataset about content from either
|
|
10
|
+
// .datasets/<id>/about/ (fetched) OR site-config localPath overrides.
|
|
11
|
+
// This spec covers both discovery paths plus the per-group path.
|
|
12
|
+
describe('process-about-pages — discovery paths', () => {
|
|
13
|
+
let root;
|
|
14
|
+
let origCwd;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
root = mkdtempSync(join(tmpdir(), 'about-pages-'));
|
|
18
|
+
origCwd = process.cwd();
|
|
19
|
+
process.chdir(root);
|
|
20
|
+
// The script writes to public/pages under CWD.
|
|
21
|
+
mkdirSync(join(root, 'public'), { recursive: true });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
process.chdir(origCwd);
|
|
26
|
+
rmSync(root, { recursive: true, force: true });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Re-import each time with a cache-busting query so main() doesn't
|
|
30
|
+
// auto-run on cached load. The exported main() is invoked explicitly.
|
|
31
|
+
async function runMain() {
|
|
32
|
+
const url = new URL(`file://${scriptPath}?t=${Date.now()}`);
|
|
33
|
+
const mod = await import(url.href);
|
|
34
|
+
return mod.main();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function writeSiteConfig(datasets) {
|
|
38
|
+
const lines = ['id: test', 'datasets:'];
|
|
39
|
+
for (const d of datasets) {
|
|
40
|
+
lines.push(` - id: ${d.id}`);
|
|
41
|
+
if (d.localPath) lines.push(` localPath: ${d.localPath}`);
|
|
42
|
+
lines.push(` title: "${d.id}"`);
|
|
43
|
+
}
|
|
44
|
+
writeFileSync(join(root, 'site-config.yml'), lines.join('\n') + '\n');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
it('discovers about pages under .datasets/<id>/about/', async () => {
|
|
48
|
+
mkdirSync(join(root, '.datasets', 'foo', 'about'), { recursive: true });
|
|
49
|
+
writeFileSync(
|
|
50
|
+
join(root, '.datasets', 'foo', 'about', 'about.md'),
|
|
51
|
+
'# Foo\n\nThe foo dataset.',
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
await runMain();
|
|
55
|
+
|
|
56
|
+
const outPath = join(root, 'public', 'pages', 'dataset-foo-about.json');
|
|
57
|
+
expect(existsSync(outPath)).toBe(true);
|
|
58
|
+
const parsed = JSON.parse(readFileSync(outPath, 'utf8'));
|
|
59
|
+
expect(parsed.title).toBe('Foo');
|
|
60
|
+
expect(parsed.html).toContain('<h1>Foo</h1>');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('discovers about pages via site-config localPath when .datasets/ is absent', async () => {
|
|
64
|
+
const localRoot = mkdtempSync(join(tmpdir(), 'local-ds-'));
|
|
65
|
+
try {
|
|
66
|
+
mkdirSync(join(localRoot, 'about'), { recursive: true });
|
|
67
|
+
writeFileSync(
|
|
68
|
+
join(localRoot, 'about', 'about.md'),
|
|
69
|
+
'# Bar\n\nLocal-path dataset.',
|
|
70
|
+
);
|
|
71
|
+
writeSiteConfig([{ id: 'bar', localPath: localRoot }]);
|
|
72
|
+
|
|
73
|
+
await runMain();
|
|
74
|
+
|
|
75
|
+
const outPath = join(root, 'public', 'pages', 'dataset-bar-about.json');
|
|
76
|
+
expect(existsSync(outPath)).toBe(true);
|
|
77
|
+
const parsed = JSON.parse(readFileSync(outPath, 'utf8'));
|
|
78
|
+
expect(parsed.title).toBe('Bar');
|
|
79
|
+
} finally {
|
|
80
|
+
rmSync(localRoot, { recursive: true, force: true });
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('discovers group about pages under site-content/groups/<id>/about/', async () => {
|
|
85
|
+
mkdirSync(join(root, 'site-content', 'groups', 'mygroup', 'about'), { recursive: true });
|
|
86
|
+
writeFileSync(
|
|
87
|
+
join(root, 'site-content', 'groups', 'mygroup', 'about', 'about.md'),
|
|
88
|
+
'# My Group\n\nA group of datasets.',
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
await runMain();
|
|
92
|
+
|
|
93
|
+
const outPath = join(root, 'public', 'pages', 'group-mygroup-about.json');
|
|
94
|
+
expect(existsSync(outPath)).toBe(true);
|
|
95
|
+
const parsed = JSON.parse(readFileSync(outPath, 'utf8'));
|
|
96
|
+
expect(parsed.title).toBe('My Group');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('skips silently when no about directories exist', async () => {
|
|
100
|
+
writeSiteConfig([{ id: 'lonely' }]);
|
|
101
|
+
// No .datasets, no localPath about/, no groups. The script runs to
|
|
102
|
+
// completion without throwing and produces no output pages.
|
|
103
|
+
await expect(runMain()).resolves.not.toThrow();
|
|
104
|
+
expect(existsSync(join(root, 'public', 'pages', 'dataset-lonely-about.json'))).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
});
|
package/scripts/build-edges.js
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* Usage: node scripts/build-edges.js
|
|
7
7
|
*/
|
|
8
8
|
import { extractSourceRefs } from './extract-source-refs.js';
|
|
9
|
-
import { readFileSync, writeFileSync, readdirSync, existsSync } from 'fs';
|
|
10
|
-
import { join, dirname } from 'path';
|
|
9
|
+
import { readFileSync, writeFileSync, readdirSync, existsSync, realpathSync } from 'fs';
|
|
10
|
+
import { join, dirname, resolve } from 'path';
|
|
11
11
|
import { fileURLToPath } from 'url';
|
|
12
12
|
import yaml from 'js-yaml';
|
|
13
13
|
import { Register } from 'glossarist';
|
|
@@ -252,6 +252,7 @@ function buildEdgesForDataset(datasetDir, registerId, uriBase, urnMap, manifest)
|
|
|
252
252
|
}
|
|
253
253
|
|
|
254
254
|
// Main
|
|
255
|
+
export function main() {
|
|
255
256
|
console.log('Building edge indexes...\n');
|
|
256
257
|
|
|
257
258
|
if (!existsSync(DATA_DIR)) {
|
|
@@ -388,3 +389,10 @@ const refCount = Object.values(crossRefIndex).reduce((sum, arr) => sum + arr.len
|
|
|
388
389
|
console.log(`Written cross-ref-index.json (${refCount} cross-references across ${datasets.length} datasets)`);
|
|
389
390
|
|
|
390
391
|
console.log('Done.');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const isDirectInvocation = process.argv[1]
|
|
395
|
+
&& realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
|
|
396
|
+
if (isDirectInvocation) {
|
|
397
|
+
main();
|
|
398
|
+
}
|
|
@@ -19,10 +19,9 @@ import path from 'path';
|
|
|
19
19
|
import JSZip from 'jszip';
|
|
20
20
|
import { loadGcr } from 'glossarist';
|
|
21
21
|
import { execFileSync } from 'child_process';
|
|
22
|
+
import { fileURLToPath } from 'url';
|
|
22
23
|
import { loadSiteConfig } from './load-site-config.mjs';
|
|
23
|
-
import { assertLocalPathSafe } from './lib/local-path-safety.mjs';
|
|
24
|
-
|
|
25
|
-
const ROOT = process.cwd();
|
|
24
|
+
import { assertLocalPathSafe } from './lib/local-path-safety.mjs';const ROOT = process.cwd();
|
|
26
25
|
const DATASETS_DIR = path.join(ROOT, '.datasets');
|
|
27
26
|
const GCR_DIR = path.join(ROOT, '.gcr');
|
|
28
27
|
|
|
@@ -136,68 +135,76 @@ function cloneOrUpdate(sourceRepo, targetDir) {
|
|
|
136
135
|
// --- localPath safety check: see scripts/lib/local-path-safety.mjs ---
|
|
137
136
|
|
|
138
137
|
// --- Main ---
|
|
139
|
-
|
|
138
|
+
export async function main() {
|
|
139
|
+
console.log('Fetching glossarist datasets...\n');
|
|
140
140
|
|
|
141
|
-
const { config } = loadSiteConfig();
|
|
142
|
-
const gcrMetadata = {};
|
|
141
|
+
const { config } = loadSiteConfig();
|
|
142
|
+
const gcrMetadata = {};
|
|
143
143
|
|
|
144
|
-
for (const ds of config.datasets) {
|
|
145
|
-
|
|
144
|
+
for (const ds of config.datasets) {
|
|
145
|
+
console.log(`${ds.id}:`);
|
|
146
146
|
|
|
147
|
-
|
|
148
|
-
|
|
147
|
+
const gcrPath = path.join(GCR_DIR, `${ds.id}.gcr`);
|
|
148
|
+
const targetDir = path.join(DATASETS_DIR, ds.id);
|
|
149
149
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
150
|
+
try {
|
|
151
|
+
if (fs.existsSync(gcrPath)) {
|
|
152
|
+
console.log(` Using local .gcr/${ds.id}.gcr`);
|
|
153
|
+
await extractGcr(gcrPath, targetDir);
|
|
154
|
+
} else if (ds.gcrPackage) {
|
|
155
|
+
console.log(` Using GCR package: ${ds.gcrPackage}`);
|
|
156
|
+
try {
|
|
157
|
+
await downloadGcr(ds.gcrPackage, gcrPath);
|
|
158
|
+
} catch (e) {
|
|
159
|
+
console.warn(` GCR download failed: ${e.message}`);
|
|
160
|
+
console.warn(` Skipping ${ds.id}`);
|
|
161
|
+
console.log();
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
await extractGcr(gcrPath, targetDir);
|
|
165
|
+
} else if (ds.localPath) {
|
|
166
|
+
// localPath means "data is here, use in-place." No copy, no staging.
|
|
167
|
+
// generate-data.mjs reads from localPath directly via datasetDir(ds).
|
|
168
|
+
const localResolved = assertLocalPathSafe(ds.id, ds.localPath);
|
|
169
|
+
console.log(` Using localPath in-place: ${localResolved}`);
|
|
170
|
+
} else if (ds.sourceRepo) {
|
|
171
|
+
cloneOrUpdate(ds.sourceRepo, targetDir);
|
|
172
|
+
} else {
|
|
173
|
+
console.warn(` No source configured, skipping`);
|
|
161
174
|
console.log();
|
|
162
175
|
continue;
|
|
163
176
|
}
|
|
164
|
-
await extractGcr(gcrPath, targetDir);
|
|
165
|
-
} else if (ds.localPath) {
|
|
166
|
-
// localPath means "data is here, use in-place." No copy, no staging.
|
|
167
|
-
// generate-data.mjs reads from localPath directly via datasetDir(ds).
|
|
168
|
-
const localResolved = assertLocalPathSafe(ds.id, ds.localPath);
|
|
169
|
-
console.log(` Using localPath in-place: ${localResolved}`);
|
|
170
|
-
} else if (ds.sourceRepo) {
|
|
171
|
-
cloneOrUpdate(ds.sourceRepo, targetDir);
|
|
172
|
-
} else {
|
|
173
|
-
console.warn(` No source configured, skipping`);
|
|
174
|
-
console.log();
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
177
177
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
178
|
+
// Read metadata for dependency validation (from GCR ZIP, not extracted dir)
|
|
179
|
+
const meta = await readGcrMetadata(gcrPath);
|
|
180
|
+
if (meta) {
|
|
181
|
+
gcrMetadata[ds.id] = meta;
|
|
182
|
+
console.log(` ${meta.concept_count || '?'} concepts, ${meta.uri_prefix || 'no uri'}`);
|
|
183
|
+
}
|
|
184
|
+
} catch (e) {
|
|
185
|
+
console.warn(` Failed: ${e.message}`);
|
|
186
|
+
console.warn(` Skipping ${ds.id}`);
|
|
183
187
|
}
|
|
184
|
-
|
|
185
|
-
console.warn(` Failed: ${e.message}`);
|
|
186
|
-
console.warn(` Skipping ${ds.id}`);
|
|
188
|
+
console.log();
|
|
187
189
|
}
|
|
188
|
-
console.log();
|
|
189
|
-
}
|
|
190
190
|
|
|
191
|
-
// Dependency validation
|
|
192
|
-
console.log('Validating dependencies...\n');
|
|
193
|
-
const errors = validateDependencies(config, gcrMetadata);
|
|
194
|
-
if (errors.length) {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
191
|
+
// Dependency validation
|
|
192
|
+
console.log('Validating dependencies...\n');
|
|
193
|
+
const errors = validateDependencies(config, gcrMetadata);
|
|
194
|
+
if (errors.length) {
|
|
195
|
+
console.error('Dependency validation FAILED:');
|
|
196
|
+
for (const err of errors) {
|
|
197
|
+
console.error(` ✗ ${err}`);
|
|
198
|
+
}
|
|
199
|
+
process.exit(1);
|
|
198
200
|
}
|
|
199
|
-
|
|
201
|
+
console.log('All dependencies satisfied.\n');
|
|
202
|
+
|
|
203
|
+
console.log('Done.');
|
|
200
204
|
}
|
|
201
|
-
console.log('All dependencies satisfied.\n');
|
|
202
205
|
|
|
203
|
-
|
|
206
|
+
const isDirectInvocation = process.argv[1]
|
|
207
|
+
&& fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
|
|
208
|
+
if (isDirectInvocation) {
|
|
209
|
+
await main();
|
|
210
|
+
}
|
|
@@ -13,6 +13,7 @@ import { buildAgentsTurtle } from './lib/agents-turtle.mjs';
|
|
|
13
13
|
import { buildVersionHistoryTurtle } from './lib/version-turtle.mjs';
|
|
14
14
|
import { buildBibliographyTurtle } from './lib/bibliography-turtle.mjs';
|
|
15
15
|
import { ttlLit } from './lib/turtle-escape.mjs';
|
|
16
|
+
import { firstNonEmpty } from './lib/first-non-empty.mjs';
|
|
16
17
|
const __dirname = path.dirname(new URL(import.meta.url).pathname);
|
|
17
18
|
const ROOT = process.cwd();
|
|
18
19
|
const PUBLIC = path.join(ROOT, 'public');
|
|
@@ -1169,12 +1170,11 @@ for (let i = 0; i < config.datasets.length; i++) {
|
|
|
1169
1170
|
// Use cached register
|
|
1170
1171
|
const reg = registerCache[ds.id] || null;
|
|
1171
1172
|
|
|
1172
|
-
//
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|| ['eng'];
|
|
1173
|
+
// Content fields: register-wins, site-config fallback. Layout-only
|
|
1174
|
+
// fields (uri, color, gcrPackage) stay on site-config.
|
|
1175
|
+
const dsLanguages = firstNonEmpty(reg?.languages, ds.languages, ['eng']);
|
|
1176
1176
|
|
|
1177
|
-
// Resolve description: register
|
|
1177
|
+
// Resolve description: register-wins (localized object), site-config fallback
|
|
1178
1178
|
const defaultLang = dsLanguages[0] || 'eng';
|
|
1179
1179
|
const regDesc = reg?.description;
|
|
1180
1180
|
const dsDesc = ds.description;
|
|
@@ -1182,28 +1182,39 @@ for (let i = 0; i < config.datasets.length; i++) {
|
|
|
1182
1182
|
? regDesc[defaultLang] || Object.values(regDesc)[0] || ''
|
|
1183
1183
|
: dsDesc || '';
|
|
1184
1184
|
|
|
1185
|
-
//
|
|
1186
|
-
|
|
1185
|
+
// Title: register.displayName wins (TODO.refactor/40 — Register.name
|
|
1186
|
+
// is first-class in glossarist-js since v0.4.13). Site-config title
|
|
1187
|
+
// is fallback, then ref (citation proxy), then id.
|
|
1188
|
+
const resolvedTitle = reg?.displayName(defaultLang)
|
|
1189
|
+
?? ds.title
|
|
1190
|
+
?? reg?.ref
|
|
1191
|
+
?? ds.id;
|
|
1187
1192
|
|
|
1188
1193
|
counts[ds.id] = await processDataset(dir, ds.id, {
|
|
1189
1194
|
title: resolvedTitle,
|
|
1190
1195
|
description: resolvedDescription,
|
|
1191
|
-
owner:
|
|
1196
|
+
owner: firstNonEmpty(reg?.owner, ds.owner),
|
|
1192
1197
|
languages: dsLanguages,
|
|
1193
|
-
sourceRepo:
|
|
1194
|
-
languageOrder:
|
|
1195
|
-
ref:
|
|
1196
|
-
refAliases:
|
|
1197
|
-
tags:
|
|
1198
|
+
sourceRepo: firstNonEmpty(reg?.sourceRepo, ds.sourceRepo),
|
|
1199
|
+
languageOrder: firstNonEmpty(reg?.languageOrder, ds.languageOrder),
|
|
1200
|
+
ref: firstNonEmpty(reg?.ref, ds.ref),
|
|
1201
|
+
refAliases: firstNonEmpty(reg?.refAliases, ds.refAliases),
|
|
1202
|
+
tags: firstNonEmpty(reg?.tags, ds.tags),
|
|
1198
1203
|
color: ds.color || DS_PALETTE[i % DS_PALETTE.length],
|
|
1199
|
-
datasetUri:
|
|
1200
|
-
uriAliases:
|
|
1201
|
-
status:
|
|
1204
|
+
datasetUri: firstNonEmpty(reg?.urn, ds.uri),
|
|
1205
|
+
uriAliases: firstNonEmpty(reg?.urnAliases, ds.uriAliases),
|
|
1206
|
+
status: firstNonEmpty(reg?.status, ds.editionStatus),
|
|
1202
1207
|
ordering: reg?.ordering || null,
|
|
1203
1208
|
sections: reg?.sections ? reg.sections.map(s => s.toJSON()) : [],
|
|
1204
1209
|
hasBibliography: fs.existsSync(path.join(datasetDir(ds), 'bibliography.yaml')),
|
|
1205
1210
|
hasImages: fs.existsSync(path.join(datasetDir(ds), 'images')),
|
|
1206
1211
|
});
|
|
1212
|
+
const resolvedOwner = firstNonEmpty(reg?.owner, ds.owner) ?? '';
|
|
1213
|
+
const resolvedTags = firstNonEmpty(reg?.tags, ds.tags) ?? [];
|
|
1214
|
+
const resolvedDatasetUri = firstNonEmpty(reg?.urn, ds.uri);
|
|
1215
|
+
const resolvedUriAliases = firstNonEmpty(reg?.urnAliases, ds.uriAliases);
|
|
1216
|
+
const resolvedRef = firstNonEmpty(reg?.ref, ds.ref);
|
|
1217
|
+
const resolvedRefAliases = firstNonEmpty(reg?.refAliases, ds.refAliases);
|
|
1207
1218
|
registry.push({
|
|
1208
1219
|
id: ds.id,
|
|
1209
1220
|
manifestUrl: `/data/${ds.id}/manifest.json`,
|
|
@@ -1212,15 +1223,15 @@ for (let i = 0; i < config.datasets.length; i++) {
|
|
|
1212
1223
|
description: resolvedDescription,
|
|
1213
1224
|
conceptCount: counts[ds.id] || 0,
|
|
1214
1225
|
languages: dsLanguages,
|
|
1215
|
-
owner:
|
|
1216
|
-
tags:
|
|
1226
|
+
owner: resolvedOwner,
|
|
1227
|
+
tags: resolvedTags,
|
|
1217
1228
|
color: ds.color || DS_PALETTE[i % DS_PALETTE.length],
|
|
1218
1229
|
},
|
|
1219
|
-
datasetUri:
|
|
1230
|
+
datasetUri: resolvedDatasetUri,
|
|
1220
1231
|
uriBase: config.uriBase || undefined,
|
|
1221
|
-
uriAliases:
|
|
1222
|
-
ref:
|
|
1223
|
-
refAliases:
|
|
1232
|
+
uriAliases: resolvedUriAliases,
|
|
1233
|
+
ref: resolvedRef,
|
|
1234
|
+
refAliases: resolvedRefAliases,
|
|
1224
1235
|
});
|
|
1225
1236
|
}
|
|
1226
1237
|
writeJson(path.join(PUBLIC, 'datasets.json'), registry);
|
|
@@ -1553,21 +1564,22 @@ for (const key of ['logo', 'footerLogo']) {
|
|
|
1553
1564
|
}
|
|
1554
1565
|
}
|
|
1555
1566
|
|
|
1556
|
-
// Build dataset translations map:
|
|
1567
|
+
// Build dataset translations map: register descriptions win, site-config fills gaps
|
|
1557
1568
|
const datasetTranslations = {};
|
|
1558
1569
|
for (const d of config.datasets) {
|
|
1559
1570
|
const reg = registerCache[d.id];
|
|
1560
1571
|
const translations = { ...d.translations };
|
|
1561
1572
|
|
|
1562
|
-
//
|
|
1573
|
+
// Register descriptions are authoritative; site-config translations fill gaps
|
|
1563
1574
|
if (reg?.description && typeof reg.description === 'object') {
|
|
1564
1575
|
for (const [lang, desc] of Object.entries(reg.description)) {
|
|
1565
1576
|
if (!translations[lang]) translations[lang] = {};
|
|
1566
|
-
|
|
1577
|
+
translations[lang].description = desc;
|
|
1567
1578
|
}
|
|
1568
1579
|
}
|
|
1569
1580
|
|
|
1570
|
-
//
|
|
1581
|
+
// Title falls back to register.ref when site-config doesn't supply one
|
|
1582
|
+
// (TODO.refactor/40 — replace ref with Register.name once it exists)
|
|
1571
1583
|
if (reg?.ref) {
|
|
1572
1584
|
const langs = reg.languages || [];
|
|
1573
1585
|
for (const lang of langs) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Register-wins resolution for dataset content fields. Returns the
|
|
3
|
+
* first non-empty value among the arguments. Empty arrays and empty
|
|
4
|
+
* strings are treated as absent so a `[]` in register falls through
|
|
5
|
+
* to site-config.
|
|
6
|
+
*
|
|
7
|
+
* Used by generate-data.mjs to apply a single, uniform precedence
|
|
8
|
+
* rule across all dataset metadata fields (TODO.refactor/38).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export function firstNonEmpty(...vals) {
|
|
12
|
+
for (const v of vals) {
|
|
13
|
+
if (v == null) continue;
|
|
14
|
+
if (Array.isArray(v) && v.length === 0) continue;
|
|
15
|
+
if (typeof v === 'string' && v === '') continue;
|
|
16
|
+
return v;
|
|
17
|
+
}
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|