glossarist 0.3.6 → 0.3.8

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glossarist",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
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",
@@ -20,7 +20,7 @@ function assertDir(dir, fnName) {
20
20
  *
21
21
  * @example
22
22
  * const concepts = readConcepts('./geolexica-v2/');
23
- * console.log(concepts[0].localizations.eng.terms[0].designation);
23
+ * console.log(concepts[0].localization('eng').terms[0].designation);
24
24
  */
25
25
  export function readConcepts(dir) {
26
26
  assertDir(dir, 'readConcepts');
package/src/gcr-reader.js CHANGED
@@ -418,7 +418,7 @@ export class GcrPackage {
418
418
  *
419
419
  * @example
420
420
  * const concept = parseConceptYaml('termid: "001"\neng:\n terms:\n - designation: test', '001');
421
- * console.log(concept.localizations.eng.terms[0].designation); // "test"
421
+ * console.log(concept.localization('eng').terms[0].designation); // "test"
422
422
  */
423
423
  export function parseConceptYaml(raw, context) {
424
424
  return conceptParser.parse(raw, context);
package/src/index.d.ts CHANGED
@@ -35,8 +35,9 @@ export { conceptUuid, localizedConceptUuid, uuidV5 } from './uuid';
35
35
  export { ReferenceResolver, Reference, referenceResolver } from './reference-resolver';
36
36
 
37
37
  export type MentionParseResult = {
38
- kind: 'cite-ref' | 'numeric' | 'designation' | 'unresolved';
38
+ kind: 'cite-ref' | 'urn-ref' | 'numeric' | 'designation' | 'unresolved';
39
39
  key?: string;
40
+ uri?: string;
40
41
  label?: string | null;
41
42
  id?: string;
42
43
  raw: string;
package/src/index.js CHANGED
@@ -7,7 +7,7 @@ export { ConceptCollection } from './concept-collection.js';
7
7
  export { ManagedConceptCollection } from './managed-concept-collection.js';
8
8
  export { validateConcept, validateRegister, validateGcrPackage, createConceptValidator, ValidationError, ValidationRule, ValidationResult, RegisterValidator, GcrValidator } from './validators/index.js';
9
9
  export { conceptUuid, localizedConceptUuid, uuidV5 } from './uuid.js';
10
- export { ReferenceResolver, Reference, referenceResolver } from './reference-resolver.js';
10
+ export { ReferenceResolver, Reference, referenceResolver, resolveBibliographyRecord } from './reference-resolver.js';
11
11
  export { parseMention } from './reference-mention.js';
12
12
  export { ReferenceClassifier } from './render-classification.js';
13
13
  export { V1Reader, migrateV1ToV2 } from './v1-reader.js';
@@ -15,4 +15,18 @@ export class GlossaristModel {
15
15
  clone() {
16
16
  return this.constructor.fromJSON(JSON.parse(JSON.stringify(this.toJSON())));
17
17
  }
18
+
19
+ _lazy(cacheKey, rawKey, wrapFn) {
20
+ if (this[cacheKey] === null) {
21
+ this[cacheKey] = this[rawKey].map(wrapFn);
22
+ }
23
+ return this[cacheKey];
24
+ }
25
+
26
+ _serialize(obj, jsonKey, cacheKey, rawKey) {
27
+ const items = this[cacheKey] ?? (this[rawKey].length > 0 ? this[rawKey] : []);
28
+ if (items.length > 0) {
29
+ obj[jsonKey] = items.map(i => (i instanceof GlossaristModel) ? i.toJSON() : i);
30
+ }
31
+ }
18
32
  }
@@ -34,11 +34,6 @@ export class Concept extends GlossaristModel {
34
34
  return this.languages;
35
35
  }
36
36
 
37
- /** @deprecated Use localization(lang) for model access, or toJSON().localizations for raw data */
38
- get localizations() {
39
- return this._rawLocalizations;
40
- }
41
-
42
37
  localization(lang) {
43
38
  if (!(lang in this._rawLocalizations)) return undefined;
44
39
  if (!this._cache[lang]) {
@@ -131,20 +131,6 @@ export class LocalizedConcept extends GlossaristModel {
131
131
  return obj;
132
132
  }
133
133
 
134
- _lazy(cacheKey, rawKey, wrapFn) {
135
- if (this[cacheKey] === null) {
136
- this[cacheKey] = this[rawKey].map(wrapFn);
137
- }
138
- return this[cacheKey];
139
- }
140
-
141
- _serialize(obj, jsonKey, cacheKey, rawKey) {
142
- const items = this[cacheKey] ?? (this[rawKey].length > 0 ? this[rawKey] : []);
143
- if (items.length > 0) {
144
- obj[jsonKey] = items.map(i => (typeof i.toJSON === 'function') ? i.toJSON() : i);
145
- }
146
- }
147
-
148
134
  static fromJSON(data) {
149
135
  return new LocalizedConcept(data);
150
136
  }
@@ -7,15 +7,20 @@
7
7
  * Convention: the ID always comes first, the display (render) text
8
8
  * always comes last. Every comma-separated form follows this:
9
9
  *
10
- * {{id}} bare ID
11
- * {{id, display text}} ID + display text
12
10
  * {{cite:key}} cite-key (source id)
13
- * {{cite:key, display text}} cite-key + display text
11
+ * {{cite:key, render term}} cite-key + render term
12
+ * {{urn:...}} URN reference
13
+ * {{urn:..., render term}} URN + render term
14
+ * {{numeric_id}} local concept ID
15
+ * {{numeric_id, render term}} local concept ID + render term
16
+ * {{designation}} designation matching
17
+ * {{designation, render term}} designation + render term
14
18
  *
15
19
  * @typedef {Object} MentionParseResult
16
- * @property {'cite-ref' | 'numeric' | 'designation' | 'unresolved'} kind
20
+ * @property {'cite-ref' | 'urn-ref' | 'numeric' | 'designation' | 'unresolved'} kind
17
21
  * @property {string} [key] — for 'cite-ref': the local key
18
- * @property {string} [label] display text (always last)
22
+ * @property {string} [uri] for 'urn-ref': the URN
23
+ * @property {string} [label] — render text (always last)
19
24
  * @property {string} [id] — for 'numeric' / 'designation': the id
20
25
  * @property {string} raw — the original mention body
21
26
  */
@@ -34,8 +39,7 @@ const NUMERIC_RE = /^\d+(?:[.-]\d+)+$/;
34
39
  export function parseMention(raw) {
35
40
  const body = raw.trim();
36
41
 
37
- // 1. cite:<key>[,display] — explicit citation reference.
38
- // Key is the source id; display text is optional.
42
+ // 1. cite:<key>[,render] — explicit citation reference.
39
43
  const citeMatch = body.match(/^cite:([^,}]+)(?:,(.*))?$/);
40
44
  if (citeMatch) {
41
45
  const label = citeMatch[2] !== undefined ? unquoteLabel(citeMatch[2].trim()) : null;
@@ -47,8 +51,20 @@ export function parseMention(raw) {
47
51
  };
48
52
  }
49
53
 
50
- // 2. Comma-separated form: {{id, display}}.
51
- // ID always comes first, display text always comes last.
54
+ // 2. urn:...[,render] URN reference.
55
+ const urnMatch = body.match(/^(urn:[^,}]+)(?:,(.*))?$/);
56
+ if (urnMatch) {
57
+ const label = urnMatch[2] !== undefined ? unquoteLabel(urnMatch[2].trim()) : null;
58
+ return {
59
+ kind: 'urn-ref',
60
+ uri: urnMatch[1].trim(),
61
+ label,
62
+ raw: body,
63
+ };
64
+ }
65
+
66
+ // 3. Comma-separated form: {{id, render}}.
67
+ // ID always comes first, render text always comes last.
52
68
  const commaIdx = body.indexOf(',');
53
69
  if (commaIdx !== -1) {
54
70
  const id = body.slice(0, commaIdx).trim();
@@ -60,12 +76,12 @@ export function parseMention(raw) {
60
76
  return { kind: 'designation', id, label, raw: body };
61
77
  }
62
78
 
63
- // 3. Bare numeric id.
79
+ // 4. Bare numeric id.
64
80
  if (NUMERIC_RE.test(body)) {
65
81
  return { kind: 'numeric', id: body, label: null, raw: body };
66
82
  }
67
83
 
68
- // 4. Anything else is unresolved at the parse layer.
84
+ // 5. Anything else is unresolved at the parse layer.
69
85
  return { kind: 'unresolved', raw: body };
70
86
  }
71
87
 
@@ -2,29 +2,11 @@ import { ConceptRef } from './models/concept-ref.js';
2
2
  import { parseMention } from './reference-mention.js';
3
3
 
4
4
  export class Reference {
5
- /**
6
- * @param {string} type — the structural kind of the reference
7
- * ('concept', 'dataset', 'bibliography', 'typed-ref',
8
- * 'standard').
9
- * @param {string | null} target — the legacy flat display
10
- * string. Kept for backward compat with callers that only
11
- * read `r.target`.
12
- * @param {string | null} [relationship] — the type of the
13
- * relationship that produced this reference (e.g. 'see',
14
- * 'supersedes', 'source').
15
- * @param {string | null} [source] — a JSON-pointer-ish path
16
- * indicating where in the concept the reference was
17
- * extracted from.
18
- * @param {object} [extras] — additional fields (v8+):
19
- * `citation`, `sourceId`, `resolution`, `lookupKey`,
20
- * `label`, `quoted`, `uri`. All optional.
21
- */
22
5
  constructor(type, target, relationship, source, extras = {}) {
23
6
  this.type = type;
24
7
  this.target = target;
25
8
  this.relationship = relationship ?? null;
26
9
  this.source = source ?? null;
27
-
28
10
  this.uri = extras.uri ?? null;
29
11
  this.citation = extras.citation ?? null;
30
12
  this.sourceId = extras.sourceId ?? null;
@@ -41,29 +23,26 @@ function refTarget(rc) {
41
23
  return '';
42
24
  }
43
25
 
26
+ export function resolveBibliographyRecord(citationRef, registry) {
27
+ if (!citationRef?.source || !citationRef?.id) return null;
28
+ const bioColl = registry[`bibliography:${citationRef.source}`]?.concepts;
29
+ if (!bioColl) return null;
30
+ if (citationRef.version) {
31
+ return bioColl.byIdAnd(citationRef.id, citationRef.version);
32
+ }
33
+ return bioColl.byId(citationRef.id);
34
+ }
35
+
44
36
  export class ReferenceResolver {
45
- /**
46
- * Extract all embedded references from a concept's localizations.
47
- *
48
- * Walks definitions, notes, examples, and annotations text.
49
- * For each `{{...}}` mention, runs `parseMention` to
50
- * classify the form, then dispatches:
51
- * - 'cite-ref' → look up the key in concept.sources; emit
52
- * Bibliography Reference with the Citation.
53
- * - 'numeric' → emit Concept Reference with the bare id
54
- * (existing behavior).
55
- * - 'unresolved' → do not emit a Reference.
56
- *
57
- * @param {Concept} concept
58
- * @returns {Reference[]}
59
- */
60
37
  extractReferences(concept) {
61
38
  const refs = [];
62
39
 
63
40
  for (const rc of concept.relatedConcepts) {
64
41
  const target = refTarget(rc);
65
42
  if (target) {
66
- refs.push(new Reference('concept', target, rc.type, 'relatedConcepts'));
43
+ refs.push(new Reference('concept', target, rc.type, 'relatedConcepts', {
44
+ lookupKey: { id: target },
45
+ }));
67
46
  }
68
47
  }
69
48
 
@@ -71,13 +50,11 @@ export class ReferenceResolver {
71
50
  const lc = concept.localization(lang);
72
51
  if (!lc) continue;
73
52
 
74
- if (lc.sources) {
75
- for (let i = 0; i < lc.sources.length; i++) {
76
- const src = lc.sources[i];
77
- const ref = src.origin?.toString() ?? '';
78
- if (ref) {
79
- refs.push(new Reference('standard', ref, src.type, `localizations.${lang}.sources[${i}]`));
80
- }
53
+ for (let i = 0; i < lc.sources.length; i++) {
54
+ const src = lc.sources[i];
55
+ const ref = src.origin?.toString() ?? '';
56
+ if (ref) {
57
+ refs.push(new Reference('standard', ref, src.type, `localizations.${lang}.sources[${i}]`));
81
58
  }
82
59
  }
83
60
 
@@ -92,37 +69,27 @@ export class ReferenceResolver {
92
69
  return refs;
93
70
  }
94
71
 
95
- /**
96
- * Collect all text fields from a localized concept, paired
97
- * with diagnostic source paths.
98
- *
99
- * @param {LocalizedConcept} lc
100
- * @param {string} lang
101
- * @returns {{text: string, source: string}[]}
102
- */
103
72
  _collectTexts(lc, lang) {
104
73
  const out = [];
105
- for (let i = 0; (lc.definitions ?? [])[i]; i++) {
74
+ for (let i = 0; i < lc.definitions.length; i++) {
106
75
  const content = lc.definitions[i]?.content;
107
76
  if (typeof content === 'string') {
108
77
  out.push({ text: content, source: `localizations.${lang}.definitions[${i}].content` });
109
78
  }
110
79
  }
111
- for (let i = 0; (lc.notes ?? [])[i]; i++) {
112
- const content = typeof lc.notes[i] === 'object'
113
- ? (lc.notes[i]?.content ?? '')
114
- : String(lc.notes[i] ?? '');
80
+ for (let i = 0; i < lc.notes.length; i++) {
81
+ const content = lc.notes[i]?.content;
115
82
  if (content) {
116
83
  out.push({ text: content, source: `localizations.${lang}.notes[${i}].content` });
117
84
  }
118
85
  }
119
- for (let i = 0; (lc.examples ?? [])[i]; i++) {
86
+ for (let i = 0; i < lc.examples.length; i++) {
120
87
  const content = lc.examples[i]?.content;
121
88
  if (typeof content === 'string') {
122
89
  out.push({ text: content, source: `localizations.${lang}.examples[${i}].content` });
123
90
  }
124
91
  }
125
- for (let i = 0; (lc.annotations ?? [])[i]; i++) {
92
+ for (let i = 0; i < lc.annotations.length; i++) {
126
93
  const content = lc.annotations[i]?.content;
127
94
  if (typeof content === 'string') {
128
95
  out.push({ text: content, source: `localizations.${lang}.annotations[${i}].content` });
@@ -131,15 +98,6 @@ export class ReferenceResolver {
131
98
  return out;
132
99
  }
133
100
 
134
- /**
135
- * Walk a single text string and emit References for each
136
- * `{{...}}` mention.
137
- *
138
- * @param {string} text
139
- * @param {string} source — diagnostic path
140
- * @param {Concept} concept — the owning concept (for cite-ref lookup)
141
- * @returns {Reference[]}
142
- */
143
101
  _extractFromText(text, source, concept) {
144
102
  const refs = [];
145
103
  const re = /\{\{([^{}]*?)\}\}/g;
@@ -150,6 +108,12 @@ export class ReferenceResolver {
150
108
  case 'cite-ref':
151
109
  refs.push(this._resolveCiteRef(parsed, source, concept));
152
110
  break;
111
+ case 'urn-ref':
112
+ refs.push(new Reference('concept', parsed.label ?? parsed.uri, 'embedded', source, {
113
+ uri: parsed.uri,
114
+ resolution: null,
115
+ }));
116
+ break;
153
117
  case 'numeric':
154
118
  refs.push(new Reference('concept', parsed.label ?? parsed.id, 'embedded', source, {
155
119
  lookupKey: { id: parsed.id },
@@ -167,17 +131,6 @@ export class ReferenceResolver {
167
131
  return refs;
168
132
  }
169
133
 
170
- /**
171
- * Resolve a `cite-ref` parser result against the concept's
172
- * sources list. Emits a Bibliography Reference with the
173
- * resolved Citation (if found) or an unresolved Reference
174
- * (if not).
175
- *
176
- * @param {MentionParseResult} parsed
177
- * @param {string} source — diagnostic path
178
- * @param {Concept} concept — the owning concept
179
- * @returns {Reference}
180
- */
181
134
  _resolveCiteRef(parsed, source, concept) {
182
135
  const sourceEntry = concept?.findSourceById(parsed.key) ?? null;
183
136
  if (!sourceEntry) {
@@ -209,118 +162,57 @@ export class ReferenceResolver {
209
162
  );
210
163
  }
211
164
 
212
- /**
213
- * Resolve a single reference against a registry (a map of
214
- * datasetId → { concepts, register? }). The registry may also
215
- * include 'bibliography:<source>' keys for bibliographic
216
- * datasets.
217
- *
218
- * For a `type: 'bibliography'` Reference with an inline
219
- * `citation`, the resolver first tries the bibliography
220
- * registry (matching `citation.ref` by source/id/version);
221
- * if not found, returns the inline Citation as a
222
- * self-contained fallback.
223
- *
224
- * For a `type: 'bibliography'` Reference with a `uri` and
225
- * `resolution.kind === 'bibliography-namespace'`, the
226
- * resolver tries the bibliography registry by
227
- * `resolution.source/id/version`.
228
- *
229
- * For `type: 'concept'` References with a `lookupKey.id`
230
- * (id-match, short-id, or numeric), the resolver looks up
231
- * the id in `lookupKey.dataset`'s ConceptCollection.
232
- *
233
- * Backward compat: when the second argument is a
234
- * ConceptCollection (has `byId` but no `concepts` field), it
235
- * is treated as a one-key registry of one default dataset.
236
- */
237
165
  resolveReference(ref, registry) {
238
- if (ref == null) return null;
239
-
240
- // Backward-compat: single ConceptCollection becomes a
241
- // one-key registry.
242
- if (isConceptCollection(registry)) {
243
- registry = { _default: { concepts: registry } };
244
- }
245
- if (registry == null) return null;
166
+ if (ref == null || registry == null) return null;
246
167
 
247
- // 1. cite:key form (Bibliography with inline Citation).
248
- if (ref.type === 'bibliography' && ref.citation) {
249
- const bioRecord = this._resolveBibliographyRecord(
250
- ref.citation.ref,
251
- registry,
252
- );
253
- if (bioRecord) return bioRecord;
254
- return ref.citation;
168
+ switch (ref.type) {
169
+ case 'concept': return this._resolveConcept(ref, registry);
170
+ case 'bibliography': return this._resolveBibliography(ref, registry);
171
+ case 'dataset': return this._resolveDataset(ref, registry);
172
+ case 'typed-ref': return this._resolveTypedRef(ref, registry);
173
+ case 'standard': return this._resolveStandard(ref, registry);
174
+ default: return null;
255
175
  }
176
+ }
256
177
 
257
- // 2. URI form (urn:... or https:...) with
258
- // bibliography-namespace resolution.
259
- if (ref.uri) {
260
- if (ref.resolution?.kind === 'bibliography-namespace'
261
- || (ref.resolution?.source && !ref.resolution?.datasetId)) {
262
- const bioRecord = this._resolveBibliographyRecord(
263
- ref.resolution,
264
- registry,
265
- );
266
- if (bioRecord) return bioRecord;
178
+ _resolveConcept(ref, registry) {
179
+ if (ref.lookupKey?.id) {
180
+ const dataset = ref.lookupKey.dataset;
181
+ if (dataset) {
182
+ return registry[dataset]?.concepts?.byId(ref.lookupKey.id) ?? null;
267
183
  }
268
- // Concept URI lookup (for non-bibliography URIs).
269
- if (ref.resolution?.datasetId) {
270
- const coll = registry[ref.resolution.datasetId]?.concepts;
271
- if (coll) {
272
- return coll.byId(ref.resolution.conceptId);
273
- }
184
+ for (const entry of Object.values(registry)) {
185
+ const found = entry?.concepts?.byId(ref.lookupKey.id);
186
+ if (found) return found;
274
187
  }
275
188
  return null;
276
189
  }
277
-
278
- // 3. Same-dataset concept id (numeric mention, id-match, etc.).
279
- if (ref.lookupKey?.id) {
280
- const coll = registry[ref.lookupKey.dataset]?.concepts;
281
- if (coll) return coll.byId(ref.lookupKey.id);
282
- return null;
190
+ if (ref.uri && ref.resolution?.datasetId) {
191
+ return registry[ref.resolution.datasetId]?.concepts?.byId(ref.resolution.conceptId) ?? null;
283
192
  }
193
+ return null;
194
+ }
284
195
 
285
- // 3b. Backward-compat: a concept ref with a `target` (id)
286
- // but no `lookupKey` is looked up in the single
287
- // collection (backward-compat one-key registry).
288
- if (ref.type === 'concept' && ref.target) {
289
- const defaultColl = registry._default?.concepts;
290
- if (defaultColl) return defaultColl.byId(ref.target);
291
- // Try every dataset in the registry as a fallback.
292
- for (const entry of Object.values(registry)) {
293
- if (entry?.concepts?.byId(ref.target)) {
294
- return entry.concepts.byId(ref.target);
295
- }
296
- }
297
- return null;
196
+ _resolveBibliography(ref, registry) {
197
+ if (ref.citation) {
198
+ return resolveBibliographyRecord(ref.citation.ref, registry) ?? ref.citation;
298
199
  }
299
-
300
- // 4. Unanchored designation — search is a separate concern
301
- // (plan 06). For v8, return null.
302
- if (ref.lookupKey?.designation) {
303
- return null;
200
+ if (ref.uri && ref.resolution?.source) {
201
+ return resolveBibliographyRecord(ref.resolution, registry) ?? null;
304
202
  }
203
+ return null;
204
+ }
305
205
 
206
+ _resolveDataset(_ref, _registry) {
306
207
  return null;
307
208
  }
308
209
 
309
- /**
310
- * Try to resolve a Citation::Ref against the bibliography
311
- * registry. The ref has shape { source, id, version? }.
312
- *
313
- * Returns the matching bibliographic record (a Concept), or
314
- * null if no match.
315
- */
316
- _resolveBibliographyRecord(citationRef, registry) {
317
- if (!citationRef?.source || !citationRef?.id) return null;
318
- const bioColl = registry[`bibliography:${citationRef.source}`]?.concepts;
319
- if (!bioColl) return null;
320
- if (citationRef.version) {
321
- return bioColl.byIdAnd(citationRef.id, citationRef.version);
322
- }
323
- return bioColl.byId(citationRef.id);
210
+ _resolveTypedRef(_ref, _registry) {
211
+ return null;
212
+ }
213
+
214
+ _resolveStandard(_ref, _registry) {
215
+ return null;
324
216
  }
325
217
 
326
218
  resolveAll(concept, registry) {
@@ -338,15 +230,4 @@ export class ReferenceResolver {
338
230
  }
339
231
  }
340
232
 
341
- /**
342
- * Type-guard for the single-collection case (backward compat).
343
- * A ConceptCollection has `byId` but no `concepts` field.
344
- */
345
- function isConceptCollection(x) {
346
- return x != null
347
- && typeof x === 'object'
348
- && typeof x.byId === 'function'
349
- && !('concepts' in x);
350
- }
351
-
352
233
  export const referenceResolver = new ReferenceResolver();
@@ -1,47 +1,26 @@
1
- /**
2
- * Classify a Reference for rendering.
3
- *
4
- * The classifier is constructed once per render with a registry
5
- * (and optional source dataset id). The classify() method is
6
- * pure and side-effect-free.
7
- *
8
- * Each `Reference.type` is its own `_classifyXxx` method. The
9
- * dispatch in classify() is closed for modification.
10
- */
1
+ import { resolveBibliographyRecord } from './reference-resolver.js';
11
2
 
12
3
  export class ReferenceClassifier {
13
- /**
14
- * @param {object} registry — the deployment's dataset registry.
15
- * @param {string} [sourceDatasetId] — the dataset the source
16
- * concept belongs to; used to determine "same-dataset".
17
- * @param {object} [options] — additional options (e.g. scope).
18
- */
19
4
  constructor(registry = {}, sourceDatasetId = null, options = {}) {
20
5
  this.registry = registry;
21
6
  this.sourceDatasetId = sourceDatasetId;
22
7
  this.options = options;
23
8
  }
24
9
 
25
- /**
26
- * @param {Reference} ref
27
- * @returns {string} — the classification (e.g. 'same-dataset',
28
- * 'internal-citation', 'unresolved', etc.)
29
- */
30
10
  classify(ref) {
31
11
  if (ref == null) return 'unknown';
32
12
 
33
13
  switch (ref.type) {
34
- case 'concept': return this._classifyConcept(ref);
35
- case 'dataset': return this._classifyDataset(ref);
14
+ case 'concept': return this._classifyConcept(ref);
15
+ case 'dataset': return this._classifyDataset(ref);
36
16
  case 'bibliography': return this._classifyBibliography(ref);
37
- case 'typed-ref': return this._classifyTypedRef(ref);
38
- case 'standard': return 'legacy-standard';
39
- default: return 'unknown';
17
+ case 'typed-ref': return this._classifyTypedRef(ref);
18
+ case 'standard': return 'legacy-standard';
19
+ default: return 'unknown';
40
20
  }
41
21
  }
42
22
 
43
23
  _classifyConcept(ref) {
44
- // 1. URI form, resolved to a dataset.
45
24
  if (ref.uri) {
46
25
  const dsId = ref.resolution?.datasetId;
47
26
  if (!dsId) return 'unresolved';
@@ -49,21 +28,15 @@ export class ReferenceClassifier {
49
28
  if (dsId === this.sourceDatasetId) return 'same-dataset';
50
29
  return 'cross-dataset';
51
30
  }
52
- // 2. Unanchored designation.
53
31
  if (ref.lookupKey?.designation) {
54
32
  return 'unresolved-designation';
55
33
  }
56
- // 3. Id-style (id-match, short-id, numeric).
57
34
  if (ref.lookupKey?.id) {
58
35
  const dsId = ref.lookupKey.dataset;
59
36
  if (!this.registry[dsId]) return 'unresolved';
60
37
  if (dsId === this.sourceDatasetId) return 'same-dataset';
61
38
  return 'cross-dataset';
62
39
  }
63
- // 4. Concept ref with target (legacy).
64
- if (ref.target) {
65
- return 'unresolved';
66
- }
67
40
  return 'unresolved';
68
41
  }
69
42
 
@@ -74,18 +47,15 @@ export class ReferenceClassifier {
74
47
  }
75
48
 
76
49
  _classifyBibliography(ref) {
77
- // 1. cite:key form: try the bibliography registry.
78
50
  if (ref.citation) {
79
- const bioRecord = this._tryBibliography(ref.citation.ref);
80
- if (bioRecord) return 'internal-citation';
81
- return 'self-contained-citation';
51
+ return resolveBibliographyRecord(ref.citation.ref, this.registry)
52
+ ? 'internal-citation'
53
+ : 'self-contained-citation';
82
54
  }
83
- // 2. URI form: try the bibliography registry, then the
84
- // resolution's datasetId (if it's a concept URI), else null.
85
55
  if (ref.uri) {
86
- const bioRecord = this._tryBibliography(ref.resolution);
87
- if (bioRecord) return 'internal-citation';
88
- return 'external-citation';
56
+ return resolveBibliographyRecord(ref.resolution, this.registry)
57
+ ? 'internal-citation'
58
+ : 'external-citation';
89
59
  }
90
60
  return 'unresolved-citation';
91
61
  }
@@ -93,14 +63,4 @@ export class ReferenceClassifier {
93
63
  _classifyTypedRef(_ref) {
94
64
  return 'typed-ref';
95
65
  }
96
-
97
- _tryBibliography(citationRef) {
98
- if (!citationRef?.source || !citationRef?.id) return null;
99
- const bioColl = this.registry[`bibliography:${citationRef.source}`]?.concepts;
100
- if (!bioColl) return null;
101
- if (citationRef.version) {
102
- return bioColl.byIdAnd(citationRef.id, citationRef.version) ?? null;
103
- }
104
- return bioColl.byId(citationRef.id) ?? null;
105
- }
106
66
  }
@@ -9,18 +9,13 @@ const VALID_ENTRY_STATUSES = new Set([
9
9
  'valid', 'draft', 'retired', 'notValid', 'superseded', 'withdrawn',
10
10
  ]);
11
11
 
12
- const _langs = (c) =>
13
- c.languages ?? (c.localizations ? Object.keys(c.localizations) : []);
14
-
15
- const _loc = (c, lang) =>
16
- typeof c.localization === 'function' ? c.localization(lang) : c.localizations?.[lang];
17
-
18
12
  export class LanguageCodeRule extends ValidationRule {
19
13
  constructor() { super('language-code'); }
20
14
  validate(concept, path, result) {
21
- for (const lang of _langs(concept)) {
15
+ for (const lang of concept.languages) {
22
16
  if (!/^[a-z]{3}$/.test(lang)) {
23
- result.addError(`${path}localizations.${lang}`,
17
+ this.addIssue(result,
18
+ `${path}localizations.${lang}`,
24
19
  `Invalid language code '${lang}': expected ISO 639-3 (3 lowercase letters)`);
25
20
  }
26
21
  }
@@ -30,15 +25,14 @@ export class LanguageCodeRule extends ValidationRule {
30
25
  export class DesignationTypeRule extends ValidationRule {
31
26
  constructor() { super('designation-type'); }
32
27
  validate(concept, path, result) {
33
- for (const lang of _langs(concept)) {
34
- const lc = _loc(concept, lang);
28
+ for (const lang of concept.languages) {
29
+ const lc = concept.localization(lang);
35
30
  if (!lc) continue;
36
- const terms = lc.terms ?? [];
37
- for (let i = 0; i < terms.length; i++) {
38
- const t = terms[i];
39
- const type = t.type ?? (typeof t.toJSON === 'function' ? t.toJSON().type : undefined);
31
+ for (let i = 0; i < lc.terms.length; i++) {
32
+ const type = lc.terms[i].type;
40
33
  if (type && !VALID_DESIGNATION_TYPES.has(type)) {
41
- result.addError(`${path}localizations.${lang}.terms[${i}].type`,
34
+ this.addIssue(result,
35
+ `${path}localizations.${lang}.terms[${i}].type`,
42
36
  `Unknown designation type '${type}'`);
43
37
  }
44
38
  }
@@ -49,13 +43,13 @@ export class DesignationTypeRule extends ValidationRule {
49
43
  export class EntryStatusRule extends ValidationRule {
50
44
  constructor() { super('entry-status'); }
51
45
  validate(concept, path, result) {
52
- for (const lang of _langs(concept)) {
53
- const lc = _loc(concept, lang);
46
+ for (const lang of concept.languages) {
47
+ const lc = concept.localization(lang);
54
48
  if (!lc) continue;
55
- const status = lc.entryStatus ?? lc.entry_status;
56
- if (status && !VALID_ENTRY_STATUSES.has(status)) {
57
- result.addError(`${path}localizations.${lang}.entry_status`,
58
- `Unknown entry status '${status}'`);
49
+ if (lc.entryStatus && !VALID_ENTRY_STATUSES.has(lc.entryStatus)) {
50
+ this.addIssue(result,
51
+ `${path}localizations.${lang}.entry_status`,
52
+ `Unknown entry status '${lc.entryStatus}'`);
59
53
  }
60
54
  }
61
55
  }
@@ -71,30 +65,21 @@ export class ConceptValidator {
71
65
 
72
66
  validate(concept) {
73
67
  const result = new ValidationResult();
74
- const hasModelApi = typeof concept.localization === 'function';
75
68
 
76
69
  if (!concept.id) {
77
70
  result.addError('id', 'Concept must have an id');
78
71
  }
79
72
 
80
- const langs = hasModelApi ? concept.languages : Object.keys(concept.localizations ?? {});
81
- if (langs.length === 0) {
73
+ if (concept.languages.length === 0) {
82
74
  result.addWarning('localizations', 'Concept must have at least one localization');
83
- } else if (hasModelApi) {
84
- for (const lang of langs) {
75
+ } else {
76
+ for (const lang of concept.languages) {
85
77
  const lc = concept.localization(lang);
86
78
  if (!lc || lc.terms.length === 0) {
87
79
  result.addWarning(`localizations.${lang}.terms`,
88
80
  `Localization '${lang}' must have at least one term`);
89
81
  }
90
82
  }
91
- } else {
92
- for (const [lang, lc] of Object.entries(concept.localizations ?? {})) {
93
- if (!lc.terms || lc.terms.length === 0) {
94
- result.addWarning(`localizations.${lang}.terms`,
95
- `Localization '${lang}' must have at least one term`);
96
- }
97
- }
98
83
  }
99
84
 
100
85
  for (const rule of this._rules) {
@@ -1,14 +1,8 @@
1
1
  import { ValidationRule } from './validation-rule.js';
2
2
 
3
- const _langs = (concept) =>
4
- concept.languages ?? (concept.localizations ? Object.keys(concept.localizations) : []);
5
-
6
- const _loc = (concept, lang) =>
7
- typeof concept.localization === 'function' ? concept.localization(lang) : concept.localizations?.[lang];
8
-
9
3
  const _eachLocalization = (concept, fn) => {
10
- for (const lang of _langs(concept)) {
11
- const lc = _loc(concept, lang);
4
+ for (const lang of concept.languages) {
5
+ const lc = concept.localization(lang);
12
6
  if (lc) fn(lang, lc);
13
7
  }
14
8
  };
@@ -19,7 +13,7 @@ export class RefShapeRule extends ValidationRule {
19
13
  validate(concept, path, result) {
20
14
  let sourceIdx = 0;
21
15
  _eachLocalization(concept, (lang, lc) => {
22
- const sources = lc.sources ?? [];
16
+ const sources = lc.sources;
23
17
  for (let i = 0; i < sources.length; i++) {
24
18
  sourceIdx++;
25
19
  const origin = sources[i].origin;
@@ -38,7 +32,7 @@ export class RefShapeRule extends ValidationRule {
38
32
  }
39
33
  });
40
34
 
41
- const related = concept.relatedConcepts ?? concept.related ?? [];
35
+ const related = concept.relatedConcepts;
42
36
  for (let i = 0; i < related.length; i++) {
43
37
  const ref = related[i].ref;
44
38
  if (!ref) continue;
@@ -56,7 +50,7 @@ export class LocalityCompletenessRule extends ValidationRule {
56
50
 
57
51
  validate(concept, path, result) {
58
52
  _eachLocalization(concept, (lang, lc) => {
59
- const sources = lc.sources ?? [];
53
+ const sources = lc.sources;
60
54
  for (let i = 0; i < sources.length; i++) {
61
55
  const origin = sources[i].origin;
62
56
  if (!origin || !origin.locality) continue;
@@ -81,14 +75,14 @@ export class LocalizationConsistencyRule extends ValidationRule {
81
75
  constructor() { super('localization-consistency'); }
82
76
 
83
77
  validate(concept, path, result) {
84
- const langs = _langs(concept);
85
- const data = concept.raw?.data || concept;
78
+ const langs = concept.languages;
79
+ const data = concept.raw?.data || {};
86
80
  const declaredLangs = data.localized_concepts
87
81
  ? Object.keys(data.localized_concepts)
88
82
  : langs;
89
83
 
90
84
  for (const lang of declaredLangs) {
91
- if (!concept.hasLocalization?.(lang) && !(concept.localizations?.[lang])) {
85
+ if (!concept.hasLocalization(lang)) {
92
86
  this.addIssue(result,
93
87
  `${path}localizations.${lang}`,
94
88
  `localized_concepts map has '${lang}' but no localization loaded`);
@@ -101,12 +95,10 @@ export class SchemaVersionRule extends ValidationRule {
101
95
  constructor() { super('schema-version', 'warning'); }
102
96
 
103
97
  validate(concept, path, result) {
104
- const version = concept.schemaVersion ?? concept.schema_version;
105
-
106
- if (version && String(version) !== '3') {
98
+ if (concept.schemaVersion && String(concept.schemaVersion) !== '3') {
107
99
  this.addIssue(result,
108
100
  `${path}schema_version`,
109
- `schema_version is '${version}', expected '3'`);
101
+ `schema_version is '${concept.schemaVersion}', expected '3'`);
110
102
  }
111
103
  }
112
104
  }
@@ -115,11 +107,8 @@ export class DomainRefRule extends ValidationRule {
115
107
  constructor() { super('domain-ref', 'warning'); }
116
108
 
117
109
  validate(concept, path, result) {
118
- const domains = concept.domains || [];
119
-
120
- for (let i = 0; i < domains.length; i++) {
121
- const domain = domains[i];
122
- const json = typeof domain.toJSON === 'function' ? domain.toJSON() : domain;
110
+ for (let i = 0; i < concept.domains.length; i++) {
111
+ const json = concept.domains[i].toJSON();
123
112
  if (!json.concept_id && !json.urn) {
124
113
  this.addIssue(result,
125
114
  `${path}domains[${i}]`,
@@ -134,7 +123,7 @@ export class UuidFormatRule extends ValidationRule {
134
123
 
135
124
  validate(concept, path, result) {
136
125
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
137
- const id = concept.id || concept.uuid;
126
+ const id = concept.id;
138
127
 
139
128
  if (id && !UUID_RE.test(String(id))) {
140
129
  if (String(id).includes('-') && String(id).length > 20) {
@@ -153,7 +142,7 @@ export class SourceUrnFormatRule extends ValidationRule {
153
142
  const URN_RE = /^urn:[a-z0-9][a-z0-9-]{0,31}:[a-z0-9()+,\-.:=@;$_!*'%/?#]+$/i;
154
143
 
155
144
  _eachLocalization(concept, (lang, lc) => {
156
- const sources = lc.sources ?? [];
145
+ const sources = lc.sources;
157
146
  for (let i = 0; i < sources.length; i++) {
158
147
  const source = lc.sources[i].origin?.ref?.source;
159
148
  if (!source || !source.startsWith('urn:')) continue;
@@ -180,23 +169,23 @@ function _findCiteMentions(concept) {
180
169
  }
181
170
  };
182
171
 
183
- for (const lang of _langs(concept)) {
184
- const lc = _loc(concept, lang);
172
+ for (const lang of concept.languages) {
173
+ const lc = concept.localization(lang);
185
174
  if (!lc) continue;
186
175
 
187
- for (let i = 0; (lc.definitions ?? [])[i]; i++) {
176
+ for (let i = 0; lc.definitions[i]; i++) {
188
177
  walkText(lc.definitions[i]?.content, `localizations.${lang}.definitions[${i}].content`);
189
178
  }
190
- for (let i = 0; (lc.notes ?? [])[i]; i++) {
179
+ for (let i = 0; lc.notes[i]; i++) {
191
180
  const content = typeof lc.notes[i] === 'object'
192
181
  ? (lc.notes[i]?.content ?? '')
193
182
  : String(lc.notes[i] ?? '');
194
183
  walkText(content, `localizations.${lang}.notes[${i}].content`);
195
184
  }
196
- for (let i = 0; (lc.examples ?? [])[i]; i++) {
185
+ for (let i = 0; lc.examples[i]; i++) {
197
186
  walkText(lc.examples[i]?.content, `localizations.${lang}.examples[${i}].content`);
198
187
  }
199
- for (let i = 0; (lc.annotations ?? [])[i]; i++) {
188
+ for (let i = 0; lc.annotations[i]; i++) {
200
189
  walkText(lc.annotations[i]?.content, `localizations.${lang}.annotations[${i}].content`);
201
190
  }
202
191
  }
@@ -212,13 +201,13 @@ function _findDuplicateSourceIds(concept) {
212
201
  seen.get(source.id).push(source);
213
202
  };
214
203
 
215
- for (const source of (concept.sources ?? [])) record(source);
216
- for (const lang of _langs(concept)) {
217
- const lc = _loc(concept, lang);
204
+ for (const source of concept.sources) record(source);
205
+ for (const lang of concept.languages) {
206
+ const lc = concept.localization(lang);
218
207
  if (!lc) continue;
219
- for (const source of (lc.sources ?? [])) record(source);
220
- for (const designation of (lc.terms ?? [])) {
221
- for (const source of (designation.sources ?? [])) record(source);
208
+ for (const source of (lc.sources)) record(source);
209
+ for (const designation of lc.terms) {
210
+ for (const source of designation.sources) record(source);
222
211
  }
223
212
  }
224
213
 
@@ -231,17 +220,17 @@ function _findDuplicateSourceIds(concept) {
231
220
 
232
221
  function _collectSourceIds(concept) {
233
222
  const ids = new Set();
234
- for (const source of (concept.sources ?? [])) {
223
+ for (const source of concept.sources) {
235
224
  if (source?.id != null) ids.add(source.id);
236
225
  }
237
- for (const lang of _langs(concept)) {
238
- const lc = _loc(concept, lang);
226
+ for (const lang of concept.languages) {
227
+ const lc = concept.localization(lang);
239
228
  if (!lc) continue;
240
- for (const source of (lc.sources ?? [])) {
229
+ for (const source of (lc.sources)) {
241
230
  if (source?.id != null) ids.add(source.id);
242
231
  }
243
- for (const designation of (lc.terms ?? [])) {
244
- for (const source of (designation.sources ?? [])) {
232
+ for (const designation of lc.terms) {
233
+ for (const source of designation.sources) {
245
234
  if (source?.id != null) ids.add(source.id);
246
235
  }
247
236
  }