@glossarist/concept-browser 0.7.77 → 0.7.80
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/astro.config.mjs +1 -0
- package/package.json +2 -2
- package/scripts/bridge-to-astro.mjs +6 -1
- package/scripts/build-edges.js +154 -1
- package/scripts/generate-data.mjs +138 -54
- package/src/__tests__/build-edges-redundancy.test.ts +101 -0
- package/src/__tests__/composables/use-concept-diff.test.ts +240 -0
- package/src/__tests__/composables/use-concept-edges-v2.test.ts +132 -0
- package/src/__tests__/composables/use-dataset-stats.test.ts +96 -0
- package/src/__tests__/config/page-types.test.ts +82 -0
- package/src/__tests__/model-bridge-bridges.test.ts +3 -2
- package/src/__tests__/model-bridge-citation.test.ts +1 -1
- package/src/__tests__/ontology-registry.test.ts +2 -2
- package/src/__tests__/partitive-relation-styling.test.ts +82 -0
- package/src/__tests__/partitive-relation.test.ts +285 -0
- package/src/__tests__/sphere-math.test.ts +56 -0
- package/src/adapters/GraphDataSource.ts +22 -5
- package/src/adapters/UriRouter.ts +5 -0
- package/src/adapters/dataset-stats.ts +13 -0
- package/src/adapters/model-bridge.ts +1 -1
- package/src/adapters/non-verbal/glossarist-augment.d.ts +155 -4
- package/src/adapters/types.ts +36 -0
- package/src/components/AppSidebar.vue +14 -57
- package/src/components/ConceptDetail.vue +29 -0
- package/src/components/ConceptDiffModal.vue +120 -0
- package/src/components/ConceptDiffView.vue +192 -0
- package/src/components/ConceptEditionRail.vue +61 -36
- package/src/components/PartitiveRelationList.vue +139 -0
- package/src/components/RelationSphere.vue +2 -36
- package/src/components/groups/DatasetGroupRenderer.vue +14 -4
- package/src/components/groups/DefaultGroupSidebar.vue +28 -11
- package/src/components/groups/LineageGroupSidebar.vue +17 -29
- package/src/composables/use-concept-diff.ts +231 -0
- package/src/composables/use-concept-edges.ts +98 -5
- package/src/composables/use-dataset-stats.ts +52 -0
- package/src/config/page-types.ts +67 -0
- package/src/config/types.ts +6 -1
- package/src/config/use-site-config.ts +13 -29
- package/src/data/taxonomies.json +24 -1
- package/src/i18n/locales/eng.yml +6 -0
- package/src/utils/partitive-relation-styling.ts +62 -0
- package/src/utils/sphere-math.ts +54 -0
- package/src/views/DatasetView.vue +5 -3
- package/src/views/StatsView.vue +9 -22
- package/src/islands/BibliographyIsland.vue +0 -26
- package/src/islands/ConceptDetailIsland.vue +0 -27
- package/src/islands/ConceptDiffIsland.vue +0 -214
- package/src/islands/ConceptIsland.vue +0 -102
- package/src/islands/GraphIsland.vue +0 -15
- package/src/islands/GroupRendererIsland.vue +0 -15
- package/src/islands/HeaderIsland.vue +0 -10
- package/src/islands/NonVerbalIsland.vue +0 -75
- package/src/islands/RelationSphereIsland.vue +0 -20
- package/src/islands/SearchIsland.vue +0 -23
- package/src/islands/SeriesCardIsland.vue +0 -12
- package/src/islands/ShortcutsIsland.vue +0 -13
- package/src/islands/SidebarIsland.vue +0 -15
- package/src/islands/SupersessionDiffIsland.vue +0 -267
package/astro.config.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glossarist/concept-browser",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.80",
|
|
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": {
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"astro": "^7.0.7",
|
|
38
38
|
"d3": "^7.9.0",
|
|
39
39
|
"favicons": "^7.2.0",
|
|
40
|
-
"glossarist": "^0.4.
|
|
40
|
+
"glossarist": "^0.4.20",
|
|
41
41
|
"js-yaml": "^4.1.0",
|
|
42
42
|
"jszip": "^3.10.1",
|
|
43
43
|
"pinia": "^2.3.1",
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
function buildConceptUri(uriBase, registerId, conceptId) {
|
|
2
|
+
return `${uriBase}/${registerId}/concept/${conceptId}`;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
|
|
1
6
|
/**
|
|
2
7
|
* Data bridge: converts public/data/*.json into Astro content collections.
|
|
3
8
|
*
|
|
@@ -77,7 +82,7 @@ for (const reg of registry) {
|
|
|
77
82
|
const conceptEntry = {
|
|
78
83
|
registerId: id,
|
|
79
84
|
conceptId: concept.id,
|
|
80
|
-
uri:
|
|
85
|
+
uri: buildConceptUri(entry.uriBase || '', id, concept.id),
|
|
81
86
|
status: concept.status || 'valid',
|
|
82
87
|
designations: concept.designations || {},
|
|
83
88
|
eng: concept.eng || concept.designations?.eng || '',
|
package/scripts/build-edges.js
CHANGED
|
@@ -1,3 +1,37 @@
|
|
|
1
|
+
function buildConceptUri(uriBase, registerId, conceptId) {
|
|
2
|
+
return `${uriBase}/${registerId}/concept/${conceptId}`;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
const VALID_COMPLETENESS = new Set(['complete', 'partial']);
|
|
6
|
+
const VALID_CERTAINTY = new Set(['confirmed', 'possible']);
|
|
7
|
+
|
|
8
|
+
function validateCompleteness(value) {
|
|
9
|
+
if (value == null) return 'complete';
|
|
10
|
+
if (!VALID_COMPLETENESS.has(value)) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
`Invalid partitive relation completeness: "${value}". Allowed: complete, partial`,
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function validateCertainty(value) {
|
|
19
|
+
if (value == null) return 'confirmed';
|
|
20
|
+
if (!VALID_CERTAINTY.has(value)) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
`Invalid partitive member certainty: "${value}". Allowed: confirmed, possible`,
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizeLocalizedString(content) {
|
|
29
|
+
if (content == null) return undefined;
|
|
30
|
+
if (typeof content === 'string') return { default: content };
|
|
31
|
+
return content;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
1
35
|
/**
|
|
2
36
|
* Pre-computes cross-reference and domain edges for each dataset.
|
|
3
37
|
* Reads all concept JSON files, extracts structured references and
|
|
@@ -108,7 +142,7 @@ function extractRelated(concept, registerId, uriBase, urnMap) {
|
|
|
108
142
|
const id = ref['gl:id'] || ref['id'];
|
|
109
143
|
if (!source || !id) continue;
|
|
110
144
|
const reg = urnMap.get(source) || source;
|
|
111
|
-
const target =
|
|
145
|
+
const target = buildConceptUri(uriBase, reg, id);
|
|
112
146
|
if (target === sourceUri) continue;
|
|
113
147
|
edges.push({
|
|
114
148
|
source: sourceUri,
|
|
@@ -121,12 +155,116 @@ function extractRelated(concept, registerId, uriBase, urnMap) {
|
|
|
121
155
|
return edges;
|
|
122
156
|
}
|
|
123
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Extracts partitive relations from a concept's gl:partitiveRelations
|
|
160
|
+
* array. Returns relation objects (NOT binary GraphEdges) so the UI
|
|
161
|
+
* can render them with their full set-based semantics: comprehensive +
|
|
162
|
+
* partitives (each with certainty), completeness, plurality, and
|
|
163
|
+
* criterion.
|
|
164
|
+
*
|
|
165
|
+
* Relations are written to partitive_relations.json, keeping
|
|
166
|
+
* edges.json binary-only.
|
|
167
|
+
*
|
|
168
|
+
* v2 shape per concept-model/TODO.partitive-relation-v2.
|
|
169
|
+
*/
|
|
170
|
+
function extractPartitiveRelations(concept, registerId, uriBase, urnMap) {
|
|
171
|
+
const relations = [];
|
|
172
|
+
const sourceUri = concept['@id'];
|
|
173
|
+
for (const rel of concept['gl:partitiveRelations'] || []) {
|
|
174
|
+
const compRef = rel['gl:comprehensive'];
|
|
175
|
+
if (!compRef) continue;
|
|
176
|
+
const comprehensive = resolveConceptUri(compRef, uriBase, urnMap);
|
|
177
|
+
if (!comprehensive) continue;
|
|
178
|
+
|
|
179
|
+
const partitives = (rel['gl:hasPartitive'] || [])
|
|
180
|
+
.map(p => {
|
|
181
|
+
const memberRef = p['gl:ref'] || p;
|
|
182
|
+
const uri = resolveConceptUri(memberRef, uriBase, urnMap);
|
|
183
|
+
if (!uri || uri === sourceUri) return null;
|
|
184
|
+
return {
|
|
185
|
+
uri,
|
|
186
|
+
certainty: validateCertainty(p['gl:certainty']),
|
|
187
|
+
};
|
|
188
|
+
})
|
|
189
|
+
.filter(p => p !== null);
|
|
190
|
+
|
|
191
|
+
if (partitives.length === 0) continue;
|
|
192
|
+
|
|
193
|
+
const plurality = resolvePlurality(rel['gl:hasPlurality']);
|
|
194
|
+
|
|
195
|
+
relations.push({
|
|
196
|
+
source: sourceUri,
|
|
197
|
+
comprehensive,
|
|
198
|
+
partitives,
|
|
199
|
+
completeness: validateCompleteness(rel['gl:completeness']),
|
|
200
|
+
plurality,
|
|
201
|
+
criterion: normalizeLocalizedString(rel['gl:criterion']),
|
|
202
|
+
register: registerId,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
return relations;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function resolvePlurality(plurality) {
|
|
209
|
+
if (!plurality) return null;
|
|
210
|
+
const isShared = plurality['gl:isShared'] ?? plurality.is_shared;
|
|
211
|
+
if (typeof isShared !== 'boolean') return null;
|
|
212
|
+
const isUncertain = plurality['gl:isUncertain'] ?? plurality.is_uncertain ?? false;
|
|
213
|
+
const sharedTypeRef = plurality['gl:sharedType'] ?? plurality.shared_type;
|
|
214
|
+
let sharedType = null;
|
|
215
|
+
if (sharedTypeRef) {
|
|
216
|
+
const source = sharedTypeRef['gl:source'] ?? sharedTypeRef.source;
|
|
217
|
+
const id = sharedTypeRef['gl:id'] ?? sharedTypeRef.id;
|
|
218
|
+
if (source && id) sharedType = `${source}:${id}`;
|
|
219
|
+
}
|
|
220
|
+
return { isShared, isUncertain, sharedType };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function resolveConceptUri(ref, uriBase, urnMap) {
|
|
224
|
+
const source = ref['gl:source'] || ref['source'];
|
|
225
|
+
const id = ref['gl:id'] || ref['id'];
|
|
226
|
+
if (!source || !id) return null;
|
|
227
|
+
const reg = urnMap.get(source) || source;
|
|
228
|
+
return buildConceptUri(uriBase, reg, id);
|
|
229
|
+
}
|
|
230
|
+
|
|
124
231
|
const EXTRACTORS = [extractReferences, extractRelated, extractDomains];
|
|
125
232
|
|
|
126
233
|
function extractAllEdges(concept, registerId, uriBase, urnMap) {
|
|
127
234
|
return EXTRACTORS.flatMap(fn => fn(concept, registerId, uriBase, urnMap));
|
|
128
235
|
}
|
|
129
236
|
|
|
237
|
+
/**
|
|
238
|
+
* Per concept-model TODO.partitive-relation-v2 item 14:
|
|
239
|
+
* a PartitiveRelation A → {b, c, d} subsumes binary has_part edges
|
|
240
|
+
* for the same pairs. Warn (don't fail) when both encodings exist.
|
|
241
|
+
*
|
|
242
|
+
* Binary edge types considered partitive: has_part, is_part_of,
|
|
243
|
+
* broader_partitive, narrower_partitive.
|
|
244
|
+
*/
|
|
245
|
+
const BINARY_PARTITIVE_TYPES = new Set(['has_part', 'is_part_of', 'broader_partitive', 'narrower_partitive']);
|
|
246
|
+
|
|
247
|
+
function warnBinaryPartitiveRedundancy(edges, relations, registerId) {
|
|
248
|
+
if (!relations?.length) return;
|
|
249
|
+
const partitivePairs = new Set();
|
|
250
|
+
for (const rel of relations) {
|
|
251
|
+
for (const member of rel.partitives ?? []) {
|
|
252
|
+
const memberUri = member.uri ?? member;
|
|
253
|
+
partitivePairs.add(`${rel.comprehensive}|${memberUri}`);
|
|
254
|
+
partitivePairs.add(`${memberUri}|${rel.comprehensive}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const redundant = edges.filter(e =>
|
|
258
|
+
BINARY_PARTITIVE_TYPES.has(e.type)
|
|
259
|
+
&& partitivePairs.has(`${e.source}|${e.target}`),
|
|
260
|
+
);
|
|
261
|
+
if (redundant.length > 0) {
|
|
262
|
+
console.warn(
|
|
263
|
+
` ⚠ ${registerId}: ${redundant.length} binary has_part/is_part_of edge(s) duplicate a PartitiveRelation member — prefer the PartitiveRelation encoding (concept-model TODO item 14).`,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
130
268
|
// --- Build ---
|
|
131
269
|
|
|
132
270
|
function buildEdgesForDataset(datasetDir, registerId, uriBase, urnMap, manifest) {
|
|
@@ -140,6 +278,7 @@ function buildEdgesForDataset(datasetDir, registerId, uriBase, urnMap, manifest)
|
|
|
140
278
|
console.log(` Processing ${files.length} concepts...`);
|
|
141
279
|
|
|
142
280
|
const allEdges = [];
|
|
281
|
+
const allPartitiveRelations = [];
|
|
143
282
|
const allSourceRefs = [];
|
|
144
283
|
const domainConceptCount = new Map();
|
|
145
284
|
let processed = 0;
|
|
@@ -148,7 +287,10 @@ function buildEdgesForDataset(datasetDir, registerId, uriBase, urnMap, manifest)
|
|
|
148
287
|
try {
|
|
149
288
|
const data = JSON.parse(readFileSync(join(conceptsDir, file), 'utf-8'));
|
|
150
289
|
const edges = extractAllEdges(data, registerId, uriBase, urnMap);
|
|
290
|
+
const relations = extractPartitiveRelations(data, registerId, uriBase, urnMap);
|
|
291
|
+
warnBinaryPartitiveRedundancy(edges, relations, registerId);
|
|
151
292
|
allEdges.push(...edges);
|
|
293
|
+
allPartitiveRelations.push(...relations);
|
|
152
294
|
allSourceRefs.push(...extractSourceRefs(data, registerId));
|
|
153
295
|
|
|
154
296
|
for (const edge of edges) {
|
|
@@ -186,6 +328,17 @@ function buildEdgesForDataset(datasetDir, registerId, uriBase, urnMap, manifest)
|
|
|
186
328
|
writeFileSync(outputPath, JSON.stringify(output));
|
|
187
329
|
console.log(` Written ${deduped.length} edges to edges.json (${(JSON.stringify(output).length / 1024).toFixed(1)} KB)`);
|
|
188
330
|
|
|
331
|
+
// Write partitive_relations.json (parallel to edges.json) for one-to-many
|
|
332
|
+
// partitive decompositions. See TODO.partitive-relation-v2.
|
|
333
|
+
const relationsOutput = {
|
|
334
|
+
registerId,
|
|
335
|
+
relationCount: allPartitiveRelations.length,
|
|
336
|
+
relations: allPartitiveRelations,
|
|
337
|
+
};
|
|
338
|
+
const relationsPath = join(datasetDir, 'partitive_relations.json');
|
|
339
|
+
writeFileSync(relationsPath, JSON.stringify(relationsOutput));
|
|
340
|
+
console.log(` Written ${allPartitiveRelations.length} partitive relations to partitive_relations.json (${(JSON.stringify(relationsOutput).length / 1024).toFixed(1)} KB)`);
|
|
341
|
+
|
|
189
342
|
// Build domain-nodes.json from manifest sections (authoritative source)
|
|
190
343
|
const manifestSections = manifest.sections;
|
|
191
344
|
if (manifestSections && manifestSections.length > 0) {
|
|
@@ -14,6 +14,11 @@ import { buildVersionHistoryTurtle } from './lib/version-turtle.mjs';
|
|
|
14
14
|
import { buildBibliographyTurtle } from './lib/bibliography-turtle.mjs';
|
|
15
15
|
import { ttlLit } from './lib/turtle-escape.mjs';
|
|
16
16
|
import { firstNonEmpty } from './lib/first-non-empty.mjs';
|
|
17
|
+
function buildConceptUri(uriBase, registerId, conceptId) {
|
|
18
|
+
return `${uriBase}/${registerId}/concept/${conceptId}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
17
22
|
const __dirname = path.dirname(new URL(import.meta.url).pathname);
|
|
18
23
|
const ROOT = process.cwd();
|
|
19
24
|
const PUBLIC = path.join(ROOT, 'public');
|
|
@@ -60,6 +65,7 @@ function loadConceptFile(filePath) {
|
|
|
60
65
|
|
|
61
66
|
// Managed concept-level fields
|
|
62
67
|
if (mc.related) result._related = mc.related;
|
|
68
|
+
if (mc.partitive_relations) result._partitiveRelations = mc.partitive_relations;
|
|
63
69
|
if (mc.data.domains) result._domains = mc.data.domains;
|
|
64
70
|
if (mc.dates) result._dates = mc.dates;
|
|
65
71
|
if (mc.sources) result._sources = mc.sources;
|
|
@@ -98,14 +104,14 @@ async function writeDatasetRdf(register, manifest, concepts, refMaps, opts) {
|
|
|
98
104
|
const datasetIri = `${uriBase}/${register}/`;
|
|
99
105
|
const topConceptUris = concepts
|
|
100
106
|
.slice(0, 32)
|
|
101
|
-
.map(c =>
|
|
107
|
+
.map(c => buildConceptUri(uriBase, register, c.id));
|
|
102
108
|
|
|
103
109
|
const sections = (manifest.sections ?? []).map(section => {
|
|
104
110
|
const sectionId = section.id ?? section.slug ?? section.title;
|
|
105
111
|
return {
|
|
106
112
|
collectionIri: `${uriBase}/${register}/section/${sectionId}`,
|
|
107
113
|
title: section.title ?? section.name ?? sectionId,
|
|
108
|
-
memberUris: (section.members ?? []).map(id =>
|
|
114
|
+
memberUris: (section.members ?? []).map(id => buildConceptUri(uriBase, register, id)),
|
|
109
115
|
};
|
|
110
116
|
});
|
|
111
117
|
|
|
@@ -242,15 +248,16 @@ function defsToJsonLd(defs) {
|
|
|
242
248
|
.filter(d => d['gl:content']);
|
|
243
249
|
}
|
|
244
250
|
|
|
245
|
-
function refToJsonLd(ref) {
|
|
251
|
+
function refToJsonLd(ref, typeName = 'gl:Ref') {
|
|
246
252
|
if (!ref) return undefined;
|
|
247
|
-
const refObj = { '@type':
|
|
253
|
+
const refObj = { '@type': typeName };
|
|
248
254
|
if (typeof ref === 'string') {
|
|
249
255
|
refObj['gl:source'] = ref;
|
|
250
256
|
} else {
|
|
251
257
|
if (ref.source) refObj['gl:source'] = ref.source;
|
|
252
258
|
if (ref.id) refObj['gl:id'] = ref.id;
|
|
253
259
|
if (ref.version) refObj['gl:version'] = ref.version;
|
|
260
|
+
if (ref.text) refObj['gl:text'] = ref.text;
|
|
254
261
|
}
|
|
255
262
|
return refObj;
|
|
256
263
|
}
|
|
@@ -366,12 +373,12 @@ function buildPatternIndex(datasets, registerCache) {
|
|
|
366
373
|
|
|
367
374
|
function resolveRefUri(term, refMaps) {
|
|
368
375
|
const resolved = refMaps.patternIndex.resolve(term);
|
|
369
|
-
if (resolved) return
|
|
376
|
+
if (resolved) return buildConceptUri(refMaps.uriBase, resolved.datasetId, resolved.conceptId);
|
|
370
377
|
|
|
371
378
|
const ievMatch = term.match(/^IEV:(\d+[-\d]+)$/);
|
|
372
379
|
if (ievMatch) {
|
|
373
380
|
const dsId = refMaps.refPrefixMap['IEV'];
|
|
374
|
-
if (dsId) return
|
|
381
|
+
if (dsId) return buildConceptUri(refMaps.uriBase, dsId, ievMatch[1]);
|
|
375
382
|
}
|
|
376
383
|
return null;
|
|
377
384
|
}
|
|
@@ -405,7 +412,7 @@ function resolveIevRef(display, term, refPrefixMap, uriBase) {
|
|
|
405
412
|
if (!display.startsWith('IEV:')) return null;
|
|
406
413
|
const datasetId = refPrefixMap['IEV'];
|
|
407
414
|
if (!datasetId) return null;
|
|
408
|
-
return { id:
|
|
415
|
+
return { id: buildConceptUri(uriBase, datasetId, display.slice(4)), term };
|
|
409
416
|
}
|
|
410
417
|
|
|
411
418
|
/**
|
|
@@ -414,7 +421,7 @@ function resolveIevRef(display, term, refPrefixMap, uriBase) {
|
|
|
414
421
|
function resolvePatternRef(identifier, display, refPrefixMap, patternIndex, uriBase) {
|
|
415
422
|
const r = patternIndex.resolve(identifier);
|
|
416
423
|
if (!r) return null;
|
|
417
|
-
return { id:
|
|
424
|
+
return { id: buildConceptUri(uriBase, r.datasetId, r.conceptId), term: display };
|
|
418
425
|
}
|
|
419
426
|
|
|
420
427
|
/**
|
|
@@ -444,7 +451,7 @@ function handleCiteRef(parsed, allSources) {
|
|
|
444
451
|
function handleNumeric(parsed, register, uriBase) {
|
|
445
452
|
if (!register) return null;
|
|
446
453
|
const term = parsed.label ?? parsed.id;
|
|
447
|
-
return { id:
|
|
454
|
+
return { id: buildConceptUri(uriBase, register, parsed.id), term };
|
|
448
455
|
}
|
|
449
456
|
|
|
450
457
|
/**
|
|
@@ -459,7 +466,7 @@ function handleDesignation(parsed, refMaps) {
|
|
|
459
466
|
const display = parsed.label ?? designation;
|
|
460
467
|
const conceptId = refMaps.designationLookup?.get(designation.toLowerCase());
|
|
461
468
|
return {
|
|
462
|
-
id:
|
|
469
|
+
id: buildConceptUri(refMaps.uriBase, register, conceptId ?? designation),
|
|
463
470
|
term: display,
|
|
464
471
|
};
|
|
465
472
|
}
|
|
@@ -485,7 +492,7 @@ function handleUnresolved(body, refMaps) {
|
|
|
485
492
|
// Same-dataset: {{conceptId, displayTerm}} where conceptId is numeric/X.Y
|
|
486
493
|
const register = refMaps.register;
|
|
487
494
|
if (register && (/^\d/.test(identifier) || /^[A-Z]\.\d/.test(identifier))) {
|
|
488
|
-
return { id:
|
|
495
|
+
return { id: buildConceptUri(refMaps.uriBase, register, identifier), term: display };
|
|
489
496
|
}
|
|
490
497
|
return null;
|
|
491
498
|
}
|
|
@@ -542,7 +549,7 @@ function extractInlineRefs(localizedData, refMaps, conceptSources = []) {
|
|
|
542
549
|
const term = parsed.label ?? uri;
|
|
543
550
|
const pattern = refMaps.patternIndex.resolve(uri);
|
|
544
551
|
if (pattern) {
|
|
545
|
-
ref = { id:
|
|
552
|
+
ref = { id: buildConceptUri(refMaps.uriBase, pattern.datasetId, pattern.conceptId), term };
|
|
546
553
|
} else {
|
|
547
554
|
ref = { id: uri, term };
|
|
548
555
|
}
|
|
@@ -571,7 +578,7 @@ function yamlToJsonLd(conceptYaml, register, refMaps) {
|
|
|
571
578
|
const base = refMaps.uriBase;
|
|
572
579
|
const doc = {
|
|
573
580
|
'@context': 'https://glossarist.org/ns/context.jsonld',
|
|
574
|
-
'@id':
|
|
581
|
+
'@id': buildConceptUri(base, register, termid),
|
|
575
582
|
'@type': 'gl:Concept',
|
|
576
583
|
'gl:identifier': termid,
|
|
577
584
|
};
|
|
@@ -672,6 +679,50 @@ function yamlToJsonLd(conceptYaml, register, refMaps) {
|
|
|
672
679
|
});
|
|
673
680
|
}
|
|
674
681
|
|
|
682
|
+
if (conceptYaml._partitiveRelations?.length > 0) {
|
|
683
|
+
doc['gl:partitiveRelations'] = conceptYaml._partitiveRelations.map(rel => {
|
|
684
|
+
const out = { '@type': 'gl:PartitiveRelation' };
|
|
685
|
+
if (rel.comprehensive) {
|
|
686
|
+
out['gl:comprehensive'] = refToJsonLd(rel.comprehensive, 'gl:ConceptRef');
|
|
687
|
+
}
|
|
688
|
+
if (Array.isArray(rel.partitives) && rel.partitives.length > 0) {
|
|
689
|
+
out['gl:hasPartitive'] = rel.partitives.map(member => {
|
|
690
|
+
const m = { '@type': 'gl:PartitiveMember' };
|
|
691
|
+
if (member.ref) {
|
|
692
|
+
m['gl:ref'] = refToJsonLd(member.ref, 'gl:ConceptRef');
|
|
693
|
+
} else {
|
|
694
|
+
m['gl:ref'] = refToJsonLd(member, 'gl:ConceptRef');
|
|
695
|
+
}
|
|
696
|
+
if (member.certainty) m['gl:certainty'] = member.certainty;
|
|
697
|
+
return m;
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
if (rel.completeness) {
|
|
701
|
+
out['gl:completeness'] = rel.completeness;
|
|
702
|
+
}
|
|
703
|
+
if (rel.plurality) {
|
|
704
|
+
const pl = { '@type': 'gl:TypeSharedPlurality' };
|
|
705
|
+
pl['gl:isShared'] = rel.plurality.is_shared ?? rel.plurality.isShared ?? false;
|
|
706
|
+
if (rel.plurality.is_uncertain ?? rel.plurality.isUncertain) {
|
|
707
|
+
pl['gl:isUncertain'] = rel.plurality.is_uncertain ?? rel.plurality.isUncertain;
|
|
708
|
+
}
|
|
709
|
+
if (rel.plurality.shared_type ?? rel.plurality.sharedType) {
|
|
710
|
+
pl['gl:sharedType'] = refToJsonLd(
|
|
711
|
+
rel.plurality.shared_type ?? rel.plurality.sharedType,
|
|
712
|
+
'gl:ConceptRef',
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
out['gl:hasPlurality'] = pl;
|
|
716
|
+
}
|
|
717
|
+
if (rel.criterion) {
|
|
718
|
+
out['gl:criterion'] = typeof rel.criterion === 'string'
|
|
719
|
+
? { default: rel.criterion }
|
|
720
|
+
: rel.criterion;
|
|
721
|
+
}
|
|
722
|
+
return out;
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
|
|
675
726
|
return doc;
|
|
676
727
|
}
|
|
677
728
|
|
|
@@ -920,8 +971,74 @@ async function processDataset(dir, register, opts) {
|
|
|
920
971
|
|
|
921
972
|
const dsRefMaps = { ...refMaps, register, designationLookup };
|
|
922
973
|
|
|
923
|
-
const
|
|
924
|
-
|
|
974
|
+
const stats = {
|
|
975
|
+
sourceMap: new Map(),
|
|
976
|
+
relTypeCounts: {},
|
|
977
|
+
partitiveRelations: {
|
|
978
|
+
count: 0,
|
|
979
|
+
byCompleteness: { complete: 0, partial: 0 },
|
|
980
|
+
byMemberCertainty: { confirmed: 0, possible: 0 },
|
|
981
|
+
withCriterion: 0,
|
|
982
|
+
withoutCriterion: 0,
|
|
983
|
+
withPlurality: 0,
|
|
984
|
+
},
|
|
985
|
+
};
|
|
986
|
+
|
|
987
|
+
const STATS_PROCESSORS = [
|
|
988
|
+
function collectSources(cy, termid, s) {
|
|
989
|
+
const refs = new Set();
|
|
990
|
+
const allSources = [
|
|
991
|
+
...(cy._sources || []),
|
|
992
|
+
...opts.languages.flatMap(l => cy[l]?.sources || []),
|
|
993
|
+
];
|
|
994
|
+
for (const src of allSources) {
|
|
995
|
+
const ref = src.origin?.ref?.source || src.origin?.ref?.id || 'Unknown';
|
|
996
|
+
refs.add(ref);
|
|
997
|
+
if (!s.sourceMap.has(ref)) s.sourceMap.set(ref, { ref, types: new Set(), conceptIds: [] });
|
|
998
|
+
if (src.type) s.sourceMap.get(ref).types.add(src.type);
|
|
999
|
+
}
|
|
1000
|
+
for (const ref of refs) s.sourceMap.get(ref)?.conceptIds.push(termid);
|
|
1001
|
+
},
|
|
1002
|
+
function countRelationships(cy, _termid, s) {
|
|
1003
|
+
for (const r of cy._related || []) {
|
|
1004
|
+
const type = r.type || 'unknown';
|
|
1005
|
+
s.relTypeCounts[type] = (s.relTypeCounts[type] || 0) + 1;
|
|
1006
|
+
}
|
|
1007
|
+
},
|
|
1008
|
+
function countPartitiveRelations(cy, _termid, s) {
|
|
1009
|
+
for (const rel of cy._partitiveRelations || []) {
|
|
1010
|
+
s.partitiveRelations.count += 1;
|
|
1011
|
+
const completeness = rel.completeness || 'complete';
|
|
1012
|
+
s.partitiveRelations.byCompleteness[completeness] =
|
|
1013
|
+
(s.partitiveRelations.byCompleteness[completeness] || 0) + 1;
|
|
1014
|
+
const partitives = rel.partitives || [];
|
|
1015
|
+
for (const member of partitives) {
|
|
1016
|
+
const certainty = member.certainty || 'confirmed';
|
|
1017
|
+
s.partitiveRelations.byMemberCertainty[certainty] =
|
|
1018
|
+
(s.partitiveRelations.byMemberCertainty[certainty] || 0) + 1;
|
|
1019
|
+
}
|
|
1020
|
+
if (rel.criterion) {
|
|
1021
|
+
s.partitiveRelations.withCriterion += 1;
|
|
1022
|
+
} else {
|
|
1023
|
+
s.partitiveRelations.withoutCriterion += 1;
|
|
1024
|
+
}
|
|
1025
|
+
if (rel.plurality) {
|
|
1026
|
+
s.partitiveRelations.withPlurality += 1;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
},
|
|
1030
|
+
function countLanguages(cy, _termid, _s) {
|
|
1031
|
+
for (const lang of opts.languages) {
|
|
1032
|
+
const lc = cy[lang];
|
|
1033
|
+
if (lc) {
|
|
1034
|
+
if (lc.terms?.length > 0) langTermCounts[lang] = (langTermCounts[lang] || 0) + 1;
|
|
1035
|
+
if (lc.definition && (Array.isArray(lc.definition) ? lc.definition.some(d => d.content) : lc.definition)) {
|
|
1036
|
+
langDefCounts[lang] = (langDefCounts[lang] || 0) + 1;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
},
|
|
1041
|
+
];
|
|
925
1042
|
|
|
926
1043
|
for (let i = 0; i < files.length; i++) {
|
|
927
1044
|
const file = files[i];
|
|
@@ -933,21 +1050,17 @@ async function processDataset(dir, register, opts) {
|
|
|
933
1050
|
const jsonld = yamlToJsonLd(conceptYaml, register, dsRefMaps);
|
|
934
1051
|
writeJson(path.join(conceptsDir, `${termid}.json`), jsonld);
|
|
935
1052
|
|
|
936
|
-
// Generate Turtle format
|
|
937
1053
|
const ttlContent = conceptJsonToTurtle(jsonld);
|
|
938
1054
|
fs.writeFileSync(path.join(conceptsDir, `${termid}.ttl`), ttlContent);
|
|
939
1055
|
|
|
940
|
-
// Generate SKOS JSON-LD format
|
|
941
1056
|
const skosJsonLd = conceptJsonToSkosJsonLd(jsonld);
|
|
942
1057
|
fs.writeFileSync(path.join(conceptsDir, `${termid}.jsonld`), skosJsonLd);
|
|
943
1058
|
|
|
944
|
-
// Generate TBX-XML format
|
|
945
1059
|
const tbxContent = conceptJsonToTbx(jsonld);
|
|
946
1060
|
if (tbxContent) {
|
|
947
1061
|
fs.writeFileSync(path.join(conceptsDir, `${termid}.tbx`), tbxContent);
|
|
948
1062
|
}
|
|
949
1063
|
|
|
950
|
-
// Copy source YAML
|
|
951
1064
|
fs.copyFileSync(path.join(dir, file), path.join(conceptsDir, `${termid}.yaml`));
|
|
952
1065
|
|
|
953
1066
|
concepts.push({
|
|
@@ -957,38 +1070,8 @@ async function processDataset(dir, register, opts) {
|
|
|
957
1070
|
status: conceptYaml.eng?.entry_status || 'valid',
|
|
958
1071
|
});
|
|
959
1072
|
|
|
960
|
-
const
|
|
961
|
-
|
|
962
|
-
...(conceptYaml._sources || []),
|
|
963
|
-
...opts.languages.flatMap(l => conceptYaml[l]?.sources || []),
|
|
964
|
-
];
|
|
965
|
-
for (const src of allSourceArrays) {
|
|
966
|
-
const ref = src.origin?.ref?.source || src.origin?.ref?.id || 'Unknown';
|
|
967
|
-
conceptSourceRefs.add(ref);
|
|
968
|
-
if (!sourceMap.has(ref)) {
|
|
969
|
-
sourceMap.set(ref, { ref, types: new Set(), conceptIds: [] });
|
|
970
|
-
}
|
|
971
|
-
if (src.type) sourceMap.get(ref).types.add(src.type);
|
|
972
|
-
}
|
|
973
|
-
for (const ref of conceptSourceRefs) {
|
|
974
|
-
sourceMap.get(ref)?.conceptIds.push(termid);
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
for (const r of conceptYaml._related || []) {
|
|
978
|
-
const type = r.type || 'unknown';
|
|
979
|
-
relTypeCounts[type] = (relTypeCounts[type] || 0) + 1;
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
for (const lang of opts.languages) {
|
|
983
|
-
const lc = conceptYaml[lang];
|
|
984
|
-
if (lc) {
|
|
985
|
-
if (lc.terms && lc.terms.length > 0) {
|
|
986
|
-
langTermCounts[lang] = (langTermCounts[lang] || 0) + 1;
|
|
987
|
-
}
|
|
988
|
-
if (lc.definition && (Array.isArray(lc.definition) ? lc.definition.some(d => d.content) : lc.definition)) {
|
|
989
|
-
langDefCounts[lang] = (langDefCounts[lang] || 0) + 1;
|
|
990
|
-
}
|
|
991
|
-
}
|
|
1073
|
+
for (const processor of STATS_PROCESSORS) {
|
|
1074
|
+
processor(conceptYaml, termid, stats);
|
|
992
1075
|
}
|
|
993
1076
|
} catch (e) {
|
|
994
1077
|
console.warn(` Skipping ${file}: ${e.message}`);
|
|
@@ -1097,7 +1180,7 @@ async function processDataset(dir, register, opts) {
|
|
|
1097
1180
|
}
|
|
1098
1181
|
}
|
|
1099
1182
|
|
|
1100
|
-
const sourceStats = Array.from(sourceMap.values())
|
|
1183
|
+
const sourceStats = Array.from(stats.sourceMap.values())
|
|
1101
1184
|
.map(s => ({
|
|
1102
1185
|
ref: s.ref,
|
|
1103
1186
|
types: Array.from(s.types),
|
|
@@ -1105,13 +1188,14 @@ async function processDataset(dir, register, opts) {
|
|
|
1105
1188
|
conceptIds: s.conceptIds,
|
|
1106
1189
|
}))
|
|
1107
1190
|
.sort((a, b) => b.conceptCount - a.conceptCount);
|
|
1108
|
-
const totalRelationships = Object.values(relTypeCounts).reduce((a, b) => a + b, 0);
|
|
1191
|
+
const totalRelationships = Object.values(stats.relTypeCounts).reduce((a, b) => a + b, 0);
|
|
1109
1192
|
|
|
1110
1193
|
writeJson(path.join(DATA, register, 'stats.json'), {
|
|
1111
1194
|
sourceCount: sourceStats.length,
|
|
1112
1195
|
sources: sourceStats,
|
|
1113
1196
|
relationshipCount: totalRelationships,
|
|
1114
|
-
relationshipTypes: relTypeCounts,
|
|
1197
|
+
relationshipTypes: stats.relTypeCounts,
|
|
1198
|
+
partitiveRelations: stats.partitiveRelations,
|
|
1115
1199
|
});
|
|
1116
1200
|
|
|
1117
1201
|
const manifest = {
|