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
@@ -0,0 +1,152 @@
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
+ import type { LocalizedString } from './generic-member.js';
17
+
18
+ export interface ConceptSystemJson {
19
+ id: string;
20
+ name: LocalizedString;
21
+ type: string;
22
+ domain?: string | null;
23
+ purpose?: LocalizedString | null;
24
+ members: ReadonlyArray<unknown>;
25
+ hyperedges?: ReadonlyArray<unknown>;
26
+ rootConcepts?: ReadonlyArray<unknown>;
27
+ sources?: ReadonlyArray<unknown>;
28
+ status?: string | null;
29
+ }
30
+
31
+ export type ConceptSystemType = string;
32
+
33
+ export class ConceptSystem extends GlossaristModel {
34
+ readonly id: string;
35
+ readonly name: LocalizedString;
36
+ readonly type: ConceptSystemType;
37
+ readonly domain: string | null;
38
+ readonly purpose: LocalizedString | null;
39
+ readonly members: ReadonlyArray<ConceptRef>;
40
+ readonly hyperedges: ReadonlyArray<unknown>;
41
+ readonly rootConcepts: ReadonlyArray<ConceptRef>;
42
+ readonly sources: ReadonlyArray<unknown>;
43
+ readonly status: string | null;
44
+
45
+ constructor(data: Partial<ConceptSystemJson> = {}) {
46
+ super();
47
+ this.id = _ensureNonEmptyString(data.id, 'id');
48
+ this.name = _ensureName(data.name);
49
+ this.type = _resolveType(data.type);
50
+ this.domain = data.domain ?? null;
51
+ this.purpose = data.purpose ?? null;
52
+ this.members = _ensureMembers(data.members);
53
+ this.hyperedges = Array.isArray(data.hyperedges) ? [...data.hyperedges] : [];
54
+ this.rootConcepts = Array.isArray(data.rootConcepts)
55
+ ? data.rootConcepts.map((c) =>
56
+ c instanceof ConceptRef ? c : new ConceptRef(c as { source?: string; id?: string; text?: string }),
57
+ )
58
+ : [];
59
+ this.sources = Array.isArray(data.sources) ? [...data.sources] : [];
60
+ this.status = data.status ?? null;
61
+ }
62
+
63
+ get isGeneric(): boolean { return this.type === CONCEPT_SYSTEM_TYPE.GENERIC; }
64
+ get isPartitive(): boolean { return this.type === CONCEPT_SYSTEM_TYPE.PARTITIVE; }
65
+ get isSequential(): boolean { return this.type === CONCEPT_SYSTEM_TYPE.SEQUENTIAL; }
66
+ get isAssociative(): boolean { return this.type === CONCEPT_SYSTEM_TYPE.ASSOCIATIVE; }
67
+ get isMixed(): boolean { return this.type === CONCEPT_SYSTEM_TYPE.MIXED; }
68
+
69
+ hasHyperedge(edgeId: string): boolean {
70
+ return (this.hyperedges as ReadonlyArray<string>).includes(edgeId);
71
+ }
72
+
73
+ hasMember(refOrUri: string | { source?: string | null; id?: string | null } | null | undefined): boolean {
74
+ const uri = typeof refOrUri === 'string' ? refOrUri : _refUri(refOrUri);
75
+ return this.members.some((m) => _refUri(m) === uri);
76
+ }
77
+
78
+ override identity(): string {
79
+ return this.id;
80
+ }
81
+
82
+ override toJSON(): ConceptSystemJson {
83
+ const obj: ConceptSystemJson = {
84
+ id: this.id,
85
+ name: { ...this.name },
86
+ type: this.type,
87
+ members: this.members.map((m) => m.toJSON()),
88
+ };
89
+ if (this.domain != null) obj.domain = this.domain;
90
+ if (this.purpose != null) obj.purpose = { ...this.purpose };
91
+ if (this.hyperedges.length > 0) obj.hyperedges = [...this.hyperedges];
92
+ if (this.rootConcepts.length > 0) obj.rootConcepts = this.rootConcepts.map((c) => c.toJSON());
93
+ if (this.sources.length > 0) obj.sources = [...this.sources];
94
+ if (this.status != null) obj.status = this.status;
95
+ return obj;
96
+ }
97
+
98
+ static override fromJSON(data: ConceptSystemJson): ConceptSystem {
99
+ return new ConceptSystem(data);
100
+ }
101
+ }
102
+
103
+ function _ensureNonEmptyString(value: unknown, fieldName: string): string {
104
+ if (typeof value !== 'string' || value.length === 0) {
105
+ throw new Error(`ConceptSystem#${fieldName} must be a non-empty string`);
106
+ }
107
+ return value;
108
+ }
109
+
110
+ function _ensureName(value: LocalizedString | null | undefined): LocalizedString {
111
+ if (value == null) return {};
112
+ if (typeof value !== 'object' || Array.isArray(value)) {
113
+ throw new Error('ConceptSystem#name must be a localized object (e.g. { eng: "..." })');
114
+ }
115
+ return { ...value };
116
+ }
117
+
118
+ function _resolveType(value: unknown): ConceptSystemType {
119
+ if (value == null) {
120
+ throw new Error(
121
+ 'ConceptSystem#type is required; one of ' +
122
+ CONCEPT_SYSTEM_TYPE_VALUES.join(', '),
123
+ );
124
+ }
125
+ if (!isValidConceptSystemType(value)) {
126
+ throw new Error(
127
+ `ConceptSystem#type ${JSON.stringify(value)} invalid; ` +
128
+ `must be one of: ${CONCEPT_SYSTEM_TYPE_VALUES.join(', ')}`,
129
+ );
130
+ }
131
+ return value;
132
+ }
133
+
134
+ function _ensureMembers(values: ReadonlyArray<unknown> | null | undefined): ReadonlyArray<ConceptRef> {
135
+ if (!Array.isArray(values) || values.length === 0) {
136
+ throw new Error('ConceptSystem#members must be a non-empty array');
137
+ }
138
+ return values.map((v) =>
139
+ v instanceof ConceptRef
140
+ ? v
141
+ : new ConceptRef(v as { source?: string; id?: string; text?: string }),
142
+ );
143
+ }
144
+
145
+ function _refUri(
146
+ ref: { ref?: { source?: string | null; id?: string | null } | null; source?: string | null; id?: string | null } | null | undefined,
147
+ ): string {
148
+ const inner = ref?.ref ?? ref ?? {};
149
+ return `${inner.source ?? ''}:${inner.id ?? ''}`;
150
+ }
151
+
152
+ export default ConceptSystem;
@@ -0,0 +1,33 @@
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 type ConceptType = 'general' | 'individual';
13
+
14
+ export const CONCEPT_TYPE: Readonly<Record<'GENERAL' | 'INDIVIDUAL', ConceptType>> =
15
+ Object.freeze({
16
+ GENERAL: 'general',
17
+ INDIVIDUAL: 'individual',
18
+ });
19
+
20
+ export const CONCEPT_TYPE_VALUES: ReadonlyArray<ConceptType> = Object.freeze(
21
+ Object.values(CONCEPT_TYPE),
22
+ );
23
+
24
+ export const DEFAULT_CONCEPT_TYPE: ConceptType = CONCEPT_TYPE.GENERAL;
25
+
26
+ export function isValidConceptType(value: unknown): value is ConceptType {
27
+ return (
28
+ typeof value === 'string' &&
29
+ (CONCEPT_TYPE_VALUES as readonly string[]).includes(value)
30
+ );
31
+ }
32
+
33
+ export default CONCEPT_TYPE;
@@ -5,27 +5,34 @@
5
5
  // - an explicit { light, dark } pair
