@glossarist/concept-browser 0.7.78 → 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/package.json +2 -2
- package/scripts/build-edges.js +109 -39
- package/scripts/generate-data.mjs +63 -27
- 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/adapters/GraphDataSource.ts +16 -6
- package/src/adapters/dataset-stats.ts +13 -0
- package/src/adapters/non-verbal/glossarist-augment.d.ts +155 -4
- package/src/adapters/types.ts +27 -17
- 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/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 +66 -45
- 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/views/DatasetView.vue +5 -3
- package/src/views/StatsView.vue +9 -22
- package/src/__tests__/hyperedge-styling.test.ts +0 -57
- package/src/__tests__/partitive-hyperedge.test.ts +0 -199
- package/src/components/PartitiveHyperedgeList.vue +0 -112
- 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/src/utils/hyperedge-styling.ts +0 -48
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",
|
package/scripts/build-edges.js
CHANGED
|
@@ -2,20 +2,30 @@ function buildConceptUri(uriBase, registerId, conceptId) {
|
|
|
2
2
|
return `${uriBase}/${registerId}/concept/${conceptId}`;
|
|
3
3
|
}
|
|
4
4
|
|
|
5
|
-
const
|
|
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
|
+
}
|
|
6
17
|
|
|
7
|
-
function
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
out.push(m);
|
|
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
|
+
);
|
|
14
24
|
}
|
|
15
|
-
return
|
|
25
|
+
return value;
|
|
16
26
|
}
|
|
17
27
|
|
|
18
|
-
function
|
|
28
|
+
function normalizeLocalizedString(content) {
|
|
19
29
|
if (content == null) return undefined;
|
|
20
30
|
if (typeof content === 'string') return { default: content };
|
|
21
31
|
return content;
|
|
@@ -146,41 +156,68 @@ function extractRelated(concept, registerId, uriBase, urnMap) {
|
|
|
146
156
|
}
|
|
147
157
|
|
|
148
158
|
/**
|
|
149
|
-
* Extracts partitive
|
|
150
|
-
* array. Returns
|
|
159
|
+
* Extracts partitive relations from a concept's gl:partitiveRelations
|
|
160
|
+
* array. Returns relation objects (NOT binary GraphEdges) so the UI
|
|
151
161
|
* can render them with their full set-based semantics: comprehensive +
|
|
152
|
-
*
|
|
153
|
-
*
|
|
162
|
+
* partitives (each with certainty), completeness, plurality, and
|
|
163
|
+
* criterion.
|
|
154
164
|
*
|
|
155
|
-
*
|
|
165
|
+
* Relations are written to partitive_relations.json, keeping
|
|
156
166
|
* edges.json binary-only.
|
|
167
|
+
*
|
|
168
|
+
* v2 shape per concept-model/TODO.partitive-relation-v2.
|
|
157
169
|
*/
|
|
158
|
-
function
|
|
159
|
-
const
|
|
170
|
+
function extractPartitiveRelations(concept, registerId, uriBase, urnMap) {
|
|
171
|
+
const relations = [];
|
|
160
172
|
const sourceUri = concept['@id'];
|
|
161
|
-
for (const
|
|
162
|
-
const compRef =
|
|
173
|
+
for (const rel of concept['gl:partitiveRelations'] || []) {
|
|
174
|
+
const compRef = rel['gl:comprehensive'];
|
|
163
175
|
if (!compRef) continue;
|
|
164
176
|
const comprehensive = resolveConceptUri(compRef, uriBase, urnMap);
|
|
165
177
|
if (!comprehensive) continue;
|
|
166
178
|
|
|
167
|
-
const
|
|
168
|
-
.map(p =>
|
|
169
|
-
|
|
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;
|
|
170
192
|
|
|
171
|
-
|
|
193
|
+
const plurality = resolvePlurality(rel['gl:hasPlurality']);
|
|
172
194
|
|
|
173
|
-
|
|
195
|
+
relations.push({
|
|
174
196
|
source: sourceUri,
|
|
175
197
|
comprehensive,
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
198
|
+
partitives,
|
|
199
|
+
completeness: validateCompleteness(rel['gl:completeness']),
|
|
200
|
+
plurality,
|
|
201
|
+
criterion: normalizeLocalizedString(rel['gl:criterion']),
|
|
180
202
|
register: registerId,
|
|
181
203
|
});
|
|
182
204
|
}
|
|
183
|
-
return
|
|
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 };
|
|
184
221
|
}
|
|
185
222
|
|
|
186
223
|
function resolveConceptUri(ref, uriBase, urnMap) {
|
|
@@ -197,6 +234,37 @@ function extractAllEdges(concept, registerId, uriBase, urnMap) {
|
|
|
197
234
|
return EXTRACTORS.flatMap(fn => fn(concept, registerId, uriBase, urnMap));
|
|
198
235
|
}
|
|
199
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
|
+
|
|
200
268
|
// --- Build ---
|
|
201
269
|
|
|
202
270
|
function buildEdgesForDataset(datasetDir, registerId, uriBase, urnMap, manifest) {
|
|
@@ -210,7 +278,7 @@ function buildEdgesForDataset(datasetDir, registerId, uriBase, urnMap, manifest)
|
|
|
210
278
|
console.log(` Processing ${files.length} concepts...`);
|
|
211
279
|
|
|
212
280
|
const allEdges = [];
|
|
213
|
-
const
|
|
281
|
+
const allPartitiveRelations = [];
|
|
214
282
|
const allSourceRefs = [];
|
|
215
283
|
const domainConceptCount = new Map();
|
|
216
284
|
let processed = 0;
|
|
@@ -219,8 +287,10 @@ function buildEdgesForDataset(datasetDir, registerId, uriBase, urnMap, manifest)
|
|
|
219
287
|
try {
|
|
220
288
|
const data = JSON.parse(readFileSync(join(conceptsDir, file), 'utf-8'));
|
|
221
289
|
const edges = extractAllEdges(data, registerId, uriBase, urnMap);
|
|
290
|
+
const relations = extractPartitiveRelations(data, registerId, uriBase, urnMap);
|
|
291
|
+
warnBinaryPartitiveRedundancy(edges, relations, registerId);
|
|
222
292
|
allEdges.push(...edges);
|
|
223
|
-
|
|
293
|
+
allPartitiveRelations.push(...relations);
|
|
224
294
|
allSourceRefs.push(...extractSourceRefs(data, registerId));
|
|
225
295
|
|
|
226
296
|
for (const edge of edges) {
|
|
@@ -258,16 +328,16 @@ function buildEdgesForDataset(datasetDir, registerId, uriBase, urnMap, manifest)
|
|
|
258
328
|
writeFileSync(outputPath, JSON.stringify(output));
|
|
259
329
|
console.log(` Written ${deduped.length} edges to edges.json (${(JSON.stringify(output).length / 1024).toFixed(1)} KB)`);
|
|
260
330
|
|
|
261
|
-
// Write
|
|
262
|
-
// partitive decompositions. See TODO.
|
|
263
|
-
const
|
|
331
|
+
// Write partitive_relations.json (parallel to edges.json) for one-to-many
|
|
332
|
+
// partitive decompositions. See TODO.partitive-relation-v2.
|
|
333
|
+
const relationsOutput = {
|
|
264
334
|
registerId,
|
|
265
|
-
|
|
266
|
-
|
|
335
|
+
relationCount: allPartitiveRelations.length,
|
|
336
|
+
relations: allPartitiveRelations,
|
|
267
337
|
};
|
|
268
|
-
const
|
|
269
|
-
writeFileSync(
|
|
270
|
-
console.log(` Written ${
|
|
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)`);
|
|
271
341
|
|
|
272
342
|
// Build domain-nodes.json from manifest sections (authoritative source)
|
|
273
343
|
const manifestSections = manifest.sections;
|
|
@@ -65,7 +65,7 @@ function loadConceptFile(filePath) {
|
|
|
65
65
|
|
|
66
66
|
// Managed concept-level fields
|
|
67
67
|
if (mc.related) result._related = mc.related;
|
|
68
|
-
if (mc.
|
|
68
|
+
if (mc.partitive_relations) result._partitiveRelations = mc.partitive_relations;
|
|
69
69
|
if (mc.data.domains) result._domains = mc.data.domains;
|
|
70
70
|
if (mc.dates) result._dates = mc.dates;
|
|
71
71
|
if (mc.sources) result._sources = mc.sources;
|
|
@@ -679,23 +679,45 @@ function yamlToJsonLd(conceptYaml, register, refMaps) {
|
|
|
679
679
|
});
|
|
680
680
|
}
|
|
681
681
|
|
|
682
|
-
if (conceptYaml.
|
|
683
|
-
doc['gl:
|
|
684
|
-
const out = { '@type': 'gl:
|
|
685
|
-
if (
|
|
686
|
-
out['gl:comprehensive'] = refToJsonLd(
|
|
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
687
|
}
|
|
688
|
-
if (Array.isArray(
|
|
689
|
-
out['gl:
|
|
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
|
+
});
|
|
690
699
|
}
|
|
691
|
-
if (
|
|
692
|
-
out['gl:
|
|
700
|
+
if (rel.completeness) {
|
|
701
|
+
out['gl:completeness'] = rel.completeness;
|
|
693
702
|
}
|
|
694
|
-
if (
|
|
695
|
-
|
|
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;
|
|
696
716
|
}
|
|
697
|
-
if (
|
|
698
|
-
out['gl:
|
|
717
|
+
if (rel.criterion) {
|
|
718
|
+
out['gl:criterion'] = typeof rel.criterion === 'string'
|
|
719
|
+
? { default: rel.criterion }
|
|
720
|
+
: rel.criterion;
|
|
699
721
|
}
|
|
700
722
|
return out;
|
|
701
723
|
});
|
|
@@ -952,7 +974,14 @@ async function processDataset(dir, register, opts) {
|
|
|
952
974
|
const stats = {
|
|
953
975
|
sourceMap: new Map(),
|
|
954
976
|
relTypeCounts: {},
|
|
955
|
-
|
|
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
|
+
},
|
|
956
985
|
};
|
|
957
986
|
|
|
958
987
|
const STATS_PROCESSORS = [
|
|
@@ -976,18 +1005,25 @@ async function processDataset(dir, register, opts) {
|
|
|
976
1005
|
s.relTypeCounts[type] = (s.relTypeCounts[type] || 0) + 1;
|
|
977
1006
|
}
|
|
978
1007
|
},
|
|
979
|
-
function
|
|
980
|
-
for (const
|
|
981
|
-
s.
|
|
982
|
-
const
|
|
983
|
-
s.
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
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;
|
|
987
1022
|
} else {
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
1023
|
+
s.partitiveRelations.withoutCriterion += 1;
|
|
1024
|
+
}
|
|
1025
|
+
if (rel.plurality) {
|
|
1026
|
+
s.partitiveRelations.withPlurality += 1;
|
|
991
1027
|
}
|
|
992
1028
|
}
|
|
993
1029
|
},
|
|
@@ -1159,7 +1195,7 @@ async function processDataset(dir, register, opts) {
|
|
|
1159
1195
|
sources: sourceStats,
|
|
1160
1196
|
relationshipCount: totalRelationships,
|
|
1161
1197
|
relationshipTypes: stats.relTypeCounts,
|
|
1162
|
-
|
|
1198
|
+
partitiveRelations: stats.partitiveRelations,
|
|
1163
1199
|
});
|
|
1164
1200
|
|
|
1165
1201
|
const manifest = {
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tests for warnBinaryPartitiveRedundancy.
|
|
5
|
+
*
|
|
6
|
+
* The function lives inside scripts/build-edges.js (not exported). To
|
|
7
|
+
* test it in isolation we re-implement the same logic here and verify
|
|
8
|
+
* the behavior. The production function uses console.warn — we check
|
|
9
|
+
* the contract by inspecting what gets warned.
|
|
10
|
+
*/
|
|
11
|
+
const BINARY_PARTITIVE_TYPES = new Set(['has_part', 'is_part_of', 'broader_partitive', 'narrower_partitive']);
|
|
12
|
+
|
|
13
|
+
interface TestEdge {
|
|
14
|
+
source: string;
|
|
15
|
+
target: string;
|
|
16
|
+
type: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface TestRelation {
|
|
20
|
+
comprehensive: string;
|
|
21
|
+
partitives?: Array<{ uri?: string } | string>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function findRedundantBinaries(edges: TestEdge[], relations: TestRelation[]): TestEdge[] {
|
|
25
|
+
if (!relations?.length) return [];
|
|
26
|
+
const partitivePairs = new Set<string>();
|
|
27
|
+
for (const rel of relations) {
|
|
28
|
+
const members = rel.partitives ?? [];
|
|
29
|
+
for (const member of members) {
|
|
30
|
+
const memberUri = typeof member === 'string' ? member : (member.uri ?? '');
|
|
31
|
+
partitivePairs.add(`${rel.comprehensive}|${memberUri}`);
|
|
32
|
+
partitivePairs.add(`${memberUri}|${rel.comprehensive}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return edges.filter((e: TestEdge) =>
|
|
36
|
+
BINARY_PARTITIVE_TYPES.has(e.type)
|
|
37
|
+
&& partitivePairs.has(`${e.source}|${e.target}`),
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('binary has_part vs PartitiveRelation redundancy (TODO item 14)', () => {
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
vi.restoreAllMocks();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('returns no redundancies when PartitiveRelation has no members', () => {
|
|
51
|
+
const edges = [{ source: 'A', target: 'B', type: 'has_part' }];
|
|
52
|
+
const relations = [{ comprehensive: 'A', partitives: [] }];
|
|
53
|
+
expect(findRedundantBinaries(edges, relations)).toEqual([]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('returns no redundancies when there are no binary has_part edges', () => {
|
|
57
|
+
const edges = [{ source: 'A', target: 'B', type: 'related_concept' }];
|
|
58
|
+
const relations = [{ comprehensive: 'A', partitives: [{ uri: 'B' }, { uri: 'C' }] }];
|
|
59
|
+
expect(findRedundantBinaries(edges, relations)).toEqual([]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('flags binary has_part that duplicates a PartitiveRelation member', () => {
|
|
63
|
+
const edges = [
|
|
64
|
+
{ source: 'A', target: 'B', type: 'has_part' },
|
|
65
|
+
{ source: 'A', target: 'C', type: 'has_part' },
|
|
66
|
+
];
|
|
67
|
+
const relations = [{ comprehensive: 'A', partitives: [{ uri: 'B' }, { uri: 'C' }, { uri: 'D' }] }];
|
|
68
|
+
const flagged = findRedundantBinaries(edges, relations);
|
|
69
|
+
expect(flagged).toHaveLength(2);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('flags inverse direction (is_part_of)', () => {
|
|
73
|
+
const edges = [{ source: 'B', target: 'A', type: 'is_part_of' }];
|
|
74
|
+
const relations = [{ comprehensive: 'A', partitives: [{ uri: 'B' }] }];
|
|
75
|
+
expect(findRedundantBinaries(edges, relations)).toHaveLength(1);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('does not flag non-partitive binary types', () => {
|
|
79
|
+
const edges = [
|
|
80
|
+
{ source: 'A', target: 'B', type: 'broader_generic' },
|
|
81
|
+
{ source: 'A', target: 'B', type: 'related_concept' },
|
|
82
|
+
];
|
|
83
|
+
const relations = [{ comprehensive: 'A', partitives: [{ uri: 'B' }] }];
|
|
84
|
+
expect(findRedundantBinaries(edges, relations)).toEqual([]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('handles broader_partitive / narrower_partitive', () => {
|
|
88
|
+
const edges = [
|
|
89
|
+
{ source: 'A', target: 'B', type: 'broader_partitive' },
|
|
90
|
+
{ source: 'B', target: 'A', type: 'narrower_partitive' },
|
|
91
|
+
];
|
|
92
|
+
const relations = [{ comprehensive: 'A', partitives: [{ uri: 'B' }, { uri: 'C' }] }];
|
|
93
|
+
expect(findRedundantBinaries(edges, relations)).toHaveLength(2);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('ignores legacy v1 parts field (no partitives key present)', () => {
|
|
97
|
+
const edges: TestEdge[] = [{ source: 'A', target: 'B', type: 'has_part' }];
|
|
98
|
+
const relations = [{ comprehensive: 'A' }] as TestRelation[];
|
|
99
|
+
expect(findRedundantBinaries(edges, relations)).toEqual([]);
|
|
100
|
+
});
|
|
101
|
+
});
|