@glossarist/concept-browser 0.7.115 → 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.
- package/cli/index.mjs +12 -15
- package/package.json +5 -1
- package/scripts/bridge-to-astro.js +132 -0
- package/scripts/build-edges.js +480 -0
- package/scripts/build-scripts.mjs +22 -0
- package/scripts/check-syntax.js +81 -0
- package/scripts/doctor.js +278 -0
- package/scripts/extract-source-refs.js +24 -0
- package/scripts/fetch-datasets.js +278 -0
- package/scripts/generate-404.js +14 -0
- package/scripts/generate-data.js +2220 -0
- package/scripts/generate-ontology-data.js +139 -0
- package/scripts/generate-ontology-schema.js +472 -0
- package/scripts/load-site-config.js +83 -0
- package/scripts/migrate-v1-to-v3.js +113 -0
- package/scripts/normalize-yaml.js +96 -0
- package/scripts/process-about-pages.js +241 -0
- package/scripts/smoke.js +211 -0
- package/scripts/sync-concept-model.js +63 -0
- package/scripts/validate-shacl.js +152 -0
- package/scripts/validate-yaml.js +132 -0
- package/src/__tests__/published-package-contract.test.ts +84 -0
- package/src/__tests__/scripts-syntax.test.ts +2 -2
- package/src/data/ontology-schema.json +1336 -183
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
// scripts/lib/concept-uri.ts
|
|
2
|
+
function buildConceptUri(uriBase, registerId, conceptId) {
|
|
3
|
+
return `${uriBase}/${registerId}/concept/${conceptId}`;
|
|
4
|
+
}
|
|
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
|
+
// scripts/build-edges.ts
|
|
29
|
+
import { readFileSync, writeFileSync, readdirSync, existsSync, realpathSync } from "fs";
|
|
30
|
+
import { join, dirname } from "path";
|
|
31
|
+
import { fileURLToPath } from "url";
|
|
32
|
+
var VALID_COMPLETENESS = /* @__PURE__ */ new Set(["complete", "partial"]);
|
|
33
|
+
function validateCompleteness(value) {
|
|
34
|
+
if (value == null) return "complete";
|
|
35
|
+
if (!VALID_COMPLETENESS.has(value)) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Invalid partitive relation completeness: "${value}". Allowed: complete, partial`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
var VALID_PRESENCE = /* @__PURE__ */ new Set(["required", "optional"]);
|
|
43
|
+
var VALID_COUNT = /* @__PURE__ */ new Set(["exactly_one", "at_least_one", "multiple"]);
|
|
44
|
+
var LEGACY_MULTIPLICITY_MAP = {
|
|
45
|
+
compulsory: { presence: "required", count: "exactly_one" },
|
|
46
|
+
optional: { presence: "optional", count: "exactly_one" },
|
|
47
|
+
compulsory_multiple: { presence: "required", count: "multiple" },
|
|
48
|
+
optional_multiple: { presence: "optional", count: "multiple" },
|
|
49
|
+
compulsory_at_least_one: { presence: "required", count: "at_least_one" }
|
|
50
|
+
};
|
|
51
|
+
function resolvePresence(member) {
|
|
52
|
+
const p = member["gl:presence"] ?? member.presence;
|
|
53
|
+
if (p != null) {
|
|
54
|
+
if (!VALID_PRESENCE.has(p)) throw new Error(`Invalid presence: "${p}"`);
|
|
55
|
+
return p;
|
|
56
|
+
}
|
|
57
|
+
return migrateMultiplicity(member).presence;
|
|
58
|
+
}
|
|
59
|
+
function resolveCount(member) {
|
|
60
|
+
const c = member["gl:count"] ?? member.count;
|
|
61
|
+
if (c != null) {
|
|
62
|
+
if (!VALID_COUNT.has(c)) throw new Error(`Invalid count: "${c}"`);
|
|
63
|
+
return c;
|
|
64
|
+
}
|
|
65
|
+
return migrateMultiplicity(member).count;
|
|
66
|
+
}
|
|
67
|
+
function migrateMultiplicity(member) {
|
|
68
|
+
const m = member["gl:multiplicity"] ?? member.multiplicity;
|
|
69
|
+
if (m && LEGACY_MULTIPLICITY_MAP[m]) return LEGACY_MULTIPLICITY_MAP[m];
|
|
70
|
+
const certainty = member["gl:certainty"] ?? member.certainty;
|
|
71
|
+
if (certainty === "possible") return { presence: "optional", count: "exactly_one" };
|
|
72
|
+
return { presence: "required", count: "exactly_one" };
|
|
73
|
+
}
|
|
74
|
+
function resolveIsDelimiting(member) {
|
|
75
|
+
return member["gl:isDelimiting"] === true || member.isDelimiting === true;
|
|
76
|
+
}
|
|
77
|
+
function normalizeLocalizedString(content) {
|
|
78
|
+
if (content == null) return void 0;
|
|
79
|
+
if (typeof content === "string") return { default: content };
|
|
80
|
+
return content;
|
|
81
|
+
}
|
|
82
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
83
|
+
var ROOT = process.cwd();
|
|
84
|
+
var DATA_DIR = join(ROOT, "public", "data");
|
|
85
|
+
function slugify(text) {
|
|
86
|
+
return text.toLowerCase().replace(/[^\w\s-]/g, "").replace(/[\s/]+/g, "-");
|
|
87
|
+
}
|
|
88
|
+
function extractReferences(concept, registerId) {
|
|
89
|
+
const edges = [];
|
|
90
|
+
const sourceUri = concept["@id"];
|
|
91
|
+
for (const [lang, lc] of Object.entries(concept["gl:localizedConcept"] || {})) {
|
|
92
|
+
if (lc["gl:references"]) {
|
|
93
|
+
for (const ref of lc["gl:references"]) {
|
|
94
|
+
if (ref["@id"] && ref["@id"] !== sourceUri) {
|
|
95
|
+
const edge = {
|
|
96
|
+
source: sourceUri,
|
|
97
|
+
target: ref["@id"],
|
|
98
|
+
type: ref["@id"].startsWith("cite:") ? "citation" : "references",
|
|
99
|
+
label: ref["gl:term"] || void 0,
|
|
100
|
+
register: registerId,
|
|
101
|
+
lang
|
|
102
|
+
};
|
|
103
|
+
if (ref["gl:sourceId"]) edge.sourceId = ref["gl:sourceId"];
|
|
104
|
+
edges.push(edge);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return edges;
|
|
110
|
+
}
|
|
111
|
+
function extractDomains(concept, registerId, uriBase) {
|
|
112
|
+
if (!uriBase) throw new Error("build-edges: uriBase is required \u2014 set uriBase in site-config.yml");
|
|
113
|
+
const base = uriBase;
|
|
114
|
+
const edges = [];
|
|
115
|
+
const sourceUri = concept["@id"];
|
|
116
|
+
const seen = /* @__PURE__ */ new Set();
|
|
117
|
+
const domains = concept["gl:domain"];
|
|
118
|
+
if (Array.isArray(domains)) {
|
|
119
|
+
for (const d of domains) {
|
|
120
|
+
const conceptId = d["gl:conceptId"] || d.concept_id;
|
|
121
|
+
if (conceptId) {
|
|
122
|
+
const refType = d["gl:refType"] || d.ref_type || "domain";
|
|
123
|
+
const isSection = refType === "section";
|
|
124
|
+
const edgeType = isSection ? "section" : "domain";
|
|
125
|
+
const domainUri = `${base}/${registerId}/domain/${conceptId}`;
|
|
126
|
+
if (!seen.has(domainUri)) {
|
|
127
|
+
seen.add(domainUri);
|
|
128
|
+
edges.push({
|
|
129
|
+
source: sourceUri,
|
|
130
|
+
target: domainUri,
|
|
131
|
+
type: edgeType,
|
|
132
|
+
label: conceptId,
|
|
133
|
+
register: registerId
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const lcs = concept["gl:localizedConcept"] || {};
|
|
140
|
+
const langs = Object.keys(lcs);
|
|
141
|
+
for (const lang of langs) {
|
|
142
|
+
const domain = lcs[lang]["gl:domain"];
|
|
143
|
+
if (domain && !seen.has(domain)) {
|
|
144
|
+
seen.add(domain);
|
|
145
|
+
edges.push({
|
|
146
|
+
source: sourceUri,
|
|
147
|
+
target: `${base}/${registerId}/domain/${slugify(domain)}`,
|
|
148
|
+
type: "domain",
|
|
149
|
+
label: domain,
|
|
150
|
+
register: registerId
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return edges;
|
|
155
|
+
}
|
|
156
|
+
function extractRelated(concept, registerId, uriBase, urnMap) {
|
|
157
|
+
const edges = [];
|
|
158
|
+
const sourceUri = concept["@id"];
|
|
159
|
+
for (const r of concept["gl:related"] || []) {
|
|
160
|
+
const ref = r["gl:ref"];
|
|
161
|
+
if (!ref) continue;
|
|
162
|
+
const source = ref["gl:source"] || ref["source"];
|
|
163
|
+
const id = ref["gl:id"] || ref["id"];
|
|
164
|
+
if (!source || !id) continue;
|
|
165
|
+
const reg = urnMap.get(source) || source;
|
|
166
|
+
const target = buildConceptUri(uriBase, reg, id);
|
|
167
|
+
if (target === sourceUri) continue;
|
|
168
|
+
edges.push({
|
|
169
|
+
source: sourceUri,
|
|
170
|
+
target,
|
|
171
|
+
type: r["gl:relationshipType"] || "references",
|
|
172
|
+
label: r["gl:term"] || void 0,
|
|
173
|
+
register: reg
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
return edges;
|
|
177
|
+
}
|
|
178
|
+
function extractPartitiveRelations(concept, registerId, uriBase, urnMap) {
|
|
179
|
+
const relations = [];
|
|
180
|
+
const sourceUri = concept["@id"];
|
|
181
|
+
for (const rel of concept["gl:partitiveRelations"] || []) {
|
|
182
|
+
const compRef = rel["gl:comprehensive"];
|
|
183
|
+
if (!compRef) continue;
|
|
184
|
+
const comprehensive = resolveConceptUri(compRef, uriBase, urnMap);
|
|
185
|
+
if (!comprehensive) continue;
|
|
186
|
+
const partitives = (rel["gl:hasPartitive"] || []).map((p) => {
|
|
187
|
+
const memberRef = p["gl:ref"] || p;
|
|
188
|
+
const uri = resolveConceptUri(memberRef, uriBase, urnMap);
|
|
189
|
+
if (!uri || uri === sourceUri) return null;
|
|
190
|
+
return {
|
|
191
|
+
uri,
|
|
192
|
+
presence: resolvePresence(p),
|
|
193
|
+
count: resolveCount(p),
|
|
194
|
+
isDelimiting: resolveIsDelimiting(p)
|
|
195
|
+
};
|
|
196
|
+
}).filter((p) => p !== null);
|
|
197
|
+
if (partitives.length === 0) continue;
|
|
198
|
+
relations.push({
|
|
199
|
+
source: sourceUri,
|
|
200
|
+
comprehensive,
|
|
201
|
+
partitives,
|
|
202
|
+
completeness: validateCompleteness(rel["gl:completeness"]),
|
|
203
|
+
criterion: normalizeLocalizedString(rel["gl:criterion"]),
|
|
204
|
+
register: registerId
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
return relations;
|
|
208
|
+
}
|
|
209
|
+
function resolveConceptUri(ref, uriBase, urnMap) {
|
|
210
|
+
const source = ref["gl:source"] || ref["source"];
|
|
211
|
+
const id = ref["gl:id"] || ref["id"];
|
|
212
|
+
if (!source || !id) return null;
|
|
213
|
+
const reg = urnMap.get(source) || source;
|
|
214
|
+
return buildConceptUri(uriBase, reg, id);
|
|
215
|
+
}
|
|
216
|
+
var EXTRACTORS = [extractReferences, extractRelated, extractDomains];
|
|
217
|
+
function extractAllEdges(concept, registerId, uriBase, urnMap) {
|
|
218
|
+
return EXTRACTORS.flatMap((fn) => fn(concept, registerId, uriBase, urnMap));
|
|
219
|
+
}
|
|
220
|
+
var BINARY_PARTITIVE_TYPES = /* @__PURE__ */ new Set(["has_part", "is_part_of", "broader_partitive", "narrower_partitive"]);
|
|
221
|
+
function warnBinaryPartitiveRedundancy(edges, relations, registerId) {
|
|
222
|
+
if (!relations?.length) return;
|
|
223
|
+
const partitivePairs = /* @__PURE__ */ new Set();
|
|
224
|
+
for (const rel of relations) {
|
|
225
|
+
for (const member of rel.partitives ?? []) {
|
|
226
|
+
const memberUri = member.uri ?? member;
|
|
227
|
+
partitivePairs.add(`${rel.comprehensive}|${memberUri}`);
|
|
228
|
+
partitivePairs.add(`${memberUri}|${rel.comprehensive}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const redundant = edges.filter(
|
|
232
|
+
(e) => BINARY_PARTITIVE_TYPES.has(e.type) && partitivePairs.has(`${e.source}|${e.target}`)
|
|
233
|
+
);
|
|
234
|
+
if (redundant.length > 0) {
|
|
235
|
+
console.warn(
|
|
236
|
+
` \u26A0 ${registerId}: ${redundant.length} binary has_part/is_part_of edge(s) duplicate a PartitiveRelation member \u2014 prefer the PartitiveRelation encoding (concept-model TODO item 14).`
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function buildEdgesForDataset(datasetDir, registerId, uriBase, urnMap, manifest) {
|
|
241
|
+
const conceptsDir = join(datasetDir, "concepts");
|
|
242
|
+
if (!existsSync(conceptsDir)) {
|
|
243
|
+
console.log(` Skipping ${registerId}: no concepts directory`);
|
|
244
|
+
return { edges: [], sourceRefs: [] };
|
|
245
|
+
}
|
|
246
|
+
const files = readdirSync(conceptsDir).filter((f) => f.endsWith(".json"));
|
|
247
|
+
console.log(` Processing ${files.length} concepts...`);
|
|
248
|
+
const allEdges = [];
|
|
249
|
+
const allPartitiveRelations = [];
|
|
250
|
+
const allSourceRefs = [];
|
|
251
|
+
const domainConceptCount = /* @__PURE__ */ new Map();
|
|
252
|
+
let processed = 0;
|
|
253
|
+
for (const file of files) {
|
|
254
|
+
try {
|
|
255
|
+
const data = JSON.parse(readFileSync(join(conceptsDir, file), "utf-8"));
|
|
256
|
+
const edges = extractAllEdges(data, registerId, uriBase, urnMap);
|
|
257
|
+
const relations = extractPartitiveRelations(data, registerId, uriBase, urnMap);
|
|
258
|
+
warnBinaryPartitiveRedundancy(edges, relations, registerId);
|
|
259
|
+
allEdges.push(...edges);
|
|
260
|
+
allPartitiveRelations.push(...relations);
|
|
261
|
+
allSourceRefs.push(...extractSourceRefs(data, registerId));
|
|
262
|
+
for (const edge of edges) {
|
|
263
|
+
if (edge.type === "domain" || edge.type === "section") {
|
|
264
|
+
domainConceptCount.set(edge.target, (domainConceptCount.get(edge.target) || 0) + 1);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
} catch (e) {
|
|
268
|
+
console.error(` Error processing ${file}: ${e.message}`);
|
|
269
|
+
}
|
|
270
|
+
processed++;
|
|
271
|
+
if (processed % 5e3 === 0) {
|
|
272
|
+
console.log(` ... ${processed}/${files.length}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
const seen = /* @__PURE__ */ new Set();
|
|
276
|
+
const deduped = [];
|
|
277
|
+
for (const edge of allEdges) {
|
|
278
|
+
const key = `${edge.source}\u2192${edge.target}\u2192${edge.type}\u2192${edge.lang || ""}`;
|
|
279
|
+
if (!seen.has(key)) {
|
|
280
|
+
seen.add(key);
|
|
281
|
+
deduped.push(edge);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
const output = {
|
|
285
|
+
registerId,
|
|
286
|
+
edgeCount: deduped.length,
|
|
287
|
+
edges: deduped
|
|
288
|
+
};
|
|
289
|
+
const outputPath = join(datasetDir, "edges.json");
|
|
290
|
+
writeFileSync(outputPath, JSON.stringify(output));
|
|
291
|
+
console.log(` Written ${deduped.length} edges to edges.json (${(JSON.stringify(output).length / 1024).toFixed(1)} KB)`);
|
|
292
|
+
const relationsOutput = {
|
|
293
|
+
registerId,
|
|
294
|
+
relationCount: allPartitiveRelations.length,
|
|
295
|
+
relations: allPartitiveRelations
|
|
296
|
+
};
|
|
297
|
+
const relationsPath = join(datasetDir, "partitive_relations.json");
|
|
298
|
+
writeFileSync(relationsPath, JSON.stringify(relationsOutput));
|
|
299
|
+
console.log(` Written ${allPartitiveRelations.length} partitive relations to partitive_relations.json (${(JSON.stringify(relationsOutput).length / 1024).toFixed(1)} KB)`);
|
|
300
|
+
const manifestSections = manifest.sections;
|
|
301
|
+
if (manifestSections && manifestSections.length > 0) {
|
|
302
|
+
let buildSectionNode2 = function(section, idx) {
|
|
303
|
+
const sectionId = `section-${section.id}`;
|
|
304
|
+
const domainUri = `${uriBase2}/${registerId}/domain/${sectionId}`;
|
|
305
|
+
const domainLabel = section.names?.eng || section.id;
|
|
306
|
+
const node = {
|
|
307
|
+
uri: domainUri,
|
|
308
|
+
id: sectionId,
|
|
309
|
+
names: section.names || {},
|
|
310
|
+
label: domainLabel,
|
|
311
|
+
registerId,
|
|
312
|
+
conceptCount: domainConceptCount.get(domainUri) || 0,
|
|
313
|
+
order: idx
|
|
314
|
+
};
|
|
315
|
+
if (section.children && section.children.length > 0) {
|
|
316
|
+
node.children = section.children.map(
|
|
317
|
+
(child, childIdx) => buildSectionNode2(child, childIdx)
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
return node;
|
|
321
|
+
};
|
|
322
|
+
var buildSectionNode = buildSectionNode2;
|
|
323
|
+
const uriBase2 = manifest.uriBase;
|
|
324
|
+
if (!uriBase2) throw new Error("build-edges: manifest.uriBase is required");
|
|
325
|
+
const domainNodes = manifestSections.map(
|
|
326
|
+
(section, idx) => buildSectionNode2(section, idx)
|
|
327
|
+
);
|
|
328
|
+
const domainOutput = { registerId, domainNodes };
|
|
329
|
+
const domainPath = join(datasetDir, "domain-nodes.json");
|
|
330
|
+
writeFileSync(domainPath, JSON.stringify(domainOutput));
|
|
331
|
+
console.log(` Written ${domainNodes.length} section-based domain nodes to domain-nodes.json`);
|
|
332
|
+
} else {
|
|
333
|
+
const domainEdgeMap = /* @__PURE__ */ new Map();
|
|
334
|
+
for (const edge of deduped) {
|
|
335
|
+
if (edge.type === "domain") {
|
|
336
|
+
const existing = domainEdgeMap.get(edge.target);
|
|
337
|
+
if (existing) {
|
|
338
|
+
existing.labels.add(edge.label);
|
|
339
|
+
} else {
|
|
340
|
+
domainEdgeMap.set(edge.target, { uri: edge.target, labels: /* @__PURE__ */ new Set([edge.label]), registerId });
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
const domainNodes = [...domainEdgeMap.values()].map((d) => ({
|
|
345
|
+
uri: d.uri,
|
|
346
|
+
label: [...d.labels][0],
|
|
347
|
+
registerId: d.registerId,
|
|
348
|
+
conceptCount: domainConceptCount.get(d.uri) || 0
|
|
349
|
+
})).sort((a, b) => b.conceptCount - a.conceptCount);
|
|
350
|
+
const domainOutput = { registerId, domainNodes };
|
|
351
|
+
const domainPath = join(datasetDir, "domain-nodes.json");
|
|
352
|
+
writeFileSync(domainPath, JSON.stringify(domainOutput));
|
|
353
|
+
console.log(` Written ${domainNodes.length} edge-derived domain nodes to domain-nodes.json`);
|
|
354
|
+
}
|
|
355
|
+
return { edges: deduped, sourceRefs: allSourceRefs };
|
|
356
|
+
}
|
|
357
|
+
function main() {
|
|
358
|
+
console.log("Building edge indexes...\n");
|
|
359
|
+
if (!existsSync(DATA_DIR)) {
|
|
360
|
+
console.log("No data directory found. Nothing to do.");
|
|
361
|
+
process.exit(0);
|
|
362
|
+
}
|
|
363
|
+
const datasets = readdirSync(DATA_DIR).filter((f) => {
|
|
364
|
+
try {
|
|
365
|
+
return existsSync(join(DATA_DIR, f, "manifest.json"));
|
|
366
|
+
} catch {
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
const urnMap = /* @__PURE__ */ new Map();
|
|
371
|
+
const manifestCache = /* @__PURE__ */ new Map();
|
|
372
|
+
for (const ds of datasets) {
|
|
373
|
+
const manifestPath = join(DATA_DIR, ds, "manifest.json");
|
|
374
|
+
try {
|
|
375
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
376
|
+
manifestCache.set(ds, manifest);
|
|
377
|
+
if (manifest.datasetUri) {
|
|
378
|
+
const base = manifest.datasetUri.endsWith("*") ? manifest.datasetUri.slice(0, -1) : manifest.datasetUri;
|
|
379
|
+
if (base) urnMap.set(base, ds);
|
|
380
|
+
}
|
|
381
|
+
for (const alias of manifest.uriAliases ?? []) {
|
|
382
|
+
const base = alias.endsWith("*") ? alias.slice(0, -1) : alias;
|
|
383
|
+
if (base) urnMap.set(base, ds);
|
|
384
|
+
}
|
|
385
|
+
} catch {
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
console.log(`URI resolution map: ${[...urnMap.entries()].map(([k, v]) => `${k}\u2192${v}`).join(", ")}
|
|
389
|
+
`);
|
|
390
|
+
const allDatasetEdges = /* @__PURE__ */ new Map();
|
|
391
|
+
const allSourceRefs = [];
|
|
392
|
+
for (const ds of datasets) {
|
|
393
|
+
const manifest = manifestCache.get(ds);
|
|
394
|
+
if (!manifest) continue;
|
|
395
|
+
try {
|
|
396
|
+
console.log(`${manifest.title} (${ds}):`);
|
|
397
|
+
const uriBase = manifest.uriBase;
|
|
398
|
+
if (!uriBase) throw new Error("build-edges: manifest.uriBase is required");
|
|
399
|
+
const result = buildEdgesForDataset(join(DATA_DIR, ds), ds, uriBase, urnMap, manifest);
|
|
400
|
+
allDatasetEdges.set(ds, result.edges);
|
|
401
|
+
allSourceRefs.push(...result.sourceRefs);
|
|
402
|
+
} catch (e) {
|
|
403
|
+
console.error(`Error reading manifest for ${ds}: ${e.message}`);
|
|
404
|
+
}
|
|
405
|
+
console.log();
|
|
406
|
+
}
|
|
407
|
+
const knownSourceStrings = /* @__PURE__ */ new Set();
|
|
408
|
+
for (const [ds, manifest] of manifestCache) {
|
|
409
|
+
if (manifest.ref) knownSourceStrings.add(manifest.ref);
|
|
410
|
+
for (const alias of manifest.refAliases ?? []) {
|
|
411
|
+
knownSourceStrings.add(alias);
|
|
412
|
+
}
|
|
413
|
+
if (manifest.datasetUri) {
|
|
414
|
+
const base = manifest.datasetUri.endsWith("*") ? manifest.datasetUri.slice(0, -1) : manifest.datasetUri;
|
|
415
|
+
knownSourceStrings.add(base);
|
|
416
|
+
}
|
|
417
|
+
for (const alias of manifest.uriAliases ?? []) {
|
|
418
|
+
const base = alias.endsWith("*") ? alias.slice(0, -1) : alias;
|
|
419
|
+
knownSourceStrings.add(base);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
const auditUnmatched = /* @__PURE__ */ new Map();
|
|
423
|
+
for (const { source, registerId } of allSourceRefs) {
|
|
424
|
+
if (knownSourceStrings.has(source)) continue;
|
|
425
|
+
if (source.startsWith("urn:")) continue;
|
|
426
|
+
if (!auditUnmatched.has(source)) {
|
|
427
|
+
auditUnmatched.set(source, registerId);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (auditUnmatched.size > 0) {
|
|
431
|
+
console.warn(`
|
|
432
|
+
\u26A0 ${auditUnmatched.size} source string(s) in concept data have no matching dataset ref:`);
|
|
433
|
+
for (const [source, fromDataset] of auditUnmatched) {
|
|
434
|
+
console.warn(` "${source}" (from ${fromDataset})`);
|
|
435
|
+
}
|
|
436
|
+
console.warn("Add refAliases to the target dataset manifest, or fix source strings to use URNs.\n");
|
|
437
|
+
}
|
|
438
|
+
const datasetUriPrefixes = /* @__PURE__ */ new Map();
|
|
439
|
+
for (const [ds, manifest] of manifestCache) {
|
|
440
|
+
const uriBase = manifest.uriBase;
|
|
441
|
+
if (!uriBase) throw new Error("build-edges: manifest.uriBase is required");
|
|
442
|
+
datasetUriPrefixes.set(ds, `${uriBase}/${ds}/`);
|
|
443
|
+
}
|
|
444
|
+
const crossRefIndex = {};
|
|
445
|
+
for (const ds of datasets) {
|
|
446
|
+
crossRefIndex[ds] = [];
|
|
447
|
+
}
|
|
448
|
+
for (const [sourceDs, edges] of allDatasetEdges) {
|
|
449
|
+
const targets = /* @__PURE__ */ new Set();
|
|
450
|
+
for (const edge of edges) {
|
|
451
|
+
for (const [targetDs, prefix] of datasetUriPrefixes) {
|
|
452
|
+
if (targetDs !== sourceDs && edge.target.startsWith(prefix)) {
|
|
453
|
+
targets.add(targetDs);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
for (const [targetDs, prefix] of datasetUriPrefixes) {
|
|
457
|
+
if (targetDs !== sourceDs && edge.source.startsWith(prefix)) {
|
|
458
|
+
targets.add(targetDs);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
for (const targetDs of targets) {
|
|
463
|
+
if (!crossRefIndex[targetDs].includes(sourceDs)) {
|
|
464
|
+
crossRefIndex[targetDs].push(sourceDs);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
const crossRefPath = join(DATA_DIR, "cross-ref-index.json");
|
|
469
|
+
writeFileSync(crossRefPath, JSON.stringify(crossRefIndex));
|
|
470
|
+
const refCount = Object.values(crossRefIndex).reduce((sum, arr) => sum + arr.length, 0);
|
|
471
|
+
console.log(`Written cross-ref-index.json (${refCount} cross-references across ${datasets.length} datasets)`);
|
|
472
|
+
console.log("Done.");
|
|
473
|
+
}
|
|
474
|
+
var isDirectInvocation = process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
|
|
475
|
+
if (isDirectInvocation) {
|
|
476
|
+
main();
|
|
477
|
+
}
|
|
478
|
+
export {
|
|
479
|
+
main
|
|
480
|
+
};
|
|
@@ -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
|
+
};
|