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
@@ -0,0 +1,43 @@
1
+ # Phase 5 — Fixtures
2
+
3
+ Priority: **P0** for canonical, **P1** for richer ones.
4
+
5
+ ## Goal
6
+
7
+ Real-world fixture YAMLs in `spec/fixtures/meetings/` that exercise
8
+ every shape the model supports.
9
+
10
+ ## Deliverables
11
+
12
+ ### P0 — required for canonical
13
+
14
+ - `spec/fixtures/meetings/simple-meeting.yaml` — minimal valid
15
+ Meeting (identifier, type, date_range, one venue, one localization,
16
+ one agenda item).
17
+ - `spec/fixtures/meetings/ciml-56-meeting.yaml` — OIML CIML 56th
18
+ meeting: bilingual (EN/FR), structured identifier, source URLs with
19
+ kind, resolution_ref URN linking to a ResolutionCollection.
20
+ - `spec/fixtures/meetings/isotc154-plenary-42-meeting.yaml` — ISO/TC
21
+ 154 42nd plenary: multi-day date_range, multi-venue, typed hosts,
22
+ full schedule with 9 entries, deadlines, chair/secretary.
23
+
24
+ ### P1 — useful coverage
25
+
26
+ - `spec/fixtures/meetings/virtual-meeting.yaml` — virtual meeting
27
+ (no venues, country_code empty).
28
+ - `spec/fixtures/meetings/multi-collection.yaml` — a
29
+ MeetingCollection wrapping multiple meetings.
30
+
31
+ ## Fixture invariants
32
+
33
+ - All fixtures MUST validate against `schema/meeting.yaml`.
34
+ - All fixtures MUST round-trip through the Ruby model
35
+ (`Meeting.from_yaml(...).to_yaml` re-parses identically).
36
+ - Real-world fixtures (ciml-56, isotc154-42) MUST reflect actual
37
+ published data — no fabricated committee names or dates.
38
+
39
+ ## Acceptance criteria
40
+
41
+ - [ ] At least the three P0 fixtures exist.
42
+ - [ ] `bundle exec exe/edoxen validate-meetings "spec/fixtures/meetings/*.yaml"` passes on all of them.
43
+ - [ ] Each fixture is referenced by at least one round-trip spec.
@@ -0,0 +1,52 @@
1
+ # Phase 6 — Specs
2
+
3
+ Priority: **P0**
4
+
5
+ ## Goal
6
+
7
+ Comprehensive spec coverage. One spec file per new model class, plus
8
+ CLI and schema-sync coverage. Real model instances only — no
9
+ `double()`.
10
+
11
+ ## Deliverables — `spec/edoxen/meeting_*_spec.rb` (one per class)
12
+
13
+ For each of the 13 new classes:
14
+
15
+ - Construction with explicit attrs.
16
+ - Round-trip YAML serialization.
17
+ - Enum coverage where applicable (one example per enum value).
18
+ - Real-instance types (e.g., `Meeting#agenda` returns an `Edoxen::Agenda`).
19
+
20
+ ## Deliverables — additional specs
21
+
22
+ - `spec/edoxen/meeting_collection_spec.rb` — top-level container,
23
+ round-trips, lookup helpers (`find_by_urn`, `find_by_identifier`).
24
+ - `spec/edoxen/meeting_lookup_spec.rb` — `Meeting#in_language`,
25
+ `Meeting#primary_localization`, `Meeting#find_agenda_item`.
26
+ - `spec/edoxen/schema_meeting_enum_sync_spec.rb` — every enum in
27
+ `schema/meeting.yaml` `$defs` matches the corresponding
28
+ `Edoxen::Enums::*` frozen array.
29
+ - `spec/edoxen/schema_meeting_model_sync_spec.rb` — every Ruby
30
+ Meeting-related class has matching properties/collection flags in
31
+ `schema/meeting.yaml` `$defs`.
32
+ - `spec/edoxen/cli_meetings_spec.rb` — `validate-meetings` and
33
+ `normalize-meetings` subcommands, spawned via `Open3.capture3`
34
+ against the new fixtures.
35
+ - `spec/edoxen/schema_validator_meeting_spec.rb` — SchemaValidator
36
+ constructed with the meeting schema path; happy path + each error
37
+ type (additionalProperties, required, enum, pattern, type).
38
+
39
+ ## Style rules
40
+
41
+ - Real model instances only — no `double()`.
42
+ - Each example ≤ 10 lines.
43
+ - Round-trip examples cover every fixture.
44
+
45
+ ## Acceptance criteria
46
+
47
+ - [ ] One spec file per new class (13 files).
48
+ - [ ] +5 additional specs (collection, lookup, schema enum sync,
49
+ schema model sync, CLI).
50
+ - [ ] +1 schema_validator spec for the meeting schema path.
51
+ - [ ] `bundle exec rspec` — 0 failures (existing 197 + new ≈ 80+).
52
+ - [ ] `bundle exec rubocop` clean.
@@ -0,0 +1,55 @@
1
+ # Phase 7 — CLI integration
2
+
3
+ Priority: **P1**
4
+
5
+ ## Goal
6
+
7
+ Add `validate-meetings` and `normalize-meetings` subcommands. Reuse
8
+ the existing `Edoxen::Cli::Batch` deep module — no scaffold
9
+ duplication.
10
+
11
+ ## Deliverables
12
+
13
+ - `lib/edoxen/cli.rb` — two new subcommands:
14
+
15
+ ```ruby
16
+ desc "validate-meetings YAML_FILE_PATTERN",
17
+ "Validate Meeting/Agenda YAML file(s) against schema/meeting.yaml."
18
+
19
+ def validate_meetings(pattern)
20
+ validator = SchemaValidator.new(meeting_schema_path)
21
+ Batch.run(self, pattern, header: "🔍 Validating meetings") do |file|
22
+ errors = validator.validate_file(file) + collect_meeting_model_errors(file)
23
+ errors.empty? ? Batch::Result.ok("VALID") : Batch::Result.bad(errors.map(&:to_clickable_format))
24
+ end
25
+ end
26
+
27
+ desc "normalize-meetings YAML_FILE_PATTERN",
28
+ "Round-trip Meeting YAML file(s) through the model (--output DIR | --inplace)."
29
+
30
+ option :output, type: :string
31
+ option :inplace, type: :boolean
32
+
33
+ def normalize_meetings(pattern)
34
+ # Same shape as normalize — uses Batch.run
35
+ end
36
+ ```
37
+
38
+ - `lib/edoxen/cli.rb` private: `meeting_schema_path`,
39
+ `collect_meeting_model_errors`.
40
+
41
+ ## Why separate subcommands
42
+
43
+ - Different root schema, different model parser.
44
+ - `validate` (existing) is for ResolutionCollection; backward
45
+ compatibility preserved.
46
+ - Future: a `validate-all` that auto-detects via `oneOf` if needed.
47
+
48
+ ## Acceptance criteria
49
+
50
+ - [ ] `edoxen validate-meetings PATTERN` exits 0 on valid fixtures.
51
+ - [ ] `edoxen validate-meetings PATTERN` exits non-zero + lists
52
+ errors on invalid input.
53
+ - [ ] `edoxen normalize-meetings PATTERN --output DIR` round-trips.
54
+ - [ ] Existing `validate` and `normalize` (for Resolutions) unchanged.
55
+ - [ ] `edoxen help` lists both pairs of commands.
@@ -0,0 +1,35 @@
1
+ # Phase 8 — Documentation
2
+
3
+ Priority: **P1**
4
+
5
+ ## Goal
6
+
7
+ README + CLAUDE.md reflect the Meeting/Agenda model. The canonical
8
+ edoxen-model README documents the new LUTAML files.
9
+
10
+ ## Deliverables — edoxen gem
11
+
12
+ - `README.adoc`:
13
+ - New "Meeting & Agenda model" section with ASCII tree.
14
+ - Quick-start example showing both Resolution and Meeting parsing.
15
+ - Cross-link to the canonical LUTAML files.
16
+ - `CLAUDE.md`:
17
+ - Architecture section mentions `Meeting`, `Agenda`, the meeting
18
+ schema, and the separate CLI subcommands.
19
+ - Audit checklist extended: schema_model_sync_spec covers both
20
+ `schema/edoxen.yaml` and `schema/meeting.yaml`.
21
+
22
+ ## Deliverables — edoxen-model
23
+
24
+ - `README.adoc`:
25
+ - File index extended with all new `.lutaml` files.
26
+ - New "Meeting model" section explaining the Meeting/Agenda split,
27
+ URN linking to Resolutions, and the localization pattern.
28
+ - Sample meeting YAML mirroring one of the gem's fixtures.
29
+
30
+ ## Acceptance criteria
31
+
32
+ - [ ] Both READMEs updated.
33
+ - [ ] edoxen CLAUDE.md updated.
34
+ - [ ] Sample YAML in edoxen-model README would pass
35
+ `edoxen validate-meetings`.
@@ -0,0 +1,41 @@
1
+ # Phase 9 — Verify, PR, merge, release
2
+
3
+ Priority: **P0**
4
+
5
+ ## Goal
6
+
7
+ Land everything via PRs. Verify GHA passes. Merge. Bump edoxen minor
8
+ (new feature) and release.
9
+
10
+ ## Branch / PR plan
11
+
12
+ | Repo | Branch | PRs against |
13
+ |---|---|---|
14
+ | `edoxen/edoxen-model` | `feat/meeting-agenda-model` | main |
15
+ | `edoxen/edoxen` | `feat/meeting-agenda-model` | main |
16
+
17
+ Order: edoxen-model first (canonical), then edoxen (mirror).
18
+
19
+ ## Verification gates
20
+
21
+ Per branch, before pushing:
22
+
23
+ - [ ] `bundle exec rspec` — 0 failures.
24
+ - [ ] `bundle exec rubocop` — 0 offenses.
25
+ - [ ] `bundle exec exe/edoxen validate-meetings "spec/fixtures/meetings/*.yaml"` — clean.
26
+ - [ ] `bundle exec exe/edoxen normalize-meetings --output /tmp/out` — round-trips.
27
+ - [ ] No AI attribution in any commit.
28
+ - [ ] No TODOs / FIXMEs in committed source.
29
+ - [ ] No transient files (.DS_Store, .bak, .swp).
30
+
31
+ ## Post-merge
32
+
33
+ - [ ] Trigger `gh workflow run release.yml --ref main -f next_version=minor`
34
+ on `edoxen/edoxen`.
35
+ - [ ] Confirm new version appears on rubygems.org.
36
+ - [ ] Confirm tag `vX.Y.Z` pushed.
37
+
38
+ ## Why minor (not patch)
39
+
40
+ The new Meeting/Agenda model is an additive feature. No breaking
41
+ change to existing Resolution APIs. Semver minor on the 0.x line.
@@ -0,0 +1,72 @@
1
+ # Phase 10 — Future enhancements (not in this release)
2
+
3
+ Priority: **P2**
4
+
5
+ These are documented as known-future-work; not executed in this pass.
6
+ Each one would be its own PR.
7
+
8
+ ## P2.1 — Per-schedule-item localization
9
+
10
+ `ScheduleItemLocalization` carrying per-language `event` and
11
+ `description`. Triggered when a real consumer needs WG meeting titles
12
+ in FR alongside EN.
13
+
14
+ ## P2.2 — Meeting series
15
+
16
+ `MeetingSeries` carrying `{identifier, name, description}` with
17
+ `Meeting.series_ref: String` (URN). Triggered when navigation across
18
+ ordinals becomes a real use case (ISO/TC 154 plenary history page).
19
+
20
+ ## P2.3 — Contribution model
21
+
22
+ Replace flat `Person.contributors` with a `Contribution` class
23
+ matching the comment in the original draft (`'contributors should
24
+ defer to Contribution model`). Triggered when we need to distinguish
25
+ "presented" vs "authored" vs "reviewed" contributions per meeting.
26
+
27
+ ## P2.4 — Auto-detect document type in CLI
28
+
29
+ A single `edoxen validate PATTERN` that auto-detects
30
+ ResolutionCollection vs Meeting via JSON-Schema `oneOf`. Triggered
31
+ when users complain about remembering two subcommand names.
32
+
33
+ ## P2.5 — Cross-document link validation
34
+
35
+ A spec/assertion that every `Meeting.resolution_refs[*]` URN has a
36
+ matching `ResolutionCollection` somewhere in the data set, and vice
37
+ versa. Triggered when broken links cause real bugs.
38
+
39
+ ## P2.6 — Attendance roster
40
+
41
+ `Meeting.attendance: Attendance[]` carrying per-person attendance
42
+ records (present/absent/apologies + affiliation). Triggered when a
43
+ consumer needs the full attendance record, not just officers.
44
+
45
+ ## P2.7 — Vote records
46
+
47
+ `VoteRecord { resolution_ref, person, vote: VoteType }` linked to a
48
+ Meeting. Triggered when ballot-by-ballot transparency becomes a
49
+ requirement.
50
+
51
+ ## P2.8 — Minutes narrative
52
+
53
+ A `Minutes` class distinct from `Agenda`, capturing the actual
54
+ narrative of what was said (linked Markdown / Asciidoc blocks per
55
+ agenda item). Triggered when the OIML `meetings/` directory starts
56
+ receiving Bulletin scans.
57
+
58
+ ## P2.9 — federation (multi-body)
59
+
60
+ A `Federation` or `JointMeeting` class for meetings organized by
61
+ multiple bodies (e.g., a JWG with TC 154 + TC 307). Triggered when
62
+ real joint-meeting data lands.
63
+
64
+ ## P2.10 — IANA / ISO 3166-1 / ISO 639-3 / ISO 15924 reference data
65
+
66
+ Built-in lookup tables to validate `country_code`, `language_code`,
67
+ `script` against the actual standard. Triggered when typo-rate
68
+ becomes painful.
69
+
70
+ ## Acceptance criteria
71
+
72
+ This file documents intent only; nothing to land in this release.
@@ -0,0 +1,30 @@
1
+ # TODO.meeting-agenda — index
2
+
3
+ The work to land a canonical Meeting + Agenda + AgendaItem model
4
+ linkable to Resolutions/Decisions across `edoxen` and `edoxen-model`.
5
+
6
+ ## Phases
7
+
8
+ | # | File | Priority | Status |
9
+ |---|---|---|---|
10
+ | 1 | [01-design-decisions.md](01-design-decisions.md) | P0 | done |
11
+ | 2 | [02-lutaml-canonical.md](02-lutaml-canonical.md) | P0 | pending |
12
+ | 3 | [03-ruby-models.md](03-ruby-models.md) | P0 | pending |
13
+ | 4 | [04-schema.md](04-schema.md) | P0 | pending |
14
+ | 5 | [05-fixtures.md](05-fixtures.md) | P0 | pending |
15
+ | 6 | [06-specs.md](06-specs.md) | P0 | pending |
16
+ | 7 | [07-cli.md](07-cli.md) | P1 | pending |
17
+ | 8 | [08-docs.md](08-docs.md) | P1 | pending |
18
+ | 9 | [09-verify-and-release.md](09-verify-and-release.md) | P0 | pending |
19
+ | 10 | [10-future-enhancements.md](10-future-enhancements.md) | P2 | documented |
20
+
21
+ ## Execution order
22
+
23
+ Phases 2 → 9 are executed in order. Phase 10 documents known-future-
24
+ work only.
25
+
26
+ ## Branches
27
+
28
+ - `edoxen/edoxen-model` ← `feat/meeting-agenda-model` (Phase 2)
29
+ - `edoxen/edoxen` ← `feat/meeting-agenda-model` (Phases 3–8)
30
+ - Phase 9 merges both, then releases `edoxen` as a minor bump.
data/edoxen.gemspec CHANGED
@@ -39,4 +39,5 @@ Gem::Specification.new do |spec|
39
39
  spec.add_dependency "json_schemer", "~> 2.0"
