Skip to content

Feature/hola modular implementation - #1

Open
gabry-at-mermaid wants to merge 129 commits into
developfrom
feature/hola-modular-implementation
Open

gabry-at-mermaid wants to merge 129 commits into
developfrom
feature/hola-modular-implementation

Conversation

@gabry-at-mermaid

Copy link
Copy Markdown

📑 Summary

Brief description about the content of your PR.

Resolves #

📏 Design Decisions

Describe the way your implementation works or what design decisions you made if applicable.

📋 Tasks

Make sure you

  • 📖 have read the contribution guidelines
  • 💻 have added necessary unit/e2e tests.
  • 📓 have added documentation. Make sure MERMAID_RELEASE_VERSION is used for all new features.
  • 🦋 If your PR makes a change that should be noted in one or more packages' changelogs, generate a changeset by running pnpm changeset and following the prompts. Changesets that add features should be minor and those that fix bugs should be patch. Please prefix changeset messages with feat:, fix:, or chore:.

knsv and others added 30 commits September 3, 2026 14:20
…nd agentflow diagrams

State, usecase, and agentflow diagrams get their own `wrappingWidth`, matching the
flowchart option, and all four diagram types get a new `minNodeWidth`. Both default
to 120 (flowchart's `wrappingWidth` was 200).

`wrappingWidth` caps the label box; `minNodeWidth` widens labels narrower than it so
nodes with short text render at a uniform width, with the node's own padding added
on top. The minimum is applied in `createText` for HTML labels (the label div is
sized in the DOM so measurement and rendering agree) and in `labelHelper` for the
SVG-text path, so every shape honours it without per-shape changes. Each diagram's
db stamps `wrappingWidth`/`minWidth` on its nodes; actors in usecase diagrams and
group nodes are excluded, and an explicit per-node width wins over the minimum.
Several associations ending on one side of a use case had their markers
stacked on a single point. Three changes, scoped to use-case diagrams
except where noted:

- Ovals get the same vertical padding as horizontal (160x38 -> 160x58), so
  a side has room for more than one marker.
- Use-case ovals set a `spreadPorts` hint; the ELK renderer maps it to
  CENTER port alignment on that node, which spreads ELK's implicit ports as
  far as the side allows. Node-level `portPort`/`portsSurrounding` are
  ignored for implicit ports (measured with elkjs 0.9.3), so alignment is
  the only lever.
- The ELK renderer read end-marker offsets from a private table that only
  knew `arrow_point`, so for extension/circle/cross markers the short
  on-border stub an off-centre port leaves outside a curved outline was
  kept, and the marker pull-back ran past it and flipped the arrow. The
  offsets now come from `utils/lineWithOffset`, and the stub removal is
  applied at the start of a path as well as the end.
Shapes position their label with `pos - (bbox.x - (bbox.left ?? 0))`, which
relies on SVG text's getBBox() rect having no `left` (it is centred on x=0)
while an HTML label's DOMRect does. The box widened to `minNodeWidth`
fabricated `left`/`top` for both kinds, so every SVG label narrower than the
minimum lost its centring correction and slid left by half its width in the
~30 shapes that use the idiom. The widened box now keeps the kind it was
given, and SVG text stays centred as it grows. Adds a dev fixture covering
the affected shapes with htmlLabels: false.
`build:types` failed on three counts introduced with the `wrappingWidth`
and `minNodeWidth` config, taking lint, unit-test, e2e, build-docs and
autofix down with it since they all run it during install:

- `stateDb`'s local `NodeData` never declared `wrappingWidth`/`minWidth`,
  which `getData` assigns.
- `withMinWidth` reads `bbox.width` in the branch guarded by
  `!('left' in bbox)`. `getBBox()` returns an SVGRect without `left`, but
  the DOM typings widen it to DOMRect, so TypeScript narrows that branch
  to `never`. Read the geometry before the check instead.
- `config.usecase.spec` compiled a subset schema holding only
  `BaseDiagramConfig`, so ajv could not resolve the new `$ref`s into
  `FlowchartDiagramConfig`. Compile against the whole `$defs` block and
  cover the two new keys in the defaults, required list and key order.
The vitepress docs build failed with 8 dead links, for the state and
agentflow pages of both properties.

jsonschema2md only resolves a `$ref` to a definition that appears earlier
in the schema file. `FlowchartDiagramConfig` sits after `StateDiagramConfig`
and `AgentflowDiagramConfig`, so those two forward refs were left pointing
at a page the generator never emitted. The same refs from
`UsecaseDiagramConfig` resolve because it is declared after the flowchart
config; the other `$ref`s on the state config all point backwards at
`BaseDiagramConfig` or `GitGraphDiagramConfig`, which is why only these two
broke.

Inline the property definitions for the two configs that precede the
flowchart one, matching how `nodeSpacing` and `rankSpacing` are already
written per diagram. `config.type.ts` is byte-identical, since the type
generator resolved the refs inline anyway.
`minNodeWidth` defaults to 120, so nodes with short labels are widened and
the diagrams these tests measure are wider than the bounds written before
the option existed. The bounds are updated to the measured widths, keeping
each file's existing tolerance.

State (`[*] --> State1 --> [*]`) goes from ~75 to 152: the label area is
widened to 120, plus the node's padding of 8 a side and the viewbox padding.
The hand-drawn flowchart goes from 446 to 644, where `C{Let me think}` has
three short-labelled children that each widen to 120.

These shards have not run since the option was added -- the type build failed
during install, so only shard 1 ever reported.
Lint the code that ships in the bundles (`packages/*/src`), with `compat/compat`.

Safari 15.4 is the floor that we're not officially supporting in Mermaid
v11, but is one we're trying to support as a rough guideline.
Node labels now state the colour the test expects ("nodes should be red"),
and the first case passes a yellow primaryColor through the render config so
the snapshot shows the directive's red taking precedence over config.
Mermaid now requires browsers that support ES2024.

We're targeting browser support for Safari 17.4, since Safari is the
main browser that tends to be out-of-date, due to iOS/MacOS devices not
being to upgrade to new iOS/MacOS versions.

Chromium v121 and Firefox 123 have also been added as targets, since
they're on the `baseline-widely-available` right now (just like Safari
17.4 in a few days), but we're not commited to supporting them, as it's
unlikely people are stuck on those browsers.

