glossarist 2.13.6 → 2.13.7

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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/config.yml +15 -0
  3. data/lib/glossarist/cli/export_command.rb +5 -5
  4. data/lib/glossarist/concept_ref.rb +49 -0
  5. data/lib/glossarist/gcr_package.rb +2 -2
  6. data/lib/glossarist/gcr_validator.rb +19 -1
  7. data/lib/glossarist/glossary_definition.rb +16 -3
  8. data/lib/glossarist/glossary_store.rb +70 -0
  9. data/lib/glossarist/managed_concept.rb +2 -2
  10. data/lib/glossarist/rdf/gloss_concept.rb +3 -0
  11. data/lib/glossarist/rdf/gloss_generic_member.rb +34 -0
  12. data/lib/glossarist/rdf/gloss_generic_relation.rb +44 -0
  13. data/lib/glossarist/rdf/gloss_nary_member.rb +43 -0
  14. data/lib/glossarist/rdf/gloss_nary_relation.rb +56 -0
  15. data/lib/glossarist/rdf/gloss_partitive_member.rb +5 -28
  16. data/lib/glossarist/rdf/gloss_partitive_relation.rb +6 -19
  17. data/lib/glossarist/rdf.rb +4 -0
  18. data/lib/glossarist/tasks/shacl.rake +4 -1
  19. data/lib/glossarist/tasks/sync.rake +4 -1
  20. data/lib/glossarist/tasks.rb +9 -0
  21. data/lib/glossarist/transforms/concept_to_gloss_transform.rb +72 -52
  22. data/lib/glossarist/v3/abstract_hyperedge.rb +187 -0
  23. data/lib/glossarist/v3/definition_type.rb +30 -0
  24. data/lib/glossarist/v3/detailed_definition.rb +4 -0
  25. data/lib/glossarist/v3/generic_hyperedge.rb +34 -0
  26. data/lib/glossarist/v3/generic_member.rb +17 -0
  27. data/lib/glossarist/v3/hyperedge_index.rb +89 -0
  28. data/lib/glossarist/v3/hyperedge_member.rb +97 -0
  29. data/lib/glossarist/v3/hyperedge_registry.rb +70 -0
  30. data/lib/glossarist/v3/hyperedge_writer.rb +87 -0
  31. data/lib/glossarist/v3/managed_concept.rb +53 -2
  32. data/lib/glossarist/v3/managed_concept_data.rb +0 -1
  33. data/lib/glossarist/v3/partitive_hyperedge.rb +53 -0
  34. data/lib/glossarist/v3/partitive_member.rb +8 -105
  35. data/lib/glossarist/v3/relation_loader.rb +98 -0
  36. data/lib/glossarist/v3.rb +25 -8
  37. data/lib/glossarist/validation/rules/concept_context.rb +10 -2
  38. data/lib/glossarist/validation/rules/{partitive_relation_rule.rb → hyperedge_coherence_rule.rb} +22 -16
  39. data/lib/glossarist/version.rb +1 -1
  40. data/lib/glossarist.rb +1 -0
  41. metadata +17 -3
  42. data/lib/glossarist/v3/partitive_relation.rb +0 -114
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 51ac508f50dca0e1c04b8a37fbb75460dc27496876321361b9e3691995b203dc
4
- data.tar.gz: a41b2f29cfaca14931ee3035c751ad296065a0159b9bc202251e1775973f3ea8
3
+ metadata.gz: b4d7af45a5f0e045d0f3a18cdcca5791a85bfcb6a16a642af0c1e840b68ef6e8
4
+ data.tar.gz: 8fd502d531f3c862c340b3aa19bb1fc4e4b205e7217ba4c79ee6bc1809db0d35
5
5
  SHA512:
