glossarist 0.3.7 → 0.4.0

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.
@@ -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) {
@@ -14,6 +14,8 @@ export {
14
14
  UuidFormatRule,
15
15
  SourceUrnFormatRule,
16
16
  CiteRefIntegrityRule,
17
+ NonVerbalRefIntegrityRule,
18
+ OrphanedImagesRule,
17
19
  } from './v3-rules.js';
18
20
 
19
21
  import { ConceptValidator, LanguageCodeRule, DesignationTypeRule, EntryStatusRule } from './concept-validator.js';
@@ -29,6 +31,7 @@ import {
29
31
  UuidFormatRule,
30
32
  SourceUrnFormatRule,
31
33
  CiteRefIntegrityRule,
34
+ NonVerbalRefIntegrityRule,
32
35
  } from './v3-rules.js';
33
36
 
34
37
  const _default = new ConceptValidator()
@@ -43,7 +46,8 @@ const _default = new ConceptValidator()
43
46
  .addRule(new UuidFormatRule())
44
47
  .addRule(new SourceUrnFormatRule())
45
48
  .addRule(new RelationshipTypeRule())
46
- .addRule(new CiteRefIntegrityRule());
49
+ .addRule(new CiteRefIntegrityRule())
50
+ .addRule(new NonVerbalRefIntegrityRule());
47
51
 
48
52
  export function validateConcept(concept) {
49
53
  return _default.validate(concept);
@@ -1,14 +1,10 @@
1
1
  import { ValidationRule } from './validation-rule.js';
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];
2
+ import { parseMention } from '../reference-mention.js';
3
+ import { GraphicalSymbol } from '../models/designation.js';
8
4
 
9
5
  const _eachLocalization = (concept, fn) => {
10
- for (const lang of _langs(concept)) {
11
- const lc = _loc(concept, lang);
6
+ for (const lang of concept.languages) {
7
+ const lc = concept.localization(lang);
12
8
  if (lc) fn(lang, lc);
13
9
  }
14
10
  };
@@ -19,7 +15,7 @@ export class RefShapeRule extends ValidationRule {
19
15
  validate(concept, path, result) {
20
16
  let sourceIdx = 0;
21
17
  _eachLocalization(concept, (lang, lc) => {
22
- const sources = lc.sources ?? [];
18
+ const sources = lc.sources;
23
19
  for (let i = 0; i < sources.length; i++) {
24
20
  sourceIdx++;
25
21
  const origin = sources[i].origin;
@@ -38,7 +34,7 @@ export class RefShapeRule extends ValidationRule {
38
34
  }
39
35
  });
40
36
 
41
- const related = concept.relatedConcepts ?? concept.related ?? [];
37
+ const related = concept.relatedConcepts;
42
38
  for (let i = 0; i < related.length; i++) {
43
39
  const ref = related[i].ref;
44
40
  if (!ref) continue;
@@ -56,7 +52,7 @@ export class LocalityCompletenessRule extends ValidationRule {
56
52
 
57
53
  validate(concept, path, result) {
58
54
  _eachLocalization(concept, (lang, lc) => {
59
- const sources = lc.sources ?? [];
55
+ const sources = lc.sources;
60
56
  for (let i = 0; i < sources.length; i++) {
61
57
  const origin = sources[i].origin;
62
58
  if (!origin || !origin.locality) continue;
@@ -81,14 +77,14 @@ export class LocalizationConsistencyRule extends ValidationRule {
81
77
  constructor() { super('localization-consistency'); }
82
78
 
83
79
  validate(concept, path, result) {
84
- const langs = _langs(concept);
85
- const data = concept.raw?.data || concept;
80
+ const langs = concept.languages;
81
+ const data = concept.raw?.data || {};
86
82
  const declaredLangs = data.localized_concepts
87
83
  ? Object.keys(data.localized_concepts)
88
84
  : langs;
89
85
 
90
86
  for (const lang of declaredLangs) {
91
- if (!concept.hasLocalization?.(lang) && !(concept.localizations?.[lang])) {
87
+ if (!concept.hasLocalization(lang)) {
92
88
  this.addIssue(result,
93
89
  `${path}localizations.${lang}`,
94
90
  `localized_concepts map has '${lang}' but no localization loaded`);
@@ -101,12 +97,10 @@ export class SchemaVersionRule extends ValidationRule {
101
97
  constructor() { super('schema-version', 'warning'); }
102
98
 
103
99
  validate(concept, path, result) {
104
- const version = concept.schemaVersion ?? concept.schema_version;
105
-
106
- if (version && String(version) !== '3') {
100
+ if (concept.schemaVersion && String(concept.schemaVersion) !== '3') {
107
101
  this.addIssue(result,
108
102
  `${path}schema_version`,
109
- `schema_version is '${version}', expected '3'`);
103
+ `schema_version is '${concept.schemaVersion}', expected '3'`);
110
104
  }
111
105
  }
112
106
  }
@@ -115,11 +109,8 @@ export class DomainRefRule extends ValidationRule {
115
109
  constructor() { super('domain-ref', 'warning'); }
116
110
 
117
111
  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;
112
+ for (let i = 0; i < concept.domains.length; i++) {
113
+ const json = concept.domains[i].toJSON();
123
114
  if (!json.concept_id && !json.urn) {
124
115
  this.addIssue(result,
125
116
  `${path}domains[${i}]`,
@@ -134,7 +125,7 @@ export class UuidFormatRule extends ValidationRule {
134
125
 
135
126
  validate(concept, path, result) {
136
127
  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;
128
+ const id = concept.id;
138
129
 
139
130
  if (id && !UUID_RE.test(String(id))) {
140
131
  if (String(id).includes('-') && String(id).length > 20) {
@@ -153,7 +144,7 @@ export class SourceUrnFormatRule extends ValidationRule {
153
144
  const URN_RE = /^urn:[a-z0-9][a-z0-9-]{0,31}:[a-z0-9()+,\-.:=@;$_!*'%/?#]+$/i;
154
145
 
155
146
  _eachLocalization(concept, (lang, lc) => {
156
- const sources = lc.sources ?? [];
147
+ const sources = lc.sources;
157
148
  for (let i = 0; i < sources.length; i++) {
158
149
  const source = lc.sources[i].origin?.ref?.source;
159
150
  if (!source || !source.startsWith('urn:')) continue;
@@ -180,23 +171,23 @@ function _findCiteMentions(concept) {
180
171
  }
181
172
  };
182
173
 
183
- for (const lang of _langs(concept)) {
184
- const lc = _loc(concept, lang);
174
+ for (const lang of concept.languages) {
175
+ const lc = concept.localization(lang);
185
176
  if (!lc) continue;
186
177
 
187
- for (let i = 0; (lc.definitions ?? [])[i]; i++) {
178
+ for (let i = 0; lc.definitions[i]; i++) {
188
179
  walkText(lc.definitions[i]?.content, `localizations.${lang}.definitions[${i}].content`);
189
180
  }
190
- for (let i = 0; (lc.notes ?? [])[i]; i++) {
181
+ for (let i = 0; lc.notes[i]; i++) {
191
182
  const content = typeof lc.notes[i] === 'object'
192
183
  ? (lc.notes[i]?.content ?? '')
193
184
  : String(lc.notes[i] ?? '');
194
185
  walkText(content, `localizations.${lang}.notes[${i}].content`);
195
186
  }
196
- for (let i = 0; (lc.examples ?? [])[i]; i++) {
187
+ for (let i = 0; lc.examples[i]; i++) {
197
188
  walkText(lc.examples[i]?.content, `localizations.${lang}.examples[${i}].content`);
198
189
  }
199
- for (let i = 0; (lc.annotations ?? [])[i]; i++) {
190
+ for (let i = 0; lc.annotations[i]; i++) {
200
191
  walkText(lc.annotations[i]?.content, `localizations.${lang}.annotations[${i}].content`);
201
192
  }
202
193
  }
@@ -212,13 +203,13 @@ function _findDuplicateSourceIds(concept) {
212
203
  seen.get(source.id).push(source);
213
204
  };
214
205
 
215
- for (const source of (concept.sources ?? [])) record(source);
216
- for (const lang of _langs(concept)) {
217
- const lc = _loc(concept, lang);
206
+ for (const source of concept.sources) record(source);
207
+ for (const lang of concept.languages) {
208
+ const lc = concept.localization(lang);
218
209
  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);
210
+ for (const source of (lc.sources)) record(source);
211
+ for (const designation of lc.terms) {
212
+ for (const source of designation.sources) record(source);
222
213
  }
223
214
  }
224
215
 
@@ -231,17 +222,17 @@ function _findDuplicateSourceIds(concept) {
231
222
 
232
223
  function _collectSourceIds(concept) {
233
224
  const ids = new Set();
234
- for (const source of (concept.sources ?? [])) {
225
+ for (const source of concept.sources) {
235
226
  if (source?.id != null) ids.add(source.id);
236
227
  }
237
- for (const lang of _langs(concept)) {
238
- const lc = _loc(concept, lang);
228
+ for (const lang of concept.languages) {
229
+ const lc = concept.localization(lang);
239
230
  if (!lc) continue;
240
- for (const source of (lc.sources ?? [])) {
231
+ for (const source of (lc.sources)) {
241
232
  if (source?.id != null) ids.add(source.id);
242
233
  }
243
- for (const designation of (lc.terms ?? [])) {
244
- for (const source of (designation.sources ?? [])) {
234
+ for (const designation of lc.terms) {
235
+ for (const source of designation.sources) {
245
236
  if (source?.id != null) ids.add(source.id);
246
237
  }
247
238
  }
@@ -278,3 +269,148 @@ export class CiteRefIntegrityRule extends ValidationRule {
278
269
  }
279
270
  }
280
271
  }
272
+
273
+
274
+ // ── NonVerbalRefIntegrityRule ────────────────────────────────────────
275
+ // Uses parseMention for classification (no regex duplication).
276
+
277
+
278
+ const NVR_ARRAYS = Object.freeze([
279
+ { name: 'figures', entityType: 'figure' },
280
+ { name: 'tables', entityType: 'table' },
281
+ { name: 'formulas', entityType: 'formula' },
282
+ ]);
283
+
284
+ function _findNvrMentions(concept) {
285
+ const mentions = [];
286
+ const walkText = (text, source) => {
287
+ if (typeof text !== 'string' || text.length === 0) return;
288
+ const re = /\{\{([^{}]*?)\}\}/g;
289
+ let m;
290
+ while ((m = re.exec(text)) !== null) {
291
+ const parsed = parseMention(m[1]);
292
+ if (parsed.kind === 'fig-ref' ||
293
+ parsed.kind === 'table-ref' ||
294
+ parsed.kind === 'formula-ref') {
295
+ mentions.push({ key: parsed.key, source });
296
+ }
297
+ }
298
+ };
299
+
300
+ for (const lang of concept.languages) {
301
+ const lc = concept.localization(lang);
302
+ if (!lc) continue;
303
+ for (let i = 0; i < lc.definitions.length; i++) {
304
+ walkText(lc.definitions[i]?.content,
305
+ `localizations.${lang}.definitions[${i}].content`);
306
+ }
307
+ for (let i = 0; i < lc.notes.length; i++) {
308
+ walkText(lc.notes[i]?.content,
309
+ `localizations.${lang}.notes[${i}].content`);
310
+ }
311
+ for (let i = 0; i < lc.examples.length; i++) {
312
+ walkText(lc.examples[i]?.content,
313
+ `localizations.${lang}.examples[${i}].content`);
314
+ }
315
+ for (let i = 0; i < lc.annotations.length; i++) {
316
+ walkText(lc.annotations[i]?.content,
317
+ `localizations.${lang}.annotations[${i}].content`);
318
+ }
319
+ }
320
+ return mentions;
321
+ }
322
+
323
+ export class NonVerbalRefIntegrityRule extends ValidationRule {
324
+ constructor() {
325
+ super('nvr-integrity', 'warning');
326
+ }
327
+
328
+ validate(concept, path, result) {
329
+ for (const { name } of NVR_ARRAYS) {
330
+ const counts = new Map();
331
+ for (const ref of concept[name]) {
332
+ if (ref.entityId == null) continue;
333
+ counts.set(ref.entityId, (counts.get(ref.entityId) ?? 0) + 1);
334
+ }
335
+ for (const [id, count] of counts) {
336
+ if (count > 1) {
337
+ this.addIssue(result, `${path}${name}`,
338
+ `duplicate ${name} reference id "${id}" appears ${count} times`);
339
+ }
340
+ }
341
+ }
342
+
343
+ const mentions = _findNvrMentions(concept);
344
+ if (mentions.length === 0) return;
345
+
346
+ const knownIds = new Set();
347
+ for (const { name } of NVR_ARRAYS) {
348
+ for (const ref of concept[name]) {
349
+ if (ref.entityId != null) knownIds.add(ref.entityId);
350
+ }
351
+ }
352
+
353
+ for (const { key, source } of mentions) {
354
+ if (!knownIds.has(key)) {
355
+ this.addIssue(result, source,
356
+ `inline NVR mention "${key}" does not resolve to any figures/tables/formulas entry`);
357
+ }
358
+ }
359
+ }
360
+ }
361
+
362
+ // ── OrphanedImagesRule ───────────────────────────────────────────────
363
+ // Collection-scope rule: needs AssetIndex + all concepts. Called
364
+ // directly by GcrValidator (not in concept validator chain).
365
+
366
+ export class OrphanedImagesRule {
367
+ constructor() {
368
+ this.name = 'orphaned-images';
369
+ this.severity = 'warning';
370
+ }
371
+
372
+ check(context) {
373
+ const { assetIndex, concepts, resolver } = context;
374
+ if (!assetIndex || assetIndex.size === 0) return [];
375
+
376
+ const referenced = new Set();
377
+
378
+ for (const concept of concepts) {
379
+ if (resolver) {
380
+ for (const ref of resolver.extractReferences(concept)) {
381
+ if (ref.target && ref.target.includes('images/')) {
382
+ referenced.add(ref.target.replace(/^\//, ''));
383
+ }
384
+ }
385
+ }
386
+
387
+ for (const lang of concept.languages) {
388
+ const lc = concept.localization(lang);
389
+ if (!lc) continue;
390
+
391
+ for (const nvr of lc.nonVerbalRep) {
392
+ for (const img of nvr.images) {
393
+ if (img.src) referenced.add(img.src.replace(/^\//, ''));
394
+ }
395
+ }
396
+ for (const term of lc.terms) {
397
+ if (term instanceof GraphicalSymbol && term.image) {
398
+ referenced.add(term.image.replace(/^\//, ''));
399
+ }
400
+ }
401
+ }
402
+ }
403
+
404
+ const issues = [];
405
+ for (const imgPath of assetIndex.paths) {
406
+ if (!referenced.has(imgPath)) {
407
+ issues.push({
408
+ path: imgPath,
409
+ severity: 'warning',
410
+ message: `orphaned image: ${imgPath} (not referenced by any concept)`,
411
+ });
412
+ }
413
+ }
414
+ return issues;
415
+ }
416
+ }