glossarist 2.8.17 → 2.9.0

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +175 -8
  3. data/Rakefile +4 -0
  4. data/config.yml +2 -3
  5. data/data/concept-model/README.md +35 -0
  6. data/data/concept-model/SOURCE.json +5 -0
  7. data/data/concept-model/glossarist.context.jsonld +209 -0
  8. data/data/concept-model/glossarist.ttl +960 -0
  9. data/data/concept-model/shapes/glossarist.shacl.ttl +575 -0
  10. data/glossarist.gemspec +2 -0
  11. data/lib/glossarist/asset_reference.rb +2 -0
  12. data/lib/glossarist/bibliographic_reference.rb +13 -0
  13. data/lib/glossarist/cli/export_command.rb +130 -7
  14. data/lib/glossarist/cli.rb +11 -2
  15. data/lib/glossarist/concept.rb +3 -7
  16. data/lib/glossarist/concept_date.rb +7 -0
  17. data/lib/glossarist/concept_document.rb +13 -0
  18. data/lib/glossarist/concept_reference.rb +6 -0
  19. data/lib/glossarist/concept_store.rb +2 -3
  20. data/lib/glossarist/dataset_register.rb +2 -2
  21. data/lib/glossarist/figure_reference.rb +0 -4
  22. data/lib/glossarist/formula_reference.rb +0 -4
  23. data/lib/glossarist/managed_concept.rb +3 -3
  24. data/lib/glossarist/non_verbal_reference.rb +10 -0
  25. data/lib/glossarist/rdf/gloss_detailed_definition.rb +6 -1
  26. data/lib/glossarist/rdf/gloss_localized_concept.rb +65 -0
  27. data/lib/glossarist/rdf/lutaml_ext.rb +33 -0
  28. data/lib/glossarist/rdf.rb +2 -0
  29. data/lib/glossarist/reference.rb +33 -0
  30. data/lib/glossarist/table_reference.rb +0 -4
  31. data/lib/glossarist/tasks/shacl.rake +30 -0
  32. data/lib/glossarist/tasks/sync.rake +14 -0
  33. data/lib/glossarist/tasks/sync_model.rb +92 -0
  34. data/lib/glossarist/transforms/concept_to_gloss_transform.rb +3 -2
  35. data/lib/glossarist/utilities/uuid.rb +5 -1
  36. data/lib/glossarist/v3/concept_data.rb +1 -0
  37. data/lib/glossarist/v3/concept_date.rb +37 -0
  38. data/lib/glossarist/v3/localized_concept.rb +9 -0
  39. data/lib/glossarist/v3/managed_concept.rb +8 -0
  40. data/lib/glossarist/v3.rb +2 -0
  41. data/lib/glossarist/validation/rules/related_concept_cycle_rule.rb +15 -1
  42. data/lib/glossarist/validation/shacl_validator.rb +97 -0
  43. data/lib/glossarist/validation.rb +1 -0
  44. data/lib/glossarist/version.rb +1 -1
  45. data/lib/glossarist.rb +1 -0
  46. metadata +42 -2
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "fileutils"
5
+ require "net/http"
6
+ require "uri"
7
+
8
+ module Glossarist
9
+ module Tasks
10
+ # Syncs vendored concept-model data artifacts from glossarist/concept-model.
11
+ #
12
+ # concept-model is a data-only repo (TTL, JSON-LD, YAML schemas). It is
13
+ # not a gem. We vendor the small set of artifacts we need (shapes,
14
+ # context, ontology) and load them at runtime via ShaclValidator.
15
+ module SyncModel
16
+ REPO = "glossarist/concept-model"
17
+ OUT_DIR = File.expand_path("data/concept-model", File.join(__dir__, "..", "..", ".."))
18
+
19
+ TARGETS = {
20
+ "glossarist.context.jsonld" => %w[
21
+ ontologies/glossarist.context.jsonld
22
+ glossarist.context.jsonld
23
+ ].freeze,
24
+ "glossarist.ttl" => %w[
25
+ ontologies/glossarist.ttl
26
+ glossarist.ttl
27
+ ].freeze,
28
+ "shapes/glossarist.shacl.ttl" => %w[
29
+ ontologies/shapes/glossarist.shacl.ttl
30
+ shapes/glossarist.shacl.ttl
31
+ ].freeze,
32
+ }.freeze
33
+
34
+ class << self
35
+ def call(ref: nil)
36
+ ref ||= latest_tag
37
+ FileUtils.mkdir_p(File.join(OUT_DIR, "shapes"))
38
+
39
+ TARGETS.each do |out_rel, candidates|
40
+ content = fetch_any(ref, candidates)
41
+ out_path = File.join(OUT_DIR, out_rel)
42
+ FileUtils.mkdir_p(File.dirname(out_path))
43
+ File.write(out_path, content)
44
+ puts " ✓ #{out_rel} (#{content.length} bytes)"
45
+ end
46
+
47
+ write_source_manifest(ref)
48
+ puts "\nSynced #{TARGETS.length} file(s) from #{REPO}@#{ref}."
49
+ end
50
+
51
+ private
52
+
53
+ def latest_tag
54
+ url = URI("https://api.github.com/repos/#{REPO}/releases/latest")
55
+ req = Net::HTTP::Get.new(url)
56
+ req["Accept"] = "application/vnd.github+json"
57
+ JSON.parse(Net::HTTP.start(url.hostname, url.port, use_ssl: true) { |http| http.request(req) }.body)
58
+ .fetch("tag_name")
59
+ rescue StandardError => e
60
+ warn "Could not determine latest concept-model tag: #{e.message}"
61
+ exit 1
62
+ end
63
+
64
+ def fetch_any(ref, candidates)
65
+ candidates.each do |path|
66
+ begin
67
+ return fetch_file(ref, path)
68
+ rescue StandardError
69
+ next
70
+ end
71
+ end
72
+ raise "Could not fetch any of: #{candidates.join(', ')}"
73
+ end
74
+
75
+ def fetch_file(ref, path)
76
+ url = URI("https://raw.githubusercontent.com/#{REPO}/#{ref}/#{path}")
77
+ Net::HTTP.get(url)
78
+ end
79
+
80
+ def write_source_manifest(ref)
81
+ manifest = {
82
+ "repo" => REPO,
83
+ "ref" => ref,
84
+ "syncedAt" => Time.now.utc.iso8601,
85
+ }
86
+ File.write(File.join(OUT_DIR, "SOURCE.json"),
87
+ JSON.pretty_generate(manifest) + "\n")
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -122,10 +122,11 @@ module Glossarist
122
122
 
