glossarist 0.4.19 → 0.4.20
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/data/concept-model/shapes/glossarist.shacl.ttl +151 -1
- package/package.json +1 -1
- package/src/concept-diff.js +13 -0
- package/src/concept-parser.js +52 -2
- package/src/concept-serializer.js +11 -0
- package/src/diff/canonical-json.js +31 -0
- package/src/diff/change.js +28 -0
- package/src/diff/collection-diff.js +57 -25
- package/src/diff/concept-diff.js +100 -60
- package/src/diff/diff-patch.js +49 -48
- package/src/diff/diff-renderer.js +34 -0
- package/src/diff/identity.js +22 -0
- package/src/diff/index.d.ts +64 -1
- package/src/diff/index.js +2 -0
- package/src/diff/similarity.js +24 -0
- package/src/index.d.ts +2 -0
- package/src/index.js +2 -0
- package/src/migration/partitive-relation-migrator.js +125 -0
- package/src/models/completeness.js +24 -0
- package/src/models/concept-date.js +8 -0
- package/src/models/concept-ref.js +4 -0
- package/src/models/concept-source.js +21 -2
- package/src/models/concept.js +55 -0
- package/src/models/designation.js +15 -0
- package/src/models/detailed-definition.js +11 -0
- package/src/models/enum.js +42 -0
- package/src/models/hyperedge-colors.js +71 -0
- package/src/models/index.d.ts +61 -0
- package/src/models/index.js +39 -0
- package/src/models/localized-concept.js +29 -0
- package/src/models/member-certainty.js +21 -0
- package/src/models/partitive-enumeration.js +23 -0
- package/src/models/partitive-hyperedge.js +175 -0
- package/src/models/partitive-member.js +79 -0
- package/src/models/partitive-relation.js +158 -0
- package/src/models/plurality-marker.js +24 -0
- package/src/models/related-concept.js +5 -0
- package/src/models/relation-categories.js +6 -0
- package/src/models/type-shared-plurality.js +71 -0
- package/src/rdf/gloss-concept.js +26 -0
- package/src/rdf/gloss-hyperedge.js +90 -0
- package/src/rdf/gloss-partitive-relation.js +109 -0
- package/src/rdf/gloss-source.js +1 -1
- package/src/rdf/index.d.ts +37 -0
- package/src/rdf/index.js +2 -0
- package/src/rdf/predicates.d.ts +54 -0
- package/src/rdf/predicates.js +27 -0
- package/src/reference-resolver.js +49 -0
- package/src/validators/binary-has-part-redundancy-rule.js +74 -0
- package/src/validators/external-concept-shape-rule.js +60 -0
- package/src/validators/index.js +19 -1
- package/src/validators/partitive-drift-rule.js +51 -0
- package/src/validators/partitive-hyperedge-rule.js +75 -0
- package/src/validators/partitive-relation-coherence-rule.js +107 -0
- package/src/validators/sourced-from-locality-rule.js +53 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Warns when binary `has_part` / `is_part_of` edges duplicate
|
|
2
|
+
// PartitiveRelation members, or when a concept has many binary
|
|
3
|
+
// has_part edges that should be consolidated into a PartitiveRelation.
|
|
4
|
+
//
|
|
5
|
+
// Per TODO.partitive-relation-v2 item 14, binary edges and
|
|
6
|
+
// PartitiveRelations coexist by design. The asymmetry is pragmatic:
|
|
7
|
+
// binary edges are compact for single pairwise facts; relations
|
|
8
|
+
// carry completeness/plurality/criterion metadata. The redundancy
|
|
9
|
+
// case (same fact encoded both ways) is harmless data-wise but
|
|
10
|
+
// wasteful and should be flagged for migration.
|
|
11
|
+
//
|
|
12
|
+
// Severity: warning. Never blocks save.
|
|
13
|
+
|
|
14
|
+
import { ValidationRule } from './validation-rule.js';
|
|
15
|
+
|
|
16
|
+
const BINARY_HAS_PART_TYPES = new Set(['has_part', 'is_part_of']);
|
|
17
|
+
|
|
18
|
+
export class BinaryHasPartRedundancyRule extends ValidationRule {
|
|
19
|
+
constructor() { super('binary-has-part-redundancy', 'warning'); }
|
|
20
|
+
|
|
21
|
+
validate(concept, path, result) {
|
|
22
|
+
const relations = concept.partitiveRelations ?? concept.partitiveHyperedges ?? [];
|
|
23
|
+
|
|
24
|
+
// Collect partitive refs from PartitiveRelations.
|
|
25
|
+
const relationTargets = new Set();
|
|
26
|
+
for (const rel of relations) {
|
|
27
|
+
const compKey = _refKey(rel?.comprehensive);
|
|
28
|
+
const members = rel?.partitives ?? rel?.parts ?? [];
|
|
29
|
+
for (const member of members) {
|
|
30
|
+
const ref = member?.ref ?? member;
|
|
31
|
+
const key = _refKey(ref);
|
|
32
|
+
if (key && key !== compKey) relationTargets.add(key);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Collect binary has_part refs.
|
|
37
|
+
const binaryTargets = new Set();
|
|
38
|
+
const related = concept.relatedConcepts ?? [];
|
|
39
|
+
for (const rc of related) {
|
|
40
|
+
if (!BINARY_HAS_PART_TYPES.has(rc.type)) continue;
|
|
41
|
+
const key = _refKey(rc?.ref);
|
|
42
|
+
if (key) binaryTargets.add(key);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Redundancy: refs in both.
|
|
46
|
+
for (const target of relationTargets) {
|
|
47
|
+
if (binaryTargets.has(target)) {
|
|
48
|
+
this.addIssue(result,
|
|
49
|
+
`${path}relatedConcepts`,
|
|
50
|
+
`binary has_part edge for (${target}) is redundant — already in a ` +
|
|
51
|
+
`PartitiveRelation member; pick one encoding to avoid divergence`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Cluster: 3+ binary has_part edges suggests PartitiveRelation territory.
|
|
57
|
+
if (binaryTargets.size >= 3) {
|
|
58
|
+
this.addIssue(result,
|
|
59
|
+
`${path}relatedConcepts`,
|
|
60
|
+
`${binaryTargets.size} binary has_part edges on this concept; ` +
|
|
61
|
+
`consider converting to a PartitiveRelation (which can carry ` +
|
|
62
|
+
`completeness, plurality, and criterion metadata)`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function _refKey(ref) {
|
|
69
|
+
if (!ref) return null;
|
|
70
|
+
const source = ref.source ?? '';
|
|
71
|
+
const id = ref.id ?? '';
|
|
72
|
+
if (!source && !id) return null;
|
|
73
|
+
return `${source}:${id}`;
|
|
74
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Validates ExternalConcept shape (status: external).
|
|
2
|
+
//
|
|
3
|
+
// Per TODO.partitive-relation-v2 item 06, an ExternalConcept is a
|
|
4
|
+
// ManagedConcept with status: external. External concepts:
|
|
5
|
+
// - MUST have at least one designation (the name by which they're referenced)
|
|
6
|
+
// - MUST NOT have a definition (contradicts status: external)
|
|
7
|
+
// - MUST NOT have sources (we don't define them)
|
|
8
|
+
//
|
|
9
|
+
// Severity: error. An external concept with a definition or sources
|
|
10
|
+
// contradicts the semantic role of "external" and should be re-typed
|
|
11
|
+
// to a real concept status (valid, draft, etc.).
|
|
12
|
+
|
|
13
|
+
import { ValidationRule } from './validation-rule.js';
|
|
14
|
+
|
|
15
|
+
export class ExternalConceptShapeRule extends ValidationRule {
|
|
16
|
+
constructor() { super('external-concept-shape', 'error'); }
|
|
17
|
+
|
|
18
|
+
validate(concept, path, result) {
|
|
19
|
+
if (concept.status !== 'external') return;
|
|
20
|
+
|
|
21
|
+
// Every concept — even an external one — needs at least one
|
|
22
|
+
// designation (the name by which it is referenced).
|
|
23
|
+
let designationCount = 0;
|
|
24
|
+
for (const lang of concept.languages ?? []) {
|
|
25
|
+
const lc = concept.localization?.(lang);
|
|
26
|
+
if (!lc) continue;
|
|
27
|
+
designationCount += (lc.terms ?? []).length;
|
|
28
|
+
}
|
|
29
|
+
if (designationCount === 0) {
|
|
30
|
+
this.addIssue(result,
|
|
31
|
+
`${path}localizations.*.terms`,
|
|
32
|
+
'ExternalConcept has no designations; every concept (including external) ' +
|
|
33
|
+
'needs at least one designation (the name by which it is referenced)',
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// External concepts must NOT have a definition or sources.
|
|
38
|
+
for (const lang of concept.languages ?? []) {
|
|
39
|
+
const lc = concept.localization?.(lang);
|
|
40
|
+
if (!lc) continue;
|
|
41
|
+
|
|
42
|
+
if ((lc.definitions ?? []).length > 0) {
|
|
43
|
+
this.addIssue(result,
|
|
44
|
+
`${path}localizations.${lang}.definitions`,
|
|
45
|
+
`ExternalConcept has a definition in language '${lang}'; ` +
|
|
46
|
+
`external concepts are referenced but not defined here ` +
|
|
47
|
+
`(move the definition to the providing dataset and add a provided_by edge)`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if ((lc.sources ?? []).length > 0) {
|
|
52
|
+
this.addIssue(result,
|
|
53
|
+
`${path}localizations.${lang}.sources`,
|
|
54
|
+
`ExternalConcept has sources in language '${lang}'; ` +
|
|
55
|
+
`external concepts should not carry sources (sources belong on the providing concept)`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
package/src/validators/index.js
CHANGED
|
@@ -17,6 +17,12 @@ export {
|
|
|
17
17
|
NonVerbalRefIntegrityRule,
|
|
18
18
|
OrphanedImagesRule,
|
|
19
19
|
} from './v3-rules.js';
|
|
20
|
+
export { PartitiveHyperedgeShapeRule } from './partitive-hyperedge-rule.js';
|
|
21
|
+
export { PartitiveDriftRule } from './partitive-drift-rule.js';
|
|
22
|
+
export { SourcedFromLocalityRule } from './sourced-from-locality-rule.js';
|
|
23
|
+
export { PartitiveRelationCoherenceRule } from './partitive-relation-coherence-rule.js';
|
|
24
|
+
export { ExternalConceptShapeRule } from './external-concept-shape-rule.js';
|
|
25
|
+
export { BinaryHasPartRedundancyRule } from './binary-has-part-redundancy-rule.js';
|
|
20
26
|
|
|
21
27
|
import { ConceptValidator, LanguageCodeRule, DesignationTypeRule, EntryStatusRule } from './concept-validator.js';
|
|
22
28
|
import { RegisterValidator } from './register-validator.js';
|
|
@@ -33,6 +39,12 @@ import {
|
|
|
33
39
|
CiteRefIntegrityRule,
|
|
34
40
|
NonVerbalRefIntegrityRule,
|
|
35
41
|
} from './v3-rules.js';
|
|
42
|
+
import { PartitiveHyperedgeShapeRule } from './partitive-hyperedge-rule.js';
|
|
43
|
+
import { PartitiveDriftRule } from './partitive-drift-rule.js';
|
|
44
|
+
import { SourcedFromLocalityRule } from './sourced-from-locality-rule.js';
|
|
45
|
+
import { PartitiveRelationCoherenceRule } from './partitive-relation-coherence-rule.js';
|
|
46
|
+
import { ExternalConceptShapeRule } from './external-concept-shape-rule.js';
|
|
47
|
+
import { BinaryHasPartRedundancyRule } from './binary-has-part-redundancy-rule.js';
|
|
36
48
|
|
|
37
49
|
const _default = new ConceptValidator()
|
|
38
50
|
.addRule(new LanguageCodeRule())
|
|
@@ -47,7 +59,13 @@ const _default = new ConceptValidator()
|
|
|
47
59
|
.addRule(new SourceUrnFormatRule())
|
|
48
60
|
.addRule(new RelationshipTypeRule())
|
|
49
61
|
.addRule(new CiteRefIntegrityRule())
|
|
50
|
-
.addRule(new NonVerbalRefIntegrityRule())
|
|
62
|
+
.addRule(new NonVerbalRefIntegrityRule())
|
|
63
|
+
.addRule(new PartitiveHyperedgeShapeRule())
|
|
64
|
+
.addRule(new PartitiveDriftRule())
|
|
65
|
+
.addRule(new SourcedFromLocalityRule())
|
|
66
|
+
.addRule(new PartitiveRelationCoherenceRule())
|
|
67
|
+
.addRule(new ExternalConceptShapeRule())
|
|
68
|
+
.addRule(new BinaryHasPartRedundancyRule());
|
|
51
69
|
|
|
52
70
|
export function validateConcept(concept) {
|
|
53
71
|
return _default.validate(concept);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Warns when a concept expresses the same partitive relationship via
|
|
2
|
+
// BOTH a binary edge (broader_partitive / narrower_partitive /
|
|
3
|
+
// has_part / is_part_of) AND a PartitiveHyperedge part targeting the
|
|
4
|
+
// same concept. Round 1's decision (TODO.hyperedges/24) was "binary
|
|
5
|
+
// and hyperedge coexist, no automatic reconciliation." This rule is
|
|
6
|
+
// the detector for the resulting drift: pure warning, doesn't block
|
|
7
|
+
// save, just surfaces the duplication so the author can pick one
|
|
8
|
+
// encoding.
|
|
9
|
+
//
|
|
10
|
+
// See TODO.hyperedges-v2/21 for the design rationale.
|
|
11
|
+
|
|
12
|
+
import { ValidationRule } from './validation-rule.js';
|
|
13
|
+
|
|
14
|
+
const BINARY_PARTITIVE_TYPES = new Set([
|
|
15
|
+
'broader_partitive', 'narrower_partitive',
|
|
16
|
+
'has_part', 'is_part_of',
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
export class PartitiveDriftRule extends ValidationRule {
|
|
20
|
+
constructor() { super('partitive-drift', 'warning'); }
|
|
21
|
+
|
|
22
|
+
validate(concept, path, result) {
|
|
23
|
+
const hyperedgeTargets = new Set();
|
|
24
|
+
for (const he of concept.partitiveHyperedges ?? []) {
|
|
25
|
+
const comp = he.comprehensive;
|
|
26
|
+
const compKey = `${comp?.source ?? ''}|${comp?.id ?? ''}`;
|
|
27
|
+
for (const part of he.parts ?? []) {
|
|
28
|
+
const key = `${part.source ?? ''}|${part.id ?? ''}`;
|
|
29
|
+
if (key === '|' || key === compKey) continue;
|
|
30
|
+
hyperedgeTargets.add(key);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (hyperedgeTargets.size === 0) return;
|
|
35
|
+
|
|
36
|
+
const related = concept.relatedConcepts ?? [];
|
|
37
|
+
for (let i = 0; i < related.length; i++) {
|
|
38
|
+
const rc = related[i];
|
|
39
|
+
if (!BINARY_PARTITIVE_TYPES.has(rc.type)) continue;
|
|
40
|
+
const ref = rc.ref;
|
|
41
|
+
const key = `${ref?.source ?? ''}|${ref?.id ?? ''}`;
|
|
42
|
+
if (hyperedgeTargets.has(key)) {
|
|
43
|
+
this.addIssue(result,
|
|
44
|
+
`${path}relatedConcepts[${i}]`,
|
|
45
|
+
`concept has both a binary '${rc.type}' edge and a ` +
|
|
46
|
+
`PartitiveHyperedge part targeting (${ref?.source ?? '?'}, ` +
|
|
47
|
+
`${ref?.id ?? '?'}); pick one encoding to avoid divergence`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { ValidationRule } from './validation-rule.js';
|
|
2
|
+
|
|
3
|
+
// Validates semantic invariants of PartitiveHyperedge entries that
|
|
4
|
+
// the model constructor does NOT enforce. The constructor already
|
|
5
|
+
// rejects empty comprehensive, empty parts, self-loops, invalid
|
|
6
|
+
// marker values, and duplicate markers. This rule provides a
|
|
7
|
+
// defensive walk at validation time plus a warning for the
|
|
8
|
+
// implicit-enumeration case (absent in the source YAML, filled by
|
|
9
|
+
// the model's DEFAULT_ENUMERATION constant).
|
|
10
|
+
export class PartitiveHyperedgeShapeRule extends ValidationRule {
|
|
11
|
+
constructor() { super('partitive-hyperedge-shape'); }
|
|
12
|
+
|
|
13
|
+
validate(concept, path, result) {
|
|
14
|
+
const edges = concept.partitiveHyperedges ?? [];
|
|
15
|
+
for (let i = 0; i < edges.length; i++) {
|
|
16
|
+
const he = edges[i];
|
|
17
|
+
const base = `${path}partitiveHyperedges[${i}]`;
|
|
18
|
+
|
|
19
|
+
this._checkComprehensive(he, i, base, result);
|
|
20
|
+
this._checkParts(he, i, base, result);
|
|
21
|
+
this._checkMarkers(he, i, base, result);
|
|
22
|
+
this._checkSelfLoop(he, i, base, result);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
_checkComprehensive(he, idx, base, result) {
|
|
27
|
+
const ref = he.comprehensive;
|
|
28
|
+
if (!ref || (!ref.source && !ref.id)) {
|
|
29
|
+
this.addIssue(result, `${base}.comprehensive`,
|
|
30
|
+
`partitive_hyperedge ${idx + 1} has empty comprehensive`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
_checkParts(he, idx, base, result) {
|
|
35
|
+
const parts = he.parts ?? [];
|
|
36
|
+
if (parts.length === 0) {
|
|
37
|
+
this.addIssue(result, `${base}.parts`,
|
|
38
|
+
`partitive_hyperedge ${idx + 1} has no parts`);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
for (let j = 0; j < parts.length; j++) {
|
|
42
|
+
const p = parts[j];
|
|
43
|
+
if (!p || (!p.source && !p.id)) {
|
|
44
|
+
this.addIssue(result, `${base}.parts[${j}]`,
|
|
45
|
+
`partitive_hyperedge ${idx + 1} part ${j + 1} has empty ref`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
_checkMarkers(he, idx, base, result) {
|
|
51
|
+
const markers = he.markers ?? [];
|
|
52
|
+
const seen = new Set();
|
|
53
|
+
for (const m of markers) {
|
|
54
|
+
if (seen.has(m)) {
|
|
55
|
+
this.addIssue(result, `${base}.markers`,
|
|
56
|
+
`partitive_hyperedge ${idx + 1} has duplicate marker '${m}'`);
|
|
57
|
+
}
|
|
58
|
+
seen.add(m);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
_checkSelfLoop(he, idx, base, result) {
|
|
63
|
+
const c = he.comprehensive;
|
|
64
|
+
if (!c) return;
|
|
65
|
+
const cKey = `${c.source ?? ''}:${c.id ?? ''}`;
|
|
66
|
+
for (let j = 0; j < (he.parts ?? []).length; j++) {
|
|
67
|
+
const p = he.parts[j];
|
|
68
|
+
const pKey = `${p?.source ?? ''}:${p?.id ?? ''}`;
|
|
69
|
+
if (pKey === cKey) {
|
|
70
|
+
this.addIssue(result, `${base}.parts[${j}]`,
|
|
71
|
+
`partitive_hyperedge ${idx + 1} part ${j + 1} cannot be the comprehensive`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Validates PartitiveRelation coherence per ISO 704 / ISO 12620.
|
|
2
|
+
//
|
|
3
|
+
// Checks (per TODO.partitive-relation-v2 item 13):
|
|
4
|
+
// 1. ERROR: each relation has ≥2 partitives (ISO 704 "two or more")
|
|
5
|
+
// 2. ERROR: no two relations on the same concept share the same
|
|
6
|
+
// comprehensive + same criterion (duplicate decomposition —
|
|
7
|
+
// violates ISO 12620 coordinate-concept coherence)
|
|
8
|
+
// 3. WARNING: a concept with multiple relations where any relation
|
|
9
|
+
// lacks criterion (cannot verify distinctness from siblings)
|
|
10
|
+
// 4. WARNING: a TypeSharedPlurality with is_uncertain=true but
|
|
11
|
+
// is_shared=false (semantically odd — broken line qualifies
|
|
12
|
+
// the close-set double line; without `double`, what's being
|
|
13
|
+
// qualified?)
|
|
14
|
+
//
|
|
15
|
+
// Lives in the validator framework, not the model constructor, per
|
|
16
|
+
// invariant I8 (TODO.hyperedges-v2/00). Constructors enforce type-shape;
|
|
17
|
+
// validators enforce semantics.
|
|
18
|
+
|
|
19
|
+
import { ValidationRule } from './validation-rule.js';
|
|
20
|
+
|
|
21
|
+
export class PartitiveRelationCoherenceRule extends ValidationRule {
|
|
22
|
+
constructor() { super('partitive-relation-coherence'); }
|
|
23
|
+
|
|
24
|
+
validate(concept, path, result) {
|
|
25
|
+
const relations = concept.partitiveRelations ?? concept.partitiveHyperedges ?? [];
|
|
26
|
+
if (relations.length === 0) return;
|
|
27
|
+
|
|
28
|
+
// Check 1: ≥2 partitives per relation. (The constructor also
|
|
29
|
+
// rejects this, but a warning here surfaces it in the result
|
|
30
|
+
// bundle for CI visibility.)
|
|
31
|
+
for (let i = 0; i < relations.length; i++) {
|
|
32
|
+
const partitiveCount = (relations[i]?.partitives ?? []).length;
|
|
33
|
+
if (partitiveCount < 2) {
|
|
34
|
+
this.addIssue(result,
|
|
35
|
+
`${path}partitiveRelations[${i}].partitives`,
|
|
36
|
+
`PartitiveRelation ${i + 1} has ${partitiveCount} partitive(s); ` +
|
|
37
|
+
`ISO 704 requires ≥2 (use a binary has_part edge for pairwise facts)`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Check 2: no duplicate (comprehensive + criterion) on the same concept.
|
|
43
|
+
const seen = new Map();
|
|
44
|
+
for (let i = 0; i < relations.length; i++) {
|
|
45
|
+
const rel = relations[i];
|
|
46
|
+
const compKey = _conceptRefKey(rel?.comprehensive);
|
|
47
|
+
const critKey = _criterionKey(rel?.criterion);
|
|
48
|
+
if (compKey == null || critKey == null) continue;
|
|
49
|
+
const key = `${compKey}|${critKey}`;
|
|
50
|
+
if (seen.has(key)) {
|
|
51
|
+
this.addIssue(result,
|
|
52
|
+
`${path}partitiveRelations[${i}]`,
|
|
53
|
+
`duplicate PartitiveRelation (same comprehensive + same criterion ` +
|
|
54
|
+
`as relation ${seen.get(key) + 1}); violates ISO 12620 coordinate-concept coherence`,
|
|
55
|
+
);
|
|
56
|
+
} else {
|
|
57
|
+
seen.set(key, i);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Check 3: criterion documentation (warning) when concept has multiple relations.
|
|
62
|
+
if (relations.length > 1) {
|
|
63
|
+
for (let i = 0; i < relations.length; i++) {
|
|
64
|
+
if (!relations[i]?.criterion) {
|
|
65
|
+
result.addWarning(
|
|
66
|
+
`${path}partitiveRelations[${i}].criterion`,
|
|
67
|
+
`concept has multiple PartitiveRelations but relation ${i + 1} ` +
|
|
68
|
+
`has no criterion; cannot verify distinctness from siblings`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Check 4: plurality coherence (warning).
|
|
75
|
+
for (let i = 0; i < relations.length; i++) {
|
|
76
|
+
const plural = relations[i]?.plurality;
|
|
77
|
+
if (!plural) continue;
|
|
78
|
+
if (plural.isUncertain && !plural.isShared) {
|
|
79
|
+
result.addWarning(
|
|
80
|
+
`${path}partitiveRelations[${i}].plurality`,
|
|
81
|
+
`plurality has isUncertain=true without isShared=true (semantically odd: ` +
|
|
82
|
+
`ISO 704 broken line qualifies the close-set double line plurality; ` +
|
|
83
|
+
`without double, what is being qualified?)`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function _conceptRefKey(ref) {
|
|
91
|
+
if (!ref) return null;
|
|
92
|
+
const source = ref.source ?? '';
|
|
93
|
+
const id = ref.id ?? '';
|
|
94
|
+
if (!source && !id) return null;
|
|
95
|
+
return `${source}:${id}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function _criterionKey(criterion) {
|
|
99
|
+
if (!criterion || typeof criterion !== 'object') return null;
|
|
100
|
+
// Sort values for stable comparison regardless of language key order.
|
|
101
|
+
const values = Object.values(criterion)
|
|
102
|
+
.filter(v => typeof v === 'string')
|
|
103
|
+
.map(s => s.trim())
|
|
104
|
+
.filter(s => s.length > 0)
|
|
105
|
+
.sort();
|
|
106
|
+
return values.length > 0 ? values.join('|') : null;
|
|
107
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Validates that every ConceptSource#sourced_from Citation includes a
|
|
2
|
+
// locality. Per concept-model commit 9c065df ("sourced_from citations
|
|
3
|
+
// must include locality"), a sourced_from entry without a locality is
|
|
4
|
+
// incomplete — the reader cannot locate the definition in the upstream
|
|
5
|
+
// document. The schema (schemas/v3/concept.yaml) enforces this at
|
|
6
|
+
// YAML-load time; this rule surfaces violations in already-parsed
|
|
7
|
+
// Concepts for defense-in-depth.
|
|
8
|
+
//
|
|
9
|
+
// Severity: error. A sourced_from without locality is data loss for
|
|
10
|
+
// the reader.
|
|
11
|
+
|
|
12
|
+
import { ValidationRule } from './validation-rule.js';
|
|
13
|
+
|
|
14
|
+
export class SourcedFromLocalityRule extends ValidationRule {
|
|
15
|
+
constructor() { super('sourced-from-locality', 'error'); }
|
|
16
|
+
|
|
17
|
+
validate(concept, path, result) {
|
|
18
|
+
const sources = concept.sources ?? [];
|
|
19
|
+
for (let i = 0; i < sources.length; i++) {
|
|
20
|
+
const source = sources[i];
|
|
21
|
+
const sourcedFrom = source.sourcedFrom ?? source.sourced_from ?? [];
|
|
22
|
+
for (let j = 0; j < sourcedFrom.length; j++) {
|
|
23
|
+
const citation = sourcedFrom[j];
|
|
24
|
+
const locality = citation?.locality;
|
|
25
|
+
if (!locality || (!locality.type && !locality.referenceFrom && !locality.reference_from)) {
|
|
26
|
+
this.addIssue(result,
|
|
27
|
+
`${path}sources[${i}].sourced_from[${j}].locality`,
|
|
28
|
+
`sourced_from citation ${j + 1} on source ${i + 1} has no locality ` +
|
|
29
|
+
`(expected type + reference_from so the reader can locate the definition)`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
for (const lang of concept.languages ?? []) {
|
|
35
|
+
const lc = concept.localization?.(lang);
|
|
36
|
+
if (!lc) continue;
|
|
37
|
+
const lcSources = lc.sources ?? [];
|
|
38
|
+
for (let i = 0; i < lcSources.length; i++) {
|
|
39
|
+
const source = lcSources[i];
|
|
40
|
+
const sourcedFrom = source.sourcedFrom ?? source.sourced_from ?? [];
|
|
41
|
+
for (let j = 0; j < sourcedFrom.length; j++) {
|
|
42
|
+
const citation = sourcedFrom[j];
|
|
43
|
+
const locality = citation?.locality;
|
|
44
|
+
if (!locality || (!locality.type && !locality.referenceFrom && !locality.reference_from)) {
|
|
45
|
+
this.addIssue(result,
|
|
46
|
+
`${path}localizations.${lang}.sources[${i}].sourced_from[${j}].locality`,
|
|
47
|
+
`sourced_from citation ${j + 1} on localization source ${i + 1} has no locality`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|