40
40
  spec.add_dependency "lutaml-model", "~> 0.7"
41
41
  spec.add_dependency "thor", "~> 1.0"
42
+ spec.add_dependency "unlocodes", "~> 0.1"
42
43
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Edoxen
4
+ # The business-order document of a Meeting. Distinct from the
5
+ # timetable (Schedule) — the Agenda orders topics; the Schedule
6
+ # orders time slots.
7
+ #
8
+ # An Agenda may be versioned independently of the Meeting: a draft
9
+ # agenda circulates weeks before; a final agenda at meeting time;
10
+ # an amended agenda if items are added during the meeting. The
11
+ # `status` field captures that lifecycle.
12
+ class Agenda < Lutaml::Model::Serializable
13
+ attribute :identifier, StructuredIdentifier, collection: true
14
+ attribute :status, :string, values: Enums::AGENDA_STATUS
15
+ attribute :source_doc, :string
16
+ attribute :items, AgendaItem, collection: true
17
+ attribute :opening_session, ScheduleItem
18
+ attribute :closing_session, ScheduleItem
19
+
20
+ # Find an agenda item by its label (e.g., "5.2"). Returns nil
21
+ # when no item matches.
22
+ def find_item(label)
23
+ items&.find { |item| item.label == label.to_s }
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Edoxen
4
+ # One entry on an Agenda. `label` is the visible identifier (e.g.,
5
+ # "5.2"); `kind` discriminates numbered/unnumbered/header/opening/
6
+ # closing; `outcome` records what happened; `resolution_ref`
7
+ # optionally links to the URN of the resolution this item produced.
8
+ class AgendaItem < Lutaml::Model::Serializable
9
+ attribute :label, :string
10
+ attribute :kind, :string, values: Enums::AGENDA_ITEM_KIND
11
+ attribute :title, :string
12
+ attribute :description, :string
13
+ attribute :references, Reference, collection: true
14
+ attribute :outcome, :string, values: Enums::AGENDA_ITEM_OUTCOME
15
+ attribute :resolution_ref, :string
16
+ end
17
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Edoxen
4
+ # One attendance record per person. Mirrors the ParticipationStatus
5
+ # enum from edoxen-model/models/participation_status.lutaml.
6
+ # Optional `affiliation` and `notes` fields let callers annotate
7
+ # proxy representatives and any meeting-specific details.
8
+ class Attendance < Lutaml::Model::Serializable
9
+ attribute :person, Person
10
+ attribute :status, :string, values: Enums::PARTICIPATION_STATUS
11
+ attribute :affiliation, :string
12
+ attribute :proxy_for, Person
13
+ attribute :notes, :string
14
+
15
+ key_value do
16
+ map "person", to: :person
17
+ map "status", to: :status
18
+ map "affiliation", to: :affiliation
19
+ map "proxy_for", to: :proxy_for
20
+ map "notes", to: :notes
21
+ end
22
+ end
23
+ end
data/lib/edoxen/cli.rb CHANGED
@@ -125,8 +125,71 @@ module Edoxen
125
125
  end
