ea 0.1.0 → 0.1.4

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 (227) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +125 -0
  3. data/Rakefile +12 -4
  4. data/TODO.next/00-publish-blocking-bugs.md +74 -0
  5. data/TODO.next/01-standalone-ea-gem-identity.md +76 -0
  6. data/TODO.next/02-optional-lutaml-uml-dependency.md +47 -0
  7. data/TODO.next/03-slim-lutaml-uml.md +79 -0
  8. data/TODO.next/04-loader-registry-for-uml-repository.md +49 -0
  9. data/TODO.next/05-extract-shared-transformer-methods.md +14 -0
  10. data/TODO.next/06-deduplicate-stereotype-loading.md +17 -0
  11. data/TODO.next/07-transformer-registry-in-factory.md +20 -0
  12. data/TODO.next/08-connector-type-registry.md +27 -0
  13. data/TODO.next/09-element-renderer-registry.md +29 -0
  14. data/TODO.next/10-connector-renderer-lsp.md +18 -0
  15. data/TODO.next/11-consolidate-style-knowledge.md +33 -0
  16. data/TODO.next/12-data-driven-from-db-row.md +24 -0
  17. data/TODO.next/13-extract-duplicated-methods.md +17 -0
  18. data/TODO.next/14-remove-dead-code.md +10 -0
  19. data/TODO.next/15-narrow-exception-handling.md +39 -0
  20. data/TODO.next/16-repository-indexes.md +28 -0
  21. data/TODO.next/17-fix-spec-quality-and-coverage.md +32 -0
  22. data/TODO.next/18-xmi-tool-specific-parser-architecture.md +172 -0
  23. data/TODO.next/19-fix-ea-gemspec-dependency-declarations.md +56 -0
  24. data/TODO.next/20-ci-requires-unreleased-lutaml-uml.md +63 -0
  25. data/TODO.next/21-qeatoxmi-via-xmi-gem.md +340 -0
  26. data/TODO.next/22-strip-respond-to-from-qeatoxmi-specs.md +32 -0
  27. data/TODO.next/23-cleanup-idallocator.md +41 -0
  28. data/TODO.next/24-tighten-parity-specs.md +42 -0
  29. data/TODO.next/25-sparx-eaid-format-for-synthesized-ids.md +62 -0
  30. data/TODO.next/26-fix-uppervalue-lowervalue-count-gap.md +51 -0
  31. data/TODO.next/27-extract-cardinality-module.md +68 -0
  32. data/TODO.next/28-extract-xml-sanitizer.md +51 -0
  33. data/TODO.next/29-ocp-registry-for-classifier-builders.md +58 -0
  34. data/TODO.next/30-struct-return-for-association-end.md +37 -0
  35. data/TODO.next/31-idallocator-specs.md +27 -0
  36. data/TODO.next/32-phase2-gap-sentinel-specs.md +53 -0
  37. data/TODO.next/33-normalize-lower-cleanup.md +30 -0
  38. data/TODO.next/34-document-member-end-order-rt-prefix.md +29 -0
  39. data/TODO.next/35-walk-runstate-for-instance-slots.md +76 -0
  40. data/TODO.next/36-wire-interface-realization.md +50 -0
  41. data/TODO.next/37-visibility-returns-real-booleans.md +36 -0
  42. data/config/diagram_styles.yml +200 -0
  43. data/config/model_transformations.yml +266 -0
  44. data/config/qea_schema.yml +1024 -0
  45. data/docs/ea_to_uml_type_mapping.md +89 -0
  46. data/docs/xmi_qea_conversion_capabilities.md +99 -0
  47. data/examples/lur/20251010_current_plateau_v5.1.lur +0 -0
  48. data/examples/lur/basic.lur +0 -0
  49. data/examples/lur/test-output.lur +0 -0
  50. data/examples/lur/test.lur +0 -0
  51. data/examples/lur_basic_usage.rb +221 -0
  52. data/examples/lur_cli_workflow.rb +263 -0
  53. data/examples/lur_statistics.rb +326 -0
  54. data/examples/qea/20251010_current_plateau_v5.1.qea +0 -0
  55. data/examples/qea/ArcGISWorkspace_template.qea +0 -0
  56. data/examples/qea/README_qea_parser.adoc +230 -0
  57. data/examples/qea/UmlModel_template.qea +0 -0
  58. data/examples/qea/basic.qea +0 -0
  59. data/examples/qea/simple.qea +0 -0
  60. data/examples/qea/simple_example.qea +0 -0
  61. data/examples/qea/test.qea +0 -0
  62. data/examples/qea_standalone_query.rb +73 -0
  63. data/examples/qea_to_repository.rb +51 -0
  64. data/examples/smoke_test_real_qea.rb +81 -0
  65. data/exe/ea +7 -0
  66. data/lib/ea/cli/app.rb +72 -0
  67. data/lib/ea/cli/command/base.rb +80 -0
  68. data/lib/ea/cli/command/convert.rb +62 -0
  69. data/lib/ea/cli/command/diagrams.rb +81 -0
  70. data/lib/ea/cli/command/list.rb +61 -0
  71. data/lib/ea/cli/command/parse.rb +29 -0
  72. data/lib/ea/cli/command/stats.rb +20 -0
  73. data/lib/ea/cli/command/validate.rb +41 -0
  74. data/lib/ea/cli/command.rb +15 -0
  75. data/lib/ea/cli/error.rb +34 -0
  76. data/lib/ea/cli/output/formatter.rb +34 -0
  77. data/lib/ea/cli/output/json_formatter.rb +20 -0
  78. data/lib/ea/cli/output/table_formatter.rb +42 -0
  79. data/lib/ea/cli/output/yaml_formatter.rb +20 -0
  80. data/lib/ea/cli/output.rb +56 -0
  81. data/lib/ea/cli.rb +17 -0
  82. data/lib/ea/diagram/configuration.rb +379 -0
  83. data/lib/ea/diagram/element_renderers/base_renderer.rb +77 -0
  84. data/lib/ea/diagram/element_renderers/class_renderer.rb +323 -0
  85. data/lib/ea/diagram/element_renderers/connector_renderer.rb +41 -0
  86. data/lib/ea/diagram/element_renderers/package_renderer.rb +61 -0
  87. data/lib/ea/diagram/element_renderers.rb +43 -0
  88. data/lib/ea/diagram/extractor.rb +560 -0
  89. data/lib/ea/diagram/layout_engine.rb +170 -0
  90. data/lib/ea/diagram/path_builder.rb +202 -0
  91. data/lib/ea/diagram/style_parser.rb +42 -0
  92. data/lib/ea/diagram/style_resolver.rb +276 -0
  93. data/lib/ea/diagram/svg_renderer.rb +274 -0
  94. data/lib/ea/diagram/util.rb +73 -0
  95. data/lib/ea/diagram.rb +47 -0
  96. data/lib/ea/qea/benchmark.rb +210 -0
  97. data/lib/ea/qea/database.rb +308 -0
  98. data/lib/ea/qea/factory/association_builder.rb +203 -0
  99. data/lib/ea/qea/factory/association_transformer.rb +91 -0
  100. data/lib/ea/qea/factory/attribute_tag_transformer.rb +57 -0
  101. data/lib/ea/qea/factory/attribute_transformer.rb +93 -0
  102. data/lib/ea/qea/factory/base_transformer.rb +177 -0
  103. data/lib/ea/qea/factory/class_transformer.rb +116 -0
  104. data/lib/ea/qea/factory/constraint_transformer.rb +75 -0
  105. data/lib/ea/qea/factory/data_type_transformer.rb +77 -0
  106. data/lib/ea/qea/factory/diagram_transformer.rb +157 -0
  107. data/lib/ea/qea/factory/document_builder.rb +283 -0
  108. data/lib/ea/qea/factory/ea_to_uml_factory.rb +229 -0
  109. data/lib/ea/qea/factory/enum_transformer.rb +74 -0
  110. data/lib/ea/qea/factory/generalization_builder.rb +227 -0
  111. data/lib/ea/qea/factory/generalization_transformer.rb +98 -0
  112. data/lib/ea/qea/factory/instance_transformer.rb +68 -0
  113. data/lib/ea/qea/factory/object_property_transformer.rb +58 -0
  114. data/lib/ea/qea/factory/operation_transformer.rb +66 -0
  115. data/lib/ea/qea/factory/package_transformer.rb +145 -0
  116. data/lib/ea/qea/factory/reference_resolver.rb +99 -0
  117. data/lib/ea/qea/factory/stereotype_loader.rb +39 -0
  118. data/lib/ea/qea/factory/tagged_value_transformer.rb +38 -0
  119. data/lib/ea/qea/factory/transformer_registry.rb +80 -0
  120. data/lib/ea/qea/factory.rb +37 -0
  121. data/lib/ea/qea/file_detector.rb +178 -0
  122. data/lib/ea/qea/infrastructure/database_connection.rb +100 -0
  123. data/lib/ea/qea/infrastructure/schema_reader.rb +136 -0
  124. data/lib/ea/qea/infrastructure/table_reader.rb +224 -0
  125. data/lib/ea/qea/infrastructure.rb +12 -0
  126. data/lib/ea/qea/models/base_model.rb +59 -0
  127. data/lib/ea/qea/models/ea_attribute.rb +109 -0
  128. data/lib/ea/qea/models/ea_attribute_tag.rb +100 -0
  129. data/lib/ea/qea/models/ea_complexity_type.rb +79 -0
  130. data/lib/ea/qea/models/ea_connector.rb +160 -0
  131. data/lib/ea/qea/models/ea_connector_type.rb +60 -0
  132. data/lib/ea/qea/models/ea_constraint_type.rb +63 -0
  133. data/lib/ea/qea/models/ea_datatype.rb +104 -0
  134. data/lib/ea/qea/models/ea_diagram.rb +115 -0
  135. data/lib/ea/qea/models/ea_diagram_link.rb +78 -0
  136. data/lib/ea/qea/models/ea_diagram_object.rb +73 -0
  137. data/lib/ea/qea/models/ea_diagram_type.rb +56 -0
  138. data/lib/ea/qea/models/ea_document.rb +63 -0
  139. data/lib/ea/qea/models/ea_object.rb +223 -0
  140. data/lib/ea/qea/models/ea_object_constraint.rb +53 -0
  141. data/lib/ea/qea/models/ea_object_property.rb +87 -0
  142. data/lib/ea/qea/models/ea_object_type.rb +73 -0
  143. data/lib/ea/qea/models/ea_operation.rb +127 -0
  144. data/lib/ea/qea/models/ea_operation_param.rb +76 -0
  145. data/lib/ea/qea/models/ea_package.rb +78 -0
  146. data/lib/ea/qea/models/ea_script.rb +62 -0
  147. data/lib/ea/qea/models/ea_status_type.rb +66 -0
  148. data/lib/ea/qea/models/ea_stereotype.rb +57 -0
  149. data/lib/ea/qea/models/ea_tagged_value.rb +99 -0
  150. data/lib/ea/qea/models/ea_xref.rb +165 -0
  151. data/lib/ea/qea/models.rb +35 -0
  152. data/lib/ea/qea/repositories/base_repository.rb +225 -0
  153. data/lib/ea/qea/repositories/object_repository.rb +219 -0
  154. data/lib/ea/qea/repositories.rb +10 -0
  155. data/lib/ea/qea/services/configuration.rb +211 -0
  156. data/lib/ea/qea/services/database_loader.rb +191 -0
  157. data/lib/ea/qea/services.rb +10 -0
  158. data/lib/ea/qea/validation/association_validator.rb +73 -0
  159. data/lib/ea/qea/validation/attribute_validator.rb +91 -0
  160. data/lib/ea/qea/validation/base_validator.rb +331 -0
  161. data/lib/ea/qea/validation/class_validator.rb +121 -0
  162. data/lib/ea/qea/validation/database/circular_reference_validator.rb +109 -0
  163. data/lib/ea/qea/validation/database/orphan_validator.rb +153 -0
  164. data/lib/ea/qea/validation/database/referential_integrity_validator.rb +128 -0
  165. data/lib/ea/qea/validation/database.rb +16 -0
  166. data/lib/ea/qea/validation/diagram_validator.rb +112 -0
  167. data/lib/ea/qea/validation/formatters/json_formatter.rb +137 -0
  168. data/lib/ea/qea/validation/formatters/text_formatter.rb +235 -0
  169. data/lib/ea/qea/validation/formatters.rb +12 -0
  170. data/lib/ea/qea/validation/operation_validator.rb +71 -0
  171. data/lib/ea/qea/validation/package_validator.rb +111 -0
  172. data/lib/ea/qea/validation/validation_engine.rb +387 -0
  173. data/lib/ea/qea/validation/validation_message.rb +144 -0
  174. data/lib/ea/qea/validation/validation_result.rb +210 -0
  175. data/lib/ea/qea/validation/validator_registry.rb +134 -0
  176. data/lib/ea/qea/validation.rb +28 -0
  177. data/lib/ea/qea/verification/comparison_result.rb +264 -0
  178. data/lib/ea/qea/verification/document_normalizer.rb +169 -0
  179. data/lib/ea/qea/verification/document_verifier.rb +322 -0
  180. data/lib/ea/qea/verification/element_comparator.rb +277 -0
  181. data/lib/ea/qea/verification/structure_matcher.rb +287 -0
  182. data/lib/ea/qea/verification.rb +14 -0
  183. data/lib/ea/qea.rb +185 -0
  184. data/lib/ea/transformations/configuration.rb +333 -0
  185. data/lib/ea/transformations/format_registry.rb +366 -0
  186. data/lib/ea/transformations/parsers/base_parser.rb +482 -0
  187. data/lib/ea/transformations/parsers/qea_parser.rb +401 -0
  188. data/lib/ea/transformations/parsers/xmi_parser.rb +243 -0
  189. data/lib/ea/transformations/transformation_engine.rb +390 -0
  190. data/lib/ea/transformations.rb +85 -0
  191. data/lib/ea/transformers/qea_to_xmi/association_end.rb +19 -0
  192. data/lib/ea/transformers/qea_to_xmi/cardinality.rb +96 -0
  193. data/lib/ea/transformers/qea_to_xmi/context.rb +106 -0
  194. data/lib/ea/transformers/qea_to_xmi/guid_format.rb +56 -0
  195. data/lib/ea/transformers/qea_to_xmi/id_allocator.rb +92 -0
  196. data/lib/ea/transformers/qea_to_xmi/run_state.rb +107 -0
  197. data/lib/ea/transformers/qea_to_xmi/transformer.rb +607 -0
  198. data/lib/ea/transformers/qea_to_xmi/visibility.rb +73 -0
  199. data/lib/ea/transformers/qea_to_xmi.rb +29 -0
  200. data/lib/ea/transformers/uml_to_xmi/id_generator.rb +54 -0
  201. data/lib/ea/transformers/uml_to_xmi/transformer.rb +152 -0
  202. data/lib/ea/transformers/uml_to_xmi/writer.rb +96 -0
  203. data/lib/ea/transformers/uml_to_xmi.rb +16 -0
  204. data/lib/ea/transformers.rb +34 -0
  205. data/lib/ea/version.rb +1 -1
  206. data/lib/ea/xmi/liquid_drops/association_drop.rb +56 -0
  207. data/lib/ea/xmi/liquid_drops/attribute_drop.rb +72 -0
  208. data/lib/ea/xmi/liquid_drops/cardinality_drop.rb +35 -0
  209. data/lib/ea/xmi/liquid_drops/connector_drop.rb +54 -0
  210. data/lib/ea/xmi/liquid_drops/constraint_drop.rb +29 -0
  211. data/lib/ea/xmi/liquid_drops/data_type_drop.rb +63 -0
  212. data/lib/ea/xmi/liquid_drops/dependency_drop.rb +36 -0
  213. data/lib/ea/xmi/liquid_drops/diagram_drop.rb +34 -0
  214. data/lib/ea/xmi/liquid_drops/enum_drop.rb +49 -0
  215. data/lib/ea/xmi/liquid_drops/enum_owned_literal_drop.rb +25 -0
  216. data/lib/ea/xmi/liquid_drops/generalization_attribute_drop.rb +87 -0
  217. data/lib/ea/xmi/liquid_drops/generalization_drop.rb +127 -0
  218. data/lib/ea/xmi/liquid_drops/klass_drop.rb +191 -0
  219. data/lib/ea/xmi/liquid_drops/operation_drop.rb +29 -0
  220. data/lib/ea/xmi/liquid_drops/package_drop.rb +108 -0
  221. data/lib/ea/xmi/liquid_drops/root_drop.rb +34 -0
  222. data/lib/ea/xmi/liquid_drops/source_target_drop.rb +43 -0
  223. data/lib/ea/xmi/lookup_service.rb +89 -0
  224. data/lib/ea/xmi/parser.rb +919 -0
  225. data/lib/ea/xmi.rb +35 -0
  226. data/lib/ea.rb +10 -1
  227. metadata +382 -9
