edoxen 0.5.0 → 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/CLAUDE.md +10 -6
- data/Gemfile +4 -0
- data/README.adoc +417 -181
- data/TODO.meeting-agenda/02-lutaml-canonical.md +1 -1
- data/TODO.meeting-agenda/09-verify-and-release.md +3 -3
- data/TODO.meeting-agenda/README.md +2 -2
- data/edoxen.gemspec +10 -9
- 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 +29 -0
- data/lib/edoxen/body_vocabulary_entry.rb +24 -0
- data/lib/edoxen/body_vocabulary_host.rb +40 -0
- data/lib/edoxen/cli.rb +186 -108
- 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 +129 -8
- 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 +194 -0
- data/lib/edoxen/localized_name.rb +16 -0
- data/lib/edoxen/localized_string.rb +22 -0
- data/lib/edoxen/meeting.rb +42 -36
- 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 +30 -0
- data/lib/edoxen/minutes_section.rb +26 -0
- 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 +85 -0
- 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 +29 -0
- data/lib/edoxen/voting.rb +45 -0
- data/lib/edoxen/voting_counts.rb +23 -0
- data/lib/edoxen.rb +53 -14
- data/schema/edoxen.yaml +1378 -247
- data/schema/meeting.yaml +1258 -201
- data/sig/edoxen.rbs +581 -1
- metadata +82 -21
- 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 -22
- data/lib/edoxen/resolution_relation.rb +0 -11
- data/lib/edoxen/resolution_set.rb +0 -17
- data/lib/edoxen/schedule_item.rb +0 -16
data/README.adoc
CHANGED
|
@@ -1,26 +1,36 @@
|
|
|
1
1
|
= Edoxen
|
|
2
2
|
|
|
3
|
-
https://github.com/
|
|
4
|
-
image:https://img.shields.io/github/license/
|
|
5
|
-
image:https://img.shields.io/github/actions/workflow/status/
|
|
3
|
+
https://github.com/edoxen/edoxen[image:https://img.shields.io/github/stars/edoxen/edoxen.svg?style=social[GitHub Stars]]
|
|
4
|
+
image:https://img.shields.io/github/license/edoxen/edoxen.svg[License]
|
|
5
|
+
image:https://img.shields.io/github/actions/workflow/status/edoxen/edoxen/test.yml?branch=main[Build Status]
|
|
6
6
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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.
|
|
24
34
|
|
|
25
35
|
== Installation
|
|
26
36
|
|
|
@@ -37,150 +47,278 @@ Then `bundle install`, or `gem install edoxen` standalone.
|
|
|
37
47
|
----
|
|
38
48
|
require 'edoxen'
|
|
39
49
|
|
|
40
|
-
yaml = File.read('
|
|
41
|
-
collection = Edoxen::
|
|
50
|
+
yaml = File.read('decisions.yaml')
|
|
51
|
+
collection = Edoxen::DecisionCollection.from_yaml(yaml)
|
|
42
52
|
|
|
43
|
-
collection.
|
|
44
|
-
|
|
45
|
-
# its TC number, SC number, and any cross-cutting reference number.
|
|
46
|
-
id = resolution.identifier.first
|
|
53
|
+
collection.decisions.each do |decision|
|
|
54
|
+
id = decision.identifier.first
|
|
47
55
|
puts "#{id.prefix}/#{id.number}"
|
|
48
56
|
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
|
|
52
|
-
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}"
|
|
53
61
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
end
|
|
62
|
+
fra = decision.title.find { |l| l.spelling == "fra" }&.value
|
|
63
|
+
puts " [fra] #{fra}" if fra
|
|
57
64
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
65
|
+
decision.actions.each do |action|
|
|
66
|
+
msg = action.message.find { |l| l.spelling == "eng" }&.value
|
|
67
|
+
puts " - #{action.type}: #{msg}"
|
|
68
|
+
end
|
|
61
69
|
end
|
|
62
70
|
|
|
63
71
|
# Round-trip back to YAML
|
|
64
72
|
puts collection.to_yaml
|
|
65
73
|
----
|
|
66
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
|
+
|
|
67
116
|
== Data model
|
|
68
117
|
|
|
69
118
|
The full model is in `lib/edoxen/*.rb` (Ruby) and `schema/edoxen.yaml`
|
|
70
|
-
(JSON-Schema). Both are kept in lockstep by
|
|
71
|
-
|
|
119
|
+
+ `schema/meeting.yaml` (JSON-Schema). Both are kept in lockstep by
|
|
120
|
+
runtime sync specs.
|
|
72
121
|
|
|
73
122
|
[source]
|
|
74
123
|
----
|
|
75
|
-
|
|
76
|
-
├── metadata:
|
|
77
|
-
│
|
|
78
|
-
│
|
|
79
|
-
│
|
|
80
|
-
│
|
|
81
|
-
│
|
|
82
|
-
└──
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
├──
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
98
197
|
----
|
|
99
198
|
|
|
100
|
-
Every
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
`
|
|
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)
|
|
104
208
|
|
|
105
|
-
|
|
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.
|
|
106
213
|
|
|
107
|
-
|
|
108
|
-
|
|
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`).
|
|
109
221
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
233
|
+
----
|
|
234
|
+
|
|
235
|
+
=== Polymorphic Venue
|
|
236
|
+
|
|
237
|
+
A `Venue` is one flat class on the wire; the `kind` field discriminates
|
|
238
|
+
physical vs virtual.
|
|
239
|
+
|
|
240
|
+
* Physical: UN/LOCODE + IATA + address + geo-coordinates.
|
|
241
|
+
* Virtual: URI + iCalendar-style features + access details.
|
|
242
|
+
|
|
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
|
|
116
251
|
|
|
117
252
|
[source,ruby]
|
|
118
253
|
----
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
resolution.primary_localization # English-or-first
|
|
254
|
+
meeting.chair # => Person (via officers + role)
|
|
255
|
+
meeting.officers_with_role(:treasurer) # => [Officer]
|
|
122
256
|
----
|
|
123
257
|
|
|
124
|
-
|
|
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:
|
|
125
265
|
|
|
126
266
|
[source,yaml]
|
|
127
267
|
----
|
|
128
|
-
|
|
268
|
+
decisions:
|
|
129
269
|
- identifier:
|
|
130
270
|
- prefix: CIML
|
|
131
271
|
number: "2025-44"
|
|
132
|
-
|
|
272
|
+
kind: resolution
|
|
273
|
+
status: decided
|
|
274
|
+
doi: 10.63493/decisions/ciml202544
|
|
133
275
|
agenda_item: "16.2"
|
|
134
276
|
dates:
|
|
135
277
|
- date: 2025-10-13
|
|
136
|
-
type:
|
|
137
|
-
|
|
138
|
-
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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: |
|
|
148
297
|
The Committee decides to renew the contract of
|
|
149
298
|
Mr Anthony Donnellan as BIML Director.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
title: Décision sur le renouvellement du contrat de M. Anthony Donnellan
|
|
153
|
-
subject: CIML
|
|
154
|
-
actions:
|
|
155
|
-
- type: decides
|
|
156
|
-
date_effective:
|
|
157
|
-
date: 2025-10-13
|
|
158
|
-
type: adoption
|
|
159
|
-
message: |
|
|
299
|
+
- spelling: fra
|
|
300
|
+
value: |
|
|
160
301
|
Le Comité décide de renouveler le contrat de
|
|
161
302
|
M. Anthony Donnellan en tant que Directeur du BIML.
|
|
162
303
|
----
|
|
163
304
|
|
|
164
305
|
== Command-line interface
|
|
165
306
|
|
|
166
|
-
The `edoxen` executable exposes
|
|
167
|
-
collections, two for Meeting/Agenda documents.
|
|
307
|
+
The `edoxen` executable exposes six commands.
|
|
168
308
|
|
|
169
|
-
=== `validate` —
|
|
309
|
+
=== `validate` — schema + model validation for Decisions
|
|
170
310
|
|
|
171
311
|
[source,sh]
|
|
172
312
|
----
|
|
173
313
|
$ edoxen validate "spec/fixtures/*.yaml"
|
|
174
314
|
----
|
|
175
315
|
|
|
176
|
-
Runs both `Edoxen::SchemaValidator` and `Edoxen::
|
|
177
|
-
against each matching file.
|
|
316
|
+
Runs both `Edoxen::SchemaValidator` and `Edoxen::DecisionCollection.from_yaml`
|
|
317
|
+
against each matching file. Schema catches `additionalProperties`,
|
|
178
318
|
`required`, enum, and pattern violations; the model catches structural
|
|
179
319
|
problems the schema can't express.
|
|
180
320
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
=== `normalize` — round-trip Resolution YAML through the model
|
|
321
|
+
=== `normalize` — round-trip Decision YAML through the model
|
|
184
322
|
|
|
185
323
|
[source,sh]
|
|
186
324
|
----
|
|
@@ -188,92 +326,120 @@ $ edoxen normalize "spec/fixtures/*.yaml" --output clean/
|
|
|
188
326
|
$ edoxen normalize legacy.yaml --inplace
|
|
189
327
|
----
|
|
190
328
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
329
|
+
Either `--output DIR` or `--inplace` is required (mutually exclusive).
|
|
330
|
+
|
|
331
|
+
=== `validate-meetings` / `normalize-meetings`
|
|
194
332
|
|
|
195
|
-
|
|
333
|
+
Same shape, for Meeting / MeetingCollection / MeetingSeries YAML against
|
|
334
|
+
`schema/meeting.yaml`.
|
|
196
335
|
|
|
197
336
|
[source,sh]
|
|
198
337
|
----
|
|
199
338
|
$ edoxen validate-meetings "spec/fixtures/meetings/*.yaml"
|
|
339
|
+
$ edoxen normalize-meetings "spec/fixtures/meetings/*.yaml" --output clean/
|
|
200
340
|
----
|
|
201
341
|
|
|
202
|
-
|
|
203
|
-
`
|
|
204
|
-
|
|
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
|
|
205
346
|
|
|
206
|
-
|
|
347
|
+
Walks `DIRECTORY` recursively, classifies every `*.yaml` / `*.yml` as a
|
|
348
|
+
Meeting, MeetingCollection, or DecisionCollection, and reports:
|
|
349
|
+
|
|
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.
|
|
207
355
|
|
|
208
356
|
[source,sh]
|
|
209
357
|
----
|
|
210
|
-
$ 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))
|
|
211
366
|
----
|
|
212
367
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
== Meeting & Agenda model
|
|
368
|
+
=== `unlocode CODE` / `iata CODE` — registry lookup
|
|
216
369
|
|
|
217
|
-
|
|
218
|
-
plus the agenda document that orders its business. Mirrors
|
|
219
|
-
`edoxen-model/models/meeting*.lutaml`.
|
|
220
|
-
|
|
221
|
-
[source]
|
|
370
|
+
[source,sh]
|
|
222
371
|
----
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
├── host, hosts: HostRef[] (typed: national_body/liaison/associate/organizer)
|
|
234
|
-
├── source_urls: SourceUrl[] (with `kind` discriminator)
|
|
235
|
-
├── landing_url, registration_url
|
|
236
|
-
├── agenda: Agenda
|
|
237
|
-
│ ├── identifier, status (AgendaStatus), source_doc
|
|
238
|
-
│ ├── items: AgendaItem[]
|
|
239
|
-
│ │ ├── label, kind (numbered/unnumbered/header/opening/closing)
|
|
240
|
-
│ │ ├── title, description
|
|
241
|
-
│ │ ├── references: Reference[]
|
|
242
|
-
│ │ ├── outcome (discussed/resolved/deferred/adopted/withdrawn)
|
|
243
|
-
│ │ └── resolution_ref (URN link to a Resolution)
|
|
244
|
-
│ └── opening_session, closing_session: ScheduleItem
|
|
245
|
-
├── schedule: ScheduleItem[] (the time-bound timetable)
|
|
246
|
-
├── deadlines: Deadline[]
|
|
247
|
-
├── localizations: MeetingLocalization[] (per-language content)
|
|
248
|
-
├── relations: MeetingRelation[] (continues_from, joint_with, ...)
|
|
249
|
-
└── 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
|
|
250
382
|
----
|
|
251
383
|
|
|
252
|
-
|
|
384
|
+
== Profile mechanism (ISO 8601-2 §15)
|
|
385
|
+
|
|
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:
|
|
253
390
|
|
|
254
|
-
|
|
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`).
|
|
255
396
|
|
|
256
|
-
|
|
257
|
-
|
|
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:
|
|
258
400
|
|
|
259
|
-
|
|
260
|
-
|
|
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
|
+
----
|
|
261
426
|
|
|
262
|
-
|
|
427
|
+
Read the typed payload in Ruby via `#typed_value`:
|
|
263
428
|
|
|
264
429
|
[source,ruby]
|
|
265
430
|
----
|
|
266
|
-
|
|
431
|
+
attr = extension.attributes.find { |a| a.key == "quorum" }
|
|
432
|
+
attr.type # => "integer"
|
|
433
|
+
attr.integer_value # => 7
|
|
434
|
+
attr.typed_value # => 7
|
|
435
|
+
----
|
|
267
436
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
meeting.primary_localization # English-or-first
|
|
271
|
-
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`).
|
|
272
439
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
----
|
|
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.
|
|
277
443
|
|
|
278
444
|
== Architecture
|
|
279
445
|
|
|
@@ -284,40 +450,110 @@ collection.find_by_identifier(prefix: "CIML", number: "56")
|
|
|
284
450
|
* Each model lives in its own file under `lib/edoxen/`. Models declare
|
|
285
451
|
`attribute` only — `lutaml-model` auto-emits an identity map (wire
|
|
286
452
|
name = snake_case attribute name) when no explicit `key_value` block
|
|
287
|
-
is present.
|
|
288
|
-
only when the wire name differs from the attribute name.
|
|
453
|
+
is present.
|
|
289
454
|
* `lib/edoxen/enums.rb` is the single source of truth for every enum
|
|
290
|
-
value used by the gem. Both the Ruby model (`attribute :
|
|
291
|
-
values: Enums::
|
|
292
|
-
|
|
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.
|
|
293
458
|
* `lib/edoxen/error.rb` defines the unified `Edoxen::ValidationError`
|
|
294
459
|
— produced by both `SchemaValidator` (with `source: :schema` or
|
|
295
460
|
`:syntax`) and by model parse rescues in the CLI (with `source:
|
|
296
|
-
|
|
297
|
-
`message_text`, and `source`.
|
|
461
|
+
`:model`).
|
|
298
462
|
* `lib/edoxen/schema_validator.rb` is intentionally small: two
|
|
299
463
|
validate methods, a `LineMap` module for line-accurate error
|
|
300
464
|
reporting (longest-prefix match — no path-shape hardcoding), and
|
|
301
465
|
date coercion so `json_schemer` can validate `format: date` against
|
|
302
466
|
YAML-loaded `Date` instances.
|
|
303
|
-
* `lib/edoxen/cli.rb` exposes
|
|
304
|
-
`normalize`, `validate-meetings`, `normalize-meetings`
|
|
305
|
-
delegate their shared scaffolding
|
|
306
|
-
(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
|
|
307
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.
|
|
308
475
|
|
|
309
476
|
=== Schema ↔ Ruby invariants
|
|
310
477
|
|
|
311
|
-
|
|
312
|
-
boundary:
|
|
478
|
+
Four pairs of runtime specs guard the schema ↔ Ruby boundary:
|
|
313
479
|
|
|
314
|
-
*
|
|
480
|
+
* Decision side: `schema_enum_sync_spec.rb` +
|
|
315
481
|
`schema_model_sync_spec.rb` against `schema/edoxen.yaml`.
|
|
316
482
|
* Meeting side: `schema_meeting_enum_sync_spec.rb` +
|
|
317
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.
|
|
318
490
|
|
|
319
491
|
Drift fails CI immediately, not at fixture-validation time.
|
|
320
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
|
+
|
|
321
557
|
== Contributing
|
|
322
558
|
|
|
323
559
|
Follow the rules in `CLAUDE.md`:
|