glossarist 2.13.7 → 2.13.9
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/lib/glossarist/rdf/gloss_generic_member.rb +9 -2
- data/lib/glossarist/rdf/gloss_nary_member.rb +21 -12
- data/lib/glossarist/rdf/gloss_partitive_member.rb +5 -0
- data/lib/glossarist/transforms/concept_to_gloss_transform.rb +16 -4
- data/lib/glossarist/v3/abstract_hyperedge.rb +64 -2
- data/lib/glossarist/v3/concept_system.rb +101 -0
- data/lib/glossarist/v3/concept_system_type.rb +16 -0
- data/lib/glossarist/v3/concept_type.rb +15 -0
- data/lib/glossarist/v3/equivalence_degree.rb +17 -0
- data/lib/glossarist/v3/generic_member.rb +43 -8
- data/lib/glossarist/v3/hyperedge_member.rb +16 -11
- data/lib/glossarist/v3/hyperedge_registry.rb +3 -1
- data/lib/glossarist/v3/hyperedge_writer.rb +5 -3
- data/lib/glossarist/v3/partitive_member.rb +24 -6
- data/lib/glossarist/v3/sequential_hyperedge.rb +34 -0
- data/lib/glossarist/v3/sequential_member.rb +15 -0
- data/lib/glossarist/v3.rb +10 -0
- data/lib/glossarist/validation/rules/concept_context.rb +10 -2
- data/lib/glossarist/validation/rules/extensional_definition_rule.rb +120 -0
- data/lib/glossarist/validation/rules/external_concept_rule.rb +91 -0
- data/lib/glossarist/version.rb +1 -1
- metadata +10 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5e42372897ecf000813b37db02afdc6461039b9ee4618182515793d548bd9951
|
|
4
|
+
data.tar.gz: 33f5ba8dfb1b35782fc3711aa9ec7a0f1f675c55a0876c830975d4ca14770761
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 02d246dd723fd2f2834c54ac2e3d8f4a38251e543d7b677ed0ada8cd11af05cddfd41999f6772bbf8235ba486af8d76a603585185f4fd8475bcfaac22ce72a59
|
|
7
|
+
data.tar.gz: d1f34a759e7505afee75884ca7d5cbaa0b02f547ce8f1464317e00fc31fb011b4cd9820faa4a13d9eaa3e87c5daae74d93236cf940759d8b0cd7367996c00f78
|
|
@@ -8,7 +8,14 @@ module Glossarist
|
|
|
8
8
|
# deterministic_id from GlossNaryMember. The `rdf do` block
|
|
9
9
|
# re-declares the predicates because lutaml-model's `rdf` DSL
|
|
10
10
|
# replaces the parent mapping (not extends).
|
|
11
|
+
#
|
|
12
|
+
# Carries `gloss:characteristic` — the delimiting characteristic
|
|
13
|
+
# text per ISO 704:2022 §5.5.4.2.1 (e.g., "detecting movement by
|
|
14
|
+
# means of light sensors"). Each species in a coordinate set has
|
|
15
|
+
# its own characteristic under the parent hyperedge's criterion.
|
|
11
16
|
class GlossGenericMember < GlossNaryMember
|
|
17
|
+
attribute :characteristic, :hash
|
|
18
|
+
|
|
12
19
|
rdf do
|
|
13
20
|
namespace Namespaces::GlossaristNamespace
|
|
14
21
|
|
|
@@ -26,8 +33,8 @@ module Glossarist
|
|
|
26
33
|
to: :presence
|
|
27
34
|
predicate :count, namespace: Namespaces::GlossaristNamespace,
|
|
28
35
|
to: :count
|
|
29
|
-
predicate :
|
|
30
|
-
|
|
36
|
+
predicate :characteristic, namespace: Namespaces::GlossaristNamespace,
|
|
37
|
+
to: :characteristic
|
|
31
38
|
end
|
|
32
39
|
end
|
|
33
40
|
end
|
|
@@ -4,27 +4,27 @@ require "lutaml/model"
|
|
|
4
4
|
|
|
5
5
|
module Glossarist
|
|
6
6
|
module Rdf
|
|
7
|
-
# Shared base for typed n-ary
|
|
7
|
+
# Shared base for typed n-ary hyperedge member RDF views
|
|
8
8
|
# (GlossPartitiveMember, GlossGenericMember, future leaves).
|
|
9
9
|
#
|
|
10
10
|
# Carries the shared attributes and the deterministic_id algorithm.
|
|
11
|
-
# Concrete leaves declare their own `rdf do` block
|
|
12
|
-
#
|
|
13
|
-
#
|
|
11
|
+
# Concrete leaves declare their own `rdf do` block AND their own
|
|
12
|
+
# type-specific attributes — `is_delimiting` on GlossPartitiveMember
|
|
13
|
+
# (binary role, ISO 704 §5.5.4.2.2); `characteristic` on
|
|
14
|
+
# GlossGenericMember (delimiting text, ISO 704 §5.5.4.2.1).
|
|
14
15
|
#
|
|
15
16
|
# Subject identity is content-derived so the same member in the
|
|
16
|
-
# same
|
|
17
|
+
# same hyperedge produces the same URI across runs (and across
|
|
17
18
|
# processes). Two members with identical ref + dimensions share a
|
|
18
19
|
# subject — that is intentional: it exposes model-level
|
|
19
|
-
# inconsistency if the same
|
|
20
|
-
#
|
|
20
|
+
# inconsistency if the same concept is given different dimensions
|
|
21
|
+
# inside the same dataset.
|
|
21
22
|
class GlossNaryMember < Lutaml::Model::Serializable
|
|
22
23
|
attribute :ref_id, :string
|
|
23
24
|
attribute :ref_source, :string
|
|
24
25
|
attribute :ref_text, :string
|
|
25
26
|
attribute :presence, :string
|
|
26
27
|
attribute :count, :string
|
|
27
|
-
attribute :is_delimiting, :boolean
|
|
28
28
|
|
|
29
29
|
# Human-readable SOURCE:ID when both are present; SHA-256 / 16
|
|
30
30
|
# hex chars fallback otherwise. Same content → same slug across
|
|
@@ -33,10 +33,19 @@ module Glossarist
|
|
|
33
33
|
readable = [member.ref_source, member.ref_id].compact.reject(&:empty?)
|
|
34
34
|
return readable.join(":") unless readable.empty?
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
)
|
|
36
|
+
parts = [member.ref_source, member.ref_id, member.ref_text,
|
|
37
|
+
member.presence, member.count]
|
|
38
|
+
# Type-specific discriminator: include the delimiting flag for
|
|
39
|
+
# partitive members (binary role) or the characteristic text
|
|
40
|
+
# for generic members (so members of different criteria don't
|
|
41
|
+
# collide on the same hash input).
|
|
42
|
+
case member
|
|
43
|
+
when GlossPartitiveMember
|
|
44
|
+
parts << member.is_delimiting
|
|
45
|
+
when GlossGenericMember
|
|
46
|
+
parts << member.characteristic
|
|
47
|
+
end
|
|
48
|
+
DeterministicSlug.from_parts(*parts)
|
|
40
49
|
end
|
|
41
50
|
end
|
|
42
51
|
end
|
|
@@ -8,7 +8,12 @@ module Glossarist
|
|
|
8
8
|
# deterministic_id from GlossNaryMember. The `rdf do` block
|
|
9
9
|
# re-declares the predicates because lutaml-model's `rdf` DSL
|
|
10
10
|
# replaces the parent mapping (not extends).
|
|
11
|
+
#
|
|
12
|
+
# Carries `gloss:isDelimiting` — the binary role flag per
|
|
13
|
+
# ISO 704:2022 §5.5.4.2.2 (a part is or is not a delimiting part).
|
|
11
14
|
class GlossPartitiveMember < GlossNaryMember
|
|
15
|
+
attribute :is_delimiting, :boolean
|
|
16
|
+
|
|
12
17
|
rdf do
|
|
13
18
|
namespace Namespaces::GlossaristNamespace
|
|
14
19
|
|
|
@@ -136,12 +136,12 @@ module Glossarist
|
|
|
136
136
|
partitive_relations: build_gloss_hyperedges(
|
|
137
137
|
relation_list, V3::PartitiveHyperedge,
|
|
138
138
|
:build_gloss_partitive_member, :partitive_member_ids,
|
|
139
|
-
:partitive_members, Rdf::GlossPartitiveRelation, identifier
|
|
139
|
+
:partitive_members, Rdf::GlossPartitiveRelation, identifier
|
|
140
140
|
),
|
|
141
141
|
generic_relations: build_gloss_hyperedges(
|
|
142
142
|
relation_list, V3::GenericHyperedge,
|
|
143
143
|
:build_gloss_generic_member, :generic_member_ids,
|
|
144
|
-
:generic_members, Rdf::GlossGenericRelation, identifier
|
|
144
|
+
:generic_members, Rdf::GlossGenericRelation, identifier
|
|
145
145
|
),
|
|
146
146
|
**rel_targets,
|
|
147
147
|
)
|
|
@@ -156,7 +156,7 @@ module Glossarist
|
|
|
156
156
|
member_ids_attr, members_attr,
|
|
157
157
|
gloss_relation_class, identifier)
|
|
158
158
|
Array(relations)
|
|
159
|
-
.
|
|
159
|
+
.grep(hyperedge_class)
|
|
160
160
|
.map { |rel| build_one_gloss_hyperedge(rel, member_builder, member_ids_attr, members_attr, gloss_relation_class, identifier) }
|
|
161
161
|
end
|
|
162
162
|
|
|
@@ -192,6 +192,8 @@ module Glossarist
|
|
|
192
192
|
|
|
193
193
|
# Shared member builder. Per-class metadata (the rdf view class
|
|
194
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`.
|
|
195
197
|
def build_gloss_nary_member(gloss_member_class, member)
|
|
196
198
|
ref = member.ref
|
|
197
199
|
ref_attrs = if ref.is_a?(Glossarist::ConceptRef)
|
|
@@ -200,11 +202,21 @@ module Glossarist
|
|
|
200
202
|
{}
|
|
201
203
|
end
|
|
202
204
|
|
|
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
|
+
|
|
203
215
|
gloss_member_class.new(
|
|
204
216
|
**ref_attrs,
|
|
205
217
|
presence: member.presence,
|
|
206
218
|
count: member.count,
|
|
207
|
-
|
|
219
|
+
**type_attrs,
|
|
208
220
|
)
|
|
209
221
|
end
|
|
210
222
|
|
|
@@ -77,6 +77,54 @@ module Glossarist
|
|
|
77
77
|
members.length >= 2
|
|
78
78
|
end
|
|
79
79
|
|
|
80
|
+
# ── External-concept awareness ─────────────────────────────────
|
|
81
|
+
#
|
|
82
|
+
# ISO 704:2022 diagrams use parenthetical notation for external
|
|
83
|
+
# concepts — concepts referenced from this dataset but defined
|
|
84
|
+
# elsewhere (status: external). Detection requires concept
|
|
85
|
+
# resolution (looking up the referenced concept's status), so
|
|
86
|
+
# these methods take a resolver block: `->(ref) { store.concept(ref) }`.
|
|
87
|
+
#
|
|
88
|
+
# The model stays pure — it does NOT carry a store reference.
|
|
89
|
+
# Consumers inject the resolver at query time.
|
|
90
|
+
|
|
91
|
+
# True if the comprehensive resolves (via the supplied resolver)
|
|
92
|
+
# to a concept with status: external.
|
|
93
|
+
def external_comprehensive?(resolver = nil)
|
|
94
|
+
return false unless comprehensive.is_a?(ConceptRef)
|
|
95
|
+
return false unless resolver
|
|
96
|
+
|
|
97
|
+
concept = resolver.call(comprehensive)
|
|
98
|
+
concept_is_external?(concept)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Array of members whose refs resolve to status: external.
|
|
102
|
+
# Empty when no resolver is supplied (detection is opt-in).
|
|
103
|
+
def external_members(resolver = nil)
|
|
104
|
+
return [] unless resolver
|
|
105
|
+
|
|
106
|
+
members.select do |m|
|
|
107
|
+
next false unless m.is_a?(HyperedgeMember)
|
|
108
|
+
next false unless m.ref.is_a?(ConceptRef)
|
|
109
|
+
|
|
110
|
+
concept = resolver.call(m.ref)
|
|
111
|
+
concept_is_external?(concept)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# True if any external comprehensive or member lacks a
|
|
116
|
+
# `provided_by` edge — i.e., the decomposition dangles. The
|
|
117
|
+
# resolver is used twice: once to detect externals, once to
|
|
118
|
+
# check each external's related edges.
|
|
119
|
+
def dangling_externals?(resolver = nil)
|
|
120
|
+
return false unless resolver
|
|
121
|
+
|
|
122
|
+
candidates = []
|
|
123
|
+
candidates << comprehensive if external_comprehensive?(resolver)
|
|
124
|
+
candidates.concat(external_members(resolver).map(&:ref))
|
|
125
|
+
candidates.any? { |ref| !has_provided_by?(ref, resolver) }
|
|
126
|
+
end
|
|
127
|
+
|
|
80
128
|
# Per-file identity derived from comprehensive + criterion.
|
|
81
129
|
# Format: `<comprehensive-id-dir>/<criterion-slug>` where the
|
|
82
130
|
# dir is `<source>-<id>` (downcased, kebab-case) and the slug
|
|
@@ -114,8 +162,8 @@ module Glossarist
|
|
|
114
162
|
parts = [comprehensive.source, comprehensive.id].compact.reject(&:empty?)
|
|
115
163
|
return nil if parts.empty?
|
|
116
164
|
|
|
117
|
-
parts.join("-").downcase.gsub(/[^a-z0-9
|
|
118
|
-
|
|
165
|
+
parts.join("-").downcase.gsub(/[^a-z0-9-]/, "-")
|
|
166
|
+
.gsub(/-{2,}/, "-").gsub(/\A-|-\z/, "")
|
|
119
167
|
end
|
|
120
168
|
|
|
121
169
|
# Kebab-case slug derived from the English criterion. Falls back
|
|
@@ -130,6 +178,20 @@ module Glossarist
|
|
|
130
178
|
|
|
131
179
|
private
|
|
132
180
|
|
|
181
|
+
def concept_is_external?(concept)
|
|
182
|
+
concept.respond_to?(:status) && concept.status == "external"
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# True if the concept referenced by `ref` has at least one
|
|
186
|
+
# `provided_by` edge in its related list. The resolver must
|
|
187
|
+
# return a concept with a `related` collection.
|
|
188
|
+
def has_provided_by?(ref, resolver)
|
|
189
|
+
concept = resolver.call(ref)
|
|
190
|
+
return false unless concept.respond_to?(:related)
|
|
191
|
+
|
|
192
|
+
Array(concept.related).any? { |r| r.respond_to?(:type) && r.type == "provided_by" }
|
|
193
|
+
end
|
|
194
|
+
|
|
133
195
|
def criterion_hash
|
|
134
196
|
return "0" unless criterion.is_a?(Hash) && !criterion.empty?
|
|
135
197
|
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glossarist
|
|
4
|
+
module V3
|
|
5
|
+
# ConceptSystem — first-class concept system entity per ISO 12620 A.7.
|
|
6
|
+
#
|
|
7
|
+
# Promotes the previously-implicit concept system (domains + tags +
|
|
8
|
+
# relation graph) to an explicit first-class entity. Per-file at
|
|
9
|
+
# `concept-systems/<id>.yaml`.
|
|
10
|
+
#
|
|
11
|
+
# A ConceptSystem carries:
|
|
12
|
+
# - id: stable identifier
|
|
13
|
+
# - name: localized human-readable name
|
|
14
|
+
# - type: ConceptSystemType (generic / partitive / sequential /
|
|
15
|
+
# associative / mixed)
|
|
16
|
+
# - members: ConceptRefs of concepts in the system
|
|
17
|
+
# - hyperedges: references to per-file hyperedge $ids
|
|
18
|
+
# - root_concepts: top-level concepts (no superordinate)
|
|
19
|
+
# - sources, status: provenance and lifecycle
|
|
20
|
+
#
|
|
21
|
+
# See docs/design/concept-systems.md in concept-model repo.
|
|
22
|
+
class ConceptSystem < Lutaml::Model::Serializable
|
|
23
|
+
attribute :id, :string
|
|
24
|
+
attribute :name, :hash
|
|
25
|
+
attribute :type, :string,
|
|
26
|
+
values: Glossarist::V3::ConceptSystemType::VALUES
|
|
27
|
+
attribute :domain, ConceptRef
|
|
28
|
+
attribute :purpose, :hash
|
|
29
|
+
attribute :members, ConceptRef, collection: true
|
|
30
|
+
attribute :hyperedges, :string, collection: true
|
|
31
|
+
attribute :root_concepts, ConceptRef, collection: true
|
|
32
|
+
attribute :sources, ConceptSource, collection: true
|
|
33
|
+
attribute :status, :string
|
|
34
|
+
|
|
35
|
+
key_value do
|
|
36
|
+
map :id, to: :id
|
|
37
|
+
map :name, to: :name
|
|
38
|
+
map :type, to: :type
|
|
39
|
+
map :domain, to: :domain
|
|
40
|
+
map :purpose, to: :purpose
|
|
41
|
+
map :members, to: :members
|
|
42
|
+
map :hyperedges, to: :hyperedges
|
|
43
|
+
map :root_concepts, to: :root_concepts, with: { from: :root_concepts_from, to: :root_concepts_to }
|
|
44
|
+
map :sources, to: :sources
|
|
45
|
+
map :status, to: :status
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def validate!
|
|
49
|
+
validate_id!
|
|
50
|
+
validate_name!
|
|
51
|
+
validate_type!
|
|
52
|
+
validate_members!
|
|
53
|
+
self
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def generic? = type == ConceptSystemType::GENERIC
|
|
57
|
+
def partitive? = type == ConceptSystemType::PARTITIVE
|
|
58
|
+
def sequential? = type == ConceptSystemType::SEQUENTIAL
|
|
59
|
+
def associative? = type == ConceptSystemType::ASSOCIATIVE
|
|
60
|
+
def mixed? = type == ConceptSystemType::MIXED
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def validate_id!
|
|
65
|
+
return if id && !id.to_s.empty?
|
|
66
|
+
|
|
67
|
+
raise ArgumentError, "ConceptSystem#id must be non-empty"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def validate_name!
|
|
71
|
+
return if name.is_a?(Hash) && !name.empty?
|
|
72
|
+
|
|
73
|
+
raise ArgumentError, "ConceptSystem#name must be a non-empty hash (localized)"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def validate_type!
|
|
77
|
+
return if ConceptSystemType::VALUES.include?(type)
|
|
78
|
+
|
|
79
|
+
raise ArgumentError,
|
|
80
|
+
"ConceptSystem#type #{type.inspect} invalid; " \
|
|
81
|
+
"must be one of #{ConceptSystemType::VALUES.join(', ')}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def validate_members!
|
|
85
|
+
return if members.is_a?(Array) && members.length.positive?
|
|
86
|
+
|
|
87
|
+
raise ArgumentError, "ConceptSystem#members must have at least one entry"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def root_concepts_from(model, value)
|
|
91
|
+
model.root_concepts = Array(value).map do |entry|
|
|
92
|
+
entry.is_a?(ConceptRef) ? entry : ConceptRef.new(entry || {})
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def root_concepts_to(model, _doc)
|
|
97
|
+
model.root_concepts&.map(&:to_hash)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glossarist
|
|
4
|
+
module V3
|
|
5
|
+
# ConceptSystemType — ISO 12620 A.7.1 concept system type enum.
|
|
6
|
+
module ConceptSystemType
|
|
7
|
+
GENERIC = "generic"
|
|
8
|
+
PARTITIVE = "partitive"
|
|
9
|
+
SEQUENTIAL = "sequential"
|
|
10
|
+
ASSOCIATIVE = "associative"
|
|
11
|
+
MIXED = "mixed"
|
|
12
|
+
|
|
13
|
+
VALUES = [GENERIC, PARTITIVE, SEQUENTIAL, ASSOCIATIVE, MIXED].freeze
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glossarist
|
|
4
|
+
module V3
|
|
5
|
+
# ConceptType — ISO 704:2022 §5.2-5.3 general vs individual concept.
|
|
6
|
+
module ConceptType
|
|
7
|
+
GENERAL = "general"
|
|
8
|
+
INDIVIDUAL = "individual"
|
|
9
|
+
|
|
10
|
+
DEFAULT = GENERAL
|
|
11
|
+
|
|
12
|
+
VALUES = [GENERAL, INDIVIDUAL].freeze
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glossarist
|
|
4
|
+
module V3
|
|
5
|
+
# EquivalenceDegree — ISO 704:2022 §7.7.3 cross-language equivalence.
|
|
6
|
+
module EquivalenceDegree
|
|
7
|
+
FULL = "full"
|
|
8
|
+
PARTIAL = "partial"
|
|
9
|
+
NONE = "none"
|
|
10
|
+
DIRECTIONAL = "directional"
|
|
11
|
+
|
|
12
|
+
DEFAULT = FULL
|
|
13
|
+
|
|
14
|
+
VALUES = [FULL, PARTIAL, NONE, DIRECTIONAL].freeze
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -2,16 +2,51 @@
|
|
|
2
2
|
|
|
3
3
|
module Glossarist
|
|
4
4
|
module V3
|
|
5
|
-
# GenericMember — one member of a GenericHyperedge.
|
|
6
|
-
#
|
|
7
|
-
#
|
|
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
8
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
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).
|
|
11
36
|
class GenericMember < HyperedgeMember
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
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
|
|
15
50
|
end
|
|
16
51
|
end
|
|
17
52
|
end
|
|
@@ -3,14 +3,25 @@
|
|
|
3
3
|
module Glossarist
|
|
4
4
|
module V3
|
|
5
5
|
# HyperedgeMember — abstract base shape for members of any
|
|
6
|
-
# n-ary concept-system
|
|
6
|
+
# n-ary concept-system hyperedge (PartitiveMember, GenericMember,
|
|
7
7
|
# future AssociativeMember, SequentialMember).
|
|
8
8
|
#
|
|
9
|
-
# Carries the shared ISO 704:2022 MECE dimensions
|
|
10
|
-
#
|
|
9
|
+
# Carries the shared ISO 704:2022 MECE dimensions on every member:
|
|
10
|
+
# presence — required (default) | optional
|
|
11
|
+
# count — exactly_one (default) | at_least_one | multiple
|
|
11
12
|
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
13
|
+
# Type-specific extensions live on the leaves:
|
|
14
|
+
# PartitiveMember — `is_delimiting` (Boolean): per ISO 704
|
|
15
|
+
# §5.5.4.2.2, a part is or is not a delimiting part. Binary role.
|
|
16
|
+
# GenericMember — `characteristic` (LocalizedString): per ISO 704
|
|
17
|
+
# §5.5.4.2.1, each species carries a delimiting characteristic
|
|
18
|
+
# text (e.g., "detecting movement by means of light sensors")
|
|
19
|
+
# that distinguishes it from coordinate concepts under the
|
|
20
|
+
# hyperedge's criterion of subdivision.
|
|
21
|
+
#
|
|
22
|
+
# The combination (optional, at_least_one) is invalid and collapses
|
|
23
|
+
# to (optional, multiple). Multiplicity is the SSOT for the
|
|
24
|
+
# validation; this class delegates to it.
|
|
14
25
|
class HyperedgeMember < Lutaml::Model::Serializable
|
|
15
26
|
DEFAULT_PRESENCE = "required"
|
|
16
27
|
DEFAULT_COUNT = "exactly_one"
|
|
@@ -22,13 +33,11 @@ module Glossarist
|
|
|
22
33
|
attribute :count, :string,
|
|
23
34
|
values: Glossarist::GlossaryDefinition::MEMBER_COUNT_VALUES,
|
|
24
35
|
default: -> { DEFAULT_COUNT }
|
|
25
|
-
attribute :is_delimiting, :boolean, default: -> { false }
|
|
26
36
|
|
|
27
37
|
key_value do
|
|
28
38
|
map :ref, to: :ref
|
|
29
39
|
map :presence, to: :presence
|
|
30
40
|
map :count, to: :count
|
|
31
|
-
map :is_delimiting, to: :is_delimiting
|
|
32
41
|
end
|
|
33
42
|
|
|
34
43
|
def initialize(*)
|
|
@@ -55,10 +64,6 @@ module Glossarist
|
|
|
55
64
|
presence == "optional"
|
|
56
65
|
end
|
|
57
66
|
|
|
58
|
-
def delimiting?
|
|
59
|
-
is_delimiting == true
|
|
60
|
-
end
|
|
61
|
-
|
|
62
67
|
private
|
|
63
68
|
|
|
64
69
|
def validate_ref!
|
|
@@ -15,10 +15,12 @@ module Glossarist
|
|
|
15
15
|
# validator, RDF emitter, RelationLoader) — they all iterate or
|
|
16
16
|
# resolve through this registry.
|
|
17
17
|
module HyperedgeRegistry
|
|
18
|
-
#
|
|
18
|
+
# rubocop:disable Style/MutableConstant — these MUST stay
|
|
19
|
+
# mutable; register() adds entries at load time.
|
|
19
20
|
BY_WIRE_KEY = {}
|
|
20
21
|
BY_TYPE_TAG = {}
|
|
21
22
|
BY_RDF_TYPE = {}
|
|
23
|
+
# rubocop:enable Style/MutableConstant
|
|
22
24
|
|
|
23
25
|
Mutex = ::Mutex.new
|
|
24
26
|
|
|
@@ -45,8 +45,10 @@ module Glossarist
|
|
|
45
45
|
hyperedge.is_a?(AbstractHyperedge)
|
|
46
46
|
|
|
47
47
|
path = hyperedge.file_path(@relations_dir)
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
unless path
|
|
49
|
+
raise WriteError,
|
|
50
|
+
"cannot derive file path — comprehensive must be non-empty"
|
|
51
|
+
end
|
|
50
52
|
|
|
51
53
|
FileUtils.mkdir_p(File.dirname(path))
|
|
52
54
|
File.write(path, serialize(hyperedge))
|
|
@@ -72,7 +74,7 @@ module Glossarist
|
|
|
72
74
|
ordered["type"] = hash.delete("type")
|
|
73
75
|
ordered["status"] = hash.delete("status") if hash.key?("status")
|
|
74
76
|
ordered["comprehensive"] = hash.delete("comprehensive")
|
|
75
|
-
ordered["members"]
|
|
77
|
+
ordered["members"] = hash.delete("members")
|
|
76
78
|
ordered["completeness"] = hash.delete("completeness") if hash.key?("completeness")
|
|
77
79
|
ordered["criterion"] = hash.delete("criterion") if hash.key?("criterion")
|
|
78
80
|
ordered["sources"] = hash.delete("sources") if hash.key?("sources")
|
|
@@ -2,15 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
module Glossarist
|
|
4
4
|
module V3
|
|
5
|
-
# PartitiveMember — one member of a PartitiveHyperedge.
|
|
6
|
-
# the ISO 704:2022 MECE shape from HyperedgeMember; the
|
|
5
|
+
# PartitiveMember — one member of a PartitiveHyperedge. The
|
|
7
6
|
# `comprehensive` of its parent PartitiveHyperedge denotes the
|
|
8
|
-
# whole concept.
|
|
7
|
+
# whole concept; this member denotes one of its parts.
|
|
9
8
|
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
9
|
+
# Per ISO 704:2022 §5.5.4.2.2, a part is either delimiting or not:
|
|
10
|
+
# a delimiting part behaves like a delimiting characteristic — it
|
|
11
|
+
# distinguishes the comprehensive (whole) from coordinate concepts.
|
|
12
|
+
# Example: for "optomechanical mouse", the delimiting parts are
|
|
13
|
+
# mouse ball, x/y-axis rollers, infrared emitter/sensor — they
|
|
14
|
+
# distinguish it from "mechanical mouse" and "optical mouse".
|
|
15
|
+
# Mouse button is NOT delimiting (all computer mice have buttons).
|
|
16
|
+
#
|
|
17
|
+
# The binary role is sufficient — no per-member delimiting text is
|
|
18
|
+
# needed because the part itself IS the delimiting marker.
|
|
12
19
|
class PartitiveMember < HyperedgeMember
|
|
13
|
-
|
|
20
|
+
attribute :is_delimiting, :boolean, default: -> { false }
|
|
21
|
+
|
|
22
|
+
key_value do
|
|
23
|
+
map :ref, to: :ref
|
|
24
|
+
map :presence, to: :presence
|
|
25
|
+
map :count, to: :count
|
|
26
|
+
map :is_delimiting, to: :is_delimiting
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def delimiting?
|
|
30
|
+
is_delimiting == true
|
|
31
|
+
end
|
|
14
32
|
end
|
|
15
33
|
end
|
|
16
34
|
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glossarist
|
|
4
|
+
module V3
|
|
5
|
+
# SequentialHyperedge — an ISO 12620 A.6.3 / ISO 704:2022 §5.5.5
|
|
6
|
+
# sequential relation connecting a comprehensive concept to two or
|
|
7
|
+
# more subordinate concepts in an ORDERED sequence (temporal,
|
|
8
|
+
# spatial, causal, developmental).
|
|
9
|
+
#
|
|
10
|
+
# Mirror of PartitiveHyperedge and GenericHyperedge. Members are
|
|
11
|
+
# ORDERED — array order is significant. Reversing the array
|
|
12
|
+
# reverses the sequence.
|
|
13
|
+
class SequentialHyperedge < AbstractHyperedge
|
|
14
|
+
WIRE_KEY = "sequential_relations"
|
|
15
|
+
TYPE_TAG = "sequential_relation"
|
|
16
|
+
RDF_TYPE = "gloss:SequentialRelation"
|
|
17
|
+
V1_WIRE_KEYS = [].freeze
|
|
18
|
+
|
|
19
|
+
attribute :members, SequentialMember, collection: true
|
|
20
|
+
|
|
21
|
+
key_value do
|
|
22
|
+
map :comprehensive, to: :comprehensive
|
|
23
|
+
map :members, to: :members
|
|
24
|
+
map :completeness, to: :completeness
|
|
25
|
+
map :criterion, to: :criterion
|
|
26
|
+
map :sources, to: :sources
|
|
27
|
+
map :notes, to: :notes
|
|
28
|
+
map :status, to: :status
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
HyperedgeRegistry.register(SequentialHyperedge)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glossarist
|
|
4
|
+
module V3
|
|
5
|
+
# SequentialMember — one member of a SequentialHyperedge. Inherits
|
|
6
|
+
# the MECE shape from HyperedgeMember.
|
|
7
|
+
#
|
|
8
|
+
# Distinct leaf class for type safety. The member's POSITION IN THE
|
|
9
|
+
# ARRAY is significant for sequential hyperedges (unlike partitive
|
|
10
|
+
# and generic where order is insignificant).
|
|
11
|
+
class SequentialMember < HyperedgeMember
|
|
12
|
+
# Inherits all attributes and validations from HyperedgeMember.
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
data/lib/glossarist/v3.rb
CHANGED
|
@@ -17,7 +17,13 @@ module Glossarist
|
|
|
17
17
|
autoload :PartitiveMember, "glossarist/v3/partitive_member"
|
|
18
18
|
autoload :GenericHyperedge, "glossarist/v3/generic_hyperedge"
|
|
19
19
|
autoload :GenericMember, "glossarist/v3/generic_member"
|
|
20
|
+
autoload :SequentialHyperedge, "glossarist/v3/sequential_hyperedge"
|
|
21
|
+
autoload :SequentialMember, "glossarist/v3/sequential_member"
|
|
20
22
|
autoload :DefinitionType, "glossarist/v3/definition_type"
|
|
23
|
+
autoload :ConceptType, "glossarist/v3/concept_type"
|
|
24
|
+
autoload :ConceptSystemType, "glossarist/v3/concept_system_type"
|
|
25
|
+
autoload :EquivalenceDegree, "glossarist/v3/equivalence_degree"
|
|
26
|
+
autoload :ConceptSystem, "glossarist/v3/concept_system"
|
|
21
27
|
autoload :RelationLoader, "glossarist/v3/relation_loader"
|
|
22
28
|
autoload :HyperedgeWriter, "glossarist/v3/hyperedge_writer"
|
|
23
29
|
autoload :Multiplicity, "glossarist/v3/multiplicity"
|
|
@@ -44,9 +50,12 @@ module Glossarist
|
|
|
44
50
|
Configuration.register_model(PartitiveMember, id: :partitive_member)
|
|
45
51
|
Configuration.register_model(GenericHyperedge, id: :generic_hyperedge)
|
|
46
52
|
Configuration.register_model(GenericMember, id: :generic_member)
|
|
53
|
+
Configuration.register_model(SequentialHyperedge, id: :sequential_hyperedge)
|
|
54
|
+
Configuration.register_model(SequentialMember, id: :sequential_member)
|
|
47
55
|
Configuration.register_model(ManagedConceptData, id: :managed_concept_data)
|
|
48
56
|
Configuration.register_model(ManagedConcept, id: :managed_concept)
|
|
49
57
|
Configuration.register_model(ConceptDocument, id: :concept_document)
|
|
58
|
+
Configuration.register_model(ConceptSystem, id: :concept_system)
|
|
50
59
|
|
|
51
60
|
# Eager-load concrete hyperedge leaves so HyperedgeRegistry
|
|
52
61
|
# auto-populates via AbstractHyperedge.inherited. Without this,
|
|
@@ -54,5 +63,6 @@ module Glossarist
|
|
|
54
63
|
# the registry empty at boot.
|
|
55
64
|
PartitiveHyperedge
|
|
56
65
|
GenericHyperedge
|
|
66
|
+
SequentialHyperedge
|
|
57
67
|
end
|
|
58
68
|
end
|
|
@@ -17,13 +17,21 @@ module Glossarist
|
|
|
17
17
|
# — the loader does not run on demand so the validator behaviour
|
|
18
18
|
# is fully deterministic given the load.
|
|
19
19
|
class ConceptContext
|
|
20
|
-
attr_reader :concept, :file_name, :collection_context, :relations
|
|
20
|
+
attr_reader :concept, :file_name, :collection_context, :relations,
|
|
21
|
+
:concept_resolver
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
# `concept_resolver` is an optional callable that takes a
|
|
24
|
+
# ConceptRef and returns the resolved ManagedConcept (or nil).
|
|
25
|
+
# Rules that need cross-concept lookups (e.g.,
|
|
26
|
+
# ExternalConceptRule's dangling-external detection) use this.
|
|
27
|
+
# When nil, those rules no-op.
|
|
28
|
+
def initialize(concept, file_name:, collection_context:, relations: [],
|
|
29
|
+
concept_resolver: nil)
|
|
23
30
|
@concept = concept
|
|
24
31
|
@file_name = file_name
|
|
25
32
|
@collection_context = collection_context
|
|
26
33
|
@relations = Array(relations)
|
|
34
|
+
@concept_resolver = concept_resolver
|
|
27
35
|
end
|
|
28
36
|
|
|
29
37
|
def concept_id
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glossarist
|
|
4
|
+
module Validation
|
|
5
|
+
module Rules
|
|
6
|
+
# Validates that extensional definitions do not contain open-ended
|
|
7
|
+
# wording, per ISO 704:2022 §6.4.5.1. An extensional definition
|
|
8
|
+
# must be an EXHAUSTIVE enumeration — open-ended wordings like
|
|
9
|
+
# `...`, `etc.`, `and so on`, `and similar`, `and others`,
|
|
10
|
+
# `including` violate this principle because they imply the
|
|
11
|
+
# enumeration is incomplete.
|
|
12
|
+
#
|
|
13
|
+
# Mirrors concept-model's `check-definition-rules`
|
|
14
|
+
# §6.4.5.1 open-ended-wording check, exposed as a consumer-side
|
|
15
|
+
# Validation::Rule.
|
|
16
|
+
#
|
|
17
|
+
# Scope: concept (rule runs once per concept). Inspects every
|
|
18
|
+
# DetailedDefinition with `type: extensional` across all of the
|
|
19
|
+
# concept's localized definitions.
|
|
20
|
+
class ExtensionalDefinitionRule < Base
|
|
21
|
+
# ISO 704:2022 §6.4.5.1 — patterns that signal incomplete
|
|
22
|
+
# enumeration in an extensional definition. The list is
|
|
23
|
+
# intentionally conservative: only wordings that clearly
|
|
24
|
+
# indicate "more exist beyond what's listed."
|
|
25
|
+
OPEN_ENDED_PATTERNS = [
|
|
26
|
+
/\.\.\./, # ellipsis
|
|
27
|
+
/\betc\b\.?/i, # etc / etc.
|
|
28
|
+
/\band\s+similar\b/i, # and similar
|
|
29
|
+
/\band\s+so\s+on\b/i, # and so on
|
|
30
|
+
/\band\s+the\s+like\b/i, # and the like
|
|
31
|
+
/\band\s+others?\b/i, # and other / and others
|
|
32
|
+
/\bsuch\s+as\b/i, # such as (often partial)
|
|
33
|
+
/\bincluding\b/i, # including (often partial)
|
|
34
|
+
/\be\.?g\.?\b/i, # e.g. / eg
|
|
35
|
+
].freeze
|
|
36
|
+
|
|
37
|
+
def code = "GLS-223"
|
|
38
|
+
def category = :schema
|
|
39
|
+
def severity = "warning"
|
|
40
|
+
def scope = :concept
|
|
41
|
+
|
|
42
|
+
def applicable?(context)
|
|
43
|
+
return false unless context.concept.is_a?(V3::ManagedConcept)
|
|
44
|
+
|
|
45
|
+
concept_has_extensional_definition?(context.concept)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def check(context)
|
|
49
|
+
issues = []
|
|
50
|
+
|
|
51
|
+
each_extensional_definition(context.concept) do |l10n_lang, defn, path|
|
|
52
|
+
check_definition(defn, l10n_lang, path, context.file_name, issues)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
issues
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def concept_has_extensional_definition?(concept)
|
|
61
|
+
each_extensional_definition(concept).any? { true }
|
|
62
|
+
rescue StopIteration
|
|
63
|
+
false
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Yields (lang, definition, path_string) for every extensional
|
|
67
|
+
# definition on every localization of the concept.
|
|
68
|
+
def each_extensional_definition(concept)
|
|
69
|
+
return enum_for(:each_extensional_definition, concept) unless block_given?
|
|
70
|
+
|
|
71
|
+
concept.localizations.each_value do |l10n|
|
|
72
|
+
next unless l10n.respond_to?(:data)
|
|
73
|
+
|
|
74
|
+
lang = l10n.language_code
|
|
75
|
+
%i[definition examples notes].each do |field|
|
|
76
|
+
value = l10n.data&.public_send(field)
|
|
77
|
+
next if value.nil?
|
|
78
|
+
|
|
79
|
+
Array(value).each_with_index do |item, idx|
|
|
80
|
+
next unless item.is_a?(Glossarist::DetailedDefinition) ||
|
|
81
|
+
item.is_a?(V3::DetailedDefinition)
|
|
82
|
+
next unless item.type.to_s == "extensional"
|
|
83
|
+
|
|
84
|
+
yield lang, item, "#{field}[#{idx}]"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def check_definition(defn, lang, path, fname, issues)
|
|
91
|
+
content = extract_content(defn)
|
|
92
|
+
return if content.nil? || content.empty?
|
|
93
|
+
|
|
94
|
+
OPEN_ENDED_PATTERNS.each do |pattern|
|
|
95
|
+
match = content.match(pattern)
|
|
96
|
+
next unless match
|
|
97
|
+
|
|
98
|
+
issues << issue(
|
|
99
|
+
"#{path} (#{lang}) extensional definition contains " \
|
|
100
|
+
"open-ended wording #{match[0].inspect}; ISO 704:2022 " \
|
|
101
|
+
"§6.4.5.1 requires exhaustive enumeration in " \
|
|
102
|
+
"extensional definitions",
|
|
103
|
+
location: fname,
|
|
104
|
+
severity: "warning",
|
|
105
|
+
suggestion: "Either enumerate all instances exhaustively, " \
|
|
106
|
+
"or change the definition type to intensional.",
|
|
107
|
+
)
|
|
108
|
+
break # one issue per definition is enough
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def extract_content(definition)
|
|
113
|
+
return definition.content if definition.respond_to?(:content)
|
|
114
|
+
|
|
115
|
+
nil
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glossarist
|
|
4
|
+
module Validation
|
|
5
|
+
module Rules
|
|
6
|
+
# Validates that external concepts referenced by hyperedges have
|
|
7
|
+
# a `provided_by` edge for resolution. Mirrors concept-model's
|
|
8
|
+
# `check-external-as-comprehensive` validator, exposed as a
|
|
9
|
+
# consumer-side Validation::Rule so it runs in the standard
|
|
10
|
+
# validation pipeline.
|
|
11
|
+
#
|
|
12
|
+
# Per concept-model docs/design/external-concepts.md: an external
|
|
13
|
+
# concept (status: external) is referenced from this dataset but
|
|
14
|
+
# defined elsewhere. Without a `provided_by` edge, the reference
|
|
15
|
+
# dangles — there is no resolvable target. ISO 704 models this
|
|
16
|
+
# case as a parenthetical term in the diagram; the data model
|
|
17
|
+
# requires the resolution edge to exist.
|
|
18
|
+
#
|
|
19
|
+
# Scope: concept (rule runs once per concept). The concept's
|
|
20
|
+
# relations are inspected via `context.relations`; each
|
|
21
|
+
# relation's comprehensive + members are checked.
|
|
22
|
+
#
|
|
23
|
+
# Requires a `concept_resolver` on the context. Without one,
|
|
24
|
+
# the rule is a no-op (cannot detect externals without
|
|
25
|
+
# resolution).
|
|
26
|
+
class ExternalConceptRule < Base
|
|
27
|
+
def code = "GLS-222"
|
|
28
|
+
def category = :schema
|
|
29
|
+
def severity = "warning"
|
|
30
|
+
def scope = :concept
|
|
31
|
+
|
|
32
|
+
def applicable?(context)
|
|
33
|
+
return false unless context.concept.is_a?(V3::ManagedConcept)
|
|
34
|
+
return false unless context.concept_resolver
|
|
35
|
+
return false if context.relations.empty?
|
|
36
|
+
|
|
37
|
+
true
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def check(context)
|
|
41
|
+
resolver = context.concept_resolver
|
|
42
|
+
issues = []
|
|
43
|
+
|
|
44
|
+
context.relations.each_with_index do |rel, idx|
|
|
45
|
+
check_comprehensive(rel, idx, context.file_name, resolver, issues)
|
|
46
|
+
check_members(rel, idx, context.file_name, resolver, issues)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
issues
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def check_comprehensive(rel, idx, fname, resolver, issues)
|
|
55
|
+
return unless rel.external_comprehensive?(resolver)
|
|
56
|
+
|
|
57
|
+
comp_id = Glossarist::ConceptRef.qualified_id(rel.comprehensive)
|
|
58
|
+
issues << issue(
|
|
59
|
+
"relation #{idx + 1} comprehensive #{comp_id.inspect} is " \
|
|
60
|
+
"external (status: external) — confirm a `provided_by` edge " \
|
|
61
|
+
"exists on the external concept so the decomposition resolves",
|
|
62
|
+
location: fname,
|
|
63
|
+
severity: "info",
|
|
64
|
+
suggestion: "Add `provided_by` to #{comp_id}'s related list.",
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def check_members(rel, idx, fname, resolver, issues)
|
|
69
|
+
rel.external_members(resolver).each_with_index do |member, mi|
|
|
70
|
+
member_id = Glossarist::ConceptRef.qualified_id(member.ref)
|
|
71
|
+
has_resolution = resolver.call(member.ref)&.related&.any? do |r|
|
|
72
|
+
r.respond_to?(:type) && r.type == "provided_by"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
next if has_resolution
|
|
76
|
+
|
|
77
|
+
issues << issue(
|
|
78
|
+
"relation #{idx + 1}.members[#{mi}] #{member_id.inspect} is " \
|
|
79
|
+
"external (status: external) without a `provided_by` edge — " \
|
|
80
|
+
"the decomposition dangles; no resolvable target",
|
|
81
|
+
location: fname,
|
|
82
|
+
severity: "warning",
|
|
83
|
+
suggestion: "Add `provided_by` to #{member_id}'s related " \
|
|
84
|
+
"list, or define the concept in this dataset.",
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
data/lib/glossarist/version.rb
CHANGED
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.13.
|
|
4
|
+
version: 2.13.9
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-08-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: lutaml-model
|
|
@@ -398,9 +398,13 @@ files:
|
|
|
398
398
|
- lib/glossarist/v3/concept_document.rb
|
|
399
399
|
- lib/glossarist/v3/concept_ref.rb
|
|
400
400
|
- lib/glossarist/v3/concept_source.rb
|
|
401
|
+
- lib/glossarist/v3/concept_system.rb
|
|
402
|
+
- lib/glossarist/v3/concept_system_type.rb
|
|
403
|
+
- lib/glossarist/v3/concept_type.rb
|
|
401
404
|
- lib/glossarist/v3/configuration.rb
|
|
402
405
|
- lib/glossarist/v3/definition_type.rb
|
|
403
406
|
- lib/glossarist/v3/detailed_definition.rb
|
|
407
|
+
- lib/glossarist/v3/equivalence_degree.rb
|
|
404
408
|
- lib/glossarist/v3/generic_hyperedge.rb
|
|
405
409
|
- lib/glossarist/v3/generic_member.rb
|
|
406
410
|
- lib/glossarist/v3/hyperedge_index.rb
|
|
@@ -415,6 +419,8 @@ files:
|
|
|
415
419
|
- lib/glossarist/v3/partitive_member.rb
|
|
416
420
|
- lib/glossarist/v3/related_concept.rb
|
|
417
421
|
- lib/glossarist/v3/relation_loader.rb
|
|
422
|
+
- lib/glossarist/v3/sequential_hyperedge.rb
|
|
423
|
+
- lib/glossarist/v3/sequential_member.rb
|
|
418
424
|
- lib/glossarist/validation.rb
|
|
419
425
|
- lib/glossarist/validation/asset_index.rb
|
|
420
426
|
- lib/glossarist/validation/bibliography_index.rb
|
|
@@ -442,6 +448,8 @@ files:
|
|
|
442
448
|
- lib/glossarist/validation/rules/domain_target_rule.rb
|
|
443
449
|
- lib/glossarist/validation/rules/duplicate_term_rule.rb
|
|
444
450
|
- lib/glossarist/validation/rules/entry_status_rule.rb
|
|
451
|
+
- lib/glossarist/validation/rules/extensional_definition_rule.rb
|
|
452
|
+
- lib/glossarist/validation/rules/external_concept_rule.rb
|
|
445
453
|
- lib/glossarist/validation/rules/filename_id_rule.rb
|
|
446
454
|
- lib/glossarist/validation/rules/gcr_context.rb
|
|
447
455
|
- lib/glossarist/validation/rules/hyperedge_coherence_rule.rb
|