We're also officially declaring a Node.JS v22.12+ requirement for users
that install mermaid for build tools. This is mainly to avoid `npm
install` errors for our users, as we're a browser-based library and we
don't actually support running mermaid in Node.JS.

- esbuild now targets ES2024 and Safari 17.4
- TypeScript targets ES2024 with ES2024 lib; class-field semantics are pinned to
  the legacy behaviour so object shapes do not change in this PR
- root and published package.json files declare engines.node >= 22.12.0
- fix an invalid `a?.b = c` assignment in architecture.spec.ts that the old ES2018
  lowering used to hide

BREAKING CHANGE: Mermaid now requires ES2024 and Safari 17.4 or later.
                 Although Mermaid is a browser library, Mermaid also
                 declares a Node.JS v22.12+ requirement so it can update
                 using dependencies that also require Node.JS v22.12+,
                 without causing `npm install` errors for users.
…ri.17.4

Conflicts:
	packages/mermaid-layout-elk/package.json
…slint-plugin-compat

Fixes the failing unit tests.
…ri.17.4

Fixes a merge conflict in `package.json` that GitHub seems to see, but
git CLI can solve without conflicts.
`**/vitepress/**` and `**/scripts/**` are unnecessary, since the only
thing they affect are in `packages/mermaid/src/docs`, which `**/docs/**`
already handles.

I've also ignored `**/__mocks__/**` since that is currently included,
but is only needed for vitest.
Use a `v<MERMAID_RELEASE_VERSION>+` tag to specify that the ES2024
support will only happen in the next version.

I've also added `ios_saf` and `edge` to our `browserlist`, even though
they're identical to `safari` and `chrome`, so it matches our ESBuild
`target`.
The gantt fixtures are dated around 1010-10-10 and three of them exercise
the today marker, which the renderer places at new Date(). The inline spec
they were migrated from installed a fake clock, but the mmd snapshot runner
did not, so the marker rendered far outside the chart and the show/hide
fixtures produced identical screenshots.
…lugin-compat

build: lint for WebKit 15.4 with eslint-plugin-compat
…ri.17.4

[v12] chore!: require ES2024, Node.js 22.12, and Safari 17.4
…all three preview packages

