edoxen 0.3.1 → 0.7.2

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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +10 -6
  3. data/Gemfile +4 -0
  4. data/README.adoc +181 -38
  5. data/TODO.meeting-agenda/01-design-decisions.md +132 -0
  6. data/TODO.meeting-agenda/02-lutaml-canonical.md +61 -0
  7. data/TODO.meeting-agenda/03-ruby-models.md +73 -0
  8. data/TODO.meeting-agenda/04-schema.md +34 -0
  9. data/TODO.meeting-agenda/05-fixtures.md +43 -0
  10. data/TODO.meeting-agenda/06-specs.md +52 -0
  11. data/TODO.meeting-agenda/07-cli.md +55 -0
  12. data/TODO.meeting-agenda/08-docs.md +35 -0
  13. data/TODO.meeting-agenda/09-verify-and-release.md +41 -0
  14. data/TODO.meeting-agenda/10-future-enhancements.md +72 -0
  15. data/TODO.meeting-agenda/README.md +30 -0
  16. data/edoxen.gemspec +1 -0
  17. data/lib/edoxen/agenda.rb +26 -0
  18. data/lib/edoxen/agenda_item.rb +17 -0
  19. data/lib/edoxen/attendance.rb +23 -0
  20. data/lib/edoxen/cli.rb +97 -0
  21. data/lib/edoxen/date_range.rb +11 -0
  22. data/lib/edoxen/deadline.rb +10 -0
  23. data/lib/edoxen/enums.rb +31 -0
  24. data/lib/edoxen/host_ref.rb +14 -0
  25. data/lib/edoxen/link_checker.rb +85 -0
  26. data/lib/edoxen/location.rb +15 -0
  27. data/lib/edoxen/meeting.rb +82 -0
  28. data/lib/edoxen/meeting_collection.rb +26 -0
  29. data/lib/edoxen/meeting_collection_metadata.rb +11 -0
  30. data/lib/edoxen/meeting_localization.rb +15 -0
  31. data/lib/edoxen/meeting_relation.rb +12 -0
  32. data/lib/edoxen/minutes.rb +31 -0
  33. data/lib/edoxen/minutes_section.rb +27 -0
  34. data/lib/edoxen/person.rb +14 -0
  35. data/lib/edoxen/reference.rb +13 -0
  36. data/lib/edoxen/reference_data.rb +95 -0
  37. data/lib/edoxen/resolution_metadata.rb +14 -0
  38. data/lib/edoxen/schedule_item.rb +27 -0
  39. data/lib/edoxen/schedule_item_localization.rb +21 -0
  40. data/lib/edoxen/source_url.rb +6 -2
  41. data/lib/edoxen/version.rb +1 -1
  42. data/lib/edoxen/vote_record.rb +25 -0
  43. data/lib/edoxen.rb +23 -0
  44. data/schema/edoxen.yaml +11 -3
  45. data/schema/meeting.yaml +480 -0
  46. metadata +48 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a87eaf24182b1fd7400efbd2af10681f4059a35cd57c807759a8015a59a1c0f1
4
- data.tar.gz: 53ede8ad87d1a25bbf766d7196e6437a8455f431af313ba5b9752cbd42d17050
3
+ metadata.gz: 69a86986a86602665699b5598622d32a63658c302737587f68b7a9b095a471e1
4
+ data.tar.gz: 1201e014ac06c2e4accb00dfd00660f577f6f6464f5d7990553714b796af8314
5
5
  SHA512:
6
- metadata.gz: a14ac6664ffa8ec1f1fcd31ace23257a740dcad3fab41ead54d542440a53618ac8ad46bb495afeb44c62db7b850d35d789ee7a888245f4e6b3ac0afe092310ee
7
- data.tar.gz: 991a18091cb7ec21ddfe70b2691244b3e8a6ba869135545f6249ffa35d56439afbf578a867d56bdce6c3555f177c59f43c73f8c27abb119527af36c42e6b17fd
6
+ metadata.gz: 7ec73b629af4dd2997fea614665347177f273569349d0221e414f9dc9b2dbb8f083b3d964675286bb9a76d449a5a4c7c2cf2981db028ebf5f0d2e54249abb77d
7
+ data.tar.gz: 52534618f6468c4da8effd9bcb23e978487e1857da1a9832fa8d1ed665e72324a13397657b0cb8f6c828be4ef45e9d669781a208b7135c09e97a1b833cc6c69e
data/CLAUDE.md CHANGED
@@ -4,15 +4,19 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
4
4
 
