pmx-canvas 0.8.0 → 0.9.0

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 (341) hide show
  1. package/.github/extensions/pmx-canvas/runtime.json +1 -1
  2. package/.github/extensions/pmx-canvas/tools.json +601 -22
  3. package/CHANGELOG.md +66 -0
  4. package/Readme.md +131 -532
  5. package/dist/canvas/global.css +2759 -510
  6. package/dist/canvas/index.js +168 -107
  7. package/dist/canvas/mcp-app.html +2936 -632
  8. package/dist/json-render/index.css +81 -1
  9. package/dist/json-render/index.js +20 -20
  10. package/dist/types/client/canvas/BarHint.d.ts +12 -2
  11. package/dist/types/client/canvas/BoardLinkDialog.d.ts +3 -1
  12. package/dist/types/client/canvas/BoardPinButton.d.ts +10 -0
  13. package/dist/types/client/canvas/BoardSwitcher.d.ts +1 -1
  14. package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
  15. package/dist/types/client/canvas/CommandBar.d.ts +3 -4
  16. package/dist/types/client/canvas/ConnectionBanner.d.ts +2 -1
  17. package/dist/types/client/canvas/ContextChip.d.ts +3 -0
  18. package/dist/types/client/canvas/CrossBoardReferences.d.ts +5 -0
  19. package/dist/types/client/canvas/EdgeLayer.d.ts +14 -3
  20. package/dist/types/client/canvas/EmptyState.d.ts +4 -4
  21. package/dist/types/client/canvas/HomeGraphView.d.ts +2 -0
  22. package/dist/types/client/canvas/HomeMapView.d.ts +1 -0
  23. package/dist/types/client/canvas/HomeReviewView.d.ts +6 -0
  24. package/dist/types/client/canvas/HomeView.d.ts +9 -7
  25. package/dist/types/client/canvas/KnowledgeDialog.d.ts +4 -0
  26. package/dist/types/client/canvas/LifecycleChrome.d.ts +5 -0
  27. package/dist/types/client/canvas/LifecycleFields.d.ts +7 -0
  28. package/dist/types/client/canvas/LifecyclePanel.d.ts +1 -0
  29. package/dist/types/client/canvas/MemoryInspector.d.ts +7 -0
  30. package/dist/types/client/canvas/NearPinLayer.d.ts +14 -0
  31. package/dist/types/client/canvas/NodeContextMark.d.ts +40 -7
  32. package/dist/types/client/canvas/RelationPresetsDialog.d.ts +4 -0
  33. package/dist/types/client/canvas/RelationsPanel.d.ts +4 -0
  34. package/dist/types/client/canvas/SessionPanel.d.ts +2 -0
  35. package/dist/types/client/canvas/canvas-area.d.ts +1 -1
  36. package/dist/types/client/canvas/graph-geometry.d.ts +27 -0
  37. package/dist/types/client/canvas/graph-model.d.ts +37 -0
  38. package/dist/types/client/canvas/memory-layout.d.ts +23 -0
  39. package/dist/types/client/canvas/use-chrome-height.d.ts +2 -0
  40. package/dist/types/client/canvas/use-focus-trap.d.ts +1 -0
  41. package/dist/types/client/canvas/use-memory-graph.d.ts +8 -0
  42. package/dist/types/client/canvas/use-pan-zoom.d.ts +4 -1
  43. package/dist/types/client/icons.d.ts +0 -10
  44. package/dist/types/client/nodes/AxStepControls.d.ts +1 -1
  45. package/dist/types/client/nodes/HtmlNode.d.ts +1 -0
  46. package/dist/types/client/nodes/InlineMarkdownEditor.d.ts +4 -4
  47. package/dist/types/client/nodes/MarkdownNode.d.ts +5 -1
  48. package/dist/types/client/nodes/McpAppNode.d.ts +4 -4
  49. package/dist/types/client/nodes/viewer-runtime-state.d.ts +1 -1
  50. package/dist/types/client/state/boards-store.d.ts +81 -9
  51. package/dist/types/client/state/canvas-store.d.ts +14 -1
  52. package/dist/types/client/state/context-chip-store.d.ts +22 -0
  53. package/dist/types/client/state/context-status-store.d.ts +35 -0
  54. package/dist/types/client/state/intent-bridge.d.ts +11 -8
  55. package/dist/types/client/state/knowledge-store.d.ts +22 -0
  56. package/dist/types/client/state/near-pin-store.d.ts +44 -1
  57. package/dist/types/client/state/presence-store.d.ts +4 -4
  58. package/dist/types/client/state/presentation.d.ts +28 -0
  59. package/dist/types/client/state/relation-store.d.ts +10 -0
  60. package/dist/types/client/state/session-store.d.ts +41 -2
  61. package/dist/types/client/state/workspace-settings.d.ts +7 -0
  62. package/dist/types/client/types.d.ts +13 -1
  63. package/dist/types/client/utils/keyboard.d.ts +2 -0
  64. package/dist/types/client/utils/time-ago.d.ts +4 -0
  65. package/dist/types/mcp/context-uri-template.d.ts +3 -1
  66. package/dist/types/server/agent-presence.d.ts +10 -3
  67. package/dist/types/server/ax-context.d.ts +1 -1
  68. package/dist/types/server/canvas-db.d.ts +30 -7
  69. package/dist/types/server/canvas-operations.d.ts +24 -2
  70. package/dist/types/server/canvas-provenance.d.ts +22 -2
  71. package/dist/types/server/canvas-serialization.d.ts +2 -0
  72. package/dist/types/server/canvas-state.d.ts +93 -15
  73. package/dist/types/server/canvas-validation.d.ts +1 -1
  74. package/dist/types/server/context-brief.d.ts +94 -6
  75. package/dist/types/server/context-reads.d.ts +33 -7
  76. package/dist/types/server/edit-summary.d.ts +20 -0
  77. package/dist/types/server/index.d.ts +71 -10
  78. package/dist/types/server/knowledge-lifecycle.d.ts +221 -0
  79. package/dist/types/server/lifecycle-status.d.ts +10 -0
  80. package/dist/types/server/memory-graph.d.ts +9 -0
  81. package/dist/types/server/mutation-history.d.ts +2 -0
  82. package/dist/types/server/operations/ops/ax-shared.d.ts +1 -1
  83. package/dist/types/server/operations/ops/boards.d.ts +2 -1
  84. package/dist/types/server/operations/ops/lifecycle.d.ts +2 -0
  85. package/dist/types/server/operations/ops/memory-graph.d.ts +2 -0
  86. package/dist/types/server/operations/ops/nodes.d.ts +5 -0
  87. package/dist/types/server/operations/ops/query.d.ts +23 -0
  88. package/dist/types/server/operations/registry.d.ts +1 -0
  89. package/dist/types/server/placement.d.ts +7 -0
  90. package/dist/types/server/scope-fence.d.ts +2 -1
  91. package/dist/types/server/spatial-analysis.d.ts +6 -3
  92. package/dist/types/server/tour-control.d.ts +32 -0
  93. package/dist/types/server/wiki-links.d.ts +14 -0
  94. package/dist/types/server/workbench-auth.d.ts +2 -0
  95. package/dist/types/shared/agent-context.d.ts +2 -0
  96. package/dist/types/shared/agent-presence.d.ts +15 -12
  97. package/dist/types/shared/auto-arrange.d.ts +1 -0
  98. package/dist/types/shared/boards.d.ts +4 -4
  99. package/dist/types/shared/knowledge-lifecycle.d.ts +41 -0
  100. package/dist/types/shared/knowledge-presets.d.ts +6 -0
  101. package/dist/types/shared/lifecycle-status.d.ts +24 -0
  102. package/dist/types/shared/memory-graph.d.ts +53 -0
  103. package/dist/types/shared/relations.d.ts +68 -0
  104. package/dist/types/shared/semantic-attention.d.ts +2 -0
  105. package/dist/types/shared/tour.d.ts +38 -0
  106. package/dist/types/shared/workspace-settings.d.ts +11 -0
  107. package/docs/RELEASE.md +21 -3
  108. package/docs/adr-001-bun-only-runtime.md +7 -1
  109. package/docs/adr-002-shared-product-separate-runtimes.md +93 -0
  110. package/docs/api-reference.md +109 -0
  111. package/docs/ax-host-adapter-contract.md +1 -1
  112. package/docs/ax-state-contract.md +5 -4
  113. package/docs/cli.md +153 -15
  114. package/docs/demos/pmx-canvas-demo.mp4 +0 -0
  115. package/docs/design/AgentContext.dc.html +3 -3
  116. package/docs/design/BoardPins.dc.html +37 -22
  117. package/docs/design/Categories.dc.html +208 -0
  118. package/docs/design/CategoryEditor.dc.html +33 -0
  119. package/docs/design/Chrome.dc.html +55 -9
  120. package/docs/design/ContextChip.dc.html +224 -0
  121. package/docs/design/ContextChip600.dc.html +136 -0
  122. package/docs/design/EmptyState.dc.html +45 -0
  123. package/docs/design/Flows.dc.html +23 -1
  124. package/docs/design/HeaderMarks.dc.html +183 -0
  125. package/docs/design/Home.dc.html +19 -1
  126. package/docs/design/HomeGraph.dc.html +86 -202
  127. package/docs/design/HomeMap.dc.html +23 -1
  128. package/docs/design/HomeTeam.dc.html +32 -14
  129. package/docs/design/Lifecycle.dc.html +22 -0
  130. package/docs/design/Lifecycle600.dc.html +18 -0
  131. package/docs/design/LifecycleVariants.dc.html +157 -0
  132. package/docs/design/LinkAuthorship.dc.html +139 -0
  133. package/docs/design/LinksChanged.dc.html +203 -0
  134. package/docs/design/LinksOptions.dc.html +137 -0
  135. package/docs/design/LinksOptions600.dc.html +134 -0
  136. package/docs/design/NearPin.dc.html +9 -10
  137. package/docs/design/Pane600.dc.html +4 -3
  138. package/docs/design/Relations.dc.html +3 -2
  139. package/docs/design/Restore.dc.html +152 -0
  140. package/docs/design/SeeChange.dc.html +267 -0
  141. package/docs/design/Uses.dc.html +12 -6
  142. package/docs/design/canvas.json +146 -19
  143. package/docs/design.md +454 -18
  144. package/docs/environment.md +0 -1
  145. package/docs/evals/curation-effect.md +128 -0
  146. package/docs/http-api.md +465 -38
  147. package/docs/mcp.md +150 -19
  148. package/docs/node-types.md +58 -3
  149. package/docs/plans/design-gaps-2026-10-08.md +53 -0
  150. package/docs/plans/plan-009-tech-debt-backlog.md +379 -0
  151. package/docs/plans/plan-016-context-made-visible-wave-1.md +150 -5
  152. package/docs/plans/plan-017-knowledge-categories-and-lifecycle.md +175 -0
  153. package/docs/plans/plan-018-connected-memory-wave-2.md +594 -0
  154. package/docs/plans/plan-019-post-0.9-roadmap.md +198 -0
  155. package/docs/product-context-vision-2026-09.md +17 -1
  156. package/docs/product-vision-2026-09.md +1082 -43
  157. package/docs/product-vision-review-2026-09.md +13 -1
  158. package/docs/screenshots/workbench-live-dark.png +0 -0
  159. package/docs/screenshots/workbench-live-light.png +0 -0
  160. package/docs/sdk.md +62 -2
  161. package/package.json +1 -1
  162. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.json +1 -1
  163. package/plugins/pmx-canvas/plugin.json +1 -1
  164. package/plugins/pmx-canvas/runtime.json +1 -1
  165. package/plugins/pmx-canvas/tools.json +601 -22
  166. package/skills/pmx-canvas/SKILL.md +234 -43
  167. package/skills/pmx-canvas/evals/evals.json +62 -0
  168. package/skills/pmx-canvas/references/ax-html-control-surface.md +6 -0
  169. package/skills/pmx-canvas/references/full-reference.md +120 -21
  170. package/skills/pmx-canvas/references/github-copilot-app-adapter.md +2 -2
  171. package/skills/pmx-canvas/references/importing-context.md +132 -0
  172. package/skills/pmx-canvas/references/installing-pmx-canvas.md +6 -6
  173. package/skills/pmx-canvas/references/tours-and-recording.md +16 -1
  174. package/skills/pmx-canvas-testing/SKILL.md +20 -0
  175. package/src/cli/agent.ts +2 -0
  176. package/src/cli/commands/ax.ts +12 -0
  177. package/src/cli/commands/boards.ts +87 -11
  178. package/src/cli/commands/edges.ts +28 -0
  179. package/src/cli/commands/groups.ts +7 -0
  180. package/src/cli/commands/nodes.ts +4 -2
  181. package/src/cli/commands/query.ts +34 -2
  182. package/src/cli/commands/record.ts +199 -48
  183. package/src/cli/index.ts +7 -1
  184. package/src/client/App.tsx +177 -70
  185. package/src/client/canvas/AttentionHistory.tsx +0 -2
  186. package/src/client/canvas/BarHint.tsx +40 -2
  187. package/src/client/canvas/BoardCopyDialog.tsx +1 -1
  188. package/src/client/canvas/BoardFolderDialog.tsx +7 -7
  189. package/src/client/canvas/BoardLinkDialog.tsx +138 -33
  190. package/src/client/canvas/BoardPinButton.tsx +33 -0
  191. package/src/client/canvas/BoardSwitcher.tsx +26 -40
  192. package/src/client/canvas/CanvasNode.tsx +360 -237
  193. package/src/client/canvas/CanvasViewport.tsx +45 -7
  194. package/src/client/canvas/CommandBar.tsx +29 -23
  195. package/src/client/canvas/CommandPalette.tsx +5 -12
  196. package/src/client/canvas/ConnectionBanner.tsx +3 -3
  197. package/src/client/canvas/ContextChip.tsx +296 -0
  198. package/src/client/canvas/ContextMenu.tsx +63 -5
  199. package/src/client/canvas/CrossBoardReferences.tsx +60 -0
  200. package/src/client/canvas/EdgeLayer.tsx +339 -76
  201. package/src/client/canvas/EmptyState.tsx +33 -18
  202. package/src/client/canvas/ExpandedNodeOverlay.tsx +72 -40
  203. package/src/client/canvas/ExternalWriters.tsx +57 -42
  204. package/src/client/canvas/HomeGraphView.tsx +942 -0
  205. package/src/client/canvas/HomeMapView.tsx +641 -0
  206. package/src/client/canvas/HomeReviewView.tsx +167 -0
  207. package/src/client/canvas/HomeView.tsx +687 -160
  208. package/src/client/canvas/IntentLayer.tsx +19 -12
  209. package/src/client/canvas/KnowledgeDialog.tsx +488 -0
  210. package/src/client/canvas/LifecycleChrome.tsx +133 -0
  211. package/src/client/canvas/LifecycleFields.tsx +91 -0
  212. package/src/client/canvas/LifecyclePanel.tsx +533 -0
  213. package/src/client/canvas/MemoryInspector.tsx +104 -0
  214. package/src/client/canvas/Minimap.tsx +3 -3
  215. package/src/client/canvas/NearPinLayer.tsx +187 -0
  216. package/src/client/canvas/NodeContextMark.tsx +175 -97
  217. package/src/client/canvas/Presentation.tsx +23 -57
  218. package/src/client/canvas/RelationPresetsDialog.tsx +413 -0
  219. package/src/client/canvas/RelationsPanel.tsx +82 -0
  220. package/src/client/canvas/SelectionBar.tsx +3 -6
  221. package/src/client/canvas/SessionPanel.tsx +90 -50
  222. package/src/client/canvas/SessionReceipt.tsx +311 -39
  223. package/src/client/canvas/ShortcutOverlay.tsx +1 -1
  224. package/src/client/canvas/SnapshotPanel.tsx +27 -36
  225. package/src/client/canvas/ToolRail.tsx +367 -192
  226. package/src/client/canvas/TopBar.tsx +354 -124
  227. package/src/client/canvas/canvas-area.ts +10 -3
  228. package/src/client/canvas/graph-geometry.ts +195 -0
  229. package/src/client/canvas/graph-model.ts +254 -0
  230. package/src/client/canvas/memory-layout.ts +106 -0
  231. package/src/client/canvas/use-chrome-height.ts +20 -0
  232. package/src/client/canvas/use-focus-trap.ts +31 -5
  233. package/src/client/canvas/use-memory-graph.ts +58 -0
  234. package/src/client/canvas/use-node-drag.ts +2 -1
  235. package/src/client/canvas/use-pan-zoom.ts +64 -7
  236. package/src/client/icons.tsx +0 -63
  237. package/src/client/mcp-app/style.css +0 -6
  238. package/src/client/nodes/AxStepControls.tsx +74 -50
  239. package/src/client/nodes/BoardNode.tsx +5 -1
  240. package/src/client/nodes/ContextNode.tsx +18 -11
  241. package/src/client/nodes/ExtAppFrame.tsx +0 -1
  242. package/src/client/nodes/FileNode.tsx +46 -42
  243. package/src/client/nodes/HtmlNode.tsx +44 -2
  244. package/src/client/nodes/ImageNode.tsx +1 -1
  245. package/src/client/nodes/InlineFormatBar.tsx +6 -9
  246. package/src/client/nodes/InlineMarkdownEditor.tsx +56 -17
  247. package/src/client/nodes/MarkdownNode.tsx +377 -109
  248. package/src/client/nodes/McpAppNode.tsx +50 -20
  249. package/src/client/nodes/MdFormatBar.tsx +21 -18
  250. package/src/client/nodes/PromptNode.tsx +1 -1
  251. package/src/client/nodes/ResponseNode.tsx +1 -1
  252. package/src/client/nodes/viewer-runtime-state.ts +54 -22
  253. package/src/client/state/attention-bridge.ts +64 -15
  254. package/src/client/state/boards-store.ts +193 -39
  255. package/src/client/state/canvas-store.ts +99 -12
  256. package/src/client/state/context-chip-store.ts +70 -0
  257. package/src/client/state/context-status-store.ts +53 -0
  258. package/src/client/state/intent-bridge.ts +51 -30
  259. package/src/client/state/knowledge-store.ts +43 -0
  260. package/src/client/state/near-pin-store.ts +115 -6
  261. package/src/client/state/presence-store.ts +7 -10
  262. package/src/client/state/presentation.ts +91 -0
  263. package/src/client/state/relation-store.ts +81 -0
  264. package/src/client/state/session-store.ts +144 -18
  265. package/src/client/state/sse-bridge.ts +206 -29
  266. package/src/client/state/workspace-settings.ts +11 -0
  267. package/src/client/theme/global.css +2759 -510
  268. package/src/client/types.ts +12 -0
  269. package/src/client/utils/keyboard.ts +10 -0
  270. package/src/client/utils/time-ago.ts +14 -0
  271. package/src/json-render/renderer/index.css +6 -1
  272. package/src/json-render/renderer/index.tsx +31 -1
  273. package/src/mcp/canvas-app.ts +12 -2
  274. package/src/mcp/context-uri-template.ts +7 -4
  275. package/src/mcp/server.ts +63 -41
  276. package/src/mcp/workbench-app.ts +39 -6
  277. package/src/server/agent-presence.ts +128 -28
  278. package/src/server/ax-context.ts +27 -13
  279. package/src/server/board-export.ts +12 -1
  280. package/src/server/canvas-db.ts +331 -42
  281. package/src/server/canvas-operations.ts +196 -84
  282. package/src/server/canvas-provenance.ts +53 -22
  283. package/src/server/canvas-serialization.ts +2 -0
  284. package/src/server/canvas-state.ts +950 -122
  285. package/src/server/canvas-validation.ts +9 -2
  286. package/src/server/context-brief.ts +345 -38
  287. package/src/server/context-reads.ts +179 -30
  288. package/src/server/demo-state.json +355 -24
  289. package/src/server/edit-summary.ts +74 -0
  290. package/src/server/html-surface.ts +8 -1
  291. package/src/server/index.ts +203 -13
  292. package/src/server/knowledge-lifecycle.ts +304 -0
  293. package/src/server/lifecycle-status.ts +232 -0
  294. package/src/server/memory-graph.ts +166 -0
  295. package/src/server/mutation-history.ts +18 -6
  296. package/src/server/operations/composites.ts +32 -11
  297. package/src/server/operations/http.ts +7 -2
  298. package/src/server/operations/index.ts +4 -0
  299. package/src/server/operations/mcp.ts +32 -1
  300. package/src/server/operations/ops/ax-presence.ts +1 -1
  301. package/src/server/operations/ops/ax-read.ts +57 -6
  302. package/src/server/operations/ops/ax-timeline.ts +28 -2
  303. package/src/server/operations/ops/backup.ts +5 -3
  304. package/src/server/operations/ops/batch.ts +35 -4
  305. package/src/server/operations/ops/boards.ts +188 -47
  306. package/src/server/operations/ops/canvas-wire.ts +31 -1
  307. package/src/server/operations/ops/edges.ts +36 -2
  308. package/src/server/operations/ops/groups.ts +6 -1
  309. package/src/server/operations/ops/json-render.ts +5 -1
  310. package/src/server/operations/ops/lifecycle.ts +262 -0
  311. package/src/server/operations/ops/memory-graph.ts +45 -0
  312. package/src/server/operations/ops/nodes.ts +107 -24
  313. package/src/server/operations/ops/query.ts +281 -68
  314. package/src/server/operations/ops/snapshots.ts +73 -1
  315. package/src/server/operations/ops/validate.ts +5 -1
  316. package/src/server/operations/ops/viewport.ts +97 -3
  317. package/src/server/operations/registry.ts +77 -26
  318. package/src/server/placement.ts +29 -0
  319. package/src/server/scope-fence.ts +78 -6
  320. package/src/server/server.ts +115 -16
  321. package/src/server/spatial-analysis.ts +48 -23
  322. package/src/server/tour-control.ts +82 -0
  323. package/src/server/wiki-links.ts +137 -0
  324. package/src/server/workbench-auth.ts +11 -0
  325. package/src/shared/agent-context.ts +3 -0
  326. package/src/shared/agent-presence.ts +15 -15
  327. package/src/shared/auto-arrange.ts +2 -0
  328. package/src/shared/boards.ts +4 -4
  329. package/src/shared/knowledge-lifecycle.ts +22 -0
  330. package/src/shared/knowledge-presets.ts +80 -0
  331. package/src/shared/lifecycle-status.ts +26 -0
  332. package/src/shared/memory-graph.ts +74 -0
  333. package/src/shared/relations.ts +98 -0
  334. package/src/shared/semantic-attention.ts +15 -1
  335. package/src/shared/tour.ts +23 -5
  336. package/src/shared/workspace-settings.ts +13 -0
  337. package/dist/types/client/canvas/ContextPinBar.d.ts +0 -1
  338. package/dist/types/client/canvas/FocusFieldLayer.d.ts +0 -1
  339. package/dist/types/client/canvas/kind-colors.d.ts +0 -3
  340. package/dist/types/server/agent-context.d.ts +0 -22
  341. package/src/client/canvas/ContextPinBar.tsx +0 -25
