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.
- checksums.yaml +4 -4
- data/CLAUDE.md +125 -0
- data/Rakefile +12 -4
- data/TODO.next/00-publish-blocking-bugs.md +74 -0
- data/TODO.next/01-standalone-ea-gem-identity.md +76 -0
- data/TODO.next/02-optional-lutaml-uml-dependency.md +47 -0
- data/TODO.next/03-slim-lutaml-uml.md +79 -0
- data/TODO.next/04-loader-registry-for-uml-repository.md +49 -0
- data/TODO.next/05-extract-shared-transformer-methods.md +14 -0
- data/TODO.next/06-deduplicate-stereotype-loading.md +17 -0
- data/TODO.next/07-transformer-registry-in-factory.md +20 -0
- data/TODO.next/08-connector-type-registry.md +27 -0
- data/TODO.next/09-element-renderer-registry.md +29 -0
- data/TODO.next/10-connector-renderer-lsp.md +18 -0
- data/TODO.next/11-consolidate-style-knowledge.md +33 -0
- data/TODO.next/12-data-driven-from-db-row.md +24 -0
- data/TODO.next/13-extract-duplicated-methods.md +17 -0
- data/TODO.next/14-remove-dead-code.md +10 -0
- data/TODO.next/15-narrow-exception-handling.md +39 -0
- data/TODO.next/16-repository-indexes.md +28 -0
- data/TODO.next/17-fix-spec-quality-and-coverage.md +32 -0
- data/TODO.next/18-xmi-tool-specific-parser-architecture.md +172 -0
- data/TODO.next/19-fix-ea-gemspec-dependency-declarations.md +56 -0
- data/TODO.next/20-ci-requires-unreleased-lutaml-uml.md +63 -0
- data/TODO.next/21-qeatoxmi-via-xmi-gem.md +340 -0
- data/TODO.next/22-strip-respond-to-from-qeatoxmi-specs.md +32 -0
- data/TODO.next/23-cleanup-idallocator.md +41 -0
- data/TODO.next/24-tighten-parity-specs.md +42 -0
- data/TODO.next/25-sparx-eaid-format-for-synthesized-ids.md +62 -0
- data/TODO.next/26-fix-uppervalue-lowervalue-count-gap.md +51 -0
- data/TODO.next/27-extract-cardinality-module.md +68 -0
- data/TODO.next/28-extract-xml-sanitizer.md +51 -0
- data/TODO.next/29-ocp-registry-for-classifier-builders.md +58 -0
- data/TODO.next/30-struct-return-for-association-end.md +37 -0
- data/TODO.next/31-idallocator-specs.md +27 -0
- data/TODO.next/32-phase2-gap-sentinel-specs.md +53 -0
- data/TODO.next/33-normalize-lower-cleanup.md +30 -0
- data/TODO.next/34-document-member-end-order-rt-prefix.md +29 -0
- data/TODO.next/35-walk-runstate-for-instance-slots.md +76 -0
- data/TODO.next/36-wire-interface-realization.md +50 -0
- data/TODO.next/37-visibility-returns-real-booleans.md +36 -0
- data/config/diagram_styles.yml +200 -0
- data/config/model_transformations.yml +266 -0
- data/config/qea_schema.yml +1024 -0
- data/docs/ea_to_uml_type_mapping.md +89 -0
- data/docs/xmi_qea_conversion_capabilities.md +99 -0
- data/examples/lur/20251010_current_plateau_v5.1.lur +0 -0
- data/examples/lur/basic.lur +0 -0
- data/examples/lur/test-output.lur +0 -0
- data/examples/lur/test.lur +0 -0
- data/examples/lur_basic_usage.rb +221 -0
- data/examples/lur_cli_workflow.rb +263 -0
- data/examples/lur_statistics.rb +326 -0
- data/examples/qea/20251010_current_plateau_v5.1.qea +0 -0
- data/examples/qea/ArcGISWorkspace_template.qea +0 -0
- data/examples/qea/README_qea_parser.adoc +230 -0
- data/examples/qea/UmlModel_template.qea +0 -0
- data/examples/qea/basic.qea +0 -0
- data/examples/qea/simple.qea +0 -0
- data/examples/qea/simple_example.qea +0 -0
- data/examples/qea/test.qea +0 -0
- data/examples/qea_standalone_query.rb +73 -0
- data/examples/qea_to_repository.rb +51 -0
- data/examples/smoke_test_real_qea.rb +81 -0
- data/exe/ea +7 -0
- data/lib/ea/cli/app.rb +72 -0
- data/lib/ea/cli/command/base.rb +80 -0
- data/lib/ea/cli/command/convert.rb +62 -0
- data/lib/ea/cli/command/diagrams.rb +81 -0
- data/lib/ea/cli/command/list.rb +61 -0
- data/lib/ea/cli/command/parse.rb +29 -0
- data/lib/ea/cli/command/stats.rb +20 -0
- data/lib/ea/cli/command/validate.rb +41 -0
- data/lib/ea/cli/command.rb +15 -0
- data/lib/ea/cli/error.rb +34 -0
- data/lib/ea/cli/output/formatter.rb +34 -0
- data/lib/ea/cli/output/json_formatter.rb +20 -0
- data/lib/ea/cli/output/table_formatter.rb +42 -0
- data/lib/ea/cli/output/yaml_formatter.rb +20 -0
- data/lib/ea/cli/output.rb +56 -0
- data/lib/ea/cli.rb +17 -0
- data/lib/ea/diagram/configuration.rb +379 -0
- data/lib/ea/diagram/element_renderers/base_renderer.rb +77 -0
- data/lib/ea/diagram/element_renderers/class_renderer.rb +323 -0
- data/lib/ea/diagram/element_renderers/connector_renderer.rb +41 -0
- data/lib/ea/diagram/element_renderers/package_renderer.rb +61 -0
- data/lib/ea/diagram/element_renderers.rb +43 -0
- data/lib/ea/diagram/extractor.rb +560 -0
- data/lib/ea/diagram/layout_engine.rb +170 -0
- data/lib/ea/diagram/path_builder.rb +202 -0
- data/lib/ea/diagram/style_parser.rb +42 -0
- data/lib/ea/diagram/style_resolver.rb +276 -0
- data/lib/ea/diagram/svg_renderer.rb +274 -0
- data/lib/ea/diagram/util.rb +73 -0
- data/lib/ea/diagram.rb +47 -0
- data/lib/ea/qea/benchmark.rb +210 -0
- data/lib/ea/qea/database.rb +308 -0
- data/lib/ea/qea/factory/association_builder.rb +203 -0
- data/lib/ea/qea/factory/association_transformer.rb +91 -0
- data/lib/ea/qea/factory/attribute_tag_transformer.rb +57 -0
- data/lib/ea/qea/factory/attribute_transformer.rb +93 -0
- data/lib/ea/qea/factory/base_transformer.rb +177 -0
- data/lib/ea/qea/factory/class_transformer.rb +116 -0
- data/lib/ea/qea/factory/constraint_transformer.rb +75 -0
- data/lib/ea/qea/factory/data_type_transformer.rb +77 -0
- data/lib/ea/qea/factory/diagram_transformer.rb +157 -0
- data/lib/ea/qea/factory/document_builder.rb +283 -0
- data/lib/ea/qea/factory/ea_to_uml_factory.rb +229 -0
- data/lib/ea/qea/factory/enum_transformer.rb +74 -0
- data/lib/ea/qea/factory/generalization_builder.rb +227 -0
- data/lib/ea/qea/factory/generalization_transformer.rb +98 -0
- data/lib/ea/qea/factory/instance_transformer.rb +68 -0
- data/lib/ea/qea/factory/object_property_transformer.rb +58 -0
- data/lib/ea/qea/factory/operation_transformer.rb +66 -0
- data/lib/ea/qea/factory/package_transformer.rb +145 -0
- data/lib/ea/qea/factory/reference_resolver.rb +99 -0
- data/lib/ea/qea/factory/stereotype_loader.rb +39 -0
- data/lib/ea/qea/factory/tagged_value_transformer.rb +38 -0
- data/lib/ea/qea/factory/transformer_registry.rb +80 -0
- data/lib/ea/qea/factory.rb +37 -0
- data/lib/ea/qea/file_detector.rb +178 -0
- data/lib/ea/qea/infrastructure/database_connection.rb +100 -0
- data/lib/ea/qea/infrastructure/schema_reader.rb +136 -0
- data/lib/ea/qea/infrastructure/table_reader.rb +224 -0
- data/lib/ea/qea/infrastructure.rb +12 -0
- data/lib/ea/qea/models/base_model.rb +59 -0
- data/lib/ea/qea/models/ea_attribute.rb +109 -0
- data/lib/ea/qea/models/ea_attribute_tag.rb +100 -0
- data/lib/ea/qea/models/ea_complexity_type.rb +79 -0
- data/lib/ea/qea/models/ea_connector.rb +160 -0
- data/lib/ea/qea/models/ea_connector_type.rb +60 -0
- data/lib/ea/qea/models/ea_constraint_type.rb +63 -0
- data/lib/ea/qea/models/ea_datatype.rb +104 -0
- data/lib/ea/qea/models/ea_diagram.rb +115 -0
- data/lib/ea/qea/models/ea_diagram_link.rb +78 -0
- data/lib/ea/qea/models/ea_diagram_object.rb +73 -0
- data/lib/ea/qea/models/ea_diagram_type.rb +56 -0
- data/lib/ea/qea/models/ea_document.rb +63 -0
- data/lib/ea/qea/models/ea_object.rb +223 -0
- data/lib/ea/qea/models/ea_object_constraint.rb +53 -0
- data/lib/ea/qea/models/ea_object_property.rb +87 -0
- data/lib/ea/qea/models/ea_object_type.rb +73 -0
- data/lib/ea/qea/models/ea_operation.rb +127 -0
- data/lib/ea/qea/models/ea_operation_param.rb +76 -0
- data/lib/ea/qea/models/ea_package.rb +78 -0
- data/lib/ea/qea/models/ea_script.rb +62 -0
- data/lib/ea/qea/models/ea_status_type.rb +66 -0
- data/lib/ea/qea/models/ea_stereotype.rb +57 -0
- data/lib/ea/qea/models/ea_tagged_value.rb +99 -0
- data/lib/ea/qea/models/ea_xref.rb +165 -0
- data/lib/ea/qea/models.rb +35 -0
- data/lib/ea/qea/repositories/base_repository.rb +225 -0
- data/lib/ea/qea/repositories/object_repository.rb +219 -0
- data/lib/ea/qea/repositories.rb +10 -0
- data/lib/ea/qea/services/configuration.rb +211 -0
- data/lib/ea/qea/services/database_loader.rb +191 -0
- data/lib/ea/qea/services.rb +10 -0
- data/lib/ea/qea/validation/association_validator.rb +73 -0
- data/lib/ea/qea/validation/attribute_validator.rb +91 -0
- data/lib/ea/qea/validation/base_validator.rb +331 -0
- data/lib/ea/qea/validation/class_validator.rb +121 -0
- data/lib/ea/qea/validation/database/circular_reference_validator.rb +109 -0
- data/lib/ea/qea/validation/database/orphan_validator.rb +153 -0
- data/lib/ea/qea/validation/database/referential_integrity_validator.rb +128 -0
- data/lib/ea/qea/validation/database.rb +16 -0
- data/lib/ea/qea/validation/diagram_validator.rb +112 -0
- data/lib/ea/qea/validation/formatters/json_formatter.rb +137 -0
- data/lib/ea/qea/validation/formatters/text_formatter.rb +235 -0
- data/lib/ea/qea/validation/formatters.rb +12 -0
- data/lib/ea/qea/validation/operation_validator.rb +71 -0
- data/lib/ea/qea/validation/package_validator.rb +111 -0
- data/lib/ea/qea/validation/validation_engine.rb +387 -0
- data/lib/ea/qea/validation/validation_message.rb +144 -0
- data/lib/ea/qea/validation/validation_result.rb +210 -0
- data/lib/ea/qea/validation/validator_registry.rb +134 -0
- data/lib/ea/qea/validation.rb +28 -0
- data/lib/ea/qea/verification/comparison_result.rb +264 -0
- data/lib/ea/qea/verification/document_normalizer.rb +169 -0
- data/lib/ea/qea/verification/document_verifier.rb +322 -0
- data/lib/ea/qea/verification/element_comparator.rb +277 -0
- data/lib/ea/qea/verification/structure_matcher.rb +287 -0
- data/lib/ea/qea/verification.rb +14 -0
- data/lib/ea/qea.rb +185 -0
- data/lib/ea/transformations/configuration.rb +333 -0
- data/lib/ea/transformations/format_registry.rb +366 -0
- data/lib/ea/transformations/parsers/base_parser.rb +482 -0
- data/lib/ea/transformations/parsers/qea_parser.rb +401 -0
- data/lib/ea/transformations/parsers/xmi_parser.rb +243 -0
- data/lib/ea/transformations/transformation_engine.rb +390 -0
- data/lib/ea/transformations.rb +85 -0
- data/lib/ea/transformers/qea_to_xmi/association_end.rb +19 -0
- data/lib/ea/transformers/qea_to_xmi/cardinality.rb +96 -0
- data/lib/ea/transformers/qea_to_xmi/context.rb +106 -0
- data/lib/ea/transformers/qea_to_xmi/guid_format.rb +56 -0
- data/lib/ea/transformers/qea_to_xmi/id_allocator.rb +92 -0
- data/lib/ea/transformers/qea_to_xmi/run_state.rb +107 -0
- data/lib/ea/transformers/qea_to_xmi/transformer.rb +607 -0
- data/lib/ea/transformers/qea_to_xmi/visibility.rb +73 -0
- data/lib/ea/transformers/qea_to_xmi.rb +29 -0
- data/lib/ea/transformers/uml_to_xmi/id_generator.rb +54 -0
- data/lib/ea/transformers/uml_to_xmi/transformer.rb +152 -0
- data/lib/ea/transformers/uml_to_xmi/writer.rb +96 -0
- data/lib/ea/transformers/uml_to_xmi.rb +16 -0
- data/lib/ea/transformers.rb +34 -0
- data/lib/ea/version.rb +1 -1
- data/lib/ea/xmi/liquid_drops/association_drop.rb +56 -0
- data/lib/ea/xmi/liquid_drops/attribute_drop.rb +72 -0
- data/lib/ea/xmi/liquid_drops/cardinality_drop.rb +35 -0
- data/lib/ea/xmi/liquid_drops/connector_drop.rb +54 -0
- data/lib/ea/xmi/liquid_drops/constraint_drop.rb +29 -0
- data/lib/ea/xmi/liquid_drops/data_type_drop.rb +63 -0
- data/lib/ea/xmi/liquid_drops/dependency_drop.rb +36 -0
- data/lib/ea/xmi/liquid_drops/diagram_drop.rb +34 -0
- data/lib/ea/xmi/liquid_drops/enum_drop.rb +49 -0
- data/lib/ea/xmi/liquid_drops/enum_owned_literal_drop.rb +25 -0
- data/lib/ea/xmi/liquid_drops/generalization_attribute_drop.rb +87 -0
- data/lib/ea/xmi/liquid_drops/generalization_drop.rb +127 -0
- data/lib/ea/xmi/liquid_drops/klass_drop.rb +191 -0
- data/lib/ea/xmi/liquid_drops/operation_drop.rb +29 -0
- data/lib/ea/xmi/liquid_drops/package_drop.rb +108 -0
- data/lib/ea/xmi/liquid_drops/root_drop.rb +34 -0
- data/lib/ea/xmi/liquid_drops/source_target_drop.rb +43 -0
- data/lib/ea/xmi/lookup_service.rb +89 -0
- data/lib/ea/xmi/parser.rb +919 -0
- data/lib/ea/xmi.rb +35 -0
- data/lib/ea.rb +10 -1
- 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.
|