**The warning.** ELK's `ARROW_MAP` put the edge *type* `arrow_open` — which
means "no arrowheads" — into the arrow-*type* slot, where `none` is the value
`addEdgeMarker` reads as a deliberate absence; anything else it has no marker
for is reported as unknown. Diagrams whose db sets `arrowTypeStart` itself
(flowchart sets `'none'`) never reached that fallback, so this only showed up
on diagrams that leave it unset — state diagrams, which warned once per edge.

Rendering is unchanged. Both spellings produced no start marker, and neither
appears in `markerOffsets`/`markerOffsets2`, so `markerPathOffset` returns 0
either way and no geometry moves. Only the log noise goes away.

**The publish workflow.** It published `packages/mermaid` alone, so a preview
could not be exercised the way users actually combine these: ELK bundled in
mermaid, and ELK registered onto the tiny build that omits it. It now also
publishes `@mermaid-js/layout-elk` and `@mermaid-js/tiny`, all three sharing
one preview version so they install as a matching set.

Both are already org-scoped, so unlike `mermaid` they need no rename. Their
`mermaid` peer range is dropped from the preview artifact: the preview mermaid
is published under a different name (`@mermaid-js/mermaid`), so that range can
never be satisfied and would only emit an unmet-peer warning on every install.

Found by a standalone browser harness that loads the packed artifacts and
checks ELK/dagre in full mermaid, the dagre fallback in tiny, and registering
the plugin onto tiny. The new unit test fails without the arrow-map fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…cation

Three standalone HTML pages that load the *packaged* artifacts from
node_modules and drive them in a real browser, so they exercise what consumers
install rather than the source tree:

- a) full mermaid runs ELK and dagre, ELK is the default, `elk.*` variants work
- b) tiny, which omits ELK, still renders flowchart and state via dagre
- c) registering @mermaid-js/layout-elk onto tiny actually gets you ELK

`verify.mjs` serves the directory, runs all three headless, prints PASS/FAIL
per check and exits non-zero on failure. `setup.mjs` packs the three packages
from a checkout — the same tarballs `npm publish` uploads — or fetches
published previews with `--from-registry`.

Registered as a workspace package like tests/webpack, so playwright comes from
`pnpm install`, and added to the changeset ignore list alongside the other
private test packages.

Two deliberate choices, both documented in the README because both were
learned the hard way here:

- Tarballs are extracted rather than `npm install`ed. The bundles are
  self-contained so they need no resolution, and installing a `file:` tarball
  whose version has not changed silently serves npm's cached copy — which
  means quietly verifying a stale artifact.
- "Did ELK actually run?" is answered by comparing node `transform` attributes
  between engines, never by trusting config, so a silent fallback cannot pass
  a check that claims ELK was used. Warnings are attributed per render for the
  same reason; that is what surfaced the `arrow_open` warning fixed here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…_internalsDoNotUse

**Docs.** `config/layouts.md` now opens with what changed in v12: ELK is part
of `mermaid` and is the default, the main package always includes it, and the
tiny build is the supported way to have Mermaid without it —
`@mermaid-js/layout-elk` stays published so tiny can opt back in.

`config/usage.md` gains an "ELK and the tiny build" section explaining why tiny
omits ELK, that diagrams asking for it fall back to Dagre rather than failing,
and how to register the package on tiny, with snippets for both the script-tag
and bundler cases.

The seven diagrams whose layout now defaults to ELK — flowchart, class, ER,
requirement, state, use-case, agentflow — have their "Default theme and look"
section renamed to "Default theme, look and layout", naming ELK as the default,
and their "previous appearance" example now pins `layout: dagre` alongside
`theme: default` and `look: classic`. Without that the example no longer
reproduced the pre-v12 rendering. `swimlanes`, `sequenceDiagram` and `venn`
have the same section but are not ELK-laid-out, so they are untouched.

`defaultAppearanceDocs.spec.ts` caught the rename, which is what it is for. It
now accepts either heading and additionally requires the layout to be pinned
back on ELK-laid-out pages — and requires it *not* to be pinned on the three
that are not, so the two sets cannot drift.

**Exports.** `createCommonLayoutRenderer`, `defaultMeasureLayout`,
`paintLayoutData`, `clearLayoutRenderState` and `applyLineJumpsToSvg` are no
longer top-level named exports; they live on `__internalsDoNotUse`, documented
as outside SemVer.

