glossarist 0.4.18 → 0.4.20
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/data/concept-model/shapes/glossarist.shacl.ttl +151 -1
- package/package.json +1 -1
- package/src/concept-diff.js +13 -0
- package/src/concept-parser.js +52 -2
- package/src/concept-serializer.js +11 -0
- package/src/diff/canonical-json.js +31 -0
- package/src/diff/change.js +28 -0
- package/src/diff/collection-diff.js +57 -25
- package/src/diff/concept-diff.js +100 -60
- package/src/diff/diff-patch.js +49 -48
- package/src/diff/diff-renderer.js +34 -0
- package/src/diff/identity.js +22 -0
- package/src/diff/index.d.ts +64 -1
- package/src/diff/index.js +2 -0
- package/src/diff/similarity.js +24 -0
- package/src/index.d.ts +2 -0
- package/src/index.js +2 -0
- package/src/migration/partitive-relation-migrator.js +125 -0
- package/src/models/completeness.js +24 -0
- package/src/models/concept-date.js +8 -0
- package/src/models/concept-ref.js +4 -0
- package/src/models/concept-source.js +21 -2
- package/src/models/concept.js +55 -0
- package/src/models/designation.js +15 -0
- package/src/models/detailed-definition.js +11 -0
- package/src/models/enum.js +42 -0
- package/src/models/hyperedge-colors.js +71 -0
- package/src/models/index.d.ts +61 -0
- package/src/models/index.js +39 -0
- package/src/models/localized-concept.js +29 -0
- package/src/models/member-certainty.js +21 -0
- package/src/models/partitive-enumeration.js +23 -0
- package/src/models/partitive-hyperedge.js +175 -0
- package/src/models/partitive-member.js +79 -0
- package/src/models/partitive-relation.js +158 -0
- package/src/models/plurality-marker.js +24 -0
- package/src/models/related-concept.js +38 -1
- package/src/models/relation-categories.js +6 -0
- package/src/models/type-shared-plurality.js +71 -0
- package/src/rdf/gloss-concept.js +26 -0
- package/src/rdf/gloss-hyperedge.js +90 -0
- package/src/rdf/gloss-partitive-relation.js +109 -0
- package/src/rdf/gloss-source.js +1 -1
- package/src/rdf/index.d.ts +37 -0
- package/src/rdf/index.js +2 -0
- package/src/rdf/predicates.d.ts +54 -0
- package/src/rdf/predicates.js +27 -0
- package/src/reference-resolver.js +49 -0
- package/src/validators/binary-has-part-redundancy-rule.js +74 -0
- package/src/validators/external-concept-shape-rule.js +60 -0
- package/src/validators/index.js +19 -1
- package/src/validators/partitive-drift-rule.js +51 -0
- package/src/validators/partitive-hyperedge-rule.js +75 -0
- package/src/validators/partitive-relation-coherence-rule.js +107 -0
- package/src/validators/sourced-from-locality-rule.js +53 -0
package/src/models/index.d.ts
CHANGED
|
@@ -100,6 +100,7 @@ export class Concept extends GlossaristModel {
|
|
|
100
100
|
readonly localizations: Record<string, any>;
|
|
101
101
|
readonly raw: Record<string, unknown> | null;
|
|
102
102
|
readonly relatedConcepts: RelatedConcept[];
|
|
103
|
+
readonly partitiveHyperedges: PartitiveHyperedge[];
|
|
103
104
|
readonly domains: ConceptReference[];
|
|
104
105
|
readonly tags: string[];
|
|
105
106
|
readonly dates: ConceptDate[];
|
|
@@ -257,6 +258,8 @@ export class ConceptSource extends GlossaristModel {
|
|
|
257
258
|
readonly type: string | null;
|
|
258
259
|
readonly origin: Citation | null;
|
|
259
260
|
readonly modification: string | null;
|
|
261
|
+
readonly sourcedFrom: Citation[];
|
|
262
|
+
/** @deprecated use sourcedFrom */
|
|
260
263
|
readonly sourced_from: Citation[];
|
|
261
264
|
}
|
|
262
265
|
|
|
@@ -267,6 +270,64 @@ export class RelatedConcept extends GlossaristModel {
|
|
|
267
270
|
readonly ref: ConceptRef | null;
|
|
268
271
|
}
|
|
269
272
|
|
|
273
|
+
export interface LocalizedContent {
|
|
274
|
+
readonly [language: string]: string;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export const PARTITIVE_ENUMERATION: Readonly<{
|
|
278
|
+
readonly CLOSED: 'closed';
|
|
279
|
+
readonly OPEN: 'open';
|
|
280
|
+
}>;
|
|
281
|
+
export const PARTITIVE_ENUMERATION_VALUES: readonly ['closed', 'open'];
|
|
282
|
+
export function isValidPartitiveEnumeration(value: unknown): value is 'closed' | 'open';
|
|
283
|
+
|
|
284
|
+
export const PLURALITY_MARKER: Readonly<{
|
|
285
|
+
readonly DOUBLE: 'double';
|
|
286
|
+
readonly DASHED: 'dashed';
|
|
287
|
+
}>;
|
|
288
|
+
export const PLURALITY_MARKER_VALUES: readonly ['double', 'dashed'];
|
|
289
|
+
export function isValidPluralityMarker(value: unknown): value is 'double' | 'dashed';
|
|
290
|
+
|
|
291
|
+
export function makeEnum<T extends string>(
|
|
292
|
+
name: string,
|
|
293
|
+
values: Record<string, T>,
|
|
294
|
+
): Readonly<Record<string, T>> & {
|
|
295
|
+
readonly name: string;
|
|
296
|
+
readonly VALUES: readonly T[];
|
|
297
|
+
readonly isValid: (value: unknown) => value is T;
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
export interface HyperedgeColorPair {
|
|
301
|
+
readonly light: string;
|
|
302
|
+
readonly dark: string;
|
|
303
|
+
readonly pattern: 'dashed' | 'double' | null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function resolveHyperedgeColor(
|
|
307
|
+
hyperedge: { enumeration?: string; markers?: string[] } | null,
|
|
308
|
+
options?: {
|
|
309
|
+
category?: string;
|
|
310
|
+
overrides?: { byCategory?: Record<string, unknown> };
|
|
311
|
+
},
|
|
312
|
+
): HyperedgeColorPair | null;
|
|
313
|
+
|
|
314
|
+
export class PartitiveHyperedge extends GlossaristModel {
|
|
315
|
+
readonly comprehensive: ConceptRef;
|
|
316
|
+
readonly parts: ConceptRef[];
|
|
317
|
+
readonly enumeration: 'closed' | 'open';
|
|
318
|
+
readonly markers: ('double' | 'dashed')[];
|
|
319
|
+
readonly content: LocalizedContent | null;
|
|
320
|
+
|
|
321
|
+
readonly isMarked: boolean;
|
|
322
|
+
readonly contentString: string | null;
|
|
323
|
+
|
|
324
|
+
hasMarker(value: string): boolean;
|
|
325
|
+
hasEnumeration(value: string): boolean;
|
|
326
|
+
identity(): string;
|
|
327
|
+
toJSON(): Record<string, unknown>;
|
|
328
|
+
static fromJSON(data: Record<string, unknown>): PartitiveHyperedge;
|
|
329
|
+
}
|
|
330
|
+
|
|
270
331
|
export const DESIGNATION_RELATIONSHIP_TYPES: readonly string[];
|
|
271
332
|
export class DesignationRelationship extends GlossaristModel {
|
|
272
333
|
readonly type: string | null;
|
package/src/models/index.js
CHANGED
|
@@ -21,6 +21,45 @@ 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
|
+
export {
|
|
26
|
+
PARTITIVE_ENUMERATION,
|
|
27
|
+
PARTITIVE_ENUMERATION_VALUES,
|
|
28
|
+
isValidPartitiveEnumeration,
|
|
29
|
+
} from './partitive-enumeration.js';
|
|
30
|
+
export {
|
|
31
|
+
PLURALITY_MARKER,
|
|
32
|
+
PLURALITY_MARKER_VALUES,
|
|
33
|
+
isValidPluralityMarker,
|
|
34
|
+
} from './plurality-marker.js';
|
|
35
|
+
|
|
36
|
+
// v2 PartitiveRelation models (TODO.partitive-relation-v2). These
|
|
37
|
+
// supersede the v1 hyperedge models above; the v1 models are kept for
|
|
38
|
+
// backward-compat input. New code should use v2 exclusively.
|
|
39
|
+
export { PartitiveRelation } from './partitive-relation.js';
|
|
40
|
+
export { PartitiveMember } from './partitive-member.js';
|
|
41
|
+
export { TypeSharedPlurality } from './type-shared-plurality.js';
|
|
42
|
+
export {
|
|
43
|
+
COMPLETENESS,
|
|
44
|
+
COMPLETENESS_VALUES,
|
|
45
|
+
DEFAULT_COMPLETENESS,
|
|
46
|
+
isValidCompleteness,
|
|
47
|
+
} from './completeness.js';
|
|
48
|
+
export {
|
|
49
|
+
MEMBER_CERTAINTY,
|
|
50
|
+
MEMBER_CERTAINTY_VALUES,
|
|
51
|
+
DEFAULT_MEMBER_CERTAINTY,
|
|
52
|
+
isValidMemberCertainty,
|
|
53
|
+
} from './member-certainty.js';
|
|
54
|
+
|
|
55
|
+
// v1 → v2 migration helper. Pure function; idempotent.
|
|
56
|
+
export {
|
|
57
|
+
migrateHyperedgeToRelation,
|
|
58
|
+
downgradeRelationToHyperedge,
|
|
59
|
+
} from '../migration/partitive-relation-migrator.js';
|
|
60
|
+
|
|
61
|
+
export { makeEnum } from './enum.js';
|
|
62
|
+
export { resolveHyperedgeColor } from './hyperedge-colors.js';
|
|
24
63
|
export { DesignationRelationship, DESIGNATION_RELATIONSHIP_TYPES } from './designation-relationship.js';
|
|
25
64
|
export { ConceptReference } from './concept-reference.js';
|
|
26
65
|
export { ConceptDate, DATE_TYPES } from './concept-date.js';
|
|
@@ -10,7 +10,36 @@ function wrapAs(Cls) {
|
|
|
10
10
|
return item => item instanceof Cls ? item : new Cls(item);
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
const LOC_WIRE_NAMES = Object.freeze({
|
|
14
|
+
entryStatus: 'entry_status',
|
|
15
|
+
reviewType: 'review_type',
|
|
16
|
+
lineageSourceSimilarity: 'lineage_source_similarity',
|
|
17
|
+
reviewDate: 'review_date',
|
|
18
|
+
reviewDecisionDate: 'review_decision_date',
|
|
19
|
+
reviewDecisionEvent: 'review_decision_event',
|
|
20
|
+
reviewStatus: 'review_status',
|
|
21
|
+
reviewDecision: 'review_decision',
|
|
22
|
+
reviewDecisionNotes: 'review_decision_notes',
|
|
23
|
+
});
|
|
24
|
+
|
|
13
25
|
export class LocalizedConcept extends GlossaristModel {
|
|
26
|
+
// Scalar metadata fields the diff layer tracks per localization.
|
|
27
|
+
// Adding a new scalar field requires only appending its name here;
|
|
28
|
+
// diff/patch/similarity pick it up automatically.
|
|
29
|
+
// (Invariant N2 — TODO.hyperedges-v2/07.)
|
|
30
|
+
static get DIFF_FIELDS() {
|
|
31
|
+
return Object.freeze([
|
|
32
|
+
'entryStatus', 'classification', 'reviewType', 'domain', 'release',
|
|
33
|
+
'lineageSourceSimilarity', 'script', 'system',
|
|
34
|
+
'reviewDate', 'reviewDecisionDate', 'reviewDecisionEvent',
|
|
35
|
+
'reviewStatus', 'reviewDecision', 'reviewDecisionNotes',
|
|
36
|
+
]);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static wireNameFor(field) {
|
|
40
|
+
return LOC_WIRE_NAMES[field] ?? field;
|
|
41
|
+
}
|
|
42
|
+
|
|
14
43
|
constructor(data = {}) {
|
|
15
44
|
super();
|
|
16
45
|
this.languageCode = data.language_code ?? data.languageCode ?? null;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// MemberCertainty — per-partitive certainty for a PartitiveMember.
|
|
2
|
+
//
|
|
3
|
+
// Glossarist extension beyond ISO 704 notation. ISO 704's broken-line
|
|
4
|
+
// notation marks plurality-level uncertainty; per-member certainty
|
|
5
|
+
// lets a relation assert "member b is confirmed, member d is possible"
|
|
6
|
+
// at the member level.
|
|
7
|
+
//
|
|
8
|
+
// Defaults to `confirmed` when omitted (a member with no certainty
|
|
9
|
+
// field is implicitly confirmed).
|
|
10
|
+
|
|
11
|
+
import { makeEnum } from './enum.js';
|
|
12
|
+
|
|
13
|
+
export const MEMBER_CERTAINTY = makeEnum('MEMBER_CERTAINTY', {
|
|
14
|
+
CONFIRMED: 'confirmed',
|
|
15
|
+
POSSIBLE: 'possible',
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const MEMBER_CERTAINTY_VALUES = MEMBER_CERTAINTY.VALUES;
|
|
19
|
+
export const isValidMemberCertainty = MEMBER_CERTAINTY.isValid;
|
|
20
|
+
|
|
21
|
+
export const DEFAULT_MEMBER_CERTAINTY = MEMBER_CERTAINTY.CONFIRMED;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// PartitiveEnumeration — allowed values for PartitiveHyperedge#enumeration.
|
|
2
|
+
//
|
|
3
|
+
// closed — the encoded parts are ALL the parts of the comprehensive.
|
|
4
|
+
// Adding or removing a part would change the relationship.
|
|
5
|
+
// open — the encoded parts are SOME of the parts; others may exist
|
|
6
|
+
// but are not encoded.
|
|
7
|
+
//
|
|
8
|
+
// See concept-model/TODO.hyperedge/00-design-overview.md for the design
|
|
9
|
+
// rationale and the open/closed distinction.
|
|
10
|
+
|
|
11
|
+
export const PARTITIVE_ENUMERATION = Object.freeze({
|
|
12
|
+
CLOSED: 'closed',
|
|
13
|
+
OPEN: 'open',
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const PARTITIVE_ENUMERATION_VALUES = Object.freeze([
|
|
17
|
+
PARTITIVE_ENUMERATION.CLOSED,
|
|
18
|
+
PARTITIVE_ENUMERATION.OPEN,
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
export function isValidPartitiveEnumeration(value) {
|
|
22
|
+
return PARTITIVE_ENUMERATION_VALUES.includes(value);
|
|
23
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// PartitiveHyperedge — a one-to-many partitive decomposition.
|
|
2
|
+
//
|
|
3
|
+
// One comprehensive concept (the whole) is related to one or more
|
|
4
|
+
// parts as a SINGLE relationship. Captures invariants that binary
|
|
5
|
+
// RelatedConcept edges cannot:
|
|
6
|
+
//
|
|
7
|
+
// - which comprehensive owns which parts (set membership)
|
|
8
|
+
// - plurality markers from the source diagram (double / dashed)
|
|
9
|
+
// - enumeration completeness (closed: all parts listed; open:
|
|
10
|
+
// other parts may exist)
|
|
11
|
+
//
|
|
12
|
+
// See concept-model/TODO.hyperedge/00-design-overview.md for the
|
|
13
|
+
// design rationale.
|
|
14
|
+
//
|
|
15
|
+
// Hyperedges coexist with binary `broader_partitive` /
|
|
16
|
+
// `narrower_partitive` edges. Use a binary edge for pairwise
|
|
17
|
+
// relationships without diagram metadata; use a hyperedge when the
|
|
18
|
+
// relationship is one-to-many with enumeration/marker metadata.
|
|
19
|
+
|
|
20
|
+
import { GlossaristModel } from './base.js';
|
|
21
|
+
import { ConceptRef } from './concept-ref.js';
|
|
22
|
+
import {
|
|
23
|
+
PARTITIVE_ENUMERATION,
|
|
24
|
+
PARTITIVE_ENUMERATION_VALUES,
|
|
25
|
+
isValidPartitiveEnumeration,
|
|
26
|
+
} from './partitive-enumeration.js';
|
|
27
|
+
import {
|
|
28
|
+
PLURALITY_MARKER_VALUES,
|
|
29
|
+
isValidPluralityMarker,
|
|
30
|
+
} from './plurality-marker.js';
|
|
31
|
+
|
|
32
|
+
const DEFAULT_ENUMERATION = PARTITIVE_ENUMERATION.CLOSED;
|
|
33
|
+
|
|
34
|
+
export class PartitiveHyperedge extends GlossaristModel {
|
|
35
|
+
constructor(data = {}) {
|
|
36
|
+
super();
|
|
37
|
+
this.comprehensive = ensureConceptRef(data.comprehensive, 'comprehensive');
|
|
38
|
+
this.parts = ensureConceptRefArray(data.parts, 'parts');
|
|
39
|
+
this.enumeration = resolveEnumeration(data.enumeration);
|
|
40
|
+
this.markers = validateMarkers(data.markers ?? []);
|
|
41
|
+
this.content = normalizeContent(data.content);
|
|
42
|
+
|
|
43
|
+
assertNonEmptyParts(this.parts);
|
|
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
|
+
}
|
|
54
|
+
|
|
55
|
+
hasEnumeration(value) {
|
|
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
|
+
}
|
|
64
|
+
|
|
65
|
+
toJSON() {
|
|
66
|
+
const obj = {
|
|
67
|
+
comprehensive: this.comprehensive.toJSON(),
|
|
68
|
+
parts: this.parts.map(p => p.toJSON()),
|
|
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;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
static identityOf(value) {
|
|
77
|
+
const v = value ?? {};
|
|
78
|
+
const c = v.comprehensive ?? {};
|
|
79
|
+
const parts = Array.isArray(v.parts) ? v.parts : [];
|
|
80
|
+
const partsStr = parts
|
|
81
|
+
.map(p => `${p?.source ?? ''}:${p?.id ?? ''}`)
|
|
82
|
+
.sort();
|
|
83
|
+
return `${c.source ?? ''}:${c.id ?? ''}|${partsStr.join('|')}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
identity() {
|
|
87
|
+
return PartitiveHyperedge.identityOf(this);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
static fromJSON(data) {
|
|
91
|
+
return new PartitiveHyperedge(data);
|
|
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
|
+
);
|
|
102
|
+
}
|
|
103
|
+
return value;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function validateMarkers(values) {
|
|
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
|
+
}
|
|
157
|
+
|
|
158
|
+
// Content is a localized string (language -> text). Plain strings on
|
|
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
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// PartitiveMember — one member of a PartitiveRelation.
|
|
2
|
+
//
|
|
3
|
+
// A partitive member carries a ConceptRef (the subordinate concept
|
|
4
|
+
// partitive) and optional certainty metadata. A PartitiveMember with
|
|
5
|
+
// no certainty field is implicitly confirmed.
|
|
6
|
+
//
|
|
7
|
+
// Glossarist extension beyond ISO 704: ISO notation expresses
|
|
8
|
+
// plurality-level uncertainty (broken line); per-member certainty
|
|
9
|
+
// lets a relation assert "member b is confirmed, member d is possible"
|
|
10
|
+
// at the member level.
|
|
11
|
+
|
|
12
|
+
import { GlossaristModel } from './base.js';
|
|
13
|
+
import { ConceptRef } from './concept-ref.js';
|
|
14
|
+
import {
|
|
15
|
+
MEMBER_CERTAINTY,
|
|
16
|
+
DEFAULT_MEMBER_CERTAINTY,
|
|
17
|
+
isValidMemberCertainty,
|
|
18
|
+
} from './member-certainty.js';
|
|
19
|
+
|
|
20
|
+
export class PartitiveMember extends GlossaristModel {
|
|
21
|
+
constructor(data = {}) {
|
|
22
|
+
super();
|
|
23
|
+
this.ref = data.ref instanceof ConceptRef
|
|
24
|
+
? data.ref
|
|
25
|
+
: new ConceptRef(data.ref ?? {});
|
|
26
|
+
this.certainty = _resolveCertainty(data.certainty);
|
|
27
|
+
|
|
28
|
+
_assertNonEmptyRef(this.ref);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get isConfirmed() {
|
|
32
|
+
return this.certainty === MEMBER_CERTAINTY.CONFIRMED;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get isPossible() {
|
|
36
|
+
return this.certainty === MEMBER_CERTAINTY.POSSIBLE;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
identity() {
|
|
40
|
+
return PartitiveMember.identityOf(this);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
toJSON() {
|
|
44
|
+
const obj = { ref: this.ref.toJSON() };
|
|
45
|
+
if (this.certainty !== DEFAULT_MEMBER_CERTAINTY) {
|
|
46
|
+
obj.certainty = this.certainty;
|
|
47
|
+
}
|
|
48
|
+
return obj;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
static identityOf(value) {
|
|
52
|
+
const v = value ?? {};
|
|
53
|
+
const ref = v.ref ?? {};
|
|
54
|
+
return `${ref.source ?? ''}|${ref.id ?? ''}|${v.certainty ?? DEFAULT_MEMBER_CERTAINTY}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
static fromJSON(data) {
|
|
58
|
+
return new PartitiveMember(data);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function _resolveCertainty(value) {
|
|
63
|
+
const cert = value ?? DEFAULT_MEMBER_CERTAINTY;
|
|
64
|
+
if (!isValidMemberCertainty(cert)) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`PartitiveMember.certainty has invalid value ${JSON.stringify(value)}; ` +
|
|
67
|
+
`must be one of: ${MEMBER_CERTAINTY.VALUES.join(', ')}`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
return cert;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function _assertNonEmptyRef(ref) {
|
|
74
|
+
if (!ref.source && !ref.id && !ref.text) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
'PartitiveMember.ref must be non-empty (source, id, or text required)',
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// PartitiveRelation — a one-to-many partitive decomposition.
|
|
2
|
+
//
|
|
3
|
+
// Connects a comprehensive concept (superordinate concept partitive)
|
|
4
|
+
// to two or more partitive concepts (subordinate concepts partitive)
|
|
5
|
+
// which fitted together constitute the comprehensive.
|
|
6
|
+
//
|
|
7
|
+
// Renamed from the v1 `PartitiveHyperedge` per TODO.partitive-relation-v2
|
|
8
|
+
// item 01. ISO 704 / 1087-1 / 12620 call this a *partitive relation*,
|
|
9
|
+
// not a hyperedge.
|
|
10
|
+
//
|
|
11
|
+
// Field renames (v1 → v2):
|
|
12
|
+
// parts [1..*] → partitives [2..*] (as PartitiveMember)
|
|
13
|
+
// enumeration: closed|open → completeness: complete|partial
|
|
14
|
+
// markers: [double, dashed] → plurality: TypeSharedPlurality (structured)
|
|
15
|
+
// content: LocalizedString → (dropped — structural edges carry no prose)
|
|
16
|
+
//
|
|
17
|
+
// New field:
|
|
18
|
+
// criterion: LocalizedString [0..1] (ISO 12620 coordinate-concept coherence)
|
|
19
|
+
//
|
|
20
|
+
// Distinguished from binary `has_part` / `is_part_of` edges, which
|
|
21
|
+
// express pairwise part-of assertions without completeness, plurality,
|
|
22
|
+
// or criterion claims. A single partitive should be expressed as a
|
|
23
|
+
// binary edge, not a PartitiveRelation (ISO requires "two or more").
|
|
24
|
+
|
|
25
|
+
import { GlossaristModel } from './base.js';
|
|
26
|
+
import { ConceptRef } from './concept-ref.js';
|
|
27
|
+
import { PartitiveMember } from './partitive-member.js';
|
|
28
|
+
import { TypeSharedPlurality } from './type-shared-plurality.js';
|
|
29
|
+
import {
|
|
30
|
+
COMPLETENESS,
|
|
31
|
+
COMPLETENESS_VALUES,
|
|
32
|
+
DEFAULT_COMPLETENESS,
|
|
33
|
+
isValidCompleteness,
|
|
34
|
+
} from './completeness.js';
|
|
35
|
+
|
|
36
|
+
export class PartitiveRelation extends GlossaristModel {
|
|
37
|
+
constructor(data = {}) {
|
|
38
|
+
super();
|
|
39
|
+
this.comprehensive = _ensureComprehensive(data.comprehensive);
|
|
40
|
+
this.partitives = _ensurePartitives(data.partitives);
|
|
41
|
+
this.completeness = _resolveCompleteness(data.completeness);
|
|
42
|
+
this.plurality = data.plurality == null
|
|
43
|
+
? null
|
|
44
|
+
: (data.plurality instanceof TypeSharedPlurality
|
|
45
|
+
? data.plurality
|
|
46
|
+
: new TypeSharedPlurality(data.plurality));
|
|
47
|
+
this.criterion = _normalizeCriterion(data.criterion);
|
|
48
|
+
|
|
49
|
+
_assertNoSelfLoop(this.comprehensive, this.partitives);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
get isComplete() { return this.completeness === COMPLETENESS.COMPLETE; }
|
|
53
|
+
get isPartial() { return this.completeness === COMPLETENESS.PARTIAL; }
|
|
54
|
+
|
|
55
|
+
// ISO 12620 coordinate-concept predicate: a relation is "coordinate"
|
|
56
|
+
// when it has ≥2 partitives (all partitives within one relation are
|
|
57
|
+
// coordinate concepts by definition).
|
|
58
|
+
get isCoordinate() { return this.partitives.length >= 2; }
|
|
59
|
+
|
|
60
|
+
hasPlurality() { return this.plurality != null; }
|
|
61
|
+
hasCriterion() { return this.criterion != null; }
|
|
62
|
+
|
|
63
|
+
identity() {
|
|
64
|
+
return PartitiveRelation.identityOf(this);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
toJSON() {
|
|
68
|
+
const obj = {
|
|
69
|
+
comprehensive: this.comprehensive.toJSON(),
|
|
70
|
+
partitives: this.partitives.map(p => p.toJSON()),
|
|
71
|
+
completeness: this.completeness,
|
|
72
|
+
};
|
|
73
|
+
if (this.plurality != null) obj.plurality = this.plurality.toJSON();
|
|
74
|
+
if (this.criterion != null) obj.criterion = { ...this.criterion };
|
|
75
|
+
return obj;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Stable identity for diffing: same comprehensive + same partitive
|
|
79
|
+
// set (by ref identity) = same relation. Changes to completeness,
|
|
80
|
+
// plurality, or criterion produce "changed" entries rather than
|
|
81
|
+
// add+remove. Matches the v1 hyperedgeIdentity semantics.
|
|
82
|
+
static identityOf(value) {
|
|
83
|
+
const v = value ?? {};
|
|
84
|
+
const c = v.comprehensive ?? {};
|
|
85
|
+
const partitiveKeys = Array.isArray(v.partitives)
|
|
86
|
+
? v.partitives.map(p => {
|
|
87
|
+
const r = p?.ref ?? p ?? {};
|
|
88
|
+
return `${r.source ?? ''}:${r.id ?? ''}`;
|
|
89
|
+
}).sort()
|
|
90
|
+
: [];
|
|
91
|
+
return `${c.source ?? ''}:${c.id ?? ''}|${partitiveKeys.join('|')}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
static fromJSON(data) {
|
|
95
|
+
return new PartitiveRelation(data);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function _ensureComprehensive(value) {
|
|
100
|
+
const ref = value instanceof ConceptRef ? value : new ConceptRef(value ?? {});
|
|
101
|
+
if (!ref.source && !ref.id && !ref.text) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
'PartitiveRelation.comprehensive must be a non-empty ConceptReference ' +
|
|
104
|
+
'(source, id, or text required)',
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
return ref;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function _ensurePartitives(values) {
|
|
111
|
+
if (!Array.isArray(values)) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`PartitiveRelation.partitives must be an array (got ${typeof values})`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
if (values.length < 2) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`PartitiveRelation requires ≥2 partitives (ISO 704); got ${values.length}`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
return values.map(v => v instanceof PartitiveMember ? v : new PartitiveMember(v));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function _resolveCompleteness(value) {
|
|
125
|
+
const c = value ?? DEFAULT_COMPLETENESS;
|
|
126
|
+
if (!isValidCompleteness(c)) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`PartitiveRelation.completeness has invalid value ${JSON.stringify(value)}; ` +
|
|
129
|
+
`must be one of: ${COMPLETENESS_VALUES.join(', ')}`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
return c;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function _normalizeCriterion(value) {
|
|
136
|
+
if (value == null) return null;
|
|
137
|
+
if (typeof value === 'string') {
|
|
138
|
+
return value.length > 0 ? { default: value } : null;
|
|
139
|
+
}
|
|
140
|
+
if (typeof value === 'object') {
|
|
141
|
+
const entries = Object.entries(value).filter(([, v]) => typeof v === 'string');
|
|
142
|
+
return entries.length > 0 ? Object.fromEntries(entries) : null;
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function _assertNoSelfLoop(comprehensive, partitives) {
|
|
148
|
+
const cKey = `${comprehensive.source ?? ''}:${comprehensive.id ?? ''}`;
|
|
149
|
+
for (const p of partitives) {
|
|
150
|
+
const ref = p.ref;
|
|
151
|
+
const pKey = `${ref.source ?? ''}:${ref.id ?? ''}`;
|
|
152
|
+
if (pKey === cKey && (ref.source || ref.id)) {
|
|
153
|
+
throw new Error(
|
|
154
|
+
'PartitiveRelation.partitives cannot include the comprehensive (no self-loops)',
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// PluralityMarker — diagram notation flags from VIM concept-relationship
|
|
2
|
+
// diagrams. Travels with a PartitiveHyperedge so the rendering layer
|
|
3
|
+
// can reproduce the source diagram's notation.
|
|
4
|
+
//
|
|
5
|
+
// double — close-set double line at the receiver end. Several
|
|
6
|
+
// partitive concepts of the same type are involved.
|
|
7
|
+
// dashed — broken/dashed line. Plurality is uncertain.
|
|
8
|
+
//
|
|
9
|
+
// Orthogonal to PartitiveEnumeration — both may be set on the same
|
|
10
|
+
// hyperedge.
|
|
11
|
+
|
|
12
|
+
export const PLURALITY_MARKER = Object.freeze({
|
|
13
|
+
DOUBLE: 'double',
|
|
14
|
+
DASHED: 'dashed',
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export const PLURALITY_MARKER_VALUES = Object.freeze([
|
|
18
|
+
PLURALITY_MARKER.DOUBLE,
|
|
19
|
+
PLURALITY_MARKER.DASHED,
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
export function isValidPluralityMarker(value) {
|
|
23
|
+
return PLURALITY_MARKER_VALUES.includes(value);
|
|
24
|
+
}
|