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.
- checksums.yaml +4 -4
- data/TODO.refactor/01-ea-model-domain-layer.md +64 -0
- data/TODO.refactor/02-ea-model-qea-source-adapter.md +58 -0
- data/TODO.refactor/03-ea-model-xmi-source-adapter.md +33 -0
- data/TODO.refactor/04-ea-model-annotations-tagged-values.md +34 -0
- data/TODO.refactor/05-ea-model-diagrams-coordinates.md +37 -0
- data/TODO.refactor/06-spa-projection-layer.md +45 -0
- data/TODO.refactor/07-spa-sharded-output-strategy.md +28 -0
- data/TODO.refactor/08-spa-build-time-search-index.md +24 -0
- data/TODO.refactor/09-spa-frontend-data-contract.md +25 -0
- data/TODO.refactor/10-spa-cli-command-rework.md +34 -0
- data/TODO.refactor/11-spa-perf-benchmark.md +23 -0
- data/TODO.refactor/12-lutaml-uml-static-site-deprecation.md +28 -0
- data/lib/ea/cli/app.rb +13 -7
- data/lib/ea/cli/command/spa.rb +33 -48
- data/lib/ea/cli/command/svg.rb +68 -0
- data/lib/ea/cli/command.rb +1 -0
- data/lib/ea/model/annotation.rb +29 -0
- data/lib/ea/model/association.rb +40 -0
- data/lib/ea/model/base.rb +27 -0
- data/lib/ea/model/bounds.rb +22 -0
- data/lib/ea/model/classifier.rb +41 -0
- data/lib/ea/model/data_type.rb +9 -0
- data/lib/ea/model/dependency.rb +19 -0
- data/lib/ea/model/diagram.rb +28 -0
- data/lib/ea/model/diagram_connector.rb +30 -0
- data/lib/ea/model/diagram_element.rb +23 -0
- data/lib/ea/model/document.rb +107 -0
- data/lib/ea/model/enumeration.rb +15 -0
- data/lib/ea/model/enumeration_literal.rb +16 -0
- data/lib/ea/model/generalization.rb +19 -0
- data/lib/ea/model/interface.rb +9 -0
- data/lib/ea/model/klass.rb +16 -0
- data/lib/ea/model/metadata.rb +31 -0
- data/lib/ea/model/operation.rb +33 -0
- data/lib/ea/model/package.rb +36 -0
- data/lib/ea/model/parameter.rb +24 -0
- data/lib/ea/model/point.rb +16 -0
- data/lib/ea/model/polymorphic_maps.rb +30 -0
- data/lib/ea/model/primitive_type.rb +9 -0
- data/lib/ea/model/property.rb +49 -0
- data/lib/ea/model/realization.rb +19 -0
- data/lib/ea/model/relationship.rb +21 -0
- data/lib/ea/model/stereotype.rb +24 -0
- data/lib/ea/model/tagged_value.rb +20 -0
- data/lib/ea/model/waypoint.rb +19 -0
- data/lib/ea/model.rb +48 -0
- data/lib/ea/sources/qea/adapter.rb +63 -0
- data/lib/ea/sources/qea/annotation_builder.rb +36 -0
- data/lib/ea/sources/qea/classifier_builder.rb +119 -0
- data/lib/ea/sources/qea/connector_relationship_map.rb +42 -0
- data/lib/ea/sources/qea/diagram_builder.rb +214 -0
- data/lib/ea/sources/qea/diagram_style_parser.rb +43 -0
- data/lib/ea/sources/qea/id_normalizer.rb +24 -0
- data/lib/ea/sources/qea/metadata_builder.rb +42 -0
- data/lib/ea/sources/qea/object_classifier_map.rb +36 -0
- data/lib/ea/sources/qea/operation_builder.rb +79 -0
- data/lib/ea/sources/qea/package_builder.rb +44 -0
- data/lib/ea/sources/qea/property_builder.rb +86 -0
- data/lib/ea/sources/qea/relationship_builder.rb +113 -0
- data/lib/ea/sources/qea/stereotype_builder.rb +44 -0
- data/lib/ea/sources/qea/tagged_value_builder.rb +39 -0
- data/lib/ea/sources/qea.rb +31 -0
- data/lib/ea/sources/xmi/adapter.rb +62 -0
- data/lib/ea/sources/xmi/annotation_builder.rb +49 -0
- data/lib/ea/sources/xmi/classifier_builder.rb +89 -0
- data/lib/ea/sources/xmi/diagram_builder.rb +82 -0
- data/lib/ea/sources/xmi/id_normalizer.rb +27 -0
- data/lib/ea/sources/xmi/metadata_builder.rb +32 -0
- data/lib/ea/sources/xmi/operation_builder.rb +59 -0
- data/lib/ea/sources/xmi/package_builder.rb +73 -0
- data/lib/ea/sources/xmi/property_builder.rb +77 -0
- data/lib/ea/sources/xmi/relationship_builder.rb +142 -0
- data/lib/ea/sources/xmi/relationship_type_map.rb +34 -0
- data/lib/ea/sources/xmi/type_classifier_map.rb +33 -0
- data/lib/ea/sources/xmi.rb +29 -0
- data/lib/ea/sources.rb +15 -0
- data/lib/ea/spa/configuration.rb +110 -0
- data/lib/ea/spa/generator.rb +45 -0
- data/lib/ea/spa/lazy_ref.rb +18 -0
- data/lib/ea/spa/output/sharded_multi_file_strategy.rb +71 -0
- data/lib/ea/spa/output/single_file_strategy.rb +56 -0
- data/lib/ea/spa/output/strategy.rb +36 -0
- data/lib/ea/spa/output.rb +11 -0
- data/lib/ea/spa/package_tree.rb +15 -0
- data/lib/ea/spa/package_tree_node.rb +26 -0
- data/lib/ea/spa/projector.rb +199 -0
- data/lib/ea/spa/search_entry.rb +28 -0
- data/lib/ea/spa/search_index.rb +18 -0
- data/lib/ea/spa/shard.rb +19 -0
- data/lib/ea/spa/skeleton.rb +22 -0
- data/lib/ea/spa/skeleton_entry.rb +27 -0
- data/lib/ea/spa.rb +31 -0
- data/lib/ea/svg/bounds_calculator.rb +74 -0
- data/lib/ea/svg/connector_path.rb +138 -0
- data/lib/ea/svg/element_box.rb +256 -0
- data/lib/ea/svg/renderer.rb +77 -0
- data/lib/ea/svg/style_resolver.rb +72 -0
- data/lib/ea/svg.rb +21 -0
- data/lib/ea/version.rb +1 -1
- data/lib/ea.rb +4 -0
- metadata +97 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cf343d41c80cffbd277dd67bf172d0c947ddae898f51dacc1a2fffc3af871b0a
|
|
4
|
+
data.tar.gz: b554c52026c03c0a431e31140f840fedd495fae447b738c4d9fda4fae30c9eda
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
17
|
-
#
|
|
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
|
|
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
|
|
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 |
|
|
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)
|
data/lib/ea/cli/command/spa.rb
CHANGED
|
@@ -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
|
-
#
|
|
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
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
21
|
+
Ea::Spa::Generator.new(
|
|
22
|
+
document,
|
|
29
23
|
output: output_path,
|
|
30
24
|
mode: mode,
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
45
|
-
(
|
|
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
|
|
49
|
-
options[:
|
|
45
|
+
def mode
|
|
46
|
+
(options[:mode] || DEFAULT_MODE).to_sym
|
|
50
47
|
end
|
|
51
48
|
|
|
52
|
-
def
|
|
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
|
-
"
|
|
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
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|