126
126
  end
127
127
 
128
+ desc "validate-meetings YAML_FILE_PATTERN",
129
+ "Validate Meeting/Agenda YAML file(s) against schema/meeting.yaml."
130
+
131
+ def validate_meetings(pattern)
132
+ validator = SchemaValidator.new(meeting_schema_path)
133
+ Batch.run(self, pattern, header: "🔍 Validating meetings") do |file|
134
+ schema_errors = validator.validate_file(file)
135
+ model_errors = collect_meeting_model_errors(file)
136
+ if schema_errors.empty? && model_errors.empty?
137
+ Batch::Result.ok("VALID")
138
+ else
139
+ errors = (schema_errors + model_errors).map(&:to_clickable_format)
140
+ Batch::Result.bad(errors)
141
+ end
142
+ end
143
+ end
144
+
145
+ desc "normalize-meetings YAML_FILE_PATTERN",
146
+ "Round-trip Meeting YAML file(s) through the model (--output DIR or --inplace)."
147
+
148
+ option :output, type: :string, desc: "Output directory for normalized files"
149
+ option :inplace, type: :boolean, desc: "Modify files in place (no backup)"
150
+
151
+ def normalize_meetings(pattern)
152
+ unless valid_normalize_options?
153
+ say normalize_options_error, :red
154
+ exit 1
155
+ end
156
+
157
+ summary_extra = [
158
+ [" Output directory", options[:output]],
159
+ [" Mode", options[:inplace] ? "in place" : "--output"]
160
+ ].compact
161
+
162
+ Batch.run(self, pattern, header: "🔄 Normalizing meetings", summary_extra: summary_extra) do |file|
163
+ Batch::Result.ok(normalize_meeting_file(file))
164
+ rescue StandardError => e
165
+ Batch::Result.bad(["#{file}:1:1: #{e.message}"])
166
+ end
167
+ end
168
+
169
+ desc "unlocode CODE", "Resolve a UN/LOCODE via the canonical registry."
170
+ def unlocode(code)
171
+ entry = Edoxen::ReferenceData.find_unlocode(code)
172
+ if entry.nil?
173
+ say "No entry for #{code.upcase} in the UN/LOCODE registry.", :red
174
+ exit 1
175
+ end
176
+
177
+ say "UN/LOCODE: #{entry.code}", :blue
178
+ say " Name: #{entry.name}"
179
+ say " Country: #{entry.country}"
180
+ say " Subdiv: #{entry.subdivision}" if entry.subdivision
181
+ coords = entry.coordinates
182
+ say " Coords: #{coords}" if coords
183
+ funcs = entry.functions.compact.map(&:to_s)
184
+ say " Functions: #{funcs.join(", ")}" unless funcs.empty?
185
+ end
186
+
128
187
  private