@@ -13,7 +13,7 @@
13
13
  * This module must never import server.ts or index.ts.
14
14
  */
15
15
  import { z } from 'zod';
16
- import { tourSchema, derivedTour } from '../../../shared/tour.js';
16
+ import { tourSchema, derivedTour, rectSchema, cameraSchema, tourStopSchema } from '../../../shared/tour.js';
17
17
  import { canvasState } from '../../canvas-state.js';
18
18
  import {
19
19
  arrangeCanvasNodes,
@@ -23,6 +23,7 @@ import {
23
23
  setClientViewportSize,
24
24
  } from '../../canvas-operations.js';
25
25
  import { validateCanvasLayout } from '../../canvas-validation.js';
26
+ import { exitTour, goToTourStop, moveCamera, tourPosition } from '../../tour-control.js';
26
27
  import { defineOperation, OperationError, type Operation } from '../types.js';
27
28
  import { closeNodeAppSession, isRecord } from './nodes.js';
28
29
 
@@ -241,8 +242,13 @@ const viewportSetShape = {
241
242
  .optional()
242
243
  .describe('World point {x, y} to centre in the canvas area (instead of x/y); uses scale or the current zoom'),
243
244
  recordHistory: z.unknown().optional().describe('Pass false to skip the undo-history entry'),
245
+ clientId: z.string().min(1).max(128).optional().describe('Originating workbench tab; echoed with camera updates'),
244
246
  clientWidth: z.unknown().optional().describe("The reporting client's window width in CSS pixels"),
245
247
  clientHeight: z.unknown().optional().describe("The reporting client's window height in CSS pixels"),
248
+ clientInsets: z
249
+ .object({ top: z.number().finite().nonnegative(), bottom: z.number().finite().nonnegative() })
250
+ .optional()
251
+ .describe('Measured floating chrome above and below the canvas, in CSS pixels'),
246
252
  };
247
253
 
248
254
  const viewportSetSchema = z.looseObject(viewportSetShape);
@@ -262,7 +268,7 @@ const viewportSetOperation = defineOperation<z.infer<typeof viewportSetSchema>,
262
268
  // The browser piggybacks its real window size here so a later agent `fit`
263
269
  // sizes to the human's actual window instead of a hardcoded 1440x900
264
270
  // (0.4.6 orb feedback #2).
265
- setClientViewportSize(body.clientWidth, body.clientHeight);
271
+ setClientViewportSize(body.clientWidth, body.clientHeight, input.clientInsets);
266
272
  // A size-only report is not a camera command. Echoing the stored camera
267
273
  // here would cancel an in-flight browser focus with its stale fallback.
268
274
  const center = body.center;
@@ -292,7 +298,10 @@ const viewportSetOperation = defineOperation<z.infer<typeof viewportSetSchema>,
292
298
  } else {
293
299
  canvasState.setViewport(next);
294
300
  }
