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.
- checksums.yaml +4 -4
- data/CLAUDE.md +10 -6
- data/Gemfile +4 -0
- data/README.adoc +181 -38
- data/TODO.meeting-agenda/01-design-decisions.md +132 -0
- data/TODO.meeting-agenda/02-lutaml-canonical.md +61 -0
- data/TODO.meeting-agenda/03-ruby-models.md +73 -0
- data/TODO.meeting-agenda/04-schema.md +34 -0
- data/TODO.meeting-agenda/05-fixtures.md +43 -0
- data/TODO.meeting-agenda/06-specs.md +52 -0
- data/TODO.meeting-agenda/07-cli.md +55 -0
- data/TODO.meeting-agenda/08-docs.md +35 -0
- data/TODO.meeting-agenda/09-verify-and-release.md +41 -0
- data/TODO.meeting-agenda/10-future-enhancements.md +72 -0
- data/TODO.meeting-agenda/README.md +30 -0
- data/edoxen.gemspec +1 -0
- data/lib/edoxen/agenda.rb +26 -0
- data/lib/edoxen/agenda_item.rb +17 -0
- data/lib/edoxen/attendance.rb +23 -0
- data/lib/edoxen/cli.rb +97 -0
- data/lib/edoxen/date_range.rb +11 -0
- data/lib/edoxen/deadline.rb +10 -0
- data/lib/edoxen/enums.rb +31 -0
- data/lib/edoxen/host_ref.rb +14 -0
- data/lib/edoxen/link_checker.rb +85 -0
- data/lib/edoxen/location.rb +15 -0
- data/lib/edoxen/meeting.rb +82 -0
- data/lib/edoxen/meeting_collection.rb +26 -0
- data/lib/edoxen/meeting_collection_metadata.rb +11 -0
- data/lib/edoxen/meeting_localization.rb +15 -0
- data/lib/edoxen/meeting_relation.rb +12 -0
- data/lib/edoxen/minutes.rb +31 -0
- data/lib/edoxen/minutes_section.rb +27 -0
- data/lib/edoxen/person.rb +14 -0
- data/lib/edoxen/reference.rb +13 -0
- data/lib/edoxen/reference_data.rb +95 -0
- data/lib/edoxen/resolution_metadata.rb +14 -0
- data/lib/edoxen/schedule_item.rb +27 -0
- data/lib/edoxen/schedule_item_localization.rb +21 -0
- data/lib/edoxen/source_url.rb +6 -2
- data/lib/edoxen/version.rb +1 -1
- data/lib/edoxen/vote_record.rb +25 -0
- data/lib/edoxen.rb +23 -0
- data/schema/edoxen.yaml +11 -3
- data/schema/meeting.yaml +480 -0
- 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
|
@@ -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
|