glossarist 0.2.2 → 0.3.1

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,44 @@
1
+ import { GlossaristModel } from './base.js';
2
+
3
+ export const ORDERING_METHODS = Object.freeze([
4
+ 'systematic',
5
+ 'mixed',
6
+ 'alphabetical',
7
+ ]);
8
+
9
+ export class Section extends GlossaristModel {
10
+ constructor(data = {}) {
11
+ super();
12
+ this.id = data.id ?? null;
13
+ this.names = data.names ?? {};
14
+ this.ordering = data.ordering ?? null;
15
+ this.children = (data.children ?? []).map(c =>
16
+ c instanceof Section ? c : new Section(c)
17
+ );
18
+ }
19
+
20
+ name(lang) {
21
+ return this.names[lang] ?? this.names.eng ?? null;
22
+ }
23
+
24
+ descendantById(id) {
25
+ for (const child of this.children) {
26
+ if (child.id === id) return child;
27
+ const found = child.descendantById(id);
28
+ if (found) return found;
29
+ }
30
+ return null;
31
+ }
32
+
33
+ toJSON() {
34
+ const obj = { id: this.id };
35
+ if (Object.keys(this.names).length > 0) obj.names = { ...this.names };
36
+ if (this.ordering != null) obj.ordering = this.ordering;
37
+ if (this.children.length > 0) obj.children = this.children.map(c => c.toJSON());
38
+ return obj;
39
+ }
40
+
41
+ static fromJSON(data) {
42
+ return new Section(data);
43
+ }
44
+ }
@@ -1,3 +1,5 @@
1
+ import { ConceptRef } from './models/concept-ref.js';
2
+
1
3
  export class Reference {
2
4
  constructor(type, target, relationship, source) {
3
5
  this.type = type;
@@ -7,12 +9,20 @@ export class Reference {
7
9
  }
8
10
  }
9
11
 
12
+ function refTarget(rc) {
13
+ if (rc.content) return rc.content;
14
+ if (rc.ref instanceof ConceptRef) {
15
+ return rc.ref.id ?? rc.ref.source ?? '';
16
+ }
17
+ return '';
18
+ }
19
+
10
20
  export class ReferenceResolver {
11
21
  extractReferences(concept) {
12
22
  const refs = [];
13
23
 
14
24
  for (const rc of concept.relatedConcepts) {
15
- const target = rc.content ?? rc.ref?.toString() ?? '';
25
+ const target = refTarget(rc);
16
26
  if (target) {
17
27
  refs.push(new Reference('concept', target, rc.type, 'relatedConcepts'));
18
28
  }
@@ -4,15 +4,40 @@ export { ValidationResult } from './validation-result.js';
4
4
  export { ConceptValidator, LanguageCodeRule, DesignationTypeRule, EntryStatusRule } from './concept-validator.js';
5
5
  export { RegisterValidator } from './register-validator.js';
6
6
  export { GcrValidator } from './gcr-validator.js';
7
+ export {
8
+ RefShapeRule,
9
+ LocalityCompletenessRule,
10
+ LocalizationConsistencyRule,
11
+ SchemaVersionRule,
12
+ DomainRefRule,
13
+ UuidFormatRule,
14
+ SourceUrnFormatRule,
15
+ } from './v3-rules.js';
7
16
 
8
17
  import { ConceptValidator, LanguageCodeRule, DesignationTypeRule, EntryStatusRule } from './concept-validator.js';
9
18
  import { RegisterValidator } from './register-validator.js';
10
19
  import { GcrValidator } from './gcr-validator.js';
20
+ import {
21
+ RefShapeRule,
22
+ LocalityCompletenessRule,
23
+ LocalizationConsistencyRule,
24
+ SchemaVersionRule,
25
+ DomainRefRule,
26
+ UuidFormatRule,
27
+ SourceUrnFormatRule,
28
+ } from './v3-rules.js';
11
29
 
12
30
  const _default = new ConceptValidator()
13
31
  .addRule(new LanguageCodeRule())
14
32
  .addRule(new DesignationTypeRule())
15
- .addRule(new EntryStatusRule());
33
+ .addRule(new EntryStatusRule())
34
+ .addRule(new RefShapeRule())
35
+ .addRule(new LocalityCompletenessRule())
36
+ .addRule(new LocalizationConsistencyRule())
37
+ .addRule(new SchemaVersionRule())
38
+ .addRule(new DomainRefRule())
39
+ .addRule(new UuidFormatRule())
40
+ .addRule(new SourceUrnFormatRule());
16
41
 
17
42
  export function validateConcept(concept) {
18
43
  return _default.validate(concept);
@@ -0,0 +1,216 @@
1
+ import { ValidationRule } from './validation-rule.js';
2
+
3
+ /**
4
+ * GLS-305: Enforces Citation#ref is a proper Citation.Ref object
5
+ * and RelatedConcept#ref has at least source or id.
6
+ */
7
+ export class RefShapeRule extends ValidationRule {
8
+ constructor() { super('ref-shape'); }
9
+
10
+ validate(value, path) {
11
+ const errors = [];
12
+
13
+ // Check sources in localizations
14
+ const localizations = value.localizations || {};
15
+ let sourceIdx = 0;
16
+ for (const [lang, lc] of Object.entries(localizations)) {
17
+ const sources = lc.sources || [];
18
+ for (let i = 0; i < sources.length; i++) {
19
+ sourceIdx++;
20
+ const origin = sources[i].origin;
21
+ if (!origin) continue;
22
+
23
+ const ref = origin.ref;
24
+ if (!ref) {
25
+ errors.push(...this.error(
26
+ `${path}localizations.${lang}.sources[${i}].origin.ref`,
27
+ `source ${sourceIdx} origin has nil ref (expected Citation.Ref hash)`,
28
+ ));
29
+ } else if (!ref.source && !ref.id) {
30
+ errors.push(...this.error(
31
+ `${path}localizations.${lang}.sources[${i}].origin.ref`,
32
+ `source ${sourceIdx} origin.ref has neither source nor id`,
33
+ ));
34
+ }
35
+ }
36
+ }
37
+
38
+ // Check related concepts
39
+ const related = value.related || [];
40
+ for (let i = 0; i < related.length; i++) {
41
+ const ref = related[i].ref;
42
+ if (!ref) continue;
43
+ if (!ref.source && !ref.id) {
44
+ errors.push(...this.error(
45
+ `${path}related[${i}].ref`,
46
+ `related concept ${i + 1} has empty ref (no source or id)`,
47
+ ));
48
+ }
49
+ }
50
+
51
+ return errors;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * GLS-308: Locality must have type and reference_from when present.
57
+ */
58
+ export class LocalityCompletenessRule extends ValidationRule {
59
+ constructor() { super('locality-completeness', 'warning'); }
60
+
61
+ validate(value, path) {
62
+ const errors = [];
63
+ const localizations = value.localizations || {};
64
+
65
+ for (const [lang, lc] of Object.entries(localizations)) {
66
+ const sources = lc.sources || [];
67
+ for (let i = 0; i < sources.length; i++) {
68
+ const origin = sources[i].origin;
69
+ if (!origin || !origin.locality) continue;
70
+
71
+ const loc = origin.locality;
72
+ if (!loc.type) {
73
+ errors.push(...this.error(
74
+ `${path}localizations.${lang}.sources[${i}].origin.locality.type`,
75
+ `source locality has no type`,
76
+ ));
77
+ }
78
+ if (!loc.reference_from) {
79
+ errors.push(...this.error(
80
+ `${path}localizations.${lang}.sources[${i}].origin.locality.reference_from`,
81
+ `source locality has no reference_from`,
82
+ ));
83
+ }
84
+ }
85
+ }
86
+
87
+ return errors;
88
+ }
89
+ }
90
+
91
+ /**
92
+ * GLS-017: Localization map consistency.
93
+ */
94
+ export class LocalizationConsistencyRule extends ValidationRule {
95
+ constructor() { super('localization-consistency'); }
96
+
97
+ validate(value, path) {
98
+ const errors = [];
99
+ const localizations = value.localizations || {};
100
+ const data = value.raw?.data || value;
101
+
102
+ const declaredLangs = data.localized_concepts
103
+ ? Object.keys(data.localized_concepts)
104
+ : Object.keys(localizations);
105
+
106
+ for (const lang of declaredLangs) {
107
+ if (!localizations[lang]) {
108
+ errors.push(...this.error(
109
+ `${path}localizations.${lang}`,
110
+ `localized_concepts map has '${lang}' but no localization loaded`,
111
+ ));
112
+ }
113
+ }
114
+
115
+ return errors;
116
+ }
117
+ }
118
+
119
+ /**
120
+ * GLS-010: Schema version should be 3.
121
+ */
122
+ export class SchemaVersionRule extends ValidationRule {
123
+ constructor() { super('schema-version', 'warning'); }
124
+
125
+ validate(value, path) {
126
+ const errors = [];
127
+ const version = value.schemaVersion || value.schema_version;
128
+
129
+ if (version && String(version) !== '3') {
130
+ errors.push(...this.error(
131
+ `${path}schema_version`,
132
+ `schema_version is '${version}', expected '3'`,
133
+ ));
134
+ }
135
+
136
+ return errors;
137
+ }
138
+ }
139
+
140
+ /**
141
+ * GLS-309: Domain references need concept_id or urn.
142
+ */
143
+ export class DomainRefRule extends ValidationRule {
144
+ constructor() { super('domain-ref', 'warning'); }
145
+
146
+ validate(value, path) {
147
+ const errors = [];
148
+ const domains = value.domains || [];
149
+
150
+ for (let i = 0; i < domains.length; i++) {
151
+ const domain = domains[i];
152
+ if (!domain.concept_id && !domain.urn) {
153
+ errors.push(...this.error(
154
+ `${path}domains[${i}]`,
155
+ `domain ${i + 1} has neither concept_id nor urn`,
156
+ ));
157
+ }
158
+ }
159
+
160
+ return errors;
161
+ }
162
+ }
163
+
164
+ /**
165
+ * GLS-016: UUID format validation.
166
+ */
167
+ export class UuidFormatRule extends ValidationRule {
168
+ constructor() { super('uuid-format'); }
169
+
170
+ validate(value, path) {
171
+ const errors = [];
172
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
173
+ const id = value.id || value.uuid;
174
+
175
+ if (id && !UUID_RE.test(String(id))) {
176
+ // Only flag if it looks like it's supposed to be a UUID
177
+ if (String(id).includes('-') && String(id).length > 20) {
178
+ errors.push(...this.error(
179
+ `${path}id`,
180
+ `concept ID '${id}' is not valid UUID format`,
181
+ ));
182
+ }
183
+ }
184
+
185
+ return errors;
186
+ }
187
+ }
188
+
189
+ /**
190
+ * GLS-310: URN format validation for sources.
191
+ */
192
+ export class SourceUrnFormatRule extends ValidationRule {
193
+ constructor() { super('source-urn-format', 'warning'); }
194
+
195
+ validate(value, path) {
196
+ const errors = [];
197
+ const URN_RE = /^urn:[a-z0-9][a-z0-9-]{0,31}:[a-z0-9()+,\-.:=@;$_!*'%/?#]+$/i;
198
+
199
+ const localizations = value.localizations || {};
200
+ for (const [lang, lc] of Object.entries(localizations)) {
201
+ const sources = lc.sources || [];
202
+ for (let i = 0; i < sources.length; i++) {
203
+ const source = sources[i].origin?.ref?.source;
204
+ if (!source || !source.startsWith('urn:')) continue;
205
+ if (!URN_RE.test(source)) {
206
+ errors.push(...this.error(
207
+ `${path}localizations.${lang}.sources[${i}].origin.ref.source`,
208
+ `malformed URN '${source}'`,
209
+ ));
210
+ }
211
+ }
212
+ }
213
+
214
+ return errors;
215
+ }
216
+ }