glossarist 0.4.35 → 0.4.37

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 (43) hide show
  1. package/package.json +15 -6
  2. package/src/models/abstract-hyperedge.ts +164 -0
  3. package/src/models/base.ts +93 -0
  4. package/src/models/{completeness.js → completeness.ts} +3 -6
  5. package/src/models/concept-date.ts +48 -0
  6. package/src/models/concept-ref.ts +64 -0
  7. package/src/models/concept-source.ts +96 -0
  8. package/src/models/concept-system-type.ts +39 -0
  9. package/src/models/concept-system.ts +152 -0
  10. package/src/models/concept-type.ts +33 -0
  11. package/src/models/{dataset-color.js → dataset-color.ts} +26 -17
  12. package/src/models/{definition-type.js → definition-type.ts} +8 -1
  13. package/src/models/enum.ts +60 -0
  14. package/src/models/equivalence-degree.ts +34 -0
  15. package/src/models/generic-hyperedge.ts +72 -0
  16. package/src/models/generic-member.ts +80 -0
  17. package/src/models/{hyperedge-index.js → hyperedge-index.ts} +32 -18
  18. package/src/models/{hyperedge-member.js → hyperedge-member.ts} +38 -26
  19. package/src/models/{hyperedge-registry.js → hyperedge-registry.ts} +59 -29
  20. package/src/models/{multiplicity.js → multiplicity.ts} +58 -29
  21. package/src/models/{partitive-count.js → partitive-count.ts} +3 -1
  22. package/src/models/partitive-enumeration.ts +27 -0
  23. package/src/models/partitive-hyperedge.ts +90 -0
  24. package/src/models/{partitive-member.js → partitive-member.ts} +18 -17
  25. package/src/models/{partitive-presence.js → partitive-presence.ts} +3 -1
  26. package/src/models/{plurality-marker.js → plurality-marker.ts} +14 -7
  27. package/src/models/sequential-hyperedge.ts +70 -0
  28. package/src/models/{sequential-member.js → sequential-member.ts} +6 -3
  29. package/src/models/abstract-hyperedge.js +0 -119
  30. package/src/models/base.js +0 -32
  31. package/src/models/concept-date.js +0 -36
  32. package/src/models/concept-ref.js +0 -36
  33. package/src/models/concept-source.js +0 -52
  34. package/src/models/concept-system-type.js +0 -26
  35. package/src/models/concept-system.js +0 -117
  36. package/src/models/concept-type.js +0 -25
  37. package/src/models/enum.js +0 -42
  38. package/src/models/equivalence-degree.js +0 -25
  39. package/src/models/generic-hyperedge.js +0 -55
  40. package/src/models/generic-member.js +0 -82
  41. package/src/models/partitive-enumeration.js +0 -23
  42. package/src/models/partitive-hyperedge.js +0 -91
  43. package/src/models/sequential-hyperedge.js +0 -56
@@ -19,21 +19,25 @@
19
19
 
20
20
  import { GlossaristModel } from './base.js';
21
21
  import { ConceptRef } from './concept-ref.js';
22
- import {
23
- PARTITIVE_PRESENCE,
24
- PARTITIVE_PRESENCE_VALUES,
25
- DEFAULT_PRESENCE,
26
- isValidPresence,
27
- } from './partitive-presence.js';
28
- import {
29
- PARTITIVE_COUNT_VALUES,
30
- DEFAULT_COUNT,
31
- isValidCount,
32
- } from './partitive-count.js';
22
+ import type { ConceptRefJson } from './concept-ref.js';
23
+ import { PARTITIVE_PRESENCE_VALUES, DEFAULT_PRESENCE, isValidPresence } from './partitive-presence.js';
24
+ import type { PartitivePresence } from './partitive-presence.js';
25
+ import { PARTITIVE_COUNT_VALUES, DEFAULT_COUNT, isValidCount } from './partitive-count.js';
26
+ import type { PartitiveCount } from './partitive-count.js';
33
27
  import { multiplicityFromPair } from './multiplicity.js';
34
28
 
