edoxen 0.7.2 → 0.8.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 (86) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +26 -1
  3. data/.rubocop_todo.yml +1 -1
  4. data/CHANGELOG.md +133 -0
  5. data/README.adoc +414 -182
  6. data/edoxen.gemspec +10 -10
  7. data/lib/edoxen/action.rb +5 -4
  8. data/lib/edoxen/agenda.rb +4 -7
  9. data/lib/edoxen/agenda_item.rb +12 -5
  10. data/lib/edoxen/approval.rb +2 -2
  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 +40 -0
  14. data/lib/edoxen/cli.rb +175 -115
  15. data/lib/edoxen/consideration.rb +6 -4
  16. data/lib/edoxen/contact.rb +35 -0
  17. data/lib/edoxen/contact_collection.rb +26 -0
  18. data/lib/edoxen/contact_identifier.rb +13 -0
  19. data/lib/edoxen/contact_method.rb +16 -0
  20. data/lib/edoxen/date_time_range.rb +16 -0
  21. data/lib/edoxen/deadline.rb +1 -1
  22. data/lib/edoxen/decision.rb +57 -0
  23. data/lib/edoxen/decision_collection.rb +10 -0
  24. data/lib/edoxen/decision_date.rb +9 -0
  25. data/lib/edoxen/decision_metadata.rb +27 -0
  26. data/lib/edoxen/decision_relation.rb +11 -0
  27. data/lib/edoxen/declaration.rb +25 -0
  28. data/lib/edoxen/entity_ref.rb +74 -0
  29. data/lib/edoxen/enums.rb +123 -9
  30. data/lib/edoxen/error.rb +1 -1
  31. data/lib/edoxen/extension_attribute.rb +78 -0
  32. data/lib/edoxen/host_ref.rb +1 -1
  33. data/lib/edoxen/link_checker.rb +143 -34
  34. data/lib/edoxen/localized_name.rb +16 -0
  35. data/lib/edoxen/localized_string.rb +22 -0
  36. data/lib/edoxen/meeting.rb +34 -42
  37. data/lib/edoxen/meeting_collection.rb +1 -2
  38. data/lib/edoxen/meeting_collection_metadata.rb +4 -2
  39. data/lib/edoxen/meeting_component.rb +37 -0
  40. data/lib/edoxen/meeting_extension.rb +25 -0
  41. data/lib/edoxen/meeting_identifier.rb +1 -1
  42. data/lib/edoxen/meeting_series.rb +26 -0
  43. data/lib/edoxen/minutes.rb +3 -4
  44. data/lib/edoxen/minutes_section.rb +12 -13
  45. data/lib/edoxen/motion.rb +51 -0
  46. data/lib/edoxen/name.rb +29 -0
  47. data/lib/edoxen/officer.rb +19 -0
  48. data/lib/edoxen/officers_host.rb +19 -0
  49. data/lib/edoxen/person.rb +5 -9
  50. data/lib/edoxen/physical_venue.rb +15 -0
  51. data/lib/edoxen/recurrence.rb +27 -0
  52. data/lib/edoxen/recurrence_by_day.rb +10 -0
  53. data/lib/edoxen/reference.rb +1 -1
  54. data/lib/edoxen/reference_data.rb +34 -44
  55. data/lib/edoxen/schema_validator.rb +3 -4
  56. data/lib/edoxen/source_url.rb +4 -4
  57. data/lib/edoxen/statement.rb +21 -0
  58. data/lib/edoxen/structured_identifier.rb +2 -2
  59. data/lib/edoxen/topic.rb +39 -0
  60. data/lib/edoxen/topic_asset.rb +14 -0
  61. data/lib/edoxen/topic_document.rb +16 -0
  62. data/lib/edoxen/venue.rb +79 -0
  63. data/lib/edoxen/venue_collection.rb +18 -0
  64. data/lib/edoxen/venue_validator.rb +74 -0
  65. data/lib/edoxen/version.rb +1 -1
  66. data/lib/edoxen/virtual_venue.rb +15 -0
  67. data/lib/edoxen/vote_record.rb +17 -13
  68. data/lib/edoxen/voting.rb +45 -0
  69. data/lib/edoxen/voting_counts.rb +23 -0
  70. data/lib/edoxen.rb +47 -15
  71. data/schema/edoxen.yaml +1376 -246
  72. data/schema/meeting.yaml +1207 -272
  73. data/sig/edoxen.rbs +581 -1
  74. metadata +62 -22
  75. data/lib/edoxen/_metadata.rb.deprecated +0 -57
  76. data/lib/edoxen/localization.rb +0 -18
  77. data/lib/edoxen/location.rb +0 -15
  78. data/lib/edoxen/meeting_localization.rb +0 -15
  79. data/lib/edoxen/resolution.rb +0 -43
  80. data/lib/edoxen/resolution_collection.rb +0 -10
  81. data/lib/edoxen/resolution_date.rb +0 -11
  82. data/lib/edoxen/resolution_metadata.rb +0 -30
  83. data/lib/edoxen/resolution_relation.rb +0 -11
  84. data/lib/edoxen/resolution_set.rb +0 -17
  85. data/lib/edoxen/schedule_item.rb +0 -27
  86. data/lib/edoxen/schedule_item_localization.rb +0 -21
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Edoxen
4
+ # Registry of Contacts indexed by scoped URN. Members carry
5
+ # `urn: urn:edoxen:contact:{scope}:{local-id}`; the collection's
6
+ # `scope` MUST match the scope segment in member URNs.
7
+ #
8
+ # Other documents (Meeting, MeetingComponent, HostRef, etc.) reference
9
+ # contacts via `ref: urn:edoxen:contact:{scope}:{local-id}` and
10
+ # resolve against the matching ContactCollection.
11
+ #
12
+ # Storage: single YAML file (typical) or YAML Stream (one Contact per
13
+ # document, for large registries that need partial updates). Stream
14
+ # loading is a service-layer concern (file-system I/O); the model
15
+ # owns only the (de)serialisation of one collection.
16
+ class ContactCollection < Lutaml::Model::Serializable
17
+ attribute :scope, :string
18
+ attribute :title, LocalizedString, collection: true
19
+ attribute :contacts, Contact, collection: true
20
+ attribute :extensions, MeetingExtension, collection: true
21
+
22
+ def find_by_urn(urn)
23
+ contacts&.find { |c| c.urn == urn }
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Edoxen
4
+ # One polymorphic external identifier for a Contact — ORCID, ISNI,
5
+ # Wikidata QID, ROR, Ringgold, GitHub handle, etc. Replaces the hard-
6
+ # coded `orcid` field. OCP: new identifier schemes are added via the
7
+ # ContactIdentifierKind enum (or `other` + extensions).
8
+ class ContactIdentifier < Lutaml::Model::Serializable
9
+ attribute :kind, :string, values: Enums::CONTACT_IDENTIFIER_KIND
10
+ attribute :value, :string
11
+ attribute :extensions, MeetingExtension, collection: true
12
+ end
13
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Edoxen
4
+ # One polymorphic communication channel — phone, email, fax, url, mail,
5
+ # etc. Replaces the hard-coded `email` / `phone` fields that used to
6
+ # live on Person and Venue. New channel kinds are added via the
7
+ # ContactMethodKind enum (or `other` + extensions for adopter-specific
8
+ # channels); the model itself never needs to change (OCP).
9
+ class ContactMethod < Lutaml::Model::Serializable
10
+ attribute :kind, :string, values: Enums::CONTACT_METHOD_KIND
11
+ attribute :value, :string
12
+ attribute :label, :string
13
+ attribute :primary, :boolean
14
+ attribute :extensions, MeetingExtension, collection: true
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Edoxen
4
+ # Start + end pair with sub-day precision. Parallel to DateRange;
5
+ # use when day granularity is insufficient — e.g.
6
+ # `Meeting#occurred_date_range` (a meeting that ran 09:00–11:30).
7
+ #
8
+ # The two are intentionally separate types so the granularity is
9
+ # visible at the type level: a caller who sees DateRange knows
10
+ # it is day-granularity; a caller who sees DateTimeRange knows
11
+ # sub-day precision is available.
12
+ class DateTimeRange < Lutaml::Model::Serializable
13
+ attribute :start, :date_time
14
+ attribute :end, :date_time
15
+ end
16
+ end
@@ -5,6 +5,6 @@ module Edoxen
5
5
  # registration cutoff, report-submission deadline, etc.
