agentic-mermaid 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +149 -4
- package/LICENSES/Apache-2.0.txt +202 -0
- package/README.md +25 -32
- package/THIRD_PARTY_NOTICES.md +29 -0
- package/assets/fonts/FONT-LICENSES.md +9 -5
- package/assets/fonts/Inter-Bold.ttf +0 -0
- package/assets/fonts/Inter-Medium.ttf +0 -0
- package/assets/fonts/Inter-Regular.ttf +0 -0
- package/assets/fonts/Inter-SemiBold.ttf +0 -0
- package/dist/agent-core.d.ts +463 -0
- package/dist/agent-core.js +397 -0
- package/dist/agent.d.ts +81 -1822
- package/dist/agent.js +336 -36839
- package/dist/agentic-mermaid-mcp.js +7 -37908
- package/dist/am.js +33638 -36261
- package/dist/chunk-4QECACIA.js +4702 -0
- package/dist/chunk-CDPKJ2BQ.js +37 -0
- package/dist/chunk-QKDLMOJJ.js +3487 -0
- package/dist/chunk-WI35L7IR.js +615 -0
- package/dist/chunk-WN3YDOBG.js +149 -0
- package/dist/chunk-ZYMMKQ3I.js +62637 -0
- package/dist/index-D54dqSdC.d.ts +6777 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +310 -32836
- package/dist/mcp-cli-SBQSTJFC.js +17 -0
- package/docs/README.md +14 -6
- package/docs/schemas/style-spec.schema.json +1221 -30
- package/examples/styles/catalog.json +54 -0
- package/examples/styles/cloudflare-workers-inspired-prototype.mmd +9 -0
- package/examples/styles/cloudflare-workers-inspired-prototype.style.json +110 -0
- package/examples/styles/cupertino-prototype.mmd +10 -0
- package/examples/styles/cupertino-prototype.style.json +63 -0
- package/examples/styles/mid-century-report.style.json +2 -2
- package/examples/styles/star-chart-atlas.style.json +2 -1
- package/examples/styles/transit-route-map.style.json +2 -2
- package/examples/styles/vercel-inspired-prototype.mmd +6 -0
- package/examples/styles/vercel-inspired-prototype.style.json +87 -0
- package/package.json +70 -43
- package/server.json +41 -0
- package/skills/agentic-mermaid-diagram-workflow/SKILL.md +17 -36
- package/skills/agentic-mermaid-diagram-workflow/references/cli.md +4 -5
- package/skills/agentic-mermaid-diagram-workflow/references/code-mode.md +15 -31
- package/skills/agentic-mermaid-diagram-workflow/references/sequence.md +10 -14
- package/skills/agentic-mermaid-live-editor/SKILL.md +100 -206
- package/AGENT_NATIVE.md +0 -602
- package/Instructions_for_agents.md +0 -83
- package/TODO.md +0 -662
- package/assets/hero.png +0 -0
- package/dist/index-x2Cj1dVv.d.ts +0 -1089
- package/docs/agent-api-cookbook.md +0 -308
- package/docs/agent-mutation-policy.md +0 -104
- package/docs/agent-workflow-examples.md +0 -46
- package/docs/api.md +0 -203
- package/docs/ascii.md +0 -103
- package/docs/assets/improvements/box-start-connector.png +0 -0
- package/docs/assets/improvements/comment-dropped-lint.png +0 -0
- package/docs/assets/improvements/er-content-loss.png +0 -0
- package/docs/assets/improvements/fan-in-trunks.png +0 -0
- package/docs/assets/improvements/gantt-family.png +0 -0
- package/docs/assets/improvements/gantt-milestone-clamp.png +0 -0
- package/docs/assets/improvements/gantt-verify-seam.png +0 -0
- package/docs/assets/improvements/subgraph-container-edges.png +0 -0
- package/docs/assets/improvements/wrapper-frontmatter.png +0 -0
- package/docs/assets/improvements/wrapper-init-directive.png +0 -0
- package/docs/assets/improvements/wrapper-leading-comment.png +0 -0
- package/docs/assets/style-cookbook/mid-century-report.png +0 -0
- package/docs/assets/style-cookbook/star-chart-atlas.png +0 -0
- package/docs/assets/style-cookbook/transit-route-map.png +0 -0
- package/docs/comparison.md +0 -94
- package/docs/config.md +0 -105
- package/docs/contributing/adding-diagram-types.md +0 -144
- package/docs/contributing/diagram-family-citizenship.matrix.json +0 -2127
- package/docs/contributing/diagram-family-citizenship.md +0 -82
- package/docs/contributing/harvesting-upstream-tests.md +0 -126
- package/docs/contributing/lessons-learned.md +0 -276
- package/docs/contributing/releasing.md +0 -71
- package/docs/contributing/visual-review-evidence.md +0 -53
- package/docs/custom-style-cookbook.md +0 -154
- package/docs/design/families/README.md +0 -26
- package/docs/design/families/architecture-beta.md +0 -48
- package/docs/design/families/flowchart-parser-conformance.md +0 -22
- package/docs/design/families/gantt-research.md +0 -137
- package/docs/design/families/gantt.md +0 -329
- package/docs/design/families/journey-agentic-cards-before.png +0 -0
- package/docs/design/families/journey-mermaid-classic-implementation.png +0 -0
- package/docs/design/families/journey-mermaid-classic-prototype.png +0 -0
- package/docs/design/families/journey-mermaid-classic-prototype.svg +0 -163
- package/docs/design/families/journey-mermaid-classic-prototype.svg.png +0 -0
- package/docs/design/families/journey-migration-parity.md +0 -433
- package/docs/design/families/journey-usage-research.md +0 -423
- package/docs/design/families/journey.md +0 -125
- package/docs/design/families/xychart.md +0 -85
- package/docs/design/system/README.md +0 -45
- package/docs/design/system/abstraction-audit.md +0 -224
- package/docs/design/system/abstraction-recommendations.md +0 -655
- package/docs/design/system/architecture.mmd +0 -29
- package/docs/design/system/architecture.svg +0 -137
- package/docs/design/system/layout-guarantees-and-robustness.md +0 -351
- package/docs/design/system/layout-rubric.md +0 -138
- package/docs/design/system/route-contracts.md +0 -848
- package/docs/design/system/source-preservation-ladder.md +0 -52
- package/docs/design/system/styles-rollout.md +0 -216
- package/docs/design/system/ugly-layouts.md +0 -119
- package/docs/diagram-families.md +0 -193
- package/docs/features.md +0 -147
- package/docs/fork-differences.md +0 -130
- package/docs/getting-started.md +0 -175
- package/docs/issue-derived-test-cases.md +0 -156
- package/docs/layout-characterization/README.md +0 -245
- package/docs/layout-characterization/contact-sheet-families.md +0 -496
- package/docs/layout-characterization/contact-sheet.md +0 -638
- package/docs/layout-characterization/properties.md +0 -281
- package/docs/layout-characterization/visual-quality.md +0 -160
- package/docs/layout-characterization/visual-snapshots/architecture.svg +0 -79
- package/docs/layout-characterization/visual-snapshots/class.svg +0 -60
- package/docs/layout-characterization/visual-snapshots/er.svg +0 -61
- package/docs/layout-characterization/visual-snapshots/flowchart.svg +0 -58
- package/docs/layout-characterization/visual-snapshots/gantt.svg +0 -52
- package/docs/layout-characterization/visual-snapshots/journey.svg +0 -118
- package/docs/layout-characterization/visual-snapshots/pie.svg +0 -36
- package/docs/layout-characterization/visual-snapshots/quadrant.svg +0 -47
- package/docs/layout-characterization/visual-snapshots/sequence.svg +0 -59
- package/docs/layout-characterization/visual-snapshots/state.svg +0 -52
- package/docs/layout-characterization/visual-snapshots/timeline.svg +0 -64
- package/docs/layout-characterization/visual-snapshots/xychart.svg +0 -97
- package/docs/mcp-code-mode-rationale.md +0 -101
- package/docs/mcp-http-transport.md +0 -223
- package/docs/mermaid-layout-complaints.md +0 -837
- package/docs/mutation-testing.md +0 -206
- package/docs/project/divergences.md +0 -98
- package/docs/project/dogfooding-docs-strategy.md +0 -66
- package/docs/project/hosted-mcp-cloudflare-plan.md +0 -338
- package/docs/project/lessons-learned.md +0 -1019
- package/docs/project/remove-role-styling-plan.md +0 -176
- package/docs/quality.md +0 -212
- package/docs/react.md +0 -97
- package/docs/style-authoring.md +0 -133
- package/docs/testing-strategy.md +0 -335
- package/docs/theming.md +0 -162
- package/examples/agent-improve-auth-flow.ts +0 -206
- package/examples/agent-loop.ts +0 -53
- package/examples/mcp-vs-cli-complex-diagrams.ts +0 -133
- package/llms.txt +0 -104
- package/skill-evals/README.md +0 -77
- package/skill-evals/fixtures/architecture-cache/input.mmd +0 -5
- package/skill-evals/fixtures/class-payment/input.mmd +0 -4
- package/skill-evals/fixtures/er-orders/input.mmd +0 -10
- package/skill-evals/fixtures/flowchart-cache/input.mmd +0 -3
- package/skill-evals/fixtures/gantt-release/input.mmd +0 -7
- package/skill-evals/fixtures/journey-checkout/input.mmd +0 -6
- package/skill-evals/fixtures/pie-exports/input.mmd +0 -5
- package/skill-evals/fixtures/quadrant-priorities/input.mmd +0 -11
- package/skill-evals/fixtures/sequence-retry/input.mmd +0 -8
- package/skill-evals/fixtures/state-lifecycle/input.mmd +0 -5
- package/skill-evals/fixtures/timeline-release/input.mmd +0 -6
- package/skill-evals/fixtures/xychart-growth/input.mmd +0 -5
- package/skill-evals/shared-benchmark.json +0 -510
- package/src/agent/analyze.ts +0 -157
- package/src/agent/apply.ts +0 -146
- package/src/agent/architecture-body.ts +0 -458
- package/src/agent/certificates.ts +0 -110
- package/src/agent/class-body.ts +0 -319
- package/src/agent/core.ts +0 -207
- package/src/agent/create.ts +0 -106
- package/src/agent/describe.ts +0 -364
- package/src/agent/er-body.ts +0 -224
- package/src/agent/facts.ts +0 -337
- package/src/agent/families-builtin.ts +0 -934
- package/src/agent/families.ts +0 -232
- package/src/agent/family-layouts.ts +0 -826
- package/src/agent/flowchart-body.ts +0 -372
- package/src/agent/flowchart-unsupported.ts +0 -243
- package/src/agent/gantt-body.ts +0 -495
- package/src/agent/index.ts +0 -10
- package/src/agent/journey-body.ts +0 -414
- package/src/agent/label-metrics.ts +0 -28
- package/src/agent/layout-to-rendered.ts +0 -63
- package/src/agent/mutate.ts +0 -97
- package/src/agent/mutation-ops.ts +0 -51
- package/src/agent/op-schema.ts +0 -439
- package/src/agent/parse.ts +0 -223
- package/src/agent/pie-body.ts +0 -270
- package/src/agent/png.ts +0 -117
- package/src/agent/quadrant-body.ts +0 -336
- package/src/agent/quality.ts +0 -363
- package/src/agent/readability-audit.ts +0 -92
- package/src/agent/rendered-route-audit.ts +0 -61
- package/src/agent/sequence-body.ts +0 -345
- package/src/agent/serialize.ts +0 -214
- package/src/agent/state-body.ts +0 -589
- package/src/agent/structural-count.ts +0 -150
- package/src/agent/timeline-body.ts +0 -280
- package/src/agent/trace-log.ts +0 -43
- package/src/agent/types.ts +0 -979
- package/src/agent/verify.ts +0 -571
- package/src/agent/xychart-body.ts +0 -472
- package/src/architecture/config.ts +0 -282
- package/src/architecture/layout.ts +0 -496
- package/src/architecture/parser.ts +0 -375
- package/src/architecture/renderer.ts +0 -611
- package/src/architecture/types.ts +0 -113
- package/src/ascii/ansi.ts +0 -423
- package/src/ascii/architecture.ts +0 -210
- package/src/ascii/canvas.ts +0 -443
- package/src/ascii/class-diagram.ts +0 -697
- package/src/ascii/converter.ts +0 -475
- package/src/ascii/draw.ts +0 -1693
- package/src/ascii/edge-bundling.ts +0 -328
- package/src/ascii/edge-routing.ts +0 -330
- package/src/ascii/er-diagram.ts +0 -439
- package/src/ascii/gantt.ts +0 -276
- package/src/ascii/grid.ts +0 -747
- package/src/ascii/index.ts +0 -196
- package/src/ascii/journey.ts +0 -189
- package/src/ascii/meta.ts +0 -244
- package/src/ascii/multiline-utils.ts +0 -80
- package/src/ascii/pathfinder.ts +0 -301
- package/src/ascii/pie.ts +0 -67
- package/src/ascii/quadrant.ts +0 -158
- package/src/ascii/reverse.ts +0 -216
- package/src/ascii/sequence.ts +0 -499
- package/src/ascii/shapes/circle.ts +0 -27
- package/src/ascii/shapes/corners.ts +0 -143
- package/src/ascii/shapes/diamond.ts +0 -27
- package/src/ascii/shapes/hexagon.ts +0 -27
- package/src/ascii/shapes/index.ts +0 -106
- package/src/ascii/shapes/rectangle.ts +0 -168
- package/src/ascii/shapes/rounded.ts +0 -27
- package/src/ascii/shapes/special.ts +0 -332
- package/src/ascii/shapes/stadium.ts +0 -110
- package/src/ascii/shapes/state.ts +0 -192
- package/src/ascii/shapes/types.ts +0 -73
- package/src/ascii/timeline.ts +0 -128
- package/src/ascii/types.ts +0 -312
- package/src/ascii/validate.ts +0 -120
- package/src/ascii/width.ts +0 -81
- package/src/ascii/xychart.ts +0 -919
- package/src/browser.ts +0 -29
- package/src/class/layout.ts +0 -240
- package/src/class/parser.ts +0 -332
- package/src/class/renderer.ts +0 -638
- package/src/class/types.ts +0 -129
- package/src/cli/agent-instructions.ts +0 -1
- package/src/cli/am-bin.ts +0 -6
- package/src/cli/exit-codes.ts +0 -18
- package/src/cli/index.ts +0 -1117
- package/src/cli/init-agent.ts +0 -147
- package/src/color-resolver.ts +0 -152
- package/src/elk-instance.ts +0 -114
- package/src/er/layout.ts +0 -187
- package/src/er/parser.ts +0 -226
- package/src/er/renderer.ts +0 -792
- package/src/er/types.ts +0 -99
- package/src/font-manifest.ts +0 -27
- package/src/gantt/layout.ts +0 -365
- package/src/gantt/parser.ts +0 -282
- package/src/gantt/pipeline.ts +0 -27
- package/src/gantt/renderer.ts +0 -414
- package/src/gantt/schedule.ts +0 -513
- package/src/gantt/types.ts +0 -255
- package/src/index.ts +0 -368
- package/src/journey/layout.ts +0 -520
- package/src/journey/parser.ts +0 -218
- package/src/journey/renderer.ts +0 -811
- package/src/journey/types.ts +0 -144
- package/src/layout/geometry.ts +0 -182
- package/src/layout/pass.ts +0 -142
- package/src/layout/passes/index.ts +0 -3916
- package/src/layout-engine.ts +0 -1565
- package/src/layout-rubric.ts +0 -659
- package/src/mcp/artifacts.ts +0 -137
- package/src/mcp/dynamic-harness.ts +0 -31
- package/src/mcp/dynamic-modules.d.ts +0 -7
- package/src/mcp/facade.ts +0 -608
- package/src/mcp/harness-runtime.ts +0 -203
- package/src/mcp/hosted-server.ts +0 -468
- package/src/mcp/mcp-bin.ts +0 -3
- package/src/mcp/mcp-cli.ts +0 -82
- package/src/mcp/protocol.ts +0 -13
- package/src/mcp/sandbox.ts +0 -195
- package/src/mcp/sdk-decl.ts +0 -387
- package/src/mcp/server.ts +0 -362
- package/src/mcp/tool-surface.ts +0 -146
- package/src/mermaid-source.ts +0 -655
- package/src/multiline-utils.ts +0 -228
- package/src/parser.ts +0 -1031
- package/src/pie/layout.ts +0 -168
- package/src/pie/parser.ts +0 -136
- package/src/pie/renderer.ts +0 -234
- package/src/pie/types.ts +0 -82
- package/src/quadrant/layout.ts +0 -191
- package/src/quadrant/parser.ts +0 -183
- package/src/quadrant/renderer.ts +0 -322
- package/src/quadrant/types.ts +0 -113
- package/src/render-family-hooks.ts +0 -244
- package/src/renderer.ts +0 -1099
- package/src/route-contracts.ts +0 -2723
- package/src/scene/backend.ts +0 -76
- package/src/scene/fidelity.ts +0 -183
- package/src/scene/hybrid-backend.ts +0 -174
- package/src/scene/ir.ts +0 -181
- package/src/scene/marks.ts +0 -127
- package/src/scene/rough-backend.ts +0 -378
- package/src/scene/seed.ts +0 -46
- package/src/scene/style-registry.ts +0 -556
- package/src/sequence/layout.ts +0 -456
- package/src/sequence/parser.ts +0 -255
- package/src/sequence/renderer.ts +0 -604
- package/src/sequence/types.ts +0 -154
- package/src/shape-clipping.ts +0 -351
- package/src/shared/accessibility-directives.ts +0 -22
- package/src/shared/batched.ts +0 -61
- package/src/shared/color-math.ts +0 -72
- package/src/shared/svg-a11y.ts +0 -29
- package/src/shared/syntax-error.ts +0 -17
- package/src/shared/unicode-ranges.ts +0 -135
- package/src/styles.ts +0 -318
- package/src/svg-paths.ts +0 -28
- package/src/text-metrics.ts +0 -248
- package/src/theme.ts +0 -639
- package/src/timeline/layout.ts +0 -381
- package/src/timeline/parser.ts +0 -215
- package/src/timeline/renderer.ts +0 -538
- package/src/timeline/types.ts +0 -105
- package/src/types.ts +0 -454
- package/src/website-worker-modules.d.ts +0 -18
- package/src/xychart/axis-utils.ts +0 -109
- package/src/xychart/colors.ts +0 -124
- package/src/xychart/config.ts +0 -88
- package/src/xychart/layout.ts +0 -656
- package/src/xychart/parser.ts +0 -504
- package/src/xychart/renderer.ts +0 -640
- package/src/xychart/types.ts +0 -282
|
@@ -1,848 +0,0 @@
|
|
|
1
|
-
# Route Contracts: Principled Routing Without Hitches
|
|
2
|
-
|
|
3
|
-
Status: implemented (Phases 0, 1 (complete), 3, and the first slice of Phase 4 of issue #25's rollout; see "Rollout status")
|
|
4
|
-
Supersedes: the draft spec in issue #25, whose claims this document re-verified against the code.
|
|
5
|
-
|
|
6
|
-
## 1. The problem
|
|
7
|
-
|
|
8
|
-
A **hitch** is a short dogleg on an edge whose semantics imply a straight lane:
|
|
9
|
-
the route jogs perpendicular to the flow axis even though nothing blocks the
|
|
10
|
-
direct lane. Issue #25 framed this correctly as a contract failure — the
|
|
11
|
-
pipeline encodes direction and rough placement, but never the invariant
|
|
12
|
-
"a forward edge is straight unless something provably blocks it."
|
|
13
|
-
|
|
14
|
-
Measured evidence (the MFA/login regression, `flowchart LR`, live
|
|
15
|
-
`layoutGraphSync` output before this work):
|
|
16
|
-
|
|
17
|
-
```
|
|
18
|
-
B -> C (269.0,120.5) (375.7,120.5) (375.7, 97.8) (416.6, 97.8) 22.7px dogleg
|
|
19
|
-
D --Yes--> E (813.6,152.3) (875.1,152.3) (875.1,158.0) (963.8,158.0) 5.7px dogleg
|
|
20
|
-
E -> F ... (1209.5,147.0) (1209.5,147.0) ... duplicated point + 16.5px dogleg
|
|
21
|
-
F --Yes--> G (1357.9,158.0) (1470.5,158.0) (1470.5,124.6) (1482.5,124.6) 33.4px doorstep jog
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
Every one of these edges is a forward edge whose direct lane is clear.
|
|
25
|
-
|
|
26
|
-
## 2. Verified claims (what issue #25 got right and wrong)
|
|
27
|
-
|
|
28
|
-
We re-verified every code claim in issue #25 before designing. Corrections
|
|
29
|
-
matter because they change where the fix belongs.
|
|
30
|
-
|
|
31
|
-
| Claim | Verdict | Evidence |
|
|
32
|
-
|---|---|---|
|
|
33
|
-
| "edge-point orthogonalization after ELK" exists | TRUE | `orthogonalizeEdgePoints()` `src/layout-engine.ts` — but it only fires for cross-hierarchy SEPARATE-mode edges |
|
|
34
|
-
| "layer-node snapping after ELK has already routed" | TRUE | `alignLayerNodes()` `src/layout-engine.ts` — adjusts only first/last edge points after moving nodes |
|
|
35
|
-
| "shape clipping after routing" | TRUE | `src/shape-clipping.ts`, diamonds only; runs after bundling |
|
|
36
|
-
| "branch bundling after route extraction" | TRUE | `bundleEdgePaths()` (SVG) and `src/ascii/edge-bundling.ts` (ASCII); bundled paths are fully rebuilt, so they are never stale |
|
|
37
|
-
| "ASCII path re-selection and label-line guessing" | TRUE | `src/ascii/edge-routing.ts`; FIFO tie-breaking already deterministic (`pathfinder.ts` `seq` field) |
|
|
38
|
-
| Hitches are caused by the aggregate of these passes | **FALSE for the common case** | Raw ELK output (before any post-pass) already contains every dogleg and the duplicated point listed above. The cause is ELK's FREE port placement spreading edge endpoints along node sides (B exits at y=120.5, C enters at y=97.8, both centers at y=126.7). |
|
|
39
|
-
| A simplifier exists but is unsafe | **FALSE** | There is no simplifier at all. `pointsToPolylinePath()` emits ELK's points verbatim — not even consecutive-duplicate or collinear-point removal. |
|
|
40
|
-
| No route classification exists | PARTIALLY TRUE | Cycle detection exists for ELK model order (`sourceAwareNodeOrder`), and fan-in/fan-out grouping exists in both bundlers, but nothing is unified or consumed as routing intent. |
|
|
41
|
-
|
|
42
|
-
Consequence: the first principled fix is **not** semantic ELK ports (issue
|
|
43
|
-
#25 Phase 2, large blast radius) but a **certifying route pass** after layout:
|
|
44
|
-
classify every edge, prove or refute the direct lane, straighten only with
|
|
45
|
-
proof, and certify everything else. Semantic ports remain the right Phase-2
|
|
46
|
-
follow-up; they reduce how often the prover has to repair, but the contract
|
|
47
|
-
layer is what makes either approach testable.
|
|
48
|
-
|
|
49
|
-
## 3. Design principles (inherited from issue #25, unchanged)
|
|
50
|
-
|
|
51
|
-
1. Intent before geometry — classify edges before deciding how they may bend.
|
|
52
|
-
2. Every bend must be intentional or explained — certificates, not vibes.
|
|
53
|
-
3. No post-layout node movement without rerouting or recertification.
|
|
54
|
-
4. Labels are route requirements: a straightened segment must host its label.
|
|
55
|
-
5. ASCII and SVG share route intent. The ASCII grid router already attempted
|
|
56
|
-
direct lanes first with FIFO-deterministic ties; this work additionally
|
|
57
|
-
gave its placement longest-path layering (a fan-in target sits after its
|
|
58
|
-
DEEPEST parent, so a later parent's edge never runs backward — the ASCII
|
|
59
|
-
side of acceptance criterion 1) and made label-segment choice prefer the
|
|
60
|
-
widest fitting segment (a mid-route channel) over the shared final
|
|
61
|
-
approach into a fan-in target.
|
|
62
|
-
|
|
63
|
-
## 4. Data model
|
|
64
|
-
|
|
65
|
-
```ts
|
|
66
|
-
type RouteClass =
|
|
67
|
-
| 'primary-forward' // added in author order without creating a cycle
|
|
68
|
-
| 'feedback' // would create a cycle; may straighten onto its OWN reverse lane, never the forward edge's lane
|
|
69
|
-
| 'self-loop'
|
|
70
|
-
| 'container' // endpoint is a subgraph id
|
|
71
|
-
| 'cross-hierarchy' // endpoints in different subgraph scopes
|
|
72
|
-
|
|
73
|
-
type RouteInvariant =
|
|
74
|
-
| 'straight' // exactly two points, axis-aligned with flow
|
|
75
|
-
| 'explained-detour' // bends, and directLaneBlockedBy is non-empty
|
|
76
|
-
| 'bundle' // path owned by the fan-out/fan-in bundler
|
|
77
|
-
| 'outer-feedback' // feedback routed around the nodes through an outer channel (ELK feedbackEdges + tightening)
|
|
78
|
-
| 'feedback-detour' // feedback whose reverse lane is blocked; blockers say why
|
|
79
|
-
| 'self-loop'
|
|
80
|
-
| 'container-attach'
|
|
81
|
-
| 'unverified-shape' // endpoint shape has no straight attachment side
|
|
82
|
-
|
|
83
|
-
type RoutePortAssignment = {
|
|
84
|
-
side: 'N' | 'E' | 'S' | 'W'
|
|
85
|
-
slotIndex: number // 0-based order along that side
|
|
86
|
-
slotCount: number // endpoints allocated to this node side
|
|
87
|
-
role: 'flow-source' | 'flow-target' | 'feedback-source' | 'feedback-target'
|
|
88
|
-
| 'self-loop-source' | 'self-loop-target'
|
|
89
|
-
| 'container-source' | 'container-target'
|
|
90
|
-
| 'cross-hierarchy-source' | 'cross-hierarchy-target'
|
|
91
|
-
port?: AnyPort // exact designated port when final geometry lands on one
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
type RouteCertificateBase = {
|
|
95
|
-
edgeIndex: number // index into MermaidGraph.edges
|
|
96
|
-
routeClass: RouteClass
|
|
97
|
-
bendCount: number
|
|
98
|
-
directLaneClear?: boolean // primary-forward and feedback
|
|
99
|
-
directLaneBlockedBy?: Array<{ kind: 'node' | 'label' | 'channel' | 'span' | 'crossing' | 'port'; id: string }>
|
|
100
|
-
sourcePort?: AnyPort
|
|
101
|
-
targetPort?: AnyPort
|
|
102
|
-
sourcePortAssignment?: RoutePortAssignment
|
|
103
|
-
targetPortAssignment?: RoutePortAssignment
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
type RouteCertificate =
|
|
107
|
-
| (RouteCertificateBase & {
|
|
108
|
-
invariant: 'straight'
|
|
109
|
-
straightened?: true // true only when the final route is two-point straight
|
|
110
|
-
})
|
|
111
|
-
| (RouteCertificateBase & {
|
|
112
|
-
invariant: Exclude<RouteInvariant, 'straight'>
|
|
113
|
-
straightened?: never // detours cannot carry stale straightening metadata
|
|
114
|
-
})
|
|
115
|
-
}
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
`PositionedEdge` gains an internal `edgeIndex` so certificates can be
|
|
119
|
-
recomputed from `(MermaidGraph, PositionedGraph)` by any consumer (verify,
|
|
120
|
-
tests, debug tooling) without trusting the layout pass to hand them over.
|
|
121
|
-
|
|
122
|
-
## 5. Edge classification
|
|
123
|
-
|
|
124
|
-
Mirrors the cycle detection ELK's model order already uses, so classification
|
|
125
|
-
and layout cannot disagree:
|
|
126
|
-
|
|
127
|
-
1. Walk `graph.edges` in author order, adding each edge to a reachability
|
|
128
|
-
relation unless `target` already reaches `source`; those become `feedback`.
|
|
129
|
-
2. `source === target` → `self-loop`.
|
|
130
|
-
3. Endpoint is a subgraph id → `container`.
|
|
131
|
-
4. Endpoints in different subgraph scopes → `cross-hierarchy`.
|
|
132
|
-
5. Everything else → `primary-forward`.
|
|
133
|
-
|
|
134
|
-
In `B --> C; C -- No --> B`, `B->C` is primary-forward and owns the straight
|
|
135
|
-
lane; `C->B` is feedback — it may render as a straight parallel back-edge on
|
|
136
|
-
its own lane (the classic reciprocal-pair rendering) but never on the forward
|
|
137
|
-
edge's lane. This matches what ELK's
|
|
138
|
-
`cycleBreaking.strategy: MODEL_ORDER` already decided, so the certificate
|
|
139
|
-
describes reality rather than re-deriving a parallel truth.
|
|
140
|
-
|
|
141
|
-
## 6. The certifying straightener
|
|
142
|
-
|
|
143
|
-
Runs at the end of `elkToPositioned()`, after bundling and shape clipping,
|
|
144
|
-
immediately before bounds are computed. Two layers:
|
|
145
|
-
|
|
146
|
-
**Proof-free simplifications (all edges):** remove consecutive duplicate
|
|
147
|
-
points and collinear midpoints. These preserve the drawn geometry exactly;
|
|
148
|
-
no certificate change beyond `bendCount`.
|
|
149
|
-
|
|
150
|
-
**Proof-carrying straightening (primary-forward and feedback, not bundled,
|
|
151
|
-
both endpoint shapes straightenable):** a route that is monotone along the
|
|
152
|
-
edge's own flow axis but not straight is collapsed to a single axis-aligned
|
|
153
|
-
segment iff a *candidate lane* exists that is provably clear. Feedback edges
|
|
154
|
-
run the identical proof with the axis sign flipped: their forward-facing
|
|
155
|
-
side is the graph's backward-facing one. The channel-separation check below
|
|
156
|
-
is what keeps a straightened feedback lane off the forward edge's lane —
|
|
157
|
-
a reciprocal pair renders as two parallel arrows, never one merged line.
|
|
158
|
-
|
|
159
|
-
Candidate lanes, tried in order (cross-axis coordinate for LR = y):
|
|
160
|
-
|
|
161
|
-
1. the current target endpoint's cross-coordinate (moves only the source end),
|
|
162
|
-
2. the current source endpoint's cross-coordinate (moves only the target end),
|
|
163
|
-
3. the extremes of the existing route — the outer channel the router already
|
|
164
|
-
proved navigable (this is how a feedback edge wrapping a node collapses
|
|
165
|
-
onto a single straight back-lane beside it),
|
|
166
|
-
4. the center of the overlap of both nodes' attachment spans,
|
|
167
|
-
5. the quartiles of that overlap — an unlabeled feedback loop's own cross
|
|
168
|
-
values all sit on the forward lane or outside the span, so the parallel
|
|
169
|
-
back-lane that collapses it to the classic two-arrow rendering lies
|
|
170
|
-
between the span center and a span end.
|
|
171
|
-
|
|
172
|
-
A candidate is valid when it lies inside both endpoint nodes' attachment
|
|
173
|
-
spans (diamonds allow the whole facet minus a 10px vertex margin — the
|
|
174
|
-
flowchart convention attaches anywhere along the slanted edge, and a
|
|
175
|
-
proportional restriction needlessly forbade straight lanes to partners
|
|
176
|
-
sitting off the diamond's centerline; rectangles use the full side minus a
|
|
177
|
-
4px margin), and the lane is **clear**:
|
|
178
|
-
|
|
179
|
-
- it does not pass through any other node's bounding box (4px inflation;
|
|
180
|
-
bbox is a conservative over-approximation of non-rectangular shapes — it
|
|
181
|
-
can only suppress a legal straightening, never allow an illegal one),
|
|
182
|
-
- it does not pass through any other edge's label **pill** (measured text
|
|
183
|
-
plus the renderer's 8px pill padding, at current positions at proof time;
|
|
184
|
-
one exception: a primary edge's proof ignores the label of its reciprocal
|
|
185
|
-
feedback partner — primary owns the lane, and the partner's label is
|
|
186
|
-
movable decoration that is re-placed when the partner straightens, or
|
|
187
|
-
stays on its detour),
|
|
188
|
-
- it does not run collinearly (within a 4px corridor) along another edge's
|
|
189
|
-
parallel segment ("channel" blocker),
|
|
190
|
-
- if the edge has a label, the new segment has capacity for the pill
|
|
191
|
-
(pill extent along the lane + clearance), and an **on-lane label slot**
|
|
192
|
-
exists — midpoint, then 1/3, then 2/3 — whose pill rect is clear of
|
|
193
|
-
nodes, other pills, and other edges' segments. On-lane only: a label
|
|
194
|
-
must sit on its own route so the label-to-edge association stays
|
|
195
|
-
unambiguous (Kakoulis & Tollis' edge-label-placement criterion; dot
|
|
196
|
-
realizes the same doctrine by making labels virtual nodes with reserved
|
|
197
|
-
layout space). A labeled edge whose lane cannot host its pill does not
|
|
198
|
-
straighten — it keeps its certified outer loop, where ELK's inline label
|
|
199
|
-
dummy has reserved space ON the loop,
|
|
200
|
-
- it does not **increase edge crossings**: a perpendicular crossing is
|
|
201
|
-
legal when the router chose it, but a repair (straightening or loop
|
|
202
|
-
tightening) must never create one the original route avoided. The
|
|
203
|
-
fixed-point iteration makes this constructive: tightening a loop out of
|
|
204
|
-
a sibling's corridor unblocks the sibling's straightening on the next
|
|
205
|
-
round.
|
|
206
|
-
|
|
207
|
-
Endpoints are re-anchored on the actual shape boundary: rectangle-like
|
|
208
|
-
shapes by side intersection, diamonds by ray-polygon intersection (the same
|
|
209
|
-
machinery as `shape-clipping.ts`). Shapes outside the straightenable set
|
|
210
|
-
(circle, stadium, asymmetric, …) are skipped, because today their endpoints
|
|
211
|
-
sit on the bbox and a straightened lane could visibly detach from the
|
|
212
|
-
rendered outline; they keep their ELK route and certify as explained or
|
|
213
|
-
unverified-shape detours.
|
|
214
|
-
|
|
215
|
-
Edges are processed in author order, each straightening updates the
|
|
216
|
-
geometry the next proof sees, and the pass iterates to a fixed point:
|
|
217
|
-
straightening one edge can vacate the channel that blocked a sibling
|
|
218
|
-
(duplicate parallel edges do exactly this), so blocked edges are re-proved
|
|
219
|
-
until nothing changes. The pass is deterministic and never lets two
|
|
220
|
-
straightened lanes collide (the channel check sees prior results).
|
|
221
|
-
|
|
222
|
-
If no candidate lane is clear, the certificate records
|
|
223
|
-
`invariant: 'explained-detour'` (`'feedback-detour'` for feedback edges)
|
|
224
|
-
with the concrete blockers, e.g. the MFA fixture's `D --No--> G` is blocked
|
|
225
|
-
by node `F` standing in every candidate lane — exactly the explained detour
|
|
226
|
-
issue #25 demands.
|
|
227
|
-
|
|
228
|
-
Diamond fan-outs have one extra port-constraint case: when sibling decision
|
|
229
|
-
branches share a target rank, branches spread across the diamond's facet/cardinal
|
|
230
|
-
ports and still enter each peer target through its facing cardinal port. If those
|
|
231
|
-
two port lanes differ, a straight segment would have to give up one endpoint
|
|
232
|
-
contract; the branch is therefore certified as an `explained-detour` blocked by
|
|
233
|
-
`kind: 'port'`, and verification treats the Z-route as intentional only while
|
|
234
|
-
the final endpoints still sit on the selected ports.
|
|
235
|
-
|
|
236
|
-
**Bundle contract (issue #25 Phase 4, first slice):** the fan-out/fan-in
|
|
237
|
-
bundler used to rebuild trunk paths with no obstacle awareness, so a skip
|
|
238
|
-
edge in `A --> X; A --> B; X --> B` could run its trunk straight through
|
|
239
|
-
`X`'s box. Bundled paths are now proved clear of every non-endpoint node
|
|
240
|
-
before they are assigned; a blocked member falls out of the bundle (keeping
|
|
241
|
-
its ELK route, which the route-contract pass then straightens or explains),
|
|
242
|
-
and the junction is re-derived from the members that remain.
|
|
243
|
-
|
|
244
|
-
**Feedback routing (issue #25 §8.1/§11.3, literature-grounded):** feedback
|
|
245
|
-
edges no longer compete with their forward partner for the same
|
|
246
|
-
flow-facing side. `elk.layered.feedbackEdges: true` (the option issue #25
|
|
247
|
-
§10 listed, enabled now that classification is wired) routes reversed
|
|
248
|
-
edges *around* the nodes through an outer channel — so the forward lane of
|
|
249
|
-
a reciprocal pair is straight directly out of ELK, and the loop carries
|
|
250
|
-
its inline label with reserved space. On top of ELK's loop the pass runs
|
|
251
|
-
**loop tightening**: ELK can wrap a loop around the source's forward side
|
|
252
|
-
before reaching its channel, parking the drop column in a sibling's
|
|
253
|
-
corridor; when the source's channel-facing facet (the decision diamond's
|
|
254
|
-
"bottom port") can reach the channel directly — on-boundary when the shape
|
|
255
|
-
spans the channel, else via one short proven hop — the excursion is cut.
|
|
256
|
-
Certified `outer-feedback`. Unlabeled feedback whose parallel reverse lane
|
|
257
|
-
proves clear still collapses to the classic two-straight-arrows rendering;
|
|
258
|
-
labeled feedback keeps the loop because its pill belongs ON its route.
|
|
259
|
-
|
|
260
|
-
**Container repair (spec §11.5):** under SEPARATE hierarchy mode (subgraph
|
|
261
|
-
direction overrides) ELK could leave a container-to-container edge floating
|
|
262
|
-
in the diagram margin, attached to neither box — the
|
|
263
|
-
`ROUTE_CONTAINER_MISANCHOR` tripwire caught exactly one such edge in the
|
|
264
|
-
docs corpus. When the two end rects are cleanly separated along one axis,
|
|
265
|
-
the route collapses onto a proven straight lane between the facing borders
|
|
266
|
-
(the container rect stands in as a rectangle; the lane axis comes from how
|
|
267
|
-
the rects are separated, not the graph direction). Composite-state edges
|
|
268
|
-
onto containers straighten through the same path.
|
|
269
|
-
|
|
270
|
-
### 6.1 Diamond ports: what the literature says (and what we adopted)
|
|
271
|
-
|
|
272
|
-
Our diamonds support geometrically unbounded attachment (endpoints
|
|
273
|
-
ray-clip onto the polygon), but the lane logic originally used only the
|
|
274
|
-
two flow-facing facets — the N/S facets sat unused, which is what forced
|
|
275
|
-
reciprocal pairs and their labels into a corridor capped by the node
|
|
276
|
-
height. The literature pass that fixed this:
|
|
277
|
-
|
|
278
|
-
- Ports on all four sides with constraints is the standard layered model —
|
|
279
|
-
Schulze, Spönemann & von Hanxleden, *Drawing Layered Graphs with Port
|
|
280
|
-
Constraints*, JVLC 2014 (ELK Layered's foundation).
|
|
281
|
-
- Labels are layout citizens with reserved space on their own edge — dot
|
|
282
|
-
represents edge labels as virtual nodes (Gansner, Koutsofios, North &
|
|
283
|
-
Vo, TSE 1993); ELK's inline edge labels are the same mechanism; Kakoulis
|
|
284
|
-
& Tollis' edge-label-placement criteria demand unambiguous
|
|
285
|
-
label-to-edge association.
|
|
286
|
-
- Feedback routes around the drawing through outer channels — the
|
|
287
|
-
Sugiyama tradition, shipped as `elk.layered.feedbackEdges`.
|
|
288
|
-
- Flowchart convention (ISO 5807 lineage) attaches decision branches at
|
|
289
|
-
the vertices, leaving in different directions.
|
|
290
|
-
|
|
291
|
-
Adopted (issue #26 Workstream 3, realized): every shape carries four
|
|
292
|
-
canonical **ports** at its bbox side midpoints — `shapePorts()` — which lie
|
|
293
|
-
exactly on the rendered outline for the whole catalog, because each shape
|
|
294
|
-
is symmetric and inscribed in its bbox: the diamond's N/E/S/W *vertices*,
|
|
295
|
-
the rectangle's side *midpoints*, and the boundary extremes of circles,
|
|
296
|
-
stadiums, hexagons and cylinders are the same four points. The industry
|
|
297
|
-
model behind this is yFiles port candidates (fixed anchors with costs and
|
|
298
|
-
capacities) and Visio connection points with static vs dynamic glue:
|
|
299
|
-
|
|
300
|
-
- **Static glue first**: straightening tries the lane through the target's
|
|
301
|
-
port, then the source's, before any floating candidate — aligned chains
|
|
302
|
-
run port to port, and a fan-in target receives its edge at the exact
|
|
303
|
-
side midpoint. Same-target edges may converge exactly collinearly at the
|
|
304
|
-
shared port (the classic fan-in merge into one arrowhead).
|
|
305
|
-
- **Dynamic glue second**: when centers don't align, a straight lane still
|
|
306
|
-
beats a port-pure Z-bend; the off-port end floats on the side/facet
|
|
307
|
-
(Mermaid's own attachment model). A port lane may attach its floating
|
|
308
|
-
end closer to a diamond vertex than the 10px aesthetic margin — exact
|
|
309
|
-
connection points outrank the margin.
|
|
310
|
-
- **Loops are port-exact**: feedback exits the source's channel-facing
|
|
311
|
-
port (the diamond's exact S/N vertex) and enters the target's
|
|
312
|
-
channel-facing side midpoint, with short proven hops to the channel.
|
|
313
|
-
- **Port-only shapes**: circle, doublecircle, stadium, hexagon, cylinder
|
|
314
|
-
and the state pseudostates joined the straightenable set — their bbox
|
|
315
|
-
side midpoints are their only on-outline bbox points, so they accept
|
|
316
|
-
port lanes exactly and nothing else.
|
|
317
|
-
- Certificates record `sourcePort`/`targetPort` whenever an endpoint sits
|
|
318
|
-
on a canonical port, computed after all geometry settles.
|
|
319
|
-
- Certificates also record `sourcePortAssignment`/`targetPortAssignment`,
|
|
320
|
-
the dynamic allocator's additive side/slot/role view: physical side,
|
|
321
|
-
deterministic slot order along that side (`N/S` left→right, `E/W`
|
|
322
|
-
top→bottom), semantic role (`flow-*`, `feedback-*`, etc.), and the exact
|
|
323
|
-
`AnyPort` when the final endpoint is designated. This does **not**
|
|
324
|
-
reinterpret the V1 `sourcePort`/`targetPort` vocabulary; it is the stable
|
|
325
|
-
bridge for debugging and for feeding port intent into placement.
|
|
326
|
-
- Pre-layout placement now consumes a conservative slice of the same intent:
|
|
327
|
-
primary-forward edges get ELK node-port hints on the flow sides (`E/W` for
|
|
328
|
-
LR/RL, `S/N` for TD/BT), and eligible unlabeled rectangle/diamond feedback
|
|
329
|
-
edges without a labeled reciprocal primary get flipped-side `FIXED_SIDE`
|
|
330
|
-
hints on the return lane. Duplicate
|
|
331
|
-
primary-forward faces owned by the duplicate-lane planner now upgrade to
|
|
332
|
-
`FIXED_ORDER` only when the whole node-side order is certified: unlabeled,
|
|
333
|
-
rectangle-local, primary-forward, and containing a real duplicate pair.
|
|
334
|
-
Duplicate feedback ports still record deterministic side slots, but feedback
|
|
335
|
-
`FIXED_ORDER` remains deferred: the measured contact-sheet duplicate-feedback
|
|
336
|
-
case regressed when ELK was allowed to enforce order. Self-loop, labeled
|
|
337
|
-
feedback, labeled-reciprocal feedback, non-rect/diamond feedback, container,
|
|
338
|
-
cross-hierarchy and direction-override cases are explicitly relaxed and
|
|
339
|
-
recorded as diagnostics, leaving family-specific contracts to the final
|
|
340
|
-
certificate pass.
|
|
341
|
-
|
|
342
|
-
State diagrams inherit all graph certificates through their projected graphs
|
|
343
|
-
(`stateBodyToGraph` → `layoutGraphSync`). Non-graph families expose
|
|
344
|
-
family-specific certificates only when they own a final-geometry model:
|
|
345
|
-
architecture debug layouts now emit `family-layout` / `side-anchored`
|
|
346
|
-
endpoint-side certificates after the architecture reroute, while class and ER
|
|
347
|
-
debug layouts emit `family-layout` / `orthogonal-box` certificates for
|
|
348
|
-
relationship endpoints anchored on class/entity boxes. Default layout JSON
|
|
349
|
-
remains certificate-free.
|
|
350
|
-
|
|
351
|
-
Class and ER still render through their own ELK engines (`src/class/layout.ts`,
|
|
352
|
-
`src/er/layout.ts` call `elkLayoutSync` directly), so they do not participate
|
|
353
|
-
in the graph route straightener or port ranking. This is deliberate: class and
|
|
354
|
-
ER boxes carry compartment/attribute-row geometry and cardinality endpoint
|
|
355
|
-
semantics that the generic flowchart graph pass does not model. Projecting them
|
|
356
|
-
through `MermaidGraph` would either discard those row/compartment constraints or
|
|
357
|
-
require a second family-specific clipping pass after graph certification, which
|
|
358
|
-
would recreate the stale-certificate problem this document forbids. Their
|
|
359
|
-
`FamilyEdgeRouteCertificate`s and verify tripwires cover the accepted invariant instead:
|
|
360
|
-
orthogonal relationship paths, on-canvas non-overlapping boxes, and endpoints on
|
|
361
|
-
class/entity boundaries. Sequence messages likewise expose opt-in family `lifeline-message`
|
|
362
|
-
`FamilyEdgeRouteCertificate`s, while timeline/chart families expose
|
|
363
|
-
`RegionContainmentCertificate`s. These are family-specific certificates rather
|
|
364
|
-
than graph-route ports:
|
|
365
|
-
sequence anchors are lifelines, and timeline/chart anchors are intervals,
|
|
366
|
-
plot boxes, legend rows, sections, or quadrant regions.
|
|
367
|
-
|
|
368
|
-
### 6.2 Port ranking — the cost model over the four ports
|
|
369
|
-
|
|
370
|
-
Ports are ranked, per shape and per side, following the yFiles
|
|
371
|
-
port-candidate cost model (fixed candidates with costs + overflow):
|
|
372
|
-
|
|
373
|
-
1. **A side carrying exactly ONE line uses its canonical port** — the
|
|
374
|
-
diamond's *vertex* (the sharp bit), the side *midpoint* elsewhere. For a
|
|
375
|
-
single-line diamond side this is absolute: lines emit from points (dot
|
|
376
|
-
and yFiles draw decisions the same way), and when the straight vertex
|
|
377
|
-
lane is blocked the repairs rank by bend count:
|
|
378
|
-
1. the straight vertex lane (0 bends);
|
|
379
|
-
2. the **1-bend hook**: vertex lane along the flow axis, then one
|
|
380
|
-
perpendicular stub into the target's *facing* cross-side port — the
|
|
381
|
-
box's top/bottom in horizontal flow (the entry the IBM flowcharting
|
|
382
|
-
manuals illustrate). Requires the vertex lane to clear the target's
|
|
383
|
-
cross extent and a stub of at least `HOOK_STUB_MIN = 8` px (a
|
|
384
|
-
shorter stub reads as a hitch, not an entry — the degenerate TD/BT
|
|
385
|
-
case keeps its Z). **A fan-in merge outranks the hook**: when a
|
|
386
|
-
same-target sibling already holds the flow-side entry port, the Z
|
|
387
|
-
that converges there into one shared arrowhead (yFiles edge
|
|
388
|
-
grouping; Kakoulis–Tollis unambiguity) beats splitting the fan-in
|
|
389
|
-
across two sides;
|
|
390
|
-
3. the 2-bend Z from the vertex into the target's flow-side port.
|
|
391
|
-
The fixed-point loop upgrades hook/Z back to a straight vertex lane
|
|
392
|
-
the moment a blocking obstacle moves, and downgrades an
|
|
393
|
-
order-dependent hook to the merging Z once the sibling settles on the
|
|
394
|
-
shared port.
|
|
395
|
-
2. **A side carrying several lines spreads them** along its legal region
|
|
396
|
-
(facet/flat) — no line hogs the point. Fan-ins INTO a side still prefer
|
|
397
|
-
the target port, where same-target edges merge into one arrowhead.
|
|
398
|
-
3. **When two single-line port lanes conflict** (misaligned centers), the
|
|
399
|
-
sharper shape wins — vertex (sharpness 2) over flat midpoint (1) — and
|
|
400
|
-
the source wins ties (emit beats receive).
|
|
401
|
-
4. Curved/pointed regions never take floating attachment (port-only);
|
|
402
|
-
flat regions (hexagon/stadium N–S, cylinder E–W walls) absorb overflow.
|
|
403
|
-
5. **A reciprocal pair (unlabeled A→B with unlabeled B→A) renders as TWO
|
|
404
|
-
EQUAL parallel lines**, straddling the shared centerline at
|
|
405
|
-
`pairCenter ± PAIR_SEPARATION/2` (primary on the low side, feedback on
|
|
406
|
-
the high side — deterministic, never colliding). Equal deviation from
|
|
407
|
-
the center gives equal lengths: a diamond facet's width depends on the
|
|
408
|
-
lane's distance from the vertex, so an asymmetric split would draw one
|
|
409
|
-
long and one short arrow. This is how dot offsets reciprocal splines
|
|
410
|
-
symmetrically about the spine. Both pair members enroll in the
|
|
411
|
-
fixed-point pool even when already straight and on-port: the symmetric
|
|
412
|
-
lane usually only proves clear after the partner has been repaired, so
|
|
413
|
-
a member that had to settle for a fallback lane (an *upgradeable*
|
|
414
|
-
success — any straightening that won on a non-preferred candidate) is
|
|
415
|
-
re-proved each round until the symmetric lane wins or geometry stops
|
|
416
|
-
moving. Port-only shapes (circles) keep both lines on the exact port —
|
|
417
|
-
their spans admit no offset, by rule 4.
|
|
418
|
-
|
|
419
|
-
Occupancy is computed from route classes: primary edges count on the flow
|
|
420
|
-
sides, unlabeled feedback on the flipped sides (parallel back-lanes), and
|
|
421
|
-
labeled feedback not at all (it leaves via the outer channel's N/S ports).
|
|
422
|
-
Unlabeled feedback whose reciprocal primary edge is labeled is also relaxed so
|
|
423
|
-
the primary label keeps owning the forward lane. A bi-directional pair therefore
|
|
424
|
-
puts two lines on each facing side —
|
|
425
|
-
the symmetric pair of rule 5 — while a retry diamond's forward side
|
|
426
|
-
carries one line and gets the vertex.
|
|
427
|
-
|
|
428
|
-
#### 6.2.1 Where multi-line attachment lands: competitive + literature grounding
|
|
429
|
-
|
|
430
|
-
When a side carries several lines (rule 2), each line takes a straight
|
|
431
|
-
lane into its own target's port, so the attachment points on the source
|
|
432
|
-
facet are **target-determined** — they land wherever those lanes cross the
|
|
433
|
-
facet, and are therefore *not* rotation-consistent across flow directions
|
|
434
|
-
(a 2-way fan-out attaches at ~20%/80% of the facet in LR but ~6%/94% in
|
|
435
|
-
TD, because ELK spaces siblings by node heights in one axis and widths in
|
|
436
|
-
the other). This is a deliberate choice; the verified landscape
|
|
437
|
-
(primary-source research, June 2026):
|
|
438
|
-
|
|
439
|
-
- **Graphviz dot does what we do**: portless edges are "aimed at the
|
|
440
|
-
node's center and … clipped at the node's boundary" (dotguide §3.1,
|
|
441
|
-
TSE93 §5) — attachment is a byproduct of the route. Merging endpoints
|
|
442
|
-
is opt-in (`samehead`/`sametail`).
|
|
443
|
-
- **ELK does the opposite**: FREE ports get a side from edge direction,
|
|
444
|
-
a crossing-minimal order (barycenter heuristic), then coordinates by
|
|
445
|
-
even distribution (`portAlignment=DISTRIBUTED`). Schulze/Spönemann/von
|
|
446
|
-
Hanxleden (JVLC 2014) themselves show even distribution causing
|
|
447
|
-
avoidable bends and name "local adjustments to eliminate edge bends"
|
|
448
|
-
(their Fig. 4b) as the alternative — the certifying straightener is
|
|
449
|
-
that adjustment, applied proof-first.
|
|
450
|
-
- **yFiles splits the difference**: `PortAssignmentMode.DEFAULT`
|
|
451
|
-
"distributes ports evenly along the border of the node" (corner gap =
|
|
452
|
-
½ inter-port gap — rotation-consistent), then buys straightness back
|
|
453
|
-
with a bounded correction (3.x `maximumPortDeviation`, default 3px;
|
|
454
|
-
2.x `straightenEdges` was off by default and mutually exclusive with
|
|
455
|
-
symmetric placement). Its flowchart-specific layer
|
|
456
|
-
(`FlowchartLayout`, demo source) instead *avoids* shared facets:
|
|
457
|
-
Yes-branches get `WithTheFlow`, No-branches `Flatwise` (a side
|
|
458
|
-
vertex), deterministically de-conflicted — as soft port candidates
|
|
459
|
-
that crossing minimization may override.
|
|
460
|
-
- **Standards do NOT mandate vertex exits**: ISO 5807:1985 (verified via
|
|
461
|
-
its identical GOST 19.701-90 adoption) says lines enter left/top,
|
|
462
|
-
leave right/bottom, "directed toward the **center** of the symbol" —
|
|
463
|
-
dot's model, not a vertex rule. ANSI X3.5-1970 has no attachment rule
|
|
464
|
-
at all, and neither standard assigns yes/no to sides; both require
|
|
465
|
-
every decision exit to be labeled. Vertex exits (top in; left/right/
|
|
466
|
-
bottom out) are an IBM-illustrated convention (C20-8008 1959,
|
|
467
|
-
GC20-8152 1969) whose own text permits exit "in any direction".
|
|
468
|
-
- **Academia has no direct answer**: the validated priority order is
|
|
469
|
-
crossings ≫ bends/continuity > symmetry (Purchase GD'97/JVLC 2002;
|
|
470
|
-
Ware et al. 2002 on path continuity), and the node-placement
|
|
471
|
-
literature trends toward straightness through ports (Brandes–Köpf;
|
|
472
|
-
Rüegg et al. GD'15). Port assignment exists as a named phase since
|
|
473
|
-
Sander (GD'95) — order from crossing reduction, raster coordinates —
|
|
474
|
-
and Eichelberger's UML aesthetics *desire* equal port spacing, but
|
|
475
|
-
**no study has ever compared attachment-point policies empirically**;
|
|
476
|
-
the question is settled by tool convention.
|
|
477
|
-
|
|
478
|
-
Conclusion: straight-into-the-target-port maximizes the empirically
|
|
479
|
-
supported aesthetics (zero bends, continuity, exact target ports) at the
|
|
480
|
-
cost of the weakest one (rotation-consistent source attachment). The
|
|
481
|
-
known divergence from yFiles/ELK even-spread is accepted. A strict
|
|
482
|
-
"midpoint-only ports" ranking flip was prototyped and measured AGAINST:
|
|
483
|
-
on the docs corpus it converted 3 straight edges into 2-bend Zs for a
|
|
484
|
-
+1.9pp port-rate gain, and our own hitch prover correctly flagged the
|
|
485
|
-
new Zs (a clear straight lane existed). The asymmetry's real fixes are
|
|
486
|
-
**placement-side**: the port-lane alignment pass below, and (future
|
|
487
|
-
work) yFiles-style branch-direction assignment — give each decision
|
|
488
|
-
branch its own side pre-layout, so no facet carries two lines.
|
|
489
|
-
|
|
490
|
-
#### 6.2.2 Port-lane alignment — the placement repair
|
|
491
|
-
|
|
492
|
-
"Straight but off both midpoints" (an edge floating through the sliver
|
|
493
|
-
where two side-spans overlap) is a *placement* defect, not a routing
|
|
494
|
-
choice: ELK's BALANCED node placement averages competing pulls and
|
|
495
|
-
aligns the node with neither neighbor, and no routing-side repair can
|
|
496
|
-
reach "straight AND port-exact" from there. `alignPortLanes`
|
|
497
|
-
(`src/layout-engine.ts`, after `alignLayerNodes`, before bundling)
|
|
498
|
-
slides ONE endpoint node along the cross axis so the two midpoint ports
|
|
499
|
-
share a lane — the port-aware coordinate assignment of Rüegg et
|
|
500
|
-
al. (GD'15), applied as a targeted post-pass. ELK option alternatives
|
|
501
|
-
were measured and rejected: `favorStraightEdges` is a no-op under
|
|
502
|
-
BALANCED; `NETWORK_SIMPLEX` straightens one edge while un-porting
|
|
503
|
-
another; `bk.fixedAlignment: LEFTUP` gets a similar local result but
|
|
504
|
-
sacrifices the globally symmetric placement BALANCED provides.
|
|
505
|
-
|
|
506
|
-
Every slide is proof-gated, mirroring the occlusion doctrine:
|
|
507
|
-
|
|
508
|
-
- candidates are monotone flow-axis staircases (≤ 4 simplified points)
|
|
509
|
-
between rect-like endpoints with misaligned midpoint lanes;
|
|
510
|
-
- the resulting shared port lane must be clear of intermediate nodes
|
|
511
|
-
(otherwise the slide manufactures a through-node route the
|
|
512
|
-
straightener can never collapse);
|
|
513
|
-
- the slid node's other edges must be bent, with a perpendicular
|
|
514
|
-
segment to absorb the shift; their terminal runs translate, and a
|
|
515
|
-
label pill riding a translated run follows it;
|
|
516
|
-
- the new bbox keeps clear of nodes, foreign edge corridors, and label
|
|
517
|
-
pills; group members never move;
|
|
518
|
-
- both endpoints of an applied alignment are FROZEN — a later slide may
|
|
519
|
-
not re-break a proven alignment (the cascade hazard found by the
|
|
520
|
-
property oracle).
|
|
521
|
-
|
|
522
|
-
Downstream, the certifying straightener collapses the aligned staircase
|
|
523
|
-
onto the shared port lane with its usual proof, and the fan-in rules
|
|
524
|
-
compose: once the side input holds the target's entry port, a
|
|
525
|
-
co-terminating decision branch converges there (merge outranks the
|
|
526
|
-
hook). Corpus effect: straight edges 97 → 99, multi-bend routes 7 → 6,
|
|
527
|
-
port-exact endpoints 76.0% → 77.1% — better on every axis, where the
|
|
528
|
-
ranking-flip experiment traded one metric for another.
|
|
529
|
-
|
|
530
|
-
The pass covers the whole **PORT_EXACT catalog**, not just rects:
|
|
531
|
-
circles, stadiums, hexagons, cylinders, pseudostates, and diamonds all
|
|
532
|
-
slide (port-only shapes can never be "straight but off-port", so for
|
|
533
|
-
them the slide converts a forced 2-bend Z into a port-to-port
|
|
534
|
-
straight). Three additional proof obligations came with the extension,
|
|
535
|
-
each driven by a property-oracle counterexample or a pinned example:
|
|
536
|
-
|
|
537
|
-
- the straight-edge re-anchor uses `shapePorts` for BOTH coordinates
|
|
538
|
-
(a diamond's sloped facet and a circle's curve change the main anchor
|
|
539
|
-
with the cross slide; bbox side midpoints are exact for every shape);
|
|
540
|
-
- a **source diamond with a fan-out never aligns** — its vertex has
|
|
541
|
-
capacity 1 (yFiles cost model) and multi-line sides must spread;
|
|
542
|
-
- when a terminal run translates, the adjoining perpendicular segment
|
|
543
|
-
STRETCHES across a swept corridor, which is proved node-free like any
|
|
544
|
-
lane (a 123px slide once swept a sibling's hop through a circle).
|
|
545
|
-
|
|
546
|
-
A companion routing rule completes the composition: an
|
|
547
|
-
`explained-detour` whose **entry is off-port** now seeks a port-exact Z
|
|
548
|
-
(equal bends, exact ports) — so the edge left behind by an alignment
|
|
549
|
-
(its sibling claimed the lane) still converges at the shared entry
|
|
550
|
-
port. Cumulative corpus effect: straight edges 97 → 101, multi-bend
|
|
551
|
-
routes 7 → 5, port-exact endpoints 76.0% → 78.2%.
|
|
552
|
-
|
|
553
|
-
**The slanted family** (trapezoids, parallelograms `[/x/]`/`[\x\]` —
|
|
554
|
-
the ISO I/O symbol, previously misparsed — and the asymmetric flag) is
|
|
555
|
-
PORT_EXACT too: their N/S flats take spans, their E/W ports sit at the
|
|
556
|
-
TRUE side midpoints — the slant midpoint or the flag point, with the
|
|
557
|
-
cross coordinate still on the node centerline so every port-lane
|
|
558
|
-
mechanism composes unchanged. shapePorts is the single source of port
|
|
559
|
-
truth (tightenOuterFeedback and tryVertexHook now consume it instead of
|
|
560
|
-
raw bbox math — byte-identical for all previously supported shapes);
|
|
561
|
-
clipping uses a generic convex-polygon clipper, and the rubric carries
|
|
562
|
-
exact polygon outline oracles and footprints for all five.
|
|
563
|
-
|
|
564
|
-
#### 6.2.3 Active fan-in centering — symmetry for peer merges
|
|
565
|
-
|
|
566
|
-
`alignPortLanes`'s default slide moves a hub onto *one* source's lane,
|
|
567
|
-
which makes a peer fan-in (A→T, C→T) asymmetric: one edge straight, the
|
|
568
|
-
other a merge-Z, with the hub stuck at the top. For an **unlabeled,
|
|
569
|
-
equal-rank peer fan-in** — a target T whose incoming edges are ≥2 distinct
|
|
570
|
-
unlabeled forward sources all stacked in one layer — the pass instead
|
|
571
|
-
snaps T to the **exact cross-axis barycenter** of its sources (correcting
|
|
572
|
-
ELK's ~20px Brandes–Köpf placement drift), so both edges leave their
|
|
573
|
-
source's port and converge **mirror-symmetric** at T's single exact port.
|
|
574
|
-
The move is gated by the same occlusion doctrine (`hubMoveSafe`: no node,
|
|
575
|
-
foreign-corridor, or label-pill collision; T not in a group, T not also
|
|
576
|
-
emitting a forward edge), and the centered hub is frozen against later
|
|
577
|
-
slides.
|
|
578
|
-
|
|
579
|
-
The boundary is deliberate (verified on the docs corpus): it applies
|
|
580
|
-
*only* to single-column unlabeled peer fan-ins; it does **not** fire when
|
|
581
|
-
any incoming edge is labeled, sources span multiple ranks (e.g. ELK wraps
|
|
582
|
-
a large fan-in into a grid — correctly out of scope, since a grid has no
|
|
583
|
-
single symmetry axis), the hub also emits a forward edge, the move would
|
|
584
|
-
occlude, or the hub is in a subgraph — each keeps the prior behavior.
|
|
585
|
-
Achieves exact symmetry (0.00px barycenter error) with a ≤1.5px clipping
|
|
586
|
-
floor on curved/pointed outlines; cost is +2 bends per fan-in (two equal
|
|
587
|
-
bent lines replace one-straight/one-Z) with **zero hard-metric
|
|
588
|
-
regressions**. This is the literature-validated trade for *balanced
|
|
589
|
-
peer merges* (Purchase's symmetry aesthetic), distinct from the
|
|
590
|
-
*main-path* cases (A–K) where straightness still wins.
|
|
591
|
-
|
|
592
|
-
#### 6.2.4 Diamond facet-mid ports (the 8-port model)
|
|
593
|
-
|
|
594
|
-
A diamond's four cardinal ports are its vertices — but each *side* of a
|
|
595
|
-
diamond is two slanted facets, so a cardinal vertex is a poor anchor when
|
|
596
|
-
a side must carry more than one designated line. Diamonds therefore
|
|
597
|
-
expose **four extra ports at the facet midpoints** (`diamondFacetPorts`:
|
|
598
|
-
NE/SE/SW/NW, the midpoint of each slant, all exactly on the outline). This
|
|
599
|
-
is strictly additive and diamond-only: `shapePorts` stays four-cardinal
|
|
600
|
-
for every shape, and `portAt` checks the four cardinals first (byte-for-
|
|
601
|
-
byte the legacy probe) and only tests the facet-mids when a diamond
|
|
602
|
-
endpoint misses all four — so non-diamonds and diamond *vertex*
|
|
603
|
-
attachments are unchanged. Certificates' `sourcePort`/`targetPort` widen
|
|
604
|
-
to `AnyPort = PortSide | DiamondFacet`, and the port-rate metric counts a
|
|
605
|
-
facet-mid as a designated port (not a floating attachment).
|
|
606
|
-
|
|
607
|
-
Three behaviors use them:
|
|
608
|
-
- **Facet-mid alignment** (E): a diamond emitting two forward edges on one
|
|
609
|
-
flow side attaches the upper at the NE facet-mid and the lower at SE,
|
|
610
|
-
and `alignPortLanes` snaps each target onto that facet-mid's cross-lane
|
|
611
|
-
(cy ∓ h/4) — so both edges are **port-to-port straight** instead of
|
|
612
|
-
spreading at target-determined facet points. Proof-gated by the existing
|
|
613
|
-
occlusion checks plus a combined-pair overlap guard (in vertical flow
|
|
614
|
-
(F) the two targets would converge and collide — wider than the facet
|
|
615
|
-
span — so the pass correctly bails and F keeps the spread).
|
|
616
|
-
- **Reciprocal facet-mids** (G): a diamond↔diamond unlabeled reciprocal
|
|
617
|
-
pair attaches at the *nearest facing* facet-mids — Q.NE→R.NW (upper),
|
|
618
|
-
R.SW→Q.SE (lower) — two parallel lines **between** the diamonds, never
|
|
619
|
-
through them. Non-diamond reciprocals keep the ±`PAIR_SEPARATION`/2
|
|
620
|
-
vertex offset.
|
|
621
|
-
- **South-vertex entry** (K): a single unlabeled forward edge whose source
|
|
622
|
-
sits below a diamond whose facing cardinal port is already claimed
|
|
623
|
-
routes into the perpendicular **S vertex** (a canonical port) rather
|
|
624
|
-
than floating on the facet.
|
|
625
|
-
|
|
626
|
-
All endpoints stay exactly on the outline (the `ROUTE_SHAPE_MISANCHOR`
|
|
627
|
-
tripwire and rubric `offOutlineEndpoints` remain 0). Measured on the
|
|
628
|
-
heuristic tracker (`bun run track`): 6 improvements (E/G/K + the
|
|
629
|
-
diamond-reciprocal example move their endpoints onto designated ports),
|
|
630
|
-
0 regressions, 0 hard violations across all 51 examples.
|
|
631
|
-
|
|
632
|
-
#### 6.2.5 Dynamic side/slot/role allocator
|
|
633
|
-
|
|
634
|
-
The allocator now runs as a first-class route-contract component. For every
|
|
635
|
-
final node endpoint it derives:
|
|
636
|
-
|
|
637
|
-
- **side** — the physical cardinal side used by the final endpoint (facet-mid
|
|
638
|
-
diamonds keep the semantic adjacent side when possible, e.g. NE/SE on an
|
|
639
|
-
E-exit);
|
|
640
|
-
- **slotIndex/slotCount** — deterministic order among endpoints on that node
|
|
641
|
-
side, sorted by final side coordinate (`N/S`: x, `E/W`: y), then source
|
|
642
|
-
order; and
|
|
643
|
-
- **role** — why the endpoint is there (`flow-source`, `flow-target`,
|
|
644
|
-
`feedback-source`, `feedback-target`, container/cross-hierarchy/self-loop
|
|
645
|
-
variants).
|
|
646
|
-
|
|
647
|
-
The same allocator also feeds the existing port-ranking occupancy map, but
|
|
648
|
-
that occupancy deliberately counts the **semantic** side for primary and
|
|
649
|
-
unlabeled feedback endpoints only, preserving the proven routing behavior
|
|
650
|
-
while exposing richer final endpoint metadata. Labeled feedback keeps its
|
|
651
|
-
outer-channel route and does not compete for the facing side. This closes the
|
|
652
|
-
#26/#38 dynamic-port slice without changing default JSON: assignments are
|
|
653
|
-
visible only on opt-in debug certificates (`layoutMermaid(d, { debug: true })`
|
|
654
|
-
or `am render --format json --certificates`).
|
|
655
|
-
|
|
656
|
-
All of these compositions are pinned by the **contact sheet**
|
|
657
|
-
(`eval/visual-rubric/scenarios.ts`, lettered A–V; rendered for humans by
|
|
658
|
-
`bun run contact:sheet`): `src/__tests__/contact-sheet.test.ts` asserts
|
|
659
|
-
zero hard rubric metrics AND snapshot-pins each scenario's full layout
|
|
660
|
-
geometry, so future changes cannot visually break these drawings
|
|
661
|
-
without a deliberate re-pin and sheet review. The L–V fan-in scenarios
|
|
662
|
-
demonstrate the symmetric merge across every PORT_EXACT shape; E/G/K
|
|
663
|
-
demonstrate the diamond facet-mid ports.
|
|
664
|
-
|
|
665
|
-
### 6.3 Hardening found by the property oracles (layout-rubric harness)
|
|
666
|
-
|
|
667
|
-
Randomized property tests over all shapes × directions × patterns
|
|
668
|
-
(`src/__tests__/layout-rubric.test.ts`) drove these fixes — each was a real
|
|
669
|
-
defect class found by a counterexample, now pinned:
|
|
670
|
-
|
|
671
|
-
- **Shape-aware clipping for the whole catalog** (`src/shape-clipping.ts`):
|
|
672
|
-
circle/ellipse, stadium, hexagon, and cylinder outlines (clipping
|
|
673
|
-
previously existed only for diamonds, so off-port endpoints floated off
|
|
674
|
-
curved outlines). Cylinder cap clipping keeps the ray's own coordinate so
|
|
675
|
-
segments stay orthogonal.
|
|
676
|
-
- **Occlusion-safe layer alignment**: `alignLayerNodes` no longer snaps a
|
|
677
|
-
layer when the move would park a node on an already-routed foreign edge
|
|
678
|
-
corridor — the root cause of stale through-node routes (rule 9 enforced
|
|
679
|
-
at the source).
|
|
680
|
-
- **Through-node repair triggers**: a route passing through a node's bbox
|
|
681
|
-
triggers the Z-repair regardless of bend count, for primary, feedback,
|
|
682
|
-
and straight-certified edges; removing an occlusion outranks the
|
|
683
|
-
never-increase-crossings rule (a legal crossing beats an occlusion).
|
|
684
|
-
- **Flat-side spans**: hexagon N/S, stadium N/S and cylinder E/W sides are
|
|
685
|
-
flat regions on the bbox edge, so they accept attachment anywhere on the
|
|
686
|
-
flat (yFiles overflow-candidate pattern); only the curved/pointed pair is
|
|
687
|
-
port-only. Duplicate edges from such shapes get parallel anchors instead
|
|
688
|
-
of fighting over one port.
|
|
689
|
-
- **Fixed-point completeness**: geometry mutations from loop tightening and
|
|
690
|
-
Z-repairs drive the retry loop like straightenings do, and
|
|
691
|
-
`simplifyPolyline` iterates to a fixed point (ELK emits degenerate
|
|
692
|
-
zero-net spikes whose midpoints only become collinear after their
|
|
693
|
-
neighbors are removed).
|
|
694
|
-
- **Residual diagonal orthogonalization**: ELK's feedback router rarely
|
|
695
|
-
joins a port with a 45° segment; such segments are replaced with an
|
|
696
|
-
axis-aligned elbow continuing the previous segment's axis.
|
|
697
|
-
- **ELK crash degradation ladder**: the bundled GWT build of ELK throws
|
|
698
|
-
internal exceptions on rare dense multigraphs (issue #34 pins a
|
|
699
|
-
3-node/9-edge cyclic trigger: tier 0 throws `Invalid hitboxes for
|
|
700
|
-
scanline constraint calculation`, tier 1 succeeds with feedback routing
|
|
701
|
-
disabled). `layoutGraphSync` retries through progressively plainer option
|
|
702
|
-
sets; the route pass repairs whatever the survivor produces.
|
|
703
|
-
Crash-freedom is part of the renderer's contract.
|
|
704
|
-
|
|
705
|
-
## 7. Validation: the ROUTE_* tripwires (issue #25 Phase 1, complete)
|
|
706
|
-
|
|
707
|
-
`verifyMermaid` gains six Tier 2 (geometric, advisory) warnings. The layout
|
|
708
|
-
pipeline upholds every one of these invariants itself, so the warnings are
|
|
709
|
-
tripwires: they fire only when a pass mutates geometry after route
|
|
710
|
-
certification, or the pipeline regresses. All are recomputed over the FINAL
|
|
711
|
-
positioned graph, not trusted from the layout pass, and all are zero across
|
|
712
|
-
the docs corpus by construction (the one corpus hit during development was
|
|
713
|
-
a real floating-edge bug, which was then fixed — see "Container repair").
|
|
714
|
-
|
|
715
|
-
```
|
|
716
|
-
ROUTE_HITCH { edge, deviationPx } bends although a clear lane exists (flipped axis for feedback)
|
|
717
|
-
ROUTE_UNEXPLAINED_BEND { edge } diagonal segment under orthogonal routing
|
|
718
|
-
ROUTE_LABEL_ON_SHARED_TRUNK{ edge, sharedWith } label pill on a line segment another edge shares collinearly
|
|
719
|
-
ROUTE_CONTAINER_MISANCHOR { edge, container } container edge not terminating on the container border (§11.5)
|
|
720
|
-
ROUTE_SHAPE_MISANCHOR { edge, node } endpoint off the rendered shape boundary (§11.6; rect-like + diamond)
|
|
721
|
-
ROUTE_STALE_AFTER_NODE_MOVE{ edge, node } endpoint detached, or a non-incident node moved onto a route
|
|
722
|
-
```
|
|
723
|
-
|
|
724
|
-
Shapes without an anchor contract yet (circle, stadium, …) are exempt from
|
|
725
|
-
`ROUTE_SHAPE_MISANCHOR` rather than flagged — their endpoints sit on the
|
|
726
|
-
bbox today and warning on every one would be noise, not signal.
|
|
727
|
-
|
|
728
|
-
Still deliberately out of scope:
|
|
729
|
-
|
|
730
|
-
- Broad ELK port mutation beyond node-local primary/feedback semantics. The
|
|
731
|
-
implemented hint layer now emits `FIXED_SIDE` for eligible primary-forward
|
|
732
|
-
routes, upgrades duplicate primary-forward faces to `FIXED_ORDER` only when
|
|
733
|
-
the pre-freeze duplicate-lane planner owns the whole side order, and emits
|
|
734
|
-
`FIXED_SIDE` for the narrow unlabeled rectangle/diamond feedback slice
|
|
735
|
-
without a labeled reciprocal primary. Labeled feedback, labeled-reciprocal
|
|
736
|
-
feedback, non-rect/diamond feedback, self-loop, container, cross-hierarchy
|
|
737
|
-
and family-rerouted cases remain owned by the certifying repair pass.
|
|
738
|
-
Feedback `FIXED_ORDER` is still gated on new evidence: applying it to
|
|
739
|
-
duplicate feedback lanes regressed `contact-sheet/AQ` in `bun run track`.
|
|
740
|
-
`FIXED_POS` everywhere would still fight ELK's crossing minimization.
|
|
741
|
-
|
|
742
|
-
## 8. Pipeline placement
|
|
743
|
-
|
|
744
|
-
```
|
|
745
|
-
parse
|
|
746
|
-
-> MermaidGraph
|
|
747
|
-
-> mermaidToElk (model-order cycle breaking ≙ classification)
|
|
748
|
-
-> ELK layered layout
|
|
749
|
-
-> extractEdgesRecursively (+ orthogonalize cross-hierarchy)
|
|
750
|
-
-> alignLayerNodes
|
|
751
|
-
-> bundleEdgePaths (returns the set of bundle-owned edges)
|
|
752
|
-
-> clipEdgeToShape
|
|
753
|
-
-> route contracts: classify -> simplify (proof-free) -> straighten (proof-carrying) -> certify
|
|
754
|
-
-> bounds, renderer
|
|
755
|
-
```
|
|
756
|
-
|
|
757
|
-
The hard rule from issue #25 stands: no node-coordinate mutation after the
|
|
758
|
-
route-contract pass. `alignLayerNodes` runs before it, so any endpoint drift
|
|
759
|
-
it introduces is repaired or explained by the certifying pass rather than
|
|
760
|
-
shipped.
|
|
761
|
-
|
|
762
|
-
The diagram above is the conceptual sketch. The **authoritative** post-ELK pass
|
|
763
|
-
order is the `LAYOUT_PIPELINE` manifest in `src/layout-engine.ts` (reified per the
|
|
764
|
-
layout-pass-pipeline design doc), reproduced here and kept
|
|
765
|
-
in sync by `src/__tests__/layout-pass-docsync.test.ts` (regenerate with
|
|
766
|
-
`UPDATE_DOCS=1 bun test src/__tests__/layout-pass-docsync.test.ts`):
|
|
767
|
-
|
|
768
|
-
<!-- LAYOUT-PIPELINE:start -->
|
|
769
|
-
|
|
770
|
-
1. `extractEdgesRecursively` - flatten ELK edges to absolute coords (+orthogonalize cross-hierarchy)
|
|
771
|
-
2. `alignLayerNodes` - snap same-layer nodes onto a shared flow-axis line
|
|
772
|
-
3. `equalizePeerNodeDimensions` - equalize peer box sizes + pack layers so symmetry is visible downstream
|
|
773
|
-
4. `alignForkRejoinPeerCenters` - center fork/rejoin hubs on their peer barycenter
|
|
774
|
-
5. `alignPortLanes` - slide one endpoint node so a floating-straight edge becomes port-exact (Ruegg GD15)
|
|
775
|
-
6. `centerPeerBarycenters` - center peer fan-in/fan-out trunks over peer barycenters (#57/#61)
|
|
776
|
-
7. `honorLinkRankDistance` - shove target sub-DAG to honor variable-length link rank distance; push ahead anything the shove lands on and rebuild blocked reconnect routes through free channels (#81)
|
|
777
|
-
8. `alignLabeledSourcePort` - slide a single-outgoing labelled source onto the lane the straightener will use so the exit stays mid-port (alignPortLanes excludes labelled edges)
|
|
778
|
-
9. `bundleEdgePaths` - bundle fan-out/fan-in edges into shared trunks (when mergeEdges)
|
|
779
|
-
10. `markCorankFanInBundles` - re-route + mark co-ranked mixed-label fan-in spokes bundle-owned (justified symmetric-convergence bend)
|
|
780
|
-
11. `clipEdgeToShape` - clip edge endpoints to real (non-rect) shape outlines
|
|
781
|
-
12. `applySymmetricFanoutEmissions` - re-route small equivalent fan-outs symmetrically; mark bundle-owned
|
|
782
|
-
13. `applySymmetricParallelEdgeLanes` - separate parallel edges into symmetric non-crossing lanes
|
|
783
|
-
14. `applyParallelDuplicateLanes` - split exact duplicate edges into separated lanes
|
|
784
|
-
15. `collapseTinyBundledHitches` - remove sub-perceptual hitches introduced by bundling
|
|
785
|
-
16. `reassignBundledSiblingLabels` - re-home labels onto the correct bundled sibling segment
|
|
786
|
-
17. `applyRouteContracts` - classify -> simplify -> straighten (fixed-point) -> certify; FREEZES node geometry
|
|
787
|
-
18. `reanchorOffOutlineEndpoints` - re-route an edge whose endpoint dangles off a moved node onto that node's flow port (edge-only, freeze-safe)
|
|
788
|
-
19. `rerouteEdgesThroughNodes` - re-route an edge left running through a node by a node-mover (honorLinkRankDistance/alignPortLanes) around the obstacle (edge-only, freeze-safe)
|
|
789
|
-
20. `repairLabelsOnSharedTrunks` - re-slot a labeled edge whose pill sits on a trunk shared with another edge (label-only, freeze-safe)
|
|
790
|
-
21. `repairLabelsOffOwnRoute` - re-slot a labeled edge whose pill sits off its OWN route onto it — ELK offset placement on an already-straight edge (label-only, freeze-safe)
|
|
791
|
-
22. `separateEdgeLabelPills` - slide colliding edge-label pills along their own routes into clear slots — parallel/reciprocal lane labels stack at midpoints (label-only, freeze-safe)
|
|
792
|
-
23. `translateGeometryToNonNegativeOrigin` - shift whole graph to a non-negative origin (allowed after freeze)
|
|
793
|
-
|
|
794
|
-
<!-- LAYOUT-PIPELINE:end -->
|
|
795
|
-
|
|
796
|
-
## 9. Testing strategy (per testing-best-practices)
|
|
797
|
-
|
|
798
|
-
- **Red-first regression**: the MFA/login fixture asserts every
|
|
799
|
-
primary-forward edge with a clear lane renders with exactly two
|
|
800
|
-
axis-aligned points; written failing against the pre-change renderer.
|
|
801
|
-
- **Blocked-lane regression**: inserting a blocker node into the direct lane
|
|
802
|
-
must prevent straightening and produce `explained-detour` naming the
|
|
803
|
-
blocker (acceptance criterion 4).
|
|
804
|
-
- **Unit tests**: `classifyRoutes` (primary/feedback/self-loop/container),
|
|
805
|
-
`directLaneClear` (node, label, channel, span blockers each),
|
|
806
|
-
straightener safety (no straightening without proof; label capacity).
|
|
807
|
-
- **Property tests (fast-check)**: random small DAG-ish flowcharts —
|
|
808
|
-
certificates exist for every edge (a post-freeze repair that re-routes an
|
|
809
|
-
edge re-certifies it via `recertifyReroutedEdge`; gated deterministically by
|
|
810
|
-
`certificate-completeness.test.ts` — corpus, shrunk #83 repros, fixed-seed
|
|
811
|
-
duplicate-edge sweep); no primary-forward hitch survives when
|
|
812
|
-
the prover says the lane is clear; straightened endpoints remain on shape
|
|
813
|
-
boundaries; repeated runs byte-identical (determinism).
|
|
814
|
-
- **Goldens**: ASCII goldens must not change (`goldens:ascii:check`); SVG
|
|
815
|
-
changes are reviewed via the corpus comparison harness
|
|
816
|
-
(`eval/layout-compare`) — regressions-first verdicts, not eyeball-only.
|
|
817
|
-
- **Mutation testing**: the route-contracts module joins the scoped Stryker
|
|
818
|
-
lane so a deleted proof check cannot survive.
|
|
819
|
-
|
|
820
|
-
## 10. Rollout status vs issue #25
|
|
821
|
-
|
|
822
|
-
| Issue #25 phase | Status here |
|
|
823
|
-
|---|---|
|
|
824
|
-
| Phase 0 — diagnostics (classification + certificates) | implemented |
|
|
825
|
-
| Phase 1 — validation warnings | complete: all six ROUTE_* codes implemented as zero-noise tripwires |
|
|
826
|
-
| Phase 2 — semantic `FIXED_SIDE` / `FIXED_ORDER` ports | conservative slice implemented: primary-forward edges feed inferred source/target side intent to ELK fixed-side ports; duplicate primary-forward faces owned by the pre-freeze duplicate-lane planner upgrade to `FIXED_ORDER`; eligible unlabeled rectangle/diamond feedback routes without a labeled reciprocal primary feed flipped-side `FIXED_SIDE` ports with deterministic slot diagnostics; feedback `FIXED_ORDER` remains deferred after measured corpus regression; labeled feedback, labeled-reciprocal feedback, unsupported-shape feedback, self-loop/container/cross-hierarchy/direction-override relaxations rely on final certificates |
|
|
827
|
-
| Phase 3 — certifying simplifier | implemented (proof-free + proof-carrying layers) |
|
|
828
|
-
| Phase 4 — bundle contract | first slice implemented: bundled paths are proved clear of nodes, blocked members fall out of the bundle; per-trunk certificates deferred |
|
|
829
|
-
| Phase 5 — family adoption | state composites (`stateBodyToGraph`) flow through graph route contracts and certificates; architecture now emits family-specific endpoint-side certificates in debug layouts; class/ER keep their own ELK engines and emit family-specific orthogonal-box certificates plus rendered-layout geometry tripwires; sequence emits lifeline-message certificates; timeline/charts emit family element-containment layout certificates |
|
|
830
|
-
|
|
831
|
-
## 11. Acceptance criteria mapping
|
|
832
|
-
|
|
833
|
-
1. MFA/login renders straight forward edges when lanes are clear, in SVG **and** ASCII (longest-path ASCII layering put the fan-in join after its deepest parent; regression tests on both renderers + regenerated visual evidence).
|
|
834
|
-
2. Feedback retry edges emit feedback-route certificates — straight-with-proof (labeled ones park their pill in the open canvas beside the lane), `feedback-detour` with blockers otherwise — unit + regression test.
|
|
835
|
-
3. Disabling the direct-lane proof reintroduces a failing test — `ROUTE_HITCH` tripwire + straightener unit tests + mutation lane.
|
|
836
|
-
4. A blocker node prevents straightening with an explained-detour certificate — blocked-lane regression.
|
|
837
|
-
5. Diamond endpoints land on the diamond polygon, not bbox corners — ray-intersection re-anchoring + property test + `ROUTE_SHAPE_MISANCHOR` tripwire.
|
|
838
|
-
6. No node movement after the route pass — pipeline placement + `ROUTE_STALE_AFTER_NODE_MOVE` tripwire.
|
|
839
|
-
7. Corpus diff shows no regressions — `eval/layout-compare` runs recorded in the PR (18 changed / 0 regressions for the full batch).
|
|
840
|
-
8. Diagnostics available to tests and tooling — `classifyRoutes` / `auditRouteContracts` / `findRouteHitches` exported; `layoutMermaid(d, { debug: true })` attaches graph `RouteCertificate`s, family `FamilyEdgeRouteCertificate`s, and `RegionContainmentCertificate`s to layout JSON; and `am render --format json --certificates` exposes the same opt-in certificate schema without changing default JSON output.
|
|
841
|
-
|
|
842
|
-
## 12. Issue #25 open questions, resolved
|
|
843
|
-
|
|
844
|
-
1. *Certificates public or test-only?* — public but opt-in: `layoutMermaid(d, { debug: true })` and `am render --format json --certificates` expose route certificates; default JSON remains certificate-free for schema stability. V1 certificates keep the concrete `AnyPort = N|E|S|W|NE|SE|SW|NW` vocabulary, and the additive side+slot+semantic-role allocator is exposed as `sourcePortAssignment` / `targetPortAssignment`.
|
|
845
|
-
2. *Mermaid source route hints?* — no, as recommended; intent stays inferred from semantics and author order.
|
|
846
|
-
3. *Should feedback always detour?* — resolved by adopting ELK's feedbackEdges outer-channel routing plus loop tightening: labeled feedback loops around the nodes (label riding the loop with reserved space), unlabeled feedback collapses to a parallel straight back-arrow when provably clear. Feedback never shares the forward lane.
|
|
847
|
-
4. *Should labels force a bend?* — a label may only sit ON its own route (Kakoulis–Tollis unambiguity; dot's virtual-node doctrine). If the straight lane cannot host the pill, the edge keeps its outer loop — the canvas grows to make room, the label never floats beside a lane it doesn't belong to.
|
|
848
|
-
5. *Should architecture use ELK?* — unchanged: architecture keeps shared placement plus its own side-anchored rerouting. Public debug layout now exposes architecture-specific endpoint-side certificates for the final geometry instead of stale graph certificates.
|