@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.
Files changed (46) hide show
  1. package/package.json +1 -1
  2. package/scripts/__tests__/standards-index.test.ts +121 -0
  3. package/scripts/build-edges.js +1 -23
  4. package/scripts/generate-data.js +206 -25
  5. package/scripts/generate-data.ts +235 -34
  6. package/scripts/lib/workers/emit-concept-worker.mjs +31 -0
  7. package/src/__tests__/app-footer.test.ts +36 -0
  8. package/src/__tests__/app-header.test.ts +32 -5
  9. package/src/__tests__/composables/use-same-standard-terms.test.ts +397 -0
  10. package/src/__tests__/concept-view.test.ts +103 -0
  11. package/src/__tests__/dataset-adapter.test.ts +150 -0
  12. package/src/__tests__/dataset-view.test.ts +41 -1
  13. package/src/__tests__/model-bridge-citation.test.ts +20 -0
  14. package/src/__tests__/same-standard-pane.test.ts +174 -0
  15. package/src/__tests__/scripts/generate-data.test.ts +78 -0
  16. package/src/__tests__/search-across-datasets.test.ts +102 -0
  17. package/src/__tests__/search-bar.test.ts +304 -0
  18. package/src/__tests__/search-results.test.ts +54 -0
  19. package/src/__tests__/search-utils.test.ts +64 -1
  20. package/src/__tests__/search-view.test.ts +194 -2
  21. package/src/__tests__/source-standard.test.ts +70 -0
  22. package/src/__tests__/ui-store.test.ts +26 -0
  23. package/src/__tests__/vocabulary-layered.test.ts +7 -7
  24. package/src/adapters/DatasetAdapter.ts +16 -8
  25. package/src/adapters/jsonld-types.ts +1 -0
  26. package/src/adapters/model-bridge.ts +1 -0
  27. package/src/adapters/standards-index.ts +7 -0
  28. package/src/adapters/types.ts +1 -0
  29. package/src/components/AppFooter.vue +7 -0
  30. package/src/components/AppHeader.vue +6 -5
  31. package/src/components/SameStandardPane.vue +70 -0
  32. package/src/components/SearchBar.vue +73 -18
  33. package/src/components/SearchResults.vue +46 -5
  34. package/src/components/site/SiteFooter.astro +6 -0
  35. package/src/composables/use-same-standard-terms.ts +106 -0
  36. package/src/config/types.ts +11 -0
  37. package/src/config/use-site-config.ts +1 -0
  38. package/src/i18n/locales/eng.yml +10 -1
  39. package/src/i18n/locales/fra.yml +10 -1
  40. package/src/stores/ui.ts +16 -1
  41. package/src/stores/vocabulary.ts +27 -31
  42. package/src/utils/search.ts +14 -1
  43. package/src/utils/source-standard.ts +34 -0
  44. package/src/views/ConceptView.vue +43 -12
  45. package/src/views/DatasetView.vue +25 -5
  46. 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.139",
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
+ });
@@ -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(...extractSourceRefs(data, registerId));
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);
@@ -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
- if (fs2.existsSync(conceptsDir)) {
1237
- for (const f of fs2.readdirSync(conceptsDir)) fs2.unlinkSync(path3.join(conceptsDir, f));
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 conceptYaml = loadConceptFile(path3.join(dir, file));
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 jsonld = yamlToJsonLd(conceptYaml, register, dsRefMaps);
1344
- writeJson(path3.join(conceptsDir, `${termid}.json`), jsonld);
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(fs2.readFileSync(path3.join(dir, file), "utf8"), termid);
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
- conceptInstances.push(conceptInstance);
1353
- fs2.writeFileSync(
1354
- path3.join(conceptsDir, `${termid}.ttl`),
1355
- conceptToTurtle(conceptInstance, { uriBase: refMaps.uriBase, registerId: register })
1356
- );
1357
- const conceptJsonLd = await emitOutputSet([conceptInstance], {
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
- writeJson(path3.join(DATA, register, "chunks", chunkFile), {
1569
+ const chunkData = {
1397
1570
  registerId: register,
1398
1571
  chunkIndex,
1399
1572
  concepts: chunk
1400
- });
1401
- chunks.push({ file: chunkFile, count: chunk.length });
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);