glossarist 0.3.4 → 0.3.6
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 +1 -1
- package/src/concept-collection.js +25 -25
- package/src/gcr-reader.js +32 -2
- package/src/index.d.ts +10 -0
- package/src/index.js +2 -0
- package/src/models/concept-source.js +2 -0
- package/src/models/concept.js +44 -0
- package/src/reference-mention.js +82 -0
- package/src/reference-resolver.js +290 -26
- package/src/render-classification.js +106 -0
- package/src/validators/concept-validator.js +51 -44
- package/src/validators/gcr-validator.js +6 -6
- package/src/validators/index.js +4 -1
- package/src/validators/register-validator.js +7 -11
- package/src/validators/relationship-type-rule.js +15 -16
- package/src/validators/v3-rules.js +171 -107
- package/src/validators/validation-error.js +4 -0
- package/src/validators/validation-result.js +29 -11
- package/src/validators/validation-rule.js +7 -5
|
@@ -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 {
|
|
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(
|
|
15
|
-
|
|
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
|
-
|
|
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(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
70
|
-
const
|
|
73
|
+
const result = new ValidationResult();
|
|
74
|
+
const hasModelApi = typeof concept.localization === 'function';
|
|
71
75
|
|
|
72
|
-
if (!
|
|
73
|
-
|
|
76
|
+
if (!concept.id) {
|
|
77
|
+
result.addError('id', 'Concept must have an id');
|
|
74
78
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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(
|
|
92
|
+
for (const [lang, lc] of Object.entries(concept.localizations ?? {})) {
|
|
80
93
|
if (!lc.terms || lc.terms.length === 0) {
|
|
81
|
-
|
|
82
|
-
`
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
61
|
+
_validateDirectoryAsset(pkg, dirPath, result) {
|
|
62
62
|
let hasFiles = false;
|
|
63
63
|
let hasEntries = false;
|
|
64
|
-
pkg.
|
|
65
|
-
if (
|
|
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
|
}
|
package/src/validators/index.js
CHANGED
|
@@ -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 {
|
|
1
|
+
import { ValidationResult } from './validation-result.js';
|
|
2
2
|
|
|
3
3
|
export class RegisterValidator {
|
|
4
4
|
validate(register) {
|
|
5
|
-
const
|
|
5
|
+
const result = new ValidationResult();
|
|
6
6
|
if (!register || typeof register !== 'object') {
|
|
7
|
-
|
|
8
|
-
return
|
|
7
|
+
result.addError('', 'Register must be a non-null object');
|
|
8
|
+
return result;
|
|
9
9
|
}
|
|
10
10
|
if (!register.schema_version) {
|
|
11
|
-
|
|
11
|
+
result.addWarning('schema_version', 'Register must have a schema_version');
|
|
12
12
|
}
|
|
13
13
|
if (!register.shortname) {
|
|
14
|
-
|
|
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(
|
|
12
|
-
const
|
|
13
|
-
this._checkRelated(
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
const langs = concept.languages ?? [];
|
|
16
|
+
for (const lang of langs) {
|
|
17
|
+
const lc = concept.localization?.(lang);
|
|
18
|
+
if (!lc) continue;
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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,
|
|
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
|
-
|
|
36
|
-
`Unknown relationship type '${type}'`)
|
|
34
|
+
this.addIssue(result, `${basePath}[${i}].type`,
|
|
35
|
+
`Unknown relationship type '${type}'`);
|
|
37
36
|
}
|
|
38
37
|
}
|
|
39
38
|
}
|