@glossarist/concept-browser 0.7.139 → 0.7.141
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/package.json +1 -1
- package/scripts/__tests__/standards-index.test.ts +121 -0
- package/scripts/build-edges.js +1 -23
- package/scripts/generate-data.js +206 -25
- package/scripts/generate-data.ts +235 -34
- package/scripts/lib/workers/emit-concept-worker.mjs +31 -0
- package/src/__tests__/app-footer.test.ts +36 -0
- package/src/__tests__/app-header.test.ts +32 -5
- package/src/__tests__/composables/use-same-standard-terms.test.ts +397 -0
- package/src/__tests__/concept-view.test.ts +103 -0
- package/src/__tests__/dataset-adapter.test.ts +150 -0
- package/src/__tests__/dataset-view.test.ts +41 -1
- package/src/__tests__/model-bridge-citation.test.ts +20 -0
- package/src/__tests__/same-standard-pane.test.ts +174 -0
- package/src/__tests__/scripts/generate-data.test.ts +78 -0
- package/src/__tests__/search-across-datasets.test.ts +102 -0
- package/src/__tests__/search-bar.test.ts +304 -0
- package/src/__tests__/search-results.test.ts +54 -0
- package/src/__tests__/search-utils.test.ts +64 -1
- package/src/__tests__/search-view.test.ts +194 -2
- package/src/__tests__/source-standard.test.ts +70 -0
- package/src/__tests__/ui-store.test.ts +26 -0
- package/src/__tests__/vocabulary-layered.test.ts +7 -7
- package/src/adapters/DatasetAdapter.ts +16 -8
- package/src/adapters/jsonld-types.ts +1 -0
- package/src/adapters/model-bridge.ts +1 -0
- package/src/adapters/standards-index.ts +7 -0
- package/src/adapters/types.ts +1 -0
- package/src/components/AppFooter.vue +7 -0
- package/src/components/AppHeader.vue +6 -5
- package/src/components/SameStandardPane.vue +70 -0
- package/src/components/SearchBar.vue +73 -18
- package/src/components/SearchResults.vue +46 -5
- package/src/components/site/SiteFooter.astro +6 -0
- package/src/composables/use-same-standard-terms.ts +106 -0
- package/src/config/types.ts +11 -0
- package/src/config/use-site-config.ts +1 -0
- package/src/i18n/locales/eng.yml +10 -1
- package/src/i18n/locales/fra.yml +10 -1
- package/src/stores/ui.ts +16 -1
- package/src/stores/vocabulary.ts +27 -31
- package/src/utils/search.ts +14 -1
- package/src/utils/source-standard.ts +34 -0
- package/src/views/ConceptView.vue +43 -12
- package/src/views/DatasetView.vue +25 -5
- package/src/views/SearchView.vue +9 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glossarist/concept-browser",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.141",
|
|
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": {
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { join, resolve } from 'node:path';
|
|
7
|
+
|
|
8
|
+
describe('generate-data standards index', () => {
|
|
9
|
+
it('emits unique authoritative standards per register and preserves source stats', () => {
|
|
10
|
+
const root = mkdtempSync(join(tmpdir(), 'generate-data-standards-'));
|
|
11
|
+
const conceptsDir = join(root, 'source', 'concepts');
|
|
12
|
+
const outputDir = join(root, 'public', 'data', 'fixture');
|
|
13
|
+
const configPath = join(root, 'site-config.yml');
|
|
14
|
+
const generator = resolve('scripts/generate-data.ts');
|
|
15
|
+
const tsx = resolve('node_modules/tsx/dist/loader.mjs');
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
mkdirSync(conceptsDir, { recursive: true });
|
|
19
|
+
writeFileSync(configPath, JSON.stringify({
|
|
20
|
+
id: 'fixture', title: 'Fixture', uri_base: 'https://example.test',
|
|
21
|
+
datasets: [{ id: 'fixture', local_path: 'source', languages: ['eng', 'fra'] }],
|
|
22
|
+
}));
|
|
23
|
+
const writeConcept = (id: string, sources: unknown[], engSources: unknown[] = []) => {
|
|
24
|
+
writeFileSync(join(conceptsDir, `${id}.yaml`), JSON.stringify({
|
|
25
|
+
termid: id,
|
|
26
|
+
_sources: sources,
|
|
27
|
+
eng: {
|
|
28
|
+
terms: [{ designation: `Fixture term ${id}` }],
|
|
29
|
+
sources: engSources,
|
|
30
|
+
},
|
|
31
|
+
}));
|
|
32
|
+
};
|
|
33
|
+
writeConcept('1', [
|
|
34
|
+
{ type: 'authoritative', origin: { ref: { source: 'ISO', id: '19165-2:2020', clause: '3.1' } } },
|
|
35
|
+
{ type: 'authoritative', origin: { ref: { source: 'ISO', id: '19165-2:2020', clause: '4.2' } } },
|
|
36
|
+
{ type: 'authoritative', origin: { ref: { source: 'IEC', id: '17000:2020' } } },
|
|
37
|
+
{ type: 'lineage', origin: { ref: { source: 'ISO', id: '704:2022' } } },
|
|
38
|
+
], [
|
|
39
|
+
{ type: 'authoritative', origin: { ref: { source: 'iso', id: '19165-2:2020', clause: '5.1' } } },
|
|
40
|
+
]);
|
|
41
|
+
writeConcept('2', [
|
|
42
|
+
{ type: 'authoritative', origin: { ref: { source: ' ISO ', id: ' 19165-2:2020 ', clause: '6.3' } } },
|
|
43
|
+
]);
|
|
44
|
+
writeConcept('3', [
|
|
45
|
+
{ type: 'lineage', origin: { ref: { source: 'ISO', id: '999:2020' } } },
|
|
46
|
+
], [
|
|
47
|
+
{ type: 'authoritative', origin: { ref: { source: 'ISO', id: '1087:2019' } } },
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
execFileSync(process.execPath, ['--import', tsx, generator], {
|
|
51
|
+
cwd: root,
|
|
52
|
+
env: { ...process.env, SITE_CONFIG: configPath },
|
|
53
|
+
stdio: 'pipe',
|
|
54
|
+
timeout: 30_000,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const standards = JSON.parse(readFileSync(join(outputDir, 'standards.json'), 'utf8'));
|
|
58
|
+
expect(standards).toEqual({
|
|
59
|
+
'iso 19165-2:2020': { label: 'ISO 19165-2:2020', conceptIds: ['1', '2'] },
|
|
60
|
+
'iec 17000:2020': { label: 'IEC 17000:2020', conceptIds: ['1'] },
|
|
61
|
+
'iso 1087:2019': { label: 'ISO 1087:2019', conceptIds: ['3'] },
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const stats = JSON.parse(readFileSync(join(outputDir, 'stats.json'), 'utf8'));
|
|
65
|
+
expect(stats.sourceCount).toBe(4);
|
|
66
|
+
expect(stats.sources).toEqual(expect.arrayContaining([
|
|
67
|
+
expect.objectContaining({ ref: 'ISO', conceptCount: 2, conceptIds: ['1', '3'] }),
|
|
68
|
+
expect.objectContaining({ ref: 'IEC', conceptCount: 1, conceptIds: ['1'] }),
|
|
69
|
+
]));
|
|
70
|
+
} finally {
|
|
71
|
+
rmSync(root, { recursive: true, force: true });
|
|
72
|
+
}
|
|
73
|
+
}, 60_000);
|
|
74
|
+
|
|
75
|
+
it('includes authoritative standards from languages absent from the dataset config', () => {
|
|
76
|
+
const root = mkdtempSync(join(tmpdir(), 'generate-data-unconfigured-language-'));
|
|
77
|
+
const conceptsDir = join(root, 'source', 'concepts');
|
|
78
|
+
const configPath = join(root, 'site-config.yml');
|
|
79
|
+
try {
|
|
80
|
+
mkdirSync(conceptsDir, { recursive: true });
|
|
81
|
+
writeFileSync(configPath, JSON.stringify({
|
|
82
|
+
id: 'fixture', title: 'Fixture', uri_base: 'https://example.test',
|
|
83
|
+
datasets: [{ id: 'fixture', local_path: 'source', languages: ['eng'] }],
|
|
84
|
+
}));
|
|
85
|
+
writeFileSync(join(conceptsDir, '1.yaml'), JSON.stringify({
|
|
86
|
+
termid: '1',
|
|
87
|
+
eng: {
|
|
88
|
+
terms: [{ designation: 'Term' }],
|
|
89
|
+
sources: [{ type: 'authoritative', origin: { ref: { source: 'ISO' } } }],
|
|
90
|
+
},
|
|
91
|
+
deu: {
|
|
92
|
+
terms: [{ designation: 'Begriff' }],
|
|
93
|
+
sources: [
|
|
94
|
+
{ type: 'authoritative', origin: { ref: { source: 'DIN', id: '1' } } },
|
|
95
|
+
{ type: 'lineage', origin: { ref: { source: 'DIN', id: '2' } } },
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
}));
|
|
99
|
+
execFileSync(process.execPath, [
|
|
100
|
+
'--import', resolve('node_modules/tsx/dist/loader.mjs'), resolve('scripts/generate-data.ts'),
|
|
101
|
+
], {
|
|
102
|
+
cwd: root,
|
|
103
|
+
env: { ...process.env, SITE_CONFIG: configPath },
|
|
104
|
+
stdio: 'pipe',
|
|
105
|
+
timeout: 30_000,
|
|
106
|
+
});
|
|
107
|
+
const outputDir = join(root, 'public', 'data', 'fixture');
|
|
108
|
+
expect(JSON.parse(readFileSync(join(outputDir, 'standards.json'), 'utf8'))).toEqual({
|
|
109
|
+
iso: { label: 'ISO', conceptIds: ['1'] },
|
|
110
|
+
'din 1': { label: 'DIN 1', conceptIds: ['1'] },
|
|
111
|
+
});
|
|
112
|
+
const stats = JSON.parse(readFileSync(join(outputDir, 'stats.json'), 'utf8'));
|
|
113
|
+
expect(stats.sourceCount).toBe(1);
|
|
114
|
+
expect(stats.sources).toEqual([
|
|
115
|
+
{ ref: 'ISO', types: ['authoritative'], conceptCount: 1, conceptIds: ['1'] },
|
|
116
|
+
]);
|
|
117
|
+
} finally {
|
|
118
|
+
rmSync(root, { recursive: true, force: true });
|
|
119
|
+
}
|
|
120
|
+
}, 60_000);
|
|
121
|
+
});
|
package/scripts/build-edges.js
CHANGED
|
@@ -3,28 +3,6 @@ function buildConceptUri(uriBase, registerId, conceptId) {
|
|
|
3
3
|
return `${uriBase}/dataset/${registerId}/concept/${conceptId}`;
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
-
// scripts/extract-source-refs.js
|
|
7
|
-
function extractSourceRefs(concept, registerId) {
|
|
8
|
-
const refs = /* @__PURE__ */ new Set();
|
|
9
|
-
for (const src of concept["gl:source"] || []) {
|
|
10
|
-
const origin = src["gl:origin"];
|
|
11
|
-
if (origin) {
|
|
12
|
-
const ref = origin["gl:ref"];
|
|
13
|
-
if (ref?.["gl:source"]) refs.add(ref["gl:source"]);
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
for (const lc of Object.values(concept["gl:localizedConcept"] || {})) {
|
|
17
|
-
for (const src of lc["gl:source"] || []) {
|
|
18
|
-
const origin = src["gl:origin"];
|
|
19
|
-
if (origin) {
|
|
20
|
-
const ref = origin["gl:ref"];
|
|
21
|
-
if (ref?.["gl:source"]) refs.add(ref["gl:source"]);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
return [...refs].map((source) => ({ source, registerId }));
|
|
26
|
-
}
|
|
27
|
-
|
|
28
6
|
// scripts/build-edges.ts
|
|
29
7
|
import { readFileSync, writeFileSync, readdirSync, existsSync, realpathSync } from "fs";
|
|
30
8
|
import { join, dirname } from "path";
|
|
@@ -285,7 +263,7 @@ function buildEdgesForDataset(datasetDir, registerId, uriBase, urnMap, manifest)
|
|
|
285
263
|
warnBinaryPartitiveRedundancy(edges, relations, registerId);
|
|
286
264
|
allEdges.push(...edges);
|
|
287
265
|
allPartitiveRelations.push(...relations);
|
|
288
|
-
allSourceRefs.push(...
|
|
266
|
+
allSourceRefs.push(...(void 0)(data, registerId));
|
|
289
267
|
for (const edge of edges) {
|
|
290
268
|
if (edge.type === "domain" || edge.type === "section") {
|
|
291
269
|
domainConceptCount.set(edge.target, (domainConceptCount.get(edge.target) || 0) + 1);
|
package/scripts/generate-data.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// scripts/generate-data.ts
|
|
2
2
|
import fs2 from "fs";
|
|
3
|
+
import os from "os";
|
|
3
4
|
import path3 from "path";
|
|
5
|
+
import { Worker as WorkerThread } from "node:worker_threads";
|
|
6
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
4
7
|
import yaml4 from "js-yaml";
|
|
5
8
|
import {
|
|
6
9
|
naturalSort,
|
|
@@ -89,6 +92,28 @@ function loadSiteConfig(args = []) {
|
|
|
89
92
|
return { config: config2, configPath };
|
|
90
93
|
}
|
|
91
94
|
|
|
95
|
+
// src/utils/source-standard.ts
|
|
96
|
+
function standardLabel(ref) {
|
|
97
|
+
const parts = [ref?.source, ref?.id].map((part) => part?.trim().replace(/\s+/g, " ") ?? "").filter(Boolean);
|
|
98
|
+
return parts.join(" ") || null;
|
|
99
|
+
}
|
|
100
|
+
function standardKey(ref) {
|
|
101
|
+
return standardLabel(ref)?.toLowerCase() ?? null;
|
|
102
|
+
}
|
|
103
|
+
function collectStandardKeys(concept) {
|
|
104
|
+
const sources = [
|
|
105
|
+
...concept.sources,
|
|
106
|
+
...concept.languages.flatMap((lang) => concept.localization(lang)?.sources ?? [])
|
|
107
|
+
];
|
|
108
|
+
const keys = /* @__PURE__ */ new Set();
|
|
109
|
+
for (const source of sources) {
|
|
110
|
+
if (source.type !== "authoritative") continue;
|
|
111
|
+
const key = standardKey(source.origin?.ref);
|
|
112
|
+
if (key !== null) keys.add(key);
|
|
113
|
+
}
|
|
114
|
+
return [...keys];
|
|
115
|
+
}
|
|
116
|
+
|
|
92
117
|
// scripts/lib/concept-groups.ts
|
|
93
118
|
function getGroups(conceptYaml) {
|
|
94
119
|
if (conceptYaml.eng && conceptYaml.eng.groups) return conceptYaml.eng.groups;
|
|
@@ -1229,18 +1254,104 @@ function getPrimaryDesignation(conceptYaml) {
|
|
|
1229
1254
|
}
|
|
1230
1255
|
return descs;
|
|
1231
1256
|
}
|
|
1257
|
+
async function inlineEmission(job, register, uriBase) {
|
|
1258
|
+
try {
|
|
1259
|
+
const outputs = await emitOutputSet([job.conceptInstance], {
|
|
1260
|
+
registerId: register,
|
|
1261
|
+
uriBase,
|
|
1262
|
+
formats: ["jsonld"]
|
|
1263
|
+
});
|
|
1264
|
+
return {
|
|
1265
|
+
termid: job.termid,
|
|
1266
|
+
ttl: conceptToTurtle(job.conceptInstance, { uriBase, registerId: register }),
|
|
1267
|
+
jsonld: outputs.jsonld,
|
|
1268
|
+
tbx: conceptsToTbx([job.conceptInstance], { registerId: register }) || null
|
|
1269
|
+
};
|
|
1270
|
+
} catch (e) {
|
|
1271
|
+
return { termid: job.termid, error: String(e && e.message || e) };
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
function createEmissionPool(register, uriBase) {
|
|
1275
|
+
const configured = Number(process.env.GCB_WORKERS);
|
|
1276
|
+
if (Number.isFinite(configured) && configured <= 0) return null;
|
|
1277
|
+
const size = Number.isFinite(configured) ? configured : Math.max(1, Math.min(4, (os.availableParallelism ? os.availableParallelism() : os.cpus().length) - 1));
|
|
1278
|
+
const workerPath = path3.join(__dirname3, "lib", "workers", "emit-concept-worker.mjs");
|
|
1279
|
+
let pool;
|
|
1280
|
+
try {
|
|
1281
|
+
pool = Array.from({ length: size }, () => new WorkerThread(workerPath, { workerData: { registerId: register, uriBase } }));
|
|
1282
|
+
} catch {
|
|
1283
|
+
return null;
|
|
1284
|
+
}
|
|
1285
|
+
const active = /* @__PURE__ */ new Map();
|
|
1286
|
+
const pending = [];
|
|
1287
|
+
const idle = /* @__PURE__ */ new Set();
|
|
1288
|
+
function dispatch() {
|
|
1289
|
+
while (pending.length > 0) {
|
|
1290
|
+
const worker = idle.values().next();
|
|
1291
|
+
if (worker.done) return;
|
|
1292
|
+
idle.delete(worker.value);
|
|
1293
|
+
const entry = pending.shift();
|
|
1294
|
+
active.set(worker.value, entry);
|
|
1295
|
+
worker.value.postMessage({ termid: entry.job.termid, sourceText: entry.job.sourceText });
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
for (const worker of pool) {
|
|
1299
|
+
idle.add(worker);
|
|
1300
|
+
worker.on("message", (msg) => {
|
|
1301
|
+
const entry = active.get(worker);
|
|
1302
|
+
active.delete(worker);
|
|
1303
|
+
idle.add(worker);
|
|
1304
|
+
entry?.resolve(msg);
|
|
1305
|
+
dispatch();
|
|
1306
|
+
});
|
|
1307
|
+
const fail = (err) => {
|
|
1308
|
+
const entry = active.get(worker);
|
|
1309
|
+
active.delete(worker);
|
|
1310
|
+
idle.delete(worker);
|
|
1311
|
+
pool = pool.filter((w) => w !== worker);
|
|
1312
|
+
entry?.reject(err);
|
|
1313
|
+
dispatch();
|
|
1314
|
+
};
|
|
1315
|
+
worker.on("error", fail);
|
|
1316
|
+
worker.on("exit", () => fail(new Error("emission worker exited")));
|
|
1317
|
+
}
|
|
1318
|
+
return {
|
|
1319
|
+
emit(job) {
|
|
1320
|
+
return new Promise((resolve2, reject) => {
|
|
1321
|
+
pending.push({ job, resolve: resolve2, reject });
|
|
1322
|
+
dispatch();
|
|
1323
|
+
});
|
|
1324
|
+
},
|
|
1325
|
+
async close() {
|
|
1326
|
+
await Promise.allSettled(pool.map((w) => w.terminate()));
|
|
1327
|
+
}
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1232
1330
|
async function processDataset(dir, register, opts) {
|
|
1233
1331
|
const files = fs2.readdirSync(dir).filter((f) => f.endsWith(".yaml")).sort((a, b) => naturalSort(a.replace(".yaml", ""), b.replace(".yaml", "")));
|
|
1234
1332
|
console.log(`Processing ${register}: ${files.length} files`);
|
|
1235
1333
|
const conceptsDir = path3.join(DATA, register, "concepts");
|
|
1236
|
-
|
|
1237
|
-
|
|
1334
|
+
fs2.mkdirSync(conceptsDir, { recursive: true });
|
|
1335
|
+
const EMISSION_CACHE_VERSION = 1;
|
|
1336
|
+
const EMISSION_EXTENSIONS = ["json", "ttl", "jsonld", "tbx", "yaml"];
|
|
1337
|
+
const cacheDir = path3.join(ROOT, ".gcb-cache");
|
|
1338
|
+
const cachePath = path3.join(cacheDir, `${register}.json`);
|
|
1339
|
+
const emissionCache = {
|
|
1340
|
+
version: EMISSION_CACHE_VERSION,
|
|
1341
|
+
files: {}
|
|
1342
|
+
};
|
|
1343
|
+
try {
|
|
1344
|
+
const loaded = JSON.parse(fs2.readFileSync(cachePath, "utf8"));
|
|
1345
|
+
if (loaded?.version === EMISSION_CACHE_VERSION && loaded.files) emissionCache.files = loaded.files;
|
|
1346
|
+
} catch {
|
|
1238
1347
|
}
|
|
1239
1348
|
const concepts = [];
|
|
1240
1349
|
const conceptInstances = [];
|
|
1241
1350
|
const langTermCounts = {};
|
|
1242
1351
|
const langDefCounts = {};
|
|
1243
1352
|
const availableFormats = ["ttl", "jsonld", "yaml", "tbx", "csv"];
|
|
1353
|
+
const producedOutputs = /* @__PURE__ */ new Set();
|
|
1354
|
+
const emissionCacheFiles = {};
|
|
1244
1355
|
const designationLookup = /* @__PURE__ */ new Map();
|
|
1245
1356
|
for (const file of files) {
|
|
1246
1357
|
try {
|
|
@@ -1275,7 +1386,26 @@ async function processDataset(dir, register, opts) {
|
|
|
1275
1386
|
withPlurality: 0
|
|
1276
1387
|
}
|
|
1277
1388
|
};
|
|
1389
|
+
const standards = /* @__PURE__ */ new Map();
|
|
1278
1390
|
const STATS_PROCESSORS = [
|
|
1391
|
+
function collectStandards(cy, termid) {
|
|
1392
|
+
const concept = {
|
|
1393
|
+
sources: cy._sources || [],
|
|
1394
|
+
languages: Object.keys(cy).filter((lang) => !lang.startsWith("_") && cy[lang] && typeof cy[lang] === "object" && !Array.isArray(cy[lang])),
|
|
1395
|
+
localization: (lang) => cy[lang]
|
|
1396
|
+
};
|
|
1397
|
+
const sources = [
|
|
1398
|
+
...concept.sources,
|
|
1399
|
+
...concept.languages.flatMap((lang) => concept.localization(lang).sources || [])
|
|
1400
|
+
];
|
|
1401
|
+
for (const key of collectStandardKeys(concept)) {
|
|
1402
|
+
if (!standards.has(key)) {
|
|
1403
|
+
const source = sources.find((source2) => source2.type === "authoritative" && standardKey(source2.origin?.ref) === key);
|
|
1404
|
+
standards.set(key, { label: standardLabel(source.origin.ref), conceptIds: [] });
|
|
1405
|
+
}
|
|
1406
|
+
standards.get(key).conceptIds.push(termid);
|
|
1407
|
+
}
|
|
1408
|
+
},
|
|
1279
1409
|
function collectSources(cy, termid, s) {
|
|
1280
1410
|
const refs = /* @__PURE__ */ new Set();
|
|
1281
1411
|
const allSources = [
|
|
@@ -1334,38 +1464,44 @@ async function processDataset(dir, register, opts) {
|
|
|
1334
1464
|
}
|
|
1335
1465
|
}
|
|
1336
1466
|
];
|
|
1467
|
+
const loopStart = Date.now();
|
|
1468
|
+
const emissionJobs = [];
|
|
1337
1469
|
for (let i = 0; i < files.length; i++) {
|
|
1338
1470
|
const file = files[i];
|
|
1471
|
+
if (i > 0 && i % 250 === 0) console.log(` ${i}/${files.length} concepts`);
|
|
1339
1472
|
try {
|
|
1340
|
-
const
|
|
1473
|
+
const filePath = path3.join(dir, file);
|
|
1474
|
+
const sourceText = fs2.readFileSync(filePath, "utf8");
|
|
1475
|
+
const sourceHash = createHash2("sha256").update(sourceText).digest("hex");
|
|
1476
|
+
const conceptYaml = loadConceptFile(filePath);
|
|
1341
1477
|
if (!conceptYaml || !conceptYaml.termid) continue;
|
|
1342
1478
|
const termid = String(conceptYaml.termid);
|
|
1343
|
-
const
|
|
1344
|
-
|
|
1479
|
+
const cached = emissionCache.files[file];
|
|
1480
|
+
const unchanged = cached?.hash === sourceHash && EMISSION_EXTENSIONS.every((ext) => fs2.existsSync(path3.join(conceptsDir, `${termid}.${ext}`)));
|
|
1345
1481
|
let conceptInstance = null;
|
|
1346
1482
|
try {
|
|
1347
|
-
conceptInstance = parseConceptYaml(
|
|
1483
|
+
conceptInstance = parseConceptYaml(sourceText, termid);
|
|
1348
1484
|
} catch (e) {
|
|
1349
1485
|
console.warn(` ${termid}: canonical parse failed (${e.message}); skipping glossarist exports`);
|
|
1350
1486
|
}
|
|
1351
|
-
if (conceptInstance)
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
)
|
|
1357
|
-
|
|
1358
|
-
registerId: register,
|
|
1359
|
-
uriBase: refMaps.uriBase,
|
|
1360
|
-
formats: ["jsonld"]
|
|
1361
|
-
});
|
|
1362
|
-
fs2.writeFileSync(path3.join(conceptsDir, `${termid}.jsonld`), conceptJsonLd.jsonld);
|
|
1363
|
-
const tbxContent = conceptsToTbx([conceptInstance], { registerId: register });
|
|
1364
|
-
if (tbxContent) {
|
|
1365
|
-
fs2.writeFileSync(path3.join(conceptsDir, `${termid}.tbx`), tbxContent);
|
|
1487
|
+
if (conceptInstance) conceptInstances.push(conceptInstance);
|
|
1488
|
+
if (!unchanged) {
|
|
1489
|
+
const jsonld = yamlToJsonLd(conceptYaml, register, dsRefMaps);
|
|
1490
|
+
writeJson(path3.join(conceptsDir, `${termid}.json`), jsonld);
|
|
1491
|
+
fs2.copyFileSync(filePath, path3.join(conceptsDir, `${termid}.yaml`));
|
|
1492
|
+
if (conceptInstance) {
|
|
1493
|
+
emissionJobs.push({ file, termid, sourceText, sourceHash, conceptInstance });
|
|
1366
1494
|
}
|
|
1495
|
+
} else {
|
|
1496
|
+
emissionCacheFiles[file] = { hash: sourceHash, termid };
|
|
1497
|
+
}
|
|
1498
|
+
producedOutputs.add(`${termid}.json`);
|
|
1499
|
+
producedOutputs.add(`${termid}.yaml`);
|
|
1500
|
+
if (conceptInstance) {
|
|
1501
|
+
producedOutputs.add(`${termid}.ttl`);
|
|
1502
|
+
producedOutputs.add(`${termid}.jsonld`);
|
|
1503
|
+
producedOutputs.add(`${termid}.tbx`);
|
|
1367
1504
|
}
|
|
1368
|
-
fs2.copyFileSync(path3.join(dir, file), path3.join(conceptsDir, `${termid}.yaml`));
|
|
1369
1505
|
concepts.push({
|
|
1370
1506
|
id: termid,
|
|
1371
1507
|
designations: getPrimaryDesignation(conceptYaml),
|
|
@@ -1379,12 +1515,49 @@ async function processDataset(dir, register, opts) {
|
|
|
1379
1515
|
console.warn(` Skipping ${file}: ${e.message}`);
|
|
1380
1516
|
}
|
|
1381
1517
|
}
|
|
1518
|
+
if (emissionJobs.length > 0) {
|
|
1519
|
+
const jobsByTermid = new Map(emissionJobs.map((j) => [j.termid, j]));
|
|
1520
|
+
const pool = createEmissionPool(register, refMaps.uriBase);
|
|
1521
|
+
const emissionStart = Date.now();
|
|
1522
|
+
try {
|
|
1523
|
+
const results = await Promise.all(emissionJobs.map((job) => {
|
|
1524
|
+
if (!pool) return inlineEmission(job, register, refMaps.uriBase);
|
|
1525
|
+
return pool.emit(job).catch(() => inlineEmission(job, register, refMaps.uriBase));
|
|
1526
|
+
}));
|
|
1527
|
+
for (const result of results) {
|
|
1528
|
+
if (result.error) {
|
|
1529
|
+
console.warn(` ${result.termid}: canonical parse failed (${result.error}); skipping glossarist exports`);
|
|
1530
|
+
const job2 = jobsByTermid.get(result.termid);
|
|
1531
|
+
if (job2) delete emissionCacheFiles[job2.file];
|
|
1532
|
+
for (const ext of ["ttl", "jsonld", "tbx"]) producedOutputs.delete(`${result.termid}.${ext}`);
|
|
1533
|
+
continue;
|
|
1534
|
+
}
|
|
1535
|
+
fs2.writeFileSync(path3.join(conceptsDir, `${result.termid}.ttl`), result.ttl);
|
|
1536
|
+
fs2.writeFileSync(path3.join(conceptsDir, `${result.termid}.jsonld`), result.jsonld);
|
|
1537
|
+
if (result.tbx) fs2.writeFileSync(path3.join(conceptsDir, `${result.termid}.tbx`), result.tbx);
|
|
1538
|
+
const job = jobsByTermid.get(result.termid);
|
|
1539
|
+
if (job) emissionCacheFiles[job.file] = { hash: job.sourceHash, termid: job.termid };
|
|
1540
|
+
}
|
|
1541
|
+
console.log(` Interchange exports: ${((Date.now() - emissionStart) / 1e3).toFixed(1)}s (${emissionJobs.length} concepts, ${pool ? "parallel" : "inline"})`);
|
|
1542
|
+
} finally {
|
|
1543
|
+
if (pool) await pool.close();
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
for (const entry of fs2.readdirSync(conceptsDir)) {
|
|
1547
|
+
if (!producedOutputs.has(entry)) fs2.unlinkSync(path3.join(conceptsDir, entry));
|
|
1548
|
+
}
|
|
1549
|
+
fs2.mkdirSync(cacheDir, { recursive: true });
|
|
1550
|
+
fs2.writeFileSync(cachePath, JSON.stringify({ version: EMISSION_CACHE_VERSION, files: emissionCacheFiles }));
|
|
1551
|
+
console.log(` Per-concept emissions: ${((Date.now() - loopStart) / 1e3).toFixed(1)}s`);
|
|
1552
|
+
const aggregateStart = Date.now();
|
|
1382
1553
|
const aggregate = await emitOutputSet(conceptInstances, {
|
|
1383
1554
|
registerId: register,
|
|
1384
1555
|
uriBase: refMaps.uriBase,
|
|
1556
|
+
formats: ["csv", "jsonld"],
|
|
1385
1557
|
languageOrder: opts.languageOrder || opts.languages,
|
|
1386
1558
|
conceptUriPrefix: buildConceptUriPrefix(refMaps.siteOrigin, register)
|
|
1387
1559
|
});
|
|
1560
|
+
console.log(` Aggregate csv+jsonld: ${((Date.now() - aggregateStart) / 1e3).toFixed(1)}s`);
|
|
1388
1561
|
fs2.writeFileSync(path3.join(DATA, register, `${register}.csv`), aggregate.csv);
|
|
1389
1562
|
fs2.writeFileSync(path3.join(DATA, register, `${register}.jsonld`), aggregate.jsonld);
|
|
1390
1563
|
const CHUNK_SIZE = 500;
|
|
@@ -1393,12 +1566,17 @@ async function processDataset(dir, register, opts) {
|
|
|
1393
1566
|
const chunk = concepts.slice(i, i + CHUNK_SIZE);
|
|
1394
1567
|
const chunkIndex = Math.floor(i / CHUNK_SIZE);
|
|
1395
1568
|
const chunkFile = `index-${String(chunkIndex).padStart(4, "0")}.json`;
|
|
1396
|
-
|
|
1569
|
+
const chunkData = {
|
|
1397
1570
|
registerId: register,
|
|
1398
1571
|
chunkIndex,
|
|
1399
1572
|
concepts: chunk
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1573
|
+
};
|
|
1574
|
+
const chunkContent = JSON.stringify(chunkData);
|
|
1575
|
+
const chunkPath = path3.join(DATA, register, "chunks", chunkFile);
|
|
1576
|
+
fs2.mkdirSync(path3.dirname(chunkPath), { recursive: true });
|
|
1577
|
+
fs2.writeFileSync(chunkPath, chunkContent);
|
|
1578
|
+
const hash = createHash2("sha256").update(chunkContent).digest("hex").slice(0, 12);
|
|
1579
|
+
chunks.push({ file: chunkFile, count: chunk.length, hash });
|
|
1402
1580
|
}
|
|
1403
1581
|
const summary = concepts.map((c) => ({
|
|
1404
1582
|
id: c.id,
|
|
@@ -1490,6 +1668,7 @@ async function processDataset(dir, register, opts) {
|
|
|
1490
1668
|
conceptIds: s.conceptIds
|
|
1491
1669
|
})).sort((a, b) => b.conceptCount - a.conceptCount);
|
|
1492
1670
|
const totalRelationships = Object.values(stats.relTypeCounts).reduce((a, b) => a + b, 0);
|
|
1671
|
+
writeJson(path3.join(DATA, register, "standards.json"), Object.fromEntries(standards));
|
|
1493
1672
|
writeJson(path3.join(DATA, register, "stats.json"), {
|
|
1494
1673
|
sourceCount: sourceStats.length,
|
|
1495
1674
|
sources: sourceStats,
|
|
@@ -1532,7 +1711,9 @@ async function processDataset(dir, register, opts) {
|
|
|
1532
1711
|
if (opts.status) manifest.editionStatus = opts.status;
|
|
1533
1712
|
if (opts.ordering) manifest.ordering = opts.ordering;
|
|
1534
1713
|
if (opts.sections && opts.sections.length > 0) manifest.sections = opts.sections;
|
|
1714
|
+
const rdfStart = Date.now();
|
|
1535
1715
|
await writeDatasetRdf(register, manifest, concepts, refMaps, opts);
|
|
1716
|
+
console.log(` Dataset RDF: ${((Date.now() - rdfStart) / 1e3).toFixed(1)}s`);
|
|
1536
1717
|
addBulkFormat(`${register}.ttl`);
|
|
1537
1718
|
manifest.bulkFormats = opts.bulkExports?.length ? bulkFormats.filter((f) => opts.bulkExports.includes(f.format)) : bulkFormats;
|
|
1538
1719
|
writeJson(path3.join(DATA, register, "manifest.json"), manifest);
|