ea 0.2.5 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/TODO.refactor/01-ea-model-domain-layer.md +64 -0
  3. data/TODO.refactor/02-ea-model-qea-source-adapter.md +58 -0
  4. data/TODO.refactor/03-ea-model-xmi-source-adapter.md +33 -0
  5. data/TODO.refactor/04-ea-model-annotations-tagged-values.md +34 -0
  6. data/TODO.refactor/05-ea-model-diagrams-coordinates.md +37 -0
  7. data/TODO.refactor/06-spa-projection-layer.md +45 -0
  8. data/TODO.refactor/07-spa-sharded-output-strategy.md +28 -0
  9. data/TODO.refactor/08-spa-build-time-search-index.md +24 -0
  10. data/TODO.refactor/09-spa-frontend-data-contract.md +25 -0
  11. data/TODO.refactor/10-spa-cli-command-rework.md +34 -0
  12. data/TODO.refactor/11-spa-perf-benchmark.md +23 -0
  13. data/TODO.refactor/12-lutaml-uml-static-site-deprecation.md +28 -0
  14. data/lib/ea/cli/app.rb +13 -7
  15. data/lib/ea/cli/command/spa.rb +33 -48
  16. data/lib/ea/cli/command/svg.rb +68 -0
  17. data/lib/ea/cli/command.rb +1 -0
  18. data/lib/ea/model/annotation.rb +29 -0
  19. data/lib/ea/model/association.rb +40 -0
  20. data/lib/ea/model/base.rb +27 -0
  21. data/lib/ea/model/bounds.rb +22 -0
  22. data/lib/ea/model/classifier.rb +41 -0
  23. data/lib/ea/model/data_type.rb +9 -0
  24. data/lib/ea/model/dependency.rb +19 -0
  25. data/lib/ea/model/diagram.rb +28 -0
  26. data/lib/ea/model/diagram_connector.rb +30 -0
  27. data/lib/ea/model/diagram_element.rb +23 -0
  28. data/lib/ea/model/document.rb +107 -0
  29. data/lib/ea/model/enumeration.rb +15 -0
  30. data/lib/ea/model/enumeration_literal.rb +16 -0
  31. data/lib/ea/model/generalization.rb +19 -0
  32. data/lib/ea/model/interface.rb +9 -0
  33. data/lib/ea/model/klass.rb +16 -0
  34. data/lib/ea/model/metadata.rb +31 -0
  35. data/lib/ea/model/operation.rb +33 -0
  36. data/lib/ea/model/package.rb +36 -0
  37. data/lib/ea/model/parameter.rb +24 -0
  38. data/lib/ea/model/point.rb +16 -0
  39. data/lib/ea/model/polymorphic_maps.rb +30 -0
  40. data/lib/ea/model/primitive_type.rb +9 -0
  41. data/lib/ea/model/property.rb +49 -0
  42. data/lib/ea/model/realization.rb +19 -0
  43. data/lib/ea/model/relationship.rb +21 -0
  44. data/lib/ea/model/stereotype.rb +24 -0
  45. data/lib/ea/model/tagged_value.rb +20 -0
  46. data/lib/ea/model/waypoint.rb +19 -0
  47. data/lib/ea/model.rb +48 -0
  48. data/lib/ea/sources/qea/adapter.rb +63 -0
  49. data/lib/ea/sources/qea/annotation_builder.rb +36 -0
  50. data/lib/ea/sources/qea/classifier_builder.rb +119 -0
  51. data/lib/ea/sources/qea/connector_relationship_map.rb +42 -0
  52. data/lib/ea/sources/qea/diagram_builder.rb +214 -0
  53. data/lib/ea/sources/qea/diagram_style_parser.rb +43 -0
  54. data/lib/ea/sources/qea/id_normalizer.rb +24 -0
  55. data/lib/ea/sources/qea/metadata_builder.rb +42 -0
  56. data/lib/ea/sources/qea/object_classifier_map.rb +36 -0
  57. data/lib/ea/sources/qea/operation_builder.rb +79 -0
  58. data/lib/ea/sources/qea/package_builder.rb +44 -0
  59. data/lib/ea/sources/qea/property_builder.rb +86 -0
  60. data/lib/ea/sources/qea/relationship_builder.rb +113 -0
  61. data/lib/ea/sources/qea/stereotype_builder.rb +44 -0
  62. data/lib/ea/sources/qea/tagged_value_builder.rb +39 -0
  63. data/lib/ea/sources/qea.rb +31 -0
  64. data/lib/ea/sources/xmi/adapter.rb +62 -0
  65. data/lib/ea/sources/xmi/annotation_builder.rb +49 -0
  66. data/lib/ea/sources/xmi/classifier_builder.rb +89 -0
  67. data/lib/ea/sources/xmi/diagram_builder.rb +82 -0
  68. data/lib/ea/sources/xmi/id_normalizer.rb +27 -0
  69. data/lib/ea/sources/xmi/metadata_builder.rb +32 -0
  70. data/lib/ea/sources/xmi/operation_builder.rb +59 -0
  71. data/lib/ea/sources/xmi/package_builder.rb +73 -0
  72. data/lib/ea/sources/xmi/property_builder.rb +77 -0
  73. data/lib/ea/sources/xmi/relationship_builder.rb +142 -0
  74. data/lib/ea/sources/xmi/relationship_type_map.rb +34 -0
  75. data/lib/ea/sources/xmi/type_classifier_map.rb +33 -0
  76. data/lib/ea/sources/xmi.rb +29 -0
  77. data/lib/ea/sources.rb +15 -0
  78. data/lib/ea/spa/configuration.rb +110 -0
  79. data/lib/ea/spa/generator.rb +45 -0
  80. data/lib/ea/spa/lazy_ref.rb +18 -0
  81. data/lib/ea/spa/output/sharded_multi_file_strategy.rb +71 -0
  82. data/lib/ea/spa/output/single_file_strategy.rb +56 -0
  83. data/lib/ea/spa/output/strategy.rb +36 -0
  84. data/lib/ea/spa/output.rb +11 -0
  85. data/lib/ea/spa/package_tree.rb +15 -0
  86. data/lib/ea/spa/package_tree_node.rb +26 -0
  87. data/lib/ea/spa/projector.rb +199 -0
  88. data/lib/ea/spa/search_entry.rb +28 -0
  89. data/lib/ea/spa/search_index.rb +18 -0
  90. data/lib/ea/spa/shard.rb +19 -0
  91. data/lib/ea/spa/skeleton.rb +22 -0
  92. data/lib/ea/spa/skeleton_entry.rb +27 -0
  93. data/lib/ea/spa.rb +31 -0
  94. data/lib/ea/svg/bounds_calculator.rb +74 -0
  95. data/lib/ea/svg/connector_path.rb +138 -0
  96. data/lib/ea/svg/element_box.rb +256 -0
  97. data/lib/ea/svg/renderer.rb +77 -0
  98. data/lib/ea/svg/style_resolver.rb +72 -0
  99. data/lib/ea/svg.rb +21 -0
  100. data/lib/ea/version.rb +1 -1
  101. data/lib/ea.rb +4 -0
  102. metadata +97 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 44e2c62c415fec32597751f87ec29e35529726a61cdcc1908ae0b0a371b94443
