edoxen 0.7.2 → 2.1.1

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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +17 -1
  3. data/.rubocop_todo.yml +1 -1
  4. data/CHANGELOG.md +246 -0
  5. data/README.adoc +308 -153
  6. data/edoxen.gemspec +10 -10
  7. data/lib/edoxen/action.rb +1 -1
  8. data/lib/edoxen/agenda.rb +4 -7
  9. data/lib/edoxen/agenda_item.rb +10 -3
  10. data/lib/edoxen/approval.rb +1 -1
  11. data/lib/edoxen/attendance.rb +16 -10
  12. data/lib/edoxen/body_vocabulary_entry.rb +24 -0
  13. data/lib/edoxen/body_vocabulary_host.rb +29 -0
  14. data/lib/edoxen/cli.rb +117 -120
  15. data/lib/edoxen/component_localization.rb +11 -0
  16. data/lib/edoxen/consideration.rb +2 -2
  17. data/lib/edoxen/contact.rb +24 -0
  18. data/lib/edoxen/contact_identifier.rb +13 -0
  19. data/lib/edoxen/contact_method.rb +16 -0
  20. data/lib/edoxen/decision.rb +69 -0
  21. data/lib/edoxen/decision_collection.rb +10 -0
  22. data/lib/edoxen/decision_date.rb +9 -0
  23. data/lib/edoxen/{resolution_metadata.rb → decision_metadata.rb} +5 -6
  24. data/lib/edoxen/decision_relation.rb +11 -0
  25. data/lib/edoxen/entity_ref.rb +74 -0
  26. data/lib/edoxen/enums.rb +117 -9
  27. data/lib/edoxen/error.rb +1 -1
  28. data/lib/edoxen/extension_attribute.rb +78 -0
  29. data/lib/edoxen/host_ref.rb +1 -1
  30. data/lib/edoxen/link_checker.rb +8 -8
  31. data/lib/edoxen/localization.rb +2 -2
  32. data/lib/edoxen/localization_host.rb +23 -0
  33. data/lib/edoxen/meeting.rb +63 -39
  34. data/lib/edoxen/meeting_collection.rb +1 -2
  35. data/lib/edoxen/meeting_collection_metadata.rb +3 -1
  36. data/lib/edoxen/meeting_component.rb +41 -0
  37. data/lib/edoxen/meeting_extension.rb +25 -0
  38. data/lib/edoxen/meeting_identifier.rb +1 -1
  39. data/lib/edoxen/meeting_localization.rb +6 -1
  40. data/lib/edoxen/meeting_series.rb +21 -0
  41. data/lib/edoxen/minutes.rb +2 -2
  42. data/lib/edoxen/minutes_section.rb +8 -11
  43. data/lib/edoxen/motion.rb +51 -0
  44. data/lib/edoxen/name.rb +29 -0
  45. data/lib/edoxen/officer.rb +19 -0
  46. data/lib/edoxen/officers_host.rb +19 -0
  47. data/lib/edoxen/person.rb +5 -9
  48. data/lib/edoxen/physical_venue.rb +15 -0
  49. data/lib/edoxen/recurrence.rb +27 -0
  50. data/lib/edoxen/recurrence_by_day.rb +10 -0
  51. data/lib/edoxen/reference_data.rb +35 -41
  52. data/lib/edoxen/schema_validator.rb +2 -3
  53. data/lib/edoxen/source_url.rb +3 -3
  54. data/lib/edoxen/structured_identifier.rb +2 -2
  55. data/lib/edoxen/topic.rb +37 -0
  56. data/lib/edoxen/topic_asset.rb +14 -0
  57. data/lib/edoxen/topic_document.rb +16 -0
  58. data/lib/edoxen/venue.rb +80 -0
  59. data/lib/edoxen/venue_validator.rb +56 -0
  60. data/lib/edoxen/version.rb +1 -1
  61. data/lib/edoxen/virtual_venue.rb +15 -0
  62. data/lib/edoxen/vote_record.rb +17 -13
  63. data/lib/edoxen/voting.rb +45 -0
  64. data/lib/edoxen/voting_counts.rb +23 -0
  65. data/lib/edoxen.rb +40 -13
  66. data/schema/edoxen.yaml +792 -145
  67. data/schema/meeting.yaml +600 -142
  68. data/sig/edoxen.rbs +581 -1
  69. metadata +57 -20
  70. data/lib/edoxen/_metadata.rb.deprecated +0 -57
  71. data/lib/edoxen/location.rb +0 -15
  72. data/lib/edoxen/resolution.rb +0 -43
  73. data/lib/edoxen/resolution_collection.rb +0 -10
  74. data/lib/edoxen/resolution_date.rb +0 -11
  75. data/lib/edoxen/resolution_relation.rb +0 -11
  76. data/lib/edoxen/resolution_set.rb +0 -17
  77. data/lib/edoxen/schedule_item.rb +0 -27
  78. data/lib/edoxen/schedule_item_localization.rb +0 -21
