feat(aspect): target output aspect ratio (#662) - #666
Draft
pinin4fjords wants to merge 851 commits into
Draft
Conversation
Closes #397. * ``l_shape_radii`` parameter ``going_down: bool`` becomes ``vertical: Direction``. * ``bypass_radii`` parameters ``going_right: bool`` / ``gap1_going_down: bool`` / ``gap2_going_down: bool`` become ``horizontal: Direction`` / ``gap1_vertical: Direction`` / ``gap2_vertical: Direction``. * core.py call sites drop the ``going_down = vertical is Direction.D`` adapter aliases; the typed values flow straight through. * Inside ``bypass_radii`` a local ``going_right`` boolean is retained for the index-mirroring arithmetic (used twice; inlining twice would be chattier). * tests/test_corners.py parametrize labels updated to match. No behaviour change; gallery render diff vs ``main`` remains empty.
refactor(routing): switch corner-radii helpers from bool to Direction
The descriptor scaffolding in ``inter_section.py`` documented a hypothetical future section-DAG propagator that would propagate ``Section.flip_lines`` along the DAG and feed WRAP_TABLE. Neither exists in the codebase: ``_propagate_wrap_flip_parity`` was rolled back in #387 before merge, and ``Section.flip_lines`` had its single reader removed in the same rollback. The propagator's job - keep bundle ordering consistent across complex wraps - is handled by per-corner offset propagation inside the wrap-route handlers (handedness-aware radii at each turn). The runtime ``check_bundle_order_preserved`` invariant catches any regression. No third layer of defense is needed. * ``inter_section.py`` module + ``TurnSequence.parity`` docstrings: drop "future propagator" / ``_propagate_wrap_flip_parity`` / ``Section.flip_lines`` references; describe ``WRAP_TABLE`` as a documentation catalogue. * ``test_inter_section_descriptor.py``: drop ``test_wrap_table_parity_matches_propagator_contribution`` - it tested a contract no consumer enforces. * ``parser/model.py``: delete the unused ``Section.flip_lines`` field. Tracked by closing issues #393 (parity drift) and #394 (wire WRAP_TABLE into dispatcher). No behaviour change; gallery render diff vs ``main`` remains empty.
chore: retire propagator references; drop unused Section.flip_lines
Aggregate fixes to the inter-section routing geometry, motivated by
visible artefacts in sarek's wraps (preprocessing -> variant_calling,
variant_calling -> post_vc, preprocessing -> reporting). Result:
every bundled route preserves its per-line ordering with no
crossovers at any corner.
* Cross-row LEFT/RIGHT-entry wraps (``_route_left_entry_wrap`` /
``_route_right_entry_wrap``):
- C1 lead-in pushed past the source section's right edge so the V1
column gets visible breathing room from the bbox.
- Handedness-aware corner radii: CW corners (C1, C2) use the
outside-of-turn radius; CCW corners (C3, C4) use the
inside-of-turn radius. Previously all four used the same
outside-of-turn r, which the renderer's segment-budget clamp
silently truncated at C3/C4 - producing the visible
"outer-line collapse" the user reported.
- Natural offset propagation through the four corners with the
outer bundle line consistently on the outer side of travel.
* ``_route_around_section_below``: new handler for the case where an
L-shape's horizontal segment would cut through an intervening
section. Routes down past the target row, leftward under the
target section, up the inter-column gap, then right into the LEFT
entry from below. Fires from ``_route_inter_section`` after the
L-shape selection when an intervening-section intersection test
reports a cross.
* Merge trunk: lands at the entry port Y instead of the
``__merge_X`` junction Y so same-row bypasses don't leave a
"hanging" disconnected stub.
* ``SECTION_ROUTE_CLEARANCE`` (16 px) applied to wrap V1, wrap V2,
and around-route V_up channels so the closest line in each bundle
keeps clear of section bbox edges.
* ``ICON_TERMINUS_FORK_LEAD``: short horizontal run added at fork
points adjacent to terminus file icons, so the fork starts past
the icon rather than inside it.
Conditional-firing of the new mechanisms, to avoid perturbing
fixtures the wrap geometry was never meant to touch:
* ``JUNCTION_MARGIN`` stays at its historical baseline (10 px). A
per-junction ``_required_junction_margin(n)`` helper in
``layout/engine.py`` derives the actual margin; in the concentric
n-line fan the per-line stagger and per-line ``r_wrap`` cancel
exactly, so every line's first-corner curve start lands at
``junction.x`` regardless of fan width. A global bump was tried
and visibly biased channels off the inter-section gap midline on
simple fixtures (02_sections, 03_fan_out, 03b_fan_in_merge).
* ``_route_bypass`` going-left ``fan_mid_x`` keeps the historical
``sx - curve_radius - (un-1)*step/2`` form (mirror of the
going-right branch). The wrap-style ``+`` formula belongs in the
wrap handlers, which compute their own first-corner geometry.
* ``_compute_junction_fan_info`` activates a shared first corner
only when handlers would otherwise pick different source-side Xs:
``(lshape + bypass)`` (the historical condition) or
``(wrap + lshape | bypass)``. Pure single-handler junctions stay
with their natural per-handler corridor.
* ``_route_merge_trunk``'s ``force_cross_row`` only fires when the
column range between trunk source and trunk target actually
contains a section in a row OTHER than the trunk's
(``_has_other_row_section_in_col_range``). Flat single-row
pipelines (genomeassembly, sarek-without-row-1-sections) keep
the historical ``cross_row=False`` placement and don't pay the
+41 px of extra canvas depth.
* ``_has_around_section_sibling`` (which gates the trunk's
``trunk_v_up_pull_away``) skips siblings whose column span pushes
them into the bypass dispatcher - those route as merge-branches or
trunks, never as around-section, so they don't compete for the
same V_up channel and pulling the trunk away on their behalf
produces visible unbundling (originally observed on
03b_fan_in_merge).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a 3-row topology fixture (``examples/topologies/around_section_below.mmd``) that triggers ``_route_around_section_below`` - until now no gallery fixture dispatched the handler, so it was dead code in CI. * Fixture: source in row 0 col 2, middle in row 1 col 1, target in row 2 col 0 with a LEFT entry port. The natural inter-row channel between source and target lands inside the middle section's bbox, so the dispatcher selects the around-section handler over the standard left-entry wrap. * Regression test (``tests/test_routing.py::test_around_section_below_dispatched_for_cross_row_left_entry``) installs a hook on ``_route_around_section_below``, runs the layout, and asserts the handler fires and produces the expected 6-point R-D-L-U-R shape with 4 concentric CW corners. * ``test_routes_dont_loop_backwards`` (the #250 pinwheel guard) also exempts the first segment now - U-turn routes (around-section-below, left-entry-wrap with dx<0) legitimately lead OUT of the source's right edge before turning back leftward. The guard still catches interior pinwheels (its actual purpose). * Wire the fixture into ``scripts/build_gallery.py`` so the docs gallery and CI render-diff preview include it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(routing): wrap geometry, around-section route, clearance
examples/variantbenchmarking.png is referenced nowhere in the repo (docs and guide embed the build-time SVG renders, not this file). It was last rendered in March and no longer matches the current layout engine output, so browsing it on GitHub showed a stale, misleading map. Drop it; the live render in the docs gallery is the source of truth. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…hmarking-png chore: remove orphaned variantbenchmarking.png
GitHub's built-in Mermaid renderer choked on subgraph titles and node labels containing parentheses (e.g. "Preprocessing (Optional)", "Liftover (Picard, UCSC)"), aborting with a parse error before any diagram appeared. Wrap those titles/labels in double quotes, which is Mermaid's escaping mechanism for special characters, and strip a single surrounding pair of quotes in the parser so nf-metro's own render is unchanged. The metro maps still won't render meaningfully on GitHub (the %%metro layout is ours), but the source at least parses instead of erroring. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix: make example mmds parse in stock Mermaid
…ad pass Layout-engine grid-strictness and robustness changes: * compute_layout warns (UserWarning) when an explicit y_spacing is below the minimum the graph's content needs, instead of silently widening. The auto path (y_spacing unset) still widens via compute_min_y_spacing. * _snap_canvas_y_to_grid (Stage 6.15) re-aligns the whole canvas to integer y_spacing multiples after late bbox-reanchoring leaves a uniform residue; half-grid symfan + convergence stations stay exempt. * Remove the inert _pad_stacked_captioned_file_icons pass and its _required_captioned_icon_pitch / _has_under_icon_caption helpers. Captioned-icon auto-widening lives in compute_min_y_spacing; an explicit too-tight pitch now surfaces the warning rather than a silent layout-time widen that overrode the user's choice. * _position_junctions re-runs after the Stage 6.13 row-tighten so junctions follow the shrink instead of leaving S-kinks at row-2 trunk boundaries. * _guard_bundle_order_preserved phase guard (validate=True) catches inter-section bundles whose per-line order flips at a corner. * Extract _fan_offsets (shared by the full-bundle redistribute/recenter passes) and split _shift_graph_into_canvas into _min_section_bbox_top + _translate_graph_y (also reused by the canvas snap). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A `%%metro file:` directive places a terminus file icon only when the station is a true sink. A mid-pipeline hub (one with both an inter-section predecessor and a successor, e.g. sarek's `cram_in` which receives from preprocessing and feeds the variant callers) now renders as a plain marker, so the file icon no longer overlaps the through-track. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add terminal_symmetric_fan (a terminal section fanning into equal-rank sinks) and trunk_through_fan (a pass-through section with a symmetric split/join), and list them in the gallery so they get render-preview and visual-diff coverage alongside the other topology fixtures. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Three invariants guarding entry/exit-port placement: a terminal section's equal-rank fan stays symmetric about its port (test_terminal_fan_symmetric_about_entry_port), a fan-and-reconverge exit port stays on its merge row (test_trunk_exit_follows_reconvergence), and a thick multi-line bundle keeps >= min_track_gap row clearance (test_thick_bundle_row_pitch). Each guards `tested >= 1` so it can't pass vacuously. * test_inter_section_routes_dont_reenter_source_section gated on an exit index so a junction-originated route's outward run isn't flagged as re-entry; boundary-originated routes keep full strictness. * Retarget the Stage 6.15 phase-guard tests onto _snap_canvas_y_to_grid after the captioned-icon pass removal. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(layout): y-grid re-snap, captioned-icon pitch, hub-terminus fix
…ces) Size each inter-section gap as A + Σ bundle_widths + (N-1)·B + A, where A = EDGE_TO_BUNDLE_CLEARANCE (section edge to nearest bundle line) and B = BUNDLE_TO_BUNDLE_CLEARANCE (between adjacent bundles sharing a gap). _bundles_in_gap enumerates the distinct concentric bundles traversing each gap (coalescing channels that share lines), and _min_gap_for_bundles applies the formula, replacing the outermost-curve-radius sizing. A single line of one bundle collapses to ~2A, below the static MIN_INTER_SECTION_GAP, so ordinary gaps are not widened; multi-line / multi-bundle corridors claim only the horizontal space their visual width occupies. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
symmetric_bundle_midpoint centres N bundles in an inter-section gap, keeping exactly B between adjacent bundles and flooring the edge-to-bundle distance at A; bundle_width gives a bundle's visual span. column_gap_edges / column_gap_midpoint and the underlying col_left_edge/col_right_edge take an optional row so a diversion travelling in one row measures the gap against that row's sections only (a wide section stacked in another row of the same column no longer skews the gap centre). RoutedPath gains normalize_exempt for routes whose vertical channels follow a concentric loop that the gap-channel normalization must not re-stack. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A post-routing pass (_normalize_gap_channels) re-stacks every inter-section vertical channel into its final position, uniformly across all routing handlers: lines running the same direction in a gap bundle OFFSET_STEP apart (segments sharing a line_id overlay at one x), a downward bundle and an upward bundle sharing a gap separate by B, a lone bundle centres with >=A clearance, and the per-line order is chosen to minimise crossings. Channels are matched to their gap by actual Y delta and row-aware gap edges; wrap / around-section / top-entry routes set normalize_exempt and are skipped. _gap_channel_base provides the initial in-gap placement during routing that the pass then refines. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… invariants Lock in the gap-width formula (dedup of fanned-to-many-targets channels), single-bundle centring (incl. junction-sourced and around-section diversions), same-direction bundling at OFFSET_STEP with same-line overlay, the down/up B-separation in a shared gap, and that normalization adds no inter-line crossings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
refactor(routing): principled inter-section gap formula + symmetric multi-bundle
At a single-upstream-source fan-out junction the upstream port->junction route and the downstream junction->target route are two separate RoutedPaths whose handoff points don't coincide: the downstream L-shape lead-in starts a curve_radius PAST the junction, leaving an along-travel gap that renders as a notch at the corner apex of the outer line. Add check_fanout_tail_join (and the fanout_junctions classifier) in routing/invariants.py: for every fan-out junction, the component of the upstream-end / downstream-start offset measured along the upstream travel direction must be within tolerance. A purely perpendicular offset (an inner concentric-corner member's approach Y, hidden under the stroke) is tolerated. Merge junctions (>1 upstream source) are excluded so their trunk routing is never implicated. Wire it into compute_layout(validate=True) as _guard_fanout_tail_join and add tests/test_fanout_tail_join_invariant.py (gallery happy-path, the variant_calling_tuned __junction_6 case, and merge-junction exclusion). The gallery test fails until the routing fix lands. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add _join_fanout_upstream_tails as a route_edges post-pass: at every single-upstream-source fan-out junction, extend each incoming port->junction route's horizontal final segment along X to the X of its same-line downstream junction->target route's first waypoint. This closes the along-travel gap (the downstream L-shape lead-in begins a curve_radius past the junction) that rendered as a dark notch at the corner apex of the outer line. The upstream Y is kept unchanged so an inner concentric-corner member meets its downstream with only a sub-line-width perpendicular offset, hidden under the stroke, rather than a tilted/stepped approach that would reintroduce an apex kink. Gated to genuine fan-out junctions (single distinct upstream source); merge junctions are excluded so their trunk bypass routing is never perturbed. Gallery diff: only the four fan-out fixtures change, each by a single upstream-tail X extension; all other renders are byte-identical. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extract _fanout_route_maps so the apex-gap check and the routing pass that closes it index fan-out-incident routes by (junction, line_id) in one place, and drop the duplicated tangent/perpendicular explanation from the guard docstring (it lives with the check function). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(routing): join fan-out upstream tails to downstream curve start
Section bbox top padding was anchored to trunk_y - SECTION_Y_PADDING while the bottom grew with content, so a fan symmetric about the trunk read as pushed up. New Stage 6.15a _grow_bboxes_to_content_top grows each bbox top to a full SECTION_Y_PADDING above the highest marker (bounded against the row above by section_y_gap + SECTION_HEADER_PROTRUSION so a grown box never crowds its header badge into an inter-section route). Shared _section_content_top_target helper feeds a new runtime guard _guard_section_top_padding and the invariant test test_section_bbox_has_top_padding (differentialabundance plots xfailed as gap-bounded). Extracted shared _bbox_cols_overlap predicate. Fixes #406
…#410) * fix(layout): re-anchor junctions after Stage 6.14 loop-station shift Stage 6.14 (_shift_and_propagate_loop_stations) can move an exit port off the Y it held when junctions were last positioned at Stage 6.13. Junctions were only re-anchored inside _snap_canvas_y_to_grid, which early-returns when the canvas is already on grid, leaving a fan-out junction stranded above its exit port. The fanned routes then dip to the stale junction Y and back, producing the S-curve on complex_multipath section 3 -> sections 4/5. Re-run _position_junctions after the loop-station shift so junctions track the settled port Ys. Add test_fanout_junction_shares_exit_port_y over the multi-section corpus and a _guard_fanout_junction_shares_exit_port_y runtime validator wired into the final validate block. Fixes #386 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor: tighten fan-out junction test helper after fix for #386 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(routing): center multi-row gap channels in the intersected gap A vertical inter-section channel that crosses several grid rows was centred in the gap of the first overlapping row only. When a sibling row's section was narrower (its column edge further out), the channel centred in that wider gap and stepped back behind its own source section edge - a right-to-left notch on a left-to-right route (complex_multipath standard/legacy climb out of Full Pre-process). _normalize_gap_channels._find_gap now narrows a channel's gap to the intersection of every crossed row's column gap, and gap_bounds shared by bundles in one (gap, row) intersects across members instead of last-write- wins, so a down/up bundle pair packed together clears all crossed rows. Add test_inter_section_route_no_x_backtrack over the multi-section corpus and _guard_inter_section_route_no_backtrack runtime validator. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…try (#411) (#413) A section whose RIGHT entry port is fed through a fold junction by a BOTTOM exit drops the bundle vertically (X offsets) then turns left into the entry. That down->left concentric corner reverses bundle ordering, but detect_reversed_sections only handled TB-section exits, so the downstream section's entry Y offsets did not match the incoming run and the lines crossed at the fold apex (caught by _guard_bundle_order_preserved under validate=True). Add Phase 1c to detect_reversed_sections to flag these sections reversed, mirroring the existing TB BOTTOM/LR-exit cases. Widen the bundle-order invariant sweep to examples/topologies and drop the now-fixed fold_stacked_branch xfail. Fixes #411 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Drop balancing._section_trunk_y; reuse the already-imported _section_lr_port_anchor_y (identical port-anchor lookup, removes a name clash with _common._section_trunk_y). - Bail _row_group_grid_spacing before classifying section Ys when the group needs no shared grid, and return section_class for reuse. - Compute rail_side only on the non-TB label paths that consume it. Gallery renders byte-identical; full suite green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…g error A cyclic or self-loop Mermaid graph crashed render with a raw NetworkXUnfeasible traceback out of assign_layers, and validate reported it as valid (exit 0), because nothing on the parse/layout path checked acyclicity. Add a find_cycle witness helper to the graph-semantic validator and a format_cycle_error formatter so both planes share one message. validate_graph now emits an ERROR naming the cycle (a -> b -> a; a -> a for a self-loop), and assign_layers raises ValueError with the same message before the topological sort. The render CLI's compute_layout catch is widened to ValueError so it surfaces as a clean ClickException. Fixes #645 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Hoist the acyclicity check out of assign_layers (called once per section and per spread/strike iteration) up to compute_layout, where it runs once per render. find_cycle now gates on is_directed_acyclic_graph so the common acyclic path pays no exception, and the witness DiGraph is no longer rebuilt inside the layout hot path. Replace the bare ValueError with a dedicated CyclicGraphError(ValueError) so the render CLI catches cycles precisely without masking unrelated layout errors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extract helpers from _compute_entry_port_offsets, _reindex_section_local, _reorder_exit_only_lines, and _compact_station_gaps. Pure structural; gallery byte-identical. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sed_sections below 30 [skip ci] Lift nested closures to module helpers and extract phase loops. Pure structural; gallery byte-identical. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…elow 30 [skip ci] Move the label-strike-clearance grow/minimize loop (with its closures) and the per-section rail retrofit out of _compute_layout_scaled. Pure structural; gallery byte-identical. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…skip ci] Lift the two closures to module helpers and extract route classification, the flat-to-internal-chain guard, and the per-station body. Pure structural; gallery byte-identical. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extract helpers from _balance_section_content_around_trunk, _compute_section_offsets, _insert_bypass_stations, and _assign_grid_positions. Pure structural; gallery byte-identical. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…o 30 [skip ci] Extract helpers from _snap_all_y_to_grid, _guard_row_trunk_cy_consistent, and check_route_segment_crossings, then set max-complexity=30. With all 15 remaining offenders under 30, C901 is now a readability guard, not just a regression backstop. Pure structural; gallery byte-identical. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Collapse the validator's two Chebyshev point-near checks (_pt_at_station / _pt_near_shared_port) into one _pt_near helper. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(layout): fail fast on cyclic / self-loop graphs with a node-naming error
Adds a free-text caption field to MetroGraph and wires it through the full stack: directive parser, CLI flag (--caption), SVG renderer, and nf-metro info output. The caption is rendered bottom-left of the canvas in the same muted style as the existing watermark, mirroring its bottom-right placement. Fixes #574 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extract WATERMARK_FILL so both the watermark and caption text share the constant rather than duplicating the rgba literal. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Demonstrates the %%metro caption directive on the motivating example from #574. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The caption (%%metro caption:) is intended to be readable, not a muted watermark. Separate CAPTION_FONT_SIZE (11) and CAPTION_FILL (0.85 opacity) constants from the WATERMARK constants (8pt, 0.6 opacity). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New `nf-metro explain` command surfaces WHY the engine made each layout decision (inferred section directions, inferred port sides, fold/row layout, fan-out junctions, bypass-V stations). Pairs with `nf-metro info` (WHAT was built); this command adds WHY each non-trivial choice was made. - `src/nf_metro/explain.py`: new `build_explain` / `format_explain_text` / `format_explain_json` API, parallel in structure to `introspect.py` - `src/nf_metro/cli.py`: `explain` command with `--json`, `--section`, `--station` filter flags - `tests/test_explain.py`: 90 tests covering schema, all decision types, filters, formatters, CLI integration, and no-crash sweep over every fixture Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a skill that simulates a never-seen-before pipeline arriving: it composes one novel, deliberately-complex metro map, renders it, and surfaces layout-engine bugs the way real new pipelines do. The loop: braid 2-4 structural axes that haven't been combined before (off the failure-mode menu) into a correct-by-construction .mmd, probe it (probe_layout.py runs parse -> layout -> validate -> route and sorts findings into authoring / validator / crash / guard buckets), present the render for aesthetic bugs the validator can't see, then confirm and understand every candidate against the laid-out geometry (inspect_layout.py flags off-trunk drags, off-track gaps, inter-row gaps; nf-metro explain names the rule that fired) before filing. Each confirmed bug ships with regression infra - an examples/topologies fixture, a GALLERY_ENTRIES render so CI render-diff tracks it, and a strict-xfail test that reds CI when the bug is fixed - so it can't silently return. Operational answer to #364 (mutation/fuzz testing) and #323 (fragility on new pipelines). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tooling [skip ci] Two additive, conditional notes so fix-issue coordinates with the nf-metro-stress-render skill without losing its general-issue scope: - Step 3 points at the bundled probe_layout.py / inspect_layout.py diagnostic scripts plus nf-metro explain/info as conveniences for the reproduce-in-numbers step (usable for any issue), and notes that a stress-render-filed issue carries a correct-by-construction repro .mmd in a <details> fold. - Step 4 adds a 'check for an existing regression lock first' preamble: if a strict-xfail referencing the issue already exists (as stress-render leaves behind), it IS the failing test - don't duplicate it or re-add the fixture/gallery entry; the fix flips it to XPASS (reds CI) and is finished by removing the xfail marker. Explicitly framed as the exception, with the bare-issue path as the common case. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A multi-line bundle entering a section through an entry port to a deep first station inserts one phantom pass-through per line, all converging on that station. `_align_phantom_pass_throughs` snapped the convergence node onto a phantom's track once per phantom, so the last-iterated phantom's track won, dragging the consumer off the trunk into a near-vertical onward climb where the bundle merged into one stroke. Snap only when a convergence node has a single phantom predecessor (a genuine bubble); a node fed by several phantoms is the section trunk head and stays on its assigned trunk track. Fixes #650 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Factor the off-track-input -> consumer edge filter shared by test_off_track_inputs_above_consumer and the new consumer-id helper into one _off_track_input_consumer_map, kept independent of the engine's own _off_track_anchor_of so the invariant stays an independent oracle. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
An off-track input that shares a consumer-anchor with a differently-columned off-track station (e.g. an input above the consumer plus a producer-fed output beside it) was lifted by the whole anchor group's depth instead of its own column's. A lone-in-its-column input was stranded an extra slot up over an empty row above an earlier trunk station, which the layout validator flags as excessive_column_gap. Count the lift step per column in _place_off_track_relative_to_anchors via the new _per_column_stack_steps helper, so stations sharing an anchor across different columns each hug one row above their consumer. Genuine same-column stacks are unchanged. Add _guard_off_track_input_column_stack (validate block) and a parametrised invariant test over single-trunk off-track-input fixtures, plus a topology fixture and gallery entry so CI's render-diff tracks the case. Fixes #651 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…crossover (#652) Treat the lines leaving a fan-out junction as one bundle so a far-column bypass no longer weaves under its sibling down-turns, and render the down-turns as truly concentric arcs. - Unclamp the shared fan corner (_restack_channel): extend each descent's source-side lead-in to its full r_first so resolve_curve_radii stops clamping the concentric radii to the short lead length; the down-turns now share one arc centre instead of squashing together. The extra lead length overlaps the upstream same-line tail, so it is visually free. - Order the gap-bundle descent by approach side (_distinct_line_order): avoid fan-side weaves before deep-end crossings, so a bypass at its natural slot descends bundled-inner and crosses its down-turns once down in the inter-row gap (a clean fork) instead of weaving at the fan. Scoped to rightward-descending fans; the leftward mirror is left to the existing deep-end ordering. Zero crossings is topologically impossible at this fan (the source emits both the straight continuation and the down-turn). test_fan_bypass_no_fan_weave guards the real invariant: no bypass-vs-descender crossing in the junction's own row band; a gap-level crossover and the same-row trunk crossing are expected. Fixes #652. Also addresses #651.
) Add a `%%metro aspect:` directive and `--aspect` flag that pick the fold_threshold whose rendered layout shape is closest to a requested width/height ratio. A metro map's aspect ratio is emergent from section topology, so the search brackets the achievable set (candidate fold thresholds bracketed by maximum fold and the single-row total), measures each, and reports the closest in log space. When the topology offers only one shape (convergence/tall-anchor layouts, an author-pinned grid, a single section) it says so and renders as-is rather than silently doing nothing. Inert unless aspect is set; gallery renders are byte-identical. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Contributor
|
Render preview: no visual changes detected. All renders match |
pinin4fjords
marked this pull request as draft
June 12, 2026 21:58
pinin4fjords
force-pushed
the
main
branch
2 times, most recently
from
July 28, 2026 11:30
a90d751 to
d134b08
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds an aspect-ratio target so a map can be steered thin-and-wide or tall-and-narrow without hand-tuning
fold_threshold.%%metro aspect: <ratio>directive and--aspect <ratio>CLI flag (width / height, e.g.4wide,0.5tall).Aspect 4 requested, 3.53 achieved (2135x605, fold_threshold=16).fold_threshold(directive or--fold-threshold) suppresses the search entirely.Mechanism
nf_metro.aspect.solve_aspectis a pure search over candidate fold thresholds against an injectedmeasure(fold) -> (w, h)callable; it dedups layouts that render to identical sizes and returnsadjustable=Falsewhen fewer than two distinct shapes exist.auto_layout.candidate_fold_thresholdsenumerates the fold values worth trying: the cumulative topo-column widths, plus1(maximum fold), bracketing most-folded to unfolded.render_svgmutates the graph, so a measured graph cannot be reused for the final render.Fixes #662
Test plan
pytestpasses, includingtests/test_aspect.py(pure-search behaviour, candidate enumeration, directive parse, and end-to-end wide-beats-tall / explicit-fold-wins / fixed-topology cases)ruff format --check+ruff check+mypyclean on the whole repomain(feature is inert unlessaspectis set) - verified locally againstorigin/main_guard_*added: this is a CLI-level selection wrapper, not a layout-geometry invariant that could silently regress (the chosen fold is guaranteed to be a measured candidate by construction).🤖 Generated with Claude Code