glossarist 2.13.6 → 2.13.8
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 +41 -0
- data/lib/glossarist/rdf/gloss_generic_relation.rb +44 -0
- data/lib/glossarist/rdf/gloss_nary_member.rb +52 -0
- data/lib/glossarist/rdf/gloss_nary_relation.rb +56 -0
- data/lib/glossarist/rdf/gloss_partitive_member.rb +7 -25
- 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 +85 -53
- 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 +52 -0
- data/lib/glossarist/v3/hyperedge_index.rb +89 -0
- data/lib/glossarist/v3/hyperedge_member.rb +102 -0
- data/lib/glossarist/v3/hyperedge_registry.rb +72 -0
- data/lib/glossarist/v3/hyperedge_writer.rb +89 -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 +13 -92
- 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 +18 -4
- data/lib/glossarist/v3/partitive_relation.rb +0 -114
|
@@ -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. " \
|
|
@@ -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,68 @@ 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
|
+
.grep(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)
|
|
189
|
+
|
|
190
|
+
build_gloss_nary_member(Rdf::GlossGenericMember, member)
|
|
191
|
+
end
|
|
172
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
|
+
# Per-type fields are dispatched by class — PartitiveMember
|
|
196
|
+
# carries `is_delimiting`; GenericMember carries `characteristic`.
|
|
197
|
+
def build_gloss_nary_member(gloss_member_class, member)
|
|
173
198
|
ref = member.ref
|
|
174
199
|
ref_attrs = if ref.is_a?(Glossarist::ConceptRef)
|
|
175
200
|
{ ref_id: ref.id, ref_source: ref.source, ref_text: ref.text }
|
|
@@ -177,27 +202,34 @@ module Glossarist
|
|
|
177
202
|
{}
|
|
178
203
|
end
|
|
179
204
|
|
|
180
|
-
|
|
205
|
+
type_attrs =
|
|
206
|
+
case member
|
|
207
|
+
when V3::PartitiveMember
|
|
208
|
+
{ is_delimiting: member.is_delimiting }
|
|
209
|
+
when V3::GenericMember
|
|
210
|
+
{ characteristic: member.characteristic }
|
|
211
|
+
else
|
|
212
|
+
{}
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
gloss_member_class.new(
|
|
181
216
|
**ref_attrs,
|
|
182
217
|
presence: member.presence,
|
|
183
218
|
count: member.count,
|
|
184
|
-
|
|
219
|
+
**type_attrs,
|
|
185
220
|
)
|
|
186
221
|
end
|
|
187
222
|
|
|
188
|
-
# Single SSOT for ConceptRef → concept URI.
|
|
189
|
-
#
|
|
190
|
-
#
|
|
191
|
-
#
|
|
192
|
-
# ones of the same id.
|
|
223
|
+
# Single SSOT for ConceptRef → concept URI. Uses
|
|
224
|
+
# Glossarist::ConceptRef.qualified_id for the [source, id] string
|
|
225
|
+
# so external ConceptRefs don't collide with local ones of the
|
|
226
|
+
# same id.
|
|
193
227
|
def concept_ref_uri(ref)
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
slug = [ref.source, ref.id || ref.text].compact.reject(&:empty?)
|
|
197
|
-
return nil if slug.empty?
|
|
228
|
+
qualified = Glossarist::ConceptRef.qualified_id(ref)
|
|
229
|
+
return nil if qualified.nil?
|
|
198
230
|
|
|
199
231
|
Glossarist::Rdf::Namespaces::GlossaristNamespace.uri +
|
|
200
|
-
"concept/#{
|
|
232
|
+
"concept/#{qualified.split(':', 2).join('/')}"
|
|
201
233
|
end
|
|
202
234
|
|
|
203
235
|
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,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glossarist
|
|
4
|
+
module V3
|
|
5
|
+
# GenericMember — one member (species) of a GenericHyperedge. The
|
|
6
|
+
# `comprehensive` of its parent GenericHyperedge denotes the genus
|
|
7
|
+
# concept; this member denotes one species.
|
|
8
|
+
#
|
|
9
|
+
# Per ISO 704:2022 §5.5.4.2.1, the intension of a species includes
|
|
10
|
+
# the intension of the genus plus at least one additional
|
|
11
|
+
# DELIMITING CHARACTERISTIC. Each species in a coordinate set
|
|
12
|
+
# (siblings sharing the same criterion of subdivision) carries its
|
|
13
|
+
# own delimiting characteristic value.
|
|
14
|
+
#
|
|
15
|
+
# Example (ISO 704 §5.5.4.2.1 Example 3):
|
|
16
|
+
# Hyperedge comprehensive: computer mouse
|
|
17
|
+
# Hyperedge criterion (of subdivision): means of movement detection
|
|
18
|
+
# Members:
|
|
19
|
+
# - mechanical mouse
|
|
20
|
+
# characteristic: "detecting movement by means of rollers"
|
|
21
|
+
# - optomechanical mouse
|
|
22
|
+
# characteristic: "detecting movement by means of rollers and light sensors"
|
|
23
|
+
# - optical mouse
|
|
24
|
+
# characteristic: "detecting movement by means of light sensors"
|
|
25
|
+
#
|
|
26
|
+
# Multidimensionality (ISO 704 §5.6.3): the same genus can have
|
|
27
|
+
# multiple GenericHyperedges, each with a different criterion
|
|
28
|
+
# (e.g., means of movement detection vs computer connection). The
|
|
29
|
+
# `characteristic` on each member is meaningful only within its
|
|
30
|
+
# parent hyperedge's criterion context.
|
|
31
|
+
#
|
|
32
|
+
# Distinct from PartitiveMember: partitive members are binary
|
|
33
|
+
# delimiting (a part is or isn't delimiting); generic members are
|
|
34
|
+
# textually delimiting (each species has its own characteristic
|
|
35
|
+
# phrase that distinguishes it from coordinate concepts).
|
|
36
|
+
class GenericMember < HyperedgeMember
|
|
37
|
+
# The delimiting characteristic of this species under the parent
|
|
38
|
+
# hyperedge's criterion of subdivision. Localized — different
|
|
39
|
+
# languages may express the same characteristic differently.
|
|
40
|
+
# Required for semantic completeness per ISO 704 §5.5.4.2.1, but
|
|
41
|
+
# the model permits nil to allow incremental population.
|
|
42
|
+
attribute :characteristic, :hash
|
|
43
|
+
|
|
44
|
+
key_value do
|
|
45
|
+
map :ref, to: :ref
|
|
46
|
+
map :presence, to: :presence
|
|
47
|
+
map :count, to: :count
|
|
48
|
+
map :characteristic, to: :characteristic
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
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
|