ea 0.5.2 → 0.6.0

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 (142) hide show
  1. checksums.yaml +4 -4
  2. data/TODO.complete/01-t-xref-parsing.md +1 -1
  3. data/TODO.complete/02-per-label-geometry.md +1 -1
  4. data/TODO.complete/03-object-properties-constraints.md +1 -1
  5. data/TODO.complete/04-operation-params.md +1 -1
  6. data/TODO.complete/05-pdata-flag-matrix.md +1 -1
  7. data/TODO.complete/06-emf-image-rendering.md +1 -1
  8. data/TODO.complete/07-auxiliary-tables.md +1 -1
  9. data/TODO.complete/10-mdg-stereotype-registry.md +1 -1
  10. data/TODO.complete/11-gml-xsd-generation.md +1 -1
  11. data/TODO.complete/13-shapescript-interpreter.md +12 -23
  12. data/TODO.complete/20-cli-diff.md +1 -1
  13. data/TODO.complete/21-cli-render.md +1 -1
  14. data/TODO.complete/22-cli-export.md +1 -1
  15. data/TODO.complete/23-cli-mdg.md +1 -1
  16. data/TODO.complete/30-headerlines-ocp-refactor.md +1 -1
  17. data/TODO.complete/32-spec-coverage.md +1 -1
  18. data/TODO.complete/33-blocked-parity-gaps.md +1 -1
  19. data/TODO.complete/36-stereotype-icon-wiring.md +1 -1
  20. data/TODO.complete/37-cli-lint.md +1 -1
  21. data/TODO.complete/38-cli-query.md +1 -1
  22. data/TODO.complete/39-cli-info.md +1 -1
  23. data/TODO.complete/40-ocl-evaluator.md +1 -1
  24. data/TODO.complete/41-round-trip-spec.md +1 -1
  25. data/TODO.complete/42-html-diff-report.md +1 -1
  26. data/TODO.complete/43-json-schema-export.md +1 -1
  27. data/TODO.complete/44-remaining-qea-tables.md +1 -1
  28. data/TODO.complete/45-shapescript-extensions.md +1 -1
  29. data/TODO.complete/46-xsd-consolidation.md +1 -1
  30. data/TODO.complete/47-headerlines-legacy-removal.md +1 -1
  31. data/TODO.complete/51-remove-respond-to.md +42 -15
  32. data/TODO.complete/52-xsd-decouple-fixtures.md +1 -1
  33. data/TODO.complete/54-wire-stereotype-icon-renderer.md +1 -1
  34. data/TODO.complete/55-wire-shapescript.md +1 -1
  35. data/TODO.complete/56-wire-emf-renderer.md +1 -1
  36. data/TODO.complete/57-wire-ocl-evaluator.md +1 -1
  37. data/TODO.complete/58-diff-modifications.md +1 -1
  38. data/TODO.complete/59-delete-toy-xmi-export.md +1 -1
  39. data/TODO.complete/60-curate-json-export.md +1 -1
  40. data/TODO.complete/61-plantuml-relationships.md +1 -1
  41. data/TODO.complete/62-changelog.md +1 -1
  42. data/TODO.complete/63-dependabot-fix.md +1 -1
  43. data/TODO.complete/64-qeatoxmi-connectors.md +9 -24
  44. data/TODO.complete/65-qeatoxmi-diagrams.md +9 -23
  45. data/TODO.complete/66-qeatoxmi-style-tags-docs.md +25 -18
  46. data/TODO.complete/67-ocl-collection-ops.md +12 -12
  47. data/TODO.complete/68-ocl-comparison-ops.md +16 -11
  48. data/TODO.complete/69-spec-new-models.md +13 -18
  49. data/TODO.complete/70-specs-for-all-models.md +21 -0
  50. data/TODO.complete/71-autoload-audit.md +26 -0
  51. data/TODO.complete/72-extension-serializer-ocp-naming.md +26 -0
  52. data/TODO.complete/73-ocl-validator-latent-bugs.md +50 -0
  53. data/TODO.complete/74-visibility-symbol-dry.md +39 -0
  54. data/TODO.complete/75-database-table-collection-registry.md +32 -0
  55. data/TODO.complete/76-stale-todo-cleanup.md +52 -0
  56. data/TODO.complete/77-eliminate-banned-patterns-from-specs.md +55 -0
  57. data/TODO.complete/78-mdg-stereotype-emission.md +66 -0
  58. data/TODO.complete/79-equivalence-fixes.md +57 -0
  59. data/TODO.complete/80-performance-extension-serializer-indexes.md +43 -0
  60. data/TODO.complete/81-dry-scope-guid-extraction.md +33 -0
  61. data/TODO.complete/82-public-send-audit-conclusion.md +25 -0
  62. data/TODO.complete/83-dry-shared-xml-escape-guid-format.md +52 -0
  63. data/TODO.complete/84-ocp-markers-shape-registry.md +54 -0
  64. data/TODO.complete/85-frozen-literal-rubocop-normalize-type-spec.md +38 -0
  65. data/TODO.complete/86-frozen-sweep-lint-query-specs.md +55 -0
  66. data/TODO.complete/STATUS.md +26 -5
  67. data/lib/ea/cli/app.rb +1 -0
  68. data/lib/ea/cli/command/export.rb +10 -1
  69. data/lib/ea/cli/command/mdg.rb +1 -10
  70. data/lib/ea/cli/command/render.rb +2 -1
  71. data/lib/ea/diff/html_reporter.rb +1 -5
  72. data/lib/ea/export/json/generator.rb +5 -6
  73. data/lib/ea/export/plantuml/generator.rb +5 -3
  74. data/lib/ea/guid_format.rb +91 -0
  75. data/lib/ea/mdg/document.rb +2 -1
  76. data/lib/ea/mdg/loader.rb +2 -1
  77. data/lib/ea/mdg/registry.rb +57 -2
  78. data/lib/ea/mdg/xml/applies_to.rb +1 -1
  79. data/lib/ea/mdg/xml/apply.rb +1 -1
  80. data/lib/ea/mdg/xml/content.rb +1 -1
  81. data/lib/ea/mdg/xml/documentation.rb +1 -1
  82. data/lib/ea/mdg/xml/stereotype.rb +1 -1
  83. data/lib/ea/mdg/xml/stereotypes.rb +1 -1
  84. data/lib/ea/mdg/xml/tag.rb +1 -1
  85. data/lib/ea/mdg/xml/tagged_values.rb +1 -1
  86. data/lib/ea/mdg/xml/technology.rb +1 -1
  87. data/lib/ea/mdg/xml/uml_profile.rb +1 -1
  88. data/lib/ea/mdg/xml/uml_profiles.rb +1 -1
  89. data/lib/ea/mdg/xml.rb +2 -1
  90. data/lib/ea/mdg.rb +2 -1
  91. data/lib/ea/model/ghost_label.rb +2 -1
  92. data/lib/ea/ocl/evaluator.rb +4 -2
  93. data/lib/ea/qea/database.rb +57 -2
  94. data/lib/ea/qea/factory/reference_resolver.rb +1 -1
  95. data/lib/ea/qea/models/ea_attribute.rb +1 -17
  96. data/lib/ea/qea/models/ea_connector.rb +3 -2
  97. data/lib/ea/qea/models/ea_object.rb +14 -9
  98. data/lib/ea/qea/models/ea_operation.rb +1 -17
  99. data/lib/ea/qea/models/ea_xref.rb +3 -0
  100. data/lib/ea/qea/models/scope_predicate.rb +29 -0
  101. data/lib/ea/qea/models.rb +1 -0
  102. data/lib/ea/qea/repositories/base_repository.rb +4 -1
  103. data/lib/ea/qea/validation/base_validator.rb +3 -48
  104. data/lib/ea/qea/validation/database/ocl_constraint_validator.rb +23 -13
  105. data/lib/ea/shapescript/renderer.rb +1 -3
  106. data/lib/ea/sources/qea/diagram_builder.rb +1 -0
  107. data/lib/ea/sources/qea/id_normalizer.rb +2 -2
  108. data/lib/ea/svg/ea_emitter/background.rb +2 -1
  109. data/lib/ea/svg/ea_emitter/element/attribute_line_builder.rb +1 -6
  110. data/lib/ea/svg/ea_emitter/element/operation_renderer.rb +2 -7
  111. data/lib/ea/svg/ea_emitter/elements.rb +1 -3
  112. data/lib/ea/svg/ea_emitter/ghost_labels.rb +2 -1
  113. data/lib/ea/svg/ea_emitter/label/end_label.rb +1 -6
  114. data/lib/ea/svg/ea_emitter/marker/package_import.rb +1 -1
  115. data/lib/ea/svg/ea_emitter/markers.rb +52 -23
  116. data/lib/ea/svg/ea_emitter/text_renderer.rb +1 -4
  117. data/lib/ea/svg/ea_emitter/visibility_symbol.rb +37 -0
  118. data/lib/ea/svg/ea_emitter.rb +1 -0
  119. data/lib/ea/svg/parity/source.rb +0 -2
  120. data/lib/ea/svg/parity/suite.rb +0 -1
  121. data/lib/ea/transformers/qea_to_xmi/cardinality.rb +40 -28
  122. data/lib/ea/transformers/qea_to_xmi/context.rb +35 -1
  123. data/lib/ea/transformers/qea_to_xmi/extension_serializer.rb +686 -0
  124. data/lib/ea/transformers/qea_to_xmi/guid_format.rb +5 -48
  125. data/lib/ea/transformers/qea_to_xmi/id_allocator.rb +55 -54
  126. data/lib/ea/transformers/qea_to_xmi/primitive_types.rb +105 -0
  127. data/lib/ea/transformers/qea_to_xmi/profile_serializer.rb +163 -0
  128. data/lib/ea/transformers/qea_to_xmi/run_state.rb +7 -6
  129. data/lib/ea/transformers/qea_to_xmi/transformer.rb +396 -185
  130. data/lib/ea/transformers/qea_to_xmi/visibility.rb +30 -19
  131. data/lib/ea/transformers/qea_to_xmi.rb +5 -2
  132. data/lib/ea/transformers.rb +7 -2
  133. data/lib/ea/validation.rb +1 -1
  134. data/lib/ea/version.rb +1 -1
  135. data/lib/ea/xmi/liquid_drops/attribute_drop.rb +12 -4
  136. data/lib/ea/xmi/liquid_drops/klass_drop.rb +1 -2
  137. data/lib/ea/xmi/lookup_service.rb +4 -0
  138. data/lib/ea/xmi/parser.rb +30 -1
  139. data/lib/ea/xml_escape.rb +21 -0
  140. data/lib/ea.rb +2 -0
  141. metadata +28 -11
  142. data/lutaml-uml-0.5.1.gem +0 -0