4
- data.tar.gz: 68fce8e4b397c9c9701d443f9dcf74f8fe7b15c286b4442fbdacd63a1808ec5f
3
+ metadata.gz: cf343d41c80cffbd277dd67bf172d0c947ddae898f51dacc1a2fffc3af871b0a
4
+ data.tar.gz: b554c52026c03c0a431e31140f840fedd495fae447b738c4d9fda4fae30c9eda
5
5
  SHA512:
6
- metadata.gz: f251cc03250dbddf267365a7c3f671c8ba4faf8203ce592529ce2cbf9ca95a723f694a2e768c6271d032fd51308bba6694e9368c63f3a47282dd3c58e5921092
7
- data.tar.gz: 14c25b3daa1ace6436f06109e23a3b01a4b75de008c8a18f789db8e81b66dbe89881a49e01cb8cc8dd90d8c950b927f594a17c2a2b6a7d4a0fd7328da9296793
6
+ metadata.gz: bdec06da4470d865e75c52cbe8c5bec83ae4f242b223e98b436e5c244e8dcf8fd3c9a03bfe3f1ef3eedd9d2443fa52c21544755e8ff9cb3c5e698ee0a4b685f3
7
+ data.tar.gz: 5db5b284986bd79b3c1165f491d95cb6a5b9cd88d0bcdcab116e77abd92ebd84b267c8f3614850fa669d544085eca6eba52a6eea45c992e42439aa0b38803194
@@ -0,0 +1,64 @@
1
+ # 01 - Ea::Model Domain Layer
2
+
3
+ ## Status: DONE (2026-07-19)
4
+
5
+ ## Problem
6
+
7
+ EA QEA/XMI sources are tightly coupled to their storage format. The
8
+ existing pipeline funnels everything through `Lutaml::Uml::Document`,
9
+ which is lossy (drops annotations, tagged values, diagram coordinates)
10
+ and slow (allocates huge Ruby object graphs).
11
+
12
+ We need a canonical in-process domain model — `Ea::Model::*` — that
13
+ both QEA and XMI sources harmonize into. The model is the single
14
+ source of truth for "what the model IS". Source format becomes
15
+ irrelevant once you're in `Ea::Model`.
16
+
17
+ ## Architectural Principle
18
+
19
+ Hexagonal / Ports & Adapters. `Ea::Model` is the hexagon:
20
+ - **Driving ports** = source adapters (`Ea::Sources::Qea`,
21
+ `Ea::Sources::Xmi`) that produce model instances from source files.
22
+ - **Driven ports** = consumer adapters (`Ea::Spa`, future
23
+ `Ea::Export::*`, `Ea::Validate::*`, `Ea::Compare::*`) that project
24
+ or transform the model for their use case.
25
+
26
+ Dependencies point inward only. The model never knows about a source
27
+ format; consumers never know about a source format.
28
+
29
+ ## Domain Types
30
+
31
+ - `Ea::Model::Document` — root container (packages, classifiers,
32
+ relationships, diagrams, stereotypes)
33
+ - `Ea::Model::Package` — hierarchical containment (sub-packages,
34
+ owned classifiers)
35
+ - `Ea::Model::Classifier` (abstract) → `Class`, `DataType`,
36
+ `PrimitiveType`, `Enumeration`, `Interface`
37
+ - `Ea::Model::Property` — typed slot, multiplicity, defaults,
38
+ stereotypes, tagged values
39
+ - `Ea::Model::Operation` — signature, parameters, return type
40
+ - `Ea::Model::Parameter`
41
+ - `Ea::Model::Relationship` (abstract) → `Association` (two ends),
42
+ `Generalization`, `Realization`, `Dependency`
43
+ - `Ea::Model::EnumerationLiteral`
44
+ - `Ea::Model::Stereotype` — first-class, with tagged-value schema
45
+ - `Ea::Model::TaggedValue` — first-class (key, value, scope)
46
+ - `Ea::Model::Annotation` — first-class (type, body, author)
47
+ - `Ea::Model::Diagram` + `DiagramElement` (with bounds/style) +
48
+ `DiagramConnector` (with waypoints/style)
49
+ - `Ea::Model::Point` (x,y), `Ea::Model::Bounds` (x,y,w,h),
50
+ `Ea::Model::Waypoint`
51
+
52
+ ## Files
53
+
54
+ - `lib/ea/model.rb` — namespace + autoloads
55
+ - `lib/ea/model/base.rb` — `Ea::Model::Base` (lutaml-model subclass,
56
+ common `id`, `name`, `qualified_name` attrs)
57
+ - One file per type under `lib/ea/model/`
58
+ - `spec/ea/model/*_spec.rb` — type-level specs
59
+
60
+ ## Verification
61
+
62
+ - Each type has a spec verifying attributes, defaults, JSON round-trip
63
+ - `Ea::Model::Document` builds in-memory fixtures for use by upstream
64
+ specs (no source-format coupling in model specs)
@@ -0,0 +1,58 @@
1
+ # 02 - QEA Source Adapter
2
+
3
+ ## Status: DONE (2026-07-19)
4
+
5
+ ## Problem
6
+
7
+ `Ea::Qea::Database` is a typed wrap of the Sparx SQLite schema. It
8
+ mirrors the schema 1:1 — `t_object`, `t_package`, `t_connector`,
9
+ `t_attribute`, `t_operation`, `t_diagram`, `t_diagramlinks`,
10
+ `t_objectproperties`, `t_objectconstrains`, etc. — with their
11
+ packed-string columns and integer-coded types. It is intentionally
12
+ faithful to the source format.
13
+
14
+ `Ea::Model::*` is the canonical domain. Something has to translate.
15
+
16
+ ## Approach
17
+
18
+ `Ea::Sources::Qea::Adapter` is a driving adapter: takes a
19
+ `Ea::Qea::Database`, produces an `Ea::Model::Document`. The adapter
20
+ walks the database tables once, builds typed model instances, and
21
+ resolves foreign-key references into model-id references.
22
+
23
+ The adapter is structured as a coordinator + per-table builders
24
+ (OCP/MECE):
25
+ - `Adapter` — orchestrates traversal
26
+ - `PackageBuilder` — `t_package → Model::Package`
27
+ - `ClassifierBuilder` — `t_object (Class/DataType/Enum/Interface)
28
+ → Model::Classifier subclass`
29
+ - `PropertyBuilder` — `t_attribute → Model::Property`
30
+ - `OperationBuilder` — `t_operation + t_operationsparams →
31
+ Model::Operation`
32
+ - `RelationshipBuilder` — `t_connector → Model::Relationship
33
+ subclass` (dispatches on `Connector_Type`)
34
+ - `StereotypeBuilder` — `t_object + stylename → Model::Stereotype
35
+ application`
36
+ - `TaggedValueBuilder` — `t_objectproperties → Model::TaggedValue`
37
+ - `AnnotationBuilder` — `t_object.Note + t_document →
38
+ Model::Annotation`
39
+ - `DiagramBuilder` — `t_diagram + t_diagramlinks + t_dg +
40
+ t_connector → Model::Diagram` (with coordinates!)
41
+
42
+ ID normalization: EA GUIDs (`{GUID}`) are normalized to plain
43
+ strings. References become model `id` references (decoupled from
44
+ source format).
45
+
46
+ ## Files
47
+
48
+ - `lib/ea/sources.rb` — namespace
49
+ - `lib/ea/sources/qea.rb` — namespace + autoloads
50
+ - `lib/ea/sources/qea/adapter.rb`
51
+ - `lib/ea/sources/qea/*.rb` — per-table builders
52
+ - `spec/ea/sources/qea/*_spec.rb`
53
+
54
+ ## Verification
55
+
56
+ - Builder specs against in-memory `Ea::Qea` fixtures
57
+ - Round-trip: real QEA fixture → `Ea::Model::Document`, assert
58
+ package/class/relationship counts and known element names
@@ -0,0 +1,33 @@
1
+ # 03 - XMI Source Adapter
2
+
3
+ ## Status: DONE (2026-07-19)
4
+
5
+ ## Problem
6
+
7
+ Sparx-flavored XMI files (`.xmi`) parse into `Xmi::Sparx::Root`.
8
+ Same harmonization problem as QEA — different format, same model.
9
+
10
+ ## Approach
11
+
12
+ `Ea::Sources::Xmi::Adapter` mirrors the QEA adapter's structure
13
+ (coordinator + per-element builders), but reads from the parsed
14
+ `Xmi::Sparx::Root` tree instead of SQLite tables. Builders
15
+ dispatch on `xmi:type` discriminator (`uml:Class`,
16
+ `uml:Association`, etc.).
17
+
18
+ Deferred until TODOs 01, 02, 06, 10 are green so the SPA pipeline
19
+ proves itself on QEA first (the perf pain point). XMI is already
20
+ served by the existing `lutaml-uml` pipeline; the EA-native QEA
21
+ pipeline is the gap.
22
+
23
+ ## Files
24
+
25
+ - `lib/ea/sources/xmi.rb` + `lib/ea/sources/xmi/adapter.rb` +
26
+ per-element builders
27
+ - `spec/ea/sources/xmi/*_spec.rb`
28
+
29
+ ## Verification
30
+
31
+ - Round-trip on existing `spec/fixtures/*.xmi`
32
+ - Parity assertions vs the QEA adapter where the same model is
33
+ available in both formats
@@ -0,0 +1,34 @@
1
+ # 04 - Annotations and Tagged Values (the EA value-add)
2
+
3
+ ## Status: DONE (2026-07-19, folded into TODO 01)
4
+
5
+ ## Problem
6
+
7
+ Pure-UML pipelines drop or downplay the EA value-add: rich
8
+ annotations (notes, documentation, change history) and tagged
9
+ values (key/value metadata scoped to stereotypes). The canonical
10
+ `Ea::Model` must capture these as first-class concepts.
11
+
12
+ ## Approach
13
+
14
+ Modeled in TODO 01 as:
15
+
16
+ - `Ea::Model::Annotation` — `type` (enum: comment/documentation/
17
+ change/file/requirement), `body`, `author`, `modified_date`
18
+ - `Ea::Model::TaggedValue` — `key`, `value`, `stereotype_ref`
19
+ (optional; nil for unscoped tagged values)
20
+ - `Ea::Model::Stereotype` — `name`, `qualified_name`, `profile`,
21
+ plus its tagged-value schema (declared keys with type/default)
22
+
23
+ These types live alongside Classifiers/Properties in the model —
24
+ they're not afterthoughts. The QEA source adapter populates them
25
+ from `t_object.Note`, `t_objectproperties`, `t_objectconstraints`,
26
+ etc.
27
+
28
+ ## Verification
29
+
30
+ - Spec: a Class with 3 annotations of different types round-trips
31
+ - Spec: a Class with 2 stereotypes, each with tagged values,
32
+ round-trips
33
+ - Spec: search projection includes annotation bodies in search
34
+ content
@@ -0,0 +1,37 @@
1
+ # 05 - Diagrams with Coordinates (the EA value-add)
2
+
3
+ ## Status: DONE (2026-07-19, folded into TODO 01)
4
+
5
+ ## Problem
6
+
7
+ EA diagrams carry pixel-accurate element positions, connector
8
+ waypoints, color and font styling. UML-only SPAs drop all of this
9
+ and present a tree of classes. For browsing the original model,
10
+ the diagram IS the artifact.
11
+
12
+ ## Approach
13
+
14
+ Modeled in TODO 01 as:
15
+
16
+ - `Ea::Model::Diagram` — owns elements + connectors + metadata
17
+ - `Ea::Model::DiagramElement` — `model_element_ref` (id to a
18
+ Classifier/Package/etc.), `bounds` (x,y,w,h), `style` (parsed
19
+ from EA's packed `Style` string into typed fields: fill, line,
20
+ font, etc.)
21
+ - `Ea::Model::DiagramConnector` — `relationship_ref` (id to a
22
+ Relationship), `waypoints` (ordered list of Points), `labels`,
23
+ `style`
24
+ - `Ea::Model::Waypoint` — `Point` + optional routing hint
25
+ - `Ea::Model::Point` (x: Integer, y: Integer)
26
+ - `Ea::Model::Bounds` (x, y, width, height)
27
+
28
+ Style parsing lives in `Ea::Sources::Qea::DiagramStyleParser` —
29
+ takes the EA `styleex`/`Style` packed strings and emits a typed
30
+ `DiagramStyle` value. Same parser pattern as `Ea::Diagram::StyleParser`
31
+ (which exists today for the SVG renderer); we share or unify.
32
+
33
+ ## Verification
34
+
35
+ - Spec: parse a real EA diagram from plateau model fixture, assert
36
+ element bounds and connector waypoints are non-empty
37
+ - Spec: diagram projects to SPA shape with bounds/waypoints intact
@@ -0,0 +1,45 @@
1
+ # 06 - SPA Projection Layer
2
+
3
+ ## Status: DONE (2026-07-19)
4
+
5
+ ## Problem
6
+
7
+ The SPA needs a view of the model that's shardable, searchable,
8
+ and lazy-loadable. But sharding/search/lazy-loading are properties
9
+ of the *view*, not the model — putting them on `Ea::Model::*`
10
+ would couple the model to one consumer.
11
+
12
+ ## Approach
13
+
14
+ `Ea::Spa::*` is the projection. One-way transform from
15
+ `Ea::Model::Document` to view-shaped artifacts:
16
+
17
+ - `Ea::Spa::Projector` — walks the model, emits:
18
+ - `Skeleton` — top-level: metadata + package tree + flat
19
+ classifier index (`id, name, type, package_id, qualified_name`)
20
+ - `Shard` (per-entity) — full detail of one Class/Package/etc.
21
+ including properties/operations/relationships/annotations/
22
+ tagged values
23
+ - `SearchIndex` — flat search entries (id, type, name, qname,
24
+ package, content) ready for MiniSearch
25
+ - `PackageTree` — hierarchical navigation tree
26
+
27
+ Projection is lossy by design: the SPA doesn't carry every model
28
+ field; it carries what browsing needs.
29
+
30
+ ## Files
31
+
32
+ - `lib/ea/spa.rb` — namespace
33
+ - `lib/ea/spa/projector.rb`
34
+ - `lib/ea/spa/skeleton.rb`, `skeleton_entry.rb`
35
+ - `lib/ea/spa/shard.rb`, `shard_writer.rb`
36
+ - `lib/ea/spa/search_index.rb`, `search_entry.rb`
37
+ - `lib/ea/spa/package_tree.rb`, `package_tree_node.rb`
38
+ - `lib/ea/spa/lazy_ref.rb`
39
+ - `spec/ea/spa/*_spec.rb`
40
+
41
+ ## Verification
42
+
43
+ - Projector spec: in-memory model → skeleton/shards/search
44
+ - Round-trip: QEA fixture → Model → Spa::Skeleton — assert
45
+ element counts match
@@ -0,0 +1,28 @@
1
+ # 07 - Sharded Output Strategy
2
+
3
+ ## Status: DONE (2026-07-19)
4
+
5
+ ## Problem
6
+
7
+ Single-file output doesn't scale. For large models, the SPA must
8
+ ship as skeleton + per-entity shards, loaded on demand.
9
+
10
+ ## Approach
11
+
12
+ Output strategy pattern (mirrors existing lutaml-uml shape):
13
+
14
+ - `Ea::Spa::Output::Strategy` (abstract)
15
+ - `Ea::Spa::Output::SingleFileStrategy` — embeds everything in
16
+ one HTML (small models only)
17
+ - `Ea::Spa::Output::ShardedMultiFileStrategy` — directory with
18
+ `skeleton.json`, `search.json`, `data/classes/{id}.json`,
19
+ `data/packages/{id}.json`, `data/diagrams/{id}.json`
20
+
21
+ Size threshold auto-selects: under 2 MB JSON → single file, else
22
+ sharded. Configurable.
23
+
24
+ ## Verification
25
+
26
+ - Spec: small model → single-file output
27
+ - Spec: large model → sharded output, skeleton lists every
28
+ classifier, each shard file exists and parses
@@ -0,0 +1,24 @@
1
+ # 08 - Build-time Search Index
2
+
3
+ ## Status: DONE (2026-07-19)
4
+
5
+ ## Problem
6
+
7
+ Frontend search currently scans a flat list in memory. For 10k+
8
+ classes, that's slow. Need a real inverted index built at build
9
+ time, lazy-loaded at run time.
10
+
11
+ ## Approach
12
+
13
+ `Ea::Spa::SearchIndex` produces documents in MiniSearch-friendly
14
+ shape (id, name, qualifiedName, type, package, content with
15
+ boosts). Frontend loads MiniSearch JS lib lazily on first search
16
+ focus,hydrates the index from `search.json`.
17
+
18
+ Frontend integration deferred until TODO 09 lands the data
19
+ contract. Ruby-side emission: implemented in TODO 06.
20
+
21
+ ## Verification
22
+
23
+ - Spec: search index includes all classifiers with correct boosts
24
+ - Spec: search content includes annotation bodies (rich)
@@ -0,0 +1,25 @@
1
+ # 09 - SPA Frontend Data Contract
2
+
3
+ ## Status: DEFERRED (frontend extraction)
4
+
5
+ ## Problem
6
+
7
+ Vue frontend lives in `lutaml-uml/frontend`. EA-side SPA needs to
8
+ share it without forking.
9
+
10
+ ## Approach
11
+
12
+ Define a versioned JSON schema for the SPA data contract. Both
13
+ `lutaml-uml`'s SPA and `Ea::Spa` produce JSON conforming to the
14
+ schema. Frontend is extracted to its own npm package
15
+ (`@lutaml/spa-frontend`) and consumed by both.
16
+
17
+ Deferred until Ruby-side projection is proven stable. For now,
18
+ `Ea::Spa` emits JSON in a shape compatible with the existing Vue
19
+ frontend's data loader, so we can copy the frontend in for
20
+ testing.
21
+
22
+ ## Verification
23
+
24
+ - Schema spec: every field documented, has a fixture
25
+ - Cross-compat: existing Vue frontend can load `Ea::Spa`'s output
@@ -0,0 +1,34 @@
1
+ # 10 - Rework `ea spa` CLI Command
2
+
3
+ ## Status: DONE (2026-07-19)
4
+
5
+ ## Problem
6
+
7
+ `Ea::Cli::Command::Spa` currently delegates to
8
+ `Lutaml::UmlRepository::StaticSite::Generator`, forcing the QEA →
9
+ UML transform. The new pipeline is QEA → `Ea::Model` → `Ea::Spa`.
10
+
11
+ ## Approach
12
+
13
+ Rewire to:
14
+ 1. `Ea.parse(file)` → source-specific model (QEA Database or XMI
15
+ Root)
16
+ 2. Source adapter → `Ea::Model::Document`
17
+ 3. `Ea::Spa::Projector` → skeleton + shards + search index
18
+ 4. `Ea::Spa::Output::Strategy` → HTML/JSON on disk
19
+
20
+ Output mode auto-selected by payload size, overridable by
21
+ `--mode=single_file|sharded` flag.
22
+
23
+ ## Files
24
+
25
+ - `lib/ea/cli/command/spa.rb` — reworked
26
+ - `lib/ea/cli/command/repository_builder.rb` — extended to feed
27
+ the new pipeline
28
+ - `spec/ea/cli/spa_spec.rb` — end-to-end on a small fixture
29
+
30
+ ## Verification
31
+
32
+ - `ea spa path/to/small.qea` produces a valid SPA
33
+ - `ea spa path/to/plateau-model/20251010_current_plateau_v5.1.qea`
34
+ completes in minutes, not hours
@@ -0,0 +1,23 @@
1
+ # 11 - SPA Performance Benchmark
2
+
3
+ ## Status: DEFERRED (until pipeline is stable)
4
+
5
+ ## Problem
6
+
7
+ Need empirical proof that the 45 MB CityGML QEA becomes
8
+ browseable.
9
+
10
+ ## Approach
11
+
12
+ Benchmark spec that:
13
+ - Loads `plateau-model/20251010_current_plateau_v5.1.qea` (6 MB)
14
+ - Loads `plateau-model/20260323_CityGML_3.0_Consolidated_Draft.qea`
15
+ (45 MB)
16
+ - Reports: parse time, model build time, projection time, output
17
+ size, shard count
18
+ - Snapshot to compare regressions
19
+
20
+ ## Verification
21
+
22
+ The plateau v5.1 must complete in < 60 s end-to-end on a laptop.
23
+ CityGML 45 MB should complete in < 10 min.
@@ -0,0 +1,28 @@
1
+ # 12 - lutaml-uml StaticSite Fate
2
+
3
+ ## Status: DEFERRED (post-validation)
4
+
5
+ ## Problem
6
+
7
+ Once `Ea::Spa` is stable, `Lutaml::UmlRepository::StaticSite` is
8
+ redundant for EA sources. Decide: deprecate, or repurpose as the
9
+ "UML-purist" view for non-EA sources.
10
+
11
+ ## Approach
12
+
13
+ Three options:
14
+ 1. **Deprecate entirely** — once EA users move to `ea spa`,
15
+ remove from `lutaml-uml`. Cleanest.
16
+ 2. **Keep as UML-purist SPA** — for users who want the abstracted
17
+ UML view (e.g. after cross-vendor harmonization via
18
+ `lutaml-uml`). Document as a separate use case.
19
+ 3. **Keep as thin wrapper** — `lutaml-uml`'s SPA calls into
20
+ `Ea::Spa` for EA sources, falls back to its own pipeline for
21
+ non-EA. Hybrid.
22
+
23
+ Decision deferred until `Ea::Spa` proves itself on the plateau
24
+ model (TODO 11). Document the decision as an ADR when made.
25
+
26
+ ## Verification
27
+
28
+ Decision recorded; downstream callers notified if deprecation.
data/lib/ea/cli/app.rb CHANGED
@@ -13,8 +13,8 @@ module Ea
13
13
  OUTPUT_OPTION = { type: :string, aliases: :o }.freeze
