@@ -1724,6 +1724,50 @@ def _find_misplaced_nodes(
17241724 ]
17251725
17261726
1727+ def _build_community_points (
1728+ ordered_communities : list [dict [str , Any ]],
1729+ node_by_id : dict ,
1730+ positions : dict [str , tuple [float , float ]],
1731+ ) -> list [dict [str , Any ]]:
1732+ """Build community point dicts from ordered communities."""
1733+ points : list [dict [str , Any ]] = []
1734+ for community in ordered_communities :
1735+ member_ids = list (community .get ("member_node_ids" , []))
1736+ member_nodes = [node_by_id [nid ] for nid in member_ids if nid in node_by_id ]
1737+ profile = _community_profile (member_nodes )
1738+ dominant_domain = profile ["dominant_domain" ]
1739+ dominant_ratio = float (profile ["dominant_ratio" ])
1740+ misplaced_nodes = _find_misplaced_nodes (
1741+ member_nodes , dominant_domain , dominant_ratio , profile ["node_domain_by_id" ]
1742+ )
1743+ x , y = positions .get (str (community ["id" ]), (0.0 , 0.0 ))
1744+ sample_nodes = community .get ("sample_nodes" ) or []
1745+ anchor = str (sample_nodes [0 ]["id" ]) if sample_nodes else ""
1746+ points .append (
1747+ {
1748+ "id" : str (community ["id" ]),
1749+ "label" : str (community ["label" ]),
1750+ "x" : x ,
1751+ "y" : y ,
1752+ "member_count" : int (community ["size" ]),
1753+ "cohesion" : float (community ["cohesion" ]),
1754+ "top_kinds" : profile ["top_kinds" ],
1755+ "domain_counts" : profile ["domain_counts" ],
1756+ "dominant_domain" : dominant_domain ,
1757+ "dominant_ratio" : dominant_ratio ,
1758+ "summary" : None ,
1759+ "anchor_node_id" : anchor ,
1760+ "sample_nodes" : list (sample_nodes ),
1761+ "member_node_ids" : [str (mid ) for mid in community .get ("member_node_ids" , [])],
1762+ "vector" : [],
1763+ "source_refs" : profile ["source_refs" ],
1764+ "name_tokens" : profile ["name_tokens" ],
1765+ "misplaced_nodes" : misplaced_nodes ,
1766+ }
1767+ )
1768+ return points
1769+
1770+
17271771async def _build_structural_community_points (
17281772 db : Any ,
17291773 * ,
@@ -1796,45 +1840,7 @@ async def _build_structural_community_points(
17961840 }
17971841 positions = _layout_code_structure_points (point_ids , scoped_weights )
17981842
1799- points : list [dict [str , Any ]] = []
1800- for community in ordered_communities :
1801- member_ids = list (community .get ("member_node_ids" , []))
1802- member_nodes = [node_by_id [node_id ] for node_id in member_ids if node_id in node_by_id ]
1803- profile = _community_profile (member_nodes )
1804- dominant_domain = profile ["dominant_domain" ]
1805- dominant_ratio = float (profile ["dominant_ratio" ])
1806- misplaced_nodes = _find_misplaced_nodes (
1807- member_nodes , dominant_domain , dominant_ratio , profile ["node_domain_by_id" ]
1808- )
1809-
1810- x , y = positions .get (str (community ["id" ]), (0.0 , 0.0 ))
1811- points .append (
1812- {
1813- "id" : str (community ["id" ]),
1814- "label" : str (community ["label" ]),
1815- "x" : x ,
1816- "y" : y ,
1817- "member_count" : int (community ["size" ]),
1818- "cohesion" : float (community ["cohesion" ]),
1819- "top_kinds" : profile ["top_kinds" ],
1820- "domain_counts" : profile ["domain_counts" ],
1821- "dominant_domain" : dominant_domain ,
1822- "dominant_ratio" : dominant_ratio ,
1823- "summary" : None ,
1824- "anchor_node_id" : str (community ["sample_nodes" ][0 ]["id" ])
1825- if community .get ("sample_nodes" )
1826- else "" ,
1827- "sample_nodes" : list (community .get ("sample_nodes" , [])),
1828- "member_node_ids" : [
1829- str (member_node_id ) for member_node_id in community .get ("member_node_ids" , [])
1830- ],
1831- "vector" : [],
1832- "source_refs" : profile ["source_refs" ],
1833- "name_tokens" : profile ["name_tokens" ],
1834- "misplaced_nodes" : misplaced_nodes ,
1835- }
1836- )
1837-
1843+ points = _build_community_points (ordered_communities , node_by_id , positions )
18381844 _normalize_xy (points )
18391845 return points , warnings
18401846
@@ -3250,6 +3256,33 @@ async def ports_adapters_view(
32503256 }
32513257
32523258
3259+ def _resolve_table_name (col : KnowledgeNode ) -> str :
3260+ """Resolve the table name from a column node's metadata or natural key."""
3261+ meta = col .meta if isinstance (col .meta , dict ) else {}
3262+ return meta .get ("table" ) or _parse_db_table_from_natural_key (col .natural_key ) or "unknown"
3263+
3264+
3265+ def _build_fk_row (
3266+ edge : KnowledgeEdge ,
3267+ column_by_id : dict [uuid .UUID , KnowledgeNode ],
3268+ ) -> dict [str , Any ] | None :
3269+ """Build a foreign-key row dict from an edge, or return None if columns are missing."""
3270+ source_col = column_by_id .get (edge .source_node_id )
3271+ target_col = column_by_id .get (edge .target_node_id )
3272+ if not source_col or not target_col :
3273+ return None
3274+ return {
3275+ "id" : str (edge .id ),
3276+ "fk_name" : (edge .meta or {}).get ("fk_name" ),
3277+ "source_table" : _resolve_table_name (source_col ),
3278+ "source_column" : source_col .name ,
3279+ "target_table" : _resolve_table_name (target_col ),
3280+ "target_column" : target_col .name ,
3281+ "source_column_node_id" : str (source_col .id ),
3282+ "target_column_node_id" : str (target_col .id ),
3283+ }
3284+
3285+
32533286def _process_erm_edges (
32543287 edges : list [KnowledgeEdge ],
32553288 table_by_id : dict [uuid .UUID , KnowledgeNode ],
@@ -3267,28 +3300,9 @@ def _process_erm_edges(
32673300 columns_by_table [source .id ].append (target )
32683301 continue
32693302
3270- source_col = column_by_id .get (edge .source_node_id )
3271- target_col = column_by_id .get (edge .target_node_id )
3272- if not source_col or not target_col :
3273- continue
3274- source_meta = source_col .meta if isinstance (source_col .meta , dict ) else {}
3275- target_meta = target_col .meta if isinstance (target_col .meta , dict ) else {}
3276- fk_rows .append (
3277- {
3278- "id" : str (edge .id ),
3279- "fk_name" : (edge .meta or {}).get ("fk_name" ),
3280- "source_table" : source_meta .get ("table" )
3281- or _parse_db_table_from_natural_key (source_col .natural_key )
3282- or "unknown" ,
3283- "source_column" : source_col .name ,
3284- "target_table" : target_meta .get ("table" )
3285- or _parse_db_table_from_natural_key (target_col .natural_key )
3286- or "unknown" ,
3287- "target_column" : target_col .name ,
3288- "source_column_node_id" : str (source_col .id ),
3289- "target_column_node_id" : str (target_col .id ),
3290- }
3291- )
3303+ fk_row = _build_fk_row (edge , column_by_id )
3304+ if fk_row :
3305+ fk_rows .append (fk_row )
32923306
32933307 return columns_by_table , fk_rows
32943308
@@ -5456,6 +5470,100 @@ async def _export_mermaid_c4_format(
54565470 }
54575471
54585472
5473+ async def _generate_export_content (
5474+ db : Any ,
5475+ scenario : Any ,
5476+ projection : GraphProjection ,
5477+ body : Any ,
5478+ ) -> tuple [dict | None , str , Any , str , GraphProjection ]:
5479+ """Dispatch to the correct export format and return (early_return, content, kind, name, projection)."""
5480+ if body .format == "lpg_jsonl" :
5481+ if projection == GraphProjection .CODE_SYMBOL :
5482+ content = await export_lpg_jsonl (db , scenario .id )
5483+ else :
5484+ graph = await get_full_scenario_graph (
5485+ session = db ,
5486+ scenario_id = scenario .id ,
5487+ layer = None ,
5488+ projection = projection ,
5489+ entity_level = body .entity_level ,
5490+ )
5491+ content = export_lpg_jsonl_from_graph (scenario .id , graph )
5492+ return (
5493+ None ,
5494+ content ,
5495+ KnowledgeArtifactKind .LPG_JSONL ,
5496+ f"{ scenario .name } .lpg.jsonl" ,
5497+ projection ,
5498+ )
5499+ if body .format == "cc_json" :
5500+ if projection == GraphProjection .CODE_SYMBOL :
5501+ projection = GraphProjection .CODE_FILE
5502+ content = await export_codecharta_json (
5503+ db ,
5504+ scenario .id ,
5505+ projection = projection ,
5506+ entity_level = body .entity_level ,
5507+ )
5508+ return None , content , KnowledgeArtifactKind .CC_JSON , f"{ scenario .name } .cc.json" , projection
5509+ if body .format == "cx2" :
5510+ if projection == GraphProjection .CODE_SYMBOL :
5511+ content = await export_cx2 (db , scenario .id )
5512+ else :
5513+ graph = await get_full_scenario_graph (
5514+ session = db ,
5515+ scenario_id = scenario .id ,
5516+ layer = None ,
5517+ projection = projection ,
5518+ entity_level = body .entity_level ,
5519+ )
5520+ content = export_cx2_from_graph (scenario .id , graph )
5521+ return None , content , KnowledgeArtifactKind .CX2 , f"{ scenario .name } .cx2.json" , projection
5522+ if body .format == "jgf" :
5523+ if projection == GraphProjection .CODE_SYMBOL :
5524+ content = await export_jgf (db , scenario .id )
5525+ else :
5526+ graph = await get_full_scenario_graph (
5527+ session = db ,
5528+ scenario_id = scenario .id ,
5529+ layer = None ,
5530+ projection = projection ,
5531+ entity_level = body .entity_level ,
5532+ )
5533+ content = export_jgf_from_graph (scenario .id , graph )
5534+ return None , content , KnowledgeArtifactKind .JGF , f"{ scenario .name } .jgf.json" , projection
5535+ if body .format == "twin_manifest" :
5536+ content = await export_twin_manifest (db , scenario .id )
5537+ return (
5538+ None ,
5539+ content ,
5540+ KnowledgeArtifactKind .TWIN_MANIFEST ,
5541+ f"{ scenario .name } .twin_manifest.json" ,
5542+ projection ,
5543+ )
5544+ # Mermaid C4 or other format
5545+ result = await _export_mermaid_c4_format (db , scenario , body )
5546+ if result is not None :
5547+ return result , "" , None , "" , projection
5548+ selected_c4_view = _parse_c4_view (body .c4_view )
5549+ selected_c4_scope = _parse_c4_scope (body .c4_scope )
5550+ selected_max_nodes = body .max_nodes or 120
5551+ content = await export_mermaid_c4 (
5552+ db ,
5553+ scenario .id ,
5554+ entity_level = body .entity_level or "container" ,
5555+ c4_view = selected_c4_view ,
5556+ c4_scope = selected_c4_scope ,
5557+ max_nodes = selected_max_nodes ,
5558+ )
5559+ kind = (
5560+ KnowledgeArtifactKind .MERMAID_C4_ASIS
5561+ if scenario .is_as_is
5562+ else KnowledgeArtifactKind .MERMAID_C4_TOBE
5563+ )
5564+ return None , content , kind , f"{ scenario .name } .mmd" , projection
5565+
5566+
54595567@router .post (
54605568 "/scenarios/{scenario_id}/exports" ,
54615569 responses = {
@@ -5475,85 +5583,11 @@ async def create_export(request: Request, scenario_id: str, body: ExportRequest)
54755583 GraphProjection (body .projection ) if body .projection else GraphProjection .ARCHITECTURE
54765584 )
54775585
5478- if body .format == "lpg_jsonl" :
5479- if projection == GraphProjection .CODE_SYMBOL :
5480- content = await export_lpg_jsonl (db , scenario .id )
5481- else :
5482- graph = await get_full_scenario_graph (
5483- session = db ,
5484- scenario_id = scenario .id ,
5485- layer = None ,
5486- projection = projection ,
5487- entity_level = body .entity_level ,
5488- )
5489- content = export_lpg_jsonl_from_graph (scenario .id , graph )
5490- kind = KnowledgeArtifactKind .LPG_JSONL
5491- name = f"{ scenario .name } .lpg.jsonl"
5492- elif body .format == "cc_json" :
5493- if projection == GraphProjection .CODE_SYMBOL :
5494- projection = GraphProjection .CODE_FILE
5495- content = await export_codecharta_json (
5496- db ,
5497- scenario .id ,
5498- projection = projection ,
5499- entity_level = body .entity_level ,
5500- )
5501- kind = KnowledgeArtifactKind .CC_JSON
5502- name = f"{ scenario .name } .cc.json"
5503- elif body .format == "cx2" :
5504- if projection == GraphProjection .CODE_SYMBOL :
5505- content = await export_cx2 (db , scenario .id )
5506- else :
5507- graph = await get_full_scenario_graph (
5508- session = db ,
5509- scenario_id = scenario .id ,
5510- layer = None ,
5511- projection = projection ,
5512- entity_level = body .entity_level ,
5513- )
5514- content = export_cx2_from_graph (scenario .id , graph )
5515- kind = KnowledgeArtifactKind .CX2
5516- name = f"{ scenario .name } .cx2.json"
5517- elif body .format == "jgf" :
5518- if projection == GraphProjection .CODE_SYMBOL :
5519- content = await export_jgf (db , scenario .id )
5520- else :
5521- graph = await get_full_scenario_graph (
5522- session = db ,
5523- scenario_id = scenario .id ,
5524- layer = None ,
5525- projection = projection ,
5526- entity_level = body .entity_level ,
5527- )
5528- content = export_jgf_from_graph (scenario .id , graph )
5529- kind = KnowledgeArtifactKind .JGF
5530- name = f"{ scenario .name } .jgf.json"
5531- elif body .format == "twin_manifest" :
5532- content = await export_twin_manifest (db , scenario .id )
5533- kind = KnowledgeArtifactKind .TWIN_MANIFEST
5534- name = f"{ scenario .name } .twin_manifest.json"
5535- else :
5536- result = await _export_mermaid_c4_format (db , scenario , body )
5537- if result is not None :
5538- return result
5539-
5540- selected_c4_view = _parse_c4_view (body .c4_view )
5541- selected_c4_scope = _parse_c4_scope (body .c4_scope )
5542- selected_max_nodes = body .max_nodes or 120
5543- content = await export_mermaid_c4 (
5544- db ,
5545- scenario .id ,
5546- entity_level = body .entity_level or "container" ,
5547- c4_view = selected_c4_view ,
5548- c4_scope = selected_c4_scope ,
5549- max_nodes = selected_max_nodes ,
5550- )
5551- kind = (
5552- KnowledgeArtifactKind .MERMAID_C4_ASIS
5553- if scenario .is_as_is
5554- else KnowledgeArtifactKind .MERMAID_C4_TOBE
5555- )
5556- name = f"{ scenario .name } .mmd"
5586+ early_return , content , kind , name , projection = await _generate_export_content (
5587+ db , scenario , projection , body
5588+ )
5589+ if early_return is not None :
5590+ return early_return
55575591
55585592 meta : dict = {
55595593 "scenario_id" : str (scenario .id ),
@@ -5562,9 +5596,9 @@ async def create_export(request: Request, scenario_id: str, body: ExportRequest)
55625596 "entity_level" : body .entity_level ,
55635597 }
55645598 if body .format == "mermaid_c4" :
5565- meta ["c4_view" ] = selected_c4_view
5566- meta ["c4_scope" ] = selected_c4_scope
5567- meta ["max_nodes" ] = selected_max_nodes
5599+ meta ["c4_view" ] = _parse_c4_view ( body . c4_view )
5600+ meta ["c4_scope" ] = _parse_c4_scope ( body . c4_scope )
5601+ meta ["max_nodes" ] = body . max_nodes or 120
55685602 artifact = await _upsert_artifact (
55695603 db ,
55705604 collection_id = scenario .collection_id ,
0 commit comments