5
5
  ## What this gem is
6
6
 
7
- `edoxen` is an information model for formal resolutions and decisions
8
- (ISO/TC 154, CIPM, OIML, etc.). It provides:
7
+ `edoxen` is an information model for formal proceedings meetings,
8
+ agendas, minutes, attendance, votes, and the resolutions adopted by
9
+ standards bodies (ISO/TC 154, CIPM, OIML, etc.). It provides:
9
10
 
10
11
  - A set of `Lutaml::Model::Serializable` subclasses that mirror the canonical
11
12
  LutaML UML model in `../edoxen-model/models/*.lutaml`.
12
- - A JSON-Schema (`schema/edoxen.yaml`) used to validate real-world YAML
13
- files via `SchemaValidator` (`json_schemer`).
14
- - A Thor CLI (`edoxen validate`, `edoxen normalize`) that wraps both the
15
- schema validator and the Ruby model parser.
13
+ - Two JSON-Schemas `schema/edoxen.yaml` for `ResolutionCollection`
14
+ and `schema/meeting.yaml` for `Meeting` / `MeetingCollection` — used
15
+ to validate real-world YAML files via `SchemaValidator`
16
+ (`json_schemer`).
17
+ - A Thor CLI that wraps both schemas and both model parsers:
18
+ `edoxen validate` / `edoxen normalize` for resolutions;
19
+ `edoxen validate-meetings` / `edoxen normalize-meetings` for meetings.
16
20
 
17
21
  ## Architecture
18
22
 
data/Gemfile CHANGED
@@ -5,6 +5,10 @@ source "https://rubygems.org"
5
5
  # Specify your gem's dependencies in edoxen.gemspec
6
6
  gemspec
7
7
 
8
+ # Pre-release sources. Move to RubyGems once published; the gemspec
9
+ # dep stays as `add_dependency "unlocode", "~> 0.1"` either way.
10
+ gem "unlocodes", github: "metanorma/unlocode"
11
+
8
12
  gem "rake"
9
13
  gem "rspec"
10
14
  gem "rubocop"
data/README.adoc CHANGED
@@ -1,26 +1,30 @@
1
1
  = Edoxen
2
2
 
3
- https://github.com/metanorma/edoxen[image:https://img.shields.io/github/stars/metanorma/edoxen.svg?style=social[GitHub Stars]]
4
- image:https://img.shields.io/github/license/metanorma/edoxen.svg[License]
5
- image:https://img.shields.io/github/actions/workflow/status/metanorma/edoxen/test.yml?branch=main[Build 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
10
  Edoxen is a Ruby library for the canonical Edoxen information model of
11
- formal resolutions and decisions. It is built on top of the
11
+ formal proceedings meetings, agendas, minutes, attendance, votes,
12
+ and the resolutions adopted by standards bodies and governance
13
+ organisations. It is built on top of the
12
14
  https://github.com/lutaml/lutaml[ lutaml-model] serialization framework.
13
15
 
14
16
  The information model is defined in
15
- https://github.com/metanorma/edoxen-model/tree/main/models[ LutaML UML
17
+ https://github.com/edoxen/edoxen-model/tree/main/models[ LutaML UML
16
18
  files] (one `.lutaml` per concept). This gem mirrors that model exactly —
17
19
  attribute declarations, enum values, and field shapes — so that anything
18
20
  you can express in LutaML you can also construct, serialize, and validate
19
21
  in Ruby.
20
22
 
21
23
  The intended users are standards organizations and governance bodies that
22
- need to publish structured records of formal decision-making. Real-world
23
- samples (ISO/TC 154, CIPM, OIML CIML) ship in `spec/fixtures/`.
24
+ need to publish structured records of formal proceedings — the full
25
+ lifecycle from draft agenda through adopted resolution, in any ISO 639-3
26
+ language. Real-world samples (ISO/TC 154, CIPM, OIML CIML) ship in
27
+ `spec/fixtures/`.
24
28
 
