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.
- checksums.yaml +4 -4
- data/.rubocop.yml +17 -1
- data/.rubocop_todo.yml +1 -1
- data/CHANGELOG.md +246 -0
- data/README.adoc +308 -153
- data/edoxen.gemspec +10 -10
- data/lib/edoxen/action.rb +1 -1
- data/lib/edoxen/agenda.rb +4 -7
- data/lib/edoxen/agenda_item.rb +10 -3
- data/lib/edoxen/approval.rb +1 -1
- data/lib/edoxen/attendance.rb +16 -10
- data/lib/edoxen/body_vocabulary_entry.rb +24 -0
- data/lib/edoxen/body_vocabulary_host.rb +29 -0
- data/lib/edoxen/cli.rb +117 -120
- data/lib/edoxen/component_localization.rb +11 -0
- data/lib/edoxen/consideration.rb +2 -2
- data/lib/edoxen/contact.rb +24 -0
- data/lib/edoxen/contact_identifier.rb +13 -0
- data/lib/edoxen/contact_method.rb +16 -0
- data/lib/edoxen/decision.rb +69 -0
- data/lib/edoxen/decision_collection.rb +10 -0
- data/lib/edoxen/decision_date.rb +9 -0
- data/lib/edoxen/{resolution_metadata.rb → decision_metadata.rb} +5 -6
- data/lib/edoxen/decision_relation.rb +11 -0
- data/lib/edoxen/entity_ref.rb +74 -0
- data/lib/edoxen/enums.rb +117 -9
- data/lib/edoxen/error.rb +1 -1
- data/lib/edoxen/extension_attribute.rb +78 -0
- data/lib/edoxen/host_ref.rb +1 -1
- data/lib/edoxen/link_checker.rb +8 -8
- data/lib/edoxen/localization.rb +2 -2
- data/lib/edoxen/localization_host.rb +23 -0
- data/lib/edoxen/meeting.rb +63 -39
- data/lib/edoxen/meeting_collection.rb +1 -2
- data/lib/edoxen/meeting_collection_metadata.rb +3 -1
- data/lib/edoxen/meeting_component.rb +41 -0
- data/lib/edoxen/meeting_extension.rb +25 -0
- data/lib/edoxen/meeting_identifier.rb +1 -1
- data/lib/edoxen/meeting_localization.rb +6 -1
- data/lib/edoxen/meeting_series.rb +21 -0
- data/lib/edoxen/minutes.rb +2 -2
- data/lib/edoxen/minutes_section.rb +8 -11
- data/lib/edoxen/motion.rb +51 -0
- data/lib/edoxen/name.rb +29 -0
- data/lib/edoxen/officer.rb +19 -0
- data/lib/edoxen/officers_host.rb +19 -0
- data/lib/edoxen/person.rb +5 -9
- data/lib/edoxen/physical_venue.rb +15 -0
- data/lib/edoxen/recurrence.rb +27 -0
- data/lib/edoxen/recurrence_by_day.rb +10 -0
- data/lib/edoxen/reference_data.rb +35 -41
- data/lib/edoxen/schema_validator.rb +2 -3
- data/lib/edoxen/source_url.rb +3 -3
- data/lib/edoxen/structured_identifier.rb +2 -2
- data/lib/edoxen/topic.rb +37 -0
- data/lib/edoxen/topic_asset.rb +14 -0
- data/lib/edoxen/topic_document.rb +16 -0
- data/lib/edoxen/venue.rb +80 -0
- data/lib/edoxen/venue_validator.rb +56 -0
- data/lib/edoxen/version.rb +1 -1
- data/lib/edoxen/virtual_venue.rb +15 -0
- data/lib/edoxen/vote_record.rb +17 -13
- data/lib/edoxen/voting.rb +45 -0
- data/lib/edoxen/voting_counts.rb +23 -0
- data/lib/edoxen.rb +40 -13
- data/schema/edoxen.yaml +792 -145
- data/schema/meeting.yaml +600 -142
- data/sig/edoxen.rbs +581 -1
- metadata +57 -20
- data/lib/edoxen/_metadata.rb.deprecated +0 -57
- data/lib/edoxen/location.rb +0 -15
- data/lib/edoxen/resolution.rb +0 -43
- data/lib/edoxen/resolution_collection.rb +0 -10
- data/lib/edoxen/resolution_date.rb +0 -11
- data/lib/edoxen/resolution_relation.rb +0 -11
- data/lib/edoxen/resolution_set.rb +0 -17
- data/lib/edoxen/schedule_item.rb +0 -27
- 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
|
-
#
|
|
6
|
-
#
|
|
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 :
|
|
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
|
data/lib/edoxen/agenda_item.rb
CHANGED
|
@@ -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; `
|
|
7
|
-
# optionally links to the URN of the
|
|
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 :
|
|
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
|
data/lib/edoxen/approval.rb
CHANGED
|
@@ -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,
|
|
9
|
+
attribute :date, DecisionDate
|
|
10
10
|
attribute :message, :string
|
|
11
11
|
end
|
|
12
12
|
end
|
data/lib/edoxen/attendance.rb
CHANGED
|
@@ -1,23 +1,29 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Edoxen
|
|
4
|
-
# One attendance record per person.
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
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
|
|
118
|
+
"Validate Decision YAML file(s) against schema/edoxen.yaml and the model."
|
|
89
119
|
|
|
90
120
|
def validate(pattern)
|
|
91
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
190
|
-
|
|
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
|
|
194
|
-
|
|
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
|
|
205
|
-
|
|
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
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
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
|
data/lib/edoxen/consideration.rb
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Edoxen
|
|
4
|
-
# Basis for a
|
|
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,
|
|
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
|