6
- metadata.gz: 5a9a6b0040e89f986f676a2dbd4b8c86244c110fb4f3c005f9d5995f3bdf7abef50c48564c5d8551d11beca5f7876def2b8970ff719b086e091562f6ce3830ae
7
- data.tar.gz: 4f81856d110124d519e3cbf10a5863623fe13b57283e89861ce1882c83d7fb1768c792c0f27e41b99c4cd7c462b00ce0a7dcfb894169c92985c4430c4fa8b84a
6
+ metadata.gz: aedc22ebce41ec42d6bc93e96abef82078b7834b66f08a0e219b08f4b2e0eded91561bcc82af7498ab50aed4e8da32d0cabc23c37b7eb56390ec7028e8b731ba
7
+ data.tar.gz: fe063d88bf1297f1c31e72ce545a9a9e32f7deb89f6d24cd6580572f830f605288e57feed04ed3af4ebb9c777320def6c5fc87e9ec82a93ef12c0a070a7f42b7
data/config.yml CHANGED
@@ -234,3 +234,18 @@ multiplicity:
234
234
  - exactly_one
235
235
  - at_least_one
236
236
  - multiple
237
+
238
+ definition_type:
239
+ # ISO 704:2022 §5.3 definition strategies.
240
+ # See docs/design/definition-types.md (in concept-model repo).
241
+ value:
242
+ - intensional
243
+ - extensional
244
+ - partitive
245
+ - translated
246
+
247
+ relation_type:
248
+ # Wire discriminator for per-file n-ary relation files.
249
+ value:
250
+ - partitive_relation
251
+ - generic_relation
@@ -154,7 +154,7 @@ module Glossarist
154
154
  def export_jsonld(concepts, name, output_dir)
155
155
  require "glossarist/transforms/concept_to_gloss_transform"
156
156
  transform = Transforms::ConceptToGlossTransform.new(nil,
157
- transform_options)
157
+ **transform_options)
158
158
  File.write(File.join(output_dir, "#{name}.jsonld"),
159
159
  transform.to_jsonld(concepts,
160
160
  figures: dataset_figures,
@@ -165,7 +165,7 @@ module Glossarist
165
165
  def export_turtle(concepts, name, output_dir)
166
166
  require "glossarist/transforms/concept_to_gloss_transform"
167
167
  transform = Transforms::ConceptToGlossTransform.new(nil,
168
- transform_options)
168
+ **transform_options)
169
169
  File.write(File.join(output_dir, "#{name}.ttl"),
170
170
  transform.to_turtle(concepts,
171
171
  figures: dataset_figures,
@@ -185,7 +185,7 @@ module Glossarist
185
185
  File.open(File.join(output_dir, "#{name}.jsonl"), "w") do |f|
186
186
  concepts.each do |concept|
187
187
  transform = Transforms::ConceptToGlossTransform.new(concept,
188
- transform_options)
188
+ **transform_options)
189
189
  f.write(transform.to_jsonl_line)
190
190
  f.write("\n")
191
191
  end
@@ -223,7 +223,7 @@ module Glossarist
223
223
  concepts.each do |concept|
224
224
  id = concept.data&.id || concept.identifier
225
225
  transform = Transforms::ConceptToGlossTransform.new(concept,
226
- transform_options)
226
+ **transform_options)
227
227
  File.write(File.join(dir, "#{id}.jsonld"), transform.to_jsonld)
228
228
  end
229
229
  end
@@ -233,7 +233,7 @@ module Glossarist
233
233
  concepts.each do |concept|
234
234
  id = concept.data&.id || concept.identifier
235
235
  transform = Transforms::ConceptToGlossTransform.new(concept,
236
- transform_options)
236
+ **transform_options)
237
237
  File.write(File.join(dir, "#{id}.ttl"), transform.to_turtle)
238
238
  end
239
239
  end
@@ -1,6 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Glossarist
4
+ # A typed reference to another concept, used inside n-ary relation
5
+ # members and other internal pointer shapes.
6
+ #
7
+ # The base ConceptRef carries `source`, `id`, and `text`. The richer
8
+ # `ConceptReference` (in `glossarist/concept_reference.rb`) extends
9
+ # this with `term`, `urn`, `version`, `ref_type` for use in
10
+ # cross-vocabulary references and relevance contexts.
11
+ #
12
+ # A ConceptRef is identified by `source:id` for external references
13
+ # or just `id` for local references. The `qualified_id` class method
14
+ # is the single SSOT for this string format — used by RelationLoader,
15
+ # the RDF transform, and ConceptRef-bearing data structures.
4
16
  class ConceptRef < Lutaml::Model::Serializable
5
17
  attribute :source, :string
6
18
  attribute :id, :string
