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
|
@@ -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 };
|
package/src/models/index.d.ts
CHANGED
|
@@ -328,11 +328,11 @@ export class PartitiveHyperedge extends GlossaristModel {
|
|
|
328
328
|
static fromJSON(data: Record<string, unknown>): PartitiveHyperedge;
|
|
329
329
|
}
|
|
330
330
|
|
|
331
|
-
// v2
|
|
331
|
+
// v2 PartitiveHyperedge + GenericHyperedge (MECE per-member model).
|
|
332
332
|
// These supersede the v1 PartitiveHyperedge model above. New code
|
|
333
333
|
// should use v2 exclusively; the v1 model is kept for backward-compat
|
|
334
334
|
// input.
|
|
335
|
-
export class
|
|
335
|
+
export class HyperedgeMember extends GlossaristModel {
|
|
336
336
|
readonly ref: ConceptRef;
|
|
337
337
|
readonly presence: 'required' | 'optional';
|
|
338
338
|
readonly count: 'exactly_one' | 'at_least_one' | 'multiple';
|
|
@@ -342,17 +342,17 @@ export class ConceptSystemMember extends GlossaristModel {
|
|
|
342
342
|
delimiting(): boolean;
|
|
343
343
|
identity(): string;
|
|
344
344
|
toJSON(): Record<string, unknown>;
|
|
345
|
-
static fromJSON(data: Record<string, unknown>):
|
|
345
|
+
static fromJSON(data: Record<string, unknown>): HyperedgeMember;
|
|
346
346
|
static identityOf(value: unknown): string;
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
-
export class PartitiveMember extends
|
|
349
|
+
export class PartitiveMember extends HyperedgeMember {}
|
|
350
350
|
|
|
351
|
-
export class GenericMember extends
|
|
351
|
+
export class GenericMember extends HyperedgeMember {}
|
|
352
352
|
|
|
353
|
-
export class
|
|
353
|
+
export class AbstractHyperedge extends GlossaristModel {
|
|
354
354
|
readonly comprehensive: ConceptRef;
|
|
355
|
-
readonly members:
|
|
355
|
+
readonly members: HyperedgeMember[];
|
|
356
356
|
readonly completeness: 'complete' | 'partial';
|
|
357
357
|
readonly criterion: Record<string, string> | null;
|
|
358
358
|
readonly sources: ConceptSource[];
|
|
@@ -362,19 +362,19 @@ export class AbstractNaryRelation extends GlossaristModel {
|
|
|
362
362
|
readonly isPartial: boolean;
|
|
363
363
|
readonly isCoordinate: boolean;
|
|
364
364
|
toJSON(): Record<string, unknown>;
|
|
365
|
-
static fromJSON(data: Record<string, unknown>):
|
|
365
|
+
static fromJSON(data: Record<string, unknown>): AbstractHyperedge;
|
|
366
366
|
static identityOf(value: unknown): string;
|
|
367
367
|
}
|
|
368
368
|
|
|
369
|
-
export class
|
|
369
|
+
export class PartitiveHyperedge extends AbstractHyperedge {
|
|
370
370
|
/** @deprecated use {@link members} — kept for v1/v2 wire compat. */
|
|
371
|
-
readonly partitives:
|
|
372
|
-
static fromJSON(data: Record<string, unknown>):
|
|
371
|
+
readonly partitives: HyperedgeMember[];
|
|
372
|
+
static fromJSON(data: Record<string, unknown>): PartitiveHyperedge;
|
|
373
373
|
static identityOf(value: unknown): string;
|
|
374
374
|
}
|
|
375
375
|
|
|
376
|
-
export class
|
|
377
|
-
static fromJSON(data: Record<string, unknown>):
|
|
376
|
+
export class GenericHyperedge extends AbstractHyperedge {
|
|
377
|
+
static fromJSON(data: Record<string, unknown>): GenericHyperedge;
|
|
378
378
|
static identityOf(value: unknown): string;
|
|
379
379
|
}
|
|
380
380
|
|
|
@@ -383,14 +383,14 @@ export const DEFINITION_TYPE: Readonly<Record<string, DefinitionType>>;
|
|
|
383
383
|
export const DEFAULT_DEFINITION_TYPE: DefinitionType;
|
|
384
384
|
export const DEFINITION_TYPE_VALUES: readonly DefinitionType[];
|
|
385
385
|
|
|
386
|
-
export const RELATION_TYPE_TO_CLASS: Readonly<Record<string, typeof
|
|
386
|
+
export const RELATION_TYPE_TO_CLASS: Readonly<Record<string, typeof AbstractHyperedge>>;
|
|
387
387
|
export class RelationLoadError extends Error {
|
|
388
388
|
readonly path: string;
|
|
389
389
|
constructor(path: string, message: string);
|
|
390
390
|
}
|
|
391
391
|
export const RelationLoader: {
|
|
392
|
-
loadAll(relationsDir: string): Map<string,
|
|
393
|
-
loadOne(path: string):
|
|
392
|
+
loadAll(relationsDir: string): Map<string, AbstractHyperedge[]>;
|
|
393
|
+
loadOne(path: string): AbstractHyperedge | null;
|
|
394
394
|
};
|
|
395
395
|
|
|
396
396
|
export const DESIGNATION_RELATIONSHIP_TYPES: readonly string[];
|
package/src/models/index.js
CHANGED
|
@@ -21,7 +21,6 @@ export { Citation } from './citation.js';
|
|
|
21
21
|
export { ConceptRef } from './concept-ref.js';
|
|
22
22
|
export { ConceptSource } from './concept-source.js';
|
|
23
23
|
export { RelatedConcept, RELATIONSHIP_TYPES } from './related-concept.js';
|
|
24
|
-
export { PartitiveHyperedge } from './partitive-hyperedge.js';
|
|
25
24
|
export {
|
|
26
25
|
PARTITIVE_ENUMERATION,
|
|
27
26
|
PARTITIVE_ENUMERATION_VALUES,
|
|
@@ -33,17 +32,17 @@ export {
|
|
|
33
32
|
isValidPluralityMarker,
|
|
34
33
|
} from './plurality-marker.js';
|
|
35
34
|
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
export {
|
|
35
|
+
// PartitiveHyperedge models. The wire format (partitive_relations)
|
|
36
|
+
// is unchanged from earlier versions; the v1 wire key (partitive_hyperedges)
|
|
37
|
+
// is parsed on input and migrated to v2 shape via hyperedge-migrator.js.
|
|
38
|
+
export { PartitiveHyperedge } from './partitive-hyperedge.js';
|
|
40
39
|
export { PartitiveMember } from './partitive-member.js';
|
|
41
40
|
|
|
42
|
-
// v2
|
|
43
|
-
//
|
|
44
|
-
export {
|
|
45
|
-
export {
|
|
46
|
-
export {
|
|
41
|
+
// v2 GenericHyperedge models (per-file relation storage). Mirrors
|
|
42
|
+
// PartitiveHyperedge shape; uses the same MECE member shape.
|
|
43
|
+
export { AbstractHyperedge } from './abstract-hyperedge.js';
|
|
44
|
+
export { HyperedgeMember } from './hyperedge-member.js';
|
|
45
|
+
export { GenericHyperedge } from './generic-hyperedge.js';
|
|
47
46
|
export { GenericMember } from './generic-member.js';
|
|
48
47
|
export {
|
|
49
48
|
DEFINITION_TYPE,
|
|
@@ -51,9 +50,13 @@ export {
|
|
|
51
50
|
} from './definition-type.js';
|
|
52
51
|
|
|
53
52
|
// Per-file relation loader (Node-side runtime; not browser-bundled).
|
|
54
|
-
// Walks a relations/ directory and dispatches to
|
|
55
|
-
//
|
|
53
|
+
// Walks a relations/ directory and dispatches to PartitiveHyperedge /
|
|
54
|
+
// GenericHyperedge by file type.
|
|
56
55
|
export { RelationLoader, RelationLoadError, TYPE_TO_CLASS as RELATION_TYPE_TO_CLASS } from './relation-loader.js';
|
|
56
|
+
|
|
57
|
+
// Directional index over hyperedge sets. Built on demand from a flat
|
|
58
|
+
// hyperedge list (typically concept.relations or a dataset union).
|
|
59
|
+
export { HyperedgeIndex, buildDatasetIndex } from './hyperedge-index.js';
|
|
57
60
|
export {
|
|
58
61
|
PARTITIVE_PRESENCE,
|
|
59
62
|
PARTITIVE_PRESENCE_VALUES,
|
|
@@ -86,7 +89,7 @@ export {
|
|
|
86
89
|
export {
|
|
87
90
|
migrateHyperedgeToRelation,
|
|
88
91
|
downgradeRelationToHyperedge,
|
|
89
|
-
} from '../migration/
|
|
92
|
+
} from '../migration/hyperedge-migrator.js';
|
|
90
93
|
|
|
91
94
|
export { makeEnum } from './enum.js';
|
|
92
95
|
export { resolveHyperedgeColor } from './hyperedge-colors.js';
|
|
@@ -53,6 +53,17 @@ export function multiplicityFromPair(presence, count) {
|
|
|
53
53
|
return name;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
// Canonical error message for the invalid (presence, count) combination.
|
|
57
|
+
// Single source of truth — hyperedge-member.js delegates to this so the
|
|
58
|
+
// error string doesn't drift across the two files. Phase 8 of
|
|
59
|
+
// TODO.abstract-hyperedge.
|
|
60
|
+
export function invalidCombinationError(presence, count) {
|
|
61
|
+
return new Error(
|
|
62
|
+
`Invalid multiplicity combination: presence=${presence}, count=${count}. ` +
|
|
63
|
+
`(optional + at_least_one collapses to optional + multiple — use count=multiple.)`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
56
67
|
export function pairFromMultiplicity(name) {
|
|
57
68
|
const pair = PAIR_BY_NAME[name];
|
|
58
69
|
if (pair == null) {
|
|
@@ -92,9 +103,3 @@ export function resolveMultiplicity(member) {
|
|
|
92
103
|
return DEFAULT_MULTIPLICITY;
|
|
93
104
|
}
|
|
94
105
|
|
|
95
|
-
function invalidCombinationError(presence, count) {
|
|
96
|
-
return new Error(
|
|
97
|
-
`Invalid multiplicity combination: presence=${presence}, count=${count}. ` +
|
|
98
|
-
`(optional + at_least_one collapses to optional + multiple — use count=multiple.)`,
|
|
99
|
-
);
|
|
100
|
-
}
|
|
@@ -1,175 +1,91 @@
|
|
|
1
|
-
// PartitiveHyperedge —
|
|
1
|
+
// PartitiveHyperedge — an ISO 704:2022 / ISO 1087-1 / ISO 12620
|
|
2
|
+
// partitive relation connecting a comprehensive concept
|
|
3
|
+
// (superordinate concept partitive) to two or more partitive
|
|
4
|
+
// concepts (subordinate concepts partitive) which fitted together
|
|
5
|
+
// constitute the comprehensive.
|
|
2
6
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
7
|
+
// Extends AbstractHyperedge; the leaf-specific concern is the
|
|
8
|
+
// PartitiveMember type for `members`. The wire-level field name
|
|
9
|
+
// stays `partitives` for v1/v2 backward compat.
|
|
6
10
|
//
|
|
7
|
-
//
|
|
8
|
-
// - plurality markers from the source diagram (double / dashed)
|
|
9
|
-
// - enumeration completeness (closed: all parts listed; open:
|
|
10
|
-
// other parts may exist)
|
|
11
|
+
// ISO 704:2022 rake diagram semantics:
|
|
11
12
|
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
13
|
+
// comprehensive concept
|
|
14
|
+
// ─────────────────────
|
|
15
|
+
// │ partitive (one or more) with per-member multiplicity + optional delimiting
|
|
16
|
+
// ─────────────────────
|
|
14
17
|
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
18
|
+
// All partitives within one relation are coordinate concepts
|
|
19
|
+
// (ISO 12620): they share the comprehensive AND share the
|
|
20
|
+
// criterion of subdivision.
|
|
21
|
+
//
|
|
22
|
+
// "Delimiting" parts (bold/3x-width line in source diagrams) behave
|
|
23
|
+
// like delimiting characteristics — they distinguish the comprehensive
|
|
24
|
+
// from coordinate concepts. Whether a part is delimiting depends on
|
|
25
|
+
// the concept system, the coordinate concepts, the inheritance
|
|
26
|
+
// principle, and the criterion of subdivision used
|
|
27
|
+
// (ISO 704:2022 §5.5.4.2.1).
|
|
28
|
+
//
|
|
29
|
+
// Distinguished from binary `has_part` / `is_part_of` edges, which
|
|
30
|
+
// express pairwise part-of assertions without completeness,
|
|
31
|
+
// multiplicity, or criterion claims. A single partitive should be
|
|
32
|
+
// expressed as a binary edge, not a PartitiveHyperedge (ISO requires
|
|
33
|
+
// "two or more").
|
|
34
|
+
|
|
35
|
+
import { AbstractHyperedge } from './abstract-hyperedge.js';
|
|
36
|
+
import { PartitiveMember } from './partitive-member.js';
|
|
37
|
+
import { HyperedgeRegistry } from './hyperedge-registry.js';
|
|
38
|
+
|
|
39
|
+
export class PartitiveHyperedge extends AbstractHyperedge {
|
|
40
|
+
// Per-class metadata block — the OCP contract. External systems
|
|
41
|
+
// (parser, serializer, diff, RDF emitter, validators) read from
|
|
42
|
+
// these statics and the HyperedgeRegistry. Adding a new hyperedge
|
|
43
|
+
// type = declaring these fields on a new subclass + registering
|
|
44
|
+
// it. Nothing else in the codebase changes.
|
|
45
|
+
static wireKey = 'partitive_relations';
|
|
46
|
+
static typeTag = 'partitive_relation';
|
|
47
|
+
static rdfType = 'gloss:PartitiveRelation';
|
|
48
|
+
static memberClass = PartitiveMember;
|
|
49
|
+
static v1WireKeys = ['partitive_hyperedges'];
|
|
50
|
+
static kindLabel = 'PART';
|
|
33
51
|
|
|
34
|
-
export class PartitiveHyperedge extends GlossaristModel {
|
|
35
52
|
constructor(data = {}) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
this.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
assertNoSelfLoop(this.comprehensive, this.parts);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
get isClosed() { return this.enumeration === PARTITIVE_ENUMERATION.CLOSED; }
|
|
48
|
-
get isOpen() { return this.enumeration === PARTITIVE_ENUMERATION.OPEN; }
|
|
49
|
-
get isMarked() { return this.markers.length > 0; }
|
|
50
|
-
|
|
51
|
-
hasMarker(value) {
|
|
52
|
-
return this.markers.includes(value);
|
|
53
|
+
const members = data?.members ?? data?.partitives;
|
|
54
|
+
super({ ...data, members });
|
|
55
|
+
this.members = members == null
|
|
56
|
+
? this.members
|
|
57
|
+
: (Array.isArray(members)
|
|
58
|
+
? members.map(m =>
|
|
59
|
+
m instanceof PartitiveMember ? m : new PartitiveMember(m))
|
|
60
|
+
: this.members);
|
|
53
61
|
}
|
|
54
62
|
|
|
55
|
-
|
|
56
|
-
return this.enumeration === value;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
get contentString() {
|
|
60
|
-
if (!this.content) return null;
|
|
61
|
-
const values = Object.values(this.content);
|
|
62
|
-
return values.length > 0 ? values[0] : null;
|
|
63
|
-
}
|
|
63
|
+
get partitives() { return this.members; }
|
|
64
64
|
|
|
65
65
|
toJSON() {
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
enumeration: this.enumeration,
|
|
70
|
-
};
|
|
71
|
-
if (this.markers.length > 0) obj.markers = [...this.markers];
|
|
72
|
-
if (this.content != null) obj.content = this.content;
|
|
73
|
-
return obj;
|
|
66
|
+
const base = super.toJSON();
|
|
67
|
+
const { members, ...rest } = base;
|
|
68
|
+
return { ...rest, partitives: members };
|
|
74
69
|
}
|
|
75
70
|
|
|
76
71
|
static identityOf(value) {
|
|
77
72
|
const v = value ?? {};
|
|
78
73
|
const c = v.comprehensive ?? {};
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
identity() {
|
|
87
|
-
return PartitiveHyperedge.identityOf(this);
|
|
74
|
+
const partKeys = Array.isArray(v.partitives ?? v.members)
|
|
75
|
+
? (v.partitives ?? v.members).map(p => {
|
|
76
|
+
const r = p?.ref ?? p ?? {};
|
|
77
|
+
return `${r.source ?? ''}:${r.id ?? ''}`;
|
|
78
|
+
}).sort()
|
|
79
|
+
: [];
|
|
80
|
+
return `${c.source ?? ''}:${c.id ?? ''}|${partKeys.join('|')}`;
|
|
88
81
|
}
|
|
89
82
|
|
|
90
83
|
static fromJSON(data) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function resolveEnumeration(value) {
|
|
96
|
-
if (value == null) return DEFAULT_ENUMERATION;
|
|
97
|
-
if (!isValidPartitiveEnumeration(value)) {
|
|
98
|
-
throw new Error(
|
|
99
|
-
`invalid enumeration ${JSON.stringify(value)}; ` +
|
|
100
|
-
`must be one of ${PARTITIVE_ENUMERATION_VALUES.join(', ')}`,
|
|
101
|
-
);
|
|
84
|
+
const normalized = { ...(data ?? {}), members: data?.partitives ?? data?.members };
|
|
85
|
+
return new PartitiveHyperedge(normalized);
|
|
102
86
|
}
|
|
103
|
-
return value;
|
|
104
87
|
}
|
|
105
88
|
|
|
106
|
-
|
|
107
|
-
const arr = Array.isArray(values) ? values : [values];
|
|
108
|
-
const seen = new Set();
|
|
109
|
-
for (const m of arr) {
|
|
110
|
-
if (!isValidPluralityMarker(m)) {
|
|
111
|
-
throw new Error(
|
|
112
|
-
`invalid plurality marker ${JSON.stringify(m)}; ` +
|
|
113
|
-
`must be one of ${PLURALITY_MARKER_VALUES.join(', ')}`,
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
if (seen.has(m)) {
|
|
117
|
-
throw new Error(
|
|
118
|
-
`duplicate plurality marker ${JSON.stringify(m)} in partitive_hyperedge`,
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
seen.add(m);
|
|
122
|
-
}
|
|
123
|
-
return arr;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function ensureConceptRef(value, fieldName) {
|
|
127
|
-
const ref = value instanceof ConceptRef ? value : new ConceptRef(value ?? {});
|
|
128
|
-
if (!ref.source && !ref.id) {
|
|
129
|
-
throw new Error(
|
|
130
|
-
`PartitiveHyperedge#${fieldName} must be a non-empty ConceptRef ` +
|
|
131
|
-
`(source or id required)`,
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
return ref;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function ensureConceptRefArray(values, fieldName) {
|
|
138
|
-
if (!Array.isArray(values)) values = values == null ? [] : [values];
|
|
139
|
-
return values.map((v, i) => ensureConceptRef(v, `${fieldName}[${i}]`));
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function assertNonEmptyParts(parts) {
|
|
143
|
-
if (!parts || parts.length === 0) {
|
|
144
|
-
throw new Error('PartitiveHyperedge requires at least one part');
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function assertNoSelfLoop(comprehensive, parts) {
|
|
149
|
-
const cKey = `${comprehensive.source ?? ''}:${comprehensive.id ?? ''}`;
|
|
150
|
-
for (const p of parts) {
|
|
151
|
-
const pKey = `${p.source ?? ''}:${p.id ?? ''}`;
|
|
152
|
-
if (pKey === cKey) {
|
|
153
|
-
throw new Error('PartitiveHyperedge#parts cannot include the comprehensive');
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
89
|
+
export default PartitiveHyperedge;
|
|
157
90
|
|
|
158
|
-
|
|
159
|
-
// input are normalized to `{ default: '...' }` so callers always see
|
|
160
|
-
// the same shape. `null` is preserved.
|
|
161
|
-
//
|
|
162
|
-
// Cross-repo symmetry: glossarist-ruby's `PartitiveHyperedge#content=`
|
|
163
|
-
// (lib/glossarist/v3/partitive_hyperedge.rb) applies the same
|
|
164
|
-
// normalization since commit c917846 (was previously a plain string;
|
|
165
|
-
// aligned with invariant I7 in TODO.hyperedges round 1, item 29).
|
|
166
|
-
// Datasets now round-trip identically across repos.
|
|
167
|
-
function normalizeContent(value) {
|
|
168
|
-
if (value == null) return null;
|
|
169
|
-
if (typeof value === 'string') return { default: value };
|
|
170
|
-
if (typeof value === 'object') {
|
|
171
|
-
const entries = Object.entries(value).filter(([, v]) => typeof v === 'string');
|
|
172
|
-
return entries.length > 0 ? Object.fromEntries(entries) : null;
|
|
173
|
-
}
|
|
174
|
-
return null;
|
|
175
|
-
}
|
|
91
|
+
HyperedgeRegistry.register(PartitiveHyperedge);
|
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
// PartitiveMember — one member of a
|
|
2
|
-
// ISO 704:2022 MECE shape from ConceptSystemMember; the `comprehensive`
|
|
3
|
-
// of its parent PartitiveRelation denotes the whole concept.
|
|
1
|
+
// PartitiveMember — one member of a PartitiveHyperedge.
|
|
4
2
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// Adds to HyperedgeMember the partitive-specific `is_delimiting` flag
|
|
4
|
+
// from ISO 704:2022 §5.5.4.2.2 (rake diagram semantics):
|
|
5
|
+
//
|
|
6
|
+
// "Delimiting" parts (bold/3x-width line in source diagrams) behave
|
|
7
|
+
// like delimiting characteristics — they distinguish the comprehensive
|
|
8
|
+
// from coordinate concepts. Whether a part is delimiting depends on
|
|
9
|
+
// the concept system, the coordinate concepts, the inheritance
|
|
10
|
+
// principle, and the criterion of subdivision used.
|
|
11
|
+
//
|
|
12
|
+
// `is_delimiting` lives here on PartitiveMember, not on the base
|
|
13
|
+
// HyperedgeMember, because it is partitive-specific. GenericMember
|
|
14
|
+
// carries `delimitingCharacteristic` instead (the intension-difference
|
|
15
|
+
// value per §5.5.4.2.1, which always applies to every generic member).
|
|
7
16
|
//
|
|
8
17
|
// Matches concept-model/models/concepts/PartitiveMember.lutaml exactly:
|
|
9
18
|
// +ref — ConceptRef [1]
|
|
@@ -13,14 +22,34 @@
|
|
|
13
22
|
//
|
|
14
23
|
// The combination (optional, at_least_one) is invalid: if a part is
|
|
15
24
|
// optional, the at-least-one constraint is vacuous and collapses to
|
|
16
|
-
// (optional, multiple). Rejected at construction
|
|
25
|
+
// (optional, multiple). Rejected at construction by HyperedgeMember's
|
|
26
|
+
// delegate to multiplicityFromPair.
|
|
27
|
+
|
|
28
|
+
import { HyperedgeMember } from './hyperedge-member.js';
|
|
17
29
|
|
|
18
|
-
|
|
30
|
+
export class PartitiveMember extends HyperedgeMember {
|
|
31
|
+
constructor(data = {}) {
|
|
32
|
+
super(data);
|
|
33
|
+
this.is_delimiting = _resolveDelimiting(data.is_delimiting);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get isDelimiting() { return this.is_delimiting === true; }
|
|
37
|
+
|
|
38
|
+
toJSON() {
|
|
39
|
+
const base = super.toJSON();
|
|
40
|
+
if (this.is_delimiting) base.is_delimiting = true;
|
|
41
|
+
return base;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
19
44
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
45
|
+
function _resolveDelimiting(value) {
|
|
46
|
+
if (value == null) return false;
|
|
47
|
+
if (typeof value !== 'boolean') {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`PartitiveMember.is_delimiting must be boolean; got ${typeof value}`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return value;
|
|
24
53
|
}
|
|
25
54
|
|
|
26
55
|
export default PartitiveMember;
|