6
6
  class Deadline < Lutaml::Model::Serializable
7
7
  attribute :date, :date
8
- attribute :description, :string
8
+ attribute :description, LocalizedString, collection: true
9
9
  end
10
10
  end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Edoxen
4
+ # A formal Decision — the base type for any outcome adopted by a Meeting.
5
+ # Was `Resolution` in legacy; renamed in 1.0 because the same formal concept
6
+ # has many names across bodies (resolution, order, ruling, determination,
7
+ # finding, opinion). `Resolution` is one `kind` value, not a class name.
8
+ #
9
+ # 1.0 (per-field localization, ISO 24229):
10
+ # - Removed `localizations[]` collection.
11
+ # - Per-field LocalizedString for title, subject, message, considering.
12
+ class Decision < Lutaml::Model::Serializable
13
+ attribute :identifier, StructuredIdentifier, collection: true
14
+ attribute :kind, :string, values: Enums::DECISION_KIND
15
+ attribute :status, :string, values: Enums::DECISION_STATUS
16
+ attribute :doi, :string
17
+ attribute :urn, :string
18
+ attribute :agenda_item, :string
19
+ attribute :dates, DecisionDate, collection: true
20
+ attribute :categories, :string, collection: true
21
+ attribute :meeting, MeetingIdentifier
22
+ attribute :relations, DecisionRelation, collection: true
23
+ attribute :urls, Url, collection: true
24
+ attribute :brought_by_motions, :string, collection: true
25
+ attribute :about_topics, :string, collection: true
26
+ attribute :made_in_component, :string
27
+ attribute :body_type, :string
28
+ attribute :title, LocalizedString, collection: true
29
+ attribute :subject, LocalizedString, collection: true
30
+ attribute :message, LocalizedString, collection: true
31
+ attribute :considering, LocalizedString, collection: true
32
+ attribute :considerations, Consideration, collection: true
33
+ attribute :approvals, Approval, collection: true
34
+ attribute :actions, Action, collection: true
35
+ attribute :extensions, MeetingExtension, collection: true
36
+
37
+ # --- 1.0 derivation accessors (1.0 design review) -------------------
38
+
39
+ def brought_by_motions_in(meeting:)
40
+ return [] unless meeting && urn
41
+
42
+ meeting.motions.select do |motion|
43
+ motion.resulting_decision == urn ||
44
+ motion.resulting_decision_ref&.urn == urn
45
+ end
46
+ end
47
+
48
+ def component_in(meeting:)
49
+ return nil unless meeting && made_in_component
50
+
51
+ meeting.components&.find do |component|
52
+ component.identifier == made_in_component ||
53
+ component.urn == made_in_component
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Edoxen
4
+ # Top-level container for a published decision collection: metadata
5
+ # plus the list of decisions. Was ResolutionCollection in legacy.
6
+ class DecisionCollection < Lutaml::Model::Serializable
7
+ attribute :metadata, DecisionMetadata
8
+ attribute :decisions, Decision, collection: true
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Edoxen
4
+ # Date with semantic kind. Was ResolutionDate in legacy.
5
+ class DecisionDate < Lutaml::Model::Serializable
6
+ attribute :date, :date
7
+ attribute :type, :string, values: Enums::DECISION_DATE_TYPE
8
+ end
9
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Edoxen
4
+ # Collection-level metadata: the localized title, the meeting date,
5
+ # the source secretariat, per-language source PDFs, and the host venue.
6
+ class DecisionMetadata < Lutaml::Model::Serializable
7
+ include BodyVocabularyHost
8
+
9
+ attribute :title, LocalizedString, collection: true
10
+ attribute :date, :date
11
+ attribute :source, :string
12
+ attribute :source_urls, SourceUrl, collection: true
13
+ attribute :city, :string
14
+ attribute :country_code, :string
15
+
16
+ # URN back-reference to the Meeting that produced this collection.
17
+ attribute :meeting_urn, :string
18
+
19
+ attribute :extensions, MeetingExtension, collection: true
20
+
21
+ def city_entry
22
+ return nil if city.nil? || city.to_s.empty?
23
+
24
+ Edoxen::ReferenceData.find_unlocode(city)
25
+ end
26
+ end
27
+ end
@@ -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,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Edoxen
4
+ # A formal declaration made by one or more meeting members.
5
+ # Covers the two BS 0:2006 declaration types via the `kind`
6
+ # discriminator: `conflict_of_interest` and `ipr`.
7
+ #
8
+ # The IPR-specific fields (`ipr_subject_ref`, `ipr_target_ref`)
9
+ # are populated only when `kind == "ipr"`. This mirrors edoxen's
10
+ # "flat class, kind discriminates which subset is populated"
11
+ # pattern (cf. Venue: physical + virtual fields on one flat
12
+ # class).
13
+ #
14
+ # Attachment points: `Meeting#declarations[]` (per-meeting) and
15
+ # `Topic#declarations[]` (standing position that travels with
16
+ # the topic).
17
+ class Declaration < Lutaml::Model::Serializable
18
+ attribute :kind, :string, values: Enums::DECLARATION_KIND
19
+ attribute :description, LocalizedString, collection: true
20
+ attribute :party, Person, collection: true
21
+ attribute :ipr_subject_ref, EntityRef
22
+ attribute :ipr_target_ref, EntityRef
23
+ attribute :extensions, MeetingExtension, collection: true
24
+ end
25
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Edoxen
4
+ # EntityRef — a typed cross-reference between entities (1.0, 1.0 design review).
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`). 1.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
+ # (1.0 design review + 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,33 @@ 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 — short abstract classification of decision
40
+ # kinds. Cap: 5 canonical values (hard limit per the design review).
41
+ DECISION_KIND_CANONICAL = %w[decision recommendation statement finding other].freeze
30
42
 
31
- RESOLUTION_RELATION_TYPE = %w[
32
- annexOf hasAnnex updates refines replaces considers
43
+ # DecisionStatus — lifecycle state machine.
44
+ DECISION_STATUS = %w[
45
+ draft proposed under_consideration
46
+ decided negatived withdrawn deferred
33
47
  ].freeze
34
48
 
35
- RESOLUTION_DATE_TYPE = %w[adoption drafted discussed].freeze
49
+ DECISION_RELATION_TYPE = %w[
50
+ annex_of has_annex updates refines replaces considers cites
51
+ ].freeze
52
+
53
+ DECISION_DATE_TYPE = %w[
54
+ adoption drafted discussed proposed decided negatived withdrawn published effective
55
+ ].freeze
36
56
 
37
57
  APPROVAL_TYPE = %w[affirmative negative].freeze
38
58
 
@@ -44,31 +64,125 @@ module Edoxen
44
64
  # Mirrors edoxen-model/models/{meeting,agenda,...}_*.lutaml.
45
65
  # schema/meeting.yaml references these via $defs/<EnumName>.enum.
46
66
 
47
- MEETING_TYPE = %w[plenary working_group task_group ad_hoc joint conference_session].freeze
67
+ MEETING_TYPE = %w[
68
+ plenary working_group task_group ad_hoc joint general_assembly
69
+ committee subcommittee conference workshop seminar webinar hearing
70
+ markup board_meeting annual_general_meeting other
71
+ ].freeze
72
+
73
+ # MeetingTypeCanonical — short abstract classification of meeting
74
+ # types. Cap: 4 canonical values (no `other`).
75
+ MEETING_TYPE_CANONICAL = %w[plenary governing working advisory].freeze
48
76
 
49
77
  MEETING_STATUS = %w[upcoming completed cancelled].freeze
50
78
 
51
79
  AGENDA_STATUS = %w[draft final amended cancelled superseded].freeze
52
80
 
53
- AGENDA_ITEM_KIND = %w[numbered unnumbered header opening closing].freeze
81
+ AGENDA_ITEM_KIND = %w[numbered unnumbered header opening closing aob].freeze
54
82
 
55
- AGENDA_ITEM_OUTCOME = %w[discussed resolved deferred adopted withdrawn].freeze
83
+ AGENDA_ITEM_OUTCOME = %w[discussed resolved deferred adopted withdrawn carried negatived].freeze
56
84
 
57
85
  HOST_TYPE = %w[national_body liaison associate organizer].freeze
58
86
 
59
87
  MEETING_RELATION_TYPE = %w[
60
88
  continues_from continues_to joint_with supersedes superseded_by rescheduled_from rescheduled_to
89
+ parent_of child_of sibling_of depends_on
90
+ finish_to_start finish_to_finish start_to_start start_to_finish
61
91
  ].freeze
62
92
 
63
93
  SOURCE_URL_KIND = %w[
64
- agenda_pdf minutes_pdf resolutions_pdf report_pdf register_url landing_page
94
+ agenda_pdf minutes_pdf decisions_pdf report_pdf register_url landing_page
65
95
  ].freeze
66
96
 
67
- # P2.6 / P2.7 — Attendance and Vote records.
68
97
  PARTICIPATION_STATUS = %w[present absent apologies observer excused].freeze
69
98
 
70
99
  VOTE_TYPE = %w[
71
100
  affirmative negative abstain absent not_applicable
72
101
  ].freeze
102
+
103
+ # --- New in 1.0 (broadened scope) -----------------------------------
104
+
105
+ VENUE_KIND = %w[physical virtual].freeze
106
+
107
+ VIRTUAL_FEATURE = %w[audio video chat phone screen feed].freeze
108
+
109
+ VISIBILITY = %w[public private confidential].freeze
110
+
111
+ ATTENDANCE_ROLE = %w[chair required optional non_participant].freeze
112
+
113
+ # iCalendar PARTSTAT, plain English (no jargon).
114
+ ATTENDANCE_RESPONSE = %w[
115
+ pending confirmed declined tentative delegated
116
+ ].freeze
117
+
118
+ COMPONENT_KIND = %w[
119
+ track session debate breakout bof
120
+ plenary_session working_group_session committee_of_the_whole
121
+ keynote address statement question_time
122
+ opening closing break reception registration networking other
123
+ ].freeze
124
+
125
+ # ComponentKindCanonical — the short abstract set (1.0, 1.0 design review).
126
+ # `other` is a temporary escape while the vocabulary stabilises.
127
+ #
128
+ # Cap: 5 canonical values.
129
+ COMPONENT_KIND_CANONICAL = %w[deliberative working ceremonial break other].freeze
130
+
131
+ MOTION_STATUS = %w[
132
+ introduced seconded debating question_put voting
133
+ carried negatived withdrawn lapsed
134
+ ].freeze
135
+
136
+ # Terminal MotionStatus values — `Motion#pending?` is the complement.
137
+ # Kept as a separate constant (not derived) because the partition is
138
+ # semantic, not lexical; the spec in motion_spec.rb asserts the union
139
+ # equals MOTION_STATUS and the intersection is empty (MECE).
140
+ MOTION_TERMINAL = %w[carried negatived withdrawn lapsed].freeze
141
+
142
+ VOTING_STATUS = %w[called in_progress decided withdrawn deferred].freeze
143
+
144
+ VOTING_METHOD = %w[
145
+ voice division show_of_hands roll_call electronic
146
+ secret_ballot unanimous_consent consensus
147
+ ].freeze
148
+
149
+ VOTING_OUTCOME = %w[passed negatived tied withdrawn].freeze
150
+
151
+ TOPIC_STATUS = %w[open under_discussion decided deferred withdrawn].freeze
152
+
153
+ OFFICER_ROLE = %w[
154
+ chair vice_chair deputy_chair secretary treasurer
155
+ parliamentarian presiding_officer sergeant_at_arms other
156
+ ].freeze
157
+
158
+ RECURRENCE_FREQ = %w[
159
+ secondly minutely hourly daily weekly monthly yearly
160
+ ].freeze
161
+
162
+ # Polymorphic communication channel. OCP: adding a new kind does
163
+ # not change the model — only this enum (or use `other` + extensions).
164
+ CONTACT_METHOD_KIND = %w[
165
+ phone mobile fax email url mail pager message other
166
+ ].freeze
167
+
168
+ # Polymorphic external identifier for a Contact (ORCID, ISNI,
169
+ # Wikidata, ROR, etc.). OCP: adding a new scheme only extends this
170
+ # enum (or use `other` + extensions).
171
+ CONTACT_IDENTIFIER_KIND = %w[
172
+ orcid isni wikidata ror ringgold github other
173
+ ].freeze
174
+
175
+ # --- BS 0:2006 meeting-minutes concepts --------------------------------
176
+
177
+ # StatementKind — discriminates the three BS 0:2006 statement
178
+ # types. Adding a new kind is a one-line enum extension; the
179
+ # Statement model itself never needs to change (OCP).
180
+ STATEMENT_KIND = %w[statement comment standpoint].freeze
181
+
182
+ # DeclarationKind — discriminates the two BS 0:2006 declaration
183
+ # types (conflict of interest, IPR). Adding a new kind is a
184
+ # one-line enum extension; the Declaration model itself never
185
+ # needs to change (OCP).
186
+ DECLARATION_KIND = %w[conflict_of_interest ipr].freeze
73
187
  end
74
188
  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 (1.0+, per 1.0 design review):
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` (1.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
+ # 1.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 1.0; `type: "string"` discriminator added in 1.0).
34
+ attribute :value, :string
35
+
36
+ # Typed variants — added in 1.0.
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 1.0 (no `type`).
69
+ value
70
+ end
71
+ end
72
+
73
+ # Back-compat alias — 1.0 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