glossarist 0.4.36 → 0.4.38

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 (29) hide show
  1. package/package.json +1 -1
  2. package/src/models/abstract-hyperedge.ts +164 -0
  3. package/src/models/bibliography-data.ts +64 -0
  4. package/src/models/{bibliography-entry.js → bibliography-entry.ts} +18 -4
  5. package/src/models/concept-system.ts +152 -0
  6. package/src/models/detailed-definition.ts +92 -0
  7. package/src/models/generic-hyperedge.ts +72 -0
  8. package/src/models/generic-member.ts +80 -0
  9. package/src/models/grammar-info.ts +78 -0
  10. package/src/models/{hyperedge-index.js → hyperedge-index.ts} +32 -18
  11. package/src/models/{hyperedge-member.js → hyperedge-member.ts} +38 -26
  12. package/src/models/{hyperedge-registry.js → hyperedge-registry.ts} +59 -29
  13. package/src/models/{locality.js → locality.ts} +16 -4
  14. package/src/models/localized-string.ts +32 -0
  15. package/src/models/partitive-hyperedge.ts +90 -0
  16. package/src/models/{partitive-member.js → partitive-member.ts} +18 -17
  17. package/src/models/{pronunciation.js → pronunciation.ts} +18 -4
  18. package/src/models/sequential-hyperedge.ts +70 -0
  19. package/src/models/{sequential-member.js → sequential-member.ts} +6 -3
  20. package/src/models/abstract-hyperedge.js +0 -119
  21. package/src/models/bibliography-data.js +0 -48
  22. package/src/models/concept-system.js +0 -117
  23. package/src/models/detailed-definition.js +0 -59
  24. package/src/models/generic-hyperedge.js +0 -55
  25. package/src/models/generic-member.js +0 -82
  26. package/src/models/grammar-info.js +0 -40
  27. package/src/models/localized-string.js +0 -19
  28. package/src/models/partitive-hyperedge.js +0 -91
  29. package/src/models/sequential-hyperedge.js +0 -56
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glossarist",
3
- "version": "0.4.36",
3
+ "version": "0.4.38",
4
4
  "description": "JavaScript SDK for Glossarist GCR packages — read, write, validate, and manage terminology concepts",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -0,0 +1,164 @@