@@ -34,6 +34,7 @@ module Ea
34
34
  MODEL_NAME = "EA_Model"
35
35
  EXPORTER = "Enterprise Architect"
36
36
  EXPORTER_VERSION = "6.5"
37
+ BUILD_ID = "1624"
37
38
 
38
39
  RELATIONSHIP_AT_PACKAGE_LEVEL = {
39
40
  "Association" => :association,
@@ -55,12 +56,36 @@ module Ea
55
56
  class: ->(obj) { build_class(obj) },
56
57
  enumeration: ->(obj) { build_enumeration(obj) },
57
58
  data_type: ->(obj) { build_data_type(obj) },
59
+ signal: ->(obj) { build_class(obj, xmi_type: "uml:Signal") },
58
60
  instance: ->(obj) { build_instance(obj) },
61
+ association_element: ->(obj) { build_association_element(obj) },
59
62
  }.freeze
60
63
 
61
- def initialize(database)
64
+ # What each builder in CLASSIFIER_BUILDERS actually emits, keyed
65
+ # by the same symbols. Every builder must appear here: a kind
66
+ # left out silently loses its attribute bounds or its nested
67
+ # children — wrong XML, no exception — so the spec asserts these
68
+ # keys match CLASSIFIER_BUILDERS exactly.
69
+ #
70
+ # `:owned_attribute` — the builder emits `<ownedAttribute>` and so
71
+ # preallocates attribute bound IDs. Enumerations emit literals
72
+ # (never bounds) and instances emit slots.
73
+ # `:nested_classifier` — the builder walks children, so a kind
74
+ # listed here can adopt an object that carries a parentid.
75
+ # `build_class` is the only builder that walks children.
76
+ CLASSIFIER_CAPABILITIES = {
77
+ class: %i[owned_attribute nested_classifier],
78
+ signal: %i[owned_attribute nested_classifier],
79
+ data_type: %i[owned_attribute],
80
+ enumeration: [],
81
+ instance: [],
82
+ association_element: []
83
+ }.freeze
84
+
85
+ def initialize(database, mdg_registry: nil)
62
86
  @database = database
63
- @context = Context.new(database: database)
87
+ @mdg_registry = mdg_registry
88
+ @context = Context.new(database: database)
64
89
  end
65
90
 
66
91
  # @return [String] XMI XML document
@@ -83,20 +108,68 @@ module Ea
83
108
  # @return [String] XMI XML