295
- ctx.emit('canvas-viewport-update', { viewport: canvasState.viewport });
301
+ ctx.emit('canvas-viewport-update', {
302
+ viewport: canvasState.viewport,
303
+ ...(input.clientId ? { clientId: input.clientId } : {}),
304
+ });
296
305
  return { ok: true, viewport: canvasState.viewport };
297
306
  },
298
307
  });
@@ -323,12 +332,97 @@ const tourGetOperation = defineOperation({
323
332
  handler: () => ({
324
333
  tour: canvasState.getTour() ?? derivedTour(canvasState.getLayout().nodes),
325
334
  derived: !canvasState.getTour(),
335
+ position: tourPosition(),
326
336
  }),
327
337
  });
328
338
 
339
+ // ── tour.go / tour.exit / camera.move ────────────────────────
340
+ // Navigation only (mutates: false, PRESENCE_EXEMPT_OPS): no undo entry, no
341
+ // writer activity, no scope-fence check. The browser animates.
342
+
343
+ const tourGoShape = {
344
+ stop: z.number().int().nonnegative().optional().describe('Zero-based stop index to go to'),
345
+ step: z.enum(['next', 'previous']).optional().describe('Step relative to the current stop (clamped at the ends)'),
346
+ present: z
347
+ .boolean()
348
+ .optional()
349
+ .describe(
350
+ 'Start presenting in viewers that are not presenting yet (default true; false only moves presenting viewers)',
351
+ ),
352
+ };
353
+ const tourGoOperation = defineOperation({
354
+ name: 'tour.go',
355
+ mutates: false,
356
+ input: z.object(tourGoShape),
357
+ inputShape: tourGoShape,
358
+ http: { method: 'POST', path: '/api/canvas/tour/go' },
359
+ mcp: {
360
+ toolName: 'canvas_tour_go',
361
+ description: 'Go to a tour stop (index, next or previous) in presenting viewers.',
362
+ },
363
+ handler: (input, ctx) => {
364
+ if ((input.stop === undefined) === (input.step === undefined)) {
365
+ throw new OperationError('Give exactly one of stop (index) or step ("next" | "previous").');
366
+ }
367
+ const result = goToTourStop(input.stop ?? (input.step as 'next' | 'previous'));
368
+ ctx.emit('canvas-tour-step', { ...result, present: input.present !== false });
369
+ return { ok: true, ...result };
370
+ },
371
+ });
372
+
373
+ const tourExitOperation = defineOperation({
374
+ name: 'tour.exit',
375
+ mutates: false,
376
+ input: z.object({}),
377
+ inputShape: {},
378
+ http: { method: 'POST', path: '/api/canvas/tour/exit' },
379
+ mcp: { toolName: 'canvas_tour_exit', description: 'Leave presentation in every viewer.' },
380
+ handler: (_input, ctx) => {
381
+ exitTour();
382
+ ctx.emit('canvas-tour-exit', {});
383
+ return { ok: true };
384
+ },
385
+ });
386
+
387
+ const cameraMoveShape = {
388
+ nodeId: z.string().optional().describe('Node or group id to frame'),
389
+ rect: rectSchema.optional().describe('World-space box { x, y, width, height } to frame'),
390
+ viewport: cameraSchema.optional().describe('Exact screen-space camera { x, y, scale }'),
391
+ duration: tourStopSchema.shape.duration.describe('Move time in seconds (default 1; 0 jumps)'),
392
+ easing: tourStopSchema.shape.easing.describe('linear | ease-in-out (default) | ease-out'),
393
+ padding: tourStopSchema.shape.padding.describe('Screen pixels around a node/rect target (default 40)'),
394
+ pullback: tourStopSchema.shape.pullback.describe('Mid-move log-zoom pull-back, 0–4 (default 0)'),
395
+ };
396
+ const cameraMoveOperation = defineOperation({
397
+ name: 'camera.move',
398
+ mutates: false,
399
+ input: z.object(cameraMoveShape),
400
+ inputShape: cameraMoveShape,
401
+ http: { method: 'POST', path: '/api/canvas/camera/move' },
402
+ mcp: {
403
+ toolName: 'canvas_camera_move',
404
+ description: 'Ease the camera to a node, a world rect or an exact viewport over a duration.',
405
+ },
406
+ handler: (input, ctx) => {
407
+ const { nodeId, rect, viewport, ...options } = input;
408
+ const targets = [
409
+ ...(nodeId !== undefined ? [{ nodeId }] : []),
410
+ ...(rect !== undefined ? [{ rect }] : []),
411
+ ...(viewport !== undefined ? [{ viewport }] : []),
412
+ ];
413
+ if (targets.length !== 1) throw new OperationError('Give exactly one of nodeId, rect or viewport.');
414
+ const result = moveCamera({ target: targets[0], ...options });
415
+ ctx.emit('canvas-camera-move', result);
416
+ return { ok: true, ...result };
417
+ },
418
+ });
419
+
329
420
  export const viewportOperations: Operation[] = [
330
421
  tourSetOperation,
331
422
  tourGetOperation,
423
+ tourGoOperation,
424
+ tourExitOperation,
425
+ cameraMoveOperation,
332
426
  arrangeOperation,
333
427
  focusOperation,
334
428
  fitOperation,
@@ -15,7 +15,8 @@ import { withCurrentActor } from '../attribution.js';
15
15
  import { withoutExternalMcp } from '../mcp-app-runtime.js';
16
16
  import { agentPresence, describeWrite } from '../agent-presence.js';
17
17
  import { checkScopeFence, checkScopeOwnership, describeOpTarget } from '../scope-fence.js';
18
- import { CONTEXT_READ_OPS, contextReadFromPayload } from '../context-reads.js';
18
+ import { CONTEXT_READ_OPS, contextReadFromPayload, pinnedBoardReads, graphReads } from '../context-reads.js';
19
+ import type { MemoryGraph } from '../../shared/memory-graph.js';
19
20
  import { openBoardForWrite } from '../canvas-operations.js';
20
21
  import { boardsPayload } from './ops/boards.js';
21
22
  import type { PmxAxIntent, PmxAxIntentKind } from '../../shared/ax-intent.js';
@@ -48,6 +49,10 @@ export function setOperationEventEmitter(emitter: OperationEventEmitter | null):
48
49
  operationEventEmitter = emitter;
49
50
  }
