Skip to content

Diagram quality: the yardstick and the change plan

Companion of diagram-style.md, which fixes the look and the file formats. This file says when an exported diagram is good enough to ship, which defects the exporter has today and what to change. Base: commit 4e88b0a, branch main. The "Now" column comes from reading the code at that commit; nothing was run or rendered while writing it, so every ? and every [CODE] statement must be confirmed by the first run of the tests of section C.

Reference: "Networking account architecture" of the AMS multi-account landing zone (docs.aws.amazon.com/managedservices/latest/onboardingguide/malz-network-arch.html, image AMS_MALZ_NET_FLOW-2.png and its legend image). Keep copies outside the repository (icon licence, see diagram-style.md 6.2 item 10).

0. How to read this document

  • Tags: [SEE] seen on the reference image; [CODE] read in the source at 4e88b0a; [REC] recommendation. The tags of diagram-style.md keep their meaning. Where a number here differs from diagram-style.md (4.2), this file wins and 4.2 is updated when the constants change.
  • Checklist columns. Check: T = pytest on the Layout (tests/test_layout_quality.py, no draw.io needed), X = pytest on the exported XML or JSON (tests/test_exporters_style.py), R = render review (section C), D = dashboard test (tests/test_dashboard.py). Pri: P0 = a diagram that breaks it is not shipped; P1 = expected in this iteration, may be waived with a written reason; P2 = reference parity, later. Now: OK, FAIL, PART (partly met), NEW (not implemented), ? (needs a render to tell).
  • Extended rect of a box: a group is its own rectangle; an icon is its square plus its caption block, that is x from min(x, cx - 46) to max(right, cx + 46) and y from y to bottom + 2 + 14 * label lines + 12 * detail lines. Every overlap rule uses extended rects. Gap between two rects = the larger of the horizontal and vertical gaps (negative when they intersect).

Target page, left to right (Q14 to Q20):

 x=40            lane         account (tab, Region, VPCs)                   external accounts
                              +---------------------------------------+     +----------------+
                              | Region                                |     | External acct  |
 +----------+                 |     +-----------------------------+   |     | (dashed)       |
 | Internet |-------------------->(IGW) VPC                       |   |     | stub VPC       |
 +----------+                 |     |   AZ rows x role columns    |   |     | shared TGW     |
                              |     +-----------------------------+   |     +----------------+
 +----------+    +--------+   |                                       |
 | On-prem  |--->| DX/VPN |----------->hub strip:   [TGW]    [NFW]    |
 +----------+    +--------+   +---------------------------------------+

 Legend: below everything, left aligned at x=40

1. Constants (module-level in layout.py unless noted)

Name Now Target Note
GRID 10 10 draw.io grid
ICON / SERVICE 48 / 64 same resource glyph / service and general icon
CELL_W 96 110 caption block 92 + MIN_GAP 16; pitch of icons in a subnet row
CAPTION_W / CAPTION_CH 92 / 14 same one label line holds 14 characters at 12 px
LABEL_LINES / DETAIL_CH / DETAIL_LINES 3 / 18 / 2 (literals in the exporters) 2 / 18 / 1 named once, used by both exporters
LINE_H / DETAIL_H 14 / literal 14 / 12 caption height = 14 per label line + 12 per detail line
TEXT_PX none 6.5 width estimate per character at 12 px, for label-fit tests
PAD 20 30 child padding; the test threshold MIN_PAD is 24
MIN_GAP none 16 threshold between siblings
TOP / TOP_TWO / AZ_TOP 40 / 50 / literal 30 same, named title strips (one line, two lines, AZ)
GAP_AZ / GAP_COL / GAP_VPC / GAP_ACCOUNT 20 / 20 / 40 / 60 20 / 20 / 80 / 100 round 2: the gap between two rows of VPCs (80) and between two accounts (100) is a corridor for edges
GAP_HUB none 72 bottom of the VPC stack to the hub strip: room for a corridor
GW_INSET none 56 left inset of AZ bands when a gateway sits on the left border (46 caption half-width + 10)
LEFT_PAD 70 70 Region left margin (46 + 24)
EXT_LEFT_W / LANE_GAP / EXT_GAP literals 200 (80) / 120 / 160 200 (80 with only Internet) / 180 / 160 named; the lane holds a 64 px icon and a 92 px caption with 44 px each side
EXT_W 300 300 minimum grows with the title (Q19)
HEADER_H / LEGEND_W / LEGEND_GAP 80 / 300 / literal 40 120 / 300 / 40 named; round 2 gives the title band a corridor for edges below the title
FLOW_MARKER_SIZE / FLOW_MARKER_STEP / FLOW_MARKER_MARGIN (registry.py) literal 18 / none / none 24 / 28 / 12 flow disc size, distance between the centres of two discs, free space between a disc and a bend or an end
FLOW_MARKER_FILL / LEGEND_DISC_X (registry.py) none #232F3E / 42 the fill of the disc of a number below 1 (the numbers 1 to 7 take registry.FLOW_COLORS); distance from the left edge of the legend to the centre of a legend disc
GAP_VPC_H none 96 round 2: the gap between two VPCs of a row, room for a corridor of four tracks (Q20)
VPCS_PER_ROW / VPCS_PER_ROW_COMPACT / MAX_ROW_W / ACCOUNT_COLUMNS 2 / 4 / 1800 / 2 removed VPCs per row and accounts per row follow the aspect of the page (MIN_COLUMNS 2, ASPECT_TARGET 1.4, _pack and _grid)
CHANNEL_H / BOTTOM_H / WORKLOAD_MAX none 36 / 84 / 3 corridor under the Region title, corridor under the last row of VPCs, most workload icons drawn one by one in a subnet
EDGE_CLEAR / EDGE_SEP / STUB_LEN / LABEL_MAX none 24 / 12 / 12 / 24 router: nearest an edge runs to a parallel border, nearest two parallel edges run, first and last segment, longest label (LABEL_PX 6.2 and LABEL_H 18 size a label)
SERVICE_CLEAR / SERVICE_HALO none 8 / 12 R5c router: nearest an edge runs to a service icon or its caption it does not end in (less than STUB_LEN, so that a stub ends outside the fence of its own icon), distance of the extra tracks round each service icon (EDGE_SEP)
LABEL_CLEAR / LABEL_STEP / BORDER_STEP none 12 / 10 / 92 round 3: nearest a label sits to a title or a border (8 px plus 4 for the rounding of label_t), distance between the spots tried along a segment for a label, nearest two icons sit on one border of a VPC (ICON + 2 * LINE_H + 16)
LANE_W, HUB_W 110, 140 removed the right-hand hub column goes away (Q16)

A. Quality checklist

A.1 Containers

