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
data/lib/edoxen/agenda.rb CHANGED
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Edoxen
4
- # The business-order document of a Meeting. Distinct from the
5
- # timetable (Schedule) — the Agenda orders topics; the Schedule
6
- # orders time slots.
4
+ # The business-order document of a Meeting. Distinct from the timetable
5
+ # (MeetingComponent) — the Agenda orders topics; components order time
6
+ # slots. Opening/closing sessions are components, not agenda entries.
7
7
  #
8
8
  # An Agenda may be versioned independently of the Meeting: a draft
9
9
  # agenda circulates weeks before; a final agenda at meeting time;
@@ -14,11 +14,8 @@ module Edoxen
14
14
  attribute :status, :string, values: Enums::AGENDA_STATUS
15
15
  attribute :source_doc, :string
16
16
  attribute :items, AgendaItem, collection: true
17
- attribute :opening_session, ScheduleItem
18
- attribute :closing_session, ScheduleItem
17
+ attribute :extensions, MeetingExtension, collection: true
19
18
 
20
- # Find an agenda item by its label (e.g., "5.2"). Returns nil
21
- # when no item matches.
22
19
  def find_item(label)
23
20
  items&.find { |item| item.label == label.to_s }
24
21
  end
@@ -3,8 +3,12 @@
3
3
  module Edoxen
4
4
  # One entry on an Agenda. `label` is the visible identifier (e.g.,
5
5
  # "5.2"); `kind` discriminates numbered/unnumbered/header/opening/
6
- # closing; `outcome` records what happened; `resolution_ref`
7
- # optionally links to the URN of the resolution this item produced.
6
+ # closing/aob; `outcome` records what happened; `decision_ref`
7
+ # optionally links to the URN of the decision this item produced.
8
+ #
9
+ # Topics (0..*) are the subject(s) of discussion at this agenda item.
10
+ # AOB (Any Other Business) items have 0 topics until raised during
11
+ # the meeting.
8
12
  class AgendaItem < Lutaml::Model::Serializable
9
13
  attribute :label, :string
10
14
  attribute :kind, :string, values: Enums::AGENDA_ITEM_KIND
@@ -12,6 +16,9 @@ module Edoxen
12
16
  attribute :description, :string
13
17
  attribute :references, Reference, collection: true
14
18
  attribute :outcome, :string, values: Enums::AGENDA_ITEM_OUTCOME
15
- attribute :resolution_ref, :string
19
+ attribute :decision_ref, :string
20
+ attribute :topics, Topic, collection: true
21
+ attribute :components, :string, collection: true
22
+ attribute :extensions, MeetingExtension, collection: true
16
23
  end
17
24
  end
@@ -6,7 +6,7 @@ module Edoxen
6
6
  class Approval < Lutaml::Model::Serializable
7
7
  attribute :type, :string, values: Enums::APPROVAL_TYPE
8
8
  attribute :degree, :string, values: Enums::APPROVAL_DEGREE
9
- attribute :date, ResolutionDate
9
+ attribute :date, DecisionDate
10
10
  attribute :message, :string
11
11
  end
12
12
  end
@@ -1,23 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
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.
4
+ # One attendance record per person. Combines:
5
+ # - status: ParticipationStatus (in-meeting observation: present/absent/
6
+ # apologies/observer/excused)
7
+ # - role: AttendanceRole (structural importance: chair/required/optional/
8
+ # non_participant — from iCalendar ROLE, plain English)
9
+ # - response: AttendanceResponse (RSVP state: pending/confirmed/declined/
10
+ # tentative/delegated — from iCalendar PARTSTAT, plain English)
8
11
  class Attendance < Lutaml::Model::Serializable
9
12
  attribute :person, Person
10
13
  attribute :status, :string, values: Enums::PARTICIPATION_STATUS
14
+ attribute :role, :string, values: Enums::ATTENDANCE_ROLE
15
+ attribute :response, :string, values: Enums::ATTENDANCE_RESPONSE
11
16
  attribute :affiliation, :string
12
17
  attribute :proxy_for, Person
13
18
  attribute :notes, :string
19
+ attribute :extensions, MeetingExtension, collection: true
14
20
 
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
+ def present?
22
+ status == "present"
23
+ end
24
+
25
+ def declined?
26
+ response == "declined"
21
27
  end