@@ -11,5 +23,42 @@ module Glossarist
11
23
  map :id, to: :id
12
24
  map :text, to: :text
13
25
  end
26
+
27
+ # SSOT for the canonical "qualified identifier" string of a
28
+ # ConceptRef. Returns:
29
+ #
30
+ # - "{source}:{id}" when both source and id are present
31
+ # - "{id}" when only id is present (local ref)
32
+ # - "{source}:{text}" when source and text are present (no id)
33
+ # - "{text}" when only text is present (external
34
+ # concept form, no resolvable id)
35
+ # - nil when the ref is empty / not a ConceptRef
36
+ #
37
+ # Two refs are considered to point at the same concept iff their
38
+ # qualified_id is equal. Tables and registries can use this
39
+ # directly as a hash key.
40
+ def self.qualified_id(ref)
41
+ return nil unless ref.is_a?(ConceptRef)
42
+
43
+ id_part = (ref.id.to_s.empty? ? nil : ref.id)
44
+ text_part = (ref.text.to_s.empty? ? nil : ref.text)
45
+ source_part = (ref.source.to_s.empty? ? nil : ref.source)
46
+
47
+ if id_part
48
+ source_part ? "#{source_part}:#{id_part}" : id_part
49
+ elsif text_part
50
+ source_part ? "#{source_part}:#{text_part}" : text_part
51
+ end
52
+ end
53
+
54
+ # Per-instance convenience delegating to the class method.
55
+ def qualified_id
56
+ self.class.qualified_id(self)
57
+ end
58
+
59
+ # True iff +other+ refers to the same concept as self.
60
+ def same_concept?(other)
61
+ qualified_id == self.class.qualified_id(other)
62
+ end
14
63
  end
15
64
  end
@@ -228,7 +228,7 @@ module Glossarist
228
228
  require "glossarist/transforms/concept_to_gloss_transform"
229
229
 
230
230
  if formats.include?("jsonld") || formats.include?("turtle")
231
- transform = Transforms::ConceptToGlossTransform.new(nil, opts)
231
+ transform = Transforms::ConceptToGlossTransform.new(nil, **opts)
232
232
 
233
233
  if formats.include?("jsonld")
234
234
  zip_file.get_output_stream("compiled/#{name}.jsonld") do |f|
@@ -247,7 +247,7 @@ module Glossarist
247
247
 
248
248
  zip_file.get_output_stream("compiled/#{name}.jsonl") do |f|
249
249
  concepts.each do |concept|
250
- transform = Transforms::ConceptToGlossTransform.new(concept, opts)
250
+ transform = Transforms::ConceptToGlossTransform.new(concept, **opts)
251
251
  f.write(transform.to_jsonl_line)
252
252
  f.write("\n")
253
253
  end
@@ -4,8 +4,19 @@ require "zip"
4
4
 
5
5
  module Glossarist
6
6
  class GcrValidator
7
- def initialize(on_progress: nil)
7
+ # N-ary relations live in per-file relation store (see
8
+ # Glossarist::V3::RelationLoader), not inside the GCR. For GCR
9
+ # validation, callers can pass a `relations:` hash keyed by
10
+ # concept_id; the validator passes the per-concept list to each
11
+ # concept's context. Without it, relations are not validated
12
+ # here — only the concepts themselves.
13
+ def initialize(on_progress: nil, relations: nil)
8
14
  @on_progress = on_progress
15
+ @relations = relations
16
+ end
17
+
18
+ def self.validate(zip_path, relations: nil, **opts)
19
+ new(**opts, relations: relations).validate(zip_path)
9
20
  end
10
21
 
11
22
  def validate(zip_path)
@@ -58,6 +69,7 @@ module Glossarist
58
69
  concept,
59
70
  file_name: concept.data&.id ? "concepts/#{concept.data.id}.yaml" : "concepts/concept-#{idx}.yaml",
60
71
  collection_context: context,
72
+ relations: relations_for_concept(concept),
61
73
  )
62
74
 
63
75
  concept_rules.each do |rule|
@@ -70,6 +82,12 @@ module Glossarist
70
82
  end
71
83
  end
72
84
 
85
+ def relations_for_concept(concept)
86
+ return [] unless @relations
87
+
88
+ @relations[concept.data&.id] || []
89
+ end
90
+
73
91
  def validate_collection(context, result)
