glossarist 2.11.3 → 2.12.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.
- checksums.yaml +4 -4
- data/CLAUDE.md +30 -0
- data/config.yml +15 -0
- data/lib/glossarist/concept_source.rb +2 -0
- data/lib/glossarist/designation/designation_relationship.rb +1 -1
- data/lib/glossarist/glossary_definition.rb +6 -0
- data/lib/glossarist/managed_concept.rb +2 -0
- data/lib/glossarist/rdf/gloss_concept.rb +3 -0
- data/lib/glossarist/rdf/gloss_concept_source.rb +5 -0
- data/lib/glossarist/rdf/gloss_hyperedge.rb +53 -0
- data/lib/glossarist/rdf/v3.rb +1 -0
- data/lib/glossarist/rdf.rb +1 -0
- data/lib/glossarist/related_concept.rb +1 -1
- data/lib/glossarist/schema_migration/v2_to_v3.rb +19 -5
- data/lib/glossarist/transforms/concept_to_gloss_transform.rb +34 -0
- data/lib/glossarist/v2/concept_source.rb +2 -0
- data/lib/glossarist/v3/concept_source.rb +2 -0
- data/lib/glossarist/v3/managed_concept.rb +2 -0
- data/lib/glossarist/v3/managed_concept_data.rb +15 -1
- data/lib/glossarist/v3/partitive_hyperedge.rb +132 -0
- data/lib/glossarist/v3.rb +2 -0
- data/lib/glossarist/validation/rules/partitive_hyperedge_rule.rb +112 -0
- data/lib/glossarist/version.rb +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4fafa61aa5261ca29216b423006cd0764470a9f8bf59733449ba3b501a63413b
|
|
4
|
+
data.tar.gz: b7c5f6148a76feca5dcbe8321e1a49bed99772a415168ddb977786d214c50110
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 163a2b94ff599127f8997de745f05ebec13ab19095790bf91ca832e7ea2c79525c65ca93a4e1cbd45ba4fcd9c4f1d4db9cb564bf370497d1e22fd5aa3e28ec73
|
|
7
|
+
data.tar.gz: 43be5caab946b3202614ec38db165f6ce72b56137ca1daa87fa0e9c12a853b3c878541c8b8a9b23589719d4239a566ddb621ddee2552e7c284ab435232c03cd1
|
data/CLAUDE.md
CHANGED
|
@@ -35,6 +35,36 @@ All model classes use `Lutaml::Model::Serializable` for serialization.
|
|
|
35
35
|
- **`ConceptReference`** (`concept_reference.rb`) — a typed reference to another concept. Local refs use `concept_id` alone; external refs use `source` (URN prefix) + `concept_id` or a direct `urn` field. Has `local?`/`external?` predicates. No `to_gcr_hash` or `from_urn` — model-driven architecture uses lutaml-model for serialization; URN construction/parsing belongs in `UrnResolver`.
|
|
36
36
|
- **`RelatedConcept`** (`related_concept.rb`) — a concept with a typed relationship. Relationship types cover ISO 10241-1 (deprecates/supersedes/superseded_by/compare/contrast/see), ISO 25964/SKOS (broader/narrower/broader_generic/narrower_generic/broader_partitive/narrower_partitive/broader_instantial/narrower_instantial/equivalent/close_match/broad_match/narrow_match/related_match), ISO 12620/TBX (homograph/false_friend/related_concept/related_concept_broader/related_concept_narrower/sequentially_related_concept/spatially_related_concept/temporally_related_concept), and designation-level (abbreviated_form_for/short_form_for). Types are defined in `config.yml` under `related_concept.type`.
|
|
37
37
|
|
|
38
|
+
#### V3 `related` placement (MECE)
|
|
39
|
+
|
|
40
|
+
In V3, `related` lives **only** on `V3::ManagedConcept#related`.
|
|
41
|
+
`V3::ManagedConceptData` does NOT declare or serialize `related`.
|
|
42
|
+
V2 placed `related` inside data; the `SchemaMigration::V2ToV3#step_v2_to_v3`
|
|
43
|
+
migration moves any data-level entries up to the concept level. The
|
|
44
|
+
V3 output never carries `related` at the data level.
|
|
45
|
+
|
|
46
|
+
This consolidation closes the legacy trap where writing to
|
|
47
|
+
`data.related` bypassed `ManagedConcept.detect_schema_version` (which
|
|
48
|
+
keys off `concept.related`).
|
|
49
|
+
|
|
50
|
+
### PartitiveHyperedge (V3 only)
|
|
51
|
+
|
|
52
|
+
`PartitiveHyperedge` (`v3/partitive_hyperedge.rb`) — a one-to-many partitive decomposition. One comprehensive concept is related to one or more parts as a SINGLE relationship. Captures invariants that binary `RelatedConcept` edges cannot:
|
|
53
|
+
|
|
54
|
+
- which comprehensive owns which parts (set membership)
|
|
55
|
+
- diagram notation flags (`PluralityMarker`: `double`, `dashed`)
|
|
56
|
+
- enumeration completeness (`PartitiveEnumeration`: `closed`, `open`)
|
|
57
|
+
|
|
58
|
+
Wired into `V3::ManagedConcept#partitive_hyperedges`. NOT on `ManagedConceptData` (relationships live at the concept level for MECE consistency with `related`).
|
|
59
|
+
|
|
60
|
+
Enum values are SSOT-loaded from `config.yml` via `GlossaryDefinition::PARTITIVE_ENUMERATION_VALUES` and `PLURALITY_MARKER_VALUES`. The `values:` option on each attribute documents the enum; the model's `initialize` override enforces them at construction (lutaml-model 0.8.17 does not enforce `values:` on assignment). Construction also rejects empty comprehensive, empty parts, self-loops, and duplicate marker values.
|
|
61
|
+
|
|
62
|
+
Semantic checks (defaulted-enumeration warning, defensive walk) live in `Validation::Rules::PartitiveHyperedgeRule` (auto-registered by `Validation::Rules`).
|
|
63
|
+
|
|
64
|
+
RDF emission: `Rdf::GlossHyperedge` view class (per-hyperedge `gloss:PartitiveHyperedge` subject with `gloss:comprehensive`, `gloss:part+`, `gloss:enumeration`, `gloss:hasPluralityMarker*`, `gloss:hyperedgeContent?`), wired into `Transforms::ConceptToGlossTransform` and emitted as `gloss:hasHyperedge` link from `GlossConcept`.
|
|
65
|
+
|
|
66
|
+
Binary `broader_partitive`/`narrower_partitive` `RelatedConcept` edges and `PartitiveHyperedge` coexist — no automatic consolidation. See `concept-model/TODO.hyperedge/00-design-overview.md`.
|
|
67
|
+
|
|
38
68
|
### Reference Resolution
|
|
39
69
|
|
|
40
70
|
- **`ReferenceExtractor`** (`reference_extractor.rb`) — extracts `ConceptReference` and `AssetReference` objects from `{{...}}` mentions and `image::...[]` references in concept text fields.
|
data/config.yml
CHANGED
|
@@ -186,3 +186,18 @@ concept_date:
|
|
|
186
186
|
- retired
|
|
187
187
|
- review
|
|
188
188
|
- review_decision
|
|
189
|
+
|
|
190
|
+
partitive_enumeration:
|
|
191
|
+
# Whether a PartitiveHyperedge's parts array constitutes the complete
|
|
192
|
+
# decomposition of the comprehensive (closed) or only a partial one
|
|
193
|
+
# (open). Default: closed.
|
|
194
|
+
value:
|
|
195
|
+
- closed
|
|
196
|
+
- open
|
|
197
|
+
|
|
198
|
+
plurality_marker:
|
|
199
|
+
# Diagram notation flags from VIM concept-relationship diagrams.
|
|
200
|
+
# Orthogonal to enumeration — both may be set on the same hyperedge.
|
|
201
|
+
value:
|
|
202
|
+
- double # close-set double line at receiver; several parts involved
|
|
203
|
+
- dashed # broken/dashed line; plurality is uncertain
|
|
@@ -7,6 +7,7 @@ module Glossarist
|
|
|
7
7
|
values: Glossarist::GlossaryDefinition::CONCEPT_SOURCE_TYPES
|
|
8
8
|
attribute :origin, Citation
|
|
9
9
|
attribute :modification, :string
|
|
10
|
+
attribute :sourced_from, Citation, collection: true
|
|
10
11
|
|
|
11
12
|
key_value do
|
|
12
13
|
map :id, to: :id
|
|
@@ -14,6 +15,7 @@ module Glossarist
|
|
|
14
15
|
map :status, to: :status
|
|
15
16
|
map :type, to: :type
|
|
16
17
|
map :modification, to: :modification
|
|
18
|
+
map :sourced_from, to: :sourced_from
|
|
17
19
|
end
|
|
18
20
|
end
|
|
19
21
|
end
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Glossarist
|
|
4
4
|
module Designation
|
|
5
5
|
class DesignationRelationship < Lutaml::Model::Serializable
|
|
6
|
-
attribute :content, :
|
|
6
|
+
attribute :content, :hash
|
|
7
7
|
attribute :type, :string,
|
|
8
8
|
values: Glossarist::GlossaryDefinition::DESIGNATION_RELATIONSHIP_TYPES
|
|
9
9
|
attribute :target, :string
|
|
@@ -33,5 +33,11 @@ module Glossarist
|
|
|
33
33
|
"relationship_type")&.freeze
|
|
34
34
|
|
|
35
35
|
ISO12620_TERM_TYPES = config.dig("iso12620", "term_type").freeze
|
|
36
|
+
|
|
37
|
+
PARTITIVE_ENUMERATION_VALUES =
|
|
38
|
+
config.dig("partitive_enumeration", "value").freeze
|
|
39
|
+
|
|
40
|
+
PLURALITY_MARKER_VALUES =
|
|
41
|
+
config.dig("plurality_marker", "value").freeze
|
|
36
42
|
end
|
|
37
43
|
end
|
|
@@ -174,6 +174,8 @@ module Glossarist
|
|
|
174
174
|
return "3" if concept.related&.any?
|
|
175
175
|
return "3" if concept.sources&.any?
|
|
176
176
|
return "3" if concept.data&.domains&.any?
|
|
177
|
+
return "3" if concept.is_a?(V3::ManagedConcept) &&
|
|
178
|
+
concept.partitive_hyperedges&.any?
|
|
177
179
|
return "3" if localization_has_references?(concept)
|
|
178
180
|
|
|
179
181
|
"2"
|
|
@@ -11,6 +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 :hyperedges, GlossHyperedge, collection: true
|
|
14
15
|
|
|
15
16
|
RelationshipPredicates::CONCEPT_REL_PREDICATES.each_key do |type|
|
|
16
17
|
attribute :"#{type}_targets", :string, collection: true
|
|
@@ -41,6 +42,8 @@ module Glossarist
|
|
|
41
42
|
link: "gloss:hasDomain"
|
|
42
43
|
members :dates,
|
|
43
44
|
link: "gloss:hasDate"
|
|
45
|
+
members :hyperedges,
|
|
46
|
+
link: "gloss:hasPartitiveHyperedge"
|
|
44
47
|
|
|
45
48
|
RelationshipPredicates::CONCEPT_REL_PREDICATES.each do |type, (ns, name)|
|
|
46
49
|
predicate name, namespace: ns, to: :"#{type}_targets",
|
|
@@ -9,6 +9,7 @@ module Glossarist
|
|
|
9
9
|
attribute :type, :string
|
|
10
10
|
attribute :modification, :string
|
|
11
11
|
attribute :origin, GlossCitation
|
|
12
|
+
attribute :sourced_from, GlossCitation, collection: true
|
|
12
13
|
|
|
13
14
|
rdf do
|
|
14
15
|
namespace Namespaces::GlossaristNamespace
|
|
@@ -25,6 +26,7 @@ module Glossarist
|
|
|
25
26
|
to: :modification
|
|
26
27
|
|
|
27
28
|
members :origin, link: "gloss:sourceOrigin"
|
|
29
|
+
members :sourced_from, link: "gloss:sourcedFrom"
|
|
28
30
|
end
|
|
29
31
|
|
|
30
32
|
def self.deterministic_id(source)
|
|
@@ -33,6 +35,9 @@ module Glossarist
|
|
|
33
35
|
if origin
|
|
34
36
|
parts << origin.source << origin.id << origin.version << origin.link
|
|
35
37
|
end
|
|
38
|
+
Array(source.sourced_from).each do |sf|
|
|
39
|
+
parts << sf&.source << sf&.id << sf&.version << sf&.link
|
|
40
|
+
end
|
|
36
41
|
Digest::MD5.hexdigest(parts.compact.join("|"))[0..11]
|
|
37
42
|
end
|
|
38
43
|
end
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
data/lib/glossarist/rdf/v3.rb
CHANGED
data/lib/glossarist/rdf.rb
CHANGED
|
@@ -42,6 +42,7 @@ 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 :GlossHyperedge, "#{__dir__}/rdf/gloss_hyperedge"
|
|
45
46
|
autoload :V3, "#{__dir__}/rdf/v3"
|
|
46
47
|
end
|
|
47
48
|
end
|
|
@@ -38,11 +38,25 @@ module Glossarist
|
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
def self.step_v2_to_v3(concept)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
# V2 placed `related` inside ManagedConceptData; V3 places it
|
|
42
|
+
# on ManagedConcept itself. Move any data-level related entries
|
|
43
|
+
# up to the concept level. V3::ManagedConceptData no longer
|
|
44
|
+
# serializes `related`, so we don't need to clear the moved-from
|
|
45
|
+
# slot — the V3 output naturally omits it.
|
|
46
|
+
data_related = concept.data&.related
|
|
47
|
+
return step_v2_to_v3_hyperedge_note if data_related.nil? || data_related.empty?
|
|
48
|
+
|
|
49
|
+
concept.related ||= []
|
|
50
|
+
concept.related = (concept.related + data_related).uniq
|
|
51
|
+
"3"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Hyperedges: V2 has no hyperedge concept. V2's binary
|
|
55
|
+
# `broader_partitive` / `narrower_partitive` edges migrate as
|
|
56
|
+
# `related` entries (see step_v2_to_v3). Hyperedges are a V3-only
|
|
57
|
+
# construct and require no migration logic — concepts upgraded
|
|
58
|
+
# from V2 to V3 simply have no hyperedges.
|
|
59
|
+
def self.step_v2_to_v3_hyperedge_note
|
|
46
60
|
"3"
|
|
47
61
|
end
|
|
48
62
|
end
|
|
@@ -130,10 +130,44 @@ 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
|
+
hyperedges: build_gloss_hyperedges(v3_hyperedges(managed_concept),
|
|
134
|
+
identifier),
|
|
133
135
|
**rel_targets,
|
|
134
136
|
)
|
|
135
137
|
end
|
|
136
138
|
|
|
139
|
+
# Hyperedges are a V3-only attribute. The transform accepts both
|
|
140
|
+
# V1/V2 and V3 concepts; for V1/V2 concepts the hyperedge list
|
|
141
|
+
# is always empty.
|
|
142
|
+
def v3_hyperedges(managed_concept)
|
|
143
|
+
return [] unless managed_concept.is_a?(V3::ManagedConcept)
|
|
144
|
+
|
|
145
|
+
managed_concept.partitive_hyperedges
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def build_gloss_hyperedges(hyperedges, identifier)
|
|
149
|
+
return [] unless hyperedges
|
|
150
|
+
|
|
151
|
+
Array(hyperedges).map do |he|
|
|
152
|
+
Rdf::GlossHyperedge.new(
|
|
153
|
+
identifier: identifier.to_s,
|
|
154
|
+
comprehensive_id: he.comprehensive.id.to_s,
|
|
155
|
+
comprehensive_uri: hyperedge_concept_uri(he.comprehensive),
|
|
156
|
+
part_uris: Array(he.parts).map { |p| hyperedge_concept_uri(p) },
|
|
157
|
+
enumeration: he.enumeration,
|
|
158
|
+
markers: Array(he.markers),
|
|
159
|
+
content: he.content,
|
|
160
|
+
)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def hyperedge_concept_uri(ref)
|
|
165
|
+
return nil unless ref.is_a?(Glossarist::ConceptRef)
|
|
166
|
+
|
|
167
|
+
Glossarist::Rdf::Namespaces::GlossaristNamespace.uri +
|
|
168
|
+
"concept/#{ref.id}"
|
|
169
|
+
end
|
|
170
|
+
|
|
137
171
|
def build_gloss_localized_concept(l10n, concept_id)
|
|
138
172
|
lang = l10n.language_code
|
|
139
173
|
data = l10n.data
|
|
@@ -4,12 +4,14 @@ module Glossarist
|
|
|
4
4
|
module V2
|
|
5
5
|
class ConceptSource < Glossarist::ConceptSource
|
|
6
6
|
attribute :origin, V2::Citation
|
|
7
|
+
attribute :sourced_from, V2::Citation, collection: true
|
|
7
8
|
|
|
8
9
|
key_value do
|
|
9
10
|
map :origin, to: :origin
|
|
10
11
|
map :status, to: :status
|
|
11
12
|
map :type, to: :type
|
|
12
13
|
map :modification, to: :modification
|
|
14
|
+
map :sourced_from, to: :sourced_from
|
|
13
15
|
end
|
|
14
16
|
end
|
|
15
17
|
end
|
|
@@ -4,12 +4,14 @@ module Glossarist
|
|
|
4
4
|
module V3
|
|
5
5
|
class ConceptSource < Glossarist::ConceptSource
|
|
6
6
|
attribute :origin, V3::Citation
|
|
7
|
+
attribute :sourced_from, V3::Citation, collection: true
|
|
7
8
|
|
|
8
9
|
key_value do
|
|
9
10
|
map :origin, to: :origin
|
|
10
11
|
map :status, to: :status
|
|
11
12
|
map :type, to: :type
|
|
12
13
|
map :modification, to: :modification
|
|
14
|
+
map :sourced_from, to: :sourced_from
|
|
13
15
|
end
|
|
14
16
|
end
|
|
15
17
|
end
|
|
@@ -5,6 +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 :partitive_hyperedges, V3::PartitiveHyperedge, collection: true
|
|
8
9
|
attribute :dates, V3::ConceptDate, collection: true
|
|
9
10
|
attribute :date_accepted, V3::ConceptDate
|
|
10
11
|
attribute :sources, V3::ConceptSource, collection: true
|
|
@@ -12,6 +13,7 @@ module Glossarist
|
|
|
12
13
|
key_value do
|
|
13
14
|
map :data, to: :data
|
|
14
15
|
map :related, to: :related
|
|
16
|
+
map :partitive_hyperedges, to: :partitive_hyperedges
|
|
15
17
|
map :dates, to: :dates
|
|
16
18
|
map %i[date_accepted dateAccepted],
|
|
17
19
|
with: { from: :date_accepted_from_yaml, to: :date_accepted_to_yaml }
|
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
module Glossarist
|
|
4
4
|
module V3
|
|
5
|
+
# V3 ManagedConceptData — the data payload inside a V3::ManagedConcept.
|
|
6
|
+
#
|
|
7
|
+
# V3 placement rule (MECE): `related` lives ONLY on V3::ManagedConcept,
|
|
8
|
+
# not on its data payload. V2 placed `related` inside data; the
|
|
9
|
+
# V2→V3 migration (SchemaMigration::V2ToV3) moves it to the concept
|
|
10
|
+
# level. Keeping `related` writable on V3 data would re-open the
|
|
11
|
+
# trap where writing to `data.related` bypasses
|
|
12
|
+
# `ManagedConcept.detect_schema_version` (which keys off
|
|
13
|
+
# `concept.related`).
|
|
14
|
+
#
|
|
15
|
+
# The base class Glossarist::ManagedConceptData still declares
|
|
16
|
+
# `related` for V1/V2 compatibility; V3 overrides that with an
|
|
17
|
+
# empty attribute via `attribute :related, nil` so the slot is
|
|
18
|
+
# inert and serializes nothing.
|
|
5
19
|
class ManagedConceptData < Glossarist::ManagedConceptData
|
|
6
20
|
attribute :sources, V3::ConceptSource, collection: true
|
|
7
21
|
attribute :localizations, V3::LocalizedConcept,
|
|
@@ -17,7 +31,6 @@ module Glossarist
|
|
|
17
31
|
with: { from: :domains_from_yaml, to: :domains_to_yaml }
|
|
18
32
|
map :tags, to: :tags
|
|
19
33
|
map :sources, to: :sources
|
|
20
|
-
map :related, to: :related
|
|
21
34
|
map :localizations, to: :localizations,
|
|
22
35
|
with: { from: :localizations_from_yaml, to: :localizations_to_yaml }
|
|
23
36
|
end
|
|
@@ -34,3 +47,4 @@ module Glossarist
|
|
|
34
47
|
end
|
|
35
48
|
end
|
|
36
49
|
end
|
|
50
|
+
|
|
@@ -0,0 +1,132 @@
|
|
|
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
|
data/lib/glossarist/v3.rb
CHANGED
|
@@ -9,6 +9,7 @@ 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 :PartitiveHyperedge, "glossarist/v3/partitive_hyperedge"
|
|
12
13
|
autoload :ConceptData, "glossarist/v3/concept_data"
|
|
13
14
|
autoload :LocalizedConcept, "glossarist/v3/localized_concept"
|
|
14
15
|
autoload :ManagedConceptData, "glossarist/v3/managed_concept_data"
|
|
@@ -23,6 +24,7 @@ module Glossarist
|
|
|
23
24
|
Configuration.register_model(LocalizedConcept, id: :localized_concept)
|
|
24
25
|
Configuration.register_model(ConceptRef, id: :concept_ref)
|
|
25
26
|
Configuration.register_model(RelatedConcept, id: :related_concept)
|
|
27
|
+
Configuration.register_model(PartitiveHyperedge, id: :partitive_hyperedge)
|
|
26
28
|
Configuration.register_model(ManagedConceptData, id: :managed_concept_data)
|
|
27
29
|
Configuration.register_model(ManagedConcept, id: :managed_concept)
|
|
28
30
|
Configuration.register_model(ConceptDocument, id: :concept_document)
|
|
@@ -0,0 +1,112 @@
|
|
|
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
|
data/lib/glossarist/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: glossarist
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.12.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-07-
|
|
11
|
+
date: 2026-07-23 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: lutaml-model
|
|
@@ -307,6 +307,7 @@ 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
|
|
310
311
|
- lib/glossarist/rdf/gloss_locality.rb
|
|
311
312
|
- lib/glossarist/rdf/gloss_localized_concept.rb
|
|
312
313
|
- lib/glossarist/rdf/gloss_non_verbal_rep.rb
|
|
@@ -394,6 +395,7 @@ files:
|
|
|
394
395
|
- lib/glossarist/v3/localized_concept.rb
|
|
395
396
|
- lib/glossarist/v3/managed_concept.rb
|
|
396
397
|
- lib/glossarist/v3/managed_concept_data.rb
|
|
398
|
+
- lib/glossarist/v3/partitive_hyperedge.rb
|
|
397
399
|
- lib/glossarist/v3/related_concept.rb
|
|
398
400
|
- lib/glossarist/validation.rb
|
|
399
401
|
- lib/glossarist/validation/asset_index.rb
|
|
@@ -436,6 +438,7 @@ files:
|
|
|
436
438
|
- lib/glossarist/validation/rules/orphaned_bibliography_rule.rb
|
|
437
439
|
- lib/glossarist/validation/rules/orphaned_images_rule.rb
|
|
438
440
|
- lib/glossarist/validation/rules/orphaned_l10n_files_rule.rb
|
|
441
|
+
- lib/glossarist/validation/rules/partitive_hyperedge_rule.rb
|
|
439
442
|
- lib/glossarist/validation/rules/preferred_term_rule.rb
|
|
440
443
|
- lib/glossarist/validation/rules/ref_shape_rule.rb
|
|
441
444
|
- lib/glossarist/validation/rules/registry.rb
|