22
28
  end
23
29
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Edoxen
4
+ # BodyVocabularyEntry — one entry in a per-dataset body_vocabulary
5
+ # list (v2.1, TODO.refactor/46).
6
+ #
7
+ # Maps a free-form `body_type` (e.g. "CIML Meeting", "Plenary") to a
8
+ # short canonical value (e.g. "plenary"). Bodies declare their
9
+ # vocabulary on the collection metadata; consumers look up the
10
+ # canonical_type via the parent collection's vocabulary.
11
+ #
12
+ # SSOT: the body_vocabulary list on collection metadata is the single
13
+ # source of truth for body_type → canonical_type resolution within
14
+ # that dataset.
15
+ #
16
+ # Permissive: when no vocabulary entry matches a body_type, the gem
17
+ # returns the body_type string itself (with a warning at debug
18
+ # level). Strict mode is a v3.x concern.
19
+ class BodyVocabularyEntry < Lutaml::Model::Serializable
20
+ attribute :body_type, :string
21
+ attribute :canonical_type, :string
22
+ attribute :definition, :string
23
+ end
24
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Edoxen
4
+ # Mixed into metadata classes that carry a per-dataset
5
+ # `body_vocabulary[]` collection (v2.1, TODO.refactor/46). Provides
6
+ # the attribute declaration and the `canonical_type_for` lookup in
7
+ # one place so DecisionMetadata and MeetingCollectionMetadata share
8
+ # a single implementation.
9
+ #
10
+ # Permissive by design: when no vocabulary entry matches a
11
+ # body_type, the body_type string itself is returned (with no
12
+ # warning). Strict mode is a v3.x concern.
13
+ module BodyVocabularyHost
14
+ def self.included(base)
15
+ base.attribute :body_vocabulary, BodyVocabularyEntry, collection: true
16
+ end
17
+
18
+ # Resolve a body_type to its canonical_type via this collection's
19
+ # vocabulary. Returns the body_type unchanged when nil/empty or
20
+ # when no matching entry exists.
21
+ def canonical_type_for(body_type)
22
+ return body_type if body_type.nil? || body_type.to_s.empty?
23
+ return body_type unless body_vocabulary
24
+
25
+ entry = body_vocabulary.find { |e| e.body_type == body_type }
26
+ entry ? entry.canonical_type : body_type
27
+ end
28
+ end
29
+ end
data/lib/edoxen/cli.rb CHANGED
@@ -5,17 +5,50 @@ require "fileutils"
5
5
 
6
6
  module Edoxen
7
7
  class Cli < Thor
8
+ # Each top-level YAML document is either a Decision collection or a
9
+ # Meeting (single or collection). The two share the same batch
10
+ # scaffold (expand → header → loop → tally → exit) but differ in
11
+ # which schema validates them and which model class loads them.
12
+ #
13
+ # A Profile bundles those two facts so the Thor command methods are
14
+ # one-line delegations. Adding a third document kind (e.g. an
15
+ # attendance-only file) is one Profile constant + one desc block —
16
+ # no new scaffolding.
17
+ Profile = Struct.new(:name, :schema_file, :loader) do
18
+ def schema_path
19
+ File.expand_path("../../schema/#{schema_file}", __dir__)
20
+ end
21
+
22
+ # Returns the loaded model object, raising StandardError on failure.
23
+ # Single-vs-collection detection is up to the loader (Meeting side
24
+ # sniffs the top-level key; Decision side always returns a
25
+ # DecisionCollection).
26
+ def load(content)
27
+ loader.call(content)
28
+ end
29
+ end
30
+
31
+ PROFILES = {
32
+ decisions: Profile.new(
33
+ "decisions", "edoxen.yaml",
34
+ ->(content) { DecisionCollection.from_yaml(content) }
35
+ ),
36
+ meetings: Profile.new(
37
+ "meetings", "meeting.yaml",
38
+ lambda do |content|
39
+ data = YAML.safe_load(content, permitted_classes: [Date, Time])
40
+ if data.is_a?(Hash) && data.key?("meetings")
41
+ MeetingCollection.from_yaml(content)
42
+ else
43
+ Meeting.from_yaml(content)
44
+ end
45
+ end
46
+ )
47
+ }.freeze
48
+
8
49
  # Deep module behind the per-command interface. Owns the