74
92
  Validation::Rules::Registry.for_scope(:collection).each do |rule|
75
93
  next unless rule.applicable?(context)
@@ -34,13 +34,26 @@ module Glossarist
34
34
 
35
35
  ISO12620_TERM_TYPES = config.dig("iso12620", "term_type").freeze
36
36
 
37
- # PartitiveRelation redesign (TODO.partitive-relation-v2).
37
+ # PartitiveHyperedge redesign (TODO.partitive-relation-v2).
38
38
  COMPLETENESS_VALUES = config.dig("completeness", "value").freeze
39
39
 
40
- PARTITIVE_PRESENCE_VALUES =
40
+ # Member presence/count values for any n-ary relation member
41
+ # (PartitiveMember, GenericMember — both inherit HyperedgeMember).
42
+ # The config key is `multiplicity:` because presence + count encode
43
+ # ISO 704:2022 multiplicity in the MECE decomposition; the constants
44
+ # are named for their consumers (HyperedgeMember fields).
45
+ MEMBER_PRESENCE_VALUES =
41
46
  config.dig("multiplicity", "presence").freeze
42
47
 
43
- PARTITIVE_COUNT_VALUES =
48
+ MEMBER_COUNT_VALUES =
44
49
  config.dig("multiplicity", "count").freeze
50
+
51
+ # ISO 704:2022 §5.3 definition strategies.
52
+ DEFINITION_TYPE_VALUES =
53
+ config.dig("definition_type", "value").freeze
54
+
55
+ # Wire discriminator for per-file n-ary relation files.
56
+ RELATION_TYPE_VALUES =
57
+ config.dig("relation_type", "value").freeze
45
58
  end
46
59
  end
@@ -78,10 +78,43 @@ module Glossarist
78
78
  def save_directory(path, format: nil, formats: {})
79
79
  @package.save(path, transport: :directory, format: format,
80
80
  formats: formats)
81
+ # Persist per-file hyperedges alongside concepts. The relations/
82
+ # subdirectory mirrors the load path (relations/<comp-id>/<slug>.yaml).
83
+ save_relations(File.join(path, "relations")) if @relations && !@relations.empty?
81
84
  end
82
85
 
83
86
  def save_zip(path, format: nil, formats: {})
84
87
  @package.save(path, transport: :zip, format: format, formats: formats)
88
+ # ZIP transport doesn't expose a writable filesystem path here —
89
+ # callers persisting to ZIP must use #save_relations separately
90
+ # against a staging dir before zipping. Documented limitation.
91
+ end
92
+
93
+ # Persist all loaded relations to the per-file store under `dir`.
94
+ # Each hyperedge becomes relations/<comp-id>/<slug>.yaml (one file
95
+ # per hyperedge, per concept-model per-file format).
96
+ def save_relations(dir)
97
+ return if @relations.nil? || @relations.empty?
98
+
99
+ V3::HyperedgeWriter.write_all(@relations, dir)
100
+ end
101
+
102
+ # Add a hyperedge to the in-memory list AND persist it immediately
103
+ # to its per-file location under <dataset_path>/relations/.
104
+ # Returns the written file path (or nil if dataset has no path).
105
+ def add_relation(hyperedge)
106
+ (@relations ||= []) << hyperedge
107
+ return nil unless @dataset_path
108
+
109
+ relations_dir = File.join(@dataset_path, "relations")
110
+ V3::HyperedgeWriter.write(hyperedge, relations_dir)
111
+ end
112
+
113
+ # Remove a hyperedge. Caller-supplied predicate selects which to
114
+ # remove. The per-file storage is NOT modified by this method —
115
+ # use #delete_relation_file to remove a single file.
116
+ def remove_relations_if(&predicate)
117
+ @relations&.reject!(&predicate)
85
118
  end
86
119
 
87
120
  # ── Concepts ──
@@ -202,6 +235,30 @@ module Glossarist
202
235
  @formulas ||= load_dataset_entities("formulas", Formula)
203
236
  end
204
237
 