14
14
 
15
15
  # Shared kwargs for any command that accepts a YAML config file.
16
- # Matches the OUTPUT_OPTION pattern so adding config-file support
17
- # to another command is a one-line addition.
16
+ # Matches OUTPUT_OPTION; adding config support to a command is a
17
+ # one-line addition.
18
18
  CONFIG_OPTION = { type: :string, aliases: :c }.freeze
19
19
 
20
20
  class << self
@@ -74,18 +74,24 @@ module Ea
74
74
  Command::Convert.new(file: file, **symbolize(options)).call
75
75
  end
76
76
 
77
- desc "spa FILE", "Generate single-page app (SPA) from QEA/XMI/LUR"
77
+ desc "spa FILE", "Generate single-page app (SPA) from QEA/XMI"
78
78
  option :output, **OUTPUT_OPTION,
79
- desc: "Output path (default: <basename>.html)"
79
+ desc: "Output path (default: <basename>.html or <basename>.spa/)"
80
80
  option :config, **CONFIG_OPTION,
81
- desc: "Path to lutaml-uml static-site YAML config " \
82
- "(sets title, description, logos, etc.)"
81
+ desc: "Path to SPA YAML config (overrides model metadata: title, description, etc.)"
83
82
  option :mode, type: :string, default: "single_file",
