glossarist 2.12.1 → 2.13.0
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.
- checksums.yaml +4 -4
- data/CLAUDE.md +22 -10
- data/config.yml +27 -11
- data/lib/glossarist/glossary_definition.rb +4 -4
- data/lib/glossarist/managed_concept.rb +1 -1
- data/lib/glossarist/rdf/gloss_concept.rb +3 -3
- data/lib/glossarist/rdf/gloss_partitive_member.rb +33 -0
- data/lib/glossarist/rdf/gloss_partitive_relation.rb +38 -0
- data/lib/glossarist/rdf/gloss_type_shared_plurality.rb +31 -0
- data/lib/glossarist/rdf/v3.rb +3 -1
- data/lib/glossarist/rdf.rb +4 -1
- data/lib/glossarist/transforms/concept_to_gloss_transform.rb +41 -19
- data/lib/glossarist/v3/managed_concept.rb +2 -2
- data/lib/glossarist/v3/partitive_member.rb +61 -0
- data/lib/glossarist/v3/partitive_relation.rb +119 -0
- data/lib/glossarist/v3/type_shared_plurality.rb +42 -0
- data/lib/glossarist/v3.rb +6 -2
- data/lib/glossarist/validation/rules/partitive_relation_rule.rb +150 -0
- data/lib/glossarist/version.rb +1 -1
- metadata +8 -4
- data/lib/glossarist/rdf/gloss_hyperedge.rb +0 -53
- data/lib/glossarist/v3/partitive_hyperedge.rb +0 -132
- data/lib/glossarist/validation/rules/partitive_hyperedge_rule.rb +0 -112
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 85ff1a35136e9b7ff920830b2bf9a2e4b477bffed405c2386e33739ed6560a45
|
|
4
|
+
data.tar.gz: f7ef8fb96a58eb21a71534fcc8afc1cf7f26053379cfb00e5ec98098620ca6a6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: beeecf5f816d5ec10741375d357a1b3602e7d415ebedfc71eff52d414f1d623ed119c7995355231c147020f54b814dc8306d37acdc6eecacdf8ca72ec6ddd65f
|
|
7
|
+
data.tar.gz: 0a7de5bd20f0f7c58c1c08066983b5c490999570249b686f84f60bd87df9b5e436913db66e43b5d1003805bfa33a6613d67bbd8c49373d21a03815dde83a8e42
|
data/CLAUDE.md
CHANGED
|
@@ -47,23 +47,35 @@ This consolidation closes the legacy trap where writing to
|
|
|
47
47
|
`data.related` bypassed `ManagedConcept.detect_schema_version` (which
|
|
48
48
|
keys off `concept.related`).
|
|
49
49
|
|
|
50
|
-
###
|
|
50
|
+
### PartitiveRelation (V3 only)
|
|
51
51
|
|
|
52
|
-
`
|
|
52
|
+
`PartitiveRelation` (`v3/partitive_relation.rb`) — an ISO 704 / ISO 1087-1 / ISO 12620 partitive relation. One comprehensive concept (superordinate concept partitive) is connected to two or more partitive concepts (subordinate concepts partitive) which fitted together constitute the comprehensive.
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
- diagram notation flags (`PluralityMarker`: `double`, `dashed`)
|
|
56
|
-
- enumeration completeness (`PartitiveEnumeration`: `closed`, `open`)
|
|
54
|
+
Each PartitiveRelation carries:
|
|
57
55
|
|
|
58
|
-
|
|
56
|
+
- `comprehensive` — ConceptRef to the superordinate concept partitive (the whole)
|
|
57
|
+
- `partitives` — `PartitiveMember [2..*]` (ISO 704 "two or more"); each member wraps a ConceptRef plus optional `MemberCertainty` (`confirmed` default, `possible`)
|
|
58
|
+
- `completeness` — `complete` (default; the encoded partitives constitute the whole) or `partial` (others exist beyond those encoded). ISO 704 rake backline notation.
|
|
59
|
+
- `plurality` — optional `TypeSharedPlurality` block (ISO 704 close-set double line promoted to data: `is_shared`, `is_uncertain`, `shared_type`). Replaces the prior opaque `markers` field.
|
|
60
|
+
- `criterion` — optional localized string hash (`{ "eng" => "physical structure" }`). ISO 12620 coordinate-concept coherence: partitives within one relation share the comprehensive AND the criterion.
|
|
59
61
|
|
|
60
|
-
|
|
62
|
+
Wired into `V3::ManagedConcept#partitive_relations`. NOT on `ManagedConceptData` (relationships live at the concept level for MECE consistency with `related`).
|
|
61
63
|
|
|
62
|
-
|
|
64
|
+
Enum values are SSOT-loaded from `config.yml` via `GlossaryDefinition::COMPLETENESS_VALUES` and `MEMBER_CERTAINTY_VALUES`. The `values:` option on each attribute documents the enum; the model's `validate!` method enforces structural invariants after construction (empty comprehensive, <2 partitives, self-loops, incoherent plurality, invalid enum values).
|
|
63
65
|
|
|
64
|
-
|
|
66
|
+
Semantic checks (≥2 partitives cardinality, duplicate comprehensive+criterion detection, criterion-absent warning, ExternalConcept shape) live in `Validation::Rules::PartitiveRelationRule` (auto-registered by `Validation::Rules`, code GLS-221).
|
|
65
67
|
|
|
66
|
-
|
|
68
|
+
RDF emission: `Rdf::GlossPartitiveRelation` view class with companion `GlossPartitiveMember` and `GlossTypeSharedPlurality`. Wired into `Transforms::ConceptToGlossTransform` and emitted as `gloss:hasPartitiveRelation` link from `GlossConcept`.
|
|
69
|
+
|
|
70
|
+
Binary `broader_partitive`/`narrower_partitive`/`has_part`/`is_part_of` `RelatedConcept` edges and `PartitiveRelation` coexist — no automatic consolidation. Use a binary edge for pairwise relationships; use a PartitiveRelation when membership is jointly significant (≥2 partitives + completeness/plurality/criterion metadata).
|
|
71
|
+
|
|
72
|
+
#### ExternalConcept
|
|
73
|
+
|
|
74
|
+
`status: external` is a value of `ConceptStatus` (not a separate class). A concept with `status: external` is referenced from this dataset but defined elsewhere — ISO 704 "parenthetic term" semantic role. Required fields (definition, sources) are relaxed; at least one designation is still required. Resolves to another dataset's concept via `provided_by` when a defining dataset is loaded.
|
|
75
|
+
|
|
76
|
+
#### Relationship type extensions
|
|
77
|
+
|
|
78
|
+
Two new values in `RelatedConceptType`: `provides` (real concept → external concept) and `provided_by` (external concept → real concept). Inverse pair, established at the collection level.
|
|
67
79
|
|
|
68
80
|
### Reference Resolution
|
|
69
81
|
|
data/config.yml
CHANGED
|
@@ -101,6 +101,12 @@ related_concept:
|
|
|
101
101
|
- version_of
|
|
102
102
|
- current_version
|
|
103
103
|
- current_version_of
|
|
104
|
+
# ExternalConcept resolution (Glossarist extension)
|
|
105
|
+
# Real concept `provides` substance for an external concept;
|
|
106
|
+
# external concept `provided_by` real concept. Inverse pair.
|
|
107
|
+
# Established at the collection level when both datasets are loaded.
|
|
108
|
+
- provides
|
|
109
|
+
- provided_by
|
|
104
110
|
|
|
105
111
|
iso12620:
|
|
106
112
|
term_type:
|
|
@@ -178,6 +184,10 @@ concept:
|
|
|
178
184
|
- valid
|
|
179
185
|
- superseded
|
|
180
186
|
- retired
|
|
187
|
+
# Glossarist extension: concept referenced from this dataset but
|
|
188
|
+
# defined elsewhere. Resolves via `provided_by` when a defining
|
|
189
|
+
# dataset is loaded. ISO 704 "parenthetic term" semantic role.
|
|
190
|
+
- external
|
|
181
191
|
|
|
182
192
|
concept_date:
|
|
183
193
|
type:
|
|
@@ -187,17 +197,23 @@ concept_date:
|
|
|
187
197
|
- review
|
|
188
198
|
- review_decision
|
|
189
199
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
200
|
+
# PartitiveRelation redesign (TODO.partitive-relation-v2)
|
|
201
|
+
# Replaces partitive_enumeration + plurality_marker.
|
|
202
|
+
|
|
203
|
+
completeness:
|
|
204
|
+
# Whether the encoded partitives fitted together constitute the
|
|
205
|
+
# whole comprehensive concept (complete) or only some of it
|
|
206
|
+
# (partial — other partitives exist but are not encoded).
|
|
207
|
+
# ISO 704: rake backline ending with a tooth = complete;
|
|
208
|
+
# backline continuing without a tooth = partial.
|
|
209
|
+
# Default: complete.
|
|
194
210
|
value:
|
|
195
|
-
-
|
|
196
|
-
-
|
|
211
|
+
- complete
|
|
212
|
+
- partial
|
|
197
213
|
|
|
198
|
-
|
|
199
|
-
#
|
|
200
|
-
#
|
|
214
|
+
member_certainty:
|
|
215
|
+
# Per-partitive certainty. Glossarist extension beyond ISO 704
|
|
216
|
+
# notation. Defaults to confirmed when omitted.
|
|
201
217
|
value:
|
|
202
|
-
-
|
|
203
|
-
-
|
|
218
|
+
- confirmed
|
|
219
|
+
- possible
|
|
@@ -34,10 +34,10 @@ module Glossarist
|
|
|
34
34
|
|
|
35
35
|
ISO12620_TERM_TYPES = config.dig("iso12620", "term_type").freeze
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
# PartitiveRelation redesign (TODO.partitive-relation-v2).
|
|
38
|
+
# Replaces PARTITIVE_ENUMERATION_VALUES + PLURALITY_MARKER_VALUES.
|
|
39
|
+
COMPLETENESS_VALUES = config.dig("completeness", "value").freeze
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
config.dig("plurality_marker", "value").freeze
|
|
41
|
+
MEMBER_CERTAINTY_VALUES = config.dig("member_certainty", "value").freeze
|
|
42
42
|
end
|
|
43
43
|
end
|
|
@@ -175,7 +175,7 @@ module Glossarist
|
|
|
175
175
|
return "3" if concept.sources&.any?
|
|
176
176
|
return "3" if concept.data&.domains&.any?
|
|
177
177
|
return "3" if concept.is_a?(V3::ManagedConcept) &&
|
|
178
|
-
concept.
|
|
178
|
+
concept.partitive_relations&.any?
|
|
179
179
|
return "3" if localization_has_references?(concept)
|
|
180
180
|
|
|
181
181
|
"2"
|
|
@@ -11,7 +11,7 @@ module Glossarist
|
|
|
11
11
|
attribute :sources, GlossConceptSource, collection: true
|
|
12
12
|
attribute :domains, GlossConceptReference, collection: true
|
|
13
13
|
attribute :dates, GlossConceptDate, collection: true
|
|
14
|
-
attribute :
|
|
14
|
+
attribute :partitive_relations, GlossPartitiveRelation, collection: true
|
|
15
15
|
|
|
16
16
|
RelationshipPredicates::CONCEPT_REL_PREDICATES.each_key do |type|
|
|
17
17
|
attribute :"#{type}_targets", :string, collection: true
|
|
@@ -42,8 +42,8 @@ module Glossarist
|
|
|
42
42
|
link: "gloss:hasDomain"
|
|
43
43
|
members :dates,
|
|
44
44
|
link: "gloss:hasDate"
|
|
45
|
-
members :
|
|
46
|
-
link: "gloss:
|
|
45
|
+
members :partitive_relations,
|
|
46
|
+
link: "gloss:hasPartitiveRelation"
|
|
47
47
|
|
|
48
48
|
RelationshipPredicates::CONCEPT_REL_PREDICATES.each do |type, (ns, name)|
|
|
49
49
|
predicate name, namespace: ns, to: :"#{type}_targets",
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
|
|
5
|
+
module Glossarist
|
|
6
|
+
module Rdf
|
|
7
|
+
# RDF view for V3::PartitiveMember. Emits a
|
|
8
|
+
# gloss:PartitiveMember subject with ref + certainty properties.
|
|
9
|
+
class GlossPartitiveMember < Lutaml::Model::Serializable
|
|
10
|
+
attribute :ref_id, :string
|
|
11
|
+
attribute :ref_source, :string
|
|
12
|
+
attribute :ref_text, :string
|
|
13
|
+
attribute :certainty, :string
|
|
14
|
+
|
|
15
|
+
rdf do
|
|
16
|
+
namespace Namespaces::GlossaristNamespace
|
|
17
|
+
|
|
18
|
+
subject { |m| "partitiveMember/#{m.ref_source}:#{m.ref_id}" }
|
|
19
|
+
|
|
20
|
+
types "gloss:PartitiveMember"
|
|
21
|
+
|
|
22
|
+
predicate :refSource, namespace: Namespaces::GlossaristNamespace,
|
|
23
|
+
to: :ref_source
|
|
24
|
+
predicate :refId, namespace: Namespaces::GlossaristNamespace,
|
|
25
|
+
to: :ref_id
|
|
26
|
+
predicate :refText, namespace: Namespaces::GlossaristNamespace,
|
|
27
|
+
to: :ref_text
|
|
28
|
+
predicate :certainty, namespace: Namespaces::GlossaristNamespace,
|
|
29
|
+
to: :certainty
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
|
|
5
|
+
module Glossarist
|
|
6
|
+
module Rdf
|
|
7
|
+
# RDF view for V3::PartitiveRelation. Emits a
|
|
8
|
+
# gloss:PartitiveRelation subject with comprehensive, hasPartitive,
|
|
9
|
+
# completeness, hasPlurality, criterion properties.
|
|
10
|
+
class GlossPartitiveRelation < Lutaml::Model::Serializable
|
|
11
|
+
attribute :identifier, :string
|
|
12
|
+
attribute :comprehensive_uri, :string
|
|
13
|
+
attribute :partitive_member_ids, :string, collection: true
|
|
14
|
+
attribute :completeness, :string
|
|
15
|
+
attribute :has_plurality, GlossTypeSharedPlurality
|
|
16
|
+
attribute :criterion, :hash
|
|
17
|
+
|
|
18
|
+
rdf do
|
|
19
|
+
namespace Namespaces::GlossaristNamespace
|
|
20
|
+
|
|
21
|
+
subject { |r| "partitiveRelation/#{r.identifier}/#{r.object_id}" }
|
|
22
|
+
|
|
23
|
+
types "gloss:PartitiveRelation"
|
|
24
|
+
|
|
25
|
+
predicate :comprehensive, namespace: Namespaces::GlossaristNamespace,
|
|
26
|
+
to: :comprehensive_uri, uri_reference: true
|
|
27
|
+
predicate :hasPartitive, namespace: Namespaces::GlossaristNamespace,
|
|
28
|
+
to: :partitive_member_ids, uri_reference: true
|
|
29
|
+
predicate :completeness, namespace: Namespaces::GlossaristNamespace,
|
|
30
|
+
to: :completeness, uri_reference: true
|
|
31
|
+
members :has_plurality,
|
|
32
|
+
link: "gloss:hasPlurality"
|
|
33
|
+
predicate :criterion, namespace: Namespaces::GlossaristNamespace,
|
|
34
|
+
to: :criterion
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
|
|
5
|
+
module Glossarist
|
|
6
|
+
module Rdf
|
|
7
|
+
# RDF view for V3::TypeSharedPlurality. Emits a
|
|
8
|
+
# gloss:TypeSharedPlurality subject with is_shared, is_uncertain,
|
|
9
|
+
# shared_type properties.
|
|
10
|
+
class GlossTypeSharedPlurality < Lutaml::Model::Serializable
|
|
11
|
+
attribute :is_shared, :boolean
|
|
12
|
+
attribute :is_uncertain, :boolean
|
|
13
|
+
attribute :shared_type_uri, :string
|
|
14
|
+
|
|
15
|
+
rdf do
|
|
16
|
+
namespace Namespaces::GlossaristNamespace
|
|
17
|
+
|
|
18
|
+
subject { |p| "plurality/#{p.object_id}" }
|
|
19
|
+
|
|
20
|
+
types "gloss:TypeSharedPlurality"
|
|
21
|
+
|
|
22
|
+
predicate :isShared, namespace: Namespaces::GlossaristNamespace,
|
|
23
|
+
to: :is_shared
|
|
24
|
+
predicate :isUncertain, namespace: Namespaces::GlossaristNamespace,
|
|
25
|
+
to: :is_uncertain
|
|
26
|
+
predicate :sharedType, namespace: Namespaces::GlossaristNamespace,
|
|
27
|
+
to: :shared_type_uri, uri_reference: true
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
data/lib/glossarist/rdf/v3.rb
CHANGED
data/lib/glossarist/rdf.rb
CHANGED
|
@@ -42,7 +42,10 @@ module Glossarist
|
|
|
42
42
|
autoload :GlossFigureImage, "#{__dir__}/rdf/gloss_figure_image"
|
|
43
43
|
autoload :GlossTable, "#{__dir__}/rdf/gloss_table"
|
|
44
44
|
autoload :GlossFormula, "#{__dir__}/rdf/gloss_formula"
|
|
45
|
-
autoload :
|
|
45
|
+
autoload :GlossPartitiveRelation, "#{__dir__}/rdf/gloss_partitive_relation"
|
|
46
|
+
autoload :GlossPartitiveMember, "#{__dir__}/rdf/gloss_partitive_member"
|
|
47
|
+
autoload :GlossTypeSharedPlurality,
|
|
48
|
+
"#{__dir__}/rdf/gloss_type_shared_plurality"
|
|
46
49
|
autoload :V3, "#{__dir__}/rdf/v3"
|
|
47
50
|
end
|
|
48
51
|
end
|
|
@@ -130,42 +130,64 @@ module Glossarist
|
|
|
130
130
|
domains: build_gloss_domains(managed_concept.data&.domains,
|
|
131
131
|
identifier),
|
|
132
132
|
dates: build_gloss_dates(managed_concept.dates, identifier),
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
partitive_relations: build_gloss_partitive_relations(
|
|
134
|
+
v3_partitive_relations(managed_concept), identifier,
|
|
135
|
+
),
|
|
135
136
|
**rel_targets,
|
|
136
137
|
)
|
|
137
138
|
end
|
|
138
139
|
|
|
139
|
-
#
|
|
140
|
-
# V1/V2 and V3 concepts; for V1/V2 concepts the
|
|
141
|
-
# is always empty.
|
|
142
|
-
def
|
|
140
|
+
# PartitiveRelations are a V3-only attribute. The transform
|
|
141
|
+
# accepts both V1/V2 and V3 concepts; for V1/V2 concepts the
|
|
142
|
+
# relation list is always empty.
|
|
143
|
+
def v3_partitive_relations(managed_concept)
|
|
143
144
|
return [] unless managed_concept.is_a?(V3::ManagedConcept)
|
|
144
145
|
|
|
145
|
-
managed_concept.
|
|
146
|
+
managed_concept.partitive_relations
|
|
146
147
|
end
|
|
147
148
|
|
|
148
|
-
def
|
|
149
|
-
return [] unless
|
|
149
|
+
def build_gloss_partitive_relations(relations, identifier)
|
|
150
|
+
return [] unless relations
|
|
150
151
|
|
|
151
|
-
Array(
|
|
152
|
-
Rdf::
|
|
152
|
+
Array(relations).map do |rel|
|
|
153
|
+
Rdf::GlossPartitiveRelation.new(
|
|
153
154
|
identifier: identifier.to_s,
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
155
|
+
comprehensive_uri: partitive_concept_uri(rel.comprehensive),
|
|
156
|
+
partitive_member_ids: Array(rel.partitives).map do |m|
|
|
157
|
+
partitive_member_uri(m)
|
|
158
|
+
end,
|
|
159
|
+
completeness: rel.completeness,
|
|
160
|
+
has_plurality: build_gloss_plurality(rel.plurality),
|
|
161
|
+
criterion: rel.criterion,
|
|
160
162
|
)
|
|
161
163
|
end
|
|
162
164
|
end
|
|
163
165
|
|
|
164
|
-
def
|
|
166
|
+
def build_gloss_plurality(plurality)
|
|
167
|
+
return nil unless plurality
|
|
168
|
+
|
|
169
|
+
Rdf::GlossTypeSharedPlurality.new(
|
|
170
|
+
is_shared: plurality.is_shared,
|
|
171
|
+
is_uncertain: plurality.is_uncertain,
|
|
172
|
+
shared_type_uri: partitive_concept_uri(plurality.shared_type),
|
|
173
|
+
)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def partitive_concept_uri(ref)
|
|
177
|
+
return nil unless ref.is_a?(Glossarist::ConceptRef)
|
|
178
|
+
|
|
179
|
+
Glossarist::Rdf::Namespaces::GlossaristNamespace.uri +
|
|
180
|
+
"concept/#{ref.id || ref.text}"
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def partitive_member_uri(member)
|
|
184
|
+
return nil unless member.is_a?(V3::PartitiveMember)
|
|
185
|
+
|
|
186
|
+
ref = member.ref
|
|
165
187
|
return nil unless ref.is_a?(Glossarist::ConceptRef)
|
|
166
188
|
|
|
167
189
|
Glossarist::Rdf::Namespaces::GlossaristNamespace.uri +
|
|
168
|
-
"concept/#{ref.id}"
|
|
190
|
+
"concept/#{ref.id || ref.text}"
|
|
169
191
|
end
|
|
170
192
|
|
|
171
193
|
def build_gloss_localized_concept(l10n, concept_id)
|
|
@@ -5,7 +5,7 @@ module Glossarist
|
|
|
5
5
|
class ManagedConcept < Glossarist::ManagedConcept
|
|
6
6
|
attribute :data, V3::ManagedConceptData, default: -> { V3::ManagedConceptData.new }
|
|
7
7
|
attribute :related, V3::RelatedConcept, collection: true
|
|
8
|
-
attribute :
|
|
8
|
+
attribute :partitive_relations, V3::PartitiveRelation, collection: true
|
|
9
9
|
attribute :dates, V3::ConceptDate, collection: true
|
|
10
10
|
attribute :date_accepted, V3::ConceptDate
|
|
11
11
|
attribute :sources, V3::ConceptSource, collection: true
|
|
@@ -13,7 +13,7 @@ module Glossarist
|
|
|
13
13
|
key_value do
|
|
14
14
|
map :data, to: :data
|
|
15
15
|
map :related, to: :related
|
|
16
|
-
map :
|
|
16
|
+
map :partitive_relations, to: :partitive_relations
|
|
17
17
|
map :dates, to: :dates
|
|
18
18
|
map %i[date_accepted dateAccepted],
|
|
19
19
|
with: { from: :date_accepted_from_yaml, to: :date_accepted_to_yaml }
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glossarist
|
|
4
|
+
module V3
|
|
5
|
+
# PartitiveMember — one member of a PartitiveRelation, carrying
|
|
6
|
+
# a ConceptRef to the partitive concept plus optional certainty
|
|
7
|
+
# metadata (Glossarist extension beyond ISO 704 notation).
|
|
8
|
+
#
|
|
9
|
+
# A PartitiveMember with no certainty field is implicitly
|
|
10
|
+
# confirmed.
|
|
11
|
+
class PartitiveMember < Lutaml::Model::Serializable
|
|
12
|
+
DEFAULT_CERTAINTY = "confirmed"
|
|
13
|
+
|
|
14
|
+
attribute :ref, ConceptRef
|
|
15
|
+
attribute :certainty, :string,
|
|
16
|
+
values: Glossarist::GlossaryDefinition::MEMBER_CERTAINTY_VALUES,
|
|
17
|
+
default: -> { DEFAULT_CERTAINTY }
|
|
18
|
+
|
|
19
|
+
key_value do
|
|
20
|
+
map :ref, to: :ref
|
|
21
|
+
map :certainty, to: :certainty
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def validate!
|
|
25
|
+
validate_ref!
|
|
26
|
+
validate_certainty!
|
|
27
|
+
self
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def confirmed?
|
|
31
|
+
certainty == DEFAULT_CERTAINTY
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def possible?
|
|
35
|
+
certainty == "possible"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def validate_ref!
|
|
41
|
+
return if ref.is_a?(ConceptRef) && (ref.source || ref.id || ref.text)
|
|
42
|
+
|
|
43
|
+
raise ArgumentError,
|
|
44
|
+
"PartitiveMember#ref must be a non-empty ConceptRef " \
|
|
45
|
+
"(source, id, or text required)"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def validate_certainty!
|
|
49
|
+
return if certainty.nil?
|
|
50
|
+
|
|
51
|
+
unless Glossarist::GlossaryDefinition::MEMBER_CERTAINTY_VALUES
|
|
52
|
+
.include?(certainty)
|
|
53
|
+
raise ArgumentError,
|
|
54
|
+
"PartitiveMember#certainty has invalid value " \
|
|
55
|
+
"#{certainty.inspect}; must be one of " \
|
|
56
|
+
"#{GlossaryDefinition::MEMBER_CERTAINTY_VALUES.join(', ')}"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glossarist
|
|
4
|
+
module V3
|
|
5
|
+
# PartitiveRelation — an ISO 704 / ISO 1087-1 / ISO 12620
|
|
6
|
+
# partitive relation connecting a comprehensive concept
|
|
7
|
+
# (superordinate concept partitive) to two or more partitive
|
|
8
|
+
# concepts (subordinate concepts partitive) which fitted together
|
|
9
|
+
# constitute the comprehensive.
|
|
10
|
+
#
|
|
11
|
+
# Shown as a rake or bracket in source diagrams. All partitives
|
|
12
|
+
# within one relation are coordinate concepts: they share the
|
|
13
|
+
# comprehensive AND share the criterion of subdivision.
|
|
14
|
+
#
|
|
15
|
+
# Glossarist extensions beyond ISO notation:
|
|
16
|
+
# - Per-partitive certainty (MemberCertainty)
|
|
17
|
+
# - plurality.shared_type (the type the partitives share)
|
|
18
|
+
# - criterion as a structured field (not just diagrammatic)
|
|
19
|
+
#
|
|
20
|
+
# Replaces the prior PartitiveHyperedge class. The "hyperedge"
|
|
21
|
+
# framing was graph-theoretic; ISO calls this a *relation*.
|
|
22
|
+
class PartitiveRelation < Lutaml::Model::Serializable
|
|
23
|
+
DEFAULT_COMPLETENESS = "complete"
|
|
24
|
+
|
|
25
|
+
attribute :comprehensive, ConceptRef
|
|
26
|
+
attribute :partitives, PartitiveMember, collection: true
|
|
27
|
+
attribute :completeness, :string,
|
|
28
|
+
values: Glossarist::GlossaryDefinition::COMPLETENESS_VALUES,
|
|
29
|
+
default: -> { DEFAULT_COMPLETENESS }
|
|
30
|
+
attribute :plurality, TypeSharedPlurality
|
|
31
|
+
attribute :criterion, :hash
|
|
32
|
+
|
|
33
|
+
key_value do
|
|
34
|
+
map :comprehensive, to: :comprehensive
|
|
35
|
+
map :partitives, to: :partitives
|
|
36
|
+
map :completeness, to: :completeness
|
|
37
|
+
map :plurality, to: :plurality
|
|
38
|
+
map :criterion, to: :criterion
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def validate!
|
|
42
|
+
validate_comprehensive!
|
|
43
|
+
validate_partitives!
|
|
44
|
+
validate_self_loop!
|
|
45
|
+
validate_completeness!
|
|
46
|
+
validate_plurality!
|
|
47
|
+
self
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def complete?
|
|
51
|
+
completeness == "complete"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def partial?
|
|
55
|
+
completeness == "partial"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# ISO 704: a partitive relation connects to two or more
|
|
59
|
+
# partitives. A single binary has_part edge is not a
|
|
60
|
+
# PartitiveRelation.
|
|
61
|
+
def coordinate?
|
|
62
|
+
partitives.length >= 2
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def validate_comprehensive!
|
|
68
|
+
return if comprehensive.is_a?(ConceptRef) &&
|
|
69
|
+
(comprehensive.source || comprehensive.id || comprehensive.text)
|
|
70
|
+
|
|
71
|
+
raise ArgumentError,
|
|
72
|
+
"PartitiveRelation#comprehensive must be a non-empty " \
|
|
73
|
+
"ConceptRef (source, id, or text required)"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def validate_partitives!
|
|
77
|
+
if partitives.empty?
|
|
78
|
+
raise ArgumentError, "PartitiveRelation requires at least one partitive"
|
|
79
|
+
end
|
|
80
|
+
unless coordinate?
|
|
81
|
+
raise ArgumentError,
|
|
82
|
+
"PartitiveRelation requires ≥2 partitives (ISO 704); a single " \
|
|
83
|
+
"binary has_part edge should be used instead"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
partitives.each(&:validate!)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def validate_self_loop!
|
|
90
|
+
return unless comprehensive.is_a?(ConceptRef)
|
|
91
|
+
|
|
92
|
+
comp_key = [comprehensive.source, comprehensive.id]
|
|
93
|
+
partitives.each do |member|
|
|
94
|
+
next unless member.ref.is_a?(ConceptRef)
|
|
95
|
+
next unless [member.ref.source, member.ref.id] == comp_key
|
|
96
|
+
|
|
97
|
+
raise ArgumentError,
|
|
98
|
+
"PartitiveRelation#partitives cannot include the comprehensive"
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def validate_completeness!
|
|
103
|
+
return if completeness.nil?
|
|
104
|
+
|
|
105
|
+
unless Glossarist::GlossaryDefinition::COMPLETENESS_VALUES
|
|
106
|
+
.include?(completeness)
|
|
107
|
+
raise ArgumentError,
|
|
108
|
+
"PartitiveRelation#completeness has invalid value " \
|
|
109
|
+
"#{completeness.inspect}; must be one of " \
|
|
110
|
+
"#{GlossaryDefinition::COMPLETENESS_VALUES.join(', ')}"
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def validate_plurality!
|
|
115
|
+
plurality&.validate!
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glossarist
|
|
4
|
+
module V3
|
|
5
|
+
# TypeSharedPlurality — semantic claims encoded by ISO 704's
|
|
6
|
+
# close-set double line and broken line notation, promoted from
|
|
7
|
+
# diagram-notation flags to structured data so tools can reason
|
|
8
|
+
# about plurality directly.
|
|
9
|
+
#
|
|
10
|
+
# A PartitiveRelation has at most one TypeSharedPlurality block.
|
|
11
|
+
# Absent means: no type-shared plurality claim is being made.
|
|
12
|
+
#
|
|
13
|
+
# Replaces the prior `markers` field (which encoded the same
|
|
14
|
+
# information as opaque strings like "double" and "dashed").
|
|
15
|
+
class TypeSharedPlurality < Lutaml::Model::Serializable
|
|
16
|
+
attribute :is_shared, :boolean
|
|
17
|
+
attribute :is_uncertain, :boolean, default: -> { false }
|
|
18
|
+
attribute :shared_type, ConceptRef
|
|
19
|
+
|
|
20
|
+
key_value do
|
|
21
|
+
map :is_shared, to: :is_shared
|
|
22
|
+
map :is_uncertain, to: :is_uncertain
|
|
23
|
+
map :shared_type, to: :shared_type
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def validate!
|
|
27
|
+
unless is_shared.is_a?(TrueClass) || is_shared.is_a?(FalseClass)
|
|
28
|
+
raise ArgumentError,
|
|
29
|
+
"TypeSharedPlurality#is_shared is required (boolean)"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
if is_uncertain == true && is_shared == false
|
|
33
|
+
raise ArgumentError,
|
|
34
|
+
"TypeSharedPlurality#is_uncertain requires is_shared: true " \
|
|
35
|
+
"(ISO 704 broken line qualifies the close-set double line claim)"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
self
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
data/lib/glossarist/v3.rb
CHANGED
|
@@ -9,7 +9,9 @@ module Glossarist
|
|
|
9
9
|
autoload :DetailedDefinition, "glossarist/v3/detailed_definition"
|
|
10
10
|
autoload :ConceptRef, "glossarist/v3/concept_ref"
|
|
11
11
|
autoload :RelatedConcept, "glossarist/v3/related_concept"
|
|
12
|
-
autoload :
|
|
12
|
+
autoload :PartitiveRelation, "glossarist/v3/partitive_relation"
|
|
13
|
+
autoload :PartitiveMember, "glossarist/v3/partitive_member"
|
|
14
|
+
autoload :TypeSharedPlurality, "glossarist/v3/type_shared_plurality"
|
|
13
15
|
autoload :ConceptData, "glossarist/v3/concept_data"
|
|
14
16
|
autoload :LocalizedConcept, "glossarist/v3/localized_concept"
|
|
15
17
|
autoload :ManagedConceptData, "glossarist/v3/managed_concept_data"
|
|
@@ -24,7 +26,9 @@ module Glossarist
|
|
|
24
26
|
Configuration.register_model(LocalizedConcept, id: :localized_concept)
|
|
25
27
|
Configuration.register_model(ConceptRef, id: :concept_ref)
|
|
26
28
|
Configuration.register_model(RelatedConcept, id: :related_concept)
|
|
27
|
-
Configuration.register_model(
|
|
29
|
+
Configuration.register_model(PartitiveRelation, id: :partitive_relation)
|
|
30
|
+
Configuration.register_model(PartitiveMember, id: :partitive_member)
|
|
31
|
+
Configuration.register_model(TypeSharedPlurality, id: :type_shared_plurality)
|
|
28
32
|
Configuration.register_model(ManagedConceptData, id: :managed_concept_data)
|
|
29
33
|
Configuration.register_model(ManagedConcept, id: :managed_concept)
|
|
30
34
|
Configuration.register_model(ConceptDocument, id: :concept_document)
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glossarist
|
|
4
|
+
module Validation
|
|
5
|
+
module Rules
|
|
6
|
+
# Validates semantic invariants of PartitiveRelation entries
|
|
7
|
+
# that the model constructor does NOT enforce. Specifically:
|
|
8
|
+
#
|
|
9
|
+
# - error when a relation has fewer than 2 partitives
|
|
10
|
+
# (ISO 704: "two or more"; single binary should use
|
|
11
|
+
# has_part edge instead)
|
|
12
|
+
# - error when two relations share the same comprehensive
|
|
13
|
+
# AND the same non-nil criterion (duplicate decomposition;
|
|
14
|
+
# ISO 12620 coordinate-concept coherence)
|
|
15
|
+
# - warning when a relation has no criterion (cannot
|
|
16
|
+
# distinguish from siblings sharing the comprehensive)
|
|
17
|
+
# - error when plurality.is_uncertain is set without
|
|
18
|
+
# plurality.is_shared: true (broken-line qualifies
|
|
19
|
+
# close-set double line)
|
|
20
|
+
# - error when ExternalConcept (status: external) lacks
|
|
21
|
+
# at least one designation
|
|
22
|
+
#
|
|
23
|
+
# The model constructor already rejects empty comprehensive,
|
|
24
|
+
# empty partitives list, self-loops, invalid enum values.
|
|
25
|
+
class PartitiveRelationRule < Base
|
|
26
|
+
def code = "GLS-221"
|
|
27
|
+
def category = :schema
|
|
28
|
+
def severity = "error"
|
|
29
|
+
def scope = :concept
|
|
30
|
+
|
|
31
|
+
def applicable?(context)
|
|
32
|
+
concept = context.concept
|
|
33
|
+
return false unless concept.is_a?(V3::ManagedConcept)
|
|
34
|
+
|
|
35
|
+
concept.partitive_relations&.any? || external?(concept)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def check(context)
|
|
39
|
+
concept = context.concept
|
|
40
|
+
fname = context.file_name
|
|
41
|
+
issues = []
|
|
42
|
+
|
|
43
|
+
return issues unless concept.is_a?(V3::ManagedConcept)
|
|
44
|
+
|
|
45
|
+
relations = Array(concept.partitive_relations)
|
|
46
|
+
|
|
47
|
+
relations.each_with_index do |rel, idx|
|
|
48
|
+
check_cardinality(rel, idx, fname, issues)
|
|
49
|
+
check_criterion_present(rel, idx, fname, issues)
|
|
50
|
+
check_plurality_coherence(rel, idx, fname, issues)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
check_duplicate_decomposition(relations, fname, issues)
|
|
54
|
+
check_external_concept(concept, fname, issues)
|
|
55
|
+
|
|
56
|
+
issues
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def external?(concept)
|
|
62
|
+
concept.status == "external"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def check_cardinality(rel, idx, fname, issues)
|
|
66
|
+
return if rel.partitives.length >= 2
|
|
67
|
+
|
|
68
|
+
issues << issue(
|
|
69
|
+
"partitive_relation #{idx + 1} has fewer than 2 partitives " \
|
|
70
|
+
"(ISO 704 requires two or more); a single binary has_part edge " \
|
|
71
|
+
"should be used instead",
|
|
72
|
+
location: fname,
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def check_criterion_present(rel, idx, fname, issues)
|
|
77
|
+
return if rel.criterion && !rel.criterion.empty?
|
|
78
|
+
|
|
79
|
+
issues << issue(
|
|
80
|
+
"partitive_relation #{idx + 1} has no criterion; cannot verify " \
|
|
81
|
+
"distinctness from sibling relations sharing the comprehensive " \
|
|
82
|
+
"(ISO 12620 coordinate-concept coherence)",
|
|
83
|
+
severity: "warning",
|
|
84
|
+
location: fname,
|
|
85
|
+
suggestion: "Add a criterion: { eng: '...' } field",
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def check_plurality_coherence(rel, idx, fname, issues)
|
|
90
|
+
plural = rel.plurality
|
|
91
|
+
return unless plural
|
|
92
|
+
return unless plural.is_uncertain
|
|
93
|
+
return if plural.is_shared
|
|
94
|
+
|
|
95
|
+
issues << issue(
|
|
96
|
+
"partitive_relation #{idx + 1}.plurality: is_uncertain requires " \
|
|
97
|
+
"is_shared: true (ISO 704 broken line qualifies the close-set " \
|
|
98
|
+
"double line claim)",
|
|
99
|
+
location: fname,
|
|
100
|
+
)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def check_duplicate_decomposition(relations, fname, issues)
|
|
104
|
+
grouped = {}
|
|
105
|
+
relations.each_with_index do |rel, idx|
|
|
106
|
+
next unless rel.criterion && !rel.criterion.empty?
|
|
107
|
+
|
|
108
|
+
key = criterion_key(rel)
|
|
109
|
+
(grouped[key] ||= []) << idx
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
grouped.each do |key, idxs|
|
|
113
|
+
next if idxs.length == 1
|
|
114
|
+
|
|
115
|
+
issues << issue(
|
|
116
|
+
"duplicate PartitiveRelation for comprehensive " \
|
|
117
|
+
"#{key.first.inspect} with criterion #{key.last.inspect} " \
|
|
118
|
+
"(relations ##{idxs.map { |i| i + 1 }.join(', ')}); " \
|
|
119
|
+
"two relations sharing comprehensive AND criterion are the " \
|
|
120
|
+
"same decomposition",
|
|
121
|
+
location: fname,
|
|
122
|
+
)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def criterion_key(rel)
|
|
127
|
+
comp = rel.comprehensive
|
|
128
|
+
comp_id = comp.is_a?(ConceptRef) ? [comp.source, comp.id] : nil
|
|
129
|
+
[comp_id, rel.criterion]
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def check_external_concept(concept, fname, issues)
|
|
133
|
+
return unless external?(concept)
|
|
134
|
+
|
|
135
|
+
has_designation = concept.localizations.any? do |_, lc|
|
|
136
|
+
lc.is_a?(LocalizedConcept) && lc.terms&.any?
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
return if has_designation
|
|
140
|
+
|
|
141
|
+
issues << issue(
|
|
142
|
+
"ExternalConcept (status: external) must have at least one " \
|
|
143
|
+
"designation — even external concepts have a name",
|
|
144
|
+
location: fname,
|
|
145
|
+
)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
data/lib/glossarist/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: glossarist
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.13.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose
|
|
@@ -307,13 +307,15 @@ files:
|
|
|
307
307
|
- lib/glossarist/rdf/gloss_figure_image.rb
|
|
308
308
|
- lib/glossarist/rdf/gloss_formula.rb
|
|
309
309
|
- lib/glossarist/rdf/gloss_grammar_info.rb
|
|
310
|
-
- lib/glossarist/rdf/gloss_hyperedge.rb
|
|
311
310
|
- lib/glossarist/rdf/gloss_locality.rb
|
|
312
311
|
- lib/glossarist/rdf/gloss_localized_concept.rb
|
|
313
312
|
- lib/glossarist/rdf/gloss_non_verbal_rep.rb
|
|
313
|
+
- lib/glossarist/rdf/gloss_partitive_member.rb
|
|
314
|
+
- lib/glossarist/rdf/gloss_partitive_relation.rb
|
|
314
315
|
- lib/glossarist/rdf/gloss_pronunciation.rb
|
|
315
316
|
- lib/glossarist/rdf/gloss_reference.rb
|
|
316
317
|
- lib/glossarist/rdf/gloss_table.rb
|
|
318
|
+
- lib/glossarist/rdf/gloss_type_shared_plurality.rb
|
|
317
319
|
- lib/glossarist/rdf/localized_literal.rb
|
|
318
320
|
- lib/glossarist/rdf/lutaml_ext.rb
|
|
319
321
|
- lib/glossarist/rdf/namespaces.rb
|
|
@@ -395,8 +397,10 @@ files:
|
|
|
395
397
|
- lib/glossarist/v3/localized_concept.rb
|
|
396
398
|
- lib/glossarist/v3/managed_concept.rb
|
|
397
399
|
- lib/glossarist/v3/managed_concept_data.rb
|
|
398
|
-
- lib/glossarist/v3/
|
|
400
|
+
- lib/glossarist/v3/partitive_member.rb
|
|
401
|
+
- lib/glossarist/v3/partitive_relation.rb
|
|
399
402
|
- lib/glossarist/v3/related_concept.rb
|
|
403
|
+
- lib/glossarist/v3/type_shared_plurality.rb
|
|
400
404
|
- lib/glossarist/validation.rb
|
|
401
405
|
- lib/glossarist/validation/asset_index.rb
|
|
402
406
|
- lib/glossarist/validation/bibliography_index.rb
|
|
@@ -438,7 +442,7 @@ files:
|
|
|
438
442
|
- lib/glossarist/validation/rules/orphaned_bibliography_rule.rb
|
|
439
443
|
- lib/glossarist/validation/rules/orphaned_images_rule.rb
|
|
440
444
|
- lib/glossarist/validation/rules/orphaned_l10n_files_rule.rb
|
|
441
|
-
- lib/glossarist/validation/rules/
|
|
445
|
+
- lib/glossarist/validation/rules/partitive_relation_rule.rb
|
|
442
446
|
- lib/glossarist/validation/rules/preferred_term_rule.rb
|
|
443
447
|
- lib/glossarist/validation/rules/ref_shape_rule.rb
|
|
444
448
|
- lib/glossarist/validation/rules/registry.rb
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "lutaml/model"
|
|
4
|
-
|
|
5
|
-
module Glossarist
|
|
6
|
-
module Rdf
|
|
7
|
-
# RDF view for a PartitiveHyperedge (concept-model v3.1.0).
|
|
8
|
-
#
|
|
9
|
-
# Emits one `gloss:PartitiveHyperedge` subject per hyperedge with:
|
|
10
|
-
#
|
|
11
|
-
# - gloss:comprehensive — IRI of the comprehensive concept
|
|
12
|
-
# - gloss:part — IRI of each part concept (zero or more)
|
|
13
|
-
# - gloss:enumeration — xsd:string ("closed" | "open")
|
|
14
|
-
# - gloss:hasPluralityMarker — xsd:string per marker ("double" |
|
|
15
|
-
# "dashed"). One triple per marker.
|
|
16
|
-
# - gloss:hyperedgeContent — xsd:string (omitted if absent)
|
|
17
|
-
#
|
|
18
|
-
# The hyperedge is anchored to the concept that carries it via
|
|
19
|
-
# `gloss:hasHyperedge` (an inverse of `gloss:comprehensive`). The
|
|
20
|
-
# subject URI is derived from the carrying concept's identifier
|
|
21
|
-
# and the comprehensive's id, so it is stable across builds.
|
|
22
|
-
class GlossHyperedge < Lutaml::Model::Serializable
|
|
23
|
-
attribute :identifier, :string
|
|
24
|
-
attribute :comprehensive_id, :string
|
|
25
|
-
attribute :comprehensive_uri, :string
|
|
26
|
-
attribute :part_uris, :string, collection: true
|
|
27
|
-
attribute :enumeration, :string
|
|
28
|
-
attribute :markers, :string, collection: true
|
|
29
|
-
attribute :content, :hash
|
|
30
|
-
|
|
31
|
-
rdf do
|
|
32
|
-
namespace Namespaces::GlossaristNamespace
|
|
33
|
-
|
|
34
|
-
subject { |h| "hyperedge/#{h.identifier}/#{h.comprehensive_id}" }
|
|
35
|
-
|
|
36
|
-
types "gloss:PartitiveHyperedge"
|
|
37
|
-
|
|
38
|
-
predicate :comprehensive, namespace: Namespaces::GlossaristNamespace,
|
|
39
|
-
to: :comprehensive_uri
|
|
40
|
-
predicate :part, namespace: Namespaces::GlossaristNamespace,
|
|
41
|
-
to: :part_uris
|
|
42
|
-
predicate :enumeration, namespace: Namespaces::GlossaristNamespace,
|
|
43
|
-
to: :enumeration
|
|
44
|
-
predicate :hasPluralityMarker,
|
|
45
|
-
namespace: Namespaces::GlossaristNamespace,
|
|
46
|
-
to: :markers
|
|
47
|
-
predicate :hyperedgeContent,
|
|
48
|
-
namespace: Namespaces::GlossaristNamespace,
|
|
49
|
-
to: :content
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Glossarist
|
|
4
|
-
module V3
|
|
5
|
-
# PartitiveHyperedge — a one-to-many partitive decomposition.
|
|
6
|
-
#
|
|
7
|
-
# One comprehensive concept (the whole) is related to one or more
|
|
8
|
-
# parts as a SINGLE relationship. Captures invariants that binary
|
|
9
|
-
# RelatedConcept edges cannot:
|
|
10
|
-
#
|
|
11
|
-
# - which comprehensive owns which parts (set membership)
|
|
12
|
-
# - plurality markers from the source diagram (double / dashed)
|
|
13
|
-
# - enumeration completeness (closed: all parts listed; open:
|
|
14
|
-
# other parts may exist)
|
|
15
|
-
#
|
|
16
|
-
# Enum values are SSOT-loaded from `config.yml` via
|
|
17
|
-
# `GlossaryDefinition`. The `values:` option on each attribute
|
|
18
|
-
# documents the enum for schema generation. lutaml-model 0.8.x
|
|
19
|
-
# does NOT enforce `values:` on assignment — semantic checks live
|
|
20
|
-
# in `Validation::Rules::PartitiveHyperedgeRule` (auto-registered
|
|
21
|
-
# via `Validation::Rules`). The model's `validate!` method covers
|
|
22
|
-
# type-shape invariants that fail loudly at construction (empty
|
|
23
|
-
# comprehensive, empty parts, self-loop, invalid enum values).
|
|
24
|
-
#
|
|
25
|
-
# Note on duplicates: lutaml-model's enum-collection setter
|
|
26
|
-
# dedupes silently via the getter, so duplicate markers at
|
|
27
|
-
# construction time are silently coerced to a unique set. We do
|
|
28
|
-
# not attempt to override this; the framework's Set semantics are
|
|
29
|
-
# the data contract.
|
|
30
|
-
#
|
|
31
|
-
# `content` is a localized string hash `{ "eng" => "...", "fra" => "..." }`
|
|
32
|
-
# keyed by ISO 639 language code. All free-form text fields in
|
|
33
|
-
# Glossarist MUST be localized — the project is multi-language by
|
|
34
|
-
# domain. Read values via the `LocalizedString` helper module:
|
|
35
|
-
#
|
|
36
|
-
# Glossarist::LocalizedString.fetch(he.content, "eng")
|
|
37
|
-
#
|
|
38
|
-
# Matches the pattern used by `Section#names`, `DatasetRegister#description`,
|
|
39
|
-
# `Formula#expression`, `Table#content`. The concept-model schema and
|
|
40
|
-
# `RelatedConcept#content` still declare `type: string` today — those
|
|
41
|
-
# are bugs to fix, not patterns to copy.
|
|
42
|
-
class PartitiveHyperedge < Lutaml::Model::Serializable
|
|
43
|
-
DEFAULT_ENUMERATION = "closed"
|
|
44
|
-
|
|
45
|
-
attribute :comprehensive, ConceptRef
|
|
46
|
-
attribute :parts, ConceptRef, collection: true
|
|
47
|
-
attribute :enumeration, :string,
|
|
48
|
-
values: Glossarist::GlossaryDefinition::PARTITIVE_ENUMERATION_VALUES,
|
|
49
|
-
default: -> { DEFAULT_ENUMERATION }
|
|
50
|
-
attribute :markers, :string,
|
|
51
|
-
values: Glossarist::GlossaryDefinition::PLURALITY_MARKER_VALUES,
|
|
52
|
-
collection: true
|
|
53
|
-
attribute :content, :hash
|
|
54
|
-
|
|
55
|
-
key_value do
|
|
56
|
-
map :comprehensive, to: :comprehensive
|
|
57
|
-
map :parts, to: :parts
|
|
58
|
-
map :enumeration, to: :enumeration
|
|
59
|
-
map :markers, to: :markers
|
|
60
|
-
map :content, to: :content
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# Type-shape validators — invoke via `validate!` after
|
|
64
|
-
# construction. We do NOT call these from `initialize` because
|
|
65
|
-
# lutaml-model deserializes attributes AFTER `new` returns (see
|
|
66
|
-
# `Lutaml::KeyValue::Transform#data_to_model`); validating in
|
|
67
|
-
# `initialize` sees a half-built instance and raises spuriously.
|
|
68
|
-
|
|
69
|
-
def validate!
|
|
70
|
-
validate_comprehensive!
|
|
71
|
-
validate_parts!
|
|
72
|
-
validate_self_loop!
|
|
73
|
-
validate_markers!
|
|
74
|
-
validate_enumeration!
|
|
75
|
-
self
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
private
|
|
79
|
-
|
|
80
|
-
def validate_comprehensive!
|
|
81
|
-
return if comprehensive.is_a?(ConceptRef) &&
|
|
82
|
-
(comprehensive.source || comprehensive.id)
|
|
83
|
-
|
|
84
|
-
raise ArgumentError,
|
|
85
|
-
"PartitiveHyperedge#comprehensive must be a non-empty " \
|
|
86
|
-
"ConceptRef (source or id required)"
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def validate_parts!
|
|
90
|
-
return if Array(parts).any?
|
|
91
|
-
|
|
92
|
-
raise ArgumentError, "PartitiveHyperedge requires at least one part"
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def validate_self_loop!
|
|
96
|
-
return unless comprehensive.is_a?(ConceptRef)
|
|
97
|
-
|
|
98
|
-
comp_key = [comprehensive.source, comprehensive.id]
|
|
99
|
-
parts.each do |p|
|
|
100
|
-
next unless p.is_a?(ConceptRef)
|
|
101
|
-
next unless [p.source, p.id] == comp_key
|
|
102
|
-
|
|
103
|
-
raise ArgumentError,
|
|
104
|
-
"PartitiveHyperedge#parts cannot include the comprehensive"
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
def validate_markers!
|
|
109
|
-
Array(markers).each do |m|
|
|
110
|
-
unless Glossarist::GlossaryDefinition::PLURALITY_MARKER_VALUES.include?(m)
|
|
111
|
-
raise ArgumentError,
|
|
112
|
-
"PartitiveHyperedge#markers contains invalid value #{m.inspect}; " \
|
|
113
|
-
"must be one of " \
|
|
114
|
-
"#{GlossaryDefinition::PLURALITY_MARKER_VALUES.join(', ')}"
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
def validate_enumeration!
|
|
120
|
-
return if enumeration.nil?
|
|
121
|
-
|
|
122
|
-
unless Glossarist::GlossaryDefinition::PARTITIVE_ENUMERATION_VALUES
|
|
123
|
-
.include?(enumeration)
|
|
124
|
-
raise ArgumentError,
|
|
125
|
-
"PartitiveHyperedge#enumeration has invalid value " \
|
|
126
|
-
"#{enumeration.inspect}; must be one of " \
|
|
127
|
-
"#{GlossaryDefinition::PARTITIVE_ENUMERATION_VALUES.join(', ')}"
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
end
|
|
131
|
-
end
|
|
132
|
-
end
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Glossarist
|
|
4
|
-
module Validation
|
|
5
|
-
module Rules
|
|
6
|
-
# Validates semantic invariants of PartitiveHyperedge entries that
|
|
7
|
-
# the model constructor does NOT enforce. Specifically:
|
|
8
|
-
#
|
|
9
|
-
# - warning when `enumeration` was filled by lutaml-model's
|
|
10
|
-
# default proc (i.e. the source YAML omitted it) — author is
|
|
11
|
-
# *encouraged* to set explicitly per the design doc
|
|
12
|
-
# - error when markers contain values not in
|
|
13
|
-
# PLURALITY_MARKER_VALUES
|
|
14
|
-
#
|
|
15
|
-
# The model constructor already rejects empty comprehensive,
|
|
16
|
-
# empty parts, self-loops, and (lutaml-model auto-dedupes
|
|
17
|
-
# duplicate markers on enum-collection assignment, so no
|
|
18
|
-
# explicit check is needed).
|
|
19
|
-
class PartitiveHyperedgeRule < Base
|
|
20
|
-
def code = "GLS-220"
|
|
21
|
-
def category = :schema
|
|
22
|
-
def severity = "error"
|
|
23
|
-
def scope = :concept
|
|
24
|
-
|
|
25
|
-
def applicable?(context)
|
|
26
|
-
concept = context.concept
|
|
27
|
-
return false unless concept.is_a?(V3::ManagedConcept)
|
|
28
|
-
|
|
29
|
-
concept.partitive_hyperedges&.any?
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def check(context)
|
|
33
|
-
concept = context.concept
|
|
34
|
-
fname = context.file_name
|
|
35
|
-
issues = []
|
|
36
|
-
|
|
37
|
-
return issues unless concept.is_a?(V3::ManagedConcept)
|
|
38
|
-
|
|
39
|
-
concept.partitive_hyperedges.each_with_index do |he, idx|
|
|
40
|
-
check_comprehensive(he, idx, fname, issues)
|
|
41
|
-
check_parts(he, idx, fname, issues)
|
|
42
|
-
check_markers(he, idx, fname, issues)
|
|
43
|
-
check_enumeration_explicit(he, idx, fname, issues)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
issues
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
private
|
|
50
|
-
|
|
51
|
-
def check_comprehensive(he, idx, fname, issues)
|
|
52
|
-
ref = he.comprehensive
|
|
53
|
-
return if ref.is_a?(Glossarist::ConceptRef) && (ref.source || ref.id)
|
|
54
|
-
|
|
55
|
-
issues << issue(
|
|
56
|
-
"partitive_hyperedge #{idx + 1} has empty comprehensive",
|
|
57
|
-
location: fname,
|
|
58
|
-
)
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def check_parts(he, idx, fname, issues)
|
|
62
|
-
parts = Array(he.parts)
|
|
63
|
-
if parts.empty?
|
|
64
|
-
issues << issue(
|
|
65
|
-
"partitive_hyperedge #{idx + 1} has no parts",
|
|
66
|
-
location: fname,
|
|
67
|
-
)
|
|
68
|
-
return
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
parts.each_with_index do |part, pi|
|
|
72
|
-
next if part.is_a?(Glossarist::ConceptRef) && (part.source || part.id)
|
|
73
|
-
|
|
74
|
-
issues << issue(
|
|
75
|
-
"partitive_hyperedge #{idx + 1} part #{pi + 1} has empty ref",
|
|
76
|
-
location: fname,
|
|
77
|
-
)
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def check_markers(he, idx, fname, issues)
|
|
82
|
-
# `using_default?` reports whether an attribute was set by
|
|
83
|
-
# the user (false) or filled by the default proc (true).
|
|
84
|
-
# `markers` has no default, so absence yields nil/[] and
|
|
85
|
-
# `using_default?(:markers)` is true. We still walk for
|
|
86
|
-
# invalid values because `values:` is informational.
|
|
87
|
-
Array(he.markers).each_with_index do |m, mi|
|
|
88
|
-
unless Glossarist::GlossaryDefinition::PLURALITY_MARKER_VALUES.include?(m)
|
|
89
|
-
issues << issue(
|
|
90
|
-
"partitive_hyperedge #{idx + 1} marker #{mi + 1} has invalid value #{m.inspect}",
|
|
91
|
-
location: fname,
|
|
92
|
-
)
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def check_enumeration_explicit(he, idx, fname, issues)
|
|
98
|
-
# `using_default?(:enumeration)` is true iff the value was
|
|
99
|
-
# NOT explicitly set by the caller (filled by default proc).
|
|
100
|
-
return unless he.using_default?(:enumeration)
|
|
101
|
-
|
|
102
|
-
issues << issue(
|
|
103
|
-
"partitive_hyperedge #{idx + 1} has implicit enumeration (default '#{Glossarist::V3::PartitiveHyperedge::DEFAULT_ENUMERATION}')",
|
|
104
|
-
severity: "warning",
|
|
105
|
-
location: fname,
|
|
106
|
-
suggestion: "Set 'enumeration: closed' or 'enumeration: open' explicitly",
|
|
107
|
-
)
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
end
|