@@ -4,7 +4,9 @@ module Edoxen
4
4
  # Collection-level metadata: the title (string for default / single-language
5
5
  # collections, or `title_localized[]` for multilingual), the meeting date,
6
6
  # the source secretariat, per-language source PDFs, and the host venue.
7
- class ResolutionMetadata < Lutaml::Model::Serializable
7
+ class DecisionMetadata < Lutaml::Model::Serializable
8
+ include BodyVocabularyHost
9
+
8
10
  attribute :title, :string
9
11
  attribute :title_localized, Localization, collection: true
10
12
  attribute :date, :date
@@ -14,13 +16,10 @@ module Edoxen
14
16
  attribute :country_code, :string
15
17
 
16
18
  # 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
19
  attribute :meeting_urn, :string
21
20
 
22
- # Resolve the collection's host-city UN/LOCODE via the canonical
23
- # `unlocode` gem registry. Returns an `Unlocodes::Entry` or nil.
21
+ attribute :extensions, MeetingExtension, collection: true
22
+
24
23
  def city_entry
25
24
  return nil if city.nil? || city.to_s.empty?
26
25
 
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Edoxen
4
+ # Directed relation between two decisions, identified by their
5
+ # StructuredIdentifier (prefix + number).
6
+ class DecisionRelation < Lutaml::Model::Serializable
7
+ attribute :source, StructuredIdentifier
8
+ attribute :destination, StructuredIdentifier
9
+ attribute :type, :string, values: Enums::DECISION_RELATION_TYPE
10
+ end
11
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Edoxen
4
+ # EntityRef — a typed cross-reference between entities (v2.1, TODO.refactor/44).
5
+ #
6
+ # Single identity: exactly one of `urn`, `identifier`, or `local_ref`
7
+ # must be set. Optional metadata: `kind`, `role`, `note`.
8
+ #
9
+ # Encapsulation:
10
+ # - Read the identity via `#resolved_identity` (returns the canonical
11
+ # form — URN string, StructuredIdentifier, or local key).
12
+ # - Validate via `#valid?` (true when exactly one identity field is
13
+ # set; `#multiple_identities?` flags the rare ambiguous case).
14
+ #
15
+ # Pilot: `Motion.resulting_decision_ref` (parallel to the existing
16
+ # `resulting_decision: String`). v3.0 will remove the String form.
17
+ class EntityRef < Lutaml::Model::Serializable
18
+ attribute :urn, :string
19
+ attribute :identifier, StructuredIdentifier
20
+ attribute :local_ref, :string
21
+
22
+ attribute :kind, :string
23
+ attribute :role, :string
24
+ attribute :note, :string
25
+
26
+ # True when exactly one identity field is set. The wire contract
27
+ # (TODO.refactor/44 + JSON-Schema) is XOR: setting 0 or ≥2 identity
28
+ # fields is a data error.
29
+ def valid?
30
+ identities_set.size == 1
31
+ end
32
+
33
+ # True when two or more identity fields are set. Useful for
34
+ # migration tooling that wants to warn on ambiguity without
35
+ # rejecting outright.
36
+ def multiple_identities?
37
+ identities_set.size > 1
38
+ end
39
+
40
+ # Returns the canonical identity (URN > identifier > local_ref
41
+ # precedence). Callers that need a single value should validate
42
+ # first via `#valid?`; on a multiple-identity ref the precedence
43
+ # is still applied but the data is suspect.
44
+ def resolved_identity
45
+ return urn if urn && !urn.to_s.empty?
46
+ return identifier if identifier
47
+ return local_ref if local_ref && !local_ref.to_s.empty?
48
+
49
+ nil
50
+ end
51
+
52
+ def to_s
53
+ identity = resolved_identity
54
+ return "(invalid EntityRef)" unless identity
55
+
56
+ case identity
57
+ when StructuredIdentifier then "#{identity.prefix}/#{identity.number}"
58
+ else identity.to_s
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ def identities_set
65
+ [urn, identifier, local_ref].reject do |value|
66
+ case value
67
+ when nil then true
68
+ when String then value.empty?
69
+ else false
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
data/lib/edoxen/enums.rb CHANGED
@@ -26,13 +26,37 @@ module Edoxen
26
26
  emphasizing concerned accepts observing referring acting empowers
