nf-metro-layout-triage

star 77

Build a self-contained HTML triage page for failing/xfailing layout-invariant tests in nf-metro (test_label_x_matches_segment_midpoint_on_horizontal_runs, test_stack_station_xs_share_column, test_row_trunk_marker_cy_consistent, test_no_kink_at_section_boundary, test_symfan_pairs_share_y, test_lines_dont_cross_non_consumer_markers, test_topological_siblings_share_y_or_symmetric, test_section_bbox_has_bottom_padding, test_off_track_inputs_above_consumer). Each row pairs the rendered fixture SVG with a red-bbox overlay on the offending element, a plain-English "Supposed issue" + "What to check" explanation, and bug / not-a-bug / ambiguous triage buttons whose state is saved in localStorage and exported as JSON. Use when the user asks to "triage layout invariants", "review xfails", "review failing layout tests", "build the triage page", or generally wants to triage the layout-invariant test suite by eye.

pinin4fjords By pinin4fjords schedule Updated 6/3/2026

name: nf-metro-layout-triage description: Build a self-contained HTML triage page for failing/xfailing layout-invariant tests in nf-metro (test_label_x_anchored_to_station_marker_on_horizontal_runs, test_visual_stack_station_xs_share_column, test_row_trunk_marker_cy_consistent, test_no_kink_at_section_boundary, test_symfan_pairs_share_y, test_lines_dont_cross_non_consumer_markers, test_topological_siblings_share_y_or_symmetric, test_section_bbox_has_bottom_padding, test_off_track_inputs_above_consumer). Each row pairs the rendered fixture SVG with a red-bbox overlay on the offending element, a plain-English "Supposed issue" + "What to check" explanation, and bug / not-a-bug / ambiguous triage buttons whose state is saved in localStorage and exported as JSON. Use when the user asks to "triage layout invariants", "review xfails", "review failing layout tests", "build the triage page", or generally wants to triage the layout-invariant test suite by eye. when-to-use: The user wants to walk through every failing or xfailing case in tests/test_layout_invariants.py and classify each one as bug / not-a-bug / ambiguous - typically before deciding which invariants to fix in the engine vs. which to relax in the test. Trigger phrases include "triage layout invariants", "review xfails", "review failing layout tests", "triage tool", "build the triage page", or any mention of triaging the nine invariants listed in the description.

nf-metro layout-invariant triage

This skill packages the triage tool that produces a single self-contained HTML page (with embedded SVGs, red-bbox overlays, explanations, and localStorage triage state) for every failing or xfailing case in tests/test_layout_invariants.py. It grew out of the xfail-review session for PR #326.

What it covers

The tool produces one card per (fixture, invariant) pair for these nine invariants:

  • test_label_x_anchored_to_station_marker_on_horizontal_runs
  • test_visual_stack_station_xs_share_column
  • test_row_trunk_marker_cy_consistent
  • test_no_kink_at_section_boundary
  • test_symfan_pairs_share_y
  • test_lines_dont_cross_non_consumer_markers
  • test_topological_siblings_share_y_or_symmetric
  • test_section_bbox_has_bottom_padding
  • test_off_track_inputs_above_consumer

If new invariants are added to the suite, the script will still surface them in the page but without a tailored explanation block; add a new finder + explanation entry in build_review.py to give them a structured highlight.

Triaging ad-hoc / one-off checks

You do not have to add a committed (x)failing test plus a find_* finder just to eyeball a candidate check. Two escape hatches drive the page from an arbitrary provider:

--violations <file.json> (render cards straight from JSON)

Pass a JSON file shaped like:

[
  {
    "fixture": "rnaseq_sections.mmd",
    "invariant": "label_vs_foreign_line",
    "rects": [
      {"x": 120, "y": 80, "w": 60, "h": 24, "note": "label grazes the qc line"}
    ],
    "issue": "The label sits on top of a line it doesn't belong to.",
    "check": "If the line strikes through the text it's a bug; a graze is Ambiguous."
  }
]
python .claude/skills/nf-metro-layout-triage/build_review.py \
    --worktree "$PWD" --output-dir "$OUT" \
    --violations /tmp/my-candidate-check.json

Each rect becomes a red dashed overlay on the rendered fixture. The optional issue / check strings (HTML allowed) become the "Supposed issue" / "What to check" blocks. This path skips pytest discovery and the INVARIANT_FINDERS registry entirely - the only engine work is rendering the fixture SVG.

--finder-module <path-or-dotted-name> (register a finder at runtime)

Point the tool at a small Python module exposing a FINDERS dict ({invariant: callable(graph, engine) -> list[violator-dict]}) and/or an EXPLANATIONS dict ({invariant: (issue_html, check_html)}):

# /tmp/my_finder.py
def find_offenders(graph, engine):
    return [{"kind": "rect", "x": ..., "y": ..., "w": ..., "h": ..., "note": "..."}]

