glossarist 0.4.32 → 0.4.33
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 +66 -44
- package/src/concept-reader.js +59 -3
- package/src/concept-serializer.js +4 -4
- package/src/diff/concept-diff.js +9 -9
- package/src/diff/diff-patch.js +19 -27
- package/src/diff/diff-renderer.js +8 -3
- package/src/index.js +2 -1
- package/src/migration/partitive-relation-migrator.js +3 -3
- package/src/models/{abstract-nary-relation.js → abstract-hyperedge.js} +4 -4
- package/src/models/completeness.js +1 -1
- package/src/models/concept.js +144 -71
- package/src/models/{generic-relation.js → generic-hyperedge.js} +18 -6
- package/src/models/generic-member.js +5 -5
- package/src/models/hyperedge-index.js +87 -0
- package/src/models/{concept-system-member.js → hyperedge-member.js} +12 -24
- package/src/models/hyperedge-registry.js +115 -0
- package/src/models/index.d.ts +16 -16
- package/src/models/index.js +13 -10
- package/src/models/multiplicity.js +11 -6
- package/src/models/partitive-hyperedge.js +69 -153
- package/src/models/partitive-member.js +6 -6
- 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 +3 -3
- 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/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/src/models/concept.js
CHANGED
|
@@ -2,10 +2,8 @@ import { GlossaristModel } from './base.js';
|
|
|
2
2
|
import { LocalizedConcept } from './localized-concept.js';
|
|
3
3
|
import { RelatedConcept } from './related-concept.js';
|
|
4
4
|
import { PartitiveHyperedge } from './partitive-hyperedge.js';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { AbstractNaryRelation } from './abstract-nary-relation.js';
|
|
8
|
-
import { TYPE_TO_CLASS } from './relation-type-registry.js';
|
|
5
|
+
import { AbstractHyperedge } from './abstract-hyperedge.js';
|
|
6
|
+
import { HyperedgeRegistry, groupHyperedgesByWireKey } from './hyperedge-registry.js';
|
|
9
7
|
import { migrateHyperedgeToRelation } from '../migration/partitive-relation-migrator.js';
|
|
10
8
|
import { ConceptReference } from './concept-reference.js';
|
|
11
9
|
import { ConceptDate } from './concept-date.js';
|
|
@@ -38,16 +36,18 @@ export class Concept extends GlossaristModel {
|
|
|
38
36
|
|
|
39
37
|
this.relatedConcepts = _mapInstances(data.relatedConcepts ?? data.related ?? data.related_concepts ?? [], RelatedConcept);
|
|
40
38
|
|
|
41
|
-
// Unified
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
39
|
+
// Unified hyperedge array. Every n-ary relation (PartitiveHyperedge,
|
|
40
|
+
// GenericHyperedge, future TemporalHyperedge / AssociativeHyperedge /
|
|
41
|
+
// SequentialHyperedge) lives here. The single public API is
|
|
42
|
+
// `.relations`; external systems that need to filter by type use
|
|
43
|
+
// `concept.relations.filter(r => r instanceof X)` or dispatch via
|
|
44
|
+
// HyperedgeRegistry (preferred — see Phases 6 and 11).
|
|
45
45
|
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
this.relations =
|
|
46
|
+
// Throws on ambiguous input (caller passed both `relations` and any
|
|
47
|
+
// typed wire key) and on duplicate relations (same identity twice).
|
|
48
|
+
// Silent drop / silent dedupe are footguns the audit (C4, C6)
|
|
49
|
+
// flagged; explicit throws make corruption loud.
|
|
50
|
+
this.relations = _resolveHyperedges(data);
|
|
51
51
|
|
|
52
52
|
this.domains = _normalizeDomains(data.domains, data.groups);
|
|
53
53
|
this.groups = Array.isArray(data.groups)
|
|
@@ -72,16 +72,17 @@ export class Concept extends GlossaristModel {
|
|
|
72
72
|
|
|
73
73
|
get termid() { return this.id; }
|
|
74
74
|
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
//
|
|
84
|
-
|
|
75
|
+
// No typed projections. Use concept.relations directly and filter
|
|
76
|
+
// by instanceof, or dispatch via HyperedgeRegistry. The old
|
|
77
|
+
// .partitiveRelations / .genericRelations / .partitiveHyperedges
|
|
78
|
+
// getters were removed per TODO Phase 4 — they were footguns
|
|
79
|
+
// (audit C3: .partitiveHyperedges returned v2 instances but callers
|
|
80
|
+
// expected v1 shape; multiple validators silently no-op'd).
|
|
81
|
+
//
|
|
82
|
+
// Snippet migration guide:
|
|
83
|
+
// concept.partitiveRelations → concept.relations.filter(r => r instanceof PartitiveHyperedge)
|
|
84
|
+
// concept.genericRelations → concept.relations.filter(r => r instanceof GenericHyperedge)
|
|
85
|
+
// concept.partitiveHyperedges → concept.relations.filter(r => r instanceof PartitiveHyperedge)
|
|
85
86
|
|
|
86
87
|
get languages() {
|
|
87
88
|
return Object.keys(this._rawLocalizations);
|
|
@@ -222,16 +223,13 @@ export class Concept extends GlossaristModel {
|
|
|
222
223
|
if (this.relatedConcepts.length > 0) {
|
|
223
224
|
obj.related = this.relatedConcepts.map(rc => rc.toJSON());
|
|
224
225
|
}
|
|
225
|
-
// Emit
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
}
|
|
233
|
-
if (genericRels.length > 0) {
|
|
234
|
-
obj.generic_relations = genericRels.map(r => r.toJSON());
|
|
226
|
+
// Emit per-type wire keys from the unified relations array,
|
|
227
|
+
// partitioned via HyperedgeRegistry. Adding a new hyperedge type
|
|
228
|
+
// means its wire key appears here automatically — no edits to
|
|
229
|
+
// Concept.toJSON.
|
|
230
|
+
const grouped = groupHyperedgesByWireKey(this.relations);
|
|
231
|
+
for (const [wireKey, rels] of Object.entries(grouped)) {
|
|
232
|
+
if (rels.length > 0) obj[wireKey] = rels.map(r => r.toJSON());
|
|
235
233
|
}
|
|
236
234
|
if (this.domains.length > 0) {
|
|
237
235
|
obj.domains = this.domains.map(d => d.toJSON());
|
|
@@ -262,72 +260,147 @@ function _mapInstances(arr, Cls) {
|
|
|
262
260
|
return arr.map(item => item instanceof Cls ? item : new Cls(item));
|
|
263
261
|
}
|
|
264
262
|
|
|
265
|
-
// Resolve the unified
|
|
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)
|
|
271
|
-
// 3. data.partitiveHyperedges / data.partitive_hyperedges (v1, migrated)
|
|
263
|
+
// Resolve the unified hyperedge array from any input shape.
|
|
272
264
|
//
|
|
273
|
-
//
|
|
274
|
-
//
|
|
265
|
+
// Three input categories are accepted; mixing them throws (audit C4 —
|
|
266
|
+
// silent drop was a footgun):
|
|
275
267
|
//
|
|
276
|
-
//
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
|
|
268
|
+
// 1. data.relations — preferred unified shape. Entries
|
|
269
|
+
// are typed instances or hashes
|
|
270
|
+
// carrying a `type` discriminator
|
|
271
|
+
// (looked up via HyperedgeRegistry).
|
|
272
|
+
// 2. Per-type v2 wire keys — one of HyperedgeRegistry.allWireKeys()
|
|
273
|
+
// (partitive_relations, generic_relations).
|
|
274
|
+
// 3. Per-type v1 legacy keys — declared on each class via
|
|
275
|
+
// `static v1WireKeys` (e.g.
|
|
276
|
+
// partitive_hyperedges → migrated).
|
|
277
|
+
//
|
|
278
|
+
// Adding a new hyperedge type means: declare the leaf class with its
|
|
279
|
+
// metadata block, register it. Concept, parser, serializer, diff,
|
|
280
|
+
// patch, renderer, RDF emitter do not change. (Phase 11 OCP contract.)
|
|
281
|
+
function _resolveHyperedges(data) {
|
|
282
|
+
_assertNoAmbiguousInput(data);
|
|
283
|
+
|
|
280
284
|
if (Array.isArray(data.relations)) {
|
|
281
|
-
return data.relations
|
|
282
|
-
.map(r => _coerceRelation(r))
|
|
283
|
-
.filter(r => r != null);
|
|
285
|
+
return _dedupeOrThrow(data.relations.map(r => _coerceHyperedge(r)).filter(r => r != null));
|
|
284
286
|
}
|
|
285
287
|
|
|
286
288
|
const out = [];
|
|
287
289
|
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
290
|
+
for (const cls of HyperedgeRegistry.allClasses()) {
|
|
291
|
+
const arr = data[cls.wireKey] ?? data[_camelCase(cls.wireKey)];
|
|
292
|
+
if (!Array.isArray(arr)) continue;
|
|
293
|
+
for (const r of arr) {
|
|
294
|
+
const coerced = _coerceHyperedge(r, cls);
|
|
292
295
|
if (coerced) out.push(coerced);
|
|
293
296
|
}
|
|
294
297
|
}
|
|
295
298
|
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
const
|
|
300
|
-
if (
|
|
299
|
+
for (const cls of HyperedgeRegistry.allClasses()) {
|
|
300
|
+
for (const v1Key of cls.v1WireKeys ?? []) {
|
|
301
|
+
const camelV1 = _camelCase(v1Key);
|
|
302
|
+
const arr = data[v1Key] ?? data[camelV1];
|
|
303
|
+
if (!Array.isArray(arr)) continue;
|
|
304
|
+
for (const h of arr) {
|
|
305
|
+
const migrated = _migrateV1Hash(cls, v1Key, h);
|
|
306
|
+
if (migrated) out.push(new cls(migrated));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return _dedupeOrThrow(out);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Audit C4: silent drop on mixed input is a footgun. Throw instead.
|
|
315
|
+
// Allows `relations` alone, or any combination of typed keys alone —
|
|
316
|
+
// but not both at once.
|
|
317
|
+
function _assertNoAmbiguousInput(data) {
|
|
318
|
+
if (!Array.isArray(data.relations)) return;
|
|
319
|
+
const typedKeys = [];
|
|
320
|
+
for (const cls of HyperedgeRegistry.allClasses()) {
|
|
321
|
+
if (Array.isArray(data[cls.wireKey])) typedKeys.push(cls.wireKey);
|
|
322
|
+
const camelWire = _camelCase(cls.wireKey);
|
|
323
|
+
if (camelWire !== cls.wireKey && Array.isArray(data[camelWire])) {
|
|
324
|
+
typedKeys.push(camelWire);
|
|
325
|
+
}
|
|
326
|
+
for (const v1 of cls.v1WireKeys ?? []) {
|
|
327
|
+
if (Array.isArray(data[v1])) typedKeys.push(v1);
|
|
328
|
+
const camelV1 = _camelCase(v1);
|
|
329
|
+
if (camelV1 !== v1 && Array.isArray(data[camelV1])) typedKeys.push(camelV1);
|
|
301
330
|
}
|
|
302
331
|
}
|
|
332
|
+
if (typedKeys.length > 0) {
|
|
333
|
+
throw new Error(
|
|
334
|
+
`Concept input is ambiguous: pass either 'relations' (unified) ` +
|
|
335
|
+
`or typed wire keys (${typedKeys.join(', ')}), not both. ` +
|
|
336
|
+
` Mixing them silently drops the typed keys, which has caused ` +
|
|
337
|
+
` data loss in production (see audit C4).`,
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
303
341
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
342
|
+
// Audit C6: do NOT silently dedupe. Duplicate relations indicate a bug
|
|
343
|
+
// in the caller; surface it. The identity function delegates to each
|
|
344
|
+
// class's static identityOf (polymorphic — handles mixed types).
|
|
345
|
+
function _dedupeOrThrow(relations) {
|
|
346
|
+
const seen = new Map();
|
|
347
|
+
for (const r of relations) {
|
|
348
|
+
const id = _hyperedgeIdentity(r);
|
|
349
|
+
if (seen.has(id)) {
|
|
350
|
+
throw new Error(
|
|
351
|
+
`Concept input has duplicate hyperedge (identity=${id}). ` +
|
|
352
|
+
`Passing the same relation twice is a bug — the parser and ` +
|
|
353
|
+
`serializer would silently emit it twice on disk. ` +
|
|
354
|
+
`Audit C6 requires we surface this loudly instead.`,
|
|
355
|
+
);
|
|
310
356
|
}
|
|
357
|
+
seen.set(id, r);
|
|
311
358
|
}
|
|
359
|
+
return relations;
|
|
360
|
+
}
|
|
312
361
|
|
|
313
|
-
|
|
362
|
+
function _hyperedgeIdentity(r) {
|
|
363
|
+
if (r == null) return '';
|
|
364
|
+
const Cls = r.constructor;
|
|
365
|
+
if (typeof Cls?.identityOf === 'function') {
|
|
366
|
+
return `${Cls.name}::${Cls.identityOf(r)}`;
|
|
367
|
+
}
|
|
368
|
+
return String(r);
|
|
314
369
|
}
|
|
315
370
|
|
|
316
|
-
function
|
|
371
|
+
function _coerceHyperedge(value, fallbackClass) {
|
|
317
372
|
if (value == null) return null;
|
|
318
|
-
if (value instanceof
|
|
373
|
+
if (value instanceof AbstractHyperedge) return value;
|
|
319
374
|
|
|
320
375
|
const hash = typeof value.toJSON === 'function' ? value.toJSON() : value;
|
|
321
|
-
const
|
|
322
|
-
if (!
|
|
323
|
-
return new
|
|
376
|
+
const cls = _classForHash(hash) ?? fallbackClass;
|
|
377
|
+
if (!cls) return null;
|
|
378
|
+
return new cls(hash);
|
|
324
379
|
}
|
|
325
380
|
|
|
326
381
|
function _classForHash(hash) {
|
|
327
382
|
if (!hash || typeof hash !== 'object') return null;
|
|
328
383
|
const type = hash.type;
|
|
329
384
|
if (typeof type !== 'string') return null;
|
|
330
|
-
return
|
|
385
|
+
return HyperedgeRegistry.forTypeTag(type);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// v1 → v2 migration for hashes from legacy wire keys. Currently only
|
|
389
|
+
// PartitiveHyperedge has v1 wire keys (partitive_hyperedges). Future
|
|
390
|
+
// types extend this dispatch — but it lives here in the migration
|
|
391
|
+
// helper, not in the model classes, since v1 is a legacy concern.
|
|
392
|
+
function _migrateV1Hash(cls, v1Key, hash) {
|
|
393
|
+
if (cls === PartitiveHyperedge && v1Key === 'partitive_hyperedges') {
|
|
394
|
+
const normalized = hash;
|
|
395
|
+
return migrateHyperedgeToRelation(normalized);
|
|
396
|
+
}
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// snake_case → camelCase
|
|
401
|
+
function _camelCase(s) {
|
|
402
|
+
if (typeof s !== 'string' || !s.includes('_')) return s;
|
|
403
|
+
return s.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
331
404
|
}
|
|
332
405
|
|
|
333
406
|
const CONCEPT_WIRE_NAMES = Object.freeze({
|
|
@@ -1,18 +1,28 @@
|
|
|
1
|
-
//
|
|
1
|
+
// GenericHyperedge — an ISO 704 / ISO 1087-1 / ISO 12620 generic
|
|
2
2
|
// relation connecting a comprehensive concept (the genus) to two
|
|
3
3
|
// or more specific concepts (the species) which together constitute
|
|
4
4
|
// a decomposition by some criterion of subdivision.
|
|
5
5
|
//
|
|
6
|
-
// Mirror of
|
|
6
|
+
// Mirror of PartitiveHyperedge. Multiple GenericRelations on the same
|
|
7
7
|
// comprehensive distinguished by `criterion` is the OIML pattern
|
|
8
8
|
// (5.1 measurement standard has 6 criterion groups).
|
|
9
9
|
//
|
|
10
10
|
// See docs/design/generic-relation.md (concept-model repo).
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import { AbstractHyperedge } from './abstract-hyperedge.js';
|
|
13
13
|
import { GenericMember } from './generic-member.js';
|
|
14
|
+
import { HyperedgeRegistry } from './hyperedge-registry.js';
|
|
15
|
+
|
|
16
|
+
export class GenericHyperedge extends AbstractHyperedge {
|
|
17
|
+
// Per-class metadata block — the OCP contract. See PartitiveHyperedge
|
|
18
|
+
// for field documentation.
|
|
19
|
+
static wireKey = 'generic_relations';
|
|
20
|
+
static typeTag = 'generic_relation';
|
|
21
|
+
static rdfType = 'gloss:GenericRelation';
|
|
22
|
+
static memberClass = GenericMember;
|
|
23
|
+
static v1WireKeys = [];
|
|
24
|
+
static kindLabel = 'GEN';
|
|
14
25
|
|
|
15
|
-
export class GenericRelation extends AbstractNaryRelation {
|
|
16
26
|
constructor(data = {}) {
|
|
17
27
|
super(data);
|
|
18
28
|
this.members = data?.members == null
|
|
@@ -36,8 +46,10 @@ export class GenericRelation extends AbstractNaryRelation {
|
|
|
36
46
|
}
|
|
37
47
|
|
|
38
48
|
static fromJSON(data) {
|
|
39
|
-
return new
|
|
49
|
+
return new GenericHyperedge(data);
|
|
40
50
|
}
|
|
41
51
|
}
|
|
42
52
|
|
|
43
|
-
export default
|
|
53
|
+
export default GenericHyperedge;
|
|
54
|
+
|
|
55
|
+
HyperedgeRegistry.register(GenericHyperedge);
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
// GenericMember — one member of a
|
|
2
|
-
// ISO 704:2022 MECE shape from
|
|
1
|
+
// GenericMember — one member of a GenericHyperedge. Inherits the
|
|
2
|
+
// ISO 704:2022 MECE shape from HyperedgeMember. Declared as a
|
|
3
3
|
// distinct leaf class for type safety and to leave room for
|
|
4
4
|
// generic-specific extensions.
|
|
5
5
|
//
|
|
6
6
|
// See docs/design/abstract-nary-relation.md and
|
|
7
7
|
// docs/design/generic-relation.md (concept-model repo).
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { HyperedgeMember } from './hyperedge-member.js';
|
|
10
10
|
|
|
11
|
-
export class GenericMember extends
|
|
12
|
-
// Inherits all attributes and validations from
|
|
11
|
+
export class GenericMember extends HyperedgeMember {
|
|
12
|
+
// Inherits all attributes and validations from HyperedgeMember.
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export default GenericMember;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// HyperedgeIndex — directional index over a hyperedge set.
|
|
2
|
+
//
|
|
3
|
+
// A hyperedge is 1→N (one comprehensive, many members). The forward
|
|
4
|
+
// query ("which hyperedges have X as comprehensive?") and the reverse
|
|
5
|
+
// query ("which hyperedges have Y as a member?") are both useful. The
|
|
6
|
+
// reverse query is the load-bearing one: OIML V 2-200:2010 concept
|
|
7
|
+
// 5.13 is a member of one of 5.1's hyperedges AND the comprehensive
|
|
8
|
+
// of its own 2 hyperedges — without an index, every "find hyperedges
|
|
9
|
+
// mentioning X" turns into an O(N) walk over every hyperedge.
|
|
10
|
+
//
|
|
11
|
+
// The index is built on demand from a flat hyperedge list (typically
|
|
12
|
+
// Concept.relations or the union of all concepts' relations in a
|
|
13
|
+
// collection). It does not store state on Concept — Concept stays a
|
|
14
|
+
// simple value object.
|
|
15
|
+
//
|
|
16
|
+
// Identity key matches HyperedgeRegistry / ConceptRef conventions:
|
|
17
|
+
// `${source}:${id}` (empty source or id is rejected)
|
|
18
|
+
//
|
|
19
|
+
// See TODO Phase 9.
|
|
20
|
+
|
|
21
|
+
export class HyperedgeIndex {
|
|
22
|
+
constructor(hyperedges = []) {
|
|
23
|
+
this._byComprehensive = new Map();
|
|
24
|
+
this._byMember = new Map();
|
|
25
|
+
|
|
26
|
+
for (const h of hyperedges) {
|
|
27
|
+
const cKey = _qualifiedId(h?.comprehensive);
|
|
28
|
+
if (cKey) _addToMapList(this._byComprehensive, cKey, h);
|
|
29
|
+
const members = Array.isArray(h?.members) ? h.members : [];
|
|
30
|
+
for (const m of members) {
|
|
31
|
+
const mKey = _qualifiedId(m?.ref ?? m);
|
|
32
|
+
if (mKey) _addToMapList(this._byMember, mKey, h);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Hyperedges where the given concept is the comprehensive (the "1" side).
|
|
38
|
+
forComprehensive(qualifiedId) {
|
|
39
|
+
return this._byComprehensive.get(qualifiedId) ?? [];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Hyperedges where the given concept appears as a member (the "many" side).
|
|
43
|
+
forMember(qualifiedId) {
|
|
44
|
+
return this._byMember.get(qualifiedId) ?? [];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// All concepts that appear as a comprehensive in any indexed hyperedge.
|
|
48
|
+
comprehensives() {
|
|
49
|
+
return [...this._byComprehensive.keys()];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// All concepts that appear as a member in any indexed hyperedge.
|
|
53
|
+
members() {
|
|
54
|
+
return [...this._byMember.keys()];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
get size() {
|
|
58
|
+
return this._byComprehensive.size;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Build a HyperedgeIndex across multiple Concepts' relations arrays
|
|
63
|
+
// (a dataset-wide view). Each Concept contributes its own .relations.
|
|
64
|
+
// Helper for the most common pattern: index once at collection load,
|
|
65
|
+
// query many times.
|
|
66
|
+
export function buildDatasetIndex(concepts = []) {
|
|
67
|
+
const all = [];
|
|
68
|
+
for (const concept of concepts) {
|
|
69
|
+
for (const h of concept.relations ?? []) {
|
|
70
|
+
all.push(h);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return new HyperedgeIndex(all);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function _qualifiedId(ref) {
|
|
77
|
+
if (!ref) return null;
|
|
78
|
+
const source = ref.source ?? '';
|
|
79
|
+
const id = ref.id ?? '';
|
|
80
|
+
if (!source && !id) return null;
|
|
81
|
+
return `${source}:${id}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function _addToMapList(map, key, value) {
|
|
85
|
+
if (!map.has(key)) map.set(key, []);
|
|
86
|
+
map.get(key).push(value);
|
|
87
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// HyperedgeMember — abstract base shape for members of any
|
|
2
2
|
// n-ary concept-system relation (PartitiveMember, GenericMember,
|
|
3
3
|
// future AssociativeMember, SequentialMember).
|
|
4
4
|
//
|
|
@@ -18,20 +18,13 @@ import {
|
|
|
18
18
|
isValidPresence,
|
|
19
19
|
} from './partitive-presence.js';
|
|
20
20
|
import {
|
|
21
|
-
PARTITIVE_COUNT,
|
|
22
21
|
PARTITIVE_COUNT_VALUES,
|
|
23
22
|
DEFAULT_COUNT,
|
|
24
23
|
isValidCount,
|
|
25
24
|
} from './partitive-count.js';
|
|
25
|
+
import { multiplicityFromPair } from './multiplicity.js';
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
// invalid (presence, count) combination rejected at construction.
|
|
29
|
-
const INVALID_PRESENCE_COUNT_COMBO = {
|
|
30
|
-
presence: PARTITIVE_PRESENCE.OPTIONAL,
|
|
31
|
-
count: PARTITIVE_COUNT.AT_LEAST_ONE,
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
export class ConceptSystemMember extends GlossaristModel {
|
|
27
|
+
export class HyperedgeMember extends GlossaristModel {
|
|
35
28
|
constructor(data = {}) {
|
|
36
29
|
super();
|
|
37
30
|
this.ref = _ensureRef(data.ref);
|
|
@@ -47,7 +40,7 @@ export class ConceptSystemMember extends GlossaristModel {
|
|
|
47
40
|
get isDelimiting() { return this.is_delimiting === true; }
|
|
48
41
|
|
|
49
42
|
identity() {
|
|
50
|
-
return
|
|
43
|
+
return HyperedgeMember.identityOf(this);
|
|
51
44
|
}
|
|
52
45
|
|
|
53
46
|
toJSON() {
|
|
@@ -65,7 +58,7 @@ export class ConceptSystemMember extends GlossaristModel {
|
|
|
65
58
|
}
|
|
66
59
|
|
|
67
60
|
static fromJSON(data) {
|
|
68
|
-
return new
|
|
61
|
+
return new HyperedgeMember(data);
|
|
69
62
|
}
|
|
70
63
|
}
|
|
71
64
|
|
|
@@ -73,7 +66,7 @@ function _ensureRef(value) {
|
|
|
73
66
|
const ref = value instanceof ConceptRef ? value : new ConceptRef(value ?? {});
|
|
74
67
|
if (!ref.source && !ref.id && !ref.text) {
|
|
75
68
|
throw new Error(
|
|
76
|
-
'
|
|
69
|
+
'HyperedgeMember.ref must be a non-empty ConceptRef ' +
|
|
77
70
|
'(source, id, or text required)',
|
|
78
71
|
);
|
|
79
72
|
}
|
|
@@ -106,21 +99,16 @@ function _resolveDelimiting(value) {
|
|
|
106
99
|
if (value == null) return false;
|
|
107
100
|
if (typeof value !== 'boolean') {
|
|
108
101
|
throw new Error(
|
|
109
|
-
`
|
|
102
|
+
`HyperedgeMember.is_delimiting must be boolean; got ${typeof value}`,
|
|
110
103
|
);
|
|
111
104
|
}
|
|
112
105
|
return value;
|
|
113
106
|
}
|
|
114
107
|
|
|
115
108
|
function _assertValidPair(presence, count) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
'ConceptSystemMember presence=optional + count=at_least_one is invalid — ' +
|
|
122
|
-
'it collapses to optional + multiple (zero or more). ' +
|
|
123
|
-
'Use presence: optional, count: multiple instead.',
|
|
124
|
-
);
|
|
125
|
-
}
|
|
109
|
+
// Delegate to the multiplicity SSOT (Phase 8 of TODO.abstract-hyperedge).
|
|
110
|
+
// The canonical error string lives in one place: multiplicity.js.
|
|
111
|
+
// Calling multiplicityFromPair throws on the invalid combination;
|
|
112
|
+
// we don't need to duplicate the error message here.
|
|
113
|
+
multiplicityFromPair(presence, count);
|
|
126
114
|
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// HyperedgeRegistry — single source of truth for hyperedge type dispatch.
|
|
2
|
+
//
|
|
3
|
+
// Each hyperedge leaf class (PartitiveHyperedge, GenericHyperedge, future
|
|
4
|
+
// TemporalHyperedge, AssociativeHyperedge, etc.) registers itself once at
|
|
5
|
+
// module load. External systems (parser, serializer, diff, RDF emitter,
|
|
6
|
+
// validators) read from this registry instead of branching on instanceof
|
|
7
|
+
// or hardcoding per-type tables.
|
|
8
|
+
//
|
|
9
|
+
// Adding a new hyperedge type:
|
|
10
|
+
// 1. Declare the leaf class with the 6-field metadata block
|
|
11
|
+
// (wireKey, typeTag, rdfType, memberClass, v1WireKeys, kindLabel).
|
|
12
|
+
// 2. Call HyperedgeRegistry.register(MyClass) at the bottom of the file.
|
|
13
|
+
//
|
|
14
|
+
// Nothing else in the codebase changes. That is the OCP contract.
|
|
15
|
+
//
|
|
16
|
+
// Three indexes support three lookup patterns:
|
|
17
|
+
// byWireKey — parser/serializer/diff-patch dispatch by YAML/JSON key
|
|
18
|
+
// byTypeTag — per-file loader dispatch by `type: ...` discriminator
|
|
19
|
+
// byRdfType — RDF emitter dispatch by ontology class IRI
|
|
20
|
+
//
|
|
21
|
+
// The abstract base (AbstractHyperedge) has no wireKey and is skipped
|
|
22
|
+
// by register() — it cannot be instantiated through the registry.
|
|
23
|
+
|
|
24
|
+
class HyperedgeRegistry {
|
|
25
|
+
static _byWireKey = new Map();
|
|
26
|
+
static _byTypeTag = new Map();
|
|
27
|
+
static _byRdfType = new Map();
|
|
28
|
+
|
|
29
|
+
static register(cls) {
|
|
30
|
+
if (!cls || !cls.wireKey || !cls.typeTag || !cls.rdfType) return false;
|
|
31
|
+
if (this._byWireKey.has(cls.wireKey)) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`HyperedgeRegistry: duplicate wireKey '${cls.wireKey}' ` +
|
|
34
|
+
`(trying to register ${cls.name}; already registered to ` +
|
|
35
|
+
`${this._byWireKey.get(cls.wireKey).name})`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
if (this._byTypeTag.has(cls.typeTag)) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`HyperedgeRegistry: duplicate typeTag '${cls.typeTag}' ` +
|
|
41
|
+
`(trying to register ${cls.name})`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
if (this._byRdfType.has(cls.rdfType)) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`HyperedgeRegistry: duplicate rdfType '${cls.rdfType}' ` +
|
|
47
|
+
`(trying to register ${cls.name})`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
this._byWireKey.set(cls.wireKey, cls);
|
|
51
|
+
this._byTypeTag.set(cls.typeTag, cls);
|
|
52
|
+
this._byRdfType.set(cls.rdfType, cls);
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Test-only. Removes a class from all three indexes. Used by the OCP
|
|
57
|
+
// spec to clean up mock types it adds during a test run.
|
|
58
|
+
static unregister(cls) {
|
|
59
|
+
if (!cls) return false;
|
|
60
|
+
const r1 = this._byWireKey.delete(cls.wireKey);
|
|
61
|
+
const r2 = this._byTypeTag.delete(cls.typeTag);
|
|
62
|
+
const r3 = this._byRdfType.delete(cls.rdfType);
|
|
63
|
+
return r1 || r2 || r3;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
static forWireKey(key) { return this._byWireKey.get(key) ?? null; }
|
|
67
|
+
static forTypeTag(tag) { return this._byTypeTag.get(tag) ?? null; }
|
|
68
|
+
static forRdfType(type) { return this._byRdfType.get(type) ?? null; }
|
|
69
|
+
|
|
70
|
+
static allClasses() {
|
|
71
|
+
// Dedup in case the same class is reachable via multiple indexes.
|
|
72
|
+
const seen = new Set();
|
|
73
|
+
const out = [];
|
|
74
|
+
for (const cls of this._byTypeTag.values()) {
|
|
75
|
+
if (!seen.has(cls)) {
|
|
76
|
+
seen.add(cls);
|
|
77
|
+
out.push(cls);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
static allWireKeys() { return [...this._byWireKey.keys()]; }
|
|
84
|
+
static allTypeTags() { return [...this._byTypeTag.keys()]; }
|
|
85
|
+
|
|
86
|
+
// Snapshot of every registered wire-shape identifier (v2 wireKey plus
|
|
87
|
+
// any v1 legacy keys). Used by the parser to reserve STRUCTURAL_KEYS
|
|
88
|
+
// and to know which top-level YAML keys to walk for hyperedge data.
|
|
89
|
+
static allWireAndLegacyKeys() {
|
|
90
|
+
const out = [];
|
|
91
|
+
for (const cls of this.allClasses()) {
|
|
92
|
+
out.push(cls.wireKey);
|
|
93
|
+
for (const k of cls.v1WireKeys ?? []) out.push(k);
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Helper: partition a unified hyperedge array by wire key. Used by
|
|
100
|
+
// Concept.toJSON, ConceptSerializer, and diff-patch to emit per-type
|
|
101
|
+
// wire keys without per-type branching in those files. Adding a new
|
|
102
|
+
// hyperedge type "just works" because the group is keyed by the
|
|
103
|
+
// class's static wireKey.
|
|
104
|
+
export function groupHyperedgesByWireKey(hyperedges) {
|
|
105
|
+
const out = {};
|
|
106
|
+
for (const h of hyperedges ?? []) {
|
|
107
|
+
const key = h?.constructor?.wireKey;
|
|
108
|
+
if (!key) continue;
|
|
109
|
+
if (!out[key]) out[key] = [];
|
|
110
|
+
out[key].push(h);
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export { HyperedgeRegistry };
|