Q Rule Check Pri Now
Q1 Nesting is account (or external account), Region, VPC, AZ, subnet, icon: an icon in a subnet has exactly that parent chain (depth 5). Border gateways, hub icons and Region services hang from the Region; Internet, users and the on-premises box have no parent; external stubs hang from their external account. Every box has one parent. Emit order: a parent before its children, edges after every vertex, discs after edges. T P0 OK
Q2 Group styles equal registry.drawio_style(kind, profile). article [SEE]: account tab #232F3E with the white "aws" mark and a thin outline; Region dashed #147EBA with flag; VPC solid #4D27AA with cloud-lock icon; AZ dashed #147EBA, no icon, no fill; public subnet fill #E9F3E6, icon and label #3F8624; private subnet fill #E6F2F8, #147EBA; subnets have no outline (grStroke=0); on-premises and external boxes #7D8998, external dashed. White page, shadow="0", no group gradient, no rounded group. aws2024: Region #00A4A6, VPC #8C4FFF with label #AAB7B8, private subnet #E6F6F7 with #00A4A6, public subnet #F2F6E8 with #7AA116 and label #248814; the rest as article. X, R P0 OK
Q3 Every group except AZ and legend shows its corner icon top-left and its label to the right of it (spacingLeft=30, 12 px). VPC and subnet labels have two lines (name, then CIDR at 11 px); label colour = group colour; the AZ label is centred and reads Availability Zone <az id>. The font stays the draw.io default (Helvetica/Arial). X, R P0 OK
Q4 A label fits: TEXT_PX * longest line <= box.w - 40 (AZ: - 20). Longer text is cut with … (ids in the middle) and the full text stays in the cell metadata. A title never wraps over the content: the first child starts at y + TOP (TOP_TWO with a second line, AZ_TOP in an AZ). T P0 FAIL
Q5 Group x, y, w, h are multiples of GRID so edits in draw.io snap cleanly; centring uses half-differences rounded down to the grid (today (abox.w - inner_w) / 2 gives 45 for a one-column VPC, which is off the grid). T P2 PART

A.2 AZ rows and subnet columns

Q Rule Check Pri Now
Q6 AZ bands of a VPC are stacked top to bottom, sorted by AZ key, never side by side; all share x and width (VPC width minus 2 * PAD minus the left inset); gap GAP_AZ. [SEE] T P0 OK
Q7 Columns are fixed. Roles in the order public, private, isolated, other (ROLES gains other); one column per (role, slot) present in any AZ; the x and width of a subnet depend only on its column, so one role has the same x and width in every row; columns are GAP_COL apart and the grid is centred in the AZ. A missing cell stays empty (no placeholder), so a VPC with one subnet per AZ and a different role in each AZ looks like a staircase by design. T P0 OK
Q8 Subnets of one row share y and height (the tallest). Inside the AZ: title strip AZ_TOP, then the subnets, then PAD at the bottom and on both sides. [CODE] today the bottom padding is 10 (bh - 40 in place_vpc). T P0 FAIL
Q9 Icons in a subnet: rows of at most 3 cells of CELL_W, centred; width snap(max(170, cols * CELL_W + 40)); height TOP_TWO + measured row heights (Q13) + PAD. T P1 PART

A.3 Icons and captions