They reach straight into the renderer, so they change whenever it does — a new
node shape or a change to edge painting alters their signatures — and as named
exports every such change counted as breaking Mermaid's public API. Removing
them outright was the other option, but `community/layout-makers-guide.md`
documents building an external layout on `createCommonLayoutRenderer`, so the
capability is advertised and third-party layouts would lose it. Hiding them
keeps the capability and drops the SemVer promise, which was the actual
concern. Guide updated with the new name and the caveat.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t striped

The hand-drawn fill drew 4px strokes 5.2px apart. Up close that is a texture;
at the size a diagram is normally read it is diagonal banding, and the shape
looks striped rather than filled. Strokes are now 1.5px at 1.5px spacing, so the
fill reads as an even tint and the drawn edge quality is what carries the look.

`solidStateFill` moves the same way. That is the striped fill behind state
diagram start, end and fork markers, which now come out close to solid — which
is what UML asks for there anyway, so the old stripes were arguably wrong.

Swimlane lane titles pinned `fillWeight: 3` locally and so ignored the shared
value entirely. They now inherit it, which is the point of the shared default.
Lane bodies are unaffected either way: they pass `fill: 'none'`.

Note this is a default, not a global. `userNodeOverrides` merges caller options
last, so the shapes that opt out — note, classBox, erBox, hourglass and the rest
that set `fillStyle: 'solid'` with `roughness: 0` — are untouched.

One property worth knowing: 1.5px strokes at 1.5px spacing sit near the
resolution floor, so the texture survives at full size and blurs to a flat tint
when a diagram is scaled down. The previous coarser values downscaled without
losing the hatch. That is the trade being made.
…ltRenderer

`defaultRenderer` in the `flowchart`, `class` and `state` config sections now
accepts `dagre`, and that is its default. `dagre-wrapper` and `dagre-d3` remain
as aliases selecting the same unified renderer, so existing configuration keeps
working untouched.

`defaultRenderer` picks the rendering engine, not the layout algorithm: a
diagram with `defaultRenderer: dagre` is still laid out by ELK, the v12 default,
unless `layout: dagre` is set as well. The two options were easy to confuse
while one of them was named after a layout library.

One behaviour change falls out of the aliasing: because `dagre-d3` now resolves
to `dagre`, `state: { defaultRenderer: 'dagre-d3' }` no longer selects the
legacy state renderer. State diagrams render through the unified renderer
whenever a renderer is configured.

Resolution lives in `diagram-api/defaultRenderer.ts` rather than being repeated
across detectors, so the three diagram families that expose the option agree on
what the names mean.
@aloisklink has opened mermaid-js#8223 against the same branch, removing
`clearLayoutRenderState`, `createCommonLayoutRenderer`, `defaultMeasureLayout`,
`paintLayoutData`, `applyLineJumpsToSvg` and the `CommonLayout*` types
outright. That is the option he originally proposed and he owns the call, so
this PR steps out of the way rather than editing the same export block and
guaranteeing a conflict.

Reverts `mermaid.ts`, the elk guard spec, the layout-makers guide section and
the changeset. The docs work in this PR — ELK bundling, the tiny build, and the
per-diagram default sections — is unrelated and stays.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two corrections from review.

`layouts.md` framed the change as "two consequences worth knowing", which
buries the actual fact. It now states plainly that `elk` is the new default for
the seven diagrams, lists them, and gives `layout: dagre` as the way back.

`usage.md` said the tiny build "does not bundle the ELK layout engine either"
from v12, which reads as though it once did. It never has — before v12 nothing
bundled ELK, it was an opt-in `@mermaid-js/layout-elk` install. ELK now sits in
the list of things tiny does not include, with no version qualifier, and the
section says what actually changed: the main package gained ELK, so the tiny
build is now the way to get a Mermaid without it rather than simply not
installing the layout package.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ashishjain0512 and others added 30 commits September 10, 2026 10:57
…hot-tests

fix(tests): run dagre specifik SS tests
Packet rows are numbered from bit 0 on the left up to the highest bit on the
right, which suits network packets. Hardware registers are conventionally drawn
the other way round, with the most significant bit on the left and bit 0 on the
right.

The new `packet.bitOrder` option selects between the two. It defaults to
`ascending`, so existing diagrams render exactly as before; `descending` mirrors
each row within `bitsPerRow`, so the row reads from its highest bit down to
bit 0. Fields are still declared lowest bit first and keep their width, so
switching a diagram between the two conventions only means changing `bitOrder`.
Also warns when a task end value is neither a valid date nor a valid duration, which is what makes a milestone declared without a duration fail silently. Refs mermaid-js#4121
feat(packet): support descending bit numbering
…g object

