glossarist 0.4.32 → 0.4.34
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 +67 -45
- package/src/concept-reader.js +59 -3
- package/src/concept-serializer.js +4 -4
- package/src/diff/concept-diff.js +13 -13
- package/src/diff/diff-patch.js +19 -27
- package/src/diff/diff-renderer.js +19 -6
- package/src/index.js +2 -1
- package/src/migration/{partitive-relation-migrator.js → hyperedge-migrator.js} +3 -3
- package/src/models/{abstract-nary-relation.js → abstract-hyperedge.js} +5 -5
- package/src/models/completeness.js +1 -1
- package/src/models/concept.js +145 -72
- package/src/models/{generic-relation.js → generic-hyperedge.js} +18 -6
- package/src/models/generic-member.js +76 -9
- package/src/models/hyperedge-index.js +87 -0
- package/src/models/{concept-system-member.js → hyperedge-member.js} +26 -43
- package/src/models/hyperedge-registry.js +115 -0
- package/src/models/index.d.ts +16 -16
- package/src/models/index.js +16 -13
- package/src/models/multiplicity.js +11 -6
- package/src/models/partitive-hyperedge.js +69 -153
- package/src/models/partitive-member.js +40 -11
- package/src/models/relation-loader.js +28 -22
- package/src/rdf/gloss-concept.js +36 -20
- package/src/rdf/gloss-generic-hyperedge.js +92 -0
- package/src/rdf/gloss-partitive-relation.js +4 -4
- package/src/rdf/index.d.ts +4 -4
- package/src/rdf/index.js +0 -1
- package/src/reference-resolver.js +14 -12
- package/src/validators/binary-has-part-redundancy-rule.js +6 -6
- package/src/validators/delimiting-coherence-rule.js +2 -2
- package/src/validators/hyperedge-coherence-rule.js +98 -0
- package/src/validators/index.js +3 -9
- package/src/validators/partitive-relation-coherence-rule.js +6 -79
- package/src/validators/ref-keys.js +2 -2
- package/src/models/partitive-relation.js +0 -76
- package/src/models/relation-type-registry.js +0 -25
- package/src/rdf/gloss-hyperedge.js +0 -90
- package/src/validators/partitive-drift-rule.js +0 -51
- package/src/validators/partitive-hyperedge-rule.js +0 -75
package/package.json
CHANGED
package/src/concept-parser.js
CHANGED
|
@@ -1,22 +1,41 @@
|
|
|
1
1
|
import * as yaml from 'js-yaml';
|
|
2
2
|
import { Concept } from './models/concept.js';
|
|
3
3
|
import { RelatedConcept } from './models/related-concept.js';
|
|
4
|
-
import {
|
|
4
|
+
import { HyperedgeRegistry } from './models/hyperedge-registry.js';
|
|
5
|
+
import { migrateHyperedgeToRelation } from './migration/hyperedge-migrator.js';
|
|
5
6
|
import { InvalidInputError, YamlParseError } from './errors.js';
|
|
6
7
|
|
|
7
8
|
// Structural keys are reserved at the concept level and excluded
|
|
8
|
-
// from language localization discovery. The
|
|
9
|
-
//
|
|
10
|
-
// and the
|
|
11
|
-
//
|
|
9
|
+
// from language localization discovery. The set is derived from
|
|
10
|
+
// HyperedgeRegistry — every registered hyperedge class contributes
|
|
11
|
+
// its wireKey and any v1WireKeys to the reserved set. Adding a new
|
|
12
|
+
// hyperedge type means the parser reserves its wire key automatically;
|
|
13
|
+
// no edits here.
|
|
12
14
|
const STRUCTURAL_KEYS = new Set([
|
|
13
15
|
'termid', 'term', 'figures', 'tables', 'formulas',
|
|
16
|
+
'related', 'relatedConcepts',
|
|
14
17
|
'relations',
|
|
15
|
-
|
|
16
|
-
'partitive_relations', 'partitiveRelations',
|
|
17
|
-
'generic_relations', 'genericRelations',
|
|
18
|
+
..._registryStructuralKeys(),
|
|
18
19
|
]);
|
|
19
20
|
|
|
21
|
+
function _registryStructuralKeys() {
|
|
22
|
+
const out = [];
|
|
23
|
+
for (const cls of HyperedgeRegistry.allClasses()) {
|
|
24
|
+
out.push(cls.wireKey);
|
|
25
|
+
out.push(_camelCase(cls.wireKey));
|
|
26
|
+
for (const k of cls.v1WireKeys ?? []) {
|
|
27
|
+
out.push(k);
|
|
28
|
+
out.push(_camelCase(k));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function _camelCase(s) {
|
|
35
|
+
if (typeof s !== 'string' || !s.includes('_')) return s;
|
|
36
|
+
return s.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
37
|
+
}
|
|
38
|
+
|
|
20
39
|
export class ConceptParser {
|
|
21
40
|
parse(raw, context) {
|
|
22
41
|
const label = context ?? 'concept';
|
|
@@ -70,7 +89,7 @@ export class ConceptParser {
|
|
|
70
89
|
figures: doc.figures,
|
|
71
90
|
tables: doc.tables,
|
|
72
91
|
formulas: doc.formulas,
|
|
73
|
-
relations:
|
|
92
|
+
relations: _resolveHyperedgeData(doc),
|
|
74
93
|
raw: doc,
|
|
75
94
|
});
|
|
76
95
|
}
|
|
@@ -87,20 +106,23 @@ export class ConceptParser {
|
|
|
87
106
|
localizations[lang] = lcData;
|
|
88
107
|
}
|
|
89
108
|
|
|
90
|
-
|
|
109
|
+
// The hyperedge wire keys are reserved at concept level (top-level
|
|
110
|
+
// of the managed concept YAML), not under data:. Derive the list
|
|
111
|
+
// from the registry so adding a type doesn't require an edit here.
|
|
112
|
+
const conceptLevelOnlyKeys = [
|
|
91
113
|
'related',
|
|
92
114
|
'relations',
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
115
|
+
...HyperedgeRegistry.allWireAndLegacyKeys(),
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
assertConceptLevelOnly(mc, conceptLevelOnlyKeys);
|
|
97
119
|
|
|
98
120
|
return new Concept({
|
|
99
121
|
id: String(mc.data.identifier),
|
|
100
122
|
term: null,
|
|
101
123
|
localizations,
|
|
102
124
|
related: _normalizeRelated(mc.related),
|
|
103
|
-
relations:
|
|
125
|
+
relations: _resolveHyperedgeData(mc),
|
|
104
126
|
domains: mc.data.domains,
|
|
105
127
|
groups: mc.data.groups,
|
|
106
128
|
dates: mc.dates ?? mc.data?.dates,
|
|
@@ -118,7 +140,7 @@ export class ConceptParser {
|
|
|
118
140
|
function assertConceptLevelOnly(mc, keys) {
|
|
119
141
|
const conceptId = mc?.data?.identifier ?? '<unknown>';
|
|
120
142
|
for (const key of keys) {
|
|
121
|
-
const camelKey = key
|
|
143
|
+
const camelKey = _camelCase(key);
|
|
122
144
|
if (mc?.data?.[key] != null && mc[key] == null && mc[camelKey] == null) {
|
|
123
145
|
throw new InvalidInputError(
|
|
124
146
|
`'${key}' must live at concept level (top-level of the managed ` +
|
|
@@ -130,17 +152,18 @@ function assertConceptLevelOnly(mc, keys) {
|
|
|
130
152
|
}
|
|
131
153
|
}
|
|
132
154
|
|
|
133
|
-
// Resolve
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
// v2 generic: { generic_relations: [...] } (hashes)
|
|
139
|
-
// v1 partitive: { partitive_hyperedges: [...] } ← migrated
|
|
155
|
+
// Resolve hyperedge data from any input shape into a single unified
|
|
156
|
+
// array. Iterates HyperedgeRegistry — for each registered class:
|
|
157
|
+
// - read its wireKey from the container (e.g. partitive_relations)
|
|
158
|
+
// - read any v1WireKeys (e.g. partitive_hyperedges) and migrate
|
|
159
|
+
// - tag each entry with the class's typeTag
|
|
140
160
|
//
|
|
141
161
|
// Returns null when no relation data is present, so Concept's
|
|
142
|
-
// `
|
|
143
|
-
|
|
162
|
+
// `_resolveHyperedges` falls back to the empty-array default.
|
|
163
|
+
//
|
|
164
|
+
// Adding a new hyperedge type means: declare it on a class, register.
|
|
165
|
+
// The parser picks it up automatically — no edits here.
|
|
166
|
+
function _resolveHyperedgeData(container) {
|
|
144
167
|
if (!container) return null;
|
|
145
168
|
|
|
146
169
|
if (Array.isArray(container.relations)) {
|
|
@@ -149,28 +172,27 @@ function _resolveNaryData(container) {
|
|
|
149
172
|
|
|
150
173
|
const out = [];
|
|
151
174
|
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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);
|
|
175
|
+
for (const cls of HyperedgeRegistry.allClasses()) {
|
|
176
|
+
// v2 wire key (e.g. partitive_relations)
|
|
177
|
+
const arr = container[cls.wireKey] ?? container[_camelCase(cls.wireKey)];
|
|
178
|
+
if (Array.isArray(arr)) {
|
|
179
|
+
for (const r of arr) {
|
|
180
|
+
const tagged = _addTypeIfMissing(r, cls.typeTag);
|
|
181
|
+
if (tagged != null) out.push(tagged);
|
|
182
|
+
}
|
|
165
183
|
}
|
|
166
|
-
}
|
|
167
184
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const
|
|
173
|
-
if (
|
|
185
|
+
// v1 legacy wire keys (e.g. partitive_hyperedges) — migrated.
|
|
186
|
+
// The migration helper produces a v2 hash; we tag it with the
|
|
187
|
+
// typeTag so Concept's registry dispatch finds the right class.
|
|
188
|
+
for (const v1Key of cls.v1WireKeys ?? []) {
|
|
189
|
+
const v1Arr = container[v1Key] ?? container[_camelCase(v1Key)];
|
|
190
|
+
if (!Array.isArray(v1Arr)) continue;
|
|
191
|
+
for (const h of v1Arr) {
|
|
192
|
+
const hash = h?.toJSON && typeof h.toJSON === 'function' ? h.toJSON() : h;
|
|
193
|
+
const migrated = migrateHyperedgeToRelation(hash);
|
|
194
|
+
if (migrated) out.push({ ...migrated, type: cls.typeTag });
|
|
195
|
+
}
|
|
174
196
|
}
|
|
175
197
|
}
|
|
176
198
|
|
package/src/concept-reader.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import * as yaml from 'js-yaml';
|
|
4
4
|
import { conceptParser } from './concept-parser.js';
|
|
5
|
+
import { RelationLoader } from './models/relation-loader.js';
|
|
5
6
|
import { naturalSort } from './sort.js';
|
|
6
7
|
import { InvalidInputError } from './errors.js';
|
|
7
8
|
import { Register } from './models/register.js';
|
|
@@ -13,7 +14,15 @@ function assertDir(dir, fnName) {
|
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
|
-
* Read all v2 glossarist concept YAML files from a directory
|
|
17
|
+
* Read all v2 glossarist concept YAML files from a directory, then
|
|
18
|
+
* merge in any per-file hyperedge YAMLs from `<dir>/relations/`.
|
|
19
|
+
*
|
|
20
|
+
* Per-file hyperedge storage (concept-model PR #83): each file at
|
|
21
|
+
* `relations/<comprehensive-id>/<slug>.yaml` is loaded and merged
|
|
22
|
+
* into the corresponding Concept.relations array. If the directory
|
|
23
|
+
* does not exist, no per-file hyperedges are loaded (concepts still
|
|
24
|
+
* get any inline hyperedges from their own YAML).
|
|
25
|
+
*
|
|
17
26
|
* @param {string} dir - path to directory containing concept YAML files
|
|
18
27
|
* @returns {import('./models/concept.js').Concept[]}
|
|
19
28
|
* @throws {InvalidInputError} if dir is missing or empty
|
|
@@ -36,11 +45,16 @@ export function readConcepts(dir) {
|
|
|
36
45
|
concepts.push(concept);
|
|
37
46
|
}
|
|
38
47
|
}
|
|
48
|
+
|
|
49
|
+
_mergePerFileHyperedges(dir, concepts);
|
|
39
50
|
return concepts;
|
|
40
51
|
}
|
|
41
52
|
|
|
42
53
|
/**
|
|
43
|
-
* Read a single concept file by ID from a directory
|
|
54
|
+
* Read a single concept file by ID from a directory, then merge in
|
|
55
|
+
* any per-file hyperedge YAMLs for that concept from
|
|
56
|
+
* `<dir>/relations/<id>/`.
|
|
57
|
+
*
|
|
44
58
|
* @param {string} dir - path to directory containing concept YAML files
|
|
45
59
|
* @param {string} id - concept identifier (filename without .yaml)
|
|
46
60
|
* @returns {import('./models/concept.js').Concept | null}
|
|
@@ -58,7 +72,49 @@ export function readConcept(dir, id) {
|
|
|
58
72
|
const filePath = path.join(dir, `${id}.yaml`);
|
|
59
73
|
if (!fs.existsSync(filePath)) return null;
|
|
60
74
|
const raw = fs.readFileSync(filePath, 'utf8');
|
|
61
|
-
|
|
75
|
+
const concept = conceptParser.parse(raw, `${id}.yaml`);
|
|
76
|
+
_mergePerFileHyperedges(dir, [concept]);
|
|
77
|
+
return concept;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Load `relations/` directory and merge per-file hyperedges into the
|
|
81
|
+
// matching Concept.relations arrays. Concepts are matched by id.
|
|
82
|
+
// Inline hyperedges from the concept YAML are preserved alongside the
|
|
83
|
+
// per-file ones (audit: this is the contract — the two sources are
|
|
84
|
+
// additive, not exclusive).
|
|
85
|
+
//
|
|
86
|
+
// Throws if a per-file hyperedge fails to construct (bad YAML, missing
|
|
87
|
+
// type, invalid members) — surfaces corruption loudly per the
|
|
88
|
+
// RelationLoader contract.
|
|
89
|
+
function _mergePerFileHyperedges(dir, concepts) {
|
|
90
|
+
const relationsDir = path.join(dir, 'relations');
|
|
91
|
+
if (!fs.existsSync(relationsDir)) return;
|
|
92
|
+
|
|
93
|
+
const byComprehensive = RelationLoader.loadAll(relationsDir);
|
|
94
|
+
if (byComprehensive.size === 0) return;
|
|
95
|
+
|
|
96
|
+
for (const concept of concepts) {
|
|
97
|
+
const rels = byComprehensive.get(concept.id);
|
|
98
|
+
if (!rels || rels.length === 0) continue;
|
|
99
|
+
// Merge per-file hyperedges with any inline ones from the concept
|
|
100
|
+
// YAML. Per-file wins on identity collision — they're the newer,
|
|
101
|
+
// more authoritative source.
|
|
102
|
+
const inline = concept.relations;
|
|
103
|
+
const merged = [...inline];
|
|
104
|
+
for (const r of rels) {
|
|
105
|
+
const exists = inline.some(x => _sameHyperedge(x, r));
|
|
106
|
+
if (!exists) merged.push(r);
|
|
107
|
+
}
|
|
108
|
+
concept.relations = merged;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function _sameHyperedge(a, b) {
|
|
113
|
+
const aCls = a?.constructor;
|
|
114
|
+
const bCls = b?.constructor;
|
|
115
|
+
if (aCls !== bCls) return false;
|
|
116
|
+
if (typeof aCls.identityOf !== 'function') return false;
|
|
117
|
+
return aCls.identityOf(a) === bCls.identityOf(b);
|
|
62
118
|
}
|
|
63
119
|
|
|
64
120
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as yaml from 'js-yaml';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { PartitiveHyperedge } from './models/partitive-hyperedge.js';
|
|
3
|
+
import { GenericHyperedge } from './models/generic-hyperedge.js';
|
|
4
4
|
|
|
5
5
|
const DUMP_OPTS = { lineWidth: -1, noRefs: true, sortKeys: false, skipInvalid: true };
|
|
6
6
|
|
|
@@ -9,8 +9,8 @@ const DUMP_OPTS = { lineWidth: -1, noRefs: true, sortKeys: false, skipInvalid: t
|
|
|
9
9
|
// types; the wire format stays split for backward compat with
|
|
10
10
|
// existing consumers and the JSON Schema in concept-model.
|
|
11
11
|
function _emitTypedRelations(concept) {
|
|
12
|
-
const partitiveRels = concept.relations.filter(r => r instanceof
|
|
13
|
-
const genericRels = concept.relations.filter(r => r instanceof
|
|
12
|
+
const partitiveRels = concept.relations.filter(r => r instanceof PartitiveHyperedge);
|
|
13
|
+
const genericRels = concept.relations.filter(r => r instanceof GenericHyperedge);
|
|
14
14
|
return {
|
|
15
15
|
partitive: partitiveRels.map(r => r.toJSON()),
|
|
16
16
|
generic: genericRels.map(r => r.toJSON()),
|
package/src/diff/concept-diff.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { GlossaristModel } from '../models/base.js';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { PartitiveHyperedge } from '../models/partitive-hyperedge.js';
|
|
3
|
+
import { GenericHyperedge } from '../models/generic-hyperedge.js';
|
|
4
4
|
import { Added, Removed, Changed } from './change.js';
|
|
5
5
|
import { ListDiff, diffList, diffSet } from './list-diff.js';
|
|
6
6
|
import { identityOf } from './identity.js';
|
|
@@ -101,7 +101,7 @@ export class ConceptLevelDiff extends GlossaristModel {
|
|
|
101
101
|
this._sources = wrapListDiff(data.sources);
|
|
102
102
|
this._dates = wrapListDiff(data.dates);
|
|
103
103
|
this._relatedConcepts = wrapListDiff(data.relatedConcepts ?? data.related_concepts);
|
|
104
|
-
// Unified
|
|
104
|
+
// Unified hyperedge diff. Accepts:
|
|
105
105
|
// - data.relations (new unified shape)
|
|
106
106
|
// - data.partitiveRelations /
|
|
107
107
|
// data.partitive_relations /
|
|
@@ -127,24 +127,24 @@ export class ConceptLevelDiff extends GlossaristModel {
|
|
|
127
127
|
get sources() { return this._sources; }
|
|
128
128
|
get dates() { return this._dates; }
|
|
129
129
|
get relatedConcepts() { return this._relatedConcepts; }
|
|
130
|
-
/** Unified
|
|
130
|
+
/** Unified hyperedge diff (PartitiveHyperedge + GenericHyperedge). */
|
|
131
131
|
get relations() { return this._relations; }
|
|
132
132
|
/**
|
|
133
|
-
* Filtered view of `relations` containing only
|
|
133
|
+
* Filtered view of `relations` containing only PartitiveHyperedge
|
|
134
134
|
* entries. Backward compat for callers that read .partitiveRelations
|
|
135
135
|
* directly on a ConceptLevelDiff.
|
|
136
136
|
*/
|
|
137
137
|
get partitiveRelations() {
|
|
138
|
-
return filterListDiffByType(this._relations,
|
|
138
|
+
return filterListDiffByType(this._relations, PartitiveHyperedge);
|
|
139
139
|
}
|
|
140
140
|
/** @deprecated use .relations */ get partitiveHyperedges() { return this.partitiveRelations; }
|
|
141
141
|
/**
|
|
142
|
-
* Filtered view of `relations` containing only
|
|
142
|
+
* Filtered view of `relations` containing only GenericHyperedge
|
|
143
143
|
* entries. Backward compat for callers that read .genericRelations
|
|
144
144
|
* directly on a ConceptLevelDiff.
|
|
145
145
|
*/
|
|
146
146
|
get genericRelations() {
|
|
147
|
-
return filterListDiffByType(this._relations,
|
|
147
|
+
return filterListDiffByType(this._relations, GenericHyperedge);
|
|
148
148
|
}
|
|
149
149
|
get groups() { return this._groups; }
|
|
150
150
|
get sections() { return this._sections; }
|
|
@@ -465,7 +465,7 @@ function diffConceptLevel(oldConcept, newConcept) {
|
|
|
465
465
|
sources: diffSources(oldConcept.sources ?? [], newConcept.sources ?? []),
|
|
466
466
|
dates: diffDates(oldConcept.dates ?? [], newConcept.dates ?? []),
|
|
467
467
|
relatedConcepts: diffRelatedConcepts(oldConcept.relatedConcepts ?? [], newConcept.relatedConcepts ?? []),
|
|
468
|
-
relations:
|
|
468
|
+
relations: diffHyperedges(
|
|
469
469
|
oldConcept.relations ?? [],
|
|
470
470
|
newConcept.relations ?? [],
|
|
471
471
|
),
|
|
@@ -565,13 +565,13 @@ function diffRelatedConcepts(oldRC, newRC) {
|
|
|
565
565
|
});
|
|
566
566
|
}
|
|
567
567
|
|
|
568
|
-
// Diff two unified
|
|
569
|
-
// dispatches via value.constructor.identityOf so
|
|
570
|
-
// and
|
|
568
|
+
// Diff two unified hyperedge arrays. Identity is polymorphic —
|
|
569
|
+
// dispatches via value.constructor.identityOf so PartitiveHyperedge
|
|
570
|
+
// and GenericHyperedge (and any future hyperedge subclass) coexist in
|
|
571
571
|
// the same list. Cross-type entries with matching identity are still
|
|
572
572
|
// treated as different (because their constructor differs and they
|
|
573
573
|
// live under different wire keys).
|
|
574
|
-
function
|
|
574
|
+
function diffHyperedges(oldR, newR) {
|
|
575
575
|
return diffSet(oldR, newR, {
|
|
576
576
|
identityKey: relationIdentityOf,
|
|
577
577
|
textKey: relationText,
|
package/src/diff/diff-patch.js
CHANGED
|
@@ -4,8 +4,7 @@ import { ConceptDate } from '../models/concept-date.js';
|
|
|
4
4
|
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
|
-
import {
|
|
8
|
-
import { GenericRelation } from '../models/generic-relation.js';
|
|
7
|
+
import { HyperedgeRegistry } from '../models/hyperedge-registry.js';
|
|
9
8
|
import { Added, Removed, Changed } from './change.js';
|
|
10
9
|
import { ListDiff } from './list-diff.js';
|
|
11
10
|
import { TextDiff, TextHunk } from './text-diff.js';
|
|
@@ -108,32 +107,25 @@ function applyConceptLevelPatch(json, conceptDiff) {
|
|
|
108
107
|
json.dates = applyListPatch(json.dates ?? [], conceptDiff.dates, ConceptDate.identityOf);
|
|
109
108
|
json.related = applyListPatch(json.related ?? [], conceptDiff.relatedConcepts, RelatedConcept.identityOf);
|
|
110
109
|
|
|
111
|
-
// Unified
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
delete json
|
|
110
|
+
// Unified hyperedge diff → typed wire keys. Iterate the registry;
|
|
111
|
+
// for each registered class, partition the unified diff by that
|
|
112
|
+
// class and apply to the class's wireKey. Adding a new hyperedge
|
|
113
|
+
// type means this loop picks it up automatically — no edits here.
|
|
114
|
+
for (const cls of HyperedgeRegistry.allClasses()) {
|
|
115
|
+
const partition = partitionListDiff(conceptDiff.relations, cls);
|
|
116
|
+
if (!partition.hasChanges) continue;
|
|
117
|
+
const wireKey = cls.wireKey;
|
|
118
|
+
// v1 wire keys (partitive_hyperedges) on the input are migrated
|
|
119
|
+
// to v2 wire key here: read from either, write to v2 only.
|
|
120
|
+
const v1ReadKeys = cls.v1WireKeys ?? [];
|
|
121
|
+
const existing = v1ReadKeys
|
|
122
|
+
.map(k => json[k])
|
|
123
|
+
.find(arr => Array.isArray(arr)) ?? json[wireKey] ?? [];
|
|
124
|
+
json[wireKey] = applyListPatch(existing, partition, cls.identityOf);
|
|
125
|
+
for (const k of v1ReadKeys) {
|
|
126
|
+
if (json[k] != null && json[wireKey] != null) delete json[k];
|
|
128
127
|
}
|
|
129
128
|
}
|
|
130
|
-
if (genericDiff.hasChanges) {
|
|
131
|
-
json.generic_relations = applyListPatch(
|
|
132
|
-
json.generic_relations ?? [],
|
|
133
|
-
genericDiff,
|
|
134
|
-
GenericRelation.identityOf,
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
129
|
|
|
138
130
|
json.tags = applyListPatch(json.tags ?? [], conceptDiff.tags);
|
|
139
131
|
|
|
@@ -253,7 +245,7 @@ function toJsonValue(value) {
|
|
|
253
245
|
// Project a unified relations ListDiff onto a single concrete class.
|
|
254
246
|
// Entries whose value (old or new) is an instance of `Cls` survive;
|
|
255
247
|
// everything else is filtered out. Cross-type changed entries (e.g.
|
|
256
|
-
// old was
|
|
248
|
+
// old was PartitiveHyperedge, new is GenericHyperedge) are not yet
|
|
257
249
|
// supported — those would need to surface as a remove + add pair.
|
|
258
250
|
function partitionListDiff(listDiff, Cls) {
|
|
259
251
|
if (!listDiff) return new ListDiff({ added: [], removed: [], changed: [] });
|
|
@@ -125,10 +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
|
-
// Unified
|
|
128
|
+
// Unified hyperedge section. Replaces the per-type sections
|
|
129
129
|
// (Partitive relations, Generic relations) — the underlying diff
|
|
130
130
|
// is a single ListDiff over the unified .relations array.
|
|
131
|
-
lines.push(...renderListDiff('
|
|
131
|
+
lines.push(...renderListDiff('Hyperedges', diff.relations, colors, relationLabel));
|
|
132
132
|
lines.push(...renderListDiff('Groups', diff.groups, colors, itemLabel));
|
|
133
133
|
lines.push(...renderListDiff('Sections', diff.sections, colors, itemLabel));
|
|
134
134
|
lines.push(...renderListDiff('Tags', diff.tags, colors, itemLabel));
|
|
@@ -206,16 +206,24 @@ function relationLabel(r) {
|
|
|
206
206
|
if (!r) return '?';
|
|
207
207
|
const c = r.comprehensive;
|
|
208
208
|
const head = c?.id ?? c?.source ?? c?.text ?? '?';
|
|
209
|
-
//
|
|
210
|
-
// .members;
|
|
209
|
+
// PartitiveHyperedge exposes .partitives as the wire alias for
|
|
210
|
+
// .members; GenericHyperedge uses .members directly. Read both.
|
|
211
211
|
const members = Array.isArray(r.partitives) ? r.partitives : r.members;
|
|
212
212
|
const memberText = Array.isArray(members)
|
|
213
213
|
? members.map(m => {
|
|
214
214
|
const ref = m?.ref ?? m ?? {};
|
|
215
215
|
let tail = '';
|
|
216
216
|
const mult = resolveMultiplicity(m);
|
|
217
|
-
if (mult && mult !== 'compulsory') tail
|
|
217
|
+
if (mult && mult !== 'compulsory') tail += ` (${mult})`;
|
|
218
|
+
// PartitiveMember uses is_delimiting (boolean flag).
|
|
218
219
|
if (m?.is_delimiting === true) tail += ' ⊘';
|
|
220
|
+
// GenericMember uses delimitingCharacteristic (string value per
|
|
221
|
+
// ISO 704:2022 §5.5.4.2.1) — show the delimiting characteristic
|
|
222
|
+
// text so reviewers can see the intension difference.
|
|
223
|
+
if (m?.delimitingCharacteristic) {
|
|
224
|
+
const c = Object.values(m.delimitingCharacteristic)[0];
|
|
225
|
+
if (typeof c === 'string' && c.length > 0) tail += ` ⟨${c}⟩`;
|
|
226
|
+
}
|
|
219
227
|
return `${ref.id ?? ref.source ?? ref.text ?? '?'}${tail}`;
|
|
220
228
|
}).join(', ')
|
|
221
229
|
: '';
|
|
@@ -223,7 +231,12 @@ function relationLabel(r) {
|
|
|
223
231
|
const criterion = r.criterion
|
|
224
232
|
? ` / ${Object.values(r.criterion)[0] ?? ''}`
|
|
225
233
|
: '';
|
|
226
|
-
|
|
234
|
+
// Each hyperedge leaf declares a short static kindLabel (PART, GEN, …).
|
|
235
|
+
// Read it via the constructor so adding a new hyperedge type needs no
|
|
236
|
+
// renderer edit. The blank-string default keeps backward-compat with
|
|
237
|
+
// any AbstractHyperedge subclass that doesn't declare kindLabel.
|
|
238
|
+
const kindLabel = r?.constructor?.kindLabel ?? '';
|
|
239
|
+
const typeTag = kindLabel ? `${kindLabel} ` : '';
|
|
227
240
|
return `${typeTag}${head} → {${memberText}}${completeness}${criterion}`;
|
|
228
241
|
}
|
|
229
242
|
|
package/src/index.js
CHANGED
|
@@ -63,7 +63,8 @@ export {
|
|
|
63
63
|
fetchLocalizedString, localizedStringIsEmpty, localizedStringIsPresent,
|
|
64
64
|
GcrMetadata, GcrStatistics,
|
|
65
65
|
RELATIONSHIP_TYPES, DESIGNATION_RELATIONSHIP_TYPES, DATE_TYPES,
|
|
66
|
-
PartitiveHyperedge,
|
|
66
|
+
PartitiveHyperedge,
|
|
67
|
+
PARTITIVE_ENUMERATION, PARTITIVE_ENUMERATION_VALUES,
|
|
67
68
|
PLURALITY_MARKER, PLURALITY_MARKER_VALUES,
|
|
68
69
|
} from './models/index.js';
|
|
69
70
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// v1 → v2
|
|
1
|
+
// v1 → v2 PartitiveHyperedge migration.
|
|
2
2
|
//
|
|
3
3
|
// Pure function: takes a v1 PartitiveHyperedge hash, returns a v2
|
|
4
|
-
//
|
|
4
|
+
// PartitiveHyperedge hash. Idempotent. Used by the parser when loading
|
|
5
5
|
// legacy YAML; available as a public API for one-shot dataset migrations.
|
|
6
6
|
//
|
|
7
7
|
// Field mapping (per TODO.partitive-relation-v2 items 01–05 and the
|
|
@@ -69,7 +69,7 @@ export function migrateHyperedgeToRelation(v1Hash) {
|
|
|
69
69
|
|
|
70
70
|
if (v1Hash.content != null) {
|
|
71
71
|
out.migrationWarning = (out.migrationWarning ?? '') +
|
|
72
|
-
` v1 'content' field was dropped (v2
|
|
72
|
+
` v1 'content' field was dropped (v2 PartitiveHyperedge carries no prose; ` +
|
|
73
73
|
`move valuable text to the comprehensive concept's notes).`;
|
|
74
74
|
}
|
|
75
75
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
//
|
|
2
|
-
// relations (
|
|
1
|
+
// AbstractHyperedge — shared base for directed 1→N hyperedges
|
|
2
|
+
// relations (PartitiveHyperedge, GenericHyperedge, future
|
|
3
3
|
// SequentialRelation, AssociativeRelation).
|
|
4
4
|
//
|
|
5
5
|
// Carries the common shape:
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// - sources / notes / status: administrative metadata
|
|
11
11
|
//
|
|
12
12
|
// Concrete leaves add type-specific fields if needed (currently none).
|
|
13
|
-
// See docs/design/abstract-
|
|
13
|
+
// See docs/design/abstract-hyperedge.md (concept-model repo).
|
|
14
14
|
|
|
15
15
|
import { GlossaristModel } from './base.js';
|
|
16
16
|
import { ConceptRef } from './concept-ref.js';
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
isValidCompleteness,
|
|
21
21
|
} from './completeness.js';
|
|
22
22
|
|
|
23
|
-
export class
|
|
23
|
+
export class AbstractHyperedge extends GlossaristModel {
|
|
24
24
|
constructor(data = {}) {
|
|
25
25
|
super();
|
|
26
26
|
this.comprehensive = _ensureComprehensive(data.comprehensive);
|
|
@@ -62,7 +62,7 @@ function _ensureComprehensive(value) {
|
|
|
62
62
|
const ref = value instanceof ConceptRef ? value : new ConceptRef(value ?? {});
|
|
63
63
|
if (!ref.source && !ref.id && !ref.text) {
|
|
64
64
|
throw new Error(
|
|
65
|
-
'
|
|
65
|
+
'AbstractHyperedge.comprehensive must be a non-empty ConceptReference ' +
|
|
66
66
|
'(source, id, or text required)',
|
|
67
67
|
);
|
|
68
68
|
}
|