27
27
  ].freeze
28
28
 
29
- RESOLUTION_TYPE = %w[resolution recommendation decision declaration].freeze
29
+ # --- Decision-side (was Resolution; renamed 2026-07) -----------------
30
+
31
+ # DecisionKind — what kind of formal outcome this is. `resolution` is
32
+ # one value; the same formal concept has many names across bodies
33
+ # (order, ruling, determination, finding, opinion, etc.).
34
+ DECISION_KIND = %w[
35
+ resolution order ruling determination
36
+ recommendation statement finding opinion other
37
+ ].freeze
38
+
39
+ # DecisionKindCanonical — the short abstract set (v2.1, TODO.refactor/46).
40
+ # Bodies extend via `body_type: String` + per-dataset `body_vocabulary[]`
41
+ # on DecisionMetadata. `other` is a temporary escape while the
42
+ # vocabulary stabilises; v3.0 removes it.
43
+ #
44
+ # Cap: 5 canonical values (hard limit per the v2.1 design review).
45
+ DECISION_KIND_CANONICAL = %w[decision recommendation statement finding other].freeze
46
+
47
+ # DecisionStatus — lifecycle state machine.
48
+ DECISION_STATUS = %w[
49
+ draft proposed under_consideration
50
+ decided negatived withdrawn deferred
51
+ ].freeze
30
52
 
