Skip to content

feat(ci): pre-commit hook to render metro maps - #1243

Open
adamrtalbot wants to merge 1583 commits into
mainfrom
feat/metro-map-precommit-hook
Open

feat(ci): pre-commit hook to render metro maps#1243
adamrtalbot wants to merge 1583 commits into
mainfrom
feat/metro-map-precommit-hook

Conversation

@adamrtalbot

@adamrtalbot adamrtalbot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

What

Adds .pre-commit-hooks.yaml — an nf-metro hook so consumer repos re-render their metro-map SVG locally on commit.

Usage

# consumer .pre-commit-config.yaml
- repo: https://github.com/seqeralabs/nf-metro
  rev: 1.1.0
  hooks:
    - id: nf-metro

Details

  • language: python installs this repo (hatchling) into an isolated env, exposing the nf-metro script.
  • Matches *.mmd; writes the sibling .svg (assets/metro_map.mmdassets/metro_map.svg). Consumers can override args (e.g. add --mode).
  • Docs: appends the Pre-commit hook section to the CI & automation page.

Verification

pre-commit try-repo . nf-metro --files metro_map.mmd: installs nf-metro from this repo, runs the hook (Passed), produces the sibling SVG.

pinin4fjords and others added 30 commits June 28, 2026 17:28
fix(docs): convert absolute internal links to relative; exclude releases from validator
…ator