6
6
  //
7
7
  // Round-trips through Register.toJSON unchanged so the data lives in
8
- // the GCR package. Consumers call resolvedColor(mode) to pick the
8
+ // the GCR package. Consumers call resolveColor(mode) to pick the
9
9
  // right hex for the current UI mode.
10
10
 
11
- /**
12
- * @typedef {string} DatasetColorSpec
13
- * @typedef {{ light: string, dark: string }} DatasetColorPair
14
- * @typedef {DatasetColorSpec | DatasetColorPair} DatasetColor
15
- */
11
+ export type ColorMode = 'light' | 'dark';
16
12
 
17
- export const COLOR_MODES = Object.freeze(['light', 'dark']);
13
+ export type DatasetColorSpec = string;
14
+
15
+ export interface DatasetColorPair {
16
+ light?: string;
17
+ dark?: string;
18
+ }
19
+
20
+ export type DatasetColor = DatasetColorSpec | DatasetColorPair;
21
+
22
+ export const COLOR_MODES: ReadonlyArray<ColorMode> = Object.freeze([
23
+ 'light',
24
+ 'dark',
25
+ ]);
18
26
 
19
27
  /**
20
28
  * Returns the right hex for the given mode. Falls back to the single
21
29
  * hex when the spec is a string. Returns null when the spec is null
22
30
  * or when the requested mode is missing from a pair.
23
- *
24
- * @param {DatasetColor | null | undefined} color
25
- * @param {'light' | 'dark'} mode
26
- * @returns {string | null}
27
31
  */