31
- RESOLUTION_RELATION_TYPE = %w[
32
- annexOf hasAnnex updates refines replaces considers
53
+ DECISION_RELATION_TYPE = %w[
54
+ annex_of has_annex updates refines replaces considers cites
33
55
  ].freeze
34
56
 
35
- RESOLUTION_DATE_TYPE = %w[adoption drafted discussed].freeze
57
+ DECISION_DATE_TYPE = %w[
58
+ adoption drafted discussed proposed decided negatived withdrawn published effective
59
+ ].freeze
36
60
 
37
61
  APPROVAL_TYPE = %w[affirmative negative].freeze
38
62
 
@@ -44,31 +68,115 @@ module Edoxen
44
68
  # Mirrors edoxen-model/models/{meeting,agenda,...}_*.lutaml.
45
69
  # schema/meeting.yaml references these via $defs/<EnumName>.enum.
46
70
 
47
- MEETING_TYPE = %w[plenary working_group task_group ad_hoc joint conference_session].freeze
71
+ MEETING_TYPE = %w[
72
+ plenary working_group task_group ad_hoc joint general_assembly
73
+ committee subcommittee conference workshop seminar webinar hearing
74
+ markup board_meeting annual_general_meeting other
75
+ ].freeze
76
+
77
+ # MeetingTypeCanonical — the short abstract set (v2.1, TODO.refactor/46).
78
+ # Bodies extend via `body_type: String` + per-dataset `body_vocabulary[]`
79
+ # on MeetingCollectionMetadata.
80
+ #
81
+ # Cap: 4 canonical values (no `other` — bodies pick the closest fit).
82
+ MEETING_TYPE_CANONICAL = %w[plenary governing working advisory].freeze
48
83
 
49
84
  MEETING_STATUS = %w[upcoming completed cancelled].freeze
50
85
 
51
86
  AGENDA_STATUS = %w[draft final amended cancelled superseded].freeze
52
87
 
53
- AGENDA_ITEM_KIND = %w[numbered unnumbered header opening closing].freeze
88
+ AGENDA_ITEM_KIND = %w[numbered unnumbered header opening closing aob].freeze
54
89
 
55
- AGENDA_ITEM_OUTCOME = %w[discussed resolved deferred adopted withdrawn].freeze
90
+ AGENDA_ITEM_OUTCOME = %w[discussed resolved deferred adopted withdrawn carried negatived].freeze
56
91
 
57
92
  HOST_TYPE = %w[national_body liaison associate organizer].freeze
58
93
 
59
94
  MEETING_RELATION_TYPE = %w[
60
95
  continues_from continues_to joint_with supersedes superseded_by rescheduled_from rescheduled_to
96
+ parent_of child_of sibling_of depends_on
97
+ finish_to_start finish_to_finish start_to_start start_to_finish
61
98
  ].freeze
62
99
 
63
100
  SOURCE_URL_KIND = %w[
64
- agenda_pdf minutes_pdf resolutions_pdf report_pdf register_url landing_page
101
+ agenda_pdf minutes_pdf decisions_pdf report_pdf register_url landing_page
65
102
  ].freeze
66
103
 
67
- # P2.6 / P2.7 — Attendance and Vote records.
68
104
  PARTICIPATION_STATUS = %w[present absent apologies observer excused].freeze
69
105
 
70
106
  VOTE_TYPE = %w[
71
107
  affirmative negative abstain absent not_applicable
72
108
  ].freeze
109
+
110
+ # --- New in v2.0 (broadened scope) -----------------------------------
111
+
112
+ VENUE_KIND = %w[physical virtual].freeze
113
+
114
+ VIRTUAL_FEATURE = %w[audio video chat phone screen feed].freeze
115
+
116
+ VISIBILITY = %w[public private confidential].freeze
117
+
118
+ ATTENDANCE_ROLE = %w[chair required optional non_participant].freeze
119
+
120
+ # iCalendar PARTSTAT, plain English (no jargon).
121
+ ATTENDANCE_RESPONSE = %w[
122
+ pending confirmed declined tentative delegated
123
+ ].freeze
124
+
125
+ COMPONENT_KIND = %w[
126
+ track session debate breakout bof
127
+ plenary_session working_group_session committee_of_the_whole
128
+ keynote address statement question_time
129
+ opening closing break reception registration networking other
130
+ ].freeze
131
+
132
+ # ComponentKindCanonical — the short abstract set (v2.1, TODO.refactor/46).
133
+ # `other` is a temporary escape while the vocabulary stabilises.
134
+ #
135
+ # Cap: 5 canonical values.
136
+ COMPONENT_KIND_CANONICAL = %w[deliberative working ceremonial break other].freeze
137
+
138
+ MOTION_STATUS = %w[
139
+ introduced seconded debating question_put voting
140
+ carried negatived withdrawn lapsed
141
+ ].freeze
142
+
143
+ # Terminal MotionStatus values — `Motion#pending?` is the complement.
144
+ # Kept as a separate constant (not derived) because the partition is
145
+ # semantic, not lexical; the spec in motion_spec.rb asserts the union
146
+ # equals MOTION_STATUS and the intersection is empty (MECE).
147
+ MOTION_TERMINAL = %w[carried negatived withdrawn lapsed].freeze
148
+
149
+ VOTING_STATUS = %w[called in_progress decided withdrawn deferred].freeze
150
+
151
+ VOTING_METHOD = %w[
152
+ voice division show_of_hands roll_call electronic
153
+ secret_ballot unanimous_consent consensus
154
+ ].freeze
155
+
156
+ VOTING_OUTCOME = %w[passed negatived tied withdrawn].freeze
157
+
158
+ TOPIC_STATUS = %w[open under_discussion decided deferred withdrawn].freeze
159
+
160
+ OFFICER_ROLE = %w[
161
+ chair vice_chair deputy_chair secretary treasurer
162
+ parliamentarian presiding_officer sergeant_at_arms other
163
+ ].freeze
164
+
165
+ RECURRENCE_FREQ = %w[
166
+ secondly minutely hourly daily weekly monthly yearly
167
+ ].freeze
168
+
169
+ # Polymorphic communication channel. OCP: adding a new kind does
170
+ # not change the model — only this enum (or use `other` + extensions).
171
+ CONTACT_METHOD_KIND = %w[
172
+ phone mobile fax email url mail pager message other
173
+ ].freeze
174
+
175
+ # Polymorphic external identifier for a Contact (ORCID, ISNI,
176
+ # Wikidata, ROR, etc.). OCP: adding a new scheme only extends this
177
+ # enum (or use `other` + extensions).
178
+ CONTACT_IDENTIFIER_KIND = %w[
179
+ orcid isni wikidata ror ringgold github other
180
+ ].freeze
73
181
  end
74
182
  end
data/lib/edoxen/error.rb CHANGED
@@ -7,7 +7,7 @@ module Edoxen
7
7
  # Unified validation failure shape. Produced by both:
8
8
  # * SchemaValidator — JSON-Schema violations and YAML syntax errors
9
9
  # carry source: :schema (or :syntax for Psych::SyntaxError).
10
- # * ResolutionCollection.from_yaml rescues in the CLI — model parse
10
+ # * DecisionCollection.from_yaml rescues in the CLI — model parse
11
11
  # failures carry source: :model.
12
12
  #
13
13
  # One type at the seam means callers (CLI, future renderers, tests)
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Edoxen
4
+ # ExtensionAttribute — one typed key/value pair within a
5
+ # MeetingExtension. Polymorphic on value type so consumers don't
6
+ # have to re-parse strings back into Int/Float/Bool/Date.
7
+ #
8
+ # Wire form (v2.1+, per TODO.refactor/47):
9
+ #
10
+ # - key: "quorum"
11
+ # type: "integer"
12
+ # intValue: 7
13
+ #
14
+ # - key: "name"
15
+ # type: "string"
16
+ # value: "quic" # string variant uses bare `value` (v2.0 wire)
17
+ #
18
+ # - key: "start"
19
+ # type: "datetime"
20
+ # dateTimeValue: 2026-07-04T10:00:00Z
21
+ #
22
+ # The `type` discriminator tells consumers which value field to read
23
+ # without probing all six. Exactly one of the value fields should be
24
+ # set; the schema enforces this.
25
+ #
26
+ # String values use the bare `value:` wire name for back-compat with
27
+ # v2.0 fixtures. Typed variants (integer/float/boolean/date/datetime)
28
+ # use camelCase wire names. The gem unifies them behind `#typed_value`.
29
+ class ExtensionAttribute < Lutaml::Model::Serializable
30
+ attribute :key, :string
31
+
32
+ # String variant — Ruby attribute is `value`, wire name is `value`
33
+ # (matches v2.0; `type: "string"` discriminator added in v2.1).
34
+ attribute :value, :string
35
+
36
+ # Typed variants — added in v2.1.
37
+ attribute :integer_value, :integer
38
+ attribute :float_value, :float
39
+ attribute :boolean_value, :boolean
40
+ attribute :date_value, :date
41
+ attribute :date_time_value, :date_time
42
+
43
+ # Wire discriminator: "string" | "integer" | "float" | "boolean" |
44
+ # "date" | "datetime". Tells consumers which value field to read.
45
+ attribute :type, :string
46
+
47
+ key_value do
48
+ map "key", to: :key
49
+ map "type", to: :type
50
+ map "value", to: :value
51
+ map "intValue", to: :integer_value
52
+ map "floatValue", to: :float_value
53
+ map "booleanValue", to: :boolean_value
54
+ map "dateValue", to: :date_value
55
+ map "dateTimeValue", to: :date_time_value
56
+ end
57
+
58
+ # Convenience: read whichever typed value is set. Returns nil when
59
+ # none of the value fields is populated.
60
+ def typed_value
61
+ case type
62
+ when "integer" then integer_value
63
+ when "float" then float_value
64
+ when "boolean" then boolean_value unless boolean_value.nil?
65
+ when "date" then date_value
66
+ when "datetime" then date_time_value
67
+ else
68
+ # Default to string for back-compat with v2.0 (no `type`).
69
+ value
70
+ end
71
+ end
72
+
73
+ # Back-compat alias — v2.1 introduced `value` as the canonical Ruby
74
+ # attribute; the older `string_value` alias keeps callers that
75
+ # already migrated to that name working.
76
+ alias string_value value
77
+ end
78
+ end
@@ -9,6 +9,6 @@ module Edoxen
9
9
  attribute :ref, :string
10
10
  attribute :type, :string, values: Enums::HOST_TYPE
11
11
  attribute :role, :string
12
- attribute :contact, Person
12
+ attribute :contact, Contact
13
13
  end
14
14
  end
@@ -1,15 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Edoxen
4
- # Walks a directory tree of Meeting and ResolutionCollection YAML
4
+ # Walks a directory tree of Meeting and DecisionCollection YAML
5
5
  # files and asserts that every cross-document URN link resolves.
6
6
  #
7
7
  # Invariants enforced:
8
8
  #
9
9
  # * Every Meeting#urn (if set) is referenced by exactly one
10
- # ResolutionCollection#metadata.meeting_urn (if the collection
10
+ # DecisionCollection#metadata.meeting_urn (if the collection
11
11
  # exists in the scanned directory).
12
- # * Every ResolutionCollection#metadata.meeting_urn (if set)
12
+ # * Every DecisionCollection#metadata.meeting_urn (if set)
13
13
  # resolves to a Meeting whose urn matches.
14
14
  #
15
15
  # Use from CLI / scripts via the public `check` class method. Returns
@@ -24,7 +24,7 @@ module Edoxen
24
24
  end
25
25
 
26
26
  # Walk `dir` (recursive scan of *.yaml and *.yml), classify each
27
- # file as Meeting or ResolutionCollection, and assert every
27
+ # file as Meeting or DecisionCollection, and assert every
28
28
  # cross-document URN resolves to a real record.
29
29
  #
30
30
  # @return [Array<LinkError>] empty when all links resolve.
@@ -41,7 +41,7 @@ module Edoxen
41
41
  def check
42
42
  require "yaml"
43
43
 
44
- Dir.glob(File.join(@dir, "**", "*.{yaml,yml}")).sort.each do |file|
44
+ Dir.glob(File.join(@dir, "**", "*.{yaml,yml}")).each do |file|
45
45
  data = safe_load_yaml(file)
46
46
  next unless data.is_a?(Hash)
47
47
 
@@ -56,7 +56,7 @@ module Edoxen
56
56
 
57
57
  errors = []
58
58
 
59
- # ResolutionCollection → Meeting
59
+ # DecisionCollection → Meeting
60
60
  @collections_by_meeting_urn.each do |meeting_urn, file|
61
61
  next if @meetings_by_urn.key?(meeting_urn)
62
62
 
@@ -69,7 +69,7 @@ module Edoxen
69
69
  private
70
70
 
71
71
  def safe_load_yaml(file)
72
- YAML.safe_load(File.read(file))
72
+ YAML.safe_load_file(file)
73
73
  rescue Psych::SyntaxError, ArgumentError
74
74
  nil
75
75
  end
@@ -79,7 +79,7 @@ module Edoxen
79
79
  end
80
80
 
81
81
  def collection_shape?(data)
82
- data["resolutions"].is_a?(Array) || data["metadata"].is_a?(Hash)
82
+ data["decisions"].is_a?(Array) || data["resolutions"].is_a?(Array) || data["metadata"].is_a?(Hash)
83
83
  end
84
84
  end
85
85
  end
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Edoxen
4
- # A monolingual rendering of a Resolution. Mirrors the glossarist
4
+ # A monolingual rendering of a Decision. Mirrors the glossarist
5
5
  # LocalizedConcept pattern: language-agnostic fields live on the
6
- # parent Resolution; per-language content lives here.
6
+ # parent Decision; per-language content lives here.
7
7
  class Localization < Lutaml::Model::Serializable
8
8
  attribute :language_code, :string
9
9
  attribute :script, :string
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Edoxen
4
+ # Mixed into entities that carry a per-language `localizations[]`
5
+ # collection. Provides `in_language(code, fallback:)` and
6
+ # `primary_localization` in one place so Decision, Meeting, etc.
7
+ # share a single implementation.
8
+ #
9
+ # The including class must declare a typed `localizations` attribute
10
+ # whose entries expose `language_code` (ISO 639-3).
11
+ module LocalizationHost
12
+ def in_language(code, fallback: false)
13
+ match = localizations&.find { |loc| loc.language_code == code.to_s }
14
+ return match if match
15
+
16
+ fallback ? localizations&.first : nil
17
+ end
18
+
19
+ def primary_localization
20
+ in_language("eng", fallback: true)
21
+ end
22
+ end
23
+ end
@@ -1,82 +1,106 @@
1
1
  # frozen_string_literal: true
2
2
 
3
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.
4
+ # A single Meeting (event) produces Decisions via Motions and Votings.
5
+ # Carries identity, time, polymorphic venues, officers, agenda, components,
6
+ # attendance, minutes, and URN links to one or more DecisionCollection
7
+ # documents.
7
8
  #
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.
9
+ # Meetings and DecisionCollections are kept as separate documents and
10
+ # joined by URN because they have different lifetimes: agendas exist
11
+ # weeks before a meeting; decisions only after adoption.
13
12
  class Meeting < Lutaml::Model::Serializable
13
+ include LocalizationHost
14
+ include OfficersHost
15
+
14
16
  attribute :identifier, StructuredIdentifier, collection: true
15
17
  attribute :urn, :string
16
18
  attribute :ordinal, :integer
19
+ attribute :series_ref, :string
17
20
  attribute :type, :string, values: Enums::MEETING_TYPE
18
21
  attribute :status, :string, values: Enums::MEETING_STATUS
19
- attribute :year, :integer
22
+ attribute :visibility, :string, values: Enums::VISIBILITY
23
+ # v2.1 (TODO.refactor/46): free-form body-specific label (e.g.
24
+ # "CIML Meeting", "Plenary", "Board Meeting"). Resolves to a short
25
+ # canonical value via the parent collection's `body_vocabulary[]`.
26
+ attribute :body_type, :string
20
27
 
21
28
  attribute :date_range, DateRange
29
+ attribute :recurrence, Recurrence
22
30
 
23
- attribute :committee, :string
24
- attribute :committee_group, :string
25
-
26
- attribute :venues, Location, collection: true
31
+ attribute :venues, Venue, collection: true
27
32
  attribute :general_area, :string
28
33
  attribute :city, :string
29
34
  attribute :country_code, :string
30
- attribute :virtual, :boolean
31
35
 
32
- attribute :chair, Person
33
- attribute :secretary, Person
34
- attribute :host, :string
36
+ attribute :committee, :string
37
+ attribute :committee_group, :string
38
+
39
+ attribute :officers, Officer, collection: true
35
40
  attribute :hosts, HostRef, collection: true
36
41
 
37
42
  attribute :source_urls, SourceUrl, collection: true
38
43
  attribute :landing_url, :string
39
44
  attribute :registration_url, :string
45
+ attribute :note, :string
46
+ attribute :contact, Contact
40
47
 
41
48
  attribute :agenda, Agenda
42
- attribute :schedule, ScheduleItem, collection: true
49
+ attribute :components, MeetingComponent, collection: true
43
50
  attribute :deadlines, Deadline, collection: true
44
51
 
45
52
  attribute :attendance, Attendance, collection: true
46
- attribute :vote_records, VoteRecord, collection: true
47
53
  attribute :minutes, Minutes, collection: true
48
54
 
55
+ # Outcomes (canonical location on Meeting)
56
+ attribute :decisions, Decision, collection: true
57
+ attribute :motions, Motion, collection: true
58
+ attribute :votings, Voting, collection: true
59
+
49
60
  attribute :localizations, MeetingLocalization, collection: true
50
61
  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
62
+ attribute :extensions, MeetingExtension, collection: true
60
63
 
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
64
  def find_agenda_item(label)
69
65
  agenda&.find_item(label)
70
66
  end
71
67
 
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.
68
+ # Resolve the meeting's UN/LOCODE via the canonical `unlocode` gem.
76
69
  def city_entry
77
70
  return nil if city.nil? || city.to_s.empty?
78
71
 
79
72
  Edoxen::ReferenceData.find_unlocode(city)
80
73
  end
74
+
75
+ def secretary
76
+ officers_with_role("secretary").first&.person
77
+ end
78
+
79
+ # All physical venues (polymorphic Venue filter).
80
+ def physical_venues
81
+ venues_by_kind("physical")
82
+ end
83
+
84
+ def virtual_venues
85
+ venues_by_kind("virtual")
86
+ end
87
+
88
+ def hybrid?
89
+ !physical_venues.empty? && !virtual_venues.empty?
90
+ end
91
+
92
+ def virtual_only?
93
+ !virtual_venues.empty? && physical_venues.empty?
94
+ end
95
+
96
+ def physical_only?
97
+ !physical_venues.empty? && virtual_venues.empty?
98
+ end
99
+
100
+ private
101
+
102
+ def venues_by_kind(kind)
103
+ (venues || []).select { |v| v.kind == kind }
104
+ end
81
105
  end
82
106
  end
@@ -1,8 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Edoxen
4
- # Top-level container for many Meetings. Parallel to
5
- # ResolutionCollection.
4
+ # Top-level container for many Meetings. Parallel to DecisionCollection.
6
5
  class MeetingCollection < Lutaml::Model::Serializable
7
6
  attribute :metadata, MeetingCollectionMetadata
8
7
  attribute :meetings, Meeting, collection: true
@@ -2,9 +2,11 @@
2
2
 
3
3
  module Edoxen
4
4
  # Top-level wrapper for many Meetings in a single YAML file. Parallel
5
- # to ResolutionCollection. The metadata block carries display-level
5
+ # to DecisionCollection. The metadata block carries display-level
6
6
  # info (title, source); per-meeting identity lives on each Meeting.
7
7
  class MeetingCollectionMetadata < Lutaml::Model::Serializable
8
+ include BodyVocabularyHost
9
+
8
10
  attribute :title, :string
9
11
  attribute :source, :string
10
12
  end