glossarist 2.13.8 → 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/v3/abstract_hyperedge.rb +62 -0
- 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/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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|