238
+ # Per-file n-ary relations (PartitiveHyperedge, GenericHyperedge)
239
+ # discovered under `relations/<comprehensive-id>/<criterion-slug>.yaml`.
240
+ # Returns a flat Array<AbstractHyperedge>; use #relations_for to
241
+ # filter by comprehensive id. Empty when the dataset has no
242
+ # relations/ directory (V1/V2 datasets, or V3 datasets that only
243
+ # carry concepts).
244
+ def relations
245
+ @relations ||= load_relations
246
+ end
247
+
248
+ # Relations whose comprehensive concept matches `qualified_id`
249
+ # (e.g. "VIM:112-02-09"). Accepts either a qualified-id string or
250
+ # a ConceptRef — the lookup goes through ConceptRef.qualified_id.
251
+ def relations_for(qualified_id_or_ref)
252
+ target = if qualified_id_or_ref.is_a?(Glossarist::ConceptRef)
253
+ Glossarist::ConceptRef.qualified_id(qualified_id_or_ref)
254
+ else
255
+ qualified_id_or_ref.to_s
256
+ end
257
+ relations.select do |rel|
258
+ Glossarist::ConceptRef.qualified_id(rel.comprehensive) == target
259
+ end
260
+ end
261
+
205
262
  # ── Stats ──
206
263
 
207
264
  def stats
@@ -244,6 +301,19 @@ module Glossarist
244
301
  end
245
302
  end
246
303
 
304
+ # Loads per-file n-ary relations via V3::RelationLoader. Returns a
305
+ # flat Array<AbstractHyperedge>. Empty when no relations/
306
+ # directory exists. The loader handles V1/V2 datasets gracefully
307
+ # (no relations directory → empty).
308
+ def load_relations
309
+ return [] unless @dataset_path
310
+
311
+ relations_dir = File.join(@dataset_path, "relations")
312
+ return [] unless File.directory?(relations_dir)
313
+
314
+ V3::RelationLoader.load_all(relations_dir).values.flatten
315
+ end
316
+
247
317
  def resolve_concept_document_class(metadata)
248
318
  version = metadata&.schema_version.to_s
249
319
  ConceptDocument.for_version(version)
@@ -165,7 +165,7 @@ module Glossarist
165
165
  @uuid = new_uuid
166
166
  end
167
167
 
168
- def self.detect_schema_version(concept) # rubocop:disable Metrics/PerceivedComplexity
168
+ def self.detect_schema_version(concept, relations: nil) # rubocop:disable Metrics/PerceivedComplexity
169
169
  raw = concept.schema_version
170
170
  if raw && !%w[legacy nil].include?(raw.to_s)
171
171
  return raw.to_s
@@ -175,7 +175,7 @@ module Glossarist
175
175
  return "3" if concept.sources&.any?
176
176
  return "3" if concept.data&.domains&.any?
177
177
  return "3" if concept.is_a?(V3::ManagedConcept) &&
178
- concept.partitive_relations&.any?
178
+ relations&.any?
179
179
  return "3" if localization_has_references?(concept)
180
180
 
181
181
  "2"
@@ -12,6 +12,7 @@ module Glossarist
12
12
  attribute :domains, GlossConceptReference, collection: true
13
13
  attribute :dates, GlossConceptDate, collection: true
14
14
  attribute :partitive_relations, GlossPartitiveRelation, collection: true
15
+ attribute :generic_relations, GlossGenericRelation, collection: true
15
16
 
16
17
  RelationshipPredicates::CONCEPT_REL_PREDICATES.each_key do |type|
17
18
  attribute :"#{type}_targets", :string, collection: true
@@ -44,6 +45,8 @@ module Glossarist
44
45
  link: "gloss:hasDate"
45
46
  members :partitive_relations,
46
47
  link: "gloss:hasPartitiveRelation"
48
+ members :generic_relations,
49
+ link: "gloss:hasGenericRelation"
47
50
 
48
51
  RelationshipPredicates::CONCEPT_REL_PREDICATES.each do |type, (ns, name)|
