edoxen 0.7.2 → 0.8.1
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/.rubocop.yml +26 -1
- data/.rubocop_todo.yml +1 -1
- data/CHANGELOG.md +133 -0
- data/README.adoc +414 -182
- data/edoxen.gemspec +10 -10
- data/lib/edoxen/action.rb +5 -4
- data/lib/edoxen/agenda.rb +4 -7
- data/lib/edoxen/agenda_item.rb +12 -5
- data/lib/edoxen/approval.rb +2 -2
- data/lib/edoxen/attendance.rb +16 -10
- data/lib/edoxen/body_vocabulary_entry.rb +24 -0
- data/lib/edoxen/body_vocabulary_host.rb +40 -0
- data/lib/edoxen/cli.rb +175 -115
- data/lib/edoxen/consideration.rb +6 -4
- data/lib/edoxen/contact.rb +35 -0
- data/lib/edoxen/contact_collection.rb +26 -0
- data/lib/edoxen/contact_identifier.rb +13 -0
- data/lib/edoxen/contact_method.rb +16 -0
- data/lib/edoxen/date_time_range.rb +16 -0
- data/lib/edoxen/deadline.rb +1 -1
- data/lib/edoxen/decision.rb +57 -0
- data/lib/edoxen/decision_collection.rb +10 -0
- data/lib/edoxen/decision_date.rb +9 -0
- data/lib/edoxen/decision_metadata.rb +27 -0
- data/lib/edoxen/decision_relation.rb +11 -0
- data/lib/edoxen/declaration.rb +25 -0
- data/lib/edoxen/entity_ref.rb +74 -0
- data/lib/edoxen/enums.rb +123 -9
- data/lib/edoxen/error.rb +1 -1
- data/lib/edoxen/extension_attribute.rb +78 -0
- data/lib/edoxen/host_ref.rb +1 -1
- data/lib/edoxen/link_checker.rb +143 -34
- data/lib/edoxen/localized_name.rb +16 -0
- data/lib/edoxen/localized_string.rb +22 -0
- data/lib/edoxen/meeting.rb +34 -42
- data/lib/edoxen/meeting_collection.rb +1 -2
- data/lib/edoxen/meeting_collection_metadata.rb +4 -2
- data/lib/edoxen/meeting_component.rb +37 -0
- data/lib/edoxen/meeting_extension.rb +25 -0
- data/lib/edoxen/meeting_identifier.rb +1 -1
- data/lib/edoxen/meeting_series.rb +26 -0
- data/lib/edoxen/minutes.rb +3 -4
- data/lib/edoxen/minutes_section.rb +12 -13
- data/lib/edoxen/motion.rb +51 -0
- data/lib/edoxen/name.rb +29 -0
- data/lib/edoxen/officer.rb +19 -0
- data/lib/edoxen/officers_host.rb +19 -0
- data/lib/edoxen/person.rb +5 -9
- data/lib/edoxen/physical_venue.rb +15 -0
- data/lib/edoxen/recurrence.rb +27 -0
- data/lib/edoxen/recurrence_by_day.rb +10 -0
- data/lib/edoxen/reference.rb +1 -1
- data/lib/edoxen/reference_data.rb +34 -44
- data/lib/edoxen/schema_validator.rb +3 -4
- data/lib/edoxen/source_url.rb +4 -4
- data/lib/edoxen/statement.rb +21 -0
- data/lib/edoxen/structured_identifier.rb +2 -2
- data/lib/edoxen/topic.rb +39 -0
- data/lib/edoxen/topic_asset.rb +14 -0
- data/lib/edoxen/topic_document.rb +16 -0
- data/lib/edoxen/venue.rb +79 -0
- data/lib/edoxen/venue_collection.rb +18 -0
- data/lib/edoxen/venue_validator.rb +74 -0
- data/lib/edoxen/version.rb +1 -1
- data/lib/edoxen/virtual_venue.rb +15 -0
- data/lib/edoxen/vote_record.rb +17 -13
- data/lib/edoxen/voting.rb +45 -0
- data/lib/edoxen/voting_counts.rb +23 -0
- data/lib/edoxen.rb +47 -15
- data/schema/edoxen.yaml +1376 -246
- data/schema/meeting.yaml +1207 -272
- data/sig/edoxen.rbs +581 -1
- metadata +62 -22
- data/lib/edoxen/_metadata.rb.deprecated +0 -57
- data/lib/edoxen/localization.rb +0 -18
- data/lib/edoxen/location.rb +0 -15
- data/lib/edoxen/meeting_localization.rb +0 -15
- data/lib/edoxen/resolution.rb +0 -43
- data/lib/edoxen/resolution_collection.rb +0 -10
- data/lib/edoxen/resolution_date.rb +0 -11
- data/lib/edoxen/resolution_metadata.rb +0 -30
- data/lib/edoxen/resolution_relation.rb +0 -11
- data/lib/edoxen/resolution_set.rb +0 -17
- data/lib/edoxen/schedule_item.rb +0 -27
- data/lib/edoxen/schedule_item_localization.rb +0 -21
data/lib/edoxen/link_checker.rb
CHANGED
|
@@ -1,31 +1,54 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "date"
|
|
5
|
+
|
|
3
6
|
module Edoxen
|
|
4
|
-
# Walks a directory tree of Meeting and
|
|
7
|
+
# Walks a directory tree of Meeting and DecisionCollection YAML
|
|
5
8
|
# files and asserts that every cross-document URN link resolves.
|
|
6
9
|
#
|
|
7
10
|
# Invariants enforced:
|
|
8
11
|
#
|
|
9
|
-
# * Every Meeting#urn (if set) is
|
|
10
|
-
#
|
|
11
|
-
# exists in the scanned directory).
|
|
12
|
-
# * Every ResolutionCollection#metadata.meeting_urn (if set)
|
|
12
|
+
# * Every Meeting#urn (if set) is unique across the scanned tree.
|
|
13
|
+
# * Every DecisionCollection#metadata.meeting_urn (if set)
|
|
13
14
|
# resolves to a Meeting whose urn matches.
|
|
14
15
|
#
|
|
15
16
|
# Use from CLI / scripts via the public `check` class method. Returns
|
|
16
17
|
# an array of `LinkError` records (empty = ok).
|
|
17
18
|
#
|
|
18
19
|
# All file IO is read-only. No edits to the YAMLs.
|
|
20
|
+
#
|
|
21
|
+
# Architecture: two passes.
|
|
22
|
+
#
|
|
23
|
+
# 1. `index_pass` walks every file, classifies its top-level shape
|
|
24
|
+
# via one of three MECE predicates (`meeting_collection_shape?`,
|
|
25
|
+
# `meeting_shape?`, `decision_collection_shape?`), and records
|
|
26
|
+
# URN → IndexEntry pairs. Duplicate URNs are reported here.
|
|
27
|
+
# 2. `verify_pass` checks every cross-document link in the indexed
|
|
28
|
+
# collections against the indexed meetings.
|
|
29
|
+
#
|
|
30
|
+
# Adding a new link type is one new field-reading line in
|
|
31
|
+
# `index_pass` (if the source side needs indexing) plus one
|
|
32
|
+
# iteration in `verify_pass`. Adding a new top-level shape is one
|
|
33
|
+
# new predicate + one new branch in `index_pass`.
|
|
19
34
|
class LinkChecker
|
|
20
|
-
|
|
35
|
+
# Internal: file + line pair carried by the URN indexes.
|
|
36
|
+
IndexEntry = Struct.new(:file, :line)
|
|
37
|
+
|
|
38
|
+
# Cross-document link failure. `line` is 1-based. `pointer` is the
|
|
39
|
+
# dotted in-document path that carries the bad reference (e.g.
|
|
40
|
+
# `metadata.meeting_urn` or `meetings[2].urn`).
|
|
41
|
+
LinkError = Struct.new(:file, :line, :pointer, :urn, :kind) do
|
|
21
42
|
def message
|
|
22
|
-
"#{file}: #{pointer} references #{urn} (#{kind})"
|
|
43
|
+
"#{file}:#{line}: #{pointer} references #{urn} (#{kind})"
|
|
23
44
|
end
|
|
45
|
+
|
|
46
|
+
alias_method :to_clickable_format, :message
|
|
24
47
|
end
|
|
25
48
|
|
|
26
49
|
# Walk `dir` (recursive scan of *.yaml and *.yml), classify each
|
|
27
|
-
# file
|
|
28
|
-
#
|
|
50
|
+
# file, and assert every cross-document URN resolves to a real
|
|
51
|
+
# record.
|
|
29
52
|
#
|
|
30
53
|
# @return [Array<LinkError>] empty when all links resolve.
|
|
31
54
|
def self.check(dir)
|
|
@@ -34,52 +57,138 @@ module Edoxen
|
|
|
34
57
|
|
|
35
58
|
def initialize(dir)
|
|
36
59
|
@dir = dir
|
|
37
|
-
@meetings_by_urn = {} # urn → file
|
|
38
|
-
@collections_by_meeting_urn = {} # meeting_urn → file
|
|
60
|
+
@meetings_by_urn = {} # urn → IndexEntry(file, line)
|
|
61
|
+
@collections_by_meeting_urn = {} # meeting_urn → IndexEntry(file, line)
|
|
39
62
|
end
|
|
40
63
|
|
|
41
64
|
def check
|
|
42
|
-
|
|
65
|
+
errors = []
|
|
66
|
+
index_pass(errors)
|
|
67
|
+
verify_pass(errors)
|
|
68
|
+
errors
|
|
69
|
+
end
|
|
43
70
|
|
|
44
|
-
|
|
45
|
-
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
# Phase 1: walk the tree, classify each file, populate the URN
|
|
74
|
+
# indexes. Reports duplicate-URN errors as they surface — the
|
|
75
|
+
# first claim keeps the index slot; subsequent claimants are
|
|
76
|
+
# rejected and flagged, so dangling-link reports always point at
|
|
77
|
+
# the authoritative source.
|
|
78
|
+
def index_pass(errors)
|
|
79
|
+
# `Dir.glob` order is filesystem-dependent; the duplicate-URN
|
|
80
|
+
# specs assert on which file is the "first claim", so sort
|
|
81
|
+
# explicitly for determinism.
|
|
82
|
+
Dir.glob(File.join(@dir, "**", "*.{yaml,yml}")).sort.each do |file| # rubocop:disable Lint/RedundantDirGlobSort
|
|
83
|
+
content = File.read(file)
|
|
84
|
+
data = safe_load_yaml(content)
|
|
46
85
|
next unless data.is_a?(Hash)
|
|
47
86
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
87
|
+
line_map = SchemaValidator::LineMap.build(content)
|
|
88
|
+
|
|
89
|
+
case data
|
|
90
|
+
when ->(d) { meeting_collection_shape?(d) }
|
|
91
|
+
index_meeting_collection(data, file, line_map, errors)
|
|
92
|
+
when ->(d) { meeting_shape?(d) }
|
|
93
|
+
line = line_of(line_map, "/urn")
|
|
94
|
+
register_meeting(data["urn"], file, line, "urn", errors) if data["urn"]
|
|
95
|
+
when ->(d) { decision_collection_shape?(d) }
|
|
96
|
+
index_decision_collection(data, file, line_map)
|
|
54
97
|
end
|
|
55
98
|
end
|
|
99
|
+
end
|
|
56
100
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
@collections_by_meeting_urn.each do |meeting_urn,
|
|
101
|
+
# Phase 2: every cross-document link is checked here. New link
|
|
102
|
+
# types land as additional iterations over the relevant index.
|
|
103
|
+
def verify_pass(errors)
|
|
104
|
+
@collections_by_meeting_urn.each do |meeting_urn, entry|
|
|
61
105
|
next if @meetings_by_urn.key?(meeting_urn)
|
|
62
106
|
|
|
63
|
-
errors << LinkError.new(file, "metadata.meeting_urn", meeting_urn,
|
|
107
|
+
errors << LinkError.new(entry.file, entry.line, "metadata.meeting_urn", meeting_urn,
|
|
108
|
+
"no matching Meeting")
|
|
64
109
|
end
|
|
65
|
-
|
|
66
|
-
errors
|
|
67
110
|
end
|
|
68
111
|
|
|
69
|
-
|
|
112
|
+
# ---- Shape predicates (MECE) --------------------------------------
|
|
113
|
+
#
|
|
114
|
+
# The three predicates partition the universe of edoxen YAML
|
|
115
|
+
# shapes that participate in cross-document URN linking. Files
|
|
116
|
+
# that match none of them (e.g. MeetingSeries, random `_data/*.yaml`)
|
|
117
|
+
# are silently skipped.
|
|
70
118
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
119
|
+
# A MeetingCollection wraps its meetings under a top-level
|
|
120
|
+
# `meetings:` array. Its own top level carries no `identifier` or
|
|
121
|
+
# `type`, so this predicate is disjoint from `meeting_shape?`.
|
|
122
|
+
def meeting_collection_shape?(data)
|
|
123
|
+
data["meetings"].is_a?(Array)
|
|
75
124
|
end
|
|
76
125
|
|
|
126
|
+
# A standalone Meeting carries `identifier` (array) and `type`.
|
|
127
|
+
# MeetingSeries has `identifier` + `kind` (no `type`), so it is
|
|
128
|
+
# correctly excluded — MeetingSeries does not yet participate in
|
|
129
|
+
# cross-document URN linking.
|
|
77
130
|
def meeting_shape?(data)
|
|
78
131
|
data["identifier"].is_a?(Array) && data.key?("type")
|
|
79
132
|
end
|
|
80
133
|
|
|
81
|
-
|
|
82
|
-
|
|
134
|
+
# A DecisionCollection carries `decisions` or `resolutions`. The
|
|
135
|
+
# earlier `metadata.is_a?(Hash)` branch was too permissive — it
|
|
136
|
+
# matched any file with a top-level `metadata:` key, including
|
|
137
|
+
# unrelated YAML. A real DecisionCollection always has one of the
|
|
138
|
+
# two collection keys.
|
|
139
|
+
def decision_collection_shape?(data)
|
|
140
|
+
data["decisions"].is_a?(Array) || data["resolutions"].is_a?(Array)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# ---- Indexers -----------------------------------------------------
|
|
144
|
+
|
|
145
|
+
# Index each nested meeting that satisfies `meeting_shape?` — the
|
|
146
|
+
# same bar applied to standalone meeting files — so a malformed
|
|
147
|
+
# entry carrying only a `urn` cannot make a dangling link appear
|
|
148
|
+
# to resolve. Duplicate URNs are reported via `register_meeting`.
|
|
149
|
+
def index_meeting_collection(data, file, line_map, errors)
|
|
150
|
+
data["meetings"].each_with_index do |meeting, idx|
|
|
151
|
+
next unless meeting.is_a?(Hash) && meeting_shape?(meeting)
|
|
152
|
+
next unless meeting["urn"]
|
|
153
|
+
|
|
154
|
+
line = line_of(line_map, "/meetings/#{idx}/urn")
|
|
155
|
+
register_meeting(meeting["urn"], file, line, "meetings[#{idx}].urn", errors)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def index_decision_collection(data, file, line_map)
|
|
160
|
+
meeting_urn = data.dig("metadata", "meeting_urn")
|
|
161
|
+
return unless meeting_urn
|
|
162
|
+
|
|
163
|
+
line = line_of(line_map, "/metadata/meeting_urn")
|
|
164
|
+
@collections_by_meeting_urn[meeting_urn] = IndexEntry.new(file, line)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Record a Meeting#urn → file/line entry. A second claim on the
|
|
168
|
+
# same URN is reported as a duplicate-URN error and the index
|
|
169
|
+
# keeps the first claim (so dangling-link reports point at the
|
|
170
|
+
# authoritative source, not the most-recent overwrite).
|
|
171
|
+
def register_meeting(urn, file, line, pointer, errors)
|
|
172
|
+
prior = @meetings_by_urn[urn]
|
|
173
|
+
if prior
|
|
174
|
+
errors << LinkError.new(file, line, pointer, urn,
|
|
175
|
+
"duplicate Meeting URN (first seen at #{prior.file}:#{prior.line})")
|
|
176
|
+
return
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
@meetings_by_urn[urn] = IndexEntry.new(file, line)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# ---- Helpers ------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
def line_of(line_map, json_pointer)
|
|
185
|
+
SchemaValidator::LineMap.locate(json_pointer, line_map).first
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def safe_load_yaml(content)
|
|
189
|
+
YAML.safe_load(content, permitted_classes: [Date, Time])
|
|
190
|
+
rescue Psych::Exception, ArgumentError
|
|
191
|
+
nil
|
|
83
192
|
end
|
|
84
193
|
end
|
|
85
194
|
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Edoxen
|
|
4
|
+
# One language-specific value of a translatable Name field.
|
|
5
|
+
# Mirrors LocalizedString but carries a structured Name value rather
|
|
6
|
+
# than a plain String.
|
|
7
|
+
#
|
|
8
|
+
# Use case: a single Contact can be known by different names in
|
|
9
|
+
# different languages — `张建方` (zho-Hans) and `Jianfang Zhang`
|
|
10
|
+
# (acadsin:zho-Hani:Latn:2002 Pinyin romanization) are the same person.
|
|
11
|
+
class LocalizedName < Lutaml::Model::Serializable
|
|
12
|
+
attribute :spelling, :string
|
|
13
|
+
attribute :value, Name
|
|
14
|
+
attribute :extensions, MeetingExtension, collection: true
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Edoxen
|
|
4
|
+
# One language-specific value of a translatable String field.
|
|
5
|
+
# Replaces the v2.x pattern of a separate MeetingLocalization/
|
|
6
|
+
# Localization/ComponentLocalization object that duplicated the
|
|
7
|
+
# entity's translatable fields.
|
|
8
|
+
#
|
|
9
|
+
# `spelling` is an ISO 24229 spelling/conversion system code:
|
|
10
|
+
# - Spelling system: {lang}-{script}[-{country}][-{extension}]
|
|
11
|
+
# e.g. `eng`, `zho-Hans`, `ind-Latn-pre1972`
|
|
12
|
+
# - Conversion system: {authority}:{source}:{target}:{identifying}
|
|
13
|
+
# e.g. `acadsin:zho-Hani:Latn:2002`
|
|
14
|
+
#
|
|
15
|
+
# Always verbose — single-language data uses the same
|
|
16
|
+
# `[{ spelling, value }]` shape as multi-language data.
|
|
17
|
+
class LocalizedString < Lutaml::Model::Serializable
|
|
18
|
+
attribute :spelling, :string
|
|
19
|
+
attribute :value, :string
|
|
20
|
+
attribute :extensions, MeetingExtension, collection: true
|
|
21
|
+
end
|
|
22
|
+
end
|
data/lib/edoxen/meeting.rb
CHANGED
|
@@ -1,78 +1,70 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Edoxen
|
|
4
|
-
# A single Meeting —
|
|
5
|
-
# identity, time,
|
|
6
|
-
# URN links to one or more
|
|
4
|
+
# A single Meeting (event) — produces Decisions via Motions and Votings.
|
|
5
|
+
# Carries identity, time, polymorphic venues, officers, agenda, components,
|
|
6
|
+
# attendance, minutes, and URN links to one or more DecisionCollection
|
|
7
|
+
# documents.
|
|
7
8
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
# resolutions only after adoption.
|
|
9
|
+
# 1.0 (per-field localization, ISO 24229):
|
|
10
|
+
# - Removed `localizations[]` collection.
|
|
11
|
+
# - Added per-field LocalizedString for title, general_area,
|
|
12
|
+
# practical_info, note.
|
|
13
13
|
class Meeting < Lutaml::Model::Serializable
|
|
14
|
+
include OfficersHost
|
|
15
|
+
|
|
14
16
|
attribute :identifier, StructuredIdentifier, collection: true
|
|
15
17
|
attribute :urn, :string
|
|
16
18
|
attribute :ordinal, :integer
|
|
19
|
+
attribute :series_ref, :string
|
|
17
20
|
attribute :type, :string, values: Enums::MEETING_TYPE
|
|
18
21
|
attribute :status, :string, values: Enums::MEETING_STATUS
|
|
19
|
-
attribute :
|
|
20
|
-
|
|
21
|
-
attribute :date_range, DateRange
|
|
22
|
+
attribute :visibility, :string, values: Enums::VISIBILITY
|
|
23
|
+
attribute :body_type, :string
|
|
22
24
|
|
|
23
|
-
attribute :
|
|
24
|
-
attribute :
|
|
25
|
+
attribute :title, LocalizedString, collection: true
|
|
26
|
+
attribute :scheduled_date_range, DateRange
|
|
27
|
+
attribute :occurred_date_range, DateTimeRange
|
|
28
|
+
attribute :recurrence, Recurrence
|
|
25
29
|
|
|
26
|
-
attribute :venues,
|
|
27
|
-
attribute :general_area, :
|
|
30
|
+
attribute :venues, Venue, collection: true
|
|
31
|
+
attribute :general_area, LocalizedString, collection: true
|
|
32
|
+
attribute :practical_info, LocalizedString, collection: true
|
|
28
33
|
attribute :city, :string
|
|
29
34
|
attribute :country_code, :string
|
|
30
|
-
attribute :virtual, :boolean
|
|
31
35
|
|
|
32
|
-
attribute :
|
|
33
|
-
attribute :
|
|
34
|
-
|
|
36
|
+
attribute :committee, :string
|
|
37
|
+
attribute :committee_group, :string
|
|
38
|
+
|
|
39
|
+
attribute :officers, Officer, collection: true
|
|
35
40
|
attribute :hosts, HostRef, collection: true
|
|
36
41
|
|
|
37
42
|
attribute :source_urls, SourceUrl, collection: true
|
|
38
43
|
attribute :landing_url, :string
|
|
39
44
|
attribute :registration_url, :string
|
|
45
|
+
attribute :note, LocalizedString, collection: true
|
|
46
|
+
attribute :contact, Contact
|
|
40
47
|
|
|
41
48
|
attribute :agenda, Agenda
|
|
42
|
-
attribute :
|
|
49
|
+
attribute :components, MeetingComponent, collection: true
|
|
43
50
|
attribute :deadlines, Deadline, collection: true
|
|
44
51
|
|
|
45
52
|
attribute :attendance, Attendance, collection: true
|
|
46
|
-
attribute :vote_records, VoteRecord, collection: true
|
|
47
53
|
attribute :minutes, Minutes, collection: true
|
|
54
|
+
attribute :declarations, Declaration, collection: true
|
|
48
55
|
|
|
49
|
-
|
|
50
|
-
attribute :
|
|
51
|
-
attribute :
|
|
52
|
-
|
|
53
|
-
# Lookup by ISO 639-3 language code. Mirrors Resolution#in_language.
|
|
54
|
-
def in_language(code, fallback: false)
|
|
55
|
-
match = localizations&.find { |loc| loc.language_code == code.to_s }
|
|
56
|
-
return match if match
|
|
57
|
-
|
|
58
|
-
fallback ? localizations&.first : nil
|
|
59
|
-
end
|
|
56
|
+
# Outcomes (canonical location on Meeting)
|
|
57
|
+
attribute :decisions, Decision, collection: true
|
|
58
|
+
attribute :motions, Motion, collection: true
|
|
59
|
+
attribute :votings, Voting, collection: true
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
in_language("eng", fallback: true)
|
|
64
|
-
end
|
|
61
|
+
attribute :relations, MeetingRelation, collection: true
|
|
62
|
+
attribute :extensions, MeetingExtension, collection: true
|
|
65
63
|
|
|
66
|
-
# Find an agenda item by label. Returns nil when no agenda or no
|
|
67
|
-
# matching label.
|
|
68
64
|
def find_agenda_item(label)
|
|
69
65
|
agenda&.find_item(label)
|
|
70
66
|
end
|
|
71
67
|
|
|
72
|
-
# Resolve the meeting's UN/LOCODE via the canonical `unlocode` gem
|
|
73
|
-
# registry. Returns an `Unlocodes::Entry` (with `#name`, `#country`,
|
|
74
|
-
# `#coordinates`, `#iata`, `#functions`, etc.) or nil when the
|
|
75
|
-
# city field is empty / not in the registry.
|
|
76
68
|
def city_entry
|
|
77
69
|
return nil if city.nil? || city.to_s.empty?
|
|
78
70
|
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Edoxen
|
|
4
|
-
# Top-level container for many Meetings. Parallel to
|
|
5
|
-
# ResolutionCollection.
|
|
4
|
+
# Top-level container for many Meetings. Parallel to DecisionCollection.
|
|
6
5
|
class MeetingCollection < Lutaml::Model::Serializable
|
|
7
6
|
attribute :metadata, MeetingCollectionMetadata
|
|
8
7
|
attribute :meetings, Meeting, collection: true
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
module Edoxen
|
|
4
4
|
# Top-level wrapper for many Meetings in a single YAML file. Parallel
|
|
5
|
-
# to
|
|
5
|
+
# to DecisionCollection. The metadata block carries display-level
|
|
6
6
|
# info (title, source); per-meeting identity lives on each Meeting.
|
|
7
7
|
class MeetingCollectionMetadata < Lutaml::Model::Serializable
|
|
8
|
-
|
|
8
|
+
include BodyVocabularyHost
|
|
9
|
+
|
|
10
|
+
attribute :title, LocalizedString, collection: true
|
|
9
11
|
attribute :source, :string
|
|
10
12
|
end
|
|
11
13
|
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Edoxen
|
|
4
|
+
# MeetingComponent — flat sub-event of a Meeting.
|
|
5
|
+
# Replaces legacy ScheduleItem.
|
|
6
|
+
#
|
|
7
|
+
# 1.0 (per-field localization, ISO 24229):
|
|
8
|
+
# - Removed `localizations[]` collection.
|
|
9
|
+
# - Per-field LocalizedString for title, description, time_label.
|
|
10
|
+
class MeetingComponent < Lutaml::Model::Serializable
|
|
11
|
+
include OfficersHost
|
|
12
|
+
|
|
13
|
+
attribute :identifier, :string
|
|
14
|
+
attribute :urn, :string
|
|
15
|
+
attribute :kind, :string, values: Enums::COMPONENT_KIND
|
|
16
|
+
# Identifier of the body this component belongs to. Optional;
|
|
17
|
+
# usually inherited from the parent Meeting.
|
|
18
|
+
attribute :body_type, :string
|
|
19
|
+
attribute :title, LocalizedString, collection: true
|
|
20
|
+
attribute :description, LocalizedString, collection: true
|
|
21
|
+
attribute :starts_at, :date_time
|
|
22
|
+
attribute :ends_at, :date_time
|
|
23
|
+
attribute :time_label, LocalizedString, collection: true
|
|
24
|
+
attribute :venue_refs, :string, collection: true
|
|
25
|
+
attribute :officers, Officer, collection: true
|
|
26
|
+
attribute :agenda_ref, :string
|
|
27
|
+
attribute :minutes_ref, :string
|
|
28
|
+
attribute :attendance_refs, :string, collection: true
|
|
29
|
+
attribute :extensions, MeetingExtension, collection: true
|
|
30
|
+
|
|
31
|
+
def duration_seconds
|
|
32
|
+
return nil unless starts_at && ends_at
|
|
33
|
+
|
|
34
|
+
ends_at.to_time - starts_at.to_time
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Edoxen
|
|
4
|
+
# MeetingExtension — the profile mechanism (ISO 8601-2 §15).
|
|
5
|
+
# Every core entity has an `extensions: MeetingExtension[0..*]` slot.
|
|
6
|
+
# Adopters register their own profile namespace (e.g. "legco",
|
|
7
|
+
# "us-congress", "ietf") and define `kind` values within it.
|
|
8
|
+
#
|
|
9
|
+
# Field semantics (tightened 1.0, per 1.0 design review):
|
|
10
|
+
#
|
|
11
|
+
# profile — the profile namespace (lowercase, hyphen-separated).
|
|
12
|
+
# kind — discriminator within the profile.
|
|
13
|
+
# ref — URN of the profile document this extension references.
|
|
14
|
+
# attributes — typed key/value pairs (ExtensionAttribute).
|
|
15
|
+
#
|
|
16
|
+
# Recursion (`extensions: MeetingExtension[0..*]`) was removed in
|
|
17
|
+
# 1.0 — no documented use case. Profiles needing nesting can encode
|
|
18
|
+
# it via dotted keys ("vote.count", "vote.method") in `attributes[]`.
|
|
19
|
+
class MeetingExtension < Lutaml::Model::Serializable
|
|
20
|
+
attribute :profile, :string
|
|
21
|
+
attribute :kind, :string
|
|
22
|
+
attribute :ref, :string
|
|
23
|
+
attribute :attributes, ExtensionAttribute, collection: true
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Edoxen
|
|
4
4
|
# Identifier of a meeting (venue + date). Singular — the meeting a
|
|
5
|
-
# particular
|
|
5
|
+
# particular Decision belongs to.
|
|
6
6
|
class MeetingIdentifier < Lutaml::Model::Serializable
|
|
7
7
|
attribute :venue, :string
|
|
8
8
|
attribute :date, :date
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Edoxen
|
|
4
|
+
# MeetingSeries — parent of recurring Meeting instances.
|
|
5
|
+
# Crossref `proceedings-series`, IETF meeting series, Apache Board
|
|
6
|
+
# meetings, etc. Carries a `Recurrence` (ISO 8601-2 §13) rule and
|
|
7
|
+
# a list of member Meeting URNs.
|
|
8
|
+
class MeetingSeries < Lutaml::Model::Serializable
|
|
9
|
+
attribute :identifier, StructuredIdentifier, collection: true
|
|
10
|
+
# Identifier of the body this series represents (e.g. 'ciml').
|
|
11
|
+
# Meetings and Decisions belonging to this series inherit the body
|
|
12
|
+
# via the series_ref join. Optional; most consumers have a single
|
|
13
|
+
# body and leave this unset.
|
|
14
|
+
attribute :body_type, :string
|
|
15
|
+
attribute :urn, :string
|
|
16
|
+
attribute :name, LocalizedString, collection: true
|
|
17
|
+
attribute :description, LocalizedString, collection: true
|
|
18
|
+
attribute :recurrence, Recurrence
|
|
19
|
+
attribute :term, :string
|
|
20
|
+
attribute :contact, Contact
|
|
21
|
+
attribute :hosts, HostRef, collection: true
|
|
22
|
+
attribute :kind, :string
|
|
23
|
+
attribute :meeting_refs, :string, collection: true
|
|
24
|
+
attribute :extensions, MeetingExtension, collection: true
|
|
25
|
+
end
|
|
26
|
+
end
|
data/lib/edoxen/minutes.rb
CHANGED
|
@@ -7,16 +7,15 @@ module Edoxen
|
|
|
7
7
|
# TODO.meeting-agenda/10-future-enhancements.md).
|
|
8
8
|
#
|
|
9
9
|
# Distinct from Agenda (the business order drafted before the
|
|
10
|
-
# meeting) and from
|
|
10
|
+
# meeting) and from DecisionCollection (the formal decisions
|
|
11
11
|
# adopted). The Minutes are the running record.
|
|
12
12
|
#
|
|
13
13
|
# Sections are typically keyed by agenda-item number so consumers
|
|
14
|
-
# can join Minutes ↔ AgendaItem ↔
|
|
14
|
+
# can join Minutes ↔ AgendaItem ↔ Decision by `number`/`label`.
|
|
15
15
|
class Minutes < Lutaml::Model::Serializable
|
|
16
16
|
attribute :identifier, StructuredIdentifier, collection: true
|
|
17
17
|
attribute :urn, :string
|
|
18
|
-
attribute :
|
|
19
|
-
attribute :script, :string
|
|
18
|
+
attribute :spelling, :string
|
|
20
19
|
attribute :source_doc, :string
|
|
21
20
|
attribute :source_pages, :string
|
|
22
21
|
attribute :sections, MinutesSection, collection: true
|
|
@@ -5,23 +5,22 @@ module Edoxen
|
|
|
5
5
|
# item by its `number` field. Carries the narrative as Markdown
|
|
6
6
|
# (the format the GLM-OCR pipeline emits) plus optional page range
|
|
7
7
|
# for provenance back to the source PDF.
|
|
8
|
+
#
|
|
9
|
+
# Localization lives on the parent Minutes document, not on each
|
|
10
|
+
# section — Minutes has a per-document language_code; the section
|
|
11
|
+
# lookup is `Minutes#find_section(number)`. Earlier copies of this
|
|
12
|
+
# class inherited `in_language`/`primary_localization` from
|
|
13
|
+
# ScheduleItem/Meeting, but those methods referenced an undeclared
|
|
14
|
+
# `localizations` attribute and raised NameError on every call.
|
|
15
|
+
# Removed 2026-07-05.
|
|
8
16
|
class MinutesSection < Lutaml::Model::Serializable
|
|
9
17
|
attribute :number, :string
|
|
10
|
-
attribute :title, :
|
|
11
|
-
attribute :narrative, :
|
|
18
|
+
attribute :title, LocalizedString, collection: true
|
|
19
|
+
attribute :narrative, LocalizedString, collection: true
|
|
12
20
|
attribute :page_start, :integer
|
|
13
21
|
attribute :page_end, :integer
|
|
14
22
|
attribute :references, Reference, collection: true
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
match = localizations&.find { |loc| loc.language_code == code.to_s }
|
|
18
|
-
return match if match
|
|
19
|
-
|
|
20
|
-
fallback ? localizations&.first : nil
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def primary_localization
|
|
24
|
-
in_language("eng", fallback: true)
|
|
25
|
-
end
|
|
23
|
+
attribute :statements, Statement, collection: true
|
|
24
|
+
attribute :topic_ref, :string
|
|
26
25
|
end
|
|
27
26
|
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Edoxen
|
|
4
|
+
# Motion — a procedural act that brings a Decision. "I move that..."
|
|
5
|
+
# Distinct from a Decision (formal outcome) and from a TopicDocument
|
|
6
|
+
# (written text). A Motion does NOT require any document.
|
|
7
|
+
#
|
|
8
|
+
# State machine (MotionStatus):
|
|
9
|
+
# introduced → seconded → debating → question_put → voting
|
|
10
|
+
# ├── carried → resulting_decision set
|
|
11
|
+
# ├── negatived
|
|
12
|
+
# ├── withdrawn
|
|
13
|
+
# └── lapsed
|
|
14
|
+
class Motion < Lutaml::Model::Serializable
|
|
15
|
+
attribute :identifier, :string
|
|
16
|
+
attribute :urn, :string
|
|
17
|
+
attribute :text, LocalizedString, collection: true
|
|
18
|
+
attribute :mover, Person
|
|
19
|
+
attribute :seconders, Person, collection: true
|
|
20
|
+
attribute :status, :string, values: Enums::MOTION_STATUS
|
|
21
|
+
attribute :introduced_at, :date_time
|
|
22
|
+
attribute :proposed_decision, :string
|
|
23
|
+
attribute :resulting_decision, :string
|
|
24
|
+
# Pilot EntityRef field (1.0, 1.0 design review). Parallel to
|
|
25
|
+
# `resulting_decision` (String). Prefer the typed form in new code;
|
|
26
|
+
# the bare String form is removed in 1.0.
|
|
27
|
+
attribute :resulting_decision_ref, EntityRef
|
|
28
|
+
attribute :votings, :string, collection: true
|
|
29
|
+
attribute :extensions, MeetingExtension, collection: true
|
|
30
|
+
|
|
31
|
+
def carried?
|
|
32
|
+
status == "carried"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def pending?
|
|
36
|
+
status && !Enums::MOTION_TERMINAL.include?(status)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# --- 1.0 derivation accessor (1.0 design review) --------------------
|
|
40
|
+
# Storage side: Voting.on_motion (SSOT for the Motion→Voting
|
|
41
|
+
# relationship). This computed method returns the Voting instances
|
|
42
|
+
# in `meeting` whose `on_motion` points at this Motion's URN.
|
|
43
|
+
# The stored `votings[]` field remains on the wire for back-compat
|
|
44
|
+
# through v2.x; 1.0 removes it and this becomes the only path.
|
|
45
|
+
def votings_in(meeting:)
|
|
46
|
+
return [] unless meeting && urn
|
|
47
|
+
|
|
48
|
+
meeting.votings.select { |voting| voting.on_motion == urn }
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
data/lib/edoxen/name.rb
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Edoxen
|
|
4
|
+
# Structured personal/organisational name. VCARD conventions
|
|
5
|
+
# (RFC 6350): separate structured components (N) from a pre-formatted
|
|
6
|
+
# display string (FN). Either or both may be populated.
|
|
7
|
+
#
|
|
8
|
+
# `formatted` is the display string when callers have it pre-built;
|
|
9
|
+
# the structured fields (`family`, `given`, `additional`, `prefix`,
|
|
10
|
+
# `suffix`) let adopters store parsed components for sorting, indexing,
|
|
11
|
+
# or locale-aware rendering.
|
|
12
|
+
class Name < Lutaml::Model::Serializable
|
|
13
|
+
attribute :formatted, :string
|
|
14
|
+
attribute :family, :string
|
|
15
|
+
attribute :given, :string
|
|
16
|
+
attribute :additional, :string
|
|
17
|
+
attribute :prefix, :string
|
|
18
|
+
attribute :suffix, :string
|
|
19
|
+
attribute :extensions, MeetingExtension, collection: true
|
|
20
|
+
|
|
21
|
+
# Convenience: returns `formatted` if present, else builds one from
|
|
22
|
+
# the structured components. Useful for callers that always want a
|
|
23
|
+
# display string regardless of how the name was authored.
|
|
24
|
+
def display
|
|
25
|
+
formatted || [prefix, given, additional, family, suffix]
|
|
26
|
+
.reject { |s| s.to_s.empty? }.join(" ")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|