35
- export class HyperedgeMember extends GlossaristModel {
36
- constructor(data = {}) {
29
+ export interface HyperedgeMemberJson {
30
+ ref: ConceptRefJson | ConceptRef;
31
+ presence?: PartitivePresence;
32
+ count?: PartitiveCount;
33
+ }
34
+
35
+ export abstract class HyperedgeMember extends GlossaristModel {
36
+ readonly ref: ConceptRef;
37
+ readonly presence: PartitivePresence;
38
+ readonly count: PartitiveCount;
39
+
40
+ constructor(data: HyperedgeMemberJson = { ref: {} }) {
37
41
  super();
38
42
  this.ref = _ensureRef(data.ref);
39
43
  this.presence = _resolvePresence(data.presence);
@@ -42,32 +46,40 @@ export class HyperedgeMember extends GlossaristModel {
42
46
  _assertValidPair(this.presence, this.count);
43
47
  }
44
48
 
45
- get isRequired() { return this.presence === PARTITIVE_PRESENCE.REQUIRED; }
46
- get isOptional() { return this.presence === PARTITIVE_PRESENCE.OPTIONAL; }
49
+ get isRequired(): boolean { return this.presence === 'required'; }
50
+ get isOptional(): boolean { return this.presence === 'optional'; }
47
51
 
48
- identity() {
52
+ override identity(): string {
49
53
  return HyperedgeMember.identityOf(this);
50
54
  }
51
55
 
52
- toJSON() {
53
- const obj = { ref: this.ref.toJSON() };
56
+ override toJSON(): HyperedgeMemberJson {
57
+ const obj: HyperedgeMemberJson = { ref: this.ref.toJSON() };
54
58
  if (this.presence !== DEFAULT_PRESENCE) obj.presence = this.presence;
55
59
  if (this.count !== DEFAULT_COUNT) obj.count = this.count;
56
60
  return obj;
57
61
  }
58
62
 
59
- static identityOf(value) {
63
+ static identityOf(
64
+ value: { ref?: { source?: string | null; id?: string | null } | null } | null | undefined,
65
+ ): string {
60
66
  const v = value ?? {};
61
67
  const ref = v.ref ?? {};
62
68
  return `${ref.source ?? ''}:${ref.id ?? ''}`;
63
69
  }
64
70
 
65
- static fromJSON(data) {
66
- return new HyperedgeMember(data);
71
+ static override fromJSON(_data: HyperedgeMemberJson): HyperedgeMember {
72
+ // Abstract base — subclasses MUST override. The base
73
+ // implementation is a typed marker that dispatches to the wrong
74
+ // class would surface as a runtime error rather than silently
75
+ // returning a mis-typed instance.
76
+ throw new Error(
77
+ 'HyperedgeMember.fromJSON must be overridden by a subclass',
78
+ );
67
79
  }
68
80
  }
69
81
 
70
- function _ensureRef(value) {
82
+ function _ensureRef(value: ConceptRefJson | ConceptRef | null | undefined): ConceptRef {
71
83
  const ref = value instanceof ConceptRef ? value : new ConceptRef(value ?? {});
72
84
  if (!ref.source && !ref.id && !ref.text) {
73
85
  throw new Error(
@@ -78,7 +90,7 @@ function _ensureRef(value) {
78
90
  return ref;
79
91
  }
80
92
 
81
- function _resolvePresence(value) {
93
+ function _resolvePresence(value: PartitivePresence | null | undefined): PartitivePresence {
82
94
  const p = value ?? DEFAULT_PRESENCE;
83
95
  if (!isValidPresence(p)) {
84
96
  throw new Error(
@@ -89,7 +101,7 @@ function _resolvePresence(value) {
89
101
  return p;
90
102
  }
91
103
 
92
- function _resolveCount(value) {
104
+ function _resolveCount(value: PartitiveCount | null | undefined): PartitiveCount {
93
105
  const c = value ?? DEFAULT_COUNT;
94
106
  if (!isValidCount(c)) {
95
107
  throw new Error(
@@ -100,9 +112,9 @@ function _resolveCount(value) {
100
112
  return c;
101
113
  }
102
114
 
103
- function _assertValidPair(presence, count) {
115
+ function _assertValidPair(presence: PartitivePresence, count: PartitiveCount): void {
104
116
  // Delegate to the multiplicity SSOT. The canonical error string
105
- // lives in one place: multiplicity.js. Calling multiplicityFromPair
117
+ // lives in one place: multiplicity.ts. Calling multiplicityFromPair
106
118
  // throws on the invalid combination; we don't need to duplicate the
107
119
  // error message here.
108
120
  multiplicityFromPair(presence, count);
@@ -21,18 +21,34 @@
21
21
  // The abstract base (AbstractHyperedge) has no wireKey and is skipped
22
22
  // by register() — it cannot be instantiated through the registry.
23
23
 
24
+ import type { AbstractHyperedge } from './abstract-hyperedge.js';
25
+
26
+ /**
27
+ * Shape a registered hyperedge class must satisfy. Every leaf class
28
+ * in src/models/ matches this shape (declared as static fields).
29
+ */
30
+ export interface HyperedgeClass {
31
+ readonly wireKey: string;
32
+ readonly typeTag: string;
33
+ readonly rdfType: string;
34
+ readonly memberClass: new (...args: never[]) => unknown;
35
+ readonly v1WireKeys?: ReadonlyArray<string>;
36
+ readonly kindLabel?: string;
37
+ readonly name: string;
38
+ }
39
+
24
40
  class HyperedgeRegistry {
25
- static _byWireKey = new Map();
26
- static _byTypeTag = new Map();
27
- static _byRdfType = new Map();
41
+ static _byWireKey: Map<string, HyperedgeClass> = new Map();
42
+ static _byTypeTag: Map<string, HyperedgeClass> = new Map();
43
+ static _byRdfType: Map<string, HyperedgeClass> = new Map();
28
44
 
29
- static register(cls) {
45
+ static register(cls: HyperedgeClass | undefined): boolean {
30
46
  if (!cls || !cls.wireKey || !cls.typeTag || !cls.rdfType) return false;
31
47
  if (this._byWireKey.has(cls.wireKey)) {
32
48
  throw new Error(
33
49
  `HyperedgeRegistry: duplicate wireKey '${cls.wireKey}' ` +
34
50
  `(trying to register ${cls.name}; already registered to ` +
35
- `${this._byWireKey.get(cls.wireKey).name})`,
51
+ `${this._byWireKey.get(cls.wireKey)?.name})`,
36
52
  );
37
53
  }
38
54
  if (this._byTypeTag.has(cls.typeTag)) {
@@ -53,9 +69,11 @@ class HyperedgeRegistry {
53
69
  return true;
54
70
  }
55
71
 
56
- // Test-only. Removes a class from all three indexes. Used by the OCP
57
- // spec to clean up mock types it adds during a test run.
58
- static unregister(cls) {
72
+ /**
73
+ * Test-only. Removes a class from all three indexes. Used by the OCP
74
+ * spec to clean up mock types it adds during a test run.
75
+ */
76
+ static unregister(cls: HyperedgeClass | undefined): boolean {
59
77
  if (!cls) return false;
60
78
  const r1 = this._byWireKey.delete(cls.wireKey);
61
79
  const r2 = this._byTypeTag.delete(cls.typeTag);
@@ -63,14 +81,20 @@ class HyperedgeRegistry {
63
81
  return r1 || r2 || r3;
64
82
  }
65
83
 
66
- static forWireKey(key) { return this._byWireKey.get(key) ?? null; }
67
- static forTypeTag(tag) { return this._byTypeTag.get(tag) ?? null; }
68
- static forRdfType(type) { return this._byRdfType.get(type) ?? null; }
84
+ static forWireKey(key: string): HyperedgeClass | null {
85
+ return this._byWireKey.get(key) ?? null;
86
+ }
87
+ static forTypeTag(tag: string): HyperedgeClass | null {
88
+ return this._byTypeTag.get(tag) ?? null;
89
+ }
90
+ static forRdfType(type: string): HyperedgeClass | null {
91
+ return this._byRdfType.get(type) ?? null;
92
+ }
69
93
 
70
- static allClasses() {
94
+ static allClasses(): HyperedgeClass[] {
71
95
  // Dedup in case the same class is reachable via multiple indexes.
72
- const seen = new Set();
73
- const out = [];
96
+ const seen = new Set<HyperedgeClass>();
97
+ const out: HyperedgeClass[] = [];
74
98
  for (const cls of this._byTypeTag.values()) {
75
99
  if (!seen.has(cls)) {
76
100
  seen.add(cls);
@@ -80,14 +104,16 @@ class HyperedgeRegistry {
80
104
  return out;
81
105
  }
82
106
 
83
- static allWireKeys() { return [...this._byWireKey.keys()]; }
84
- static allTypeTags() { return [...this._byTypeTag.keys()]; }
107
+ static allWireKeys(): string[] { return [...this._byWireKey.keys()]; }
108
+ static allTypeTags(): string[] { return [...this._byTypeTag.keys()]; }
85
109
 
86
- // Snapshot of every registered wire-shape identifier (v2 wireKey plus
87
- // any v1 legacy keys). Used by the parser to reserve STRUCTURAL_KEYS
88
- // and to know which top-level YAML keys to walk for hyperedge data.
89
- static allWireAndLegacyKeys() {
90
- const out = [];
110
+ /**
111
+ * Snapshot of every registered wire-shape identifier (v2 wireKey plus
112
+ * any v1 legacy keys). Used by the parser to reserve STRUCTURAL_KEYS
113
+ * and to know which top-level YAML keys to walk for hyperedge data.
114
+ */
115
+ static allWireAndLegacyKeys(): string[] {
116
+ const out: string[] = [];
91
117
  for (const cls of this.allClasses()) {
92
118
  out.push(cls.wireKey);
93
119
  for (const k of cls.v1WireKeys ?? []) out.push(k);
@@ -96,15 +122,19 @@ class HyperedgeRegistry {
96
122
  }
97
123
  }
98
124
 
99
- // Helper: partition a unified hyperedge array by wire key. Used by
100
- // Concept.toJSON, ConceptSerializer, and diff-patch to emit per-type
101
- // wire keys without per-type branching in those files. Adding a new
102
- // hyperedge type "just works" because the group is keyed by the
103
- // class's static wireKey.
104
- export function groupHyperedgesByWireKey(hyperedges) {
105
- const out = {};
125
+ /**
126
+ * Helper: partition a unified hyperedge array by wire key. Used by
127
+ * Concept.toJSON, ConceptSerializer, and diff-patch to emit per-type
128
+ * wire keys without per-type branching in those files. Adding a new
129
+ * hyperedge type "just works" because the group is keyed by the
130
+ * class's static wireKey.
131
+ */
132
+ export function groupHyperedgesByWireKey(
133
+ hyperedges: ReadonlyArray<AbstractHyperedge> | null | undefined,
134
+ ): Record<string, AbstractHyperedge[]> {
135
+ const out: Record<string, AbstractHyperedge[]> = {};
106
136
  for (const h of hyperedges ?? []) {
107
- const key = h?.constructor?.wireKey;
137
+ const key = (h?.constructor as unknown as HyperedgeClass | undefined)?.wireKey;
108
138
  if (!key) continue;
109
139
  if (!out[key]) out[key] = [];
110
140
  out[key].push(h);
@@ -18,34 +18,47 @@
18
18
  // mapping. Both MULTIPLICITY constants and MULTIPLICITY_VALUES are
19
19
  // derived from this single table; no duplicate string literals.
20
20
 
21
- import { PARTITIVE_PRESENCE } from './partitive-presence.js';
22
- import { PARTITIVE_COUNT } from './partitive-count.js';
21
+ import type { PartitivePresence } from './partitive-presence.js';
22
+ import type { PartitiveCount } from './partitive-count.js';
23
23
 
24
- const NAME_BY_PAIR = Object.freeze({
25
- [`${PARTITIVE_PRESENCE.REQUIRED}:${PARTITIVE_COUNT.EXACTLY_ONE}`]: 'compulsory',
26
- [`${PARTITIVE_PRESENCE.OPTIONAL}:${PARTITIVE_COUNT.EXACTLY_ONE}`]: 'optional',
27
- [`${PARTITIVE_PRESENCE.REQUIRED}:${PARTITIVE_COUNT.MULTIPLE}`]: 'compulsory_multiple',
28
- [`${PARTITIVE_PRESENCE.OPTIONAL}:${PARTITIVE_COUNT.MULTIPLE}`]: 'optional_multiple',
29
- [`${PARTITIVE_PRESENCE.REQUIRED}:${PARTITIVE_COUNT.AT_LEAST_ONE}`]: 'compulsory_at_least_one',
24
+ export type MultiplicityName =
25
+ | 'compulsory'
26
+ | 'optional'
27
+ | 'compulsory_multiple'
28
+ | 'optional_multiple'
29
+ | 'compulsory_at_least_one';
30
+
31
+ const NAME_BY_PAIR: Readonly<Record<string, MultiplicityName>> = Object.freeze({
32
+ 'required:exactly_one': 'compulsory',
33
+ 'optional:exactly_one': 'optional',
34
+ 'required:multiple': 'compulsory_multiple',
35
+ 'optional:multiple': 'optional_multiple',
36
+ 'required:at_least_one': 'compulsory_at_least_one',
30
37
  });
31
38
 
32
- const PAIR_BY_NAME = Object.freeze(
39
+ const PAIR_BY_NAME: Readonly<Record<MultiplicityName, string>> = Object.freeze(
33
40
  Object.fromEntries(
34
41
  Object.entries(NAME_BY_PAIR).map(([pair, name]) => [name, pair]),
35
- ),
42
+ ) as Record<MultiplicityName, string>,
36
43
  );
37
44
 
38
- export const MULTIPLICITY = Object.freeze(
39
- Object.fromEntries(
40
- Object.values(NAME_BY_PAIR).map(name => [name.toUpperCase(), name]),
41
- ),
42
- );
45
+ export const MULTIPLICITY: Readonly<Record<Uppercase<MultiplicityName>, MultiplicityName>> =
46
+ Object.freeze(
47
+ Object.fromEntries(
48
+ Object.values(NAME_BY_PAIR).map((name) => [name.toUpperCase(), name]),
49
+ ),
50
+ ) as Readonly<Record<Uppercase<MultiplicityName>, MultiplicityName>>;
43
51
 
44
- export const MULTIPLICITY_VALUES = Object.freeze(Object.values(NAME_BY_PAIR));
52
+ export const MULTIPLICITY_VALUES: ReadonlyArray<MultiplicityName> = Object.freeze(
53
+ Object.values(NAME_BY_PAIR),
54
+ );
45
55
 
46
- export const DEFAULT_MULTIPLICITY = MULTIPLICITY.COMPULSORY;
56
+ export const DEFAULT_MULTIPLICITY: MultiplicityName = MULTIPLICITY.COMPULSORY;
47
57
 
48
- export function multiplicityFromPair(presence, count) {
58
+ export function multiplicityFromPair(
59
+ presence: PartitivePresence,
60
+ count: PartitiveCount,
61
+ ): MultiplicityName {
49
62
  const name = NAME_BY_PAIR[`${presence}:${count}`];
50
63
  if (name == null) {
51
64
  throw invalidCombinationError(presence, count);
@@ -53,28 +66,43 @@ export function multiplicityFromPair(presence, count) {
53
66
  return name;
54
67
  }
55
68
 
56
- // Canonical error message for the invalid (presence, count) combination.
57
- // Single source of truth — hyperedge-member.js delegates to this so the
58
- // error string doesn't drift across the two files. Phase 8 of
59
- // TODO.abstract-hyperedge.
60
- export function invalidCombinationError(presence, count) {
69
+ /**
70
+ * Canonical error for the invalid (presence, count) combination.
71
+ * Single source of truth — hyperedge-member.ts delegates to this so
72
+ * the error string doesn't drift across the two files.
73
+ */
74
+ export function invalidCombinationError(
75
+ presence: unknown,
76
+ count: unknown,
77
+ ): Error {
61
78
  return new Error(
62
79
  `Invalid multiplicity combination: presence=${presence}, count=${count}. ` +
63
80
  `(optional + at_least_one collapses to optional + multiple — use count=multiple.)`,
64
81
  );
65
82
  }
66
83
 
67
- export function pairFromMultiplicity(name) {
84
+ export function pairFromMultiplicity(
85
+ name: MultiplicityName,
86
+ ): { presence: PartitivePresence; count: PartitiveCount } {
68
87
  const pair = PAIR_BY_NAME[name];
69
88
  if (pair == null) {
70
89
  throw new Error(`Unknown multiplicity: ${JSON.stringify(name)}`);
71
90
  }
72
- const [presence, count] = pair.split(':');
91
+ const [presence, count] = pair.split(':') as [PartitivePresence, PartitiveCount];
73
92
  return { presence, count };
74
93
  }
75
94
 
76
- export function isValidMultiplicity(name) {
77
- return Object.prototype.hasOwnProperty.call(PAIR_BY_NAME, name);
95
+ export function isValidMultiplicity(name: unknown): name is MultiplicityName {
96
+ return (
97
+ typeof name === 'string' &&
98
+ Object.prototype.hasOwnProperty.call(PAIR_BY_NAME, name)
99
+ );
100
+ }
101
+
102
+ interface MemberWithMultiplicity {
103
+ presence?: PartitivePresence;
104
+ count?: PartitiveCount;
105
+ multiplicity?: string;
78
106
  }
79
107
 
80
108
  /**
@@ -93,7 +121,9 @@ export function isValidMultiplicity(name) {
93
121
  * rather than silently returning 'compulsory' (which would mask the
94
122
  * data corruption).
95
123
  */
96
- export function resolveMultiplicity(member) {
124
+ export function resolveMultiplicity(
125
+ member: MemberWithMultiplicity | null | undefined,
126
+ ): MultiplicityName | string {
97
127
  if (member?.presence && member?.count) {
98
128
  return multiplicityFromPair(member.presence, member.count);
99
129
  }
@@ -102,4 +132,3 @@ export function resolveMultiplicity(member) {
102
132
  }
103
133
  return DEFAULT_MULTIPLICITY;
104
134
  }
105
-
@@ -9,6 +9,8 @@
9
9
 
10
10
  import { makeEnum } from './enum.js';
11
11
 
12
+ export type PartitiveCount = 'exactly_one' | 'at_least_one' | 'multiple';
13
+
12
14
  export const PARTITIVE_COUNT = makeEnum('PARTITIVE_COUNT', {
13
15
  EXACTLY_ONE: 'exactly_one',
14
16
  AT_LEAST_ONE: 'at_least_one',
@@ -16,5 +18,5 @@ export const PARTITIVE_COUNT = makeEnum('PARTITIVE_COUNT', {
16
18
  });
17
19
 
18
20
  export const PARTITIVE_COUNT_VALUES = PARTITIVE_COUNT.VALUES;
19
- export const DEFAULT_COUNT = PARTITIVE_COUNT.EXACTLY_ONE;
21
+ export const DEFAULT_COUNT: PartitiveCount = PARTITIVE_COUNT.EXACTLY_ONE;
20
22
  export const isValidCount = PARTITIVE_COUNT.isValid;
@@ -0,0 +1,27 @@
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
+ export type PartitiveEnumeration = 'closed' | 'open';
9
+
10
+ export const PARTITIVE_ENUMERATION: Readonly<
11
+ Record<'CLOSED' | 'OPEN', PartitiveEnumeration>
12
+ > = Object.freeze({
13
+ CLOSED: 'closed',
14
+ OPEN: 'open',
15
+ });
16
+
17
+ export const PARTITIVE_ENUMERATION_VALUES: ReadonlyArray<PartitiveEnumeration> =
18
+ Object.freeze([PARTITIVE_ENUMERATION.CLOSED, PARTITIVE_ENUMERATION.OPEN]);
19
+
20
+ export function isValidPartitiveEnumeration(
21
+ value: unknown,
22
+ ): value is PartitiveEnumeration {
23
+ return (
24
+ typeof value === 'string' &&
25
+ (PARTITIVE_ENUMERATION_VALUES as readonly string[]).includes(value)
26
+ );
27
+ }
@@ -0,0 +1,90 @@
1
+ // PartitiveHyperedge — an ISO 704:2022 / ISO 1087-1 / ISO 12620
2
+ // partitive relation connecting a comprehensive concept
3
+ // (superordinate concept partitive) to two or more partitive
4
+ // concepts (subordinate concepts partitive) which fitted together
5
+ // constitute the comprehensive.
6
+ //
7
+ // Extends AbstractHyperedge; the leaf-specific concern is the
8
+ // PartitiveMember type for `members`. The wire-level field name
9
+ // stays `partitives` for v1/v2 backward compat.
10
+
11
+ import { AbstractHyperedge } from './abstract-hyperedge.js';
12
+ import type { AbstractHyperedgeJson } from './abstract-hyperedge.js';
13
+ import { PartitiveMember } from './partitive-member.js';
14
+ import type { PartitiveMemberJson } from './partitive-member.js';
15
+ import { HyperedgeRegistry } from './hyperedge-registry.js';
16
+
17
+ export interface PartitiveHyperedgeJson extends AbstractHyperedgeJson {
18
+ members: ReadonlyArray<PartitiveMemberJson | PartitiveMember>;
19
+ /** PartitiveHyperedge wire alias of `members`. Both emitted for
20
+ * backward compat with consumers that read `partitives`. */
21
+ partitives?: ReadonlyArray<PartitiveMemberJson | PartitiveMember>;
22
+ }
23
+
24
+ export class PartitiveHyperedge extends AbstractHyperedge {
25
+ // Per-class metadata block — the OCP contract. External systems
26
+ // (parser, serializer, diff, RDF emitter, validators) read from
27
+ // these statics and the HyperedgeRegistry. Adding a new hyperedge
28
+ // type = declaring these fields on a new subclass + registering
29
+ // it. Nothing else in the codebase changes.
30
+ static wireKey = 'partitive_relations' as const;
31
+ static typeTag = 'partitive_relation' as const;
32
+ static rdfType = 'gloss:PartitiveRelation' as const;
33
+ static memberClass = PartitiveMember;
34
+ static v1WireKeys = ['partitive_hyperedges'];
35
+ static kindLabel = 'PART';
36
+
37
+ declare readonly members: ReadonlyArray<PartitiveMember>;
38
+
39
+ constructor(data: Partial<PartitiveHyperedgeJson> = {}) {
40
+ const memberSource = data.members ?? data.partitives;
41
+ const normalized: AbstractHyperedgeJson = {
42
+ comprehensive: data.comprehensive ?? {},
43
+ members: memberSource ?? [],
44
+ completeness: data.completeness ?? 'complete',
45
+ criterion: data.criterion,
46
+ sources: data.sources,
47
+ notes: data.notes,
48
+ status: data.status,
49
+ };
50
+ super(normalized);
51
+ this.members = (Array.isArray(memberSource) ? memberSource : []).map((m) =>
52
+ m instanceof PartitiveMember ? m : new PartitiveMember(m as PartitiveMemberJson),
53
+ );
54
+ }
55
+
56
+ get partitives(): ReadonlyArray<PartitiveMember> { return this.members; }
57
+
58
+ override toJSON(): PartitiveHyperedgeJson {
59
+ const base = super.toJSON() as Omit<PartitiveHyperedgeJson, 'partitives'>;
60
+ const { members, ...rest } = base;
61
+ return { ...rest, members: members as ReadonlyArray<PartitiveMemberJson | PartitiveMember>, partitives: members };
62
+ }
63
+
64
+ static identityOf(
65
+ value: {
66
+ comprehensive?: { source?: string | null; id?: string | null } | null;
67
+ partitives?: ReadonlyArray<{ ref?: { source?: string | null; id?: string | null } | null } | null>;
68
+ members?: ReadonlyArray<{ ref?: { source?: string | null; id?: string | null } | null } | null>;
69
+ } | null | undefined,
70
+ ): string {
71
+ const v = value ?? {};
72
+ const c = v.comprehensive ?? {};
73
+ const source = v.partitives ?? v.members ?? [];
74
+ const partKeys = source
75
+ .map((p) => {
76
+ const r = p?.ref ?? (p as { source?: string; id?: string } | null) ?? {};
77
+ return `${r.source ?? ''}:${r.id ?? ''}`;
78
+ })
79
+ .sort();
80
+ return `${c.source ?? ''}:${c.id ?? ''}|${partKeys.join('|')}`;
81
+ }
82
+
83
+ static override fromJSON(data: PartitiveHyperedgeJson): PartitiveHyperedge {
84
+ return new PartitiveHyperedge(data);
85
+ }
86
+ }
87
+
88
+ export default PartitiveHyperedge;
89
+
90
+ HyperedgeRegistry.register(PartitiveHyperedge);
@@ -13,36 +13,37 @@
13
13
  // HyperedgeMember, because it is partitive-specific. GenericMember
14
14
  // carries `delimitingCharacteristic` instead (the intension-difference
15
15
  // value per §5.5.4.2.1, which always applies to every generic member).
16
- //
17
- // Matches concept-model/models/concepts/PartitiveMember.lutaml exactly:
18
- // +ref — ConceptRef [1]
19
- // +presence — PartitivePresence [0..1] (default: required)
20
- // +count — PartitiveCount [0..1] (default: exactly_one)
21
- // +is_delimiting — Boolean [0..1] (default: false)
22
- //
23
- // The combination (optional, at_least_one) is invalid: if a part is
24
- // optional, the at-least-one constraint is vacuous and collapses to
25
- // (optional, multiple). Rejected at construction by HyperedgeMember's
26
- // delegate to multiplicityFromPair.
27
16
 
28
17
  import { HyperedgeMember } from './hyperedge-member.js';
18
+ import type { HyperedgeMemberJson } from './hyperedge-member.js';
19
+
20
+ export interface PartitiveMemberJson extends HyperedgeMemberJson {
21
+ is_delimiting?: boolean;
22
+ }
29
23
 
30
24
  export class PartitiveMember extends HyperedgeMember {
31
- constructor(data = {}) {
25
+ readonly is_delimiting: boolean;
26
+
27
+ constructor(data: PartitiveMemberJson = { ref: {} }) {
32
28
  super(data);
33
29
  this.is_delimiting = _resolveDelimiting(data.is_delimiting);
34
30
  }
35
31
 
36
- get isDelimiting() { return this.is_delimiting === true; }
32
+ get isDelimiting(): boolean { return this.is_delimiting === true; }
37
33
 
38
- toJSON() {
34
+ override toJSON(): PartitiveMemberJson {
39
35
  const base = super.toJSON();
40
- if (this.is_delimiting) base.is_delimiting = true;
41
- return base;
36
+ const out: PartitiveMemberJson = { ...base };
37
+ if (this.is_delimiting) out.is_delimiting = true;
38
+ return out;
39
+ }
40
+
41
+ static override fromJSON(data: PartitiveMemberJson): PartitiveMember {
42
+ return new PartitiveMember(data);
42
43
  }
43
44
  }
44
45
 
45
- function _resolveDelimiting(value) {
46
+ function _resolveDelimiting(value: boolean | null | undefined): boolean {
46
47
  if (value == null) return false;
47
48
  if (typeof value !== 'boolean') {
48
49
  throw new Error(
@@ -8,11 +8,13 @@
8
8
 
9
9
  import { makeEnum } from './enum.js';
10
10
 
11
+ export type PartitivePresence = 'required' | 'optional';
12
+
11
13
  export const PARTITIVE_PRESENCE = makeEnum('PARTITIVE_PRESENCE', {
12
14
  REQUIRED: 'required',
13
15
  OPTIONAL: 'optional',
14
16
  });
15
17
 
16
18
  export const PARTITIVE_PRESENCE_VALUES = PARTITIVE_PRESENCE.VALUES;
17
- export const DEFAULT_PRESENCE = PARTITIVE_PRESENCE.REQUIRED;
19
+ export const DEFAULT_PRESENCE: PartitivePresence = PARTITIVE_PRESENCE.REQUIRED;
18
20
  export const isValidPresence = PARTITIVE_PRESENCE.isValid;
@@ -9,16 +9,23 @@
9
9
  // Orthogonal to PartitiveEnumeration — both may be set on the same
10
10
  // hyperedge.
11
11
 
12
- export const PLURALITY_MARKER = Object.freeze({
12
+ export type PluralityMarker = 'double' | 'dashed';
13
+
14
+ export const PLURALITY_MARKER: Readonly<
15
+ Record<'DOUBLE' | 'DASHED', PluralityMarker>
16
+ > = Object.freeze({
13
17
  DOUBLE: 'double',
14
18
  DASHED: 'dashed',
15
19
  });
16
20
 
17
- export const PLURALITY_MARKER_VALUES = Object.freeze([
18
- PLURALITY_MARKER.DOUBLE,
19
- PLURALITY_MARKER.DASHED,
20
- ]);
21
+ export const PLURALITY_MARKER_VALUES: ReadonlyArray<PluralityMarker> =
22
+ Object.freeze([PLURALITY_MARKER.DOUBLE, PLURALITY_MARKER.DASHED]);
21
23
 
22
- export function isValidPluralityMarker(value) {
23
- return PLURALITY_MARKER_VALUES.includes(value);
24
+ export function isValidPluralityMarker(
25
+ value: unknown,
26
+ ): value is PluralityMarker {
27
+ return (
28
+ typeof value === 'string' &&
29
+ (PLURALITY_MARKER_VALUES as readonly string[]).includes(value)
30
+ );
24
31
  }