9
50
  # expand/sort/empty/header/loop/tally/summary/exit scaffold so
10
- # `validate` and `normalize` collapse to per-file blocks.
11
- #
12
- # Commands call `Batch.run(self, pattern, header:)` and yield a block
13
- # that returns `Result.ok(message)` or `Result.bad(errors)`. The
14
- # batch runner prints progress, tallies, prints the summary, and
15
- # exits with the right code.
16
- #
17
- # In-process; no adapter. The seam is the call site in each
18
- # command method — internal to the CLI.
51
+ # `validate` and `normalize` collapse to one-line delegations.
19
52
  module Batch
20
53
  # Per-file outcome. `ok` carries an optional message appended to
21
54
  # the success indicator (e.g. "NORMALIZED → /out/path"). `bad`
@@ -32,11 +65,6 @@ module Edoxen
32
65
 
33
66
  module_function
34
67
 
35
- # Run a batch over every YAML file matching `pattern`.
36
- #
37
- # Yields each file path to the caller's block. Block must return
38
- # a Batch::Result. The batch runner handles progress output,
39
- # tallies, summary, and the exit code.
40
68
  def run(cli, pattern, header:, summary_extra: [])
41
69
  files = expand(pattern)
42
70
  if files.empty?
@@ -84,62 +112,32 @@ module Edoxen
84
112
 
85
113
  package_name "edoxen"
86
114
 
115
+ # --- Decision-side commands -----------------------------------------
116
+
87
117
  desc "validate YAML_FILE_PATTERN",
88
- "Validate one or more Edoxen YAML files against the schema and the model."
118
+ "Validate Decision YAML file(s) against schema/edoxen.yaml and the model."
89
119
 
90
120
  def validate(pattern)
91
- validator = SchemaValidator.new
92
- Batch.run(self, pattern, header: "🔍 Validating") do |file|
93
- schema_errors = validator.validate_file(file)
94
- model_errors = collect_model_errors(file)
95
- if schema_errors.empty? && model_errors.empty?
96
- Batch::Result.ok("VALID")
97
- else
98
- errors = (schema_errors + model_errors).map(&:to_clickable_format)
99
- Batch::Result.bad(errors)
100
- end
101
- end
121
+ run_validate(PROFILES.fetch(:decisions), pattern)
102
122
  end
103
123
 
104
124
  desc "normalize YAML_FILE_PATTERN",
105
- "Round-trip YAML file(s) through the Edoxen model (--output DIR or --inplace)."
125
+ "Round-trip Decision YAML file(s) through the model (--output DIR or --inplace)."
106
126
 
107
127
  option :output, type: :string, desc: "Output directory for normalized files"
108
128
  option :inplace, type: :boolean, desc: "Modify files in place (no backup)"
109
129
 
110
130
  def normalize(pattern)
111
- unless valid_normalize_options?
112
- say normalize_options_error, :red
113
- exit 1
114
- end
115
-
116
- summary_extra = [
117
- [" Output directory", options[:output]],
118
- [" Mode", options[:inplace] ? "in place" : "--output"]
119
- ].compact
120
-
121
- Batch.run(self, pattern, header: "🔄 Normalizing", summary_extra: summary_extra) do |file|
122
- Batch::Result.ok(normalize_file(file))
123
- rescue StandardError => e
124
- Batch::Result.bad(["#{file}:1:1: #{e.message}"])
125
- end
131
+ run_normalize(PROFILES.fetch(:decisions), pattern)
126
132
  end
127
133
 
134
+ # --- Meeting-side commands ------------------------------------------
135
+
128
136
  desc "validate-meetings YAML_FILE_PATTERN",
129
137
  "Validate Meeting/Agenda YAML file(s) against schema/meeting.yaml."
130
138
 
131
139
  def validate_meetings(pattern)
132
- validator = SchemaValidator.new(meeting_schema_path)
133
- Batch.run(self, pattern, header: "🔍 Validating meetings") do |file|
134
- schema_errors = validator.validate_file(file)
135
- model_errors = collect_meeting_model_errors(file)
136
- if schema_errors.empty? && model_errors.empty?
137
- Batch::Result.ok("VALID")
138
- else
139
- errors = (schema_errors + model_errors).map(&:to_clickable_format)
140
- Batch::Result.bad(errors)
141
- end
142
- end
140
+ run_validate(PROFILES.fetch(:meetings), pattern)
143
141
  end
