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/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,150 +47,278 @@ 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
|
-
#
|
|
55
|
-
|
|
56
|
-
puts " [
|
|
57
|
+
# Per-field Localized (ISO 24229): every translatable field is an
|
|
58
|
+
# array of { spelling, value } entries. Pick the one you want.
|
|
59
|
+
title = decision.title.find { |l| l.spelling == "eng" }&.value
|
|
60
|
+
puts " [eng] #{title}"
|
|
57
61
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
end
|
|
62
|
+
fra = decision.title.find { |l| l.spelling == "fra" }&.value
|
|
63
|
+
puts " [fra] #{fra}" if fra
|
|
61
64
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
decision.actions.each do |action|
|
|
66
|
+
msg = action.message.find { |l| l.spelling == "eng" }&.value
|
|
67
|
+
puts " - #{action.type}: #{msg}"
|
|
68
|
+
end
|
|
65
69
|
end
|
|
66
70
|
|
|
67
71
|
# Round-trip back to YAML
|
|
68
72
|
puts collection.to_yaml
|
|
69
73
|
----
|
|
70
74
|
|
|
75
|
+
== The five faces of a meeting
|
|
76
|
+
|
|
77
|
+
A meeting has **five concerns**, each modelled as a distinct first-class
|
|
78
|
+
entity rather than a column on a flat table:
|
|
79
|
+
|
|
80
|
+
[cols="1,3,2"]
|
|
81
|
+
|===
|
|
82
|
+
|
|
83
|
+
| Concern | What it captures | Top-level
|
|
84
|
+
|
|
85
|
+
| *Decisions*
|
|
86
|
+
| Formal outcomes adopted by the meeting: resolution, order, ruling,
|
|
87
|
+
determination, recommendation, statement, finding, opinion.
|
|
88
|
+
| `DecisionCollection`
|
|
89
|
+
|
|
90
|
+
| *Motions* + *Votings*
|
|
91
|
+
| The procedural record — who moved what, how the question was put, how
|
|
92
|
+
members voted, what the chair declared.
|
|
93
|
+
| `Meeting.motions[]` / `Meeting.votings[]`
|
|
94
|
+
|
|
95
|
+
| *Topics*
|
|
96
|
+
| The subjects of discussion. A topic carries documents, assets,
|
|
97
|
+
references, and URN links to the motions and decisions it produced.
|
|
98
|
+
| `AgendaItem.topics[]`
|
|
99
|
+
|
|
100
|
+
| *Meetings*
|
|
101
|
+
| The event itself: identifier, dates, polymorphic venues, officers,
|
|
102
|
+
agenda, components, attendance, minutes.
|
|
103
|
+
| `MeetingCollection`
|
|
104
|
+
|
|
105
|
+
| *Series*
|
|
106
|
+
| The recurring parent of meetings — annual plenaries, monthly board
|
|
107
|
+
meetings, IETF meeting series, etc.
|
|
108
|
+
| (top-level standalone)
|
|
109
|
+
|
|
110
|
+
|===
|
|
111
|
+
|
|
112
|
+
A Meeting carries its decisions, motions, and votings directly. The
|
|
113
|
+
DecisionCollection is the standalone form for publishing decisions
|
|
114
|
+
without meeting-level detail.
|
|
115
|
+
|
|
71
116
|
== Data model
|
|
72
117
|
|
|
73
118
|
The full model is in `lib/edoxen/*.rb` (Ruby) and `schema/edoxen.yaml`
|
|
74
|
-
(JSON-Schema). Both are kept in lockstep by
|
|
75
|
-
|
|
119
|
+
+ `schema/meeting.yaml` (JSON-Schema). Both are kept in lockstep by
|
|
120
|
+
runtime sync specs.
|
|
76
121
|
|
|
77
122
|
[source]
|
|
78
123
|
----
|
|
79
|
-
|
|
80
|
-
├── metadata:
|
|
81
|
-
│
|
|
82
|
-
│
|
|
83
|
-
│
|
|
84
|
-
│
|
|
85
|
-
│
|
|
86
|
-
└──
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
├──
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
124
|
+
DecisionCollection MeetingCollection
|
|
125
|
+
├── metadata: DecisionMetadata ├── metadata: MeetingCollectionMetadata
|
|
126
|
+
│ ├── title: LocalizedString[] └── meetings: Meeting[]
|
|
127
|
+
│ ├── date, source ├── identifier, urn, ordinal
|
|
128
|
+
│ ├── source_urls[] ├── series_ref (→ MeetingSeries)
|
|
129
|
+
│ ├── city (UN/LOCODE), country_code ├── type, status, visibility
|
|
130
|
+
│ └── meeting_urn (back-ref to Meeting) ├── scheduled_date_range, occurred_date_range, recurrence
|
|
131
|
+
└── decisions: Decision[] ├── venues: Venue[] (polymorphic)
|
|
132
|
+
├── identifier: StructuredIdentifier[1..*]| ├── kind: physical | virtual
|
|
133
|
+
├── kind (DecisionKind) | ├── name, label, capacity
|
|
134
|
+
├── status (DecisionStatus) | ├── (physical) unlocode, iata_code,
|
|
135
|
+
├── doi, urn, agenda_item | │ address, country_code, lat, lon
|
|
136
|
+
├── dates: DecisionDate[] | └── (virtual) uri, features, passcode,
|
|
137
|
+
├── categories, relations, urls ├── officers: Officer[] (role + person + term)
|
|
138
|
+
├── brought_by_motions[] ├── hosts: HostRef[] (typed)
|
|
139
|
+
├── about_topics[] ├── source_urls[], landing_url, registration_url
|
|
140
|
+
├── made_in_component ├── agenda: Agenda (items, status)
|
|
141
|
+
├── title: LocalizedString[] ├── components: MeetingComponent[]
|
|
142
|
+
├── subject: LocalizedString[] │ (track, session, debate, breakout, keynote,
|
|
143
|
+
├── message: LocalizedString[] │ opening, closing, break, reception, ...)
|
|
144
|
+
├── considering: LocalizedString[] ├── deadlines: Deadline[]
|
|
145
|
+
├── considerations: Consideration[] ├── attendance: Attendance[]
|
|
146
|
+
│ └── message: LocalizedString[] │ (status, role, response, proxy_for)
|
|
147
|
+
├── approvals: Approval[] ├── minutes: Minutes[]
|
|
148
|
+
│ └── message: LocalizedString[] ├── motions: Motion[]
|
|
149
|
+
└── actions: Action[] │ (status: introduced → seconded → debating
|
|
150
|
+
└── message: LocalizedString[] │ → question_put → voting → carried/negatived)
|
|
151
|
+
├── votings: Voting[]
|
|
152
|
+
Motion │ (status: called → in_progress → decided;
|
|
153
|
+
├── identifier, urn │ voting_method, counts, casting_vote,
|
|
154
|
+
├── text: LocalizedString[] │ vote_records[])
|
|
155
|
+
├── mover, seconders[] ├── decisions: Decision[] (inline)
|
|
156
|
+
├── status (MotionStatus) ├── declarations: Declaration[] (BS 0:2006 §7.6)
|
|
157
|
+
├── introduced_at ├── relations: MeetingRelation[]
|
|
158
|
+
├── proposed_decision, resulting_decision └── extensions: MeetingExtension[]
|
|
159
|
+
└── votings[]
|
|
160
|
+
MeetingSeries (top-level standalone)
|
|
161
|
+
Voting ├── identifier, urn
|
|
162
|
+
├── status (VotingStatus) ├── name: LocalizedString[]
|
|
163
|
+
├── voting_method (VotingMethod) ├── recurrence
|
|
164
|
+
├── result (VotingOutcome) ├── meeting_refs[]
|
|
165
|
+
├── counts: VotingCounts {ayes, noes, └── extensions: MeetingExtension[]
|
|
166
|
+
│ abstentions, absent}
|
|
167
|
+
├── casting_vote: VoteRecord
|
|
168
|
+
└── vote_records: VoteRecord[]
|
|
169
|
+
|
|
170
|
+
MeetingExtension (profile mechanism)
|
|
171
|
+
├── profile (e.g. "legco", "ietf", "us-congress")
|
|
172
|
+
├── kind (in-profile discriminator)
|
|
173
|
+
├── ref (URN to external profile document)
|
|
174
|
+
└── attributes: ExtensionAttribute[]
|
|
175
|
+
├── key
|
|
176
|
+
├── type (string | integer | float | boolean | date | datetime)
|
|
177
|
+
└── value / integer_value / float_value / boolean_value /
|
|
178
|
+
date_value / date_time_value (one of, per `type`)
|
|
179
|
+
|
|
180
|
+
Statement (BS 0:2006 §7.6)
|
|
181
|
+
├── kind (StatementKind: statement | comment | standpoint)
|
|
182
|
+
├── description: LocalizedString[]
|
|
183
|
+
├── party: Person[]
|
|
184
|
+
└── extensions: MeetingExtension[]
|
|
185
|
+
|
|
186
|
+
Declaration (BS 0:2006 §7.6)
|
|
187
|
+
├── kind (DeclarationKind: conflict_of_interest | ipr)
|
|
188
|
+
├── description: LocalizedString[]
|
|
189
|
+
├── party: Person[]
|
|
190
|
+
├── ipr_subject_ref: EntityRef (only when kind == "ipr")
|
|
191
|
+
├── ipr_target_ref: EntityRef (only when kind == "ipr")
|
|
192
|
+
└── extensions: MeetingExtension[]
|
|
193
|
+
|
|
194
|
+
DateTimeRange
|
|
195
|
+
├── start: DateTime
|
|
196
|
+
└── end: DateTime
|
|
197
|
+
----
|
|
198
|
+
|
|
199
|
+
Every core entity has an `extensions: MeetingExtension[0..*]` slot.
|
|
200
|
+
Adopters extend the generic core without modifying it.
|
|
201
|
+
|
|
202
|
+
`ExtensionAttribute` is polymorphic on value type — consumers read the
|
|
203
|
+
typed payload via `#typed_value` without re-parsing strings back into
|
|
204
|
+
Int/Float/Bool/Date. The pre-1.0 bare `value: String` wire shape still
|
|
205
|
+
parses (routed into the string variant).
|
|
206
|
+
|
|
207
|
+
=== Per-field multilingual (ISO 24229)
|
|
208
|
+
|
|
209
|
+
Every translatable field is `Localized<String|Name>[0..*]` — an array
|
|
210
|
+
with one entry per ISO 24229 *spelling* code. There is no separate
|
|
211
|
+
`Localization[]` collection; each field carries its own language tags
|
|
212
|
+
inline.
|
|
213
|
+
|
|
214
|
+
* `LocalizedString` — `{ spelling, value: String, extensions }`.
|
|
215
|
+
* `LocalizedName` — `{ spelling, value: Name, extensions }`.
|
|
216
|
+
* `spelling` accepts ISO 24229 *spelling-system codes*
|
|
217
|
+
(`{lang}-{script}[-{country}][-{extension}]`, e.g. `zho-Hans`,
|
|
218
|
+
`ind-Latn-pre1972`) AND *conversion-system codes*
|
|
219
|
+
(`{authority}:{source-spelling}:{target-spelling}:{identifying}`,
|
|
220
|
+
e.g. `acadsin:zho-Hani:Latn:2002`, `UN:ara-Arab:Latn:2017`).
|
|
221
|
+
|
|
222
|
+
Always verbose — single-language data uses the same
|
|
223
|
+
`[{ spelling, value }]` shape as multi-language data. This lets a
|
|
224
|
+
single field capture both native-script content and its romanized /
|
|
225
|
+
transliterated forms.
|
|
226
|
+
|
|
227
|
+
Look up an entry by spelling with plain Ruby:
|
|
228
|
+
|
|
229
|
+
[source,ruby]
|
|
230
|
+
----
|
|
231
|
+
decision.title.find { |l| l.spelling == "eng" }&.value
|
|
232
|
+
decision.title.find { |l| l.spelling == "acadsin:zho-Hani:Latn:2002" }&.value
|
|
102
233
|
----
|
|
103
234
|
|
|
104
|
-
|
|
105
|
-
`localizations[]` entries. Language-agnostic admin fields
|
|
106
|
-
(`identifier`, `doi`, `urn`, `agenda_item`, `dates`, `categories`,
|
|
107
|
-
`meeting`, `relations`, `urls`) live on the parent `Resolution`.
|
|
235
|
+
=== Polymorphic Venue
|
|
108
236
|
|
|
109
|
-
|
|
237
|
+
A `Venue` is one flat class on the wire; the `kind` field discriminates
|
|
238
|
+
physical vs virtual.
|
|
110
239
|
|
|
111
|
-
|
|
112
|
-
|
|
240
|
+
* Physical: UN/LOCODE + IATA + address + geo-coordinates.
|
|
241
|
+
* Virtual: URI + iCalendar-style features + access details.
|
|
113
242
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
243
|
+
A meeting can have multiple venues of either kind (hybrid meetings,
|
|
244
|
+
multi-room conferences bridged by video, etc.). UN/LOCODE and IATA codes
|
|
245
|
+
are validated against the canonical `unlocodes` and `iata` gems by
|
|
246
|
+
`Edoxen::VenueValidator` (pure validation via `valid?`; call
|
|
247
|
+
`populate_from_registry!` separately if you want city/country_code
|
|
248
|
+
back-filled from the UN/LOCODE entry).
|
|
249
|
+
|
|
250
|
+
=== Meeting helpers
|
|
120
251
|
|
|
121
252
|
[source,ruby]
|
|
122
253
|
----
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
resolution.primary_localization # English-or-first
|
|
254
|
+
meeting.chair # => Person (via officers + role)
|
|
255
|
+
meeting.officers_with_role(:treasurer) # => [Officer]
|
|
126
256
|
----
|
|
127
257
|
|
|
128
|
-
|
|
258
|
+
`OfficersHost` is mixed into `Meeting` and `MeetingComponent`; both
|
|
259
|
+
expose `chair` (the first officer with `role: "chair"`) and
|
|
260
|
+
`officers_with_role(role)`.
|
|
261
|
+
|
|
262
|
+
== Multilingual example
|
|
263
|
+
|
|
264
|
+
A bilingual CIML decision in EN + FR, modelled with per-field Localized:
|
|
129
265
|
|
|
130
266
|
[source,yaml]
|
|
131
267
|
----
|
|
132
|
-
|
|
268
|
+
decisions:
|
|
133
269
|
- identifier:
|
|
134
270
|
- prefix: CIML
|
|
135
271
|
number: "2025-44"
|
|
136
|
-
|
|
272
|
+
kind: resolution
|
|
273
|
+
status: decided
|
|
274
|
+
doi: 10.63493/decisions/ciml202544
|
|
137
275
|
agenda_item: "16.2"
|
|
138
276
|
dates:
|
|
139
277
|
- date: 2025-10-13
|
|
140
|
-
type:
|
|
141
|
-
|
|
142
|
-
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
278
|
+
type: adoption
|
|
279
|
+
title:
|
|
280
|
+
- spelling: eng
|
|
281
|
+
value: Decision on the renewal of the contract of Mr Anthony Donnellan
|
|
282
|
+
- spelling: fra
|
|
283
|
+
value: Décision sur le renouvellement du contrat de M. Anthony Donnellan
|
|
284
|
+
subject:
|
|
285
|
+
- spelling: eng
|
|
286
|
+
value: CIML
|
|
287
|
+
- spelling: fra
|
|
288
|
+
value: CIML
|
|
289
|
+
actions:
|
|
290
|
+
- type: decides
|
|
291
|
+
date_effective:
|
|
292
|
+
date: 2025-10-13
|
|
293
|
+
type: adoption
|
|
294
|
+
message:
|
|
295
|
+
- spelling: eng
|
|
296
|
+
value: |
|
|
152
297
|
The Committee decides to renew the contract of
|
|
153
298
|
Mr Anthony Donnellan as BIML Director.
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
title: Décision sur le renouvellement du contrat de M. Anthony Donnellan
|
|
157
|
-
subject: CIML
|
|
158
|
-
actions:
|
|
159
|
-
- type: decides
|
|
160
|
-
date_effective:
|
|
161
|
-
date: 2025-10-13
|
|
162
|
-
type: adoption
|
|
163
|
-
message: |
|
|
299
|
+
- spelling: fra
|
|
300
|
+
value: |
|
|
164
301
|
Le Comité décide de renouveler le contrat de
|
|
165
302
|
M. Anthony Donnellan en tant que Directeur du BIML.
|
|
166
303
|
----
|
|
167
304
|
|
|
168
305
|
== Command-line interface
|
|
169
306
|
|
|
170
|
-
The `edoxen` executable exposes
|
|
171
|
-
collections, two for Meeting/Agenda documents.
|
|
307
|
+
The `edoxen` executable exposes six commands.
|
|
172
308
|
|
|
173
|
-
=== `validate` —
|
|
309
|
+
=== `validate` — schema + model validation for Decisions
|
|
174
310
|
|
|
175
311
|
[source,sh]
|
|
176
312
|
----
|
|
177
313
|
$ edoxen validate "spec/fixtures/*.yaml"
|
|
178
314
|
----
|
|
179
315
|
|
|
180
|
-
Runs both `Edoxen::SchemaValidator` and `Edoxen::
|
|
181
|
-
against each matching file.
|
|
316
|
+
Runs both `Edoxen::SchemaValidator` and `Edoxen::DecisionCollection.from_yaml`
|
|
317
|
+
against each matching file. Schema catches `additionalProperties`,
|
|
182
318
|
`required`, enum, and pattern violations; the model catches structural
|
|
183
319
|
problems the schema can't express.
|
|
184
320
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
=== `normalize` — round-trip Resolution YAML through the model
|
|
321
|
+
=== `normalize` — round-trip Decision YAML through the model
|
|
188
322
|
|
|
189
323
|
[source,sh]
|
|
190
324
|
----
|
|
@@ -192,92 +326,120 @@ $ edoxen normalize "spec/fixtures/*.yaml" --output clean/
|
|
|
192
326
|
$ edoxen normalize legacy.yaml --inplace
|
|
193
327
|
----
|
|
194
328
|
|
|
195
|
-
|
|
196
|
-
result back. Either `--output DIR` or `--inplace` is required (mutually
|
|
197
|
-
exclusive).
|
|
329
|
+
Either `--output DIR` or `--inplace` is required (mutually exclusive).
|
|
198
330
|
|
|
199
|
-
=== `validate-meetings`
|
|
331
|
+
=== `validate-meetings` / `normalize-meetings`
|
|
332
|
+
|
|
333
|
+
Same shape, for Meeting / MeetingCollection / MeetingSeries YAML against
|
|
334
|
+
`schema/meeting.yaml`.
|
|
200
335
|
|
|
201
336
|
[source,sh]
|
|
202
337
|
----
|
|
203
338
|
$ edoxen validate-meetings "spec/fixtures/meetings/*.yaml"
|
|
339
|
+
$ edoxen normalize-meetings "spec/fixtures/meetings/*.yaml" --output clean/
|
|
204
340
|
----
|
|
205
341
|
|
|
206
|
-
|
|
207
|
-
`
|
|
208
|
-
|
|
342
|
+
The schema's root is `oneOf` — single `Meeting`, `MeetingCollection`,
|
|
343
|
+
and `MeetingSeries` are all accepted.
|
|
344
|
+
|
|
345
|
+
=== `check-links DIRECTORY` — cross-document URN audit
|
|
346
|
+
|
|
347
|
+
Walks `DIRECTORY` recursively, classifies every `*.yaml` / `*.yml` as a
|
|
348
|
+
Meeting, MeetingCollection, or DecisionCollection, and reports:
|
|
209
349
|
|
|
210
|
-
|
|
350
|
+
* every `metadata.meeting_urn` that does not resolve to an indexed
|
|
351
|
+
Meeting;
|
|
352
|
+
* every Meeting `urn` claimed by more than one file.
|
|
353
|
+
|
|
354
|
+
Error messages include file:line and the in-document pointer.
|
|
211
355
|
|
|
212
356
|
[source,sh]
|
|
213
357
|
----
|
|
214
|
-
$ edoxen
|
|
358
|
+
$ edoxen check-links ./spec/fixtures
|
|
359
|
+
🔗 Checking cross-document URN links in ./spec/fixtures...
|
|
360
|
+
✅ No broken or duplicate links found
|
|
361
|
+
|
|
362
|
+
$ edoxen check-links ./_data
|
|
363
|
+
❌ Found 2 link error(s):
|
|
364
|
+
./_data/decisions.yaml:4: metadata.meeting_urn references urn:oiml:ciml:meeting:ciml-99 (no matching Meeting)
|
|
365
|
+
./_data/dupe.yaml:7: urn references urn:oiml:ciml:meeting:ciml-55 (duplicate Meeting URN (first seen at ./_data/meeting-55.yaml:4))
|
|
215
366
|
----
|
|
216
367
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
== Meeting & Agenda model
|
|
220
|
-
|
|
221
|
-
The Meeting model represents the *event* that produces Resolutions,
|
|
222
|
-
plus the agenda document that orders its business. Mirrors
|
|
223
|
-
`edoxen-model/models/meeting*.lutaml`.
|
|
368
|
+
=== `unlocode CODE` / `iata CODE` — registry lookup
|
|
224
369
|
|
|
225
|
-
[source]
|
|
370
|
+
[source,sh]
|
|
226
371
|
----
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
├── host, hosts: HostRef[] (typed: national_body/liaison/associate/organizer)
|
|
238
|
-
├── source_urls: SourceUrl[] (with `kind` discriminator)
|
|
239
|
-
├── landing_url, registration_url
|
|
240
|
-
├── agenda: Agenda
|
|
241
|
-
│ ├── identifier, status (AgendaStatus), source_doc
|
|
242
|
-
│ ├── items: AgendaItem[]
|
|
243
|
-
│ │ ├── label, kind (numbered/unnumbered/header/opening/closing)
|
|
244
|
-
│ │ ├── title, description
|
|
245
|
-
│ │ ├── references: Reference[]
|
|
246
|
-
│ │ ├── outcome (discussed/resolved/deferred/adopted/withdrawn)
|
|
247
|
-
│ │ └── resolution_ref (URN link to a Resolution)
|
|
248
|
-
│ └── opening_session, closing_session: ScheduleItem
|
|
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)
|
|
372
|
+
$ edoxen unlocode FRPAR
|
|
373
|
+
UN/LOCODE: FRPAR
|
|
374
|
+
Name: Paris
|
|
375
|
+
Country: FR
|
|
376
|
+
...
|
|
377
|
+
|
|
378
|
+
$ edoxen iata JFK
|
|
379
|
+
IATA: JFK
|
|
380
|
+
Name: John F. Kennedy International Airport
|
|
381
|
+
Country: US
|
|
254
382
|
----
|
|
255
383
|
|
|
256
|
-
|
|
384
|
+
== Profile mechanism (ISO 8601-2 §15)
|
|
257
385
|
|
|
258
|
-
|
|
386
|
+
Every core entity has an `extensions: MeetingExtension[0..*]` slot.
|
|
387
|
+
Adopters register a profile namespace and define extension kinds within
|
|
388
|
+
it. `MeetingExtension` carries three identity fields plus a typed
|
|
389
|
+
`attributes[]` list:
|
|
259
390
|
|
|
260
|
-
|
|
261
|
-
|
|
391
|
+
* `profile` — the namespace (lowercase, hyphen-separated).
|
|
392
|
+
* `kind` — discriminator within the profile.
|
|
393
|
+
* `ref` — URN of an external profile document, when the data lives
|
|
394
|
+
elsewhere.
|
|
395
|
+
* `attributes[]` — typed key/value pairs (`ExtensionAttribute`).
|
|
262
396
|
|
|
263
|
-
|
|
264
|
-
|
|
397
|
+
`ExtensionAttribute` is polymorphic on value type. Set `type`
|
|
398
|
+
to one of `string | integer | float | boolean | date | datetime` and
|
|
399
|
+
populate the matching value field:
|
|
400
|
+
|
|
401
|
+
[source,yaml]
|
|
402
|
+
----
|
|
403
|
+
extensions:
|
|
404
|
+
- profile: legco
|
|
405
|
+
kind: vote_block
|
|
406
|
+
ref: urn:legco:vote-block:2024-01-15:item-5
|
|
407
|
+
- profile: ietf
|
|
408
|
+
kind: wg_meeting_meta
|
|
409
|
+
attributes:
|
|
410
|
+
- key: wg_name
|
|
411
|
+
type: string
|
|
412
|
+
value: quic
|
|
413
|
+
- key: draft_name
|
|
414
|
+
type: string
|
|
415
|
+
value: draft-ietf-quic-v2
|
|
416
|
+
- key: quorum
|
|
417
|
+
type: integer
|
|
418
|
+
integer_value: 7
|
|
419
|
+
- key: live_stream
|
|
420
|
+
type: boolean
|
|
421
|
+
boolean_value: true
|
|
422
|
+
- key: start
|
|
423
|
+
type: datetime
|
|
424
|
+
date_time_value: 2026-07-04T10:00:00Z
|
|
425
|
+
----
|
|
265
426
|
|
|
266
|
-
|
|
427
|
+
Read the typed payload in Ruby via `#typed_value`:
|
|
267
428
|
|
|
268
429
|
[source,ruby]
|
|
269
430
|
----
|
|
270
|
-
|
|
431
|
+
attr = extension.attributes.find { |a| a.key == "quorum" }
|
|
432
|
+
attr.type # => "integer"
|
|
433
|
+
attr.integer_value # => 7
|
|
434
|
+
attr.typed_value # => 7
|
|
435
|
+
----
|
|
271
436
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
meeting.primary_localization # English-or-first
|
|
275
|
-
meeting.find_agenda_item("5.2") # AgendaItem by label
|
|
437
|
+
The pre-1.0 bare `value: String` wire shape still parses (routed into
|
|
438
|
+
the string variant; `type` defaults to `string`).
|
|
276
439
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
----
|
|
440
|
+
Consumers ignore profile extensions they don't understand. See
|
|
441
|
+
https://github.com/edoxen/edoxen-model/tree/main/references/profiles[the
|
|
442
|
+
HK LegCo profile example] for a real-world reference.
|
|
281
443
|
|
|
282
444
|
== Architecture
|
|
283
445
|
|
|
@@ -288,40 +450,110 @@ collection.find_by_identifier(prefix: "CIML", number: "56")
|
|
|
288
450
|
* Each model lives in its own file under `lib/edoxen/`. Models declare
|
|
289
451
|
`attribute` only — `lutaml-model` auto-emits an identity map (wire
|
|
290
452
|
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.
|
|
453
|
+
is present.
|
|
293
454
|
* `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
|
-
|
|
455
|
+
value used by the gem. Both the Ruby model (`attribute :kind, :string,
|
|
456
|
+
values: Enums::DECISION_KIND`) and the schemas reference the same
|
|
457
|
+
constants.
|
|
297
458
|
* `lib/edoxen/error.rb` defines the unified `Edoxen::ValidationError`
|
|
298
459
|
— produced by both `SchemaValidator` (with `source: :schema` or
|
|
299
460
|
`:syntax`) and by model parse rescues in the CLI (with `source:
|
|
300
|
-
|
|
301
|
-
`message_text`, and `source`.
|
|
461
|
+
`:model`).
|
|
302
462
|
* `lib/edoxen/schema_validator.rb` is intentionally small: two
|
|
303
463
|
validate methods, a `LineMap` module for line-accurate error
|
|
304
464
|
reporting (longest-prefix match — no path-shape hardcoding), and
|
|
305
465
|
date coercion so `json_schemer` can validate `format: date` against
|
|
306
466
|
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
|
|
467
|
+
* `lib/edoxen/cli.rb` exposes seven Thor commands (`validate`,
|
|
468
|
+
`normalize`, `validate-meetings`, `normalize-meetings`, `check-links`,
|
|
469
|
+
`unlocode`, `iata`) that delegate their shared scaffolding to the deep
|
|
311
470
|
`Edoxen::Cli::Batch` module.
|
|
471
|
+
* `lib/edoxen/venue_validator.rb` validates polymorphic `Venue`
|
|
472
|
+
instances against the `unlocodes` and `iata` gems. `valid?` is pure;
|
|
473
|
+
`populate_from_registry!` is a separate, explicitly-mutating method
|
|
474
|
+
for callers that want UN/LOCODE → city/country_code back-fill.
|
|
312
475
|
|
|
313
476
|
=== Schema ↔ Ruby invariants
|
|
314
477
|
|
|
315
|
-
|
|
316
|
-
boundary:
|
|
478
|
+
Four pairs of runtime specs guard the schema ↔ Ruby boundary:
|
|
317
479
|
|
|
318
|
-
*
|
|
480
|
+
* Decision side: `schema_enum_sync_spec.rb` +
|
|
319
481
|
`schema_model_sync_spec.rb` against `schema/edoxen.yaml`.
|
|
320
482
|
* Meeting side: `schema_meeting_enum_sync_spec.rb` +
|
|
321
483
|
`schema_meeting_model_sync_spec.rb` against `schema/meeting.yaml`.
|
|
484
|
+
* Canonical sync: `schema_model_canonical_sync_spec.rb` asserts the
|
|
485
|
+
gem schemas byte-match `../edoxen-model/schema/{decision-collection,
|
|
486
|
+
meeting}.yaml`.
|
|
487
|
+
* Lutaml sync: `lutaml_ruby_sync_spec.rb` walks every `*.lutaml`
|
|
488
|
+
class and enum and asserts attribute names + collection flags match
|
|
489
|
+
the Ruby side.
|
|
322
490
|
|
|
323
491
|
Drift fails CI immediately, not at fixture-validation time.
|
|
324
492
|
|
|
493
|
+
== Migrating from pre-1.0 (Resolution → Decision)
|
|
494
|
+
|
|
495
|
+
Edoxen Model 1.0 is the first public release; the intermediate `v0.x` /
|
|
496
|
+
`v2.x` history never shipped publicly. The migration from a pre-1.0
|
|
497
|
+
shape is mechanical:
|
|
498
|
+
|
|
499
|
+
[cols="1,3"]
|
|
500
|
+
|===
|
|
501
|
+
|
|
502
|
+
| `Resolution` class
|
|
503
|
+
| `Decision` class with `kind: resolution`
|
|
504
|
+
|
|
505
|
+
| `ResolutionType` enum (4 values)
|
|
506
|
+
| `DecisionKind` enum (9 values: resolution, order, ruling, determination,
|
|
507
|
+
recommendation, statement, finding, opinion, other)
|
|
508
|
+
|
|
509
|
+
| `ResolutionCollection`
|
|
510
|
+
| `DecisionCollection`
|
|
511
|
+
|
|
512
|
+
| `Meeting.virtual: Boolean`
|
|
513
|
+
| `Meeting.venues: Venue[]` (polymorphic)
|
|
514
|
+
|
|
515
|
+
| `Meeting.chair` / `Meeting.secretary` (direct)
|
|
516
|
+
| `Meeting.officers: Officer[]` (role discriminates); `Meeting#chair`
|
|
517
|
+
accessor via `OfficersHost`
|
|
518
|
+
|
|
519
|
+
| `Meeting.schedule[]` (ScheduleItem)
|
|
520
|
+
| `Meeting.components: MeetingComponent[]` (flat)
|
|
521
|
+
|
|
522
|
+
| `Resolution` alone
|
|
523
|
+
| `Decision` + `Motion` + `Voting` (procedural core)
|
|
524
|
+
|
|
525
|
+
| `localizations: Localization[1..*]` per entity
|
|
526
|
+
| Per-field `Localized<String|Name>[0..*]` (ISO 24229); each
|
|
527
|
+
translatable field carries its own spelling tag inline.
|
|
528
|
+
|
|
529
|
+
| `decision.in_language(code)` / `decision.primary_localization`
|
|
530
|
+
| `decision.title.find { |l| l.spelling == code }` — direct array
|
|
531
|
+
lookup, no wrapper.
|
|
532
|
+
|
|
533
|
+
| (none)
|
|
534
|
+
| `Topic` + `TopicDocument` + `TopicAsset`
|
|
535
|
+
|
|
536
|
+
| (none)
|
|
537
|
+
| `MeetingSeries`, `Recurrence` (ISO 8601-2 §13)
|
|
538
|
+
|
|
539
|
+
| (none)
|
|
540
|
+
| `MeetingExtension` (profile mechanism, ISO 8601-2 §15)
|
|
541
|
+
|
|
542
|
+
| (none)
|
|
543
|
+
| `ContactCollection`, `VenueCollection` (scoped-URN registries)
|
|
544
|
+
|
|
545
|
+
| `Meeting.date_range` (single range)
|
|
546
|
+
| `Meeting.scheduled_date_range` + `Meeting.occurred_date_range`
|
|
547
|
+
(split scheduled vs actual; occurred uses DateTimeRange for
|
|
548
|
+
sub-day precision)
|
|
549
|
+
|
|
550
|
+
| (none)
|
|
551
|
+
| `Statement`, `Declaration`, `DateTimeRange` — BS 0:2006 §7.6
|
|
552
|
+
meeting-minutes concepts integrated into core. Statements and
|
|
553
|
+
declarations attach to `Meeting`, `Topic`, and `MinutesSection`.
|
|
554
|
+
|
|
555
|
+
|===
|
|
556
|
+
|
|
325
557
|
== Contributing
|
|
326
558
|
|
|
327
559
|
Follow the rules in `CLAUDE.md`:
|