edoxen 0.7.2 → 2.1.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 +17 -1
- data/.rubocop_todo.yml +1 -1
- data/CHANGELOG.md +246 -0
- data/README.adoc +308 -153
- data/edoxen.gemspec +10 -10
- data/lib/edoxen/action.rb +1 -1
- data/lib/edoxen/agenda.rb +4 -7
- data/lib/edoxen/agenda_item.rb +10 -3
- data/lib/edoxen/approval.rb +1 -1
- data/lib/edoxen/attendance.rb +16 -10
- data/lib/edoxen/body_vocabulary_entry.rb +24 -0
- data/lib/edoxen/body_vocabulary_host.rb +29 -0
- data/lib/edoxen/cli.rb +117 -120
- data/lib/edoxen/component_localization.rb +11 -0
- data/lib/edoxen/consideration.rb +2 -2
- data/lib/edoxen/contact.rb +24 -0
- data/lib/edoxen/contact_identifier.rb +13 -0
- data/lib/edoxen/contact_method.rb +16 -0
- data/lib/edoxen/decision.rb +69 -0
- data/lib/edoxen/decision_collection.rb +10 -0
- data/lib/edoxen/decision_date.rb +9 -0
- data/lib/edoxen/{resolution_metadata.rb → decision_metadata.rb} +5 -6
- data/lib/edoxen/decision_relation.rb +11 -0
- data/lib/edoxen/entity_ref.rb +74 -0
- data/lib/edoxen/enums.rb +117 -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 +8 -8
- data/lib/edoxen/localization.rb +2 -2
- data/lib/edoxen/localization_host.rb +23 -0
- data/lib/edoxen/meeting.rb +63 -39
- data/lib/edoxen/meeting_collection.rb +1 -2
- data/lib/edoxen/meeting_collection_metadata.rb +3 -1
- data/lib/edoxen/meeting_component.rb +41 -0
- data/lib/edoxen/meeting_extension.rb +25 -0
- data/lib/edoxen/meeting_identifier.rb +1 -1
- data/lib/edoxen/meeting_localization.rb +6 -1
- data/lib/edoxen/meeting_series.rb +21 -0
- data/lib/edoxen/minutes.rb +2 -2
- data/lib/edoxen/minutes_section.rb +8 -11
- 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_data.rb +35 -41
- data/lib/edoxen/schema_validator.rb +2 -3
- data/lib/edoxen/source_url.rb +3 -3
- data/lib/edoxen/structured_identifier.rb +2 -2
- data/lib/edoxen/topic.rb +37 -0
- data/lib/edoxen/topic_asset.rb +14 -0
- data/lib/edoxen/topic_document.rb +16 -0
- data/lib/edoxen/venue.rb +80 -0
- data/lib/edoxen/venue_validator.rb +56 -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 +40 -13
- data/schema/edoxen.yaml +792 -145
- data/schema/meeting.yaml +600 -142
- data/sig/edoxen.rbs +581 -1
- metadata +57 -20
- data/lib/edoxen/_metadata.rb.deprecated +0 -57
- data/lib/edoxen/location.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_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
|
@@ -4,7 +4,9 @@ module Edoxen
|
|
|
4
4
|
# Collection-level metadata: the title (string for default / single-language
|
|
5
5
|
# collections, or `title_localized[]` for multilingual), the meeting date,
|
|
6
6
|
# the source secretariat, per-language source PDFs, and the host venue.
|
|
7
|
-
class
|
|
7
|
+
class DecisionMetadata < Lutaml::Model::Serializable
|
|
8
|
+
include BodyVocabularyHost
|
|
9
|
+
|
|
8
10
|
attribute :title, :string
|
|
9
11
|
attribute :title_localized, Localization, collection: true
|
|
10
12
|
attribute :date, :date
|
|
@@ -14,13 +16,10 @@ module Edoxen
|
|
|
14
16
|
attribute :country_code, :string
|
|
15
17
|
|
|
16
18
|
# URN back-reference to the Meeting that produced this collection.
|
|
17
|
-
# Join key for the Meeting ↔ ResolutionCollection link. Optional —
|
|
18
|
-
# collections without a parent meeting (e.g., ad-hoc resolution
|
|
19
|
-
# sets) simply omit it.
|
|
20
19
|
attribute :meeting_urn, :string
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
attribute :extensions, MeetingExtension, collection: true
|
|
22
|
+
|
|
24
23
|
def city_entry
|
|
25
24
|
return nil if city.nil? || city.to_s.empty?
|
|
26
25
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Edoxen
|
|
4
|
+
# Directed relation between two decisions, identified by their
|
|
5
|
+
# StructuredIdentifier (prefix + number).
|
|
6
|
+
class DecisionRelation < Lutaml::Model::Serializable
|
|
7
|
+
attribute :source, StructuredIdentifier
|
|
8
|
+
attribute :destination, StructuredIdentifier
|
|
9
|
+
attribute :type, :string, values: Enums::DECISION_RELATION_TYPE
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Edoxen
|
|
4
|
+
# EntityRef — a typed cross-reference between entities (v2.1, TODO.refactor/44).
|
|
5
|
+
#
|
|
6
|
+
# Single identity: exactly one of `urn`, `identifier`, or `local_ref`
|
|
7
|
+
# must be set. Optional metadata: `kind`, `role`, `note`.
|
|
8
|
+
#
|
|
9
|
+
# Encapsulation:
|
|
10
|
+
# - Read the identity via `#resolved_identity` (returns the canonical
|
|
11
|
+
# form — URN string, StructuredIdentifier, or local key).
|
|
12
|
+
# - Validate via `#valid?` (true when exactly one identity field is
|
|
13
|
+
# set; `#multiple_identities?` flags the rare ambiguous case).
|
|
14
|
+
#
|
|
15
|
+
# Pilot: `Motion.resulting_decision_ref` (parallel to the existing
|
|
16
|
+
# `resulting_decision: String`). v3.0 will remove the String form.
|
|
17
|
+
class EntityRef < Lutaml::Model::Serializable
|
|
18
|
+
attribute :urn, :string
|
|
19
|
+
attribute :identifier, StructuredIdentifier
|
|
20
|
+
attribute :local_ref, :string
|
|
21
|
+
|
|
22
|
+
attribute :kind, :string
|
|
23
|
+
attribute :role, :string
|
|
24
|
+
attribute :note, :string
|
|
25
|
+
|
|
26
|
+
# True when exactly one identity field is set. The wire contract
|
|
27
|
+
# (TODO.refactor/44 + JSON-Schema) is XOR: setting 0 or ≥2 identity
|
|
28
|
+
# fields is a data error.
|
|
29
|
+
def valid?
|
|
30
|
+
identities_set.size == 1
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# True when two or more identity fields are set. Useful for
|
|
34
|
+
# migration tooling that wants to warn on ambiguity without
|
|
35
|
+
# rejecting outright.
|
|
36
|
+
def multiple_identities?
|
|
37
|
+
identities_set.size > 1
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Returns the canonical identity (URN > identifier > local_ref
|
|
41
|
+
# precedence). Callers that need a single value should validate
|
|
42
|
+
# first via `#valid?`; on a multiple-identity ref the precedence
|
|
43
|
+
# is still applied but the data is suspect.
|
|
44
|
+
def resolved_identity
|
|
45
|
+
return urn if urn && !urn.to_s.empty?
|
|
46
|
+
return identifier if identifier
|
|
47
|
+
return local_ref if local_ref && !local_ref.to_s.empty?
|
|
48
|
+
|
|
49
|
+
nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def to_s
|
|
53
|
+
identity = resolved_identity
|
|
54
|
+
return "(invalid EntityRef)" unless identity
|
|
55
|
+
|
|
56
|
+
case identity
|
|
57
|
+
when StructuredIdentifier then "#{identity.prefix}/#{identity.number}"
|
|
58
|
+
else identity.to_s
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def identities_set
|
|
65
|
+
[urn, identifier, local_ref].reject do |value|
|
|
66
|
+
case value
|
|
67
|
+
when nil then true
|
|
68
|
+
when String then value.empty?
|
|
69
|
+
else false
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
data/lib/edoxen/enums.rb
CHANGED
|
@@ -26,13 +26,37 @@ module Edoxen
|
|
|
26
26
|
emphasizing concerned accepts observing referring acting empowers
|
|
27
27
|
].freeze
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
# --- Decision-side (was Resolution; renamed 2026-07) -----------------
|
|
30
|
+
|
|
31
|
+
# DecisionKind — what kind of formal outcome this is. `resolution` is
|
|
32
|
+
# one value; the same formal concept has many names across bodies
|
|
33
|
+
# (order, ruling, determination, finding, opinion, etc.).
|
|
34
|
+
DECISION_KIND = %w[
|
|
35
|
+
resolution order ruling determination
|
|
36
|
+
recommendation statement finding opinion other
|
|
37
|
+
].freeze
|
|
38
|
+
|
|
39
|
+
# DecisionKindCanonical — the short abstract set (v2.1, TODO.refactor/46).
|
|
40
|
+
# Bodies extend via `body_type: String` + per-dataset `body_vocabulary[]`
|
|
41
|
+
# on DecisionMetadata. `other` is a temporary escape while the
|
|
42
|
+
# vocabulary stabilises; v3.0 removes it.
|
|
43
|
+
#
|
|
44
|
+
# Cap: 5 canonical values (hard limit per the v2.1 design review).
|
|
45
|
+
DECISION_KIND_CANONICAL = %w[decision recommendation statement finding other].freeze
|
|
46
|
+
|
|
47
|
+
# DecisionStatus — lifecycle state machine.
|
|
48
|
+
DECISION_STATUS = %w[
|
|
49
|
+
draft proposed under_consideration
|
|
50
|
+
decided negatived withdrawn deferred
|
|
51
|
+
].freeze
|
|
30
52
|
|
|
31
|
-
|
|
32
|
-
|
|
53
|
+
DECISION_RELATION_TYPE = %w[
|
|
54
|
+
annex_of has_annex updates refines replaces considers cites
|
|
33
55
|
].freeze
|
|
34
56
|
|
|
35
|
-
|
|
57
|
+
DECISION_DATE_TYPE = %w[
|
|
58
|
+
adoption drafted discussed proposed decided negatived withdrawn published effective
|
|
59
|
+
].freeze
|
|
36
60
|
|
|
37
61
|
APPROVAL_TYPE = %w[affirmative negative].freeze
|
|
38
62
|
|
|
@@ -44,31 +68,115 @@ module Edoxen
|
|
|
44
68
|
# Mirrors edoxen-model/models/{meeting,agenda,...}_*.lutaml.
|
|
45
69
|
# schema/meeting.yaml references these via $defs/<EnumName>.enum.
|
|
46
70
|
|
|
47
|
-
MEETING_TYPE = %w[
|
|
71
|
+
MEETING_TYPE = %w[
|
|
72
|
+
plenary working_group task_group ad_hoc joint general_assembly
|
|
73
|
+
committee subcommittee conference workshop seminar webinar hearing
|
|
74
|
+
markup board_meeting annual_general_meeting other
|
|
75
|
+
].freeze
|
|
76
|
+
|
|
77
|
+
# MeetingTypeCanonical — the short abstract set (v2.1, TODO.refactor/46).
|
|
78
|
+
# Bodies extend via `body_type: String` + per-dataset `body_vocabulary[]`
|
|
79
|
+
# on MeetingCollectionMetadata.
|
|
80
|
+
#
|
|
81
|
+
# Cap: 4 canonical values (no `other` — bodies pick the closest fit).
|
|
82
|
+
MEETING_TYPE_CANONICAL = %w[plenary governing working advisory].freeze
|
|
48
83
|
|
|
49
84
|
MEETING_STATUS = %w[upcoming completed cancelled].freeze
|
|
50
85
|
|
|
51
86
|
AGENDA_STATUS = %w[draft final amended cancelled superseded].freeze
|
|
52
87
|
|
|
53
|
-
AGENDA_ITEM_KIND = %w[numbered unnumbered header opening closing].freeze
|
|
88
|
+
AGENDA_ITEM_KIND = %w[numbered unnumbered header opening closing aob].freeze
|
|
54
89
|
|
|
55
|
-
AGENDA_ITEM_OUTCOME = %w[discussed resolved deferred adopted withdrawn].freeze
|
|
90
|
+
AGENDA_ITEM_OUTCOME = %w[discussed resolved deferred adopted withdrawn carried negatived].freeze
|
|
56
91
|
|
|
57
92
|
HOST_TYPE = %w[national_body liaison associate organizer].freeze
|
|
58
93
|
|
|
59
94
|
MEETING_RELATION_TYPE = %w[
|
|
60
95
|
continues_from continues_to joint_with supersedes superseded_by rescheduled_from rescheduled_to
|
|
96
|
+
parent_of child_of sibling_of depends_on
|
|
97
|
+
finish_to_start finish_to_finish start_to_start start_to_finish
|
|
61
98
|
].freeze
|
|
62
99
|
|
|
63
100
|
SOURCE_URL_KIND = %w[
|
|
64
|
-
agenda_pdf minutes_pdf
|
|
101
|
+
agenda_pdf minutes_pdf decisions_pdf report_pdf register_url landing_page
|
|
65
102
|
].freeze
|
|
66
103
|
|
|
67
|
-
# P2.6 / P2.7 — Attendance and Vote records.
|
|
68
104
|
PARTICIPATION_STATUS = %w[present absent apologies observer excused].freeze
|
|
69
105
|
|
|
70
106
|
VOTE_TYPE = %w[
|
|
71
107
|
affirmative negative abstain absent not_applicable
|
|
72
108
|
].freeze
|
|
109
|
+
|
|
110
|
+
# --- New in v2.0 (broadened scope) -----------------------------------
|
|
111
|
+
|
|
112
|
+
VENUE_KIND = %w[physical virtual].freeze
|
|
113
|
+
|
|
114
|
+
VIRTUAL_FEATURE = %w[audio video chat phone screen feed].freeze
|
|
115
|
+
|
|
116
|
+
VISIBILITY = %w[public private confidential].freeze
|
|
117
|
+
|
|
118
|
+
ATTENDANCE_ROLE = %w[chair required optional non_participant].freeze
|
|
119
|
+
|
|
120
|
+
# iCalendar PARTSTAT, plain English (no jargon).
|
|
121
|
+
ATTENDANCE_RESPONSE = %w[
|
|
122
|
+
pending confirmed declined tentative delegated
|
|
123
|
+
].freeze
|
|
124
|
+
|
|
125
|
+
COMPONENT_KIND = %w[
|
|
126
|
+
track session debate breakout bof
|
|
127
|
+
plenary_session working_group_session committee_of_the_whole
|
|
128
|
+
keynote address statement question_time
|
|
129
|
+
opening closing break reception registration networking other
|
|
130
|
+
].freeze
|
|
131
|
+
|
|
132
|
+
# ComponentKindCanonical — the short abstract set (v2.1, TODO.refactor/46).
|
|
133
|
+
# `other` is a temporary escape while the vocabulary stabilises.
|
|
134
|
+
#
|
|
135
|
+
# Cap: 5 canonical values.
|
|
136
|
+
COMPONENT_KIND_CANONICAL = %w[deliberative working ceremonial break other].freeze
|
|
137
|
+
|
|
138
|
+
MOTION_STATUS = %w[
|
|
139
|
+
introduced seconded debating question_put voting
|
|
140
|
+
carried negatived withdrawn lapsed
|
|
141
|
+
].freeze
|
|
142
|
+
|
|
143
|
+
# Terminal MotionStatus values — `Motion#pending?` is the complement.
|
|
144
|
+
# Kept as a separate constant (not derived) because the partition is
|
|
145
|
+
# semantic, not lexical; the spec in motion_spec.rb asserts the union
|
|
146
|
+
# equals MOTION_STATUS and the intersection is empty (MECE).
|
|
147
|
+
MOTION_TERMINAL = %w[carried negatived withdrawn lapsed].freeze
|
|
148
|
+
|
|
149
|
+
VOTING_STATUS = %w[called in_progress decided withdrawn deferred].freeze
|
|
150
|
+
|
|
151
|
+
VOTING_METHOD = %w[
|
|
152
|
+
voice division show_of_hands roll_call electronic
|
|
153
|
+
secret_ballot unanimous_consent consensus
|
|
154
|
+
].freeze
|
|
155
|
+
|
|
156
|
+
VOTING_OUTCOME = %w[passed negatived tied withdrawn].freeze
|
|
157
|
+
|
|
158
|
+
TOPIC_STATUS = %w[open under_discussion decided deferred withdrawn].freeze
|
|
159
|
+
|
|
160
|
+
OFFICER_ROLE = %w[
|
|
161
|
+
chair vice_chair deputy_chair secretary treasurer
|
|
162
|
+
parliamentarian presiding_officer sergeant_at_arms other
|
|
163
|
+
].freeze
|
|
164
|
+
|
|
165
|
+
RECURRENCE_FREQ = %w[
|
|
166
|
+
secondly minutely hourly daily weekly monthly yearly
|
|
167
|
+
].freeze
|
|
168
|
+
|
|
169
|
+
# Polymorphic communication channel. OCP: adding a new kind does
|
|
170
|
+
# not change the model — only this enum (or use `other` + extensions).
|
|
171
|
+
CONTACT_METHOD_KIND = %w[
|
|
172
|
+
phone mobile fax email url mail pager message other
|
|
173
|
+
].freeze
|
|
174
|
+
|
|
175
|
+
# Polymorphic external identifier for a Contact (ORCID, ISNI,
|
|
176
|
+
# Wikidata, ROR, etc.). OCP: adding a new scheme only extends this
|
|
177
|
+
# enum (or use `other` + extensions).
|
|
178
|
+
CONTACT_IDENTIFIER_KIND = %w[
|
|
179
|
+
orcid isni wikidata ror ringgold github other
|
|
180
|
+
].freeze
|
|
73
181
|
end
|
|
74
182
|
end
|
data/lib/edoxen/error.rb
CHANGED
|
@@ -7,7 +7,7 @@ module Edoxen
|
|
|
7
7
|
# Unified validation failure shape. Produced by both:
|
|
8
8
|
# * SchemaValidator — JSON-Schema violations and YAML syntax errors
|
|
9
9
|
# carry source: :schema (or :syntax for Psych::SyntaxError).
|
|
10
|
-
# *
|
|
10
|
+
# * DecisionCollection.from_yaml rescues in the CLI — model parse
|
|
11
11
|
# failures carry source: :model.
|
|
12
12
|
#
|
|
13
13
|
# One type at the seam means callers (CLI, future renderers, tests)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Edoxen
|
|
4
|
+
# ExtensionAttribute — one typed key/value pair within a
|
|
5
|
+
# MeetingExtension. Polymorphic on value type so consumers don't
|
|
6
|
+
# have to re-parse strings back into Int/Float/Bool/Date.
|
|
7
|
+
#
|
|
8
|
+
# Wire form (v2.1+, per TODO.refactor/47):
|
|
9
|
+
#
|
|
10
|
+
# - key: "quorum"
|
|
11
|
+
# type: "integer"
|
|
12
|
+
# intValue: 7
|
|
13
|
+
#
|
|
14
|
+
# - key: "name"
|
|
15
|
+
# type: "string"
|
|
16
|
+
# value: "quic" # string variant uses bare `value` (v2.0 wire)
|
|
17
|
+
#
|
|
18
|
+
# - key: "start"
|
|
19
|
+
# type: "datetime"
|
|
20
|
+
# dateTimeValue: 2026-07-04T10:00:00Z
|
|
21
|
+
#
|
|
22
|
+
# The `type` discriminator tells consumers which value field to read
|
|
23
|
+
# without probing all six. Exactly one of the value fields should be
|
|
24
|
+
# set; the schema enforces this.
|
|
25
|
+
#
|
|
26
|
+
# String values use the bare `value:` wire name for back-compat with
|
|
27
|
+
# v2.0 fixtures. Typed variants (integer/float/boolean/date/datetime)
|
|
28
|
+
# use camelCase wire names. The gem unifies them behind `#typed_value`.
|
|
29
|
+
class ExtensionAttribute < Lutaml::Model::Serializable
|
|
30
|
+
attribute :key, :string
|
|
31
|
+
|
|
32
|
+
# String variant — Ruby attribute is `value`, wire name is `value`
|
|
33
|
+
# (matches v2.0; `type: "string"` discriminator added in v2.1).
|
|
34
|
+
attribute :value, :string
|
|
35
|
+
|
|
36
|
+
# Typed variants — added in v2.1.
|
|
37
|
+
attribute :integer_value, :integer
|
|
38
|
+
attribute :float_value, :float
|
|
39
|
+
attribute :boolean_value, :boolean
|
|
40
|
+
attribute :date_value, :date
|
|
41
|
+
attribute :date_time_value, :date_time
|
|
42
|
+
|
|
43
|
+
# Wire discriminator: "string" | "integer" | "float" | "boolean" |
|
|
44
|
+
# "date" | "datetime". Tells consumers which value field to read.
|
|
45
|
+
attribute :type, :string
|
|
46
|
+
|
|
47
|
+
key_value do
|
|
48
|
+
map "key", to: :key
|
|
49
|
+
map "type", to: :type
|
|
50
|
+
map "value", to: :value
|
|
51
|
+
map "intValue", to: :integer_value
|
|
52
|
+
map "floatValue", to: :float_value
|
|
53
|
+
map "booleanValue", to: :boolean_value
|
|
54
|
+
map "dateValue", to: :date_value
|
|
55
|
+
map "dateTimeValue", to: :date_time_value
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Convenience: read whichever typed value is set. Returns nil when
|
|
59
|
+
# none of the value fields is populated.
|
|
60
|
+
def typed_value
|
|
61
|
+
case type
|
|
62
|
+
when "integer" then integer_value
|
|
63
|
+
when "float" then float_value
|
|
64
|
+
when "boolean" then boolean_value unless boolean_value.nil?
|
|
65
|
+
when "date" then date_value
|
|
66
|
+
when "datetime" then date_time_value
|
|
67
|
+
else
|
|
68
|
+
# Default to string for back-compat with v2.0 (no `type`).
|
|
69
|
+
value
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Back-compat alias — v2.1 introduced `value` as the canonical Ruby
|
|
74
|
+
# attribute; the older `string_value` alias keeps callers that
|
|
75
|
+
# already migrated to that name working.
|
|
76
|
+
alias string_value value
|
|
77
|
+
end
|
|
78
|
+
end
|
data/lib/edoxen/host_ref.rb
CHANGED
data/lib/edoxen/link_checker.rb
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Edoxen
|
|
4
|
-
# Walks a directory tree of Meeting and
|
|
4
|
+
# Walks a directory tree of Meeting and DecisionCollection YAML
|
|
5
5
|
# files and asserts that every cross-document URN link resolves.
|
|
6
6
|
#
|
|
7
7
|
# Invariants enforced:
|
|
8
8
|
#
|
|
9
9
|
# * Every Meeting#urn (if set) is referenced by exactly one
|
|
10
|
-
#
|
|
10
|
+
# DecisionCollection#metadata.meeting_urn (if the collection
|
|
11
11
|
# exists in the scanned directory).
|
|
12
|
-
# * Every
|
|
12
|
+
# * Every DecisionCollection#metadata.meeting_urn (if set)
|
|
13
13
|
# resolves to a Meeting whose urn matches.
|
|
14
14
|
#
|
|
15
15
|
# Use from CLI / scripts via the public `check` class method. Returns
|
|
@@ -24,7 +24,7 @@ module Edoxen
|
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
# Walk `dir` (recursive scan of *.yaml and *.yml), classify each
|
|
27
|
-
# file as Meeting or
|
|
27
|
+
# file as Meeting or DecisionCollection, and assert every
|
|
28
28
|
# cross-document URN resolves to a real record.
|
|
29
29
|
#
|
|
30
30
|
# @return [Array<LinkError>] empty when all links resolve.
|
|
@@ -41,7 +41,7 @@ module Edoxen
|
|
|
41
41
|
def check
|
|
42
42
|
require "yaml"
|
|
43
43
|
|
|
44
|
-
Dir.glob(File.join(@dir, "**", "*.{yaml,yml}")).
|
|
44
|
+
Dir.glob(File.join(@dir, "**", "*.{yaml,yml}")).each do |file|
|
|
45
45
|
data = safe_load_yaml(file)
|
|
46
46
|
next unless data.is_a?(Hash)
|
|
47
47
|
|
|
@@ -56,7 +56,7 @@ module Edoxen
|
|
|
56
56
|
|
|
57
57
|
errors = []
|
|
58
58
|
|
|
59
|
-
#
|
|
59
|
+
# DecisionCollection → Meeting
|
|
60
60
|
@collections_by_meeting_urn.each do |meeting_urn, file|
|
|
61
61
|
next if @meetings_by_urn.key?(meeting_urn)
|
|
62
62
|
|
|
@@ -69,7 +69,7 @@ module Edoxen
|
|
|
69
69
|
private
|
|
70
70
|
|
|
71
71
|
def safe_load_yaml(file)
|
|
72
|
-
YAML.
|
|
72
|
+
YAML.safe_load_file(file)
|
|
73
73
|
rescue Psych::SyntaxError, ArgumentError
|
|
74
74
|
nil
|
|
75
75
|
end
|
|
@@ -79,7 +79,7 @@ module Edoxen
|
|
|
79
79
|
end
|
|
80
80
|
|
|
81
81
|
def collection_shape?(data)
|
|
82
|
-
data["resolutions"].is_a?(Array) || data["metadata"].is_a?(Hash)
|
|
82
|
+
data["decisions"].is_a?(Array) || data["resolutions"].is_a?(Array) || data["metadata"].is_a?(Hash)
|
|
83
83
|
end
|
|
84
84
|
end
|
|
85
85
|
end
|
data/lib/edoxen/localization.rb
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Edoxen
|
|
4
|
-
# A monolingual rendering of a
|
|
4
|
+
# A monolingual rendering of a Decision. Mirrors the glossarist
|
|
5
5
|
# LocalizedConcept pattern: language-agnostic fields live on the
|
|
6
|
-
# parent
|
|
6
|
+
# parent Decision; per-language content lives here.
|
|
7
7
|
class Localization < Lutaml::Model::Serializable
|
|
8
8
|
attribute :language_code, :string
|
|
9
9
|
attribute :script, :string
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Edoxen
|
|
4
|
+
# Mixed into entities that carry a per-language `localizations[]`
|
|
5
|
+
# collection. Provides `in_language(code, fallback:)` and
|
|
6
|
+
# `primary_localization` in one place so Decision, Meeting, etc.
|
|
7
|
+
# share a single implementation.
|
|
8
|
+
#
|
|
9
|
+
# The including class must declare a typed `localizations` attribute
|
|
10
|
+
# whose entries expose `language_code` (ISO 639-3).
|
|
11
|
+
module LocalizationHost
|
|
12
|
+
def in_language(code, fallback: false)
|
|
13
|
+
match = localizations&.find { |loc| loc.language_code == code.to_s }
|
|
14
|
+
return match if match
|
|
15
|
+
|
|
16
|
+
fallback ? localizations&.first : nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def primary_localization
|
|
20
|
+
in_language("eng", fallback: true)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
data/lib/edoxen/meeting.rb
CHANGED
|
@@ -1,82 +1,106 @@
|
|
|
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
|
-
# Meetings and
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# different lifetimes: agendas exist weeks before a meeting;
|
|
12
|
-
# resolutions only after adoption.
|
|
9
|
+
# Meetings and DecisionCollections are kept as separate documents and
|
|
10
|
+
# joined by URN because they have different lifetimes: agendas exist
|
|
11
|
+
# weeks before a meeting; decisions only after adoption.
|
|
13
12
|
class Meeting < Lutaml::Model::Serializable
|
|
13
|
+
include LocalizationHost
|
|
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 :
|
|
22
|
+
attribute :visibility, :string, values: Enums::VISIBILITY
|
|
23
|
+
# v2.1 (TODO.refactor/46): free-form body-specific label (e.g.
|
|
24
|
+
# "CIML Meeting", "Plenary", "Board Meeting"). Resolves to a short
|
|
25
|
+
# canonical value via the parent collection's `body_vocabulary[]`.
|
|
26
|
+
attribute :body_type, :string
|
|
20
27
|
|
|
21
28
|
attribute :date_range, DateRange
|
|
29
|
+
attribute :recurrence, Recurrence
|
|
22
30
|
|
|
23
|
-
attribute :
|
|
24
|
-
attribute :committee_group, :string
|
|
25
|
-
|
|
26
|
-
attribute :venues, Location, collection: true
|
|
31
|
+
attribute :venues, Venue, collection: true
|
|
27
32
|
attribute :general_area, :string
|
|
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, :string
|
|
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
|
|
48
54
|
|
|
55
|
+
# Outcomes (canonical location on Meeting)
|
|
56
|
+
attribute :decisions, Decision, collection: true
|
|
57
|
+
attribute :motions, Motion, collection: true
|
|
58
|
+
attribute :votings, Voting, collection: true
|
|
59
|
+
|
|
49
60
|
attribute :localizations, MeetingLocalization, collection: true
|
|
50
61
|
attribute :relations, MeetingRelation, collection: true
|
|
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
|
|
62
|
+
attribute :extensions, MeetingExtension, collection: true
|
|
60
63
|
|
|
61
|
-
# English when available, else the first declared localization.
|
|
62
|
-
def primary_localization
|
|
63
|
-
in_language("eng", fallback: true)
|
|
64
|
-
end
|
|
65
|
-
|
|
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.
|
|
68
|
+
# Resolve the meeting's UN/LOCODE via the canonical `unlocode` gem.
|
|
76
69
|
def city_entry
|
|
77
70
|
return nil if city.nil? || city.to_s.empty?
|
|
78
71
|
|
|
79
72
|
Edoxen::ReferenceData.find_unlocode(city)
|
|
80
73
|
end
|
|
74
|
+
|
|
75
|
+
def secretary
|
|
76
|
+
officers_with_role("secretary").first&.person
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# All physical venues (polymorphic Venue filter).
|
|
80
|
+
def physical_venues
|
|
81
|
+
venues_by_kind("physical")
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def virtual_venues
|
|
85
|
+
venues_by_kind("virtual")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def hybrid?
|
|
89
|
+
!physical_venues.empty? && !virtual_venues.empty?
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def virtual_only?
|
|
93
|
+
!virtual_venues.empty? && physical_venues.empty?
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def physical_only?
|
|
97
|
+
!physical_venues.empty? && virtual_venues.empty?
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def venues_by_kind(kind)
|
|
103
|
+
(venues || []).select { |v| v.kind == kind }
|
|
104
|
+
end
|
|
81
105
|
end
|
|
82
106
|
end
|
|
@@ -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,9 +2,11 @@
|
|
|
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
|
+
include BodyVocabularyHost
|
|
9
|
+
|
|
8
10
|
attribute :title, :string
|
|
9
11
|
attribute :source, :string
|
|
10
12
|
end
|