glossarist 2.13.5 → 2.13.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/config.yml +15 -0
- data/lib/glossarist/cli/export_command.rb +5 -5
- data/lib/glossarist/concept_ref.rb +49 -0
- data/lib/glossarist/gcr_package.rb +2 -2
- data/lib/glossarist/gcr_validator.rb +19 -1
- data/lib/glossarist/glossary_definition.rb +16 -3
- data/lib/glossarist/glossary_store.rb +70 -0
- data/lib/glossarist/managed_concept.rb +2 -2
- data/lib/glossarist/rdf/deterministic_slug.rb +33 -0
- data/lib/glossarist/rdf/gloss_citation.rb +1 -1
- data/lib/glossarist/rdf/gloss_concept.rb +3 -0
- data/lib/glossarist/rdf/gloss_concept_source.rb +1 -1
- data/lib/glossarist/rdf/gloss_detailed_definition.rb +1 -2
- data/lib/glossarist/rdf/gloss_generic_member.rb +34 -0
- data/lib/glossarist/rdf/gloss_generic_relation.rb +44 -0
- data/lib/glossarist/rdf/gloss_nary_member.rb +43 -0
- data/lib/glossarist/rdf/gloss_nary_relation.rb +56 -0
- data/lib/glossarist/rdf/gloss_partitive_member.rb +5 -28
- data/lib/glossarist/rdf/gloss_partitive_relation.rb +9 -23
- data/lib/glossarist/rdf/gloss_reference.rb +1 -2
- data/lib/glossarist/rdf.rb +5 -0
- data/lib/glossarist/tasks/shacl.rake +4 -1
- data/lib/glossarist/tasks/sync.rake +4 -1
- data/lib/glossarist/tasks.rb +9 -0
- data/lib/glossarist/transforms/concept_to_gloss_transform.rb +72 -52
- data/lib/glossarist/v3/abstract_hyperedge.rb +187 -0
- data/lib/glossarist/v3/definition_type.rb +30 -0
- data/lib/glossarist/v3/detailed_definition.rb +4 -0
- data/lib/glossarist/v3/generic_hyperedge.rb +34 -0
- data/lib/glossarist/v3/generic_member.rb +17 -0
- data/lib/glossarist/v3/hyperedge_index.rb +89 -0
- data/lib/glossarist/v3/hyperedge_member.rb +97 -0
- data/lib/glossarist/v3/hyperedge_registry.rb +70 -0
- data/lib/glossarist/v3/hyperedge_writer.rb +87 -0
- data/lib/glossarist/v3/managed_concept.rb +53 -2
- data/lib/glossarist/v3/managed_concept_data.rb +0 -1
- data/lib/glossarist/v3/partitive_hyperedge.rb +53 -0
- data/lib/glossarist/v3/partitive_member.rb +8 -105
- data/lib/glossarist/v3/relation_loader.rb +98 -0
- data/lib/glossarist/v3.rb +25 -8
- data/lib/glossarist/validation/rules/concept_context.rb +10 -2
- data/lib/glossarist/validation/rules/{partitive_relation_rule.rb → hyperedge_coherence_rule.rb} +22 -16
- data/lib/glossarist/version.rb +1 -1
- data/lib/glossarist.rb +1 -0
- metadata +19 -4
- data/lib/glossarist/v3/partitive_relation.rb +0 -114
|
@@ -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
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glossarist
|
|
4
|
+
module V3
|
|
5
|
+
# HyperedgeMember — abstract base shape for members of any
|
|
6
|
+
# n-ary concept-system relation (PartitiveMember, GenericMember,
|
|
7
|
+
# future AssociativeMember, SequentialMember).
|
|
8
|
+
#
|
|
9
|
+
# Carries the shared ISO 704:2022 MECE dimensions: presence × count,
|
|
10
|
+
# plus the orthogonal is_delimiting flag.
|
|
11
|
+
#
|
|
12
|
+
# Concrete leaf classes inherit and may add type-specific fields.
|
|
13
|
+
# See docs/design/abstract-nary-relation.md (concept-model repo).
|
|
14
|
+
class HyperedgeMember < Lutaml::Model::Serializable
|
|
15
|
+
DEFAULT_PRESENCE = "required"
|
|
16
|
+
DEFAULT_COUNT = "exactly_one"
|
|
17
|
+
|
|
18
|
+
attribute :ref, ConceptRef
|
|
19
|
+
attribute :presence, :string,
|
|
20
|
+
values: Glossarist::GlossaryDefinition::MEMBER_PRESENCE_VALUES,
|
|
21
|
+
default: -> { DEFAULT_PRESENCE }
|
|
22
|
+
attribute :count, :string,
|
|
23
|
+
values: Glossarist::GlossaryDefinition::MEMBER_COUNT_VALUES,
|
|
24
|
+
default: -> { DEFAULT_COUNT }
|
|
25
|
+
attribute :is_delimiting, :boolean, default: -> { false }
|
|
26
|
+
|
|
27
|
+
key_value do
|
|
28
|
+
map :ref, to: :ref
|
|
29
|
+
map :presence, to: :presence
|
|
30
|
+
map :count, to: :count
|
|
31
|
+
map :is_delimiting, to: :is_delimiting
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def initialize(*)
|
|
35
|
+
if instance_of?(HyperedgeMember)
|
|
36
|
+
raise NotImplementedError,
|
|
37
|
+
"HyperedgeMember is abstract; instantiate " \
|
|
38
|
+
"PartitiveMember or GenericMember instead"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
super
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def validate!
|
|
45
|
+
validate_ref!
|
|
46
|
+
validate_presence_count!
|
|
47
|
+
self
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def required?
|
|
51
|
+
presence == "required"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def optional?
|
|
55
|
+
presence == "optional"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def delimiting?
|
|
59
|
+
is_delimiting == true
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def validate_ref!
|
|
65
|
+
return if ref.is_a?(ConceptRef) && (ref.source || ref.id || ref.text)
|
|
66
|
+
|
|
67
|
+
raise ArgumentError,
|
|
68
|
+
"#{self.class.name}#ref must be a non-empty ConceptRef " \
|
|
69
|
+
"(source, id, or text required)"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Delegates the MECE combination check to Multiplicity (the SSOT).
|
|
73
|
+
# Multiplicity.multiplicity_from_pair raises ArgumentError on the
|
|
74
|
+
# invalid (optional + at_least_one) combo with the canonical message.
|
|
75
|
+
# The returned name is discarded — only the validation side matters.
|
|
76
|
+
def validate_presence_count!
|
|
77
|
+
unless Glossarist::GlossaryDefinition::MEMBER_PRESENCE_VALUES
|
|
78
|
+
.include?(presence)
|
|
79
|
+
raise ArgumentError,
|
|
80
|
+
"#{self.class.name}#presence has invalid value " \
|
|
81
|
+
"#{presence.inspect}; must be one of " \
|
|
82
|
+
"#{GlossaryDefinition::MEMBER_PRESENCE_VALUES.join(', ')}"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
unless Glossarist::GlossaryDefinition::MEMBER_COUNT_VALUES
|
|
86
|
+
.include?(count)
|
|
87
|
+
raise ArgumentError,
|
|
88
|
+
"#{self.class.name}#count has invalid value " \
|
|
89
|
+
"#{count.inspect}; must be one of " \
|
|
90
|
+
"#{GlossaryDefinition::MEMBER_COUNT_VALUES.join(', ')}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
Multiplicity.multiplicity_from_pair(presence, count)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glossarist
|
|
4
|
+
module V3
|
|
5
|
+
# HyperedgeRegistry — single SSOT for hyperedge-class lookup by
|
|
6
|
+
# every external identifier (wire key, type tag, RDF type).
|
|
7
|
+
#
|
|
8
|
+
# Auto-populates on class inheritance via AbstractHyperedge.inherited.
|
|
9
|
+
# Concrete leaves declare WIRE_KEY / TYPE_TAG / RDF_TYPE constants
|
|
10
|
+
# in a per-class metadata block; the registry reads them once at
|
|
11
|
+
# registration time.
|
|
12
|
+
#
|
|
13
|
+
# Adding a new hyperedge type means declaring one leaf class with
|
|
14
|
+
# the metadata block. No edit to any other file (parser, serializer,
|
|
15
|
+
# validator, RDF emitter, RelationLoader) — they all iterate or
|
|
16
|
+
# resolve through this registry.
|
|
17
|
+
module HyperedgeRegistry
|
|
18
|
+
# Mutable: register adds entries at load time. DO NOT freeze.
|
|
19
|
+
BY_WIRE_KEY = {}
|
|
20
|
+
BY_TYPE_TAG = {}
|
|
21
|
+
BY_RDF_TYPE = {}
|
|
22
|
+
|
|
23
|
+
Mutex = ::Mutex.new
|
|
24
|
+
|
|
25
|
+
class << self
|
|
26
|
+
# Register a concrete hyperedge class. Idempotent.
|
|
27
|
+
def register(cls)
|
|
28
|
+
return if cls.nil? || cls.const_defined?(:WIRE_KEY) == false
|
|
29
|
+
|
|
30
|
+
Mutex.synchronize do
|
|
31
|
+
wire = cls::WIRE_KEY
|
|
32
|
+
tag = cls::TYPE_TAG
|
|
33
|
+
rdf = cls::RDF_TYPE
|
|
34
|
+
|
|
35
|
+
BY_WIRE_KEY[wire] = cls unless wire.nil? || wire.empty?
|
|
36
|
+
BY_TYPE_TAG[tag] = cls unless tag.nil? || tag.empty?
|
|
37
|
+
BY_RDF_TYPE[rdf] = cls unless rdf.nil? || rdf.empty?
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Iterate every concrete leaf (use this in parser / serializer
|
|
42
|
+
# / loader to stay type-blind).
|
|
43
|
+
def all_classes
|
|
44
|
+
BY_TYPE_TAG.values.uniq
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def for_wire_key(key)
|
|
48
|
+
BY_WIRE_KEY[key]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def for_type_tag(tag)
|
|
52
|
+
BY_TYPE_TAG[tag]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def for_rdf_type(rdf_type)
|
|
56
|
+
BY_RDF_TYPE[rdf_type]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Reset (spec helper — never call from production code).
|
|
60
|
+
def reset!
|
|
61
|
+
Mutex.synchronize do
|
|
62
|
+
BY_WIRE_KEY.clear
|
|
63
|
+
BY_TYPE_TAG.clear
|
|
64
|
+
BY_RDF_TYPE.clear
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "pathname"
|
|
6
|
+
|
|
7
|
+
module Glossarist
|
|
8
|
+
module V3
|
|
9
|
+
# HyperedgeWriter — the WRITE half of per-file hyperedge storage.
|
|
10
|
+
#
|
|
11
|
+
# Mirror of RelationLoader. Each hyperedge is serialized to
|
|
12
|
+
# `relations/<comprehensive-id>/<criterion-slug>.yaml` with the
|
|
13
|
+
# full per-file wire format ($id, type, comprehensive, members,
|
|
14
|
+
# criterion, sources, notes, status, completeness).
|
|
15
|
+
#
|
|
16
|
+
# Per-file storage means a single concept (e.g., OIML 5.1
|
|
17
|
+
# measurement standard) can have N hyperedges (one per criterion),
|
|
18
|
+
# each in its own file under relations/<comp-id>/.
|
|
19
|
+
class HyperedgeWriter
|
|
20
|
+
class WriteError < ::StandardError
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class << self
|
|
24
|
+
# Write a single hyperedge to its per-file location under
|
|
25
|
+
# `relations_dir`. Creates the comprehensive-id subdirectory
|
|
26
|
+
# if missing. Returns the absolute path written.
|
|
27
|
+
def write(hyperedge, relations_dir)
|
|
28
|
+
new(relations_dir).write(hyperedge)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Write a batch of hyperedges. Returns an Array<String> of
|
|
32
|
+
# paths written. Hyperedges with the same comprehensive are
|
|
33
|
+
# written to the same subdirectory.
|
|
34
|
+
def write_all(hyperedges, relations_dir)
|
|
35
|
+
new(relations_dir).write_all(hyperedges)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def initialize(relations_dir)
|
|
40
|
+
@relations_dir = Pathname.new(relations_dir)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def write(hyperedge)
|
|
44
|
+
raise WriteError, "not an AbstractHyperedge: #{hyperedge.class}" unless
|
|
45
|
+
hyperedge.is_a?(AbstractHyperedge)
|
|
46
|
+
|
|
47
|
+
path = hyperedge.file_path(@relations_dir)
|
|
48
|
+
raise WriteError,
|
|
49
|
+
"cannot derive file path — comprehensive must be non-empty" unless path
|
|
50
|
+
|
|
51
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
52
|
+
File.write(path, serialize(hyperedge))
|
|
53
|
+
path
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def write_all(hyperedges)
|
|
57
|
+
Array(hyperedges).map { |h| write(h) }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
# Serialize with stable key ordering: $id, type, status,
|
|
63
|
+
# comprehensive, members, completeness, criterion, sources,
|
|
64
|
+
# notes. Matches the concept-model per-file format.
|
|
65
|
+
def serialize(hyperedge)
|
|
66
|
+
hash = hyperedge.to_hash
|
|
67
|
+
hash["$id"] = hyperedge.file_id || hyperedge.derived_file_id
|
|
68
|
+
hash["type"] = hyperedge.class::TYPE_TAG
|
|
69
|
+
# Re-order for stable output.
|
|
70
|
+
ordered = {}
|
|
71
|
+
ordered["$id"] = hash.delete("$id")
|
|
72
|
+
ordered["type"] = hash.delete("type")
|
|
73
|
+
ordered["status"] = hash.delete("status") if hash.key?("status")
|
|
74
|
+
ordered["comprehensive"] = hash.delete("comprehensive")
|
|
75
|
+
ordered["members"] = hash.delete("members")
|
|
76
|
+
ordered["completeness"] = hash.delete("completeness") if hash.key?("completeness")
|
|
77
|
+
ordered["criterion"] = hash.delete("criterion") if hash.key?("criterion")
|
|
78
|
+
ordered["sources"] = hash.delete("sources") if hash.key?("sources")
|
|
79
|
+
ordered["notes"] = hash.delete("notes") if hash.key?("notes")
|
|
80
|
+
ordered.merge!(hash) # any future fields append in declaration order
|
|
81
|
+
ordered.compact!
|
|
82
|
+
|
|
83
|
+
YAML.dump(ordered).gsub(/^---\s*\n/, "---\n")
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -2,18 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
module Glossarist
|
|
4
4
|
module V3
|
|
5
|
+
# V3 ManagedConcept.
|
|
6
|
+
#
|
|
7
|
+
# V3 storage model:
|
|
8
|
+
# - Concept metadata lives here on the concept (data, related,
|
|
9
|
+
# dates, sources, status).
|
|
10
|
+
# - Hyperedges (PartitiveHyperedge, GenericHyperedge) are PER-FILE:
|
|
11
|
+
# stored at relations/<comprehensive-id>/<criterion-slug>.yaml
|
|
12
|
+
# and loaded via Glossarist::V3::RelationLoader. They are NOT
|
|
13
|
+
# serialized inline on the concept YAML.
|
|
14
|
+
#
|
|
15
|
+
# This is the v3 clean break — the bundled format
|
|
16
|
+
# (`partitive_relations: [...]` inline on the concept YAML) is
|
|
17
|
+
# removed. Concept-model removed it without backward compat in
|
|
18
|
+
# commit a62cf85; ruby aligns.
|
|
19
|
+
#
|
|
20
|
+
# #relations is the unified accessor. The list is populated by
|
|
21
|
+
# callers (GlossaryStore#relations_for, RelationLoader, direct
|
|
22
|
+
# construction). NO typed projections — callers filter:
|
|
23
|
+
#
|
|
24
|
+
# concept.relations.select { |r| r.is_a?(PartitiveHyperedge) }
|
|
25
|
+
# concept.relations.select { |r| r.comprehensive.id == my_id }
|
|
5
26
|
class ManagedConcept < Glossarist::ManagedConcept
|
|
6
27
|
attribute :data, V3::ManagedConceptData, default: -> { V3::ManagedConceptData.new }
|
|
7
28
|
attribute :related, V3::RelatedConcept, collection: true
|
|
8
|
-
attribute :partitive_relations, V3::PartitiveRelation, collection: true
|
|
9
29
|
attribute :dates, V3::ConceptDate, collection: true
|
|
10
30
|
attribute :date_accepted, V3::ConceptDate
|
|
11
31
|
attribute :sources, V3::ConceptSource, collection: true
|
|
12
32
|
|
|
33
|
+
# Unified hyperedge accessor. NOT serialized (per-file storage);
|
|
34
|
+
# populated externally by GlossaryStore / RelationLoader.
|
|
35
|
+
attribute :relations, V3::AbstractHyperedge, collection: true
|
|
36
|
+
|
|
13
37
|
key_value do
|
|
14
38
|
map :data, to: :data
|
|
15
39
|
map :related, to: :related
|
|
16
|
-
map :partitive_relations, to: :partitive_relations
|
|
17
40
|
map :dates, to: :dates
|
|
18
41
|
map %i[date_accepted dateAccepted],
|
|
19
42
|
with: { from: :date_accepted_from_yaml, to: :date_accepted_to_yaml }
|
|
@@ -22,6 +45,7 @@ module Glossarist
|
|
|
22
45
|
with: { from: :uuid_from_yaml, to: :uuid_to_yaml }
|
|
23
46
|
map :schema_version, to: :schema_version
|
|
24
47
|
map :sources, to: :sources
|
|
48
|
+
# relations: intentionally NOT mapped — per-file storage only.
|
|
25
49
|
end
|
|
26
50
|
|
|
27
51
|
def date_accepted_from_yaml(model, value)
|
|
@@ -29,6 +53,33 @@ module Glossarist
|
|
|
29
53
|
{ "date" => value, "type" => "accepted" },
|
|
30
54
|
)
|
|
31
55
|
end
|
|
56
|
+
|
|
57
|
+
# Strict setter — rejects duplicates by identity (type +
|
|
58
|
+
# comprehensive + criterion fingerprint). Adding the same
|
|
59
|
+
# hyperedge twice is always a bug; dedupe hides bugs.
|
|
60
|
+
def relations=(list)
|
|
61
|
+
seen = {}
|
|
62
|
+
Array(list).each do |rel|
|
|
63
|
+
key = hyperedge_identity(rel)
|
|
64
|
+
if seen[key]
|
|
65
|
+
raise ArgumentError,
|
|
66
|
+
"duplicate hyperedge #{key.inspect} — pass each " \
|
|
67
|
+
"decomposition once"
|
|
68
|
+
end
|
|
69
|
+
seen[key] = rel
|
|
70
|
+
end
|
|
71
|
+
super(seen.values)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def hyperedge_identity(rel)
|
|
77
|
+
return rel.object_id.to_s unless rel.is_a?(V3::AbstractHyperedge)
|
|
78
|
+
|
|
79
|
+
comp = Glossarist::ConceptRef.qualified_id(rel.comprehensive)
|
|
80
|
+
crit = rel.criterion.is_a?(Hash) ? rel.criterion.sort.to_h : rel.criterion
|
|
81
|
+
"#{rel.class}:#{comp}:#{crit}"
|
|
82
|
+
end
|
|
32
83
|
end
|
|
33
84
|
end
|
|
34
85
|
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glossarist
|
|
4
|
+
module V3
|
|
5
|
+
# PartitiveHyperedge — an ISO 704 / ISO 1087-1 / ISO 12620 partitive
|
|
6
|
+
# hyperedge connecting a comprehensive concept (superordinate concept
|
|
7
|
+
# partitive) to two or more partitive concepts (subordinate concepts
|
|
8
|
+
# partitive) which fitted together constitute the comprehensive.
|
|
9
|
+
#
|
|
10
|
+
# Inherits structure and validations from AbstractHyperedge.
|
|
11
|
+
# The `comprehensive` field denotes the whole concept.
|
|
12
|
+
#
|
|
13
|
+
# Per-file storage: lives at
|
|
14
|
+
# relations/<comprehensive-id>/<criterion-slug>.yaml — see
|
|
15
|
+
# docs/design/relations-as-files.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 — the SSOT for this leaf's external
|
|
21
|
+
# identifiers. Adding a new hyperedge type means declaring a new
|
|
22
|
+
# class with an equivalent block; no other file needs editing
|
|
23
|
+
# (parsers, serializers, validators, RDF emitters, and the loader
|
|
24
|
+
# all dispatch through HyperedgeRegistry by these constants).
|
|
25
|
+
class PartitiveHyperedge < AbstractHyperedge
|
|
26
|
+
# YAML key on Concept (legacy bundled-format wire name; kept for
|
|
27
|
+
# backward compat with concept-model).
|
|
28
|
+
WIRE_KEY = "partitive_relations"
|
|
29
|
+
|
|
30
|
+
# Per-file `type:` discriminator in relations/<id>/<slug>.yaml.
|
|
31
|
+
TYPE_TAG = "partitive_relation"
|
|
32
|
+
|
|
33
|
+
# RDF type URI (gloss: ontology — concept-model contract).
|
|
34
|
+
RDF_TYPE = "gloss:PartitiveRelation"
|
|
35
|
+
|
|
36
|
+
# Member class — narrows the parent's members: HyperedgeMember.
|
|
37
|
+
MEMBER_CLASS = PartitiveMember
|
|
38
|
+
|
|
39
|
+
# Legacy v1 wire keys that migrate to this class on parse.
|
|
40
|
+
V1_WIRE_KEYS = %w[partitive_hyperedges].freeze
|
|
41
|
+
|
|
42
|
+
# Short label for diff display ("PART" / "GEN" / etc.).
|
|
43
|
+
KIND_LABEL = "PART"
|
|
44
|
+
|
|
45
|
+
attribute :members, PartitiveMember, collection: true
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Auto-register with HyperedgeRegistry. Adding a new hyperedge
|
|
49
|
+
# type means adding a class with an equivalent block + this one
|
|
50
|
+
# register call — nothing else in the codebase changes.
|
|
51
|
+
HyperedgeRegistry.register(PartitiveHyperedge)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -2,112 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
module Glossarist
|
|
4
4
|
module V3
|
|
5
|
-
# PartitiveMember — one member of a
|
|
6
|
-
#
|
|
7
|
-
#
|
|
5
|
+
# PartitiveMember — one member of a PartitiveHyperedge. Inherits
|
|
6
|
+
# the ISO 704:2022 MECE shape from HyperedgeMember; the
|
|
7
|
+
# `comprehensive` of its parent PartitiveHyperedge denotes the
|
|
8
|
+
# whole concept.
|
|
8
9
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
# required — solid line (must exist in every instance)
|
|
14
|
-
# optional — dashed line (may exist; exists in some instances)
|
|
15
|
-
#
|
|
16
|
-
# count (line count — how many):
|
|
17
|
-
# exactly_one — 1 line
|
|
18
|
-
# at_least_one — 1 solid + 1 dashed
|
|
19
|
-
# multiple — 2 lines
|
|
20
|
-
#
|
|
21
|
-
# Valid combinations (5):
|
|
22
|
-
# required + exactly_one = compulsory (1 solid)
|
|
23
|
-
# optional + exactly_one = optional (1 dashed)
|
|
24
|
-
# required + multiple = compulsory_multiple (2 solid)
|
|
25
|
-
# optional + multiple = optional_multiple (2 dashed)
|
|
26
|
-
# required + at_least_one = compulsory_at_least_one (1 solid + 1 dashed)
|
|
27
|
-
#
|
|
28
|
-
# Invalid: optional + at_least_one (collapses to optional + multiple
|
|
29
|
-
# because "at least one, if present" = "zero or more" = optional_multiple).
|
|
30
|
-
#
|
|
31
|
-
# is_delimiting (orthogonal, bold 3x-width line in diagram):
|
|
32
|
-
# A delimiting part behaves like a delimiting characteristic:
|
|
33
|
-
# it distinguishes the comprehensive from coordinate concepts.
|
|
34
|
-
# Example (ISO 704 §5.5.4.2.2): for "optomechanical mouse",
|
|
35
|
-
# the delimiting parts are mouse ball, x/y-axis rollers,
|
|
36
|
-
# infrared emitter/sensor — they distinguish it from
|
|
37
|
-
# "mechanical mouse" and "optical mouse". Mouse button is
|
|
38
|
-
# NOT delimiting (all computer mice have buttons).
|
|
39
|
-
class PartitiveMember < Lutaml::Model::Serializable
|
|
40
|
-
DEFAULT_PRESENCE = "required"
|
|
41
|
-
DEFAULT_COUNT = "exactly_one"
|
|
42
|
-
|
|
43
|
-
attribute :ref, ConceptRef
|
|
44
|
-
attribute :presence, :string,
|
|
45
|
-
values: Glossarist::GlossaryDefinition::PARTITIVE_PRESENCE_VALUES,
|
|
46
|
-
default: -> { DEFAULT_PRESENCE }
|
|
47
|
-
attribute :count, :string,
|
|
48
|
-
values: Glossarist::GlossaryDefinition::PARTITIVE_COUNT_VALUES,
|
|
49
|
-
default: -> { DEFAULT_COUNT }
|
|
50
|
-
attribute :is_delimiting, :boolean, default: -> { false }
|
|
51
|
-
|
|
52
|
-
key_value do
|
|
53
|
-
map :ref, to: :ref
|
|
54
|
-
map :presence, to: :presence
|
|
55
|
-
map :count, to: :count
|
|
56
|
-
map :is_delimiting, to: :is_delimiting
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def validate!
|
|
60
|
-
validate_ref!
|
|
61
|
-
validate_presence_count!
|
|
62
|
-
self
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def required?
|
|
66
|
-
presence == "required"
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def optional?
|
|
70
|
-
presence == "optional"
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def delimiting?
|
|
74
|
-
is_delimiting == true
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
private
|
|
78
|
-
|
|
79
|
-
def validate_ref!
|
|
80
|
-
return if ref.is_a?(ConceptRef) && (ref.source || ref.id || ref.text)
|
|
81
|
-
|
|
82
|
-
raise ArgumentError,
|
|
83
|
-
"PartitiveMember#ref must be a non-empty ConceptRef " \
|
|
84
|
-
"(source, id, or text required)"
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def validate_presence_count!
|
|
88
|
-
unless Glossarist::GlossaryDefinition::PARTITIVE_PRESENCE_VALUES
|
|
89
|
-
.include?(presence)
|
|
90
|
-
raise ArgumentError,
|
|
91
|
-
"PartitiveMember#presence has invalid value " \
|
|
92
|
-
"#{presence.inspect}; must be one of " \
|
|
93
|
-
"#{GlossaryDefinition::PARTITIVE_PRESENCE_VALUES.join(', ')}"
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
unless Glossarist::GlossaryDefinition::PARTITIVE_COUNT_VALUES
|
|
97
|
-
.include?(count)
|
|
98
|
-
raise ArgumentError,
|
|
99
|
-
"PartitiveMember#count has invalid value " \
|
|
100
|
-
"#{count.inspect}; must be one of " \
|
|
101
|
-
"#{GlossaryDefinition::PARTITIVE_COUNT_VALUES.join(', ')}"
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
if presence == "optional" && count == "at_least_one"
|
|
105
|
-
raise ArgumentError,
|
|
106
|
-
"PartitiveMember presence=optional + count=at_least_one is " \
|
|
107
|
-
"invalid — it collapses to optional + multiple (zero or more). " \
|
|
108
|
-
"Use presence: optional, count: multiple instead."
|
|
109
|
-
end
|
|
110
|
-
end
|
|
10
|
+
# Distinct leaf class for type safety and to leave room for
|
|
11
|
+
# partitive-specific extensions if needed.
|
|
12
|
+
class PartitiveMember < HyperedgeMember
|
|
13
|
+
# Inherits all attributes and validations from HyperedgeMember.
|
|
111
14
|
end
|
|
112
15
|
end
|
|
113
16
|
end
|