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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ebf6ec1d9cc3f2c1932fff4d389b19e60dff4846e3f82167fa6e13bef8347f7d
4
- data.tar.gz: 2aee9b19e15588b9fc74a973d02406de96b1ed01ebefcdd7b87bc472d3b8d53b
3
+ metadata.gz: f074d0113023ae82f1ae9cc8186cee518bc10ad87faabe3895117dcbbc700524
4
+ data.tar.gz: 8d89647b60c955419db263514521fcefcaa2efc07b4f6a16510bae2fa0313a82
5
5
  SHA512:
6
- metadata.gz: '09eeb3261b6e753a4e1690496f13db04053429318108d52a031963087a9c908f842a12802b310014874f78f5d47bfd3a54dd1672e610e0b55684133a82ec2331'
7
- data.tar.gz: de663d1b6fe2581a17ae52e2fb69d2ea6accf6ca46f92be084ec603c5b8f0f3d614a5dbeaf5a20178bb8d08511ea046210ed09a183c9c35b0f9d462b0dcb62d6
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
- # gloss:PartitiveMember subject with ref, presence, count, and
9
- # is_delimiting properties.
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/#{m.ref_source}:#{m.ref_id}" }
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, hasPartitive,
9
- # completeness, criterion properties.
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/#{r.identifier}/#{r.object_id}" }
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: partitive_concept_uri(rel.comprehensive),
156
- partitive_member_ids: Array(rel.partitives).map do |m|
157
- partitive_member_uri(m)
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 partitive_concept_uri(ref)
166
- return nil unless ref.is_a?(Glossarist::ConceptRef)
170
+ def build_gloss_partitive_member(member)
171
+ return Rdf::GlossPartitiveMember.new unless member.is_a?(V3::PartitiveMember)
167
172
 
168
- Glossarist::Rdf::Namespaces::GlossaristNamespace.uri +
169
- "concept/#{ref.id || ref.text}"
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
- def partitive_member_uri(member)
173
- return nil unless member.is_a?(V3::PartitiveMember)
174
-
175
- ref = member.ref
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/#{ref.id || ref.text}"
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 multiplicity is not the default compulsory
18
- # (encourages explicit statement of optional/multiple)
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
- check_member_multiplicity(rel, idx, fname, issues)
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
- def check_member_multiplicity(rel, idx, fname, issues)
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
- unless member.required? && member.count == "exactly_one"
91
- issues << issue(
92
- "partitive_relation #{idx + 1}.partitives[#{mi}] has " \
93
- "non-default presence='#{member.presence}' count='#{member.count}' " \
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}] is marked " \
105
- "delimiting (ISO 704 bold 3x-width line); behaves like a " \
106
- "delimiting characteristic distinguishing the comprehensive " \
107
- "from coordinate concepts",
108
- severity: "info",
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
@@ -4,5 +4,5 @@
4
4
  #
5
5
 
6
6
  module Glossarist
7
- VERSION = "2.13.4"
7
+ VERSION = "2.13.5"
8
8
  end
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
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