@@ -0,0 +1,340 @@
1
+ # 21 - QeaToXmi via xmi gem
2
+
3
+ ## Status: ✅ PHASE 1 + PHASE 2 LANDED (2026-07-01)
4
+
5
+ Phase 1 (the xmi gem model construction rewrite) shipped earlier.
6
+ Phase 2 (TODO.next/21 §1-§3) is now complete:
7
+
8
+ | § | Item | Status | Resolution |
9
+ |---|------|--------|------------|
10
+ | 1 | xmi gem empty-element rendering | DONE | `Xmi::VALUE_MAP` now `to: { nil: :omitted, ... }` — skips empties at serialize time. Closes TODO 28 by deleting XmlSanitizer entirely. |
11
+ | 2 | xmi gem attribute gaps | DONE | `visibility`, `isAbstract`, `aggregation`, `classifier` declared on the right models; ea transformer wires `Visibility.from_scope` / `aggregation_from_containment` / `boolean_from_flag`. |
12
+ | 3 | xmi gem missing models | DONE | `Xmi::Uml::Slot`, `OpaqueExpression`, `InterfaceRealization` added with full specs; wired into `PackagedElement`. |
13
+ | 4 | File-size refactoring | DEFERRED | transformer.rb at 469 LOC; explicit "deferred because the class is conceptually cohesive — one orchestrator with private walk methods" per the original plan. |
14
+
15
+ Full ea gem suite: **2005 examples, 0 failures, 37 pending**.
16
+
17
+ Plateau smoke (20251010_current_plateau_v5.1.qea): 58 packages, 581
18
+ classes, 11 enumerations, 431 associations, 420 generalizations, 0 XML
19
+ errors. Byte-for-byte-equivalent structural parity with the previous
20
+ implementation.
21
+
22
+ ## What was delivered
23
+
24
+ ## What was delivered
25
+
26
+ **Deleted (~1000 lines):**
27
+ - `lib/ea/transformers/qea_to_xmi/xml_builder.rb`
28
+ - `lib/ea/transformers/qea_to_xmi/writer.rb`
29
+ - `lib/ea/transformers/qea_to_xmi/emitter_registry.rb`
30
+ - `lib/ea/transformers/qea_to_xmi/sparx_namespaces.rb`
31
+ - `lib/ea/transformers/qea_to_xmi/emitters/{base,package,class,enumeration,
32
+ data_type,instance,attribute,operation,association,generalization,
33
+ realization,dependency,comment,slot}_emitter.rb`
34
+ - Matching spec files for the deleted modules
35
+
36
+ **Kept (unchanged):**
37
+ - `lib/ea/transformers/qea_to_xmi/id_allocator.rb` — EA-specific EAID synthesis
38
+ - `lib/ea/transformers/qea_to_xmi/guid_format.rb` — `{GUID}` ↔ `EAID_` translation
39
+
40
+ **Rewritten:**
41
+ - `lib/ea/transformers/qea_to_xmi/transformer.rb` — single class that walks
42
+ the database and constructs `Xmi::Sparx::Root` / `Xmi::Uml::UmlModel` /
43
+ `Xmi::Uml::PackagedElement` / etc., then calls `to_xml(use_prefix: true)`.
44
+ Element-kind dispatch lives in a single case statement in
45
+ `#build_classifier` — adding a new kind = one new branch, no new file.
46
+ - `lib/ea/transformers/qea_to_xmi/context.rb` — slimmed down (dropped the
47
+ writer dependency; kept database + IdAllocator).
48
+ - `lib/ea/transformers/qea_to_xmi.rb` — autoload list pruned.
49
+
50
+ **Spec coverage added:**
51
+ - Round-trip via xmi gem parser (parse output back through
52
+ `Xmi::Sparx::Root.parse_xml`, verify structure).
53
+ - API stability (idempotent serialize, no database mutation).
54
+ - Plus all pre-existing parity, mixed-prefix, GUID, well-formedness specs
55
+ continue to pass.
56
+
57
+ ## Phase 2 work (deferred)
58
+
59
+ These items fell out of the rewrite but are tracked separately:
60
+
61
+ ### 1. xmi gem empty-element rendering (architectural debt)
62
+
63
+ The xmi gem's UML models declare `value_map: Xmi::VALUE_MAP` on every
64
+ child-element mapping. VALUE_MAP is round-trip-oriented: it forces
65
+ empty-element emission (`<generalization/>`, `<ownedEnd/>`, etc.) so the
66
+ parser can distinguish absence from emptiness. For *generation*, those
67
+ empty elements are noise.
68
+
69
+ The rewrite works around this by post-processing the serialized XML
70
+ (`Transformer#strip_empty_elements`) — a Nokogiri pass that removes
71
+ truly-empty elements (no children, no attributes). This is functionally
72
+ correct but adds a parse/serialize cycle to every emit.
73
+
74
+ The clean fix lives in the xmi gem: introduce a generation-friendly
75
+ value_map (e.g., `Xmi::GENERATION_VALUE_MAP = { to: { nil: :nil, empty:
76
+ :nil, omitted: :nil } }`) and let the ea gem opt in. Then drop
77
+ `strip_empty_elements` from this gem.
78
+
79
+ ### 2. xmi gem attribute gaps
80
+
81
+ The xmi gem's UML models don't yet declare every attribute real Sparx
82
+ XMI carries. This rewrite drops the following attributes that the
83
+ previous XmlBuilder emitted:
84
+ - `visibility` on `<ownedAttribute>` / `<ownedOperation>` /
85
+ `<ownedParameter>` (Sparx always emits `visibility="private"|"public"|
86
+ ...`)
87
+ - `isAbstract` on `<packagedElement>` for abstract classes
88
+ - `classifier` on `<packagedElement>` for InstanceSpecification
89
+ - `aggregation` on `<ownedEnd>` for composite vs. shared
90
+ - `direction` is exposed by `OwnedParameter` but the value mapping needs
91
+ to align with EA's `kind` field
92
+
93
+ Each gap is a small, focused PR to the xmi gem: add the attribute,
94
+ add a spec round-tripping a fixture that uses it.
95
+
96
+ ### 3. xmi gem missing models
97
+
98
+ - `Xmi::Uml::Slot` — for instance specification attribute values
99
+ - `Xmi::Uml::OpaqueExpression` — for slot values and default expressions
100
+ - `Xmi::Uml::InterfaceRealization` — currently collapsed into
101
+ `PackagedElement` with `type="uml:Realization"`
102
+
103
+ These were emitted by the previous XmlBuilder but are dropped in
104
+ Phase 1 because the xmi gem has no models for them. Phase 2 adds the
105
+ models, then the Transformer wires them up.
106
+
107
+ ### 4. File-size refactoring
108
+
109
+ `lib/ea/transformers/qea_to_xmi/transformer.rb` is 503 lines (369 LOC),
110
+ over the project's ~300-line guideline. The class is cohesive (one
111
+ orchestrator with private walk methods), but it could be split into:
112
+
113
+ - `Transformer` — orchestration and walk order (~150 lines)
114
+ - `ElementFactory` — leaf element construction (attributes, operations,
115
+ literals, comments; ~200 lines)
116
+ - `RelationshipFactory` — association / dependency / generalization
117
+ construction (~150 lines)
118
+
119
+ Deferred because splitting now would risk regressions for cosmetic
120
+ gain; the file is conceptually one thing (the QEA walk).
121
+
122
+ ## Goal
123
+ Replace the custom XML construction layer in `Ea::Transformers::QeaToXmi` with
124
+ the `xmi` gem's typed models (`Xmi::Sparx::Root`, `Xmi::Uml::UmlModel`,
125
+ `Xmi::Uml::PackagedElement`, etc.). The Transformer becomes a
126
+ **model construction walk** over `Ea::Qea::Database`, not an XML emitter.
127
+
128
+ ## Why
129
+
130
+ ### Why switch from custom XML to xmi gem models
131
+
132
+ The current implementation builds Sparx XMI through a hand-rolled
133
+ `XmlBuilder` + `Writer` + 13 `Emitters` + `EmitterRegistry`. That layer
134
+ exists because Sparx XMI uses a mixed-prefix style that lutaml-model's
135
+ default behavior doesn't produce (prefixed root `<uml:Model>`,
136
+ unprefixed children `<packagedElement>`). The `XmlBuilder` works around
137
+ this by using Nokogiri's low-level `Document#create_element` API and
138
+ assigning per-element namespaces from a `PREFIXED_ELEMENTS` map.
139
+
140
+ That workaround is no longer necessary. The xmi gem at v0.5.10+ now
141
+ emits the Sparx mixed-prefix style natively:
142
+ - `UmlModel` declares `namespace Uml` → emits `<uml:Model>`
143
+ - Every child UML class declares `namespace :blank` → emits unprefixed
144
+ `<packagedElement>`, `<ownedAttribute>`, etc.
145
+ - `XmiType`, `XmiId`, `XmiIdRef` types declare `namespace Xmi` → emits
146
+ `xmi:type`, `xmi:id`, `xmi:idref` attributes
147
+ - Calling `to_xml(use_prefix: true)` forces prefix format on the root
148
+
149
+ With the xmi gem producing correct Sparx output via standard
150
+ lutaml-model serialization, the custom XML construction layer has no
151
+ remaining purpose. Every byte it produces is something the xmi gem
152
+ also produces from the same input.
153
+
154
+ ### What this eliminates
155
+
156
+ - `lib/ea/transformers/qea_to_xmi/xml_builder.rb` — low-level Nokogiri
157
+ wrapper with `method_missing` dynamic dispatch for arbitrary tag
158
+ names. ~104 lines of XML plumbing that duplicates what the xmi
159
+ gem + lutaml-model already do.
160
+ - `lib/ea/transformers/qea_to_xmi/writer.rb` — XML shape primitives
161
+ routing through the XmlBuilder. ~232 lines that re-express what
162
+ `Xmi::Sparx::Root` already represents structurally.
163
+ - `lib/ea/transformers/qea_to_xmi/emitter_registry.rb` — OCP registry
164
+ pattern for emitter dispatch. ~49 lines. With xmi gem models, the
165
+ polymorphism lives in lutaml-model's polymorphic mapping, not in our
166
+ code.
167
+ - `lib/ea/transformers/qea_to_xmi/emitters/*` — 13 emitter classes
168
+ (BaseEmitter, PackageEmitter, ClassEmitter, EnumerationEmitter,
169
+ DataTypeEmitter, AttributeEmitter, OperationEmitter, AssociationEmitter,
170
+ GeneralizationEmitter, DependencyEmitter, RealizationEmitter,
171
+ CommentEmitter, InstanceEmitter, SlotEmitter). ~650 lines of
172
+ element-shape code. Each emitter maps one EA database row kind to
173
+ one XMI element kind. The xmi gem already provides typed models
174
+ for every one of these element kinds.
175
+
176
+ Net reduction: ~1000 lines deleted from the ea gem.
177
+
178
+ ### What stays
179
+
180
+ - `lib/ea/transformers/qea_to_xmi/transformer.rb` — orchestrator,
181
+ rewritten as a model-construction walk.
182
+ - `lib/ea/transformers/qea_to_xmi/context.rb` — wraps the
183
+ `Ea::Qea::Database` and the new model graph. Holds lookups and
184
+ IdAllocator.
185
+ - `lib/ea/transformers/qea_to_xmi/id_allocator.rb` — EAID generation
186
+ is EA-specific; the xmi gem has no equivalent.
187
+ - `lib/ea/transformers/qea_to_xmi/guid_format.rb` — `{GUID}` ↔
188
+ `EAID_` translation is EA-specific; xmi gem doesn't know about it.
189
+ - `lib/ea/transformers/qea_to_xmi/sparx_namespaces.rb` — constants
190
+ used by the new code (or inlined where appropriate).
191
+
192
+ ### What changes in the API surface
193
+
194
+ `Ea::Transformers::QeaToXmi.qea_to_xmi_xml(database)` — same public
195
+ signature, same return type (an XMI string). The internal
196
+ implementation is replaced.
197
+
198
+ ## Architecture
199
+
200
+ ### New Transformer flow
201
+
202
+ ```
203
+ Ea::Qea::Database (raw rows)
204
+
205
+
206
+ Ea::Transformers::QeaToXmi::Transformer
207
+ │ walks packages → elements → connectors
208
+ │ instantiates Xmi::Uml::UmlModel, Xmi::Uml::PackagedElement,
209
+ │ Xmi::Uml::OwnedAttribute, Xmi::Uml::Association, etc.
210
+ │ builds Xmi::Sparx::Root
211
+
212
+ Xmi::Sparx::Root
213
+ │ .to_xml(use_prefix: true)
214
+
215
+ Sparx XMI string
216
+ ```
217
+
218
+ ### Responsibilities (MECE)
219
+
220
+ | Component | Owns | Does NOT own |
221
+ |--------------------|-------------------------------------------------------------|-------------------------------------------|
222
+ | `Transformer` | walk order, orchestration, top-level `Root` construction | element shape, EAID generation |
223
+ | `Context` | Database handle, IdAllocator, GUID↔EAID lookups | walk order, element shape |
224
+ | `IdAllocator` | generating EAID_xxx identifiers from a seed + counter | knowing which elements get which ID |
225
+ | `GuidFormat` | `{GUID}` ↔ `EAID_` bidirectional format | anything else |
226
+ | `Models` | (delegated to xmi gem) | — |
227
+ | `Serializer` | (delegated to xmi gem `to_xml`) | — |
228
+
229
+ ### Polymorphism / OCP
230
+
231
+ The current emitter registry pattern (`EmitterRegistry.register(:kind,
232
+ emitter_instance)`) is replaced by lutaml-model's polymorphic
233
+ mapping on `PackagedElement.type`. When `PackagedElement.xmi.type ==
234
+ "uml:Class"`, lutaml-model instantiates `Xmi::Uml::PackagedElement`
235
+ with the right shape; when `xmi.type == "uml:Interface"`, it
236
+ instantiates an interface model. We don't write a polymorphic switch
237
+ — the framework does it.
238
+
239
+ Adding a new UML element type becomes "add a model class to the xmi
240
+ gem" (one file, one mapping), not "write an emitter, register it in
241
+ the dispatch code".
242
+
243
+ ### Performance
244
+
245
+ - Construction walk is O(N) over database rows; xmi gem
246
+ serialization is O(M) over the model graph. Same complexity as the
247
+ custom path; should be neutral or faster because the framework
248
+ amortizes XML construction overhead.
249
+ - Skip Reference resolution on the ea side: we already know the
250
+ `t_connector.Start_Object_ID` → referenced EAID mapping at
251
+ construction time.
252
+ - No double-buffering or string concatenation in our code — let
253
+ lutaml-model handle the serialization.
254
+
255
+ ### Code-quality rules (strict)
256
+
257
+ - **No `send` on private methods.** If dispatch needs to reach into
258
+ another object, redesign the API.
259
+ - **No `instance_variable_set`/`_get`.** Access via public readers or
260
+ rethink ownership.
261
+ - **No `respond_to?`.** Use `is_a?` or design the type hierarchy so
262
+ the check is unnecessary.
263
+ - **No `require_relative` or internal `require`.** Use `autoload` in
264
+ the immediate parent namespace's file. Define new autoload entries
265
+ in `ea/transformers/qea_to_xmi.rb` (the parent file).
266
+ - **No hand-rolled `to_h`/`from_h`/`to_json`/`from_json` on models.**
267
+ All (de)serialization goes through lutaml-model. (We use the xmi
268
+ gem's models, which already follow this rule.)
269
+ - **No `double()` in specs.** Use real `Xmi::Sparx::Root`,
270
+ `Xmi::Uml::PackagedElement` instances. For raw test data, use
271
+ `Struct.new`.
272
+ - **Files under ~300 lines.** The current 232-line `writer.rb` and
273
+ 142-line `association_emitter.rb` are at the edge; the rewrite
274
+ brings all files comfortably under.
275
+ - **All public methods have specs.** Behavioral edge cases covered.
276
+
277
+ ### Spec strategy
278
+
279
+ Specs live in `spec/ea/transformers/qea_to_xmi/` and assert three
280
+ properties:
281
+
282
+ 1. **Parity with current implementation** — given the same database,
283
+ the new transformer's output produces a structurally equivalent
284
+ XMI document (same EAIDs, same xmi:type discriminators, same
285
+ hierarchy, same element counts). Compared by re-parsing both
286
+ outputs with `Xmi::Sparx::Root.from_xml` and diffing the model
287
+ graphs.
288
+ 2. **Round-trip** — the new output can be parsed by the xmi gem
289
+ (`Xmi::Sparx::Root.from_xml(transformer.call(database))`) without
290
+ errors, and the parsed model has the expected element counts.
291
+ 3. **Plateau smoke** — `Ea::Qea.load("spec/fixtures/plateau.qea")
292
+ .then { |db| Transformer.new(db).call.to_xml }` produces the
293
+ same 1.32 MB output with 581 classes, 420 generalizations, 431
294
+ associations, 0 XML errors.
295
+
296
+ Specs use real models — no doubles, no stubs of xmi gem classes.
297
+
298
+ ## Open questions / known gaps
299
+
300
+ 1. **EA Extension block** — the current implementation emits a
301
+ stub `<xmi:Extension>` with diagrams and tagged values. The xmi
302
+ gem has typed models for `Extension`, `Element`, `Connector`,
303
+ `Diagram`, `PrimitiveType`, `CustomProfile`. We can wire these
304
+ up; that's what the new Transformer emits.
305
+ 2. **RunState slot emission** — Phase 2 of the original plan. Out of
306
+ scope for this TODO unless it falls out naturally from walking
307
+ `t_object.RunState` in the Instance element construction path.
308
+ 3. **Stereotype / profile application** — emitted as `<profileApplication>`
309
+ in current code. The xmi gem has `Xmi::Uml::ProfileApplication` and
310
+ `Xmi::Uml::ProfileApplicationAppliedProfile`. Wire up if the
311
+ current code emits them.
312
+
313
+ ## Implementation plan
314
+
315
+ 1. Create feature branch `feat/qeatoxmi-via-xmi-gem` in this repo
316
+ 2. Add xmi gem to gemspec if not already present (TODO.next/19
317
+ resolved the gemspec recently — confirm)
318
+ 3. Rewrite `Transformer` to walk Database → build `Xmi::Sparx::Root`
319
+ 4. Slim `Context` to just Database + IdAllocator (drop Writer ref)
320
+ 5. Delete `xml_builder.rb`, `writer.rb`, `emitter_registry.rb`,
321
+ `emitters/`
322
+ 6. Update specs: replace emitter/writer specs with parity +
323
+ round-trip + plateau specs
324
+ 7. Run full ea gem suite, ensure parity
325
+ 8. PR for review
326
+
327
+ ## Risks
328
+
329
+ - **Behavior change.** The output bytes will differ from the
330
+ current XmlBuilder output in some cases (whitespace, attribute
331
+ ordering, namespace declarations). We assert structural parity via
332
+ re-parse, not byte-equality.
333
+ - **xmi gem schema coverage gaps.** If a database row kind has no
334
+ matching xmi gem model, the new Transformer will fail. The current
335
+ plateau fixture must be fully representable; if not, we add the
336
+ missing model to the xmi gem first.
337
+ - **xmi gem PR #87 must merge** before we can rely on
338
+ `to_xml(use_prefix: true)` producing the Sparx mixed-prefix style.
339
+ Until then, we develop against the branch in `Gemfile`/local
340
+ checkout.
@@ -0,0 +1,32 @@
1
+ # 22 - Strip `respond_to?` from QeaToXmi specs
2
+
3
+ ## Status: DONE (2026-07-01)
4
+
5
+ ## Problem
6
+ `spec/ea/transformers/qea_to_xmi/transformer_spec.rb:174` uses
7
+ `model.respond_to?(:packaged_element)` for duck-typing during the
8
+ recursive count walk. The project rule (CLAUDE.md) explicitly forbids
9
+ `respond_to?` for type dispatch — it hides type errors until runtime
10
+ and bypasses the type hierarchy.
11
+
12
+ ## Fix
13
+ Replace with an explicit type check against the two xmi gem classes
14
+ that own `packaged_element`:
15
+
16
+ ```ruby
17
+ def count_xmi_type_recursive(model, type)
18
+ count = model.is_a?(::Xmi::Uml::PackagedElement) && model.type == type ? 1 : 0
19
+ children = model.is_a?(::Xmi::Uml::PackagedElement) || model.is_a?(::Xmi::Uml::UmlModel) \
20
+ ? model.packaged_element : []
21
+ count + children.sum { |child| count_xmi_type_recursive(child, type) }
22
+ end
23
+ ```
24
+
25
+ Both classes (`PackagedElement`, `UmlModel`) are the only xmi gem
26
+ types that own packaged children; the explicit `is_a?` check makes
27
+ that contract visible in the spec.
28
+
29
+ ## Verification
30
+ `bundle exec rspec spec/ea/transformers/qea_to_xmi/` still passes
31
+ 39 examples, 0 failures. Grep confirms no `respond_to?` remains in
32
+ `spec/ea/transformers/qea_to_xmi/`.
@@ -0,0 +1,41 @@
1
+ # 23 - Clean up IdAllocator: dead constant, unused param, DRY
2
+
3
+ ## Status: DONE (2026-07-01)
4
+
5
+ ## Problem
6
+ `lib/ea/transformers/qea_to_xmi/id_allocator.rb` has three issues:
7
+
8
+ 1. **Dead constant.** `LITERAL_UNLIMITED = "LI"` (line 16) is declared
9
+ with a comment "Sparx reuses the LI prefix for both" but is never
10
+ referenced anywhere in `lib/` or `spec/`. The comment explains
11
+ intent that was never realised in code.
12
+
13
+ 2. **Ignored parameter.** `for_multiplicity(value, seed:)` (line 31)
14
+ takes a positional `value` arg (`:upper` or `:lower`) but never
15
+ reads it. The method always uses `LITERAL_INTEGER`. Misleading API
16
+ — callers believe the arg affects the output.
17
+
18
+ 3. **DRY violation.** `allocate` and `for_multiplicity` have nearly
19
+ identical bodies (counter increment, format with `LITERAL_INTEGER`,
20
+ memoize by seed).
21
+
22
+ ## Fix
23
+ - Drop `LITERAL_UNLIMITED`.
24
+ - Drop `for_multiplicity` entirely; have callers use `allocate` with
25
+ the appropriate prefix constant.
26
+ - The transformer's two multiplicity callers
27
+ (`build_upper_value`, `build_lower_value`) become:
28
+ ```ruby
29
+ id: @context.id_allocator.allocate(prefix: IdAllocator::LITERAL_INTEGER, seed: seed)
30
+ ```
31
+
32
+ The xmi:type discriminator (`uml:LiteralUnlimitedNatural` vs
33
+ `uml:LiteralInteger`) is already set on the model constructor — it
34
+ does not belong in the ID allocator.
35
+
36
+ ## Verification
37
+ - New `spec/ea/transformers/qea_to_xmi/id_allocator_spec.rb` covers
38
+ `allocate` (counter, memoization, prefix).
39
+ - Full QeaToXmi specs unchanged at 39 examples.
40
+ - Grep confirms `LITERAL_UNLIMITED` and `for_multiplicity` no longer
41
+ appear anywhere in `lib/` or `spec/`.
@@ -0,0 +1,42 @@
1
+ # 24 - Tighten QeaToXmi parity specs (exact counts, not ranges)
2
+
3
+ ## Status: DONE (2026-07-01)
4
+
5
+ ## Problem
6
+ The round-trip class count spec at
7
+ `spec/ea/transformers/qea_to_xmi/transformer_spec.rb:178-185` accepts
8
+ *any* count between 1 and the database class count:
9
+
10
+ ```ruby
11
+ expect(actual).to be > 0
12
+ expect(actual).to be <= expected
13
+ ```
14
+
15
+ That range-based parity hides regressions silently — if the
16
+ transformer started dropping half the classes, the spec would still
17
+ pass. The comment "the count is approximate" makes the looseness
18
+ explicit but doesn't justify it: every filter the transformer applies
19
+ (`transformer_type` returning nil for Note / Text / ProxyConnector)
20
+ is deterministic and countable.
21
+
22
+ ## Fix
23
+ - Compute the expected count by applying the same filter
24
+ `EaObject#transformer_type` applies, then assert equality.
25
+ - Document in the spec which object_types are intentionally dropped
26
+ (Note, Text, ProxyConnector — see `ea_object.rb:177-208`).
27
+
28
+ ```ruby
29
+ it "preserves class count from the database (filtering dropped types)" do
30
+ # The transformer drops Note / Text / ProxyConnector rows because
31
+ # they have no UML model equivalent (see EaObject#transformer_type).
32
+ expected = database.objects.count { |o| o.transformer_type == :class }
33
+ actual = count_xmi_type_recursive(reparsed.model, "uml:Class")
34
+ expect(actual).to eq(expected)
35
+ end
36
+ ```
37
+
38
+ ## Verification
39
+ - Round-trip class count spec now asserts equality.
40
+ - New spec covers enumeration and data_type round-trip counts with
41
+ the same pattern.
42
+ - Full QeaToXmi specs pass.
@@ -0,0 +1,62 @@
1
+ # 25 - Sparx-conformant EAID format for synthesized IDs
2
+
3
+ ## Status: DONE (2026-07-01)
4
+
5
+ ## Problem
6
+ Real Sparx XMI emits `<upperValue>` / `<lowerValue>` identifiers in
7
+ the form:
8
+
9
+ ```
10
+ EAID_LI000001__EEB1_4de7_98F5_670D6EE4A52B
11
+ ```
12
+
13
+ The `LI` prefix identifies a LiteralInteger; the suffix is the parent
14
+ element's GUID tail, making each synthesized ID globally unique and
15
+ traceable back to its owner.
16
+
17
+ The current IdAllocator emits bare counter IDs:
18
+
19
+ ```
20
+ LI000009
21
+ ```
22
+
23
+ No `EAID_` prefix, no GUID suffix. Two consequences:
24
+
25
+ 1. **Not round-trip-safe.** Sparx importer may treat the IDs as
26
+ foreign and synthesise new ones, breaking ID stability across
27
+ import/export cycles.
28
+ 2. **Not traceable.** A bare `LI000009` in serialized output gives
29
+ no hint which element owns it; debugging serialized XMI is harder.
30
+
31
+ The spec parity tests did not catch this because they count
32
+ `<upperValue>` elements but never assert the ID shape.
33
+
34
+ ## Fix
35
+ Two changes:
36
+
37
+ 1. **IdAllocator gains a `parent_guid` parameter** on `allocate`.
38
+ When provided, the allocated ID incorporates the parent GUID tail
39
+ so it is traceable and Sparx-conformant.
40
+ 2. **Transformer passes the parent object's GUID** at every
41
+ allocation site.
42
+
43
+ New ID format: `EAID_<PREFIX><NN>_<GUID_TAIL>`
44
+
45
+ - `EAID_` prefix matches other element IDs (packages, classes).
46
+ - `<PREFIX>` is the well-known Sparx prefix (LI, SL, OE, RT, ...).
47
+ - `<NN>` is a zero-padded 6-digit counter scoped to the parent
48
+ (so multiple LiteralIntegers on the same parent get distinct IDs).
49
+ - `<GUID_TAIL>` is the parent EA GUID with braces/dashes normalised
50
+ to underscores (reusing `GuidFormat.ea_guid_to_xmi_id`'s normalisation).
51
+
52
+ For the legacy/no-parent case (synthesizing an ID without an owner
53
+ GUID), `allocate` still produces `EAID_<PREFIX><NN>` — better than
54
+ the bare `LI000009`, still uniquely identifiable.
55
+
56
+ ## Verification
57
+ - New `id_allocator_spec.rb` asserts the Sparx format for both
58
+ parented and parentless allocations.
59
+ - Transformer output now emits IDs like
60
+ `EAID_LI000001_EEB1_4de7_98F5_670D6EE4A52B` matching the reference
61
+ fixture's shape (verified against `spec/fixtures/basic.xmi`).
62
+ - Round-trip via `Xmi::Sparx::Root.parse_xml` still succeeds.
@@ -0,0 +1,51 @@
1
+ # 26 - Always emit upperValue / lowerValue on Property and OwnedEnd
2
+
3
+ ## Status: ✅ DONE (2026-07-01) — attribute AND association-end paths complete
4
+
5
+ ## Problem
6
+ Reference `spec/fixtures/basic.xmi` carried 149 `<upperValue>` and
7
+ 149 `<lowerValue>` elements. Pre-fix transformer output emitted 102 of
8
+ each — a 47-element gap per type. The gap came from two paths:
9
+
10
+ 1. **Attributes without bounds in QEA.** `build_attribute` returned
11
+ `nil` for `upper_value` / `lower_value` when the QEA bound field
12
+ was blank, silently omitting the child element.
13
+ 2. **Association-end bounds.** `build_association_end` had the same
14
+ nil-return-on-blank pattern.
15
+
16
+ Real Sparx XMI always materialises both child elements on every
17
+ `<ownedAttribute>` and `<ownedEnd>` — empty bounds render as
18
+ `<lowerValue value="0"/>` and `<upperValue value="-1"/>` (UML
19
+ unspecified-multiplicity defaults).
20
+
21
+ ## Fix
22
+
23
+ ### Path 1 (attributes): ea-side
24
+ `build_attribute` always calls `build_upper_value` /
25
+ `build_lower_value`. `Cardinality.normalize_upper("")` returns `"-1"`,
26
+ `Cardinality.normalize_lower("")` returns `"0"`. Every Property now
27
+ has both child elements.
28
+
29
+ ### Path 2 (association ends): xmi gem schema migration
30
+ `Xmi::Uml::OwnedEnd` declared `upper`/`lower` as Integer attributes
31
+ — NOT `upper_value`/`lower_value` child-element models like
32
+ `OwnedAttribute`. The ea transformer was passing `upper_value:`/
33
+ `lower_value:` kwargs that the xmi gem silently dropped.
34
+
35
+ The xmi gem refactor branch `refactor/owned-end-schema-gap` (commit
36
+ on `lutaml/xmi` 2026-07-01) unified the schema: dropped Integer
37
+ attrs, added child-element models matching OwnedAttribute. After
38
+ this migration, the ea transformer's existing `build_association_end`
39
+ calls now produce the correct child elements with no ea-side change.
40
+
41
+ ## Verification
42
+ - Every `<ownedAttribute>` in output has both `<upperValue>` and
43
+ `<lowerValue>` children.
44
+ - Every `<ownedEnd>` in output has both `<upperValue>` and
45
+ `<lowerValue>` children.
46
+ - Output upperValue count: 182 (was 102). Reference basic.xmi has
47
+ 149 — the difference is because real Sparx omits bounds for some
48
+ attribute kinds; the ea gem emits consistently for round-trip
49
+ safety. Round-trip via `Xmi::Sparx::Root.parse_xml` succeeds.
50
+ - Phase 2 sentinel spec in `transformer_spec.rb` flipped from
51
+ negative ("does not emit") to positive ("emits") assertion.
@@ -0,0 +1,68 @@
1
+ # 27 - Extract CardinalityParser module from Transformer
2
+
3
+ ## Status: DONE (2026-07-01)
4
+
5
+ ## Problem
6
+ `lib/ea/transformers/qea_to_xmi/transformer.rb` mixes five concerns
7
+ in one 503-line class:
8
+
9
+ - orchestration (build_root, build_model, build_extension)
10
+ - walk traversal (build_package, package_children, subpackages)
11
+ - element construction (build_class, build_attribute, ...)
12
+ - **cardinality parsing** (parse_cardinality, parse_range,
13
+ normalize_bound, normalize_upper, normalize_lower, UNLIMITED_TOKENS)
14
+ - XML post-processing (strip_empty_elements)
15
+
16
+ Cardinality parsing is a pure-function concern with no dependency on
17
+ the transformer's state. It belongs in its own module so it can be
18
+ unit-tested in isolation and reused if other transformers need the
19
+ same logic (e.g., UmlToXmi, future LutamlToXmi).
20
+
21
+ ## Fix
22
+ Extract a `Cardinality` module under
23
+ `lib/ea/transformers/qea_to_xmi/cardinality.rb`:
24
+
25
+ ```ruby
26
+ module Ea::Transformers::QeaToXmi
27
+ module Cardinality
28
+ UNLIMITED_TOKENS = %w[* *-1 unbounded].freeze
29
+ DEFAULT_UPPER = "-1"
30
+ DEFAULT_LOWER = "0"
31
+
32
+ module_function
33
+
34
+ def parse(raw)
35
+ return empty_bounds if raw.nil? || raw.to_s.empty?
36
+ stripped = raw.to_s.strip
37
+ return parse_range(stripped) if stripped.include?("..")
38
+ single = normalize_bound(stripped)
39
+ { lower: single, upper: single }
40
+ end
41
+
42
+ def normalize_upper(raw)
43
+ UNLIMITED_TOKENS.include?(raw.to_s.strip.downcase) ? "-1" : raw.to_s
44
+ end
45
+
46
+ def normalize_lower(raw)
47
+ raw = raw.to_s.strip
48
+ raw.empty? ? DEFAULT_LOWER : raw
49
+ end
50
+
51
+ # ... private helpers
52
+ end
53
+ end
54
+ ```
55
+
56
+ The transformer imports it (`extend Cardinality` or call via
57
+ `Cardinality.parse(...)`). File size drops by ~40 lines; logic is
58
+ testable in isolation.
59
+
60
+ This change also resolves TODO 33 (`normalize_lower` was identity;
61
+ now it normalises empty → "0").
62
+
63
+ ## Verification
64
+ - New `spec/ea/transformers/qea_to_xmi/cardinality_spec.rb` covers
65
+ edge cases: `nil`, `""`, `"*"`, `"0..*"`, `"1..1"`, `"unbounded"`,
66
+ malformed input.
67
+ - Transformer file drops below 470 LOC.
68
+ - All existing transformer specs continue to pass.