glossarist 0.4.31 → 0.4.32
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 +1 -1
- package/src/concept-parser.js +68 -22
- package/src/concept-serializer.js +21 -10
- package/src/diff/concept-diff.js +70 -18
- package/src/diff/diff-patch.js +44 -12
- package/src/diff/diff-renderer.js +11 -4
- package/src/models/concept.js +95 -26
- package/src/models/relation-loader.js +7 -9
- package/src/models/relation-type-registry.js +25 -0
package/package.json
CHANGED
package/src/concept-parser.js
CHANGED
|
@@ -5,13 +5,16 @@ import { migrateHyperedgeToRelation } from './migration/partitive-relation-migra
|
|
|
5
5
|
import { InvalidInputError, YamlParseError } from './errors.js';
|
|
6
6
|
|
|
7
7
|
// Structural keys are reserved at the concept level and excluded
|
|
8
|
-
// from language localization discovery.
|
|
9
|
-
//
|
|
10
|
-
//
|
|
8
|
+
// from language localization discovery. The unified `relations`
|
|
9
|
+
// key, the v2 typed keys (`partitive_relations`, `generic_relations`),
|
|
10
|
+
// and the v1 backward-compat key (`partitive_hyperedges`) are all
|
|
11
|
+
// reserved.
|
|
11
12
|
const STRUCTURAL_KEYS = new Set([
|
|
12
13
|
'termid', 'term', 'figures', 'tables', 'formulas',
|
|
14
|
+
'relations',
|
|
13
15
|
'partitive_hyperedges', 'partitiveHyperedges',
|
|
14
16
|
'partitive_relations', 'partitiveRelations',
|
|
17
|
+
'generic_relations', 'genericRelations',
|
|
15
18
|
]);
|
|
16
19
|
|
|
17
20
|
export class ConceptParser {
|
|
@@ -67,7 +70,7 @@ export class ConceptParser {
|
|
|
67
70
|
figures: doc.figures,
|
|
68
71
|
tables: doc.tables,
|
|
69
72
|
formulas: doc.formulas,
|
|
70
|
-
|
|
73
|
+
relations: _resolveNaryData(doc),
|
|
71
74
|
raw: doc,
|
|
72
75
|
});
|
|
73
76
|
}
|
|
@@ -84,14 +87,20 @@ export class ConceptParser {
|
|
|
84
87
|
localizations[lang] = lcData;
|
|
85
88
|
}
|
|
86
89
|
|
|
87
|
-
assertConceptLevelOnly(mc, [
|
|
90
|
+
assertConceptLevelOnly(mc, [
|
|
91
|
+
'related',
|
|
92
|
+
'relations',
|
|
93
|
+
'partitive_relations',
|
|
94
|
+
'partitive_hyperedges',
|
|
95
|
+
'generic_relations',
|
|
96
|
+
]);
|
|
88
97
|
|
|
89
98
|
return new Concept({
|
|
90
99
|
id: String(mc.data.identifier),
|
|
91
100
|
term: null,
|
|
92
101
|
localizations,
|
|
93
102
|
related: _normalizeRelated(mc.related),
|
|
94
|
-
|
|
103
|
+
relations: _resolveNaryData(mc),
|
|
95
104
|
domains: mc.data.domains,
|
|
96
105
|
groups: mc.data.groups,
|
|
97
106
|
dates: mc.dates ?? mc.data?.dates,
|
|
@@ -121,26 +130,63 @@ function assertConceptLevelOnly(mc, keys) {
|
|
|
121
130
|
}
|
|
122
131
|
}
|
|
123
132
|
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
// v2 camel: { partitiveRelations: [...] }
|
|
127
|
-
// v1 wire: { partitive_hyperedges: [...] } ← migrated to v2
|
|
128
|
-
// v1 camel: { partitiveHyperedges: [...] } ← migrated to v2
|
|
133
|
+
// Resolve n-ary relation data from any input shape into a single
|
|
134
|
+
// unified array. Accepts:
|
|
129
135
|
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
136
|
+
// unified wire: { relations: [...] } (hashes with `type`)
|
|
137
|
+
// v2 partitive: { partitive_relations: [...] } (hashes)
|
|
138
|
+
// v2 generic: { generic_relations: [...] } (hashes)
|
|
139
|
+
// v1 partitive: { partitive_hyperedges: [...] } ← migrated
|
|
140
|
+
//
|
|
141
|
+
// Returns null when no relation data is present, so Concept's
|
|
142
|
+
// `_resolveNaryRelations` falls back to the empty-array default.
|
|
143
|
+
function _resolveNaryData(container) {
|
|
144
|
+
if (!container) return null;
|
|
145
|
+
|
|
146
|
+
if (Array.isArray(container.relations)) {
|
|
147
|
+
return container.relations;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const out = [];
|
|
151
|
+
|
|
152
|
+
const v2Partitive = container.partitive_relations ?? container.partitiveRelations;
|
|
153
|
+
if (Array.isArray(v2Partitive)) {
|
|
154
|
+
for (const r of v2Partitive) {
|
|
155
|
+
const tagged = _addTypeIfMissing(r, 'partitive_relation');
|
|
156
|
+
if (tagged != null) out.push(tagged);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const v2Generic = container.generic_relations ?? container.genericRelations;
|
|
161
|
+
if (Array.isArray(v2Generic)) {
|
|
162
|
+
for (const r of v2Generic) {
|
|
163
|
+
const tagged = _addTypeIfMissing(r, 'generic_relation');
|
|
164
|
+
if (tagged != null) out.push(tagged);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
135
167
|
|
|
136
|
-
const v1 = container
|
|
168
|
+
const v1 = container.partitive_hyperedges ?? container.partitiveHyperedges;
|
|
137
169
|
if (Array.isArray(v1)) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
.
|
|
170
|
+
for (const h of v1) {
|
|
171
|
+
const hash = h?.toJSON && typeof h.toJSON === 'function' ? h.toJSON() : h;
|
|
172
|
+
const migrated = migrateHyperedgeToRelation(hash);
|
|
173
|
+
if (migrated) out.push({ ...migrated, type: 'partitive_relation' });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return out.length > 0 ? out : null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function _addTypeIfMissing(value, type) {
|
|
181
|
+
if (value == null) return null;
|
|
182
|
+
if (typeof value.toJSON === 'function') {
|
|
183
|
+
const hash = value.toJSON();
|
|
184
|
+
return hash.type ? hash : { ...hash, type };
|
|
185
|
+
}
|
|
186
|
+
if (typeof value === 'object') {
|
|
187
|
+
return value.type ? value : { ...value, type };
|
|
142
188
|
}
|
|
143
|
-
return
|
|
189
|
+
return value;
|
|
144
190
|
}
|
|
145
191
|
|
|
146
192
|
function _normalizeRelated(arr) {
|
|
@@ -1,18 +1,30 @@
|
|
|
1
1
|
import * as yaml from 'js-yaml';
|
|
2
|
+
import { PartitiveRelation } from './models/partitive-relation.js';
|
|
3
|
+
import { GenericRelation } from './models/generic-relation.js';
|
|
2
4
|
|
|
3
5
|
const DUMP_OPTS = { lineWidth: -1, noRefs: true, sortKeys: false, skipInvalid: true };
|
|
4
6
|
|
|
7
|
+
// Partition the unified relations array by concrete type and emit
|
|
8
|
+
// each type under its own wire key. Concept.relations holds mixed
|
|
9
|
+
// types; the wire format stays split for backward compat with
|
|
10
|
+
// existing consumers and the JSON Schema in concept-model.
|
|
11
|
+
function _emitTypedRelations(concept) {
|
|
12
|
+
const partitiveRels = concept.relations.filter(r => r instanceof PartitiveRelation);
|
|
13
|
+
const genericRels = concept.relations.filter(r => r instanceof GenericRelation);
|
|
14
|
+
return {
|
|
15
|
+
partitive: partitiveRels.map(r => r.toJSON()),
|
|
16
|
+
generic: genericRels.map(r => r.toJSON()),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
5
20
|
export class ConceptSerializer {
|
|
6
21
|
toCanonicalYaml(concept) {
|
|
7
22
|
const doc = { termid: concept.id };
|
|
8
23
|
if (concept.term) doc.term = concept.term;
|
|
9
24
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if (concept.partitiveRelations && concept.partitiveRelations.length > 0) {
|
|
14
|
-
doc.partitive_relations = concept.partitiveRelations.map(r => r.toJSON());
|
|
15
|
-
}
|
|
25
|
+
const { partitive, generic } = _emitTypedRelations(concept);
|
|
26
|
+
if (partitive.length > 0) doc.partitive_relations = partitive;
|
|
27
|
+
if (generic.length > 0) doc.generic_relations = generic;
|
|
16
28
|
|
|
17
29
|
for (const lang of concept.languages) {
|
|
18
30
|
const lc = concept.localization(lang);
|
|
@@ -53,10 +65,9 @@ export class ConceptSerializer {
|
|
|
53
65
|
if (concept.relatedConcepts.length > 0) {
|
|
54
66
|
mainDoc.related = concept.relatedConcepts.map(rc => rc.toJSON());
|
|
55
67
|
}
|
|
56
|
-
|
|
57
|
-
if (
|
|
58
|
-
|
|
59
|
-
}
|
|
68
|
+
const { partitive, generic } = _emitTypedRelations(concept);
|
|
69
|
+
if (partitive.length > 0) mainDoc.partitive_relations = partitive;
|
|
70
|
+
if (generic.length > 0) mainDoc.generic_relations = generic;
|
|
60
71
|
if (concept.sources.length > 0) {
|
|
61
72
|
mainDoc.sources = concept.sources.map(s => s.toJSON());
|
|
62
73
|
}
|
package/src/diff/concept-diff.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { GlossaristModel } from '../models/base.js';
|
|
2
|
+
import { PartitiveRelation } from '../models/partitive-relation.js';
|
|
3
|
+
import { GenericRelation } from '../models/generic-relation.js';
|
|
2
4
|
import { Added, Removed, Changed } from './change.js';
|
|
3
5
|
import { ListDiff, diffList, diffSet } from './list-diff.js';
|
|
4
6
|
import { identityOf } from './identity.js';
|
|
@@ -99,11 +101,17 @@ export class ConceptLevelDiff extends GlossaristModel {
|
|
|
99
101
|
this._sources = wrapListDiff(data.sources);
|
|
100
102
|
this._dates = wrapListDiff(data.dates);
|
|
101
103
|
this._relatedConcepts = wrapListDiff(data.relatedConcepts ?? data.related_concepts);
|
|
102
|
-
//
|
|
103
|
-
// (
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
// Unified n-ary relations diff. Accepts:
|
|
105
|
+
// - data.relations (new unified shape)
|
|
106
|
+
// - data.partitiveRelations /
|
|
107
|
+
// data.partitive_relations /
|
|
108
|
+
// data.partitiveHyperedges /
|
|
109
|
+
// data.partitive_hyperedges (legacy partitive-only shape)
|
|
110
|
+
// Old diffs only had partitive, so the legacy fallback treats them
|
|
111
|
+
// as a unified list with one type present.
|
|
112
|
+
this._relations = wrapListDiff(
|
|
113
|
+
data.relations
|
|
114
|
+
?? data.partitiveRelations
|
|
107
115
|
?? data.partitive_relations
|
|
108
116
|
?? data.partitiveHyperedges
|
|
109
117
|
?? data.partitive_hyperedges,
|
|
@@ -119,8 +127,25 @@ export class ConceptLevelDiff extends GlossaristModel {
|
|
|
119
127
|
get sources() { return this._sources; }
|
|
120
128
|
get dates() { return this._dates; }
|
|
121
129
|
get relatedConcepts() { return this._relatedConcepts; }
|
|
122
|
-
|
|
123
|
-
|
|
130
|
+
/** Unified n-ary relations diff (PartitiveRelation + GenericRelation). */
|
|
131
|
+
get relations() { return this._relations; }
|
|
132
|
+
/**
|
|
133
|
+
* Filtered view of `relations` containing only PartitiveRelation
|
|
134
|
+
* entries. Backward compat for callers that read .partitiveRelations
|
|
135
|
+
* directly on a ConceptLevelDiff.
|
|
136
|
+
*/
|
|
137
|
+
get partitiveRelations() {
|
|
138
|
+
return filterListDiffByType(this._relations, PartitiveRelation);
|
|
139
|
+
}
|
|
140
|
+
/** @deprecated use .relations */ get partitiveHyperedges() { return this.partitiveRelations; }
|
|
141
|
+
/**
|
|
142
|
+
* Filtered view of `relations` containing only GenericRelation
|
|
143
|
+
* entries. Backward compat for callers that read .genericRelations
|
|
144
|
+
* directly on a ConceptLevelDiff.
|
|
145
|
+
*/
|
|
146
|
+
get genericRelations() {
|
|
147
|
+
return filterListDiffByType(this._relations, GenericRelation);
|
|
148
|
+
}
|
|
124
149
|
get groups() { return this._groups; }
|
|
125
150
|
get sections() { return this._sections; }
|
|
126
151
|
get tags() { return this._tags; }
|
|
@@ -130,7 +155,7 @@ export class ConceptLevelDiff extends GlossaristModel {
|
|
|
130
155
|
return this._sources.hasChanges
|
|
131
156
|
|| this._dates.hasChanges
|
|
132
157
|
|| this._relatedConcepts.hasChanges
|
|
133
|
-
|| this.
|
|
158
|
+
|| this._relations.hasChanges
|
|
134
159
|
|| this._groups.hasChanges
|
|
135
160
|
|| this._sections.hasChanges
|
|
136
161
|
|| this._tags.hasChanges
|
|
@@ -141,7 +166,7 @@ export class ConceptLevelDiff extends GlossaristModel {
|
|
|
141
166
|
yield* walkList('concept.sources', this._sources);
|
|
142
167
|
yield* walkList('concept.dates', this._dates);
|
|
143
168
|
yield* walkList('concept.relatedConcepts', this._relatedConcepts);
|
|
144
|
-
yield* walkList('concept.
|
|
169
|
+
yield* walkList('concept.relations', this._relations);
|
|
145
170
|
yield* walkList('concept.groups', this._groups);
|
|
146
171
|
yield* walkList('concept.sections', this._sections);
|
|
147
172
|
yield* walkList('concept.tags', this._tags);
|
|
@@ -153,7 +178,7 @@ export class ConceptLevelDiff extends GlossaristModel {
|
|
|
153
178
|
sources: this._sources.toJSON(),
|
|
154
179
|
dates: this._dates.toJSON(),
|
|
155
180
|
related_concepts: this._relatedConcepts.toJSON(),
|
|
156
|
-
|
|
181
|
+
relations: this._relations.toJSON(),
|
|
157
182
|
groups: this._groups.toJSON(),
|
|
158
183
|
sections: this._sections.toJSON(),
|
|
159
184
|
tags: this._tags.toJSON(),
|
|
@@ -428,7 +453,7 @@ function diffConceptLevel(oldConcept, newConcept) {
|
|
|
428
453
|
sources: fullListDiff(c.sources ?? [], Direction),
|
|
429
454
|
dates: fullListDiff(c.dates ?? [], Direction),
|
|
430
455
|
relatedConcepts: fullListDiff(c.relatedConcepts ?? [], Direction),
|
|
431
|
-
|
|
456
|
+
relations: fullListDiff(c.relations ?? [], Direction),
|
|
432
457
|
groups: fullListDiff(c.groups ?? [], Direction),
|
|
433
458
|
sections: fullListDiff(c.sections ?? [], Direction),
|
|
434
459
|
tags: fullListDiff(c.tags ?? [], Direction),
|
|
@@ -440,9 +465,9 @@ function diffConceptLevel(oldConcept, newConcept) {
|
|
|
440
465
|
sources: diffSources(oldConcept.sources ?? [], newConcept.sources ?? []),
|
|
441
466
|
dates: diffDates(oldConcept.dates ?? [], newConcept.dates ?? []),
|
|
442
467
|
relatedConcepts: diffRelatedConcepts(oldConcept.relatedConcepts ?? [], newConcept.relatedConcepts ?? []),
|
|
443
|
-
|
|
444
|
-
oldConcept.
|
|
445
|
-
newConcept.
|
|
468
|
+
relations: diffNaryRelations(
|
|
469
|
+
oldConcept.relations ?? [],
|
|
470
|
+
newConcept.relations ?? [],
|
|
446
471
|
),
|
|
447
472
|
groups: diffStringSet(oldConcept.groups ?? [], newConcept.groups ?? []),
|
|
448
473
|
sections: diffStringSet(oldConcept.sections ?? [], newConcept.sections ?? []),
|
|
@@ -540,13 +565,40 @@ function diffRelatedConcepts(oldRC, newRC) {
|
|
|
540
565
|
});
|
|
541
566
|
}
|
|
542
567
|
|
|
543
|
-
|
|
568
|
+
// Diff two unified n-ary relations arrays. Identity is polymorphic —
|
|
569
|
+
// dispatches via value.constructor.identityOf so PartitiveRelation
|
|
570
|
+
// and GenericRelation (and any future n-ary subclass) coexist in
|
|
571
|
+
// the same list. Cross-type entries with matching identity are still
|
|
572
|
+
// treated as different (because their constructor differs and they
|
|
573
|
+
// live under different wire keys).
|
|
574
|
+
function diffNaryRelations(oldR, newR) {
|
|
544
575
|
return diffSet(oldR, newR, {
|
|
545
|
-
identityKey:
|
|
576
|
+
identityKey: relationIdentityOf,
|
|
546
577
|
textKey: relationText,
|
|
547
578
|
});
|
|
548
579
|
}
|
|
549
580
|
|
|
581
|
+
function relationIdentityOf(value) {
|
|
582
|
+
if (value == null) return '';
|
|
583
|
+
const Cls = value.constructor;
|
|
584
|
+
if (typeof Cls?.identityOf === 'function') {
|
|
585
|
+
const typed = Cls.identityOf(value);
|
|
586
|
+
return `${Cls.name}::${typed}`;
|
|
587
|
+
}
|
|
588
|
+
return identityOf(value);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function filterListDiffByType(listDiff, Cls) {
|
|
592
|
+
if (!listDiff) return listDiff;
|
|
593
|
+
return new ListDiff({
|
|
594
|
+
added: listDiff.added.filter(e => e.value instanceof Cls),
|
|
595
|
+
removed: listDiff.removed.filter(e => e.value instanceof Cls),
|
|
596
|
+
changed: listDiff.changed.filter(e =>
|
|
597
|
+
(e.oldValue == null || e.oldValue instanceof Cls) &&
|
|
598
|
+
(e.newValue == null || e.newValue instanceof Cls)),
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
|
|
550
602
|
function relationText(r) {
|
|
551
603
|
if (!r) return '';
|
|
552
604
|
return canonicalJson(typeof r.toJSON === 'function' ? r.toJSON() : r);
|
|
@@ -598,8 +650,8 @@ function countConceptItems(oldConcept, newConcept, langs) {
|
|
|
598
650
|
count += maxOf(oldConcept?.dates?.length, newConcept?.dates?.length);
|
|
599
651
|
count += maxOf(oldConcept?.relatedConcepts?.length, newConcept?.relatedConcepts?.length);
|
|
600
652
|
count += maxOf(
|
|
601
|
-
oldConcept?.
|
|
602
|
-
newConcept?.
|
|
653
|
+
oldConcept?.relations?.length,
|
|
654
|
+
newConcept?.relations?.length,
|
|
603
655
|
);
|
|
604
656
|
count += maxOf(oldConcept?.groups?.length, newConcept?.groups?.length);
|
|
605
657
|
count += maxOf(oldConcept?.sections?.length, newConcept?.sections?.length);
|
package/src/diff/diff-patch.js
CHANGED
|
@@ -5,6 +5,7 @@ import { RelatedConcept } from '../models/related-concept.js';
|
|
|
5
5
|
import { Designation } from '../models/designation.js';
|
|
6
6
|
import { DetailedDefinition } from '../models/detailed-definition.js';
|
|
7
7
|
import { PartitiveRelation } from '../models/partitive-relation.js';
|
|
8
|
+
import { GenericRelation } from '../models/generic-relation.js';
|
|
8
9
|
import { Added, Removed, Changed } from './change.js';
|
|
9
10
|
import { ListDiff } from './list-diff.js';
|
|
10
11
|
import { TextDiff, TextHunk } from './text-diff.js';
|
|
@@ -67,9 +68,7 @@ export function reverseDiff(diff) {
|
|
|
67
68
|
sources: reverseListDiff(diff.concept.sources),
|
|
68
69
|
dates: reverseListDiff(diff.concept.dates),
|
|
69
70
|
relatedConcepts: reverseListDiff(diff.concept.relatedConcepts),
|
|
70
|
-
|
|
71
|
-
diff.concept.partitiveRelations ?? diff.concept.partitiveHyperedges,
|
|
72
|
-
),
|
|
71
|
+
relations: reverseListDiff(diff.concept.relations),
|
|
73
72
|
groups: reverseListDiff(diff.concept.groups),
|
|
74
73
|
sections: reverseListDiff(diff.concept.sections),
|
|
75
74
|
tags: reverseListDiff(diff.concept.tags),
|
|
@@ -108,15 +107,32 @@ function applyConceptLevelPatch(json, conceptDiff) {
|
|
|
108
107
|
json.sources = applyListPatch(json.sources ?? [], conceptDiff.sources, ConceptSource.identityOf);
|
|
109
108
|
json.dates = applyListPatch(json.dates ?? [], conceptDiff.dates, ConceptDate.identityOf);
|
|
110
109
|
json.related = applyListPatch(json.related ?? [], conceptDiff.relatedConcepts, RelatedConcept.identityOf);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
110
|
+
|
|
111
|
+
// Unified relations diff → typed wire keys. Partition the unified
|
|
112
|
+
// diff by concrete class and apply each half to its own wire key.
|
|
113
|
+
// The wire format stays split (partitive_relations + generic_relations)
|
|
114
|
+
// for backward compat with consumers; the in-memory model is unified.
|
|
115
|
+
const partitiveDiff = partitionListDiff(conceptDiff.relations, PartitiveRelation);
|
|
116
|
+
const genericDiff = partitionListDiff(conceptDiff.relations, GenericRelation);
|
|
117
|
+
|
|
118
|
+
if (partitiveDiff.hasChanges) {
|
|
119
|
+
json.partitive_relations = applyListPatch(
|
|
120
|
+
json.partitive_relations ?? json.partitive_hyperedges ?? [],
|
|
121
|
+
partitiveDiff,
|
|
122
|
+
PartitiveRelation.identityOf,
|
|
123
|
+
);
|
|
124
|
+
// v1 wire key is removed once v2 has replaced it; otherwise leave
|
|
125
|
+
// v1 data in place for backward-compat readers.
|
|
126
|
+
if (json.partitive_relations != null && json.partitive_hyperedges != null) {
|
|
127
|
+
delete json.partitive_hyperedges;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (genericDiff.hasChanges) {
|
|
131
|
+
json.generic_relations = applyListPatch(
|
|
132
|
+
json.generic_relations ?? [],
|
|
133
|
+
genericDiff,
|
|
134
|
+
GenericRelation.identityOf,
|
|
135
|
+
);
|
|
120
136
|
}
|
|
121
137
|
|
|
122
138
|
json.tags = applyListPatch(json.tags ?? [], conceptDiff.tags);
|
|
@@ -233,3 +249,19 @@ function toJsonValue(value) {
|
|
|
233
249
|
if (typeof value.toJSON === 'function') return value.toJSON();
|
|
234
250
|
return value;
|
|
235
251
|
}
|
|
252
|
+
|
|
253
|
+
// Project a unified relations ListDiff onto a single concrete class.
|
|
254
|
+
// Entries whose value (old or new) is an instance of `Cls` survive;
|
|
255
|
+
// everything else is filtered out. Cross-type changed entries (e.g.
|
|
256
|
+
// old was PartitiveRelation, new is GenericRelation) are not yet
|
|
257
|
+
// supported — those would need to surface as a remove + add pair.
|
|
258
|
+
function partitionListDiff(listDiff, Cls) {
|
|
259
|
+
if (!listDiff) return new ListDiff({ added: [], removed: [], changed: [] });
|
|
260
|
+
return new ListDiff({
|
|
261
|
+
added: listDiff.added.filter(e => e.value instanceof Cls),
|
|
262
|
+
removed: listDiff.removed.filter(e => e.value instanceof Cls),
|
|
263
|
+
changed: listDiff.changed.filter(e =>
|
|
264
|
+
(e.oldValue == null || e.oldValue instanceof Cls) &&
|
|
265
|
+
(e.newValue == null || e.newValue instanceof Cls)),
|
|
266
|
+
});
|
|
267
|
+
}
|
|
@@ -125,7 +125,10 @@ function renderConceptLevel(diff, colors) {
|
|
|
125
125
|
lines.push(...renderListDiff('Sources', diff.sources, colors, itemLabel));
|
|
126
126
|
lines.push(...renderListDiff('Dates', diff.dates, colors, itemLabel));
|
|
127
127
|
lines.push(...renderListDiff('Related', diff.relatedConcepts, colors, itemLabel));
|
|
128
|
-
|
|
128
|
+
// Unified n-ary relations section. Replaces the per-type sections
|
|
129
|
+
// (Partitive relations, Generic relations) — the underlying diff
|
|
130
|
+
// is a single ListDiff over the unified .relations array.
|
|
131
|
+
lines.push(...renderListDiff('N-ary relations', diff.relations, colors, relationLabel));
|
|
129
132
|
lines.push(...renderListDiff('Groups', diff.groups, colors, itemLabel));
|
|
130
133
|
lines.push(...renderListDiff('Sections', diff.sections, colors, itemLabel));
|
|
131
134
|
lines.push(...renderListDiff('Tags', diff.tags, colors, itemLabel));
|
|
@@ -203,8 +206,11 @@ function relationLabel(r) {
|
|
|
203
206
|
if (!r) return '?';
|
|
204
207
|
const c = r.comprehensive;
|
|
205
208
|
const head = c?.id ?? c?.source ?? c?.text ?? '?';
|
|
206
|
-
|
|
207
|
-
|
|
209
|
+
// PartitiveRelation exposes .partitives as the wire alias for
|
|
210
|
+
// .members; GenericRelation uses .members directly. Read both.
|
|
211
|
+
const members = Array.isArray(r.partitives) ? r.partitives : r.members;
|
|
212
|
+
const memberText = Array.isArray(members)
|
|
213
|
+
? members.map(m => {
|
|
208
214
|
const ref = m?.ref ?? m ?? {};
|
|
209
215
|
let tail = '';
|
|
210
216
|
const mult = resolveMultiplicity(m);
|
|
@@ -217,7 +223,8 @@ function relationLabel(r) {
|
|
|
217
223
|
const criterion = r.criterion
|
|
218
224
|
? ` / ${Object.values(r.criterion)[0] ?? ''}`
|
|
219
225
|
: '';
|
|
220
|
-
|
|
226
|
+
const typeTag = r?.constructor?.name === 'GenericRelation' ? 'GEN ' : '';
|
|
227
|
+
return `${typeTag}${head} → {${memberText}}${completeness}${criterion}`;
|
|
221
228
|
}
|
|
222
229
|
|
|
223
230
|
// Aggregate multiplicity + delimiting stats across a concept's
|
package/src/models/concept.js
CHANGED
|
@@ -3,6 +3,9 @@ import { LocalizedConcept } from './localized-concept.js';
|
|
|
3
3
|
import { RelatedConcept } from './related-concept.js';
|
|
4
4
|
import { PartitiveHyperedge } from './partitive-hyperedge.js';
|
|
5
5
|
import { PartitiveRelation } from './partitive-relation.js';
|
|
6
|
+
import { GenericRelation } from './generic-relation.js';
|
|
7
|
+
import { AbstractNaryRelation } from './abstract-nary-relation.js';
|
|
8
|
+
import { TYPE_TO_CLASS } from './relation-type-registry.js';
|
|
6
9
|
import { migrateHyperedgeToRelation } from '../migration/partitive-relation-migrator.js';
|
|
7
10
|
import { ConceptReference } from './concept-reference.js';
|
|
8
11
|
import { ConceptDate } from './concept-date.js';
|
|
@@ -34,12 +37,18 @@ export class Concept extends GlossaristModel {
|
|
|
34
37
|
this._cache = {};
|
|
35
38
|
|
|
36
39
|
this.relatedConcepts = _mapInstances(data.relatedConcepts ?? data.related ?? data.related_concepts ?? [], RelatedConcept);
|
|
37
|
-
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
|
|
40
|
+
|
|
41
|
+
// Unified n-ary relations array. Both PartitiveRelation and
|
|
42
|
+
// GenericRelation live here. Adding a new n-ary type (Sequential,
|
|
43
|
+
// Associative) = adding to _resolveNaryRelations, not editing
|
|
44
|
+
// Concept, parser, serializer, diff, patch, or renderer.
|
|
45
|
+
//
|
|
46
|
+
// Backward-compat getters (.partitiveRelations, .partitiveHyperedges,
|
|
47
|
+
// .genericRelations) filter this single array by type. Callers that
|
|
48
|
+
// use the typed getters see the same behavior; new callers should
|
|
49
|
+
// iterate .relations directly and dispatch via instanceof.
|
|
50
|
+
this.relations = _resolveNaryRelations(data);
|
|
51
|
+
|
|
43
52
|
this.domains = _normalizeDomains(data.domains, data.groups);
|
|
44
53
|
this.groups = Array.isArray(data.groups)
|
|
45
54
|
? data.groups.map(g => typeof g === 'string' ? g : (g?.id ?? g?.sectionId ?? null)).filter(Boolean)
|
|
@@ -63,6 +72,17 @@ export class Concept extends GlossaristModel {
|
|
|
63
72
|
|
|
64
73
|
get termid() { return this.id; }
|
|
65
74
|
|
|
75
|
+
// Typed getters that filter the unified relations array. Backward
|
|
76
|
+
// compat for callers that read .partitiveRelations directly.
|
|
77
|
+
get partitiveRelations() {
|
|
78
|
+
return this.relations.filter(r => r instanceof PartitiveRelation);
|
|
79
|
+
}
|
|
80
|
+
get genericRelations() {
|
|
81
|
+
return this.relations.filter(r => r instanceof GenericRelation);
|
|
82
|
+
}
|
|
83
|
+
// v1 alias
|
|
84
|
+
get partitiveHyperedges() { return this.partitiveRelations; }
|
|
85
|
+
|
|
66
86
|
get languages() {
|
|
67
87
|
return Object.keys(this._rawLocalizations);
|
|
68
88
|
}
|
|
@@ -202,8 +222,16 @@ export class Concept extends GlossaristModel {
|
|
|
202
222
|
if (this.relatedConcepts.length > 0) {
|
|
203
223
|
obj.related = this.relatedConcepts.map(rc => rc.toJSON());
|
|
204
224
|
}
|
|
205
|
-
|
|
206
|
-
|
|
225
|
+
// Emit typed wire keys from the unified relations array.
|
|
226
|
+
// Each relation type gets its own wire key so existing consumers
|
|
227
|
+
// see no wire-shape change.
|
|
228
|
+
const partitiveRels = this.relations.filter(r => r instanceof PartitiveRelation);
|
|
229
|
+
const genericRels = this.relations.filter(r => r instanceof GenericRelation);
|
|
230
|
+
if (partitiveRels.length > 0) {
|
|
231
|
+
obj.partitive_relations = partitiveRels.map(r => r.toJSON());
|
|
232
|
+
}
|
|
233
|
+
if (genericRels.length > 0) {
|
|
234
|
+
obj.generic_relations = genericRels.map(r => r.toJSON());
|
|
207
235
|
}
|
|
208
236
|
if (this.domains.length > 0) {
|
|
209
237
|
obj.domains = this.domains.map(d => d.toJSON());
|
|
@@ -234,31 +262,72 @@ function _mapInstances(arr, Cls) {
|
|
|
234
262
|
return arr.map(item => item instanceof Cls ? item : new Cls(item));
|
|
235
263
|
}
|
|
236
264
|
|
|
237
|
-
// Resolve
|
|
238
|
-
//
|
|
239
|
-
//
|
|
265
|
+
// Resolve the unified n-ary relations array from any input shape.
|
|
266
|
+
// Order of precedence:
|
|
267
|
+
// 1. data.relations (preferred unified shape — already typed instances
|
|
268
|
+
// or hashes carrying a `type` discriminator)
|
|
269
|
+
// 2. data.partitiveRelations / data.partitive_relations (v2 typed)
|
|
270
|
+
// + data.genericRelations / data.generic_relations (v2 typed)
|
|
240
271
|
// 3. data.partitiveHyperedges / data.partitive_hyperedges (v1, migrated)
|
|
241
272
|
//
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
273
|
+
// Hash dispatch uses TYPE_TO_CLASS from relation-type-registry.js —
|
|
274
|
+
// the single source of truth shared with the per-file loader.
|
|
275
|
+
//
|
|
276
|
+
// Adding a new n-ary relation type (Sequential, Associative, …) means
|
|
277
|
+
// one entry in TYPE_TO_CLASS plus one new class — Concept, parser,
|
|
278
|
+
// serializer, diff, patch, and renderer do not change.
|
|
279
|
+
function _resolveNaryRelations(data) {
|
|
280
|
+
if (Array.isArray(data.relations)) {
|
|
281
|
+
return data.relations
|
|
282
|
+
.map(r => _coerceRelation(r))
|
|
283
|
+
.filter(r => r != null);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const out = [];
|
|
287
|
+
|
|
288
|
+
const v2Partitive = data.partitiveRelations ?? data.partitive_relations;
|
|
289
|
+
if (Array.isArray(v2Partitive)) {
|
|
290
|
+
for (const r of v2Partitive) {
|
|
291
|
+
const coerced = _coerceRelation(r, PartitiveRelation);
|
|
292
|
+
if (coerced) out.push(coerced);
|
|
293
|
+
}
|
|
249
294
|
}
|
|
250
|
-
|
|
251
|
-
|
|
295
|
+
|
|
296
|
+
const v2Generic = data.genericRelations ?? data.generic_relations;
|
|
297
|
+
if (Array.isArray(v2Generic)) {
|
|
298
|
+
for (const r of v2Generic) {
|
|
299
|
+
const coerced = _coerceRelation(r, GenericRelation);
|
|
300
|
+
if (coerced) out.push(coerced);
|
|
301
|
+
}
|
|
252
302
|
}
|
|
303
|
+
|
|
253
304
|
const v1 = data.partitiveHyperedges ?? data.partitive_hyperedges;
|
|
254
305
|
if (Array.isArray(v1)) {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
.
|
|
259
|
-
|
|
306
|
+
for (const h of v1) {
|
|
307
|
+
const hash = h instanceof PartitiveHyperedge ? h.toJSON() : h;
|
|
308
|
+
const migrated = migrateHyperedgeToRelation(hash);
|
|
309
|
+
if (migrated) out.push(new PartitiveRelation(migrated));
|
|
310
|
+
}
|
|
260
311
|
}
|
|
261
|
-
|
|
312
|
+
|
|
313
|
+
return out;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function _coerceRelation(value, fallbackClass) {
|
|
317
|
+
if (value == null) return null;
|
|
318
|
+
if (value instanceof AbstractNaryRelation) return value;
|
|
319
|
+
|
|
320
|
+
const hash = typeof value.toJSON === 'function' ? value.toJSON() : value;
|
|
321
|
+
const Cls = _classForHash(hash) ?? fallbackClass;
|
|
322
|
+
if (!Cls) return null;
|
|
323
|
+
return new Cls(hash);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function _classForHash(hash) {
|
|
327
|
+
if (!hash || typeof hash !== 'object') return null;
|
|
328
|
+
const type = hash.type;
|
|
329
|
+
if (typeof type !== 'string') return null;
|
|
330
|
+
return TYPE_TO_CLASS[type] ?? null;
|
|
262
331
|
}
|
|
263
332
|
|
|
264
333
|
const CONCEPT_WIRE_NAMES = Object.freeze({
|
|
@@ -17,18 +17,16 @@
|
|
|
17
17
|
|
|
18
18
|
import { readdirSync, readFileSync, existsSync } from 'node:fs';
|
|
19
19
|
import * as yaml from 'js-yaml';
|
|
20
|
-
import {
|
|
21
|
-
import { GenericRelation } from './generic-relation.js';
|
|
20
|
+
import { TYPE_TO_CLASS } from './relation-type-registry.js';
|
|
22
21
|
|
|
23
22
|
/**
|
|
24
|
-
* Per-type class dispatch.
|
|
25
|
-
*
|
|
26
|
-
*
|
|
23
|
+
* Per-type class dispatch. Re-exported from the registry so the
|
|
24
|
+
* loader's old `TYPE_TO_CLASS` import path keeps working. The
|
|
25
|
+
* registry is the single source of truth — adding a relation type
|
|
26
|
+
* there makes it available to both the loader and Concept's
|
|
27
|
+
* unified relations array.
|
|
27
28
|
*/
|
|
28
|
-
export
|
|
29
|
-
partitive_relation: PartitiveRelation,
|
|
30
|
-
generic_relation: GenericRelation,
|
|
31
|
-
});
|
|
29
|
+
export { TYPE_TO_CLASS };
|
|
32
30
|
|
|
33
31
|
/**
|
|
34
32
|
* Thrown by `loadAll` when a relation file has an unknown `type`,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// N-ary relation type registry. Single source of truth for the
|
|
2
|
+
// `type` → class dispatch used by the parser, the per-file loader,
|
|
3
|
+
// and Concept's unified relations resolver.
|
|
4
|
+
//
|
|
5
|
+
// Adding a new n-ary relation type (Sequential, Associative, …)
|
|
6
|
+
// = one entry here plus one new class. Nothing else changes:
|
|
7
|
+
// not Concept, not parser, not serializer, not diff, not patch,
|
|
8
|
+
// not renderer.
|
|
9
|
+
//
|
|
10
|
+
// Lives in its own module (not relation-loader.js) because the loader
|
|
11
|
+
// pulls in node:fs, which is off-limits on the browser-gcr path that
|
|
12
|
+
// transitively imports this registry via Concept.
|
|
13
|
+
|
|
14
|
+
import { PartitiveRelation } from './partitive-relation.js';
|
|
15
|
+
import { GenericRelation } from './generic-relation.js';
|
|
16
|
+
|
|
17
|
+
export const TYPE_TO_CLASS = Object.freeze({
|
|
18
|
+
partitive_relation: PartitiveRelation,
|
|
19
|
+
generic_relation: GenericRelation,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export function classForRelationType(type) {
|
|
23
|
+
if (typeof type !== 'string') return null;
|
|
24
|
+
return TYPE_TO_CLASS[type] ?? null;
|
|
25
|
+
}
|