49
52
  predicate name, namespace: ns, to: :"#{type}_targets",
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Glossarist
6
+ module Rdf
7
+ # RDF view for V3::GenericMember. Inherits attributes and
8
+ # deterministic_id from GlossNaryMember. The `rdf do` block
9
+ # re-declares the predicates because lutaml-model's `rdf` DSL
10
+ # replaces the parent mapping (not extends).
11
+ class GlossGenericMember < GlossNaryMember
12
+ rdf do
13
+ namespace Namespaces::GlossaristNamespace
14
+
15
+ subject { |m| "genericMember/#{GlossGenericMember.deterministic_id(m)}" }
16
+
17
+ types "gloss:GenericMember"
18
+
19
+ predicate :refSource, namespace: Namespaces::GlossaristNamespace,
20
+ to: :ref_source
21
+ predicate :refId, namespace: Namespaces::GlossaristNamespace,
22
+ to: :ref_id
23
+ predicate :refText, namespace: Namespaces::GlossaristNamespace,
24
+ to: :ref_text
25
+ predicate :presence, namespace: Namespaces::GlossaristNamespace,
26
+ to: :presence
27
+ predicate :count, namespace: Namespaces::GlossaristNamespace,
28
+ to: :count
29
+ predicate :isDelimiting, namespace: Namespaces::GlossaristNamespace,
30
+ to: :is_delimiting
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Glossarist
6
+ module Rdf
7
+ # RDF view for V3::GenericHyperedge. Inherits structure and
8
+ # helpers from GlossNaryRelation. The `rdf do` block re-declares
9
+ # the predicates because lutaml-model's `rdf` DSL replaces the
10
+ # parent mapping (not extends).
11
+ class GlossGenericRelation < GlossNaryRelation
12
+ attribute :generic_member_ids, :string, collection: true
13
+ attribute :generic_members, GlossGenericMember, collection: true
14
+
15
+ rdf do
16
+ namespace Namespaces::GlossaristNamespace
17
+
18
+ subject { |r| "genericRelation/#{GlossGenericRelation.deterministic_id(r)}" }
19
+
20
+ types "gloss:GenericHyperedge"
21
+
22
+ predicate :comprehensive, namespace: Namespaces::GlossaristNamespace,
23
+ to: :comprehensive_uri, uri_reference: true
24
+ predicate :hasGeneric, namespace: Namespaces::GlossaristNamespace,
25
+ to: :generic_member_ids, uri_reference: true
26
+ predicate :completeness, namespace: Namespaces::GlossaristNamespace,
27
+ to: :completeness, uri_reference: true
28
+ predicate :criterion, namespace: Namespaces::GlossaristNamespace,
29
+ to: :criterion
30
+
31
+ members :generic_members, link: "gloss:hasGenericMember"
32
+ end
33
+
34
+ def self.deterministic_id(rel)
35
+ parts = [rel.identifier, rel.comprehensive_uri, rel.completeness,
36
+ criterion_fingerprint(rel.criterion)]
37
+ Array(rel.generic_members).each do |m|
38
+ parts << GlossGenericMember.deterministic_id(m)
39
+ end
40
+ DeterministicSlug.from_parts(*parts)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Glossarist
6
+ module Rdf
7
+ # Shared base for typed n-ary relation member RDF views
8
+ # (GlossPartitiveMember, GlossGenericMember, future leaves).
9
+ #
10
+ # Carries the shared attributes and the deterministic_id algorithm.
11
+ # Concrete leaves declare their own `rdf do` block because
12
+ # lutaml-model's `rdf` DSL replaces (not extends) the parent's
13
+ # mapping — predicate declarations stay in each leaf.
14
+ #
15
+ # Subject identity is content-derived so the same member in the
16
+ # same relation produces the same URI across runs (and across
17
+ # processes). Two members with identical ref + dimensions share a
18
+ # subject — that is intentional: it exposes model-level
19
+ # inconsistency if the same partitive concept is given different
20
+ # dimensions inside the same dataset.
21
+ class GlossNaryMember < Lutaml::Model::Serializable
22
+ attribute :ref_id, :string
23
+ attribute :ref_source, :string
24
+ attribute :ref_text, :string
25
+ attribute :presence, :string
26
+ attribute :count, :string
27
+ attribute :is_delimiting, :boolean
28
+
29
+ # Human-readable SOURCE:ID when both are present; SHA-256 / 16
30
+ # hex chars fallback otherwise. Same content → same slug across
31
+ # instances, runs, and processes.
32
+ def self.deterministic_id(member)
33
+ readable = [member.ref_source, member.ref_id].compact.reject(&:empty?)
34
+ return readable.join(":") unless readable.empty?
35
+
36
+ DeterministicSlug.from_parts(
37
+ member.ref_source, member.ref_id, member.ref_text,
38
+ member.presence, member.count, member.is_delimiting
39
+ )
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Glossarist
6
+ module Rdf
7
+ # Shared base for typed n-ary relation RDF views
8
+ # (GlossPartitiveRelation, GlossGenericRelation, future leaves).
9
+ #
10
+ # Concrete leaves override the `rdf do` block to declare their
11
+ # type-specific subject prefix, `types` declaration, member-class
12
+ # attribute, and link predicate name. The deterministic_id and
13
+ # criterion_fingerprint algorithms are inherited unchanged.
14
+ class GlossNaryRelation < Lutaml::Model::Serializable
15
+ attribute :identifier, :string
16
+ attribute :comprehensive_uri, :string
17
+ attribute :completeness, :string
18
+ attribute :criterion, :hash
19
+
20
+ rdf do
21
+ namespace Namespaces::GlossaristNamespace
22
+
23
+ predicate :comprehensive, namespace: Namespaces::GlossaristNamespace,
24
+ to: :comprehensive_uri, uri_reference: true
25
+ predicate :completeness, namespace: Namespaces::GlossaristNamespace,
26
+ to: :completeness, uri_reference: true
27
+ predicate :criterion, namespace: Namespaces::GlossaristNamespace,
28
+ to: :criterion
29
+ end
30
+
31
+ # Content-derived subject identifier. Combines identifier,
32
+ # comprehensive_uri, completeness, criterion fingerprint, and
33
+ # each member fingerprint. Same content → same hash across
34
+ # instances, runs, and processes.
35
+ #
36
+ # Concrete subclasses override to specify the member attribute
37
+ # name and member deterministic_id callable.
38
+ def self.deterministic_id(_relation)
39
+ raise NotImplementedError,
40
+ "#{name}.deterministic_id must be overridden by the concrete leaf"
41
+ end
42
+
43
+ # Stable, ordering-insensitive fingerprint of a localized
44
+ # criterion hash. Two relations with the same comprehensive,
45
+ # completeness, and criterion produce the same fingerprint
46
+ # regardless of hash-iteration order.
47
+ def self.criterion_fingerprint(criterion)
48
+ return nil unless criterion.is_a?(Hash) && !criterion.empty?
49
+
50
+ criterion.sort_by { |k, _| k.to_s }
51
+ .map { |k, v| "#{k}=#{v}" }
52
+ .join(";")
53
+ end
54
+ end
55
+ end
56
+ end
@@ -4,24 +4,11 @@ require "lutaml/model"
4
4
 
