glossarist 0.4.28 → 0.4.29
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/models/abstract-nary-relation.js +119 -0
- package/src/models/concept-system-member.js +126 -0
- package/src/models/definition-type.js +17 -0
- package/src/models/generic-member.js +15 -0
- package/src/models/generic-relation.js +43 -0
- package/src/models/index.d.ts +65 -0
- package/src/models/index.js +16 -0
- package/src/models/partitive-member.js +11 -90
- package/src/models/partitive-relation.js +24 -111
- package/src/models/relation-loader.js +107 -0
package/package.json
CHANGED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// AbstractNaryRelation — shared base for n-ary concept-system
|
|
2
|
+
// relations (PartitiveRelation, GenericRelation, future
|
|
3
|
+
// SequentialRelation, AssociativeRelation).
|
|
4
|
+
//
|
|
5
|
+
// Carries the common shape:
|
|
6
|
+
// - comprehensive: ConceptRef
|
|
7
|
+
// - members: 2+ MECE dimensioned concept refs (≥2 per ISO 704)
|
|
8
|
+
// - completeness: complete | partial
|
|
9
|
+
// - criterion: localized subdivision criterion (ISO 12620)
|
|
10
|
+
// - sources / notes / status: administrative metadata
|
|
11
|
+
//
|
|
12
|
+
// Concrete leaves add type-specific fields if needed (currently none).
|
|
13
|
+
// See docs/design/abstract-nary-relation.md (concept-model repo).
|
|
14
|
+
|
|
15
|
+
import { GlossaristModel } from './base.js';
|
|
16
|
+
import { ConceptRef } from './concept-ref.js';
|
|
17
|
+
import {
|
|
18
|
+
COMPLETENESS,
|
|
19
|
+
DEFAULT_COMPLETENESS,
|
|
20
|
+
isValidCompleteness,
|
|
21
|
+
} from './completeness.js';
|
|
22
|
+
|
|
23
|
+
export class AbstractNaryRelation extends GlossaristModel {
|
|
24
|
+
constructor(data = {}) {
|
|
25
|
+
super();
|
|
26
|
+
this.comprehensive = _ensureComprehensive(data.comprehensive);
|
|
27
|
+
this.members = _ensureMembers(data.members, this.constructor.name);
|
|
28
|
+
this.completeness = _resolveCompleteness(data.completeness);
|
|
29
|
+
this.criterion = _normalizeCriterion(data.criterion);
|
|
30
|
+
this.sources = Array.isArray(data.sources) ? [...data.sources] : [];
|
|
31
|
+
this.notes = data.notes ?? null;
|
|
32
|
+
this.status = data.status ?? null;
|
|
33
|
+
|
|
34
|
+
_assertNoSelfLoop(this.comprehensive, this.members);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get isComplete() { return this.completeness === COMPLETENESS.COMPLETE; }
|
|
38
|
+
get isPartial() { return this.completeness === COMPLETENESS.PARTIAL; }
|
|
39
|
+
get isCoordinate() { return this.members.length >= 2; }
|
|
40
|
+
|
|
41
|
+
hasCriterion() { return this.criterion != null; }
|
|
42
|
+
|
|
43
|
+
identity() {
|
|
44
|
+
return this.constructor.identityOf(this);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
toJSON() {
|
|
48
|
+
const obj = {
|
|
49
|
+
comprehensive: this.comprehensive.toJSON(),
|
|
50
|
+
members: this.members.map(m => m.toJSON()),
|
|
51
|
+
completeness: this.completeness,
|
|
52
|
+
};
|
|
53
|
+
if (this.criterion != null) obj.criterion = { ...this.criterion };
|
|
54
|
+
if (this.sources.length > 0) obj.sources = [...this.sources];
|
|
55
|
+
if (this.notes != null) obj.notes = { ...this.notes };
|
|
56
|
+
if (this.status != null) obj.status = this.status;
|
|
57
|
+
return obj;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function _ensureComprehensive(value) {
|
|
62
|
+
const ref = value instanceof ConceptRef ? value : new ConceptRef(value ?? {});
|
|
63
|
+
if (!ref.source && !ref.id && !ref.text) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
'AbstractNaryRelation.comprehensive must be a non-empty ConceptReference ' +
|
|
66
|
+
'(source, id, or text required)',
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return ref;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function _ensureMembers(values, typeName) {
|
|
73
|
+
if (!Array.isArray(values)) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`${typeName}.members must be an array (got ${typeof values})`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
if (values.length < 2) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`${typeName} requires ≥2 partitives (ISO 704); got ${values.length}`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
return values;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function _resolveCompleteness(value) {
|
|
87
|
+
const c = value ?? DEFAULT_COMPLETENESS;
|
|
88
|
+
if (!isValidCompleteness(c)) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`${c === value ? 'completeness' : 'completeness'} has invalid value ${JSON.stringify(value)}`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return c;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function _normalizeCriterion(value) {
|
|
97
|
+
if (value == null) return null;
|
|
98
|
+
if (typeof value === 'string') {
|
|
99
|
+
return value.length > 0 ? { default: value } : null;
|
|
100
|
+
}
|
|
101
|
+
if (typeof value === 'object') {
|
|
102
|
+
const entries = Object.entries(value).filter(([, v]) => typeof v === 'string');
|
|
103
|
+
return entries.length > 0 ? Object.fromEntries(entries) : null;
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function _assertNoSelfLoop(comprehensive, members) {
|
|
109
|
+
const cKey = `${comprehensive.source ?? ''}:${comprehensive.id ?? ''}`;
|
|
110
|
+
for (const m of members) {
|
|
111
|
+
const ref = m.ref;
|
|
112
|
+
const pKey = `${ref?.source ?? ''}:${ref?.id ?? ''}`;
|
|
113
|
+
if (pKey === cKey && (ref?.source || ref?.id)) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
'Relation.members cannot include the comprehensive (no self-loops)',
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// ConceptSystemMember — abstract base shape for members of any
|
|
2
|
+
// n-ary concept-system relation (PartitiveMember, GenericMember,
|
|
3
|
+
// future AssociativeMember, SequentialMember).
|
|
4
|
+
//
|
|
5
|
+
// Carries the shared ISO 704:2022 MECE dimensions: presence × count,
|
|
6
|
+
// plus the orthogonal is_delimiting flag.
|
|
7
|
+
//
|
|
8
|
+
// Concrete leaf classes extend this with type-specific fields if
|
|
9
|
+
// needed (currently none). See docs/design/abstract-nary-relation.md
|
|
10
|
+
// (concept-model repo).
|
|
11
|
+
|
|
12
|
+
import { GlossaristModel } from './base.js';
|
|
13
|
+
import { ConceptRef } from './concept-ref.js';
|
|
14
|
+
import {
|
|
15
|
+
PARTITIVE_PRESENCE,
|
|
16
|
+
PARTITIVE_PRESENCE_VALUES,
|
|
17
|
+
DEFAULT_PRESENCE,
|
|
18
|
+
isValidPresence,
|
|
19
|
+
} from './partitive-presence.js';
|
|
20
|
+
import {
|
|
21
|
+
PARTITIVE_COUNT,
|
|
22
|
+
PARTITIVE_COUNT_VALUES,
|
|
23
|
+
DEFAULT_COUNT,
|
|
24
|
+
isValidCount,
|
|
25
|
+
} from './partitive-count.js';
|
|
26
|
+
|
|
27
|
+
// Module-private (not exported). Single source of truth for the
|
|
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 {
|
|
35
|
+
constructor(data = {}) {
|
|
36
|
+
super();
|
|
37
|
+
this.ref = _ensureRef(data.ref);
|
|
38
|
+
this.presence = _resolvePresence(data.presence);
|
|
39
|
+
this.count = _resolveCount(data.count);
|
|
40
|
+
this.is_delimiting = _resolveDelimiting(data.is_delimiting);
|
|
41
|
+
|
|
42
|
+
_assertValidPair(this.presence, this.count);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get isRequired() { return this.presence === PARTITIVE_PRESENCE.REQUIRED; }
|
|
46
|
+
get isOptional() { return this.presence === PARTITIVE_PRESENCE.OPTIONAL; }
|
|
47
|
+
get isDelimiting() { return this.is_delimiting === true; }
|
|
48
|
+
|
|
49
|
+
identity() {
|
|
50
|
+
return ConceptSystemMember.identityOf(this);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
toJSON() {
|
|
54
|
+
const obj = { ref: this.ref.toJSON() };
|
|
55
|
+
if (this.presence !== DEFAULT_PRESENCE) obj.presence = this.presence;
|
|
56
|
+
if (this.count !== DEFAULT_COUNT) obj.count = this.count;
|
|
57
|
+
if (this.is_delimiting) obj.is_delimiting = true;
|
|
58
|
+
return obj;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
static identityOf(value) {
|
|
62
|
+
const v = value ?? {};
|
|
63
|
+
const ref = v.ref ?? {};
|
|
64
|
+
return `${ref.source ?? ''}:${ref.id ?? ''}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
static fromJSON(data) {
|
|
68
|
+
return new ConceptSystemMember(data);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function _ensureRef(value) {
|
|
73
|
+
const ref = value instanceof ConceptRef ? value : new ConceptRef(value ?? {});
|
|
74
|
+
if (!ref.source && !ref.id && !ref.text) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
'ConceptSystemMember.ref must be a non-empty ConceptRef ' +
|
|
77
|
+
'(source, id, or text required)',
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
return ref;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function _resolvePresence(value) {
|
|
84
|
+
const p = value ?? DEFAULT_PRESENCE;
|
|
85
|
+
if (!isValidPresence(p)) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`invalid presence ${JSON.stringify(value)}; ` +
|
|
88
|
+
`must be one of: ${PARTITIVE_PRESENCE_VALUES.join(', ')}`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
return p;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function _resolveCount(value) {
|
|
95
|
+
const c = value ?? DEFAULT_COUNT;
|
|
96
|
+
if (!isValidCount(c)) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`invalid count ${JSON.stringify(value)}; ` +
|
|
99
|
+
`must be one of: ${PARTITIVE_COUNT_VALUES.join(', ')}`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
return c;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function _resolveDelimiting(value) {
|
|
106
|
+
if (value == null) return false;
|
|
107
|
+
if (typeof value !== 'boolean') {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`ConceptSystemMember.is_delimiting must be boolean; got ${typeof value}`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
return value;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function _assertValidPair(presence, count) {
|
|
116
|
+
if (
|
|
117
|
+
presence === INVALID_PRESENCE_COUNT_COMBO.presence &&
|
|
118
|
+
count === INVALID_PRESENCE_COUNT_COMBO.count
|
|
119
|
+
) {
|
|
120
|
+
throw new Error(
|
|
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
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// DefinitionType — ISO 704:2022 §5.3 definition strategy enum.
|
|
2
|
+
//
|
|
3
|
+
// intensional (default): genus + differentiae
|
|
4
|
+
// extensional: enumerate instances or species
|
|
5
|
+
// partitive: describe composition
|
|
6
|
+
// translated: translated from another language
|
|
7
|
+
|
|
8
|
+
import { makeEnum } from './enum.js';
|
|
9
|
+
|
|
10
|
+
export const DEFINITION_TYPE = makeEnum('DEFINITION_TYPE', {
|
|
11
|
+
INTENSIONAL: 'intensional',
|
|
12
|
+
EXTENSIONAL: 'extensional',
|
|
13
|
+
PARTITIVE: 'partitive',
|
|
14
|
+
TRANSLATED: 'translated',
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export const DEFAULT_DEFINITION_TYPE = DEFINITION_TYPE.INTENSIONAL;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// GenericMember — one member of a GenericRelation. Inherits the
|
|
2
|
+
// ISO 704:2022 MECE shape from ConceptSystemMember. Declared as a
|
|
3
|
+
// distinct leaf class for type safety and to leave room for
|
|
4
|
+
// generic-specific extensions.
|
|
5
|
+
//
|
|
6
|
+
// See docs/design/abstract-nary-relation.md and
|
|
7
|
+
// docs/design/generic-relation.md (concept-model repo).
|
|
8
|
+
|
|
9
|
+
import { ConceptSystemMember } from './concept-system-member.js';
|
|
10
|
+
|
|
11
|
+
export class GenericMember extends ConceptSystemMember {
|
|
12
|
+
// Inherits all attributes and validations from ConceptSystemMember.
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default GenericMember;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// GenericRelation — an ISO 704 / ISO 1087-1 / ISO 12620 generic
|
|
2
|
+
// relation connecting a comprehensive concept (the genus) to two
|
|
3
|
+
// or more specific concepts (the species) which together constitute
|
|
4
|
+
// a decomposition by some criterion of subdivision.
|
|
5
|
+
//
|
|
6
|
+
// Mirror of PartitiveRelation. Multiple GenericRelations on the same
|
|
7
|
+
// comprehensive distinguished by `criterion` is the OIML pattern
|
|
8
|
+
// (5.1 measurement standard has 6 criterion groups).
|
|
9
|
+
//
|
|
10
|
+
// See docs/design/generic-relation.md (concept-model repo).
|
|
11
|
+
|
|
12
|
+
import { AbstractNaryRelation } from './abstract-nary-relation.js';
|
|
13
|
+
import { GenericMember } from './generic-member.js';
|
|
14
|
+
|
|
15
|
+
export class GenericRelation extends AbstractNaryRelation {
|
|
16
|
+
constructor(data = {}) {
|
|
17
|
+
super(data);
|
|
18
|
+
this.members = data?.members == null
|
|
19
|
+
? this.members
|
|
20
|
+
: (Array.isArray(data.members)
|
|
21
|
+
? data.members.map(m =>
|
|
22
|
+
m instanceof GenericMember ? m : new GenericMember(m))
|
|
23
|
+
: this.members);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static identityOf(value) {
|
|
27
|
+
const v = value ?? {};
|
|
28
|
+
const c = v.comprehensive ?? {};
|
|
29
|
+
const memberKeys = Array.isArray(v.members)
|
|
30
|
+
? v.members.map(m => {
|
|
31
|
+
const r = m?.ref ?? m ?? {};
|
|
32
|
+
return `${r.source ?? ''}:${r.id ?? ''}`;
|
|
33
|
+
}).sort()
|
|
34
|
+
: [];
|
|
35
|
+
return `${c.source ?? ''}:${c.id ?? ''}|${memberKeys.join('|')}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
static fromJSON(data) {
|
|
39
|
+
return new GenericRelation(data);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default GenericRelation;
|
package/src/models/index.d.ts
CHANGED
|
@@ -328,6 +328,71 @@ export class PartitiveHyperedge extends GlossaristModel {
|
|
|
328
328
|
static fromJSON(data: Record<string, unknown>): PartitiveHyperedge;
|
|
329
329
|
}
|
|
330
330
|
|
|
331
|
+
// v2 PartitiveRelation + GenericRelation (MECE per-member model).
|
|
332
|
+
// These supersede the v1 PartitiveHyperedge model above. New code
|
|
333
|
+
// should use v2 exclusively; the v1 model is kept for backward-compat
|
|
334
|
+
// input.
|
|
335
|
+
export class ConceptSystemMember extends GlossaristModel {
|
|
336
|
+
readonly ref: ConceptRef;
|
|
337
|
+
readonly presence: 'required' | 'optional';
|
|
338
|
+
readonly count: 'exactly_one' | 'at_least_one' | 'multiple';
|
|
339
|
+
readonly is_delimiting: boolean;
|
|
340
|
+
required(): boolean;
|
|
341
|
+
optional(): boolean;
|
|
342
|
+
delimiting(): boolean;
|
|
343
|
+
identity(): string;
|
|
344
|
+
toJSON(): Record<string, unknown>;
|
|
345
|
+
static fromJSON(data: Record<string, unknown>): ConceptSystemMember;
|
|
346
|
+
static identityOf(value: unknown): string;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export class PartitiveMember extends ConceptSystemMember {}
|
|
350
|
+
|
|
351
|
+
export class GenericMember extends ConceptSystemMember {}
|
|
352
|
+
|
|
353
|
+
export class AbstractNaryRelation extends GlossaristModel {
|
|
354
|
+
readonly comprehensive: ConceptRef;
|
|
355
|
+
readonly members: ConceptSystemMember[];
|
|
356
|
+
readonly completeness: 'complete' | 'partial';
|
|
357
|
+
readonly criterion: Record<string, string> | null;
|
|
358
|
+
readonly sources: ConceptSource[];
|
|
359
|
+
readonly notes: Record<string, string> | null;
|
|
360
|
+
readonly status: string | null;
|
|
361
|
+
readonly isComplete: boolean;
|
|
362
|
+
readonly isPartial: boolean;
|
|
363
|
+
readonly isCoordinate: boolean;
|
|
364
|
+
toJSON(): Record<string, unknown>;
|
|
365
|
+
static fromJSON(data: Record<string, unknown>): AbstractNaryRelation;
|
|
366
|
+
static identityOf(value: unknown): string;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export class PartitiveRelation extends AbstractNaryRelation {
|
|
370
|
+
/** @deprecated use {@link members} — kept for v1/v2 wire compat. */
|
|
371
|
+
readonly partitives: ConceptSystemMember[];
|
|
372
|
+
static fromJSON(data: Record<string, unknown>): PartitiveRelation;
|
|
373
|
+
static identityOf(value: unknown): string;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export class GenericRelation extends AbstractNaryRelation {
|
|
377
|
+
static fromJSON(data: Record<string, unknown>): GenericRelation;
|
|
378
|
+
static identityOf(value: unknown): string;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export type DefinitionType = 'intensional' | 'extensional' | 'partitive' | 'translated';
|
|
382
|
+
export const DEFINITION_TYPE: Readonly<Record<string, DefinitionType>>;
|
|
383
|
+
export const DEFAULT_DEFINITION_TYPE: DefinitionType;
|
|
384
|
+
export const DEFINITION_TYPE_VALUES: readonly DefinitionType[];
|
|
385
|
+
|
|
386
|
+
export const RELATION_TYPE_TO_CLASS: Readonly<Record<string, typeof AbstractNaryRelation>>;
|
|
387
|
+
export class RelationLoadError extends Error {
|
|
388
|
+
readonly path: string;
|
|
389
|
+
constructor(path: string, message: string);
|
|
390
|
+
}
|
|
391
|
+
export const RelationLoader: {
|
|
392
|
+
loadAll(relationsDir: string): Map<string, AbstractNaryRelation[]>;
|
|
393
|
+
loadOne(path: string): AbstractNaryRelation | null;
|
|
394
|
+
};
|
|
395
|
+
|
|
331
396
|
export const DESIGNATION_RELATIONSHIP_TYPES: readonly string[];
|
|
332
397
|
export class DesignationRelationship extends GlossaristModel {
|
|
333
398
|
readonly type: string | null;
|
package/src/models/index.js
CHANGED
|
@@ -38,6 +38,22 @@ export {
|
|
|
38
38
|
// backward-compat input. New code should use v2 exclusively.
|
|
39
39
|
export { PartitiveRelation } from './partitive-relation.js';
|
|
40
40
|
export { PartitiveMember } from './partitive-member.js';
|
|
41
|
+
|
|
42
|
+
// v2 GenericRelation models (per-file relation storage). Mirrors
|
|
43
|
+
// PartitiveRelation shape; uses the same MECE member shape.
|
|
44
|
+
export { AbstractNaryRelation } from './abstract-nary-relation.js';
|
|
45
|
+
export { ConceptSystemMember } from './concept-system-member.js';
|
|
46
|
+
export { GenericRelation } from './generic-relation.js';
|
|
47
|
+
export { GenericMember } from './generic-member.js';
|
|
48
|
+
export {
|
|
49
|
+
DEFINITION_TYPE,
|
|
50
|
+
DEFAULT_DEFINITION_TYPE,
|
|
51
|
+
} from './definition-type.js';
|
|
52
|
+
|
|
53
|
+
// Per-file relation loader (Node-side runtime; not browser-bundled).
|
|
54
|
+
// Walks a relations/ directory and dispatches to PartitiveRelation /
|
|
55
|
+
// GenericRelation by file type.
|
|
56
|
+
export { RelationLoader, RelationLoadError, TYPE_TO_CLASS as RELATION_TYPE_TO_CLASS } from './relation-loader.js';
|
|
41
57
|
export {
|
|
42
58
|
PARTITIVE_PRESENCE,
|
|
43
59
|
PARTITIVE_PRESENCE_VALUES,
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
// PartitiveMember — one member of a PartitiveRelation.
|
|
1
|
+
// PartitiveMember — one member of a PartitiveRelation. Inherits the
|
|
2
|
+
// ISO 704:2022 MECE shape from ConceptSystemMember; the `comprehensive`
|
|
3
|
+
// of its parent PartitiveRelation denotes the whole concept.
|
|
2
4
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
+
// Distinct leaf class for type safety and to leave room for
|
|
6
|
+
// Partitive-specific extensions.
|
|
5
7
|
//
|
|
6
8
|
// Matches concept-model/models/concepts/PartitiveMember.lutaml exactly:
|
|
7
9
|
// +ref — ConceptRef [1]
|
|
@@ -12,94 +14,13 @@
|
|
|
12
14
|
// The combination (optional, at_least_one) is invalid: if a part is
|
|
13
15
|
// optional, the at-least-one constraint is vacuous and collapses to
|
|
14
16
|
// (optional, multiple). Rejected at construction.
|
|
15
|
-
//
|
|
16
|
-
// ISO 704 "name" strings (compulsory, optional, etc.) are NOT part of
|
|
17
|
-
// the model — they're a display concern handled by multiplicityFromPair
|
|
18
|
-
// in the renderer/viewer layer. The model is purely presence + count.
|
|
19
|
-
|
|
20
|
-
import { GlossaristModel } from './base.js';
|
|
21
|
-
import { ConceptRef } from './concept-ref.js';
|
|
22
|
-
import {
|
|
23
|
-
PARTITIVE_PRESENCE,
|
|
24
|
-
DEFAULT_PRESENCE,
|
|
25
|
-
PARTITIVE_PRESENCE_VALUES,
|
|
26
|
-
isValidPresence,
|
|
27
|
-
} from './partitive-presence.js';
|
|
28
|
-
import {
|
|
29
|
-
DEFAULT_COUNT,
|
|
30
|
-
PARTITIVE_COUNT_VALUES,
|
|
31
|
-
isValidCount,
|
|
32
|
-
} from './partitive-count.js';
|
|
33
|
-
|
|
34
|
-
export class PartitiveMember extends GlossaristModel {
|
|
35
|
-
constructor(data = {}) {
|
|
36
|
-
super();
|
|
37
|
-
this.ref = data.ref instanceof ConceptRef
|
|
38
|
-
? data.ref
|
|
39
|
-
: new ConceptRef(data.ref ?? {});
|
|
40
|
-
this.presence = _resolvePresence(data.presence);
|
|
41
|
-
this.count = _resolveCount(data.count);
|
|
42
|
-
this.is_delimiting = data.is_delimiting === true;
|
|
43
|
-
|
|
44
|
-
_assertNonEmptyRef(this.ref);
|
|
45
|
-
_assertValidCombination(this.presence, this.count);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
get isRequired() { return this.presence === PARTITIVE_PRESENCE.REQUIRED; }
|
|
49
|
-
get isOptional() { return this.presence === PARTITIVE_PRESENCE.OPTIONAL; }
|
|
50
|
-
get isDelimiting() { return this.is_delimiting === true; }
|
|
51
|
-
|
|
52
|
-
identity() {
|
|
53
|
-
return PartitiveMember.identityOf(this);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
toJSON() {
|
|
57
|
-
const obj = { ref: this.ref.toJSON() };
|
|
58
|
-
if (this.presence !== DEFAULT_PRESENCE) obj.presence = this.presence;
|
|
59
|
-
if (this.count !== DEFAULT_COUNT) obj.count = this.count;
|
|
60
|
-
if (this.is_delimiting === true) obj.is_delimiting = true;
|
|
61
|
-
return obj;
|
|
62
|
-
}
|
|
63
17
|
|
|
64
|
-
|
|
65
|
-
const v = value ?? {};
|
|
66
|
-
const ref = v.ref ?? {};
|
|
67
|
-
return `${ref.source ?? ''}:${ref.id ?? ''}`;
|
|
68
|
-
}
|
|
18
|
+
import { ConceptSystemMember } from './concept-system-member.js';
|
|
69
19
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
20
|
+
export class PartitiveMember extends ConceptSystemMember {
|
|
21
|
+
// Inherits all attributes and validations from ConceptSystemMember.
|
|
22
|
+
// Declared as a distinct class for type safety and to leave room
|
|
23
|
+
// for future partitive-specific extensions.
|
|
73
24
|
}
|
|
74
25
|
|
|
75
|
-
|
|
76
|
-
if (value == null) return DEFAULT_PRESENCE;
|
|
77
|
-
if (!isValidPresence(value)) {
|
|
78
|
-
throw new Error(`invalid presence ${JSON.stringify(value)}; must be one of ${PARTITIVE_PRESENCE_VALUES.join(', ')}`);
|
|
79
|
-
}
|
|
80
|
-
return value;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function _resolveCount(value) {
|
|
84
|
-
if (value == null) return DEFAULT_COUNT;
|
|
85
|
-
if (!isValidCount(value)) {
|
|
86
|
-
throw new Error(`invalid count ${JSON.stringify(value)}; must be one of ${PARTITIVE_COUNT_VALUES.join(', ')}`);
|
|
87
|
-
}
|
|
88
|
-
return value;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function _assertNonEmptyRef(ref) {
|
|
92
|
-
if (!ref.source && !ref.id && !ref.text) {
|
|
93
|
-
throw new Error('PartitiveMember#ref must be a non-empty ConceptRef (source, id, or text required)');
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function _assertValidCombination(presence, count) {
|
|
98
|
-
if (presence === 'optional' && count === 'at_least_one') {
|
|
99
|
-
throw new Error(
|
|
100
|
-
'PartitiveMember presence=optional + count=at_least_one is invalid — ' +
|
|
101
|
-
'it collapses to optional + multiple (zero or more). ' +
|
|
102
|
-
'Use presence: optional, count: multiple instead.',
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
26
|
+
export default PartitiveMember;
|
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
// concepts (subordinate concepts partitive) which fitted together
|
|
5
5
|
// constitute the comprehensive.
|
|
6
6
|
//
|
|
7
|
+
// Extends AbstractNaryRelation; the leaf-specific concern is the
|
|
8
|
+
// PartitiveMember type for `members`. The wire-level field name
|
|
9
|
+
// stays `partitives` for v1/v2 backward compat.
|
|
10
|
+
//
|
|
7
11
|
// ISO 704:2022 rake diagram semantics:
|
|
8
12
|
//
|
|
9
13
|
// comprehensive concept
|
|
@@ -22,142 +26,51 @@
|
|
|
22
26
|
// principle, and the criterion of subdivision used
|
|
23
27
|
// (ISO 704:2022 §5.5.4.2.1).
|
|
24
28
|
//
|
|
25
|
-
// Field renames (v1 → v2):
|
|
26
|
-
// parts [1..*] → partitives [2..*] (as PartitiveMember)
|
|
27
|
-
// enumeration: closed|open → completeness: complete|partial
|
|
28
|
-
// markers: [double, dashed] → (per-member multiplicity + is_delimiting)
|
|
29
|
-
// content: LocalizedString → (dropped — structural edges carry no prose)
|
|
30
|
-
// plural-positive is_shared / is_uncertain → multiplicity (ISO 704:2022)
|
|
31
|
-
//
|
|
32
29
|
// Distinguished from binary `has_part` / `is_part_of` edges, which
|
|
33
30
|
// express pairwise part-of assertions without completeness,
|
|
34
31
|
// multiplicity, or criterion claims. A single partitive should be
|
|
35
32
|
// expressed as a binary edge, not a PartitiveRelation (ISO requires
|
|
36
33
|
// "two or more").
|
|
37
34
|
|
|
38
|
-
import {
|
|
39
|
-
import { ConceptRef } from './concept-ref.js';
|
|
35
|
+
import { AbstractNaryRelation } from './abstract-nary-relation.js';
|
|
40
36
|
import { PartitiveMember } from './partitive-member.js';
|
|
41
|
-
import {
|
|
42
|
-
COMPLETENESS,
|
|
43
|
-
COMPLETENESS_VALUES,
|
|
44
|
-
DEFAULT_COMPLETENESS,
|
|
45
|
-
isValidCompleteness,
|
|
46
|
-
} from './completeness.js';
|
|
47
37
|
|
|
48
|
-
export class PartitiveRelation extends
|
|
38
|
+
export class PartitiveRelation extends AbstractNaryRelation {
|
|
49
39
|
constructor(data = {}) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
this.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
40
|
+
const members = data?.members ?? data?.partitives;
|
|
41
|
+
super({ ...data, members });
|
|
42
|
+
this.members = members == null
|
|
43
|
+
? this.members
|
|
44
|
+
: (Array.isArray(members)
|
|
45
|
+
? members.map(m =>
|
|
46
|
+
m instanceof PartitiveMember ? m : new PartitiveMember(m))
|
|
47
|
+
: this.members);
|
|
57
48
|
}
|
|
58
49
|
|
|
59
|
-
get
|
|
60
|
-
get isPartial() { return this.completeness === COMPLETENESS.PARTIAL; }
|
|
61
|
-
|
|
62
|
-
// ISO 12620 coordinate-concept predicate: a relation is "coordinate"
|
|
63
|
-
// when it has ≥2 partitives (all partitives within one relation are
|
|
64
|
-
// coordinate concepts by definition).
|
|
65
|
-
get isCoordinate() { return this.partitives.length >= 2; }
|
|
66
|
-
|
|
67
|
-
hasCriterion() { return this.criterion != null; }
|
|
68
|
-
|
|
69
|
-
identity() {
|
|
70
|
-
return PartitiveRelation.identityOf(this);
|
|
71
|
-
}
|
|
50
|
+
get partitives() { return this.members; }
|
|
72
51
|
|
|
73
52
|
toJSON() {
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
completeness: this.completeness,
|
|
78
|
-
};
|
|
79
|
-
if (this.criterion != null) obj.criterion = { ...this.criterion };
|
|
80
|
-
return obj;
|
|
53
|
+
const base = super.toJSON();
|
|
54
|
+
const { members, ...rest } = base;
|
|
55
|
+
return { ...rest, partitives: members };
|
|
81
56
|
}
|
|
82
57
|
|
|
83
|
-
// Stable identity for diffing: same comprehensive + same partitive
|
|
84
|
-
// set (by ref identity) = same relation. Changes to completeness,
|
|
85
|
-
// multiplicity, is_delimiting, or criterion produce "changed"
|
|
86
|
-
// entries rather than add+remove.
|
|
87
58
|
static identityOf(value) {
|
|
88
59
|
const v = value ?? {};
|
|
89
60
|
const c = v.comprehensive ?? {};
|
|
90
|
-
const
|
|
91
|
-
? v.partitives.map(p => {
|
|
61
|
+
const partKeys = Array.isArray(v.partitives ?? v.members)
|
|
62
|
+
? (v.partitives ?? v.members).map(p => {
|
|
92
63
|
const r = p?.ref ?? p ?? {};
|
|
93
64
|
return `${r.source ?? ''}:${r.id ?? ''}`;
|
|
94
65
|
}).sort()
|
|
95
66
|
: [];
|
|
96
|
-
return `${c.source ?? ''}:${c.id ?? ''}|${
|
|
67
|
+
return `${c.source ?? ''}:${c.id ?? ''}|${partKeys.join('|')}`;
|
|
97
68
|
}
|
|
98
69
|
|
|
99
70
|
static fromJSON(data) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function _ensureComprehensive(value) {
|
|
105
|
-
const ref = value instanceof ConceptRef ? value : new ConceptRef(value ?? {});
|
|
106
|
-
if (!ref.source && !ref.id && !ref.text) {
|
|
107
|
-
throw new Error(
|
|
108
|
-
'PartitiveRelation.comprehensive must be a non-empty ConceptReference ' +
|
|
109
|
-
'(source, id, or text required)',
|
|
110
|
-
);
|
|
71
|
+
const normalized = { ...(data ?? {}), members: data?.partitives ?? data?.members };
|
|
72
|
+
return new PartitiveRelation(normalized);
|
|
111
73
|
}
|
|
112
|
-
return ref;
|
|
113
74
|
}
|
|
114
75
|
|
|
115
|
-
|
|
116
|
-
if (!Array.isArray(values)) {
|
|
117
|
-
throw new Error(
|
|
118
|
-
`PartitiveRelation.partitives must be an array (got ${typeof values})`,
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
if (values.length < 2) {
|
|
122
|
-
throw new Error(
|
|
123
|
-
`PartitiveRelation requires ≥2 partitives (ISO 704); got ${values.length}`,
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
return values.map(v => v instanceof PartitiveMember ? v : new PartitiveMember(v));
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function _resolveCompleteness(value) {
|
|
130
|
-
const c = value ?? DEFAULT_COMPLETENESS;
|
|
131
|
-
if (!isValidCompleteness(c)) {
|
|
132
|
-
throw new Error(
|
|
133
|
-
`PartitiveRelation.completeness has invalid value ${JSON.stringify(value)}; ` +
|
|
134
|
-
`must be one of: ${COMPLETENESS_VALUES.join(', ')}`,
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
|
-
return c;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function _normalizeCriterion(value) {
|
|
141
|
-
if (value == null) return null;
|
|
142
|
-
if (typeof value === 'string') {
|
|
143
|
-
return value.length > 0 ? { default: value } : null;
|
|
144
|
-
}
|
|
145
|
-
if (typeof value === 'object') {
|
|
146
|
-
const entries = Object.entries(value).filter(([, v]) => typeof v === 'string');
|
|
147
|
-
return entries.length > 0 ? Object.fromEntries(entries) : null;
|
|
148
|
-
}
|
|
149
|
-
return null;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function _assertNoSelfLoop(comprehensive, partitives) {
|
|
153
|
-
const cKey = `${comprehensive.source ?? ''}:${comprehensive.id ?? ''}`;
|
|
154
|
-
for (const p of partitives) {
|
|
155
|
-
const ref = p.ref;
|
|
156
|
-
const pKey = `${ref.source ?? ''}:${ref.id ?? ''}`;
|
|
157
|
-
if (pKey === cKey && (ref.source || ref.id)) {
|
|
158
|
-
throw new Error(
|
|
159
|
-
'PartitiveRelation.partitives cannot include the comprehensive (no self-loops)',
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
76
|
+
export default PartitiveRelation;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// RelationLoader — runtime loader for per-file n-ary relation YAMLs.
|
|
2
|
+
//
|
|
3
|
+
// Walks a `relations/<comprehensive-id>/<slug>.yaml` directory and
|
|
4
|
+
// dispatches each file to the appropriate class (PartitiveRelation
|
|
5
|
+
// or GenericRelation) based on its `type` field.
|
|
6
|
+
//
|
|
7
|
+
// The wire-shape contract is owned by the JSON Schema at
|
|
8
|
+
// `schemas/v3/relation.yaml` (concept-model repo) — this loader
|
|
9
|
+
// produces the corresponding JS model instances. See
|
|
10
|
+
// docs/design/relations-as-files.md (concept-model).
|
|
11
|
+
//
|
|
12
|
+
// This is the runtime equivalent of concept-model's
|
|
13
|
+
// CheckPartitiveRelationCoherence#each_relation /
|
|
14
|
+
// CheckGenericRelationCoherence#each_relation validators.
|
|
15
|
+
// Both readers and validators must agree on the type-dispatch
|
|
16
|
+
// table (TYPE_TO_CLASS). Keep them in sync via the JSON Schema.
|
|
17
|
+
|
|
18
|
+
import { readdirSync, readFileSync, existsSync } from 'node:fs';
|
|
19
|
+
import * as yaml from 'js-yaml';
|
|
20
|
+
import { PartitiveRelation } from './partitive-relation.js';
|
|
21
|
+
import { GenericRelation } from './generic-relation.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Per-type class dispatch. Keys are the `type` field of the relation
|
|
25
|
+
* file. Adding a new relation type (e.g. AssociativeRelation) means
|
|
26
|
+
* one entry here plus one new class.
|
|
27
|
+
*/
|
|
28
|
+
export const TYPE_TO_CLASS = Object.freeze({
|
|
29
|
+
partitive_relation: PartitiveRelation,
|
|
30
|
+
generic_relation: GenericRelation,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Thrown by `loadAll` when a relation file has an unknown `type`,
|
|
35
|
+
* malformed structure, or a class that fails to instantiate.
|
|
36
|
+
*/
|
|
37
|
+
export class RelationLoadError extends Error {
|
|
38
|
+
constructor(path, message) {
|
|
39
|
+
super(`relation-loader: ${path}: ${message}`);
|
|
40
|
+
this.path = path;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Walk a `relations/` directory and load every relation file,
|
|
46
|
+
* returning a Map keyed by comprehensive-id.
|
|
47
|
+
*
|
|
48
|
+
* Returns an empty Map if `relationsDir` doesn't exist or is empty.
|
|
49
|
+
*
|
|
50
|
+
* @param {string} relationsDir — path to a `relations/` directory
|
|
51
|
+
* containing `<comprehensive-id>/<slug>.yaml` files (per the
|
|
52
|
+
* schema in schemas/v3/relation.yaml).
|
|
53
|
+
* @returns {Map<string, Array<AbstractNaryRelation>>} — for each
|
|
54
|
+
* comprehensive id, the list of relations that have it as their
|
|
55
|
+
* comprehensive. Type-discriminated (PartitiveRelation or
|
|
56
|
+
* GenericRelation).
|
|
57
|
+
*/
|
|
58
|
+
export function loadAll(relationsDir) {
|
|
59
|
+
const result = new Map();
|
|
60
|
+
if (!relationsDir) return result;
|
|
61
|
+
if (!existsSync(relationsDir)) return result;
|
|
62
|
+
|
|
63
|
+
const entries = readdirSync(relationsDir, { withFileTypes: true });
|
|
64
|
+
for (const compEntry of entries) {
|
|
65
|
+
if (!compEntry.isDirectory()) continue;
|
|
66
|
+
const compId = compEntry.name;
|
|
67
|
+
const compDir = `${relationsDir}/${compId}`;
|
|
68
|
+
if (!result.has(compId)) result.set(compId, []);
|
|
69
|
+
const files = readdirSync(compDir)
|
|
70
|
+
.filter(f => f.endsWith('.yaml') || f.endsWith('.yml'))
|
|
71
|
+
.sort();
|
|
72
|
+
for (const file of files) {
|
|
73
|
+
const path = `${compDir}/${file}`;
|
|
74
|
+
const rel = loadOne(path);
|
|
75
|
+
if (rel) result.get(compId).push(rel);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Namespace bundle for the loader — callers import it as
|
|
83
|
+
* `import { RelationLoader } from './relation-loader.js'` and call
|
|
84
|
+
* `RelationLoader.loadAll(dir)`. The individual functions are also
|
|
85
|
+
* exported directly for tree-shakeable single-call sites.
|
|
86
|
+
*/
|
|
87
|
+
export const RelationLoader = Object.freeze({ loadAll });
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Load a single relation file. Returns null if the file is missing,
|
|
91
|
+
* unreadable, or doesn't have a recognized `type` field. Throws
|
|
92
|
+
* `RelationLoadError` on malformed content (e.g., invalid `members`
|
|
93
|
+
* count, wrong comprehensive shape, YAML parse error).
|
|
94
|
+
*/
|
|
95
|
+
export function loadOne(path) {
|
|
96
|
+
let doc;
|
|
97
|
+
try {
|
|
98
|
+
doc = yaml.load(readFileSync(path, 'utf8'));
|
|
99
|
+
} catch (e) {
|
|
100
|
+
throw new RelationLoadError(path, `YAML parse failed: ${e.message}`);
|
|
101
|
+
}
|
|
102
|
+
if (!doc || typeof doc !== 'object') return null;
|
|
103
|
+
if (!doc.type) return null;
|
|
104
|
+
const Cls = TYPE_TO_CLASS[doc.type];
|
|
105
|
+
if (!Cls) return null; // Unknown type — let the validator report it
|
|
106
|
+
return Cls.fromJSON(doc);
|
|
107
|
+
}
|