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/src/models/concept.js
DELETED
|
@@ -1,420 +0,0 @@
|
|
|
1
|
-
import { GlossaristModel } from './base.js';
|
|
2
|
-
import { LocalizedConcept } from './localized-concept.js';
|
|
3
|
-
import { RelatedConcept } from './related-concept.js';
|
|
4
|
-
import { PartitiveHyperedge } from './partitive-hyperedge.js';
|
|
5
|
-
import { AbstractHyperedge } from './abstract-hyperedge.js';
|
|
6
|
-
import { HyperedgeRegistry, groupHyperedgesByWireKey } from './hyperedge-registry.js';
|
|
7
|
-
import { migrateHyperedgeToRelation } from '../migration/hyperedge-migrator.js';
|
|
8
|
-
import { ConceptReference } from './concept-reference.js';
|
|
9
|
-
import { ConceptDate } from './concept-date.js';
|
|
10
|
-
import { ConceptSource } from './concept-source.js';
|
|
11
|
-
import { FigureReference } from './non-verbal-references.js';
|
|
12
|
-
import { TableReference } from './non-verbal-references.js';
|
|
13
|
-
import { FormulaReference } from './non-verbal-references.js';
|
|
14
|
-
import { diffConcepts } from '../diff/concept-diff.js';
|
|
15
|
-
|
|
16
|
-
export class Concept extends GlossaristModel {
|
|
17
|
-
// Scalar fields the diff layer tracks at the concept level. Adding
|
|
18
|
-
// a new scalar metadata field requires only appending its name here;
|
|
19
|
-
// diff detection, patch application, and similarity scoring all pick
|
|
20
|
-
// it up automatically. (Invariant N2 — TODO.hyperedges-v2/07.)
|
|
21
|
-
static get DIFF_FIELDS() {
|
|
22
|
-
return Object.freeze(['status', 'term', 'uri', 'schemaVersion']);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
static wireNameFor(field) {
|
|
26
|
-
return CONCEPT_WIRE_NAMES[field] ?? field;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
constructor(data = {}) {
|
|
30
|
-
super();
|
|
31
|
-
this.id = String(data.id ?? data.termid ?? '');
|
|
32
|
-
this.term = data.term ?? null;
|
|
33
|
-
this.uri = data.uri ?? null;
|
|
34
|
-
this._rawLocalizations = data.localizations ?? {};
|
|
35
|
-
this._cache = {};
|
|
36
|
-
|
|
37
|
-
this.relatedConcepts = _mapInstances(data.relatedConcepts ?? data.related ?? data.related_concepts ?? [], RelatedConcept);
|
|
38
|
-
|
|
39
|
-
// Unified hyperedge array. Every hyperedge (PartitiveHyperedge,
|
|
40
|
-
// GenericHyperedge, future TemporalHyperedge / AssociativeHyperedge /
|
|
41
|
-
// SequentialHyperedge) lives here. The single public API is
|
|
42
|
-
// `.relations`; external systems that need to filter by type use
|
|
43
|
-
// `concept.relations.filter(r => r instanceof X)` or dispatch via
|
|
44
|
-
// HyperedgeRegistry (preferred — see Phases 6 and 11).
|
|
45
|
-
//
|
|
46
|
-
// Throws on ambiguous input (caller passed both `relations` and any
|
|
47
|
-
// typed wire key) and on duplicate relations (same identity twice).
|
|
48
|
-
// Silent drop / silent dedupe are footguns the audit (C4, C6)
|
|
49
|
-
// flagged; explicit throws make corruption loud.
|
|
50
|
-
this.relations = _resolveHyperedges(data);
|
|
51
|
-
|
|
52
|
-
this.domains = _normalizeDomains(data.domains, data.groups);
|
|
53
|
-
this.groups = Array.isArray(data.groups)
|
|
54
|
-
? data.groups.map(g => typeof g === 'string' ? g : (g?.id ?? g?.sectionId ?? null)).filter(Boolean)
|
|
55
|
-
: [];
|
|
56
|
-
this.sections = Array.isArray(data.sections)
|
|
57
|
-
? data.sections.map(s => typeof s === 'string' ? s : (s?.id ?? s?.sectionId ?? null)).filter(Boolean)
|
|
58
|
-
: [];
|
|
59
|
-
this.tags = Array.isArray(data.tags) ? [...data.tags] : [];
|
|
60
|
-
this.dates = _mapInstances(data.dates ?? [], ConceptDate);
|
|
61
|
-
this.sources = _mapInstances(data.sources ?? [], ConceptSource);
|
|
62
|
-
this._rawFigures = data.figures ?? [];
|
|
63
|
-
this._rawTables = data.tables ?? [];
|
|
64
|
-
this._rawFormulas = data.formulas ?? [];
|
|
65
|
-
this._figures = null;
|
|
66
|
-
this._tables = null;
|
|
67
|
-
this._formulas = null;
|
|
68
|
-
this.status = data.status ?? null;
|
|
69
|
-
this.schemaVersion = data.schemaVersion ?? data.schema_version ?? '3';
|
|
70
|
-
this.raw = data.raw ?? null;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
get termid() { return this.id; }
|
|
74
|
-
|
|
75
|
-
// No typed projections. Use concept.relations directly and filter
|
|
76
|
-
// by instanceof, or dispatch via HyperedgeRegistry. The old
|
|
77
|
-
// .partitiveRelations / .genericRelations / .partitiveHyperedges
|
|
78
|
-
// getters were removed per TODO Phase 4 — they were footguns
|
|
79
|
-
// (audit C3: .partitiveHyperedges returned v2 instances but callers
|
|
80
|
-
// expected v1 shape; multiple validators silently no-op'd).
|
|
81
|
-
//
|
|
82
|
-
// Snippet migration guide:
|
|
83
|
-
// concept.partitiveRelations → concept.relations.filter(r => r instanceof PartitiveHyperedge)
|
|
84
|
-
// concept.genericRelations → concept.relations.filter(r => r instanceof GenericHyperedge)
|
|
85
|
-
// concept.partitiveHyperedges → concept.relations.filter(r => r instanceof PartitiveHyperedge)
|
|
86
|
-
|
|
87
|
-
get languages() {
|
|
88
|
-
return Object.keys(this._rawLocalizations);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
get languageCodes() {
|
|
92
|
-
return this.languages;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
localization(lang) {
|
|
96
|
-
if (!(lang in this._rawLocalizations)) return undefined;
|
|
97
|
-
if (!this._cache[lang]) {
|
|
98
|
-
const raw = this._rawLocalizations[lang];
|
|
99
|
-
this._cache[lang] = new LocalizedConcept({ ...raw, language_code: lang });
|
|
100
|
-
}
|
|
101
|
-
return this._cache[lang];
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
diff(other, language = 'eng') {
|
|
105
|
-
return diffConcepts(this, other, language);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
primaryDesignation(lang) {
|
|
109
|
-
return this.localization(lang)?.primaryDesignation ?? null;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
definition(lang) {
|
|
113
|
-
return this.localization(lang)?.primaryDefinition ?? null;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
setLocalization(lang, lc) {
|
|
117
|
-
if (lc instanceof LocalizedConcept) {
|
|
118
|
-
this._cache[lang] = lc;
|
|
119
|
-
const json = lc.toJSON();
|
|
120
|
-
delete json.language_code;
|
|
121
|
-
this._rawLocalizations[lang] = json;
|
|
122
|
-
} else {
|
|
123
|
-
this._rawLocalizations[lang] = lc;
|
|
124
|
-
delete this._cache[lang];
|
|
125
|
-
}
|
|
126
|
-
return this;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
hasLocalization(lang) {
|
|
130
|
-
return lang in this._rawLocalizations;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Find a source by its local id within this concept.
|
|
135
|
-
*
|
|
136
|
-
* The lookup walks concept-level, localization-level, and
|
|
137
|
-
* designation-level sources in that order, returning the first
|
|
138
|
-
* source whose `id` matches. Sources without an `id` are
|
|
139
|
-
* skipped. The id is local to the concept; uniqueness is
|
|
140
|
-
* enforced by the validator (see CiteRefIntegrityRule).
|
|
141
|
-
*
|
|
142
|
-
* @param {string} id
|
|
143
|
-
* @returns {ConceptSource | null}
|
|
144
|
-
*/
|
|
145
|
-
findSourceById(id) {
|
|
146
|
-
if (typeof id !== 'string' || id.length === 0) return null;
|
|
147
|
-
|
|
148
|
-
// 1. Concept-level sources.
|
|
149
|
-
for (const source of this.sources) {
|
|
150
|
-
if (source.id === id) return source;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// 2. Localization-level sources.
|
|
154
|
-
for (const lang of this.languages) {
|
|
155
|
-
const lc = this.localization(lang);
|
|
156
|
-
if (!lc) continue;
|
|
157
|
-
for (const source of lc.sources) {
|
|
158
|
-
if (source.id === id) return source;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// 3. Designation-level sources.
|
|
162
|
-
for (const designation of lc.terms) {
|
|
163
|
-
for (const source of designation.sources) {
|
|
164
|
-
if (source.id === id) return source;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
return null;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
get figures() {
|
|
173
|
-
return this._lazy('_figures', '_rawFigures',
|
|
174
|
-
r => FigureReference.fromJSON(r));
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
get tables() {
|
|
178
|
-
return this._lazy('_tables', '_rawTables',
|
|
179
|
-
r => TableReference.fromJSON(r));
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
get formulas() {
|
|
183
|
-
return this._lazy('_formulas', '_rawFormulas',
|
|
184
|
-
r => FormulaReference.fromJSON(r));
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Yield every content-text fragment in this concept across all
|
|
189
|
-
* localizations, recursing through nested examples. Each yielded
|
|
190
|
-
* `{ text, source }` carries a dotted path rooted at
|
|
191
|
-
* `localizations.<lang>.<slot>[i]...content`.
|
|
192
|
-
*
|
|
193
|
-
* Designations are not included; they live on `LocalizedConcept.terms`
|
|
194
|
-
* and have a different shape.
|
|
195
|
-
*/
|
|
196
|
-
*walkTexts() {
|
|
197
|
-
for (const lang of this.languages) {
|
|
198
|
-
const lc = this.localization(lang);
|
|
199
|
-
if (!lc) continue;
|
|
200
|
-
yield* lc.walkTexts(`localizations.${lang}`);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
toJSON() {
|
|
205
|
-
const obj = { id: this.id };
|
|
206
|
-
if (this.term != null) obj.term = this.term;
|
|
207
|
-
if (this.uri != null) obj.uri = this.uri;
|
|
208
|
-
|
|
209
|
-
if (Object.keys(this._rawLocalizations).length > 0) {
|
|
210
|
-
obj.localizations = {};
|
|
211
|
-
for (const lang of this.languages) {
|
|
212
|
-
const lc = this.localization(lang);
|
|
213
|
-
if (lc) {
|
|
214
|
-
const json = lc.toJSON();
|
|
215
|
-
delete json.language_code;
|
|
216
|
-
obj.localizations[lang] = json;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
} else {
|
|
220
|
-
obj.localizations = {};
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
if (this.relatedConcepts.length > 0) {
|
|
224
|
-
obj.related = this.relatedConcepts.map(rc => rc.toJSON());
|
|
225
|
-
}
|
|
226
|
-
// Emit per-type wire keys from the unified relations array,
|
|
227
|
-
// partitioned via HyperedgeRegistry. Adding a new hyperedge type
|
|
228
|
-
// means its wire key appears here automatically — no edits to
|
|
229
|
-
// Concept.toJSON.
|
|
230
|
-
const grouped = groupHyperedgesByWireKey(this.relations);
|
|
231
|
-
for (const [wireKey, rels] of Object.entries(grouped)) {
|
|
232
|
-
if (rels.length > 0) obj[wireKey] = rels.map(r => r.toJSON());
|
|
233
|
-
}
|
|
234
|
-
if (this.domains.length > 0) {
|
|
235
|
-
obj.domains = this.domains.map(d => d.toJSON());
|
|
236
|
-
}
|
|
237
|
-
if (this.tags.length > 0) {
|
|
238
|
-
obj.tags = [...this.tags];
|
|
239
|
-
}
|
|
240
|
-
if (this.dates.length > 0) {
|
|
241
|
-
obj.dates = this.dates.map(d => d.toJSON());
|
|
242
|
-
}
|
|
243
|
-
if (this.sources.length > 0) {
|
|
244
|
-
obj.sources = this.sources.map(s => s.toJSON());
|
|
245
|
-
}
|
|
246
|
-
this._serialize(obj, 'figures', '_figures', '_rawFigures');
|
|
247
|
-
this._serialize(obj, 'tables', '_tables', '_rawTables');
|
|
248
|
-
this._serialize(obj, 'formulas', '_formulas', '_rawFormulas');
|
|
249
|
-
if (this.status != null) obj.status = this.status;
|
|
250
|
-
obj.schema_version = this.schemaVersion;
|
|
251
|
-
return obj;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
static fromJSON(data) {
|
|
255
|
-
return new Concept(data);
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function _mapInstances(arr, Cls) {
|
|
260
|
-
return arr.map(item => item instanceof Cls ? item : new Cls(item));
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// Resolve the unified hyperedge array from any input shape.
|
|
264
|
-
//
|
|
265
|
-
// Three input categories are accepted; mixing them throws (audit C4 —
|
|
266
|
-
// silent drop was a footgun):
|
|
267
|
-
//
|
|
268
|
-
// 1. data.relations — preferred unified shape. Entries
|
|
269
|
-
// are typed instances or hashes
|
|
270
|
-
// carrying a `type` discriminator
|
|
271
|
-
// (looked up via HyperedgeRegistry).
|
|
272
|
-
// 2. Per-type v2 wire keys — one of HyperedgeRegistry.allWireKeys()
|
|
273
|
-
// (partitive_relations, generic_relations).
|
|
274
|
-
// 3. Per-type v1 legacy keys — declared on each class via
|
|
275
|
-
// `static v1WireKeys` (e.g.
|
|
276
|
-
// partitive_hyperedges → migrated).
|
|
277
|
-
//
|
|
278
|
-
// Adding a new hyperedge type means: declare the leaf class with its
|
|
279
|
-
// metadata block, register it. Concept, parser, serializer, diff,
|
|
280
|
-
// patch, renderer, RDF emitter do not change. (Phase 11 OCP contract.)
|
|
281
|
-
function _resolveHyperedges(data) {
|
|
282
|
-
_assertNoAmbiguousInput(data);
|
|
283
|
-
|
|
284
|
-
if (Array.isArray(data.relations)) {
|
|
285
|
-
return _dedupeOrThrow(data.relations.map(r => _coerceHyperedge(r)).filter(r => r != null));
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const out = [];
|
|
289
|
-
|
|
290
|
-
for (const cls of HyperedgeRegistry.allClasses()) {
|
|
291
|
-
const arr = data[cls.wireKey] ?? data[_camelCase(cls.wireKey)];
|
|
292
|
-
if (!Array.isArray(arr)) continue;
|
|
293
|
-
for (const r of arr) {
|
|
294
|
-
const coerced = _coerceHyperedge(r, cls);
|
|
295
|
-
if (coerced) out.push(coerced);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
for (const cls of HyperedgeRegistry.allClasses()) {
|
|
300
|
-
for (const v1Key of cls.v1WireKeys ?? []) {
|
|
301
|
-
const camelV1 = _camelCase(v1Key);
|
|
302
|
-
const arr = data[v1Key] ?? data[camelV1];
|
|
303
|
-
if (!Array.isArray(arr)) continue;
|
|
304
|
-
for (const h of arr) {
|
|
305
|
-
const migrated = _migrateV1Hash(cls, v1Key, h);
|
|
306
|
-
if (migrated) out.push(new cls(migrated));
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
return _dedupeOrThrow(out);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// Audit C4: silent drop on mixed input is a footgun. Throw instead.
|
|
315
|
-
// Allows `relations` alone, or any combination of typed keys alone —
|
|
316
|
-
// but not both at once.
|
|
317
|
-
function _assertNoAmbiguousInput(data) {
|
|
318
|
-
if (!Array.isArray(data.relations)) return;
|
|
319
|
-
const typedKeys = [];
|
|
320
|
-
for (const cls of HyperedgeRegistry.allClasses()) {
|
|
321
|
-
if (Array.isArray(data[cls.wireKey])) typedKeys.push(cls.wireKey);
|
|
322
|
-
const camelWire = _camelCase(cls.wireKey);
|
|
323
|
-
if (camelWire !== cls.wireKey && Array.isArray(data[camelWire])) {
|
|
324
|
-
typedKeys.push(camelWire);
|
|
325
|
-
}
|
|
326
|
-
for (const v1 of cls.v1WireKeys ?? []) {
|
|
327
|
-
if (Array.isArray(data[v1])) typedKeys.push(v1);
|
|
328
|
-
const camelV1 = _camelCase(v1);
|
|
329
|
-
if (camelV1 !== v1 && Array.isArray(data[camelV1])) typedKeys.push(camelV1);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
if (typedKeys.length > 0) {
|
|
333
|
-
throw new Error(
|
|
334
|
-
`Concept input is ambiguous: pass either 'relations' (unified) ` +
|
|
335
|
-
`or typed wire keys (${typedKeys.join(', ')}), not both. ` +
|
|
336
|
-
` Mixing them silently drops the typed keys, which has caused ` +
|
|
337
|
-
` data loss in production (see audit C4).`,
|
|
338
|
-
);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// Audit C6: do NOT silently dedupe. Duplicate relations indicate a bug
|
|
343
|
-
// in the caller; surface it. The identity function delegates to each
|
|
344
|
-
// class's static identityOf (polymorphic — handles mixed types).
|
|
345
|
-
function _dedupeOrThrow(relations) {
|
|
346
|
-
const seen = new Map();
|
|
347
|
-
for (const r of relations) {
|
|
348
|
-
const id = _hyperedgeIdentity(r);
|
|
349
|
-
if (seen.has(id)) {
|
|
350
|
-
throw new Error(
|
|
351
|
-
`Concept input has duplicate hyperedge (identity=${id}). ` +
|
|
352
|
-
`Passing the same relation twice is a bug — the parser and ` +
|
|
353
|
-
`serializer would silently emit it twice on disk. ` +
|
|
354
|
-
`Audit C6 requires we surface this loudly instead.`,
|
|
355
|
-
);
|
|
356
|
-
}
|
|
357
|
-
seen.set(id, r);
|
|
358
|
-
}
|
|
359
|
-
return relations;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
function _hyperedgeIdentity(r) {
|
|
363
|
-
if (r == null) return '';
|
|
364
|
-
const Cls = r.constructor;
|
|
365
|
-
if (typeof Cls?.identityOf === 'function') {
|
|
366
|
-
return `${Cls.name}::${Cls.identityOf(r)}`;
|
|
367
|
-
}
|
|
368
|
-
return String(r);
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
function _coerceHyperedge(value, fallbackClass) {
|
|
372
|
-
if (value == null) return null;
|
|
373
|
-
if (value instanceof AbstractHyperedge) return value;
|
|
374
|
-
|
|
375
|
-
const hash = typeof value.toJSON === 'function' ? value.toJSON() : value;
|
|
376
|
-
const cls = _classForHash(hash) ?? fallbackClass;
|
|
377
|
-
if (!cls) return null;
|
|
378
|
-
return new cls(hash);
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
function _classForHash(hash) {
|
|
382
|
-
if (!hash || typeof hash !== 'object') return null;
|
|
383
|
-
const type = hash.type;
|
|
384
|
-
if (typeof type !== 'string') return null;
|
|
385
|
-
return HyperedgeRegistry.forTypeTag(type);
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// v1 → v2 migration for hashes from legacy wire keys. Currently only
|
|
389
|
-
// PartitiveHyperedge has v1 wire keys (partitive_hyperedges). Future
|
|
390
|
-
// types extend this dispatch — but it lives here in the migration
|
|
391
|
-
// helper, not in the model classes, since v1 is a legacy concern.
|
|
392
|
-
function _migrateV1Hash(cls, v1Key, hash) {
|
|
393
|
-
if (cls === PartitiveHyperedge && v1Key === 'partitive_hyperedges') {
|
|
394
|
-
const normalized = hash;
|
|
395
|
-
return migrateHyperedgeToRelation(normalized);
|
|
396
|
-
}
|
|
397
|
-
return null;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
// snake_case → camelCase
|
|
401
|
-
function _camelCase(s) {
|
|
402
|
-
if (typeof s !== 'string' || !s.includes('_')) return s;
|
|
403
|
-
return s.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
const CONCEPT_WIRE_NAMES = Object.freeze({
|
|
407
|
-
schemaVersion: 'schema_version',
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
function _normalizeDomains(domains, groups) {
|
|
411
|
-
if (domains) {
|
|
412
|
-
return domains.map(d => d instanceof ConceptReference ? d : new ConceptReference(d));
|
|
413
|
-
}
|
|
414
|
-
if (groups) {
|
|
415
|
-
return groups.map(g => typeof g === 'string'
|
|
416
|
-
? ConceptReference.domain(g)
|
|
417
|
-
: new ConceptReference(g));
|
|
418
|
-
}
|
|
419
|
-
return [];
|
|
420
|
-
}
|
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
import { GlossaristModel } from './base.js';
|
|
2
|
-
import { Designation } from './designation.js';
|
|
3
|
-
import { DetailedDefinition } from './detailed-definition.js';
|
|
4
|
-
import { ConceptSource } from './concept-source.js';
|
|
5
|
-
import { ConceptDate } from './concept-date.js';
|
|
6
|
-
import { NonVerbRep } from './non-verb-rep.js';
|
|
7
|
-
import { RelatedConcept } from './related-concept.js';
|
|
8
|
-
|
|
9
|
-
function wrapAs(Cls) {
|
|
10
|
-
return item => item instanceof Cls ? item : new Cls(item);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const LOC_WIRE_NAMES = Object.freeze({
|
|
14
|
-
entryStatus: 'entry_status',
|
|
15
|
-
reviewType: 'review_type',
|
|
16
|
-
lineageSourceSimilarity: 'lineage_source_similarity',
|
|
17
|
-
reviewDate: 'review_date',
|
|
18
|
-
reviewDecisionDate: 'review_decision_date',
|
|
19
|
-
reviewDecisionEvent: 'review_decision_event',
|
|
20
|
-
reviewStatus: 'review_status',
|
|
21
|
-
reviewDecision: 'review_decision',
|
|
22
|
-
reviewDecisionNotes: 'review_decision_notes',
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
export class LocalizedConcept extends GlossaristModel {
|
|
26
|
-
// Scalar metadata fields the diff layer tracks per localization.
|
|
27
|
-
// Adding a new scalar field requires only appending its name here;
|
|
28
|
-
// diff/patch/similarity pick it up automatically.
|
|
29
|
-
// (Invariant N2 — TODO.hyperedges-v2/07.)
|
|
30
|
-
static get DIFF_FIELDS() {
|
|
31
|
-
return Object.freeze([
|
|
32
|
-
'entryStatus', 'classification', 'reviewType', 'domain', 'release',
|
|
33
|
-
'lineageSourceSimilarity', 'script', 'system',
|
|
34
|
-
'reviewDate', 'reviewDecisionDate', 'reviewDecisionEvent',
|
|
35
|
-
'reviewStatus', 'reviewDecision', 'reviewDecisionNotes',
|
|
36
|
-
]);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
static wireNameFor(field) {
|
|
40
|
-
return LOC_WIRE_NAMES[field] ?? field;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
constructor(data = {}) {
|
|
44
|
-
super();
|
|
45
|
-
this.languageCode = data.language_code ?? data.languageCode ?? null;
|
|
46
|
-
this.script = data.script ?? null;
|
|
47
|
-
this.system = data.system ?? null;
|
|
48
|
-
this.entryStatus = data.entry_status ?? data.entryStatus ?? null;
|
|
49
|
-
this.classification = data.classification ?? null;
|
|
50
|
-
this.reviewType = data.review_type ?? data.reviewType ?? null;
|
|
51
|
-
this.domain = data.domain ?? null;
|
|
52
|
-
this.release = data.release ?? null;
|
|
53
|
-
this.lineageSourceSimilarity = data.lineage_source_similarity ?? data.lineageSourceSimilarity ?? null;
|
|
54
|
-
|
|
55
|
-
this.reviewDate = data.review_date ?? data.reviewDate ?? null;
|
|
56
|
-
this.reviewDecisionDate = data.review_decision_date ?? data.reviewDecisionDate ?? null;
|
|
57
|
-
this.reviewDecisionEvent = data.review_decision_event ?? data.reviewDecisionEvent ?? null;
|
|
58
|
-
this.reviewStatus = data.review_status ?? data.reviewStatus ?? null;
|
|
59
|
-
this.reviewDecision = data.review_decision ?? data.reviewDecision ?? null;
|
|
60
|
-
this.reviewDecisionNotes = data.review_decision_notes ?? data.reviewDecisionNotes ?? null;
|
|
61
|
-
|
|
62
|
-
this._rawTerms = data.terms ?? [];
|
|
63
|
-
this._rawDefinition = data.definition ?? [];
|
|
64
|
-
this._rawSources = data.sources ?? [];
|
|
65
|
-
this._rawNotes = data.notes ?? [];
|
|
66
|
-
this._rawAnnotations = data.annotations ?? [];
|
|
67
|
-
this._rawExamples = data.examples ?? [];
|
|
68
|
-
this._rawDates = data.dates ?? [];
|
|
69
|
-
this._rawNonVerbal = data.non_verbal_rep ?? data.non_verb ?? [];
|
|
70
|
-
this._rawRelated = data.related ?? [];
|
|
71
|
-
|
|
72
|
-
this._terms = null;
|
|
73
|
-
this._definitions = null;
|
|
74
|
-
this._sources = null;
|
|
75
|
-
this._notes = null;
|
|
76
|
-
this._annotations = null;
|
|
77
|
-
this._examples = null;
|
|
78
|
-
this._dates = null;
|
|
79
|
-
this._nonVerbal = null;
|
|
80
|
-
this._related = null;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
get terms() {
|
|
84
|
-
return this._lazy('_terms', '_rawTerms', t => Designation.fromData(t));
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
get definitions() {
|
|
88
|
-
return this._lazy('_definitions', '_rawDefinition', wrapAs(DetailedDefinition));
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
get definition() {
|
|
92
|
-
return this.definitions;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
get sources() {
|
|
96
|
-
return this._lazy('_sources', '_rawSources', wrapAs(ConceptSource));
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
get notes() {
|
|
100
|
-
return this._lazy('_notes', '_rawNotes', wrapAs(DetailedDefinition));
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
get annotations() {
|
|
104
|
-
return this._lazy('_annotations', '_rawAnnotations', wrapAs(DetailedDefinition));
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
get examples() {
|
|
108
|
-
return this._lazy('_examples', '_rawExamples', wrapAs(DetailedDefinition));
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
get dates() {
|
|
112
|
-
return this._lazy('_dates', '_rawDates', wrapAs(ConceptDate));
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
get nonVerbalRep() {
|
|
116
|
-
return this._lazy('_nonVerbal', '_rawNonVerbal', wrapAs(NonVerbRep));
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
get related() {
|
|
120
|
-
return this._lazy('_related', '_rawRelated', wrapAs(RelatedConcept));
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
get primaryDesignation() {
|
|
124
|
-
return this.terms[0]?.designation ?? null;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
get primaryDefinition() {
|
|
128
|
-
return this.definitions[0]?.content ?? null;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
*_textSlots() {
|
|
132
|
-
yield ['definitions', this.definitions];
|
|
133
|
-
yield ['notes', this.notes];
|
|
134
|
-
yield ['examples', this.examples];
|
|
135
|
-
yield ['annotations', this.annotations];
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Yield every content-text fragment in this localization, recursing
|
|
140
|
-
* through nested examples. `basePath` prefixes every emitted
|
|
141
|
-
* `source` path; pass `localizations.<lang>` to get paths consistent
|
|
142
|
-
* with the rest of the codebase. Designations are not included.
|
|
143
|
-
*/
|
|
144
|
-
*walkTexts(basePath) {
|
|
145
|
-
for (const [name, arr] of this._textSlots()) {
|
|
146
|
-
for (let i = 0; i < arr.length; i++) {
|
|
147
|
-
yield* arr[i].walkTexts(`${basePath}.${name}[${i}]`);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
toJSON() {
|
|
153
|
-
const obj = {};
|
|
154
|
-
if (this.languageCode) obj.language_code = this.languageCode;
|
|
155
|
-
if (this.script) obj.script = this.script;
|
|
156
|
-
if (this.system) obj.system = this.system;
|
|
157
|
-
if (this.entryStatus) obj.entry_status = this.entryStatus;
|
|
158
|
-
if (this.classification) obj.classification = this.classification;
|
|
159
|
-
if (this.reviewType) obj.review_type = this.reviewType;
|
|
160
|
-
if (this.domain) obj.domain = this.domain;
|
|
161
|
-
if (this.release) obj.release = this.release;
|
|
162
|
-
if (this.lineageSourceSimilarity != null) obj.lineage_source_similarity = this.lineageSourceSimilarity;
|
|
163
|
-
|
|
164
|
-
if (this.reviewDate) obj.review_date = this.reviewDate;
|
|
165
|
-
if (this.reviewDecisionDate) obj.review_decision_date = this.reviewDecisionDate;
|
|
166
|
-
if (this.reviewDecisionEvent) obj.review_decision_event = this.reviewDecisionEvent;
|
|
167
|
-
if (this.reviewStatus) obj.review_status = this.reviewStatus;
|
|
168
|
-
if (this.reviewDecision) obj.review_decision = this.reviewDecision;
|
|
169
|
-
if (this.reviewDecisionNotes) obj.review_decision_notes = this.reviewDecisionNotes;
|
|
170
|
-
|
|
171
|
-
this._serialize(obj, 'terms', '_terms', '_rawTerms');
|
|
172
|
-
this._serialize(obj, 'definition', '_definitions', '_rawDefinition');
|
|
173
|
-
this._serialize(obj, 'notes', '_notes', '_rawNotes');
|
|
174
|
-
this._serialize(obj, 'annotations', '_annotations', '_rawAnnotations');
|
|
175
|
-
this._serialize(obj, 'examples', '_examples', '_rawExamples');
|
|
176
|
-
this._serialize(obj, 'sources', '_sources', '_rawSources');
|
|
177
|
-
this._serialize(obj, 'dates', '_dates', '_rawDates');
|
|
178
|
-
this._serialize(obj, 'non_verbal_rep', '_nonVerbal', '_rawNonVerbal');
|
|
179
|
-
this._serialize(obj, 'related', '_related', '_rawRelated');
|
|
180
|
-
|
|
181
|
-
return obj;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
static fromJSON(data) {
|
|
185
|
-
return new LocalizedConcept(data);
|
|
186
|
-
}
|
|
187
|
-
}
|