FINDERS = {"my_candidate_check": find_offenders}
EXPLANATIONS = {"my_candidate_check": ("Issue prose...", "What-to-check prose...")}
python .claude/skills/nf-metro-layout-triage/build_review.py \
    --worktree "$PWD" --output-dir "$OUT" \
    --fail-list /tmp/my-faillist.txt --finder-module /tmp/my_finder.py

These are merged over the built-in registries, so the nine built-in invariants keep working unchanged. A violator-dict uses the same shape the built-in finders emit (kind, x, y, w, h, note).

Generic explanation fallback

When an invariant has no tailored explanation (no *_info block on its violators and no matching EXPLANATIONS entry), the card simply renders the overlay and notes with no prose block - which is the graceful generic path. Supply issue / check in the violations JSON, or an EXPLANATIONS entry in the finder module, to add prose; otherwise the red overlay plus the per-rect note strings are enough to classify by eye.

Recipe

Assume an nf-metro checkout at $PWD (or a worktree off it) and the nf-metro micromamba env is available.

  1. Activate the env and pin PYTHONPATH to the worktree's src/ (the script does not require a pip install, just an importable engine):

    source ~/.local/bin/mm-activate nf-metro
    export PYTHONPATH="$PWD/src"
    
  2. Pick an output directory outside the repo to keep generated SVGs out of git, for example /Users/jonathan.manning/projects/nf-metro-triage or /tmp/triage-<task>:

    OUT=/Users/jonathan.manning/projects/nf-metro-triage
    mkdir -p "$OUT"
    
  3. Run the build script. By default it invokes pytest itself to discover the FAILED/XFAIL set, then renders each fixture and writes index.html plus renders/:

    python .claude/skills/nf-metro-layout-triage/build_review.py \
        --worktree "$PWD" \
        --output-dir "$OUT"
    

    If you already have pytest output captured in a log file (e.g. pytest tests/test_layout_invariants.py -rfX --tb=no -q > /tmp/inv.log), pass it via --fail-list /tmp/inv.log to skip re-running the suite.

  4. Serve the output. The HTML is self-contained (SVGs are inlined as base64) but a local server makes loading and JSON export reliable:

    cd "$OUT" && python -m http.server 8765
    

    Then point the user at http://localhost:8765.

  5. Triage in the browser. For each row pick Bug, Not a bug, or Ambiguous and optionally add a note. The state persists in localStorage per browser. When done, click Export JSON in the page header - this downloads xfail-review-tags-<timestamp>.json containing {key: {tag, notes}} keyed by <fixture>__<invariant>.

  6. Clean up when the triage is finished:

    # Stop the http.server (Ctrl-C) and optionally
    rm -rf "$OUT"
    

How the script works (in brief)

  • --worktree is added to sys.path, so the script imports the parser, layout engine, routing, labels, and SVG renderer directly from that checkout. No pip install required.
  • Each fixture is rendered once via the nf-metro CLI (cached in <output-dir>/renders/<fixture>.svg) and laid out once via compute_layout().
  • For each invariant the script runs a finder that re-derives the offending geometry (port Y, trunk marker cy, fan column, label X, etc.) and emits a red dashed rectangle into a per-row annotated SVG (<output-dir>/renders/annotated/<key>.svg).
  • The HTML embeds the annotated SVG as base64 data URI, so the page is fully portable - you can drop index.html anywhere and it still works (although the page also references renders/ for debugging).
  • If pytest is rerun later, the rendered SVGs are cached - delete <output-dir>/renders/ to force a fresh render.

Output anatomy

<output-dir>/
  index.html                       # the page to open
  fail-list.txt                    # raw pytest output (only when --fail-list not passed)
  renders/
    <fixture>.svg                  # base render per fixture (cached)
    annotated/
      <fixture>__<invariant>.svg   # base + red overlay per row

The exported triage JSON lands in the user's browser Downloads folder; it is not written by the script.

When the explanation is generic

If the invariant fires inside the test harness but the embedded finder cannot reproduce the offending element (e.g. the test relies on a slightly different layout-param path), the row shows a yellow "no offending element" note plus a generic invariant-level explanation. Treat those as "no red highlight, classify from the whole render".

See also

  • tests/test_layout_invariants.py - the actual assertions the page mirrors.
  • tests/layout_validator.py - lower-level programmatic checks used by other test files.
  • PR #326 (feat/comprehensive-invariants) - the original session this tool was built for.
Install via CLI
npx skills add https://github.com/pinin4fjords/nf-metro --skill nf-metro-layout-triage
Repository Details
star Stars 77
call_split Forks 5
navigation Branch main
article Path SKILL.md
More from Creator
pinin4fjords
pinin4fjords Explore all skills →