@glossarist/concept-browser 0.7.114 → 0.7.116

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 (103) hide show
  1. package/README.md +45 -2
  2. package/cli/index.mjs +10 -13
  3. package/cli/index.ts +9 -11
  4. package/data/concept-model/shapes/glossarist.concept.yaml.schema.json +255 -0
  5. package/package.json +18 -12
  6. package/scripts/__tests__/{bibliography.test.mjs → bibliography.test.ts} +1 -1
  7. package/scripts/__tests__/{cli-script-contract.test.mjs → cli-script-contract.test.ts} +3 -3
  8. package/scripts/__tests__/{concept-groups.test.mjs → concept-groups.test.ts} +1 -1
  9. package/scripts/__tests__/{doctor.test.mjs → doctor.test.ts} +1 -1
  10. package/scripts/__tests__/{fetch-datasets.test.mjs → fetch-datasets.test.ts} +1 -1
  11. package/scripts/__tests__/{first-non-empty.test.mjs → first-non-empty.test.ts} +1 -1
  12. package/scripts/__tests__/{process-about-pages.test.mjs → process-about-pages.test.ts} +1 -1
  13. package/scripts/bridge-to-astro.js +132 -0
  14. package/scripts/{bridge-to-astro.mjs → bridge-to-astro.ts} +2 -4
  15. package/scripts/build-edges.js +242 -336
  16. package/scripts/build-edges.ts +572 -0
  17. package/scripts/build-scripts.mjs +22 -0
  18. package/scripts/check-syntax.js +81 -0
  19. package/scripts/{check-syntax.mjs → check-syntax.ts} +20 -13
  20. package/scripts/doctor.js +278 -0
  21. package/scripts/extract-source-refs.js +16 -24
  22. package/scripts/extract-source-refs.ts +32 -0
  23. package/scripts/fetch-datasets.js +278 -0
  24. package/scripts/{fetch-datasets.mjs → fetch-datasets.ts} +2 -2
  25. package/scripts/generate-404.js +10 -11
  26. package/scripts/generate-404.ts +15 -0
  27. package/scripts/generate-data.js +2220 -0
  28. package/scripts/{generate-data.mjs → generate-data.ts} +73 -460
  29. package/scripts/generate-ontology-data.js +139 -0
  30. package/scripts/generate-ontology-schema.js +472 -0
  31. package/scripts/{generate-ontology-schema.mjs → generate-ontology-schema.ts} +2 -2
  32. package/scripts/lib/{build-cache.mjs → build-cache.ts} +3 -2
  33. package/scripts/lib/concept-formats.ts +206 -0
  34. package/scripts/lib/concept-uri.ts +14 -0
  35. package/scripts/lib/{dataset-turtle.mjs → dataset-turtle.ts} +4 -3
  36. package/scripts/lib/harmonize-concept.ts +86 -0
  37. package/scripts/lib/jsonld-emitter.ts +169 -0
  38. package/scripts/lib/{local-path-safety.mjs → local-path-safety.ts} +6 -2
  39. package/scripts/lib/yaml-types.ts +262 -0
  40. package/scripts/load-site-config.js +83 -0
  41. package/scripts/migrate-v1-to-v3.js +113 -0
  42. package/scripts/normalize-yaml.js +96 -0
  43. package/scripts/process-about-pages.js +241 -0
  44. package/scripts/{process-about-pages.mjs → process-about-pages.ts} +1 -1
  45. package/scripts/smoke.js +211 -0
  46. package/scripts/{smoke.mjs → smoke.ts} +4 -3
  47. package/scripts/sync-concept-model.js +63 -0
  48. package/scripts/validate-shacl.js +152 -0
  49. package/scripts/{validate-shacl.mjs → validate-shacl.ts} +5 -5
  50. package/scripts/validate-yaml.js +132 -0
  51. package/scripts/validate-yaml.ts +144 -0
  52. package/src/__tests__/extract-source-refs.test.ts +2 -3
  53. package/src/__tests__/perf/serialization-perf.test.ts +5 -5
  54. package/src/__tests__/published-package-contract.test.ts +84 -0
  55. package/src/__tests__/scripts/agents-turtle.test.ts +1 -1
  56. package/src/__tests__/scripts/bibliography-turtle.test.ts +1 -1
  57. package/src/__tests__/scripts/build-activity-turtle.test.ts +1 -1
  58. package/src/__tests__/scripts/build-cache.test.ts +1 -1
  59. package/src/__tests__/scripts/build-integration.test.ts +19 -19
  60. package/src/__tests__/scripts/dataset-turtle.test.ts +1 -1
  61. package/src/__tests__/scripts/migrate-v1-to-v3.test.ts +1 -1
  62. package/src/__tests__/scripts/normalize-yaml.test.ts +1 -1
  63. package/src/__tests__/scripts/turtle-escape.test.ts +1 -1
  64. package/src/__tests__/scripts/validate-yaml.test.ts +76 -0
  65. package/src/__tests__/scripts/version-turtle.test.ts +1 -1
  66. package/src/__tests__/scripts/vocab-turtle.test.ts +1 -1
  67. package/src/__tests__/scripts-syntax.test.ts +4 -4
  68. package/src/__tests__/use-rdf-document.test.ts +2 -3
  69. package/src/__tests__/utils/content-renderer-xss.test.ts +57 -0
  70. package/src/__tests__/validate-shacl.test.ts +3 -3
  71. package/src/adapters/wire/index.ts +69 -0
  72. package/src/data/ontology-schema.json +1336 -183
  73. package/src/utils/content-renderer.ts +7 -3
  74. package/src/utils/url-safety.ts +1 -5
  75. package/tsconfig.json +2 -2
  76. package/vite.config.ts +1 -1
  77. package/src/__tests__/composables/use-concept-edges-v2.test.ts +0 -167
  78. package/src/__tests__/composables/use-concept-edges-v2.test.ts.bak +0 -166
  79. package/src/types/agents-version-turtle.d.ts +0 -27
  80. package/src/types/bibliography-turtle.d.ts +0 -12
  81. package/src/types/build-activity-turtle.d.ts +0 -16
  82. package/src/types/build-cache.d.ts +0 -20
  83. package/src/types/dataset-turtle.d.ts +0 -32
  84. package/src/types/normalize-yaml.d.ts +0 -16
  85. package/src/types/turtle-escape.d.ts +0 -6
  86. package/src/types/vocab-turtle.d.ts +0 -13
  87. /package/scripts/{doctor.mjs → doctor.ts} +0 -0
  88. /package/scripts/{generate-ontology-data.mjs → generate-ontology-data.ts} +0 -0
  89. /package/scripts/lib/{agents-turtle.mjs → agents-turtle.ts} +0 -0
  90. /package/scripts/lib/{bibliography-turtle.mjs → bibliography-turtle.ts} +0 -0
  91. /package/scripts/lib/{bibliography.mjs → bibliography.ts} +0 -0
  92. /package/scripts/lib/build/{image-assets.mjs → image-assets.ts} +0 -0
  93. /package/scripts/lib/build/{non-verbal-consumer.mjs → non-verbal-consumer.ts} +0 -0
  94. /package/scripts/lib/{build-activity-turtle.mjs → build-activity-turtle.ts} +0 -0
  95. /package/scripts/lib/{concept-groups.mjs → concept-groups.ts} +0 -0
  96. /package/scripts/lib/{first-non-empty.mjs → first-non-empty.ts} +0 -0
  97. /package/scripts/lib/{turtle-escape.mjs → turtle-escape.ts} +0 -0
  98. /package/scripts/lib/{version-turtle.mjs → version-turtle.ts} +0 -0
  99. /package/scripts/lib/{vocab-turtle.mjs → vocab-turtle.ts} +0 -0
  100. /package/scripts/{load-site-config.mjs → load-site-config.ts} +0 -0
  101. /package/scripts/{migrate-v1-to-v3.mjs → migrate-v1-to-v3.ts} +0 -0
  102. /package/scripts/{normalize-yaml.mjs → normalize-yaml.ts} +0 -0
  103. /package/scripts/{sync-concept-model.mjs → sync-concept-model.ts} +0 -0