129
188
 
189
+ def meeting_schema_path
190
+ File.expand_path("../../schema/meeting.yaml", __dir__)
191
+ end
192
+
130
193
  def collect_model_errors(file)
131
194
  ResolutionCollection.from_yaml(File.read(file))
132
195
  []
@@ -138,6 +201,30 @@ module Edoxen
138
201
  )]
139
202
  end
140
203
 
204
+ def collect_meeting_model_errors(file)
205
+ load_meeting_document(file)
206
+ []
207
+ rescue StandardError => e
208
+ [Edoxen::ValidationError.new(
209
+ file: file, line: 1, column: 1,
210
+ message_text: "Meeting model parsing failed: #{e.message}",
211
+ source: Edoxen::ValidationError::SOURCE_MODEL
212
+ )]
213
+ end
214
+
215
+ # A meeting YAML may be either a single Meeting or a
216
+ # MeetingCollection. Detect by the presence of a top-level
217
+ # `meetings:` key and parse accordingly. Returns whichever
218
+ # model object was successfully constructed.
219
+ def load_meeting_document(file)
220
+ data = YAML.safe_load(File.read(file), permitted_classes: [Date, Time])
221
+ if data.is_a?(Hash) && data.key?("meetings")
222
+ MeetingCollection.from_yaml(File.read(file))
223
+ else
224
+ Meeting.from_yaml(File.read(file))
225
+ end
226
+ end
227
+
141
228
  def extract_yaml_language_server_comment(content)
