Guard tiers
nf-metro defends its layout output with two families of runtime invariant:
_guard_*functions insrc/nf_metro/layout/phases/guards.pyraisePhaseInvariantErroron a violation.check_*functions insrc/nf_metro/layout/routing/invariants.pyreturn a list of violation objects, and the caller decides whether to raise.
The render path runs the cheap Tier-A subset of both families on the settled
geometry, so a layout defect reaches the end user as a warning, or as a
--strict error, instead of a silently broken SVG:
assert_render_layout_invariants(phases/guards.py) runs the Tier-A_guard_*postconditions, andassert_render_curve_invariants(routing/invariants.py) runs the Tier-A routingcheck_*invariants.
The Tier-B remainder of the _guard_* suite is gated behind
compute_layout(validate=True), which the nf-metro render path never sets.
Tier-B members are costlier, or they depend on a mid-pipeline reroute.
Tier A is the always-on render-path set. Tier B is the validate=True set.
| Tier | Meaning | Promotion intent |
|---|---|---|
| A | Cheap, observational structural checks: placement (finite coords, bbox containment with marker overhang, station overlap, ports on boundaries) plus _guard_stations_within_bbox, the inter-section backtrack/wrap guards (_guard_inter_section_route_no_backtrack, _guard_inter_section_route_no_full_width_backtrack, _guard_serpentine_no_backtrack, _guard_inter_section_route_clears_own_section_interior), the non-consumer-section breeze-through guards (_guard_no_route_through_section, _guard_no_line_crosses_non_consumer), and the routing check_* invariants already always-on via the render chokepoint. | Run on the default render path, warning by default with a --strict escalation. |
| B | The remaining validate=True set: route-shape, bundle-order, label, rail, and merge-port geometry. Correct but either costlier or dependent on a mid-pipeline reroute (they consume route_edges output), so not cheap-always-on. | Stay behind validate=True / --strict. |
| C | Test-only oracles: too slow, too fixture-specific, or non-observational. | Live in the test suite, not the runtime suite. |
Tier A is mostly the cheap observational set that the cost audit confirms runs
in low tens of microseconds. It also holds a handful of routed-geometry sweeps
in the ~10-90 us range whose visibility matters more than their cost: the
inter-section backtrack and wrap guards, of which the dearest is ~11 us, and the
non-consumer-section breeze-through guards (_guard_no_route_through_section
at ~86 us and _guard_no_line_crosses_non_consumer at ~75 us). A line plotted
over a section or station it never touches is at least as visibly broken as a
backtracking bundle, so those stay on the always-on path despite the cost.
Tier B holds everything else that is materially more expensive or depends on a
mid-pipeline reroute. Its most expensive members,
_guard_no_opposing_line_overlap at ~86 us and
_guard_trunk_bands_crossing_optimal at ~57 us, are routed-geometry sweeps. The
always-on check_* routing invariants in routing/invariants.py run through a
separate chokepoint and include members up to check_no_hanging_routes at
~470 us. Cost alone does not gate Tier-A membership in either family. The
visibility of the defect does.
Tier C contains checks that run only against the test corpus. Two of them
check section seams: check_seam_approach_equals_departure and
check_seam_segments_meet_at_port verify that each inter-section approach uses
the lane assigned by lane_x. They live in tests/test_seam_lane_x.py.
The third is check_merge_feeders_land_on_trunk, in
tests/test_merge_branch_trunk_invariant.py. It requires exact contact between
a feeder and its trunk. Its Tier-A counterpart allows a small tolerance, because
an alternate routing handler can produce a valid map without exact contact.
Every other guard and check is reachable from compute_layout or rendering.
Registries
Section titled “Registries”The classification is data rather than prose:
GUARD_REGISTRY(phases/guards.py) is the single ordered source of truth for thevalidate=Trueguard call sequence. Its order is the call order, andrun_validate_guardsiterates it. Each entry is aGuardSpecfromlayout/phase_state.py, carryingtier, theneedsset naming which ofoffsets,routesandsection_y_*the guard takes, whether it isbisection_safe(running at every Pass C checkpoint, gated byfirst_valid_stage) or final-only, and the_BISECTION_FIRST_VALIDdata derived back out for the engine re-export.INLINE_GUARD_REGISTRY(phases/guards.py) applies the sameGuardSpecschema to the guardsengine.pyinvokes directly at a specific pipeline stage rather than through the Pass C or final runner. LikeCHECK_REGISTRYit is classification only, with noneedsorbisection_safedispatch data. Every defined_guard_*therefore lives in exactly one of the two guard registries, and none escapes tier and issue-pin classification.
The Tier-A _guard_* postconditions across both registries run on the render
path through assert_render_layout_invariants, the sibling of the routing
chokepoint. render_layout_invariant_specs selects them: every Tier-A guard
except _RENDER_CHOKEPOINT_AUTHORING_GUARDS. That exception holds the two
authoring-error guards, _guard_no_same_row_backward_feed and
_guard_no_mixed_entry_directions, which raise a ValueError on un-renderable
input and stay always-on hard fails in the engine rather than joining the
warn-by-default chokepoint.
CHECK_REGISTRY(routing/invariants.py) applies the sameGuardSpecschema to the routing checks. Checks return lists rather than raising, so this is a classification registry rather than a dispatcher, and the runtime chokepoint staysassert_render_curve_invariants. The registries are unified through the shared schema and this page, not by merging the raise-against- return error protocols.
Each GuardSpec also carries issue_pin and narrow_reason. issue_pin holds
the #NNN issues a guard was born from, kept as data so consolidation cannot
lose the regression trail. narrow_reason says why an issue-pinned guard stays
scoped to its case rather than stating a general property.
tests/test_guard_registry.py keeps these honest: every guard and check is
registered exactly once, the Tier-A check set equals the curve render chokepoint
exactly, the render-layout chokepoint equals the Tier-A guard set minus the
authoring guards, no validate-only guard duplicates an always-on check, and
every issue-pinned guard records its issue and a narrow_reason.
Cost audit
Section titled “Cost audit”scripts/guard_cost_audit.py lays out every fixture under examples/ and
examples/topologies/ once, then times each registered guard and check against
the final geometry. The mean microseconds per fixture below come from that run.
Regenerate them with:
python scripts/guard_cost_audit.py --json /tmp/guard_cost.jsonValidate-suite guards (GUARD_REGISTRY)
Section titled “Validate-suite guards (GUARD_REGISTRY)”| guard | tier | mean us | dispatch |
|---|---|---|---|
_guard_coordinates_finite | A | 1.5 | bisection (first valid: start) |
_guard_section_bboxes_positive | A | 0.4 | bisection (first valid: start) |
_guard_stations_in_sections | A | 3.5 | bisection (first valid: after Stage 5.3) |
_guard_ports_on_boundaries | A | 1.3 | bisection (first valid: start) |
_guard_ports_clear_unanchored_box_edges | A | 7.9 | final only |
_guard_no_station_overlap | A | 13.5 | bisection (first valid: after Stage 6.4) |
_guard_no_coincident_station_coords | A | 3.3 | bisection (first valid: after Stage 6.4) |
_guard_no_line_crosses_non_consumer | A | 74.7 | bisection (first valid: after Stage 6.14) |
_guard_station_x_column_drift | A | 6.0 | bisection (first valid: start) |
_guard_row_trunk_cy_consistent | B | 12.1 | final-only |
_guard_off_track_clear_of_anchor | B | 3.5 | final-only |
_guard_fanout_junction_shares_exit_port_y | B | 0.9 | final-only |
_guard_fanout_junction_resolves_upstream | B | 0.7 | final-only |
_guard_entry_port_fed_only_by_ports | B | 0.7 | final-only |
_guard_flow_exit_anchored_to_carrier | B | 3.5 | final-only |
_guard_perp_entry_feed_not_collinear | B | 0.7 | final-only |
_guard_merge_port_approach_side | B | 5.0 | final-only |
_guard_merge_port_outgoing_side_preserved | B | 5.0 | final-only |
_guard_exit_inherits_entry_bundle_order | B | 1.9 | final-only |
_guard_bypass_port_no_slot_gaps | B | 4.9 | final-only |
_guard_partial_branch_offset_gaps | B | 2.1 | final-only |
_guard_row_gaps | B | 0.5 | final-only |
_guard_section_top_padding | B | 0.6 | final-only |
_guard_terminus_icons_within_bbox | B | 0.4 | final-only |
_guard_inter_section_routes_in_row_band | B | 4.6 | final-only |
_guard_topmost_row_top_entry_hugs_section | B | 6.7 | final-only |
_guard_off_track_output_clears_non_producer | B | 2.4 | final-only |
_guard_tb_exit_corner_column_order | B | 2.0 | final-only |
_guard_no_split_same_line_fanout_descents | B | 2.4 | final-only |
_guard_no_distinct_line_fanout_crossing | B | 3.4 | final-only |
_guard_no_dogleg_crosses_exempt_trunk | B | 1.9 | final-only |
_guard_no_stacked_elbow_graze | B | 7.6 | final-only |
_guard_fanout_tail_join | B | 2.7 | final-only |
_guard_perp_entry_boundary_consistent | B | 7.4 | final-only |
_guard_perp_exit_over_leadin_no_overdip | B | 2.9 | final-only |
_guard_right_entry_drop_in_when_clear | B | 2.5 | final-only |
_guard_inter_section_route_no_backtrack | A | 4.7 | final-only |
_guard_inter_section_route_no_full_width_backtrack | A | 5.3 | final-only |
_guard_routes_enter_sections_at_ports | B | 61.9 | final-only |
_guard_no_route_through_section | A | 85.7 | final-only |
_guard_inter_section_route_clears_own_section_interior | A | 11.3 | final-only |
_guard_feeder_exits_section_through_side | B | 8.2 | final-only |
_guard_entry_approach_from_port_side | A | 5.3 | final-only |
_guard_no_opposing_line_overlap | B | 86.1 | final-only |
_guard_serpentine_no_backtrack | A | 3.9 | final-only |
_guard_no_artefactual_counter_flow | B | 4.6 | final-only |
_guard_inter_row_run_clearance | B | 3.6 | final-only |
_guard_trunk_bands_crossing_optimal | B | 56.9 | final-only |
_guard_inter_section_descent_edge_clearance | B | 7.1 | final-only |
_guard_fan_bundles_coincide_or_separate | B | 14.5 | final-only |
Routing checks (CHECK_REGISTRY)
Section titled “Routing checks (CHECK_REGISTRY)”| check | tier | mean us | runs |
|---|---|---|---|
check_bundle_order_preserved | A | 34.9 | render chokepoint (always-on) |
check_concentric_bundle_corners | A | 41.1 | render chokepoint (always-on) |
check_collinear_distinct_lines | A | 200.0 | render chokepoint (always-on) |
check_no_same_line_parallel_descents | A | 5.6 | render chokepoint (always-on) |
check_no_fused_cotravelling_lines | A | 63.5 | render chokepoint (always-on) |
check_merge_branches_meet_trunk | A | 6.9 | render chokepoint (always-on) |
check_no_hanging_routes | A | 430.0 | render chokepoint (always-on) |
check_bottom_row_climb_stays_at_row_level | A | 2.9 | render chokepoint (always-on) |
check_gap_channels_materialized | A | 22.5 | render chokepoint (always-on) |
check_trunks_declared | A | 1.8 | render chokepoint (always-on) |
check_peeloff_concentric | A | 4.2 | render chokepoint (always-on) |
check_tb_exit_corner_preserves_column_order | B | 1.5 | via _guard_* wrapper |
check_fanout_tail_join | B | 2.3 | via _guard_* wrapper |
check_merge_port_approach_side | B | 4.6 | via _guard_* wrapper |
check_merge_port_outgoing_side_preserved | B | 4.6 | via _guard_* wrapper |
check_exit_inherits_entry_bundle_order | B | 1.6 | via _guard_* wrapper |
check_partial_branch_offset_gaps | B | 1.8 | via _guard_* wrapper |
check_no_split_same_line_fanout_descents | B | 2.0 | via _guard_* wrapper |
check_no_distinct_line_fanout_crossing | B | 2.9 | via _guard_* wrapper |
check_no_dogleg_crosses_exempt_trunk | B | 1.5 | via _guard_* wrapper |
check_stacked_elbow_clearance | B | 7.1 | via _guard_* wrapper |
check_perp_entry_boundary_consistent | B | 6.9 | via _guard_* wrapper |
check_perp_exit_over_leadin_clears_only_spanned_sections | B | 2.5 | via _guard_* wrapper |
check_right_entry_drop_in_when_clear | B | 2.1 | via _guard_* wrapper |
check_seam_approach_equals_departure | C | - | test suite only (tests/test_seam_lane_x.py) |
check_seam_segments_meet_at_port | C | - | test suite only (tests/test_seam_lane_x.py) |
check_merge_feeders_land_on_trunk | C | - | test suite only (tests/test_merge_branch_trunk_invariant.py) |
Inline guards (INLINE_GUARD_REGISTRY)
Section titled “Inline guards (INLINE_GUARD_REGISTRY)”These _guard_* functions are invoked directly at specific pipeline stages
rather than through the Pass C or final dispatch, so they carry no needs or
bisection_safe dispatch data, only their tier and any issue_pin and
narrow_reason. Their costs are not measured separately, since each is a single
structural pass.
_guard_canvas_margin_settled is the one inline guard whose tier does not
describe its gating. The render’s canvas-margin move loop calls it on every
render, not only under validate=True. Its input is the shortfall a move left
behind, which exists nowhere else, so it cannot join the dispatched Tier-A set
the render chokepoint runs.
| guard | tier | role |
|---|---|---|
_guard_stations_within_bbox | A | Always-on postcondition: every station centre lies within its section bbox. |
_guard_no_negative_grid_columns | A | No section sits at a negative grid column. |
_guard_explicit_grid_directions | A | Explicit-grid sections retain direction unless resolution reverses it. |
_guard_no_mixed_entry_directions | A | A section’s incoming lines approach from a single side. |
_guard_independent_components_disjoint | A | Independently-stacked components do not overlap. |
_guard_no_same_row_backward_feed | A | A same-row inter-section edge does not run against source flow. |
_guard_anchors_frozen_during_placement | B | Content placement leaves resolved anchors fixed. |
_guard_canvas_margin_settled | B | No left/top canvas margin outlives the render moves that own it (#1769). |
_guard_bypass_v_flat_visible | B | Every bypass V keeps a visible horizontal run through its X. |
_guard_centered_line_spread_balanced | B | A centered section’s weave balances about its trunk. |
_guard_file_icon_no_name_label | B | A file-icon station gets no separate node-name label. |
_guard_interchange_bar_clears_non_members | B | An interchange bar does not cross a non-member station. |
_guard_no_diagonal_strikes_horizontal_label | B | No foreign fan diagonal rakes a stacked station’s name. |
_guard_no_label_overlap | B | No station label overlaps another label or a marker. |
_guard_no_line_crosses_file_icon | B | No rendered line passes through a file/terminus icon. |
_guard_no_line_strikes_label | B | No rendered line strikes through a station label. |
_guard_no_wrapped_label_trunk_strike | B | No wrapped label overruns a foreign horizontal trunk. |
_guard_off_track_consumer_on_trunk | B | An off-track input’s straight-through consumer stays on trunk (#650). |
_guard_off_track_input_column_stack | B | Single-trunk off-track inputs hug their consumer column (#651). |
_guard_off_track_not_hub | B | No off-track station has edges on both sides (#1295). |
_guard_rail_above_label_band | B | A rail section reserves room above its top rail for labels. |
_guard_rail_one_station_per_column | B | Rails place one distinct station per column. |
_guard_rail_stations_seat_on_rails | B | Rail stations seat on their lines’ fixed rails. |
_guard_single_trunk_off_track_step | B | Single-trunk sections lift off-track stations by the base pitch (#580). |
_guard_tall_anchor_stack_well_formed | B | A tall-anchor vertical stack keeps its downstream chain intact. |
_guard_tb_top_entry_drop_hugs_top | B | A clean TB TOP-entry drop seats its first station at the top. |
Consolidation
Section titled “Consolidation”The consolidation pass (#922) removed the validate-only _guard_* wrappers that
only raised around a check already in the always-on render chokepoint:
_guard_bundle_order_preserved, _guard_concentric_bundle_corners,
_guard_no_collinear_distinct_lines,
_guard_no_intra_section_collinear_distinct_lines and
_guard_no_same_line_parallel_descents. Each check is the single authority and
runs on every render through assert_render_curve_invariants, so the wrapper
was pure duplication. test_no_registry_guard_duplicates_an_always_on_check
keeps the duplication from returning.
The remaining issue-pinned guards each express a distinct geometric property,
and their docstrings distinguish them from one another explicitly. Rather than
force-merging their bodies, the pass records each guard’s originating issue in
issue_pin and documents its scope in narrow_reason. The two genuinely general
inline guards, _guard_no_negative_grid_columns and
_guard_explicit_grid_directions, carry no pin, because they state a general
structural property rather than a special case.
The collinear-distinct overlay checks are unified as one always-on
check_collinear_distinct_lines, whose scopes argument selects the
inter-section, intra-section and diagonal scans. The render chokepoint runs every
scope, while callers that need a subset, such as the strike-clearance probe and
the per-scope unit tests, pass the scopes they want.