144
142
 
145
143
  desc "normalize-meetings YAML_FILE_PATTERN",
@@ -149,24 +147,13 @@ module Edoxen
149
147
  option :inplace, type: :boolean, desc: "Modify files in place (no backup)"
150
148
 
151
149
  def normalize_meetings(pattern)
152
- unless valid_normalize_options?
153
- say normalize_options_error, :red
154
- exit 1
155
- end
156
-
157
- summary_extra = [
158
- [" Output directory", options[:output]],
159
- [" Mode", options[:inplace] ? "in place" : "--output"]
160
- ].compact
161
-
162
- Batch.run(self, pattern, header: "🔄 Normalizing meetings", summary_extra: summary_extra) do |file|
163
- Batch::Result.ok(normalize_meeting_file(file))
164
- rescue StandardError => e
165
- Batch::Result.bad(["#{file}:1:1: #{e.message}"])
166
- end
150
+ run_normalize(PROFILES.fetch(:meetings), pattern)
167
151
  end
168
152
 
153
+ # --- Reference-data lookups -----------------------------------------
154
+
169
155
  desc "unlocode CODE", "Resolve a UN/LOCODE via the canonical registry."
156
+
170
157
  def unlocode(code)
171
158
  entry = Edoxen::ReferenceData.find_unlocode(code)
172
159
  if entry.nil?
@@ -180,18 +167,59 @@ module Edoxen
180
167
  say " Subdiv: #{entry.subdivision}" if entry.subdivision
181
168
  coords = entry.coordinates
182
169
  say " Coords: #{coords}" if coords
183
- funcs = entry.functions.compact.map(&:to_s)
170
+ funcs = entry.function_codes.compact
184
171
  say " Functions: #{funcs.join(", ")}" unless funcs.empty?
185
172
  end
186
173
 
174
+ desc "iata CODE", "Resolve an IATA airport/city code via the canonical registry."
175
+
176
+ def iata(code)
177
+ entry = Edoxen::ReferenceData.find_iata(code)
178
+ if entry.nil?
179
+ say "No entry for #{code.upcase} in the IATA registry.", :red
180
+ exit 1
181
+ end
182
+
183
+ say "IATA: #{entry.code}", :blue
184
+ say " Name: #{entry.name}"
185
+ say " Country: #{entry.country_iso2}"
186
+ end
187
+
187
188
  private
188
189
 
189
- def meeting_schema_path
190
- File.expand_path("../../schema/meeting.yaml", __dir__)
190
+ def run_validate(profile, pattern)
191
+ validator = SchemaValidator.new(profile.schema_path)
192
+ Batch.run(self, pattern, header: "🔍 Validating #{profile.name}") do |file|
193
+ schema_errors = validator.validate_file(file)
194
+ model_errors = collect_model_errors(profile, file)
195
+ if schema_errors.empty? && model_errors.empty?
196
+ Batch::Result.ok("VALID")
197
+ else
198
+ Batch::Result.bad((schema_errors + model_errors).map(&:to_clickable_format))
199
+ end
200
+ end
191
201
  end
192
202
 
193
- def collect_model_errors(file)
194
- ResolutionCollection.from_yaml(File.read(file))
203
+ def run_normalize(profile, pattern)
204
+ unless valid_normalize_options?
205
+ say normalize_options_error, :red
206
+ exit 1
207
+ end
208
+
209
+ summary_extra = [
210
+ [" Output directory", options[:output]],
211
+ [" Mode", options[:inplace] ? "in place" : "--output"]
212
+ ].compact
213
+
214
+ Batch.run(self, pattern, header: "🔄 Normalizing #{profile.name}", summary_extra: summary_extra) do |file|
215
+ Batch::Result.ok(normalize_file(profile, file))
216
+ rescue StandardError => e
217
+ Batch::Result.bad(["#{file}:1:1: #{e.message}"])
218
+ end
219
+ end
220
+
221
+ def collect_model_errors(profile, file)
222
+ profile.load(File.read(file))
195
223
  []