The starlight-links-validator explicitly rejects relative links, so the
prior fix (PR #1152) was wrong. Revert all docs files to absolute
/nf-metro/ links and instead configure the validator to skip any link
that starts with /nf-metro/ but not with the current build base.

Versioned/dev builds use a different base (e.g. /nf-metro/dev/), so
/nf-metro/xxx/ links resolve to pages outside the current build. The
production build (base=/nf-metro/) validates them normally, so skipping
them in dev/versioned builds is safe.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fix(docs): exclude cross-base links from validator; revert relative links
…ctions

Under a tightened fold_threshold a convergence sink folds onto a lower row of
its branches' column, fed through a TOP entry. Each upper branch's BOTTOM-exit
feeder dropped straight down the exit column, ploughing through the intervening
branch boxes away from any port.

Add a dispatch rule, ahead of the plain TB bottom-exit drop, for a feeder whose
straight drop would cross an intervening section: it diverts out into the clear
inter-column gap left of the column, descends past every stacked box, and turns
into the TOP entry. Adjacent feeders with no box in the way keep the straight
drop. Refine check_perp_exit_over_leadin_clears_only_spanned_sections so its
overshoot test exempts a same-row section no deeper than the source (the
corridor must clear the source's own depth regardless), which the divert needs.

Fixes #1148

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… fixture

Add examples/topologies/convergence_sink_fold.mmd (a fold that stacks parallel
branches in one column and drops the shared sink onto a lower row via a TOP
entry) to the gallery and corpus, plus a route-around invariant test over the
multi-line repro at folds 1-3 and the committed single-line fixture. Regenerate
the guard golden (additive: the new fixture's trace), the routing gate-coverage
baseline + matrix (the divert's crossing predicate now exercises an arm the
baseline listed as a gap), and prune the now-covered triage entry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts:
#	docs/dev/routing_gate_coverage.md
Docs author internal links with the production `/nf-metro/` base, but
versioned deploys build with a different base (`/nf-metro/dev/`,
`/nf-metro/latest/`, `/nf-metro/<version>/`), so those links pointed at
pages that do not exist on those deploys - broken at runtime and flagged
by starlight-links-validator on the deploy build.

Add a remark plugin that rewrites the `/nf-metro/` prefix to the active
build base. It runs before the validator's rehype pass, so links resolve
correctly at runtime on every base and the validator checks them against
real pages instead of being told to skip them.

With links resolving, drop the cross-base exclusion: the validator now
validates all internal cross-references (including the releases pages,
which are real content entries). The only remaining exclusions are the
gallery/ and pipelines/ custom Astro routes (opaque to the validator)
and the live_demo.mp4 static asset.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Build the Astro site on PRs that touch docs or the site so
starlight-links-validator runs and fails the PR on a broken internal
link, instead of the breakage only surfacing when the post-merge deploy
builds and fails.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fix(docs): rebase internal links to the build base so link validation works on every deploy
fix(routing): folded convergence-sink corner + route-around (#1142, #1148)
…#1144)

Under a tightened fold_threshold, distinct lines reaching a horizontal-flow
section through a shared TOP/BOTTOM entry (one single-line feeder per line,
all stacked on one column trunk) collapsed onto the port's single trunk X:
the converging approaches overlaid one vertical channel and any shared run
through the section drew as a zero-offset collinear bundle.

Detect this distinct-line perp entry (needs_perp_approach_fan) and:
- skip the entry-port offset collapse so the lines keep distinct slots and
  any shared run separates;
- fan each line's approach X by its cross-boundary bundle index
  (_perp_approach_fan_x), shared by the inter-section feeder drop and the
  intra-section drop so they meet on one per-line channel.

Excludes redundant same-bundle feeders (each whole bundle keeps its feeder
lane) and vertical-flow consumers (their shared run separates in X already).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- detect horizontal-flow sections via lanes_run_along_y rather than a raw
  ("LR","RL") direction tuple
- factor the shared bundled-feeder index lookup out of _perp_approach_fan_x
  and _perp_entry_crossing_x into _bundled_feeders

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…trix

scripts/routing_gate_coverage.py wrote the matrix doc with a bare H1 and no
frontmatter, so each regenerate stripped the title/description the Starlight
content collection requires, failing the docs link-validation build. Emit the
frontmatter block from the generator and regenerate the committed doc.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… do not cross

The distinct-line fan slotted approach channels and the intra-section bundle by
raw bundle index, which under a section reflection placed the outermost feeder
(the one wrapping around the intervening boxes from furthest away) on the inner
channel. Its riser then crossed its bundle-mate to reach that channel, and the
shared run drew it on the wrong side.

Order both by approach instead: the feeder descending from furthest away (the
around-stack wrap) takes the outermost channel and the top bundle slot, the
trunk-near feeder stays on the column trunk. The descent, the turn, and the
shared run now agree, so the lines stay on their own side throughout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- read the lone bundled feeder via next(iter(...)) rather than max() over a
  0-or-1 element list
- note the source-Y ordering coupling between _slot_perp_fan_bundle and
  compute_bundle_info that the approach fan relies on
- drop a stray trailing comma in the feeder sort

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace text-only tile grids with inline <Metro purpose="showcase">
renders grouped by category (gallery) or per pipeline. Each entry
heading links to its dedicated page for full-screen viewing.

[skip ci]
…l-collapse

fix: fan distinct lines onto parallel channels at a folded perp entry (#1144)
Pass explicit headings to <StarlightPage> and add matching id attributes
to h2 elements so Starlight's right-hand panel links to each category /
pipeline section.

[skip ci]
feat(docs): render maps inline on gallery and pipelines index pages
…or_scheme parity fix (#981)

Introduce RenderConfig to group the render-side kwargs on render_string,
making the embedding API easier to extend. Add a shared _render_graph
private helper that both render_string and the CLI now delegate to,
eliminating the duplicated font_portability/render_svg/render_html branch
in cli.py. Also close the self_color_scheme parity gap: render_string was
silently hardcoding True; it is now an explicit kwarg (default True) that
maps to --no-self-color-scheme on the CLI.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ny to satisfy mypy [skip ci]

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…n on shadowed kwargs

Address review nits on the RenderConfig change:

- embed_basename uses a plain string default instead of field(default=...);
  drop the now-unused dataclasses.field import.
- Promote _render_graph to a public render_graph: render_string's docstring
  already points advanced callers at prepare_graph + the renderer, and this
  is exactly the render half they need without re-parsing. cli.py imports the
  public name.
- render_string now warns when config= is passed alongside non-default flat
  render kwargs, so the "pass one or the other" contract is enforced rather
  than only documented.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Anchor the pytest.warns regex on `ignoring [...'responsive'` rather than a
bare substring so it asserts the kwarg appears in the ignored list.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
refactor(api): RenderConfig dataclass + self_color_scheme parity fix
…1143)

Under a tightened fold_threshold an inter-section fan from a section's LEFT
exit into a relocated section's RIGHT entry built a bundle-order flip plus a
non-concentric corner. Two coupled defects:

- compute_station_offsets desynchronised the bundle: a reconvergence section
  fed by a single multi-line feeder whose lines originate at separate
  single-line producers has no well-defined delivered order (the producers each
  sit on a local slot 0, so two lines collide on one offset). Settling the
  section on that ambiguous, hash-seed-dependent order desynchronised its exit
  port from the relocated section's entry port, forcing the staircase to cross.
  Leave such a section on its priority order instead.

- the staircase handler always built the per-leg fan, which sizes each line's
  half-corners independently and pinches the bundle when the descent is too
  short for two corners to fit. A rigid bundle now takes the concentric builder
  there, sizing the merged bend wholesale.

Adds examples/topologies/fold_left_exit_right_entry.mmd (minimal repro, in the
gallery), a regression test over it plus the epitopeprediction motivating case,
and the guard golden baseline entry for the new fixture.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ry (#1161)

A folded vertical-flow (TB/BT) section that exits LEFT/RIGHT into a
relocated section one row below aligns its exit port to the target's
entry Y in Stage 3.4.  When the target spans several sub-rows its entry
Y keeps descending as those sub-rows settle (Stages 6.13-6.15), after
the exit was aligned, so the exit ends a sub-row above the final entry
and the inter-section run renders with a jog.

Follow the exit down to the now-settled entry at Stage 6.15a, before the
structural-extent snapshot, so the snapshot and the next relay's
inter-row cascade both see the lowered exit.  Only a target that settled
along the flow from the exit is followed; one seated against the flow
keeps its own descent (an intentional staircase), and the bbox-contained
alignment target keeps the snapped exit inside its section.

Adds a runtime guard (_guard_fold_lr_exit_follows_target) and a
parametrised layout invariant covering the whole multi-section corpus.
Reframes the fold_left_exit_right_entry regression to assert the run is
straight (the offset-desync bundle-order guard still applies).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…#1161

The fold LEFT-exit -> RIGHT-entry connector no longer descends through a
short staircase: #1161 aligns the exit to the target's settled entry, so
the run is straight (descent 0).  No corpus fixture reaches the
short-descent (0 < descent < 2*radius) rigid arm of
_route_left_exit_right_entry_step; only the long-descent staircase
(tb_left_exit_step) routes there, via the per-leg fan.  Remove the
unreachable concentric branch, restoring the per-leg-only handler.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
adamrtalbot and others added 12 commits July 1, 2026 11:32
chore(docs): point root redirect at latest/ instead of dev/
Note the single-branch clone option and why gh-pages history exists
and gets manually squashed rather than continuously rewritten.

Ref #1240
The site root now redirects to the latest stable release rather than the
in-progress dev build, but the README's documentation links still pointed
at the dev/ version segment.
docs: document gh-pages clone size and history squash
docs: point README doc links at latest/ instead of dev/
render took exactly one positional INPUT_FILE. A pre-commit hook (or
any batch caller) staging more than one .mmd file in a single
invocation had no way to render them together short of hand-rolling a
manifest for render-many, which only accepts named per-job options
(theme, mode, embed_font, ...) rather than passing shared CLI flags
through generically.

INPUT_FILE is now INPUT_FILE... (nargs='+'). Given more than one file,
all render within the same process (amortising interpreter/import
startup, like render-many), each write to their own sibling
<input>.<format>, every file is attempted even if an earlier one
fails, and the command exits non-zero if any failed without dropping
the successful outputs. -o/--output is rejected when more than one
input is given, since a single explicit path can't serve multiple
outputs.

Single-file behavior is unchanged: -o still targets that exact path,
a failure still raises immediately with the original error message,
and the existing "Rendered N stations, N edges, N lines -> path"
success message is preserved (the [i/total] OK/FAIL summary lines
only appear once there's more than one file).
Every gallery and pipelines page (index and per-entry, now that #1103's
dynamic routes exist) gets a distinct 1200x630 Open Graph preview pairing
its title/description with the page's own metro map, rendered with baked
colours and composited via satori + sharp. Other docs pages fall back to a
site-wide default image. Also wires README hero regeneration into the
publish workflow so the hero renders track engine changes automatically
instead of needing a manual PR each time.

Fixes #1105
render's new multi-file loop only caught click.ClickException, so any
other exception escaping a file's render (an OSError from an
unwritable output directory, a UnicodeDecodeError, an unanticipated
layout bug) aborted the whole batch instead of continuing to the
remaining files -- contradicting the "every file is attempted even if
an earlier one fails" guarantee. render-many already handled this
correctly with a bare except Exception; the narrower catch in render
was an avoidable regression relative to that existing pattern in the
same file.

Extract a shared _run_batch(items) helper (attempt every item, print
[i/total] OK/FAIL, catch any exception, raise one ClickException with
the failure count) and use it from both render's multi-file path and
render-many, instead of two independently-drifting copies of the same
loop with a duplicated failure-summary string.

Go further and have render-many's per-job body call _render_one
(the single-file render logic extracted for render's multi-file
support) instead of carrying its own ~70-line duplicate of the
parse/theme/render/write sequence. This also fixes a latent gap:
render-many never passed source_dir to prepare_graph, so a relative
%%metro logo:/file: path in a manifest job resolved against the
caller's cwd instead of the job's own input directory; render always
passed it. source_dir resolution tries the path as-is first and only
falls back to source_dir, so this cannot change behavior for a path
that already resolved (confirmed against examples/rnaseq_sections.mmd,
whose logo path is used by the render-many byte-identical-output
test). render_many's own out_path.parent.mkdir(...) moved into
_render_one so render -o gains the same missing-parent-dir handling.

Verified: full suite 27099 passed / 0 failed (before and after);
manually reproduced the original bug (unwritable output dir for the
middle of 3 files) and confirmed the other two still render with the
overall exit reflecting the one failure.
feat(cli): render accepts multiple INPUT_FILEs
@adamrtalbot
adamrtalbot force-pushed the feat/metro-map-precommit-hook branch 2 times, most recently from 8a372b5 to 45b3fa0 Compare July 1, 2026 12:36
pinin4fjords and others added 2 commits July 1, 2026 13:41
- scripts/render_readme_hero.py now calls nf_metro.api.render_string
  instead of hand-recomposing parse/layout/theme/render, matching the
  shared entry point the CLI itself uses.
- Extract shared page-meta.ts (title/description) so index.astro pages and
  their og/*.png.ts siblings can't drift, and og-targets.mjs (fixed .mmd
  paths) so those three routes' maps are guaranteed to join the batched
  chrome-less pre-warm rather than coincidentally overlapping with the
  gallery/pipelines content collections.
- Extract a pure ogImageMetaTags() helper shared by astro.config.mjs's
  site-wide default and og-head.ts's per-page override, removing a
  hand-duplicated meta-tag shape.
- findOgSourceFiles() now warns on a malformed content JSON instead of
  silently treating it the same as "not generated yet".
- Truncate the OG card title (in addition to the subtitle) as a safety net
  against a future title long enough to overflow the fixed card height.
- FONT_DIR now resolves via process.cwd() instead of round-tripping
  through REPO_ROOT back into website/.
- publish.yml's readme-hero job gets a concurrency group so two releases
  published close together can't race pushing to main.
feat(ci): composite action to render metro maps
@adamrtalbot
adamrtalbot changed the base branch from feat/metro-map-reusable-workflow to main July 1, 2026 12:45
@adamrtalbot
adamrtalbot force-pushed the feat/metro-map-precommit-hook branch from 45b3fa0 to 1b30854 Compare July 1, 2026 12:47
pinin4fjords and others added 2 commits July 1, 2026 13:53
Gallery entries are internal layout-regression fixtures (177 of them) that
nobody links to externally, so a per-entry OG image for each isn't worth
the render/build/storage cost. All gallery pages now share one generic
card; only the 8 real nf-core/other pipelines get their own per-entry OG
image. Cuts generated OG images from 188 to 11 and returns build time and
output size to roughly their pre-feature baseline.
Adds `.pre-commit-hooks.yaml` exposing an `nf-metro` hook so consumer repos
re-render their metro-map SVG locally on commit. `language: python` installs
this repo (hatchling) into an isolated env, exposing the `nf-metro` script.

The hook matches `*.mmd` and writes the sibling `.svg`; `nf-metro render`
handles one or more staged files in a single invocation, so a commit touching
several maps re-renders them all. Consumers override the render flags via `args`.

Documents the hook in the CI & automation page.

Generated by Claude Code
@adamrtalbot
adamrtalbot force-pushed the feat/metro-map-precommit-hook branch from 1b30854 to 8fd485c Compare July 1, 2026 12:55
Keeping this PR scoped to the OG image feature - the release-workflow
automation (auto-committing regenerated hero renders to main on every
release) needs more thought before shipping, particularly around branch
protection compatibility and the concurrency/push-race handling.
pinin4fjords and others added 5 commits July 1, 2026 14:06
feat(docs): build-time OG image generation per gallery/pipeline page
Pipeline maps are usually landscape, so a left-side text panel squeezed
the map into a narrow vertical strip. Move the title/description to a
fixed-height band across the top instead, giving the map the full card
width below it. Also swap the text "nf-metro" wordmark for the real
logo asset, with its CSS custom-property colours baked to concrete
values since satori can't resolve them.
.sidebar-pane is position:fixed and always spans the full viewport
height below the nav, but .main-frame had no flex-grow, so on a page
short enough that main content + footer fit within one viewport, the
footer sat directly below the content instead of at the viewport
bottom - leaving a gap where the fixed sidebar's border-inline-end
still rendered underneath. Pre-existing (reproduces on gallery pages
too), not introduced by the OG-image work.
refactor(docs): OG card layout, real logo, sticky-footer fix
@pinin4fjords
pinin4fjords force-pushed the main branch 2 times, most recently from a90d751 to d134b08 Compare July 28, 2026 11:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants