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.
- checksums.yaml +4 -4
- data/config.yml +15 -0
- data/lib/glossarist/cli/export_command.rb +5 -5
- data/lib/glossarist/concept_ref.rb +49 -0
- data/lib/glossarist/gcr_package.rb +2 -2
- data/lib/glossarist/gcr_validator.rb +19 -1
- data/lib/glossarist/glossary_definition.rb +16 -3
- data/lib/glossarist/glossary_store.rb +70 -0
- data/lib/glossarist/managed_concept.rb +2 -2
- data/lib/glossarist/rdf/gloss_concept.rb +3 -0
- data/lib/glossarist/rdf/gloss_generic_member.rb +34 -0
- data/lib/glossarist/rdf/gloss_generic_relation.rb +44 -0
- data/lib/glossarist/rdf/gloss_nary_member.rb +43 -0
- data/lib/glossarist/rdf/gloss_nary_relation.rb +56 -0
- data/lib/glossarist/rdf/gloss_partitive_member.rb +5 -28
- data/lib/glossarist/rdf/gloss_partitive_relation.rb +6 -19
- data/lib/glossarist/rdf.rb +4 -0
- data/lib/glossarist/tasks/shacl.rake +4 -1
- data/lib/glossarist/tasks/sync.rake +4 -1
- data/lib/glossarist/tasks.rb +9 -0
- data/lib/glossarist/transforms/concept_to_gloss_transform.rb +72 -52
- data/lib/glossarist/v3/abstract_hyperedge.rb +187 -0
- data/lib/glossarist/v3/definition_type.rb +30 -0
- data/lib/glossarist/v3/detailed_definition.rb +4 -0
- data/lib/glossarist/v3/generic_hyperedge.rb +34 -0
- data/lib/glossarist/v3/generic_member.rb +17 -0
- data/lib/glossarist/v3/hyperedge_index.rb +89 -0
- data/lib/glossarist/v3/hyperedge_member.rb +97 -0
- data/lib/glossarist/v3/hyperedge_registry.rb +70 -0
- data/lib/glossarist/v3/hyperedge_writer.rb +87 -0
- data/lib/glossarist/v3/managed_concept.rb +53 -2
- data/lib/glossarist/v3/managed_concept_data.rb +0 -1
- data/lib/glossarist/v3/partitive_hyperedge.rb +53 -0
- data/lib/glossarist/v3/partitive_member.rb +8 -105
- data/lib/glossarist/v3/relation_loader.rb +98 -0
- data/lib/glossarist/v3.rb +25 -8
- data/lib/glossarist/validation/rules/concept_context.rb +10 -2
- data/lib/glossarist/validation/rules/{partitive_relation_rule.rb → hyperedge_coherence_rule.rb} +22 -16
- data/lib/glossarist/version.rb +1 -1
- data/lib/glossarist.rb +1 -0
- metadata +17 -3
- data/lib/glossarist/v3/partitive_relation.rb +0 -114
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b4d7af45a5f0e045d0f3a18cdcca5791a85bfcb6a16a642af0c1e840b68ef6e8
|
|
4
|
+
data.tar.gz: 8fd502d531f3c862c340b3aa19bb1fc4e4b205e7217ba4c79ee6bc1809db0d35
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
#
|
|
37
|
+
# PartitiveHyperedge redesign (TODO.partitive-relation-v2).
|
|
38
38
|
COMPLETENESS_VALUES = config.dig("completeness", "value").freeze
|
|
39
39
|
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
|
|
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::
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
|
|
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:
|
|
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
|
data/lib/glossarist/rdf.rb
CHANGED
|
@@ -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
|
|
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. " \
|