glossarist 0.4.19 → 0.4.22

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.
Files changed (54) hide show
  1. package/data/concept-model/shapes/glossarist.shacl.ttl +151 -1
  2. package/package.json +1 -1
  3. package/src/concept-diff.js +13 -0
  4. package/src/concept-parser.js +52 -2
  5. package/src/concept-serializer.js +11 -0
  6. package/src/diff/canonical-json.js +31 -0
  7. package/src/diff/change.js +28 -0
  8. package/src/diff/collection-diff.js +57 -25
  9. package/src/diff/concept-diff.js +100 -60
  10. package/src/diff/diff-patch.js +49 -48
  11. package/src/diff/diff-renderer.js +34 -0
  12. package/src/diff/identity.js +22 -0
  13. package/src/diff/index.d.ts +64 -1
  14. package/src/diff/index.js +2 -0
  15. package/src/diff/similarity.js +24 -0
  16. package/src/index.d.ts +2 -0
  17. package/src/index.js +2 -0
  18. package/src/migration/partitive-relation-migrator.js +99 -0
  19. package/src/models/completeness.js +24 -0
  20. package/src/models/concept-date.js +8 -0
  21. package/src/models/concept-ref.js +4 -0
  22. package/src/models/concept-source.js +21 -2
  23. package/src/models/concept.js +55 -0
  24. package/src/models/designation.js +15 -0
  25. package/src/models/detailed-definition.js +11 -0
  26. package/src/models/enum.js +42 -0
  27. package/src/models/hyperedge-colors.js +71 -0
  28. package/src/models/index.d.ts +61 -0
  29. package/src/models/index.js +38 -0
  30. package/src/models/localized-concept.js +29 -0
  31. package/src/models/multiplicity.js +24 -0
  32. package/src/models/partitive-enumeration.js +23 -0
  33. package/src/models/partitive-hyperedge.js +175 -0
  34. package/src/models/partitive-member.js +96 -0
  35. package/src/models/partitive-relation.js +149 -0
  36. package/src/models/plurality-marker.js +24 -0
  37. package/src/models/related-concept.js +5 -0
  38. package/src/models/relation-categories.js +6 -0
  39. package/src/rdf/gloss-concept.js +26 -0
  40. package/src/rdf/gloss-hyperedge.js +90 -0
  41. package/src/rdf/gloss-partitive-relation.js +88 -0
  42. package/src/rdf/gloss-source.js +1 -1
  43. package/src/rdf/index.d.ts +37 -0
  44. package/src/rdf/index.js +2 -0
  45. package/src/rdf/predicates.d.ts +46 -0
  46. package/src/rdf/predicates.js +23 -0
  47. package/src/reference-resolver.js +49 -0
  48. package/src/validators/binary-has-part-redundancy-rule.js +74 -0
  49. package/src/validators/external-concept-shape-rule.js +60 -0
  50. package/src/validators/index.js +19 -1
  51. package/src/validators/partitive-drift-rule.js +51 -0
  52. package/src/validators/partitive-hyperedge-rule.js +75 -0
  53. package/src/validators/partitive-relation-coherence-rule.js +98 -0
  54. package/src/validators/sourced-from-locality-rule.js +53 -0
@@ -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,98 @@
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 — removed. The prior plurality
75
+ // block was based on a misreading of ISO 704; the corrected model
76
+ // uses per-member multiplicity + is_delimiting, which are validated
77
+ // at construction by PartitiveMember itself.)
78
+ }
79
+ }
80
+
81
+ function _conceptRefKey(ref) {
82
+ if (!ref) return null;
83
+ const source = ref.source ?? '';
84
+ const id = ref.id ?? '';
85
+ if (!source && !id) return null;
86
+ return `${source}:${id}`;
87
+ }
88
+
89
+ function _criterionKey(criterion) {
90
+ if (!criterion || typeof criterion !== 'object') return null;
91
+ // Sort values for stable comparison regardless of language key order.
92
+ const values = Object.values(criterion)
93
+ .filter(v => typeof v === 'string')
94
+ .map(s => s.trim())
95
+ .filter(s => s.length > 0)
96
+ .sort();
97
+ return values.length > 0 ? values.join('|') : null;
98
+ }
@@ -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
+ }