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
data/README.adoc
CHANGED
|
@@ -7,24 +7,30 @@ image:https://img.shields.io/gem/v/edoxen.svg[RubyGems Version]
|
|
|
7
7
|
|
|
8
8
|
== Purpose
|
|
9
9
|
|
|
10
|
-
Edoxen is a Ruby library for the canonical Edoxen information model
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
https://github.com/
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
10
|
+
Edoxen is a Ruby library for the canonical Edoxen information model —
|
|
11
|
+
a **generic meeting, agenda, motion, voting, and decision model** that
|
|
12
|
+
covers standards bodies (ISO, IEC, ITU, BIPM, OIML, ILO), parliamentary
|
|
13
|
+
bodies (UK Hansard, HK LegCo, US Congress), technical community meetings
|
|
14
|
+
(IETF, W3C, Apache), academic conferences (Crossref-registered),
|
|
15
|
+
corporate boards, and generic web/virtual meetings.
|
|
16
|
+
|
|
17
|
+
Built on top of the https://github.com/lutaml/lutaml[lutaml-model]
|
|
18
|
+
serialization framework. The information model is defined in
|
|
19
|
+
https://github.com/edoxen/edoxen-model/tree/main/models[LutaML UML files]
|
|
20
|
+
(one `.lutaml` per concept); this gem mirrors that model exactly —
|
|
21
|
+
attribute declarations, enum values, and field shapes — so anything
|
|
22
|
+
expressible in LutaML is constructable, serializable, and validatable in
|
|
23
|
+
Ruby.
|
|
24
|
+
|
|
25
|
+
=== Generic core + profile extensions
|
|
26
|
+
|
|
27
|
+
The core schema is the **intersection** of all domains. Domain-specific
|
|
28
|
+
concepts (Bill, Witness, Petition, Address, Quorum Bell, etc.) live in
|
|
29
|
+
**profile extensions** via the `MeetingExtension` slot every core entity
|
|
30
|
+
carries — the ISO 8601-2 §15 profile mechanism. Adopters register a
|
|
31
|
+
profile namespace (`legco`, `us-congress`, `ietf`, `oiml`) and define
|
|
32
|
+
extension kinds within it; consumers ignore profiles they don't
|
|
33
|
+
understand.
|
|
28
34
|
|
|
29
35
|
== Installation
|
|
30
36
|
|
|
@@ -41,103 +47,208 @@ Then `bundle install`, or `gem install edoxen` standalone.
|
|
|
41
47
|
----
|
|
42
48
|
require 'edoxen'
|
|
43
49
|
|
|
44
|
-
yaml = File.read('
|
|
45
|
-
collection = Edoxen::
|
|
50
|
+
yaml = File.read('decisions.yaml')
|
|
51
|
+
collection = Edoxen::DecisionCollection.from_yaml(yaml)
|
|
46
52
|
|
|
47
|
-
collection.
|
|
48
|
-
|
|
49
|
-
# its TC number, SC number, and any cross-cutting reference number.
|
|
50
|
-
id = resolution.identifier.first
|
|
53
|
+
collection.decisions.each do |decision|
|
|
54
|
+
id = decision.identifier.first
|
|
51
55
|
puts "#{id.prefix}/#{id.number}"
|
|
52
56
|
|
|
53
|
-
|
|
54
|
-
# declared localization. See "Lookup accessors" below.
|
|
55
|
-
loc = resolution.primary_localization
|
|
57
|
+
loc = decision.primary_localization
|
|
56
58
|
puts " [#{loc.language_code}/#{loc.script}] #{loc.title}"
|
|
57
59
|
|
|
58
60
|
loc.actions.each do |action|
|
|
59
61
|
puts " - #{action.type}: #{action.message}"
|
|
60
62
|
end
|
|
61
|
-
|
|
62
|
-
# Or look up a specific language explicitly:
|
|
63
|
-
fra = resolution.in_language("fra")
|
|
64
|
-
puts " FR: #{fra.title}" if fra
|
|
65
63
|
end
|
|
66
64
|
|
|
65
|
+
# Or look up a specific language explicitly:
|
|
66
|
+
fra = decision.in_language("fra")
|
|
67
|
+
puts " FR: #{fra.title}" if fra
|
|
68
|
+
|
|
67
69
|
# Round-trip back to YAML
|
|
68
70
|
puts collection.to_yaml
|
|
69
71
|
----
|
|
70
72
|
|
|
73
|
+
== The five faces of a meeting
|
|
74
|
+
|
|
75
|
+
A meeting has **five concerns**, each modelled as a distinct first-class
|
|
76
|
+
entity rather than a column on a flat table:
|
|
77
|
+
|
|
78
|
+
[cols="1,3,2"]
|
|
79
|
+
|===
|
|
80
|
+
|
|
81
|
+
| Concern | What it captures | Top-level
|
|
82
|
+
|
|
83
|
+
| *Decisions*
|
|
84
|
+
| Formal outcomes adopted by the meeting: resolution, order, ruling,
|
|
85
|
+
determination, recommendation, statement, finding, opinion.
|
|
86
|
+
| `DecisionCollection`
|
|
87
|
+
|
|
88
|
+
| *Motions* + *Votings*
|
|
89
|
+
| The procedural record — who moved what, how the question was put, how
|
|
90
|
+
members voted, what the chair declared.
|
|
91
|
+
| `Meeting.motions[]` / `Meeting.votings[]`
|
|
92
|
+
|
|
93
|
+
| *Topics*
|
|
94
|
+
| The subjects of discussion. A topic carries documents, assets,
|
|
95
|
+
references, and URN links to the motions and decisions it produced.
|
|
96
|
+
| `AgendaItem.topics[]`
|
|
97
|
+
|
|
98
|
+
| *Meetings*
|
|
99
|
+
| The event itself: identifier, dates, polymorphic venues, officers,
|
|
100
|
+
agenda, components, attendance, minutes.
|
|
101
|
+
| `MeetingCollection`
|
|
102
|
+
|
|
103
|
+
| *Series*
|
|
104
|
+
| The recurring parent of meetings — annual plenaries, monthly board
|
|
105
|
+
meetings, IETF meeting series, etc.
|
|
106
|
+
| (top-level standalone)
|
|
107
|
+
|
|
108
|
+
|===
|
|
109
|
+
|
|
110
|
+
A Meeting carries its decisions, motions, and votings directly. The
|
|
111
|
+
DecisionCollection is the standalone form for publishing decisions
|
|
112
|
+
without meeting-level detail.
|
|
113
|
+
|
|
71
114
|
== Data model
|
|
72
115
|
|
|
73
116
|
The full model is in `lib/edoxen/*.rb` (Ruby) and `schema/edoxen.yaml`
|
|
74
|
-
(JSON-Schema). Both are kept in lockstep by
|
|
75
|
-
|
|
117
|
+
+ `schema/meeting.yaml` (JSON-Schema). Both are kept in lockstep by
|
|
118
|
+
runtime sync specs.
|
|
76
119
|
|
|
77
120
|
[source]
|
|
78
121
|
----
|
|
79
|
-
|
|
80
|
-
├── metadata:
|
|
81
|
-
│
|
|
82
|
-
│
|
|
83
|
-
│
|
|
84
|
-
│
|
|
85
|
-
│
|
|
86
|
-
└──
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
├──
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
122
|
+
DecisionCollection MeetingCollection
|
|
123
|
+
├── metadata: DecisionMetadata ├── metadata: MeetingCollectionMetadata
|
|
124
|
+
│ ├── title / title_localized[] └── meetings: Meeting[]
|
|
125
|
+
│ ├── date, source ├── identifier, urn, ordinal
|
|
126
|
+
│ ├── source_urls[] ├── series_ref (→ MeetingSeries)
|
|
127
|
+
│ ├── city (UN/LOCODE), country_code ├── type, status, visibility
|
|
128
|
+
│ └── meeting_urn (back-ref to Meeting) ├── date_range, recurrence
|
|
129
|
+
└── decisions: Decision[] ├── venues: Venue[] (polymorphic)
|
|
130
|
+
├── identifier: StructuredIdentifier[1..*]| ├── kind: physical | virtual
|
|
131
|
+
├── kind (DecisionKind) | ├── name, label, capacity
|
|
132
|
+
├── status (DecisionStatus) | ├── (physical) unlocode, iata_code,
|
|
133
|
+
├── doi, urn, agenda_item | │ address, country_code, lat, lon
|
|
134
|
+
├── dates: DecisionDate[] | └── (virtual) uri, features, passcode,
|
|
135
|
+
├── categories, relations, urls ├── officers: Officer[] (role + person + term)
|
|
136
|
+
├── brought_by_motions[] ├── hosts: HostRef[] (typed)
|
|
137
|
+
├── about_topics[] ├── source_urls[], landing_url, registration_url
|
|
138
|
+
├── made_in_component ├── agenda: Agenda (items, status)
|
|
139
|
+
└── localizations: Localization[1..*] ├── components: MeetingComponent[]
|
|
140
|
+
├── language_code, script │ (track, session, debate, breakout, keynote,
|
|
141
|
+
├── title, subject, message │ opening, closing, break, reception, ...)
|
|
142
|
+
├── considerations: Consideration[] ├── deadlines: Deadline[]
|
|
143
|
+
├── approvals: Approval[] ├── attendance: Attendance[]
|
|
144
|
+
└── actions: Action[] │ (status, role, response, proxy_for)
|
|
145
|
+
├── minutes: Minutes[]
|
|
146
|
+
Motion ├── motions: Motion[]
|
|
147
|
+
├── identifier, urn │ (status: introduced → seconded → debating
|
|
148
|
+
├── text, mover, seconders[] │ → question_put → voting → carried/negatived)
|
|
149
|
+
├── status (MotionStatus) ├── votings: Voting[]
|
|
150
|
+
├── introduced_at │ (status: called → in_progress → decided;
|
|
151
|
+
├── proposed_decision, resulting_decision │ voting_method, counts, casting_vote,
|
|
152
|
+
└── votings[] │ vote_records[])
|
|
153
|
+
├── decisions: Decision[] (inline)
|
|
154
|
+
Voting ├── localizations: MeetingLocalization[]
|
|
155
|
+
├── status (VotingStatus) ├── relations: MeetingRelation[]
|
|
156
|
+
├── voting_method (VotingMethod) └── extensions: MeetingExtension[]
|
|
157
|
+
├── result (VotingOutcome)
|
|
158
|
+
├── counts: VotingCounts {ayes, noes,
|
|
159
|
+
│ abstentions, absent}
|
|
160
|
+
├── casting_vote: VoteRecord
|
|
161
|
+
└── vote_records: VoteRecord[]
|
|
162
|
+
|
|
163
|
+
MeetingExtension (profile mechanism)
|
|
164
|
+
├── profile (e.g. "legco", "ietf", "us-congress")
|
|
165
|
+
├── kind (in-profile discriminator)
|
|
166
|
+
├── ref (URN to external profile document)
|
|
167
|
+
└── attributes: ExtensionAttribute[]
|
|
168
|
+
├── key
|
|
169
|
+
├── type (string | integer | float | boolean | date | datetime)
|
|
170
|
+
└── value / integer_value / float_value / boolean_value /
|
|
171
|
+
date_value / date_time_value (one of, per `type`)
|
|
102
172
|
----
|
|
103
173
|
|
|
104
|
-
Every
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
`
|
|
174
|
+
Every core entity has an `extensions: MeetingExtension[0..*]` slot.
|
|
175
|
+
Adopters extend the generic core without modifying it.
|
|
176
|
+
|
|
177
|
+
`ExtensionAttribute` is polymorphic on value type — consumers read the
|
|
178
|
+
typed payload via `#typed_value` without re-parsing strings back into
|
|
179
|
+
Int/Float/Bool/Date. The v2.0 bare `value: String` wire shape still
|
|
180
|
+
parses (routed into the string variant).
|
|
181
|
+
|
|
182
|
+
=== Polymorphic Venue
|
|
183
|
+
|
|
184
|
+
A `Venue` is one flat class on the wire; the `kind` field discriminates
|
|
185
|
+
physical vs virtual and validators enforce that fields match `kind`.
|
|
186
|
+
|
|
187
|
+
* Physical: UN/LOCODE + IATA + address + geo-coordinates.
|
|
188
|
+
* Virtual: URI + iCalendar-style features + access details.
|
|
189
|
+
|
|
190
|
+
A meeting can have multiple venues of either kind (hybrid meetings,
|
|
191
|
+
multi-room conferences bridged by video, etc.). UN/LOCODE and IATA codes
|
|
192
|
+
are validated against the canonical `unlocodes` and `iata` gems by
|
|
193
|
+
`Edoxen::VenueValidator`.
|
|
108
194
|
|
|
109
195
|
=== Lookup accessors
|
|
110
196
|
|
|
111
197
|
Direct iteration over `localizations[]` is discouraged — the
|
|
112
198
|
language-preference policy lives behind two accessors:
|
|
113
199
|
|
|
114
|
-
* `
|
|
200
|
+
* `Decision#in_language(code, fallback: false)` — exact match by
|
|
115
201
|
ISO 639-3 code, or `nil` (or the first declared localization when
|
|
116
202
|
`fallback: true`).
|
|
117
|
-
* `
|
|
118
|
-
the first declared localization.
|
|
119
|
-
|
|
203
|
+
* `Decision#primary_localization` — English when available, else
|
|
204
|
+
the first declared localization.
|
|
205
|
+
|
|
206
|
+
The same pair exists on `Meeting` (`MeetingLocalization`).
|
|
120
207
|
|
|
121
208
|
[source,ruby]
|
|
122
209
|
----
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
210
|
+
decision.in_language("fra") # => Localization or nil
|
|
211
|
+
decision.in_language("deu", fallback: true)
|
|
212
|
+
decision.primary_localization # English-or-first
|
|
213
|
+
|
|
214
|
+
meeting.chair # => Person (via officers + role)
|
|
215
|
+
meeting.secretary # => Person
|
|
216
|
+
meeting.officers_with_role(:treasurer) # => [Officer]
|
|
217
|
+
meeting.hybrid? # true if both physical + virtual venues
|
|
218
|
+
meeting.physical_only? / meeting.virtual_only?
|
|
219
|
+
|
|
220
|
+
validator = Edoxen::VenueValidator.new(venue)
|
|
221
|
+
validator.valid? # checks UN/LOCODE + IATA against the gems
|
|
222
|
+
validator.validate(auto_populate: true) # also fills city/country_code from UN/LOCODE
|
|
126
223
|
----
|
|
127
224
|
|
|
128
|
-
== Multilingual
|
|
225
|
+
== Multilingual by default
|
|
226
|
+
|
|
227
|
+
Per-language content lives inside `localizations[]` siblings — one
|
|
228
|
+
entry per language. Admin fields (`identifier`, `doi`, `urn`, `dates`,
|
|
229
|
+
`meeting`) are declared once on the parent; translatable content
|
|
230
|
+
(`title`, `subject`, `considerations`, `actions`, `approvals`) lives
|
|
231
|
+
inside each `Localization`.
|
|
232
|
+
|
|
233
|
+
This pattern — borrowed from the
|
|
234
|
+
https://github.com/glossarist/concept-model[glossarist] project — lets
|
|
235
|
+
translators work on each language in isolation while comparing them
|
|
236
|
+
side-by-side in the same file. Drift shows up on git diff, not on a
|
|
237
|
+
translator's spreadsheet.
|
|
129
238
|
|
|
130
239
|
[source,yaml]
|
|
131
240
|
----
|
|
132
|
-
|
|
241
|
+
decisions:
|
|
133
242
|
- identifier:
|
|
134
243
|
- prefix: CIML
|
|
135
244
|
number: "2025-44"
|
|
136
|
-
|
|
245
|
+
kind: resolution
|
|
246
|
+
status: decided
|
|
247
|
+
doi: 10.63493/decisions/ciml202544
|
|
137
248
|
agenda_item: "16.2"
|
|
138
249
|
dates:
|
|
139
250
|
- date: 2025-10-13
|
|
140
|
-
type:
|
|
251
|
+
type: adoption
|
|
141
252
|
localizations:
|
|
142
253
|
- language_code: eng
|
|
143
254
|
script: Latn
|
|
@@ -167,24 +278,21 @@ resolutions:
|
|
|
167
278
|
|
|
168
279
|
== Command-line interface
|
|
169
280
|
|
|
170
|
-
The `edoxen` executable exposes
|
|
171
|
-
collections, two for Meeting/Agenda documents.
|
|
281
|
+
The `edoxen` executable exposes six commands.
|
|
172
282
|
|
|
173
|
-
=== `validate` —
|
|
283
|
+
=== `validate` — schema + model validation for Decisions
|
|
174
284
|
|
|
175
285
|
[source,sh]
|
|
176
286
|
----
|
|
177
287
|
$ edoxen validate "spec/fixtures/*.yaml"
|
|
178
288
|
----
|
|
179
289
|
|
|
180
|
-
Runs both `Edoxen::SchemaValidator` and `Edoxen::
|
|
181
|
-
against each matching file.
|
|
290
|
+
Runs both `Edoxen::SchemaValidator` and `Edoxen::DecisionCollection.from_yaml`
|
|
291
|
+
against each matching file. Schema catches `additionalProperties`,
|
|
182
292
|
`required`, enum, and pattern violations; the model catches structural
|
|
183
293
|
problems the schema can't express.
|
|
184
294
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
=== `normalize` — round-trip Resolution YAML through the model
|
|
295
|
+
=== `normalize` — round-trip Decision YAML through the model
|
|
188
296
|
|
|
189
297
|
[source,sh]
|
|
190
298
|
----
|
|
@@ -192,92 +300,96 @@ $ edoxen normalize "spec/fixtures/*.yaml" --output clean/
|
|
|
192
300
|
$ edoxen normalize legacy.yaml --inplace
|
|
193
301
|
----
|
|
194
302
|
|
|
195
|
-
|
|
196
|
-
result back. Either `--output DIR` or `--inplace` is required (mutually
|
|
197
|
-
exclusive).
|
|
303
|
+
Either `--output DIR` or `--inplace` is required (mutually exclusive).
|
|
198
304
|
|
|
199
|
-
=== `validate-meetings`
|
|
305
|
+
=== `validate-meetings` / `normalize-meetings`
|
|
306
|
+
|
|
307
|
+
Same shape, for Meeting/Agenda YAML against `schema/meeting.yaml`.
|
|
200
308
|
|
|
201
309
|
[source,sh]
|
|
202
310
|
----
|
|
203
311
|
$ edoxen validate-meetings "spec/fixtures/meetings/*.yaml"
|
|
312
|
+
$ edoxen normalize-meetings "spec/fixtures/meetings/*.yaml" --output clean/
|
|
204
313
|
----
|
|
205
314
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
(wrappers). The schema's root is `oneOf` — both shapes are accepted.
|
|
315
|
+
The schema's root is `oneOf` — both single-Meeting and MeetingCollection
|
|
316
|
+
shapes are accepted.
|
|
209
317
|
|
|
210
|
-
=== `
|
|
318
|
+
=== `unlocode CODE` / `iata CODE` — registry lookup
|
|
211
319
|
|
|
212
320
|
[source,sh]
|
|
213
321
|
----
|
|
214
|
-
$ edoxen
|
|
322
|
+
$ edoxen unlocode FRPAR
|
|
323
|
+
UN/LOCODE: FRPAR
|
|
324
|
+
Name: Paris
|
|
325
|
+
Country: FR
|
|
326
|
+
...
|
|
327
|
+
|
|
328
|
+
$ edoxen iata JFK
|
|
329
|
+
IATA: JFK
|
|
330
|
+
Name: John F. Kennedy International Airport
|
|
331
|
+
Country: US
|
|
215
332
|
----
|
|
216
333
|
|
|
217
|
-
|
|
334
|
+
== Profile mechanism (ISO 8601-2 §15)
|
|
218
335
|
|
|
219
|
-
|
|
336
|
+
Every core entity has an `extensions: MeetingExtension[0..*]` slot.
|
|
337
|
+
Adopters register a profile namespace and define extension kinds within
|
|
338
|
+
it. `MeetingExtension` carries three identity fields plus a typed
|
|
339
|
+
`attributes[]` list:
|
|
220
340
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
`
|
|
341
|
+
* `profile` — the namespace (lowercase, hyphen-separated).
|
|
342
|
+
* `kind` — discriminator within the profile.
|
|
343
|
+
* `ref` — URN of an external profile document, when the data lives
|
|
344
|
+
elsewhere.
|
|
345
|
+
* `attributes[]` — typed key/value pairs (`ExtensionAttribute`).
|
|
224
346
|
|
|
225
|
-
|
|
347
|
+
`ExtensionAttribute` is polymorphic on value type (v2.1+). Set `type`
|
|
348
|
+
to one of `string | integer | float | boolean | date | datetime` and
|
|
349
|
+
populate the matching value field:
|
|
350
|
+
|
|
351
|
+
[source,yaml]
|
|
226
352
|
----
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
├── schedule: ScheduleItem[] (the time-bound timetable)
|
|
250
|
-
├── deadlines: Deadline[]
|
|
251
|
-
├── localizations: MeetingLocalization[] (per-language content)
|
|
252
|
-
├── relations: MeetingRelation[] (continues_from, joint_with, ...)
|
|
253
|
-
└── resolution_refs: String[] (URN links to ResolutionCollections)
|
|
353
|
+
extensions:
|
|
354
|
+
- profile: legco
|
|
355
|
+
kind: vote_block
|
|
356
|
+
ref: urn:legco:vote-block:2024-01-15:item-5
|
|
357
|
+
- profile: ietf
|
|
358
|
+
kind: wg_meeting_meta
|
|
359
|
+
attributes:
|
|
360
|
+
- key: wg_name
|
|
361
|
+
type: string
|
|
362
|
+
value: quic
|
|
363
|
+
- key: draft_name
|
|
364
|
+
type: string
|
|
365
|
+
value: draft-ietf-quic-v2
|
|
366
|
+
- key: quorum
|
|
367
|
+
type: integer
|
|
368
|
+
integer_value: 7
|
|
369
|
+
- key: live_stream
|
|
370
|
+
type: boolean
|
|
371
|
+
boolean_value: true
|
|
372
|
+
- key: start
|
|
373
|
+
type: datetime
|
|
374
|
+
date_time_value: 2026-07-04T10:00:00Z
|
|
254
375
|
----
|
|
255
376
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
Meetings and ResolutionCollections are separate documents joined by URN:
|
|
259
|
-
|
|
260
|
-
- `Meeting.resolution_refs: [urn:...]`
|
|
261
|
-
- `ResolutionCollection.metadata.meeting_urn: urn:...`
|
|
262
|
-
|
|
263
|
-
The join is by URN because the two have different lifetimes — agendas
|
|
264
|
-
exist weeks before a meeting, resolutions only after adoption.
|
|
265
|
-
|
|
266
|
-
=== Lookup accessors (parallel to the Resolution side)
|
|
377
|
+
Read the typed payload in Ruby via `#typed_value`:
|
|
267
378
|
|
|
268
379
|
[source,ruby]
|
|
269
380
|
----
|
|
270
|
-
|
|
381
|
+
attr = extension.attributes.find { |a| a.key == "quorum" }
|
|
382
|
+
attr.type # => "integer"
|
|
383
|
+
attr.integer_value # => 7
|
|
384
|
+
attr.typed_value # => 7
|
|
385
|
+
----
|
|
271
386
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
meeting.primary_localization # English-or-first
|
|
275
|
-
meeting.find_agenda_item("5.2") # AgendaItem by label
|
|
387
|
+
The v2.0 bare `value: String` wire shape still parses (routed into
|
|
388
|
+
the string variant; `type` defaults to `string`).
|
|
276
389
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
----
|
|
390
|
+
Consumers ignore profile extensions they don't understand. See
|
|
391
|
+
https://github.com/edoxen/edoxen-model/tree/main/references/profiles[the
|
|
392
|
+
HK LegCo profile example] for a real-world reference.
|
|
281
393
|
|
|
282
394
|
== Architecture
|
|
283
395
|
|
|
@@ -288,40 +400,83 @@ collection.find_by_identifier(prefix: "CIML", number: "56")
|
|
|
288
400
|
* Each model lives in its own file under `lib/edoxen/`. Models declare
|
|
289
401
|
`attribute` only — `lutaml-model` auto-emits an identity map (wire
|
|
290
402
|
name = snake_case attribute name) when no explicit `key_value` block
|
|
291
|
-
is present.
|
|
292
|
-
only when the wire name differs from the attribute name.
|
|
403
|
+
is present.
|
|
293
404
|
* `lib/edoxen/enums.rb` is the single source of truth for every enum
|
|
294
|
-
value used by the gem. Both the Ruby model (`attribute :
|
|
295
|
-
values: Enums::
|
|
296
|
-
|
|
405
|
+
value used by the gem. Both the Ruby model (`attribute :kind, :string,
|
|
406
|
+
values: Enums::DECISION_KIND`) and the schemas reference the same
|
|
407
|
+
constants.
|
|
297
408
|
* `lib/edoxen/error.rb` defines the unified `Edoxen::ValidationError`
|
|
298
409
|
— produced by both `SchemaValidator` (with `source: :schema` or
|
|
299
410
|
`:syntax`) and by model parse rescues in the CLI (with `source:
|
|
300
|
-
:model`).
|
|
301
|
-
`message_text`, and `source`.
|
|
411
|
+
:model`).
|
|
302
412
|
* `lib/edoxen/schema_validator.rb` is intentionally small: two
|
|
303
413
|
validate methods, a `LineMap` module for line-accurate error
|
|
304
414
|
reporting (longest-prefix match — no path-shape hardcoding), and
|
|
305
415
|
date coercion so `json_schemer` can validate `format: date` against
|
|
306
416
|
YAML-loaded `Date` instances.
|
|
307
|
-
* `lib/edoxen/cli.rb` exposes
|
|
308
|
-
`normalize`, `validate-meetings`, `normalize-meetings`
|
|
309
|
-
delegate their shared scaffolding
|
|
310
|
-
(expand/sort/empty/header/loop/tally/summary/exit) to the deep
|
|
417
|
+
* `lib/edoxen/cli.rb` exposes six Thor commands (`validate`,
|
|
418
|
+
`normalize`, `validate-meetings`, `normalize-meetings`, `unlocode`,
|
|
419
|
+
`iata`) that delegate their shared scaffolding to the deep
|
|
311
420
|
`Edoxen::Cli::Batch` module.
|
|
421
|
+
* `lib/edoxen/venue_validator.rb` validates polymorphic `Venue`
|
|
422
|
+
instances using the `unlocodes` and `iata` gems; optionally
|
|
423
|
+
auto-populates `city` and `country_code` from the UN/LOCODE registry.
|
|
312
424
|
|
|
313
425
|
=== Schema ↔ Ruby invariants
|
|
314
426
|
|
|
315
427
|
Two pairs of runtime specs guard each side of the schema ↔ Ruby
|
|
316
428
|
boundary:
|
|
317
429
|
|
|
318
|
-
*
|
|
430
|
+
* Decision side: `schema_enum_sync_spec.rb` +
|
|
319
431
|
`schema_model_sync_spec.rb` against `schema/edoxen.yaml`.
|
|
320
432
|
* Meeting side: `schema_meeting_enum_sync_spec.rb` +
|
|
321
433
|
`schema_meeting_model_sync_spec.rb` against `schema/meeting.yaml`.
|
|
322
434
|
|
|
323
435
|
Drift fails CI immediately, not at fixture-validation time.
|
|
324
436
|
|
|
437
|
+
== Migrating from v0.x (Resolution → Decision)
|
|
438
|
+
|
|
439
|
+
v2.0 is a breaking release. The migration is mechanical:
|
|
440
|
+
|
|
441
|
+
[cols="1,3"]
|
|
442
|
+
|===
|
|
443
|
+
|
|
444
|
+
| `Resolution` class
|
|
445
|
+
| `Decision` class with `kind: resolution`
|
|
446
|
+
|
|
447
|
+
| `ResolutionType` enum (4 values)
|
|
448
|
+
| `DecisionKind` enum (9 values: resolution, order, ruling, determination,
|
|
449
|
+
recommendation, statement, finding, opinion, other)
|
|
450
|
+
|
|
451
|
+
| `ResolutionCollection`
|
|
452
|
+
| `DecisionCollection`
|
|
453
|
+
|
|
454
|
+
| `Meeting.virtual: Boolean`
|
|
455
|
+
| `Meeting.venues: Venue[]` (polymorphic)
|
|
456
|
+
|
|
457
|
+
| `Meeting.chair` / `Meeting.secretary`
|
|
458
|
+
| `Meeting.officers: Officer[]` (role discriminates)
|
|
459
|
+
|
|
460
|
+
| `Meeting.schedule[]` (ScheduleItem)
|
|
461
|
+
| `Meeting.components: MeetingComponent[]` (flat)
|
|
462
|
+
|
|
463
|
+
| `Resolution` alone
|
|
464
|
+
| `Decision` + `Motion` + `Voting` (procedural core)
|
|
465
|
+
|
|
466
|
+
| (none)
|
|
467
|
+
| `Topic` + `TopicDocument` + `TopicAsset`
|
|
468
|
+
|
|
469
|
+
| (none)
|
|
470
|
+
| `MeetingSeries`, `Recurrence` (ISO 8601-2 §13)
|
|
471
|
+
|
|
472
|
+
| (none)
|
|
473
|
+
| `MeetingExtension` (profile mechanism, ISO 8601-2 §15)
|
|
474
|
+
|
|
475
|
+
|===
|
|
476
|
+
|
|
477
|
+
See the https://github.com/edoxen/edoxen.github.io/blob/main/docs/migration-v2.md[migration
|
|
478
|
+
guide] for full details.
|
|
479
|
+
|
|
325
480
|
== Contributing
|
|
326
481
|
|
|
327
482
|
Follow the rules in `CLAUDE.md`:
|
data/edoxen.gemspec
CHANGED
|
@@ -8,25 +8,24 @@ Gem::Specification.new do |spec|
|
|
|
8
8
|
spec.authors = ["Ribose Inc."]
|
|
9
9
|
spec.email = ["open.source@ribose.com"]
|
|
10
10
|
|
|
11
|
-
spec.summary = "Edoxen is a
|
|
11
|
+
spec.summary = "Edoxen is a generic information model for meetings, agendas, and decisions."
|
|
12
12
|
spec.description = <<~HEREDOC
|
|
13
|
-
Edoxen provides a Ruby library for working with
|
|
14
|
-
users to create, manipulate, and serialize
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
Edoxen provides a Ruby library for working with a generic meeting/decision
|
|
14
|
+
model, allowing users to create, manipulate, and serialize meeting, agenda,
|
|
15
|
+
motion, voting, and decision data in a structured format. Built on top of
|
|
16
|
+
the lutaml-model serialization framework, with profile-based customization
|
|
17
|
+
(ISO 8601-2 §15) for domain-specific extensions.
|
|
18
18
|
HEREDOC
|
|
19
19
|
|
|
20
20
|
spec.homepage = "https://github.com/metanorma/edoxen"
|
|
21
21
|
spec.license = "BSD-2-Clause"
|
|
22
|
-
spec.required_ruby_version = ">= 3.
|
|
22
|
+
spec.required_ruby_version = ">= 3.1.0"
|
|
23
23
|
|
|
24
24
|
spec.metadata["homepage_uri"] = spec.homepage
|
|
25
25
|
spec.metadata["source_code_uri"] = "https://github.com/metanorma/edoxen"
|
|
26
26
|
spec.metadata["changelog_uri"] = "https://github.com/metanorma/edoxen"
|
|
27
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
|
27
28
|
|
|
28
|
-
# Specify which files should be added to the gem when it is released.
|
|
29
|
-
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
30
29
|
spec.files = Dir.chdir(__dir__) do
|
|
31
30
|
`git ls-files -z`.split("\x0").reject do |f|
|
|
32
31
|
(f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
|
|
@@ -39,5 +38,6 @@ Gem::Specification.new do |spec|
|
|
|
39
38
|
spec.add_dependency "json_schemer", "~> 2.0"
|
|
40
39
|
spec.add_dependency "lutaml-model", "~> 0.7"
|
|
41
40
|
spec.add_dependency "thor", "~> 1.0"
|
|
42
|
-
spec.add_dependency "unlocodes", "~> 0.
|
|
41
|
+
spec.add_dependency "unlocodes", "~> 0.3"
|
|
42
|
+
spec.add_dependency "iata", "~> 0.1"
|
|
43
43
|
end
|
data/lib/edoxen/action.rb
CHANGED
|
@@ -5,7 +5,7 @@ module Edoxen
|
|
|
5
5
|
# Localization to express the multilingual part of an action.
|
|
6
6
|
class Action < Lutaml::Model::Serializable
|
|
7
7
|
attribute :type, :string, values: Enums::ACTION_TYPE
|
|
8
|
-
attribute :date_effective,
|
|
8
|
+
attribute :date_effective, DecisionDate
|
|
9
9
|
attribute :message, :string
|
|
10
10
|
end
|
|
11
11
|
end
|