Q Rule Check Pri Now
Q10 Sizes are the registry sizes (resource glyph 48, service and general icon 64, aspect=fixed). Colours per profile: Networking glyph #4D27AA, service square gradient #4E26AC to #7B49D7 (aws2024: flat #8C4FFF); Compute #ED7100; Security #DD344C; general #232F3D; label colour #232F3E. X P0 OK
Q11 Caption: centred under the icon, at most CAPTION_W (92) wide: at most 2 label lines of 14 characters (12 px) plus at most 1 detail line of 18 characters (10 px, #5A6C86). An AWS id (a token starting with a prefix of graphview._ID_PREFIX) is never split: it goes whole to the detail line, shortened in the middle when longer than 18 (first 9, …, last 6). The full text stays in the metadata. T P0 FAIL
Q12 Label of a resource = Name tag, else the registry type label (TGW attachment, Internet gateway, NAT gateway, Load balancer; endpoints use the short service name), never a raw id. Detail line = the one useful scalar (NAT public IP, attached TGW id, ASN, LB scheme, endpoint id). [CODE] graphview._make_node falls back to the id for name. T P0 FAIL
Q13 One caption model: caption_lines(box) in layout.py returns (label lines, detail lines) and is the only wrapper; drawio, lucid, the block height and the router obstacles call it. Block height = icon + 2 + 14 per label line + 12 per detail line; icon pitch = block height + MIN_GAP. T P0 FAIL

A.4 Canonical placement

Q Rule Check Pri Now
Q14 Border gateways. The IGW is centred on the VPC's left border (abs(cx - vpc.x) <= 4), level with the centre of the first AZ band that holds a public subnet (the first band if none), parent = Region; the VGW stacks below it. A VPC with such a gateway insets its AZ bands by GW_INSET so the 92 px caption never touches an AZ. On a compact VPC (overview, vpc detail) the icon starts at vpc.y + TOP_TWO or lower and never intersects the 30 x 30 corner icon; the compact height grows to fit the gateways. Stack step = block height + MIN_GAP. Gateway endpoints (S3, DynamoDB) are centred on the bottom border. [SEE] T, R P0 FAIL
Q15 NAT gateway centred in its public subnet; one TGW-attachment icon (ENI-style glyph transit_gateway_attachment) in every subnet the attachment uses; firewall, resolver and interface endpoints in their subnets, grouped by kind. [SEE] T P0 OK
Q16 Hub strip: the TGW (64 px), Network Firewall and other Region services sit in one row inside the Region, below the lowest VPC: top = VPC stack bottom + GAP_HUB, centred on the VPC column, blocks 3 x MIN_GAP apart, PAD below. Never inside a VPC; no hub column at the right. [SEE] T, R P1 FAIL
Q17 Page columns, left to right: external column at x = 40 (EXT_LEFT_W) with Internet on top (never above HEADER_H, level with the first IGW when possible) and the on-premises box under it, centred on the icons it links; then a lane of LANE_GAP holding the DX and VPN icons, level with the hub strip they connect to so the link is one straight edge; then the accounts; then EXT_GAP and the external accounts. DX and VPN are outside every account and Region box. [SEE] T, R P1 PART
Q18 Load balancers: internet-facing in the first public subnet of its AZs (never in a private one when a public one exists); internal ones centred on the VPC's right border at the middle of the span of their subnets. Icon by type, caption = Name, detail = scheme. [SEE] T P1 FAIL
Q19 External (not audited) accounts: dashed account_external boxes in one column at the right, GAP_ACCOUNT apart, title External account <id> (<alias>) that fits (Q4), width max(EXT_W, 40 + TEXT_PX * title length). Stubs are stacked by block height + MIN_GAP; a vpc_stub is 240 x 100, dashed, CIDR on its second line; box height = sum of the blocks + TOP + PAD. T P0 PART
Q20 Reference arrangement: VPCs of a Region in rows of at most 2 (4 when compact), ordered by TGW then name, a gutter of GAP_VPC_H (96) plus 46 px for each border gateway that hangs in it, rows at most MAX_ROW_W (1800) px wide; the account that owns the TGW first and full width, the other accounts below it, ACCOUNT_COLUMNS (2) to a row. diagram-style.md 4.3 already asks for side-by-side VPCs; today there is one column of VPCs and one of accounts. [SEE] R P2 FAIL

A.5 Edges

Q Rule Check Pri Now
Q21 Every segment of Edge.points is axis-aligned; the first and last segment are at least 12 px (room for the head). The rendered polyline has the same vertices as Edge.points; if draw.io re-routes (edgeStyle=orthogonalEdgeStyle may treat waypoints as hints), drop edgeStyle on edges that carry waypoints. T, R P0 ?
Q22 Style = registry.edge_style(kind, profile): grey #6A6A6A (article) or #545B64, 2 px (association 1), rounded=1;arcSize=8, white label background. Heads, as in the reference (it draws flows): every edge but the association has a filled block head at its target (endArrow=block;endFill=1: link, flow, xaccount), the kinds that run both ways (peering, vpn, dx) have one at each end, and the association has none. Round 2 decision: an attachment is drawn with a head too. X P0 OK
Q23 An edge starts and ends on an icon whenever the resource has an icon on the page, on the side facing the other end. A container endpoint is allowed only where the page draws the resource as a container: compact VPC, VPC stub, external account. Never on a border of Region, AZ or subnet. [SEE] T P0 FAIL
Q24 TGW to VPC: one edge per attachment, from the attachment icon nearest to the TGW (Manhattan distance) to the TGW side facing it; the other AZ icons of the same attachment have no edge. T P0 FAIL
Q25 No segment crosses a group title strip. A segment may run through an icon or a caption it does not end in (ICON_COST 40 for an icon on a border, less than a bend, so a free track of the same length wins and nothing bends around an icon; WORKLOAD_COST 240 for an icon inside a subnet, more than a title strip, so a line crosses the title or bends once more before it seems to join the workloads it passes); the drawing cuts the line at the outline of the icon block (behind.pieces: the first piece keeps the tail and the source, the last the head and the target, the rest take the id with ~p2, ~p3...), and an icon outside a subnet gets a white backing (<id>~bg) and a white caption background, so that the line of the container it sits on stops at it too. A container never opens a stub behind an icon on its border. Crossing a container border is fine at 90 degrees and 12 px from a corner. Parallel segments are at least EDGE_SEP (12) px apart and no edge runs closer than EDGE_CLEAR (24) px to a parallel border of a container it does not end in; collinear overlap of different edges is 0 px. Tracks also run inside the subnets and the Availability Zones (Q46) and two edges never meet at a bend (Q47). When no clean route exists the router logs it and records it in Page.issues. T P0 ?
Q26 Label: at most 24 characters (ids shortened as in Q11), 11 px, white background, centred on the longest segment (Edge.label_t; draw.io geometry x = 2 * t - 1; Lucid position = t), never over an icon or caption. Plain attachments have none. T, X P1 FAIL
Q27 Peering: dashed 8 4, filled heads at both ends, label = pcx- id plus the state when it is not active. X P0 OK
Q28 A link between resources of different accounts has kind xaccount: dash-dot (dashPattern=12 3 2 3), 2 px, a filled head at the target, label carries the last 4 digits of the other account, own legend row. Lucid has no dash-dot: it uses dashed (documented loss, unverified). X P1 NEW

A.6 Flow numbers (automatic, by kind of traffic)

Q Rule Check Pri Now
Q29 Flows are derived from the kind of edge, never read from a file (no --flows option, no input to ship). The layout numbers the kinds of traffic a page draws in this fixed order, and only those; a kind has the same number on every page, so a page may skip numbers: 1 Internet ingress (Internet to IGW), 2 NAT egress (private subnet to NAT gateway to IGW; when the page has no explicit edge for the NAT path, the disc rides the Internet-to-IGW edge of the VPC that has a NAT gateway and its legend row says so), 3 Transit Gateway attachment (attachment to hub, cross-account attachments included), 4 Direct Connect / VPN (on-premises to DX gateway or VGW to hub), 5 VPC peering, 6 shared prefix list / RAM (the stubs of external accounts). Every edge of a kind carries Edge.flow = [number]; an edge that serves several kinds lists them all, in number order, and gets one disc each. A page with no such edge has no disc and no legend block. T P2 NEW
Q30 Disc: a separate vertex of FLOW_MARKER_SIZE x FLOW_MARKER_SIZE (24), no connector, style exactly registry.flow_marker(n) (badge_style is its old name): ellipse;whiteSpace=wrap;html=1;aspect=fixed;fillColor=#232F3E;strokeColor=none;fontColor=#FFFFFF;fontStyle=1;fontSize=12;align=center;verticalAlign=middle; with the fill of the flow (registry.badge_color(n), from registry.FLOW_COLORS, as the reference colours its seven discs; the dark registry.FLOW_MARKER_FILL of the example is for a number below 1). One disc per number per edge: centre on the polyline (within 1 px) on the longest segment at t = 0.5 (0.25 when the edge has a label, which takes the middle) and a set of its own at the middle of every other segment of at least FLOW_SEGMENT_MIN (160) px, FLOW_MARKER_STEP (28) between the centres of two discs (shift along the segment), FLOW_MARKER_MARGIN (12) from bends and endpoints, parent 1, emitted after all edges, id <edge id>~b<number> (<edge id>~b<number>.<order> for the later discs of a number). Lucid: same id, centre and size, a rectangle with rounding 24, fill and outline in the colour of the flow, the digit in a bold white 9 pt paragraph (the 12 px of draw.io). A disc never covers an icon, a caption or a label: a set slides along its segment to the nearest place clear of every icon square and caption block of the page (behind.icon_blocks, FLOW_DISC_GAP of air), an extra set with no such place is dropped, the first keeps its middle when there is none (Q43). T, X, R P2 ?
Q31 The legend gets a "Traffic flows" block when the page has numbered edges: a heading row, then one row per number in use, in number order: a disc (24 px, centre LEGEND_DISC_X = 42 px from the left edge of the legend, the column of the swatches) and the title, at most 60 characters (Internet ingress, NAT egress, Transit Gateway attachment, Direct Connect / VPN, VPC peering, Shared prefix list / RAM). The layout writes a row as a swatch and a text row that both carry meta["flow"] = the number (the text starts with a circled digit, as layout._add_legend does), or as one text box inside the legend whose label is "<number> <title>" (meta["marker"] is a synonym of flow), or as a swatch whose label is only the number. registry.legend_discs settles the rows for both exporters: the swatch is the disc when there is one, otherwise the text row gets a disc of its own, and the text row keeps its title without the number. Both draw the same disc: in draw.io a child of the legend (id <row id>~b<number>, or the swatch's own id), in Lucid a rounded rectangle at the same centre. Never a sample line. The rows count in the height of Q32. The discs of the legend sit on their rows, so the avoid rule of Q30 does not apply to them. T, X P2 PART

A.7 Legend and title

Q Rule Check Pri Now
Q32 Legend at x = 40, y = lowest point of everything else (boxes, edge points, discs) + LEGEND_GAP; width LEGEND_W, plus a second 300 px column beyond 12 rows; footer lines wrapped at 40 characters (the accounts list too); height from content (36 + 28 per row + 16 per footer line + 20); only kinds in use. Disjoint from everything by at least 40 and counted in the page size. T P0 ?
Q33 Title at (40, 16), 48 high: 16 px bold title, then accounts, UTC stamp and version. Nothing else (boxes, edges, lanes) above HEADER_H - 8. [CODE] an edge lane runs at HEADER_H - 14. T P1 PART

A.8 Spacing and overlaps

Q Rule Check Pri Now
Q34 Siblings (same parent; top-level boxes among themselves): gap between extended rects >= MIN_GAP (16), except a straddling icon (Q14, Q18) against its own VPC. Compact VPCs of a row are stretched to the tallest VPC of the row (Q48); the row keeps its height unless it grows to match the same row of the Region beside it. T P0 FAIL
Q35 A child's extended rect lies inside its parent inset by MIN_PAD (24) left, right and bottom, and below the title strip. Exempt: border gateways, internal load balancers, gateway endpoints (Q14, Q18) and the group's own corner icon. T P0 FAIL
Q36 Two boxes with no ancestor relation never intersect (extended rects), titles, legend and external stubs included; border gateways may touch only their own VPC and Region. T P0 ?
Q37 Every number of this file is a named module constant (section 1); no bare 120, 160, 30, 40 left in _assemble or place_*. code review P1 FAIL

A.9 Parity

Q Rule Check Pri Now
Q38 Same graph, same stamp, same options give byte-identical output, also in two processes with different PYTHONHASHSEED. Changing the profile changes styles only, never a coordinate. X P0 ?
Q39 draw.io and Lucid of one page: same box ids and bounding boxes (0.1 tolerance), same caption text (tags stripped), same line count, endpoints and waypoints, same label text, same discs and colours, same page size; Lucid JSON <= 2 MB and <= 20,000 px per side. X P0 PART
Q40 The dashboard viewer loads the XML that the download serves: same exporter, same parameters, identical bytes. Both call drawio.render(graph, scope) with the defaults (profile article, detail subnet); any future option is passed by the CLI and the dashboard alike or appears in the file name. GET /api/diagrams/<key> field xml == body of GET /export/<key>.drawio == drawio.render(graph, scope); the all file holds the same <diagram> blocks byte for byte. D P0 PART
Q41 Every shape=, resIcon= and grIcon= value mxgraph.aws4.<name> in the output is a name that sections 2.4 and 2.5 of diagram-style.md list, and the registry only uses listed names (test_registry_shapes_are_listed_in_the_style_guide, test_drawn_styles_use_listed_shapes_and_the_profile_edge_colour). A new name is also searched for in the bundle of the installed draw.io app (a made-up name must not be found) and appears in the render of C.3 with no blank square. X, R P0 PART
Q42 Lucid Standard Import: the sublabel of a route table or security group card (kind card) is set in Courier New and the title in the usual family, so the rows padded with U+00A0 line up in columns; no other shape changes its font (_para(..., family=), _CARD_FONT). X P1 ?
Q43 No flow disc lies on an icon square or a caption block: drawio.marker_discs(edge, boxes) hands behind.icon_blocks(boxes) to registry.flow_marker_centers(..., avoid=), and layout._disc_rects builds the same rectangles from ext_rect, so the labels keep away from the places where the discs really are. The flow numbers keep their order along the discs (registry.flow_numbers, repeated per set). draw.io and Lucid draw the same ids and centres. When the longest segment has no free place, the first set moves to a shorter segment or a finer step (registry._first_set, FLOW_MARKER_FINE) before it keeps its middle; a short edge between two lane icons is such a case. T, X P1 ?
Q44 layout_of keeps one layout per graph and level (account, vpc, all): alternating account and VPC renders of one graph (the dashboard scan, the CLI with --level all) builds each level once. A change of profile, detail, timestamp or size of the graph still rebuilds the slot of that level. T P1 ?
Q45 The legend of a VPC page says "Changes since " like the account pages: build_layout passes previous and since to vpc_level.build_vpc_pages, which names the older scan with since, else the generated_at of previous. T P2 ?
Q46 Tracks inside subnets and zones: layout._lanes adds to the router grid the strips of a subnet that are free of icons (between its icon cells, so also beside the first and last icon of a row; between two rows; LANE_TOP 5 px above the first row, under the title strip), the margin between neighbouring subnets and the margins below and between the zones. A stub that leaves an icon in a subnet turns there and leaves by a margin, so no edge runs through a workload icon or caption (an icon inside a subnet) it does not end in. The EDGE_CLEAR bands (near) stay on the frames of _BOX_GROUPS. T P0 ?
Q47 Two edges never meet at a bend: _Router._clash counts a segment that starts or ends less than EDGE_SEP (12) beyond the end of a parallel segment of an earlier edge, as it counts an overlap (forbidden at relax 0, SHARED_COST after, and reported in Page.issues then). No bend of an edge is within EDGE_SEP of a segment of another edge, along it or past its end, unless the two edges share an end box. T P0 ?
Q48 Level peering icons: the compact VPC boxes of a row are as tall as the tallest VPC of the row (region_geom; the row height does not change), and the Regions that sit side by side in a row of accounts line up their rows of VPCs down to the last row with a compact VPC that has a peering (_row_floors, _grid), so _level finds a height free on both facing borders. On the MALZ Global page the two icons of every peering share their centre (0.5 px) and the line has two points. T P1 ?
Q49 Workloads in a subnet (account level): after the gateways and endpoints, one icon per kind present with the service icon and a caption: instance groups (instance, "n instances", counted per subnet from the interfaces that name the instance, the rest spread over the subnets of the group), ECS services (fargate_service for launch type FARGATE, else ecs_service, "n tasks"), EKS (eks_cluster, version), RDS (aurora for the aurora engines, else rds, engine), ElastiCache (engine), EFS (efs, one per mount-target subnet), Lambda in the VPC (lambda, runtime). Several resources of one kind in one subnet become one icon, "N services" (count and names in the metadata), as _place_workloads does. The interfaces these icons stand for (an instance of a group, an RDS or Lambda interface) are not drawn again and still count against workload_count. [CODE] layout._service_items, _covered. T P1 ?
Q50 An edge label never sits on a segment of another edge: _Router.place_labels makes the segments of every edge but its own obstacles, first grown by EDGE_SEP, then by a pixel (_edge_rects, _Index.meet_other); a label with no such place moves to meta["detail"]. T P1 ?
Q51 A flow disc keeps FLOW_MARKER_SIZE (Chebyshev distance) from every crossing of its edge with another: layout._mark_crossings sets Edge.crossings after routing, registry.flow_marker_sets(..., crossings=) adds a FLOW_MARKER_SIZE square around each to avoid, and flow_marker_centers(edge) and flow_discs read them from the edge, so draw.io, Lucid and the labels agree. The first set is never dropped: when the longest segment has no place clear of icons and crossings it tries the shorter segments and the finer steps, then settles for a place clear of the icons alone (a disc on an icon hides it, one near a crossing only crowds it), then of the crossings alone, and last keeps its middle (registry._first_set). T P1 ?
Q52 A stub on the border of a container (VPC, account) whose other end is an icon lines up with the coordinate of the icon's centre (y on the left and right borders, x on the top and bottom ones) when that coordinate lies at least PORT_INSET (8) px inside the border: _Router._aim, _ports(box, other). The line is straight; otherwise the old centre of the border is used. On the MALZ Global page the stub of the Application VPC towards the Transit Gateway has no leg followed by a parallel one within 40 px. T P1 ?
Q53 A horizontal lane inside an Availability Zone is at least EDGE_CLEAR (24) px above the top border of the subnets beneath it, else _lanes drops it and the router uses the lane of the band above (_crowded_strips). T P2 ?
Q54 VPC level, services: each resource is one icon in every subnet it has an interface in (_service_items(group=False)): instance group (name, "n instances"), ECS service (name, "n tasks"), RDS (identifier, engine), ElastiCache, EFS mount target, Lambda; nothing is grouped, and the interfaces they stand for are left out of the workload icons. T P1 ?
Q55 VPC level, targets: a target group is a card (card "target_group": protocol:port (target type), healthy and unhealthy counts, health check path, its balancers, at most TG_TARGETS (8) targets and "+N more") in the right panel beside the route table cards, with a legend row. The balancer joins each instance group and ECS service registered in the group (the targets relations of the linker) by a link edge labelled protocol:port; the edge of an internet-facing balancer carries flow 1 (_balancer_flow), that of an internal one carries none. T P1 ?
Q56 Edge lane (service layer): a Region page with services draws a lane under the title of each Region frame, left to right: Internet users (users), Route 53 (route53), CloudFront, the web ACLs that no CloudFront carries, API Gateway, then a certificate or Shield that nothing carries. The icons share one centre line (service_rows.LANE_ICON), cells are LANE_PITCH (96) apart, so that the edge between two cells has a place for its disc clear of the edges that drop through the gap, and the VPCs start CHANNEL_H below the lane (region_geom adds the band to the Region). A global service (CloudFront, Route 53, a CLOUDFRONT web ACL, Shield) goes to the Region where its origin or target lives, else to the first Region of its account (ServiceModel.home). T P1 ?
Q57 Every service node of the account appears exactly once on its Region page: in the lane, in a row, as a badge, or inside a VPC (the last is the subnet filling's job). Several nodes of one kind fold into one icon whose meta["members"] lists their ids separated by commas (as every folded icon does, so that a diff marks it) and meta["keys"] their graph keys separated by a vertical bar; a folded icon is the anchor of each member key, so an edge can end at any of them. A Lambda with a vpc_id and every VPC-bound kind are not drawn by the rows. T P0 ?
Q58 Badges: a WAF web ACL that protects a distribution, Shield, and an ACM certificate (of CloudFront, of API Gateway, or of the balancer behind a web ACL) are 32 px icons with a one-word caption (WAF, Shield, ACM) in a column one MIN_GAP to the right of the service they belong to, stacked BADGE_GAP apart, never nodes of their own; they carry meta["badge"]. The legend says Badge: WAF, Shield, ACM when the page has any. T P1 ?
Q59 Rows: the regional row (only the kinds present, in this order: Lambda without VPC as one icon with a count, referenced S3 buckets one by one up to S3_MAX (4) then the S3 icon with "N other buckets", DynamoDB, ECR, KMS with counts) sits BAND_GAP below the lane unless the two fit on one line; the account row (CloudTrail, GuardDuty, Config, CloudWatch, each with its key attribute as the detail line: trails, enabled, recorder on, alarms) joins the band of the last Region of the account. The band packs the lane, the regional row and the account row, in that order, in lines of at most BAND_MAX_W (1500) px with BAND_BLOCK_GAP (72) px between blocks (service_rows.band_plan); a block that does not fit starts the next line, BAND_GAP lower. Stacked as three full-width bands they made the MALZ Region page a column (3290 x 4210 px, R1). Icons of a row are ROW_PITCH apart. Region frames side by side in a row of accounts get the same band height, so their VPCs stay level (Q48). T P1 ?
Q60 Service flows: ingress through the CDN, API traffic and database access are numbered after the seven network flows (8, 9, 10; registry.SERVICE_FLOW_BASE + SERVICE_FLOWS), so a digit never means two flows on a page, and wear the SERVICE_FLOW_COLORS (registry.flow_fill, used by flow_marker and the legend swatch). Edges of the service layer are serves, origin, protects, integrates and, from the users, flow; they are routed like any edge, page.issues stays empty, and the legend lists the service flows and the service edge kinds present, no others. T P0 ?
Q61 The Global page keeps the Region frames compact: no lane and no regional row inside them. The global services (Route 53, CloudFront with its badges) of every account form one lane above the accounts, GAP_HUB clear of them, and each account keeps its account row (summed over its Regions), in the band of its last Region frame. T P1 ?
Q62 An API VPC link is a border icon of its VPC (right border, as high as _free_centre allows, after the balancers and before the peerings); the API Gateway of the lane reaches it by an integrates edge and it reaches the internal balancer (or the function) by another. A link whose VPC is not on the page joins the regional row. T P1 ?
Q63 Service icons and their captions are obstacles for the router, on the Region pages and on the Global page. service_rows records every icon it draws (lane, rows, account row, badges) in _PageBuilder.service_ids, _route_all hands them to _Router(service=...), and the router fences the icon square and the caption block of each, grown by SERVICE_CLEAR (8, less than STUB_LEN), so that no edge segment crosses a caption, the captions of its own ends included: the attachment edge of the Global page ran through "enabled", the detail line of GuardDuty (R5c, D1). T P0 ?
Q64 No edge runs behind a service icon it does not end in, in the lane or in a row (R5c, D2): the same fence keeps the line itself, not only the pieces behind.pieces cuts, off every icon and caption of the service layer, and the tracks SERVICE_HALO (12) px outside each block (_halos) give an edge a way past, between two icons and above and below a row. A route that has none falls to the relax levels of a title and is reported in Page.issues. The unit test shows both sides: an icon outside the service layer is still run through (C1), one inside is gone round. T P0 ?
Q65 The edges of the lane and the band are routed like every other edge, by _route_all and the one _Router of the page (R5c, D3: service_rows does not route anything), so _clash applies to them. No two edges that touch a service icon run along each other nearer than EDGE_SEP, and no bend of one lies within EDGE_SEP of a segment of the other, pairs that share an end box included (Q47 leaves those out). T P1 ?
Q66 Ports: the edges that end or start on one side of an icon are never nearer than EDGE_SEP to each other (R5c, D4). The first edge takes the middle of the side; each next one takes the point farthest from the ports already there (_Router._spread), so two ports are h/2 - PORT_INSET apart, the second at the far end of the side: 16 px on a 48 px icon, 24 on a 64 px one (at 12 px the arrow heads of an open and an oval end touched). A side with no such point is full and the edge takes another side; containers keep the slots of _peek. T P1 ?
Q67 A count in a caption takes the plural of its noun (service_rows._plural): a y after a consonant becomes ies, s, x, z, ch and sh take es, the rest s: "6 repositories", "5 keys", "17 alarms", "4 other buckets" (R5c: "6 repositorys"). T P2 ?

B. Defects, rules and code to touch

D1 to D7 were seen in a render of the fixture tests/atlas_fixtures.py (profile article); D8 onwards were found while reading the code. Causes marked [CODE] are read, not rendered.

# Defect Cause Rules Touch
D1 The stub org-hub (shared TGW) of the external account overlaps the external VPC container [CODE] _place_external and _ext_size stack stubs by _cell_h(size) = size + 48, but a caption can need 3 label lines plus 2 detail lines; by the layout's own numbers (64 px icon, then 14 + 2 x 12 px of caption) the TGW caption ends about 8 px above the next stub, under MIN_GAP, and draw.io line heights are larger Q13, Q19, Q34, Q36 _ext_size, _place_external, _cell_h (replace by a measured block height), wrap_caption
D2 The TGW to VPC edge ends on the VPC border, not on the attachment icon [CODE] _links emits Link(Ref(<vpc id>), tgw); _assemble resolves anchor[<vpc id>], the VPC box. _PageBuilder.add keeps only the first box per key (setdefault) Q23, Q24 Link (new via = attachment id), _links, _PageBuilder.add and icon (anchors as lists), _assemble (pending), _Router._peek and _side (stagger limited to the icon side)
D3 The attachment icon in the private subnet has a badly split two-line label [CODE] label = att.name, which is the id when untagged; wrap_caption cuts after hyphens (tgw-attach- / 0main); the detail line is the TGW id Q11, Q12, Q13 build_plan (attachment Item), wrap_caption, registry.KindSpec (+ type_label), drawio._caption, lucid._Writer._caption
D4 AZ rows do not read as a grid [CODE] the columns are already per (role, slot) with identical x and width in every row (vpc_geom, place_vpc); the sample has public-a in one AZ and private-b in another, so each row shows one cell. Real defect: bottom padding of an AZ is 10 px Q7, Q8 place_vpc (bh - 40), vpc_geom (band_h), the MALZ fixture as the grid control
D5 No flow numbers [CODE] Edge.flow is never assigned and a legend row cannot wear a disc; registry.flow_marker and both exporters already draw the discs of the edges Q29, Q30, Q31 layout.py (Edge.flow by kind of traffic, _add_legend rows), registry.legend_discs, drawio._row, lucid._Writer.shape
D6 The legend overlaps the diagram when it grows [CODE] not reproducible by reading: _assemble puts it at bottom + 40 after routing. Likely: footer lines (accounts list) overflow the 300 px box, and anything added after the call (discs, lanes) is not counted Q32, Q36 _add_legend (wrap, measure), _assemble (call last, include discs)
D7 On the Global page the IGW icon covers the VPC corner [CODE] compact VPC: gy = vbox.y + 10 and centre vx - 12, so the icon spans vx - 36 .. vx + 12 by vy + 10 .. vy + 58; the corner icon is vx .. vx + 30 by vy .. vy + 30 Q14, Q35 place_region (gy, centre), vpc_geom (compact height)
D8 Hub (TGW, firewalls) in a column at the right [CODE] region_geom, place_region: hx = rx + LEFT_PAD + vpc_w + LANE_W + HUB_W / 2 Q16 region_geom, place_region, LANE_W, HUB_W
D9 DX and VPN sit in the hub column, far from the on-premises box [CODE] _HUB_ORDER, build_plan.hub; the on-premises box is at the far left, so its link crosses the Region Q17 _assemble, new lane placement, region_geom
D10 Three caption models [CODE] _caption_lines counts label lines + 1; drawio._caption wraps the detail to 2 lines; lucid._Writer._caption keeps its own copy and a 14 px detail line Q13, Q39 layout (new caption_lines), both exporters
D11 Several gateways on one border step by a fixed 88 px [CODE] place_region: gy += _cell_h(ICON) - 8 Q14, Q34 place_region
D12 Edge labels sit at the path midpoint, not on the longest segment [CODE] drawio._edge sets no label position; lucid._Writer.line uses 0.5 Q26 Edge (new label_t), both exporters
D13 A load balancer goes to the first subnet by AZ order whatever its scheme [CODE] build_plan: put(sids[0], ...) Q18 build_plan, place_vpc (right border)
D14 Cross-account links look like in-account links [CODE] no kind for them in registry.EDGE_KINDS Q28 registry (EDGE_KINDS, EDGE_TITLES, _EDGE_TAIL), lucid._END_STYLES, lucid._LINE_STYLE, _links
D15 The router keeps a crossing route silently [CODE] _Router.route keeps the cheapest candidate even at cost >= 1000 Q25 _Router.route, Page.issues
D16 The dashboard all file is re-serialised [CODE] scan.merge_drawio parses and writes each page with ElementTree; the per-page endpoints are already identical Q40 dashboard/scan.py (build all with drawio.render(..., all_pages=True))
D17 Icons in a subnet row are 4 px apart [CODE] CELL_W 96 against CAPTION_W 92 Q9, Q34 constants

C. Verification protocol

C.1 Order of work

  1. Write or extend the tests and run them on the base commit in a throwaway worktree (git worktree add ../vpcatlas-base 4e88b0a, copy the tests in). Every test that covers a defect of section B must fail there. A test that passes on the base does not measure that defect: rewrite it.
  2. Make the change. 3. make lint test. 4. Render and look (C.3). 5. Only then mark the package done.

C.2 Automated tests

Test Rules Guards against
tests/test_layout_quality.py, on the sample and MALZ graphs at detail vpc and subnet: test_siblings_keep_their_distance, test_children_fit_their_parent_with_padding, test_unrelated_boxes_never_intersect Q34 to Q36 D1, D11, D17
same file: test_subnet_columns_are_aligned_in_every_vpc, test_subnet_detail_draws_one_band_per_zone (the control that the grid code is right) Q6, Q7 none expected
same file: test_internet_gateways_straddle_the_left_border_of_their_vpc Q14 D7, D11
same file: test_every_edge_joins_two_icons, test_each_attachment_has_one_edge_from_its_own_icon, test_edges_are_orthogonal_polylines Q21, Q23, Q24 D2
same file: test_an_id_is_shortened_not_split, test_a_caption_block_counts_label_and_detail_lines Q11, Q13 D3, D10
same file: test_title_and_legend_stay_clear_of_the_diagram Q32, Q33 D6
same file, round 3: test_a_peering_between_neighbours_is_a_short_direct_line, test_border_icons_of_a_vpc_keep_their_distance, test_edge_labels_keep_8_px_from_titles_and_borders Q26, Q27, Q34 none expected
same file, round 5: test_no_edge_runs_through_a_workload_it_does_not_end_in, test_two_edges_never_meet_at_a_bend, test_the_two_icons_of_a_peering_sit_level_on_the_malz_global_page Q25, Q34, Q46, Q47, Q48 none expected
same file, service layer: test_the_edge_lane_runs_left_to_right_above_the_vpcs, test_every_service_node_of_the_application_account_is_drawn_exactly_once, test_badges_sit_beside_the_service_they_belong_to, test_the_regional_row_and_the_account_row_hold_only_the_kinds_present, test_the_service_flows_are_numbered_after_the_network_ones_and_the_legend_lists_them, test_the_service_edges_are_routed_without_issues, test_the_sample_graph_has_no_service_flow, test_the_global_page_draws_the_global_services_in_a_lane_above_the_accounts, test_the_api_vpc_link_sits_on_the_right_border_of_its_vpc Q56 to Q62 none expected
same file, round 5c: test_no_edge_segment_crosses_the_caption_of_a_service_icon, test_no_edge_runs_behind_a_service_icon_it_does_not_end_in, test_the_router_runs_round_a_service_icon_in_its_way, test_no_two_service_edges_share_a_segment_or_a_corner, test_ports_on_one_side_of_an_icon_keep_their_distance, test_two_edges_ending_on_one_side_of_an_icon_fan_out, test_a_count_in_a_caption_takes_the_plural_of_its_noun, test_the_regional_row_says_repositories Q63 to Q67 the attachment edge through "enabled", a line hugging the S3 icon, ports 12 px apart on the balancer, "6 repositorys"
same file, flows: test_flows_follow_the_type_of_traffic_and_the_legend_lists_them, test_every_page_numbers_known_flows_and_lists_exactly_those, test_two_builds_of_one_graph_give_the_same_flows_and_labels Q29, Q31 D5
tests/test_exporters_style.py, shapes and styles: test_registry_shapes_are_listed_in_the_style_guide, test_every_kind_has_a_style_of_its_family, test_group_colours_follow_the_profile, test_workload_and_directory_icons_have_aws4_styles, test_drawn_styles_use_listed_shapes_and_the_profile_edge_colour Q2, Q3, Q10, Q41 none expected
same file, edges and discs: test_edge_styles_carry_the_required_attributes, test_edge_kinds_differ_by_dash_pattern, test_edge_label_sits_where_the_layout_says, test_flow_markers_wear_the_colour_of_their_flow_and_a_white_digit, test_flow_marker_centres_sit_on_the_longest_segment, test_drawio_draws_one_disc_per_flow_after_the_edges, test_lucid_draws_the_same_discs_in_the_same_places, test_each_edge_gets_one_disc_per_flow_number_in_both_formats, test_a_label_sits_at_label_t_in_both_formats Q22, Q26 to Q28, Q30 D12, D14
same file, legend discs: test_legend_rows_of_the_flow_block_wear_a_numbered_disc, test_the_landing_zone_legend_has_a_disc_row_for_every_flow_its_edges_carry Q31 D5
same file, disc placement: test_a_disc_slides_off_an_icon_or_a_caption_on_its_line, test_no_flow_disc_covers_an_icon_or_a_caption_on_any_page Q30, Q43 D5
same file, metadata and nesting: test_cell_metadata_order_and_cidr, test_the_full_name_and_detail_travel_in_the_metadata, test_every_cell_is_an_object_with_its_metadata, test_cells_hang_from_cells_that_exist Q1, Q4 none expected
same file, parity: test_both_exporters_show_the_same_caption, test_lucid_lines_match_the_drawio_edges, test_drawio_and_lucid_agree_on_ids_boxes_lines_and_page_size, test_the_model_declares_the_size_of_its_page, test_a_lucid_page_of_the_landing_zone_stays_within_the_import_limits Q13, Q39 D10
same file, stability: test_the_same_graph_gives_the_same_files (two builds in one process stand for two runs with another PYTHONHASHSEED), test_profiles_share_geometry Q38 none expected
tests/test_vpc_level.py, legend and cache: test_the_legend_of_a_vpc_page_names_the_older_scan_like_the_account_pages, test_alternating_levels_of_one_graph_builds_each_layout_once Q44, Q45 D16
tests/test_dashboard.py: test_api_diagram_xml_is_the_download, test_dashboard_draws_what_the_library_and_the_cli_export, test_multi_page_file_has_one_diagram_per_page_in_order Q40 D16

No test yet for Q5, Q8, Q9, Q12, Q15 to Q20, Q25 and Q37 (code review). The next ones to write are the parent chain of Q1 (account, Region, VPC, AZ, subnet, icon) and the router report of Q25 (Page.issues).

C.3 Render and look

Render through the command line, the route a user takes (vpcatlas diagram), not through the library.

cd ~/src/vpcatlas
make render-malz        # MALZ graph to .build/malz-graph.json, then .build/malz/diagrams/vpcatlas-<scope>.drawio and .lucid
mkdir -p .build/sample
PYTHONPATH=tests python -c "
from atlas_fixtures import make_sample_graph
open('.build/sample/graph.json', 'w').write(make_sample_graph().to_json())"
python -m vpcatlas.cli diagram --cache .build/sample/graph.json -o .build/sample
xmllint --noout .build/*/diagrams/*.drawio
D=/Applications/draw.io.app/Contents/MacOS/draw.io
for f in .build/*/diagrams/*.drawio; do      # each file holds one page, so -o can name the file
  # scale 2 only while width*height*4 stays under ~24 million pixels: past that draw.io desktop
  # silently cuts the bottom of the bitmap (measured on the MALZ region page, 3130x2820 at scale 2)
  "$D" -x -f png -s 2 -a -b 20 -o "${f%.drawio}.png" "$f"
  "$D" -x -f svg -a -b 20 -o "${f%.drawio}.svg" "$f"
done
sips --resampleWidth 1196 .build/malz/diagrams/vpcatlas-eu-west-1.png --out .build/cmp-malz.png   # width of the reference image

Open every PNG and look at it; an exit code of 0 proves nothing. Repeat the MALZ render for the aws2024 profile through Python (export(make_malz_graph(), 'drawio', out, name='malz-aws2024', profile='aws2024'), the command line has no style option). Compare with the reference image:

Look at Reference [SEE] Rules
Page white, no shadows, plain sans-serif, generous margins Q2, Q3, Q35
Account thin grey outline, navy tab with the white "aws" mark, name beside it Q2
Region and VPC blue dotted outline with flag; purple thin outline with cloud-lock icon, purple name and CIDR Q2, Q3
AZ stacked bands, dashed blue, centred "Availability Zone ..." Q6
Subnets public green, private blue, lock icons, same columns in every band Q7, Q8
Icons purple Networking glyphs, captions centred, two lines at most, nothing touching Q10, Q11, Q34
IGW centred on the VPC's left border, caption clear of the AZ; on the Global page clear of the corner icon Q14
TGW and hub larger purple square below the VPCs, inside the Region Q16
Left side Internet on top, on-premises box below, DX and VPN in the lane, none inside an account Q17
Arrows grey, 2 px, orthogonal, end on icons, never through a caption, same path as the polyline Q21 to Q25
Discs dark disc and white digit as in Q30, one per edge and number, explained by the legend rows of Q31 Q30, Q31
Legend and title outside the drawing, nothing overlapping Q32, Q33
External accounts dashed boxes at the right, stubs not touching Q19

Text checks on the SVG (case-insensitive grep -io): the count of E9F3E6 fills equals the public subnets of the page, E6F2F8 the private ones, 6A6A6A is at least the number of edges. Shapes: render one page with a cell per registry.KINDS entry (style from drawio_style); no cell may be an empty square.

C.4 Instruments that can mislead

  • The desktop app and the dashboard viewer (viewer.diagrams.net) are different builds: sign off with the desktop render, then open one page in the dashboard.
  • Label widths depend on the installed fonts; TEXT_PX is conservative, so trust the render for borderline cases.
  • Only the MALZ fixture has 3 AZs, several accounts, load balancers and a DX gateway; it has no VPN, VGW, endpoints or flows (no fixture covers those yet) and its attachments are Name-tagged. The sample fixture is sparse. A green run on one fixture alone proves little.
  • Every text or pixel check needs a negative control (run it on the base render, where the property is missing) and a positive control (the MALZ fixture).

C.5 Sign-off

All P0 rows OK; every waived P1 or P2 row listed with a reason; PNGs of the two fixtures (global and Region pages, profile article) looked at; tests green; one dashboard page opened and compared with its download.

D. Change plan (work packages)

One package per pull request, in this order. A package is done when its tests pass and its renders were looked at.

WP Scope Touch Closes Accept (rules)
W0 The yardstick first: the geometry checks as tests on the Layout in tests/test_layout_quality.py (no draw.io needed), the exporter checks in tests/test_exporters_style.py, the dashboard checks in tests/test_dashboard.py, and two fixtures: the sample graph in tests/atlas_fixtures.py (sparse) and tests/fixtures/malz.py (make_malz_graph(): shaped like the AMS diagram, three accounts, DMZ and egress VPCs over 3 AZs, TGW, DX gateway, load balancers, peering, an external account), rendered with make render-malz. A third, stress fixture for what the other two lack (an untagged TGW attachment, one VPC with both an IGW and a VGW, a subnet with three endpoint icons, a VPN with a customer gateway, a firewall endpoint, an isolated subnet, long VPC and account names) is still to write (E.7) tests, tests/fixtures/malz.py none on the base commit the new tests fail for D1, D2, D3, D7, D11, D17 and pass for Q1, Q2, Q6, Q7
W1 Measured sizes and spacing: constants of section 1, caption_lines, block height, external stacking, AZ and subnet padding, CELL_W layout.py D1, D4, D10 (layout side), D17 Q4, Q8, Q9, Q13, Q34 to Q37 on the three fixtures
W2 Captions: id-aware wrap_caption, type labels, one model shared by both exporters layout.py, registry.py, drawio.py, lucid.py D3, D10 Q11, Q12, Q13, Q39
W3 Border gateways layout.py D7, D11 Q14
W4 Edges end on icons, label position, cross-account kind, router report layout.py, registry.py, drawio.py, lucid.py D2, D12, D14, D15 Q21 to Q28
W5 Placement: hub strip, left column and DX/VPN lane, load balancers layout.py D8, D9, D13 Q16 to Q19
W6 Legend and title band layout.py D6 Q32, Q33
W7 Flow numbers layout.py, registry.py, drawio.py, lucid.py D5 Q29 to Q31
W8 Parity: dashboard against export, all file, determinism across hash seeds, shape names against the style guide dashboard/scan.py, tests D16 Q38, Q40, Q41
W9 Reference arrangement (P2): VPC rows, hub account on top layout.py none Q20

Dependencies: W0, then W1; W2, W3 and W6 are independent after W1; W4 needs W2; W5 needs W1 and W3; W7 needs W4; W8 can start after W0.

E. Open points

  1. Heads on link edges (closed in round 2): the reference draws arrowheads everywhere, because it draws flows, and the code follows it. Every edge but the association has a head; Q22 says which kinds have one at each end.
  2. Sparse AZ rows (Q7): keep empty cells, or draw faint placeholders so the grid is visible.
  3. Q20 (VPC rows, account grid) now or after W8.
  4. Whether draw.io honours every waypoint under orthogonalEdgeStyle (Q21): only a render can tell.
  5. Flow numbers (Q29): derived from the kind of edge in a fixed order, with no input file; deriving them from routes is out of scope for v1. The reference colours its seven discs, and so does the atlas (Q30).
  6. Lucid dash-dot (Q28): unverified; the fallback is dashed.
  7. A stress fixture (W0): untagged attachment, IGW and VGW on one VPC, endpoints, VPN, firewall endpoint, isolated subnet, long names.