142
229
  lines = content.split("\n").first(5)
143
230
  lines.find { |l| l.strip.match?(/\A#\s*yaml-language-server:\s*\$schema=/) }&.rstrip
@@ -165,6 +252,16 @@ module Edoxen
165
252
  original = File.read(file)
166
253
  yaml_language_server_comment = extract_yaml_language_server_comment(original)
167
254
  normalized = ResolutionCollection.from_yaml(original).to_yaml
255
+ write_normalized(file, normalized, yaml_language_server_comment)
256
+ end
257
+
258
+ def normalize_meeting_file(file)
259
+ yaml_language_server_comment = extract_yaml_language_server_comment(File.read(file))
260
+ normalized = load_meeting_document(file).to_yaml
261
+ write_normalized(file, normalized, yaml_language_server_comment)
262
+ end
263
+
264
+ def write_normalized(file, normalized, yaml_language_server_comment)
168
265
  normalized = "#{yaml_language_server_comment}\n#{normalized}" if yaml_language_server_comment
169
266
 
170
267
  if options[:inplace]
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Edoxen
4
+ # Start + end pair for multi-day meetings. Both endpoints are Date.
5
+ # Either may be nil for one-sided ranges (theoretically), though the
6
+ # schema enforces both required when DateRange is present.
7
+ class DateRange < Lutaml::Model::Serializable
8
+ attribute :start, :date
9
+ attribute :end, :date
10
+ end
11
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Edoxen
4
+ # A time-bound requirement leading up to (or following) a meeting —
5
+ # registration cutoff, report-submission deadline, etc.
6
+ class Deadline < Lutaml::Model::Serializable
7
+ attribute :date, :date
8
+ attribute :description, :string
9
+ end
10
+ end
data/lib/edoxen/enums.rb CHANGED
@@ -39,5 +39,36 @@ module Edoxen
39
39
  APPROVAL_DEGREE = %w[unanimous majority minority].freeze
40
40
 
41
41
  URL_KIND = %w[access report].freeze
42
+
43
+ # --- Meeting/Agenda side ----------------------------------------------
44
+ # Mirrors edoxen-model/models/{meeting,agenda,...}_*.lutaml.
45
+ # schema/meeting.yaml references these via $defs/<EnumName>.enum.
46
+
47
+ MEETING_TYPE = %w[plenary working_group task_group ad_hoc joint conference_session].freeze
48
+
49
+ MEETING_STATUS = %w[upcoming completed cancelled].freeze
50
+
51
+ AGENDA_STATUS = %w[draft final amended cancelled superseded].freeze
52
+
53
+ AGENDA_ITEM_KIND = %w[numbered unnumbered header opening closing].freeze
54
+
55
+ AGENDA_ITEM_OUTCOME = %w[discussed resolved deferred adopted withdrawn].freeze
56
+
57
+ HOST_TYPE = %w[national_body liaison associate organizer].freeze
58
+
59
+ MEETING_RELATION_TYPE = %w[
60
+ continues_from continues_to joint_with supersedes superseded_by rescheduled_from rescheduled_to
61
+ ].freeze
62
+
63
+ SOURCE_URL_KIND = %w[
64
+ agenda_pdf minutes_pdf resolutions_pdf report_pdf register_url landing_page
65
+ ].freeze
66
+
67
+ # P2.6 / P2.7 — Attendance and Vote records.
68
+ PARTICIPATION_STATUS = %w[present absent apologies observer excused].freeze
69
+
70
+ VOTE_TYPE = %w[
71
+ affirmative negative abstain absent not_applicable
72
+ ].freeze
42
73
  end
43
74
  end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Edoxen
4
+ # Typed reference to an organization that hosts or co-organizes a
5
+ # meeting. `ref` is a stable identifier into an external registry
6
+ # (national_bodies.yml, liaisons.yml, associates.yml in ISO/TC 154);
7
+ # `type` discriminates which registry.
8
+ class HostRef < Lutaml::Model::Serializable
9
+ attribute :ref, :string
10
+ attribute :type, :string, values: Enums::HOST_TYPE
11
+ attribute :role, :string
12
+ attribute :contact, Person
13
+ end
14
+ end