Skip to content

Commit da0d40d

Browse files
feat(diagrams): add UDM caller-supplied structured data path for all 13 diagram types
Every generate_* method on UMLDiagramGenerator previously produced structure one of two ways: AST-scanning project_root, or (for 8 of 13 types -- activity, state, usecase, object, deployment, communication, composite, interaction) handing a natural-language summary to an LLM and returning its freeform output verbatim as the diagram. Neither mode works for a project whose structure is authoritative but not expressed as scannable source code (e.g. shakti-os: 458 architecture records, zero .py files), and the LLM path is a standing violation of any consumer's rule that structural diagrams must not be LLM-authored. This adds a second input mode alongside the existing derived mode: generate_from_data(diagram_type, data) dispatches to a deterministic renderer for each of the 13 diagram types, never touching an LLM, the filesystem, or the network. The schema (UDM v1, UDM_PRIMARY_KEY) matches langgraph_engine.diagrams.drawio.converter.DrawioConverter's existing analysis_data field names exactly -- that converter was already fully deterministic and dict-driven for 12 types with zero LLM calls, just never exposed to a tool caller -- so one JSON payload now renders consistently on both the Mermaid/PlantUML side (here) and the draw.io side. class and call_graph reuse existing overrides (classes=, and a new ~12-line _DictCallGraph adapter around the existing call_graph= override) rather than duplicating their renderers. package, component, and sequence get dedicated new renderers instead of reusing their existing dep_graph=/call_chains= overrides: those predate UDM and use an incompatible shape (flat {module: set(deps)} adjacency, not UDM's packages/imports or components/dependencies+provides/requires; literal caller/callee keys only, not UDM's caller|from|source aliases) -- reusing them would silently drop caller-supplied structure or KeyError on valid UDM input. Also, while verifying the existing code against the intended UDM contract: - generate_component_diagram no longer silently drops every isolated module when a manually-supplied dep_graph has no matching .py files on disk to rglob (previously: zero groups resolved -> zero nodes declared -> a dependency-free module simply vanished, with no diagnostic). It now takes an optional explicit groups= mapping, and any module that resolves to no group lands in an "ungrouped" subgraph instead of disappearing. - generate_component_diagram's Mermaid renderer for UDM `component` data also renders `requires` as dashed edges to a synthetic interface node -- DrawioConverter reads `requires` but never renders it, so this keeps the Mermaid side from silently dropping information the payload declared. - generate_deployment_diagram(infra_files=...) was a dead parameter: passing it made the method ignore it AND skip auto-detection, falling through to a generic "Python project with modules: ..." fallback -- strictly worse than omitting it. Fixed so supplying it is never worse than not supplying it. - DrawioConverter gains a 13th builder, _call_graph_diagram, mirroring generate_call_graph_diagram's subgraph-per-class layout, entry-point bold border, and cyclomatic>=5 red fill. call_graph was previously absent from both the base and enriched converters' SUPPORTED_TYPES, so any caller requesting it silently fell through to _generic_diagram and got a meaningless flat list of strings, written to disk and reported success. Backward compatible: every new parameter is optional and appended last; every existing positional call site is unaffected; the LLM/AST path is reached exactly when data is not supplied, byte-for-byte unchanged. call_graph now rendering real content instead of _generic_diagram's placeholder is a behavior change, but only for callers of a type that previously produced meaningless output. 35 new tests in tests/test_udm_generate_from_data.py (determinism, no-LLM via monkeypatch detonators, missing-primary-key rejection per type, the component/deployment regression fixes). Existing tests/test_uml_generators.py (67 tests) and the full diagram/drawio/call_graph/component test sweep pass unmodified. Implements techdeveloper-org/mcp-uml-diagram#4 and closes #311. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 53ec3d6 commit da0d40d

5 files changed

Lines changed: 1162 additions & 35 deletions

File tree

‎langgraph_engine/diagrams/drawio/converter.py‎

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ class DrawioConverter:
100100
"communication",
101101
"composite",
102102
"interaction",
103+
"call_graph",
103104
]
104105

105106
def convert(self, diagram_type, analysis_data):
@@ -128,6 +129,7 @@ def convert(self, diagram_type, analysis_data):
128129
"communication": self._communication_diagram,
129130
"composite": self._composite_diagram,
130131
"interaction": self._interaction_diagram,
132+
"call_graph": self._call_graph_diagram,
131133
}
132134

133135
gen = _generators.get(diagram_type, self._generic_diagram)
@@ -1122,6 +1124,102 @@ def _interaction_diagram(self, data, nid):
11221124

