pmx-canvas 0.11.0 → 0.12.1

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 (300) hide show
  1. package/.github/extensions/pmx-canvas/extension.mjs +4 -1
  2. package/.github/extensions/pmx-canvas/runtime.json +1 -1
  3. package/.github/extensions/pmx-canvas/tools.json +440 -18
  4. package/CHANGELOG.md +51 -0
  5. package/dist/amp-plugin/pmx-canvas/LICENSE +21 -0
  6. package/dist/amp-plugin/pmx-canvas/THIRD_PARTY_LICENSES.txt +331 -0
  7. package/dist/amp-plugin/pmx-canvas/index.js +5445 -0
  8. package/dist/amp-plugin/pmx-canvas/runtime.json +3 -0
  9. package/dist/amp-plugin/pmx-canvas/shared-runtime.mjs +194 -0
  10. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/SKILL.md +267 -0
  11. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/evals/evals.json +1168 -0
  12. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/evals/fixtures/code-exploration/src/auth/jwt.ts +17 -0
  13. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/evals/fixtures/code-exploration/src/auth/login.ts +12 -0
  14. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/evals/fixtures/code-exploration/src/auth/middleware.ts +13 -0
  15. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/evals/fixtures/code-exploration/src/routes/auth.ts +13 -0
  16. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/evals/fixtures/investigation-board/src/handlers/users.ts +27 -0
  17. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/mcp.json +36 -0
  18. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/ax-html-control-surface.md +101 -0
  19. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/codex-app-adapter.md +151 -0
  20. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/excalidraw-diagram-authoring.md +150 -0
  21. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/full-reference.md +1839 -0
  22. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/github-copilot-app-adapter.md +223 -0
  23. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/html-primitives.md +139 -0
  24. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/importing-context.md +132 -0
  25. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/installing-pmx-canvas.md +301 -0
  26. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/tours-and-recording.md +117 -0
  27. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/workbench-workflows.md +908 -0
  28. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas-orchestration/SKILL.md +141 -0
  29. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas-testing/SKILL.md +266 -0
  30. package/dist/amp-plugin/pmx-canvas/start-mcp.mjs +14857 -0
  31. package/dist/amp-plugin/pmx-canvas/tools.json +10963 -0
  32. package/dist/canvas/chunks/{abnfDiagram-N423BO3Z-p8dtp6va.js → abnfDiagram-N423BO3Z-cwtsds5d.js} +1 -1
  33. package/dist/canvas/chunks/architecture-TIHT7OUA-x63cwejs.js +1 -0
  34. package/dist/canvas/chunks/{architectureDiagram-T3A2C74G-5jhmxrzf.js → architectureDiagram-T3A2C74G-b4xc9tpz.js} +3 -3
  35. package/dist/canvas/chunks/{blockDiagram-VBNYF7ZC-pw1fa9yb.js → blockDiagram-VBNYF7ZC-ahp3yxxs.js} +7 -7
  36. package/dist/canvas/chunks/{c4Diagram-5PPSVZJV-x07nq0tt.js → c4Diagram-5PPSVZJV-nappnay7.js} +4 -4
  37. package/dist/canvas/chunks/classDiagram-JCYQIIEL-k1j2xc1t.js +1 -0
  38. package/dist/canvas/chunks/classDiagram-v2-OCEON4UE-33x08xf2.js +1 -0
  39. package/dist/canvas/chunks/{cose-bilkent-JH36ORCC-nm39npq0.js → cose-bilkent-JH36ORCC-a428syda.js} +1 -1
  40. package/dist/canvas/chunks/cynefin-VYW2F7L2-8v6jxfz8.js +1 -0
  41. package/dist/canvas/chunks/{cynefinDiagram-MW4NZA55-vdxrwahz.js → cynefinDiagram-MW4NZA55-nspsqebj.js} +1 -1
  42. package/dist/canvas/chunks/{dagre-VZM6K2ZE-bqdm7wpd.js → dagre-VZM6K2ZE-8y6177fh.js} +2 -2
  43. package/dist/canvas/chunks/{diagram-7IWD3JNH-gs5gpr93.js → diagram-7IWD3JNH-vym028wn.js} +4 -4
  44. package/dist/canvas/chunks/{diagram-B4RE2ZJO-cgwjg47s.js → diagram-B4RE2ZJO-jddqb8sy.js} +1 -1
  45. package/dist/canvas/chunks/{diagram-LBJQPF4R-w3vahpav.js → diagram-LBJQPF4R-bjwbpkrn.js} +1 -1
  46. package/dist/canvas/chunks/{diagram-Q27KOJAE-42837z6h.js → diagram-Q27KOJAE-j6ph6hcx.js} +2 -2
  47. package/dist/canvas/chunks/{diagram-UB23O5K3-4fz83q59.js → diagram-UB23O5K3-p09w8jej.js} +1 -1
  48. package/dist/canvas/chunks/{ebnfDiagram-BXEA7PRR-zphtgegj.js → ebnfDiagram-BXEA7PRR-j975ycq5.js} +1 -1
  49. package/dist/canvas/chunks/{erDiagram-JOGREHBK-zvbbdxp4.js → erDiagram-JOGREHBK-sxp6hjn8.js} +5 -5
  50. package/dist/canvas/chunks/eventmodeling-45OFAUF4-xzfgn6xg.js +1 -0
  51. package/dist/canvas/chunks/flowDiagram-UKHOOZJN-2nsmkskb.js +1 -0
  52. package/dist/canvas/chunks/{ganttDiagram-PKOTCBZU-282jzycm.js → ganttDiagram-PKOTCBZU-4tv230y2.js} +2 -2
  53. package/dist/canvas/chunks/gitGraph-TEB2WS4Q-macbhand.js +1 -0
  54. package/dist/canvas/chunks/{gitGraphDiagram-DS77QQ5N-vqqy6p1b.js → gitGraphDiagram-DS77QQ5N-mqgmnw9g.js} +1 -1
  55. package/dist/canvas/chunks/{index-wgv7en21.js → index-037z5dz2.js} +2 -2
  56. package/dist/canvas/chunks/{index-w2mra17t.js → index-081n3har.js} +1 -1
  57. package/dist/canvas/chunks/{index-353m7b0p.js → index-5n03dmb9.js} +1 -1
  58. package/dist/canvas/chunks/{index-yw8h9vha.js → index-60pgzdqe.js} +1 -1
  59. package/dist/canvas/chunks/index-8g1vb57n.js +2 -0
  60. package/dist/canvas/chunks/{index-vv56h019.js → index-8rjes9r6.js} +1 -1
  61. package/dist/canvas/chunks/{index-nprm96xp.js → index-9022d1n9.js} +1 -1
  62. package/dist/canvas/chunks/{index-194m3hc3.js → index-9b02042t.js} +1 -1
  63. package/dist/canvas/chunks/{index-0rk4gp1g.js → index-ap4q4vez.js} +1 -1
  64. package/dist/canvas/chunks/{index-rh7nv68n.js → index-dyp2nhd3.js} +1 -1
  65. package/dist/canvas/chunks/index-frvta4tm.js +2 -0
  66. package/dist/canvas/chunks/{index-s4j678gj.js → index-h9ycj8n2.js} +1 -1
  67. package/dist/canvas/chunks/{index-fjtdggjn.js → index-mergmj97.js} +2 -2
  68. package/dist/canvas/chunks/index-nwsya6dj.js +301 -0
  69. package/dist/canvas/chunks/{index-3cy1bzwh.js → index-nz8qbd1p.js} +1 -1
  70. package/dist/canvas/chunks/{index-d3b3gqe9.js → index-qhfrazv3.js} +1 -1
  71. package/dist/canvas/chunks/{index-6qmsnxwy.js → index-v119nznk.js} +4 -4
  72. package/dist/canvas/chunks/{index-r2fyc9w1.js → index-vpt37m3d.js} +6 -6
  73. package/dist/canvas/chunks/{index-qj8fxwsf.js → index-y9ma6azy.js} +1 -1
  74. package/dist/canvas/chunks/{index-tb1wf525.js → index-zx0j3a2z.js} +26 -26
  75. package/dist/canvas/chunks/info-DKCQHKI2-a0ykzs9z.js +1 -0
  76. package/dist/canvas/chunks/{infoDiagram-6WML65LV-v1yvggqj.js → infoDiagram-6WML65LV-qv97qq6a.js} +2 -2
  77. package/dist/canvas/chunks/ishikawaDiagram-WSZJBQD7-p9306gh0.js +70 -0
  78. package/dist/canvas/chunks/{journeyDiagram-NVQOT4AX-zjcffveh.js → journeyDiagram-NVQOT4AX-sv67zats.js} +5 -5
  79. package/dist/canvas/chunks/{kanban-definition-27J2QSJJ-dakqtfp1.js → kanban-definition-27J2QSJJ-k2pfam6p.js} +1 -1
  80. package/dist/canvas/chunks/{mermaid.core-j2geswrd.js → mermaid.core-83r0fdce.js} +4 -4
  81. package/dist/canvas/chunks/{mindmap-definition-FAOFIHXS-142xhr3k.js → mindmap-definition-FAOFIHXS-vg2newm1.js} +1 -1
  82. package/dist/canvas/chunks/packet-7NZHBO7P-tqnabcdv.js +1 -0
  83. package/dist/canvas/chunks/{pegDiagram-VL7TDLO6-5wvk7vsd.js → pegDiagram-VL7TDLO6-k2h7zc5n.js} +1 -1
  84. package/dist/canvas/chunks/pie-RZYD4A2V-wke6gyhe.js +1 -0
  85. package/dist/canvas/chunks/{pieDiagram-7S7Q4E2Y-ztdhwycc.js → pieDiagram-7S7Q4E2Y-hg169p1v.js} +2 -2
  86. package/dist/canvas/chunks/{quadrantDiagram-CIZ2JOQS-c5p9gmg6.js → quadrantDiagram-CIZ2JOQS-bxtvsq0b.js} +5 -5
  87. package/dist/canvas/chunks/radar-I7S5WNFK-w7ka0drw.js +1 -0
  88. package/dist/canvas/chunks/railroad-3IZDKUUU-y3vg318x.js +1 -0
  89. package/dist/canvas/chunks/railroad-abnf-AHOZXSZD-6hqvcz3c.js +1 -0
  90. package/dist/canvas/chunks/railroad-ebnf-EBAXGLYW-vcae260m.js +1 -0
  91. package/dist/canvas/chunks/railroad-peg-LSFZ7HO6-0dbyme23.js +1 -0
  92. package/dist/canvas/chunks/{railroadDiagram-AXF67PYL-xkmqkjwf.js → railroadDiagram-AXF67PYL-njyhcp4p.js} +1 -1
  93. package/dist/canvas/chunks/{requirementDiagram-LRYGKXZP-9wm2dyaw.js → requirementDiagram-LRYGKXZP-2z6v09gm.js} +4 -4
  94. package/dist/canvas/chunks/{sankeyDiagram-W5VNT64P-2w19etgb.js → sankeyDiagram-W5VNT64P-15mzam0e.js} +1 -1
  95. package/dist/canvas/chunks/{sequenceDiagram-SI44F4Z6-0jn0vwmz.js → sequenceDiagram-SI44F4Z6-rbshyd2c.js} +12 -12
  96. package/dist/canvas/chunks/{sizeCapture-X5ZJPWSS-x75epwdq.js → sizeCapture-X5ZJPWSS-sg26xwts.js} +1 -1
  97. package/dist/canvas/chunks/{stateDiagram-OKZ733FA-schjpy6d.js → stateDiagram-OKZ733FA-6eahp98e.js} +1 -1
  98. package/dist/canvas/chunks/stateDiagram-v2-UEYNNEHI-tty0ggn5.js +1 -0
  99. package/dist/canvas/chunks/{swimlanes-SLNWSIFB-gkw4xce3.js → swimlanes-SLNWSIFB-ze5db10t.js} +1 -1
  100. package/dist/canvas/chunks/swimlanesDiagram-ULZ7WXOC-thze75gq.js +8 -0
  101. package/dist/canvas/chunks/{timeline-definition-Z64GVDOM-x7nkcy01.js → timeline-definition-Z64GVDOM-vnp7jyy8.js} +3 -3
  102. package/dist/canvas/chunks/treeView-QDETBFTQ-6ysffnm6.js +1 -0
  103. package/dist/canvas/chunks/treemap-6X3UGDF4-m3h9p066.js +1 -0
  104. package/dist/canvas/chunks/vennDiagram-T6HMQDX7-0qrnb7mj.js +34 -0
  105. package/dist/canvas/chunks/wardley-OPB4EBWU-dp37drwx.js +1 -0
  106. package/dist/canvas/chunks/{wardleyDiagram-T6FBY63Y-t1j52t9r.js → wardleyDiagram-T6FBY63Y-8nxg8wqt.js} +4 -4
  107. package/dist/canvas/chunks/{xychartDiagram-ELKLHX3M-38t16pme.js → xychartDiagram-ELKLHX3M-nj921e7s.js} +5 -5
  108. package/dist/canvas/global.css +94 -1
  109. package/dist/canvas/index.js +154 -145
  110. package/dist/canvas/mcp-app.html +600 -498
  111. package/dist/json-render/index.css +1 -1
  112. package/dist/types/client/canvas/BoardMoveDialog.d.ts +2 -0
  113. package/dist/types/client/canvas/EdgeEditor.d.ts +19 -0
  114. package/dist/types/client/canvas/GroupNavigation.d.ts +2 -0
  115. package/dist/types/client/canvas/TextPrompt.d.ts +4 -0
  116. package/dist/types/client/nodes/PromptNode.d.ts +1 -0
  117. package/dist/types/client/nodes/ViewerFrame.d.ts +1 -1
  118. package/dist/types/client/nodes/WorkCard.d.ts +7 -0
  119. package/dist/types/client/nodes/use-ax-surface-bridge.d.ts +1 -0
  120. package/dist/types/client/state/canvas-store.d.ts +15 -0
  121. package/dist/types/client/state/human-store.d.ts +2 -0
  122. package/dist/types/client/state/intent-bridge.d.ts +4 -2
  123. package/dist/types/client/state/relation-store.d.ts +11 -0
  124. package/dist/types/client/types.d.ts +1 -1
  125. package/dist/types/json-render/catalog.d.ts +2 -2
  126. package/dist/types/server/agent-presence.d.ts +1 -0
  127. package/dist/types/server/ax-interaction.d.ts +1 -0
  128. package/dist/types/server/ax-state-manager.d.ts +10 -2
  129. package/dist/types/server/board-local.d.ts +4 -0
  130. package/dist/types/server/board-move.d.ts +38 -0
  131. package/dist/types/server/canvas-db.d.ts +12 -2
  132. package/dist/types/server/canvas-operations.d.ts +5 -6
  133. package/dist/types/server/canvas-provenance.d.ts +1 -1
  134. package/dist/types/server/canvas-serialization.d.ts +1 -0
  135. package/dist/types/server/canvas-state.d.ts +84 -20
  136. package/dist/types/server/card-relocation.d.ts +6 -0
  137. package/dist/types/server/context-reads.d.ts +3 -1
  138. package/dist/types/server/human-presence.d.ts +1 -1
  139. package/dist/types/server/index.d.ts +16 -5
  140. package/dist/types/server/intent-registry.d.ts +8 -4
  141. package/dist/types/server/knowledge-lifecycle.d.ts +4 -2
  142. package/dist/types/server/mutation-history.d.ts +7 -5
  143. package/dist/types/server/operations/registry.d.ts +2 -0
  144. package/dist/types/server/trace-manager.d.ts +1 -0
  145. package/dist/types/shared/ax-kinds.d.ts +6 -0
  146. package/dist/types/shared/ax-surface-protocol.d.ts +8 -0
  147. package/dist/types/shared/human-presence.d.ts +1 -0
  148. package/dist/types/shared/lifecycle-status.d.ts +2 -0
  149. package/docs/RELEASE.md +21 -14
  150. package/docs/agent-plugin.md +22 -2
  151. package/docs/amp-plugin.md +270 -0
  152. package/docs/cli.md +42 -3
  153. package/docs/design/GuestName.dc.html +50 -0
  154. package/docs/design/PluginLogo.svg +7 -0
  155. package/docs/design/WorkCards.dc.html +39 -0
  156. package/docs/design/canvas.json +16 -0
  157. package/docs/design/plugin-logo.png +0 -0
  158. package/docs/design.md +111 -8
  159. package/docs/evals/agents-md-sections.md +38 -0
  160. package/docs/http-api.md +128 -10
  161. package/docs/mcp.md +61 -9
  162. package/docs/node-types.md +71 -1
  163. package/docs/plans/plan-009-tech-debt-backlog.md +1 -1
  164. package/docs/plans/plan-019-post-0.9-roadmap.md +44 -20
  165. package/docs/plans/plan-020-wave-5-work-planning.md +553 -0
  166. package/docs/product-vision-2026-09.md +33 -17
  167. package/docs/sdk.md +18 -0
  168. package/package.json +6 -3
  169. package/plugins/pmx-canvas/assets/preview.png +0 -0
  170. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/extension.mjs +4 -1
  171. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.json +1 -1
  172. package/plugins/pmx-canvas/plugin.json +2 -2
  173. package/plugins/pmx-canvas/runtime.json +1 -1
  174. package/plugins/pmx-canvas/tools.json +440 -18
  175. package/skills/data-analysis/SKILL.md +13 -26
  176. package/skills/frontend-design/LICENSE.txt +177 -0
  177. package/skills/frontend-design/SKILL.md +3 -0
  178. package/skills/pmx-canvas/SKILL.md +248 -842
  179. package/skills/pmx-canvas/evals/evals.json +110 -5
  180. package/skills/pmx-canvas/references/full-reference.md +142 -28
  181. package/skills/pmx-canvas/references/html-primitives.md +7 -0
  182. package/skills/pmx-canvas/references/installing-pmx-canvas.md +61 -11
  183. package/skills/pmx-canvas/references/tours-and-recording.md +39 -6
  184. package/skills/pmx-canvas/references/workbench-workflows.md +908 -0
  185. package/skills/pmx-canvas-testing/SKILL.md +7 -5
  186. package/skills/published-consumer-e2e/SKILL.md +25 -8
  187. package/skills/published-consumer-e2e/scripts/run-published-consumer-e2e.sh +51 -75
  188. package/skills/web-artifacts-builder/LICENSE.txt +202 -0
  189. package/skills/web-artifacts-builder/SKILL.md +6 -0
  190. package/skills/web-artifacts-builder/scripts/bundle-artifact.sh +2 -2
  191. package/skills/web-artifacts-builder/scripts/init-artifact.sh +8 -8
  192. package/src/cli/commands/apps.ts +1 -0
  193. package/src/cli/commands/boards.ts +30 -0
  194. package/src/client/App.tsx +20 -4
  195. package/src/client/canvas/AttentionHistory.tsx +10 -2
  196. package/src/client/canvas/BoardMoveDialog.tsx +225 -0
  197. package/src/client/canvas/CanvasNode.tsx +45 -2
  198. package/src/client/canvas/CanvasViewport.tsx +38 -9
  199. package/src/client/canvas/ContextMenu.tsx +34 -12
  200. package/src/client/canvas/EdgeEditor.tsx +149 -0
  201. package/src/client/canvas/EdgeLayer.tsx +87 -16
  202. package/src/client/canvas/ExpandedNodeOverlay.tsx +4 -0
  203. package/src/client/canvas/GroupNavigation.tsx +85 -0
  204. package/src/client/canvas/RelationsPanel.tsx +9 -0
  205. package/src/client/canvas/SelectionBar.tsx +21 -0
  206. package/src/client/canvas/SessionPanel.tsx +35 -1
  207. package/src/client/canvas/TextPrompt.tsx +18 -1
  208. package/src/client/canvas/TopBar.tsx +30 -0
  209. package/src/client/canvas/canvas-area.ts +1 -1
  210. package/src/client/nodes/ExtAppFrame.tsx +26 -10
  211. package/src/client/nodes/MarkdownNode.tsx +6 -7
  212. package/src/client/nodes/PromptNode.tsx +32 -3
  213. package/src/client/nodes/StatusNode.tsx +12 -0
  214. package/src/client/nodes/StatusSummary.tsx +11 -0
  215. package/src/client/nodes/ViewerFrame.tsx +21 -10
  216. package/src/client/nodes/WorkCard.tsx +175 -0
  217. package/src/client/nodes/use-ax-surface-bridge.ts +18 -14
  218. package/src/client/state/attention-bridge.ts +5 -0
  219. package/src/client/state/attention-store.ts +1 -0
  220. package/src/client/state/canvas-store.ts +185 -15
  221. package/src/client/state/human-store.ts +22 -2
  222. package/src/client/state/intent-bridge.ts +19 -5
  223. package/src/client/state/relation-store.ts +127 -1
  224. package/src/client/theme/global.css +94 -1
  225. package/src/client/types.ts +1 -0
  226. package/src/mcp/server.ts +13 -3
  227. package/src/server/agent-presence.ts +43 -23
  228. package/src/server/ax-flow-loop.ts +6 -0
  229. package/src/server/ax-gate-ttl.ts +8 -1
  230. package/src/server/ax-interaction.ts +16 -10
  231. package/src/server/ax-state-manager.ts +17 -4
  232. package/src/server/ax-wait.ts +20 -6
  233. package/src/server/board-export.ts +19 -6
  234. package/src/server/board-local.ts +20 -0
  235. package/src/server/board-move.ts +297 -0
  236. package/src/server/canvas-db.ts +262 -160
  237. package/src/server/canvas-operations.ts +139 -109
  238. package/src/server/canvas-schema.ts +21 -0
  239. package/src/server/canvas-serialization.ts +2 -0
  240. package/src/server/canvas-state.ts +792 -159
  241. package/src/server/card-relocation.ts +53 -0
  242. package/src/server/code-graph.ts +4 -3
  243. package/src/server/context-reads.ts +83 -55
  244. package/src/server/demo-state.json +35 -15
  245. package/src/server/file-watcher.ts +30 -10
  246. package/src/server/human-presence.ts +18 -5
  247. package/src/server/index.ts +35 -15
  248. package/src/server/intent-registry.ts +43 -5
  249. package/src/server/knowledge-lifecycle.ts +32 -9
  250. package/src/server/lifecycle-status.ts +29 -1
  251. package/src/server/mcp-app-runtime.ts +26 -7
  252. package/src/server/mutation-history.ts +37 -8
  253. package/src/server/operations/composites.ts +6 -3
  254. package/src/server/operations/mcp-contracts.ts +15 -0
  255. package/src/server/operations/mcp.ts +36 -9
  256. package/src/server/operations/ops/annotation.ts +22 -1
  257. package/src/server/operations/ops/ax-await.ts +1 -0
  258. package/src/server/operations/ops/ax-timeline.ts +28 -2
  259. package/src/server/operations/ops/batch.ts +25 -11
  260. package/src/server/operations/ops/boards.ts +44 -3
  261. package/src/server/operations/ops/nodes.ts +70 -0
  262. package/src/server/operations/ops/query.ts +32 -3
  263. package/src/server/operations/ops/snapshots.ts +1 -0
  264. package/src/server/operations/registry.ts +49 -4
  265. package/src/server/server.ts +62 -16
  266. package/src/server/tour-control.ts +37 -12
  267. package/src/server/trace-manager.ts +4 -0
  268. package/src/server/web-artifacts/scripts/bundle-artifact.sh +2 -2
  269. package/src/server/web-artifacts/scripts/init-artifact.sh +8 -8
  270. package/src/shared/agent-context.ts +7 -1
  271. package/src/shared/auto-arrange.ts +5 -4
  272. package/src/shared/ax-kinds.ts +13 -0
  273. package/src/shared/ax-surface-protocol.ts +22 -0
  274. package/src/shared/human-presence.ts +1 -0
  275. package/src/shared/lifecycle-status.ts +2 -0
  276. package/dist/canvas/chunks/architecture-TIHT7OUA-9tffvpsv.js +0 -1
  277. package/dist/canvas/chunks/classDiagram-JCYQIIEL-nc6g5x1r.js +0 -1
  278. package/dist/canvas/chunks/classDiagram-v2-OCEON4UE-0qnbpwvh.js +0 -1
  279. package/dist/canvas/chunks/cynefin-VYW2F7L2-ys8p46ts.js +0 -1
  280. package/dist/canvas/chunks/eventmodeling-45OFAUF4-v9q6dn4c.js +0 -1
  281. package/dist/canvas/chunks/flowDiagram-UKHOOZJN-yrr5z8tc.js +0 -1
  282. package/dist/canvas/chunks/gitGraph-TEB2WS4Q-kdwb8m0a.js +0 -1
  283. package/dist/canvas/chunks/index-1r72hdzb.js +0 -301
  284. package/dist/canvas/chunks/index-g3pwnqeb.js +0 -2
  285. package/dist/canvas/chunks/index-rft2hcz5.js +0 -2
  286. package/dist/canvas/chunks/info-DKCQHKI2-wpzs6sz3.js +0 -1
  287. package/dist/canvas/chunks/ishikawaDiagram-WSZJBQD7-5hnq1a9p.js +0 -70
  288. package/dist/canvas/chunks/packet-7NZHBO7P-xn439f0s.js +0 -1
  289. package/dist/canvas/chunks/pie-RZYD4A2V-0kf16rwh.js +0 -1
  290. package/dist/canvas/chunks/radar-I7S5WNFK-0g2pz36t.js +0 -1
  291. package/dist/canvas/chunks/railroad-3IZDKUUU-fcaran6j.js +0 -1
  292. package/dist/canvas/chunks/railroad-abnf-AHOZXSZD-wgdya41q.js +0 -1
  293. package/dist/canvas/chunks/railroad-ebnf-EBAXGLYW-gy07a4ka.js +0 -1
  294. package/dist/canvas/chunks/railroad-peg-LSFZ7HO6-ewk6wtkh.js +0 -1
  295. package/dist/canvas/chunks/stateDiagram-v2-UEYNNEHI-k8sfp2e7.js +0 -1
  296. package/dist/canvas/chunks/swimlanesDiagram-ULZ7WXOC-nqysw67j.js +0 -8
  297. package/dist/canvas/chunks/treeView-QDETBFTQ-cesw6gp0.js +0 -1
  298. package/dist/canvas/chunks/treemap-6X3UGDF4-zdv4knp9.js +0 -1
  299. package/dist/canvas/chunks/vennDiagram-T6HMQDX7-x8dqn9yf.js +0 -34
  300. package/dist/canvas/chunks/wardley-OPB4EBWU-44pdddez.js +0 -1