84
- desc: "Output mode: single_file | multi_file"
83
+ desc: "Output mode: single_file | sharded"
85
84
  def spa(file)
86
85
  Command::Spa.new(file: file, **symbolize(options)).call
87
86
  end
88
87
 
88
+ desc "svg NAME FILE", "Render a diagram from QEA/XMI to standalone SVG"
89
+ option :output, **OUTPUT_OPTION,
90
+ desc: "Output path (default: <basename>.<diagram>.svg)"
91
+ def svg(name, file)
92
+ Command::Svg.new(name: name, file: file, **symbolize(options)).call
93
+ end
94
+
89
95
  private
90
96
 
91
97
  def symbolize(opts)
@@ -5,88 +5,73 @@ module Ea
5
5
  module Command
6
6
  # `ea spa FILE [--output=PATH] [--mode=MODE]`
7
7
  #
8
- # Generates a static single-page application (SPA) from a QEA,
9
- # XMI, or LUR file. The SPA shows the package hierarchy, class
10
- # details, diagram list, and member navigation in a browser.
8
+ # Generates a static single-page application (SPA) from a QEA
9
+ # or XMI file using the Ea::Model native pipeline:
11
10
  #
12
- # Pipeline:
13
- # 1. Ea::Transformations.to_uml(file)Lutaml::Uml::Document
14
- # 2. Lutaml::UmlRepository::Repository.from_document(document)
15
- # 3. Lutaml::UmlRepository::StaticSite::Generator.new(repo, ...).generate
16
- #
17
- # Output format defaults to single-file Vue IIFE HTML.
11
+ # 1. Source parse (Ea::Qea::Database / Xmi::Sparx::Root)
12
+ # 2. Source adapter Ea::Model::Document
13
+ # 3. Ea::Spa::Projector → skeleton + shards + search index
14
+ # 4. Ea::Spa::Output::Strategy → HTML/JSON on disk
18
15
  class Spa < Base