84
109
  def serialize(with_extensions: true)
85
110
  xml = build_root.to_xml(use_prefix: true)
111
+ xml = normalize_namespaces(xml)
112
+ xml = plain_parameter_type(xml)
86
113
  with_extensions ? inject_extension_content(xml) : xml
87
114
  end
88
115
 
89
- # Injects EA-specific extension content (connectors, diagrams)
90
- # into the serialized XMI. The xmi gem's Extension type is
91
- # modeled but doesn't declare `connectors` / `diagrams`
92
- # children. We post-process the serialized XML to insert
93
- # them inside the existing `<xmi:Extension>` element.
116
+ # EA writes a return parameter's type unprefixed:
117
+ # `<ownedParameter type="EAnone_void"/>`. Its reference export
118
+ # carries 12 of these and no `xmi:type` on ownedParameter at all.
119
+ #
120
+ # `xmi:type` is the XMI metaclass discriminator; `type` is the
121
+ # classifier reference. Two different attributes — but
122
+ # lutaml-model matches by local name and collapses them into one
123
+ # slot, so the xmi gem can only emit one of them. It keeps the
124
+ # namespaced form, which is right for general UML XMI, and
125
+ # rewriting that in the shared model would break round-tripping
126
+ # for every non-Sparx consumer. Restoring the reference EA
127
+ # actually writes is this exporter's job, so it happens here.
128
+ #
129
+ # Scoped to ownedParameter: no other element wants it. Walks
130
+ # complete name="value" pairs rather than scanning the tag as
131
+ # text — a parameter name is free text out of EA and may itself
132
+ # contain the literal `xmi:type=`, which a raw substitution
133
+ # would happily rewrite inside the quotes.
134
+ ATTRIBUTE_PAIR = /([A-Za-z_][\w:.-]*)=("[^"]*")/
135
+
136
+ def plain_parameter_type(xml)
137
+ xml.gsub(/<ownedParameter\b[^>]*?>/m) do |tag|
138
+ tag.gsub(ATTRIBUTE_PAIR) do
139
+ name = Regexp.last_match(1)
140
+ name == "xmi:type" ? %(type=#{Regexp.last_match(2)}) : Regexp.last_match(0)
141
+ end
142
+ end
143
+ end
144
+
145
+ # EA's reference XMI uses the 2011-07-01 XMI/UML namespace URIs;
146
+ # the xmi gem's Root model hard-codes 2013-10-01. Post-process
147
+ # the serialized XML to match EA's namespace version. This is a
148
+ # targeted string replacement on the xmlns declarations only,
149
+ # not a semantic change.
150
+ def normalize_namespaces(xml)
151
+ xml.gsub("http://www.omg.org/spec/XMI/20131001",
152
+ "http://www.omg.org/spec/XMI/20110701")
153
+ .gsub("http://www.omg.org/spec/UML/20131001/UMLDI",
154
+ "http://www.omg.org/spec/UML/20110701/UMLDI")
155
+ .gsub("http://www.omg.org/spec/UML/20131001/UMLDC",
156
+ "http://www.omg.org/spec/UML/20110701/UMLDC")
157
+ .gsub("http://www.omg.org/spec/UML/20131001",
158
+ "http://www.omg.org/spec/UML/20110701")
159
+ end
160
+
161
+ # Injects EA-specific extension content (elements, connectors,
162
+ # diagrams) into the serialized XMI. The xmi gem's Extension
163
+ # type is modeled but doesn't declare these children. We
164
+ # post-process the serialized XML to insert them inside the
165
+ # existing `<xmi:Extension>` element.
166
+ #
167
+ # Delegated to {ExtensionSerializer} — this class only handles
168
+ # the string substitution mechanics.
94
169
  def inject_extension_content(xml)
95
- content = extension_inner_xml
170
+ content = ExtensionSerializer.new(@database, @context).call
96
171
  return xml if content.empty?
97
172
 
98
- # Replace the empty `<xmi:Extension .../>` or the
99
- # closing `</xmi:Extension>` with the populated version.
100
173
  if xml.include?("<xmi:Extension")
101
174
  xml.sub(%r{<xmi:Extension ([^>]+)>\s*</xmi:Extension>},
102
175
  "<xmi:Extension \\1>\n#{content}\n\t</xmi:Extension>")
@@ -107,109 +180,6 @@ module Ea
107
180
  end
108
181
  end
109
182
 
110
- # @return [String] XML for the inner content of <xmi:Extension>:
111
- # <elements>...</elements>, <connectors>...</connectors>,
112
- # <diagrams>...</diagrams>. Empty when none apply.
113
- def extension_inner_xml
114
- parts = []
115
- connectors_xml = build_connectors_extension
116
- parts << connectors_xml unless connectors_xml.empty?
117
- diagrams_xml = build_diagrams_extension
118
- parts << diagrams_xml unless diagrams_xml.empty?
119
- parts.join("\n")
120
- end
121
-
122
- def build_connectors_extension
123
- connectors = @database.collections[:connectors] || []
124
- return "" if connectors.empty?
125
-
126
- inner = connectors.map { |conn| connector_xml(conn) }.compact.join("\n")
127
- return "" if inner.empty?
128
-
129
- "\t\t<connectors>\n#{inner}\n\t\t</connectors>"
130
- end
131
-
132
- def build_diagrams_extension
133
- diagrams = @database.collections[:diagrams] || []
134
- return "" if diagrams.empty?
135
-
136
- inner = diagrams.map { |dgm| diagram_xml(dgm) }.compact.join("\n")
137
- return "" if inner.empty?
138
-
139
- "\t\t<diagrams>\n#{inner}\n\t\t</diagrams>"
140
- end
141
-
142
- # Per-connector XML with xmi:idref to the model-level
143
- # association (when present) plus source/target references.
144
- def connector_xml(conn)
145
- return nil unless conn.ea_guid
146
-
147
- connector_id = "EAID_#{guid_to_xmi_id(conn.ea_guid)}"
148
- source_obj = @database.collections[:objects]&.find { |o| o.ea_object_id == conn.start_object_id }
149
- target_obj = @database.collections[:objects]&.find { |o| o.ea_object_id == conn.end_object_id }
150
- return nil unless source_obj && target_obj
151
-
152
- name_attr = conn.name ? %( name="#{escape(conn.name)}") : ""
153
- inner = source_end_xml(conn, source_obj) + target_end_xml(conn, target_obj)
154
- %(<connector xmi:idref="#{connector_id}"#{name_attr}>\n#{inner}\t\t</connector>)
155
- end
156
-
157
- def source_end_xml(conn, source_obj)
158
- end_xml(source_obj, "source")
159
- end
160
-
161
- def target_end_xml(conn, target_obj)
162
- end_xml(target_obj, "target")
163
- end
164
-
165
- def end_xml(obj, kind)
166
- xmi_id = "EAID_#{guid_to_xmi_id(obj.ea_guid)}"
167
- lines = [
168
- "\t\t\t<#{kind} xmi:idref=\"#{xmi_id}\">",
169
- "\t\t\t\t<model ea_localid=\"#{obj.ea_object_id}\" " \
170
- "type=\"#{obj.object_type}\" name=\"#{escape(obj.name.to_s)}\"/>",
171
- "\t\t\t\t<role visibility=\"Public\" targetScope=\"instance\"/>",
172
- "\t\t\t</#{kind}>"
173
- ]
174
- lines.join("\n") + "\n"
175
- end
176
-
177
- def diagram_xml(dgm)
178
- return nil unless dgm.ea_guid
179
-
180
- diagram_id = "EAID_#{guid_to_xmi_id(dgm.ea_guid)}"
181
- pkg_guid = package_guid_for(dgm)
182
- pkg_ref = pkg_guid ? "EAPK_#{guid_to_xmi_id(pkg_guid)}" : ""
183
- lines = [
184
- "<diagram xmi:id=\"#{diagram_id}\">",
185
- "\t\t\t<model package=\"#{pkg_ref}\" localID=\"#{dgm.diagram_id}\" owner=\"#{pkg_ref}\"/>",
186
- "\t\t\t<properties name=\"#{escape(dgm.name.to_s)}\" type=\"#{escape(dgm.diagram_type.to_s)}\"/>",
187
- "\t\t</diagram>"
188
- ]
189
- lines.join("\n")
190
- end
191
-
192
- def package_guid_for(dgm)
193
- pkg = (@database.collections[:packages] || []).find { |p| p.package_id == dgm.package_id }
194
- pkg&.ea_guid || dgm.ea_guid
195
- end
196
-
197
- def guid_to_xmi_id(guid)
198
- return "" unless guid
199
-
200
- guid.to_s.gsub(/[{}]/, "")
201
- .tr("-", "_")
202
- .prepend("EA") # truncated below
203
- .sub(/\AEA/, "")
204
- end
205
-
206
- def escape(text)
207
- text.to_s.gsub("&", "&amp;")
208
- .gsub("<", "&lt;")
209
- .gsub(">", "&gt;")
210
- .gsub('"', "&quot;")
211
- end
212
-
213
183
  private
214
184
 
215
185
  # ---- Top-level construction --------------------------------------
@@ -226,6 +196,7 @@ module Ea
226
196
  ::Xmi::Documentation.new(
227
197
  exporter: EXPORTER,
228
198
  exporter_version: EXPORTER_VERSION,
199
+ exporter_id: BUILD_ID,
229
200
  )
230
201
  end
231
202
 
@@ -233,7 +204,7 @@ module Ea
233
204
  ::Xmi::Uml::UmlModel.new(
234
205
  type: "uml:Model",
235
206
  name: MODEL_NAME,
236
- packaged_element: root_packages.map { |pkg| build_package(pkg) },
207
+ packaged_element: root_packages.map { |pkg| build_package(pkg) } + profile_elements,
237
208
  )
238
209
  end
239
210
 
@@ -247,6 +218,13 @@ module Ea
247
218
  .sort_by { |p| [p.tpos || 0, p.name.to_s] }
248
219
  end
249
220
 
221
+ # MDG stereotype definitions to include in the model tree.
222
+ # Empty when no MDG registry is wired. Each registered MDG
223
+ # contributes its stereotypes and extension relationships.
224
+ def profile_elements
225
+ ProfileSerializer.new(@mdg_registry).call
226
+ end
227
+
250
228
  # ---- Package tree ------------------------------------------------
251
229
 
252
230
  def build_package(pkg)
@@ -268,12 +246,128 @@ module Ea
268
246
  end
269
247
 
270
248
  def subpackages(pkg)
271
- sorted_by_position(@context.child_packages(pkg.package_id))
272
- .map { |sub| build_package(sub) }
249
+ @context.sorted_by_position(@context.child_packages(pkg.package_id))
250
+ .map { |sub| build_package(sub) }
273
251
  end
274
252
 
253
+ # EA emits a package's own classifiers first, then the children
254
+ # nobody adopted. The global LI counter follows that order.
275
255
  def classifier_objects(pkg)
276
- classifiers_in(pkg).map { |obj| build_classifier(obj) }.compact
256
+ top_level, orphaned = package_level_classifiers(pkg)
257
+ .partition { |obj| obj.parentid.to_i.zero? }
258
+ (top_level + orphaned).filter_map { |obj| build_classifier_with_bounds(obj) }
259
+ end
260
+
261
+ # Everything EA leaves at package level: an object stays here
262
+ # unless a classifier in the same package actually adopts it as
263
+ # a nestedClassifier. A child of an enumeration, of an
264
+ # instance, of a type we don't build, of a parent in another
265
+ # package, or of one that isn't there at all has no adopter.
266
+ #
267
+ # Adoption is decided from the package's own contents, never
268
+ # from what the walk has emitted so far — a child can sort
269
+ # before its own parent, and asking a set that the walk is
270
+ # still filling would emit that child twice.
271
+ def package_level_classifiers(pkg)
272
+ buildable_objects_in(pkg).reject { |obj| adopted?(obj) }
273
+ end
274
+
275
+ # A parent adopts a child only if it is a nesting kind in the
276
+ # same package AND the ParentID chain above the child actually
277
+ # terminates. A corrupt model can point an object at itself or
278
+ # round a cycle; leaving those adopted would drop them from the
279
+ # export, since the parent that owes them an emission is never
280
+ # reached. Shared with nested_classifiers_under so the two
281
+ # cannot disagree about who owns a child.
282
+ def adopted?(obj)
283
+ parent = nesting_parent_of(obj)
284
+ !parent.nil? && !cyclic_ancestry?(obj)
285
+ end
286
+
287
+ def nesting_parent_of(obj)
288
+ parent_id = obj.parentid.to_i
289
+ return nil unless parent_id.positive?
290
+
291
+ parent = @context.object_by_id(parent_id)
292
+ return nil unless parent && parent.package_id == obj.package_id
293
+ return nil unless nests_classifiers?(parent)
294
+
295
+ parent
296
+ end
297
+
298
+ def cyclic_ancestry?(obj)
299
+ seen = Set[obj.ea_object_id]
300
+ current = obj
301
+ while (current = nesting_parent_of(current))
302
+ return true unless seen.add?(current.ea_object_id)
303
+ end
304
+ false
305
+ end
306
+
307
+ # EA's class-centric LI allocation: attribute bounds first (name
308
+ # order), then the classifier itself, then the association-end
309
+ # bounds anchored here. The memoized allocator lets later
310
+ # builders reuse these IDs.
311
+ def build_classifier_with_bounds(obj)
312
+ preallocate_attribute_bounds(obj)
313
+ element = build_classifier(obj)
314
+ preallocate_end_bounds(obj)
315
+ element
316
+ end
317
+
318
+ # EA allocates a classifier's attribute LI bounds in attribute
319
+ # NAME order, while the document emits attributes in Pos order.
320
+ # Pre-allocating here (memoized) makes the later Pos-ordered
321
+ # build reuse the name-ordered counters. Only owners that go on
322
+ # to emit ownedAttribute take counters — an enumeration's
323
+ # literals never carry bounds.
324
+ def preallocate_attribute_bounds(obj)
325
+ return unless emits_attributes?(obj)
326
+
327
+ @context.attributes_for(obj.ea_object_id)
328
+ .sort_by { |a| a.name.to_s }
329
+ .each { |attr| build_attribute_bounds(attr) }
330
+ end
331
+
332
+ # Lower before upper — the global LI counter follows this order.
333
+ # @return [Array(Xmi::Uml::LowerValue, Xmi::Uml::UpperValue)]
334
+ def build_attribute_bounds(attr)
335
+ attr_id = @context.xmi_id_for(attr)
336
+ bounds = Cardinality.attribute_bounds(attr.lowerbound, attr.upperbound)
337
+ [build_lower_value(bounds[:lower],
338
+ seed: "mult-attr-#{attr.id}-lower", owner_id: attr_id),
339
+ build_upper_value(bounds[:upper],
340
+ seed: "mult-attr-#{attr.id}-upper", owner_id: attr_id)]
341
+ end
342
+
343
+ # EA's global LI counter is allocated class-centrically, not in
344
+ # document order: after a classifier's attribute bounds come the
345
+ # association-end bounds whose OPPOSITE endpoint is that
346
+ # classifier (dst ends of connectors starting here, src ends of
347
+ # connectors ending here). The memoized allocator lets the
348
+ # Association element reuse these IDs when it is built later.
349
+ def preallocate_end_bounds(obj)
350
+ association_ends_anchored_at(obj)
351
+ .each { |conn, side| build_association_end(conn, side: side) }
352
+ end
353
+
354
+ # The ends anchored at a classifier, ordered by connector GUID
355
+ # (ascending string order — the only key that fits every tied
356
+ # case in the reference exports; names, connector ids and
357
+ # opposite-endpoint ids all contradict at least one).
358
+ def association_ends_anchored_at(obj)
359
+ @context.connectors_for(obj.ea_object_id)
360
+ .flat_map { |conn| anchored_end_sides(conn, obj.ea_object_id) }
361
+ .sort_by { |conn, side| [conn.ea_guid.to_s, side == :destination ? 0 : 1] }
362
+ end
363
+
364
+ def anchored_end_sides(conn, object_id)
365
+ return [] unless RELATIONSHIP_AT_PACKAGE_LEVEL[conn.connector_type] == :association
366
+
367
+ pairs = []
368
+ pairs << [conn, :destination] if conn.start_object_id == object_id
369
+ pairs << [conn, :source] if conn.end_object_id == object_id
370
+ pairs
277
371
  end
278
372
 
279
373
  # Connectors owned by this package: connectors whose start_object is
@@ -282,7 +376,7 @@ module Ea
282
376
  # Realization are emitted inside the classifier itself.
283
377
  def package_level_relationships(pkg)
284
378
  emitted = Set.new
285
- classifiers_in(pkg).flat_map do |obj|
379
+ relationship_sources_in(pkg).flat_map do |obj|
286
380
  @context.connectors_starting_at(obj.ea_object_id).filter_map do |conn|
287
381
  key = RELATIONSHIP_AT_PACKAGE_LEVEL[conn.connector_type]
288
382
  next nil unless key
@@ -293,6 +387,13 @@ module Ea
293
387
  end
294
388
  end
295
389
 
390
+ # Relationship discovery must also see Package objects —
391
+ # package-to-package Dependency connectors hang off them even
392
+ # though they never emit as classifiers.
393
+ def relationship_sources_in(pkg)
394
+ @context.objects_in_package(pkg.package_id).reject { |o| note?(o) }
395
+ end
396
+
296
397
  def build_package_relationship(kind, conn)
297
398
  case kind
298
399
  when :association then build_association(conn)
@@ -307,16 +408,29 @@ module Ea
307
408
  # ---- Classifier dispatch (OCP registry) --------------------------
308
409
 
309
410
  def build_classifier(obj)
310
- kind = obj.transformer_type || obj.object_type&.downcase&.to_sym
311
- builder = CLASSIFIER_BUILDERS[kind]
411
+ builder = CLASSIFIER_BUILDERS[obj.transformer_type]
312
412
  return nil unless builder
313
413
 
314
414
  instance_exec(obj, &builder)
315
415
  end
316
416
 
317
- def build_class(obj)
417
+ # Objects that build no classifier at all (a Component parent, a
418
+ # Note) reach these predicates too, and answer no to both.
419
+ def capabilities_of(obj)
420
+ CLASSIFIER_CAPABILITIES.fetch(obj.transformer_type, [])
421
+ end
422
+
423
+ def emits_attributes?(obj)
424
+ capabilities_of(obj).include?(:owned_attribute)
425
+ end
426
+
427
+ def nests_classifiers?(obj)
428
+ capabilities_of(obj).include?(:nested_classifier)
429
+ end
430
+
431
+ def build_class(obj, xmi_type: nil)
318
432
  ::Xmi::Uml::PackagedElement.new(
319
- type: class_xmi_type(obj),
433
+ type: xmi_type || class_xmi_type(obj),
320
434
  id: @context.xmi_id_for(obj),
321
435
  name: obj.name,
322
436
  visibility: Visibility.from_scope(obj.scope),
@@ -325,9 +439,16 @@ module Ea
325
439
  interface_realization: interface_realizations_for(obj),
326
440
  owned_attribute: attributes_for(obj),
327
441
  owned_operation: operations_for(obj),
442
+ nested_classifier: nested_children_for(obj),
328
443
  )
329
444
  end
330
445
 
446
+ # Recursive: nested children carry their own nested children,
447
+ # walked with the same class-centric bound preallocation.
448
+ def nested_children_for(obj)
449
+ nested_classifiers_under(obj).filter_map { |child| build_classifier_with_bounds(child) }
450
+ end
451
+
331
452
  def build_enumeration(obj)
332
453
  ::Xmi::Uml::PackagedElement.new(
333
454
  type: "uml:Enumeration",
@@ -360,6 +481,15 @@ module Ea
360
481
  )
361
482
  end
362
483
 
484
+ # EA stores some associations as t_object rows. Its export
485
+ # emits them bare — id and name only, no ownedEnds and no
486
+ # visibility. The connector-backed associations
487
+ # (build_association) are a separate path.
488
+ def build_association_element(obj)
489
+ ::Xmi::Uml::PackagedElement.new(type: "uml:Association", id: @context.xmi_id_for(obj),
490
+ name: obj.name)
491
+ end
492
+
363
493
  def build_comment(obj)
364
494
  ::Xmi::Uml::OwnedComment.new(
365
495
  type: "uml:Comment",
@@ -381,28 +511,31 @@ module Ea
381
511
  # rather than as package-level `<packagedElement type=
382
512
  # "uml:Realization">`. We do the same.
383
513
  def interface_realizations_for(obj)
384
- realization_connectors(obj).map { |conn| build_interface_realization(conn) }
514
+ realization_connectors(obj).filter_map { |conn| build_interface_realization(conn) }
385
515
  end
386
516
 
387
517
  def attributes_for(obj)
388
- sorted_by_position(@context.attributes_for(obj.ea_object_id))
389
- .map { |attr| build_attribute(attr) }
518
+ @context.sorted_by_position(@context.attributes_for(obj.ea_object_id))
519
+ .map { |attr| build_attribute(attr) }
390
520
  end
391
521
 
392
522
  def operations_for(obj)
393
- sorted_by_position(@context.operations_for(obj.ea_object_id))
394
- .map { |op| build_operation(op) }
523
+ @context.sorted_by_position(@context.operations_for(obj.ea_object_id))
524
+ .map { |op| build_operation(op) }
395
525
  end
396
526
 
397
527
  def enum_literals(obj)
398
- sorted_by_position(@context.attributes_for(obj.ea_object_id))
399
- .map { |attr| build_owned_literal(attr) }
528
+ @context.sorted_by_position(@context.attributes_for(obj.ea_object_id))
529
+ .map { |attr| build_owned_literal(attr) }
400
530
  end
401
531
 
402
532
  # ---- Leaf element builders --------------------------------------
403
533
 
404
534
  def build_attribute(attr)
405
- parent_guid = parent_guid_for_attribute(attr)
535
+ # An attribute always carries both bounds; association ends
536
+ # check their single card field instead and stay bare when it
537
+ # is blank.
538
+ lower, upper = build_attribute_bounds(attr)
406
539
  ::Xmi::Uml::OwnedAttribute.new(
407
540
  type: "uml:Property",
408
541
  id: @context.xmi_id_for(attr),
@@ -412,8 +545,8 @@ module Ea
412
545
  is_ordered: Visibility.boolean_from_flag(attr.isordered),
413
546
  is_derived: Visibility.boolean_from_flag(attr.derived),
414
547
  uml_type: type_reference_model(attr.type, attr.classifier),
415
- upper_value: build_upper_value(attr.upperbound, seed: "mult-attr-#{attr.id}-upper", parent_guid: parent_guid),
416
- lower_value: build_lower_value(attr.lowerbound, seed: "mult-attr-#{attr.id}-lower", parent_guid: parent_guid),
548
+ upper_value: upper,
549
+ lower_value: lower,
417
550
  )
418
551
  end
419
552
 
@@ -425,7 +558,7 @@ module Ea
425
558
  is_static: Visibility.boolean_from_flag(op.isstatic),
426
559
  is_abstract: Visibility.boolean_from_flag(op.abstract),
427
560
  is_query: Visibility.boolean_from_flag(op.pure),
428
- concurrency: op.concurrency&.downcase,
561
+ concurrency: normalize_concurrency(op.concurrency),
429
562
  owned_parameter: operation_parameters(op),
430
563
  )
431
564
  end
@@ -450,30 +583,43 @@ module Ea
450
583
  end
451
584
 
452
585
  def build_interface_realization(conn)
586
+ client = @context.object_by_id(conn.start_object_id)
453
587
  supplier = @context.object_by_id(conn.end_object_id)
588
+ return nil unless client && supplier
589
+
590
+ supplier_ref = @context.xmi_id_for(supplier)
454
591
  ::Xmi::Uml::InterfaceRealization.new(
455
- type: "uml:InterfaceRealization",
456
- id: @context.xmi_id_for(conn),
457
- name: conn.name,
458
- client: @context.xmi_id_for(conn.start_object_id),
459
- supplier: supplier ? @context.xmi_id_for(supplier) : nil,
460
- contract: supplier ? @context.xmi_id_for(supplier) : nil,
592
+ type: "uml:InterfaceRealization", id: @context.xmi_id_for(conn),
593
+ name: conn.name, client: @context.xmi_id_for(client),
594
+ supplier: supplier_ref, contract: supplier_ref
461
595
  )
462
596
  end
463
597
 
464
598
  def build_dependency(conn)
465
- client = @context.object_by_id(conn.start_object_id)
466
- supplier = @context.object_by_id(conn.end_object_id)
599
+ client = dependency_endpoint_ref(conn.start_object_id)
600
+ supplier = dependency_endpoint_ref(conn.end_object_id)
467
601
  return nil unless client && supplier
468
602
 
469
603
  ::Xmi::Uml::PackagedElement.new(
470
604
  type: "uml:Dependency",
471
605
  id: @context.xmi_id_for(conn),
472
- client: @context.xmi_id_for(client),
473
- supplier: @context.xmi_id_for(supplier),
606
+ client: client,
607
+ supplier: supplier,
474
608
  )
475
609
  end
476
610
 
611
+ # Package-object endpoints resolve to their t_package via PDATA1
612
+ # and reference it as EAPK_ (matching EA); everything else keeps
613
+ # its own EAID_ reference.
614
+ def dependency_endpoint_ref(object_id)
615
+ obj = @context.object_by_id(object_id)
616
+ return nil unless obj
617
+ return @context.xmi_id_for(obj) unless package_object?(obj)
618
+
619
+ pkg = @database.find_package(obj.pdata1.to_i)
620
+ pkg && @context.xmi_id_for(pkg, prefix: "EAPK")
621
+ end
622
+
477
623
  # Sparx serialisation order for `uml:Association` is
478
624
  # destination member-end first, source member-end second.
479
625
  # Reordering these breaks round-trip fidelity with Sparx EA
@@ -501,8 +647,18 @@ module Ea
501
647
  target_id = side == :source ? conn.start_object_id : conn.end_object_id
502
648
  target_obj = @context.object_by_id(target_id)
503
649
  target_ref = target_obj ? @context.xmi_id_for(target_obj) : nil
504
- bounds = Cardinality.parse(cardinality_for(conn, side))
505
650
  containment = containment_for(conn, side)
651
+ # EA emits lower/upper only when the end's cardinality is set —
652
+ # a blank card gets a bare ownedEnd. Allocation order is lower
653
+ # before upper, with the END's id (EAID_dst…/EAID_src…) as the
654
+ # tail source.
655
+ raw_card = cardinality_for(conn, side)
656
+ lower = upper = nil
657
+ unless raw_card.to_s.strip.empty?
658
+ bounds = Cardinality.parse(raw_card)
659
+ lower = build_lower_value(bounds[:lower], seed: "mult-#{conn.connector_id}-#{side}-lower", owner_id: end_id)
660
+ upper = build_upper_value(bounds[:upper], seed: "mult-#{conn.connector_id}-#{side}-upper", owner_id: end_id)
661
+ end
506
662
 
507
663
  model = ::Xmi::Uml::OwnedEnd.new(
508
664
  type: "uml:Property",
@@ -512,8 +668,8 @@ module Ea
512
668
  aggregation: Visibility.aggregation_from_containment(containment),
513
669
  association: @context.xmi_id_for(conn),
514
670
  uml_type: target_ref ? ::Xmi::Uml::Type.new(idref: target_ref) : nil,
515
- upper_value: build_upper_value(bounds[:upper], seed: "mult-#{conn.connector_id}-#{side}-upper", parent_guid: conn.ea_guid),
516
- lower_value: build_lower_value(bounds[:lower], seed: "mult-#{conn.connector_id}-#{side}-lower", parent_guid: conn.ea_guid),
671
+ upper_value: upper,
672
+ lower_value: lower,
517
673
  )
518
674
 
519
675
  AssociationEnd.new(end_id, model)
@@ -521,28 +677,28 @@ module Ea
521
677
 
522
678
  # ---- Multiplicity helpers ---------------------------------------
523
679
 
524
- # Always emit both bounds UML defaults (lower=0, upper=-1) are
525
- # used when the EA field is blank. Matches real Sparx XMI, which
526
- # never omits `<upperValue>`/`<lowerValue>` on a Property.
527
- def build_upper_value(raw, seed:, parent_guid:)
680
+ # Callers emit bounds only when the EA cardinality field is set —
681
+ # EA's reference exports omit `<upperValue>`/`<lowerValue>`
682
+ # entirely on ends whose card is blank.
683
+ def build_upper_value(raw, seed:, owner_id:)
528
684
  ::Xmi::Uml::UpperValue.new(
529
685
  type: "uml:LiteralUnlimitedNatural",
530
686
  id: @context.id_allocator.allocate(
531
687
  prefix: IdAllocator::LITERAL_INTEGER,
532
688
  seed: seed,
533
- parent_guid: parent_guid,
689
+ owner_id: owner_id,
534
690
  ),
535
691
  value: Cardinality.normalize_upper(raw),
536
692
  )
537
693
  end
538
694
 
539
- def build_lower_value(raw, seed:, parent_guid:)
695
+ def build_lower_value(raw, seed:, owner_id:)
540
696
  ::Xmi::Uml::LowerValue.new(
541
697
  type: "uml:LiteralInteger",
542
698
  id: @context.id_allocator.allocate(
543
699
  prefix: IdAllocator::LITERAL_INTEGER,
544
700
  seed: seed,
545
- parent_guid: parent_guid,
701
+ owner_id: owner_id,
546
702
  ),
547
703
  value: Cardinality.normalize_lower(raw),
548
704
  )
@@ -551,18 +707,31 @@ module Ea
551
707
  # ---- Operation parameters ---------------------------------------
552
708
 
553
709
  def operation_parameters(op)
554
- params = sorted_by_position(@context.params_for_operation(op.operationid))
710
+ params = @context.params_for_operation(op.operationid)
555
711
  .reject(&:return?)
556
712
  .map { |p| build_owned_parameter(p) }
557
- params << build_return_parameter(op) if op.type && !op.type.empty?
713
+ params << build_return_parameter(op) if synthesized_return?(op)
558
714
  params
559
715
  end
560
716
 
717
+ # An untyped operation has no return to describe. One with no
718
+ # usable GUID has nothing to anchor the RT id's tail on, and the
719
+ # extension block skips it for the same reason — emitting it
720
+ # here would leave the two describing different operations.
721
+ # Type is blank-checked the way PrimitiveTypes normalizes names,
722
+ # so a whitespace-only Type cannot synthesize a return that
723
+ # primitive discovery then treats as absent.
724
+ def synthesized_return?(op)
725
+ return false if PrimitiveTypes.normalize_name(op.type).empty?
726
+
727
+ @context.identifiable?(op)
728
+ end
729
+
561
730
  def build_owned_parameter(param)
562
731
  ::Xmi::Uml::OwnedParameter.new(
563
732
  id: @context.xmi_id_for(param),
564
733
  name: param.name,
565
- direction: param.kind&.downcase,
734
+ direction: normalize_direction(param.kind),
566
735
  )
567
736
  end
568
737
 
@@ -571,10 +740,11 @@ module Ea
571
740
  id: @context.id_allocator.allocate(
572
741
  prefix: IdAllocator::RETURN_PARAMETER,
573
742
  seed: "return-#{op.operationid}",
574
- parent_guid: op.ea_guid,
743
+ owner_id: @context.xmi_id_for(op),
575
744
  ),
576
745
  name: "return",
577
746
  direction: "return",
747
+ type: type_reference(op.type, op.classifier),
578
748
  )
579
749
  end
580
750
 
@@ -584,6 +754,26 @@ module Ea
584
754
  obj.interface? ? "uml:Interface" : "uml:Class"
585
755
  end
586
756
 
757
+ # EA's operation concurrency defaults to "Sequential" — omit
758
+ # it so the xmi gem doesn't emit concurrency="sequential"
759
+ # (EA's reference XMI omits the default).
760
+ def normalize_concurrency(raw)
761
+ return nil if raw.nil? || raw.to_s.strip.empty?
762
+
763
+ value = raw.to_s.downcase
764
+ value == "sequential" ? nil : value
765
+ end
766
+
767
+ # UML parameter direction defaults to "in" — omit it so the
768
+ # xmi gem doesn't emit direction="in" (EA's reference XMI
769
+ # omits the default).
770
+ def normalize_direction(raw)
771
+ return nil if raw.nil? || raw.to_s.strip.empty?
772
+
773
+ value = raw.to_s.downcase
774
+ value == "in" ? nil : value
775
+ end
776
+
587
777
  def inheritance_connectors(obj, type)
588
778
  @context.connectors_for(obj.ea_object_id).select do |conn|
589
779
  conn.start_object_id == obj.ea_object_id && conn.connector_type == type
@@ -591,15 +781,32 @@ module Ea
591
781
  end
592
782
 
593
783
  # Realization connectors owned by this class — those where
594
- # this object is the source (client) and the connector type
595
- # is Realization. Each emits an `<interfaceRealization>`.
784
+ # this object is the source (client). Each emits an
785
+ # `<interfaceRealization>`. Matched via the model predicate so
786
+ # both EA spellings (Realization/Realisation) are recognized.
596
787
  def realization_connectors(obj)
597
- inheritance_connectors(obj, "Realization")
788
+ @context.connectors_for(obj.ea_object_id).select do |conn|
789
+ conn.start_object_id == obj.ea_object_id && conn.realization?
790
+ end
598
791
  end
599
792
 
600
- def classifiers_in(pkg)
793
+ # EA walks classifiers in tree-position order (t_object.TPos,
794
+ # ties broken by name then object id), not insertion order.
795
+ # The global LI counter depends on it.
796
+ def buildable_objects_in(pkg)
601
797
  @context.objects_in_package(pkg.package_id)
602
798
  .reject { |o| note?(o) || package_object?(o) }
799
+ .sort_by { |o| [o.sort_position, o.name.to_s, o.ea_object_id] }
800
+ end
801
+
802
+ # Children nested under a classifier via t_object.ParentID.
803
+ # Level-1 order in EA's reference is descending object id;
804
+ # deeper levels don't match any column we've found — the LI
805
+ # counters inside deep nests may drift until that's derived.
806
+ def nested_classifiers_under(obj)
807
+ @context.objects_in_package(obj.package_id)
808
+ .select { |o| o.parentid.to_i == obj.ea_object_id && adopted?(o) }
809
+ .sort_by { |o| [o.sort_position, -o.ea_object_id] }
603
810
  end
604
811
 
605
812
  def notes_in(pkg)
@@ -614,27 +821,39 @@ module Ea
614
821
  obj.object_type == "Package"
615
822
  end
616
823
 
617
- def sorted_by_position(records)
618
- records.sort_by { |r| [r.sort_position, r.name.to_s] }
619
- end
620
-
621
824
  def primitive?(obj)
622
825
  obj.object_type == "PrimitiveType" ||
623
826
  (obj.gentype == "Java" && obj.stereotype_is?("primitive"))
624
827
  end
625
828
 
626
- def type_reference(type_name, classifier_guid)
627
- return nil if type_name.nil? || type_name.empty?
829
+ # The fallback id comes from the same module that emits the
830
+ # <primitivetypes> definitions, so the two normalize the name
831
+ # identically. A name that is blank once stripped gets no
832
+ # reference at all — PrimitiveTypes defines nothing for it.
833
+ def type_reference(type_name, classifier)
834
+ return nil if PrimitiveTypes.normalize_name(type_name).empty?
628
835
 
629
- if classifier_guid
630
- GuidFormat.ea_guid_to_xmi_id(classifier_guid)
631
- else
632
- "EAnone_#{type_name}"
633
- end
836
+ classifier_ref(classifier) || PrimitiveTypes.definition_id(type_name)
634
837
  end
635
838
 
636
- def type_reference_model(type_name, classifier_guid)
637
- ref = type_reference(type_name, classifier_guid)
839
+ # QEA stores t_attribute.Classifier / t_operation.Classifier as
840
+ # an INTEGER t_object id ("0" = unresolved); EAP-era data can
841
+ # carry a GUID instead. Resolve either to the object's xmi id.
842
+ def classifier_ref(classifier)
843
+ text = PrimitiveTypes.normalize_name(classifier)
844
+ return nil if PrimitiveTypes.blank_classifier?(text)
845
+ return GuidFormat.ea_guid_to_xmi_id(text) unless text.match?(/\A\d+\z/)
846
+
847
+ obj = @context.object_by_id(text.to_i)
848
+ obj && @context.xmi_id_for(obj)
849
+ end
850
+
851
+ def type_reference_model(type_name, classifier)
852
+ if (href = PrimitiveTypes.href_for(type_name, classifier))
853
+ return ::Xmi::Uml::Type.new(href: href)
854
+ end
855
+
856
+ ref = type_reference(type_name, classifier)
638
857
  ref ? ::Xmi::Uml::Type.new(idref: ref) : nil
639
858
  end
640
859
 
@@ -647,7 +866,7 @@ module Ea
647
866
  end
648
867
 
649
868
  def containment_for(conn, side)
650
- side == :source ? conn.sourcecontainment : conn.destcontainment
869
+ side == :source ? conn.sourceisaggregate : conn.destisaggregate
651
870
  end
652
871
 
653
872
  # EA's t_connector does not expose per-end visibility (the
@@ -658,14 +877,6 @@ module Ea
658
877
  nil
659
878
  end
660
879
 
661
- # The owning element for an attribute's synthesised IDs is the
662
- # attribute's classifier (parent object), not the attribute
663
- # itself — Sparx encodes the parent class GUID in the suffix.
664
- def parent_guid_for_attribute(attr)
665
- parent = @context.object_by_id(attr.ea_object_id)
666
- parent&.ea_guid
667
- end
668
-
669
880
  # InstanceSpecification classifier reference. EA stores this
670
881
  # directly in t_object.classifier as the classifier's
671
882
  # ea_object_id (NOT pdata1, which is a separate column used
@@ -710,7 +921,7 @@ module Ea
710
921
  @context.id_allocator.allocate(
711
922
  prefix: IdAllocator::SLOT,
712
923
  seed: "slot-#{instance.ea_object_id}-#{binding.variable}",
713
- parent_guid: instance.ea_guid,
924
+ owner_id: @context.xmi_id_for(instance),
714
925
  )
715
926
  end
716
927
 
@@ -718,7 +929,7 @@ module Ea
718
929
  @context.id_allocator.allocate(
719
930
  prefix: IdAllocator::OPAQUE_EXPRESSION,
720
931
  seed: "oe-#{instance.ea_object_id}-#{binding.variable}",
721
- parent_guid: instance.ea_guid,
932
+ owner_id: @context.xmi_id_for(instance),
722
933
  )
723
934
  end
724
935