28
- export function resolveColor(color, mode) {
32
+ export function resolveColor(
33
+ color: DatasetColor | null | undefined,
34
+ mode: ColorMode,
35
+ ): string | null {
29
36
  if (color == null) return null;
30
37
  if (typeof color === 'string') return color;
31
38
  if (typeof color === 'object') {
@@ -37,10 +44,10 @@ export function resolveColor(color, mode) {
37
44
 
38
45
  /**
39
46
  * True when the spec declares an explicit {light, dark} object.
40
- * Lets callers distinguish "single hex" from "per-mode pair" without
41
- * repeating the typeof check.
42
47
  */
43
- export function isColorPair(color) {
48
+ export function isColorPair(
49
+ color: DatasetColor | null | undefined,
50
+ ): color is DatasetColorPair {
44
51
  return color != null && typeof color === 'object';
45
52
  }
46
53
 
@@ -52,7 +59,9 @@ export function isColorPair(color) {
52
59
  *
53
60
  * Returns null when valid, or an error message describing the issue.
54
61
  */
55
- export function validateColor(color) {
62
+ export function validateColor(
63
+ color: DatasetColor | null | undefined,
64
+ ): string | null {
56
65
  if (color == null) return null;
57
66
  if (typeof color === 'string') return hexError(color);
58
67
  if (typeof color === 'object') {
@@ -73,7 +82,7 @@ export function validateColor(color) {
73
82
  }
74
83
 
75
84
  const HEX_RE = /^#[0-9a-fA-F]{3,8}$/;
76
- function hexError(s) {
85
+ function hexError(s: unknown): string | null {
77
86
  if (typeof s !== 'string') return `expected string, got ${typeof s}`;
78
87
  return HEX_RE.test(s) ? null : `not a valid hex color: ${JSON.stringify(s)}`;
79
88
  }
@@ -7,6 +7,12 @@
7
7
 
8
8
  import { makeEnum } from './enum.js';
9
9
 
10
+ export type DefinitionType =
11
+ | 'intensional'
12
+ | 'extensional'
13
+ | 'partitive'
14
+ | 'translated';
15
+
10
16
  export const DEFINITION_TYPE = makeEnum('DEFINITION_TYPE', {
11
17
  INTENSIONAL: 'intensional',
12
18
  EXTENSIONAL: 'extensional',
@@ -14,4 +20,5 @@ export const DEFINITION_TYPE = makeEnum('DEFINITION_TYPE', {
14
20
  TRANSLATED: 'translated',
15
21
  });
16
22
 
17
- export const DEFAULT_DEFINITION_TYPE = DEFINITION_TYPE.INTENSIONAL;
23
+ export const DEFAULT_DEFINITION_TYPE: DefinitionType =
24
+ DEFINITION_TYPE.INTENSIONAL;
@@ -0,0 +1,60 @@
1
+ // Enum factory — produces a frozen KEY/VALUES bundle, a frozen values
2
+ // array, and an isValid predicate from a single `makeEnum` call.
3
+ //
4
+ // Adding a new enum means one line:
5
+ //
6
+ // export const COLOR = makeEnum('COLOR', { RED: 'red', BLUE: 'blue' });
7
+ //
8
+ // The factory returns an object that exposes both the per-value
9
+ // constants (COLOR.RED, COLOR.BLUE) and the VALUES list + isValid
10
+ // predicate. The bundle is itself frozen.
11
+
12
+ /**
13
+ * Output of {@link makeEnum}. Captures both the original key→value
14
+ * pairs (so `MY_ENUM.MY_KEY` is the literal string value) and the
15
+ * helper properties (`name`, `VALUES`, `isValid`).
16
+ */
17
+ export type EnumBundle<TValues extends Readonly<Record<string, string>>> = {
18
+ readonly name: string;
19
+ readonly VALUES: ReadonlyArray<TValues[keyof TValues]>;
20
+ readonly isValid: (value: unknown) => value is TValues[keyof TValues];
21
+ } & { readonly [K in keyof TValues]: TValues[K] };
22
+
23
+ /**
24
+ * Build a frozen enum bundle.
25
+ *
26
+ * @param name Human-readable name used in error messages.
27
+ * @param values Object whose keys are the constant names and whose
28
+ * string values are the wire representations.
29
+ */
30
+ export function makeEnum<const TValues extends Readonly<Record<string, string>>>(
31
+ name: string,
32
+ values: TValues,
33
+ ): EnumBundle<TValues> {
34
+ if (typeof name !== 'string') {
35
+ throw new Error(`makeEnum: name must be a string, got ${typeof name}`);
36
+ }
37
+ if (typeof values !== 'object' || values === null) {
38
+ throw new Error(`makeEnum: values must be an object, got ${typeof values}`);
39
+ }
40
+ for (const [k, v] of Object.entries(values)) {
41
+ if (typeof v !== 'string') {
42
+ throw new Error(
43
+ `makeEnum(${name}): value for ${k} must be a string, got ${typeof v}`,
44
+ );
45
+ }
46
+ }
47
+
48
+ const frozen = Object.freeze({ ...values });
49
+ const valuesArr = Object.freeze(Object.values(frozen)) as ReadonlyArray<
50
+ TValues[keyof TValues]
51
+ >;
52
+ const isValid = (v: unknown): v is TValues[keyof TValues] =>
53
+ typeof v === 'string' && (valuesArr as readonly string[]).includes(v);
54
+
55
+ return Object.assign(Object.create(null) as object, frozen, {
56
+ name,
57
+ VALUES: valuesArr,
58
+ isValid,
59
+ }) as EnumBundle<TValues>;
60
+ }
@@ -0,0 +1,34 @@
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 type EquivalenceDegree =
9
+ | 'full'
10
+ | 'partial'
11
+ | 'none'
12
+ | 'directional';
13
+
14
+ export const EQUIVALENCE_DEGREE: Readonly<Record<Uppercase<EquivalenceDegree>, EquivalenceDegree>> = Object.freeze({
15
+ FULL: 'full',
16
+ PARTIAL: 'partial',
17
+ NONE: 'none',
18
+ DIRECTIONAL: 'directional',
19
+ });
20
+
21
+ export const EQUIVALENCE_DEGREE_VALUES: ReadonlyArray<EquivalenceDegree> =
22
+ Object.freeze(Object.values(EQUIVALENCE_DEGREE));
23
+
24
+ export const DEFAULT_EQUIVALENCE_DEGREE: EquivalenceDegree =
25
+ EQUIVALENCE_DEGREE.FULL;
26
+
27
+ export function isValidEquivalenceDegree(value: unknown): value is EquivalenceDegree {
28
+ return (
29
+ typeof value === 'string' &&
30
+ (EQUIVALENCE_DEGREE_VALUES as readonly string[]).includes(value)
31
+ );
32
+ }
33
+
34
+ export default EQUIVALENCE_DEGREE;
@@ -0,0 +1,72 @@
1
+ // GenericHyperedge — an ISO 704 / ISO 1087-1 / ISO 12620 generic
2
+ // relation connecting a comprehensive concept (the genus) to two
3
+ // or more specific concepts (the species) which together constitute
4
+ // a decomposition by some criterion of subdivision.
5
+ //
6
+ // Mirror of PartitiveHyperedge. Multiple GenericRelations on the same
7
+ // comprehensive distinguished by `criterion` is the OIML pattern
8
+ // (5.1 measurement standard has 6 criterion groups).
9
+ //
10
+ // See docs/design/generic-relation.md (concept-model repo).
11
+
12
+ import { AbstractHyperedge } from './abstract-hyperedge.js';
13
+ import type { AbstractHyperedgeJson } from './abstract-hyperedge.js';
14
+ import { GenericMember } from './generic-member.js';
15
+ import type { GenericMemberJson } from './generic-member.js';
16
+ import { HyperedgeRegistry } from './hyperedge-registry.js';
17
+
18
+ export interface GenericHyperedgeJson extends AbstractHyperedgeJson {
19
+ members: ReadonlyArray<GenericMemberJson | GenericMember>;
20
+ }
21
+
22
+ export class GenericHyperedge extends AbstractHyperedge {
23
+ // Per-class metadata block — the OCP contract.
24
+ static wireKey = 'generic_relations' as const;
25
+ static typeTag = 'generic_relation' as const;
26
+ static rdfType = 'gloss:GenericRelation' as const;
27
+ static memberClass = GenericMember;
28
+ static v1WireKeys: ReadonlyArray<string> = [];
29
+ static kindLabel = 'GEN';
30
+
31
+ declare readonly members: ReadonlyArray<GenericMember>;
32
+
33
+ constructor(data: Partial<GenericHyperedgeJson> = {}) {
34
+ super({
35
+ comprehensive: data.comprehensive ?? {},
36
+ members: data.members ?? [],
37
+ completeness: data.completeness ?? 'complete',
38
+ criterion: data.criterion,
39
+ sources: data.sources,
40
+ notes: data.notes,
41
+ status: data.status,
42
+ });
43
+ this.members = (Array.isArray(data.members) ? data.members : []).map((m) =>
44
+ m instanceof GenericMember ? m : new GenericMember(m as GenericMemberJson),
45
+ );
46
+ }
47
+
48
+ static identityOf(
49
+ value: {
50
+ comprehensive?: { source?: string | null; id?: string | null } | null;
51
+ members?: ReadonlyArray<{ ref?: { source?: string | null; id?: string | null } | null } | null>;
52
+ } | null | undefined,
53
+ ): string {
54
+ const v = value ?? {};
55
+ const c = v.comprehensive ?? {};
56
+ const memberKeys = (v.members ?? [])
57
+ .map((m) => {
58
+ const r = m?.ref ?? (m as { source?: string; id?: string } | null) ?? {};
59
+ return `${r.source ?? ''}:${r.id ?? ''}`;
60
+ })
61
+ .sort();
62
+ return `${c.source ?? ''}:${c.id ?? ''}|${memberKeys.join('|')}`;
63
+ }
64
+
65
+ static override fromJSON(data: GenericHyperedgeJson): GenericHyperedge {
66
+ return new GenericHyperedge(data);
67
+ }
68
+ }
69
+
70
+ export default GenericHyperedge;
71
+
72
+ HyperedgeRegistry.register(GenericHyperedge);
@@ -0,0 +1,80 @@
1
+ // GenericMember — one member of a GenericHyperedge.
2
+ //
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
+ // See docs/design/abstract-hyperedge.md for the full rationale and
10
+ // the computer-mouse worked example.
11
+
12
+ import { HyperedgeMember } from './hyperedge-member.js';
13
+ import type { HyperedgeMemberJson } from './hyperedge-member.js';
14
+
15
+ export type LocalizedString = Readonly<Record<string, string>>;
16
+
17
+ export interface GenericMemberJson extends HyperedgeMemberJson {
18
+ delimitingCharacteristic: LocalizedString | string;
19
+ }
20
+
21
+ export class GenericMember extends HyperedgeMember {
22
+ readonly delimitingCharacteristic: LocalizedString;
23
+
24
+ constructor(data: Partial<GenericMemberJson> = {}) {
25
+ // @ts-expect-error -- Partial<GenericMemberJson> defers the required
26
+ // delimitingCharacteristic check to _resolveDelimitingCharacteristic.
27
+ super(data);
28
+ this.delimitingCharacteristic = _resolveDelimitingCharacteristic(
29
+ data.delimitingCharacteristic,
30
+ );
31
+ }
32
+
33
+ override toJSON(): GenericMemberJson {
34
+ const base = super.toJSON() as GenericMemberJson;
35
+ base.delimitingCharacteristic = { ...this.delimitingCharacteristic };
36
+ return base;
37
+ }
38
+
39
+ static override fromJSON(data: GenericMemberJson): GenericMember {
40
+ return new GenericMember(data);
41
+ }
42
+ }
43
+
44
+ function _resolveDelimitingCharacteristic(
45
+ value: LocalizedString | string | null | undefined,
46
+ ): LocalizedString {
47
+ if (value == null) {
48
+ throw new Error(
49
+ 'GenericMember.delimitingCharacteristic is required per ISO 704:2022 §5.5.4.2.1 ' +
50
+ '(the intension of a subordinate concept must include at least one additional ' +
51
+ 'delimiting characteristic beyond the superordinate).',
52
+ );
53
+ }
54
+ if (typeof value === 'string') {
55
+ if (value.length === 0) {
56
+ throw new Error(
57
+ 'GenericMember.delimitingCharacteristic cannot be empty (ISO 704:2022 §5.5.4.2.1)',
58
+ );
59
+ }
60
+ return { default: value };
61
+ }
62
+ if (typeof value === 'object') {
63
+ const entries = Object.entries(value).filter(
64
+ ([, v]) => typeof v === 'string' && v.length > 0,
65
+ );
66
+ if (entries.length === 0) {
67
+ throw new Error(
68
+ 'GenericMember.delimitingCharacteristic must have at least one non-empty string ' +
69
+ 'value (ISO 704:2022 §5.5.4.2.1)',
70
+ );
71
+ }
72
+ return Object.fromEntries(entries) as LocalizedString;
73
+ }
74
+ throw new Error(
75
+ `GenericMember.delimitingCharacteristic must be a string or a language-keyed object; ` +
76
+ `got ${typeof value}`,
77
+ );
78
+ }
79
+
80
+ export default GenericMember;
@@ -15,11 +15,19 @@
15
15
  //
16
16
  // Identity key matches HyperedgeRegistry / ConceptRef conventions:
17
17
  // `${source}:${id}` (empty source or id is rejected)
18
- //
19
- // See TODO Phase 9.
18
+
19
+ import type { AbstractHyperedge } from './abstract-hyperedge.js';
20
+
21
+ interface HyperedgeLike {
22
+ comprehensive?: { source?: string | null; id?: string | null } | null;
23
+ members?: ReadonlyArray<{ ref?: { source?: string | null; id?: string | null } | null }>;
24
+ }
20
25
 
21
26
  export class HyperedgeIndex {
22
- constructor(hyperedges = []) {
27
+ private readonly _byComprehensive: Map<string, HyperedgeLike[]>;
28
+ private readonly _byMember: Map<string, HyperedgeLike[]>;
29
+
30
+ constructor(hyperedges: ReadonlyArray<HyperedgeLike> = []) {
23
31
  this._byComprehensive = new Map();
24
32
  this._byMember = new Map();
25
33
 
@@ -35,45 +43,51 @@ export class HyperedgeIndex {
35
43
  }
36
44
 
37
45
  // Hyperedges where the given concept is the comprehensive (the "1" side).
38
- forComprehensive(qualifiedId) {
46
+ forComprehensive(qualifiedId: string): HyperedgeLike[] {
39
47
  return this._byComprehensive.get(qualifiedId) ?? [];
40
48
  }
41
49
 
42
50
  // Hyperedges where the given concept appears as a member (the "many" side).
43
- forMember(qualifiedId) {
51
+ forMember(qualifiedId: string): HyperedgeLike[] {
44
52
  return this._byMember.get(qualifiedId) ?? [];
45
53
  }
46
54
 
47
55
  // All concepts that appear as a comprehensive in any indexed hyperedge.
48
- comprehensives() {
56
+ comprehensives(): string[] {
49
57
  return [...this._byComprehensive.keys()];
50
58
  }
51
59
 
52
60
  // All concepts that appear as a member in any indexed hyperedge.
53
- members() {
61
+ members(): string[] {
54
62
  return [...this._byMember.keys()];
55
63
  }
56
64
 
57
- get size() {
65
+ get size(): number {
58
66
  return this._byComprehensive.size;
59
67
  }
60
68
  }
61
69
 
62
- // Build a HyperedgeIndex across multiple Concepts' relations arrays
63
- // (a dataset-wide view). Each Concept contributes its own .relations.
64
- // Helper for the most common pattern: index once at collection load,
65
- // query many times.
66
- export function buildDatasetIndex(concepts = []) {
67
- const all = [];
70
+ /**
71
+ * Build a HyperedgeIndex across multiple Concepts' relations arrays
72
+ * (a dataset-wide view). Each Concept contributes its own .relations.
73
+ * Helper for the most common pattern: index once at collection load,
74
+ * query many times.
75
+ */
76
+ export function buildDatasetIndex(
77
+ concepts: ReadonlyArray<{ relations?: ReadonlyArray<AbstractHyperedge | HyperedgeLike> }> = [],
78
+ ): HyperedgeIndex {
79
+ const all: HyperedgeLike[] = [];
68
80
  for (const concept of concepts) {
69
81
  for (const h of concept.relations ?? []) {
70
- all.push(h);
82
+ all.push(h as HyperedgeLike);
71
83
  }
72
84
  }
73
85
  return new HyperedgeIndex(all);
74
86
  }
75
87
 
76
- function _qualifiedId(ref) {
88
+ function _qualifiedId(
89
+ ref: { source?: string | null; id?: string | null } | null | undefined,
90
+ ): string | null {
77
91
  if (!ref) return null;
78
92
  const source = ref.source ?? '';
79
93
  const id = ref.id ?? '';
@@ -81,7 +95,7 @@ function _qualifiedId(ref) {
81
95
  return `${source}:${id}`;
82
96
  }
83
97
 
84
- function _addToMapList(map, key, value) {
98
+ function _addToMapList<T>(map: Map<string, T[]>, key: string, value: T): void {
85
99
  if (!map.has(key)) map.set(key, []);
86
- map.get(key).push(value);
100
+ map.get(key)?.push(value);
87
101
  }