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.
Files changed (333) hide show
  1. package/CHANGELOG.md +149 -4
  2. package/LICENSES/Apache-2.0.txt +202 -0
  3. package/README.md +25 -32
  4. package/THIRD_PARTY_NOTICES.md +29 -0
  5. package/assets/fonts/FONT-LICENSES.md +9 -5
  6. package/assets/fonts/Inter-Bold.ttf +0 -0
  7. package/assets/fonts/Inter-Medium.ttf +0 -0
  8. package/assets/fonts/Inter-Regular.ttf +0 -0
  9. package/assets/fonts/Inter-SemiBold.ttf +0 -0
  10. package/dist/agent-core.d.ts +463 -0
  11. package/dist/agent-core.js +397 -0
  12. package/dist/agent.d.ts +81 -1822
  13. package/dist/agent.js +336 -36839
  14. package/dist/agentic-mermaid-mcp.js +7 -37908
  15. package/dist/am.js +33638 -36261
  16. package/dist/chunk-4QECACIA.js +4702 -0
  17. package/dist/chunk-CDPKJ2BQ.js +37 -0
  18. package/dist/chunk-QKDLMOJJ.js +3487 -0
  19. package/dist/chunk-WI35L7IR.js +615 -0
  20. package/dist/chunk-WN3YDOBG.js +149 -0
  21. package/dist/chunk-ZYMMKQ3I.js +62637 -0
  22. package/dist/index-D54dqSdC.d.ts +6777 -0
  23. package/dist/index.d.ts +1 -1
  24. package/dist/index.js +310 -32836
  25. package/dist/mcp-cli-SBQSTJFC.js +17 -0
  26. package/docs/README.md +14 -6
  27. package/docs/schemas/style-spec.schema.json +1221 -30
  28. package/examples/styles/catalog.json +54 -0
  29. package/examples/styles/cloudflare-workers-inspired-prototype.mmd +9 -0
  30. package/examples/styles/cloudflare-workers-inspired-prototype.style.json +110 -0
  31. package/examples/styles/cupertino-prototype.mmd +10 -0
  32. package/examples/styles/cupertino-prototype.style.json +63 -0
  33. package/examples/styles/mid-century-report.style.json +2 -2
  34. package/examples/styles/star-chart-atlas.style.json +2 -1
  35. package/examples/styles/transit-route-map.style.json +2 -2
  36. package/examples/styles/vercel-inspired-prototype.mmd +6 -0
  37. package/examples/styles/vercel-inspired-prototype.style.json +87 -0
  38. package/package.json +70 -43
  39. package/server.json +41 -0
  40. package/skills/agentic-mermaid-diagram-workflow/SKILL.md +17 -36
  41. package/skills/agentic-mermaid-diagram-workflow/references/cli.md +4 -5
  42. package/skills/agentic-mermaid-diagram-workflow/references/code-mode.md +15 -31
  43. package/skills/agentic-mermaid-diagram-workflow/references/sequence.md +10 -14
  44. package/skills/agentic-mermaid-live-editor/SKILL.md +100 -206
  45. package/AGENT_NATIVE.md +0 -602
  46. package/Instructions_for_agents.md +0 -83
  47. package/TODO.md +0 -662
  48. package/assets/hero.png +0 -0
  49. package/dist/index-x2Cj1dVv.d.ts +0 -1089
  50. package/docs/agent-api-cookbook.md +0 -308
  51. package/docs/agent-mutation-policy.md +0 -104
  52. package/docs/agent-workflow-examples.md +0 -46
  53. package/docs/api.md +0 -203
  54. package/docs/ascii.md +0 -103
  55. package/docs/assets/improvements/box-start-connector.png +0 -0
  56. package/docs/assets/improvements/comment-dropped-lint.png +0 -0
  57. package/docs/assets/improvements/er-content-loss.png +0 -0
  58. package/docs/assets/improvements/fan-in-trunks.png +0 -0
  59. package/docs/assets/improvements/gantt-family.png +0 -0
  60. package/docs/assets/improvements/gantt-milestone-clamp.png +0 -0
  61. package/docs/assets/improvements/gantt-verify-seam.png +0 -0
  62. package/docs/assets/improvements/subgraph-container-edges.png +0 -0
  63. package/docs/assets/improvements/wrapper-frontmatter.png +0 -0
  64. package/docs/assets/improvements/wrapper-init-directive.png +0 -0
  65. package/docs/assets/improvements/wrapper-leading-comment.png +0 -0
  66. package/docs/assets/style-cookbook/mid-century-report.png +0 -0
  67. package/docs/assets/style-cookbook/star-chart-atlas.png +0 -0
  68. package/docs/assets/style-cookbook/transit-route-map.png +0 -0
  69. package/docs/comparison.md +0 -94
  70. package/docs/config.md +0 -105
  71. package/docs/contributing/adding-diagram-types.md +0 -144
  72. package/docs/contributing/diagram-family-citizenship.matrix.json +0 -2127
  73. package/docs/contributing/diagram-family-citizenship.md +0 -82
  74. package/docs/contributing/harvesting-upstream-tests.md +0 -126
  75. package/docs/contributing/lessons-learned.md +0 -276
  76. package/docs/contributing/releasing.md +0 -71
  77. package/docs/contributing/visual-review-evidence.md +0 -53
  78. package/docs/custom-style-cookbook.md +0 -154
  79. package/docs/design/families/README.md +0 -26
  80. package/docs/design/families/architecture-beta.md +0 -48
  81. package/docs/design/families/flowchart-parser-conformance.md +0 -22
  82. package/docs/design/families/gantt-research.md +0 -137
  83. package/docs/design/families/gantt.md +0 -329
  84. package/docs/design/families/journey-agentic-cards-before.png +0 -0
  85. package/docs/design/families/journey-mermaid-classic-implementation.png +0 -0
  86. package/docs/design/families/journey-mermaid-classic-prototype.png +0 -0
  87. package/docs/design/families/journey-mermaid-classic-prototype.svg +0 -163
  88. package/docs/design/families/journey-mermaid-classic-prototype.svg.png +0 -0
  89. package/docs/design/families/journey-migration-parity.md +0 -433
  90. package/docs/design/families/journey-usage-research.md +0 -423
  91. package/docs/design/families/journey.md +0 -125
  92. package/docs/design/families/xychart.md +0 -85
  93. package/docs/design/system/README.md +0 -45
  94. package/docs/design/system/abstraction-audit.md +0 -224
  95. package/docs/design/system/abstraction-recommendations.md +0 -655
  96. package/docs/design/system/architecture.mmd +0 -29
  97. package/docs/design/system/architecture.svg +0 -137
  98. package/docs/design/system/layout-guarantees-and-robustness.md +0 -351
  99. package/docs/design/system/layout-rubric.md +0 -138
  100. package/docs/design/system/route-contracts.md +0 -848
  101. package/docs/design/system/source-preservation-ladder.md +0 -52
  102. package/docs/design/system/styles-rollout.md +0 -216
  103. package/docs/design/system/ugly-layouts.md +0 -119
  104. package/docs/diagram-families.md +0 -193
  105. package/docs/features.md +0 -147
  106. package/docs/fork-differences.md +0 -130
  107. package/docs/getting-started.md +0 -175
  108. package/docs/issue-derived-test-cases.md +0 -156
  109. package/docs/layout-characterization/README.md +0 -245
  110. package/docs/layout-characterization/contact-sheet-families.md +0 -496
  111. package/docs/layout-characterization/contact-sheet.md +0 -638
  112. package/docs/layout-characterization/properties.md +0 -281
  113. package/docs/layout-characterization/visual-quality.md +0 -160
  114. package/docs/layout-characterization/visual-snapshots/architecture.svg +0 -79
  115. package/docs/layout-characterization/visual-snapshots/class.svg +0 -60
  116. package/docs/layout-characterization/visual-snapshots/er.svg +0 -61
  117. package/docs/layout-characterization/visual-snapshots/flowchart.svg +0 -58
  118. package/docs/layout-characterization/visual-snapshots/gantt.svg +0 -52
  119. package/docs/layout-characterization/visual-snapshots/journey.svg +0 -118
  120. package/docs/layout-characterization/visual-snapshots/pie.svg +0 -36
  121. package/docs/layout-characterization/visual-snapshots/quadrant.svg +0 -47
  122. package/docs/layout-characterization/visual-snapshots/sequence.svg +0 -59
  123. package/docs/layout-characterization/visual-snapshots/state.svg +0 -52
  124. package/docs/layout-characterization/visual-snapshots/timeline.svg +0 -64
  125. package/docs/layout-characterization/visual-snapshots/xychart.svg +0 -97
  126. package/docs/mcp-code-mode-rationale.md +0 -101
  127. package/docs/mcp-http-transport.md +0 -223
  128. package/docs/mermaid-layout-complaints.md +0 -837
  129. package/docs/mutation-testing.md +0 -206
  130. package/docs/project/divergences.md +0 -98
  131. package/docs/project/dogfooding-docs-strategy.md +0 -66
  132. package/docs/project/hosted-mcp-cloudflare-plan.md +0 -338
  133. package/docs/project/lessons-learned.md +0 -1019
  134. package/docs/project/remove-role-styling-plan.md +0 -176
  135. package/docs/quality.md +0 -212
  136. package/docs/react.md +0 -97
  137. package/docs/style-authoring.md +0 -133
  138. package/docs/testing-strategy.md +0 -335
  139. package/docs/theming.md +0 -162
  140. package/examples/agent-improve-auth-flow.ts +0 -206
  141. package/examples/agent-loop.ts +0 -53
  142. package/examples/mcp-vs-cli-complex-diagrams.ts +0 -133
  143. package/llms.txt +0 -104
  144. package/skill-evals/README.md +0 -77
  145. package/skill-evals/fixtures/architecture-cache/input.mmd +0 -5
  146. package/skill-evals/fixtures/class-payment/input.mmd +0 -4
  147. package/skill-evals/fixtures/er-orders/input.mmd +0 -10
  148. package/skill-evals/fixtures/flowchart-cache/input.mmd +0 -3
  149. package/skill-evals/fixtures/gantt-release/input.mmd +0 -7
  150. package/skill-evals/fixtures/journey-checkout/input.mmd +0 -6
  151. package/skill-evals/fixtures/pie-exports/input.mmd +0 -5
  152. package/skill-evals/fixtures/quadrant-priorities/input.mmd +0 -11
  153. package/skill-evals/fixtures/sequence-retry/input.mmd +0 -8
  154. package/skill-evals/fixtures/state-lifecycle/input.mmd +0 -5
  155. package/skill-evals/fixtures/timeline-release/input.mmd +0 -6
  156. package/skill-evals/fixtures/xychart-growth/input.mmd +0 -5
  157. package/skill-evals/shared-benchmark.json +0 -510
  158. package/src/agent/analyze.ts +0 -157
  159. package/src/agent/apply.ts +0 -146
  160. package/src/agent/architecture-body.ts +0 -458
  161. package/src/agent/certificates.ts +0 -110
  162. package/src/agent/class-body.ts +0 -319
  163. package/src/agent/core.ts +0 -207
  164. package/src/agent/create.ts +0 -106
  165. package/src/agent/describe.ts +0 -364
  166. package/src/agent/er-body.ts +0 -224
  167. package/src/agent/facts.ts +0 -337
  168. package/src/agent/families-builtin.ts +0 -934
  169. package/src/agent/families.ts +0 -232
  170. package/src/agent/family-layouts.ts +0 -826
  171. package/src/agent/flowchart-body.ts +0 -372
  172. package/src/agent/flowchart-unsupported.ts +0 -243
  173. package/src/agent/gantt-body.ts +0 -495
  174. package/src/agent/index.ts +0 -10
  175. package/src/agent/journey-body.ts +0 -414
  176. package/src/agent/label-metrics.ts +0 -28
  177. package/src/agent/layout-to-rendered.ts +0 -63
  178. package/src/agent/mutate.ts +0 -97
  179. package/src/agent/mutation-ops.ts +0 -51
  180. package/src/agent/op-schema.ts +0 -439
  181. package/src/agent/parse.ts +0 -223
  182. package/src/agent/pie-body.ts +0 -270
  183. package/src/agent/png.ts +0 -117
  184. package/src/agent/quadrant-body.ts +0 -336
  185. package/src/agent/quality.ts +0 -363
  186. package/src/agent/readability-audit.ts +0 -92
  187. package/src/agent/rendered-route-audit.ts +0 -61
  188. package/src/agent/sequence-body.ts +0 -345
  189. package/src/agent/serialize.ts +0 -214
  190. package/src/agent/state-body.ts +0 -589
  191. package/src/agent/structural-count.ts +0 -150
  192. package/src/agent/timeline-body.ts +0 -280
  193. package/src/agent/trace-log.ts +0 -43
  194. package/src/agent/types.ts +0 -979
  195. package/src/agent/verify.ts +0 -571
  196. package/src/agent/xychart-body.ts +0 -472
  197. package/src/architecture/config.ts +0 -282
  198. package/src/architecture/layout.ts +0 -496
  199. package/src/architecture/parser.ts +0 -375
  200. package/src/architecture/renderer.ts +0 -611
  201. package/src/architecture/types.ts +0 -113
  202. package/src/ascii/ansi.ts +0 -423
  203. package/src/ascii/architecture.ts +0 -210
  204. package/src/ascii/canvas.ts +0 -443
  205. package/src/ascii/class-diagram.ts +0 -697
  206. package/src/ascii/converter.ts +0 -475
  207. package/src/ascii/draw.ts +0 -1693
  208. package/src/ascii/edge-bundling.ts +0 -328
  209. package/src/ascii/edge-routing.ts +0 -330
  210. package/src/ascii/er-diagram.ts +0 -439
  211. package/src/ascii/gantt.ts +0 -276
  212. package/src/ascii/grid.ts +0 -747
  213. package/src/ascii/index.ts +0 -196
  214. package/src/ascii/journey.ts +0 -189
  215. package/src/ascii/meta.ts +0 -244
  216. package/src/ascii/multiline-utils.ts +0 -80
  217. package/src/ascii/pathfinder.ts +0 -301
  218. package/src/ascii/pie.ts +0 -67
  219. package/src/ascii/quadrant.ts +0 -158
  220. package/src/ascii/reverse.ts +0 -216
  221. package/src/ascii/sequence.ts +0 -499
  222. package/src/ascii/shapes/circle.ts +0 -27
  223. package/src/ascii/shapes/corners.ts +0 -143
  224. package/src/ascii/shapes/diamond.ts +0 -27
  225. package/src/ascii/shapes/hexagon.ts +0 -27
  226. package/src/ascii/shapes/index.ts +0 -106
  227. package/src/ascii/shapes/rectangle.ts +0 -168
  228. package/src/ascii/shapes/rounded.ts +0 -27
  229. package/src/ascii/shapes/special.ts +0 -332
  230. package/src/ascii/shapes/stadium.ts +0 -110
  231. package/src/ascii/shapes/state.ts +0 -192
  232. package/src/ascii/shapes/types.ts +0 -73
  233. package/src/ascii/timeline.ts +0 -128
  234. package/src/ascii/types.ts +0 -312
  235. package/src/ascii/validate.ts +0 -120
  236. package/src/ascii/width.ts +0 -81
  237. package/src/ascii/xychart.ts +0 -919
  238. package/src/browser.ts +0 -29
  239. package/src/class/layout.ts +0 -240
  240. package/src/class/parser.ts +0 -332
  241. package/src/class/renderer.ts +0 -638
  242. package/src/class/types.ts +0 -129
  243. package/src/cli/agent-instructions.ts +0 -1
  244. package/src/cli/am-bin.ts +0 -6
  245. package/src/cli/exit-codes.ts +0 -18
  246. package/src/cli/index.ts +0 -1117
  247. package/src/cli/init-agent.ts +0 -147
  248. package/src/color-resolver.ts +0 -152
  249. package/src/elk-instance.ts +0 -114
  250. package/src/er/layout.ts +0 -187
  251. package/src/er/parser.ts +0 -226
  252. package/src/er/renderer.ts +0 -792
  253. package/src/er/types.ts +0 -99
  254. package/src/font-manifest.ts +0 -27
  255. package/src/gantt/layout.ts +0 -365
  256. package/src/gantt/parser.ts +0 -282
  257. package/src/gantt/pipeline.ts +0 -27
  258. package/src/gantt/renderer.ts +0 -414
  259. package/src/gantt/schedule.ts +0 -513
  260. package/src/gantt/types.ts +0 -255
  261. package/src/index.ts +0 -368
  262. package/src/journey/layout.ts +0 -520
  263. package/src/journey/parser.ts +0 -218
  264. package/src/journey/renderer.ts +0 -811
  265. package/src/journey/types.ts +0 -144
  266. package/src/layout/geometry.ts +0 -182
  267. package/src/layout/pass.ts +0 -142
  268. package/src/layout/passes/index.ts +0 -3916
  269. package/src/layout-engine.ts +0 -1565
  270. package/src/layout-rubric.ts +0 -659
  271. package/src/mcp/artifacts.ts +0 -137
  272. package/src/mcp/dynamic-harness.ts +0 -31
  273. package/src/mcp/dynamic-modules.d.ts +0 -7
  274. package/src/mcp/facade.ts +0 -608
  275. package/src/mcp/harness-runtime.ts +0 -203
  276. package/src/mcp/hosted-server.ts +0 -468
  277. package/src/mcp/mcp-bin.ts +0 -3
  278. package/src/mcp/mcp-cli.ts +0 -82
  279. package/src/mcp/protocol.ts +0 -13
  280. package/src/mcp/sandbox.ts +0 -195
  281. package/src/mcp/sdk-decl.ts +0 -387
  282. package/src/mcp/server.ts +0 -362
  283. package/src/mcp/tool-surface.ts +0 -146
  284. package/src/mermaid-source.ts +0 -655
  285. package/src/multiline-utils.ts +0 -228
  286. package/src/parser.ts +0 -1031
  287. package/src/pie/layout.ts +0 -168
  288. package/src/pie/parser.ts +0 -136
  289. package/src/pie/renderer.ts +0 -234
  290. package/src/pie/types.ts +0 -82
  291. package/src/quadrant/layout.ts +0 -191
  292. package/src/quadrant/parser.ts +0 -183
  293. package/src/quadrant/renderer.ts +0 -322
  294. package/src/quadrant/types.ts +0 -113
  295. package/src/render-family-hooks.ts +0 -244
  296. package/src/renderer.ts +0 -1099
  297. package/src/route-contracts.ts +0 -2723
  298. package/src/scene/backend.ts +0 -76
  299. package/src/scene/fidelity.ts +0 -183
  300. package/src/scene/hybrid-backend.ts +0 -174
  301. package/src/scene/ir.ts +0 -181
  302. package/src/scene/marks.ts +0 -127
  303. package/src/scene/rough-backend.ts +0 -378
  304. package/src/scene/seed.ts +0 -46
  305. package/src/scene/style-registry.ts +0 -556
  306. package/src/sequence/layout.ts +0 -456
  307. package/src/sequence/parser.ts +0 -255
  308. package/src/sequence/renderer.ts +0 -604
  309. package/src/sequence/types.ts +0 -154
  310. package/src/shape-clipping.ts +0 -351
  311. package/src/shared/accessibility-directives.ts +0 -22
  312. package/src/shared/batched.ts +0 -61
  313. package/src/shared/color-math.ts +0 -72
  314. package/src/shared/svg-a11y.ts +0 -29
  315. package/src/shared/syntax-error.ts +0 -17
  316. package/src/shared/unicode-ranges.ts +0 -135
  317. package/src/styles.ts +0 -318
  318. package/src/svg-paths.ts +0 -28
  319. package/src/text-metrics.ts +0 -248
  320. package/src/theme.ts +0 -639
  321. package/src/timeline/layout.ts +0 -381
  322. package/src/timeline/parser.ts +0 -215
  323. package/src/timeline/renderer.ts +0 -538
  324. package/src/timeline/types.ts +0 -105
  325. package/src/types.ts +0 -454
  326. package/src/website-worker-modules.d.ts +0 -18
  327. package/src/xychart/axis-utils.ts +0 -109
  328. package/src/xychart/colors.ts +0 -124
  329. package/src/xychart/config.ts +0 -88
  330. package/src/xychart/layout.ts +0 -656
  331. package/src/xychart/parser.ts +0 -504
  332. package/src/xychart/renderer.ts +0 -640
  333. 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.