25
29
  == Installation
26
30
 
@@ -41,16 +45,23 @@ yaml = File.read('resolutions.yaml')
41
45
  collection = Edoxen::ResolutionCollection.from_yaml(yaml)
42
46
 
43
47
  collection.resolutions.each do |resolution|
44
- prefix = resolution.identifier.first.prefix
45
- number = resolution.identifier.first.number
46
- puts "#{prefix}/#{number}"
47
-
48
- resolution.localizations.each do |loc|
49
- puts " [#{loc.language_code}/#{loc.script}] #{loc.title}"
50
- loc.actions.each do |action|
51
- puts " - #{action.type}: #{action.message}"
52
- end
48
+ # identifier is StructuredIdentifier[1..*] — a Resolution can carry
49
+ # its TC number, SC number, and any cross-cutting reference number.
50
+ id = resolution.identifier.first
51
+ puts "#{id.prefix}/#{id.number}"
52
+
53
+ # Canonical rendering — English when available, else the first
54
+ # declared localization. See "Lookup accessors" below.
55
+ loc = resolution.primary_localization
56
+ puts " [#{loc.language_code}/#{loc.script}] #{loc.title}"
57
+
58
+ loc.actions.each do |action|
59
+ puts " - #{action.type}: #{action.message}"
53
60
  end
61
+
62
+ # Or look up a specific language explicitly:
63
+ fra = resolution.in_language("fra")
64
+ puts " FR: #{fra.title}" if fra
54
65
  end
55
66
 
56
67
  # Round-trip back to YAML
@@ -95,6 +106,25 @@ Every translatable field on a Resolution is wrapped inside one of its
95
106
  (`identifier`, `doi`, `urn`, `agenda_item`, `dates`, `categories`,
96
107
  `meeting`, `relations`, `urls`) live on the parent `Resolution`.
97
108
 
109
+ === Lookup accessors
110
+
111
+ Direct iteration over `localizations[]` is discouraged — the
112
+ language-preference policy lives behind two accessors:
113
+
114
+ * `Resolution#in_language(code, fallback: false)` — exact match by
115
+ ISO 639-3 code, or `nil` (or the first declared localization when
116
+ `fallback: true`).
117
+ * `Resolution#primary_localization` — English when available, else
118
+ the first declared localization. Mirrors the glossarist
119
+ LocalizedConcept "preferred language" notion.
120
+
121
+ [source,ruby]
122
+ ----
123
+ resolution.in_language("fra") # => Localization or nil
124
+ resolution.in_language("deu", fallback: true)
125
+ resolution.primary_localization # English-or-first
126
+ ----
127
+
98
128
  == Multilingual resolution sets
99
129
 
100
130
  [source,yaml]
@@ -137,9 +167,10 @@ resolutions:
137
167
 
138
168
  == Command-line interface
139
169
 
140
- The `edoxen` executable exposes two commands.
170
+ The `edoxen` executable exposes four commands — two for Resolution
171
+ collections, two for Meeting/Agenda documents.
141
172
 
142
- === `validate` — JSON-Schema validation + model parsing
173
+ === `validate` — JSON-Schema validation + model parsing for Resolutions
143
174
 
144
175
  [source,sh]
145
176
  ----
@@ -153,7 +184,7 @@ problems the schema can't express.
153
184
 
154
185
  Exit code is non-zero if any file fails.
155
186
 
156
- === `normalize` — round-trip every fixture through the model
187
+ === `normalize` — round-trip Resolution YAML through the model
157
188
 
158
189
  [source,sh]
159
190
  ----
@@ -162,28 +193,134 @@ $ edoxen normalize legacy.yaml --inplace
162
193
  ----
163
194
 
164
195
  Loads each YAML file, parses it through the Ruby model, and writes the