@@ -0,0 +1,572 @@
1
+ import { buildConceptUri } from './lib/concept-uri';
2
+
3
+ const VALID_COMPLETENESS = new Set(['complete', 'partial']);
4
+
5
+ function validateCompleteness(value) {
6
+ if (value == null) return 'complete';
7
+ if (!VALID_COMPLETENESS.has(value)) {
8
+ throw new Error(
9
+ `Invalid partitive relation completeness: "${value}". Allowed: complete, partial`,
10
+ );
11
+ }
12
+ return value;
13
+ }
14
+
15
+ /**
16
+ * Resolve a partitive member's multiplicity from JSON-LD.
17
+ *
18
+ * Reads gl:multiplicity first (v3 wire shape, ISO 704:2022). Falls back
19
+ * to migrating from gl:certainty (v2 wire shape) for one release cycle
20
+ * until upstream emits multiplicity natively.
21
+ *
22
+ * confirmed → compulsory
23
+ * possible → optional
24
+ */
25
+ const VALID_PRESENCE = new Set(['required', 'optional']);
26
+ const VALID_COUNT = new Set(['exactly_one', 'at_least_one', 'multiple']);
27
+ const LEGACY_MULTIPLICITY_MAP = {
28
+ compulsory: { presence: 'required', count: 'exactly_one' },
29
+ optional: { presence: 'optional', count: 'exactly_one' },
30
+ compulsory_multiple: { presence: 'required', count: 'multiple' },
31
+ optional_multiple: { presence: 'optional', count: 'multiple' },
32
+ compulsory_at_least_one: { presence: 'required', count: 'at_least_one' },
33
+ };
34
+
35
+ function resolvePresence(member) {
36
+ const p = member['gl:presence'] ?? member.presence;
37
+ if (p != null) {
38
+ if (!VALID_PRESENCE.has(p)) throw new Error(`Invalid presence: "${p}"`);
39
+ return p;
40
+ }
41
+ return migrateMultiplicity(member).presence;
42
+ }
43
+
44
+ function resolveCount(member) {
45
+ const c = member['gl:count'] ?? member.count;
46
+ if (c != null) {
47
+ if (!VALID_COUNT.has(c)) throw new Error(`Invalid count: "${c}"`);
48
+ return c;
49
+ }
50
+ return migrateMultiplicity(member).count;
51
+ }
52
+
53
+ function migrateMultiplicity(member) {
54
+ const m = member['gl:multiplicity'] ?? member.multiplicity;
55
+ if (m && LEGACY_MULTIPLICITY_MAP[m]) return LEGACY_MULTIPLICITY_MAP[m];
56
+ const certainty = member['gl:certainty'] ?? member.certainty;
57
+ if (certainty === 'possible') return { presence: 'optional', count: 'exactly_one' };
58
+ return { presence: 'required', count: 'exactly_one' };
59
+ }
60
+
61
+ function resolveIsDelimiting(member) {
62
+ return member['gl:isDelimiting'] === true || member.isDelimiting === true;
63
+ }
64
+
65
+ function normalizeLocalizedString(content) {
66
+ if (content == null) return undefined;
67
+ if (typeof content === 'string') return { default: content };
68
+ return content;
69
+ }
70
+
71
+
72
+ /**
73
+ * Pre-computes cross-reference and domain edges for each dataset.
74
+ * Reads all concept JSON files, extracts structured references and
75
+ * authoritative sources (domains), and writes edges.json + domain-nodes.json.
76
+ *
77
+ * Usage: node scripts/build-edges.js
78
+ */
79
+ import { extractSourceRefs } from './extract-source-refs.js';
80
+ import { readFileSync, writeFileSync, readdirSync, existsSync, realpathSync } from 'fs';
81
+ import { join, dirname, resolve } from 'path';
82
+ import { fileURLToPath } from 'url';
83
+ import yaml from 'js-yaml';
84
+ import { Register } from 'glossarist';
85
+
86
+ const __dirname = dirname(fileURLToPath(import.meta.url));
87
+ const ROOT = process.cwd();
88
+ const DATA_DIR = join(ROOT, 'public', 'data');
89
+
90
+ // --- Normalization ---
91
+
92
+ function slugify(text) {
93
+ return text.toLowerCase().replace(/[^\w\s-]/g, '').replace(/[\s/]+/g, '-');
94
+ }
95
+
96
+ // --- Extractors (open/closed: add new extractors to EXTRACTORS array) ---
97
+
98
+ function extractReferences(concept: Record<string, any>, registerId: string) {
99
+ const edges: Record<string, any>[] = [];
100
+ const sourceUri = concept['@id'];
101
+ for (const [lang, lc] of Object.entries(concept['gl:localizedConcept'] || {}) as [string, any][]) {
102
+ if (lc['gl:references']) {
103
+ for (const ref of lc['gl:references'] as Record<string, any>[]) {
104
+ if (ref['@id'] && ref['@id'] !== sourceUri) {
105
+ const edge: Record<string, any> = {
106
+ source: sourceUri,
107
+ target: ref['@id'],
108
+ type: ref['@id'].startsWith('cite:') ? 'citation' : 'references',
109
+ label: ref['gl:term'] || undefined,
110
+ register: registerId,
111
+ lang,
112
+ };
113
+ if (ref['gl:sourceId']) edge.sourceId = ref['gl:sourceId'];
114
+ edges.push(edge);
115
+ }
116
+ }
117
+ }
118
+ }
119
+ return edges;
120
+ }
121
+
122
+ function extractDomains(concept, registerId, uriBase) {
123
+ if (!uriBase) throw new Error('build-edges: uriBase is required — set uriBase in site-config.yml');
124
+ const base = uriBase;
125
+ const edges = [];
126
+ const sourceUri = concept['@id'];
127
+ const seen = new Set();
128
+
129
+ // v3 managed concept-level section/domain references
130
+ const domains = concept['gl:domain'];
131
+ if (Array.isArray(domains)) {
132
+ for (const d of domains) {
133
+ const conceptId = d['gl:conceptId'] || d.concept_id;
134
+ if (conceptId) {
135
+ const refType = d['gl:refType'] || d.ref_type || 'domain';
136
+ const isSection = refType === 'section';
137
+ const edgeType = isSection ? 'section' : 'domain';
138
+ const domainUri = `${base}/${registerId}/domain/${conceptId}`;
139
+ if (!seen.has(domainUri)) {
140
+ seen.add(domainUri);
141
+ edges.push({
142
+ source: sourceUri,
143
+ target: domainUri,
144
+ type: edgeType,
145
+ label: conceptId,
146
+ register: registerId,
147
+ });
148
+ }
149
+ }
150
+ }
151
+ }
152
+
153
+ // Legacy: localized domain strings
154
+ const lcs = concept['gl:localizedConcept'] || {};
155
+ const langs = Object.keys(lcs);
156
+ for (const lang of langs) {
157
+ const domain = lcs[lang]['gl:domain'];
158
+ if (domain && !seen.has(domain)) {
159
+ seen.add(domain);
160
+ edges.push({
161
+ source: sourceUri,
162
+ target: `${base}/${registerId}/domain/${slugify(domain)}`,
163
+ type: 'domain',
164
+ label: domain,
165
+ register: registerId,
166
+ });
167
+ }
168
+ }
169
+ return edges;
170
+ }
171
+
172
+ function extractRelated(concept, registerId, uriBase, urnMap) {
173
+ const edges = [];
174
+ const sourceUri = concept['@id'];
175
+ for (const r of concept['gl:related'] || []) {
176
+ const ref = r['gl:ref'];
177
+ if (!ref) continue;
178
+ const source = ref['gl:source'] || ref['source'];
179
+ const id = ref['gl:id'] || ref['id'];
180
+ if (!source || !id) continue;
181
+ const reg = urnMap.get(source) || source;
182
+ const target = buildConceptUri(uriBase, reg, id);
183
+ if (target === sourceUri) continue;
184
+ edges.push({
185
+ source: sourceUri,
186
+ target,
187
+ type: r['gl:relationshipType'] || 'references',
188
+ label: r['gl:term'] || undefined,
189
+ register: reg,
190
+ });
191
+ }
192
+ return edges;
193
+ }
194
+
195
+ /**
196
+ * Extracts partitive relations from a concept's gl:partitiveRelations
197
+ * array. Returns relation objects (NOT binary GraphEdges) so the UI
198
+ * can render them with their full set-based semantics: comprehensive +
199
+ * partitives (each with certainty), completeness, plurality, and
200
+ * criterion.
201
+ *
202
+ * Relations are written to partitive_relations.json, keeping
203
+ * edges.json binary-only.
204
+ *
205
+ * v2 shape per concept-model/TODO.partitive-relation-v2.
206
+ */
207
+ function extractPartitiveRelations(concept, registerId, uriBase, urnMap) {
208
+ const relations = [];
209
+ const sourceUri = concept['@id'];
210
+ for (const rel of concept['gl:partitiveRelations'] || []) {
211
+ const compRef = rel['gl:comprehensive'];
212
+ if (!compRef) continue;
213
+ const comprehensive = resolveConceptUri(compRef, uriBase, urnMap);
214
+ if (!comprehensive) continue;
215
+
216
+ const partitives = (rel['gl:hasPartitive'] || [])
217
+ .map(p => {
218
+ const memberRef = p['gl:ref'] || p;
219
+ const uri = resolveConceptUri(memberRef, uriBase, urnMap);
220
+ if (!uri || uri === sourceUri) return null;
221
+ return {
222
+ uri,
223
+ presence: resolvePresence(p),
224
+ count: resolveCount(p),
225
+ isDelimiting: resolveIsDelimiting(p),
226
+ };
227
+ })
228
+ .filter(p => p !== null);
229
+
230
+ if (partitives.length === 0) continue;
231
+
232
+ relations.push({
233
+ source: sourceUri,
234
+ comprehensive,
235
+ partitives,
236
+ completeness: validateCompleteness(rel['gl:completeness']),
237
+ criterion: normalizeLocalizedString(rel['gl:criterion']),
238
+ register: registerId,
239
+ });
240
+ }
241
+ return relations;
242
+ }
243
+
244
+ function resolveConceptUri(ref, uriBase, urnMap) {
245
+ const source = ref['gl:source'] || ref['source'];
246
+ const id = ref['gl:id'] || ref['id'];
247
+ if (!source || !id) return null;
248
+ const reg = urnMap.get(source) || source;
249
+ return buildConceptUri(uriBase, reg, id);
250
+ }
251
+
252
+ const EXTRACTORS = [extractReferences, extractRelated, extractDomains];
253
+
254
+ function extractAllEdges(concept, registerId, uriBase, urnMap) {
255
+ return EXTRACTORS.flatMap(fn => fn(concept, registerId, uriBase, urnMap));
256
+ }
257
+
258
+ /**
259
+ * Per concept-model TODO.partitive-relation-v2 item 14:
260
+ * a PartitiveRelation A → {b, c, d} subsumes binary has_part edges
261
+ * for the same pairs. Warn (don't fail) when both encodings exist.
262
+ *
263
+ * Binary edge types considered partitive: has_part, is_part_of,
264
+ * broader_partitive, narrower_partitive.
265
+ */
266
+ const BINARY_PARTITIVE_TYPES = new Set(['has_part', 'is_part_of', 'broader_partitive', 'narrower_partitive']);
267
+
268
+ function warnBinaryPartitiveRedundancy(edges, relations, registerId) {
269
+ if (!relations?.length) return;
270
+ const partitivePairs = new Set();
271
+ for (const rel of relations) {
272
+ for (const member of rel.partitives ?? []) {
273
+ const memberUri = member.uri ?? member;
274
+ partitivePairs.add(`${rel.comprehensive}|${memberUri}`);
275
+ partitivePairs.add(`${memberUri}|${rel.comprehensive}`);
276
+ }
277
+ }
278
+ const redundant = edges.filter(e =>
279
+ BINARY_PARTITIVE_TYPES.has(e.type)
280
+ && partitivePairs.has(`${e.source}|${e.target}`),
281
+ );
282
+ if (redundant.length > 0) {
283
+ console.warn(
284
+ ` ⚠ ${registerId}: ${redundant.length} binary has_part/is_part_of edge(s) duplicate a PartitiveRelation member — prefer the PartitiveRelation encoding (concept-model TODO item 14).`,
285
+ );
286
+ }
287
+ }
288
+
289
+ // --- Build ---
290
+
291
+ function buildEdgesForDataset(datasetDir, registerId, uriBase, urnMap, manifest) {
292
+ const conceptsDir = join(datasetDir, 'concepts');
293
+ if (!existsSync(conceptsDir)) {
294
+ console.log(` Skipping ${registerId}: no concepts directory`);
295
+ return { edges: [], sourceRefs: [] };
296
+ }
297
+
298
+ const files = readdirSync(conceptsDir).filter(f => f.endsWith('.json'));
299
+ console.log(` Processing ${files.length} concepts...`);
300
+
301
+ const allEdges = [];
302
+ const allPartitiveRelations = [];
303
+ const allSourceRefs = [];
304
+ const domainConceptCount = new Map();
305
+ let processed = 0;
306
+
307
+ for (const file of files) {
308
+ try {
309
+ const data = JSON.parse(readFileSync(join(conceptsDir, file), 'utf-8'));
310
+ const edges = extractAllEdges(data, registerId, uriBase, urnMap);
311
+ const relations = extractPartitiveRelations(data, registerId, uriBase, urnMap);
312
+ warnBinaryPartitiveRedundancy(edges, relations, registerId);
313
+ allEdges.push(...edges);
314
+ allPartitiveRelations.push(...relations);
315
+ allSourceRefs.push(...extractSourceRefs(data, registerId));
316
+
317
+ for (const edge of edges) {
318
+ if (edge.type === 'domain' || edge.type === 'section') {
319
+ domainConceptCount.set(edge.target, (domainConceptCount.get(edge.target) || 0) + 1);
320
+ }
321
+ }
322
+ } catch (e) {
323
+ console.error(` Error processing ${file}: ${e.message}`);
324
+ }
325
+ processed++;
326
+ if (processed % 5000 === 0) {
327
+ console.log(` ... ${processed}/${files.length}`);
328
+ }
329
+ }
330
+
331
+ // Deduplicate edges by source+target+type+lang
332
+ const seen = new Set();
333
+ const deduped = [];
334
+ for (const edge of allEdges) {
335
+ const key = `${edge.source}→${edge.target}→${edge.type}→${edge.lang || ''}`;
336
+ if (!seen.has(key)) {
337
+ seen.add(key);
338
+ deduped.push(edge);
339
+ }
340
+ }
341
+
342
+ const output = {
343
+ registerId,
344
+ edgeCount: deduped.length,
345
+ edges: deduped,
346
+ };
347
+
348
+ const outputPath = join(datasetDir, 'edges.json');
349
+ writeFileSync(outputPath, JSON.stringify(output));
350
+ console.log(` Written ${deduped.length} edges to edges.json (${(JSON.stringify(output).length / 1024).toFixed(1)} KB)`);
351
+
352
+ // Write partitive_relations.json (parallel to edges.json) for one-to-many
353
+ // partitive decompositions. See TODO.partitive-relation-v2.
354
+ const relationsOutput = {
355
+ registerId,
356
+ relationCount: allPartitiveRelations.length,
357
+ relations: allPartitiveRelations,
358
+ };
359
+ const relationsPath = join(datasetDir, 'partitive_relations.json');
360
+ writeFileSync(relationsPath, JSON.stringify(relationsOutput));
361
+ console.log(` Written ${allPartitiveRelations.length} partitive relations to partitive_relations.json (${(JSON.stringify(relationsOutput).length / 1024).toFixed(1)} KB)`);
362
+
363
+ // Build domain-nodes.json from manifest sections (authoritative source)
364
+ const manifestSections = manifest.sections;
365
+ if (manifestSections && manifestSections.length > 0) {
366
+ const uriBase = manifest.uriBase;
367
+ if (!uriBase) throw new Error('build-edges: manifest.uriBase is required');
368
+
369
+ function buildSectionNode(section: Record<string, any>, idx: number): Record<string, any> {
370
+ const sectionId = `section-${section.id}`;
371
+ const domainUri = `${uriBase}/${registerId}/domain/${sectionId}`;
372
+ const domainLabel = section.names?.eng || section.id;
373
+ const node: Record<string, any> = {
374
+ uri: domainUri,
375
+ id: sectionId,
376
+ names: section.names || {},
377
+ label: domainLabel,
378
+ registerId,
379
+ conceptCount: domainConceptCount.get(domainUri) || 0,
380
+ order: idx,
381
+ };
382
+ if (section.children && section.children.length > 0) {
383
+ node.children = section.children.map((child, childIdx) =>
384
+ buildSectionNode(child, childIdx)
385
+ );
386
+ }
387
+ return node;
388
+ }
389
+
390
+ const domainNodes = manifestSections.map((section, idx) =>
391
+ buildSectionNode(section, idx)
392
+ );
393
+
394
+ const domainOutput = { registerId, domainNodes };
395
+ const domainPath = join(datasetDir, 'domain-nodes.json');
396
+ writeFileSync(domainPath, JSON.stringify(domainOutput));
397
+ console.log(` Written ${domainNodes.length} section-based domain nodes to domain-nodes.json`);
398
+ } else {
399
+ // Fallback: derive domain nodes from concept edges (legacy behavior)
400
+ const domainEdgeMap = new Map();
401
+ for (const edge of deduped) {
402
+ if (edge.type === 'domain') {
403
+ const existing = domainEdgeMap.get(edge.target);
404
+ if (existing) {
405
+ existing.labels.add(edge.label);
406
+ } else {
407
+ domainEdgeMap.set(edge.target, { uri: edge.target, labels: new Set([edge.label]), registerId });
408
+ }
409
+ }
410
+ }
411
+
412
+ const domainNodes = [...domainEdgeMap.values()].map(d => ({
413
+ uri: d.uri,
414
+ label: [...d.labels][0],
415
+ registerId: d.registerId,
416
+ conceptCount: domainConceptCount.get(d.uri) || 0,
417
+ })).sort((a, b) => b.conceptCount - a.conceptCount);
418
+
419
+ const domainOutput = { registerId, domainNodes };
420
+ const domainPath = join(datasetDir, 'domain-nodes.json');
421
+ writeFileSync(domainPath, JSON.stringify(domainOutput));
422
+ console.log(` Written ${domainNodes.length} edge-derived domain nodes to domain-nodes.json`);
423
+ }
424
+
425
+ return { edges: deduped, sourceRefs: allSourceRefs };
426
+ }
427
+
428
+ // Main
429
+ export function main() {
430
+ console.log('Building edge indexes...\n');
431
+
432
+ if (!existsSync(DATA_DIR)) {
433
+ console.log('No data directory found. Nothing to do.');
434
+ process.exit(0);
435
+ }
436
+
437
+ const datasets = readdirSync(DATA_DIR).filter(f => {
438
+ try {
439
+ return existsSync(join(DATA_DIR, f, 'manifest.json'));
440
+ } catch {
441
+ return false;
442
+ }
443
+ });
444
+
445
+ // Build URI→datasetId prefix map from all manifests
446
+ const urnMap = new Map();
447
+ const manifestCache = new Map();
448
+ for (const ds of datasets) {
449
+ const manifestPath = join(DATA_DIR, ds, 'manifest.json');
450
+ try {
451
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
452
+ manifestCache.set(ds, manifest);
453
+ if (manifest.datasetUri) {
454
+ const base = manifest.datasetUri.endsWith('*') ? manifest.datasetUri.slice(0, -1) : manifest.datasetUri;
455
+ if (base) urnMap.set(base, ds);
456
+ }
457
+ for (const alias of manifest.uriAliases ?? []) {
458
+ const base = alias.endsWith('*') ? alias.slice(0, -1) : alias;
459
+ if (base) urnMap.set(base, ds);
460
+ }
461
+ } catch {}
462
+ }
463
+ console.log(`URI resolution map: ${[...urnMap.entries()].map(([k,v]) => `${k}→${v}`).join(', ')}\n`);
464
+
465
+ const allDatasetEdges = new Map();
466
+ const allSourceRefs = [];
467
+
468
+ for (const ds of datasets) {
469
+ const manifest = manifestCache.get(ds);
470
+ if (!manifest) continue;
471
+ try {
472
+ console.log(`${manifest.title} (${ds}):`);
473
+ const uriBase = manifest.uriBase;
474
+ if (!uriBase) throw new Error('build-edges: manifest.uriBase is required');
475
+ const result = buildEdgesForDataset(join(DATA_DIR, ds), ds, uriBase, urnMap, manifest);
476
+ allDatasetEdges.set(ds, result.edges);
477
+ allSourceRefs.push(...result.sourceRefs);
478
+ } catch (e) {
479
+ console.error(`Error reading manifest for ${ds}: ${e.message}`);
480
+ }
481
+ console.log();
482
+ }
483
+
484
+ // Audit — report concept source strings not covered by manifest refs.
485
+ // Bibliography mapping (ref → URN) is declared in register.yaml and flows
486
+ // through manifest.json to datasets.json at generate time. No separate
487
+ // source-refs file needed — the registry IS the single source of truth.
488
+
489
+ // Build a lookup of all known source strings from manifests
490
+ const knownSourceStrings = new Set();
491
+ for (const [ds, manifest] of manifestCache) {
492
+ if (manifest.ref) knownSourceStrings.add(manifest.ref);
493
+ for (const alias of manifest.refAliases ?? []) {
494
+ knownSourceStrings.add(alias);
495
+ }
496
+ if (manifest.datasetUri) {
497
+ const base = manifest.datasetUri.endsWith('*') ? manifest.datasetUri.slice(0, -1) : manifest.datasetUri;
498
+ knownSourceStrings.add(base);
499
+ }
500
+ for (const alias of manifest.uriAliases ?? []) {
501
+ const base = alias.endsWith('*') ? alias.slice(0, -1) : alias;
502
+ knownSourceStrings.add(base);
503
+ }
504
+ }
505
+
506
+ const auditUnmatched = new Map();
507
+ for (const { source, registerId } of allSourceRefs) {
508
+ if (knownSourceStrings.has(source)) continue;
509
+ // URN source strings resolve via URI routing — not a bibliography concern
510
+ if (source.startsWith('urn:')) continue;
511
+ if (!auditUnmatched.has(source)) {
512
+ auditUnmatched.set(source, registerId);
513
+ }
514
+ }
515
+
516
+ if (auditUnmatched.size > 0) {
517
+ console.warn(`\n⚠ ${auditUnmatched.size} source string(s) in concept data have no matching dataset ref:`);
518
+ for (const [source, fromDataset] of auditUnmatched) {
519
+ console.warn(` "${source}" (from ${fromDataset})`);
520
+ }
521
+ console.warn('Add refAliases to the target dataset manifest, or fix source strings to use URNs.\n');
522
+ }
523
+
524
+ // Build cross-reference index: for each dataset, which other datasets'
525
+ // edges.json contains edges targeting that dataset's URIs.
526
+ const datasetUriPrefixes = new Map<string, string>();
527
+ for (const [ds, manifest] of manifestCache) {
528
+ const uriBase = manifest.uriBase;
529
+ if (!uriBase) throw new Error('build-edges: manifest.uriBase is required');
530
+ datasetUriPrefixes.set(ds, `${uriBase}/${ds}/`);
531
+ }
532
+
533
+ const crossRefIndex = {};
534
+ for (const ds of datasets) {
535
+ crossRefIndex[ds] = [];
536
+ }
537
+
538
+ for (const [sourceDs, edges] of allDatasetEdges) {
539
+ const targets = new Set<string>();
540
+ for (const edge of edges) {
541
+ for (const [targetDs, prefix] of datasetUriPrefixes) {
542
+ if (targetDs !== sourceDs && edge.target.startsWith(prefix)) {
543
+ targets.add(targetDs);
544
+ }
545
+ }
546
+ // Also check source URIs targeting other datasets
547
+ for (const [targetDs, prefix] of datasetUriPrefixes) {
548
+ if (targetDs !== sourceDs && edge.source.startsWith(prefix)) {
549
+ targets.add(targetDs);
550
+ }
551
+ }
552
+ }
553
+ for (const targetDs of targets) {
554
+ if (!crossRefIndex[targetDs].includes(sourceDs)) {
555
+ crossRefIndex[targetDs].push(sourceDs);
556
+ }
557
+ }
558
+ }
559
+
560
+ const crossRefPath = join(DATA_DIR, 'cross-ref-index.json');
561
+ writeFileSync(crossRefPath, JSON.stringify(crossRefIndex));
562
+ const refCount = Object.values(crossRefIndex).reduce((sum: number, arr: string[]) => sum + arr.length, 0);
563
+ console.log(`Written cross-ref-index.json (${refCount} cross-references across ${datasets.length} datasets)`);
564
+
565
+ console.log('Done.');
566
+ }
567
+
568
+ const isDirectInvocation = process.argv[1]
569
+ && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
570
+ if (isDirectInvocation) {
571
+ main();
572
+ }
@@ -0,0 +1,22 @@
1
+ import { build } from 'esbuild';
2
+ import { globSync } from 'glob';
3
+ import { dirname, relative } from 'path';
4
+
5
+ const files = globSync('scripts/*.ts').filter(f => !f.includes('__tests__'));
6
+
7
+ await Promise.all(files.map(async (file) => {
8
+ const outfile = file.replace(/\.ts$/, '.js');
9
+ await build({
10
+ entryPoints: [file],
11
+ bundle: true,
12
+ format: 'esm',
13
+ platform: 'node',
14
+ outfile,
15
+ packages: 'external',
16
+ sourcemap: false,
17
+ logLevel: 'info',
18
+ });
19
+ console.log(` ${relative(process.cwd(), file)} → ${relative(process.cwd(), outfile)}`);
20
+ }));
21
+
22
+ console.log(`\nCompiled ${files.length} scripts.`);
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env node
2
+
3
+ // scripts/check-syntax.ts
4
+ import { spawnSync } from "node:child_process";
5
+ import { readdirSync, statSync } from "node:fs";
6
+ import path from "node:path";
7
+ import process from "node:process";
8
+ var ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname), "..");
9
+ var TARGET_DIRS = ["scripts", "cli"];
10
+ var JS_EXTENSIONS = /* @__PURE__ */ new Set([".mjs", ".js", ".cjs"]);
11
+ var ALL_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ...JS_EXTENSIONS]);
12
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["__tests__", "node_modules"]);
13
+ function walk(dir) {
14
+ const out = [];
15
+ for (const entry of readdirSync(dir)) {
16
+ if (SKIP_DIRS.has(entry)) continue;
17
+ const full = path.join(dir, entry);
18
+ const st = statSync(full);
19
+ if (st.isDirectory()) {
20
+ out.push(...walk(full));
21
+ } else if (ALL_EXTENSIONS.has(path.extname(entry))) {
22
+ out.push(full);
23
+ }
24
+ }
25
+ return out;
26
+ }
27
+ function checkAllScripts({ root = ROOT } = {}) {
28
+ const files = [];
29
+ for (const sub of TARGET_DIRS) {
30
+ const dir = path.join(root, sub);
31
+ try {
32
+ statSync(dir);
33
+ } catch {
34
+ continue;
35
+ }
36
+ files.push(...walk(dir));
37
+ }
38
+ files.sort();
39
+ const failures = [];
40
+ for (const file of files.filter((f) => JS_EXTENSIONS.has(path.extname(f)))) {
41
+ const result = spawnSync(process.execPath, ["--check", file], { encoding: "utf8" });
42
+ if (result.status !== 0) {
43
+ failures.push({ file: path.relative(root, file), stderr: result.stderr || "" });
44
+ }
45
+ }
46
+ if (files.some((f) => path.extname(f) === ".ts")) {
47
+ const tsconfig = path.join(root, "tsconfig.scripts.json");
48
+ const result = spawnSync("npx", ["tsc", "-p", tsconfig, "--noEmit"], {
49
+ encoding: "utf8",
50
+ cwd: root
51
+ });
52
+ if (result.status !== 0) {
53
+ failures.push({ file: "tsconfig.scripts.json (tsc)", stderr: result.stdout || result.stderr || "" });
54
+ }
55
+ }
56
+ return { files, failures };
57
+ }
58
+ function main() {
59
+ const { files, failures } = checkAllScripts();
60
+ if (failures.length === 0) {
61
+ process.stdout.write(`syntax OK: ${files.length} file(s) checked
62
+ `);
63
+ return;
64
+ }
65
+ process.stderr.write(`syntax check failed: ${failures.length} of ${files.length} file(s)
66
+
67
+ `);
68
+ for (const { file, stderr } of failures) {
69
+ process.stderr.write(`--- ${file} ---
70
+ ${stderr}
71
+ `);
72
+ }
73
+ process.exit(1);
74
+ }
75
+ var isDirectEntry = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(new URL(import.meta.url).pathname);
76
+ if (isDirectEntry) {
77
+ main();
78
+ }
79
+ export {
80
+ checkAllScripts
81
+ };