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,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
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Edoxen
|
|
4
|
+
# Venue geography. Multi-venue meetings (e.g., a plenary week with
|
|
5
|
+
# different hotels) carry one Location per venue.
|
|
6
|
+
class Location < Lutaml::Model::Serializable
|
|
7
|
+
attribute :name, :string
|
|
8
|
+
attribute :address, :string
|
|
9
|
+
attribute :link, :string
|
|
10
|
+
attribute :phone, :string
|
|
11
|
+
attribute :note, :string
|
|
12
|
+
attribute :lat, :float
|
|
13
|
+
attribute :lon, :float
|
|
14
|
+
end
|
|
15
|
+
end
|
data/lib/edoxen/meeting.rb
CHANGED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Edoxen
|
|
4
|
+
# A single Meeting — the event that produces Resolutions. Carries
|
|
5
|
+
# identity, time, venue, officers, agenda, schedule, deadlines, and
|
|
6
|
+
# URN links to one or more ResolutionCollection documents.
|
|
7
|
+
#
|
|
8
|
+
# Meetings and ResolutionCollections are kept as separate documents
|
|
9
|
+
# and joined by URN (Meeting.resolution_refs ↔
|
|
10
|
+
# ResolutionCollection.metadata.meeting_urn) because they have
|
|
11
|
+
# different lifetimes: agendas exist weeks before a meeting;
|
|
12
|
+
# resolutions only after adoption.
|
|
13
|
+
class Meeting < Lutaml::Model::Serializable
|
|
14
|
+
attribute :identifier, StructuredIdentifier, collection: true
|
|
15
|
+
attribute :urn, :string
|
|
16
|
+
attribute :ordinal, :integer
|
|
17
|
+
attribute :type, :string, values: Enums::MEETING_TYPE
|
|
18
|
+
attribute :status, :string, values: Enums::MEETING_STATUS
|
|
19
|
+
attribute :year, :integer
|
|
20
|
+
|
|
21
|
+
attribute :date_range, DateRange
|
|
22
|
+
|
|
23
|
+
attribute :committee, :string
|
|
24
|
+
attribute :committee_group, :string
|
|
25
|
+
|
|
26
|
+
attribute :venues, Location, collection: true
|
|
27
|
+
attribute :general_area, :string
|
|
28
|
+
attribute :city, :string
|
|
29
|
+
attribute :country_code, :string
|
|
30
|
+
attribute :virtual, :boolean
|
|
31
|
+
|
|
32
|
+
attribute :chair, Person
|
|
33
|
+
attribute :secretary, Person
|
|
34
|
+
attribute :host, :string
|
|
35
|
+
attribute :hosts, HostRef, collection: true
|
|
36
|
+
|
|
37
|
+
attribute :source_urls, SourceUrl, collection: true
|
|
38
|
+
attribute :landing_url, :string
|
|
39
|
+
attribute :registration_url, :string
|
|
40
|
+
|
|
41
|
+
attribute :agenda, Agenda
|
|
42
|
+
attribute :schedule, ScheduleItem, collection: true
|
|
43
|
+
attribute :deadlines, Deadline, collection: true
|
|
44
|
+
|
|
45
|
+
attribute :attendance, Attendance, collection: true
|
|
46
|
+
attribute :vote_records, VoteRecord, collection: true
|
|
47
|
+
attribute :minutes, Minutes, collection: true
|
|
48
|
+
|
|
49
|
+
attribute :localizations, MeetingLocalization, collection: true
|
|
50
|
+
attribute :relations, MeetingRelation, collection: true
|
|
51
|
+
attribute :resolution_refs, :string, collection: true
|
|
52
|
+
|
|
53
|
+
# Lookup by ISO 639-3 language code. Mirrors Resolution#in_language.
|
|
54
|
+
def in_language(code, fallback: false)
|
|
55
|
+
match = localizations&.find { |loc| loc.language_code == code.to_s }
|
|
56
|
+
return match if match
|
|
57
|
+
|
|
58
|
+
fallback ? localizations&.first : nil
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# English when available, else the first declared localization.
|
|
62
|
+
def primary_localization
|
|
63
|
+
in_language("eng", fallback: true)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Find an agenda item by label. Returns nil when no agenda or no
|
|
67
|
+
# matching label.
|
|
68
|
+
def find_agenda_item(label)
|
|
69
|
+
agenda&.find_item(label)
|
|
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
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Edoxen
|
|
4
|
+
# Top-level container for many Meetings. Parallel to
|
|
5
|
+
# ResolutionCollection.
|
|
6
|
+
class MeetingCollection < Lutaml::Model::Serializable
|
|
7
|
+
attribute :metadata, MeetingCollectionMetadata
|
|
8
|
+
attribute :meetings, Meeting, collection: true
|
|
9
|
+
|
|
10
|
+
# Find a meeting by URN. Returns nil when no meeting matches.
|
|
11
|
+
def find_by_urn(urn)
|
|
12
|
+
meetings&.find { |m| m.urn == urn.to_s }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Find a meeting by StructuredIdentifier components.
|
|
16
|
+
# Returns the first meeting whose any identifier matches both
|
|
17
|
+
# prefix and number.
|
|
18
|
+
def find_by_identifier(prefix:, number:)
|
|
19
|
+
meetings&.find do |meeting|
|
|
20
|
+
meeting.identifier&.any? do |id|
|
|
21
|
+
id.prefix == prefix.to_s && id.number == number.to_s
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Edoxen
|
|
4
|
+
# Top-level wrapper for many Meetings in a single YAML file. Parallel
|
|
5
|
+
# to ResolutionCollection. The metadata block carries display-level
|
|
6
|
+
# info (title, source); per-meeting identity lives on each Meeting.
|
|
7
|
+
class MeetingCollectionMetadata < Lutaml::Model::Serializable
|
|
8
|
+
attribute :title, :string
|
|
9
|
+
attribute :source, :string
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Edoxen
|
|
4
|
+
# Per-language content for a Meeting. Mirrors the glossarist
|
|
5
|
+
# pattern used by `Localization` for Resolutions: language-agnostic
|
|
6
|
+
# admin fields live on the parent Meeting; per-language content
|
|
7
|
+
# lives here.
|
|
8
|
+
class MeetingLocalization < Lutaml::Model::Serializable
|
|
9
|
+
attribute :language_code, :string
|
|
10
|
+
attribute :script, :string
|
|
11
|
+
attribute :title, :string
|
|
12
|
+
attribute :general_area, :string
|
|
13
|
+
attribute :practical_info, :string
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Edoxen
|
|
4
|
+
# Directed link between two meetings, identified by their
|
|
5
|
+
# StructuredIdentifier. Reuses the same relation-type vocabulary
|
|
6
|
+
# pattern as ResolutionRelation.
|
|
7
|
+
class MeetingRelation < Lutaml::Model::Serializable
|
|
8
|
+
attribute :source, StructuredIdentifier
|
|
9
|
+
attribute :destination, StructuredIdentifier
|
|
10
|
+
attribute :type, :string, values: Enums::MEETING_RELATION_TYPE
|
|
11
|
+
end
|
|
12
|
+
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,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Edoxen
|
|
4
|
+
# Identity + role + affiliation + contact. Used for meeting officers
|
|
5
|
+
# (chair, secretary, local contact) and host-ref contacts.
|
|
6
|
+
class Person < Lutaml::Model::Serializable
|
|
7
|
+
attribute :name, :string
|
|
8
|
+
attribute :role, :string
|
|
9
|
+
attribute :affiliation, :string
|
|
10
|
+
attribute :email, :string
|
|
11
|
+
attribute :phone, :string
|
|
12
|
+
attribute :orcid, :string
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Edoxen
|
|
4
|
+
# Generic document reference (used by AgendaItem.references). `kind`
|
|
5
|
+
# is a free-form discriminator (e.g., "standard", "document",
|
|
6
|
+
# "ballot", "resolution"); the schema leaves it open deliberately so
|
|
7
|
+
# new reference kinds need no schema change.
|
|
8
|
+
class Reference < Lutaml::Model::Serializable
|
|
9
|
+
attribute :ref, :string
|
|
10
|
+
attribute :kind, :string
|
|
11
|
+
attribute :title, :string
|
|
12
|
+
end
|
|
13
|
+
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
|
|
@@ -12,5 +12,19 @@ module Edoxen
|
|
|
12
12
|
attribute :source_urls, SourceUrl, collection: true
|
|
13
13
|
attribute :city, :string
|
|
14
14
|
attribute :country_code, :string
|
|
15
|
+
|
|
16
|
+
# URN back-reference to the Meeting that produced this collection.
|
|
17
|
+
# Join key for the Meeting ↔ ResolutionCollection link. Optional —
|
|
18
|
+
# collections without a parent meeting (e.g., ad-hoc resolution
|
|
19
|
+
# sets) simply omit it.
|
|
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
|
|
15
29
|
end
|
|
16
30
|
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Edoxen
|
|
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.
|
|
8
|
+
class ScheduleItem < Lutaml::Model::Serializable
|
|
9
|
+
attribute :date, :date
|
|
10
|
+
attribute :time, :string
|
|
11
|
+
attribute :event, :string
|
|
12
|
+
attribute :description, :string
|
|
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
|
|
26
|
+
end
|
|
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
|
data/lib/edoxen/source_url.rb
CHANGED
|
@@ -2,11 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
module Edoxen
|
|
4
4
|
# Per-language canonical source URL (e.g. one PDF per language).
|
|
5
|
-
# Carries the URL ref, its format,
|
|
6
|
-
# is the canonical source for
|
|
5
|
+
# Carries the URL ref, its format, the ISO 639-3 language_code the URL
|
|
6
|
+
# is the canonical source for, and an optional `kind` discriminator
|
|
7
|
+
# (agenda_pdf, minutes_pdf, resolutions_pdf, ...) used by the Meeting
|
|
8
|
+
# model. Existing Resolution fixtures without `kind` still parse —
|
|
9
|
+
# the field is optional.
|
|
7
10
|
class SourceUrl < Lutaml::Model::Serializable
|
|
8
11
|
attribute :ref, :string
|
|
9
12
|
attribute :format, :string
|
|
10
13
|
attribute :language_code, :string
|
|
14
|
+
attribute :kind, :string, values: Enums::SOURCE_URL_KIND
|
|
11
15
|
end
|
|
12
16
|
end
|
data/lib/edoxen/version.rb
CHANGED
|
@@ -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.
|
|
@@ -40,7 +41,29 @@ module Edoxen
|
|
|
40
41
|
autoload :ResolutionMetadata, "edoxen/resolution_metadata"
|
|
41
42
|
autoload :ResolutionCollection, "edoxen/resolution_collection"
|
|
42
43
|
|
|
44
|
+
# Meeting / Agenda side. Mirrors edoxen-model/models/meeting*.lutaml.
|
|
45
|
+
autoload :DateRange, "edoxen/date_range"
|
|
46
|
+
autoload :Location, "edoxen/location"
|
|
47
|
+
autoload :Person, "edoxen/person"
|
|
48
|
+
autoload :HostRef, "edoxen/host_ref"
|
|
49
|
+
autoload :ScheduleItemLocalization, "edoxen/schedule_item_localization"
|
|
50
|
+
autoload :ScheduleItem, "edoxen/schedule_item"
|
|
51
|
+
autoload :Deadline, "edoxen/deadline"
|
|
52
|
+
autoload :Reference, "edoxen/reference"
|
|
53
|
+
autoload :AgendaItem, "edoxen/agenda_item"
|
|
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"
|
|
59
|
+
autoload :MeetingRelation, "edoxen/meeting_relation"
|
|
60
|
+
autoload :MeetingLocalization, "edoxen/meeting_localization"
|
|
61
|
+
autoload :Meeting, "edoxen/meeting"
|
|
62
|
+
autoload :MeetingCollectionMetadata, "edoxen/meeting_collection_metadata"
|
|
63
|
+
autoload :MeetingCollection, "edoxen/meeting_collection"
|
|
64
|
+
|
|
43
65
|
# Services.
|
|
44
66
|
autoload :SchemaValidator, "edoxen/schema_validator"
|
|
67
|
+
autoload :LinkChecker, "edoxen/link_checker"
|
|
45
68
|
autoload :Cli, "edoxen/cli"
|
|
46
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/
|
|
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/
|
|
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,
|
|
@@ -207,6 +207,10 @@ $defs:
|
|
|
207
207
|
language_code:
|
|
208
208
|
type: string
|
|
209
209
|
pattern: "^[a-z]{3}$"
|
|
210
|
+
kind:
|
|
211
|
+
type: string
|
|
212
|
+
description: "Optional discriminator (agenda_pdf, minutes_pdf, ...)."
|
|
213
|
+
enum: [agenda_pdf, minutes_pdf, resolutions_pdf, report_pdf, register_url, landing_page]
|
|
210
214
|
|
|
211
215
|
# ====================================================================
|
|
212
216
|
# Top-level structures.
|
|
@@ -285,10 +289,14 @@ $defs:
|
|
|
285
289
|
description: "Canonical URLs to the source PDFs in each available language."
|
|
286
290
|
city:
|
|
287
291
|
type: string
|
|
288
|
-
|
|
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."
|
|
289
294
|
country_code:
|
|
290
295
|
type: string
|
|
291
296
|
description: "ISO 3166-1 alpha-2 country code (2-letter, e.g. DE, FR, JP) of the host country."
|
|
297
|
+
meeting_urn:
|
|
298
|
+
type: string
|
|
299
|
+
description: "URN back-reference to the Meeting that produced this collection."
|
|
292
300
|
|
|
293
301
|
# ====================================================================
|
|
294
302
|
# Enums. Each is referenced by its $ref name above. The schema
|