165
- result back. Used to migrate between model versions. Either `--output DIR`
166
- or `--inplace` is required (mutually exclusive).
196
+ result back. Either `--output DIR` or `--inplace` is required (mutually
197
+ exclusive).
198
+
199
+ === `validate-meetings` — validate Meeting/Agenda YAML
200
+
201
+ [source,sh]
202
+ ----
203
+ $ edoxen validate-meetings "spec/fixtures/meetings/*.yaml"
204
+ ----
205
+
206
+ Validates against `schema/meeting.yaml` and parses through
207
+ `Edoxen::Meeting` (single-Meeting files) or `Edoxen::MeetingCollection`
208
+ (wrappers). The schema's root is `oneOf` — both shapes are accepted.
209
+
210
+ === `normalize-meetings` — round-trip Meeting YAML through the model
211
+
212
+ [source,sh]
213
+ ----
214
+ $ edoxen normalize-meetings "spec/fixtures/meetings/*.yaml" --output clean/
215
+ ----
216
+
217
+ Same shape as `normalize`, but for Meeting/Agenda documents.
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`.
224
+
225
+ [source]
226
+ ----
227
+ MeetingCollection
228
+ ├── metadata: MeetingCollectionMetadata
229
+ └── meetings: Meeting[]
230
+ ├── identifier: StructuredIdentifier[1..*]
231
+ ├── urn, ordinal, type (MeetingType), status (MeetingStatus), year
232
+ ├── date_range: DateRange {start, end}
233
+ ├── committee, committee_group
234
+ ├── venues: Location[] (multi-venue supported)
235
+ ├── general_area, city (UN/LOCODE, e.g. FRPAR), country_code (ISO 3166-1), virtual
236
+ ├── chair, secretary: Person
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)
254
+ ----
255
+
256
+ === Linking Meetings to ResolutionCollections
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)
267
+
268
+ [source,ruby]
269
+ ----
270
+ meeting = Edoxen::Meeting.from_yaml(File.read('meeting.yaml'))
271
+
272
+ meeting.in_language("fra") # => MeetingLocalization or nil
273
+ meeting.in_language("deu", fallback: true) # falls back to first
274
+ meeting.primary_localization # English-or-first
275
+ meeting.find_agenda_item("5.2") # AgendaItem by label
276
+
277
+ collection = Edoxen::MeetingCollection.from_yaml(...)
278
+ collection.find_by_urn("urn:oiml:ciml:meeting:ciml-56")
279
+ collection.find_by_identifier(prefix: "CIML", number: "56")
280
+ ----
167
281
 
168
282
  == Architecture
169
283
 
170
284
  * `lib/edoxen.rb` is the single entry-point. It configures
171
285
  `Lutaml::Model::Config` and `autoload`s every model class and service.
172
- * Each model lives in its own file under `lib/edoxen/`. No `require_relative`
173
- — all cross-references resolve through Ruby autoload.
286
+ No `require_relative` in library code cross-references resolve
287
+ through Ruby autoload.
288
+ * Each model lives in its own file under `lib/edoxen/`. Models declare
289
+ `attribute` only — `lutaml-model` auto-emits an identity map (wire
290
+ name = snake_case attribute name) when no explicit `key_value` block
291
+ is present. Add a `key_value do; map "wire", to: :attr; end` block
292
+ only when the wire name differs from the attribute name.
174
293
  * `lib/edoxen/enums.rb` is the single source of truth for every enum
175
294
  value used by the gem. Both the Ruby model (`attribute :type, :string,