123
123
  dd_attrs = if data
124
124
  data.class.detailed_definition_fields.to_h do |field|
125
- [field, build_gloss_definitions(data.public_send(field))]
125
+ key = field == :definition ? :definitions : field
126
+ [key, build_gloss_definitions(data.public_send(field))]
126
127
  end
127
128
  else
128
- { definition: [], notes: [], examples: [] }
129
+ { definitions: [], notes: [], examples: [] }
129
130
  end
130
131
 
131
132
  sources = build_gloss_sources(data&.sources)
@@ -65,7 +65,11 @@ module Glossarist
65
65
  else
66
66
  match_data = namespace.match(/\A(\h{8})-(\h{4})-(\h{4})-(\h{4})-(\h{4})(\h{8})\z/)
67
67
 
68
- unless match_data.present?
68
+ # `match_data` is a MatchData on success, nil on failure. Do not
69
+ # use ActiveSupport's `#present?` here — ActiveSupport is not a
70
+ # declared dependency of this gem, and `#present?` raises
71
+ # NoMethodError in bundles where it isn't loaded.
72
+ unless match_data
69
73
  raise ArgumentError,
70
74
  "Only UUIDs are valid namespace identifiers"
71
75
  end
@@ -6,6 +6,7 @@ module Glossarist
6
6
  attribute :sources, V3::ConceptSource,
7
7
  collection: Collections::ConceptSourceCollection,
8
8
  initialize_empty: true
9
+ attribute :dates, V3::ConceptDate, collection: true
9
10
  attribute :definition, V3::DetailedDefinition,
10
11
  collection: Collections::DetailedDefinitionCollection,
