recon_gen.common.l2.topology
Topology projection of an L2Instance — typed value object + renderer.
Two layers:
The typed projection (
TopologyGraph+TopologyNode+TopologyEdge, built bytopology_graph_for). Pure data — one walk over anL2Instance, no rendering. Studio’s diagram chrome reads this for entity counts (rails / chains / templates / role scopes); the per-rail emitter also reuses it for role-node iteration so the typed walk isn’t duplicated.The graphviz renderer (
build_topology_graph_per_rail). Builds agraphviz.Digraphwith rails as first-class nodes (src_role → rail → dst_rolebecomes a 3-rank chain dot can lay out deterministically). Bundle nodes consolidate parallel pure-connectivity rails (anchored rails — chain endpoints / template leg-rails — stay individual). Templates render as clusters around their leg-rails. Chains as dashed edges between rail/template nodes. Control_parent (subledger → control role) as dashed gray edges. Optional focus filter (focus_node_id+ smart-default hops) for click-to-zoom-in re-render.
The X.4.b spike (locked 2026-05-13) chose this rails-as-nodes /
graphviz-dot model over the d3-force alternative. The dot pivot
makes the user’s mental “roles → rails → roles” reading fall out of
dot’s rank algorithm with zero knobs; force-directed layouts required
extensive per-graph tuning. See docs/audits/x_4_b_diagram_renderer_spike.md.
Functions
|
Build a Graphviz Digraph with Rails as first-class nodes (X.4.b dot pivot). |
|
Walk an L2Instance and return its typed topology projection. |
|
Return the L2 entity IDs visible in a focused diagram subgraph. |
Classes
|
An edge in the L2 topology projection. |
|
Typed projection of an |
|
A node in the L2 topology projection — role, rail, or template. |
- class recon_gen.common.l2.topology.TopologyEdge(source, target, kind, label, metadata=<factory>)[source]
Bases:
objectAn edge in the L2 topology projection.
kinddiscriminates the five edge flavors:rail_bundle— one or more parallel TwoLegRails between the same(source, destination)role pair.metadatacarriesrail_count(str-of-int) so the renderer can scale stroke width and the d3 side can show a count badge.self_loop— a SingleLegRail rendered as a self-loop on itsleg_role.metadatacarriesdirection(Debit / Credit / Variable).template_member— a dotted membership edge from a TransferTemplate’s node to one of itsleg_rails. The graphviz renderer wraps these inside the template’s cluster.chain— a Chain row’s parent → child relationship. One edge per child inchain.children(singleton row = 1 edge, multi-children row = N edges).metadatacarriescardinality("required"for singleton-children rows,"xor"for multi-children rows) and, for"xor"edges,xor_siblings(the comma-joined sibling names so the renderer can group them).control_parent— an Account / AccountTemplate’sparent_rolerelationship (subledger rolls up to control account). Structural, not flow — the chart-of-accounts hierarchy that explains why a “control” account exists even when no rail terminates on it.metadatacarrieschild_kind(“account” / “template”) so the renderer can style differently. When the parent role also carries one or moreLimitScheduleentries,has_limits=trueflags it for cap-badge rendering.
labelis the human-readable display label (may be empty for membership edges; the graphviz renderer suppresses labels on those).- Parameters:
source (str)
target (str)
kind (Literal['rail_bundle', 'self_loop', 'template_member', 'chain', 'control_parent'])
label (str)
metadata (Mapping[str, str])
- kind: Literal['rail_bundle', 'self_loop', 'template_member', 'chain', 'control_parent']
- label: str
- metadata: Mapping[str, str]
- source: str
- target: str
- class recon_gen.common.l2.topology.TopologyGraph(instance_name, nodes, edges)[source]
Bases:
objectTyped projection of an
L2Instance’s topology.Frozen value object — both spike arms read it; neither mutates. Iteration order (nodes + edges) is deterministic across runs of the same input, matching the existing graphviz renderer’s walk so the rendered DOT stays stable for the docs-site diagrams that snapshot against it.
- Parameters:
instance_name (str)
nodes (tuple[TopologyNode, ...])
edges (tuple[TopologyEdge, ...])
- edges: tuple[TopologyEdge, ...]
- instance_name: str
- nodes: tuple[TopologyNode, ...]
- class recon_gen.common.l2.topology.TopologyNode(id, kind, label, scope=None, templated=False, metadata=<factory>)[source]
Bases:
objectA node in the L2 topology projection — role, rail, or template.
idcarries the discriminated prefix scheme used by the existing graphviz renderer (role__<role>,rail__<rail>,tmpl__<name>) so arm B’s post-processed SVG can key off the renderedidattr to find each node and tag it withdata-kind/data-id.labelis the human-readable display label (may contain\nfor multi-line). For templates it carries the<name>\nkeys: <list>inner label that the existing renderer puts on the template’sshape="component"node.scope+templatedare role-only (None/Falsefor rails + templates).metadatacarries kind-specific extras the renderer may need but the typed model doesn’t promote to first-class fields:On a
templatenode:transfer_key(comma-joined str) — used by the graphviz renderer to build the cluster header text.Open for future use (e.g., row-counts for the X.4.c.5 coverage tint).
- Parameters:
id (str)
kind (Literal['role', 'rail', 'template'])
label (str)
scope (Literal['internal', 'external'] | None)
templated (bool)
metadata (Mapping[str, str])
- id: str
- kind: Literal['role', 'rail', 'template']
- label: str
- metadata: Mapping[str, str]
- scope: Literal['internal', 'external'] | None
- templated: bool
- recon_gen.common.l2.topology.build_topology_graph_per_rail(instance, *, db_table_prefix, bundle_parallel_rails=True, focus_node_id=None, layer=3)[source]
Build a Graphviz Digraph with Rails as first-class nodes (X.4.b dot pivot).
Sibling to
build_topology_graph(which models rails as edges between roles + clusters them inside templates). This view promotes every Rail to its own node + connects it to its endpoint roles via directed edges (src_role → rail → dst_rolefor TwoLegRail;leg_role → railorrail → leg_rolefor SingleLegRail by direction). The dot algorithm can then rank-layout the result — the user’s mental “roles → rails → roles” 3-rank reading falls out of dot’s DAG ranking deterministically, no force tuning, no knobs.The d3-force arm A’s per-rail emit (
to_d3_per_rail_json) drove the same model insight; this is the graphviz analog so the dot renderer can be re-evaluated against the layered reading the user wanted. Both emits share the bundling rule: pure-connectivity rails (TwoLegRails sharing exact source/destination role expressions AND SingleLegRails sharing leg_role/direction, with NEITHER referenced by any chain or template) collapse into one bundle node per group. Anchored rails (chain endpoints / template leg-rails) always stay individual since the sequencing/composition edges need stable rail identity.Templates render as clusters containing their leg-rail nodes; chains as dashed edges between rail/template nodes; control_parent as dashed edges between roles. Orphan roles (declared but unreferenced) are filtered at emit time so the dot layout stays focused on the connectivity story.
bundle_parallel_rails(default True) is the bundling switch; set False to render every rail as its own node (denser graph, occasionally clearer for low-rail-count instances).focus_node_id(optional) — when set, filter the diagram to that node’s “direct connections + complete rail” neighborhood (see_focus_set). Adjacency is computed over the FULL graph (so bundle IDs stay stable across full-vs-focused renders). Nodes / edges outside the focus set are skipped at emit time; dot re-lays out the smaller subgraph cleanly. Click-away in the chrome navigates back to the no-focus URL to restore the full picture.layer(1 / 2 / 3, default 3) — conceptual progressive disclosure of the model:1— roles + control hierarchy only (chart of accounts).2— adds rails + their endpoint connectivity.3— adds chains + transfer templates (the full diagram).
Implemented as a server-side filter so dot re-lays-out the smaller subset cleanly per layer (the same “click to zoom in, get a fresh layout” pattern the focus filter uses). Default 3 keeps Python callers (tests, etc.) seeing the full diagram unless they ask otherwise.
Returns a
graphviz.Digraphready for.render()or.sourceinspection. Typed asAnybecause thegraphvizpackage ships without type stubs.- Return type:
Any- Parameters:
instance (L2Instance)
db_table_prefix (str)
bundle_parallel_rails (bool)
focus_node_id (str | None)
layer (int)
- recon_gen.common.l2.topology.topology_graph_for(instance, *, db_table_prefix)[source]
Walk an L2Instance and return its typed topology projection.
Pure construction — no graphviz import, no rendering, no I/O. Both spike arms consume this single projection so the topology walk isn’t duplicated between renderers.
Iteration order matches the legacy
build_topology_graphwalk (roles sorted; templates in declaration order; chains in declaration order) so the graphviz renderer that consumes it produces the same DOT shape it always did.Z.C —
db_table_prefixis the cfg.db_table_prefix (formerly read off the droppedL2Instance.instancefield) and surfaces asTopologyGraph.instance_nameso the rendered diagram still carries the operator-facing prefix label.- Return type:
- Parameters:
instance (L2Instance)
db_table_prefix (str)
- recon_gen.common.l2.topology.visible_entities_for(instance, focus_node_id)[source]
Return the L2 entity IDs visible in a focused diagram subgraph.
Used by Studio’s home page (X.4.f.8) to filter the entity-card sections when the operator clicks a node in the diagram. The keys are the editor-route entity-kind slugs (
account,account_template,rail,transfer_template,chain,limit_schedule); the values are frozen sets of entity IDs in the same shape Studio’s/l2_shape/<kind>/<id>URLs use:account.id,account_template.role,rail.name,transfer_template.name;"<parent>::<child>"composite for chains and"<parent_role>::<rail>"composite for limit_schedules (matches_entity_idin_studio_editor_routes).
When
focus_node_idis None or the node ID is unrecognized (typo / stale URL / synthetic bundle id likerail__bundle_3that doesn’t have a matching individual rail), returns the FULL set per kind so the home page un-filters cleanly.Adjacency is built directly from
instance(rather than fromtopology_graph_for’s typed projection) so each Rail keeps its own role↔rail edges instead of being collapsed into a bundle edge — focusing on a single Rail must still pull in its endpoint roles even when several parallel rails share those roles.- Return type:
Mapping[str,frozenset[str]]- Parameters:
instance (L2Instance)
focus_node_id (str | None)