@glossarist/concept-browser 0.7.66 → 0.7.68
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 +40 -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__/tc204-deployment.test.ts +139 -0
- package/src/__tests__/use-rdf-document.test.ts +125 -0
- package/src/adapters/types.ts +7 -0
- package/src/components/ConceptRdfView.vue +4 -4
- package/src/components/use-rdf-document.ts +137 -0
- package/src/composables/useDatasetSeries.ts +1 -1
- 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.68",
|
|
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');
|
|
@@ -1080,6 +1081,7 @@ async function processDataset(dir, register, opts) {
|
|
|
1080
1081
|
sourceRepo: opts.sourceRepo,
|
|
1081
1082
|
chunkSize: CHUNK_SIZE,
|
|
1082
1083
|
color: opts.color,
|
|
1084
|
+
year: opts.year ?? undefined,
|
|
1083
1085
|
languageStats: langStats,
|
|
1084
1086
|
availableFormats,
|
|
1085
1087
|
bulkFormats,
|
|
@@ -1169,12 +1171,11 @@ for (let i = 0; i < config.datasets.length; i++) {
|
|
|
1169
1171
|
// Use cached register
|
|
1170
1172
|
const reg = registerCache[ds.id] || null;
|
|
1171
1173
|
|
|
1172
|
-
//
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|| ['eng'];
|
|
1174
|
+
// Content fields: register-wins, site-config fallback. Layout-only
|
|
1175
|
+
// fields (uri, color, gcrPackage) stay on site-config.
|
|
1176
|
+
const dsLanguages = firstNonEmpty(reg?.languages, ds.languages, ['eng']);
|
|
1176
1177
|
|
|
1177
|
-
// Resolve description: register
|
|
1178
|
+
// Resolve description: register-wins (localized object), site-config fallback
|
|
1178
1179
|
const defaultLang = dsLanguages[0] || 'eng';
|
|
1179
1180
|
const regDesc = reg?.description;
|
|
1180
1181
|
const dsDesc = ds.description;
|
|
@@ -1182,28 +1183,40 @@ for (let i = 0; i < config.datasets.length; i++) {
|
|
|
1182
1183
|
? regDesc[defaultLang] || Object.values(regDesc)[0] || ''
|
|
1183
1184
|
: dsDesc || '';
|
|
1184
1185
|
|
|
1185
|
-
//
|
|
1186
|
-
|
|
1186
|
+
// Title: register.displayName wins (TODO.refactor/40 — Register.name
|
|
1187
|
+
// is first-class in glossarist-js since v0.4.13). Site-config title
|
|
1188
|
+
// is fallback, then ref (citation proxy), then id.
|
|
1189
|
+
const resolvedTitle = reg?.displayName(defaultLang)
|
|
1190
|
+
?? ds.title
|
|
1191
|
+
?? reg?.ref
|
|
1192
|
+
?? ds.id;
|
|
1187
1193
|
|
|
1188
1194
|
counts[ds.id] = await processDataset(dir, ds.id, {
|
|
1189
1195
|
title: resolvedTitle,
|
|
1190
1196
|
description: resolvedDescription,
|
|
1191
|
-
owner:
|
|
1197
|
+
owner: firstNonEmpty(reg?.owner, ds.owner),
|
|
1192
1198
|
languages: dsLanguages,
|
|
1193
|
-
sourceRepo:
|
|
1194
|
-
languageOrder:
|
|
1195
|
-
ref:
|
|
1196
|
-
refAliases:
|
|
1197
|
-
|
|
1199
|
+
sourceRepo: firstNonEmpty(reg?.sourceRepo, ds.sourceRepo),
|
|
1200
|
+
languageOrder: firstNonEmpty(reg?.languageOrder, ds.languageOrder),
|
|
1201
|
+
ref: firstNonEmpty(reg?.ref, ds.ref),
|
|
1202
|
+
refAliases: firstNonEmpty(reg?.refAliases, ds.refAliases),
|
|
1203
|
+
year: reg?.year ?? null,
|
|
1204
|
+
tags: firstNonEmpty(reg?.tags, ds.tags),
|
|
1198
1205
|
color: ds.color || DS_PALETTE[i % DS_PALETTE.length],
|
|
1199
|
-
datasetUri:
|
|
1200
|
-
uriAliases:
|
|
1201
|
-
status:
|
|
1206
|
+
datasetUri: firstNonEmpty(reg?.urn, ds.uri),
|
|
1207
|
+
uriAliases: firstNonEmpty(reg?.urnAliases, ds.uriAliases),
|
|
1208
|
+
status: firstNonEmpty(reg?.status, ds.editionStatus),
|
|
1202
1209
|
ordering: reg?.ordering || null,
|
|
1203
1210
|
sections: reg?.sections ? reg.sections.map(s => s.toJSON()) : [],
|
|
1204
1211
|
hasBibliography: fs.existsSync(path.join(datasetDir(ds), 'bibliography.yaml')),
|
|
1205
1212
|
hasImages: fs.existsSync(path.join(datasetDir(ds), 'images')),
|
|
1206
1213
|
});
|
|
1214
|
+
const resolvedOwner = firstNonEmpty(reg?.owner, ds.owner) ?? '';
|
|
1215
|
+
const resolvedTags = firstNonEmpty(reg?.tags, ds.tags) ?? [];
|
|
1216
|
+
const resolvedDatasetUri = firstNonEmpty(reg?.urn, ds.uri);
|
|
1217
|
+
const resolvedUriAliases = firstNonEmpty(reg?.urnAliases, ds.uriAliases);
|
|
1218
|
+
const resolvedRef = firstNonEmpty(reg?.ref, ds.ref);
|
|
1219
|
+
const resolvedRefAliases = firstNonEmpty(reg?.refAliases, ds.refAliases);
|
|
1207
1220
|
registry.push({
|
|
1208
1221
|
id: ds.id,
|
|
1209
1222
|
manifestUrl: `/data/${ds.id}/manifest.json`,
|
|
@@ -1212,15 +1225,15 @@ for (let i = 0; i < config.datasets.length; i++) {
|
|
|
1212
1225
|
description: resolvedDescription,
|
|
1213
1226
|
conceptCount: counts[ds.id] || 0,
|
|
1214
1227
|
languages: dsLanguages,
|
|
1215
|
-
owner:
|
|
1216
|
-
tags:
|
|
1228
|
+
owner: resolvedOwner,
|
|
1229
|
+
tags: resolvedTags,
|
|
1217
1230
|
color: ds.color || DS_PALETTE[i % DS_PALETTE.length],
|
|
1218
1231
|
},
|
|
1219
|
-
datasetUri:
|
|
1232
|
+
datasetUri: resolvedDatasetUri,
|
|
1220
1233
|
uriBase: config.uriBase || undefined,
|
|
1221
|
-
uriAliases:
|
|
1222
|
-
ref:
|
|
1223
|
-
refAliases:
|
|
1234
|
+
uriAliases: resolvedUriAliases,
|
|
1235
|
+
ref: resolvedRef,
|
|
1236
|
+
refAliases: resolvedRefAliases,
|
|
1224
1237
|
});
|
|
1225
1238
|
}
|
|
1226
1239
|
writeJson(path.join(PUBLIC, 'datasets.json'), registry);
|
|
@@ -1553,21 +1566,22 @@ for (const key of ['logo', 'footerLogo']) {
|
|
|
1553
1566
|
}
|
|
1554
1567
|
}
|
|
1555
1568
|
|
|
1556
|
-
// Build dataset translations map:
|
|
1569
|
+
// Build dataset translations map: register descriptions win, site-config fills gaps
|
|
1557
1570
|
const datasetTranslations = {};
|
|
1558
1571
|
for (const d of config.datasets) {
|
|
1559
1572
|
const reg = registerCache[d.id];
|
|
1560
1573
|
const translations = { ...d.translations };
|
|
1561
1574
|
|
|
1562
|
-
//
|
|
1575
|
+
// Register descriptions are authoritative; site-config translations fill gaps
|
|
1563
1576
|
if (reg?.description && typeof reg.description === 'object') {
|
|
1564
1577
|
for (const [lang, desc] of Object.entries(reg.description)) {
|
|
1565
1578
|
if (!translations[lang]) translations[lang] = {};
|
|
1566
|
-
|
|
1579
|
+
translations[lang].description = desc;
|
|
1567
1580
|
}
|
|
1568
1581
|
}
|
|
1569
1582
|
|
|
1570
|
-
//
|
|
1583
|
+
// Title falls back to register.ref when site-config doesn't supply one
|
|
1584
|
+
// (TODO.refactor/40 — replace ref with Register.name once it exists)
|
|
1571
1585
|
if (reg?.ref) {
|
|
1572
1586
|
const langs = reg.languages || [];
|
|
1573
1587
|
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
|
+
}
|