11231125
return cells
11241126

1127+
# ------------------------------------------------------------------
1128+
# 13. CALL GRAPH DIAGRAM
1129+
# ------------------------------------------------------------------
1130+
# Mirrors legacy_generator.generate_call_graph_diagram: one swimlane per
1131+
# parent class, methods as child rows, call edges between them. Standalone
1132+
# functions group under "Functions". Entry points (public, never a callee)
1133+
# get a bold border; methods with cyclomatic >= 5 get a red fill.
1134+
1135+
def _call_graph_diagram(self, data, nid):
1136+
cells = []
1137+
methods = data.get("methods") or []
1138+
edges = [e for e in (data.get("edges") or []) if e.get("type") != "inheritance"]
1139+
1140+
if not methods:
1141+
return [_vertex(nid(), "No call graph data", S_ACT_ACTION, 100, 100, 240, 50)]
1142+
1143+
by_class = {}
1144+
standalone = []
1145+
for m in methods[:40]:
1146+
fqn = m.get("fqn", "")
1147+
if not fqn:
1148+
continue
1149+
parent = m.get("parent_class")
1150+
if parent:
1151+
cls_name = parent.split("::")[-1] if "::" in parent else parent
1152+
by_class.setdefault(cls_name, []).append(m)
1153+
else:
1154+
standalone.append(m)
1155+
1156+
callee_fqns = set(e.get("to", "") for e in edges)
1157+
1158+
MAX_COLS = 4
1159+
BOX_W = 220
1160+
HDR_H = 26
1161+
ROW_H = 20
1162+
GAP_X = 70
1163+
GAP_Y = 80
1164+
ORIGIN_X = 40
1165+
ORIGIN_Y = 40
1166+
1167+
fqn_to_row = {}
1168+
groups = list(sorted(by_class.items()))
1169+
if standalone:
1170+
groups.append(("Functions", standalone))
1171+
1172+
for idx, (group_name, group_methods) in enumerate(groups):
1173+
rows_h = max(len(group_methods) * ROW_H, ROW_H) + 4
1174+
total_h = HDR_H + rows_h
1175+
1176+
col = idx % MAX_COLS
1177+
row = idx // MAX_COLS
1178+
x = ORIGIN_X + col * (BOX_W + GAP_X)
1179+
y = ORIGIN_Y + row * (total_h + GAP_Y)
1180+
1181+
cid = nid()
1182+
cells.append(_vertex(cid, group_name, S_CLASS_HDR, x, y, BOX_W, total_h))
1183+
1184+
for k, m in enumerate(group_methods):
1185+
fqn = m.get("fqn", "")
1186+
name = m.get("name", "")
1187+
params = m.get("params", [])
1188+
params_str = ", ".join(str(p).split(":")[0].strip() for p in params[:3])
1189+
cyclomatic = m.get("cyclomatic", 1)
1190+
is_entry = not name.startswith("_") and fqn not in callee_fqns
1191+
1192+
row_style = S_CLASS_ROW
1193+
if cyclomatic >= 5:
1194+
row_style = row_style + ";fillColor=#ff6666"
1195+
if is_entry:
1196+
row_style = row_style + ";strokeWidth=3"
1197+
1198+
rid = nid()
1199+
fqn_to_row[fqn] = rid
1200+
cells.append(
1201+
_vertex_child(
1202+
rid,
1203+
"%s(%s)" % (name, params_str),
1204+
row_style,
1205+
0,
1206+
HDR_H + k * ROW_H,
1207+
BOX_W,
1208+
ROW_H,
1209+
cid,
1210+
)
1211+
)
1212+
1213+
edge_count = 0
1214+
for e in edges[:60]:
1215+
from_id = fqn_to_row.get(e.get("from", ""))
1216+
to_id = fqn_to_row.get(e.get("to", ""))
1217+
if from_id and to_id and from_id != to_id:
1218+
cells.append(_edge(nid(), "", S_COMP_DEP, from_id, to_id))
1219+
edge_count += 1
1220+
1221+
return cells
1222+
11251223
# ------------------------------------------------------------------
11261224
# Generic fallback
11271225
# ------------------------------------------------------------------

‎langgraph_engine/diagrams/drawio/drawio_converter_enriched.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ class DrawioConverter(_BaseDrawioConverter):
168168
"communication",
169169
"composite",
170170
"interaction",
171+
"call_graph",
171172
"timing",
172173
"call_graph_rich",
173174
] # type: List[str]

0 commit comments

Comments
 (0)