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.
Files changed (40) hide show
  1. package/package.json +1 -1
  2. package/src/concept-parser.js +67 -45
  3. package/src/concept-reader.js +59 -3
  4. package/src/concept-serializer.js +4 -4
  5. package/src/diff/concept-diff.js +13 -13
  6. package/src/diff/diff-patch.js +19 -27
  7. package/src/diff/diff-renderer.js +19 -6
  8. package/src/index.js +2 -1
  9. package/src/migration/{partitive-relation-migrator.js → hyperedge-migrator.js} +3 -3
  10. package/src/models/{abstract-nary-relation.js → abstract-hyperedge.js} +5 -5
  11. package/src/models/completeness.js +1 -1
  12. package/src/models/concept.js +145 -72
  13. package/src/models/{generic-relation.js → generic-hyperedge.js} +18 -6
  14. package/src/models/generic-member.js +76 -9
  15. package/src/models/hyperedge-index.js +87 -0
  16. package/src/models/{concept-system-member.js → hyperedge-member.js} +26 -43
  17. package/src/models/hyperedge-registry.js +115 -0
  18. package/src/models/index.d.ts +16 -16
  19. package/src/models/index.js +16 -13
  20. package/src/models/multiplicity.js +11 -6
  21. package/src/models/partitive-hyperedge.js +69 -153
  22. package/src/models/partitive-member.js +40 -11
  23. package/src/models/relation-loader.js +28 -22
  24. package/src/rdf/gloss-concept.js +36 -20
  25. package/src/rdf/gloss-generic-hyperedge.js +92 -0
  26. package/src/rdf/gloss-partitive-relation.js +4 -4
  27. package/src/rdf/index.d.ts +4 -4
  28. package/src/rdf/index.js +0 -1
  29. package/src/reference-resolver.js +14 -12
  30. package/src/validators/binary-has-part-redundancy-rule.js +6 -6
  31. package/src/validators/delimiting-coherence-rule.js +2 -2
  32. package/src/validators/hyperedge-coherence-rule.js +98 -0
  33. package/src/validators/index.js +3 -9
  34. package/src/validators/partitive-relation-coherence-rule.js +6 -79
  35. package/src/validators/ref-keys.js +2 -2
  36. package/src/models/partitive-relation.js +0 -76
  37. package/src/models/relation-type-registry.js +0 -25
  38. package/src/rdf/gloss-hyperedge.js +0 -90
  39. package/src/validators/partitive-drift-rule.js +0 -51
  40. package/src/validators/partitive-hyperedge-rule.js +0 -75
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glossarist",
3
- "version": "0.4.32",
3
+ "version": "0.4.34",
4
4
  "description": "JavaScript SDK for Glossarist GCR packages — read, write, validate, and manage terminology concepts",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -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 { migrateHyperedgeToRelation } from './migration/partitive-relation-migrator.js';
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 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.
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
- 'partitive_hyperedges', 'partitiveHyperedges',
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: _resolveNaryData(doc),
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
- assertConceptLevelOnly(mc, [
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
- 'partitive_relations',
94
- 'partitive_hyperedges',
95
- 'generic_relations',
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: _resolveNaryData(mc),
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.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
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 n-ary relation data from any input shape into a single
134
- // unified array. Accepts:
135
- //
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
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
- // `_resolveNaryRelations` falls back to the empty-array default.
143
- function _resolveNaryData(container) {
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 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);
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
- const v1 = container.partitive_hyperedges ?? container.partitiveHyperedges;
169
- if (Array.isArray(v1)) {
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' });
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
 
@@ -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
- return conceptParser.parse(raw, `${id}.yaml`);
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 { PartitiveRelation } from './models/partitive-relation.js';
3
- import { GenericRelation } from './models/generic-relation.js';
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 PartitiveRelation);
13
- const genericRels = concept.relations.filter(r => r instanceof GenericRelation);
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()),
@@ -1,6 +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
+ 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 n-ary relations diff. Accepts:
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 n-ary relations diff (PartitiveRelation + GenericRelation). */
130
+ /** Unified hyperedge diff (PartitiveHyperedge + GenericHyperedge). */
131
131
  get relations() { return this._relations; }
132
132
  /**
133
- * Filtered view of `relations` containing only PartitiveRelation
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, PartitiveRelation);
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 GenericRelation
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, GenericRelation);
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: diffNaryRelations(
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 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
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 diffNaryRelations(oldR, newR) {
574
+ function diffHyperedges(oldR, newR) {
575
575
  return diffSet(oldR, newR, {
576
576
  identityKey: relationIdentityOf,
577
577
  textKey: relationText,
@@ -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 { PartitiveRelation } from '../models/partitive-relation.js';
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 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;
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 PartitiveRelation, new is GenericRelation) are not yet
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 n-ary relations section. Replaces the per-type sections
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('N-ary relations', diff.relations, colors, relationLabel));
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
- // PartitiveRelation exposes .partitives as the wire alias for
210
- // .members; GenericRelation uses .members directly. Read both.
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 = ` (${mult})`;
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
- const typeTag = r?.constructor?.name === 'GenericRelation' ? 'GEN ' : '';
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, PARTITIVE_ENUMERATION, PARTITIVE_ENUMERATION_VALUES,
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 PartitiveRelation migration.
1
+ // v1 → v2 PartitiveHyperedge migration.
2
2
  //
3
3
  // Pure function: takes a v1 PartitiveHyperedge hash, returns a v2
4
- // PartitiveRelation hash. Idempotent. Used by the parser when loading
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 PartitiveRelation carries no prose; ` +
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
- // AbstractNaryRelation — shared base for n-ary concept-system
2
- // relations (PartitiveRelation, GenericRelation, future
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-nary-relation.md (concept-model repo).
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 AbstractNaryRelation extends GlossaristModel {
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
- 'AbstractNaryRelation.comprehensive must be a non-empty ConceptReference ' +
65
+ 'AbstractHyperedge.comprehensive must be a non-empty ConceptReference ' +
66
66
  '(source, id, or text required)',
67
67
  );
68
68
  }
@@ -1,4 +1,4 @@
1
- // Completeness — whether a PartitiveRelation's encoded partitives
1
+ // Completeness — whether a PartitiveHyperedge's encoded partitives
2
2
  // constitute the whole comprehensive (complete) or only some of it
3
3
  // (partial).
4
4
  //