opencode-arch 1.0.0__py3-none-any.whl
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.
- opencode_arch/__init__.py +3 -0
- opencode_arch/artifacts/__init__.py +48 -0
- opencode_arch/artifacts/context.py +451 -0
- opencode_arch/artifacts/diagrams.py +451 -0
- opencode_arch/artifacts/selector.py +331 -0
- opencode_arch/artifacts/templates.py +444 -0
- opencode_arch/cli/__init__.py +1 -0
- opencode_arch/cli/bench.py +25 -0
- opencode_arch/cli/calibrate.py +208 -0
- opencode_arch/cli/confidence.py +66 -0
- opencode_arch/cli/docs.py +333 -0
- opencode_arch/cli/docs_validator.py +295 -0
- opencode_arch/cli/export_data.py +133 -0
- opencode_arch/cli/extract.py +93 -0
- opencode_arch/cli/gap_analyzer.py +107 -0
- opencode_arch/cli/generate.py +68 -0
- opencode_arch/cli/launch.py +264 -0
- opencode_arch/cli/main.py +360 -0
- opencode_arch/cli/metrics.py +186 -0
- opencode_arch/cli/prompts.py +20 -0
- opencode_arch/cli/regen_loop.py +1028 -0
- opencode_arch/context/__init__.py +29 -0
- opencode_arch/context/formatter.py +492 -0
- opencode_arch/context/pipeline_bridge.py +201 -0
- opencode_arch/extract/__init__.py +8 -0
- opencode_arch/extract/constraint_detector.py +398 -0
- opencode_arch/extract/from_artifacts.py +837 -0
- opencode_arch/extract/from_code.py +646 -0
- opencode_arch/extract/route_detector.py +400 -0
- opencode_arch/extract/table_parser.py +177 -0
- opencode_arch/learning/__init__.py +19 -0
- opencode_arch/learning/adapter.py +157 -0
- opencode_arch/learning/assessor.py +170 -0
- opencode_arch/learning/classifier.py +144 -0
- opencode_arch/learning/lessons.py +139 -0
- opencode_arch/learning/maintainer.py +281 -0
- opencode_arch/learning/patterns.py +51 -0
- opencode_arch/mcp/__init__.py +1 -0
- opencode_arch/mcp/__main__.py +8 -0
- opencode_arch/mcp/server.py +183 -0
- opencode_arch/mcp/tools/__init__.py +1 -0
- opencode_arch/mcp/tools/check.py +159 -0
- opencode_arch/mcp/tools/extract.py +107 -0
- opencode_arch/mcp/tools/feedback.py +65 -0
- opencode_arch/mcp/tools/generate.py +104 -0
- opencode_arch/mcp/tools/group.py +62 -0
- opencode_arch/mcp/tools/ingest.py +101 -0
- opencode_arch/mcp/tools/require.py +77 -0
- opencode_arch/mcp/tools/scan.py +53 -0
- opencode_arch/mcp/tools/slice.py +235 -0
- opencode_arch/mcp/tools/validate.py +59 -0
- opencode_arch/prompts/__init__.py +1 -0
- opencode_arch/prompts/regen.py +36 -0
- opencode_arch/runner/__init__.py +5 -0
- opencode_arch/runner/base.py +21 -0
- opencode_arch/runner/opencode.py +66 -0
- opencode_arch/telemetry/__init__.py +6 -0
- opencode_arch/telemetry/collector.py +40 -0
- opencode_arch/telemetry/recorder.py +12 -0
- opencode_arch/telemetry/store.py +537 -0
- opencode_arch-1.0.0.dist-info/METADATA +247 -0
- opencode_arch-1.0.0.dist-info/RECORD +65 -0
- opencode_arch-1.0.0.dist-info/WHEEL +4 -0
- opencode_arch-1.0.0.dist-info/entry_points.txt +2 -0
- opencode_arch-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
"""PlantUML diagram generation from architecture model data.
|
|
2
|
+
|
|
3
|
+
Generates deterministic PlantUML syntax strings from model entities and
|
|
4
|
+
relationships. These can be embedded in documentation or rendered by any
|
|
5
|
+
PlantUML-compatible tool.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from architecture_model.core.types import (
|
|
11
|
+
ArchitectureModel,
|
|
12
|
+
ActorType,
|
|
13
|
+
Behavior,
|
|
14
|
+
RelationType,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _sanitize_id(id_str: str) -> str:
|
|
19
|
+
"""Sanitize an entity ID for use as a PlantUML identifier.
|
|
20
|
+
|
|
21
|
+
Replace hyphens, dots, and spaces with underscores.
|
|
22
|
+
"""
|
|
23
|
+
return id_str.replace("-", "_").replace(".", "_").replace(" ", "_")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def generate_component_diagram(model: ArchitectureModel) -> str:
|
|
27
|
+
"""Generate a C4-style component diagram showing system structure.
|
|
28
|
+
|
|
29
|
+
Groups components by layer, renders actors, and shows
|
|
30
|
+
depends-on/exposes/consumes relationships.
|
|
31
|
+
"""
|
|
32
|
+
lines: list[str] = []
|
|
33
|
+
lines.append("@startuml")
|
|
34
|
+
lines.append("!include <C4/C4_Component>")
|
|
35
|
+
lines.append("")
|
|
36
|
+
lines.append(f"title Component Diagram - {model.meta.project}")
|
|
37
|
+
lines.append("")
|
|
38
|
+
|
|
39
|
+
# Actors
|
|
40
|
+
for actor in model.entities.actors:
|
|
41
|
+
aid = _sanitize_id(actor.id)
|
|
42
|
+
if actor.type == ActorType.HUMAN:
|
|
43
|
+
lines.append(f'Person({aid}, "{actor.name}")')
|
|
44
|
+
else:
|
|
45
|
+
lines.append(f'System_Ext({aid}, "{actor.name}")')
|
|
46
|
+
|
|
47
|
+
if model.entities.actors:
|
|
48
|
+
lines.append("")
|
|
49
|
+
|
|
50
|
+
# Group components by layer
|
|
51
|
+
layer_ids = {layer.id for layer in model.entities.layers}
|
|
52
|
+
layer_map: dict[str, list] = {layer.id: [] for layer in model.entities.layers}
|
|
53
|
+
unlayered: list = []
|
|
54
|
+
|
|
55
|
+
for comp in model.entities.components:
|
|
56
|
+
if comp.layer and comp.layer in layer_ids:
|
|
57
|
+
layer_map[comp.layer].append(comp)
|
|
58
|
+
else:
|
|
59
|
+
unlayered.append(comp)
|
|
60
|
+
|
|
61
|
+
# Render layer boundaries with their components
|
|
62
|
+
for layer in model.entities.layers:
|
|
63
|
+
comps = layer_map.get(layer.id, [])
|
|
64
|
+
if not comps:
|
|
65
|
+
continue
|
|
66
|
+
lid = _sanitize_id(layer.id)
|
|
67
|
+
lines.append(f'Container_Boundary({lid}, "{layer.name}") {{')
|
|
68
|
+
for comp in comps:
|
|
69
|
+
cid = _sanitize_id(comp.id)
|
|
70
|
+
tech = f', "{comp.technology}"' if comp.technology else ', ""'
|
|
71
|
+
lines.append(f' Component({cid}, "{comp.name}"{tech}, "{comp.kind.value}")')
|
|
72
|
+
lines.append("}")
|
|
73
|
+
lines.append("")
|
|
74
|
+
|
|
75
|
+
# Components without layers
|
|
76
|
+
for comp in unlayered:
|
|
77
|
+
cid = _sanitize_id(comp.id)
|
|
78
|
+
tech = f', "{comp.technology}"' if comp.technology else ', ""'
|
|
79
|
+
lines.append(f'Component({cid}, "{comp.name}"{tech}, "{comp.kind.value}")')
|
|
80
|
+
|
|
81
|
+
if unlayered:
|
|
82
|
+
lines.append("")
|
|
83
|
+
|
|
84
|
+
# Relationships (only depends-on, exposes, consumes)
|
|
85
|
+
allowed_types = {RelationType.DEPENDS_ON, RelationType.EXPOSES, RelationType.CONSUMES}
|
|
86
|
+
rels_rendered = False
|
|
87
|
+
for rel in model.relationships:
|
|
88
|
+
if rel.type in allowed_types:
|
|
89
|
+
fid = _sanitize_id(rel.from_id)
|
|
90
|
+
tid = _sanitize_id(rel.to_id)
|
|
91
|
+
lines.append(f'Rel({fid}, {tid}, "{rel.type.value}")')
|
|
92
|
+
rels_rendered = True
|
|
93
|
+
|
|
94
|
+
if rels_rendered:
|
|
95
|
+
lines.append("")
|
|
96
|
+
|
|
97
|
+
lines.append("@enduml")
|
|
98
|
+
return "\n".join(lines)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def generate_dependency_diagram(model: ArchitectureModel) -> str:
|
|
102
|
+
"""Generate a dependency graph showing component relationships.
|
|
103
|
+
|
|
104
|
+
Simpler than C4 — plain PlantUML with rectangles and arrows.
|
|
105
|
+
Only includes entities that participate in at least one relationship.
|
|
106
|
+
"""
|
|
107
|
+
lines: list[str] = []
|
|
108
|
+
lines.append("@startuml")
|
|
109
|
+
lines.append(f"title Dependency Graph - {model.meta.project}")
|
|
110
|
+
lines.append("")
|
|
111
|
+
|
|
112
|
+
# Filter to relevant relationship types
|
|
113
|
+
allowed_types = {RelationType.DEPENDS_ON, RelationType.EXPOSES, RelationType.CONSUMES}
|
|
114
|
+
relevant_rels = [r for r in model.relationships if r.type in allowed_types]
|
|
115
|
+
|
|
116
|
+
# Collect IDs that participate in relationships
|
|
117
|
+
connected_ids: set[str] = set()
|
|
118
|
+
for rel in relevant_rels:
|
|
119
|
+
connected_ids.add(rel.from_id)
|
|
120
|
+
connected_ids.add(rel.to_id)
|
|
121
|
+
|
|
122
|
+
if not connected_ids:
|
|
123
|
+
lines.append("@enduml")
|
|
124
|
+
return "\n".join(lines)
|
|
125
|
+
|
|
126
|
+
# Build name lookup from all entity types
|
|
127
|
+
name_lookup: dict[str, str] = {}
|
|
128
|
+
for comp in model.entities.components:
|
|
129
|
+
name_lookup[comp.id] = comp.name
|
|
130
|
+
for iface in model.entities.interfaces:
|
|
131
|
+
name_lookup[iface.id] = iface.name
|
|
132
|
+
|
|
133
|
+
# Render rectangles for connected entities
|
|
134
|
+
for entity_id in sorted(connected_ids):
|
|
135
|
+
sid = _sanitize_id(entity_id)
|
|
136
|
+
name = name_lookup.get(entity_id, entity_id)
|
|
137
|
+
lines.append(f'rectangle "{name}" as {sid}')
|
|
138
|
+
|
|
139
|
+
lines.append("")
|
|
140
|
+
|
|
141
|
+
# Render arrows
|
|
142
|
+
for rel in relevant_rels:
|
|
143
|
+
fid = _sanitize_id(rel.from_id)
|
|
144
|
+
tid = _sanitize_id(rel.to_id)
|
|
145
|
+
if rel.type == RelationType.DEPENDS_ON:
|
|
146
|
+
lines.append(f"{fid} --> {tid} : depends-on")
|
|
147
|
+
elif rel.type == RelationType.EXPOSES:
|
|
148
|
+
lines.append(f"{fid} ..> {tid} : exposes")
|
|
149
|
+
elif rel.type == RelationType.CONSUMES:
|
|
150
|
+
lines.append(f"{fid} ..> {tid} : consumes")
|
|
151
|
+
|
|
152
|
+
lines.append("")
|
|
153
|
+
lines.append("@enduml")
|
|
154
|
+
return "\n".join(lines)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def generate_sequence_diagram(behavior: Behavior, model: ArchitectureModel) -> str:
|
|
158
|
+
"""Generate a sequence diagram from a behavior's steps.
|
|
159
|
+
|
|
160
|
+
Returns empty string if behavior has no steps.
|
|
161
|
+
"""
|
|
162
|
+
if not behavior.steps:
|
|
163
|
+
return ""
|
|
164
|
+
|
|
165
|
+
lines: list[str] = []
|
|
166
|
+
lines.append("@startuml")
|
|
167
|
+
lines.append(f"title {behavior.name}")
|
|
168
|
+
lines.append("")
|
|
169
|
+
|
|
170
|
+
# Resolve actor if present
|
|
171
|
+
actor_name: str | None = None
|
|
172
|
+
actor_alias: str | None = None
|
|
173
|
+
if behavior.actor:
|
|
174
|
+
# Look up actor entity by ID
|
|
175
|
+
for act in model.entities.actors:
|
|
176
|
+
if act.id == behavior.actor:
|
|
177
|
+
actor_name = act.name
|
|
178
|
+
actor_alias = _sanitize_id(act.id)
|
|
179
|
+
break
|
|
180
|
+
if not actor_name:
|
|
181
|
+
# Use raw actor ID as fallback
|
|
182
|
+
actor_name = behavior.actor
|
|
183
|
+
actor_alias = _sanitize_id(behavior.actor)
|
|
184
|
+
|
|
185
|
+
lines.append(f'actor "{actor_name}" as {actor_alias}')
|
|
186
|
+
|
|
187
|
+
# Add System participant for generic step mapping
|
|
188
|
+
lines.append(f'participant "System" as System')
|
|
189
|
+
lines.append("")
|
|
190
|
+
|
|
191
|
+
# Render steps as messages
|
|
192
|
+
source = actor_alias if actor_alias else "System"
|
|
193
|
+
for i, step in enumerate(behavior.steps):
|
|
194
|
+
if i == 0 and actor_alias:
|
|
195
|
+
lines.append(f"{source} -> System : {step}")
|
|
196
|
+
else:
|
|
197
|
+
lines.append(f"System -> System : {step}")
|
|
198
|
+
|
|
199
|
+
lines.append("")
|
|
200
|
+
lines.append("@enduml")
|
|
201
|
+
return "\n".join(lines)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def generate_nav_diagram(model: ArchitectureModel) -> str:
|
|
205
|
+
"""Generate a full-model navigational traceability diagram.
|
|
206
|
+
|
|
207
|
+
Shows ALL entities (typed shapes) and ALL relationships (labeled edges)
|
|
208
|
+
on a single page. Components are nested inside their layer packages.
|
|
209
|
+
Every node displays its ID for precise referencing by systems engineers.
|
|
210
|
+
"""
|
|
211
|
+
lines: list[str] = []
|
|
212
|
+
lines.append("@startuml")
|
|
213
|
+
lines.append(f"title Architecture Navigation - {model.meta.project}")
|
|
214
|
+
lines.append("left to right direction")
|
|
215
|
+
lines.append("")
|
|
216
|
+
|
|
217
|
+
# Track which entity IDs are rendered (for relationship filtering)
|
|
218
|
+
rendered_ids: set[str] = set()
|
|
219
|
+
|
|
220
|
+
# --- Actors (leftmost) ---
|
|
221
|
+
if model.entities.actors:
|
|
222
|
+
for act in model.entities.actors:
|
|
223
|
+
aid = _sanitize_id(act.id)
|
|
224
|
+
lines.append(f'actor "{act.name}\\n{act.id}" as {aid}')
|
|
225
|
+
rendered_ids.add(act.id)
|
|
226
|
+
lines.append("")
|
|
227
|
+
|
|
228
|
+
# --- Capabilities (functional view) ---
|
|
229
|
+
if model.entities.capabilities:
|
|
230
|
+
lines.append("package \"Functional View\" {")
|
|
231
|
+
for cap in model.entities.capabilities:
|
|
232
|
+
cid = _sanitize_id(cap.id)
|
|
233
|
+
lines.append(f' usecase "{cap.name}\\n{cap.id}" as {cid}')
|
|
234
|
+
rendered_ids.add(cap.id)
|
|
235
|
+
lines.append("}")
|
|
236
|
+
lines.append("")
|
|
237
|
+
|
|
238
|
+
# --- Behaviors (behavioral view) ---
|
|
239
|
+
if model.entities.behaviors:
|
|
240
|
+
lines.append("package \"Behavioral View\" {")
|
|
241
|
+
for beh in model.entities.behaviors:
|
|
242
|
+
bid = _sanitize_id(beh.id)
|
|
243
|
+
lines.append(f' rectangle "{beh.name}\\n{beh.id}" as {bid} <<behavior>>')
|
|
244
|
+
rendered_ids.add(beh.id)
|
|
245
|
+
lines.append("}")
|
|
246
|
+
lines.append("")
|
|
247
|
+
|
|
248
|
+
# --- Components in layers (logical/physical view) ---
|
|
249
|
+
layer_ids = {layer.id for layer in model.entities.layers}
|
|
250
|
+
layer_map: dict[str, list] = {layer.id: [] for layer in model.entities.layers}
|
|
251
|
+
unlayered: list = []
|
|
252
|
+
|
|
253
|
+
for comp in model.entities.components:
|
|
254
|
+
if comp.layer and comp.layer in layer_ids:
|
|
255
|
+
layer_map[comp.layer].append(comp)
|
|
256
|
+
else:
|
|
257
|
+
unlayered.append(comp)
|
|
258
|
+
|
|
259
|
+
for layer in model.entities.layers:
|
|
260
|
+
comps = layer_map.get(layer.id, [])
|
|
261
|
+
lid = _sanitize_id(layer.id)
|
|
262
|
+
lines.append(f'package "{layer.name}" as {lid} {{')
|
|
263
|
+
for comp in comps:
|
|
264
|
+
cid = _sanitize_id(comp.id)
|
|
265
|
+
lines.append(f' component "{comp.name}\\n{comp.id}" as {cid}')
|
|
266
|
+
rendered_ids.add(comp.id)
|
|
267
|
+
lines.append("}")
|
|
268
|
+
rendered_ids.add(layer.id)
|
|
269
|
+
|
|
270
|
+
for comp in unlayered:
|
|
271
|
+
cid = _sanitize_id(comp.id)
|
|
272
|
+
lines.append(f'component "{comp.name}\\n{comp.id}" as {cid}')
|
|
273
|
+
rendered_ids.add(comp.id)
|
|
274
|
+
|
|
275
|
+
if model.entities.components or model.entities.layers:
|
|
276
|
+
lines.append("")
|
|
277
|
+
|
|
278
|
+
# --- Interfaces ---
|
|
279
|
+
if model.entities.interfaces:
|
|
280
|
+
for iface in model.entities.interfaces:
|
|
281
|
+
iid = _sanitize_id(iface.id)
|
|
282
|
+
lines.append(f'() "{iface.name}\\n{iface.id}" as {iid}')
|
|
283
|
+
rendered_ids.add(iface.id)
|
|
284
|
+
lines.append("")
|
|
285
|
+
|
|
286
|
+
# --- Constraints ---
|
|
287
|
+
if model.entities.constraints:
|
|
288
|
+
for con in model.entities.constraints:
|
|
289
|
+
coid = _sanitize_id(con.id)
|
|
290
|
+
lines.append(f'card "{con.name}\\n{con.id}" as {coid}')
|
|
291
|
+
rendered_ids.add(con.id)
|
|
292
|
+
lines.append("")
|
|
293
|
+
|
|
294
|
+
# --- Relationships ---
|
|
295
|
+
for rel in model.relationships:
|
|
296
|
+
if rel.from_id in rendered_ids and rel.to_id in rendered_ids:
|
|
297
|
+
fid = _sanitize_id(rel.from_id)
|
|
298
|
+
tid = _sanitize_id(rel.to_id)
|
|
299
|
+
rtype = rel.type.value
|
|
300
|
+
if rel.type in (RelationType.EXPOSES, RelationType.CONSUMES):
|
|
301
|
+
lines.append(f"{fid} ..> {tid} : {rtype}")
|
|
302
|
+
else:
|
|
303
|
+
lines.append(f"{fid} --> {tid} : {rtype}")
|
|
304
|
+
|
|
305
|
+
lines.append("")
|
|
306
|
+
lines.append("@enduml")
|
|
307
|
+
return "\n".join(lines)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def generate_focused_diagram(
|
|
311
|
+
model: ArchitectureModel,
|
|
312
|
+
entity_id: str,
|
|
313
|
+
depth: int = 2,
|
|
314
|
+
) -> str:
|
|
315
|
+
"""Generate a focused subgraph centered on a specific entity.
|
|
316
|
+
|
|
317
|
+
Performs BFS from entity_id through relationships (both directions)
|
|
318
|
+
up to `depth` hops. Renders only the reachable subgraph with the
|
|
319
|
+
focus entity highlighted.
|
|
320
|
+
|
|
321
|
+
Returns empty string if entity_id is not found in the model.
|
|
322
|
+
"""
|
|
323
|
+
# Build entity lookup (id -> (type, name))
|
|
324
|
+
entity_info: dict[str, tuple[str, str]] = {}
|
|
325
|
+
for act in model.entities.actors:
|
|
326
|
+
entity_info[act.id] = ("actor", act.name)
|
|
327
|
+
for cap in model.entities.capabilities:
|
|
328
|
+
entity_info[cap.id] = ("capability", cap.name)
|
|
329
|
+
for beh in model.entities.behaviors:
|
|
330
|
+
entity_info[beh.id] = ("behavior", beh.name)
|
|
331
|
+
for comp in model.entities.components:
|
|
332
|
+
entity_info[comp.id] = ("component", comp.name)
|
|
333
|
+
for iface in model.entities.interfaces:
|
|
334
|
+
entity_info[iface.id] = ("interface", iface.name)
|
|
335
|
+
for con in model.entities.constraints:
|
|
336
|
+
entity_info[con.id] = ("constraint", con.name)
|
|
337
|
+
for layer in model.entities.layers:
|
|
338
|
+
entity_info[layer.id] = ("layer", layer.name)
|
|
339
|
+
|
|
340
|
+
if entity_id not in entity_info:
|
|
341
|
+
return ""
|
|
342
|
+
|
|
343
|
+
# Build adjacency (both directions)
|
|
344
|
+
adjacency: dict[str, set[str]] = {}
|
|
345
|
+
for eid in entity_info:
|
|
346
|
+
adjacency[eid] = set()
|
|
347
|
+
for rel in model.relationships:
|
|
348
|
+
if rel.from_id in adjacency:
|
|
349
|
+
adjacency[rel.from_id].add(rel.to_id)
|
|
350
|
+
if rel.to_id in adjacency:
|
|
351
|
+
adjacency[rel.to_id].add(rel.from_id)
|
|
352
|
+
|
|
353
|
+
# BFS from entity_id
|
|
354
|
+
visited: dict[str, int] = {entity_id: 0}
|
|
355
|
+
queue: list[tuple[str, int]] = [(entity_id, 0)]
|
|
356
|
+
while queue:
|
|
357
|
+
current, d = queue.pop(0)
|
|
358
|
+
if d >= depth:
|
|
359
|
+
continue
|
|
360
|
+
for neighbor in adjacency.get(current, set()):
|
|
361
|
+
if neighbor not in visited and neighbor in entity_info:
|
|
362
|
+
visited[neighbor] = d + 1
|
|
363
|
+
queue.append((neighbor, d + 1))
|
|
364
|
+
|
|
365
|
+
if len(visited) <= 1 and not adjacency.get(entity_id):
|
|
366
|
+
# Isolated node — still render it
|
|
367
|
+
pass
|
|
368
|
+
|
|
369
|
+
# Render
|
|
370
|
+
focus_name = entity_info[entity_id][1]
|
|
371
|
+
lines: list[str] = []
|
|
372
|
+
lines.append("@startuml")
|
|
373
|
+
lines.append(f"title Focus: {entity_id} ({focus_name}) - depth {depth}")
|
|
374
|
+
lines.append("")
|
|
375
|
+
|
|
376
|
+
# Render nodes
|
|
377
|
+
for eid, d in sorted(visited.items(), key=lambda x: x[1]):
|
|
378
|
+
etype, ename = entity_info[eid]
|
|
379
|
+
sid = _sanitize_id(eid)
|
|
380
|
+
highlight = " #LightBlue" if eid == entity_id else ""
|
|
381
|
+
if etype == "actor":
|
|
382
|
+
lines.append(f'actor "{ename}\\n{eid}" as {sid}{highlight}')
|
|
383
|
+
elif etype == "capability":
|
|
384
|
+
lines.append(f'usecase "{ename}\\n{eid}" as {sid}{highlight}')
|
|
385
|
+
elif etype == "behavior":
|
|
386
|
+
lines.append(f'rectangle "{ename}\\n{eid}" as {sid} <<behavior>>{highlight}')
|
|
387
|
+
elif etype == "component":
|
|
388
|
+
lines.append(f'component "{ename}\\n{eid}" as {sid}{highlight}')
|
|
389
|
+
elif etype == "interface":
|
|
390
|
+
lines.append(f'() "{ename}\\n{eid}" as {sid}{highlight}')
|
|
391
|
+
elif etype == "constraint":
|
|
392
|
+
lines.append(f'card "{ename}\\n{eid}" as {sid}{highlight}')
|
|
393
|
+
elif etype == "layer":
|
|
394
|
+
lines.append(f'package "{ename}\\n{eid}" as {sid}{highlight}')
|
|
395
|
+
|
|
396
|
+
lines.append("")
|
|
397
|
+
|
|
398
|
+
# Render edges (only between visited nodes)
|
|
399
|
+
for rel in model.relationships:
|
|
400
|
+
if rel.from_id in visited and rel.to_id in visited:
|
|
401
|
+
fid = _sanitize_id(rel.from_id)
|
|
402
|
+
tid = _sanitize_id(rel.to_id)
|
|
403
|
+
rtype = rel.type.value
|
|
404
|
+
if rel.type in (RelationType.EXPOSES, RelationType.CONSUMES):
|
|
405
|
+
lines.append(f"{fid} ..> {tid} : {rtype}")
|
|
406
|
+
else:
|
|
407
|
+
lines.append(f"{fid} --> {tid} : {rtype}")
|
|
408
|
+
|
|
409
|
+
lines.append("")
|
|
410
|
+
lines.append("@enduml")
|
|
411
|
+
return "\n".join(lines)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def generate_all_diagrams(model: ArchitectureModel) -> dict[str, str]:
|
|
415
|
+
"""Generate all applicable diagrams for the model.
|
|
416
|
+
|
|
417
|
+
Returns dict mapping diagram name to PlantUML string.
|
|
418
|
+
Only includes diagrams where there's enough data.
|
|
419
|
+
"""
|
|
420
|
+
result: dict[str, str] = {}
|
|
421
|
+
|
|
422
|
+
# Component diagram: requires at least 2 components
|
|
423
|
+
if len(model.entities.components) >= 2:
|
|
424
|
+
result["component-diagram"] = generate_component_diagram(model)
|
|
425
|
+
|
|
426
|
+
# Dependency graph: requires at least 1 relevant relationship
|
|
427
|
+
allowed_types = {RelationType.DEPENDS_ON, RelationType.EXPOSES, RelationType.CONSUMES}
|
|
428
|
+
has_dep_rels = any(r.type in allowed_types for r in model.relationships)
|
|
429
|
+
if has_dep_rels:
|
|
430
|
+
result["dependency-graph"] = generate_dependency_diagram(model)
|
|
431
|
+
|
|
432
|
+
# Navigation diagram: requires at least 2 entities total
|
|
433
|
+
total_entities = (
|
|
434
|
+
len(model.entities.actors)
|
|
435
|
+
+ len(model.entities.capabilities)
|
|
436
|
+
+ len(model.entities.behaviors)
|
|
437
|
+
+ len(model.entities.components)
|
|
438
|
+
+ len(model.entities.interfaces)
|
|
439
|
+
+ len(model.entities.constraints)
|
|
440
|
+
)
|
|
441
|
+
if total_entities >= 2:
|
|
442
|
+
result["nav-diagram"] = generate_nav_diagram(model)
|
|
443
|
+
|
|
444
|
+
# Sequence diagrams: one per behavior with steps
|
|
445
|
+
for behavior in model.entities.behaviors:
|
|
446
|
+
if behavior.steps:
|
|
447
|
+
diagram = generate_sequence_diagram(behavior, model)
|
|
448
|
+
if diagram:
|
|
449
|
+
result[f"sequence-{behavior.id}"] = diagram
|
|
450
|
+
|
|
451
|
+
return result
|