176
- values: Enums::ACTION_TYPE`) and `schema/edoxen.yaml` refer to the
177
- same constant; `spec/edoxen/schema_enum_sync_spec.rb` asserts equality
178
- at runtime.
179
- * `lib/edoxen/schema_validator.rb` is intentionally small: a
180
- `SchemaValidator::ValidationError`, a `validate_file` method, a
181
- `validate_content` method, and a `LineMap` module for line-accurate
182
- error reporting. Adding new collection paths never requires touching
183
- it the lookup is longest-prefix match against an indent-heuristic
184
- line map.
185
- * `lib/edoxen/cli.rb` is the Thor surface that glues SchemaValidator and
186
- `ResolutionCollection.from_yaml` together for the two CLI commands.
295
+ values: Enums::ACTION_TYPE`) and the schemas reference the
296
+ same constants.
297
+ * `lib/edoxen/error.rb` defines the unified `Edoxen::ValidationError`
298
+ produced by both `SchemaValidator` (with `source: :schema` or
299
+ `:syntax`) and by model parse rescues in the CLI (with `source:
300
+ :model`). Carries `file`, `line`, `column`, `pointer`,
301
+ `message_text`, and `source`.
302
+ * `lib/edoxen/schema_validator.rb` is intentionally small: two
303
+ validate methods, a `LineMap` module for line-accurate error
304
+ reporting (longest-prefix match no path-shape hardcoding), and
305
+ date coercion so `json_schemer` can validate `format: date` against
306
+ YAML-loaded `Date` instances.
307
+ * `lib/edoxen/cli.rb` exposes four Thor commands (`validate`,
308
+ `normalize`, `validate-meetings`, `normalize-meetings`) that
309
+ delegate their shared scaffolding
310
+ (expand/sort/empty/header/loop/tally/summary/exit) to the deep
311
+ `Edoxen::Cli::Batch` module.
312
+
313
+ === Schema ↔ Ruby invariants
314
+
315
+ Two pairs of runtime specs guard each side of the schema ↔ Ruby
316
+ boundary:
317
+
318
+ * Resolution side: `schema_enum_sync_spec.rb` +
319
+ `schema_model_sync_spec.rb` against `schema/edoxen.yaml`.
320
+ * Meeting side: `schema_meeting_enum_sync_spec.rb` +
321
+ `schema_meeting_model_sync_spec.rb` against `schema/meeting.yaml`.
322
+
323
+ Drift fails CI immediately, not at fixture-validation time.
187
324
 
188
325
  == Contributing
189
326
 
@@ -192,10 +329,16 @@ Follow the rules in `CLAUDE.md`:
192
329
  * All public methods have specs.
193
330
  * Specs use real model instances — never `double()`.
194
331
  * Serialization goes through `lutaml-model` only — no hand-rolled
195
- `to_h`, `from_h`, `to_yaml`, or `to_json` on a model class.
196
- * Schema and Ruby enum values must agree; the schema_sync_spec catches
197
- drift.
198
- * All changes go through PRs. Never commit to `main`, never push tags.
332
+ `to_h`, `from_h`, `to_yaml`, or `to_json` on a model class. Declare
333
+ `attribute`; the wire map is auto-emitted.
334
+ * Schema and Ruby must agree on enum values *and* on property shape.
335
+ Both `schema_enum_sync_spec` and `schema_model_sync_spec` catch
336
+ drift at CI time.
337
+ * Library code uses autoload (declared in `lib/edoxen.rb`), never
338
+ `require_relative`. No `send` to private methods, no
339
+ `instance_variable_set`/`get`, no `respond_to?` for type checks.
340
+ * All changes go through PRs. Never commit to `main`, never push tags,
341
+ never add AI attribution to commits.
199
342
 
200
343
  == License
201
344
 