The ACTOR lexer rule required the name to sit directly against the
opening @{ of a config object, via a plain (?=@{) lookahead. Any space
in between (participant Bob @{ ... }) made every ACTOR rule miss, so
the line fell through to the INVALID token and the diagram failed to
parse with a confusing error, even though the same participant without
a config object tolerates trailing whitespace fine.

Widened the lookahead to (?=\s*@{) so the name is still recognized
once whitespace is skipped by the existing same-line whitespace rule.
…g-object-whitespace

fix(sequence): allow whitespace before a participant's @{ ... } config object
Adds the hand-curated corpus the improve loop hill-climbs on, a sweep spec
over it with no floor assertion (an invalid fixture here is a target, not a
build failure), and a Playwright-driven size-capture harness since the
dev-explorer referenced in existing fixture metadata is not in this repo.
…nresolved-task-references

fix(gantt): warn when an after/until reference or a duration cannot be resolved
… gap

Targets complete_graph_k5 and edge-types, both invalid solely on
edge-parallel-segment-too-close. Loop Fixtures total 51725.31 -> 52647.31,
invalid 17 -> 16.

HOLA parks a route's legs at whichever distance the step that made them cared
about: a terminal leg at minTerminalLegLength (18), a route passing the same
node at routingClearance (12). The two constants are chosen independently and
nothing downstream compares them, so wherever a terminal leg runs alongside a
passing route the pair lands exactly 6px apart -- under validateLayout's 7px
EPS_PARALLEL_EDGE_GAP, structurally, every time.

Re-tuning either constant does not work: on edge-types the colliding legs are
20 and 18, so raising the minimum fuses them into a shared subpath instead.
This pass runs last, on final geometry, and separates only the pairs that
actually collided. It moves interior segments only -- the first and last
segment of a polyline carry the node-attached point -- and skips any shift
that would invert a neighbouring leg, so routes it cannot fix safely are left
as they were.

Theory input unavailable: the papers-query subagent is not registered in this
environment, so this rests on code evidence only and is not corpus-backed.
…ntegration

docs: add Schema3D to community integrations
Targets edge-types, which the previous commit left invalid on
edge-bend-near-endpoint. Loop Fixtures total 52647.31 -> 53607.31,
invalid 16 -> 15.

Separating two parallel runs is worthless if it parks one inside the band
validateLayout keeps clear around the side an edge attaches to: the previous
commit pushed edge-types' run from exactly 18 off its end node -- the
threshold, and safe -- to 15, trading one hard issue for another on the same
two edges.

The guard mirrors nearEndpointBandDistance, both halves of it. A run is
refused a target within EPS_ENDPOINT_BAND(18) of an adjacent terminal point
only when it ALSO overlaps that node's extent along its own axis. Dropping the
span test rejects far more than the validator would ever penalise; an earlier
attempt did exactly that and was reverted.

Spec set verified byte-identical to the pre-pass baseline: 31 failures before
and after, none new, none fixed -- the pass does not touch it.

Theory input unavailable: the papers-query subagent is not registered in this
environment, so this rests on code evidence only.
Targets life-choices and nested-subgraphs-2, both invalid solely on
edge-bend-near-endpoint. Loop Fixtures total 53607.31 -> 55378.06,
invalid 15 -> 13.

The previous commit taught the separation pass to refuse moving a run INTO the
band validateLayout keeps clear around an edge's attachment side. This is the
other half: a run already sitting in that band is moved out. life-choices had
one at 12 where 18 is required, put there by the router, not by this pass.

Runs are only ever moved directly away from the node they graze, so the
terminal leg they share a corner with can only get longer --
edge-bend-near-endpoint also fails on legs under 10, and a lift that shortened
one would trade the same issue straight back.

Order matters: lift, then separate. Lifting can bring a run alongside another,
and the separation pass that follows is what pulls those apart. Reversed, the
lift would undo the separation's work.

Spec set verified complete (1142 tests) and byte-identical to the pre-pass
baseline: 31 failures, none new, none fixed.

Theory input unavailable: the papers-query subagent is not registered in this
environment, so this rests on code evidence only.
… Fixtures

Corpus curation by the repo owner, recorded before the loop takes a baseline so
keeps and reverts have a clean tree to work against. The fixture was invalid on
three hard issues (edge-intersects-obstacle, edge-non-orthogonal,
edge-shared-subpath), so it scored 0 and its removal does not move the total --
only cases 71 -> 70 and invalid 13 -> 12.
Targets domus1, invalid on the triple edge-same-port-departure +
edge-shared-attachment-point + edge-shared-subpath. Loop Fixtures total
55378.06 -> 56348.06, invalid 12 -> 11.

Those three issues are one geometric fact reported three ways. The orthogonal
router takes a sourcePortOffset/targetPortOffset per route and defaults both to
0 -- the centre of the chosen side. Tree connectors get real offsets from
spreadPorts; core edges never do. So every core edge on one side of a node
attaches at the same point, departs in the same direction, and runs the same
minTerminalLegLength leg before diverging.

Endpoints are slid along the side they already attach to, with the bend
travelling alongside, so the terminal leg only translates and the segment past
the bend keeps its axis. The spread is 8, not EPS_SHARED_ATTACH's 3: clearing
the attachment rule alone leaves the two legs running parallel 3px apart, which
trips edge-parallel-segment-too-close instead.

Two-point straight edges are collected as PINNED rather than skipped. They
cannot be slid -- their other end is on a different node, so moving one end
tilts the edge off-axis -- but they do occupy the port, and they are usually
the reason it is contested. Skipping them entirely made the first version of
this pass a no-op on domus1: the straight edge was invisible, so nothing
collided.

Also shrinks the shared hola/ corpus specs from 31 failures to 29: 'gives every
edge touching one node its own attachment point' now passes for domus1 and for
the HOLA paper main example. No new failures.

Theory input unavailable: the papers-query subagent is not registered in this
environment, so this rests on code evidence only.
…ther

Targets 'GRAPH - hola 5 nodes loop + trees', invalid solely on
edge-shared-subpath. Loop Fixtures total 56348.06 -> 57264.06, invalid 11 -> 10.

The separation pass skipped exactly-collinear runs with a comment calling them
'a different defect with a different fix'. They are the same defect one step
further on. Two runs 6px apart read as one thick line; two drawn over each
other lose a connection entirely, which is strictly worse.

There is no gap to widen and so no side worth preserving, so the pair is
ordered by edge id. Any consistent choice draws both edges, and a stable one
keeps the layout reproducible between runs. The existing guards still apply:
a run is not moved into an endpoint band, and not past a neighbour's far end.

Spec set verified complete (1142 tests) and identical to the previous commit's:
29 failures, none new.

Theory input unavailable: the papers-query subagent is not registered in this
environment, so this rests on code evidence only.
Targets life-choices, invalid solely on edge-bend-near-endpoint. Loop Fixtures
total 57264.06 -> 58259.06, invalid 10 -> 9.

The leg-inversion guard tested a candidate position against the min and max of
the run's two neighbours. That is only correct when the run sits BETWEEN them.
life-choices' edge is a U -- down from n2, across, up to ne -- so both
neighbours are above the horizontal run, and moving it further down, which
lengthens both legs, read as crossing past one and was refused. That is why the
endpoint-band lift could never repair this fixture.

Each neighbour is now judged on its own: the leg must keep its sign and stay at
least MIN_LEG long. Strictly more permissive on U-shaped routes, no less safe
anywhere else.

Spec set verified complete (1142 tests) and identical to the previous commit's:
29 failures, none new.

Theory input unavailable: the papers-query subagent is not registered in this
environment, so this rests on code evidence only.
Targets nested-sg-outgoing-4, invalid solely on edge-corner-connection. Loop
Fixtures total 58259.06 -> 59194.66, invalid 9 -> 8.

An edge meeting a box at its corner gives the reader no cue which side it
belongs to, and the arrowhead renders across two borders. validateLayout draws
the line at EPS_CORNER(3); ports are now kept 6 clear of both ends of their own
side, using the same slide as the spread pass -- endpoint and bend together,
along the side -- so it inherits the same safety.

It runs after the spread rather than before, because spreading a contested port
is itself what pushes one out towards a corner.

The interesting case is nested-sg-outgoing-4 itself: a single 'a --> b' whose
ends land 1px above the bottom-right and bottom-left corners of two boxes. That
edge has two points, so every pass here treats it as pinned -- moving one end of
a straight edge tilts it off-axis. But its ends sit on PARALLEL OPPOSITE sides,
and moving both by the same amount keeps it straight. That is the one case where
a pinned endpoint can be repaired, and handling it is what took this commit from
a no-op to the fix.

Spec set verified complete (1142 tests) and identical to the previous commit's:
29 failures, none new.

Theory input unavailable: the papers-query subagent is not registered in this
environment, so this rests on code evidence only.
…-e2e

fix(ci): shard scoped e2e runs to avoid snapshot timeouts
…cidence

Targets both complete_graph_k4 copies, invalid on edge-same-port-departure +
edge-shared-attachment-point + edge-parallel-segment-too-close. Loop Fixtures
total 59194.66 -> 61002.66, invalid 8 -> 6.

The port-spreading pass tested for contention with a 0.5 tolerance, so it only
ever fired on ports the router had placed at identical offsets. validateLayout
draws the line at EPS_SHARED_ATTACH(3). complete_graph_k4 puts three edges on
one side of A, at 176.36, 177.70 and 190.36: the first two are 1.34 apart --
flagged by the instrument, invisible to a coincidence test. That is why a
fixture the pass was written for survived it.

Detecting at the validator's own threshold also re-spreads the whole side at 8,
which moves the terminal legs with the ports and clears the fixture's
edge-parallel-segment-too-close as well -- all three hard issues from one
threshold.

Spec set verified complete (1142 tests) and identical to the previous commit's:
29 failures, none new.

Theory input unavailable: the papers-query subagent is not registered in this
environment, so this rests on code evidence only.
Targets nested-subgraphs-reverse-order, invalid solely on edge-border-hugging.
Loop Fixtures total 61002.66 -> 61906.89, invalid 6 -> 5.

An edge may cross a group border; running ALONG it is what reads wrong, because
route and frame become one line and the reader cannot see where the edge enters.
'b --> c' leaves b, turns, and runs 43.9 units 1px above the top of group A.

The interior-run passes cannot reach this. That edge has three points, so it has
no interior run at all -- both segments carry a node-attached point -- yet its
middle leg is exactly the one skimming the frame. It is reachable as c's
TERMINAL leg instead, moved by sliding c's attachment along its own side.

The lift always moves the leg away from the rect it hugs. Moving it the other
way clears the border by routing through the group's interior, trading
edge-border-hugging for edge-intersects-obstacle.

Spec set verified complete (1142 tests) and identical to the previous commit's:
29 failures, none new.

Theory input unavailable: the papers-query subagent is not registered in this
environment, so this rests on code evidence only.
Targets nested-subgraphs, subgraph-variation and event, all carrying
node-too-close-to-group. Loop Fixtures total 61906.89 -> 63660.12,
invalid 5 -> 3.

validateLayout wants nodeGroupClearance (30) between a node and the border of
any group it does not belong to. HOLA has no notion of the rule: nodes are
placed, frames are fitted around their members, and nothing afterwards compares
a frame to the foreign nodes beside it. The gap is whatever the two independent
steps leave -- 26 on nested-subgraphs, four short.

Moving a node after routing normally invalidates its edges, so the push is
taken only when every attached edge can absorb that exact motion: an edge with
a bend keeps its leg's direction if the bend travels on the axis the leg is
constant in, and a two-point edge has no bend so it can only absorb motion
parallel to itself.

An edge carrying a LABEL blocks its node outright. A label is positioned
against geometry this pass does not model, so moving the edge under it slides
the label onto whatever is now there. The first version omitted this, scored
identically, and broke architecture's 'keeps every edge label off every node
box' -- in the shared hola/ corpus, which this loop does not even score. It was
reverted; this is the label-aware variant, and it costs nothing here because
the three fixtures it fixes have unlabelled edges.

Spec set verified complete (1142 tests) and identical to the previous commit's:
29 failures, none new.

Theory input unavailable: the papers-query subagent is not registered in this
environment, so this rests on code evidence only.
…implementation

100 commits from the OSS repo since our merge-base (2026-09-03), taken from the
develop head at cf83441 rather than the mermaid@12.0.0 tag, on the repo
owner's explicit instruction.

Conflict surface was two files: rendering-util/render.ts and
rendering-util/types.ts. No upstream change touches layout-algorithms/hola/, so
none of the HOLA layout work conflicts.
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.