glossarist 2.13.4 → 2.13.5
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/lib/glossarist/rdf/gloss_partitive_member.rb +21 -4
- data/lib/glossarist/rdf/gloss_partitive_relation.rb +26 -3
- data/lib/glossarist/transforms/concept_to_gloss_transform.rb +35 -14
- data/lib/glossarist/v3/multiplicity.rb +76 -0
- data/lib/glossarist/v3/partitive_member.rb +0 -14
- data/lib/glossarist/v3.rb +7 -0
- data/lib/glossarist/validation/rules/partitive_relation_rule.rb +27 -22
- data/lib/glossarist/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f074d0113023ae82f1ae9cc8186cee518bc10ad87faabe3895117dcbbc700524
|
|
4
|
+
data.tar.gz: 8d89647b60c955419db263514521fcefcaa2efc07b4f6a16510bae2fa0313a82
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 539a902946b70e6dac712f088a73b456e0732b29740ff770194a3165624edffcc88b8169221d37d97cf357d2bb0b486acb0f9b042c716634df04fe9ba0c9d1c9
|
|
7
|
+
data.tar.gz: 3ba5d4428d42ab96861986029fe8ca919cbabefa90c42c5106b888e27e8bc74e02c4dc31413b4cb6915858ef4a261c0c4818edbe46e89e0ae01474e4b589ce85
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "digest"
|
|
3
4
|
require "lutaml/model"
|
|
4
5
|
|
|
5
6
|
module Glossarist
|
|
6
7
|
module Rdf
|
|
7
|
-
# RDF view for V3::PartitiveMember. Emits a
|
|
8
|
-
#
|
|
9
|
-
# is_delimiting
|
|
8
|
+
# RDF view for V3::PartitiveMember. Emits a gloss:PartitiveMember
|
|
9
|
+
# subject carrying the ConceptRef target plus the ISO 704:2022
|
|
10
|
+
# orthogonal dimensions (presence, count, is_delimiting).
|
|
11
|
+
#
|
|
12
|
+
# Subject identity is content-derived so the same member in the
|
|
13
|
+
# same relation produces the same URI across runs (and across
|
|
14
|
+
# processes). Two members with identical ref + dimensions share a
|
|
15
|
+
# subject — that is intentional: it exposes model-level
|
|
16
|
+
# inconsistency if the same partitive concept is given different
|
|
17
|
+
# dimensions inside the same dataset.
|
|
10
18
|
class GlossPartitiveMember < Lutaml::Model::Serializable
|
|
11
19
|
attribute :ref_id, :string
|
|
12
20
|
attribute :ref_source, :string
|
|
@@ -18,7 +26,7 @@ module Glossarist
|
|
|
18
26
|
rdf do
|
|
19
27
|
namespace Namespaces::GlossaristNamespace
|
|
20
28
|
|
|
21
|
-
subject { |m| "partitiveMember/#{
|
|
29
|
+
subject { |m| "partitiveMember/#{GlossPartitiveMember.deterministic_id(m)}" }
|
|
22
30
|
|
|
23
31
|
types "gloss:PartitiveMember"
|
|
24
32
|
|
|
@@ -35,6 +43,15 @@ module Glossarist
|
|
|
35
43
|
predicate :isDelimiting, namespace: Namespaces::GlossaristNamespace,
|
|
36
44
|
to: :is_delimiting
|
|
37
45
|
end
|
|
46
|
+
|
|
47
|
+
def self.deterministic_id(member)
|
|
48
|
+
readable = [member.ref_source, member.ref_id].compact.reject(&:empty?)
|
|
49
|
+
return readable.join(":") unless readable.empty?
|
|
50
|
+
|
|
51
|
+
parts = [member.ref_source, member.ref_id, member.ref_text,
|
|
52
|
+
member.presence, member.count, member.is_delimiting]
|
|
53
|
+
Digest::MD5.hexdigest(parts.compact.join("|"))[0..11]
|
|
54
|
+
end
|
|
38
55
|
end
|
|
39
56
|
end
|
|
40
57
|
end
|
|
@@ -1,23 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "digest"
|
|
3
4
|
require "lutaml/model"
|
|
4
5
|
|
|
5
6
|
module Glossarist
|
|
6
7
|
module Rdf
|
|
7
8
|
# RDF view for V3::PartitiveRelation. Emits a
|
|
8
|
-
# gloss:PartitiveRelation subject with comprehensive,
|
|
9
|
-
#
|
|
9
|
+
# gloss:PartitiveRelation subject with comprehensive, completeness,
|
|
10
|
+
# criterion properties, plus hasPartitive (concept URIs, for graph
|
|
11
|
+
# traversal) and hasPartitiveMember (typed subjects carrying the
|
|
12
|
+
# ISO 704:2022 per-member dimensions).
|
|
10
13
|
class GlossPartitiveRelation < Lutaml::Model::Serializable
|
|
11
14
|
attribute :identifier, :string
|
|
12
15
|
attribute :comprehensive_uri, :string
|
|
13
16
|
attribute :partitive_member_ids, :string, collection: true
|
|
17
|
+
attribute :partitive_members, GlossPartitiveMember, collection: true
|
|
14
18
|
attribute :completeness, :string
|
|
15
19
|
attribute :criterion, :hash
|
|
16
20
|
|
|
17
21
|
rdf do
|
|
18
22
|
namespace Namespaces::GlossaristNamespace
|
|
19
23
|
|
|
20
|
-
subject { |r| "partitiveRelation/#{
|
|
24
|
+
subject { |r| "partitiveRelation/#{GlossPartitiveRelation.deterministic_id(r)}" }
|
|
21
25
|
|
|
22
26
|
types "gloss:PartitiveRelation"
|
|
23
27
|
|
|
@@ -29,6 +33,25 @@ module Glossarist
|
|
|
29
33
|
to: :completeness, uri_reference: true
|
|
30
34
|
predicate :criterion, namespace: Namespaces::GlossaristNamespace,
|
|
31
35
|
to: :criterion
|
|
36
|
+
|
|
37
|
+
members :partitive_members, link: "gloss:hasPartitiveMember"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.deterministic_id(rel)
|
|
41
|
+
parts = [rel.identifier, rel.comprehensive_uri, rel.completeness]
|
|
42
|
+
parts << criterion_fingerprint(rel.criterion)
|
|
43
|
+
Array(rel.partitive_members).each do |m|
|
|
44
|
+
parts << GlossPartitiveMember.deterministic_id(m)
|
|
45
|
+
end
|
|
46
|
+
Digest::MD5.hexdigest(parts.compact.join("|"))[0..11]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.criterion_fingerprint(criterion)
|
|
50
|
+
return nil unless criterion.is_a?(Hash) && !criterion.empty?
|
|
51
|
+
|
|
52
|
+
criterion.sort_by { |k, _| k.to_s }
|
|
53
|
+
.map { |k, v| "#{k}=#{v}" }
|
|
54
|
+
.join(";")
|
|
32
55
|
end
|
|
33
56
|
end
|
|
34
57
|
end
|
|
@@ -131,7 +131,7 @@ module Glossarist
|
|
|
131
131
|
identifier),
|
|
132
132
|
dates: build_gloss_dates(managed_concept.dates, identifier),
|
|
133
133
|
partitive_relations: build_gloss_partitive_relations(
|
|
134
|
-
v3_partitive_relations(managed_concept), identifier
|
|
134
|
+
v3_partitive_relations(managed_concept), identifier
|
|
135
135
|
),
|
|
136
136
|
**rel_targets,
|
|
137
137
|
)
|
|
@@ -150,33 +150,54 @@ module Glossarist
|
|
|
150
150
|
return [] unless relations
|
|
151
151
|
|
|
152
152
|
Array(relations).map do |rel|
|
|
153
|
+
source_members = Array(rel.partitives)
|
|
154
|
+
gloss_members = source_members.map { |m| build_gloss_partitive_member(m) }
|
|
155
|
+
member_uris = source_members
|
|
156
|
+
.select { |m| m.is_a?(V3::PartitiveMember) && m.ref.is_a?(Glossarist::ConceptRef) }
|
|
157
|
+
.map { |m| concept_ref_uri(m.ref) }
|
|
158
|
+
|
|
153
159
|
Rdf::GlossPartitiveRelation.new(
|
|
154
160
|
identifier: identifier.to_s,
|
|
155
|
-
comprehensive_uri:
|
|
156
|
-
partitive_member_ids:
|
|
157
|
-
|
|
158
|
-
end,
|
|
161
|
+
comprehensive_uri: concept_ref_uri(rel.comprehensive),
|
|
162
|
+
partitive_member_ids: member_uris,
|
|
163
|
+
partitive_members: gloss_members,
|
|
159
164
|
completeness: rel.completeness,
|
|
160
165
|
criterion: rel.criterion,
|
|
161
166
|
)
|
|
162
167
|
end
|
|
163
168
|
end
|
|
164
169
|
|
|
165
|
-
def
|
|
166
|
-
return
|
|
170
|
+
def build_gloss_partitive_member(member)
|
|
171
|
+
return Rdf::GlossPartitiveMember.new unless member.is_a?(V3::PartitiveMember)
|
|
167
172
|
|
|
168
|
-
|
|
169
|
-
|
|
173
|
+
ref = member.ref
|
|
174
|
+
ref_attrs = if ref.is_a?(Glossarist::ConceptRef)
|
|
175
|
+
{ ref_id: ref.id, ref_source: ref.source, ref_text: ref.text }
|
|
176
|
+
else
|
|
177
|
+
{}
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
Rdf::GlossPartitiveMember.new(
|
|
181
|
+
**ref_attrs,
|
|
182
|
+
presence: member.presence,
|
|
183
|
+
count: member.count,
|
|
184
|
+
is_delimiting: member.is_delimiting,
|
|
185
|
+
)
|
|
170
186
|
end
|
|
171
187
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
188
|
+
# Single SSOT for ConceptRef → concept URI. Handles all four
|
|
189
|
+
# ref shapes (source+id, id-only, source+text, text-only) and
|
|
190
|
+
# mirrors GlossCitation.slug's `[source, id].compact.join("/")`
|
|
191
|
+
# convention so external ConceptRefs don't collide with local
|
|
192
|
+
# ones of the same id.
|
|
193
|
+
def concept_ref_uri(ref)
|
|
176
194
|
return nil unless ref.is_a?(Glossarist::ConceptRef)
|
|
177
195
|
|
|
196
|
+
slug = [ref.source, ref.id || ref.text].compact.reject(&:empty?)
|
|
197
|
+
return nil if slug.empty?
|
|
198
|
+
|
|
178
199
|
Glossarist::Rdf::Namespaces::GlossaristNamespace.uri +
|
|
179
|
-
"concept/#{
|
|
200
|
+
"concept/#{slug.join('/')}"
|
|
180
201
|
end
|
|
181
202
|
|
|
182
203
|
def build_gloss_localized_concept(l10n, concept_id)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glossarist
|
|
4
|
+
module V3
|
|
5
|
+
# Multiplicity — ISO 704:2022 partitive multiplicity (derived view).
|
|
6
|
+
#
|
|
7
|
+
# ISO 704:2022 multiplicity is the cross product of two orthogonal
|
|
8
|
+
# dimensions (PartitivePresence × PartitiveCount). The 5 valid
|
|
9
|
+
# combinations map to the 5 ISO 704 names:
|
|
10
|
+
#
|
|
11
|
+
# required × exactly_one → compulsory
|
|
12
|
+
# optional × exactly_one → optional
|
|
13
|
+
# required × multiple → compulsory_multiple
|
|
14
|
+
# optional × multiple → optional_multiple
|
|
15
|
+
# required × at_least_one → compulsory_at_least_one
|
|
16
|
+
#
|
|
17
|
+
# (optional × at_least_one is REDUNDANT with optional × multiple —
|
|
18
|
+
# "may exist, ≥1 if it does" = "may exist in any number" — and is
|
|
19
|
+
# explicitly rejected at construction with a clear error message.)
|
|
20
|
+
#
|
|
21
|
+
# This module is the SSOT for the (presence, count) → ISO name
|
|
22
|
+
# mapping. Both MULTIPLICITY constants and MULTIPLICITY_VALUES are
|
|
23
|
+
# derived from this single table; no duplicate string literals.
|
|
24
|
+
module Multiplicity
|
|
25
|
+
NAME_BY_PAIR = {
|
|
26
|
+
%w[required exactly_one] => "compulsory",
|
|
27
|
+
%w[optional exactly_one] => "optional",
|
|
28
|
+
%w[required multiple] => "compulsory_multiple",
|
|
29
|
+
%w[optional multiple] => "optional_multiple",
|
|
30
|
+
%w[required at_least_one] => "compulsory_at_least_one",
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
33
|
+
# Precomputed reverse map: ISO name → (presence, count) pair.
|
|
34
|
+
# Avoids NAME_BY_PAIR.find for O(1) reverse lookup. Mirrors the
|
|
35
|
+
# glossarist-js PAIR_BY_NAME so the JS and Ruby MECE derivation
|
|
36
|
+
# stay in lockstep.
|
|
37
|
+
PAIR_BY_NAME = NAME_BY_PAIR.each_with_object({}) do |(pair, name), h|
|
|
38
|
+
h[name] = pair
|
|
39
|
+
end.freeze
|
|
40
|
+
|
|
41
|
+
MULTIPLICITY = NAME_BY_PAIR.values.to_h do |name|
|
|
42
|
+
[name.upcase.to_sym, name]
|
|
43
|
+
end.freeze
|
|
44
|
+
|
|
45
|
+
MULTIPLICITY_VALUES = NAME_BY_PAIR.values.freeze
|
|
46
|
+
DEFAULT_MULTIPLICITY = MULTIPLICITY[:COMPULSORY]
|
|
47
|
+
|
|
48
|
+
def self.multiplicity_from_pair(presence, count)
|
|
49
|
+
name = NAME_BY_PAIR[[presence, count]]
|
|
50
|
+
raise invalid_combination_error(presence, count) if name.nil?
|
|
51
|
+
|
|
52
|
+
name
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.pair_from_multiplicity(name)
|
|
56
|
+
pair = PAIR_BY_NAME[name]
|
|
57
|
+
raise ArgumentError, "Unknown multiplicity: #{name.inspect}" if pair.nil?
|
|
58
|
+
|
|
59
|
+
{ presence: pair[0], count: pair[1] }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.valid_multiplicity?(name)
|
|
63
|
+
PAIR_BY_NAME.key?(name)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.invalid_combination_error(presence, count)
|
|
67
|
+
ArgumentError.new(
|
|
68
|
+
"Invalid multiplicity combination: " \
|
|
69
|
+
"presence=#{presence}, count=#{count}. " \
|
|
70
|
+
"(optional + at_least_one collapses to " \
|
|
71
|
+
"optional + multiple — use count=multiple.)",
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -74,20 +74,6 @@ module Glossarist
|
|
|
74
74
|
is_delimiting == true
|
|
75
75
|
end
|
|
76
76
|
|
|
77
|
-
# Derived ISO 704 name for display. Returns the flat-enum
|
|
78
|
-
# name that corresponds to this (presence, count) pair.
|
|
79
|
-
def iso704_name
|
|
80
|
-
case [presence, count]
|
|
81
|
-
when ["required", "exactly_one"] then "compulsory"
|
|
82
|
-
when ["optional", "exactly_one"] then "optional"
|
|
83
|
-
when ["required", "multiple"] then "compulsory_multiple"
|
|
84
|
-
when ["optional", "multiple"] then "optional_multiple"
|
|
85
|
-
when ["required", "at_least_one"] then "compulsory_at_least_one"
|
|
86
|
-
else raise ArgumentError,
|
|
87
|
-
"invalid combination presence=#{presence} count=#{count}"
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
|
|
91
77
|
private
|
|
92
78
|
|
|
93
79
|
def validate_ref!
|
data/lib/glossarist/v3.rb
CHANGED
|
@@ -11,6 +11,13 @@ module Glossarist
|
|
|
11
11
|
autoload :RelatedConcept, "glossarist/v3/related_concept"
|
|
12
12
|
autoload :PartitiveRelation, "glossarist/v3/partitive_relation"
|
|
13
13
|
autoload :PartitiveMember, "glossarist/v3/partitive_member"
|
|
14
|
+
# Multiplicity is a derived-view utility module (SSOT for the
|
|
15
|
+
# ISO 704:2022 (presence, count) → name mapping), not a Lutaml
|
|
16
|
+
# model — so it is autoloaded but NOT registered via
|
|
17
|
+
# Configuration.register_model. Used by renderers/viewers to
|
|
18
|
+
# display the ISO name; the PartitiveMember model itself only
|
|
19
|
+
# carries the orthogonal presence + count dimensions.
|
|
20
|
+
autoload :Multiplicity, "glossarist/v3/multiplicity"
|
|
14
21
|
autoload :ConceptData, "glossarist/v3/concept_data"
|
|
15
22
|
autoload :LocalizedConcept, "glossarist/v3/localized_concept"
|
|
16
23
|
autoload :ManagedConceptData, "glossarist/v3/managed_concept_data"
|
|
@@ -14,14 +14,19 @@ module Glossarist
|
|
|
14
14
|
# ISO 12620 coordinate-concept coherence)
|
|
15
15
|
# - warning when a relation has no criterion (cannot
|
|
16
16
|
# distinguish from siblings sharing the comprehensive)
|
|
17
|
-
# - warning when
|
|
18
|
-
# (
|
|
17
|
+
# - warning when a member's presence or count deviates from
|
|
18
|
+
# the defaults (required / exactly_one) — encourages an
|
|
19
|
+
# explicit choice rather than an accidental non-default
|
|
19
20
|
# - error when ExternalConcept (status: external) lacks
|
|
20
21
|
# at least one designation
|
|
21
22
|
#
|
|
22
23
|
# The model constructor already rejects empty comprehensive,
|
|
23
|
-
# empty partitives list, self-loops, invalid enum values
|
|
24
|
+
# empty partitives list, self-loops, invalid enum values, and
|
|
25
|
+
# the optional + at_least_one combination.
|
|
24
26
|
class PartitiveRelationRule < Base
|
|
27
|
+
DEFAULT_PRESENCE = "required"
|
|
28
|
+
DEFAULT_COUNT = "exactly_one"
|
|
29
|
+
|
|
25
30
|
def code = "GLS-221"
|
|
26
31
|
def category = :schema
|
|
27
32
|
def severity = "error"
|
|
@@ -46,7 +51,7 @@ module Glossarist
|
|
|
46
51
|
relations.each_with_index do |rel, idx|
|
|
47
52
|
check_cardinality(rel, idx, fname, issues)
|
|
48
53
|
check_criterion_present(rel, idx, fname, issues)
|
|
49
|
-
|
|
54
|
+
check_member_dimensions(rel, idx, fname, issues)
|
|
50
55
|
end
|
|
51
56
|
|
|
52
57
|
check_duplicate_decomposition(relations, fname, issues)
|
|
@@ -85,28 +90,28 @@ module Glossarist
|
|
|
85
90
|
)
|
|
86
91
|
end
|
|
87
92
|
|
|
88
|
-
|
|
93
|
+
# Warns once per member when presence or count deviates from
|
|
94
|
+
# the model defaults. The warning is intentionally generic —
|
|
95
|
+
# it fires for required+multiple (non-default count) just as
|
|
96
|
+
# for optional+exactly_one (non-default presence). Both are
|
|
97
|
+
# legal; the warning encourages an explicit, reviewed choice
|
|
98
|
+
# rather than an accidental non-default.
|
|
99
|
+
def check_member_dimensions(rel, idx, fname, issues)
|
|
89
100
|
rel.partitives.each_with_index do |member, mi|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
"(ISO 704 name: #{member.iso704_name}); confirm the " \
|
|
95
|
-
"optionality is intentional",
|
|
96
|
-
severity: "warning",
|
|
97
|
-
location: fname,
|
|
98
|
-
)
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
next unless member.delimiting?
|
|
101
|
+
non_default = []
|
|
102
|
+
non_default << "presence='#{member.presence}'" if member.presence != DEFAULT_PRESENCE
|
|
103
|
+
non_default << "count='#{member.count}'" if member.count != DEFAULT_COUNT
|
|
104
|
+
next if non_default.empty?
|
|
102
105
|
|
|
103
106
|
issues << issue(
|
|
104
|
-
"partitive_relation #{idx + 1}.partitives[#{mi}]
|
|
105
|
-
"
|
|
106
|
-
"
|
|
107
|
-
"
|
|
108
|
-
severity: "
|
|
107
|
+
"partitive_relation #{idx + 1}.partitives[#{mi}] uses " \
|
|
108
|
+
"non-default #{non_default.join(' ')} " \
|
|
109
|
+
"(defaults: presence=#{DEFAULT_PRESENCE}, count=#{DEFAULT_COUNT}) " \
|
|
110
|
+
"— confirm the dimensions are intentional",
|
|
111
|
+
severity: "warning",
|
|
109
112
|
location: fname,
|
|
113
|
+
suggestion: "Non-default dimensions are valid; this warning " \
|
|
114
|
+
"exists to catch accidental drift from the model defaults.",
|
|
110
115
|
)
|
|
111
116
|
end
|
|
112
117
|
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.13.
|
|
4
|
+
version: 2.13.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose
|
|
@@ -396,6 +396,7 @@ files:
|
|
|
396
396
|
- lib/glossarist/v3/localized_concept.rb
|
|
397
397
|
- lib/glossarist/v3/managed_concept.rb
|
|
398
398
|
- lib/glossarist/v3/managed_concept_data.rb
|
|
399
|
+
- lib/glossarist/v3/multiplicity.rb
|
|
399
400
|
- lib/glossarist/v3/partitive_member.rb
|
|
400
401
|
- lib/glossarist/v3/partitive_relation.rb
|
|
401
402
|
- lib/glossarist/v3/related_concept.rb
|