@@ -0,0 +1,132 @@
1
+ # Phase 1 — Design decisions
2
+
3
+ Priority: **P0** (canonical — blocks every later phase)
4
+
5
+ ## Goal
6
+
7
+ Lock down the conceptual model before any code lands. Every later
8
+ phase references these decisions; if they shift, downstream work
9
+ reopens.
10
+
11
+ ## Decisions
12
+
13
+ ### D1 — Meeting is the event; Agenda is a document
14
+
15
+ The previous draft conflated these. Split:
16
+
17
+ - **Meeting** — the event itself. Identity, time, venue, officers,
18
+ schedule, deadlines, links to Resolutions.
19
+ - **Agenda** — the business order document produced for the Meeting.
20
+ Carries items, opening/closing sessions, status (draft/final), and a
21
+ pointer to the source PDF.
22
+
23
+ A Meeting *has* an Agenda (`Meeting.agenda: Agenda`). An Agenda is
24
+ meaningless without a Meeting but may be versioned independently
25
+ (draft → final → amended).
26
+
27
+ ### D2 — Schedule ≠ Agenda
28
+
29
+ - **Schedule** — the time-bound timetable: date, time slot, room,
30
+ event title. Lives on `Meeting.schedule: ScheduleItem[]`.
31
+ - **Agenda** — the topic-bound business order: numbered/unnumbered
32
+ items, opening session, closing session. Lives on `Meeting.agenda`.
33
+
34
+ ISO/TC 154's data already separates these (`schedule[]` and
35
+ `agenda.{opening_session, closing_session, wg_meetings}`). Don't
36
+ collapse them.
37
+
38
+ ### D3 — Linking to Resolutions is by URN, not embedding
39
+
40
+ - `Meeting.resolution_refs: String[]` — URNs of resolution
41
+ collections produced by this meeting.
42
+ - `ResolutionCollection.metadata.meeting_urn: String` — back-reference.
43
+
44
+ Rationale: meetings and resolutions have different lifetimes (an
45
+ agenda exists weeks before the meeting; resolutions exist only after
46
+ adoption). Embedding forces re-serialization of the whole meeting when
47
+ one resolution is amended. URN linking matches how both repos already
48
+ work (ISO: `_data/events/` separate from `_data/resolutions/`; OIML:
49
+ the metadata block of a ResolutionCollection already comments its
50
+ meeting URN).
51
+
52
+ ### D4 — Localization parallels the Resolution pattern
53
+
54
+ `Meeting.localizations: MeetingLocalization[]` carries per-language
55
+ `title`, `general_area`, `practical_info`. ISO 639-3 + ISO 15924,
56
+ identical to `Localization` for Resolutions.
57
+
58
+ The schedule itself stays language-agnostic at the structural level
59
+ (date, time, room); localized `event` and `description` text live
60
+ inside `MeetingLocalization`. **v1 decision:** keep schedule
61
+ monolingual on `ScheduleItem` (event/description as plain strings)
62
+ and only localize at the Meeting level. If a future caller needs
63
+ per-schedule-item localization, add `ScheduleItemLocalization`
64
+ then — don't pre-bake it.
65
+
66
+ ### D5 — Reuse existing edoxen types
67
+
68
+ | Concept | Reuse |
69
+ |---|---|
70
+ | Identifier | `StructuredIdentifier` |
71
+ | Source URL | `SourceUrl` (extend with optional `kind` in this work) |
72
+ | Validation error | `Edoxen::ValidationError` |
73
+ | CLI scaffold | `Edoxen::Cli::Batch` |
74
+
75
+ ### D6 — Meeting/Agenda have their own top-level containers
76
+
77
+ - `MeetingCollection` — top-level wrapper for a YAML containing many
78
+ meetings (parallel to `ResolutionCollection`).
79
+ - Single-meeting files are also valid (`Meeting` directly under root,
80
+ detected by `oneOf` in JSON-Schema).
81
+
82
+ ### D7 — Wire-name are snake_case
83
+
84
+ LUTAML uses camelCase notation; YAML wire is snake_case. Same
85
+ convention as the rest of edoxen.
86
+
87
+ ### D8 — All enums closed and synced
88
+
89
+ New enums land in `Edoxen::Enums` and the schema's `$defs/<EnumName>`
90
+ blocks. Both `schema_enum_sync_spec` and `schema_model_sync_spec`
91
+ extend to cover them.
92
+
93
+ ## Type inventory (MECE check)
94
+
95
+ | Concept | Owns |
96
+ |---|---|
97
+ | `Meeting` | the event (identity, time, venue, officers, agenda, schedule) |
98
+ | `MeetingLocalization` | per-language content for a Meeting |
99
+ | `MeetingCollection` | top-level wrapper for many Meetings |
100
+ | `MeetingRelation` | directed link between two Meetings |
101
+ | `Agenda` | the business order document |
102
+ | `AgendaItem` | one entry on an Agenda |
103
+ | `Location` | venue geography |
104
+ | `Person` | identity + contact + affiliation |
105
+ | `HostRef` | typed reference to an organization |
106
+ | `ScheduleItem` | one entry in the timetable |
107
+ | `Deadline` | a time-bound requirement |
108
+ | `DateRange` | start + end pair |
109
+ | `Reference` | generic document reference (used by AgendaItem) |
110
+
111
+ 13 new classes. No overlaps.
112
+
113
+ ## Enums
114
+
115
+ | Enum | Values |
116
+ |---|---|
117
+ | `MEETING_TYPE` | plenary, working_group, task_group, ad_hoc, joint, conference_session |
118
+ | `MEETING_STATUS` | upcoming, completed, cancelled |
119
+ | `AGENDA_STATUS` | draft, final, amended, cancelled, superseded |
120
+ | `AGENDA_ITEM_KIND` | numbered, unnumbered, header, opening, closing |
121
+ | `AGENDA_ITEM_OUTCOME` | discussed, resolved, deferred, adopted, withdrawn |
122
+ | `HOST_TYPE` | national_body, liaison, associate, organizer |
123
+ | `MEETING_RELATION_TYPE` | continues_from, continues_to, joint_with, supersedes, supersedes_by, rescheduled_from, rescheduled_to |
124
+ | `SOURCE_URL_KIND` | agenda_pdf, minutes_pdf, resolutions_pdf, report_pdf, register_url, landing_page |
125
+
126
+ 8 new enums.
127
+
128
+ ## Acceptance criteria
129
+
130
+ - [x] D1–D8 written
131
+ - [x] Type inventory listed
132
+ - [x] Enum inventory listed
@@ -0,0 +1,61 @@
1
+ # Phase 2 — Canonical LUTAML in edoxen-model
2
+
3
+ Priority: **P0**
4
+
5
+ ## Goal
6
+
7
+ Land the canonical LutaML UML definitions in
8
+ `/Users/mulgogi/src/mn/edoxen-model/models/`. The Ruby gem mirrors
9
+ these files 1:1.
10
+
11
+ ## Deliverables — one `.lutaml` per concept
12
+
13
+ ### Top-level
14
+
15
+ - `meeting_collection.lutaml` — top-level wrapper.
16
+ - `meeting.lutaml` — the event class.
17
+ - `meeting_localization.lutaml` — per-language content.
18
+ - `meeting_relation.lutaml` — cross-meeting link.
19
+
20
+ ### Sub-structures
21
+
22
+ - `agenda.lutaml`
23
+ - `agenda_item.lutaml`
24
+ - `schedule_item.lutaml`
25
+ - `deadline.lutaml`
26
+ - `date_range.lutaml`
27
+ - `location.lutaml`
28
+ - `person.lutaml`
29
+ - `host_ref.lutaml`
30
+ - `reference.lutaml`
31
+
32
+ ### Enums (one per file, mirroring existing convention)
33
+
34
+ - `meeting_type.lutaml`
35
+ - `meeting_status.lutaml`
36
+ - `agenda_status.lutaml`
37
+ - `agenda_item_kind.lutaml`
38
+ - `agenda_item_outcome.lutaml`
39
+ - `host_type.lutaml`
40
+ - `meeting_relation_type.lutaml`
41
+ - `source_url_kind.lutaml`
42
+
43
+ ### Removed
44
+
45
+ - `meeting-model.adoc` — superseded; the LUTAML files are now
46
+ authoritative. Move to `meeting-model.adoc.deprecated` rather than
47
+ delete (history preservation).
48
+
49
+ ## Attribute shape per class
50
+
51
+ (See Phase 1 for the canonical field list. Each `.lutaml` declares
52
+ attributes exactly matching the Ruby model in Phase 3 — types are
53
+ LUTAML notation, wire names are snake_case per D7.)
54
+
55
+ ## Acceptance criteria
56
+
57
+ - [ ] 21 new `.lutaml` files in `models/`.
58
+ - [ ] `README.adoc` updated: new "Meeting model" section, file index
59
+ extended.
60
+ - [ ] Old `meeting-model.adoc` renamed to `.deprecated`.
61
+ - [ ] Commit + PR to `edoxen/edoxen-model`.
@@ -0,0 +1,73 @@
1
+ # Phase 3 — Ruby model mirror in edoxen
2
+
3
+ Priority: **P0**
4
+
5
+ ## Goal
6
+
7
+ One Ruby class per LUTAML file. autoload entries in `lib/edoxen.rb`.
8
+ No `require_relative` in library code. No `send` to private, no
9
+ `instance_variable_set`/`get`, no `respond_to?` for typing.
10
+
11
+ ## Deliverables — `lib/edoxen/*.rb`
12
+
13
+ ### New files
14
+
15
+ - `meeting_collection.rb` — top-level wrapper.
16
+ - `meeting.rb` — the event class.
17
+ - `meeting_localization.rb`
18
+ - `meeting_relation.rb`
19
+ - `agenda.rb`
20
+ - `agenda_item.rb`
21
+ - `schedule_item.rb`
22
+ - `deadline.rb`
23
+ - `date_range.rb`
24
+ - `location.rb`
25
+ - `person.rb`
26
+ - `host_ref.rb`
27
+ - `reference.rb`
28
+
29
+ ### Modified files
30
+
31
+ - `lib/edoxen.rb` — add 13 autoload entries.
32
+ - `lib/edoxen/enums.rb` — add 8 new enum constants.
33
+ - `lib/edoxen/resolution_metadata.rb` — add `meeting_urn: String`
34
+ field (back-reference to a Meeting).
35
+ - `lib/edoxen/source_url.rb` — add optional `kind: String, values:
36
+ Enums::SOURCE_URL_KIND` field (orthogonal addition; existing
37
+ fixtures without it still parse).
38
+
39
+ ### Class shape
40
+
41
+ Each class is `Lutaml::Model::Serializable` with:
42
+
43
+ - `attribute :name, Type, **opts` declarations only.
44
+ - No explicit `key_value do; map ...; end` block — lutaml-model
45
+ auto-emits identity maps (established in v0.3.1 refactor).
46
+ - Enum-typed attributes use `values: Enums::FOO`.
47
+ - Convenience accessors added where the lookup is non-trivial
48
+ (parallel to `Resolution#in_language`).
49
+
50
+ ### Convenience accessors
51
+
52
+ - `Meeting#in_language(code, fallback: false)` — exact-match lookup
53
+ on `localizations`.
54
+ - `Meeting#primary_localization` — English-or-first.
55
+ - `Meeting#find_agenda_item(label)` — by label string.
56
+ - `MeetingCollection#find_by_urn(urn)` — URN lookup.
57
+ - `MeetingCollection#find_by_identifier(prefix:, number:)` —
58
+ structured-identifier lookup.
59
+
60
+ ### Invariants
61
+
62
+ - `Meeting.identifier` is `StructuredIdentifier[1..*]` (required by
63
+ schema; permissive in model).
64
+ - `Meeting.localizations[*].language_code` matches `^[a-z]{3}$`
65
+ (schema-enforced).
66
+ - `Meeting.date_range` is a `DateRange` with optional `start`/`end`.
67
+
68
+ ## Acceptance criteria
69
+
70
+ - [ ] 13 new Ruby files, each ≤40 lines.
71
+ - [ ] No `require_relative` in any new file.
72
+ - [ ] `bundle exec rspec` passes (existing 197 examples).
73
+ - [ ] `bundle exec rubocop` clean.
@@ -0,0 +1,34 @@
1
+ # Phase 4 — JSON-Schema
2
+
3
+ Priority: **P0**
4
+
5
+ ## Goal
6
+
7
+ A separate `schema/meeting.yaml` for Meeting/Agenda validation. The
8
+ existing `schema/edoxen.yaml` (Resolutions) is untouched.
9
+
10
+ ## Why a separate schema file
11
+
12
+ - Resolutions and Meetings are different document types with
13
+ different lifecycles.
14
+ - Different CLI subcommands validate each.
15
+ - Avoids touching the existing schema (which is referenced by 1,241
16
+ OIML fixtures via `yaml-language-server: $schema=`).
17
+
18
+ ## Deliverables
19
+
20
+ - `schema/meeting.yaml` — full JSON-Schema (draft-07).
21
+ - Root: `oneOf` — `MeetingCollection` OR `Meeting`.
22
+ - `$defs` for every class + every enum.
23
+ - `additionalProperties: false` everywhere.
24
+ - `required` on identifier + type for Meeting.
25
+ - `pattern` on `language_code` (`^[a-z]{3}$`), `script`
26
+ (`^[A-Z][a-z]{3}$`), `country_code` (`^[A-Z]{2}$`).
27
+ - `minItems: 1` on `Meeting.identifier`.
28
+
29
+ ## Acceptance criteria
30
+
31
+ - [ ] `schema/meeting.yaml` exists and is valid JSON-Schema draft-07.
32
+ - [ ] `Edoxen::SchemaValidator.new("path/to/meeting.yaml")` loads it.
33
+ - [ ] All Phase 5 fixtures validate cleanly.
34
+ - [ ] All Phase 6 specs (including schema enum sync) pass.