196
224
  rescue StandardError => e
197
225
  [Edoxen::ValidationError.new(
@@ -201,27 +229,26 @@ module Edoxen
201
229
  )]
202
230
  end
203
231
 
204
- def collect_meeting_model_errors(file)
205
- load_meeting_document(file)
206
- []
232
+ def normalize_file(profile, file)
233
+ original = File.read(file)
234
+ schema_comment = extract_yaml_language_server_comment(original)
235
+ normalized = profile.load(original).to_yaml
236
+ write_normalized(file, normalized, schema_comment)
207
237
  rescue StandardError => e
208
- [Edoxen::ValidationError.new(
209
- file: file, line: 1, column: 1,
210
- message_text: "Meeting model parsing failed: #{e.message}",
211
- source: Edoxen::ValidationError::SOURCE_MODEL
212
- )]
238
+ raise "#{file}: #{e.message}"
213
239
  end
214
240
 
215
- # A meeting YAML may be either a single Meeting or a
216
- # MeetingCollection. Detect by the presence of a top-level
217
- # `meetings:` key and parse accordingly. Returns whichever
218
- # model object was successfully constructed.
219
- def load_meeting_document(file)
220
- data = YAML.safe_load(File.read(file), permitted_classes: [Date, Time])
221
- if data.is_a?(Hash) && data.key?("meetings")
222
- MeetingCollection.from_yaml(File.read(file))
241
+ def write_normalized(file, normalized, schema_comment)
242
+ normalized = "#{schema_comment}\n#{normalized}" if schema_comment
243
+
244
+ if options[:inplace]
245
+ File.write(file, normalized)
246
+ "NORMALIZED"
223
247
  else
224
- Meeting.from_yaml(File.read(file))
248
+ out = File.join(options[:output], File.basename(file))
249
+ FileUtils.mkdir_p(File.dirname(out))
250
+ File.write(out, normalized)
251
+ "NORMALIZED → #{out}"
225
252
  end
226
253
  end
227
254
 
@@ -244,35 +271,5 @@ module Edoxen
244
271
  "Error: Must specify either --output or --inplace option"
245
272
  end
246
273
  end
247
-
248
- # Writes the normalized YAML either to the original file (--inplace)
249
- # or under the --output directory. Returns a one-line status message
250
- # for the batch runner to print after the ✅.
251
- def normalize_file(file)
252
- original = File.read(file)
253
- yaml_language_server_comment = extract_yaml_language_server_comment(original)
254
- normalized = ResolutionCollection.from_yaml(original).to_yaml
255
- write_normalized(file, normalized, yaml_language_server_comment)
256
- end
257
-
258
- def normalize_meeting_file(file)
259
- yaml_language_server_comment = extract_yaml_language_server_comment(File.read(file))
260
- normalized = load_meeting_document(file).to_yaml
261
- write_normalized(file, normalized, yaml_language_server_comment)
262
- end
263
-
264
- def write_normalized(file, normalized, yaml_language_server_comment)
265
- normalized = "#{yaml_language_server_comment}\n#{normalized}" if yaml_language_server_comment
266
-
267
- if options[:inplace]
268
- File.write(file, normalized)
269
- "NORMALIZED"
270
- else
271
- out = File.join(options[:output], File.basename(file))
272
- FileUtils.mkdir_p(File.dirname(out))
273
- File.write(out, normalized)
274
- "NORMALIZED → #{out}"
275
- end
276
- end
277
274
  end
278
275
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Edoxen
4
+ # ComponentLocalization — per-language content for a MeetingComponent.
5
+ class ComponentLocalization < Lutaml::Model::Serializable
6
+ attribute :language_code, :string
7
+ attribute :script, :string
8
+ attribute :title, :string
9
+ attribute :description, :string
10
+ end
11
+ end
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Edoxen
4
- # Basis for a resolution: a verb (having, noting, considering, ...) plus
4
+ # Basis for a Decision: a verb (having, noting, considering, ...) plus
5
5
  # one effective date and the elaborated reasoning.
6
6
  class Consideration < Lutaml::Model::Serializable
7
7
  attribute :type, :string, values: Enums::CONSIDERATION_TYPE
8
- attribute :date_effective, ResolutionDate
8
+ attribute :date_effective, DecisionDate
9
9
  attribute :message, :string