1
+ // AbstractHyperedge — shared base for directed 1→N hyperedges
2
+ // (PartitiveHyperedge, GenericHyperedge, SequentialHyperedge, future
3
+ // AssociativeHyperedge, etc.).
4
+ //
5
+ // Carries the common shape:
6
+ // - comprehensive: ConceptRef
7
+ // - members: 2+ MECE dimensioned concept refs (≥2 per ISO 704)
8
+ // - completeness: complete | partial
9
+ // - criterion: localized subdivision criterion (ISO 12620)
10
+ // - sources / notes / status: administrative metadata
11
+ //
12
+ // See docs/design/abstract-hyperedge.md (concept-model repo).
13
+
14
+ import { GlossaristModel } from './base.js';
15
+ import { ConceptRef } from './concept-ref.js';
16
+ import type { ConceptRefJson } from './concept-ref.js';
17
+ import { DEFAULT_COMPLETENESS, isValidCompleteness } from './completeness.js';
18
+ import type { Completeness } from './completeness.js';
19
+ import type { HyperedgeMember } from './hyperedge-member.js';
20
+ import type { LocalizedString } from './generic-member.js';
21
+
22
+ export interface AbstractHyperedgeJson {
23
+ comprehensive: ConceptRefJson;
24
+ members: ReadonlyArray<unknown>;
25
+ completeness: Completeness;
26
+ criterion?: LocalizedString;
27
+ sources?: ReadonlyArray<unknown>;
28
+ notes?: Record<string, unknown> | null;
29
+ status?: string | null;
30
+ /** PartitiveHyperedge wire alias (only present on PartitiveHyperedge JSON). */
31
+ partitives?: ReadonlyArray<unknown>;
32
+ }
33
+
34
+ export type CriterionInput = string | LocalizedString | null | undefined;
35
+
36
+ export abstract class AbstractHyperedge extends GlossaristModel {
37
+ readonly comprehensive: ConceptRef;
38
+ readonly members: ReadonlyArray<HyperedgeMember>;
39
+ readonly completeness: Completeness;
40
+ readonly criterion: LocalizedString | null;
41
+ readonly sources: ReadonlyArray<unknown>;
42
+ readonly notes: Record<string, unknown> | null;
43
+ readonly status: string | null;
44
+
45
+ constructor(
46
+ data: AbstractHyperedgeJson = {
47
+ comprehensive: {},
48
+ members: [],
49
+ completeness: 'complete',
50
+ },
51
+ ) {
52
+ super();
53
+ this.comprehensive = _ensureComprehensive(data.comprehensive);
54
+ this.members = _ensureMembers(
55
+ data.members,
56
+ this.constructor.name,
57
+ ) as ReadonlyArray<HyperedgeMember>;
58
+ this.completeness = _resolveCompleteness(data.completeness);
59
+ this.criterion = _normalizeCriterion(data.criterion);
60
+ this.sources = Array.isArray(data.sources) ? [...data.sources] : [];
61
+ this.notes = (data.notes as Record<string, unknown> | null) ?? null;
62
+ this.status = data.status ?? null;
63
+
64
+ _assertNoSelfLoop(this.comprehensive, this.members);
65
+ }
66
+
67
+ get isComplete(): boolean { return this.completeness === 'complete'; }
68
+ get isPartial(): boolean { return this.completeness === 'partial'; }
69
+ get isCoordinate(): boolean { return this.members.length >= 2; }
70
+
71
+ hasCriterion(): boolean { return this.criterion != null; }
72
+
73
+ override identity(): string {
74
+ const Ctor = this.constructor as unknown as { identityOf: (v: unknown) => string };
75
+ return Ctor.identityOf(this);
76
+ }
77
+
78
+ override toJSON(): AbstractHyperedgeJson {
79
+ const obj: AbstractHyperedgeJson = {
80
+ comprehensive: this.comprehensive.toJSON(),
81
+ members: this.members.map((m) => m.toJSON()),
82
+ completeness: this.completeness,
83
+ };
84
+ if (this.criterion != null) obj.criterion = { ...this.criterion };
85
+ if (this.sources.length > 0) obj.sources = [...this.sources];
86
+ if (this.notes != null) obj.notes = { ...this.notes };
87
+ if (this.status != null) obj.status = this.status;
88
+ return obj;
89
+ }
90
+ }
91
+
92
+ function _ensureComprehensive(
93
+ value: ConceptRefJson | ConceptRef | null | undefined,
94
+ ): ConceptRef {
95
+ const ref = value instanceof ConceptRef ? value : new ConceptRef(value ?? {});
96
+ if (!ref.source && !ref.id && !ref.text) {
97
+ throw new Error(
98
+ 'AbstractHyperedge.comprehensive must be a non-empty ConceptReference ' +
99
+ '(source, id, or text required)',
100
+ );
101
+ }
102
+ return ref;
103
+ }
104
+
105
+ function _ensureMembers(
106
+ values: ReadonlyArray<unknown> | null | undefined,
107
+ typeName: string,
108
+ ): ReadonlyArray<unknown> {
109
+ if (!Array.isArray(values)) {
110
+ throw new Error(
111
+ `${typeName}.members must be an array (got ${values === null ? 'null' : typeof values})`,
112
+ );
113
+ }
114
+ if (values.length < 2) {
115
+ throw new Error(
116
+ `${typeName} requires ≥2 partitives (ISO 704); got ${values.length}`,
117
+ );
118
+ }
119
+ return values;
120
+ }
121
+
122
+ function _resolveCompleteness(
123
+ value: Completeness | null | undefined,
124
+ ): Completeness {
125
+ const c = value ?? DEFAULT_COMPLETENESS;
126
+ if (!isValidCompleteness(c)) {
127
+ throw new Error(
128
+ `completeness has invalid value ${JSON.stringify(value)}`,
129
+ );
130
+ }
131
+ return c;
132
+ }
133
+
134
+ function _normalizeCriterion(value: CriterionInput): LocalizedString | null {
135
+ if (value == null) return null;
136
+ if (typeof value === 'string') {
137
+ return value.length > 0 ? { default: value } : null;
138
+ }
139
+ if (typeof value === 'object') {
140
+ const entries = Object.entries(value).filter(
141
+ ([, v]) => typeof v === 'string',
142
+ );
143
+ return entries.length > 0
144
+ ? (Object.fromEntries(entries) as LocalizedString)
145
+ : null;
146
+ }
147
+ return null;
148
+ }
149
+
150
+ function _assertNoSelfLoop(
151
+ comprehensive: ConceptRef,
152
+ members: ReadonlyArray<{ ref?: { source?: string | null; id?: string | null } }>,
153
+ ): void {
154
+ const cKey = `${comprehensive.source ?? ''}:${comprehensive.id ?? ''}`;
155
+ for (const m of members) {
156
+ const ref = m.ref;
157
+ const pKey = `${ref?.source ?? ''}:${ref?.id ?? ''}`;
158
+ if (pKey === cKey && (ref?.source || ref?.id)) {
159
+ throw new Error(
160
+ 'Relation.members cannot include the comprehensive (no self-loops)',
161
+ );
162
+ }
163
+ }
164
+ }
@@ -0,0 +1,64 @@
1
+ import * as yaml from 'js-yaml';
2
+ import { GlossaristModel } from './base.js';
3
+ import { BibliographyEntry } from './bibliography-entry.js';
4
+ import type { BibliographyEntryJson } from './bibliography-entry.js';
5
+
6
+ export interface BibliographyDataJson {
7
+ bibliography?: ReadonlyArray<BibliographyEntryJson | BibliographyEntry>;
8
+ entries?: ReadonlyArray<BibliographyEntryJson | BibliographyEntry>;
9
+ }
10
+
11
+ export class BibliographyData extends GlossaristModel {
12
+ private readonly _rawEntries: ReadonlyArray<BibliographyEntryJson | BibliographyEntry>;
13
+ private _entries: ReadonlyArray<BibliographyEntry> | null = null;
14
+
15
+ constructor(data: BibliographyDataJson | ReadonlyArray<BibliographyEntryJson | BibliographyEntry> = {}) {
16
+ super();
17
+ let entriesData: ReadonlyArray<BibliographyEntryJson | BibliographyEntry> | undefined;
18
+ if (Array.isArray(data)) {
19
+ entriesData = data;
20
+ } else {
21
+ entriesData = (data as BibliographyDataJson)?.bibliography ?? (data as BibliographyDataJson)?.entries ?? [];
22
+ }
23
+ this._rawEntries = Array.isArray(entriesData) ? entriesData : [];
24
+ }
25
+
26
+ get entries(): ReadonlyArray<BibliographyEntry> {
27
+ return this._lazy<BibliographyEntry>(
28
+ '_entries',
29
+ '_rawEntries',
30
+ (e) => (e instanceof BibliographyEntry ? e : new BibliographyEntry(e as BibliographyEntryJson)),
31
+ );
32
+ }
33
+
34
+ find(id: string): BibliographyEntry | null {
35
+ return this.entries.find((e) => e.id === id) ?? null;
36
+ }
37
+
38
+ get keys(): ReadonlyArray<string | null> {
39
+ return this.entries.map((e) => e.id);
40
+ }
41
+
42
+ override toJSON(): BibliographyDataJson {
43
+ if (this.entries.length === 0) return { bibliography: [] };
44
+ return { bibliography: this.entries.map((e) => e.toJSON()) };
45
+ }
46
+
47
+ toYAML(): string {
48
+ return yaml.dump(this.toJSON());
49
+ }
50
+
51
+ static fromYAML(yamlString: string): BibliographyData {
52
+ const parsed = yaml.load(yamlString) as
53
+ | BibliographyDataJson
54
+ | ReadonlyArray<BibliographyEntryJson>
55
+ | undefined;
56
+ return new BibliographyData(parsed ?? {});
57
+ }
58
+
59
+ static override fromJSON(
60
+ data: BibliographyDataJson | ReadonlyArray<BibliographyEntryJson>,
61
+ ): BibliographyData {
62
+ return new BibliographyData(data);
63
+ }
64
+ }
@@ -1,7 +1,21 @@
1
1
  import { GlossaristModel } from './base.js';