5
5
  module Glossarist
6
6
  module Rdf
7
- # RDF view for V3::PartitiveMember. Emits a gloss:PartitiveMember
8
- # subject carrying the ConceptRef target plus the ISO 704:2022
9
- # orthogonal dimensions (presence, count, is_delimiting).
10
- #
11
- # Subject identity is content-derived so the same member in the
12
- # same relation produces the same URI across runs (and across
13
- # processes). Two members with identical ref + dimensions share a
14
- # subject — that is intentional: it exposes model-level
15
- # inconsistency if the same partitive concept is given different
16
- # dimensions inside the same dataset.
17
- class GlossPartitiveMember < Lutaml::Model::Serializable
18
- attribute :ref_id, :string
19
- attribute :ref_source, :string
20
- attribute :ref_text, :string
21
- attribute :presence, :string
22
- attribute :count, :string
23
- attribute :is_delimiting, :boolean
24
-
7
+ # RDF view for V3::PartitiveMember. Inherits attributes and
8
+ # deterministic_id from GlossNaryMember. The `rdf do` block
9
+ # re-declares the predicates because lutaml-model's `rdf` DSL
10
+ # replaces the parent mapping (not extends).
11
+ class GlossPartitiveMember < GlossNaryMember
25
12
  rdf do
26
13
  namespace Namespaces::GlossaristNamespace
27
14
 
@@ -42,16 +29,6 @@ module Glossarist
42
29
  predicate :isDelimiting, namespace: Namespaces::GlossaristNamespace,