19
16
  DEFAULT_MODE = :single_file
20
17
 
21
18
  def call
22
- repository = RepositoryBuilder.build_repository(file_path)
23
-
24
- require_lutaml_uml_static_site!
25
- strategy = resolve_output_strategy
26
-
19
+ document = build_model_document
27
20
  output_path = resolve_output_path
28
- options = {
21
+ Ea::Spa::Generator.new(
22
+ document,
29
23
  output: output_path,
30
24
  mode: mode,
31
- output_strategy: strategy,
32
- config_path: resolve_config_path,
33
- }.compact
34
-
35
- Lutaml::UmlRepository::StaticSite::Generator
36
- .new(repository, options)
37
- .generate
25
+ configuration: spa_configuration
26
+ ).generate
38
27
 
39
28
  formatter.render([[output_path]], columns: [:written_to])
40
29
  end
41
30
 
42
31
  private
43
32
 
44
- def mode
45
- (options[:mode] || DEFAULT_MODE).to_sym
33
+ def build_model_document
34
+ case File.extname(file_path).downcase
35
+ when ".qea"
36
+ Ea::Sources::Qea::Adapter.from_path(file_path)
37
+ when ".xmi"
38
+ Ea::Sources::Xmi::Adapter.from_path(file_path)
39
+ else
40
+ raise Ea::Cli::UnsupportedFormat,
41
+ "Unknown file format: #{File.extname(file_path)}"
42
+ end
46
43
  end
