edoxen 0.1.2 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e2f2624a6aaa7b4768b08653550d783b384d7cf0e7316ac2f06f6dd4937a94cd
4
- data.tar.gz: 3863fe31175019944f0d5dd2c1a1d0ab6146f01403a0f16da0db310214cfb99e
3
+ metadata.gz: a87eaf24182b1fd7400efbd2af10681f4059a35cd57c807759a8015a59a1c0f1
4
+ data.tar.gz: 53ede8ad87d1a25bbf766d7196e6437a8455f431af313ba5b9752cbd42d17050
5
5
  SHA512:
6
- metadata.gz: a0ae0c3bd3870ead7201ca924480a29deb712cc41b0590a2dde5f3136da50bd78ef24e6e03580e2b7fec274cfa2bed3c3b1d80ccc604f2b69aaeed53d9b26f99
7
- data.tar.gz: bb0470982922625d7f6ff36e4b98137486ebd9938a102392bbfbe3fb5e95fba1f727e1ddcf8125778fbfaf10ee08bd06edda1dc2a896f8d12cf34c87f0e0e2b9
6
+ metadata.gz: a14ac6664ffa8ec1f1fcd31ace23257a740dcad3fab41ead54d542440a53618ac8ad46bb495afeb44c62db7b850d35d789ee7a888245f4e6b3ac0afe092310ee
7
+ data.tar.gz: 991a18091cb7ec21ddfe70b2691244b3e8a6ba869135545f6249ffa35d56439afbf578a867d56bdce6c3555f177c59f43c73f8c27abb119527af36c42e6b17fd
data/CLAUDE.md ADDED
@@ -0,0 +1,234 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## What this gem is
6
+
7
+ `edoxen` is an information model for formal resolutions and decisions
8
+ (ISO/TC 154, CIPM, OIML, etc.). It provides:
9
+
10
+ - A set of `Lutaml::Model::Serializable` subclasses that mirror the canonical
11
+ LutaML UML model in `../edoxen-model/models/*.lutaml`.
12
+ - A JSON-Schema (`schema/edoxen.yaml`) used to validate real-world YAML
13
+ files via `SchemaValidator` (`json_schemer`).
14
+ - A Thor CLI (`edoxen validate`, `edoxen normalize`) that wraps both the
15
+ schema validator and the Ruby model parser.
16
+
17
+ ## Architecture
18
+
19
+ ### Loading
20
+
21
+ `lib/edoxen.rb` configures `Lutaml::Model::Config` and `autoload`s every
22
+ constant defined under `Edoxen::...` from its native file. **No
23
+ `require_relative` is used anywhere in this gem.** Cross-references between
24
+ model classes resolve through Ruby autoload (works for circular references
25
+ because autoload is lazy).
26
+
27
+ If you add a new model class under `lib/edoxen/<name>.rb`, register one
28
+ autoload entry in `lib/edoxen.rb`. The namespace is flat under `Edoxen::`,
29
+ matching the LUTAML naming.
30
+
31
+ ### Three sources of truth that must stay in sync
32
+
33
+ There are three descriptions of the same data, and they must agree:
34
+
35
+ 1. **`lib/edoxen/*.rb`** — Ruby model. Attribute declarations + `key_value`
36
+ `map` blocks drive (de)serialization. The model is permissive about
37
+ field names and tolerant of unknown fields (lutaml-model drops them
38
+ silently) — the schema is the strict source.
39
+ 2. **`schema/edoxen.yaml`** — JSON-Schema (draft-07). Used by
40
+ `SchemaValidator`. Adds `additionalProperties: false`, `required`,
41
+ `enum`, `pattern`, and `minItems` constraints the model does not.
42
+ 3. **`spec/fixtures/*.yaml`** — real-world samples. Must validate against
43
+ the schema AND round-trip through the Ruby model.
44
+
45
+ When you change one, change all three in the same PR. The
46
+ `spec/edoxen/schema_enum_sync_spec.rb` enforces the schema ↔ Ruby enum
47
+ boundary at runtime.
48
+
49
+ ### Enum single source of truth
50
+
51
+ `lib/edoxen/enums.rb` is the authoritative list for every enum used by
52
+ the gem (`ACTION_TYPE`, `CONSIDERATION_TYPE`, `RESOLUTION_TYPE`,
53
+ `RESOLUTION_RELATION_TYPE`, `RESOLUTION_DATE_TYPE`, `APPROVAL_TYPE`,
54
+ `APPROVAL_DEGREE`, `URL_KIND`). Both the Ruby model
55
+ (`attribute :type, :string, values: Enums::ACTION_TYPE`) and the
56
+ `schema/edoxen.yaml` `$defs/<EnumName>` block reference these constants.
57
+
58
+ `schema_enum_sync_spec.rb` loads both, walks `$defs` for enum-typed
59
+ definitions, and asserts each value-list matches the corresponding
60
+ `Edoxen::Enums::*` array.
61
+
62
+ If you add a value (e.g. a new verb), add it to the Ruby constant AND
63
+ the schema enum block in the same commit.
64
+
65
+ ### Glossarist-style localization pattern
66
+
67
+ Per-language content lives inside `Resolution#localizations[]` (one
68
+ `Localization` per language). `Localization` carries `language_code`
69
+ (ISO 639-3) and `script` (ISO 15924), plus `title`, `subject`, `message`,
70
+ `considering`, `considerations`, `actions`, `approvals`.
71
+
72
+ Language-agnostic admin fields (`identifier`, `doi`, `urn`,
73
+ `agenda_item`, `dates`, `categories`, `meeting`, `relations`, `urls`)
74
+ live on the parent `Resolution`.
75
+
76
+ `Metadata#title_localized` and `Metadata#source_urls` mirror this
77
+ pattern for collection-level metadata.
78
+
79
+ ### LUTAML fidelity + LUTAML quirks
80
+
81
+ The Ruby model is faithful to the LUTAML files. Two LUTAML quirks
82
+ that propagate:
83
+
84
+ - `empowers` is in both `ConsiderationType` and `ActionType` (upstream
85
+ duplication). `enums_spec.rb` documents this with an explicit test.
86
+ - LUTAML uses `camelCase` attribute names (e.g. `agendaItem`). Wire names
87
+ in this gem are `snake_case` (`agenda_item`) for YAML convention;
88
+ `dateEffective` becomes `date_effective`. This is a deliberate
89
+ one-way mapping from the LUTAML notation; the LUTAML files remain
90
+ authoritative for the semantic model.
91
+
92
+ ### SchemaValidator
93
+
94
+ `lib/edoxen/schema_validator.rb` is intentionally small:
95
+
96
+ - `Edoxen::ValidationError` (defined in `lib/edoxen/error.rb`) — the unified
97
+ validation shape. `SchemaValidator::ValidationError` is a back-compat
98
+ alias. Carries `file`, `line`, `column`, `pointer`, `message_text`, and
99
+ `source` (`:schema` / `:model` / `:syntax`) so renderers can branch on
100
+ failure origin. Rendered as `file:line:col: msg at \`/pointer\``.
101
+ - `#validate_file(path)` → `Array<Edoxen::ValidationError>`.
102
+ - `#validate_content(content, path)` → `Array<Edoxen::ValidationError>`.
103
+ - `SchemaValidator::LineMap` — builds an indent-heuristic
104
+ `{json_pointer => line_no}` map and resolves a JSON-Schema data
105
+ pointer to a line via longest-prefix match. **No path-shape
106
+ hardcoding.** Adding new collection paths never requires touching this.
107
+ - Date coercion (`normalize_dates`) converts `Date` / `Time` instances
108
+ back to ISO strings before handing data to `json_schemer` because the
109
+ schema declares them as `type: string, format: date`.
110
+
111
+ ### CLI
112
+
113
+ `Edoxen::Cli` (Thor) lives in `lib/edoxen/cli.rb`. Two commands:
114
+
115
+ - `validate PATTERN` — glob-expands YAML files and runs both
116
+ `SchemaValidator#validate_file` and `Collection.from_yaml`. The dual
117
+ check is intentional: the schema catches `additionalProperties` /
118
+ `enum` / `required` / `pattern` violations that the model silently
119
+ drops, and the model catches structural issues json_schemer can't
120
+ express (numeric/date coercion, missing nested classes).
121
+ - `normalize PATTERN (--output DIR | --inplace)` — round-trips each
122
+ YAML file through `ResolutionCollection.from_yaml` and writes the
123
+ result back. Preserves any `# yaml-language-server: $schema=...`
124
+ directive in the first 5 lines.
125
+
126
+ Both commands delegate their expand/sort/empty/header/loop/tally/summary/exit
127
+ scaffold to `Edoxen::Cli::Batch` — the deep module behind the seam. The
128
+ command bodies are per-file blocks returning `Batch::Result.ok(msg)` or
129
+ `Batch::Result.bad(errors)`. Adding a third command (e.g. `lint`, `diff`)
130
+ is one block.
131
+
132
+ `Resolution#in_language(code, fallback:)` and `Resolution#primary_localization`
133
+ provide the canonical lookup interface — callers should not iterate
134
+ `localizations.find { |l| l.language_code == code }` directly.
135
+
136
+ Exit code is non-zero if any file fails.
137
+
138
+ ## Build, test, lint
139
+
140
+ ```bash
141
+ bundle install # first-time setup
142
+ bundle exec rspec # full test suite
143
+ bundle exec rspec spec/edoxen/schema_validator_spec.rb
144
+ bundle exec rspec spec/edoxen/enums_spec.rb
145
+ bundle exec rspec spec/edoxen/resolution_spec.rb:32 # one example by line
146
+ bundle exec rubocop # lint (line length 120, double-quoted strings)
147
+ bundle exec rake # default: spec + rubocop
148
+ bundle exec exe/edoxen validate "spec/fixtures/*.yaml"
149
+ mkdir -p /tmp/out && bundle exec exe/edoxen normalize "spec/fixtures/*.yaml" --output /tmp/out
150
+ ```
151
+
152
+ ## Conventions specific to this gem
153
+
154
+ - **No `double()` in specs** — use real `Edoxen::*` instances and fixture files.
155
+ - **Serialization is framework-only.** Never hand-roll `to_h` / `from_h` /
156
+ `to_yaml` / `from_yaml` / `to_json` / `from_json` on a model class. Declare
157
+ `attribute` only — lutaml-model auto-emits an identity map for each
158
+ attribute when no `key_value` block is present. Add a `key_value do … end`
159
+ block with `map "wire_name", to: :attr` only when the wire name differs
160
+ from the attribute name.
161
+ - **Wire names are `snake_case`.** Even when the LUTAML notation uses
162
+ camelCase, the YAML wire form is `snake_case` (`agenda_item`, not
163
+ `agendaItem`). The attribute name on the Ruby side is also `snake_case`,
164
+ and the wire name defaults to it.
165
+ - **Enums on the model and the schema must agree.** When you add or remove
166
+ a value to `Enums::*`, also update the matching `enum:` list in
167
+ `schema/edoxen.yaml` $defs. The `schema_enum_sync_spec` will catch any
168
+ drift.
169
+ - **Fixtures are load-bearing.** `spec/fixtures/{ciml,cipm,isotc154,...}.yaml`
170
+ are real-world extracts. If a model change invalidates them, that's a
171
+ signal to either (a) update the fixture in the same PR, or (b)
172
+ reconsider the model change. Do not leave them broken.
173
+ - **No backwards-compat shims on `Resolution`.** The flat `#title`,
174
+ `#subject`, `#considerations`, `#approvals`, `#actions` form is gone.
175
+ Always go through `localizations[]`.
176
+ - **Branch discipline.** Never commit to `main`, never push tags, all
177
+ changes via PR. See the global rules.
178
+ - **No AI attribution** in commits, PRs, or code comments.
179
+
180
+ ## Source hygiene — do not violate
181
+
182
+ - Never hand-roll `to_h` / `from_h` / `to_yaml` / `to_json` on a model.
183
+ - Never use `send` to call private methods.
184
+ - Never use `instance_variable_set` or `instance_variable_get` to cross
185
+ another object's boundary.
186
+ - Never use `respond_to?` for type checking — design so the type
187
+ hierarchy makes the check unnecessary, or use `is_a?`.
188
+ - Never use `require_relative` in library code. Use the autoload entries
189
+ in `lib/edoxen.rb`.
190
+
191
+ ## Adding a new model class (the OCP test)
192
+
193
+ When you introduce a new concept (e.g. `Subject`):
194
+
195
+ 1. Add `lib/edoxen/subject.rb` declaring
196
+ `class Subject < Lutaml::Model::Serializable` with attributes and
197
+ `key_value do … end` mapping.
198
+ 2. Add one autoload line in `lib/edoxen.rb`.
199
+ 3. Add a `$defs/Subject` block in `schema/edoxen.yaml` and reference
200
+ it from any parent that uses it.
201
+ 4. Add `spec/edoxen/subject_spec.rb` covering round-trip for every
202
+ field, every enum value, real-instance construction, no doubles.
203
+ 5. Add a fixture or update an existing one to exercise the new model.
204
+ 6. Run `bundle exec rspec` (full suite passes including
205
+ `schema_enum_sync_spec`) and `bundle exec rubocop`.
206
+
207
+ No existing class requires modification. The library grows only by
208
+ extension.
209
+
210
+ ## Adding a new CLI subcommand
211
+
212
+ 1. Add a `desc "name ARGS", "Description"` block in `lib/edoxen/cli.rb`
213
+ followed by a method. The CLI is intentionally a thin glue layer;
214
+ put logic in a model or service class and call it from the command.
215
+ 2. Add `spec/edoxen/cli_spec.rb` integration-style coverage that spawns
216
+ the real `exe/edoxen` process via `Open3.capture3` and asserts on
217
+ stdout/stderr/exit code. Don't shell out to a mocked binary — the
218
+ CLI is exercised as an end-to-end contract.
219
+ 3. Update the README's "Command-line interface" section.
220
+
221
+ ## Audit checklist — run before approving any contribution
222
+
223
+ 1. `bundle exec rspec` passes (currently 136 / 0).
224
+ 2. `bundle exec exe/edoxen validate spec/fixtures/*.yaml` is clean
225
+ (currently 4 / 0).
226
+ 3. `bundle exec exe/edoxen normalize --output /tmp/out` round-trips
227
+ every fixture without data loss (re-parse and assert equality).
228
+ 4. Schema and Ruby model agree on: field names, enum values,
229
+ required vs. optional, collection vs. scalar, `additionalProperties: false`.
230
+ 5. No new hand-rolled `to_h` / `from_h` on a model class.
231
+ 6. No new `LineMap`-style hardcoded path-shape branches.
232
+ 7. README example YAML parses against the schema (currently three
233
+ real-world fixtures do).
234
+ 8. `bundle exec rubocop` clean (target Ruby ≥ 3.0, line length 120).