glossarist 0.4.40 → 0.4.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
import { GlossaristModel } from './base.js';
|
|
2
|
+
import { LocalizedConcept } from './localized-concept.js';
|
|
3
|
+
import type { LocalizedConceptJson } from './localized-concept.js';
|
|
4
|
+
import { RelatedConcept } from './related-concept.js';
|
|
5
|
+
import type { RelatedConceptJson } from './related-concept.js';
|
|
6
|
+
import { PartitiveHyperedge } from './partitive-hyperedge.js';
|
|
7
|
+
import { AbstractHyperedge } from './abstract-hyperedge.js';
|
|
8
|
+
import type { AbstractHyperedgeJson } from './abstract-hyperedge.js';
|
|
9
|
+
import { HyperedgeRegistry, groupHyperedgesByWireKey } from './hyperedge-registry.js';
|
|
10
|
+
import type { HyperedgeClass } from './hyperedge-registry.js';
|
|
11
|
+
import { migrateHyperedgeToRelation } from '../migration/hyperedge-migrator.js';
|
|
12
|
+
import { ConceptReference } from './concept-reference.js';
|
|
13
|
+
import type { ConceptReferenceJson } from './concept-reference.js';
|
|
14
|
+
import { ConceptDate } from './concept-date.js';
|
|
15
|
+
import type { ConceptDateJson } from './concept-date.js';
|
|
16
|
+
import { ConceptSource } from './concept-source.js';
|
|
17
|
+
import type { ConceptSourceJson } from './concept-source.js';
|
|
18
|
+
import { FigureReference } from './non-verbal-references.js';
|
|
19
|
+
import { TableReference } from './non-verbal-references.js';
|
|
20
|
+
import { FormulaReference } from './non-verbal-references.js';
|
|
21
|
+
import { diffConcepts } from '../diff/concept-diff.js';
|
|
22
|
+
|
|
23
|
+
export interface ConceptJson {
|
|
24
|
+
id?: string;
|
|
25
|
+
termid?: string;
|
|
26
|
+
term?: string | null;
|
|
27
|
+
uri?: string | null;
|
|
28
|
+
localizations?: Record<string, LocalizedConceptJson>;
|
|
29
|
+
related?: ReadonlyArray<RelatedConceptJson | RelatedConcept>;
|
|
30
|
+
relatedConcepts?: ReadonlyArray<RelatedConceptJson | RelatedConcept>;
|
|
31
|
+
related_concepts?: ReadonlyArray<RelatedConceptJson | RelatedConcept>;
|
|
32
|
+
relations?: ReadonlyArray<unknown>;
|
|
33
|
+
domains?: ReadonlyArray<ConceptReferenceJson | ConceptReference>;
|
|
34
|
+
groups?: ReadonlyArray<string | { id?: string; sectionId?: string } | ConceptReferenceJson>;
|
|
35
|
+
sections?: ReadonlyArray<string | { id?: string; sectionId?: string }>;
|
|
36
|
+
tags?: ReadonlyArray<string>;
|
|
37
|
+
dates?: ReadonlyArray<ConceptDateJson | ConceptDate>;
|
|
38
|
+
sources?: ReadonlyArray<ConceptSourceJson | ConceptSource>;
|
|
39
|
+
figures?: ReadonlyArray<unknown>;
|
|
40
|
+
tables?: ReadonlyArray<unknown>;
|
|
41
|
+
formulas?: ReadonlyArray<unknown>;
|
|
42
|
+
status?: string | null;
|
|
43
|
+
schemaVersion?: string;
|
|
44
|
+
schema_version?: string;
|
|
45
|
+
raw?: unknown;
|
|
46
|
+
[key: string]: unknown;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const CONCEPT_WIRE_NAMES: Readonly<Record<string, string>> = Object.freeze({
|
|
50
|
+
schemaVersion: 'schema_version',
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
type GroupOrSection = string | { id?: string; sectionId?: string };
|
|
54
|
+
|
|
55
|
+
export class Concept extends GlossaristModel {
|
|
56
|
+
static get DIFF_FIELDS(): ReadonlyArray<string> {
|
|
57
|
+
return Object.freeze(['status', 'term', 'uri', 'schemaVersion']);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
static wireNameFor(field: string): string {
|
|
61
|
+
return CONCEPT_WIRE_NAMES[field] ?? field;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
readonly id: string;
|
|
65
|
+
readonly term: string | null;
|
|
66
|
+
readonly uri: string | null;
|
|
67
|
+
private readonly _rawLocalizations: Record<string, LocalizedConceptJson>;
|
|
68
|
+
private _cache: Record<string, LocalizedConcept> = {};
|
|
69
|
+
|
|
70
|
+
readonly relatedConcepts: ReadonlyArray<RelatedConcept>;
|
|
71
|
+
relations: ReadonlyArray<AbstractHyperedge>;
|
|
72
|
+
readonly domains: ReadonlyArray<ConceptReference>;
|
|
73
|
+
readonly groups: ReadonlyArray<string>;
|
|
74
|
+
readonly sections: ReadonlyArray<string>;
|
|
75
|
+
readonly tags: ReadonlyArray<string>;
|
|
76
|
+
readonly dates: ReadonlyArray<ConceptDate>;
|
|
77
|
+
readonly sources: ReadonlyArray<ConceptSource>;
|
|
78
|
+
private readonly _rawFigures: ReadonlyArray<unknown>;
|
|
79
|
+
private readonly _rawTables: ReadonlyArray<unknown>;
|
|
80
|
+
private readonly _rawFormulas: ReadonlyArray<unknown>;
|
|
81
|
+
private _figures: ReadonlyArray<FigureReference> | null = null;
|
|
82
|
+
private _tables: ReadonlyArray<TableReference> | null = null;
|
|
83
|
+
private _formulas: ReadonlyArray<FormulaReference> | null = null;
|
|
84
|
+
readonly status: string | null;
|
|
85
|
+
readonly schemaVersion: string;
|
|
86
|
+
readonly raw: unknown;
|
|
87
|
+
|
|
88
|
+
constructor(data: ConceptJson = {}) {
|
|
89
|
+
super();
|
|
90
|
+
this.id = String(data.id ?? (data.termid as string | undefined) ?? '');
|
|
91
|
+
this.term = data.term ?? null;
|
|
92
|
+
this.uri = data.uri ?? null;
|
|
93
|
+
this._rawLocalizations =
|
|
94
|
+
(data.localizations as Record<string, LocalizedConceptJson>) ?? {};
|
|
95
|
+
this._cache = {};
|
|
96
|
+
|
|
97
|
+
this.relatedConcepts = _mapInstances(
|
|
98
|
+
(data.relatedConcepts ?? data.related ?? data.related_concepts ?? []) as ReadonlyArray<RelatedConceptJson | RelatedConcept>,
|
|
99
|
+
RelatedConcept,
|
|
100
|
+
);
|
|
101
|
+
this.relations = _resolveHyperedges(data);
|
|
102
|
+
|
|
103
|
+
this.domains = _normalizeDomains(data.domains, data.groups);
|
|
104
|
+
this.groups = Array.isArray(data.groups)
|
|
105
|
+
? (data.groups as ReadonlyArray<GroupOrSection>)
|
|
106
|
+
.map((g) =>
|
|
107
|
+
typeof g === 'string'
|
|
108
|
+
? g
|
|
109
|
+
: (g as { id?: string; sectionId?: string })?.id ??
|
|
110
|
+
(g as { sectionId?: string })?.sectionId ??
|
|
111
|
+
null,
|
|
112
|
+
)
|
|
113
|
+
.filter((g): g is string => Boolean(g))
|
|
114
|
+
: [];
|
|
115
|
+
this.sections = Array.isArray(data.sections)
|
|
116
|
+
? (data.sections as ReadonlyArray<GroupOrSection>)
|
|
117
|
+
.map((s) =>
|
|
118
|
+
typeof s === 'string'
|
|
119
|
+
? s
|
|
120
|
+
: (s as { id?: string; sectionId?: string })?.id ??
|
|
121
|
+
(s as { sectionId?: string })?.sectionId ??
|
|
122
|
+
null,
|
|
123
|
+
)
|
|
124
|
+
.filter((s): s is string => Boolean(s))
|
|
125
|
+
: [];
|
|
126
|
+
this.tags = Array.isArray(data.tags) ? [...data.tags] : [];
|
|
127
|
+
this.dates = _mapInstances(
|
|
128
|
+
(data.dates ?? []) as ReadonlyArray<ConceptDateJson | ConceptDate>,
|
|
129
|
+
ConceptDate,
|
|
130
|
+
);
|
|
131
|
+
this.sources = _mapInstances(
|
|
132
|
+
(data.sources ?? []) as ReadonlyArray<ConceptSourceJson | ConceptSource>,
|
|
133
|
+
ConceptSource,
|
|
134
|
+
);
|
|
135
|
+
this._rawFigures = data.figures ?? [];
|
|
136
|
+
this._rawTables = data.tables ?? [];
|
|
137
|
+
this._rawFormulas = data.formulas ?? [];
|
|
138
|
+
this.status = data.status ?? null;
|
|
139
|
+
this.schemaVersion = data.schemaVersion ?? data.schema_version ?? '3';
|
|
140
|
+
this.raw = data.raw ?? null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
get termid(): string { return this.id; }
|
|
144
|
+
|
|
145
|
+
get languages(): string[] {
|
|
146
|
+
return Object.keys(this._rawLocalizations);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
get languageCodes(): string[] {
|
|
150
|
+
return this.languages;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
localization(lang: string): LocalizedConcept | undefined {
|
|
154
|
+
if (!(lang in this._rawLocalizations)) return undefined;
|
|
155
|
+
if (!this._cache[lang]) {
|
|
156
|
+
const raw = this._rawLocalizations[lang];
|
|
157
|
+
this._cache[lang] = new LocalizedConcept({ ...raw, language_code: lang });
|
|
158
|
+
}
|
|
159
|
+
return this._cache[lang];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
diff(other: Concept, language: string = 'eng'): unknown {
|
|
163
|
+
return diffConcepts(this, other, language);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
primaryDesignation(lang: string): unknown {
|
|
167
|
+
return (
|
|
168
|
+
(this.localization(lang) as { primaryDesignation?: unknown } | undefined)
|
|
169
|
+
?.primaryDesignation ?? null
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
definition(lang: string): unknown {
|
|
174
|
+
return (
|
|
175
|
+
(this.localization(lang) as { primaryDefinition?: unknown } | undefined)
|
|
176
|
+
?.primaryDefinition ?? null
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
setLocalization(lang: string, lc: LocalizedConcept | LocalizedConceptJson): this {
|
|
181
|
+
if (lc instanceof LocalizedConcept) {
|
|
182
|
+
this._cache[lang] = lc;
|
|
183
|
+
const json = lc.toJSON() as LocalizedConceptJson & { language_code?: string };
|
|
184
|
+
delete json.language_code;
|
|
185
|
+
this._rawLocalizations[lang] = json;
|
|
186
|
+
} else {
|
|
187
|
+
this._rawLocalizations[lang] = lc;
|
|
188
|
+
delete this._cache[lang];
|
|
189
|
+
}
|
|
190
|
+
return this;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
hasLocalization(lang: string): boolean {
|
|
194
|
+
return lang in this._rawLocalizations;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
findSourceById(id: string): ConceptSource | null {
|
|
198
|
+
if (typeof id !== 'string' || id.length === 0) return null;
|
|
199
|
+
for (const source of this.sources) {
|
|
200
|
+
if (source.id === id) return source;
|
|
201
|
+
}
|
|
202
|
+
for (const lang of this.languages) {
|
|
203
|
+
const lc = this.localization(lang);
|
|
204
|
+
if (!lc) continue;
|
|
205
|
+
const lcSources = (
|
|
206
|
+
lc as { sources?: ReadonlyArray<{ id?: string | null }> }
|
|
207
|
+
).sources ?? [];
|
|
208
|
+
for (const source of lcSources) {
|
|
209
|
+
if (source.id === id) return source as unknown as ConceptSource;
|
|
210
|
+
}
|
|
211
|
+
const lcTerms = (
|
|
212
|
+
lc as {
|
|
213
|
+
terms?: ReadonlyArray<{
|
|
214
|
+
sources?: ReadonlyArray<{ id?: string | null }>;
|
|
215
|
+
}>;
|
|
216
|
+
}
|
|
217
|
+
).terms ?? [];
|
|
218
|
+
for (const designation of lcTerms) {
|
|
219
|
+
for (const source of designation.sources ?? []) {
|
|
220
|
+
if (source.id === id) return source as unknown as ConceptSource;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
get figures(): ReadonlyArray<FigureReference> {
|
|
228
|
+
return this._lazy<FigureReference>(
|
|
229
|
+
'_figures',
|
|
230
|
+
'_rawFigures',
|
|
231
|
+
(r) => FigureReference.fromJSON(r as never),
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
get tables(): ReadonlyArray<TableReference> {
|
|
236
|
+
return this._lazy<TableReference>(
|
|
237
|
+
'_tables',
|
|
238
|
+
'_rawTables',
|
|
239
|
+
(r) => TableReference.fromJSON(r as never),
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
get formulas(): ReadonlyArray<FormulaReference> {
|
|
244
|
+
return this._lazy<FormulaReference>(
|
|
245
|
+
'_formulas',
|
|
246
|
+
'_rawFormulas',
|
|
247
|
+
(r) => FormulaReference.fromJSON(r as never),
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
*walkTexts(): Generator<{ text: string; source: string; language?: string }> {
|
|
252
|
+
for (const lang of this.languages) {
|
|
253
|
+
const lc = this.localization(lang);
|
|
254
|
+
if (!lc) continue;
|
|
255
|
+
const walker = (
|
|
256
|
+
lc as {
|
|
257
|
+
walkTexts?: (
|
|
258
|
+
p: string,
|
|
259
|
+
) => Generator<{ text: string; source: string }>;
|
|
260
|
+
}
|
|
261
|
+
).walkTexts;
|
|
262
|
+
if (typeof walker === 'function') {
|
|
263
|
+
yield* walker.call(lc, `localizations.${lang}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
override toJSON(): ConceptJson {
|
|
269
|
+
const obj: ConceptJson = { id: this.id };
|
|
270
|
+
if (this.term != null) obj.term = this.term;
|
|
271
|
+
if (this.uri != null) obj.uri = this.uri;
|
|
272
|
+
|
|
273
|
+
if (Object.keys(this._rawLocalizations).length > 0) {
|
|
274
|
+
obj.localizations = {};
|
|
275
|
+
const loc = obj.localizations;
|
|
276
|
+
for (const lang of this.languages) {
|
|
277
|
+
const lc = this.localization(lang);
|
|
278
|
+
if (lc) {
|
|
279
|
+
const json = lc.toJSON() as LocalizedConceptJson & {
|
|
280
|
+
language_code?: string;
|
|
281
|
+
};
|
|
282
|
+
delete json.language_code;
|
|
283
|
+
loc[lang] = json;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
} else {
|
|
287
|
+
obj.localizations = {};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (this.relatedConcepts.length > 0) {
|
|
291
|
+
obj.related = this.relatedConcepts.map((rc) => rc.toJSON());
|
|
292
|
+
}
|
|
293
|
+
const grouped = groupHyperedgesByWireKey(this.relations);
|
|
294
|
+
for (const [wireKey, rels] of Object.entries(grouped)) {
|
|
295
|
+
if (rels.length > 0) obj[wireKey] = rels.map((r) => r.toJSON());
|
|
296
|
+
}
|
|
297
|
+
if (this.domains.length > 0) {
|
|
298
|
+
obj.domains = this.domains.map((d) => d.toJSON());
|
|
299
|
+
}
|
|
300
|
+
if (this.tags.length > 0) {
|
|
301
|
+
obj.tags = [...this.tags];
|
|
302
|
+
}
|
|
303
|
+
if (this.dates.length > 0) {
|
|
304
|
+
obj.dates = this.dates.map((d) => d.toJSON());
|
|
305
|
+
}
|
|
306
|
+
if (this.sources.length > 0) {
|
|
307
|
+
obj.sources = this.sources.map((s) => s.toJSON());
|
|
308
|
+
}
|
|
309
|
+
this._serialize(obj as unknown as Record<string, unknown>, 'figures', '_figures', '_rawFigures');
|
|
310
|
+
this._serialize(obj as unknown as Record<string, unknown>, 'tables', '_tables', '_rawTables');
|
|
311
|
+
this._serialize(obj as unknown as Record<string, unknown>, 'formulas', '_formulas', '_rawFormulas');
|
|
312
|
+
if (this.status != null) obj.status = this.status;
|
|
313
|
+
obj.schema_version = this.schemaVersion;
|
|
314
|
+
return obj;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
static override fromJSON(data: ConceptJson): Concept {
|
|
318
|
+
return new Concept(data);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function _mapInstances<T, J>(
|
|
323
|
+
arr: ReadonlyArray<T | J>,
|
|
324
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
325
|
+
Cls: new (...args: any[]) => T,
|
|
326
|
+
): ReadonlyArray<T> {
|
|
327
|
+
return arr.map((item) => (item instanceof Cls ? item : new Cls(item))) as ReadonlyArray<T>;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
type HyperedgeInput =
|
|
331
|
+
| AbstractHyperedge
|
|
332
|
+
| (AbstractHyperedgeJson & { type?: string })
|
|
333
|
+
| { type: string; [key: string]: unknown }
|
|
334
|
+
| { toJSON(): unknown };
|
|
335
|
+
|
|
336
|
+
function _resolveHyperedges(data: ConceptJson): ReadonlyArray<AbstractHyperedge> {
|
|
337
|
+
_assertNoAmbiguousInput(data);
|
|
338
|
+
|
|
339
|
+
const relations = data.relations as ReadonlyArray<HyperedgeInput> | undefined;
|
|
340
|
+
if (Array.isArray(relations)) {
|
|
341
|
+
const out: AbstractHyperedge[] = [];
|
|
342
|
+
for (const r of relations) {
|
|
343
|
+
const coerced = _coerceHyperedge(r);
|
|
344
|
+
if (coerced) out.push(coerced);
|
|
345
|
+
}
|
|
346
|
+
return _dedupeOrThrow(out);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const out: AbstractHyperedge[] = [];
|
|
350
|
+
|
|
351
|
+
for (const cls of HyperedgeRegistry.allClasses()) {
|
|
352
|
+
const arr = data[cls.wireKey] as ReadonlyArray<HyperedgeInput> | undefined;
|
|
353
|
+
const camelArr = data[_camelCase(cls.wireKey)] as
|
|
354
|
+
| ReadonlyArray<HyperedgeInput>
|
|
355
|
+
| undefined;
|
|
356
|
+
const source = arr ?? camelArr;
|
|
357
|
+
if (!Array.isArray(source)) continue;
|
|
358
|
+
for (const r of source) {
|
|
359
|
+
const coerced = _coerceHyperedge(r, cls);
|
|
360
|
+
if (coerced) out.push(coerced);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
for (const cls of HyperedgeRegistry.allClasses()) {
|
|
365
|
+
for (const v1Key of cls.v1WireKeys ?? []) {
|
|
366
|
+
const camelV1 = _camelCase(v1Key);
|
|
367
|
+
const arr = data[v1Key] as
|
|
368
|
+
| ReadonlyArray<Record<string, unknown>>
|
|
369
|
+
| undefined;
|
|
370
|
+
const camelArr = data[camelV1] as
|
|
371
|
+
| ReadonlyArray<Record<string, unknown>>
|
|
372
|
+
| undefined;
|
|
373
|
+
const source = arr ?? camelArr;
|
|
374
|
+
if (!Array.isArray(source)) continue;
|
|
375
|
+
for (const h of source) {
|
|
376
|
+
const migrated = _migrateV1Hash(cls, v1Key, h);
|
|
377
|
+
if (migrated) {
|
|
378
|
+
const Ctor = cls as unknown as new (data: unknown) => AbstractHyperedge;
|
|
379
|
+
out.push(new Ctor(migrated));
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return _dedupeOrThrow(out);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function _assertNoAmbiguousInput(data: ConceptJson): void {
|
|
389
|
+
if (!Array.isArray(data.relations)) return;
|
|
390
|
+
const typedKeys: string[] = [];
|
|
391
|
+
for (const cls of HyperedgeRegistry.allClasses()) {
|
|
392
|
+
if (Array.isArray(data[cls.wireKey])) typedKeys.push(cls.wireKey);
|
|
393
|
+
const camelWire = _camelCase(cls.wireKey);
|
|
394
|
+
if (camelWire !== cls.wireKey && Array.isArray(data[camelWire])) {
|
|
395
|
+
typedKeys.push(camelWire);
|
|
396
|
+
}
|
|
397
|
+
for (const v1 of cls.v1WireKeys ?? []) {
|
|
398
|
+
if (Array.isArray(data[v1])) typedKeys.push(v1);
|
|
399
|
+
const camelV1 = _camelCase(v1);
|
|
400
|
+
if (camelV1 !== v1 && Array.isArray(data[camelV1])) typedKeys.push(camelV1);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (typedKeys.length > 0) {
|
|
404
|
+
throw new Error(
|
|
405
|
+
`Concept input is ambiguous: pass either 'relations' (unified) ` +
|
|
406
|
+
`or typed wire keys (${typedKeys.join(', ')}), not both.`,
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function _dedupeOrThrow(
|
|
412
|
+
relations: ReadonlyArray<AbstractHyperedge>,
|
|
413
|
+
): ReadonlyArray<AbstractHyperedge> {
|
|
414
|
+
const seen = new Map<string, AbstractHyperedge>();
|
|
415
|
+
for (const r of relations) {
|
|
416
|
+
const id = _hyperedgeIdentity(r);
|
|
417
|
+
if (seen.has(id)) {
|
|
418
|
+
throw new Error(
|
|
419
|
+
`Concept input has duplicate hyperedge (identity=${id}). ` +
|
|
420
|
+
`Passing the same relation twice is a bug.`,
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
seen.set(id, r);
|
|
424
|
+
}
|
|
425
|
+
return relations;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function _hyperedgeIdentity(r: AbstractHyperedge | null | undefined): string {
|
|
429
|
+
if (r == null) return '';
|
|
430
|
+
const Cls = r.constructor as {
|
|
431
|
+
name: string;
|
|
432
|
+
identityOf?: (v: unknown) => string;
|
|
433
|
+
};
|
|
434
|
+
if (typeof Cls?.identityOf === 'function') {
|
|
435
|
+
return `${Cls.name}::${Cls.identityOf(r)}`;
|
|
436
|
+
}
|
|
437
|
+
return String(r);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function _coerceHyperedge(
|
|
441
|
+
value: HyperedgeInput | null | undefined,
|
|
442
|
+
fallbackClass?: HyperedgeClass,
|
|
443
|
+
): AbstractHyperedge | null {
|
|
444
|
+
if (value == null) return null;
|
|
445
|
+
if (value instanceof AbstractHyperedge) return value;
|
|
446
|
+
|
|
447
|
+
const hash =
|
|
448
|
+
typeof (value as { toJSON?: () => unknown }).toJSON === 'function'
|
|
449
|
+
? ((value as { toJSON: () => unknown }).toJSON() as Record<string, unknown>)
|
|
450
|
+
: (value as Record<string, unknown>);
|
|
451
|
+
const cls = _classForHash(hash) ?? fallbackClass;
|
|
452
|
+
if (!cls) return null;
|
|
453
|
+
const Ctor = cls as unknown as new (data: unknown) => AbstractHyperedge;
|
|
454
|
+
return new Ctor(hash);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function _classForHash(hash: unknown): HyperedgeClass | null {
|
|
458
|
+
if (!hash || typeof hash !== 'object') return null;
|
|
459
|
+
const type = (hash as { type?: unknown }).type;
|
|
460
|
+
if (typeof type !== 'string') return null;
|
|
461
|
+
return HyperedgeRegistry.forTypeTag(type);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function _migrateV1Hash(
|
|
465
|
+
cls: HyperedgeClass,
|
|
466
|
+
v1Key: string,
|
|
467
|
+
hash: Record<string, unknown>,
|
|
468
|
+
): Record<string, unknown> | null {
|
|
469
|
+
if (cls === PartitiveHyperedge && v1Key === 'partitive_hyperedges') {
|
|
470
|
+
return migrateHyperedgeToRelation(hash) as Record<string, unknown>;
|
|
471
|
+
}
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function _camelCase(s: string): string {
|
|
476
|
+
if (typeof s !== 'string' || !s.includes('_')) return s;
|
|
477
|
+
return s.replace(/_([a-z])/g, (_, c: string) => c.toUpperCase());
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function _normalizeDomains(
|
|
481
|
+
domains: ReadonlyArray<ConceptReferenceJson | ConceptReference> | null | undefined,
|
|
482
|
+
groups: ReadonlyArray<unknown> | null | undefined,
|
|
483
|
+
): ReadonlyArray<ConceptReference> {
|
|
484
|
+
if (domains) {
|
|
485
|
+
return domains.map((d) =>
|
|
486
|
+
d instanceof ConceptReference
|
|
487
|
+
? d
|
|
488
|
+
: new ConceptReference(d as ConceptReferenceJson),
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
if (groups) {
|
|
492
|
+
return groups.map((g) =>
|
|
493
|
+
typeof g === 'string'
|
|
494
|
+
? ConceptReference.domain(g)
|
|
495
|
+
: new ConceptReference(g as ConceptReferenceJson),
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
return [];
|
|
499
|
+
}
|