glossarist 0.3.4 → 0.3.5

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.
@@ -0,0 +1,106 @@
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
+ */
11
+
12
+ 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
+ constructor(registry = {}, sourceDatasetId = null, options = {}) {
20
+ this.registry = registry;
21
+ this.sourceDatasetId = sourceDatasetId;
22
+ this.options = options;
23
+ }
24
+
25
+ /**
26
+ * @param {Reference} ref
27
+ * @returns {string} — the classification (e.g. 'same-dataset',
28
+ * 'internal-citation', 'unresolved', etc.)
29
+ */
30
+ classify(ref) {
31
+ if (ref == null) return 'unknown';
32
+
33
+ switch (ref.type) {
34
+ case 'concept': return this._classifyConcept(ref);
35
+ case 'dataset': return this._classifyDataset(ref);
36
+ case 'bibliography': return this._classifyBibliography(ref);
37
+ case 'typed-ref': return this._classifyTypedRef(ref);
38
+ case 'standard': return 'legacy-standard';
39
+ default: return 'unknown';
40
+ }
41
+ }
42
+
43
+ _classifyConcept(ref) {
44
+ // 1. URI form, resolved to a dataset.
45
+ if (ref.uri) {
46
+ const dsId = ref.resolution?.datasetId;
47
+ if (!dsId) return 'unresolved';
48
+ if (!this.registry[dsId]) return 'external-citation';
49
+ if (dsId === this.sourceDatasetId) return 'same-dataset';
50
+ return 'cross-dataset';
51
+ }
52
+ // 2. Unanchored designation.
53
+ if (ref.lookupKey?.designation) {
54
+ return 'unresolved-designation';
55
+ }
56
+ // 3. Id-style (id-match, short-id, numeric).
57
+ if (ref.lookupKey?.id) {
58
+ const dsId = ref.lookupKey.dataset;
59
+ if (!this.registry[dsId]) return 'unresolved';
60
+ if (dsId === this.sourceDatasetId) return 'same-dataset';
61
+ return 'cross-dataset';
62
+ }
63
+ // 4. Concept ref with target (legacy).
64
+ if (ref.target) {
65
+ return 'unresolved';
66
+ }
67
+ return 'unresolved';
68
+ }
69
+
70
+ _classifyDataset(ref) {
71
+ if (ref.resolution?.kind === 'dataset-self') return 'dataset-self';
72
+ if (ref.resolution?.kind === 'dataset-namespace') return 'dataset-self';
73
+ return 'unknown';
74
+ }
75
+
76
+ _classifyBibliography(ref) {
77
+ // 1. cite:key form: try the bibliography registry.
78
+ if (ref.citation) {
79
+ const bioRecord = this._tryBibliography(ref.citation.ref);
80
+ if (bioRecord) return 'internal-citation';
81
+ return 'self-contained-citation';
82
+ }
83
+ // 2. URI form: try the bibliography registry, then the
84
+ // resolution's datasetId (if it's a concept URI), else null.
85
+ if (ref.uri) {
86
+ const bioRecord = this._tryBibliography(ref.resolution);
87
+ if (bioRecord) return 'internal-citation';
88
+ return 'external-citation';
89
+ }
90
+ return 'unresolved-citation';
91
+ }
92
+
93
+ _classifyTypedRef(_ref) {
94
+ return 'typed-ref';
95
+ }
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
+ }
@@ -1,5 +1,5 @@
1
1
  import { ValidationRule } from './validation-rule.js';
2
- import { ValidationError } from './validation-error.js';
2
+ import { ValidationResult } from './validation-result.js';
3
3
 