47
44
 
48
- def resolve_output_path
49
- options[:output] || default_output_path
45
+ def mode
46
+ (options[:mode] || DEFAULT_MODE).to_sym
50
47
  end
51
48
 
52
- def resolve_config_path
49
+ def spa_configuration
53
50
  path = options[:config]
54
51
  return nil unless path
55
52
 
56
53
  expanded = File.expand_path(path)
57
54
  unless File.exist?(expanded)
58
55
  raise Ea::Cli::FileNotFound,
59
- "Config file not found: #{path}"
56
+ "SPA config not found: #{path}"
60
57
  end
61
58
 
62
- expanded
59
+ Ea::Spa::Configuration.load(expanded)
60
+ end
61
+
62
+ def resolve_output_path
63
+ options[:output] || default_output_path
63
64
  end
64
65
 
65
66
  def default_output_path
66
67
  base = File.basename(file_path, ".*")
67
68
  dir = File.dirname(file_path)
68
- File.join(dir, "#{base}.html")
69
- end
70
-
71
- def resolve_output_strategy
72
- case mode
73
- when :single_file, "single_file"
74
- Lutaml::UmlRepository::StaticSite::Output::VueInlinedStrategy
75
- when :multi_file, "multi_file"
76
- Lutaml::UmlRepository::StaticSite::Output::MultiFileStrategy
69
+ if mode == :sharded
70
+ File.join(dir, "#{base}.spa")
77
71
  else
78
- raise Ea::Cli::UnsupportedFormat,
79
- "Unknown spa mode '#{mode}': use single_file or multi_file"
72
+ File.join(dir, "#{base}.html")
80
73
  end
81
74
  end
82
-
83
- def require_lutaml_uml_static_site!
84
- require "lutaml/uml_repository/static_site"
85
- rescue LoadError => e
86
- raise Ea::Cli::MissingUmlDependency,
87
- "spa command requires the `lutaml-uml` gem. " \
88
- "Install it via `gem install lutaml-uml`. (#{e.message})"
89
- end
90
75
  end
91
76
  end
92
77
  end