@@ -1,4 +1,4 @@
1
- import { batch, computed, signal } from '@preact/signals';
1
+ import { batch, computed, effect, signal } from '@preact/signals';
2
2
  import {
3
3
  isExcalidrawNode,
4
4
  type CanvasAnnotation,
@@ -159,6 +159,7 @@ export function resetCanvasInteractionState(): void {
159
159
  }
160
160
  batch(() => {
161
161
  canvasInteractionEpoch.value += 1;
162
+ groupPath.value = [];
162
163
  contextPinsRevision++;
163
164
  contextPinsPendingWrites = 0;
164
165
  activeNodeId.value = null;
@@ -226,13 +227,14 @@ export function trackDragMembership(nodeId: string): void {
226
227
  const cy = node.position.y + node.size.height / 2;
227
228
  let candidate: CanvasNodeState | null = null;
228
229
  for (const other of nodes.value.values()) {
230
+ if (groupContents.value && !groupContents.value.has(other.id)) continue;
229
231
  if (other.type !== 'group' || other.id === parentId || other.collapsed) continue;
230
232
  if (contains(other, cx, cy)) candidate = other;
231
233
  }
232
234
  if (!dropSuppressedForDrag) {
233
235
  const next = candidate
234
236
  ? { nodeId, groupId: candidate.id, mode: 'add' as const }
235
- : parent && fullyOutside(node, parent)
237
+ : parent && parent.id !== groupPath.value.at(-1)?.id && fullyOutside(node, parent)
236
238
  ? { nodeId, groupId: parent.id, mode: 'remove' as const }
237
239
  : null;
238
240
  const current = dragDropTarget.value;
@@ -266,9 +268,14 @@ export const hiddenByCollapsedGroup = computed(() => {
266
268
  const hidden = new Map<string, string>();
267
269
  for (const node of nodes.value.values()) {
268
270
  if (node.type !== 'group' || !node.collapsed) continue;
269
- const children = Array.isArray(node.data.children) ? node.data.children : [];
270
- for (const childId of children) {
271
- if (typeof childId === 'string') hidden.set(childId, node.id);
271
+ if (groupContents.value && !groupContents.value.has(node.id)) continue;
272
+ const pending = new Set(Array.isArray(node.data.children) ? node.data.children : []);
273
+ for (const childId of pending) {
274
+ if (typeof childId !== 'string' || childId === node.id) continue;
275
+ hidden.set(childId, node.id);
276
+ const child = nodes.value.get(childId);
277
+ if (child?.type === 'group' && Array.isArray(child.data.children))
278
+ for (const descendant of child.data.children) pending.add(descendant);
272
279
  }
273
280
  }
274
281
  return hidden;
@@ -276,10 +283,131 @@ export const hiddenByCollapsedGroup = computed(() => {
276
283
 
277
284
  /** The node that stands in for `id` on the canvas: itself, or its collapsed group's chip. */
278
285
  export function visibleNodeFor(id: string): CanvasNodeState | undefined {
279
- const chipId = hiddenByCollapsedGroup.value.get(id);
280
- return nodes.value.get(chipId ?? id);
286
+ const visited = new Set<string>();
287
+ while (hiddenByCollapsedGroup.value.has(id) && !visited.has(id)) {
288
+ visited.add(id);
289
+ id = hiddenByCollapsedGroup.value.get(id)!;
290
+ }
291
+ return nodes.value.get(id);
292
+ }
293
+
294
+ interface GroupVisit {
295
+ id: string;
296
+ parent: unknown;
297
+ viewport: ViewportState;
298
+ selection: Set<string>;
299
+ active: string | null;
300
+ }
301
+
302
+ /** Human navigation only. The canonical node/edge maps and agent reads remain whole-board. */
303
+ export const groupPath = signal<GroupVisit[]>([]);
304
+ export const groupContents = computed(() => {
305
+ const id = groupPath.value.at(-1)?.id;
306
+ if (!id) return null;
307
+ const ids = new Set<string>();
308
+ const pending = [id];
309
+ const visited = new Set<string>();
310
+ for (const parent of pending) {
311
+ if (visited.has(parent)) continue;
312
+ visited.add(parent);
313
+ const node = nodes.value.get(parent);
314
+ if (node?.type !== 'group' || !Array.isArray(node.data.children)) continue;
315
+ for (const child of node.data.children) {
316
+ if (typeof child !== 'string' || child === id || !nodes.value.has(child)) continue;
317
+ ids.add(child);
318
+ pending.push(child);
319
+ }
320
+ }
321
+ return ids;
322
+ });
323
+
324
+ /** The drawing's outside endpoints are view-only stubs, never inserted into nodes. */
325
+ export const groupViewNodes = computed(() => {
326
+ const inside = groupContents.value;
327
+ if (!inside) return nodes.value;
328
+ const view = new Map([...nodes.value].filter(([id]) => inside.has(id)));
329
+ const outside = new Set<string>();
330
+ for (const edge of edges.value.values()) {
331
+ if (edge.toBoardId || inside.has(edge.from) === inside.has(edge.to)) continue;
332
+ outside.add(inside.has(edge.from) ? edge.to : edge.from);
333
+ }
334
+ const root = nodes.value.get(groupPath.value.at(-1)!.id);
335
+ const bounds = [...view.values()];
336
+ const left = bounds.length ? Math.min(...bounds.map((node) => node.position.x)) : (root?.position.x ?? 0);
337
+ const bottom = bounds.length
338
+ ? Math.max(...bounds.map((node) => node.position.y + node.size.height))
339
+ : (root?.position.y ?? 0) + 80;
340
+ [...outside].sort().forEach((id, index) => {
341
+ const node = nodes.value.get(id);
342
+ if (node)
343
+ view.set(id, {
344
+ ...node,
345
+ position: { x: left + (index % 3) * 194, y: bottom + 88 + Math.floor(index / 3) * 84 },
346
+ size: { width: 170, height: 60 },
347
+ collapsed: false,
348
+ });
349
+ });
350
+ return view;
351
+ });
352
+
353
+ export function leaveGroup(depth = groupPath.value.length - 1): void {
354
+ const visit = groupPath.value[depth];
355
+ if (!visit) return;
356
+ cancelViewportAnimation();
357
+ batch(() => {
358
+ groupPath.value = groupPath.value.slice(0, depth);
359
+ viewport.value = visit.viewport;
360
+ selectedNodeIds.value = filterNodeIdSet(visit.selection, nodes.value);
361
+ activeNodeId.value = visit.active && nodes.value.has(visit.active) ? visit.active : null;
362
+ selectedEdgeId.value = null;
363
+ });
364
+ void updateViewportFromClient(visit.viewport, { recordHistory: false });
365
+ }
366
+
367
+ export function openGroup(id: string, fit = true): void {
368
+ const node = nodes.value.get(id);
369
+ if (node?.type !== 'group' || groupPath.value.some((visit) => visit.id === id)) return;
370
+ if (groupContents.value && !groupContents.value.has(id)) leaveGroup(0);
371
+ cancelViewportAnimation();
372
+ batch(() => {
373
+ groupPath.value = [
374
+ ...groupPath.value,
375
+ {
376
+ id,
377
+ parent: node.data.parentGroup,
378
+ viewport: { ...viewport.value },
379
+ selection: new Set(selectedNodeIds.value),
380
+ active: activeNodeId.value,
381
+ },
382
+ ];
383
+ clearSelection();
384
+ activeNodeId.value = null;
385
+ });
386
+ // Wait for the breadcrumb/legend to paint before measuring their screen insets.
387
+ if (!fit) return;
388
+ afterGroupView(() => {
389
+ const area = canvasArea();
390
+ fitAll(area.width, area.height);
391
+ });
281
392
  }
282
393
 
394
+ effect(() => {
395
+ const path = groupPath.value;
396
+ const map = nodes.value;
397
+ const invalid = path.findIndex((visit) => {
398
+ const node = map.get(visit.id);
399
+ return node?.type !== 'group' || node.data.parentGroup !== visit.parent;
400
+ });
401
+ if (invalid >= 0) leaveGroup(invalid);
402
+ const inside = groupContents.value;
403
+ if (inside) {
404
+ const selected = selectedNodeIds.value;
405
+ if ([...selected].some((id) => !inside.has(id)))
406
+ selectedNodeIds.value = new Set([...selected].filter((id) => inside.has(id)));
407
+ if (activeNodeId.value && !inside.has(activeNodeId.value)) activeNodeId.value = null;
408
+ }
409
+ });
410
+
283
411
  /** Collapsed groups render as a chip; children keep their positions for restore. */
284
412
  export function groupsOfSelection(): string[] {
285
413
  const ids = new Set<string>();
@@ -821,6 +949,17 @@ export function cancelViewportAnimation(): void {
821
949
  }
822
950
  }
823
951
 
952
+ /** Share the camera slot so newer navigation and direct gestures cancel queued fits. */
953
+ function afterGroupView(action: () => void): void {
954
+ cancelViewportAnimation();
955
+ const epoch = canvasInteractionEpoch.value;
956
+ const path = groupPath.value;
957
+ animationId = requestAnimationFrame(() => {
958
+ animationId = null;
959
+ if (epoch === canvasInteractionEpoch.value && path === groupPath.value) action();
960
+ });
961
+ }
962
+
824
963
  // ── Persistence ───────────────────────────────────────────────
825
964
  const STORAGE_KEY = 'pmx-canvas-layout';
826
965
 
@@ -903,7 +1042,9 @@ export function restoreLayout(): Map<string, Partial<CanvasNodeState>> | null {
903
1042
 
904
1043
  // ── Fit all ───────────────────────────────────────────────────
905
1044
  export function fitAll(containerW: number, containerH: number): void {
906
- const all = Array.from(nodes.value.values());
1045
+ const all = Array.from(groupViewNodes.value.values());
1046
+ const group = nodes.value.get(groupPath.value.at(-1)?.id ?? '');
1047
+ if (!all.length && group) all.push(group);
907
1048
  if (all.length === 0) return;
908
1049
 
909
1050
  let minX = Number.POSITIVE_INFINITY;
@@ -916,6 +1057,12 @@ export function fitAll(containerW: number, containerH: number): void {
916
1057
  maxX = Math.max(maxX, n.position.x + n.size.width);
917
1058
  maxY = Math.max(maxY, n.position.y + n.size.height);
918
1059
  }
1060
+ if (groupPath.value.length) {
1061
+ minX -= 24;
1062
+ minY -= 40;
1063
+ maxX += 24;
1064
+ maxY += 24;
1065
+ }
919
1066
 
920
1067
  // Screen-space fit (architecture rule 9): the margins that must clear the
921
1068
  // floating chrome are SCREEN constants. The old world-space PAD shrank with
@@ -930,17 +1077,41 @@ export function fitAll(containerW: number, containerH: number): void {
930
1077
  const cx = (minX + maxX) / 2;
931
1078
  const cy = (minY + maxY) / 2;
932
1079
 
933
- animateViewport({
934
- x: insets.left + usableW / 2 - cx * scale,
935
- y: insets.top + usableH / 2 - cy * scale,
936
- scale,
937
- });
1080
+ animateViewport(
1081
+ {
1082
+ x: insets.left + usableW / 2 - cx * scale,
1083
+ y: insets.top + usableH / 2 - cy * scale,
1084
+ scale,
1085
+ },
1086
+ 300,
1087
+ { recordHistory: groupPath.value.length === 0 },
1088
+ );
938
1089
  }
939
1090
 
940
1091
  // ── Focus node ────────────────────────────────────────────────
941
1092
  export function focusNode(id: string, options: { recordHistory?: boolean; persistStacking?: boolean } = {}): void {
942
1093
  const node = nodes.value.get(id);
943
1094
  if (!node) return;
1095
+ cancelViewportAnimation();
1096
+ const previousPath = groupPath.value;
1097
+ if (groupContents.value && !groupContents.value.has(id)) leaveGroup(0);
1098
+ // Reveal collapsed targets through view state, without changing authored collapse flags.
1099
+ const ancestors: string[] = [];
1100
+ let parent = node.data.parentGroup;
1101
+ while (typeof parent === 'string' && !ancestors.includes(parent)) {
1102
+ ancestors.push(parent);
1103
+ parent = nodes.value.get(parent)?.data.parentGroup;
1104
+ }
1105
+ for (const ancestor of ancestors.reverse()) {
1106
+ if (nodes.value.get(ancestor)?.collapsed && !groupPath.value.some((visit) => visit.id === ancestor))
1107
+ openGroup(ancestor, false);
1108
+ }
1109
+ bringToFront(id, options.persistStacking !== false);
1110
+ if (groupPath.value.length || previousPath !== groupPath.value) afterGroupView(() => focusNodeCamera(node, options));
1111
+ else focusNodeCamera(node, options);
1112
+ }
1113
+
1114
+ function focusNodeCamera(node: CanvasNodeState, options: { recordHistory?: boolean }): void {
944
1115
  const v = viewport.value;
945
1116
  // Same screen-space frame as fitAll: viewport x/y are CONTAINER-relative
946
1117
  // (screen = world*scale + viewport inside the canvas region), so the centre
@@ -966,12 +1137,11 @@ export function focusNode(id: string, options: { recordHistory?: boolean; persis
966
1137
  300,
967
1138
  options,
968
1139
  );
969
- bringToFront(id, options.persistStacking !== false);
970
1140
  }
971
1141
 
972
1142
  // ── Cycle focus ───────────────────────────────────────────────
973
1143
  export function cycleActiveNode(direction: 1 | -1 = 1): void {
974
- const all = Array.from(nodes.value.keys());
1144
+ const all = Array.from(nodes.value.keys()).filter((id) => !groupContents.value || groupContents.value.has(id));
975
1145
  if (all.length === 0) return;
976
1146
  const currentIdx = activeNodeId.value ? all.indexOf(activeNodeId.value) : -1;
977
1147
  const nextIdx = (currentIdx + direction + all.length) % all.length;
@@ -1,6 +1,7 @@
1
1
  import { computed, signal } from '@preact/signals';
2
2
  import {
3
3
  HUMAN_CURSOR_THROTTLE_MS,
4
+ HUMAN_NAME_MAX_LENGTH,
4
5
  type HumanPresence,
5
6
  type HumanPresenceSnapshot,
6
7
  } from '../../shared/human-presence.js';
@@ -34,12 +35,12 @@ export const humanClientId = sessionScoped(
34
35
 
35
36
  function resolveName(): string {
36
37
  try {
37
- const fromQuery = new URLSearchParams(window.location.search).get('name')?.trim();
38
+ const fromQuery = new URLSearchParams(window.location.search).get('name')?.trim().slice(0, HUMAN_NAME_MAX_LENGTH);
38
39
  if (fromQuery) {
39
40
  window.localStorage.setItem('pmx-canvas-human-name', fromQuery);
40
41
  return fromQuery;
41
42
  }
42
- const stored = window.localStorage.getItem('pmx-canvas-human-name')?.trim();
43
+ const stored = window.localStorage.getItem('pmx-canvas-human-name')?.trim().slice(0, HUMAN_NAME_MAX_LENGTH);
43
44
  if (stored) return stored;
44
45
  } catch {
45
46
  // storage unavailable: fall through to a generated name
@@ -51,6 +52,25 @@ export const humanName = signal<string>(resolveName());
51
52
  export const humans = signal<HumanPresence[]>([]);
52
53
  export const otherHumans = computed(() => humans.value.filter((human) => human.clientId !== humanClientId));
53
54
 
55
+ /** Change the display label, never the per-tab identity or anyone's access. */
56
+ export function setHumanName(name: string): void {
57
+ const value = name.trim().slice(0, HUMAN_NAME_MAX_LENGTH);
58
+ if (!value) return;
59
+ humanName.value = value;
60
+ try {
61
+ window.localStorage.setItem('pmx-canvas-human-name', value);
62
+ } catch {
63
+ // Storage can be blocked in embedded panes; the current tab still has its name.
64
+ }
65
+ // A link's initial name must not undo an explicit rename on reload.
66
+ const url = new URL(window.location.href);
67
+ if (url.searchParams.has('name')) {
68
+ url.searchParams.delete('name');
69
+ window.history.replaceState(window.history.state, '', url);
70
+ }
71
+ post({ cursor: lastCursor, grabbingNodeId: grabbingNodeId.value });
72
+ }
73
+
54
74
  /** Nodes a human took over mid-edit → that human's name, shown as the yield pill for a moment. */
55
75
  export const yieldedNodes = signal<Map<string, string>>(new Map());
56
76
 
@@ -300,11 +300,13 @@ export async function createNodeFromClient(opts: {
300
300
  type?: string;
301
301
  title?: string;
302
302
  content?: string;
303
+ data?: Record<string, unknown>;
303
304
  x?: number;
304
305
  y?: number;
305
306
  width?: number;
306
307
  height?: number;
307
308
  boardId?: string;
309
+ expectedBoardId?: string;
308
310
  }): Promise<{ ok: boolean; id?: string }> {
309
311
  return requestJson(
310
312
  'createNodeFromClient',
@@ -490,7 +492,7 @@ export async function reportClientViewportSize(): Promise<void> {
490
492
  if (isHostedWorkbench() && !boardsLoaded.value) return;
491
493
  const size = clientWindowSize();
492
494
  if (!('clientWidth' in size)) return;
493
- await requestBestEffort('reportClientViewportSize', '/api/canvas/viewport', {
495
+ const init: RequestInit = {
494
496
  method: 'POST',
495
497
  headers: { 'Content-Type': 'application/json' },
496
498
  body: JSON.stringify({
@@ -498,7 +500,15 @@ export async function reportClientViewportSize(): Promise<void> {
498
500
  ...(boardsLoaded.value ? { expectedBoardId: activeBoardId.value } : {}),
499
501
  recordHistory: false,
500
502
  }),
501
- });
503
+ };
504
+ try {
505
+ const res = await workbenchFetch('/api/canvas/viewport', withWorkbenchMarker(init));
506
+ // Resize observations can cross a board switch. Discard that stale report,
507
+ // without telling the human an edit failed; actual camera writes still report refusals.
508
+ if (!res.ok && res.status !== 409) reportRefusedWrite(res, init, await readJsonBody(res));
509
+ } catch (error) {
510
+ logRequestError('reportClientViewportSize', error);
511
+ }
502
512
  }
503
513
 
504
514
  // ── Group API ─────────────────────────────────────────────────
@@ -612,6 +622,7 @@ export async function updateEdgeFromClient(
612
622
  style?: string;
613
623
  animated?: boolean;
614
624
  },
625
+ expectedBoardId = activeBoardId.value,
615
626
  ): Promise<{ ok: boolean }> {
616
627
  return requestJson(
617
628
  'updateEdgeFromClient',
@@ -620,13 +631,16 @@ export async function updateEdgeFromClient(
620
631
  {
621
632
  method: 'PATCH',
622
633
  headers: { 'Content-Type': 'application/json' },
623
- body: JSON.stringify(patch),
634
+ body: JSON.stringify({ ...patch, expectedBoardId }),
624
635
  },
625
636
  );
626
637
  }
627
638
 
628
639
  /** Remove a canvas edge via the server. */
629
- export async function removeEdgeFromClient(edgeId: string): Promise<{ ok: boolean }> {
640
+ export async function removeEdgeFromClient(
641
+ edgeId: string,
642
+ expectedBoardId = activeBoardId.value,
643
+ ): Promise<{ ok: boolean }> {
630
644
  return requestJson(
631
645
  'removeEdgeFromClient',
632
646
  '/api/canvas/edge',
@@ -634,7 +648,7 @@ export async function removeEdgeFromClient(edgeId: string): Promise<{ ok: boolea
634
648
  {
635
649
  method: 'DELETE',
636
650
  headers: { 'Content-Type': 'application/json' },
637
- body: JSON.stringify({ edge_id: edgeId }),
651
+ body: JSON.stringify({ edge_id: edgeId, expectedBoardId }),
638
652
  },
639
653
  );
640
654
  }
@@ -1,9 +1,123 @@
1
1
  import { computed, effect, signal } from '@preact/signals';
2
2
  import { relationKind, relationPreset, relationSourceEnd } from '../../shared/relations.js';
3
+ import { memoryKey } from '../../shared/memory-graph.js';
3
4
  import { activeBoard, activeBoardId } from './boards-store';
4
- import { activeNodeId, edges, nodes, selectedNodeIds } from './canvas-store';
5
+ import {
6
+ activeNodeId,
7
+ edges,
8
+ nodes,
9
+ selectedNodeIds,
10
+ focusNode,
11
+ collapseExpandedNode,
12
+ canvasInteractionEpoch,
13
+ } from './canvas-store';
14
+ import { cardQualification } from './knowledge-store';
15
+ import type { CanvasEdge } from '../types';
5
16
 
6
17
  export const relationFilter = signal<{ boardId: string; kind: string } | null>(null);
18
+ const inspection = signal<{ boardId: string; nodeId: string; mode: 'lineage' | 'connections'; epoch: number } | null>(
19
+ null,
20
+ );
21
+
22
+ export function showRelationsOnCanvas(nodeId: string, mode: 'lineage' | 'connections'): void {
23
+ const boardId = activeBoardId.value;
24
+ if (!boardId) return;
25
+ collapseExpandedNode();
26
+ selectedNodeIds.value = new Set();
27
+ relationFilter.value = null;
28
+ focusNode(nodeId, { recordHistory: false, persistStacking: false });
29
+ activeNodeId.value = nodeId;
30
+ inspection.value = { boardId, nodeId, mode, epoch: canvasInteractionEpoch.value };
31
+ }
32
+
33
+ effect(() => {
34
+ const value = inspection.value;
35
+ if (
36
+ value &&
37
+ (value.boardId !== activeBoardId.value ||
38
+ value.nodeId !== activeNodeId.value ||
39
+ value.epoch !== canvasInteractionEpoch.value ||
40
+ relationFilter.value)
41
+ )
42
+ inspection.value = null;
43
+ });
44
+
45
+ /** Only qualified dependency/lineage sources warn; citations and illustrated loops never do. */
46
+ export function relationWarning(edge: CanvasEdge): string[] {
47
+ const boardId = activeBoardId.value;
48
+ const kind = relationKind(edge);
49
+ if (!boardId || !['depends-on', 'derived-from'].includes(kind)) return [];
50
+ const from = { boardId, cardId: edge.from };
51
+ const to = { boardId: edge.toBoardId ?? boardId, cardId: edge.to };
52
+ const source = relationSourceEnd(edge) === 'from' ? from : to;
53
+ const dependent = relationSourceEnd(edge) === 'from' ? to : from;
54
+ if (dependent.boardId !== boardId) return [];
55
+ return (
56
+ cardQualification(dependent.cardId)?.sourceWarnings?.[
57
+ JSON.stringify([boardId, edge.id, kind, memoryKey(source)])
58
+ ] ?? []
59
+ );
60
+ }
61
+
62
+ export const dependencyAnalysis = computed(() => {
63
+ const links = [...edges.value.values()].filter((edge) => !edge.toBoardId && relationKind(edge) === 'depends-on');
64
+ const forward = new Map<string, Set<string>>();
65
+ const reverse = new Map<string, Set<string>>();
66
+ for (const edge of links) {
67
+ if (!forward.has(edge.from)) forward.set(edge.from, new Set());
68
+ if (!forward.has(edge.to)) forward.set(edge.to, new Set());
69
+ if (!reverse.has(edge.from)) reverse.set(edge.from, new Set());
70
+ if (!reverse.has(edge.to)) reverse.set(edge.to, new Set());
71
+ forward.get(edge.from)!.add(edge.to);
72
+ reverse.get(edge.to)!.add(edge.from);
73
+ }
74
+ // Iterative strongly connected components: one count per cyclic dependency cluster.
75
+ const visited = new Set<string>();
76
+ const order: string[] = [];
77
+ for (const root of forward.keys()) {
78
+ const stack: Array<[string, boolean]> = [[root, false]];
79
+ while (stack.length) {
80
+ const [id, done] = stack.pop()!;
81
+ if (done) {
82
+ order.push(id);
83
+ continue;
84
+ }
85
+ if (visited.has(id)) continue;
86
+ visited.add(id);
87
+ stack.push([id, true]);
88
+ for (const next of forward.get(id) ?? []) if (!visited.has(next)) stack.push([next, false]);
89
+ }
90
+ }
91
+ const assigned = new Set<string>();
92
+ const cyclic = new Map<string, number>();
93
+ let cycles = 0;
94
+ for (const root of order.reverse()) {
95
+ if (assigned.has(root)) continue;
96
+ const component = new Set<string>([root]);
97
+ assigned.add(root);
98
+ for (const id of component)
99
+ for (const next of reverse.get(id) ?? []) {
100
+ if (assigned.has(next)) continue;
101
+ assigned.add(next);
102
+ component.add(next);
103
+ }
104
+ if (component.size > 1 || forward.get(root)?.has(root)) {
105
+ cycles++;
106
+ for (const id of component) cyclic.set(id, cycles);
107
+ }
108
+ }
109
+ const cycleEdges = new Set(
110
+ links
111
+ .filter((edge) => cyclic.has(edge.from) && cyclic.get(edge.from) === cyclic.get(edge.to))
112
+ .map((edge) => edge.id),
113
+ );
114
+ const roots = new Set(selectedNodeIds.value);
115
+ if (activeNodeId.value) roots.add(activeNodeId.value);
116
+ const affected = new Set(roots);
117
+ for (const id of affected) for (const next of reverse.get(id) ?? []) affected.add(next);
118
+ for (const id of roots) affected.delete(id);
119
+ return { cycles, cycleEdges, affected, focused: roots.size > 0, hasDependencies: links.length > 0 };
120
+ });
7
121
 
8
122
  effect(() => {
9
123
  const filter = relationFilter.value;
@@ -40,6 +154,18 @@ export const relationHighlight = computed(() => {
40
154
  cards.add(edge.from);
41
155
  cards.add(edge.to);
42
156
  };
157
+ const view = inspection.value;
158
+ if (view && view.boardId === activeBoardId.value && view.nodeId === focus && !kind) {
159
+ if (view.mode === 'connections') {
160
+ for (const edge of list) if (edge.from === focus || edge.to === focus) add(edge);
161
+ } else {
162
+ for (const id of cards)
163
+ for (const edge of list)
164
+ if (['derived-from', 'depends-on'].includes(relationKind(edge)) && (edge.from === id || edge.to === id))
165
+ add(edge);
166
+ }
167
+ return { cards, links };
168
+ }
43
169
  if (!kind) for (const edge of list) if (focusedCards.has(edge.from) && focusedCards.has(edge.to)) add(edge);
44
170
  if (kind) {
45
171
  for (const edge of list) if (relationKind(edge) === kind) add(edge);