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 ofdiagram-style.mdkeep their meaning. Where a number here differs fromdiagram-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)tomax(right, cx + 46)and y fromytobottom + 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 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¶
- 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. - 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_PXis 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¶
- Heads on
linkedges (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. - Sparse AZ rows (Q7): keep empty cells, or draw faint placeholders so the grid is visible.
- Q20 (VPC rows, account grid) now or after W8.
- Whether draw.io honours every waypoint under
orthogonalEdgeStyle(Q21): only a render can tell. - 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).
- Lucid dash-dot (Q28): unverified; the fallback is
dashed. - A stress fixture (W0): untagged attachment, IGW and VGW on one VPC, endpoints, VPN, firewall endpoint, isolated subnet, long names.