43
30
  to: :is_delimiting
44
31
  end
45
-
46
- def self.deterministic_id(member)
47
- readable = [member.ref_source, member.ref_id].compact.reject(&:empty?)
48
- return readable.join(":") unless readable.empty?
49
-
50
- DeterministicSlug.from_parts(
51
- member.ref_source, member.ref_id, member.ref_text,
52
- member.presence, member.count, member.is_delimiting
53
- )
54
- end
55
32
  end
56
33
  end
57
34
  end
@@ -4,25 +4,20 @@ require "lutaml/model"
4
4
 
5
5
  module Glossarist
6
6
  module Rdf
7
- # RDF view for V3::PartitiveRelation. Emits a
8
- # gloss:PartitiveRelation subject with comprehensive, completeness,
9
- # criterion properties, plus hasPartitive (concept URIs, for graph
10
- # traversal) and hasPartitiveMember (typed subjects carrying the
11
- # ISO 704:2022 per-member dimensions).
12
- class GlossPartitiveRelation < Lutaml::Model::Serializable
13
- attribute :identifier, :string
14
- attribute :comprehensive_uri, :string
7
+ # RDF view for V3::PartitiveHyperedge. Inherits structure and
8
+ # helpers from GlossNaryRelation. The `rdf do` block re-declares
9
+ # the predicates because lutaml-model's `rdf` DSL replaces the
10
+ # parent mapping (not extends).
11
+ class GlossPartitiveRelation < GlossNaryRelation
15
12
  attribute :partitive_member_ids, :string, collection: true
16
13
  attribute :partitive_members, GlossPartitiveMember, collection: true
17
- attribute :completeness, :string
18
- attribute :criterion, :hash
19
14
 
20
15
  rdf do
21
16
  namespace Namespaces::GlossaristNamespace
22
17
 
23
18
  subject { |r| "partitiveRelation/#{GlossPartitiveRelation.deterministic_id(r)}" }
24
19
 
25
- types "gloss:PartitiveRelation"
20
+ types "gloss:PartitiveHyperedge"
26
21
 
27
22
  predicate :comprehensive, namespace: Namespaces::GlossaristNamespace,
28
23
  to: :comprehensive_uri, uri_reference: true
@@ -44,14 +39,6 @@ module Glossarist
44
39
  end
45
40
  DeterministicSlug.from_parts(*parts)
46
41
  end
47
-
48
- def self.criterion_fingerprint(criterion)
49
- return nil unless criterion.is_a?(Hash) && !criterion.empty?
50
-
51
- criterion.sort_by { |k, _| k.to_s }
52
- .map { |k, v| "#{k}=#{v}" }
53
- .join(";")
54
- end
55
42
  end
56
43
  end
57
44
  end
@@ -45,6 +45,10 @@ module Glossarist
45
45
  autoload :GlossFormula, "#{__dir__}/rdf/gloss_formula"
46
46
  autoload :GlossPartitiveRelation, "#{__dir__}/rdf/gloss_partitive_relation"
47
47
  autoload :GlossPartitiveMember, "#{__dir__}/rdf/gloss_partitive_member"
48
+ autoload :GlossGenericRelation, "#{__dir__}/rdf/gloss_generic_relation"
49
+ autoload :GlossGenericMember, "#{__dir__}/rdf/gloss_generic_member"
50
+ autoload :GlossNaryRelation, "#{__dir__}/rdf/gloss_nary_relation"
51
+ autoload :GlossNaryMember, "#{__dir__}/rdf/gloss_nary_member"
48
52
  autoload :V3, "#{__dir__}/rdf/v3"
49
53
  end
50
54
  end
@@ -3,7 +3,10 @@
3
3
  require "rdf/turtle"
4
4
  require "shacl"
5
5
  require "pathname"
6
- require "glossarist/validation/shacl_validator"
6
+ # Entry-point require triggers the autoload chain
7
+ # (Glossarist → Validation → ShaclValidator). Internal-path requires
8
+ # are forbidden by the autoload rule.
9
+ require "glossarist"
7
10
 
8
11
  namespace :glossarist do
9
12
  desc "Validate all .ttl outputs against concept-model SHACL shapes. " \