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/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
|
-
// v2
|
|
35
|
+
// v2 PartitiveHyperedge models (TODO.partitive-relation-v2). These
|
|
37
36
|
// supersede the v1 hyperedge models above; the v1 models are kept for
|
|
38
37
|
// backward-compat input. New code should use v2 exclusively.
|
|
39
|
-
export {
|
|
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,
|
|
@@ -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,6 +1,6 @@
|
|
|
1
|
-
// PartitiveMember — one member of a
|
|
2
|
-
// ISO 704:2022 MECE shape from
|
|
3
|
-
// of its parent
|
|
1
|
+
// PartitiveMember — one member of a PartitiveHyperedge. Inherits the
|
|
2
|
+
// ISO 704:2022 MECE shape from HyperedgeMember; the `comprehensive`
|
|
3
|
+
// of its parent PartitiveHyperedge denotes the whole concept.
|
|
4
4
|
//
|
|
5
5
|
// Distinct leaf class for type safety and to leave room for
|
|
6
6
|
// Partitive-specific extensions.
|
|
@@ -15,10 +15,10 @@
|
|
|
15
15
|
// optional, the at-least-one constraint is vacuous and collapses to
|
|
16
16
|
// (optional, multiple). Rejected at construction.
|
|
17
17
|
|
|
18
|
-
import {
|
|
18
|
+
import { HyperedgeMember } from './hyperedge-member.js';
|
|
19
19
|
|
|
20
|
-
export class PartitiveMember extends
|
|
21
|
-
// Inherits all attributes and validations from
|
|
20
|
+
export class PartitiveMember extends HyperedgeMember {
|
|
21
|
+
// Inherits all attributes and validations from HyperedgeMember.
|
|
22
22
|
// Declared as a distinct class for type safety and to leave room
|
|
23
23
|
// for future partitive-specific extensions.
|
|
24
24
|
}
|
|
@@ -1,32 +1,39 @@
|
|
|
1
|
-
//
|
|
1
|
+
// HyperedgeLoader — runtime loader for per-file hyperedge YAMLs.
|
|
2
2
|
//
|
|
3
3
|
// Walks a `relations/<comprehensive-id>/<slug>.yaml` directory and
|
|
4
|
-
// dispatches each file to the appropriate class (
|
|
5
|
-
//
|
|
4
|
+
// dispatches each file to the appropriate class (PartitiveHyperedge,
|
|
5
|
+
// GenericHyperedge, future leaves) based on its `type` field, via
|
|
6
|
+
// HyperedgeRegistry.
|
|
6
7
|
//
|
|
7
8
|
// The wire-shape contract is owned by the JSON Schema at
|
|
8
9
|
// `schemas/v3/relation.yaml` (concept-model repo) — this loader
|
|
9
10
|
// produces the corresponding JS model instances. See
|
|
10
11
|
// docs/design/relations-as-files.md (concept-model).
|
|
11
12
|
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// Both readers and validators must agree on the type-dispatch
|
|
16
|
-
// table (TYPE_TO_CLASS). Keep them in sync via the JSON Schema.
|
|
13
|
+
// Adding a new hyperedge type means: declare the leaf class with the
|
|
14
|
+
// metadata block, register it. The loader picks it up automatically —
|
|
15
|
+
// no edits here.
|
|
17
16
|
|
|
18
17
|
import { readdirSync, readFileSync, existsSync } from 'node:fs';
|
|
19
18
|
import * as yaml from 'js-yaml';
|
|
20
|
-
import {
|
|
19
|
+
import { HyperedgeRegistry } from './hyperedge-registry.js';
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
21
|
+
// Backward-compat: snapshot of registered classes keyed by typeTag.
|
|
22
|
+
// Prefer HyperedgeRegistry.forTypeTag() for new code — this snapshot
|
|
23
|
+
// is frozen at module load and won't pick up runtime registrations
|
|
24
|
+
// from tests (the OCP test in Phase 11 registers a mock type after
|
|
25
|
+
// import).
|
|
26
|
+
export const TYPE_TO_CLASS = new Proxy({}, {
|
|
27
|
+
get(_target, prop) {
|
|
28
|
+
return HyperedgeRegistry.forTypeTag(prop);
|
|
29
|
+
},
|
|
30
|
+
ownKeys() {
|
|
31
|
+
return HyperedgeRegistry.allTypeTags();
|
|
32
|
+
},
|
|
33
|
+
getOwnPropertyDescriptor() {
|
|
34
|
+
return { enumerable: true, configurable: true };
|
|
35
|
+
},
|
|
36
|
+
});
|
|
30
37
|
|
|
31
38
|
/**
|
|
32
39
|
* Thrown by `loadAll` when a relation file has an unknown `type`,
|
|
@@ -48,10 +55,9 @@ export class RelationLoadError extends Error {
|
|
|
48
55
|
* @param {string} relationsDir — path to a `relations/` directory
|
|
49
56
|
* containing `<comprehensive-id>/<slug>.yaml` files (per the
|
|
50
57
|
* schema in schemas/v3/relation.yaml).
|
|
51
|
-
* @returns {Map<string, Array<
|
|
58
|
+
* @returns {Map<string, Array<AbstractHyperedge>>} — for each
|
|
52
59
|
* comprehensive id, the list of relations that have it as their
|
|
53
|
-
* comprehensive.
|
|
54
|
-
* GenericRelation).
|
|
60
|
+
* comprehensive.
|
|
55
61
|
*/
|
|
56
62
|
export function loadAll(relationsDir) {
|
|
57
63
|
const result = new Map();
|
|
@@ -82,7 +88,7 @@ export function loadAll(relationsDir) {
|
|
|
82
88
|
* `RelationLoader.loadAll(dir)`. The individual functions are also
|
|
83
89
|
* exported directly for tree-shakeable single-call sites.
|
|
84
90
|
*/
|
|
85
|
-
export const RelationLoader = Object.freeze({ loadAll });
|
|
91
|
+
export const RelationLoader = Object.freeze({ loadAll, loadOne });
|
|
86
92
|
|
|
87
93
|
/**
|
|
88
94
|
* Load a single relation file. Returns null if the file is missing,
|
|
@@ -99,7 +105,7 @@ export function loadOne(path) {
|
|
|
99
105
|
}
|
|
100
106
|
if (!doc || typeof doc !== 'object') return null;
|
|
101
107
|
if (!doc.type) return null;
|
|
102
|
-
const Cls =
|
|
108
|
+
const Cls = HyperedgeRegistry.forTypeTag(doc.type);
|
|
103
109
|
if (!Cls) return null; // Unknown type — let the validator report it
|
|
104
110
|
return Cls.fromJSON(doc);
|
|
105
111
|
}
|
package/src/rdf/gloss-concept.js
CHANGED
|
@@ -8,9 +8,29 @@ import { WELL_KNOWN } from './prefixes.js';
|
|
|
8
8
|
import { localizedConceptToQuads } from './gloss-localized-concept.js';
|
|
9
9
|
import { conceptSourceToQuads } from './gloss-source.js';
|
|
10
10
|
import { partitiveRelationToQuads, partitiveRelationSubjectUri } from './gloss-partitive-relation.js';
|
|
11
|
-
import {
|
|
11
|
+
import { genericHyperedgeToQuads, genericHyperedgeSubjectUri, GENERIC_HYPEREDGE_LINK_PREDICATE } from './gloss-generic-hyperedge.js';
|
|
12
|
+
import { PartitiveHyperedge } from '../models/partitive-hyperedge.js';
|
|
13
|
+
import { GenericHyperedge } from '../models/generic-hyperedge.js';
|
|
12
14
|
import { namedNode, literal, quad } from './terms.js';
|
|
13
15
|
|
|
16
|
+
// Per-type RDF emitter dispatch. Keyed by class. Adding a new hyperedge
|
|
17
|
+
// type with its own emitter means: (1) write the emitter module,
|
|
18
|
+
// (2) add one entry here. Per TODO Phase 6e this should be a
|
|
19
|
+
// self-registering registry; for the two current types, the dispatch
|
|
20
|
+
// table is simpler and keeps the dependency graph acyclic.
|
|
21
|
+
const HYPEREDGE_EMITTERS = new Map([
|
|
22
|
+
[PartitiveHyperedge, {
|
|
23
|
+
linkPredicate: PRED.gloss.hasPartitiveRelation,
|
|
24
|
+
subjectUri: partitiveRelationSubjectUri,
|
|
25
|
+
toQuads: partitiveRelationToQuads,
|
|
26
|
+
}],
|
|
27
|
+
[GenericHyperedge, {
|
|
28
|
+
linkPredicate: GENERIC_HYPEREDGE_LINK_PREDICATE,
|
|
29
|
+
subjectUri: genericHyperedgeSubjectUri,
|
|
30
|
+
toQuads: genericHyperedgeToQuads,
|
|
31
|
+
}],
|
|
32
|
+
]);
|
|
33
|
+
|
|
14
34
|
// Kind tag → URI segment. Each NonVerbalReference subtype has a
|
|
15
35
|
// predictable URL path so the same entity is reachable from any
|
|
16
36
|
// concept that references it.
|
|
@@ -72,27 +92,23 @@ export function* conceptToQuads(concept, options) {
|
|
|
72
92
|
srcIndex += 1;
|
|
73
93
|
}
|
|
74
94
|
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
// work here.
|
|
78
|
-
const relations = concept.partitiveRelations ?? concept.partitiveHyperedges ?? [];
|
|
95
|
+
// Iterate the unified relations array. Dispatch via per-type
|
|
96
|
+
// emitter table.
|
|
79
97
|
let relIdx = 0;
|
|
80
|
-
for (const relation of relations) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
yield* hyperedgeToQuads(relation, { parentUri: subjectUri, index: relIdx });
|
|
91
|
-
} else {
|
|
92
|
-
const relSubject = partitiveRelationSubjectUri(subjectUri, relation, relIdx);
|
|
93
|
-
yield quad(s, namedNode(PRED.gloss.hasPartitiveRelation), namedNode(relSubject));
|
|
94
|
-
yield* partitiveRelationToQuads(relation, { parentUri: subjectUri, index: relIdx });
|
|
98
|
+
for (const relation of concept.relations ?? []) {
|
|
99
|
+
const emitter = HYPEREDGE_EMITTERS.get(relation.constructor);
|
|
100
|
+
if (!emitter) {
|
|
101
|
+
// Unknown hyperedge type — skip rather than crash. The OCP
|
|
102
|
+
// contract (Phase 11) tests that new types register an emitter;
|
|
103
|
+
// skipping here gives external consumers a soft-failure path
|
|
104
|
+
// when they pass around abstract hyperedges the JS layer doesn't
|
|
105
|
+
// recognize yet.
|
|
106
|
+
relIdx += 1;
|
|
107
|
+
continue;
|
|
95
108
|
}
|
|
109
|
+
const relSubject = emitter.subjectUri(subjectUri, relation, relIdx);
|
|
110
|
+
yield quad(s, namedNode(emitter.linkPredicate), namedNode(relSubject));
|
|
111
|
+
yield* emitter.toQuads(relation, { parentUri: subjectUri, index: relIdx });
|
|
96
112
|
relIdx += 1;
|
|
97
113
|
}
|
|
98
114
|
}
|