edoxen 0.5.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f82e1428e5307aac53edfd1f1cdcdac401b7bae76b03ab102acf0a0a42da5382
4
- data.tar.gz: 13a900902194b1420fb9fe0f9b4fd7382c30f0803770f399c58f3b5053bf763d
3
+ metadata.gz: 69a86986a86602665699b5598622d32a63658c302737587f68b7a9b095a471e1
4
+ data.tar.gz: 1201e014ac06c2e4accb00dfd00660f577f6f6464f5d7990553714b796af8314
5
5
  SHA512:
6
- metadata.gz: a7e099bea2ef576a990544b0d68f527c52e99e617f8f16122f798d16638acc671468859f987d25ca2391fbd62da645e38a3e13c324127cb74a9287cf1e650cdd
7
- data.tar.gz: b80c4a49ffc97adacf9871a49de120fad0725c871a2f4a4a4dd31ced9d68fa4ffb4f54c5bd6d5550eefa54b5cbbdd83f779dcc434c366a0cd9740ab6cba63376
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
 
@@ -228,7 +232,7 @@ MeetingCollection
228
232
  ├── date_range: DateRange {start, end}
229
233
  ├── committee, committee_group
230
234
  ├── venues: Location[] (multi-venue supported)
231
- ├── general_area, city (IATA), country_code (ISO 3166-1), virtual
235
+ ├── general_area, city (UN/LOCODE, e.g. FRPAR), country_code (ISO 3166-1), virtual
232
236
  ├── chair, secretary: Person
233
237
  ├── host, hosts: HostRef[] (typed: national_body/liaison/associate/organizer)
234
238
  ├── source_urls: SourceUrl[] (with `kind` discriminator)
@@ -58,4 +58,4 @@ LUTAML notation, wire names are snake_case per D7.)
58
58
  - [ ] `README.adoc` updated: new "Meeting model" section, file index
59
59
  extended.
60
60
  - [ ] Old `meeting-model.adoc` renamed to `.deprecated`.
61
- - [ ] Commit + PR to `metanorma/edoxen-model`.
61
+ - [ ] Commit + PR to `edoxen/edoxen-model`.
@@ -11,8 +11,8 @@ Land everything via PRs. Verify GHA passes. Merge. Bump edoxen minor
11
11
 
12
12
  | Repo | Branch | PRs against |
13
13
  |---|---|---|
14
- | `metanorma/edoxen-model` | `feat/meeting-agenda-model` | main |
15
- | `metanorma/edoxen` | `feat/meeting-agenda-model` | main |
14
+ | `edoxen/edoxen-model` | `feat/meeting-agenda-model` | main |
15
+ | `edoxen/edoxen` | `feat/meeting-agenda-model` | main |
16
16
 
17
17
  Order: edoxen-model first (canonical), then edoxen (mirror).
18
18
 
@@ -31,7 +31,7 @@ Per branch, before pushing:
31
31
  ## Post-merge
32
32
 
33
33
  - [ ] Trigger `gh workflow run release.yml --ref main -f next_version=minor`
34
- on `metanorma/edoxen`.
34
+ on `edoxen/edoxen`.
35
35
  - [ ] Confirm new version appears on rubygems.org.
36
36
  - [ ] Confirm tag `vX.Y.Z` pushed.
37
37
 
@@ -25,6 +25,6 @@ work only.
25
25
 
26
26
  ## Branches
27
27
 
28
- - `metanorma/edoxen-model` ← `feat/meeting-agenda-model` (Phase 2)
29
- - `metanorma/edoxen` ← `feat/meeting-agenda-model` (Phases 3–8)
28
+ - `edoxen/edoxen-model` ← `feat/meeting-agenda-model` (Phase 2)
29
+ - `edoxen/edoxen` ← `feat/meeting-agenda-model` (Phases 3–8)
30
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,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
@@ -166,6 +166,24 @@ module Edoxen
166
166
  end
167
167
  end
168
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
+
169
187
  private
170
188
 
171
189
  def meeting_schema_path
