glossarist 0.4.19 → 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.
Files changed (55) hide show
  1. package/data/concept-model/shapes/glossarist.shacl.ttl +151 -1
  2. package/package.json +1 -1
  3. package/src/concept-diff.js +13 -0
  4. package/src/concept-parser.js +52 -2
  5. package/src/concept-serializer.js +11 -0
  6. package/src/diff/canonical-json.js +31 -0
  7. package/src/diff/change.js +28 -0
  8. package/src/diff/collection-diff.js +57 -25
  9. package/src/diff/concept-diff.js +100 -60
  10. package/src/diff/diff-patch.js +49 -48
  11. package/src/diff/diff-renderer.js +34 -0
  12. package/src/diff/identity.js +22 -0
  13. package/src/diff/index.d.ts +64 -1
  14. package/src/diff/index.js +2 -0
  15. package/src/diff/similarity.js +24 -0
  16. package/src/index.d.ts +2 -0
  17. package/src/index.js +2 -0
  18. package/src/migration/partitive-relation-migrator.js +125 -0
  19. package/src/models/completeness.js +24 -0
  20. package/src/models/concept-date.js +8 -0
  21. package/src/models/concept-ref.js +4 -0
  22. package/src/models/concept-source.js +21 -2
  23. package/src/models/concept.js +55 -0
  24. package/src/models/designation.js +15 -0
  25. package/src/models/detailed-definition.js +11 -0
  26. package/src/models/enum.js +42 -0
  27. package/src/models/hyperedge-colors.js +71 -0
  28. package/src/models/index.d.ts +61 -0
  29. package/src/models/index.js +39 -0
  30. package/src/models/localized-concept.js +29 -0
  31. package/src/models/member-certainty.js +21 -0
  32. package/src/models/partitive-enumeration.js +23 -0
  33. package/src/models/partitive-hyperedge.js +175 -0
  34. package/src/models/partitive-member.js +79 -0
  35. package/src/models/partitive-relation.js +158 -0
  36. package/src/models/plurality-marker.js +24 -0
  37. package/src/models/related-concept.js +5 -0
  38. package/src/models/relation-categories.js +6 -0
  39. package/src/models/type-shared-plurality.js +71 -0
  40. package/src/rdf/gloss-concept.js +26 -0
  41. package/src/rdf/gloss-hyperedge.js +90 -0
  42. package/src/rdf/gloss-partitive-relation.js +109 -0
  43. package/src/rdf/gloss-source.js +1 -1
  44. package/src/rdf/index.d.ts +37 -0
  45. package/src/rdf/index.js +2 -0
  46. package/src/rdf/predicates.d.ts +54 -0
  47. package/src/rdf/predicates.js +27 -0
  48. package/src/reference-resolver.js +49 -0
  49. package/src/validators/binary-has-part-redundancy-rule.js +74 -0
  50. package/src/validators/external-concept-shape-rule.js +60 -0
  51. package/src/validators/index.js +19 -1
  52. package/src/validators/partitive-drift-rule.js +51 -0
  53. package/src/validators/partitive-hyperedge-rule.js +75 -0
  54. package/src/validators/partitive-relation-coherence-rule.js +107 -0
  55. package/src/validators/sourced-from-locality-rule.js +53 -0
@@ -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;
@@ -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
+ }
@@ -25,6 +25,11 @@ export const RELATIONSHIP_TYPES = Object.freeze([
25
25
  // Associative (ISO 10241-1 / ISO 25964)
26
26
  'see', 'related_concept', 'related_concept_broader', 'related_concept_narrower',
27
27
  'references',
28
+ // ExternalConcept resolution (Glossarist extension — v2 partitive-relation)
29
+ // Direction: real concept `provides` substance for an external concept;
30
+ // external concept `provided_by` real concept. Stored on the
31
+ // ExternalConcept. See TODO.partitive-relation-v2 item 07.
32
+ 'provides', 'provided_by',
28
33
  // Comparative (ISO 10241-1)
29
34
  'compare', 'contrast',
30
35
  // Spatiotemporal (ISO 25964 / TBX)
@@ -96,6 +96,11 @@ export const RELATION_CATEGORIES = Object.freeze({
96
96
  // (a version IS a definitional variant of the same concept identity).
97
97
  // Renaming aligns glossarist-js with the canonical taxonomy at
98
98
  // concept-browser:src/data/taxonomies.json.
99
+ //
100
+ // `provides` / `provided_by` (Glossarist v2 extension) live here too:
101
+ // they record ExternalConcept resolution — a definitional linkage
102
+ // between a placeholder and the concept that fills its substance.
103
+ // See TODO.partitive-relation-v2 item 07.
99
104
  definitional: Object.freeze({
100
105
  label: 'Definitional',
101
106
  description: 'ISO 19135 concept-to-concept relations: definition, part, instance, inheritance, versioning.',
@@ -107,6 +112,7 @@ export const RELATION_CATEGORIES = Object.freeze({
107
112
  'inherits', 'inherited_by',
108
113
  'has_version', 'version_of',
109
114
  'current_version', 'current_version_of',
115
+ 'provides', 'provided_by',
110
116
  ]),
111
117
  }),
112
118
  });