11
12
  initialize_empty: true
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glossarist
4
+ module V3
5
+ # V3 variant of ConceptDate. The base {Glossarist::ConceptDate} types
6
+ # `date` as `:date_time` (ISO 8601 datetime only), but the v3 schema
7
+ # (`concept-model/schemas/v3/concept.yaml`) declares
8
+ #
9
+ # concept_date:
10
+ # properties:
11
+ # date:
12
+ # type: string
13
+ # format: date
14
+ #
15
+ # which accepts any date string — full ISO 8601 datetime ("2023-01-01T00:00:00+00:00"),
16
+ # calendar date ("2023-01-01"), or year-only ("2023"). Datasets such as
17
+ # the IALA Dictionary (datasets/iala-*/concepts/*.yaml) use year-only
18
+ # strings for accepted/retired lifecycle markers, so the v3 model needs
19
+ # to round-trip those without losing the value.
20
+ #
21
+ # See BUG_REPORT.md for the full investigation.
22
+ class ConceptDate < Glossarist::ConceptDate
23
+ attribute :date, :string
24
+
25
+ key_value do
26
+ map :date, to: :date
27
+ map :type, to: :type
28
+ end
29
+
30
+ # The `date` is already a free-form string in V3, so the wire form
31
+ # is the value itself — no iso8601 coercion.
32
+ def to_yaml_date
33
+ date
34
+ end
35
+ end
36
+ end
37
+ end
@@ -4,6 +4,15 @@ module Glossarist
4
4
  module V3
5
5
  class LocalizedConcept < Glossarist::LocalizedConcept
6
6
  attribute :data, V3::ConceptData, default: -> { V3::ConceptData.new }
7
+
8
+ def date_accepted_from_yaml(model, value)
9
+ return if model.date_accepted
10
+
11
+ model.data.dates ||= []
12
+ model.data.dates << V3::ConceptDate.of_yaml(
13
+ { "date" => value, "type" => "accepted" },
14
+ )
15
+ end
7
16
  end
8
17
  end
9
18
  end
@@ -5,6 +5,8 @@ module Glossarist
5
5
  class ManagedConcept < Glossarist::ManagedConcept
6
6
  attribute :data, V3::ManagedConceptData, default: -> { V3::ManagedConceptData.new }
7
7
  attribute :related, V3::RelatedConcept, collection: true
8
+ attribute :dates, V3::ConceptDate, collection: true
9
+ attribute :date_accepted, V3::ConceptDate
8
10
  attribute :sources, V3::ConceptSource, collection: true
9
11
 
10
12
  key_value do
@@ -19,6 +21,12 @@ module Glossarist
19
21
  map :schema_version, to: :schema_version
20
22
  map :sources, to: :sources
21
23
  end
24
+
25
+ def date_accepted_from_yaml(model, value)
26
+ model.date_accepted = V3::ConceptDate.of_yaml(
27
+ { "date" => value, "type" => "accepted" },
28
+ )
29
+ end
22
30
  end
23
31
  end
24
32
  end
data/lib/glossarist/v3.rb CHANGED
@@ -4,6 +4,7 @@ module Glossarist
4
4
  module V3
5
5
  autoload :Configuration, "glossarist/v3/configuration"
6
6
  autoload :Citation, "glossarist/v3/citation"
7
+ autoload :ConceptDate, "glossarist/v3/concept_date"
7
8
  autoload :ConceptSource, "glossarist/v3/concept_source"
8
9
  autoload :DetailedDefinition, "glossarist/v3/detailed_definition"
9
10
  autoload :ConceptRef, "glossarist/v3/concept_ref"
@@ -15,6 +16,7 @@ module Glossarist
15
16
  autoload :ConceptDocument, "glossarist/v3/concept_document"
16
17
 
17
18
  Configuration.register_model(Citation, id: :citation)
19
+ Configuration.register_model(ConceptDate, id: :concept_date)
18
20
  Configuration.register_model(ConceptSource, id: :concept_source)
19
21
  Configuration.register_model(DetailedDefinition, id: :detailed_definition)
20
22
  Configuration.register_model(ConceptData, id: :concept_data)
@@ -54,11 +54,25 @@ module Glossarist
54
54
  graph
55
55
  end
56
56
 
57
+ # Returns a graph node identifier for the target of a relationship.
58
+ #
59
+ # For intra-edition references (ref.source is nil), the target is
60
+ # just ref.id — a clause identifier unique within the dataset.
61
+ #
62
+ # Returns nil for cross-edition references (ref.source is a URN,
63
+ # e.g. a +supersedes+ edge pointing at the previous edition's
64
+ # concept with the same clause id). Such edges cannot form
65
+ # cycles within the current dataset and are excluded from the
66
+ # graph. Without this exclusion, a cross-edition edge from
67
+ # concept 3.1.1.1 to its predecessor in another edition (which
68
+ # also has identifier 3.1.1.1) would look like a self-loop and
69
+ # trip the cycle detector with a false positive.
57
70
  def resolve_target_id(rel)