data/lib/edoxen/enums.rb CHANGED
@@ -63,5 +63,12 @@ module Edoxen
63
63
  SOURCE_URL_KIND = %w[
64
64
  agenda_pdf minutes_pdf resolutions_pdf report_pdf register_url landing_page
65
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
66
73
  end
67
74
  end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Edoxen
4
+ # Walks a directory tree of Meeting and ResolutionCollection YAML
5
+ # files and asserts that every cross-document URN link resolves.
6
+ #
7
+ # Invariants enforced:
8
+ #
9
+ # * Every Meeting#urn (if set) is referenced by exactly one
10
+ # ResolutionCollection#metadata.meeting_urn (if the collection
11
+ # exists in the scanned directory).
12
+ # * Every ResolutionCollection#metadata.meeting_urn (if set)
13
+ # resolves to a Meeting whose urn matches.
14
+ #
15
+ # Use from CLI / scripts via the public `check` class method. Returns
16
+ # an array of `LinkError` records (empty = ok).
17
+ #
18
+ # All file IO is read-only. No edits to the YAMLs.
19
+ class LinkChecker
20
+ LinkError = Struct.new(:file, :pointer, :urn, :kind) do
21
+ def message
22
+ "#{file}: #{pointer} references #{urn} (#{kind})"
23
+ end
24
+ end
25
+
26
+ # Walk `dir` (recursive scan of *.yaml and *.yml), classify each
27
+ # file as Meeting or ResolutionCollection, and assert every
28
+ # cross-document URN resolves to a real record.
29
+ #
30
+ # @return [Array<LinkError>] empty when all links resolve.
31
+ def self.check(dir)
32
+ new(dir).check
33
+ end
34
+
35
+ def initialize(dir)
36
+ @dir = dir
37
+ @meetings_by_urn = {} # urn → file
38
+ @collections_by_meeting_urn = {} # meeting_urn → file
39
+ end
40
+
41
+ def check
42
+ require "yaml"
43
+
44
+ Dir.glob(File.join(@dir, "**", "*.{yaml,yml}")).sort.each do |file|
45
+ data = safe_load_yaml(file)
46
+ next unless data.is_a?(Hash)
47
+
48
+ if meeting_shape?(data)
49
+ urn = data["urn"]
50
+ @meetings_by_urn[urn] = file if urn
51
+ elsif collection_shape?(data)
52
+ meeting_urn = data.dig("metadata", "meeting_urn")
53
+ @collections_by_meeting_urn[meeting_urn] = file if meeting_urn
54
+ end
55
+ end
56
+
57
+ errors = []
58
+
59
+ # ResolutionCollection → Meeting
60
+ @collections_by_meeting_urn.each do |meeting_urn, file|
61
+ next if @meetings_by_urn.key?(meeting_urn)
62
+
63
+ errors << LinkError.new(file, "metadata.meeting_urn", meeting_urn, "no matching Meeting")
64
+ end
65
+
66
+ errors
67
+ end
68
+
69
+ private
70
+
71
+ def safe_load_yaml(file)
72
+ YAML.safe_load(File.read(file))
73
+ rescue Psych::SyntaxError, ArgumentError
74
+ nil
75
+ end
76
+
77
+ def meeting_shape?(data)
78
+ data["identifier"].is_a?(Array) && data.key?("type")
79
+ end
80
+
81
+ def collection_shape?(data)
82
+ data["resolutions"].is_a?(Array) || data["metadata"].is_a?(Hash)
83
+ end
84
+ end
85
+ end
@@ -42,6 +42,10 @@ module Edoxen
42
42
  attribute :schedule, ScheduleItem, collection: true
43
43
  attribute :deadlines, Deadline, collection: true
44
44
 
45
+ attribute :attendance, Attendance, collection: true
46
+ attribute :vote_records, VoteRecord, collection: true
47
+ attribute :minutes, Minutes, collection: true
48
+
45
49
  attribute :localizations, MeetingLocalization, collection: true
46
50
  attribute :relations, MeetingRelation, collection: true
47
51
  attribute :resolution_refs, :string, collection: true
@@ -64,5 +68,15 @@ module Edoxen
64
68
  def find_agenda_item(label)
65
69
  agenda&.find_item(label)
66
70
  end
71
+
72
+ # Resolve the meeting's UN/LOCODE via the canonical `unlocode` gem
73
+ # registry. Returns an `Unlocodes::Entry` (with `#name`, `#country`,
74
+ # `#coordinates`, `#iata`, `#functions`, etc.) or nil when the
75
+ # city field is empty / not in the registry.
76
+ def city_entry
77
+ return nil if city.nil? || city.to_s.empty?
78
+
79
+ Edoxen::ReferenceData.find_unlocode(city)
80
+ end
67
81
  end
68
82
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Edoxen
4
+ # The narrative record of a Meeting — what was said, by whom, in
5
+ # what order, with what outcome. Mirrors
6
+ # edoxen-model/models/minutes.lutaml (P2.8 from
7
+ # TODO.meeting-agenda/10-future-enhancements.md).
8
+ #
9
+ # Distinct from Agenda (the business order drafted before the
10
+ # meeting) and from ResolutionCollection (the formal decisions
11
+ # adopted). The Minutes are the running record.
12
+ #
13
+ # Sections are typically keyed by agenda-item number so consumers
14
+ # can join Minutes ↔ AgendaItem ↔ Resolution by `number`/`label`.
15
+ class Minutes < Lutaml::Model::Serializable
16
+ attribute :identifier, StructuredIdentifier, collection: true
17
+ attribute :urn, :string
18
+ attribute :language_code, :string
19
+ attribute :script, :string
20
+ attribute :source_doc, :string
21
+ attribute :source_pages, :string
22
+ attribute :sections, MinutesSection, collection: true
23
+
24
+ def find_section(number)
25
+ return nil if number.nil?
26
+
27
+ target = number.to_s
28
+ sections&.find { |s| s.number.to_s == target }
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Edoxen
4
+ # One section of a Meeting's minutes — typically tied to an agenda
5
+ # item by its `number` field. Carries the narrative as Markdown
6
+ # (the format the GLM-OCR pipeline emits) plus optional page range
7
+ # for provenance back to the source PDF.
8
+ class MinutesSection < Lutaml::Model::Serializable
9
+ attribute :number, :string
10
+ attribute :title, :string
11
+ attribute :narrative, :string
12
+ attribute :page_start, :integer
13
+ attribute :page_end, :integer
14
+ attribute :references, Reference, collection: true
15
+
16
+ def in_language(code, fallback: false)
17
+ match = localizations&.find { |loc| loc.language_code == code.to_s }
18
+ return match if match
19
+
20
+ fallback ? localizations&.first : nil
21
+ end
22
+
23
+ def primary_localization
24
+ in_language("eng", fallback: true)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Edoxen
4
+ # Built-in reference data for ISO codes used across the Edoxen model:
5
+ # ISO 3166-1 alpha-2 country codes, ISO 639-3 language codes, ISO
6
+ # 15924 script codes, and UN/LOCODEs.
7
+ #
8
+ # UN/LOCODE lookups delegate to the `unlocode` gem (the canonical
9
+ # Ruby registry of the UNECE UN/LOCODE dataset). The frozen
10
+ # ReferenceData::UNLOCODES list stays as a curated offline subset
11
+ # — used by specs and consumers that don't want to load the full
12
+ # ~100k-entry registry.
13
+ #
14
+ # Reference data sources:
15
+ # * https://www.iso.org/iso-3166-country-codes.html
16
+ # * https://iso639-3.sil.org/
17
+ # * https://unicode.org/iso15924/
18
+ # * https://unece.org/trade/cefact/unlocode-code-list-country-and-territory
19
+ #
20
+ # UN/LOCODE format: 2-letter ISO 3166-1 country code + 3-character
21
+ # location alphanumeric (e.g., FRPAR = Paris, France; HKHKG = Hong
22
+ # Kong; THCNM = Chiang Mai). UN-maintained, supersedes the
23
+ # airport-centric IATA city codes edoxen previously used.
24
+ module ReferenceData
25
+ # Hard require the unlocode gem. It's a declared runtime dep, so a
26
+ # clean install of edoxen brings it in. The bundled dataset ships
27
+ # 4 sample entries; full coverage requires `rake unlocodes:fetch`
28
+ # from the unlocode gem.
29
+ require "unlocodes"
30
+
31
+ # ISO 3166-1 alpha-2 — countries ISO/TC 154 / OIML / BIPM operate in.
32
+ # Two-letter uppercase.
33
+ COUNTRY_CODES = %w[
34
+ AE AF AR AT AU BE BG BR BY CA CH CL CN CO CY CZ DE DK EE ES FI FR
35
+ GB GR HK HR HU ID IE IL IN IS IT JP KE KR LK LT LU LV MT MX MY NL NO PH
36
+ NZ PL PT RO RS RU SA SE SG SI SK TH TN TR TW UA US VN ZA
37
+ ].freeze
38
+
39
+ # ISO 639-3 — languages actually used in the edoxen fixtures
40
+ # (eng + fra in OIML, plus the ISO official languages).
41
+ # Three-letter lowercase.
42
+ LANGUAGE_CODES = %w[
43
+ ara chi deu eng fra jpn rus spa zho
44
+ ].freeze
45
+
46
+ # ISO 15924 — scripts. Latn + Cyrl + Hant + Hans cover every script
47
+ # ISO and OIML resolutions are written in.
48
+ SCRIPT_CODES = %w[
49
+ Arab Cyrl Hans Hant Hang Hebr Jpan Kore Latn
50
+ ].freeze
51
+
52
+ # Curated offline subset of UN/LOCODEs — the cities ISO/TC 154 and
53
+ # OIML actually meet in plus the major transit hubs. Used by specs
54
+ # and consumers that don't want to load the full registry. For
55
+ # arbitrary lookups, use {find_unlocode}.
56
+ UNLOCODES = %w[
57
+ AUMEL ATVIE BEBRU BGSOF BRBSB CHGVA CNCAN CNHKG CNSHA CNXSZ COCTG
58
+ CYLCA CZPRG DEBER DEHAM DEFRA DKCPH ESMAD FIHEL FRARC FRLYS FRMRS
59
+ FRPAR GBLON HKHKG HUBUD IDJKT IEDUB IEORK ILTLV INDEL ISREY ITROM
60
+ JPTYO KEMBA KRSEL LKCMB LVRIX LULUX MYKUL NLRTM NOOSL NZAKL PHMNL
61
+ PLWAW PTLIS ROBUH RSBEG SESTO SGSIN SKBTS THBKK THCNM TRIST TWTPE
62
+ UAIEV USMIA USNYC USORL VNSGN ZACPT
63
+ ].freeze
64
+
65
+ # @deprecated Use {UNLOCODES}. Retained for one release to ease
66
+ # migration; will be removed in the next minor.
67
+ CITY_CODES = UNLOCODES
68
+
69
+ class << self
70
+ # Look up a UN/LOCODE entry via the canonical `unlocode` gem.
71
+ # Returns an `Unlocodes::Entry` (with `#name`, `#country`,
72
+ # `#coordinates`, `#iata`, `#functions`, etc.) or nil when the
73
+ # code is not in the registry.
74
+ #
75
+ # Requires the full UN/LOCODE dataset to be loaded. The gem
76
+ # ships a 4-entry sample by default; run `rake unlocodes:fetch`
77
+ # from the unlocode gem to populate the full ~100k-entry
78
+ # registry.
79
+ #
80
+ # @param code [String, #to_s] 5-character UN/LOCODE
81
+ # @return [Unlocodes::Entry, nil]
82
+ def find_unlocode(code)
83
+ Unlocodes.find(code.to_s.upcase)
84
+ end
85
+
86
+ # True when the code resolves to a real UN/LOCODE entry via the
87
+ # canonical registry. Use this when validating user-supplied
88
+ # city codes; use the schema's pattern check for cheap shape
89
+ # validation.
90
+ def unlocode_exists?(code)
91
+ !find_unlocode(code).nil?
92
+ end
93
+ end
94
+ end
95
+ end
@@ -18,5 +18,13 @@ module Edoxen
18
18
  # collections without a parent meeting (e.g., ad-hoc resolution
19
19
  # sets) simply omit it.
20
20
  attribute :meeting_urn, :string
21
+
22
+ # Resolve the collection's host-city UN/LOCODE via the canonical
23
+ # `unlocode` gem registry. Returns an `Unlocodes::Entry` or nil.
24
+ def city_entry
25
+ return nil if city.nil? || city.to_s.empty?
26
+
27
+ Edoxen::ReferenceData.find_unlocode(city)
28
+ end
21
29
  end
22
30
  end
@@ -1,16 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Edoxen
4
- # One entry in a meeting timetable. Structural fields (date, time,
5
- # room) are language-agnostic; descriptive fields (event,
6
- # description) live here as plain strings in v1 — see
7
- # TODO.meeting-agenda/10-future-enhancements.md P2.1 for per-item
8
- # localization.
4
+ # One entry in the meeting timetable. Structural fields (date, time,
5
+ # room) are language-agnostic; per-language content (event name,
6
+ # description) lives in `localizations[]`. Mirrors the glossarist
7
+ # LocalizedConcept pattern.
9
8
  class ScheduleItem < Lutaml::Model::Serializable
10
9
  attribute :date, :date
11
10
  attribute :time, :string
12
11
  attribute :event, :string
13
12
  attribute :description, :string
14
13
  attribute :room, :string
14
+ attribute :localizations, ScheduleItemLocalization, collection: true
15
+
16
+ def in_language(code, fallback: false)
17
+ match = localizations&.find { |loc| loc.language_code == code.to_s }
18
+ return match if match
19
+
20
+ fallback ? localizations&.first : nil
21
+ end
22
+
23
+ def primary_localization
24
+ in_language("eng", fallback: true)
25
+ end
15
26
  end
16
27
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Edoxen
4
+ # Per-language content for a ScheduleItem. Mirrors the glossarist
5
+ # LocalizedConcept pattern: structural fields (date, time, room) live
6
+ # on the parent ScheduleItem; per-language content (event name,
7
+ # description) lives here.
8
+ class ScheduleItemLocalization < Lutaml::Model::Serializable
9
+ attribute :language_code, :string
10
+ attribute :script, :string
11
+ attribute :event, :string
12
+ attribute :description, :string
13
+
14
+ key_value do
15
+ map "language_code", to: :language_code
16
+ map "script", to: :script
17
+ map "event", to: :event
18
+ map "description", to: :description
19
+ end
20
+ end
21
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Edoxen
4
- VERSION = "0.5.0"
4
+ VERSION = "0.7.2"
5
5
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Edoxen
4
+ # A single vote on a Resolution, recorded against the meeting at
5
+ # which the vote was taken. Mirrors
6
+ # edoxen-model/models/vote_record.lutaml. Each record carries the
7
+ # person, their affiliation (national body / liaison / committee),
8
+ # and the outcome (affirmative / negative / abstain / absent /
9
+ # not_applicable).
10
+ class VoteRecord < Lutaml::Model::Serializable
11
+ attribute :resolution_ref, :string
12
+ attribute :person, Person
13
+ attribute :affiliation, :string
14
+ attribute :vote, :string, values: Enums::VOTE_TYPE
15
+ attribute :notes, :string
16
+
17
+ key_value do
18
+ map "resolution_ref", to: :resolution_ref
19
+ map "person", to: :person
20
+ map "affiliation", to: :affiliation
21
+ map "vote", to: :vote
22
+ map "notes", to: :notes
23
+ end
24
+ end
25
+ end
data/lib/edoxen.rb CHANGED
@@ -23,6 +23,7 @@ module Edoxen
23
23
  autoload :Error, "edoxen/error"
24
24
  autoload :ValidationError, "edoxen/error"
25
25
  autoload :Enums, "edoxen/enums"
26
+ autoload :ReferenceData, "edoxen/reference_data"
26
27
 
27
28
  # Information-model classes (one per file, one concept per class).
28
29
  # Names mirror ../edoxen-model/models/*.lutaml.
@@ -45,11 +46,16 @@ module Edoxen
45
46
  autoload :Location, "edoxen/location"
46
47
  autoload :Person, "edoxen/person"
47
48
  autoload :HostRef, "edoxen/host_ref"
49
+ autoload :ScheduleItemLocalization, "edoxen/schedule_item_localization"
48
50
  autoload :ScheduleItem, "edoxen/schedule_item"
49
51
  autoload :Deadline, "edoxen/deadline"
50
52
  autoload :Reference, "edoxen/reference"
51
53
  autoload :AgendaItem, "edoxen/agenda_item"
52
54
  autoload :Agenda, "edoxen/agenda"
55
+ autoload :Attendance, "edoxen/attendance"
56
+ autoload :VoteRecord, "edoxen/vote_record"
57
+ autoload :MinutesSection, "edoxen/minutes_section"
58
+ autoload :Minutes, "edoxen/minutes"
53
59
  autoload :MeetingRelation, "edoxen/meeting_relation"
54
60
  autoload :MeetingLocalization, "edoxen/meeting_localization"
55
61
  autoload :Meeting, "edoxen/meeting"
@@ -58,5 +64,6 @@ module Edoxen
58
64
 
59
65
  # Services.
60
66
  autoload :SchemaValidator, "edoxen/schema_validator"
67
+ autoload :LinkChecker, "edoxen/link_checker"
61
68
  autoload :Cli, "edoxen/cli"
62
69
  end
data/schema/edoxen.yaml CHANGED
@@ -1,12 +1,12 @@
1
1
  ---
2
2
  $schema: "http://json-schema.org/draft-07/schema#"
3
- $id: "https://github.com/metanorma/edoxen/schema/edoxen.yaml"
3
+ $id: "https://github.com/edoxen/edoxen/schema/edoxen.yaml"
4
4
  title: "Edoxen Resolution Collection Schema"
5
5
  description: |
6
6
  Schema for validating Edoxen ResolutionCollection YAML files.
7
7
 
8
8
  Mirrors the canonical LutaML information model in
9
- https://github.com/metanorma/edoxen-model/tree/main/models .
9
+ https://github.com/edoxen/edoxen-model/tree/main/models .
10
10
 
11
11
  The enum constants declared under `$defs` (ActionType,
12
12
  ConsiderationType, ApprovalType, ApprovalDegree, ResolutionType,
@@ -289,7 +289,8 @@ $defs:
289
289
  description: "Canonical URLs to the source PDFs in each available language."
290
290
  city:
291
291
  type: string
292
- description: "IATA city code (3-letter, e.g. BER, CPT, PAR) of the host city."
292
+ pattern: "^[A-Z]{2}[A-Z0-9]{3}$"
293
+ description: "UN/LOCODE of the host city (5-char: 2-letter ISO 3166-1 country + 3-char location, e.g. FRPAR, CNSHA, HKHKG). Supersedes the IATA city code."
293
294
  country_code:
294
295
  type: string
295
296
  description: "ISO 3166-1 alpha-2 country code (2-letter, e.g. DE, FR, JP) of the host country."
data/schema/meeting.yaml CHANGED
@@ -1,12 +1,12 @@
1
1
  ---
2
2
  $schema: "http://json-schema.org/draft-07/schema#"
3
- $id: "https://github.com/metanorma/edoxen/schema/meeting.yaml"
3
+ $id: "https://github.com/edoxen/edoxen/schema/meeting.yaml"
4
4
  title: "Edoxen Meeting Schema"
5
5
  description: |
6
6
  Schema for validating Edoxen Meeting/Agenda YAML files.
7
7
 
8
8
  Mirrors the canonical LutaML information model in
9
- https://github.com/metanorma/edoxen-model/tree/main/models (meeting*.lutaml,
9
+ https://github.com/edoxen/edoxen-model/tree/main/models (meeting*.lutaml,
10
10
  agenda*.lutaml, etc.).
11
11
 
12
12
  The root accepts either a single Meeting or a MeetingCollection.
@@ -104,6 +104,21 @@ $defs:
104
104
  contact:
105
105
  $ref: "#/$defs/Person"
106
106
 
107
+ ScheduleItemLocalization:
108
+ type: object
109
+ description: "Per-language content for a ScheduleItem."
110
+ additionalProperties: false
111
+ required: [language_code]
112
+ properties:
113
+ language_code:
114
+ type: string
115
+ pattern: "^[a-z]{3}$"
116
+ script:
117
+ type: string
118
+ pattern: "^[A-Z][a-z]{3}$"
119
+ event: { type: string }
120
+ description: { type: string }
121
+
107
122
  ScheduleItem:
108
123
  type: object
109
124
  description: "One entry in the meeting timetable."
@@ -116,6 +131,88 @@ $defs:
116
131
  event: { type: string }
117
132
  description: { type: string }
118
133
  room: { type: string }
134
+ localizations:
135
+ type: array
136
+ items:
137
+ $ref: "#/$defs/ScheduleItemLocalization"
138
+
139
+ Attendance:
140
+ type: object
141
+ description: "One attendance record per person at a Meeting."
142
+ additionalProperties: false
143
+ required: [person, status]
144
+ properties:
145
+ person:
146
+ $ref: "#/$defs/Person"
147
+ status:
148
+ $ref: "#/$defs/ParticipationStatus"
149
+ affiliation: { type: string }
150
+ proxy_for:
151
+ $ref: "#/$defs/Person"
152
+ notes: { type: string }
153
+
154
+ VoteRecord:
155
+ type: object
156
+ description: "One vote by one person on one resolution at one meeting."
157
+ additionalProperties: false
158
+ required: [resolution_ref, person, vote]
159
+ properties:
160
+ resolution_ref: { type: string }
161
+ person:
162
+ $ref: "#/$defs/Person"
163
+ affiliation: { type: string }
164
+ vote:
165
+ $ref: "#/$defs/VoteType"
166
+ notes: { type: string }
167
+
168
+ MinutesSection:
169
+ type: object
170
+ description: |
171
+ One section of a Meeting's minutes — typically tied to an agenda
172
+ item by its `number` field. Carries the narrative as Markdown
173
+ (the format the GLM-OCR pipeline emits) plus an optional page
174
+ range for provenance back to the source PDF.
175
+ additionalProperties: false
176
+ properties:
177
+ number: { type: string }
178
+ title: { type: string }
179
+ narrative: { type: string }
180
+ page_start: { type: integer }
181
+ page_end: { type: integer }
182
+ references:
183
+ type: array
184
+ items:
185
+ $ref: "#/$defs/Reference"
186
+
187
+ Minutes:
188
+ type: object
189
+ description: |
190
+ The narrative record of a Meeting — what was said, by whom, in
191
+ what order, with what outcome. Distinct from Agenda (the business
192
+ order drafted before the meeting) and from ResolutionCollection
193
+ (the formal decisions adopted). The Minutes are the running
194
+ record. One Minutes document per language; sections are typically
195
+ keyed by agenda-item number so consumers can join
196
+ Minutes ↔ AgendaItem ↔ Resolution.
197
+ additionalProperties: false
198
+ properties:
199
+ identifier:
200
+ type: array
201
+ items:
202
+ $ref: "#/$defs/StructuredIdentifier"
203
+ urn: { type: string }
204
+ language_code:
205
+ type: string
206
+ pattern: "^[a-z]{3}$"
207
+ script:
208
+ type: string
209
+ pattern: "^[A-Z][a-z]{3}$"
210
+ source_doc: { type: string }
211
+ source_pages: { type: string }
212
+ sections:
213
+ type: array
214
+ items:
215
+ $ref: "#/$defs/MinutesSection"
119
216
 
120
217
  Deadline:
121
218
  type: object
@@ -247,7 +344,10 @@ $defs:
247
344
  items:
248
345
  $ref: "#/$defs/Location"
249
346
  general_area: { type: string }
250
- city: { type: string }
347
+ city:
348
+ type: string
349
+ pattern: "^[A-Z]{2}[A-Z0-9]{3}$"
350
+ description: "UN/LOCODE of the host city (5-char: 2-letter ISO 3166-1 country + 3-char location, e.g. FRPAR, HKHKG, THCNM)."
251
351
  country_code:
252
352
  type: string
253
353
  pattern: "^[A-Z]{2}$"
@@ -278,6 +378,18 @@ $defs:
278
378
  type: array
279
379
  items:
280
380
  $ref: "#/$defs/Deadline"
381
+ attendance:
382
+ type: array
383
+ items:
384
+ $ref: "#/$defs/Attendance"
385
+ vote_records:
386
+ type: array
387
+ items:
388
+ $ref: "#/$defs/VoteRecord"
389
+ minutes:
390
+ type: array
391
+ items:
392
+ $ref: "#/$defs/Minutes"
281
393
 
282
394
  localizations:
283
395
  type: array
@@ -356,3 +468,13 @@ $defs:
356
468
  type: string
357
469
  description: "Edoxen::Enums::SOURCE_URL_KIND."
358
470
  enum: [agenda_pdf, minutes_pdf, resolutions_pdf, report_pdf, register_url, landing_page]
471
+
472
+ ParticipationStatus:
473
+ type: string
474
+ description: "Edoxen::Enums::PARTICIPATION_STATUS."
475
+ enum: [present, absent, apologies, observer, excused]
476
+
477
+ VoteType:
478
+ type: string
479
+ description: "Edoxen::Enums::VOTE_TYPE."
480
+ enum: [affirmative, negative, abstain, absent, not_applicable]
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: edoxen
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.7.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-07-01 00:00:00.000000000 Z
11
+ date: 2026-07-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json_schemer
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '1.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: unlocodes
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.1'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.1'
55
69
  description: |
56
70
  Edoxen provides a Ruby library for working with resolution models, allowing
57
71
  users to create, manipulate, and serialize resolution data in a structured
@@ -92,6 +106,7 @@ files:
92
106
  - lib/edoxen/agenda.rb
93
107
  - lib/edoxen/agenda_item.rb
94
108
  - lib/edoxen/approval.rb
109
+ - lib/edoxen/attendance.rb
95
110
  - lib/edoxen/cli.rb
96
111
  - lib/edoxen/consideration.rb
97
112
  - lib/edoxen/date_range.rb
@@ -99,6 +114,7 @@ files:
99
114
  - lib/edoxen/enums.rb
100
115
  - lib/edoxen/error.rb
101
116
  - lib/edoxen/host_ref.rb
117
+ - lib/edoxen/link_checker.rb
102
118
  - lib/edoxen/localization.rb
103
119
  - lib/edoxen/location.rb
104
120
  - lib/edoxen/meeting.rb
@@ -107,8 +123,11 @@ files:
107
123
  - lib/edoxen/meeting_identifier.rb
108
124
  - lib/edoxen/meeting_localization.rb
109
125
  - lib/edoxen/meeting_relation.rb
126
+ - lib/edoxen/minutes.rb
127
+ - lib/edoxen/minutes_section.rb
110
128
  - lib/edoxen/person.rb
111
129
  - lib/edoxen/reference.rb
130
+ - lib/edoxen/reference_data.rb
112
131
  - lib/edoxen/resolution.rb
113
132
  - lib/edoxen/resolution_collection.rb
114
133
  - lib/edoxen/resolution_date.rb
@@ -116,11 +135,13 @@ files:
116
135
  - lib/edoxen/resolution_relation.rb
117
136
  - lib/edoxen/resolution_set.rb
118
137
  - lib/edoxen/schedule_item.rb
138
+ - lib/edoxen/schedule_item_localization.rb
119
139
  - lib/edoxen/schema_validator.rb
120
140
  - lib/edoxen/source_url.rb
121
141
  - lib/edoxen/structured_identifier.rb
122
142
  - lib/edoxen/url.rb
123
143
  - lib/edoxen/version.rb
144
+ - lib/edoxen/vote_record.rb
124
145
  - schema/edoxen.yaml
125
146
  - schema/meeting.yaml
126
147
  - sig/edoxen.rbs