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
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# Entry-point require triggers the autoload chain
|
|
4
|
+
# (Glossarist → Tasks → SyncModel) defined in lib/glossarist.rb.
|
|
5
|
+
# Internal-path requires are forbidden by the autoload rule.
|
|
6
|
+
require "glossarist"
|
|
4
7
|
|
|
5
8
|
namespace :glossarist do
|
|
6
9
|
namespace :sync do
|
|
@@ -33,18 +33,20 @@ module Glossarist
|
|
|
33
33
|
Designation::Symbol => :build_gloss_symbol,
|
|
34
34
|
}.freeze
|
|
35
35
|
|
|
36
|
-
def self.transform(managed_concept,
|
|
37
|
-
new(managed_concept, options).build
|
|
36
|
+
def self.transform(managed_concept, relations: [], **options)
|
|
37
|
+
new(managed_concept, relations: relations, **options).build
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
def self.transform_document(concepts, figures: [], tables: [],
|
|
40
|
+
def self.transform_document(concepts, relations: [], figures: [], tables: [],
|
|
41
41
|
formulas: [], **options)
|
|
42
|
-
new(nil, options).build_document(
|
|
43
|
-
|
|
42
|
+
new(nil, relations: relations, **options).build_document(
|
|
43
|
+
concepts, figures: figures, tables: tables, formulas: formulas
|
|
44
|
+
)
|
|
44
45
|
end
|
|
45
46
|
|
|
46
|
-
def initialize(managed_concept,
|
|
47
|
+
def initialize(managed_concept, relations: [], **options)
|
|
47
48
|
@concept = managed_concept
|
|
49
|
+
@relations = Array(relations)
|
|
48
50
|
@options = options
|
|
49
51
|
end
|
|
50
52
|
|
|
@@ -63,7 +65,7 @@ module Glossarist
|
|
|
63
65
|
Rdf::GlossDocument.to_turtle(doc)
|
|
64
66
|
end
|
|
65
67
|
|
|
66
|
-
def to_turtle(concepts_or_concept = nil, figures: [], tables: [],
|
|
68
|
+
def to_turtle(concepts_or_concept = nil, relations: nil, figures: [], tables: [],
|
|
67
69
|
formulas: [])
|
|
68
70
|
if concepts_or_concept.is_a?(Array)
|
|
69
71
|
build_document(concepts_or_concept, figures: figures,
|
|
@@ -72,12 +74,12 @@ module Glossarist
|
|
|
72
74
|
target = concepts_or_concept || @concept
|
|
73
75
|
return "" unless target
|
|
74
76
|
|
|
75
|
-
gc = build_gloss_concept(target)
|
|
77
|
+
gc = build_gloss_concept(target, relations: relations)
|
|
76
78
|
Rdf::GlossConcept.to_turtle(gc)
|
|
77
79
|
end
|
|
78
80
|
end
|
|
79
81
|
|
|
80
|
-
def to_jsonld(concepts_or_concept = nil, figures: [], tables: [],
|
|
82
|
+
def to_jsonld(concepts_or_concept = nil, relations: nil, figures: [], tables: [],
|
|
81
83
|
formulas: [])
|
|
82
84
|
if concepts_or_concept.is_a?(Array)
|
|
83
85
|
gloss_concepts = concepts_or_concept.map do |c|
|
|
@@ -94,7 +96,7 @@ module Glossarist
|
|
|
94
96
|
target = concepts_or_concept || @concept
|
|
95
97
|
return "" unless target
|
|
96
98
|
|
|
97
|
-
gc = build_gloss_concept(target)
|
|
99
|
+
gc = build_gloss_concept(target, relations: relations)
|
|
98
100
|
Rdf::GlossConcept.to_jsonld(gc)
|
|
99
101
|
end
|
|
100
102
|
end
|
|
@@ -108,9 +110,10 @@ module Glossarist
|
|
|
108
110
|
|
|
109
111
|
private
|
|
110
112
|
|
|
111
|
-
attr_reader :concept, :options
|
|
113
|
+
attr_reader :concept, :relations, :options
|
|
112
114
|
|
|
113
|
-
def build_gloss_concept(managed_concept)
|
|
115
|
+
def build_gloss_concept(managed_concept, relations: nil)
|
|
116
|
+
relation_list = relations || @relations
|
|
114
117
|
identifier = managed_concept.data&.id || managed_concept.identifier
|
|
115
118
|
|
|
116
119
|
localizations = managed_concept.localizations.each_value.map do |l10n|
|
|
@@ -130,46 +133,66 @@ module Glossarist
|
|
|
130
133
|
domains: build_gloss_domains(managed_concept.data&.domains,
|
|
131
134
|
identifier),
|
|
132
135
|
dates: build_gloss_dates(managed_concept.dates, identifier),
|
|
133
|
-
partitive_relations:
|
|
134
|
-
|
|
136
|
+
partitive_relations: build_gloss_hyperedges(
|
|
137
|
+
relation_list, V3::PartitiveHyperedge,
|
|
138
|
+
:build_gloss_partitive_member, :partitive_member_ids,
|
|
139
|
+
:partitive_members, Rdf::GlossPartitiveRelation, identifier,
|
|
140
|
+
),
|
|
141
|
+
generic_relations: build_gloss_hyperedges(
|
|
142
|
+
relation_list, V3::GenericHyperedge,
|
|
143
|
+
:build_gloss_generic_member, :generic_member_ids,
|
|
144
|
+
:generic_members, Rdf::GlossGenericRelation, identifier,
|
|
135
145
|
),
|
|
136
146
|
**rel_targets,
|
|
137
147
|
)
|
|
138
148
|
end
|
|
139
149
|
|
|
140
|
-
#
|
|
141
|
-
#
|
|
142
|
-
#
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
150
|
+
# Type-blind hyperedge builder. Dispatches via HyperedgeRegistry;
|
|
151
|
+
# the only per-type data (member class, RDF view attribute names,
|
|
152
|
+
# RDF view class) is passed in. Adding a new hyperedge type means
|
|
153
|
+
# adding one entry to the calling sites above; no edit to this
|
|
154
|
+
# method.
|
|
155
|
+
def build_gloss_hyperedges(relations, hyperedge_class, member_builder,
|
|
156
|
+
member_ids_attr, members_attr,
|
|
157
|
+
gloss_relation_class, identifier)
|
|
158
|
+
Array(relations)
|
|
159
|
+
.select { |rel| rel.is_a?(hyperedge_class) }
|
|
160
|
+
.map { |rel| build_one_gloss_hyperedge(rel, member_builder, member_ids_attr, members_attr, gloss_relation_class, identifier) }
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def build_one_gloss_hyperedge(rel, member_builder, member_ids_attr,
|
|
164
|
+
members_attr, gloss_relation_class, identifier)
|
|
165
|
+
source_members = Array(rel.members)
|
|
166
|
+
gloss_members = source_members.map { |m| send(member_builder, m) }
|
|
167
|
+
member_uris = source_members
|
|
168
|
+
.select { |m| m.ref.is_a?(Glossarist::ConceptRef) }
|
|
169
|
+
.map { |m| concept_ref_uri(m.ref) }
|
|
170
|
+
|
|
171
|
+
gloss_relation_class.new(
|
|
172
|
+
identifier: identifier.to_s,
|
|
173
|
+
comprehensive_uri: concept_ref_uri(rel.comprehensive),
|
|
174
|
+
member_ids_attr => member_uris,
|
|
175
|
+
members_attr => gloss_members,
|
|
176
|
+
completeness: rel.completeness,
|
|
177
|
+
criterion: rel.criterion,
|
|
178
|
+
)
|
|
147
179
|
end
|
|
148
180
|
|
|
149
|
-
def
|
|
150
|
-
return
|
|
151
|
-
|
|
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) }
|
|
181
|
+
def build_gloss_partitive_member(member)
|
|
182
|
+
return Rdf::GlossPartitiveMember.new unless member.is_a?(V3::PartitiveMember)
|
|
158
183
|
|
|
159
|
-
|
|
160
|
-
identifier: identifier.to_s,
|
|
161
|
-
comprehensive_uri: concept_ref_uri(rel.comprehensive),
|
|
162
|
-
partitive_member_ids: member_uris,
|
|
163
|
-
partitive_members: gloss_members,
|
|
164
|
-
completeness: rel.completeness,
|
|
165
|
-
criterion: rel.criterion,
|
|
166
|
-
)
|
|
167
|
-
end
|
|
184
|
+
build_gloss_nary_member(Rdf::GlossPartitiveMember, member)
|
|
168
185
|
end
|
|
169
186
|
|
|
170
|
-
def
|
|
171
|
-
return Rdf::
|
|
187
|
+
def build_gloss_generic_member(member)
|
|
188
|
+
return Rdf::GlossGenericMember.new unless member.is_a?(V3::GenericMember)
|
|
172
189
|
|
|
190
|
+
build_gloss_nary_member(Rdf::GlossGenericMember, member)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Shared member builder. Per-class metadata (the rdf view class
|
|
194
|
+
# is dispatched by the caller) keeps the duplication to one place.
|
|
195
|
+
def build_gloss_nary_member(gloss_member_class, member)
|
|
173
196
|
ref = member.ref
|
|
174
197
|
ref_attrs = if ref.is_a?(Glossarist::ConceptRef)
|
|
175
198
|
{ ref_id: ref.id, ref_source: ref.source, ref_text: ref.text }
|
|
@@ -177,7 +200,7 @@ module Glossarist
|
|
|
177
200
|
{}
|
|
178
201
|
end
|
|
179
202
|
|
|
180
|
-
|
|
203
|
+
gloss_member_class.new(
|
|
181
204
|
**ref_attrs,
|
|
182
205
|
presence: member.presence,
|
|
183
206
|
count: member.count,
|
|
@@ -185,19 +208,16 @@ module Glossarist
|
|
|
185
208
|
)
|
|
186
209
|
end
|
|
187
210
|
|
|
188
|
-
# Single SSOT for ConceptRef → concept URI.
|
|
189
|
-
#
|
|
190
|
-
#
|
|
191
|
-
#
|
|
192
|
-
# ones of the same id.
|
|
211
|
+
# Single SSOT for ConceptRef → concept URI. Uses
|
|
212
|
+
# Glossarist::ConceptRef.qualified_id for the [source, id] string
|
|
213
|
+
# so external ConceptRefs don't collide with local ones of the
|
|
214
|
+
# same id.
|
|
193
215
|
def concept_ref_uri(ref)
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
slug = [ref.source, ref.id || ref.text].compact.reject(&:empty?)
|
|
197
|
-
return nil if slug.empty?
|
|
216
|
+
qualified = Glossarist::ConceptRef.qualified_id(ref)
|
|
217
|
+
return nil if qualified.nil?
|
|
198
218
|
|
|
199
219
|
Glossarist::Rdf::Namespaces::GlossaristNamespace.uri +
|
|
200
|
-
"concept/#{
|
|
220
|
+
"concept/#{qualified.split(':', 2).join('/')}"
|
|
201
221
|
end
|
|
202
222
|
|
|
203
223
|
def build_gloss_localized_concept(l10n, concept_id)
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module Glossarist
|
|
6
|
+
module V3
|
|
7
|
+
# AbstractHyperedge — abstract base shape for all n-ary
|
|
8
|
+
# concept-system relations (PartitiveHyperedge, GenericHyperedge,
|
|
9
|
+
# future AssociativeRelation, SequentialRelation).
|
|
10
|
+
#
|
|
11
|
+
# Carries the shared fields: comprehensive, members[2..*],
|
|
12
|
+
# completeness, criterion, sources, notes, status.
|
|
13
|
+
#
|
|
14
|
+
# Concrete leaves override `members` to specify the typed member
|
|
15
|
+
# class. Shared validations live here.
|
|
16
|
+
class AbstractHyperedge < Lutaml::Model::Serializable
|
|
17
|
+
DEFAULT_COMPLETENESS = "complete"
|
|
18
|
+
|
|
19
|
+
attribute :comprehensive, ConceptRef
|
|
20
|
+
attribute :members, HyperedgeMember, collection: true
|
|
21
|
+
attribute :completeness, :string,
|
|
22
|
+
values: Glossarist::GlossaryDefinition::COMPLETENESS_VALUES,
|
|
23
|
+
default: -> { DEFAULT_COMPLETENESS }
|
|
24
|
+
attribute :criterion, :hash
|
|
25
|
+
attribute :sources, ConceptSource, collection: true
|
|
26
|
+
attribute :notes, :hash
|
|
27
|
+
attribute :status, :string,
|
|
28
|
+
values: Glossarist::GlossaryDefinition::CONCEPT_STATUSES
|
|
29
|
+
|
|
30
|
+
# Per-file identity — set by RelationLoader on parse, used by
|
|
31
|
+
# HyperedgeWriter on serialize. The wire field is `$id` (JSON
|
|
32
|
+
# Schema convention); the value is `<comp-id>/<criterion-slug>`.
|
|
33
|
+
# Not in key_value because lutaml::Model key_value does not
|
|
34
|
+
# natively express `$`-prefixed keys; handled via custom
|
|
35
|
+
# round-trip in HyperedgeWriter / RelationLoader.
|
|
36
|
+
attr_accessor :file_id
|
|
37
|
+
|
|
38
|
+
key_value do
|
|
39
|
+
map :comprehensive, to: :comprehensive
|
|
40
|
+
map :members, to: :members
|
|
41
|
+
map :completeness, to: :completeness
|
|
42
|
+
map :criterion, to: :criterion
|
|
43
|
+
map :sources, to: :sources
|
|
44
|
+
map :notes, to: :notes
|
|
45
|
+
map :status, to: :status
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def initialize(*)
|
|
49
|
+
if instance_of?(AbstractHyperedge)
|
|
50
|
+
raise NotImplementedError,
|
|
51
|
+
"AbstractHyperedge is abstract; instantiate " \
|
|
52
|
+
"PartitiveHyperedge or GenericHyperedge instead"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
super
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def validate!
|
|
59
|
+
validate_comprehensive!
|
|
60
|
+
validate_members!
|
|
61
|
+
validate_self_loop!
|
|
62
|
+
validate_completeness!
|
|
63
|
+
self
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def complete?
|
|
67
|
+
completeness == "complete"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def partial?
|
|
71
|
+
completeness == "partial"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# ISO 704: a rake connects to two or more members. A single
|
|
75
|
+
# binary edge is not an n-ary relation.
|
|
76
|
+
def coordinate?
|
|
77
|
+
members.length >= 2
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Per-file identity derived from comprehensive + criterion.
|
|
81
|
+
# Format: `<comprehensive-id-dir>/<criterion-slug>` where the
|
|
82
|
+
# dir is `<source>-<id>` (downcased, kebab-case) and the slug
|
|
83
|
+
# is the English criterion (kebab-case) or a structural
|
|
84
|
+
# fallback. Stable across runs for the same content.
|
|
85
|
+
#
|
|
86
|
+
# Returns nil if comprehensive is empty.
|
|
87
|
+
def derived_file_id
|
|
88
|
+
return nil unless comprehensive.is_a?(ConceptRef) && comprehensive.id
|
|
89
|
+
|
|
90
|
+
comp_dir = comprehensive_dir_name
|
|
91
|
+
return nil unless comp_dir
|
|
92
|
+
|
|
93
|
+
slug = criterion_slug
|
|
94
|
+
"#{comp_dir}/#{slug}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# File path under `relations/` directory. Uses #derived_file_id
|
|
98
|
+
# unless #file_id was explicitly set (e.g., by RelationLoader
|
|
99
|
+
# preserving the source path on parse).
|
|
100
|
+
def file_path(relations_dir)
|
|
101
|
+
id = file_id || derived_file_id
|
|
102
|
+
return nil unless id
|
|
103
|
+
|
|
104
|
+
File.join(relations_dir, "#{id}.yaml")
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Comprehensive concept as a directory-safe name:
|
|
108
|
+
# `<source>-<id>` lowercased, kebab-case, special chars
|
|
109
|
+
# (including dots) replaced with dashes. Matches the concept-model
|
|
110
|
+
# fixtures (vim-112-02-09, oiml-5-1, example-116-01-01).
|
|
111
|
+
def comprehensive_dir_name
|
|
112
|
+
return nil unless comprehensive.is_a?(ConceptRef)
|
|
113
|
+
|
|
114
|
+
parts = [comprehensive.source, comprehensive.id].compact.reject(&:empty?)
|
|
115
|
+
return nil if parts.empty?
|
|
116
|
+
|
|
117
|
+
parts.join("-").downcase.gsub(/[^a-z0-9\-]/, "-")
|
|
118
|
+
.gsub(/-{2,}/, "-").gsub(/\A-|-\z/, "")
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Kebab-case slug derived from the English criterion. Falls back
|
|
122
|
+
# to "decomposition-N" when no English criterion exists (N is
|
|
123
|
+
# derived from the criterion hash for stability).
|
|
124
|
+
def criterion_slug
|
|
125
|
+
eng = criterion.is_a?(Hash) ? (criterion["eng"] || criterion[:eng]) : nil
|
|
126
|
+
return "decomposition-#{criterion_hash}" if eng.nil? || eng.to_s.empty?
|
|
127
|
+
|
|
128
|
+
eng.to_s.downcase.gsub(/[^a-z0-9]+/, "-").gsub(/\A-|-\z/, "")
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
def criterion_hash
|
|
134
|
+
return "0" unless criterion.is_a?(Hash) && !criterion.empty?
|
|
135
|
+
|
|
136
|
+
Digest::MD5.hexdigest(criterion.sort.to_h.inspect)[0..5]
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def validate_comprehensive!
|
|
140
|
+
return if comprehensive.is_a?(ConceptRef) &&
|
|
141
|
+
(comprehensive.source || comprehensive.id || comprehensive.text)
|
|
142
|
+
|
|
143
|
+
raise ArgumentError,
|
|
144
|
+
"#{self.class.name}#comprehensive must be a non-empty " \
|
|
145
|
+
"ConceptRef (source, id, or text required)"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def validate_members!
|
|
149
|
+
if members.empty?
|
|
150
|
+
raise ArgumentError, "#{self.class.name} requires at least one member"
|
|
151
|
+
end
|
|
152
|
+
unless coordinate?
|
|
153
|
+
raise ArgumentError,
|
|
154
|
+
"#{self.class.name} requires >=2 members (ISO 704); a single " \
|
|
155
|
+
"binary edge should be used instead"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
members.each(&:validate!)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def validate_self_loop!
|
|
162
|
+
return unless comprehensive.is_a?(ConceptRef)
|
|
163
|
+
|
|
164
|
+
comp_key = [comprehensive.source, comprehensive.id]
|
|
165
|
+
members.each do |member|
|
|
166
|
+
next unless member.ref.is_a?(ConceptRef)
|
|
167
|
+
next unless comp_key == [member.ref.source, member.ref.id]
|
|
168
|
+
|
|
169
|
+
raise ArgumentError,
|
|
170
|
+
"#{self.class.name}#members cannot include the comprehensive"
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def validate_completeness!
|
|
175
|
+
return if completeness.nil?
|
|
176
|
+
|
|
177
|
+
unless Glossarist::GlossaryDefinition::COMPLETENESS_VALUES
|
|
178
|
+
.include?(completeness)
|
|
179
|
+
raise ArgumentError,
|
|
180
|
+
"#{self.class.name}#completeness has invalid value " \
|
|
181
|
+
"#{completeness.inspect}; must be one of " \
|
|
182
|
+
"#{GlossaryDefinition::COMPLETENESS_VALUES.join(', ')}"
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glossarist
|
|
4
|
+
module V3
|
|
5
|
+
# DefinitionType — ISO 704:2022 §5.3 definition strategy constants.
|
|
6
|
+
#
|
|
7
|
+
# SSOT for the set of definition-type values. `DetailedDefinition#type`
|
|
8
|
+
# reads `VALUES` from here, not from `GlossaryDefinition`. The config
|
|
9
|
+
# (config.yml) feeds `GlossaryDefinition::DEFINITION_TYPE_VALUES`, which
|
|
10
|
+
# is consumed here so the source-of-truth chain is:
|
|
11
|
+
#
|
|
12
|
+
# config.yml → GlossaryDefinition::DEFINITION_TYPE_VALUES
|
|
13
|
+
# → DefinitionType::VALUES (consumer-facing)
|
|
14
|
+
# → DetailedDefinition#type (model attribute)
|
|
15
|
+
module DefinitionType
|
|
16
|
+
INTENSIONAL = "intensional"
|
|
17
|
+
EXTENSIONAL = "extensional"
|
|
18
|
+
PARTITIVE = "partitive"
|
|
19
|
+
TRANSLATED = "translated"
|
|
20
|
+
|
|
21
|
+
DEFAULT = INTENSIONAL
|
|
22
|
+
|
|
23
|
+
VALUES = Glossarist::GlossaryDefinition::DEFINITION_TYPE_VALUES.freeze
|
|
24
|
+
|
|
25
|
+
def self.valid?(value)
|
|
26
|
+
VALUES.include?(value)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -6,9 +6,13 @@ module Glossarist
|
|
|
6
6
|
attribute :sources, V3::ConceptSource, collection: true
|
|
7
7
|
attribute :examples, V3::DetailedDefinition, collection: true,
|
|
8
8
|
initialize_empty: true
|
|
9
|
+
attribute :type, :string,
|
|
10
|
+
values: DefinitionType::VALUES,
|
|
11
|
+
default: -> { DefinitionType::DEFAULT }
|
|
9
12
|
|
|
10
13
|
key_value do
|
|
11
14
|
map :content, to: :content
|
|
15
|
+
map :type, to: :type
|
|
12
16
|
map :sources, to: :sources
|
|
13
17
|
map :examples, to: :examples
|
|
14
18
|
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glossarist
|
|
4
|
+
module V3
|
|
5
|
+
# GenericHyperedge — an ISO 704 / ISO 1087-1 / ISO 12620 generic
|
|
6
|
+
# hyperedge connecting a comprehensive concept (the genus) to two
|
|
7
|
+
# or more specific concepts (the species) which together
|
|
8
|
+
# constitute a decomposition by some criterion of subdivision.
|
|
9
|
+
#
|
|
10
|
+
# Mirror of PartitiveHyperedge. The `comprehensive` field denotes
|
|
11
|
+
# the genus concept. Multiple GenericHyperedges on the same
|
|
12
|
+
# comprehensive distinguished by `criterion` is the OIML pattern
|
|
13
|
+
# (5.1 measurement standard has 6 criterion groups).
|
|
14
|
+
#
|
|
15
|
+
# See docs/design/generic-relation.md (concept-model repo).
|
|
16
|
+
#
|
|
17
|
+
# The `key_value` mapping is inherited from AbstractHyperedge
|
|
18
|
+
# (single SSOT). Only the typed member collection is narrowed here.
|
|
19
|
+
#
|
|
20
|
+
# Per-class metadata block — see PartitiveHyperedge for field docs.
|
|
21
|
+
class GenericHyperedge < AbstractHyperedge
|
|
22
|
+
WIRE_KEY = "generic_relations"
|
|
23
|
+
TYPE_TAG = "generic_relation"
|
|
24
|
+
RDF_TYPE = "gloss:GenericRelation"
|
|
25
|
+
MEMBER_CLASS = GenericMember
|
|
26
|
+
V1_WIRE_KEYS = [].freeze
|
|
27
|
+
KIND_LABEL = "GEN"
|
|
28
|
+
|
|
29
|
+
attribute :members, GenericMember, collection: true
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
HyperedgeRegistry.register(GenericHyperedge)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glossarist
|
|
4
|
+
module V3
|
|
5
|
+
# GenericMember — one member of a GenericHyperedge. Carries the same
|
|
6
|
+
# MECE dimensions as PartitiveMember; the comprehensive of its
|
|
7
|
+
# parent GenericHyperedge denotes the genus concept.
|
|
8
|
+
#
|
|
9
|
+
# See docs/design/generic-relation.md and docs/design/abstract-nary-relation.md
|
|
10
|
+
# (concept-model repo).
|
|
11
|
+
class GenericMember < HyperedgeMember
|
|
12
|
+
# Inherits all attributes and validations from HyperedgeMember.
|
|
13
|
+
# Declared as a distinct class for type safety and to leave room
|
|
14
|
+
# for future generic-specific extensions.
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glossarist
|
|
4
|
+
module V3
|
|
5
|
+
# HyperedgeIndex — derived reverse-lookup index over a flat list
|
|
6
|
+
# of hyperedges.
|
|
7
|
+
#
|
|
8
|
+
# A hyperedge is directional: 1 comprehensive → N members. To
|
|
9
|
+
# answer the reverse query ("which hyperedges is X a member of?"),
|
|
10
|
+
# build a HyperedgeIndex on demand from the flat list and consult
|
|
11
|
+
# #for_member. The index is NOT stored on the concept — it's a
|
|
12
|
+
# derived view.
|
|
13
|
+
#
|
|
14
|
+
# Use cases:
|
|
15
|
+
# index.for_comprehensive("OIML:5.1")
|
|
16
|
+
# # => 6 hyperedges (different criteria — the OIML pattern)
|
|
17
|
+
# index.for_member("OIML:5.13")
|
|
18
|
+
# # => hyperedges where 5.13 appears as a member
|
|
19
|
+
class HyperedgeIndex
|
|
20
|
+
attr_reader :by_comprehensive, :by_member
|
|
21
|
+
|
|
22
|
+
def initialize(hyperedges)
|
|
23
|
+
@by_comprehensive = {}
|
|
24
|
+
@by_member = {}
|
|
25
|
+
|
|
26
|
+
Array(hyperedges).each do |h|
|
|
27
|
+
register_comprehensive(h)
|
|
28
|
+
register_members(h)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
@by_comprehensive.freeze
|
|
32
|
+
@by_member.freeze
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# All hyperedges where the given concept is the comprehensive.
|
|
36
|
+
# Accepts either a qualified-id string ("VIM:1.2") or a ConceptRef.
|
|
37
|
+
def for_comprehensive(qualified_id_or_ref)
|
|
38
|
+
key = resolve_key(qualified_id_or_ref)
|
|
39
|
+
@by_comprehensive[key] || []
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# All hyperedges where the given concept is one of the members.
|
|
43
|
+
# Accepts either a qualified-id string or a ConceptRef.
|
|
44
|
+
def for_member(qualified_id_or_ref)
|
|
45
|
+
key = resolve_key(qualified_id_or_ref)
|
|
46
|
+
@by_member[key] || []
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Every concept that appears anywhere in this hyperedge set
|
|
50
|
+
# (as comprehensive OR as a member), with its roles.
|
|
51
|
+
def all_concept_ids
|
|
52
|
+
(@by_comprehensive.keys | @by_member.keys).freeze
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def register_comprehensive(hyperedge)
|
|
58
|
+
return unless hyperedge.is_a?(AbstractHyperedge)
|
|
59
|
+
|
|
60
|
+
key = Glossarist::ConceptRef.qualified_id(hyperedge.comprehensive)
|
|
61
|
+
return unless key
|
|
62
|
+
|
|
63
|
+
(@by_comprehensive[key] ||= []) << hyperedge
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def register_members(hyperedge)
|
|
67
|
+
return unless hyperedge.is_a?(AbstractHyperedge)
|
|
68
|
+
|
|
69
|
+
Array(hyperedge.members).each do |m|
|
|
70
|
+
next unless m.is_a?(HyperedgeMember)
|
|
71
|
+
next unless m.ref.is_a?(Glossarist::ConceptRef)
|
|
72
|
+
|
|
73
|
+
key = Glossarist::ConceptRef.qualified_id(m.ref)
|
|
74
|
+
next unless key
|
|
75
|
+
|
|
76
|
+
(@by_member[key] ||= []) << hyperedge
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def resolve_key(qualified_id_or_ref)
|
|
81
|
+
if qualified_id_or_ref.is_a?(Glossarist::ConceptRef)
|
|
82
|
+
Glossarist::ConceptRef.qualified_id(qualified_id_or_ref)
|
|
83
|
+
else
|
|
84
|
+
qualified_id_or_ref.to_s
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|