58
71
  ref = rel.ref
59
72
  return nil unless ref
73
+ return nil if ref.source
60
74
 
61
- ref.id || ref.source
75
+ ref.id
62
76
  end
63
77
 
64
78
  def detect_cycles(graph)
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rdf/turtle"
4
+ require "shacl"
5
+ require "pathname"
6
+
7
+ module Glossarist
8
+ module Validation
9
+ # Validates Turtle output against SHACL shapes.
10
+ #
11
+ # Shapes resolution order:
12
+ # 1. Explicit :shapes_path option
13
+ # 2. Vendored shapes at data/concept-model/shapes/glossarist.shacl.ttl
14
+ # (shipped with the gem, relative to the gem root)
15
+ class ShaclValidator
16
+ VENDORED_SHAPES_PATH =
17
+ File.expand_path(
18
+ "../../../data/concept-model/shapes/glossarist.shacl.ttl",
19
+ __dir__
20
+ ).freeze
21
+
22
+ attr_reader :shapes_path
23
+
24
+ def initialize(shapes_path: nil)
25
+ @shapes_path = shapes_path || self.class.default_shapes_path
26
+ end
27
+
28
+ def validate_files(paths)
29
+ validate_graphs(Array(paths).map { |p| load_graph(p) })
30
+ end
31
+
32
+ def validate_graphs(graphs)
33
+ shapes = SHACL::Shapes.from_graph(load_graph(@shapes_path))
34
+ failures = []
35
+
36
+ graphs.each do |graph|
37
+ report = shapes.execute(graph)
38
+ next if report.conform?
39
+
40
+ failures << Report.new(results: report.results)
41
+ end
42
+
43
+ AggregateReport.new(failures: failures)
44
+ end
45
+
46
+ class << self
47
+ def default_shapes_path
48
+ return VENDORED_SHAPES_PATH if File.exist?(VENDORED_SHAPES_PATH)
49
+
50
+ raise ArgumentError,
51
+ "No SHACL shapes path provided and the vendored shapes " \
52
+ "file (#{VENDORED_SHAPES_PATH}) is missing. Pass " \
53
+ "shapes_path: '/path/to/glossarist.shacl.ttl'."
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def load_graph(path)
60
+ return path if path.is_a?(RDF::Graph)
61
+
62
+ graph = RDF::Graph.new
63
+ RDF::Turtle::Reader.new(File.read(path)) do |reader|
64
+ reader.each_statement { |stmt| graph << stmt }
65
+ end
66
+ graph
67
+ end
68
+ end
69
+
70
+ Report = Struct.new(:results, keyword_init: true) do
71
+ def conformant?
72
+ results.empty?
73
+ end
74
+
75
+ def to_s
76
+ " #{results.length} violation(s)"
77
+ end
78
+ end
79
+
80
+ AggregateReport = Struct.new(:failures, keyword_init: true) do
81
+ def conformant?
82
+ failures.empty?
83
+ end
84
+
85
+ def to_s
86
+ lines = ["SHACL validation failed: #{failures.length} file(s) with violations"]
87
+ failures.each do |failure|
88
+ lines << failure.to_s
89
+ failure.results.each do |result|
90
+ lines << " #{result.path}: #{result.message}"
91
+ end
92
+ end
93
+ lines.join("\n")
94
+ end
95
+ end
96
+ end
97
+ end
@@ -8,5 +8,6 @@ module Glossarist
8
8
  autoload :Rules, "glossarist/validation/rules"
9
9
  autoload :BibliographyIndex, "glossarist/validation/bibliography_index"
10
10
  autoload :AssetIndex, "glossarist/validation/asset_index"
11
+ autoload :ShaclValidator, "glossarist/validation/shacl_validator"
11
12
  end
12
13
  end
@@ -4,5 +4,5 @@
4
4
  #
5
5
 
6
6
  module Glossarist
7
- VERSION = "2.8.17"
7
+ VERSION = "2.9.0"
8
8
  end
data/lib/glossarist.rb CHANGED
@@ -23,6 +23,7 @@ module Glossarist
23
23
  autoload :ConceptData, "glossarist/concept_data"
24
24
  autoload :ConceptRef, "glossarist/concept_ref"
25
25
  autoload :ConceptReference, "glossarist/concept_reference"