50
51
 
52
+ export function getOperationEventEmitter(): OperationEventEmitter | null {
53
+ return operationEventEmitter;
54
+ }
55
+
51
56
  // Batch suppression belongs to its async call chain, never concurrent human
52
57
  // operations (especially board switches while a batch waits on the network).
53
58
  const suppressedEmits = new AsyncLocalStorage<boolean>();
@@ -190,6 +195,7 @@ function autoGhostInput(name: string, rawInput: unknown, kind: string): Record<s
190
195
  return typeof body.id === 'string' ? { ...base, nodeId: body.id } : null;
191
196
  case 'connect': {
192
197
  if (typeof body.from !== 'string' || typeof body.to !== 'string') return null;
198
+ if (body.toBoardId && body.toBoardId !== canvasState.activeBoardId) return null;
193
199
  const edgeType = typeof body.type === 'string' && AUTO_GHOST_EDGE_TYPES.has(body.type) ? body.type : 'flow';
194
200
  return { ...base, nodeType: undefined, edge: { from: body.from, to: body.to, type: edgeType } };
195
201
  }
@@ -252,7 +258,10 @@ function heldByHuman(name: string, rawInput: unknown): { nodeId: string; name: s
252
258
  const locked = humanPresence.lockedNodes();
253
259
  if (locked.size === 0) return null;
254
260
  const input = asRecord(rawInput);
255
- const ids = [input.id, input.nodeId, input.groupId, input.from, input.to].filter(isString);
261
+ const foreignTarget = name === 'edge.add' && input.toBoardId && input.toBoardId !== canvasState.activeBoardId;
262
+ const ids = [input.id, input.nodeId, input.groupId, input.from, ...(foreignTarget ? [] : [input.to])].filter(
263
+ isString,
264
+ );
256
265
  if (name === 'canvas.apply-updates' || name === 'node.update') {
257
266
  // Fence and lock targets must agree, including translated children and
258
267
  // the automatic parent frames re-fitted by a child write.
@@ -312,6 +321,7 @@ function touchedNodeId(rawInput: unknown, result: unknown): string | null {
312
321
  * and re-touching after a `session-end` would resurrect the writer).
313
322
  */
314
323
  const PRESENCE_EXEMPT_OPS = new Set([
324
+ 'settings.set',
315
325
  'ax.presence.set',
316
326
  'ax.presence.get',
317
327
  'ax.activity.ingest',
@@ -330,6 +340,9 @@ const PRESENCE_EXEMPT_OPS = new Set([
330
340
  'viewport.set',
331
341
  'view.fit',
332
342
  'node.focus',
343
+ 'tour.go',
344
+ 'tour.exit',
345
+ 'camera.move',
333
346
  'ax.focus.set',
334
347
  ]);
335
348
 
@@ -391,8 +404,10 @@ export async function executeOperation(
391
404
  ...(typeof inputAgentId === 'string' && inputAgentId.trim() ? { agentId: inputAgentId.trim() } : {}),
392
405
  },
393
406
  async () => {
394
- const recordRead = CONTEXT_READ_OPS.has(name) && !meta.fromWorkbench && !meta.proxiedRead;
395
407
  const input = asRecord(rawInput);
408
+ const deliversContext =
409
+ name !== 'node.get' || (input.contextDelivery !== false && input.contextDelivery !== 'false');
410
+ const recordRead = CONTEXT_READ_OPS.has(name) && deliversContext && !meta.fromWorkbench && !meta.proxiedRead;
396
411
  const requestedBoard =
397
412
  recordRead && 'board' in getOperation(name).inputShape && isString(input.board) && input.board.trim()
398
413
  ? input.board.trim()
@@ -410,28 +425,32 @@ export async function executeOperation(
410
425
  ? withoutExternalMcp(run)
411
426
  : run());
412
427
  if (recordRead) {
413
- canvasState.recordContextRead(
414
- contextReadFromPayload(
415
- {
416
- channel: 'operation',
417
- resource: name,
418
- source: meta.source ?? 'api',
419
- consumer: isString(input.consumer) && input.consumer.trim() ? input.consumer.trim() : null,
420
- agentId: isString(input.agentId) && input.agentId.trim() ? input.agentId.trim() : null,
421
- pinnedNodeIds: readPins,
422
- },
423
- result,
424
- ),
425
- readBoardId,
426
- );
428
+ const base = {
429
+ channel: 'operation' as const,
430
+ resource: name,
431
+ source: meta.source ?? 'api',
432
+ consumer: isString(input.consumer) && input.consumer.trim() ? input.consumer.trim() : null,
433
+ agentId: isString(input.agentId) && input.agentId.trim() ? input.agentId.trim() : null,
434
+ };
435
+ if (name === 'memory-graph.get') {
436
+ for (const read of graphReads(base, result as MemoryGraph)) canvasState.recordContextRead(read, read.boardId);
437
+ } else {
438
+ canvasState.recordContextRead(
439
+ contextReadFromPayload({ ...base, pinnedNodeIds: readPins }, result),
440
+ readBoardId,
441
+ );
442
+ }
443
+ if (name === 'context.get') {
444
+ for (const read of pinnedBoardReads(base, result)) canvasState.recordContextRead(read, read.boardId);
445
+ }
427
446
  }
428
447
  return result;
429
448
  },
430
449
  );
431
450
  }
432
451
 
433
- /** Board deletion and import review actions require the human in the workbench. */
434
- const HUMAN_ONLY_OPS = new Set(['board.delete', 'import.request', 'import.cancel', 'import.commit']);
452
+ /** Workspace preferences, board deletion and import review require the human workbench. */
453
+ const HUMAN_ONLY_OPS = new Set(['settings.set', 'board.delete', 'import.request', 'import.cancel', 'import.commit']);
435
454
  const TRUSTED_HUMAN_ONLY_OPS = new Set(['board.delete', 'import.request', 'import.cancel', 'import.commit']);
436
455
 
437
456
  /**
@@ -461,6 +480,9 @@ function needsOpenBoard(op: Operation): boolean {
461
480
 
462
481
  async function executeOperationInner(name: string, rawInput: unknown, meta: ExecuteOperationMeta): Promise<unknown> {
463
482
  const op = getOperation(name);
483
+ const storedCreation =
484
+ ['node.add', 'group.create', 'edge.add'].includes(name) &&
485
+ (asRecord(rawInput).destinationBoardId !== undefined || asRecord(rawInput).requestId !== undefined);
464
486
  if (HUMAN_ONLY_OPS.has(name) && !meta.fromWorkbench) {
465
487
  throw new OperationError('This action requires the human in the workbench.', 403);
466
488
  }
@@ -481,7 +503,7 @@ async function executeOperationInner(name: string, rawInput: unknown, meta: Exec
481
503
  if (name === 'node.add' && typeof capturedBoard === 'string' && capturedBoard !== canvasState.activeBoardId) {
482
504
  throw new OperationError('The board changed while the file was loading.', 409);
483
505
  }
484
- if (!canvasState.activeBoardId && needsOpenBoard(op)) {
506
+ if (!canvasState.activeBoardId && needsOpenBoard(op) && !storedCreation) {
485
507
  openBoardForWrite();
486
508
  if (canvasState.activeBoardId) emitOperationEvent('boards-changed', boardsPayload());
487
509
  }
@@ -493,11 +515,19 @@ async function executeOperationInner(name: string, rawInput: unknown, meta: Exec
493
515
  // carries one, else its transport/source label.
494
516
  const rawAgentId = asRecord(rawInput).agentId;
495
517
  const writerKey = typeof rawAgentId === 'string' && rawAgentId.trim() ? rawAgentId.trim() : (meta.source ?? 'api');
496
- const refusal = op.mutates
497
- ? checkScopeFence(op, rawInput, writerKey)
498
- : name === 'ax.policy.set'
499
- ? checkScopeOwnership(rawInput, writerKey)
500
- : null;
518
+ const refusal =
519
+ op.mutates ||
520
+ name === 'knowledge.category-set' ||
521
+ name === 'knowledge.category-remove' ||
522
+ name === 'knowledge.relation-preset-set' ||
523
+ name === 'knowledge.relation-preset-remove' ||
524
+ name === 'knowledge.folder-set' ||
525
+ (name === 'board.update' &&
526
+ ['folder', 'relationVocabulary'].some((key) => Object.hasOwn(asRecord(rawInput), key)))
527
+ ? checkScopeFence(op, rawInput, writerKey)
528
+ : name === 'ax.policy.set'
529
+ ? checkScopeOwnership(rawInput, writerKey)
530
+ : null;
501
531
  if (refusal) throw new OperationError(`Outside the agent scope: ${refusal}`, 403);
502
532
  // User wins (item 6): a node a human is holding right now is not the
503
533
  // agent's to change — refuse with 409 so the agent requeues the edit.
@@ -511,6 +541,16 @@ async function executeOperationInner(name: string, rawInput: unknown, meta: Exec
511
541
  }
512
542
  }
513
543
  }
544
+ if (storedCreation) {
545
+ const revision = canvasState.getContentRevision().revision;
546
+ const result = await op.execute(rawInput, operationContext);
547
+ const destination = asRecord(rawInput).destinationBoardId ?? canvasState.activeBoardId;
548
+ if (destination === canvasState.activeBoardId || revision !== canvasState.getContentRevision().revision)
549
+ emitOperationEvent('canvas-layout-update', { layout: canvasState.getLayout() });
550
+ emitOperationEvent('boards-changed', boardsPayload());
551
+ if (destination === canvasState.activeBoardId) notePresence(name, rawInput, meta, op, result);
552
+ return result;
553
+ }
514
554
  const linkedId = linkedIntentId(rawInput);
515
555
  // A ghost that expired QUIETLY (no veto) does not block its mutation — the
516
556
  // write proceeds unlinked (and takes the auto-ghost path below). Vetoed and
@@ -578,8 +618,19 @@ async function executeOperationInner(name: string, rawInput: unknown, meta: Exec
578
618
  }
579
619
  }
580
620
 
621
+ const beforeBoard = canvasState.activeBoardId;
622
+ const beforeRevision =
623
+ name === 'board.create-from' || name === 'board.delete' ? canvasState.getContentRevision().revision : null;
581
624
  const result = await op.execute(rawInput, operationContext);
582
- if (op.mutates) emitOperationEvent('canvas-layout-update', { layout: canvasState.getLayout() });
625
+ // Library writes can invalidate relations on the open board without being
626
+ // layout mutations themselves. Publish those clocks once, after the write.
627
+ if (
628
+ op.mutates ||
629
+ (beforeRevision !== null &&
630
+ beforeBoard === canvasState.activeBoardId &&
631
+ beforeRevision !== canvasState.getContentRevision().revision)
632
+ )
633
+ emitOperationEvent('canvas-layout-update', { layout: canvasState.getLayout() });
583
634
  notePresence(name, rawInput, meta, op, result);
584
635
  return result;
585
636
  }
@@ -14,6 +14,35 @@ const GROUP_LAYOUT_MIN_ROW_WIDTH = 1200;
14
14
  const GROUP_LAYOUT_MAX_ROW_WIDTH = 1800;
15
15
  const GROUP_TO_GROUP_GAP = 48;
16
16
 
17
+ /** Pack members inside an explicitly sized frame without changing that frame. */
18
+ export function computeManualGroupLayout<T extends CanvasPlacementRect & { id: string }>(
19
+ children: T[],
20
+ frame: CanvasPlacementRect,
21
+ layout: 'grid' | 'column' | 'flow',
22
+ ): Map<string, { x: number; y: number }> {
23
+ const positions = new Map<string, { x: number; y: number }>();
24
+ const left = frame.position.x + GROUP_PAD;
25
+ const top = frame.position.y + GROUP_TITLEBAR_HEIGHT + GROUP_PAD;
26
+ const right = frame.position.x + frame.size.width - GROUP_PAD;
27
+ let x = left,
28
+ y = top,
29
+ rowHeight = 0;
30
+ for (const child of [...children].sort((a, b) => a.position.y - b.position.y || a.position.x - b.position.x)) {
31
+ if (layout === 'grid' && x > left && x + child.size.width > right) {
32
+ x = left;
33
+ y += rowHeight + 24;
34
+ rowHeight = 0;
35
+ }
36
+ positions.set(child.id, { x: layout === 'column' ? left : x, y: layout === 'flow' ? top : y });
37
+ if (layout === 'column') y += child.size.height + 24;
38
+ else {
39
+ x += child.size.width + 24;
40
+ rowHeight = Math.max(rowHeight, child.size.height);
41
+ }
42
+ }
43
+ return positions;
44
+ }
45
+
17
46
  /**
18
47
  * Compute bounding box for a group that should contain the given child rects.
19
48
  * Returns position and size with padding, or null if no valid children.
@@ -24,8 +24,11 @@
24
24
  * authentication boundary.
25
25
  */
26
26
  import { type FenceRect, fenceRectFromNodes } from '../shared/scope-fence.js';
27
+ import { normalizeBoardFolder } from '../shared/boards.js';
27
28
  import { canvasState } from './canvas-state.js';
28
29
  import { resolvePatchGeometry, validCanvasNodeUpdates } from './canvas-geometry.js';
30
+ import { normalizeAxPolicy, type PmxAxPolicy } from './ax-state.js';
31
+ import { categorySchema, folderPropertiesSchema } from './knowledge-lifecycle.js';
29
32
  import type { Operation } from './operations/types.js';
30
33
 
31
34
  /** Bounding box of the fenced nodes plus padding; null when none of them exist. */
@@ -55,8 +58,12 @@ function inside(rect: FenceRect, x: number, y: number): boolean {
55
58
  }
56
59
 
57
60
  /** Returns a human-readable refusal, or null when the target is inside the fence. */
58
- export function checkFenceTarget(target: FenceTarget, opName: string, writerKey?: string): string | null {
59
- const policy = canvasState.getPolicy();
61
+ export function checkFenceTarget(
62
+ target: FenceTarget,
63
+ opName: string,
64
+ writerKey?: string,
65
+ policy: PmxAxPolicy = canvasState.getPolicy(),
66
+ ): string | null {
60
67
  // Per-agent territory (fleet orchestration) overrides the global fence for
61
68
  // that writer; writers without a territory fall under the global fence.
62
69
  const territory = writerKey ? policy.agentScopes[writerKey] : undefined;
@@ -154,13 +161,24 @@ function createTarget(input: Record<string, unknown>, extraNodeIds: string[] = [
154
161
  }
155
162
 
156
163
  /** Ops that rewrite the whole board — never allowed under a fence. */
157
- const BOARD_WIDE_OPS = new Set(['arrange', 'canvas.clear', 'snapshot.restore', 'render.workboard', 'tour.set']);
164
+ const BOARD_WIDE_OPS = new Set([
165
+ 'arrange',
166
+ 'canvas.clear',
167
+ 'snapshot.restore',
168
+ 'render.workboard',
169
+ 'tour.set',
170
+ 'board.update',
171
+ ]);
158
172
 
159
173
  /** Describe what a registry op would write, in fence terms. */
160
174
  export function describeOpTarget(op: Operation, rawInput: unknown): FenceTarget {
161
175
  const input = asRecord(rawInput);
162
176
  if (BOARD_WIDE_OPS.has(op.name)) return { boardWide: true };
163
177
  switch (op.name) {
178
+ case 'node.lifecycle-set':
179
+ case 'node.review':
180
+ case 'node.lifecycle-restore':
181
+ return { nodeIds: [str(input.id)] };
164
182
  case 'canvas.apply-updates':
165
183
  return applyUpdatesTarget(input);
166
184
  case 'node.update': {
@@ -177,6 +195,8 @@ export function describeOpTarget(op: Operation, rawInput: unknown): FenceTarget
177
195
  }
178
196
  case 'node.remove':
179
197
  return { nodeIds: [str(input.id)] };
198
+ case 'snapshot.restore-node':
199
+ return { nodeIds: [str(input.nodeId)] };
180
200
  case 'node.add':
181
201
  return createTarget(input, input.type === 'group' ? groupChildren(input) : []);
182
202
  case 'jsonrender.add':
@@ -188,11 +208,16 @@ export function describeOpTarget(op: Operation, rawInput: unknown): FenceTarget
188
208
  case 'edge.add':
189
209
  // Search-resolved endpoints (`fromSearch`/`toSearch`) cannot be checked
190
210
  // before resolution and so read as '' — refused, not bypassed.
191
- return { nodeIds: [str(input.from), str(input.to)] };
211
+ return {
212
+ nodeIds:
213
+ input.toBoardId && input.toBoardId !== canvasState.activeBoardId
214
+ ? [str(input.from)]
215
+ : [str(input.from), str(input.to)],
216
+ };
192
217
  case 'edge.remove': {
193
218
  const id = str(input.id) || str(input.edge_id);
194
219
  const edge = canvasState.getEdges().find((entry) => entry.id === id);
195
- return edge ? { nodeIds: [edge.from, edge.to] } : {}; // missing edge: the op 404s on its own
220
+ return edge ? { nodeIds: edge.toBoardId ? [edge.from] : [edge.from, edge.to] } : {}; // missing edge: the op 404s on its own
196
221
  }
197
222
  case 'group.create':
198
223
  return { nodeIds: stringList(input.childIds ?? input.children) };
@@ -228,7 +253,54 @@ export function describeOpTarget(op: Operation, rawInput: unknown): FenceTarget
228
253
 
229
254
  /** Registry entry point: refusal text or null. Only call for mutating, agent-originated ops. */
230
255
  export function checkScopeFence(op: Operation, rawInput: unknown, writerKey?: string): string | null {
231
- return checkFenceTarget(describeOpTarget(op, rawInput), op.name, writerKey);
256
+ const target = describeOpTarget(op, rawInput);
257
+ const activeRefusal = checkFenceTarget(target, op.name, writerKey);
258
+ if (activeRefusal) return activeRefusal;
259
+ // Re-filing changes every card's lifecycle inheritance, even on a closed
260
+ // board. Its stored policy applies without opening it or dropping the active fence.
261
+ const input = asRecord(rawInput);
262
+ if (op.name === 'board.update' && typeof input.id === 'string' && input.id !== canvasState.activeBoardId) {
263
+ const board = canvasState.readBoard(input.id, false);
264
+ return checkFenceTarget(target, op.name, writerKey, normalizeAxPolicy(board?.state.ax?.policy));
265
+ }
266
+ if (op.name === 'knowledge.category-set' || op.name === 'knowledge.folder-set') {
267
+ const before = canvasState.knowledge.list();
268
+ const after = { ...before };
269
+ if (op.name === 'knowledge.category-set') {
270
+ const parsed = categorySchema.safeParse(input.category);
271
+ if (!parsed.success) return null; // The operation reports invalid input without writing.
272
+ const category = parsed.data;
273
+ after.categories = [...before.categories.filter((entry) => entry.id !== category.id), category];
274
+ } else {
275
+ const parsed = folderPropertiesSchema.safeParse(rawInput);
276
+ if (!parsed.success) return null;
277
+ const folder = parsed.data;
278
+ let path: string | null;
279
+ try {
280
+ path = normalizeBoardFolder(folder.path);
281
+ } catch {
282
+ return null; // Folder validation belongs to the operation handler.
283
+ }
284
+ if (!path) return null;
285
+ after.folders = [...before.folders.filter((entry) => entry.path !== path), { ...folder, path }];
286
+ }
287
+ for (const board of canvasState.listBoards()) {
288
+ if (
289
+ JSON.stringify(canvasState.knowledge.resolve(board.folder, null, before)) ===
290
+ JSON.stringify(canvasState.knowledge.resolve(board.folder, null, after))
291
+ )
292
+ continue;
293
+ const stored = canvasState.readBoard(board.id, false);
294
+ const refusal = checkFenceTarget(
295
+ { boardWide: true },
296
+ op.name,
297
+ writerKey,
298
+ normalizeAxPolicy(stored?.state.ax?.policy),
299
+ );
300
+ if (refusal) return refusal;
301
+ }
302
+ }
303
+ return null;
232
304
  }
233
305
 
234
306
  /**