glossarist 2.13.3 → 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/config.yml +21 -13
- data/lib/glossarist/glossary_definition.rb +5 -1
- data/lib/glossarist/rdf/gloss_partitive_member.rb +27 -7
- 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 +62 -31
- data/lib/glossarist/v3/partitive_relation.rb +3 -4
- 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 +3 -2
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
|
data/config.yml
CHANGED
|
@@ -212,17 +212,25 @@ completeness:
|
|
|
212
212
|
- partial
|
|
213
213
|
|
|
214
214
|
multiplicity:
|
|
215
|
-
# ISO 704:2022 partitive member multiplicity.
|
|
216
|
-
#
|
|
217
|
-
#
|
|
218
|
-
#
|
|
219
|
-
#
|
|
220
|
-
#
|
|
221
|
-
#
|
|
222
|
-
#
|
|
223
|
-
|
|
224
|
-
|
|
215
|
+
# MECE decomposition of ISO 704:2022 partitive member multiplicity.
|
|
216
|
+
# Two orthogonal dimensions replace the prior flat 5-value enum.
|
|
217
|
+
#
|
|
218
|
+
# presence (line style):
|
|
219
|
+
# required — solid line (must exist)
|
|
220
|
+
# optional — dashed line (may exist)
|
|
221
|
+
#
|
|
222
|
+
# count (line count):
|
|
223
|
+
# exactly_one — 1 line (exactly one instance)
|
|
224
|
+
# at_least_one — 1 solid + 1 dashed (at least one instance)
|
|
225
|
+
# multiple — 2 lines (multiple instances)
|
|
226
|
+
#
|
|
227
|
+
# Valid combinations (5): required+exactly_one, optional+exactly_one,
|
|
228
|
+
# required+multiple, optional+multiple, required+at_least_one.
|
|
229
|
+
# Invalid: optional+at_least_one (collapses to optional+multiple).
|
|
230
|
+
presence:
|
|
231
|
+
- required
|
|
225
232
|
- optional
|
|
226
|
-
|
|
227
|
-
-
|
|
228
|
-
-
|
|
233
|
+
count:
|
|
234
|
+
- exactly_one
|
|
235
|
+
- at_least_one
|
|
236
|
+
- multiple
|
|
@@ -37,6 +37,10 @@ module Glossarist
|
|
|
37
37
|
# PartitiveRelation redesign (TODO.partitive-relation-v2).
|
|
38
38
|
COMPLETENESS_VALUES = config.dig("completeness", "value").freeze
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
PARTITIVE_PRESENCE_VALUES =
|
|
41
|
+
config.dig("multiplicity", "presence").freeze
|
|
42
|
+
|
|
43
|
+
PARTITIVE_COUNT_VALUES =
|
|
44
|
+
config.dig("multiplicity", "count").freeze
|
|
41
45
|
end
|
|
42
46
|
end
|
|
@@ -1,23 +1,32 @@
|
|
|
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
|
|
13
21
|
attribute :ref_text, :string
|
|
14
|
-
attribute :
|
|
22
|
+
attribute :presence, :string
|
|
23
|
+
attribute :count, :string
|
|
15
24
|
attribute :is_delimiting, :boolean
|
|
16
25
|
|
|
17
26
|
rdf do
|
|
18
27
|
namespace Namespaces::GlossaristNamespace
|
|
19
28
|
|
|
20
|
-
subject { |m| "partitiveMember/#{
|
|
29
|
+
subject { |m| "partitiveMember/#{GlossPartitiveMember.deterministic_id(m)}" }
|
|
21
30
|
|
|
22
31
|
types "gloss:PartitiveMember"
|
|
23
32
|
|
|
@@ -27,11 +36,22 @@ module Glossarist
|
|
|
27
36
|
to: :ref_id
|
|
28
37
|
predicate :refText, namespace: Namespaces::GlossaristNamespace,
|
|
29
38
|
to: :ref_text
|
|
30
|
-
predicate :
|
|
31
|
-
|
|
39
|
+
predicate :presence, namespace: Namespaces::GlossaristNamespace,
|
|
40
|
+
to: :presence
|
|
41
|
+
predicate :count, namespace: Namespaces::GlossaristNamespace,
|
|
42
|
+
to: :count
|
|
32
43
|
predicate :isDelimiting, namespace: Namespaces::GlossaristNamespace,
|
|
33
44
|
to: :is_delimiting
|
|
34
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
|
|
35
55
|
end
|
|
36
56
|
end
|
|
37
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
|
|
@@ -6,50 +6,68 @@ module Glossarist
|
|
|
6
6
|
# a ConceptRef to the partitive concept plus ISO 704:2022
|
|
7
7
|
# multiplicity and delimiting metadata.
|
|
8
8
|
#
|
|
9
|
-
# ISO 704:2022 partitive member notation
|
|
9
|
+
# ISO 704:2022 partitive member notation uses two orthogonal
|
|
10
|
+
# dimensions (MECE decomposition):
|
|
10
11
|
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
# optional
|
|
14
|
-
# compulsory_multiple — 2 solid lines (multiple must exist)
|
|
15
|
-
# optional_multiple — 2 dashed lines (multiple may exist)
|
|
16
|
-
# compulsory_at_least_one — 1 solid + 1 dashed line (≥1 must exist)
|
|
12
|
+
# presence (line style — solid vs dashed):
|
|
13
|
+
# required — solid line (must exist in every instance)
|
|
14
|
+
# optional — dashed line (may exist; exists in some instances)
|
|
17
15
|
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
16
|
+
# count (line count — how many):
|
|
17
|
+
# exactly_one — 1 line
|
|
18
|
+
# at_least_one — 1 solid + 1 dashed
|
|
19
|
+
# multiple — 2 lines
|
|
20
|
+
#
|
|
21
|
+
# Valid combinations (5):
|
|
22
|
+
# required + exactly_one = compulsory (1 solid)
|
|
23
|
+
# optional + exactly_one = optional (1 dashed)
|
|
24
|
+
# required + multiple = compulsory_multiple (2 solid)
|
|
25
|
+
# optional + multiple = optional_multiple (2 dashed)
|
|
26
|
+
# required + at_least_one = compulsory_at_least_one (1 solid + 1 dashed)
|
|
27
|
+
#
|
|
28
|
+
# Invalid: optional + at_least_one (collapses to optional + multiple
|
|
29
|
+
# because "at least one, if present" = "zero or more" = optional_multiple).
|
|
30
|
+
#
|
|
31
|
+
# is_delimiting (orthogonal, bold 3x-width line in diagram):
|
|
32
|
+
# A delimiting part behaves like a delimiting characteristic:
|
|
33
|
+
# it distinguishes the comprehensive from coordinate concepts.
|
|
34
|
+
# Example (ISO 704 §5.5.4.2.2): for "optomechanical mouse",
|
|
35
|
+
# the delimiting parts are mouse ball, x/y-axis rollers,
|
|
36
|
+
# infrared emitter/sensor — they distinguish it from
|
|
37
|
+
# "mechanical mouse" and "optical mouse". Mouse button is
|
|
38
|
+
# NOT delimiting (all computer mice have buttons).
|
|
26
39
|
class PartitiveMember < Lutaml::Model::Serializable
|
|
27
|
-
|
|
40
|
+
DEFAULT_PRESENCE = "required"
|
|
41
|
+
DEFAULT_COUNT = "exactly_one"
|
|
28
42
|
|
|
29
43
|
attribute :ref, ConceptRef
|
|
30
|
-
attribute :
|
|
31
|
-
values: Glossarist::GlossaryDefinition::
|
|
32
|
-
default: -> {
|
|
44
|
+
attribute :presence, :string,
|
|
45
|
+
values: Glossarist::GlossaryDefinition::PARTITIVE_PRESENCE_VALUES,
|
|
46
|
+
default: -> { DEFAULT_PRESENCE }
|
|
47
|
+
attribute :count, :string,
|
|
48
|
+
values: Glossarist::GlossaryDefinition::PARTITIVE_COUNT_VALUES,
|
|
49
|
+
default: -> { DEFAULT_COUNT }
|
|
33
50
|
attribute :is_delimiting, :boolean, default: -> { false }
|
|
34
51
|
|
|
35
52
|
key_value do
|
|
36
53
|
map :ref, to: :ref
|
|
37
|
-
map :
|
|
54
|
+
map :presence, to: :presence
|
|
55
|
+
map :count, to: :count
|
|
38
56
|
map :is_delimiting, to: :is_delimiting
|
|
39
57
|
end
|
|
40
58
|
|
|
41
59
|
def validate!
|
|
42
60
|
validate_ref!
|
|
43
|
-
|
|
61
|
+
validate_presence_count!
|
|
44
62
|
self
|
|
45
63
|
end
|
|
46
64
|
|
|
47
|
-
def
|
|
48
|
-
|
|
65
|
+
def required?
|
|
66
|
+
presence == "required"
|
|
49
67
|
end
|
|
50
68
|
|
|
51
69
|
def optional?
|
|
52
|
-
|
|
70
|
+
presence == "optional"
|
|
53
71
|
end
|
|
54
72
|
|
|
55
73
|
def delimiting?
|
|
@@ -66,15 +84,28 @@ module Glossarist
|
|
|
66
84
|
"(source, id, or text required)"
|
|
67
85
|
end
|
|
68
86
|
|
|
69
|
-
def
|
|
70
|
-
|
|
87
|
+
def validate_presence_count!
|
|
88
|
+
unless Glossarist::GlossaryDefinition::PARTITIVE_PRESENCE_VALUES
|
|
89
|
+
.include?(presence)
|
|
90
|
+
raise ArgumentError,
|
|
91
|
+
"PartitiveMember#presence has invalid value " \
|
|
92
|
+
"#{presence.inspect}; must be one of " \
|
|
93
|
+
"#{GlossaryDefinition::PARTITIVE_PRESENCE_VALUES.join(', ')}"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
unless Glossarist::GlossaryDefinition::PARTITIVE_COUNT_VALUES
|
|
97
|
+
.include?(count)
|
|
98
|
+
raise ArgumentError,
|
|
99
|
+
"PartitiveMember#count has invalid value " \
|
|
100
|
+
"#{count.inspect}; must be one of " \
|
|
101
|
+
"#{GlossaryDefinition::PARTITIVE_COUNT_VALUES.join(', ')}"
|
|
102
|
+
end
|
|
71
103
|
|
|
72
|
-
|
|
73
|
-
.include?(multiplicity)
|
|
104
|
+
if presence == "optional" && count == "at_least_one"
|
|
74
105
|
raise ArgumentError,
|
|
75
|
-
"PartitiveMember
|
|
76
|
-
"
|
|
77
|
-
"
|
|
106
|
+
"PartitiveMember presence=optional + count=at_least_one is " \
|
|
107
|
+
"invalid — it collapses to optional + multiple (zero or more). " \
|
|
108
|
+
"Use presence: optional, count: multiple instead."
|
|
78
109
|
end
|
|
79
110
|
end
|
|
80
111
|
end
|
|
@@ -12,10 +12,9 @@ module Glossarist
|
|
|
12
12
|
# within one relation are coordinate concepts: they share the
|
|
13
13
|
# comprehensive AND share the criterion of subdivision.
|
|
14
14
|
#
|
|
15
|
-
# Per-partitive metadata (ISO 704:2022):
|
|
16
|
-
# -
|
|
17
|
-
#
|
|
18
|
-
# line notation as data
|
|
15
|
+
# Per-partitive metadata (ISO 704:2022, MECE decomposition):
|
|
16
|
+
# - presence (required, optional) — line style: solid vs dashed
|
|
17
|
+
# - count (exactly_one, at_least_one, multiple) — line count
|
|
19
18
|
# - is_delimiting — orthogonal flag; a delimiting part behaves
|
|
20
19
|
# like a delimiting characteristic (distinguishes the
|
|
21
20
|
# comprehensive from coordinate concepts)
|
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 diagram uses dashed/multiple-line notation for " \
|
|
95
|
-
"this — confirm the 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,14 +1,14 @@
|
|
|
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
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-07-
|
|
11
|
+
date: 2026-07-28 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: lutaml-model
|
|
@@ -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
|