10
10
  end
11
11
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Edoxen
4
+ # VCARD-like abstract contact. Generalises Person for cases where the
5
+ # contact may be a person, an organisation, a department, a role
6
+ # ("Secretariat"), or any other entity with a name and one or more
7
+ # communication channels.
8
+ #
9
+ # Use Contact where the kind of contact is open or mixed (meeting
10
+ # local-contact, meeting-series organiser, host-ref contact). Use
11
+ # Person (which inherits from Contact) where the contact is
12
+ # specifically an individual with identity (officer, attendee, voter).
13
+ class Contact < Lutaml::Model::Serializable
14
+ attribute :name, Name
15
+ attribute :kind, :string
16
+ attribute :role, :string
17
+ attribute :title, :string
18
+ attribute :affiliation, :string
19
+ attribute :contact_methods, ContactMethod, collection: true
20
+ attribute :identifiers, ContactIdentifier, collection: true
21
+ attribute :address, :string
22
+ attribute :extensions, MeetingExtension, collection: true
23
+ end
24
+ 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,69 @@
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 v0.x; renamed in v2.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
+ # Language-agnostic admin fields live here; every translatable field is
10
+ # wrapped inside `localizations[]` (one entry per available language; at
11
+ # least one is required by the schema).
12
+ class Decision < Lutaml::Model::Serializable
13
+ include LocalizationHost
14
+
15
+ attribute :identifier, StructuredIdentifier, collection: true
16
+ attribute :kind, :string, values: Enums::DECISION_KIND
17
+ attribute :status, :string, values: Enums::DECISION_STATUS
18
+ attribute :doi, :string
19
+ attribute :urn, :string
20
+ attribute :agenda_item, :string
21
+ attribute :dates, DecisionDate, collection: true
22
+ attribute :categories, :string, collection: true
23
+ attribute :meeting, MeetingIdentifier
24
+ attribute :relations, DecisionRelation, collection: true
25
+ attribute :urls, Url, collection: true
26
+ attribute :brought_by_motions, :string, collection: true
27
+ attribute :about_topics, :string, collection: true
28
+ attribute :made_in_component, :string
29
+ attribute :localizations, Localization, collection: true
30
+ # v2.1 (TODO.refactor/46): free-form body-specific label (e.g.
31
+ # "Resolution", "Order", "Ruling"). Resolves to a short canonical
32
+ # value via the parent collection's `body_vocabulary[]`.
33
+ attribute :body_type, :string
34
+ attribute :extensions, MeetingExtension, collection: true
35
+
36
+ # --- v2.1 derivation accessors (TODO.refactor/45) -------------------
37
+ # These methods compute the reverse direction of relationships whose
38
+ # canonical storage lives elsewhere. They are additive — the stored
39
+ # `brought_by_motions[]`, `about_topics[]`, and `made_in_component`
40
+ # fields remain on the wire for back-compat through v2.x; v3.0
41
+ # removes them and these methods become the only path.
42
+
43
+ # Returns the Motions in `meeting` whose `resulting_decision` (or
44
+ # `resulting_decision_ref`) points at this Decision's URN.
45
+ #
46
+ # Storage side: Motion.resultingDecision (SSOT for the relationship).
47
+ def brought_by_motions_in(meeting:)
48
+ return [] unless meeting && urn
49
+
50
+ meeting.motions.select do |motion|
51
+ motion.resulting_decision == urn ||
52
+ motion.resulting_decision_ref&.urn == urn
53
+ end
54
+ end
55
+
56
+ # Returns the MeetingComponent in `meeting` whose key matches
57
+ # `made_in_component`.
58
+ #
59
+ # Storage side: Decision.madeInComponent (SSOT for the relationship).
60
+ def component_in(meeting:)
61
+ return nil unless meeting && made_in_component
62
+
63
+ meeting.components&.find do |component|
64
+ component.identifier == made_in_component ||
65
+ component.urn == made_in_component
66
+ end
67
+ end
68
+ end
69
+ 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 v0.x.
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 v0.x.
5
+ class DecisionDate < Lutaml::Model::Serializable
6
+ attribute :date, :date
7
+ attribute :type, :string, values: Enums::DECISION_DATE_TYPE
8
+ end
9
+ end