glossarist 0.4.33 → 0.4.35
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 +1 -1
- package/src/diff/concept-diff.js +6 -6
- package/src/diff/diff-renderer.js +11 -3
- package/src/models/abstract-hyperedge.js +2 -2
- package/src/models/concept-system-type.js +26 -0
- package/src/models/concept-system.js +117 -0
- package/src/models/concept-type.js +25 -0
- package/src/models/concept.js +2 -2
- package/src/models/equivalence-degree.js +25 -0
- package/src/models/generic-member.js +74 -7
- package/src/models/hyperedge-member.js +20 -25
- package/src/models/index.js +4 -4
- package/src/models/partitive-member.js +38 -9
- package/src/models/sequential-hyperedge.js +56 -0
- package/src/models/sequential-member.js +13 -0
- package/src/rdf/gloss-partitive-relation.js +1 -1
- package/src/validators/ref-keys.js +2 -2
- /package/src/migration/{partitive-relation-migrator.js → hyperedge-migrator.js} +0 -0
package/package.json
CHANGED
package/src/concept-parser.js
CHANGED
|
@@ -2,7 +2,7 @@ import * as yaml from 'js-yaml';
|
|
|
2
2
|
import { Concept } from './models/concept.js';
|
|
3
3
|
import { RelatedConcept } from './models/related-concept.js';
|
|
4
4
|
import { HyperedgeRegistry } from './models/hyperedge-registry.js';
|
|
5
|
-
import { migrateHyperedgeToRelation } from './migration/
|
|
5
|
+
import { migrateHyperedgeToRelation } from './migration/hyperedge-migrator.js';
|
|
6
6
|
import { InvalidInputError, YamlParseError } from './errors.js';
|
|
7
7
|
|
|
8
8
|
// Structural keys are reserved at the concept level and excluded
|
package/src/diff/concept-diff.js
CHANGED
|
@@ -101,7 +101,7 @@ export class ConceptLevelDiff extends GlossaristModel {
|
|
|
101
101
|
this._sources = wrapListDiff(data.sources);
|
|
102
102
|
this._dates = wrapListDiff(data.dates);
|
|
103
103
|
this._relatedConcepts = wrapListDiff(data.relatedConcepts ?? data.related_concepts);
|
|
104
|
-
// Unified
|
|
104
|
+
// Unified hyperedge diff. Accepts:
|
|
105
105
|
// - data.relations (new unified shape)
|
|
106
106
|
// - data.partitiveRelations /
|
|
107
107
|
// data.partitive_relations /
|
|
@@ -127,7 +127,7 @@ export class ConceptLevelDiff extends GlossaristModel {
|
|
|
127
127
|
get sources() { return this._sources; }
|
|
128
128
|
get dates() { return this._dates; }
|
|
129
129
|
get relatedConcepts() { return this._relatedConcepts; }
|
|
130
|
-
/** Unified
|
|
130
|
+
/** Unified hyperedge diff (PartitiveHyperedge + GenericHyperedge). */
|
|
131
131
|
get relations() { return this._relations; }
|
|
132
132
|
/**
|
|
133
133
|
* Filtered view of `relations` containing only PartitiveHyperedge
|
|
@@ -465,7 +465,7 @@ function diffConceptLevel(oldConcept, newConcept) {
|
|
|
465
465
|
sources: diffSources(oldConcept.sources ?? [], newConcept.sources ?? []),
|
|
466
466
|
dates: diffDates(oldConcept.dates ?? [], newConcept.dates ?? []),
|
|
467
467
|
relatedConcepts: diffRelatedConcepts(oldConcept.relatedConcepts ?? [], newConcept.relatedConcepts ?? []),
|
|
468
|
-
relations:
|
|
468
|
+
relations: diffHyperedges(
|
|
469
469
|
oldConcept.relations ?? [],
|
|
470
470
|
newConcept.relations ?? [],
|
|
471
471
|
),
|
|
@@ -565,13 +565,13 @@ function diffRelatedConcepts(oldRC, newRC) {
|
|
|
565
565
|
});
|
|
566
566
|
}
|
|
567
567
|
|
|
568
|
-
// Diff two unified
|
|
568
|
+
// Diff two unified hyperedge arrays. Identity is polymorphic —
|
|
569
569
|
// dispatches via value.constructor.identityOf so PartitiveHyperedge
|
|
570
|
-
// and GenericHyperedge (and any future
|
|
570
|
+
// and GenericHyperedge (and any future hyperedge subclass) coexist in
|
|
571
571
|
// the same list. Cross-type entries with matching identity are still
|
|
572
572
|
// treated as different (because their constructor differs and they
|
|
573
573
|
// live under different wire keys).
|
|
574
|
-
function
|
|
574
|
+
function diffHyperedges(oldR, newR) {
|
|
575
575
|
return diffSet(oldR, newR, {
|
|
576
576
|
identityKey: relationIdentityOf,
|
|
577
577
|
textKey: relationText,
|
|
@@ -125,10 +125,10 @@ function renderConceptLevel(diff, colors) {
|
|
|
125
125
|
lines.push(...renderListDiff('Sources', diff.sources, colors, itemLabel));
|
|
126
126
|
lines.push(...renderListDiff('Dates', diff.dates, colors, itemLabel));
|
|
127
127
|
lines.push(...renderListDiff('Related', diff.relatedConcepts, colors, itemLabel));
|
|
128
|
-
// Unified
|
|
128
|
+
// Unified hyperedge section. Replaces the per-type sections
|
|
129
129
|
// (Partitive relations, Generic relations) — the underlying diff
|
|
130
130
|
// is a single ListDiff over the unified .relations array.
|
|
131
|
-
lines.push(...renderListDiff('
|
|
131
|
+
lines.push(...renderListDiff('Hyperedges', diff.relations, colors, relationLabel));
|
|
132
132
|
lines.push(...renderListDiff('Groups', diff.groups, colors, itemLabel));
|
|
133
133
|
lines.push(...renderListDiff('Sections', diff.sections, colors, itemLabel));
|
|
134
134
|
lines.push(...renderListDiff('Tags', diff.tags, colors, itemLabel));
|
|
@@ -214,8 +214,16 @@ function relationLabel(r) {
|
|
|
214
214
|
const ref = m?.ref ?? m ?? {};
|
|
215
215
|
let tail = '';
|
|
216
216
|
const mult = resolveMultiplicity(m);
|
|
217
|
-
if (mult && mult !== 'compulsory') tail
|
|
217
|
+
if (mult && mult !== 'compulsory') tail += ` (${mult})`;
|
|
218
|
+
// PartitiveMember uses is_delimiting (boolean flag).
|
|
218
219
|
if (m?.is_delimiting === true) tail += ' ⊘';
|
|
220
|
+
// GenericMember uses delimitingCharacteristic (string value per
|
|
221
|
+
// ISO 704:2022 §5.5.4.2.1) — show the delimiting characteristic
|
|
222
|
+
// text so reviewers can see the intension difference.
|
|
223
|
+
if (m?.delimitingCharacteristic) {
|
|
224
|
+
const c = Object.values(m.delimitingCharacteristic)[0];
|
|
225
|
+
if (typeof c === 'string' && c.length > 0) tail += ` ⟨${c}⟩`;
|
|
226
|
+
}
|
|
219
227
|
return `${ref.id ?? ref.source ?? ref.text ?? '?'}${tail}`;
|
|
220
228
|
}).join(', ')
|
|
221
229
|
: '';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// AbstractHyperedge — shared base for
|
|
1
|
+
// AbstractHyperedge — shared base for directed 1→N hyperedges
|
|
2
2
|
// relations (PartitiveHyperedge, GenericHyperedge, future
|
|
3
3
|
// SequentialRelation, AssociativeRelation).
|
|
4
4
|
//
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// - sources / notes / status: administrative metadata
|
|
11
11
|
//
|
|
12
12
|
// Concrete leaves add type-specific fields if needed (currently none).
|
|
13
|
-
// See docs/design/abstract-
|
|
13
|
+
// See docs/design/abstract-hyperedge.md (concept-model repo).
|
|
14
14
|
|
|
15
15
|
import { GlossaristModel } from './base.js';
|
|
16
16
|
import { ConceptRef } from './concept-ref.js';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// ConceptSystemType — ISO 12620 A.7.1 concept system type enum.
|
|
2
|
+
//
|
|
3
|
+
// Permissible instances per ISO 12620:
|
|
4
|
+
// generic — species are part of the extension of the genus
|
|
5
|
+
// partitive — based on whole-part relations
|
|
6
|
+
// sequential — based on spatial and temporal relations
|
|
7
|
+
// associative — based on thematic or pragmatic relations
|
|
8
|
+
// mixed — two or more of the above (the realistic case)
|
|
9
|
+
|
|
10
|
+
export const CONCEPT_SYSTEM_TYPE = Object.freeze({
|
|
11
|
+
GENERIC: 'generic',
|
|
12
|
+
PARTITIVE: 'partitive',
|
|
13
|
+
SEQUENTIAL: 'sequential',
|
|
14
|
+
ASSOCIATIVE: 'associative',
|
|
15
|
+
MIXED: 'mixed',
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const CONCEPT_SYSTEM_TYPE_VALUES = Object.freeze(
|
|
19
|
+
Object.values(CONCEPT_SYSTEM_TYPE),
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
export function isValidConceptSystemType(value) {
|
|
23
|
+
return CONCEPT_SYSTEM_TYPE_VALUES.includes(value);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default CONCEPT_SYSTEM_TYPE;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// ConceptSystem — first-class concept system entity per ISO 12620 A.7.
|
|
2
|
+
//
|
|
3
|
+
// Promotes the previously-implicit concept system (domains + tags +
|
|
4
|
+
// relation graph) to an explicit first-class entity. Per-file at
|
|
5
|
+
// `concept-systems/<id>.yaml`.
|
|
6
|
+
//
|
|
7
|
+
// See docs/design/concept-systems.md (concept-model repo).
|
|
8
|
+
|
|
9
|
+
import { GlossaristModel } from './base.js';
|
|
10
|
+
import { ConceptRef } from './concept-ref.js';
|
|
11
|
+
import {
|
|
12
|
+
CONCEPT_SYSTEM_TYPE,
|
|
13
|
+
CONCEPT_SYSTEM_TYPE_VALUES,
|
|
14
|
+
isValidConceptSystemType,
|
|
15
|
+
} from './concept-system-type.js';
|
|
16
|
+
|
|
17
|
+
export class ConceptSystem extends GlossaristModel {
|
|
18
|
+
constructor(data = {}) {
|
|
19
|
+
super();
|
|
20
|
+
this.id = _ensureNonEmptyString(data.id, 'id');
|
|
21
|
+
this.name = _ensureName(data.name);
|
|
22
|
+
this.type = _resolveType(data.type);
|
|
23
|
+
this.domain = data.domain ?? null;
|
|
24
|
+
this.purpose = data.purpose ?? null;
|
|
25
|
+
this.members = _ensureMembers(data.members);
|
|
26
|
+
this.hyperedges = Array.isArray(data.hyperedges) ? [...data.hyperedges] : [];
|
|
27
|
+
this.rootConcepts = Array.isArray(data.rootConcepts)
|
|
28
|
+
? data.rootConcepts.map(c => c instanceof ConceptRef ? c : new ConceptRef(c))
|
|
29
|
+
: [];
|
|
30
|
+
this.sources = Array.isArray(data.sources) ? [...data.sources] : [];
|
|
31
|
+
this.status = data.status ?? null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get isGeneric() { return this.type === CONCEPT_SYSTEM_TYPE.GENERIC; }
|
|
35
|
+
get isPartitive() { return this.type === CONCEPT_SYSTEM_TYPE.PARTITIVE; }
|
|
36
|
+
get isSequential() { return this.type === CONCEPT_SYSTEM_TYPE.SEQUENTIAL; }
|
|
37
|
+
get isAssociative() { return this.type === CONCEPT_SYSTEM_TYPE.ASSOCIATIVE; }
|
|
38
|
+
get isMixed() { return this.type === CONCEPT_SYSTEM_TYPE.MIXED; }
|
|
39
|
+
|
|
40
|
+
hasHyperedge(edgeId) {
|
|
41
|
+
return this.hyperedges.includes(edgeId);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
hasMember(refOrUri) {
|
|
45
|
+
const uri = typeof refOrUri === 'string' ? refOrUri : _refUri(refOrUri);
|
|
46
|
+
return this.members.some(m => _refUri(m) === uri);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
identity() {
|
|
50
|
+
return this.id;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
toJSON() {
|
|
54
|
+
const obj = {
|
|
55
|
+
id: this.id,
|
|
56
|
+
name: { ...this.name },
|
|
57
|
+
type: this.type,
|
|
58
|
+
members: this.members.map(m => m.toJSON()),
|
|
59
|
+
};
|
|
60
|
+
if (this.domain != null) obj.domain = this.domain;
|
|
61
|
+
if (this.purpose != null) obj.purpose = { ...this.purpose };
|
|
62
|
+
if (this.hyperedges.length > 0) obj.hyperedges = [...this.hyperedges];
|
|
63
|
+
if (this.rootConcepts.length > 0) obj.rootConcepts = this.rootConcepts.map(c => c.toJSON());
|
|
64
|
+
if (this.sources.length > 0) obj.sources = [...this.sources];
|
|
65
|
+
if (this.status != null) obj.status = this.status;
|
|
66
|
+
return obj;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
static fromJSON(data) {
|
|
70
|
+
return new ConceptSystem(data);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function _ensureNonEmptyString(value, fieldName) {
|
|
75
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
76
|
+
throw new Error(`ConceptSystem#${fieldName} must be a non-empty string`);
|
|
77
|
+
}
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function _ensureName(value) {
|
|
82
|
+
if (value == null) return {};
|
|
83
|
+
if (typeof value !== 'object' || Array.isArray(value)) {
|
|
84
|
+
throw new Error('ConceptSystem#name must be a localized object (e.g. { eng: "..." })');
|
|
85
|
+
}
|
|
86
|
+
return { ...value };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function _resolveType(value) {
|
|
90
|
+
if (value == null) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
'ConceptSystem#type is required; one of ' +
|
|
93
|
+
CONCEPT_SYSTEM_TYPE_VALUES.join(', '),
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
if (!isValidConceptSystemType(value)) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`ConceptSystem#type ${JSON.stringify(value)} invalid; ` +
|
|
99
|
+
`must be one of: ${CONCEPT_SYSTEM_TYPE_VALUES.join(', ')}`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
return value;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function _ensureMembers(values) {
|
|
106
|
+
if (!Array.isArray(values) || values.length === 0) {
|
|
107
|
+
throw new Error('ConceptSystem#members must be a non-empty array');
|
|
108
|
+
}
|
|
109
|
+
return values.map(v => v instanceof ConceptRef ? v : new ConceptRef(v));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function _refUri(ref) {
|
|
113
|
+
const r = ref?.ref ?? ref ?? {};
|
|
114
|
+
return `${r.source ?? ''}:${r.id ?? ''}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export default ConceptSystem;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// ConceptType — ISO 704:2022 §5.2-5.3 general vs individual concept.
|
|
2
|
+
//
|
|
3
|
+
// general — concept corresponds to a category of objects
|
|
4
|
+
// (designated by terms or symbols)
|
|
5
|
+
// individual — concept corresponds to a unique object
|
|
6
|
+
// (designated by proper names / appellations)
|
|
7
|
+
//
|
|
8
|
+
// Individual concepts occupy the bottom rung of a concept ladder
|
|
9
|
+
// and cannot be subdivided into more specific concepts, but CAN be
|
|
10
|
+
// subdivided into parts via a partitive relation.
|
|
11
|
+
|
|
12
|
+
export const CONCEPT_TYPE = Object.freeze({
|
|
13
|
+
GENERAL: 'general',
|
|
14
|
+
INDIVIDUAL: 'individual',
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export const CONCEPT_TYPE_VALUES = Object.freeze(Object.values(CONCEPT_TYPE));
|
|
18
|
+
|
|
19
|
+
export const DEFAULT_CONCEPT_TYPE = CONCEPT_TYPE.GENERAL;
|
|
20
|
+
|
|
21
|
+
export function isValidConceptType(value) {
|
|
22
|
+
return CONCEPT_TYPE_VALUES.includes(value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default CONCEPT_TYPE;
|
package/src/models/concept.js
CHANGED
|
@@ -4,7 +4,7 @@ import { RelatedConcept } from './related-concept.js';
|
|
|
4
4
|
import { PartitiveHyperedge } from './partitive-hyperedge.js';
|
|
5
5
|
import { AbstractHyperedge } from './abstract-hyperedge.js';
|
|
6
6
|
import { HyperedgeRegistry, groupHyperedgesByWireKey } from './hyperedge-registry.js';
|
|
7
|
-
import { migrateHyperedgeToRelation } from '../migration/
|
|
7
|
+
import { migrateHyperedgeToRelation } from '../migration/hyperedge-migrator.js';
|
|
8
8
|
import { ConceptReference } from './concept-reference.js';
|
|
9
9
|
import { ConceptDate } from './concept-date.js';
|
|
10
10
|
import { ConceptSource } from './concept-source.js';
|
|
@@ -36,7 +36,7 @@ export class Concept extends GlossaristModel {
|
|
|
36
36
|
|
|
37
37
|
this.relatedConcepts = _mapInstances(data.relatedConcepts ?? data.related ?? data.related_concepts ?? [], RelatedConcept);
|
|
38
38
|
|
|
39
|
-
// Unified hyperedge array. Every
|
|
39
|
+
// Unified hyperedge array. Every hyperedge (PartitiveHyperedge,
|
|
40
40
|
// GenericHyperedge, future TemporalHyperedge / AssociativeHyperedge /
|
|
41
41
|
// SequentialHyperedge) lives here. The single public API is
|
|
42
42
|
// `.relations`; external systems that need to filter by type use
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// EquivalenceDegree — ISO 704:2022 §7.7.3 cross-language equivalence.
|
|
2
|
+
//
|
|
3
|
+
// full — equivalent intension across languages (default)
|
|
4
|
+
// partial — overlapping but not identical intension
|
|
5
|
+
// none — no corresponding concept in this language
|
|
6
|
+
// directional — monodirectional equivalence (see equivalence_note)
|
|
7
|
+
|
|
8
|
+
export const EQUIVALENCE_DEGREE = Object.freeze({
|
|
9
|
+
FULL: 'full',
|
|
10
|
+
PARTIAL: 'partial',
|
|
11
|
+
NONE: 'none',
|
|
12
|
+
DIRECTIONAL: 'directional',
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const EQUIVALENCE_DEGREE_VALUES = Object.freeze(
|
|
16
|
+
Object.values(EQUIVALENCE_DEGREE),
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
export const DEFAULT_EQUIVALENCE_DEGREE = EQUIVALENCE_DEGREE.FULL;
|
|
20
|
+
|
|
21
|
+
export function isValidEquivalenceDegree(value) {
|
|
22
|
+
return EQUIVALENCE_DEGREE_VALUES.includes(value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default EQUIVALENCE_DEGREE;
|
|
@@ -1,15 +1,82 @@
|
|
|
1
|
-
// GenericMember — one member of a GenericHyperedge.
|
|
2
|
-
// ISO 704:2022 MECE shape from HyperedgeMember. Declared as a
|
|
3
|
-
// distinct leaf class for type safety and to leave room for
|
|
4
|
-
// generic-specific extensions.
|
|
1
|
+
// GenericMember — one member of a GenericHyperedge.
|
|
5
2
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
3
|
+
// Per ISO 704:2022 §5.5.4.2.1 (Generic relations and general concepts):
|
|
4
|
+
//
|
|
5
|
+
// "In a generic relation, the intension of the subordinate concept
|
|
6
|
+
// includes the intension of the superordinate concept plus at least
|
|
7
|
+
// one additional delimiting characteristic."
|
|
8
|
+
//
|
|
9
|
+
// In hyperedge terms: when a GenericHyperedge has comprehensive=computer mouse
|
|
10
|
+
// and criterion=means of movement detection, each member (mechanical mouse,
|
|
11
|
+
// optomechanical mouse, optical mouse) carries a delimitingCharacteristic —
|
|
12
|
+
// the value of the criterion for that specific concept:
|
|
13
|
+
//
|
|
14
|
+
// mechanical mouse → 'detecting movement by means of rollers'
|
|
15
|
+
// optomechanical mouse → 'detecting movement by means of rollers and light sensors'
|
|
16
|
+
// optical mouse → 'detecting movement by means of light sensors'
|
|
17
|
+
//
|
|
18
|
+
// Multidimensionality (§5.6.3): the same comprehensive can be the
|
|
19
|
+
// '1' side of multiple generic hyperedges simultaneously, each by a
|
|
20
|
+
// different criterion. computer mouse is also comprehensive of
|
|
21
|
+
// {wired mouse, wireless mouse} under criterion=computer connection.
|
|
22
|
+
// That second hyperedge's members carry their own delimitingCharacteristic
|
|
23
|
+
// values ('using a corded electrical connection', 'using a cordless
|
|
24
|
+
// light or sound connection').
|
|
25
|
+
//
|
|
26
|
+
// delimitingCharacteristic is REQUIRED on GenericMember. Without it,
|
|
27
|
+
// the member is just a coordinate concept with no semantic content
|
|
28
|
+
// tying it to the criterion — ISO 704 says the relation is meaningless
|
|
29
|
+
// without the delimiting characteristic.
|
|
30
|
+
//
|
|
31
|
+
// LocalizedString shape (mirrors criterion on the parent hyperedge):
|
|
32
|
+
// { eng: 'detecting movement by means of light sensors' }
|
|
33
|
+
// { eng: '...', fra: '...' } — multiple languages supported
|
|
8
34
|
|
|
9
35
|
import { HyperedgeMember } from './hyperedge-member.js';
|
|
10
36
|
|
|
11
37
|
export class GenericMember extends HyperedgeMember {
|
|
12
|
-
|
|
38
|
+
constructor(data = {}) {
|
|
39
|
+
super(data);
|
|
40
|
+
this.delimitingCharacteristic = _resolveDelimitingCharacteristic(data.delimitingCharacteristic);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
toJSON() {
|
|
44
|
+
const base = super.toJSON();
|
|
45
|
+
base.delimitingCharacteristic = { ...this.delimitingCharacteristic };
|
|
46
|
+
return base;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function _resolveDelimitingCharacteristic(value) {
|
|
51
|
+
if (value == null) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
'GenericMember.delimitingCharacteristic is required per ISO 704:2022 §5.5.4.2.1 ' +
|
|
54
|
+
'(the intension of a subordinate concept must include at least one additional ' +
|
|
55
|
+
'delimiting characteristic beyond the superordinate).',
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
if (typeof value === 'string') {
|
|
59
|
+
if (value.length === 0) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
'GenericMember.delimitingCharacteristic cannot be empty (ISO 704:2022 §5.5.4.2.1)',
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
return { default: value };
|
|
65
|
+
}
|
|
66
|
+
if (typeof value === 'object') {
|
|
67
|
+
const entries = Object.entries(value).filter(([, v]) => typeof v === 'string' && v.length > 0);
|
|
68
|
+
if (entries.length === 0) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
'GenericMember.delimitingCharacteristic must have at least one non-empty string ' +
|
|
71
|
+
'value (ISO 704:2022 §5.5.4.2.1)',
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
return Object.fromEntries(entries);
|
|
75
|
+
}
|
|
76
|
+
throw new Error(
|
|
77
|
+
`GenericMember.delimitingCharacteristic must be a string or a language-keyed object; ` +
|
|
78
|
+
`got ${typeof value}`,
|
|
79
|
+
);
|
|
13
80
|
}
|
|
14
81
|
|
|
15
82
|
export default GenericMember;
|
|
@@ -1,13 +1,21 @@
|
|
|
1
|
-
// HyperedgeMember — abstract base shape for members of any
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// HyperedgeMember — abstract base shape for the members of any directed
|
|
2
|
+
// 1→N hyperedge (PartitiveMember, GenericMember, future AssociativeMember,
|
|
3
|
+
// SequentialMember).
|
|
4
4
|
//
|
|
5
|
-
// Carries the shared
|
|
6
|
-
//
|
|
5
|
+
// Carries ONLY the dimensions shared by every hyperedge member type:
|
|
6
|
+
// ref — ConceptRef [1] (the member concept)
|
|
7
|
+
// presence — 'required' | 'optional' (default: required)
|
|
8
|
+
// count — 'exactly_one' | 'at_least_one' | 'multiple' (default: exactly_one)
|
|
7
9
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
10
|
+
// These three form the ISO 704:2022 MECE pair (presence × count). The
|
|
11
|
+
// pair is the shared invariant across all hyperedge types — that's
|
|
12
|
+
// why it lives on the base.
|
|
13
|
+
//
|
|
14
|
+
// Type-specific extensions live on the leaves:
|
|
15
|
+
// PartitiveMember — is_delimiting (rake-diagram semantics, ISO 704 §5.5.4.2.2)
|
|
16
|
+
// GenericMember — delimitingCharacteristic (intension difference, ISO 704 §5.5.4.2.1)
|
|
17
|
+
//
|
|
18
|
+
// See docs/design/abstract-hyperedge.md (concept-model repo).
|
|
11
19
|
|
|
12
20
|
import { GlossaristModel } from './base.js';
|
|
13
21
|
import { ConceptRef } from './concept-ref.js';
|
|
@@ -30,14 +38,12 @@ export class HyperedgeMember extends GlossaristModel {
|
|
|
30
38
|
this.ref = _ensureRef(data.ref);
|
|
31
39
|
this.presence = _resolvePresence(data.presence);
|
|
32
40
|
this.count = _resolveCount(data.count);
|
|
33
|
-
this.is_delimiting = _resolveDelimiting(data.is_delimiting);
|
|
34
41
|
|
|
35
42
|
_assertValidPair(this.presence, this.count);
|
|
36
43
|
}
|
|
37
44
|
|
|
38
45
|
get isRequired() { return this.presence === PARTITIVE_PRESENCE.REQUIRED; }
|
|
39
46
|
get isOptional() { return this.presence === PARTITIVE_PRESENCE.OPTIONAL; }
|
|
40
|
-
get isDelimiting() { return this.is_delimiting === true; }
|
|
41
47
|
|
|
42
48
|
identity() {
|
|
43
49
|
return HyperedgeMember.identityOf(this);
|
|
@@ -47,7 +53,6 @@ export class HyperedgeMember extends GlossaristModel {
|
|
|
47
53
|
const obj = { ref: this.ref.toJSON() };
|
|
48
54
|
if (this.presence !== DEFAULT_PRESENCE) obj.presence = this.presence;
|
|
49
55
|
if (this.count !== DEFAULT_COUNT) obj.count = this.count;
|
|
50
|
-
if (this.is_delimiting) obj.is_delimiting = true;
|
|
51
56
|
return obj;
|
|
52
57
|
}
|
|
53
58
|
|
|
@@ -95,20 +100,10 @@ function _resolveCount(value) {
|
|
|
95
100
|
return c;
|
|
96
101
|
}
|
|
97
102
|
|
|
98
|
-
function _resolveDelimiting(value) {
|
|
99
|
-
if (value == null) return false;
|
|
100
|
-
if (typeof value !== 'boolean') {
|
|
101
|
-
throw new Error(
|
|
102
|
-
`HyperedgeMember.is_delimiting must be boolean; got ${typeof value}`,
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
return value;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
103
|
function _assertValidPair(presence, count) {
|
|
109
|
-
// Delegate to the multiplicity SSOT
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
104
|
+
// Delegate to the multiplicity SSOT. The canonical error string
|
|
105
|
+
// lives in one place: multiplicity.js. Calling multiplicityFromPair
|
|
106
|
+
// throws on the invalid combination; we don't need to duplicate the
|
|
107
|
+
// error message here.
|
|
113
108
|
multiplicityFromPair(presence, count);
|
|
114
109
|
}
|
package/src/models/index.js
CHANGED
|
@@ -32,9 +32,9 @@ export {
|
|
|
32
32
|
isValidPluralityMarker,
|
|
33
33
|
} from './plurality-marker.js';
|
|
34
34
|
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
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
38
|
export { PartitiveHyperedge } from './partitive-hyperedge.js';
|
|
39
39
|
export { PartitiveMember } from './partitive-member.js';
|
|
40
40
|
|
|
@@ -89,7 +89,7 @@ export {
|
|
|
89
89
|
export {
|
|
90
90
|
migrateHyperedgeToRelation,
|
|
91
91
|
downgradeRelationToHyperedge,
|
|
92
|
-
} from '../migration/
|
|
92
|
+
} from '../migration/hyperedge-migrator.js';
|
|
93
93
|
|
|
94
94
|
export { makeEnum } from './enum.js';
|
|
95
95
|
export { resolveHyperedgeColor } from './hyperedge-colors.js';
|
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
// PartitiveMember — one member of a PartitiveHyperedge.
|
|
2
|
-
// ISO 704:2022 MECE shape from HyperedgeMember; the `comprehensive`
|
|
3
|
-
// of its parent PartitiveHyperedge 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.
|
|
17
27
|
|
|
18
28
|
import { HyperedgeMember } from './hyperedge-member.js';
|
|
19
29
|
|
|
20
30
|
export class PartitiveMember extends HyperedgeMember {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
+
}
|
|
44
|
+
|
|
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;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// SequentialHyperedge — an ISO 12620 A.6.3 / ISO 704:2022 §5.5.5
|
|
2
|
+
// sequential relation connecting a comprehensive concept to two or
|
|
3
|
+
// more subordinate concepts in an ORDERED sequence (temporal,
|
|
4
|
+
// spatial, causal, developmental).
|
|
5
|
+
//
|
|
6
|
+
// Mirror of PartitiveHyperedge and GenericHyperedge. Member array
|
|
7
|
+
// order IS significant — reversing the array reverses the sequence.
|
|
8
|
+
// Use cases: manufacturing process steps, biological life cycles,
|
|
9
|
+
// software release lifecycles, supply chain stages, geological eras.
|
|
10
|
+
//
|
|
11
|
+
// See docs/design/abstract-nary-relation.md (concept-model repo).
|
|
12
|
+
|
|
13
|
+
import { AbstractHyperedge } from './abstract-hyperedge.js';
|
|
14
|
+
import { SequentialMember } from './sequential-member.js';
|
|
15
|
+
import { HyperedgeRegistry } from './hyperedge-registry.js';
|
|
16
|
+
|
|
17
|
+
export class SequentialHyperedge extends AbstractHyperedge {
|
|
18
|
+
// Per-class metadata block — the OCP contract.
|
|
19
|
+
static wireKey = 'sequential_relations';
|
|
20
|
+
static typeTag = 'sequential_relation';
|
|
21
|
+
static rdfType = 'gloss:SequentialRelation';
|
|
22
|
+
static memberClass = SequentialMember;
|
|
23
|
+
static v1WireKeys = [];
|
|
24
|
+
static kindLabel = 'SEQ';
|
|
25
|
+
|
|
26
|
+
constructor(data = {}) {
|
|
27
|
+
super(data);
|
|
28
|
+
this.members = data?.members == null
|
|
29
|
+
? this.members
|
|
30
|
+
: (Array.isArray(data.members)
|
|
31
|
+
? data.members.map(m =>
|
|
32
|
+
m instanceof SequentialMember ? m : new SequentialMember(m))
|
|
33
|
+
: this.members);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static identityOf(value) {
|
|
37
|
+
const v = value ?? {};
|
|
38
|
+
const c = v.comprehensive ?? {};
|
|
39
|
+
// For sequential, member order IS significant — do NOT sort.
|
|
40
|
+
const memberKeys = Array.isArray(v.members)
|
|
41
|
+
? v.members.map(m => {
|
|
42
|
+
const r = m?.ref ?? m ?? {};
|
|
43
|
+
return `${r.source ?? ''}:${r.id ?? ''}`;
|
|
44
|
+
})
|
|
45
|
+
: [];
|
|
46
|
+
return `${c.source ?? ''}:${c.id ?? ''}|${memberKeys.join('|')}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static fromJSON(data) {
|
|
50
|
+
return new SequentialHyperedge(data);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default SequentialHyperedge;
|
|
55
|
+
|
|
56
|
+
HyperedgeRegistry.register(SequentialHyperedge);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// SequentialMember — one member of a SequentialHyperedge. Inherits
|
|
2
|
+
// the MECE shape from HyperedgeMember. Distinct leaf class for type
|
|
3
|
+
// safety. Member array order IS significant for sequential hyperedges.
|
|
4
|
+
//
|
|
5
|
+
// See docs/design/abstract-nary-relation.md (concept-model repo).
|
|
6
|
+
|
|
7
|
+
import { HyperedgeMember } from './hyperedge-member.js';
|
|
8
|
+
|
|
9
|
+
export class SequentialMember extends HyperedgeMember {
|
|
10
|
+
// Inherits all attributes and validations from HyperedgeMember.
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default SequentialMember;
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
//
|
|
15
15
|
// The carrying concept links via `gloss:hasPartitiveRelation`.
|
|
16
16
|
//
|
|
17
|
-
//
|
|
17
|
+
// Per
|
|
18
18
|
// TODO.partitive-relation-v2 item 01.
|
|
19
19
|
// Cross-repo alignment with glossarist-ruby's Rdf::GlossPartitiveRelation.
|
|
20
20
|
//
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Shared identity-key helpers for cross-relation validators.
|
|
2
2
|
//
|
|
3
|
-
// Extracted from
|
|
4
|
-
// BinaryHasPartRedundancyRule
|
|
3
|
+
// Extracted from HyperedgeCoherenceRule and
|
|
4
|
+
// BinaryHasPartRedundancyRule.
|
|
5
5
|
// Eliminates drift: both rules now use the same notion of "same ref"
|
|
6
6
|
// and "same criterion" instead of each defining its own _refKey and
|
|
7
7
|
// _criterionKey with subtly different behaviors.
|
|
File without changes
|