2
2
 
3
+ export interface BibliographyEntryJson {
4
+ id?: string | null;
5
+ reference?: string | null;
6
+ title?: string | null;
7
+ link?: string | null;
8
+ type?: string | null;
9
+ }
10
+
3
11
  export class BibliographyEntry extends GlossaristModel {
4
- constructor(data = {}) {
12
+ readonly id: string | null;
13
+ readonly reference: string | null;
14
+ readonly title: string | null;
15
+ readonly link: string | null;
16
+ readonly type: string | null;
17
+
18
+ constructor(data: BibliographyEntryJson = {}) {
5
19
  super();
6
20
  this.id = data.id ?? null;
7
21
  this.reference = data.reference ?? null;
@@ -10,8 +24,8 @@ export class BibliographyEntry extends GlossaristModel {
10
24
  this.type = data.type ?? null;
11
25
  }
12
26
 
13
- toJSON() {
14
- const obj = {};
27
+ override toJSON(): BibliographyEntryJson {
28
+ const obj: BibliographyEntryJson = {};
15
29
  if (this.id != null) obj.id = this.id;
16
30
  if (this.reference != null) obj.reference = this.reference;
17
31
  if (this.title != null) obj.title = this.title;
@@ -20,7 +34,7 @@ export class BibliographyEntry extends GlossaristModel {
20
34
  return obj;
21
35
  }
22
36
 
23
- static fromJSON(data) {
37
+ static override fromJSON(data: BibliographyEntryJson): BibliographyEntry {
24
38
  return new BibliographyEntry(data);
25
39
  }
26
40
  }
@@ -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,92 @@
1
+ import { GlossaristModel } from './base.js';
2
+ import { Citation } from './citation.js';
3
+ import { canonicalJson } from '../diff/canonical-json.js';
4
+
5
+ export interface DetailedDefinitionJson {
6
+ content?: string;
7
+ sources?: ReadonlyArray<unknown>;
8
+ examples?: ReadonlyArray<unknown>;
9
+ }
10
+
11
+ export interface WalkTextEntry {
12
+ text: string;
13
+ source: string;
14
+ }
15
+
16
+ export class DetailedDefinition extends GlossaristModel {
17
+ readonly content: string;
18
+ private readonly _rawSources: ReadonlyArray<unknown>;
19
+ private readonly _rawExamples: ReadonlyArray<unknown>;
20
+ private _sources: ReadonlyArray<Citation> | null = null;
21
+ private _examples: ReadonlyArray<DetailedDefinition> | null = null;
22
+
23
+ constructor(data: DetailedDefinitionJson = {}) {
24
+ super();
25
+ this.content = data.content ?? '';
26
+ this._rawSources = data.sources ?? [];
27
+ this._rawExamples = data.examples ?? [];
28
+ }
29
+
30
+ get sources(): ReadonlyArray<Citation> {
31
+ return this._lazy<Citation>(
32
+ '_sources',
33
+ '_rawSources',
34
+ (s) => (s instanceof Citation ? s : new Citation(s as never)),
35
+ );
36
+ }
37
+
38
+ get examples(): ReadonlyArray<DetailedDefinition> {
39
+ return this._lazy<DetailedDefinition>(
40
+ '_examples',
41
+ '_rawExamples',
42
+ (e) =>
43
+ e instanceof DetailedDefinition
44
+ ? e
45
+ : new DetailedDefinition(e as DetailedDefinitionJson),
46
+ );
47
+ }
48
+
49
+ override toJSON(): DetailedDefinitionJson {
50
+ const obj: DetailedDefinitionJson = { content: this.content };
51
+ this._serialize(obj as Record<string, unknown>, 'sources', '_sources', '_rawSources');
52
+ this._serialize(obj as Record<string, unknown>, 'examples', '_examples', '_rawExamples');
53
+ return obj;
54
+ }
55
+
56
+ /**
57
+ * Yield this definition's content and the content of every nested
58
+ * example (recursively). Each item carries `{ text, source }` where
59
+ * `source` is `<path>.content` rooted at the `path` argument.
60
+ */
61
+ *walkTexts(path: string): Generator<WalkTextEntry> {
62
+ if (typeof this.content === 'string' && this.content.length > 0) {
63
+ yield { text: this.content, source: `${path}.content` };
64
+ }
65
+ const examples = this.examples;
66
+ for (let i = 0; i < examples.length; i++) {
67
+ const example = examples[i];
68
+ if (example) {
69
+ yield* example.walkTexts(`${path}.examples[${i}]`);
70
+ }
71
+ }
72
+ }
73
+
74
+ static identityOf(
75
+ value: { toJSON?: () => unknown } | unknown | null,
76
+ ): string {
77
+ if (value == null) return '';
78
+ if (typeof value === 'object' && value !== null && 'toJSON' in value &&
79
+ typeof (value as { toJSON: unknown }).toJSON === 'function') {
80
+ return canonicalJson((value as { toJSON: () => unknown }).toJSON());
81
+ }
82
+ return canonicalJson(value);
83
+ }
84
+
85
+ override identity(): string {
86
+ return DetailedDefinition.identityOf(this);
87
+ }
88
+
89
+ static override fromJSON(data: DetailedDefinitionJson): DetailedDefinition {
90
+ return new DetailedDefinition(data);
91
+ }
92
+ }
@@ -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;