26
+ autoload :Reference, "glossarist/reference"
26
27
  autoload :ReferenceExtractor, "glossarist/reference_extractor"
27
28
  autoload :ReferenceResolver, "glossarist/reference_resolver"
28
29
  autoload :ResolutionAdapter, "glossarist/resolution_adapter"
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.8.17
4
+ version: 2.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-06-21 00:00:00.000000000 Z
11
+ date: 2026-07-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: lutaml-model
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '2.3'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rdf-turtle
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.3'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.3'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: relaton
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -92,6 +106,20 @@ dependencies:
92
106
  - - "<"
93
107
  - !ruby/object:Gem::Version
94
108
  version: '3'
109
+ - !ruby/object:Gem::Dependency
110
+ name: shacl
111
+ requirement: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - "~>"
114
+ - !ruby/object:Gem::Version
115
+ version: '0.4'
116
+ type: :runtime
117
+ prerelease: false
118
+ version_requirements: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - "~>"
121
+ - !ruby/object:Gem::Version
122
+ version: '0.4'
95
123
  - !ruby/object:Gem::Dependency
96
124
  name: sts
97
125
  requirement: !ruby/object:Gem::Requirement
@@ -170,6 +198,11 @@ files:
170
198
  - README.adoc
171
199
  - Rakefile
172
200
  - config.yml
201
+ - data/concept-model/README.md
202
+ - data/concept-model/SOURCE.json
203
+ - data/concept-model/glossarist.context.jsonld
204
+ - data/concept-model/glossarist.ttl
205
+ - data/concept-model/shapes/glossarist.shacl.ttl
173
206
  - exe/glossarist
174
207
  - glossarist.gemspec
175
208
  - lib/glossarist.rb
@@ -276,6 +309,7 @@ files:
276
309
  - lib/glossarist/rdf/gloss_pronunciation.rb
277
310
  - lib/glossarist/rdf/gloss_reference.rb
278
311
  - lib/glossarist/rdf/localized_literal.rb
312
+ - lib/glossarist/rdf/lutaml_ext.rb
279
313
  - lib/glossarist/rdf/namespaces.rb
280
314
  - lib/glossarist/rdf/namespaces/dcterms_namespace.rb
281
315
  - lib/glossarist/rdf/namespaces/glossarist_namespace.rb
@@ -288,6 +322,7 @@ files:
288
322
  - lib/glossarist/rdf/relationship_predicates.rb
289
323
  - lib/glossarist/rdf/v3.rb
290
324
  - lib/glossarist/rdf/v3/configuration.rb
325
+ - lib/glossarist/reference.rb
291
326
  - lib/glossarist/reference_extractor.rb
292
327
  - lib/glossarist/reference_resolver.rb
293
328
  - lib/glossarist/register_data.rb
@@ -313,6 +348,9 @@ files:
313
348
  - lib/glossarist/sts/term_mapper.rb
314
349
  - lib/glossarist/table.rb
315
350
  - lib/glossarist/table_reference.rb
351
+ - lib/glossarist/tasks/shacl.rake
352
+ - lib/glossarist/tasks/sync.rake
353
+ - lib/glossarist/tasks/sync_model.rb
316
354
  - lib/glossarist/transforms.rb
317
355
  - lib/glossarist/transforms/concept_to_gloss_transform.rb
318
356
  - lib/glossarist/transforms/concept_to_tbx_transform.rb
@@ -339,6 +377,7 @@ files:
339
377
  - lib/glossarist/v3.rb
340
378
  - lib/glossarist/v3/citation.rb
341
379
  - lib/glossarist/v3/concept_data.rb
380
+ - lib/glossarist/v3/concept_date.rb
342
381
  - lib/glossarist/v3/concept_document.rb
343
382
  - lib/glossarist/v3/concept_ref.rb
344
383
  - lib/glossarist/v3/concept_source.rb
@@ -401,6 +440,7 @@ files:
401
440
  - lib/glossarist/validation/rules/source_urn_format_rule.rb
402
441
  - lib/glossarist/validation/rules/terms_presence_rule.rb
403
442
  - lib/glossarist/validation/rules/uuid_format_rule.rb
443
+ - lib/glossarist/validation/shacl_validator.rb
404
444
  - lib/glossarist/validation/validation_issue.rb
405
445
  - lib/glossarist/validation_result.rb
406
446
  - lib/glossarist/version.rb