fix: QEA to XMI export parity - #33
Conversation
There was a problem hiding this comment.
Pull request overview
This PR tightens QEA → Sparx XMI export parity by aligning synthesized IDs, visibility handling, xref/$XREFPROP serialization, diagram geometry/path encoding, and emitting additional UML elements (nested classifiers, dependencies, signals, primitive types). It also adds a parity “ratchet” spec to prevent regressions against the checked-in EA reference exports.
Changes:
- Reworked synthesized ID allocation (LI/SL/OE/RT) and classifier walk/allocation order to better match EA’s counters and nesting conventions.
- Implemented/expanded Extension serialization: populated
<xrefs>via$XREFPROP, added<primitivetypes>, and improved diagram<elements>geometry/style/path output. - Added/expanded specs for parity, visibility/textual scopes, run-state operator handling, MDG registry discovery, and CLI export behavior.
Reviewed changes
Copilot reviewed 28 out of 28 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| spec/ea/transformers/qea_to_xmi/visibility_spec.rb | Adds coverage for textual scope values and Public suppression. |
| spec/ea/transformers/qea_to_xmi/transformer_spec.rb | Updates expectations for nested classifiers, visibility, synthesized IDs, signals, primitive hrefs, deps, and realizations. |
| spec/ea/transformers/qea_to_xmi/run_state_spec.rb | Updates expectations to keep multi-char operators verbatim. |
| spec/ea/transformers/qea_to_xmi/parity_spec.rb | New parity ratchet over example models + self-parse check. |
| spec/ea/transformers/qea_to_xmi/id_allocator_spec.rb | Refactors allocator specs to assert EA-derived ID shape/counters. |
| spec/ea/transformers/qea_to_xmi/extension_serializer_spec.rb | Adds expectations for xrefs, signals, primitivetypes, diagram element serialization. |
| spec/ea/transformers/qea_to_xmi/cardinality_spec.rb | Updates unlimited upper bound wire form to * and treats stored -1 as unlimited. |
| spec/ea/qea/models/ea_xref_spec.rb | Adds mapping coverage for Visibility/Behavior/Partition. |
| spec/ea/qea/models/ea_connector_spec.rb | Adds UK spelling realization coverage. |
| spec/ea/mdg/registry_from_paths_spec.rb | New tests for registry loading from files/dirs with graceful skipping. |
| spec/ea/cli/command/export_spec.rb | New CLI export specs for --mdg behavior (xmi-only). |
| lib/ea/transformers/qea_to_xmi/visibility.rb | Supports textual scopes with integer fallback. |
| lib/ea/transformers/qea_to_xmi/transformer.rb | Implements EA-like walk/nesting, bounds preallocation, dependency endpoint refs, signals, classifier ref resolution, realization filtering. |
| lib/ea/transformers/qea_to_xmi/run_state.rb | Emits run-state operator verbatim in OpaqueExpression bodies. |
| lib/ea/transformers/qea_to_xmi/primitive_types.rb | New helper to synthesize EA’s primitivetypes extension hierarchy + OMG href mapping. |
| lib/ea/transformers/qea_to_xmi/id_allocator.rb | Redesigns allocator to memoize by (owner,prefix,seed) with global vs per-owner counters and EA-like tail/width rules. |
| lib/ea/transformers/qea_to_xmi/extension_serializer.rb | Populates xrefs via $XREFPROP, adds primitivetypes section, improves diagram element serialization and Path separator handling. |
| lib/ea/transformers/qea_to_xmi/cardinality.rb | Switches unlimited wire form to * and expands unlimited token normalization. |
| lib/ea/transformers/qea_to_xmi.rb | Autoloads PrimitiveTypes. |
| lib/ea/qea/models/ea_xref.rb | Adds visibility/behavior/partition attributes for xref serialization. |
| lib/ea/qea/models/ea_object.rb | Centralizes transformer type mapping (adds Signal) and keeps enum stereotype override. |
| lib/ea/qea/models/ea_connector.rb | Treats both Realization/Realisation as realizations. |
| lib/ea/mdg/registry.rb | Adds from_paths and recursive directory expansion with safe skipping. |
| lib/ea/cli/command/mdg.rb | Uses Registry.from_paths for MDG discovery. |
| lib/ea/cli/command/export.rb | Threads optional MDG registry through xmi exporter only. |
| lib/ea/cli/app.rb | Adds --mdg CLI option (xmi only). |
| Gemfile | Temporarily pins xmi to a GitHub branch for unreleased features. |
| ea.gemspec | Loosens xmi dependency floor to support the temporary pin. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 36 out of 36 changed files in this pull request and generated 1 comment.
Suppressed comments (2)
lib/ea/transformers/qea_to_xmi/transformer.rb:724
- build_return_parameter allocates the RT id tail from @context.xmi_id_for(op). When t_operation.ea_guid is nil/blank, xmi_id_for(op) is nil, which produces tailless RT ids and (because the RT counter is per-owner) can also make RT numbering drift across operations. Consider skipping the synthesized return parameter when the operation has no xmi id (matching the extension serializer’s behavior).
id: @context.id_allocator.allocate(
prefix: IdAllocator::RETURN_PARAMETER,
seed: "return-#{op.operationid}",
owner_id: @context.xmi_id_for(op),
),
lib/ea/transformers/qea_to_xmi/transformer.rb:305
- build_classifier_with_bounds preallocates association-end bounds even when build_classifier returns nil (e.g., object types without a registered builder). That can advance the global LI counter and allocate IDs for elements that will never be emitted, skewing synthesized-ID parity.
preallocate_attribute_bounds(obj)
element = build_classifier(obj)
preallocate_end_bounds(obj)
element
d04468d to
563608b
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 36 out of 36 changed files in this pull request and generated 1 comment.
Suppressed comments (2)
lib/ea/transformers/qea_to_xmi/transformer.rb:381
package_level_relationshipsusesSet.new/add?, butSetis not required anywhere in lib. This can raiseNameErrorin production usage depending on load order. Switching to a Hash for de-duplication avoids the dependency onset(or add an explicitrequire "set").
def package_level_relationships(pkg)
emitted = Set.new
relationship_sources_in(pkg).flat_map do |obj|
@context.connectors_starting_at(obj.ea_object_id).filter_map do |conn|
key = RELATIONSHIP_AT_PACKAGE_LEVEL[conn.connector_type]
next nil unless key
lib/ea/transformers/qea_to_xmi/transformer.rb:716
Context#xmi_id_forreturns nil whent_operation.ea_guidis nil; in that case EA can't derive the RT tail, and exporting a synthesized return parameter will either allocate a tailless RT id or (depending on downstream behavior) produce broken/parity-divergent XML. Since the extension serializer already skips return entries when the operation guid is blank, the UML-tree return parameter should also be skipped whenxmi_id_for(op)is nil.
# Blank-checked the way PrimitiveTypes normalizes names, so a
# whitespace-only Type cannot synthesize a return here that
# primitive discovery then treats as absent.
params << build_return_parameter(op) unless PrimitiveTypes.normalize_name(op.type).empty?
params
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 36 out of 36 changed files in this pull request and generated no new comments.
Suppressed comments (2)
lib/ea/transformers/qea_to_xmi/transformer.rb:302
Setis used here without an explicitrequire "set"anywhere inlib/, so loading this transformer in isolation can raiseNameError: uninitialized constant Setdepending on load order. Consider avoidingSethere (or adding a require at the top of the file).
def cyclic_ancestry?(obj)
seen = Set[obj.ea_object_id]
current = obj
while (current = nesting_parent_of(current))
return true unless seen.add?(current.ea_object_id)
lib/ea/transformers/qea_to_xmi/transformer.rb:384
Setis used to track emitted connector IDs, but the transformer file does not requireset(and there is norequire "set"underlib/). This can raiseNameErrorin some load orders. Using a Hash avoids the dependency and keeps the same semantics.
def package_level_relationships(pkg)
emitted = Set.new
relationship_sources_in(pkg).flat_map do |obj|
@context.connectors_starting_at(obj.ea_object_id).filter_map do |conn|
key = RELATIONSHIP_AT_PACKAGE_LEVEL[conn.connector_type]
next nil unless key
next nil unless emitted.add?(conn.connector_id)
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 36 out of 36 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
lib/ea/transformers/qea_to_xmi/transformer.rb:304
Setis used here but the stdlibsetis not required anywhere in the project, which will raiseNameError: uninitialized constant Setwhen this code path runs.
def cyclic_ancestry?(obj)
seen = Set[obj.ea_object_id]
current = obj
while (current = nesting_parent_of(current))
return true unless seen.add?(current.ea_object_id)
end
false
Fixes #32.
Gets our QEA→XMI export much closer to EA's own output.
What changed:
$XREFPROPmini-language.*for unlimited bounds, never-1.Parity distance (only-ours / only-EA / mismatched):
Merge order matters
Companion PR: lutaml/xmi#94. Merge and release that one first.
lutaml-model drops an undeclared attribute without raising. So on xmi
0.6.2 the
nested_classifier:we pass vanishes silently. That is 35 ofbasic.qea's 65 classes, gone, with no error.
Temporary until xmi 0.7.0 ships:
>= 0.6.2.~> 0.7.Known gaps
uml:PackageImportfor Package connectors.<type>child. EA gives all 12 in basic one.uml:Associationplusuml:Class.EAnone_reference.Defining those over-emits, so a pending spec pins it instead.
ExtensionEndwrites a<type>child where EA writestype=.Blocked: lutaml-model matches attributes by local name, so a plain
typecollides withxmi:type.OwnedAttributehas nois_static, soIsStatic=1is dropped.No fixture hits it and EA's references carry no
isStatic.Pre-existing, worth their own issue
All eight QEA schemas declare
ea_guidnullable and the loader filtersnothing, so these are reachable from real data. None is introduced by
this PR — each is on
maintoday. No fixture carries a blank GUID, sonothing here is exercised by the suite.
t_connector.ea_guidcrashes the export.connector_end_xmi_idcalls.subon nil —NoMethodError.t_object.ea_guidallocatestailless SL and OE ids. Null and empty share one allocator key while
distinct whitespace values get separate counters, so these can emit
duplicate
xmi:idvalues.t_attribute.ea_guidgives its bounds tailless LI ids. Theglobal LI counter keeps them unique, so this one is cosmetic.
packagedElementcarrying noxmi:idin the model tree, while the extension block skips it entirely.