Commit Graph

2 Commits

Author SHA1 Message Date
dayuan.jiang
526f1e14e7 refactor(diagram-engine): apply review findings, fix vertical pool phases
Four reviewers went over the previous commit (three Claude, one Codex). Their
findings, verified independently before applying:

A REAL BUG. A vertical pool with milestone labels drew the label strip outside
the pool frame. The measure pass reserves width as padding + content + strip with
no gap between the last two; the renderer placed the strip one gap further out.
No test caught it because every vertical case omitted phases and every phases
case was horizontal — both regression cases added.

Duplicated logic, now single-sourced:
  - messageCount existed byte-identically in layout.ts and render.ts. Two copies
    that had to agree or the lifelines stop reaching the last message.
  - sequenceMetrics was called twice per sequence container, once inside the
    chrome builder and again for the message positions. Same drift hazard, in the
    file whose own comment warns about it.

Dead code, each verified unreachable rather than assumed:
  - Placed.extent: declared and documented, never written or read. Every .extent
    access belongs to RadialTree.
  - SequenceMetrics.top: computed, returned, no reader.
  - spread()'s level parameter: threaded through the recursion, never used.
  - radialReach's .slice(0, generations): widestPerLevel writes one entry per
    generation, so its length IS the depth. Confirmed over 20,000 random trees;
    removing it made RadialTree.depth dead too.
  - Two of three cycle guards in radialHierarchy: self-links are already skipped
    when the parent map is built, and that map holds one parent per node, so the
    structure is a forest and the visited-set filter cannot fire. The rootOf
    guard does fire and stays.
  - GraphOptions.layerGap/nodeGap/idPrefix: no caller, not in the tool schema.

Simplifications:
  - LayoutContext wrapped a single field; the link array now passes directly,
    which also removes the NO_CONTEXT default no call site ever took.
  - stretches() and the mirror-image check five lines below it expressed one rule
    two ways; unified, with the rationale stated once.
  - hasStencilFrame/isDirectional: one caller each, and isDirectional's name
    contradicted its body, which the guarded branch then re-discriminated anyway.
  - poolFrameStyle() took no arguments and had one caller.
  - poolCellOf clamped a value already clamped at the model boundary and
    unreachable-by-construction from the parser.
  - A comment on stampPoolDecoration described container behaviour the function
    does not implement.

Kept deliberately, with evidence:
  - The best-arrangement tracking in the crossing reducer. Two reviewers
    suspected it was dead weight. Measured: barycentre sweeping regressed below
    its own running best in 180 of 500 random graphs, so without it a third of
    flowcharts would keep a worse arrangement than one already found.
  - Vertical pools. Two reviewers recommended deleting the feature as
    undiscoverable. The bug was one line, and vertical swimlanes are a real
    convention — documented to the model instead, which is what was actually
    missing.
  - styleValue duplicating readMarker, isLeaf, findPageIndex: all genuinely
    redundant, all predating this branch. Left alone to keep the diff scoped.

525 unit tests and 11 diagram e2e tests pass.
2026-08-09 14:47:46 +09:00
dayuan.jiang
a3814f702d feat(diagram-engine): flowcharts, swimlanes, sequence diagrams and mind maps
Extends the declarative engine past cloud architecture. The tool routing was
divided by icon library — AWS through the engine, everything else hand-written
XML — which is the wrong axis. What matters is the LAYOUT SHAPE.

Measured first: a six-step approval flow declared in its natural order comes out
as one column, because the layout only arranges what nesting tells it to and
never looked at the arrows. That forces the arrow from the decision to its second
branch to jump over the first branch.

graph.ts computes what the layout should have looked at: layer assignment by
longest path, cycle breaking so a loop is drawn without setting the order, and
barycentre sweeping to cut edge crossings. It emits ordinary container
operations, so layout, routing and round-tripping are unchanged — reaching zero
arrows-through-boxes on a 14-node pipeline and zero crossings on a bipartite
graph whose declared order forces three.

Three new container kinds, each because one layout rule cannot serve them all:

  pool     — swimlanes. Lanes are real cells and each step is parented to its
             band, so dragging a step to another role records the change.
  sequence — participants across the top, one lifeline cell per participant so
             head and line stay together on a drag. Messages bypass the router:
             a message's height IS its order.
  radial   — mind maps and org charts. Children are a flat list and the
             hierarchy comes from the links, because a branch is a box and a box
             cannot hold children.

Flowchart box shapes (diamond, stadium, parallelogram, document) so a reader can
tell a branch from a step.

Two bugs the new tests caught: the duplicate-link guard blocked a sequence
diagram from having two messages between the same pair, and the fallback message
numbering was shared across containers, pushing a second diagram's messages off
its own lifelines.

523 unit tests and 17 diagram e2e tests pass. Every kind verified round-trip
stable to a fixed point, and rendered in a real browser — draw.io keeps the
lifeline shape and the lane markers.
2026-08-09 13:49:11 +09:00