4
4
  const VALID_DESIGNATION_TYPES = new Set([
5
5
  'expression', 'abbreviation', 'symbol', 'graphical symbol', 'graphical_symbol',
@@ -9,51 +9,55 @@ 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
+
12
18
  export class LanguageCodeRule extends ValidationRule {
13
19
  constructor() { super('language-code'); }
14
- validate(value, path) {
15
- if (!value.localizations) return [];
16
- const errors = [];
17
- for (const lang of Object.keys(value.localizations)) {
20
+ validate(concept, path, result) {
21
+ for (const lang of _langs(concept)) {
18
22
  if (!/^[a-z]{3}$/.test(lang)) {
19
- errors.push(...this.error(`${path}localizations.${lang}`,
20
- `Invalid language code '${lang}': expected ISO 639-3 (3 lowercase letters)`));
23
+ result.addError(`${path}localizations.${lang}`,
24
+ `Invalid language code '${lang}': expected ISO 639-3 (3 lowercase letters)`);
21
25
  }
22
26
  }
23
- return errors;
24
27
  }
25
28
  }
26
29
 
27
30
  export class DesignationTypeRule extends ValidationRule {
28
31
  constructor() { super('designation-type'); }
29
- validate(value, path) {
30
- if (!value.localizations) return [];
31
- const errors = [];
32
- for (const [lang, lc] of Object.entries(value.localizations)) {
33
- for (let i = 0; i < (lc.terms?.length ?? 0); i++) {
34
- const t = lc.terms[i];
35
- if (t.type && !VALID_DESIGNATION_TYPES.has(t.type)) {
36
- errors.push(...this.error(`${path}localizations.${lang}.terms[${i}].type`,
37
- `Unknown designation type '${t.type}'`));
32
+ validate(concept, path, result) {
33
+ for (const lang of _langs(concept)) {
34
+ const lc = _loc(concept, lang);
35
+ 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);
40
+ if (type && !VALID_DESIGNATION_TYPES.has(type)) {
41
+ result.addError(`${path}localizations.${lang}.terms[${i}].type`,
42
+ `Unknown designation type '${type}'`);
38
43
  }
39
44
  }
40
45
  }
41
- return errors;
42
46
  }
43
47
  }
44
48
 
45
49
  export class EntryStatusRule extends ValidationRule {
46
50
  constructor() { super('entry-status'); }
47
- validate(value, path) {
48
- if (!value.localizations) return [];
49
- const errors = [];
50
- for (const [lang, lc] of Object.entries(value.localizations)) {
51
- if (lc.entry_status && !VALID_ENTRY_STATUSES.has(lc.entry_status)) {
52
- errors.push(...this.error(`${path}localizations.${lang}.entry_status`,
53
- `Unknown entry status '${lc.entry_status}'`));
51
+ validate(concept, path, result) {
52
+ for (const lang of _langs(concept)) {
53
+ const lc = _loc(concept, lang);
54
+ 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}'`);
54
59
  }
55
60
  }
56
- return errors;
57
61
  }
58
62
  }
59
63
 
@@ -66,34 +70,37 @@ export class ConceptValidator {
66
70
  }
67
71
 
68
72
  validate(concept) {
69
- const errors = [];
70
- const json = typeof concept.toJSON === 'function' ? concept.toJSON() : concept;
73
+ const result = new ValidationResult();
74
+ const hasModelApi = typeof concept.localization === 'function';
71
75
 
72
- if (!json.id) {
73
- errors.push(new ValidationError('id', 'Concept must have an id'));
76
+ if (!concept.id) {
77
+ result.addError('id', 'Concept must have an id');
74
78
  }
75
- if (!json.localizations || Object.keys(json.localizations).length === 0) {
76
- errors.push(new ValidationError('localizations',
77
- 'Concept must have at least one localization', 'warning'));
79
+
80
+ const langs = hasModelApi ? concept.languages : Object.keys(concept.localizations ?? {});
81
+ if (langs.length === 0) {
82
+ result.addWarning('localizations', 'Concept must have at least one localization');
83
+ } else if (hasModelApi) {
84
+ for (const lang of langs) {
85
+ const lc = concept.localization(lang);
86
+ if (!lc || lc.terms.length === 0) {
87
+ result.addWarning(`localizations.${lang}.terms`,
88
+ `Localization '${lang}' must have at least one term`);
89
+ }
90
+ }
78
91
  } else {
79
- for (const [lang, lc] of Object.entries(json.localizations)) {
92
+ for (const [lang, lc] of Object.entries(concept.localizations ?? {})) {
80
93
  if (!lc.terms || lc.terms.length === 0) {
81
- errors.push(new ValidationError(
82
- `localizations.${lang}.terms`,
83
- `Localization '${lang}' must have at least one term`, 'warning'));
94
+ result.addWarning(`localizations.${lang}.terms`,
95
+ `Localization '${lang}' must have at least one term`);
84
96
  }
85
97
  }
86
98
  }
87
99
 
88
100
  for (const rule of this._rules) {
89
- errors.push(...rule.validate(json, ''));
101
+ rule.validate(concept, '', result);
90
102
  }
91
103
 
92
- return {
93
- valid: errors.filter(e => e.severity === 'error').length === 0,
94
- errors: errors.filter(e => e.severity === 'error'),
95
- warnings: errors.filter(e => e.severity === 'warning'),
96
- };
104
+ return result;
97
105
  }
98
106
  }
99
-
@@ -12,7 +12,7 @@ export class GcrValidator {
12
12
  }
13
13
 
14
14
  async _validateMetadata(pkg, result) {
15
- const raw = await pkg._readText('metadata.yaml');
15
+ const raw = await pkg.readText('metadata.yaml');
16
16
  if (!raw) {
17
17
  result.addError('metadata.yaml is missing');
18
18
  return;
@@ -49,7 +49,7 @@ export class GcrValidator {
49
49
  }
50
50
 
51
51
  async _validateFileAsset(pkg, path, result) {
52
- const raw = await pkg._readText(path);
52
+ const raw = await pkg.readText(path);
53
53
  if (!raw) return;
54
54
  try {
55
55
  yaml.load(raw);
@@ -58,15 +58,15 @@ export class GcrValidator {
58
58
  }
59
59
  }
60
60
 
61
- async _validateDirectoryAsset(pkg, dirPath, result) {
61
+ _validateDirectoryAsset(pkg, dirPath, result) {
62
62
  let hasFiles = false;
63
63
  let hasEntries = false;
64
- pkg._zip.forEach((relativePath, entry) => {
65
- if (relativePath.startsWith(`${dirPath}/`)) {
64
+ for (const entry of pkg.entryPaths()) {
65
+ if (entry.path.startsWith(`${dirPath}/`)) {
66
66
  hasEntries = true;
67
67
  if (!entry.dir) hasFiles = true;
68
68
  }
69
- });
69
+ }
70
70
  if (hasEntries && !hasFiles) {
71
71
  result.addWarning(`${dirPath}/ directory exists but is empty`);
72
72
  }
@@ -13,6 +13,7 @@ export {
13
13
  DomainRefRule,
14
14
  UuidFormatRule,
15
15
  SourceUrnFormatRule,
16
+ CiteRefIntegrityRule,
16
17
  } from './v3-rules.js';
17
18
 
18
19
  import { ConceptValidator, LanguageCodeRule, DesignationTypeRule, EntryStatusRule } from './concept-validator.js';
@@ -27,6 +28,7 @@ import {
27
28
  DomainRefRule,
28
29
  UuidFormatRule,
29
30
  SourceUrnFormatRule,
31
+ CiteRefIntegrityRule,
30
32
  } from './v3-rules.js';
31
33
 
32
34
  const _default = new ConceptValidator()
@@ -40,7 +42,8 @@ const _default = new ConceptValidator()
40
42
  .addRule(new DomainRefRule())
41
43
  .addRule(new UuidFormatRule())
42
44
  .addRule(new SourceUrnFormatRule())
43
- .addRule(new RelationshipTypeRule());
45
+ .addRule(new RelationshipTypeRule())
46
+ .addRule(new CiteRefIntegrityRule());
44
47
 
45
48
  export function validateConcept(concept) {
46
49
  return _default.validate(concept);
@@ -1,22 +1,18 @@
1
- import { ValidationError } from './validation-error.js';
1
+ import { ValidationResult } from './validation-result.js';
2
2
 
3
3
  export class RegisterValidator {
4
4
  validate(register) {
5
- const errors = [];
5
+ const result = new ValidationResult();
6
6
  if (!register || typeof register !== 'object') {
7
- errors.push(new ValidationError('', 'Register must be a non-null object'));
8
- return { valid: false, errors, warnings: [] };
7
+ result.addError('', 'Register must be a non-null object');
8
+ return result;
9
9
  }
10
10
  if (!register.schema_version) {
11
- errors.push(new ValidationError('schema_version', 'Register must have a schema_version', 'warning'));
11
+ result.addWarning('schema_version', 'Register must have a schema_version');
12
12
  }
13
13
  if (!register.shortname) {
14
- errors.push(new ValidationError('shortname', 'Register should have a shortname', 'warning'));
14
+ result.addWarning('shortname', 'Register should have a shortname');
15
15
  }
16
- return {
17
- valid: errors.filter(e => e.severity === 'error').length === 0,
18
- errors: errors.filter(e => e.severity === 'error'),
19
- warnings: errors.filter(e => e.severity === 'warning'),
20
- };
16
+ return result;
21
17
  }
22
18
  }
@@ -8,32 +8,31 @@ const KNOWN_DESIGNATION_TYPES = new Set(DESIGNATION_RELATIONSHIP_TYPES);
8
8
  export class RelationshipTypeRule extends ValidationRule {
9
9
  constructor() { super('relationship-type', 'warning'); }
10
10
 
11
- validate(value, path) {
12
- const errors = [];
13
- this._checkRelated(value.related, `${path}related`, KNOWN_CONCEPT_TYPES, errors);
11
+ validate(concept, path, result) {
12
+ const related = concept.relatedConcepts ?? concept.related ?? [];
13
+ this._checkRelated(related, `${path}related`, KNOWN_CONCEPT_TYPES, result);
14
14
 
15
- if (value.localizations) {
16
- for (const [lang, lc] of Object.entries(value.localizations)) {
17
- this._checkRelated(lc.related, `${path}localizations.${lang}.related`, KNOWN_CONCEPT_TYPES, errors);
15
+ const langs = concept.languages ?? [];
16
+ for (const lang of langs) {
17
+ const lc = concept.localization?.(lang);
18
+ if (!lc) continue;
18
19
 
19
- if (lc.terms) {
20
- for (let ti = 0; ti < lc.terms.length; ti++) {
21
- this._checkRelated(lc.terms[ti]?.related,
22
- `${path}localizations.${lang}.terms[${ti}].related`, KNOWN_DESIGNATION_TYPES, errors);
23
- }
24
- }
20
+ this._checkRelated(lc.related, `${path}localizations.${lang}.related`, KNOWN_CONCEPT_TYPES, result);
21
+
22
+ for (let ti = 0; ti < lc.terms.length; ti++) {
23
+ this._checkRelated(lc.terms[ti]?.related,
24
+ `${path}localizations.${lang}.terms[${ti}].related`, KNOWN_DESIGNATION_TYPES, result);
25
25
  }
26
26
  }
27
- return errors;
28
27
  }
29
28
 
30
- _checkRelated(arr, basePath, knownTypes, errors) {
29
+ _checkRelated(arr, basePath, knownTypes, result) {
31
30
  if (!arr) return;
32
31
  for (let i = 0; i < arr.length; i++) {
33
32
  const type = arr[i]?.type;
34
33
  if (type && !knownTypes.has(type)) {
35
- errors.push(...this.error(`${basePath}[${i}].type`,
36
- `Unknown relationship type '${type}'`));
34
+ this.addIssue(result, `${basePath}[${i}].type`,
35
+ `Unknown relationship type '${type}'`);
37
36
  }
38
37
  }
39
38
  }