pmx-canvas 0.11.0 → 0.12.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.
- package/.github/extensions/pmx-canvas/extension.mjs +3 -1
- package/.github/extensions/pmx-canvas/runtime.json +1 -1
- package/.github/extensions/pmx-canvas/tools.json +440 -18
- package/CHANGELOG.md +37 -0
- package/dist/amp-plugin/pmx-canvas/LICENSE +21 -0
- package/dist/amp-plugin/pmx-canvas/THIRD_PARTY_LICENSES.txt +331 -0
- package/dist/amp-plugin/pmx-canvas/index.js +5445 -0
- package/dist/amp-plugin/pmx-canvas/runtime.json +3 -0
- package/dist/amp-plugin/pmx-canvas/shared-runtime.mjs +194 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/SKILL.md +251 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/evals/evals.json +1086 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/evals/fixtures/code-exploration/src/auth/jwt.ts +17 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/evals/fixtures/code-exploration/src/auth/login.ts +12 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/evals/fixtures/code-exploration/src/auth/middleware.ts +13 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/evals/fixtures/code-exploration/src/routes/auth.ts +13 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/evals/fixtures/investigation-board/src/handlers/users.ts +27 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/mcp.json +36 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/ax-html-control-surface.md +101 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/codex-app-adapter.md +151 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/excalidraw-diagram-authoring.md +150 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/full-reference.md +1785 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/github-copilot-app-adapter.md +223 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/html-primitives.md +139 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/importing-context.md +132 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/installing-pmx-canvas.md +251 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/tours-and-recording.md +111 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/workbench-workflows.md +900 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas-orchestration/SKILL.md +141 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas-testing/SKILL.md +266 -0
- package/dist/amp-plugin/pmx-canvas/start-mcp.mjs +14857 -0
- package/dist/amp-plugin/pmx-canvas/tools.json +10963 -0
- package/dist/canvas/chunks/{abnfDiagram-N423BO3Z-p8dtp6va.js → abnfDiagram-N423BO3Z-cwtsds5d.js} +1 -1
- package/dist/canvas/chunks/architecture-TIHT7OUA-x63cwejs.js +1 -0
- package/dist/canvas/chunks/{architectureDiagram-T3A2C74G-5jhmxrzf.js → architectureDiagram-T3A2C74G-b4xc9tpz.js} +3 -3
- package/dist/canvas/chunks/{blockDiagram-VBNYF7ZC-pw1fa9yb.js → blockDiagram-VBNYF7ZC-ahp3yxxs.js} +7 -7
- package/dist/canvas/chunks/{c4Diagram-5PPSVZJV-x07nq0tt.js → c4Diagram-5PPSVZJV-nappnay7.js} +4 -4
- package/dist/canvas/chunks/classDiagram-JCYQIIEL-k1j2xc1t.js +1 -0
- package/dist/canvas/chunks/classDiagram-v2-OCEON4UE-33x08xf2.js +1 -0
- package/dist/canvas/chunks/{cose-bilkent-JH36ORCC-nm39npq0.js → cose-bilkent-JH36ORCC-a428syda.js} +1 -1
- package/dist/canvas/chunks/cynefin-VYW2F7L2-8v6jxfz8.js +1 -0
- package/dist/canvas/chunks/{cynefinDiagram-MW4NZA55-vdxrwahz.js → cynefinDiagram-MW4NZA55-nspsqebj.js} +1 -1
- package/dist/canvas/chunks/{dagre-VZM6K2ZE-bqdm7wpd.js → dagre-VZM6K2ZE-8y6177fh.js} +2 -2
- package/dist/canvas/chunks/{diagram-7IWD3JNH-gs5gpr93.js → diagram-7IWD3JNH-vym028wn.js} +4 -4
- package/dist/canvas/chunks/{diagram-B4RE2ZJO-cgwjg47s.js → diagram-B4RE2ZJO-jddqb8sy.js} +1 -1
- package/dist/canvas/chunks/{diagram-LBJQPF4R-w3vahpav.js → diagram-LBJQPF4R-bjwbpkrn.js} +1 -1
- package/dist/canvas/chunks/{diagram-Q27KOJAE-42837z6h.js → diagram-Q27KOJAE-j6ph6hcx.js} +2 -2
- package/dist/canvas/chunks/{diagram-UB23O5K3-4fz83q59.js → diagram-UB23O5K3-p09w8jej.js} +1 -1
- package/dist/canvas/chunks/{ebnfDiagram-BXEA7PRR-zphtgegj.js → ebnfDiagram-BXEA7PRR-j975ycq5.js} +1 -1
- package/dist/canvas/chunks/{erDiagram-JOGREHBK-zvbbdxp4.js → erDiagram-JOGREHBK-sxp6hjn8.js} +5 -5
- package/dist/canvas/chunks/eventmodeling-45OFAUF4-xzfgn6xg.js +1 -0
- package/dist/canvas/chunks/flowDiagram-UKHOOZJN-2nsmkskb.js +1 -0
- package/dist/canvas/chunks/{ganttDiagram-PKOTCBZU-282jzycm.js → ganttDiagram-PKOTCBZU-4tv230y2.js} +2 -2
- package/dist/canvas/chunks/gitGraph-TEB2WS4Q-macbhand.js +1 -0
- package/dist/canvas/chunks/{gitGraphDiagram-DS77QQ5N-vqqy6p1b.js → gitGraphDiagram-DS77QQ5N-mqgmnw9g.js} +1 -1
- package/dist/canvas/chunks/{index-wgv7en21.js → index-037z5dz2.js} +2 -2
- package/dist/canvas/chunks/{index-w2mra17t.js → index-081n3har.js} +1 -1
- package/dist/canvas/chunks/{index-353m7b0p.js → index-5n03dmb9.js} +1 -1
- package/dist/canvas/chunks/{index-yw8h9vha.js → index-60pgzdqe.js} +1 -1
- package/dist/canvas/chunks/index-8g1vb57n.js +2 -0
- package/dist/canvas/chunks/{index-vv56h019.js → index-8rjes9r6.js} +1 -1
- package/dist/canvas/chunks/{index-nprm96xp.js → index-9022d1n9.js} +1 -1
- package/dist/canvas/chunks/{index-194m3hc3.js → index-9b02042t.js} +1 -1
- package/dist/canvas/chunks/{index-0rk4gp1g.js → index-ap4q4vez.js} +1 -1
- package/dist/canvas/chunks/{index-rh7nv68n.js → index-dyp2nhd3.js} +1 -1
- package/dist/canvas/chunks/index-frvta4tm.js +2 -0
- package/dist/canvas/chunks/{index-s4j678gj.js → index-h9ycj8n2.js} +1 -1
- package/dist/canvas/chunks/{index-fjtdggjn.js → index-mergmj97.js} +2 -2
- package/dist/canvas/chunks/index-nwsya6dj.js +301 -0
- package/dist/canvas/chunks/{index-3cy1bzwh.js → index-nz8qbd1p.js} +1 -1
- package/dist/canvas/chunks/{index-d3b3gqe9.js → index-qhfrazv3.js} +1 -1
- package/dist/canvas/chunks/{index-6qmsnxwy.js → index-v119nznk.js} +4 -4
- package/dist/canvas/chunks/{index-r2fyc9w1.js → index-vpt37m3d.js} +6 -6
- package/dist/canvas/chunks/{index-qj8fxwsf.js → index-y9ma6azy.js} +1 -1
- package/dist/canvas/chunks/{index-tb1wf525.js → index-zx0j3a2z.js} +26 -26
- package/dist/canvas/chunks/info-DKCQHKI2-a0ykzs9z.js +1 -0
- package/dist/canvas/chunks/{infoDiagram-6WML65LV-v1yvggqj.js → infoDiagram-6WML65LV-qv97qq6a.js} +2 -2
- package/dist/canvas/chunks/ishikawaDiagram-WSZJBQD7-p9306gh0.js +70 -0
- package/dist/canvas/chunks/{journeyDiagram-NVQOT4AX-zjcffveh.js → journeyDiagram-NVQOT4AX-sv67zats.js} +5 -5
- package/dist/canvas/chunks/{kanban-definition-27J2QSJJ-dakqtfp1.js → kanban-definition-27J2QSJJ-k2pfam6p.js} +1 -1
- package/dist/canvas/chunks/{mermaid.core-j2geswrd.js → mermaid.core-83r0fdce.js} +4 -4
- package/dist/canvas/chunks/{mindmap-definition-FAOFIHXS-142xhr3k.js → mindmap-definition-FAOFIHXS-vg2newm1.js} +1 -1
- package/dist/canvas/chunks/packet-7NZHBO7P-tqnabcdv.js +1 -0
- package/dist/canvas/chunks/{pegDiagram-VL7TDLO6-5wvk7vsd.js → pegDiagram-VL7TDLO6-k2h7zc5n.js} +1 -1
- package/dist/canvas/chunks/pie-RZYD4A2V-wke6gyhe.js +1 -0
- package/dist/canvas/chunks/{pieDiagram-7S7Q4E2Y-ztdhwycc.js → pieDiagram-7S7Q4E2Y-hg169p1v.js} +2 -2
- package/dist/canvas/chunks/{quadrantDiagram-CIZ2JOQS-c5p9gmg6.js → quadrantDiagram-CIZ2JOQS-bxtvsq0b.js} +5 -5
- package/dist/canvas/chunks/radar-I7S5WNFK-w7ka0drw.js +1 -0
- package/dist/canvas/chunks/railroad-3IZDKUUU-y3vg318x.js +1 -0
- package/dist/canvas/chunks/railroad-abnf-AHOZXSZD-6hqvcz3c.js +1 -0
- package/dist/canvas/chunks/railroad-ebnf-EBAXGLYW-vcae260m.js +1 -0
- package/dist/canvas/chunks/railroad-peg-LSFZ7HO6-0dbyme23.js +1 -0
- package/dist/canvas/chunks/{railroadDiagram-AXF67PYL-xkmqkjwf.js → railroadDiagram-AXF67PYL-njyhcp4p.js} +1 -1
- package/dist/canvas/chunks/{requirementDiagram-LRYGKXZP-9wm2dyaw.js → requirementDiagram-LRYGKXZP-2z6v09gm.js} +4 -4
- package/dist/canvas/chunks/{sankeyDiagram-W5VNT64P-2w19etgb.js → sankeyDiagram-W5VNT64P-15mzam0e.js} +1 -1
- package/dist/canvas/chunks/{sequenceDiagram-SI44F4Z6-0jn0vwmz.js → sequenceDiagram-SI44F4Z6-rbshyd2c.js} +12 -12
- package/dist/canvas/chunks/{sizeCapture-X5ZJPWSS-x75epwdq.js → sizeCapture-X5ZJPWSS-sg26xwts.js} +1 -1
- package/dist/canvas/chunks/{stateDiagram-OKZ733FA-schjpy6d.js → stateDiagram-OKZ733FA-6eahp98e.js} +1 -1
- package/dist/canvas/chunks/stateDiagram-v2-UEYNNEHI-tty0ggn5.js +1 -0
- package/dist/canvas/chunks/{swimlanes-SLNWSIFB-gkw4xce3.js → swimlanes-SLNWSIFB-ze5db10t.js} +1 -1
- package/dist/canvas/chunks/swimlanesDiagram-ULZ7WXOC-thze75gq.js +8 -0
- package/dist/canvas/chunks/{timeline-definition-Z64GVDOM-x7nkcy01.js → timeline-definition-Z64GVDOM-vnp7jyy8.js} +3 -3
- package/dist/canvas/chunks/treeView-QDETBFTQ-6ysffnm6.js +1 -0
- package/dist/canvas/chunks/treemap-6X3UGDF4-m3h9p066.js +1 -0
- package/dist/canvas/chunks/vennDiagram-T6HMQDX7-0qrnb7mj.js +34 -0
- package/dist/canvas/chunks/wardley-OPB4EBWU-dp37drwx.js +1 -0
- package/dist/canvas/chunks/{wardleyDiagram-T6FBY63Y-t1j52t9r.js → wardleyDiagram-T6FBY63Y-8nxg8wqt.js} +4 -4
- package/dist/canvas/chunks/{xychartDiagram-ELKLHX3M-38t16pme.js → xychartDiagram-ELKLHX3M-nj921e7s.js} +5 -5
- package/dist/canvas/global.css +94 -1
- package/dist/canvas/index.js +154 -145
- package/dist/canvas/mcp-app.html +600 -498
- package/dist/json-render/index.css +1 -1
- package/dist/types/client/canvas/BoardMoveDialog.d.ts +2 -0
- package/dist/types/client/canvas/EdgeEditor.d.ts +19 -0
- package/dist/types/client/canvas/GroupNavigation.d.ts +2 -0
- package/dist/types/client/canvas/TextPrompt.d.ts +4 -0
- package/dist/types/client/nodes/PromptNode.d.ts +1 -0
- package/dist/types/client/nodes/ViewerFrame.d.ts +1 -1
- package/dist/types/client/nodes/WorkCard.d.ts +7 -0
- package/dist/types/client/nodes/use-ax-surface-bridge.d.ts +1 -0
- package/dist/types/client/state/canvas-store.d.ts +15 -0
- package/dist/types/client/state/human-store.d.ts +2 -0
- package/dist/types/client/state/intent-bridge.d.ts +4 -2
- package/dist/types/client/state/relation-store.d.ts +11 -0
- package/dist/types/client/types.d.ts +1 -1
- package/dist/types/json-render/catalog.d.ts +2 -2
- package/dist/types/server/agent-presence.d.ts +1 -0
- package/dist/types/server/ax-interaction.d.ts +1 -0
- package/dist/types/server/ax-state-manager.d.ts +10 -2
- package/dist/types/server/board-local.d.ts +4 -0
- package/dist/types/server/board-move.d.ts +38 -0
- package/dist/types/server/canvas-db.d.ts +12 -2
- package/dist/types/server/canvas-operations.d.ts +5 -6
- package/dist/types/server/canvas-provenance.d.ts +1 -1
- package/dist/types/server/canvas-serialization.d.ts +1 -0
- package/dist/types/server/canvas-state.d.ts +84 -20
- package/dist/types/server/card-relocation.d.ts +6 -0
- package/dist/types/server/context-reads.d.ts +3 -1
- package/dist/types/server/human-presence.d.ts +1 -1
- package/dist/types/server/index.d.ts +16 -5
- package/dist/types/server/intent-registry.d.ts +8 -4
- package/dist/types/server/knowledge-lifecycle.d.ts +4 -2
- package/dist/types/server/mutation-history.d.ts +7 -5
- package/dist/types/server/operations/registry.d.ts +2 -0
- package/dist/types/server/trace-manager.d.ts +1 -0
- package/dist/types/shared/ax-kinds.d.ts +6 -0
- package/dist/types/shared/ax-surface-protocol.d.ts +8 -0
- package/dist/types/shared/human-presence.d.ts +1 -0
- package/dist/types/shared/lifecycle-status.d.ts +2 -0
- package/docs/RELEASE.md +21 -14
- package/docs/agent-plugin.md +16 -2
- package/docs/amp-plugin.md +271 -0
- package/docs/cli.md +42 -3
- package/docs/design/GuestName.dc.html +50 -0
- package/docs/design/PluginLogo.svg +7 -0
- package/docs/design/WorkCards.dc.html +39 -0
- package/docs/design/canvas.json +16 -0
- package/docs/design/plugin-logo.png +0 -0
- package/docs/design.md +111 -8
- package/docs/evals/agents-md-sections.md +38 -0
- package/docs/http-api.md +128 -10
- package/docs/mcp.md +61 -9
- package/docs/node-types.md +71 -1
- package/docs/plans/plan-009-tech-debt-backlog.md +1 -1
- package/docs/plans/plan-019-post-0.9-roadmap.md +44 -20
- package/docs/plans/plan-020-wave-5-work-planning.md +553 -0
- package/docs/product-vision-2026-09.md +33 -17
- package/docs/sdk.md +18 -0
- package/package.json +6 -3
- package/plugins/pmx-canvas/assets/preview.png +0 -0
- package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/extension.mjs +3 -1
- package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.json +1 -1
- package/plugins/pmx-canvas/plugin.json +2 -2
- package/plugins/pmx-canvas/runtime.json +1 -1
- package/plugins/pmx-canvas/tools.json +440 -18
- package/skills/data-analysis/SKILL.md +13 -26
- package/skills/frontend-design/LICENSE.txt +177 -0
- package/skills/frontend-design/SKILL.md +3 -0
- package/skills/pmx-canvas/SKILL.md +232 -842
- package/skills/pmx-canvas/evals/evals.json +26 -3
- package/skills/pmx-canvas/references/full-reference.md +80 -20
- package/skills/pmx-canvas/references/html-primitives.md +7 -0
- package/skills/pmx-canvas/references/installing-pmx-canvas.md +6 -6
- package/skills/pmx-canvas/references/tours-and-recording.md +29 -2
- package/skills/pmx-canvas/references/workbench-workflows.md +900 -0
- package/skills/pmx-canvas-testing/SKILL.md +7 -5
- package/skills/published-consumer-e2e/SKILL.md +25 -8
- package/skills/published-consumer-e2e/scripts/run-published-consumer-e2e.sh +51 -75
- package/skills/web-artifacts-builder/LICENSE.txt +202 -0
- package/skills/web-artifacts-builder/SKILL.md +6 -0
- package/skills/web-artifacts-builder/scripts/init-artifact.sh +2 -2
- package/src/cli/commands/apps.ts +1 -0
- package/src/cli/commands/boards.ts +30 -0
- package/src/client/App.tsx +20 -4
- package/src/client/canvas/AttentionHistory.tsx +10 -2
- package/src/client/canvas/BoardMoveDialog.tsx +225 -0
- package/src/client/canvas/CanvasNode.tsx +45 -2
- package/src/client/canvas/CanvasViewport.tsx +38 -9
- package/src/client/canvas/ContextMenu.tsx +34 -12
- package/src/client/canvas/EdgeEditor.tsx +149 -0
- package/src/client/canvas/EdgeLayer.tsx +87 -16
- package/src/client/canvas/ExpandedNodeOverlay.tsx +4 -0
- package/src/client/canvas/GroupNavigation.tsx +85 -0
- package/src/client/canvas/RelationsPanel.tsx +9 -0
- package/src/client/canvas/SelectionBar.tsx +21 -0
- package/src/client/canvas/SessionPanel.tsx +35 -1
- package/src/client/canvas/TextPrompt.tsx +18 -1
- package/src/client/canvas/TopBar.tsx +30 -0
- package/src/client/canvas/canvas-area.ts +1 -1
- package/src/client/nodes/ExtAppFrame.tsx +26 -10
- package/src/client/nodes/MarkdownNode.tsx +6 -7
- package/src/client/nodes/PromptNode.tsx +32 -3
- package/src/client/nodes/StatusNode.tsx +12 -0
- package/src/client/nodes/StatusSummary.tsx +11 -0
- package/src/client/nodes/ViewerFrame.tsx +21 -10
- package/src/client/nodes/WorkCard.tsx +175 -0
- package/src/client/nodes/use-ax-surface-bridge.ts +18 -14
- package/src/client/state/attention-bridge.ts +5 -0
- package/src/client/state/attention-store.ts +1 -0
- package/src/client/state/canvas-store.ts +185 -15
- package/src/client/state/human-store.ts +22 -2
- package/src/client/state/intent-bridge.ts +19 -5
- package/src/client/state/relation-store.ts +127 -1
- package/src/client/theme/global.css +94 -1
- package/src/client/types.ts +1 -0
- package/src/mcp/server.ts +13 -3
- package/src/server/agent-presence.ts +43 -23
- package/src/server/ax-flow-loop.ts +6 -0
- package/src/server/ax-gate-ttl.ts +8 -1
- package/src/server/ax-interaction.ts +16 -10
- package/src/server/ax-state-manager.ts +17 -4
- package/src/server/ax-wait.ts +20 -6
- package/src/server/board-export.ts +19 -6
- package/src/server/board-local.ts +20 -0
- package/src/server/board-move.ts +297 -0
- package/src/server/canvas-db.ts +249 -158
- package/src/server/canvas-operations.ts +139 -109
- package/src/server/canvas-schema.ts +21 -0
- package/src/server/canvas-serialization.ts +2 -0
- package/src/server/canvas-state.ts +792 -159
- package/src/server/card-relocation.ts +53 -0
- package/src/server/code-graph.ts +4 -3
- package/src/server/context-reads.ts +83 -55
- package/src/server/demo-state.json +35 -15
- package/src/server/file-watcher.ts +30 -10
- package/src/server/human-presence.ts +18 -5
- package/src/server/index.ts +35 -15
- package/src/server/intent-registry.ts +43 -5
- package/src/server/knowledge-lifecycle.ts +32 -9
- package/src/server/lifecycle-status.ts +29 -1
- package/src/server/mcp-app-runtime.ts +26 -7
- package/src/server/mutation-history.ts +37 -8
- package/src/server/operations/composites.ts +6 -3
- package/src/server/operations/mcp-contracts.ts +15 -0
- package/src/server/operations/mcp.ts +36 -9
- package/src/server/operations/ops/annotation.ts +22 -1
- package/src/server/operations/ops/ax-await.ts +1 -0
- package/src/server/operations/ops/ax-timeline.ts +28 -2
- package/src/server/operations/ops/batch.ts +25 -11
- package/src/server/operations/ops/boards.ts +44 -3
- package/src/server/operations/ops/nodes.ts +70 -0
- package/src/server/operations/ops/query.ts +32 -3
- package/src/server/operations/ops/snapshots.ts +1 -0
- package/src/server/operations/registry.ts +49 -4
- package/src/server/server.ts +62 -16
- package/src/server/tour-control.ts +37 -12
- package/src/server/trace-manager.ts +4 -0
- package/src/server/web-artifacts/scripts/init-artifact.sh +2 -2
- package/src/shared/agent-context.ts +7 -1
- package/src/shared/auto-arrange.ts +5 -4
- package/src/shared/ax-kinds.ts +13 -0
- package/src/shared/ax-surface-protocol.ts +22 -0
- package/src/shared/human-presence.ts +1 -0
- package/src/shared/lifecycle-status.ts +2 -0
- package/dist/canvas/chunks/architecture-TIHT7OUA-9tffvpsv.js +0 -1
- package/dist/canvas/chunks/classDiagram-JCYQIIEL-nc6g5x1r.js +0 -1
- package/dist/canvas/chunks/classDiagram-v2-OCEON4UE-0qnbpwvh.js +0 -1
- package/dist/canvas/chunks/cynefin-VYW2F7L2-ys8p46ts.js +0 -1
- package/dist/canvas/chunks/eventmodeling-45OFAUF4-v9q6dn4c.js +0 -1
- package/dist/canvas/chunks/flowDiagram-UKHOOZJN-yrr5z8tc.js +0 -1
- package/dist/canvas/chunks/gitGraph-TEB2WS4Q-kdwb8m0a.js +0 -1
- package/dist/canvas/chunks/index-1r72hdzb.js +0 -301
- package/dist/canvas/chunks/index-g3pwnqeb.js +0 -2
- package/dist/canvas/chunks/index-rft2hcz5.js +0 -2
- package/dist/canvas/chunks/info-DKCQHKI2-wpzs6sz3.js +0 -1
- package/dist/canvas/chunks/ishikawaDiagram-WSZJBQD7-5hnq1a9p.js +0 -70
- package/dist/canvas/chunks/packet-7NZHBO7P-xn439f0s.js +0 -1
- package/dist/canvas/chunks/pie-RZYD4A2V-0kf16rwh.js +0 -1
- package/dist/canvas/chunks/radar-I7S5WNFK-0g2pz36t.js +0 -1
- package/dist/canvas/chunks/railroad-3IZDKUUU-fcaran6j.js +0 -1
- package/dist/canvas/chunks/railroad-abnf-AHOZXSZD-wgdya41q.js +0 -1
- package/dist/canvas/chunks/railroad-ebnf-EBAXGLYW-gy07a4ka.js +0 -1
- package/dist/canvas/chunks/railroad-peg-LSFZ7HO6-ewk6wtkh.js +0 -1
- package/dist/canvas/chunks/stateDiagram-v2-UEYNNEHI-k8sfp2e7.js +0 -1
- package/dist/canvas/chunks/swimlanesDiagram-ULZ7WXOC-nqysw67j.js +0 -8
- package/dist/canvas/chunks/treeView-QDETBFTQ-cesw6gp0.js +0 -1
- package/dist/canvas/chunks/treemap-6X3UGDF4-zdv4knp9.js +0 -1
- package/dist/canvas/chunks/vennDiagram-T6HMQDX7-x8dqn9yf.js +0 -34
- package/dist/canvas/chunks/wardley-OPB4EBWU-44pdddez.js +0 -1
|
@@ -12,7 +12,17 @@
|
|
|
12
12
|
* the one-shot boot migration into SQLite was retired.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
15
16
|
import { createHash, randomUUID } from 'node:crypto';
|
|
17
|
+
import {
|
|
18
|
+
prepareBoardMove,
|
|
19
|
+
type BoardMoveInput,
|
|
20
|
+
type BoardMovePreview,
|
|
21
|
+
type BoardMoveResult,
|
|
22
|
+
type MoveBoard,
|
|
23
|
+
} from './board-move.js';
|
|
24
|
+
import { boardMoveBoundary, recordCardRelocation } from './card-relocation.js';
|
|
25
|
+
import { mutationHistory } from './mutation-history.js';
|
|
16
26
|
import {
|
|
17
27
|
DEFAULT_WORKSPACE_SETTINGS,
|
|
18
28
|
workspaceSettingsPatchSchema,
|
|
@@ -43,6 +53,8 @@ import {
|
|
|
43
53
|
openCanvasDb,
|
|
44
54
|
saveStateToDB,
|
|
45
55
|
loadStateFromDB,
|
|
56
|
+
boardStorageVersion,
|
|
57
|
+
assertBoardStorageVersion,
|
|
46
58
|
saveSnapshotToDB,
|
|
47
59
|
loadSnapshotFromDB,
|
|
48
60
|
listSnapshotsFromDB,
|
|
@@ -455,33 +467,232 @@ function isPersistedBlobRef(value: unknown): value is PersistedBlobRef {
|
|
|
455
467
|
);
|
|
456
468
|
}
|
|
457
469
|
|
|
470
|
+
/** The mutable, board-owned partition. Workspace services remain on the manager. */
|
|
471
|
+
interface LiveBoardState {
|
|
472
|
+
id: string | null;
|
|
473
|
+
disposed: boolean;
|
|
474
|
+
cleanup: Array<() => void>;
|
|
475
|
+
nodes: Map<string, CanvasNodeState>;
|
|
476
|
+
edges: Map<string, CanvasEdge>;
|
|
477
|
+
annotations: Map<string, CanvasAnnotation>;
|
|
478
|
+
viewport: ViewportState;
|
|
479
|
+
tour: Tour | undefined;
|
|
480
|
+
contextPinnedNodeIds: Set<string>;
|
|
481
|
+
contextPinMeta: Map<string, ContextPinMeta>;
|
|
482
|
+
storageVersion: number;
|
|
483
|
+
contentRevision: number;
|
|
484
|
+
revisionFloor: number;
|
|
485
|
+
deletionTombstones: NodeDeletionTombstone[];
|
|
486
|
+
generation: number;
|
|
487
|
+
saveTimer: ReturnType<typeof setTimeout> | null;
|
|
488
|
+
persistenceError: { message: string; at: string } | null;
|
|
489
|
+
ax: AxStateManager;
|
|
490
|
+
}
|
|
491
|
+
|
|
458
492
|
class CanvasStateManager {
|
|
459
|
-
private
|
|
460
|
-
private
|
|
461
|
-
private
|
|
462
|
-
private
|
|
463
|
-
private
|
|
493
|
+
private readonly boardExecution = new AsyncLocalStorage<LiveBoardState>();
|
|
494
|
+
private readonly liveBoards = new Map<string, LiveBoardState>();
|
|
495
|
+
private nextGeneration = 0;
|
|
496
|
+
private navigationGeneration = 0;
|
|
497
|
+
private readonly hostState: { capability: PmxAxHostCapability | null } = { capability: null };
|
|
498
|
+
private visibleBoard = this.createBoardState();
|
|
499
|
+
|
|
500
|
+
private get boardState(): LiveBoardState {
|
|
501
|
+
const board = this.boardExecution.getStore() ?? this.visibleBoard;
|
|
502
|
+
if (board.disposed) throw new OperationError('The target board was deleted or the workspace was replaced.', 409);
|
|
503
|
+
return board;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** Identity for board-owned runtime caches. Never shared by a replacement board. */
|
|
507
|
+
get boardIdentity(): object {
|
|
508
|
+
return this.boardState;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
onBoardDispose(cleanup: () => void): () => void {
|
|
512
|
+
const board = this.boardState;
|
|
513
|
+
board.cleanup.push(cleanup);
|
|
514
|
+
return () => {
|
|
515
|
+
const index = board.cleanup.indexOf(cleanup);
|
|
516
|
+
if (index >= 0) board.cleanup.splice(index, 1);
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
private disposeBoardRuntime(board: LiveBoardState): void {
|
|
521
|
+
for (const cleanup of board.cleanup.splice(0)) cleanup();
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/** The human's navigation target, independent of the calling operation's board. */
|
|
525
|
+
get visibleBoardId(): string | null {
|
|
526
|
+
return this.visibleBoard.id;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
get visibleBoardGeneration(): number {
|
|
530
|
+
return this.navigationGeneration;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
get isBackgroundBoard(): boolean {
|
|
534
|
+
return this.boardState !== this.visibleBoard;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/** Execute on a live board without activating it, retaining ownership across awaits. */
|
|
538
|
+
withBoard<T>(id: string, fn: () => T): T {
|
|
539
|
+
const board = this.loadBoardState(id);
|
|
540
|
+
return this.boardExecution.run(board, fn);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/** Bind callbacks to their board, rather than the context of whoever invokes them. */
|
|
544
|
+
bindBoard<T extends unknown[], R>(fn: (...args: T) => R): (...args: T) => R {
|
|
545
|
+
const board = this.boardState;
|
|
546
|
+
return (...args) => this.boardExecution.run(board, () => fn(...args));
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/** Viewer/global projections must not inherit the producer's board scope. */
|
|
550
|
+
withVisibleBoard<T>(fn: () => T): T {
|
|
551
|
+
return this.boardExecution.exit(fn);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/** Sweep resident runtime owners without loading the entire stored library. */
|
|
555
|
+
forEachLiveBoard(fn: () => void): void {
|
|
556
|
+
for (const board of new Set([this.visibleBoard, ...this.liveBoards.values()])) {
|
|
557
|
+
this.boardExecution.run(board, fn);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
private loadBoardState(id: string): LiveBoardState {
|
|
562
|
+
const cached = this.liveBoards.get(id);
|
|
563
|
+
if (cached && this._db && cached.storageVersion === boardStorageVersion(this._db, id)) return cached;
|
|
564
|
+
if (cached?.saveTimer || cached?.persistenceError)
|
|
565
|
+
throw new OperationError(
|
|
566
|
+
`Board "${id}" changed in another writer. Reconcile unsaved edits before continuing.`,
|
|
567
|
+
409,
|
|
568
|
+
);
|
|
569
|
+
const state = this._db ? this.readProjectedBoard(id) : null;
|
|
570
|
+
if (!state) throw new OperationError(`Board "${id}" not found.`, 404);
|
|
571
|
+
const board = cached ?? this.createBoardState();
|
|
572
|
+
board.id = id;
|
|
573
|
+
this.boardExecution.run(board, () => {
|
|
574
|
+
this.initializePersistedWikiLinks(state, id);
|
|
575
|
+
this.applyPersistedState({ ...state, theme: this._theme });
|
|
576
|
+
});
|
|
577
|
+
this.liveBoards.set(id, board);
|
|
578
|
+
return board;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
private createBoardState(): LiveBoardState {
|
|
582
|
+
return {
|
|
583
|
+
id: null,
|
|
584
|
+
disposed: false,
|
|
585
|
+
cleanup: [],
|
|
586
|
+
nodes: new Map(),
|
|
587
|
+
edges: new Map(),
|
|
588
|
+
annotations: new Map(),
|
|
589
|
+
viewport: { x: 0, y: 0, scale: 1 },
|
|
590
|
+
tour: undefined,
|
|
591
|
+
contextPinnedNodeIds: new Set(),
|
|
592
|
+
contextPinMeta: new Map(),
|
|
593
|
+
storageVersion: 0,
|
|
594
|
+
contentRevision: 0,
|
|
595
|
+
revisionFloor: 0,
|
|
596
|
+
deletionTombstones: [],
|
|
597
|
+
generation: ++this.nextGeneration,
|
|
598
|
+
saveTimer: null,
|
|
599
|
+
persistenceError: null,
|
|
600
|
+
ax: new AxStateManager(
|
|
601
|
+
{
|
|
602
|
+
getNodeIds: () => this.currentNodeIdSet(),
|
|
603
|
+
getDb: () => this._db,
|
|
604
|
+
scheduleSave: () => this.scheduleSave(),
|
|
605
|
+
notifyChange: (type) => this.notifyChange(type),
|
|
606
|
+
recordMutation: (info) => this.recordMutation(info),
|
|
607
|
+
suppressed: (fn) => this.suppressed(fn),
|
|
608
|
+
},
|
|
609
|
+
this.hostState,
|
|
610
|
+
),
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
private get nodes() {
|
|
615
|
+
return this.boardState.nodes;
|
|
616
|
+
}
|
|
617
|
+
private get edges() {
|
|
618
|
+
return this.boardState.edges;
|
|
619
|
+
}
|
|
620
|
+
private get annotations() {
|
|
621
|
+
return this.boardState.annotations;
|
|
622
|
+
}
|
|
623
|
+
private get _viewport() {
|
|
624
|
+
return this.boardState.viewport;
|
|
625
|
+
}
|
|
626
|
+
private set _viewport(value: ViewportState) {
|
|
627
|
+
this.boardState.viewport = value;
|
|
628
|
+
}
|
|
629
|
+
private get _tour() {
|
|
630
|
+
return this.boardState.tour;
|
|
631
|
+
}
|
|
632
|
+
private set _tour(value: Tour | undefined) {
|
|
633
|
+
this.boardState.tour = value;
|
|
634
|
+
}
|
|
635
|
+
private get _contextPinnedNodeIds() {
|
|
636
|
+
return this.boardState.contextPinnedNodeIds;
|
|
637
|
+
}
|
|
638
|
+
private set _contextPinnedNodeIds(value: Set<string>) {
|
|
639
|
+
this.boardState.contextPinnedNodeIds = value;
|
|
640
|
+
}
|
|
641
|
+
private get _contextPinMeta() {
|
|
642
|
+
return this.boardState.contextPinMeta;
|
|
643
|
+
}
|
|
644
|
+
private set _contextPinMeta(value: Map<string, ContextPinMeta>) {
|
|
645
|
+
this.boardState.contextPinMeta = value;
|
|
646
|
+
}
|
|
647
|
+
private get _storageVersion() {
|
|
648
|
+
return this.boardState.storageVersion;
|
|
649
|
+
}
|
|
650
|
+
private set _storageVersion(value: number) {
|
|
651
|
+
this.boardState.storageVersion = value;
|
|
652
|
+
}
|
|
653
|
+
private get _contentRevision() {
|
|
654
|
+
return this.boardState.contentRevision;
|
|
655
|
+
}
|
|
656
|
+
private set _contentRevision(value: number) {
|
|
657
|
+
this.boardState.contentRevision = value;
|
|
658
|
+
}
|
|
659
|
+
private get _revisionFloor() {
|
|
660
|
+
return this.boardState.revisionFloor;
|
|
661
|
+
}
|
|
662
|
+
private set _revisionFloor(value: number) {
|
|
663
|
+
this.boardState.revisionFloor = value;
|
|
664
|
+
}
|
|
665
|
+
private get _deletionTombstones() {
|
|
666
|
+
return this.boardState.deletionTombstones;
|
|
667
|
+
}
|
|
668
|
+
private set _deletionTombstones(value: NodeDeletionTombstone[]) {
|
|
669
|
+
this.boardState.deletionTombstones = value;
|
|
670
|
+
}
|
|
671
|
+
private get _boardGeneration() {
|
|
672
|
+
return this.boardState.generation;
|
|
673
|
+
}
|
|
674
|
+
private set _boardGeneration(value: number) {
|
|
675
|
+
this.boardState.generation = value;
|
|
676
|
+
}
|
|
677
|
+
private get _saveTimer() {
|
|
678
|
+
return this.boardState.saveTimer;
|
|
679
|
+
}
|
|
680
|
+
private set _saveTimer(value: ReturnType<typeof setTimeout> | null) {
|
|
681
|
+
this.boardState.saveTimer = value;
|
|
682
|
+
}
|
|
683
|
+
private get _lastPersistenceError() {
|
|
684
|
+
return this.boardState.persistenceError;
|
|
685
|
+
}
|
|
686
|
+
private set _lastPersistenceError(value: LiveBoardState['persistenceError']) {
|
|
687
|
+
this.boardState.persistenceError = value;
|
|
688
|
+
}
|
|
689
|
+
private get ax() {
|
|
690
|
+
return this.boardState.ax;
|
|
691
|
+
}
|
|
692
|
+
|
|
464
693
|
private _theme: CanvasTheme = 'harbor';
|
|
465
|
-
private _contextPinnedNodeIds = new Set<string>();
|
|
466
|
-
/** Who pinned each pin, when and why; kept only for ids in `_contextPinnedNodeIds`. */
|
|
467
|
-
private _contextPinMeta = new Map<string, ContextPinMeta>();
|
|
468
694
|
private _workspaceRoot = process.cwd();
|
|
469
|
-
private
|
|
470
|
-
private _revisionFloor = 0;
|
|
471
|
-
private _deletionTombstones: NodeDeletionTombstone[] = [];
|
|
472
|
-
|
|
473
|
-
// ── AX state (canvas-bound + timeline + host partitions) ──────────
|
|
474
|
-
// Extracted into a dedicated manager (plan-007 Slice A). CanvasStateManager
|
|
475
|
-
// holds it and delegates its public AX methods so the SDK/HTTP/MCP surface is
|
|
476
|
-
// byte-stable; the manager receives the host hooks it needs as injected deps.
|
|
477
|
-
private readonly ax = new AxStateManager({
|
|
478
|
-
getNodeIds: () => this.currentNodeIdSet(),
|
|
479
|
-
getDb: () => this._db,
|
|
480
|
-
scheduleSave: () => this.scheduleSave(),
|
|
481
|
-
notifyChange: (type) => this.notifyChange(type),
|
|
482
|
-
recordMutation: (info) => this.recordMutation(info),
|
|
483
|
-
suppressed: (fn) => this.suppressed(fn),
|
|
484
|
-
});
|
|
695
|
+
private moveRuntimeBlockers: () => ReadonlyMap<string, string> = () => new Map();
|
|
485
696
|
|
|
486
697
|
// ── Change listeners (for MCP resource notifications) ──────
|
|
487
698
|
private _changeListeners: ((type: CanvasChangeType) => void)[] = [];
|
|
@@ -493,9 +704,15 @@ class CanvasStateManager {
|
|
|
493
704
|
* — e.g. the long-lived MCP subscription — may ignore the return value).
|
|
494
705
|
*/
|
|
495
706
|
onChange(cb: (type: CanvasChangeType) => void): () => void {
|
|
496
|
-
this.
|
|
707
|
+
const owner = this.boardExecution.getStore();
|
|
708
|
+
const listener = owner
|
|
709
|
+
? (type: CanvasChangeType) => {
|
|
710
|
+
if (owner === this.boardState) this.boardExecution.run(owner, () => cb(type));
|
|
711
|
+
}
|
|
712
|
+
: cb;
|
|
713
|
+
this._changeListeners.push(listener);
|
|
497
714
|
return () => {
|
|
498
|
-
const i = this._changeListeners.indexOf(
|
|
715
|
+
const i = this._changeListeners.indexOf(listener);
|
|
499
716
|
if (i >= 0) this._changeListeners.splice(i, 1);
|
|
500
717
|
};
|
|
501
718
|
}
|
|
@@ -529,6 +746,7 @@ class CanvasStateManager {
|
|
|
529
746
|
}
|
|
530
747
|
|
|
531
748
|
private notifyChange(type: CanvasChangeType): void {
|
|
749
|
+
if (type === 'ax') this.refreshLiveWorkCards();
|
|
532
750
|
// Iterate a snapshot: a listener (e.g. a blocking-wait via onChange) may dispose
|
|
533
751
|
// itself synchronously here, and splicing the live array mid-iteration would skip
|
|
534
752
|
// the next listener for this notification.
|
|
@@ -641,8 +859,29 @@ class CanvasStateManager {
|
|
|
641
859
|
return { group, childIds, children };
|
|
642
860
|
}
|
|
643
861
|
|
|
644
|
-
private normalizeNode(
|
|
862
|
+
private normalizeNode(
|
|
863
|
+
node: CanvasNodeState,
|
|
864
|
+
workItems?: PmxAxWorkItem[],
|
|
865
|
+
options: { boardId?: string; projectWork?: boolean } = {},
|
|
866
|
+
): CanvasNodeState {
|
|
645
867
|
const data = { ...node.data };
|
|
868
|
+
// These persisted fields are a derived display cache, never a second task record.
|
|
869
|
+
if (options.projectWork !== false && node.type === 'status' && typeof data.workItemId === 'string') {
|
|
870
|
+
const tasks =
|
|
871
|
+
typeof data.workItemBoardId === 'string' && data.workItemBoardId !== options.boardId
|
|
872
|
+
? this.getWorkItemsOnBoard(data.workItemBoardId)
|
|
873
|
+
: (workItems ?? this.getWorkItems());
|
|
874
|
+
const task = tasks.find((item) => item.id === data.workItemId);
|
|
875
|
+
data.title = task?.title ?? 'Task unavailable';
|
|
876
|
+
data.detail = task?.detail ?? '';
|
|
877
|
+
data.liveWorkAvailable = !!task;
|
|
878
|
+
data.liveWorkSource = task?.agentId ?? task?.source ?? '';
|
|
879
|
+
delete data.progress;
|
|
880
|
+
delete data.content;
|
|
881
|
+
delete data.axWorkStatus;
|
|
882
|
+
if (task) data.workState = task.status;
|
|
883
|
+
else delete data.workState;
|
|
884
|
+
}
|
|
646
885
|
// Lifecycle is optional metadata, never a card taxonomy or validation clock.
|
|
647
886
|
delete data.categoryId;
|
|
648
887
|
if (data.contentOrigin !== undefined && data.contentOrigin !== 'authored' && data.contentOrigin !== 'imported') {
|
|
@@ -666,6 +905,104 @@ class CanvasStateManager {
|
|
|
666
905
|
};
|
|
667
906
|
}
|
|
668
907
|
|
|
908
|
+
private refreshLiveWorkCards(): void {
|
|
909
|
+
const owner = this.boardState;
|
|
910
|
+
this.forEachLiveBoard(() => {
|
|
911
|
+
// Derived cache refreshes must not turn a clean stale board into an unsaved conflict.
|
|
912
|
+
if (
|
|
913
|
+
this.boardState !== owner &&
|
|
914
|
+
this._db &&
|
|
915
|
+
this._activeBoardId &&
|
|
916
|
+
!this._saveTimer &&
|
|
917
|
+
!this._lastPersistenceError &&
|
|
918
|
+
this._storageVersion !== boardStorageVersion(this._db, this._activeBoardId)
|
|
919
|
+
)
|
|
920
|
+
return;
|
|
921
|
+
this.refreshCurrentLiveWorkCards();
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
private refreshCurrentLiveWorkCards(preserveAttribution = false): void {
|
|
926
|
+
this.withSuppressedRecording(() => {
|
|
927
|
+
const tasks = this.getWorkItems();
|
|
928
|
+
for (const node of this.nodes.values()) {
|
|
929
|
+
if (node.type !== 'status' || typeof node.data.workItemId !== 'string') continue;
|
|
930
|
+
const next = this.normalizeNode(node, tasks);
|
|
931
|
+
if (!this.nodeContentChanged(node, next)) continue;
|
|
932
|
+
if (preserveAttribution) {
|
|
933
|
+
delete next.data.agentSummary;
|
|
934
|
+
delete next.data.summary;
|
|
935
|
+
this.nodes.set(node.id, {
|
|
936
|
+
...next,
|
|
937
|
+
contentRevision: this.nextContentRevision(),
|
|
938
|
+
contentChangedAt: new Date().toISOString(),
|
|
939
|
+
});
|
|
940
|
+
this.scheduleSave();
|
|
941
|
+
} else this.updateNode(node.id, { data: next.data });
|
|
942
|
+
}
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/** Reconcile a stored display cache and its cursor clock in the same SQLite transaction. */
|
|
947
|
+
private readProjectedBoard(boardId: string): PersistedCanvasState | null {
|
|
948
|
+
const db = this._db!;
|
|
949
|
+
// Validation inside another write must not change its captured versions or
|
|
950
|
+
// publish resident clocks before that outer transaction commits.
|
|
951
|
+
if (db.inTransaction) return loadStateFromDB(db, boardId);
|
|
952
|
+
const projected: CanvasNodeState[] = [];
|
|
953
|
+
let previousVersion = 0;
|
|
954
|
+
const state = db
|
|
955
|
+
.transaction(() => {
|
|
956
|
+
const state = loadStateFromDB(db, boardId);
|
|
957
|
+
if (!state) return null;
|
|
958
|
+
previousVersion = state.storageVersion ?? 0;
|
|
959
|
+
state.revisionState ??= { revision: 0, floor: 0, tombstones: [] };
|
|
960
|
+
const revision = state.revisionState;
|
|
961
|
+
state.nodes = state.nodes.map((node) => {
|
|
962
|
+
if (node.type !== 'status' || typeof node.data.workItemId !== 'string') return node;
|
|
963
|
+
const next = this.normalizeNode(node, state.ax?.workItems ?? [], { boardId });
|
|
964
|
+
if (!this.nodeContentChanged(node, next)) return node;
|
|
965
|
+
delete next.data.agentSummary;
|
|
966
|
+
delete next.data.summary;
|
|
967
|
+
next.contentRevision = ++revision.revision;
|
|
968
|
+
next.contentChangedAt = new Date().toISOString();
|
|
969
|
+
projected.push(next);
|
|
970
|
+
return next;
|
|
971
|
+
});
|
|
972
|
+
if (projected.length) state.storageVersion = saveStateToDB(db, boardId, state);
|
|
973
|
+
return state;
|
|
974
|
+
})
|
|
975
|
+
.immediate();
|
|
976
|
+
const live = this.liveBoards.get(boardId);
|
|
977
|
+
if (
|
|
978
|
+
state &&
|
|
979
|
+
projected.length &&
|
|
980
|
+
live &&
|
|
981
|
+
!live.saveTimer &&
|
|
982
|
+
!live.persistenceError &&
|
|
983
|
+
live.storageVersion === previousVersion
|
|
984
|
+
) {
|
|
985
|
+
// Only the derived cache changed. Do not reinstall AX or renew approval TTLs.
|
|
986
|
+
this.boardExecution.run(live, () => {
|
|
987
|
+
for (const node of projected) this.nodes.set(node.id, structuredClone(node));
|
|
988
|
+
this._contentRevision = state.revisionState!.revision;
|
|
989
|
+
this._storageVersion = state.storageVersion!;
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
return state;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/** Call before capturing a context snapshot, never from per-node getters used during writes. */
|
|
996
|
+
refreshContextWorkCards(): void {
|
|
997
|
+
if (!this._db || !this._activeBoardId) return;
|
|
998
|
+
if (this._saveTimer || this._lastPersistenceError) {
|
|
999
|
+
this.refreshCurrentLiveWorkCards(true);
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
const state = this.readProjectedBoard(this._activeBoardId);
|
|
1003
|
+
if (state && state.storageVersion !== this._storageVersion) this.applyPersistedState(state, false);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
669
1006
|
private nextContentRevision(): number {
|
|
670
1007
|
this._contentRevision += 1;
|
|
671
1008
|
return this._contentRevision;
|
|
@@ -706,7 +1043,9 @@ class CanvasStateManager {
|
|
|
706
1043
|
}
|
|
707
1044
|
|
|
708
1045
|
private nodeForRead(node: CanvasNodeState): CanvasNodeState {
|
|
709
|
-
const resolved = this.resolveNodeDataBlobs(
|
|
1046
|
+
const resolved = this.resolveNodeDataBlobs(
|
|
1047
|
+
node.type === 'status' && typeof node.data.workItemId === 'string' ? this.normalizeNode(node) : node,
|
|
1048
|
+
);
|
|
710
1049
|
return {
|
|
711
1050
|
...resolved,
|
|
712
1051
|
pinned: resolved.pinned || this._contextPinnedNodeIds.has(resolved.id),
|
|
@@ -809,7 +1148,6 @@ class CanvasStateManager {
|
|
|
809
1148
|
// ── Persistence ────────────────────────────────────────────
|
|
810
1149
|
private _db: import('bun:sqlite').Database | null = null;
|
|
811
1150
|
private _dbPath: string | null = null;
|
|
812
|
-
private _saveTimer: ReturnType<typeof setTimeout> | null = null;
|
|
813
1151
|
readonly knowledge = new KnowledgeLifecycle(
|
|
814
1152
|
() => this._db,
|
|
815
1153
|
() => this.notifyChange('nodes'),
|
|
@@ -1039,7 +1377,7 @@ class CanvasStateManager {
|
|
|
1039
1377
|
if (this._db) {
|
|
1040
1378
|
try {
|
|
1041
1379
|
const boardId = getActiveBoardIdFromDB(this._db);
|
|
1042
|
-
const state = boardId ?
|
|
1380
|
+
const state = boardId ? this.readProjectedBoard(boardId) : null;
|
|
1043
1381
|
if (boardId && state) {
|
|
1044
1382
|
this.initializePersistedWikiLinks(state, boardId);
|
|
1045
1383
|
this.applyPersistedState(state);
|
|
@@ -1130,15 +1468,24 @@ class CanvasStateManager {
|
|
|
1130
1468
|
|
|
1131
1469
|
// ── Boards ──────────────────────────────────────────────────
|
|
1132
1470
|
|
|
1133
|
-
private _activeBoardId: string | null
|
|
1134
|
-
|
|
1471
|
+
private get _activeBoardId(): string | null {
|
|
1472
|
+
return this.boardState.id;
|
|
1473
|
+
}
|
|
1474
|
+
private set _activeBoardId(id: string | null) {
|
|
1475
|
+
this.boardState.id = id;
|
|
1476
|
+
if (id) this.liveBoards.set(id, this.boardState);
|
|
1477
|
+
}
|
|
1135
1478
|
|
|
1136
1479
|
/** Invalidates asynchronous work whenever the loaded board state is replaced. */
|
|
1137
1480
|
get boardGeneration(): number {
|
|
1481
|
+
return this.boardExecution.getStore() ? this._boardGeneration : this.navigationGeneration;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
get boardLifetimeGeneration(): number {
|
|
1138
1485
|
return this._boardGeneration;
|
|
1139
1486
|
}
|
|
1140
1487
|
|
|
1141
|
-
/** The
|
|
1488
|
+
/** The execution board, or the open board outside a scoped operation. */
|
|
1142
1489
|
get activeBoardId(): string | null {
|
|
1143
1490
|
return this._activeBoardId;
|
|
1144
1491
|
}
|
|
@@ -1344,62 +1691,67 @@ class CanvasStateManager {
|
|
|
1344
1691
|
let before: PersistedCanvasState | undefined;
|
|
1345
1692
|
const activeLinkRevisions = new Map<string, number>();
|
|
1346
1693
|
let activeRevision = this._contentRevision;
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
// without publishing or changing the active board before commit.
|
|
1363
|
-
if (this._activeBoardId) saveStateToDB(db, this._activeBoardId, this.persistedState());
|
|
1364
|
-
const state = loadStateFromDB(db, boardId);
|
|
1365
|
-
if (!state) throw new OperationError(`Board "${boardId}" not found.`, 404);
|
|
1366
|
-
const policies = [this.getPolicy(), state.ax?.policy];
|
|
1367
|
-
if (policies.some((policy) => policy && (policy.scope || Object.keys(policy.agentScopes ?? {}).length)))
|
|
1368
|
-
throw new OperationError('Destination/retry creation is refused while either board has a scope fence.', 403);
|
|
1369
|
-
state.revisionState = {
|
|
1370
|
-
...state.revisionState,
|
|
1371
|
-
revision: state.revisionState?.revision ?? 0,
|
|
1372
|
-
floor: state.revisionState?.floor ?? 0,
|
|
1373
|
-
tombstones: state.revisionState?.tombstones ?? [],
|
|
1374
|
-
};
|
|
1375
|
-
before = structuredClone(state);
|
|
1376
|
-
const invalidations: Array<[string, string]> = [];
|
|
1377
|
-
let next = build(state, (targetBoardId, nodeId) => invalidations.push([targetBoardId, nodeId]));
|
|
1378
|
-
// Theme is workspace-global; never let an inactive projection overwrite it.
|
|
1379
|
-
state.theme = this._theme;
|
|
1380
|
-
saveStateToDB(db, boardId, state);
|
|
1381
|
-
const invalidate = (targetBoardId: string, targetNodeId: string) => {
|
|
1382
|
-
const revision = invalidateLinksInDB(db, targetBoardId, targetNodeId);
|
|
1383
|
-
if (targetBoardId === this._activeBoardId && revision !== undefined) {
|
|
1384
|
-
activeLinkRevisions.set(targetNodeId, revision);
|
|
1385
|
-
activeRevision = Math.max(activeRevision, revision);
|
|
1694
|
+
let activeStorageVersion = this._storageVersion;
|
|
1695
|
+
const result = db
|
|
1696
|
+
.transaction(() => {
|
|
1697
|
+
const previous = requestId
|
|
1698
|
+
? db
|
|
1699
|
+
.query<{ board_id: string; fingerprint: string; node: string }, [string]>(
|
|
1700
|
+
'SELECT board_id, fingerprint, node FROM node_creation_requests WHERE request_id = ?',
|
|
1701
|
+
)
|
|
1702
|
+
.get(requestId)
|
|
1703
|
+
: null;
|
|
1704
|
+
if (previous) {
|
|
1705
|
+
if (previous.board_id !== boardId || previous.fingerprint !== fingerprint)
|
|
1706
|
+
throw new OperationError('requestId was already used with different creation input or destination.', 409);
|
|
1707
|
+
// Return the original receipt even if the node has since been edited or removed.
|
|
1708
|
+
return JSON.parse(previous.node) as T;
|
|
1386
1709
|
}
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1710
|
+
// Include pending edits in catalog resolution and durable relation clocks,
|
|
1711
|
+
// without publishing or changing the active board before commit.
|
|
1712
|
+
this.stageLiveBoards(db);
|
|
1713
|
+
const state = loadStateFromDB(db, boardId);
|
|
1714
|
+
if (!state) throw new OperationError(`Board "${boardId}" not found.`, 404);
|
|
1715
|
+
const policies = [this.getPolicy(), state.ax?.policy];
|
|
1716
|
+
if (policies.some((policy) => policy && (policy.scope || Object.keys(policy.agentScopes ?? {}).length)))
|
|
1717
|
+
throw new OperationError('Destination/retry creation is refused while either board has a scope fence.', 403);
|
|
1718
|
+
state.revisionState = {
|
|
1719
|
+
...state.revisionState,
|
|
1720
|
+
revision: state.revisionState?.revision ?? 0,
|
|
1721
|
+
floor: state.revisionState?.floor ?? 0,
|
|
1722
|
+
tombstones: state.revisionState?.tombstones ?? [],
|
|
1723
|
+
};
|
|
1724
|
+
before = structuredClone(state);
|
|
1725
|
+
const invalidations: Array<[string, string]> = [];
|
|
1726
|
+
let next = build(state, (targetBoardId, nodeId) => invalidations.push([targetBoardId, nodeId]));
|
|
1727
|
+
// Theme is workspace-global; never let an inactive projection overwrite it.
|
|
1728
|
+
state.theme = this._theme;
|
|
1729
|
+
saveStateToDB(db, boardId, state);
|
|
1730
|
+
const invalidate = (targetBoardId: string, targetNodeId: string) => {
|
|
1731
|
+
const revision = invalidateLinksInDB(db, targetBoardId, targetNodeId);
|
|
1732
|
+
if (targetBoardId === this._activeBoardId && revision !== undefined) {
|
|
1733
|
+
activeLinkRevisions.set(targetNodeId, revision);
|
|
1734
|
+
activeRevision = Math.max(activeRevision, revision);
|
|
1735
|
+
}
|
|
1736
|
+
};
|
|
1737
|
+
for (const [targetBoardId, nodeId] of invalidations) invalidate(targetBoardId, nodeId);
|
|
1738
|
+
if ('data' in next) invalidateIncomingLinksInDB(db, boardId, next.id, invalidate);
|
|
1739
|
+
committed = loadStateFromDB(db, boardId)!;
|
|
1740
|
+
next = ([...committed.nodes, ...committed.edges].find((candidate) => candidate.id === next.id) ?? next) as T;
|
|
1741
|
+
if (active) activeRevision = committed.revisionState!.revision;
|
|
1742
|
+
if (this._activeBoardId) activeStorageVersion = boardStorageVersion(db, this._activeBoardId);
|
|
1743
|
+
if (requestId)
|
|
1744
|
+
db.run('INSERT INTO node_creation_requests VALUES (?, ?, ?, ?)', [
|
|
1745
|
+
requestId,
|
|
1746
|
+
boardId,
|
|
1747
|
+
fingerprint,
|
|
1748
|
+
JSON.stringify(next),
|
|
1749
|
+
]);
|
|
1750
|
+
return next;
|
|
1751
|
+
})
|
|
1752
|
+
.immediate();
|
|
1402
1753
|
if (committed) {
|
|
1754
|
+
this._storageVersion = activeStorageVersion;
|
|
1403
1755
|
if (active) {
|
|
1404
1756
|
const changed = committed.nodes.filter(
|
|
1405
1757
|
(node) => JSON.stringify(before!.nodes.find((old) => old.id === node.id)) !== JSON.stringify(node),
|
|
@@ -1443,16 +1795,48 @@ class CanvasStateManager {
|
|
|
1443
1795
|
}
|
|
1444
1796
|
this._contentRevision = activeRevision;
|
|
1445
1797
|
if (active || activeLinkRevisions.size) this.notifyChange('nodes');
|
|
1798
|
+
this.reconcileStoredBoards();
|
|
1446
1799
|
}
|
|
1447
1800
|
return result;
|
|
1448
1801
|
}
|
|
1449
1802
|
|
|
1803
|
+
/** Called inside a transaction: publish no storage clocks until it commits. */
|
|
1804
|
+
private stageLiveBoards(db: import('bun:sqlite').Database): void {
|
|
1805
|
+
for (const board of this.liveBoards.values()) {
|
|
1806
|
+
if (board !== this.boardState && !board.saveTimer && !board.persistenceError) continue;
|
|
1807
|
+
this.boardExecution.run(board, () => saveStateToDB(db, board.id!, this.persistedState()));
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
/** A stored multi-board write retains each live owner's runtime and camera. */
|
|
1812
|
+
private reconcileStoredBoards(ids: Iterable<string> = this.liveBoards.keys()): void {
|
|
1813
|
+
if (!this._db) return;
|
|
1814
|
+
for (const id of ids) {
|
|
1815
|
+
const board = this.liveBoards.get(id);
|
|
1816
|
+
if (!board) continue;
|
|
1817
|
+
if (board.storageVersion === boardStorageVersion(this._db, board.id!)) continue;
|
|
1818
|
+
const state = loadStateFromDB(this._db, board.id!)!;
|
|
1819
|
+
this.boardExecution.run(board, () => {
|
|
1820
|
+
if (board.saveTimer) clearTimeout(board.saveTimer);
|
|
1821
|
+
board.saveTimer = null;
|
|
1822
|
+
board.persistenceError = null;
|
|
1823
|
+
this.applyPersistedState(state, false);
|
|
1824
|
+
this.notifyChange('nodes');
|
|
1825
|
+
this.notifyChange('pins');
|
|
1826
|
+
this.notifyChange('ax');
|
|
1827
|
+
});
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1450
1831
|
/** Read a board without activating it. This projection is not a write payload. */
|
|
1451
1832
|
readBoard(
|
|
1452
1833
|
id: string,
|
|
1453
1834
|
includeBlobs = true,
|
|
1454
1835
|
): { board: CanvasBoard; state: PersistedCanvasState; layout: CanvasLayout } | null {
|
|
1455
1836
|
if (!this._db) return null;
|
|
1837
|
+
const live = this.liveBoards.get(id);
|
|
1838
|
+
if (id !== this._activeBoardId && live && (live.saveTimer || live.persistenceError))
|
|
1839
|
+
return this.boardExecution.run(live, () => this.readBoard(id, includeBlobs));
|
|
1456
1840
|
const board = this.listBoards().find((entry) => entry.id === id);
|
|
1457
1841
|
if (!board) return null;
|
|
1458
1842
|
if (id === this._activeBoardId) {
|
|
@@ -1466,6 +1850,7 @@ class CanvasStateManager {
|
|
|
1466
1850
|
board,
|
|
1467
1851
|
layout,
|
|
1468
1852
|
state: {
|
|
1853
|
+
storageVersion: this._storageVersion,
|
|
1469
1854
|
version: 1,
|
|
1470
1855
|
theme: this._theme,
|
|
1471
1856
|
tour: layout.tour,
|
|
@@ -1480,12 +1865,16 @@ class CanvasStateManager {
|
|
|
1480
1865
|
},
|
|
1481
1866
|
};
|
|
1482
1867
|
}
|
|
1483
|
-
const state =
|
|
1868
|
+
const state = this.readProjectedBoard(id);
|
|
1484
1869
|
if (!state) return null;
|
|
1485
1870
|
this.initializePersistedWikiLinks(state, id);
|
|
1486
1871
|
const pins = new Set(state.contextPins);
|
|
1487
1872
|
state.nodes = state.nodes.map((node) => ({
|
|
1488
|
-
...(
|
|
1873
|
+
...(node.type === 'status' && typeof node.data.workItemId === 'string'
|
|
1874
|
+
? this.normalizeNode(node, state.ax?.workItems ?? [], { boardId: id })
|
|
1875
|
+
: includeBlobs
|
|
1876
|
+
? this.resolveNodeDataBlobs(node)
|
|
1877
|
+
: node),
|
|
1489
1878
|
pinned: node.pinned || pins.has(node.id),
|
|
1490
1879
|
}));
|
|
1491
1880
|
return {
|
|
@@ -1505,9 +1894,10 @@ class CanvasStateManager {
|
|
|
1505
1894
|
/** Every board in the workspace, most recently opened first. */
|
|
1506
1895
|
listBoards(): CanvasBoard[] {
|
|
1507
1896
|
if (!this._db) return [];
|
|
1508
|
-
return listBoardsFromDB(this._db).map((board) =>
|
|
1509
|
-
|
|
1510
|
-
|
|
1897
|
+
return listBoardsFromDB(this._db).map((board) => {
|
|
1898
|
+
const live = this.liveBoards.get(board.id);
|
|
1899
|
+
return live ? { ...board, nodeCount: live.nodes.size } : board;
|
|
1900
|
+
});
|
|
1511
1901
|
}
|
|
1512
1902
|
|
|
1513
1903
|
storeAttachment(input: { boardId: string; name: string; mime: string; bytes: Uint8Array }): Attachment | null {
|
|
@@ -1550,10 +1940,15 @@ class CanvasStateManager {
|
|
|
1550
1940
|
clearTimeout(this._saveTimer);
|
|
1551
1941
|
this._saveTimer = null;
|
|
1552
1942
|
}
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1943
|
+
let storageVersion = this._storageVersion;
|
|
1944
|
+
const result = this._db
|
|
1945
|
+
.transaction(() => {
|
|
1946
|
+
if (this._activeBoardId) storageVersion = saveStateToDB(this._db!, this._activeBoardId, this.persistedState());
|
|
1947
|
+
return updateImport(this._db!, id, 'committed', { committedNodeIds: nodeIds });
|
|
1948
|
+
})
|
|
1949
|
+
.immediate();
|
|
1950
|
+
this._storageVersion = storageVersion;
|
|
1951
|
+
return result;
|
|
1557
1952
|
}
|
|
1558
1953
|
|
|
1559
1954
|
getContextBriefCursor(boardId: string, consumer: string): number | null {
|
|
@@ -1572,6 +1967,159 @@ class CanvasStateManager {
|
|
|
1572
1967
|
return createBoardInDB(this._db, name, folder);
|
|
1573
1968
|
}
|
|
1574
1969
|
|
|
1970
|
+
/** The host supplies runtime ownership, not another copy of board state. */
|
|
1971
|
+
setMoveRuntimeBlockers(read: () => ReadonlyMap<string, string>): void {
|
|
1972
|
+
this.moveRuntimeBlockers = read;
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
moveSelectionToNewBoard(
|
|
1976
|
+
input: BoardMoveInput,
|
|
1977
|
+
): { preview: BoardMovePreview } | { result: BoardMoveResult; replayed: boolean } {
|
|
1978
|
+
const db = this._db;
|
|
1979
|
+
if (!db) throw new OperationError('Moving cards requires persistent workspace storage.', 409);
|
|
1980
|
+
const name = input.name.trim();
|
|
1981
|
+
if (!name || name.length > 120) throw new OperationError('Board names must be 1–120 characters.');
|
|
1982
|
+
if (!input.requestId.trim() || input.requestId.length > 200)
|
|
1983
|
+
throw new OperationError('A move requires a requestId of 1–200 characters.');
|
|
1984
|
+
const normalized = { ...input, name, nodeIds: [...new Set(input.nodeIds)].sort() };
|
|
1985
|
+
const fingerprint = JSON.stringify({
|
|
1986
|
+
sourceBoardId: input.sourceBoardId,
|
|
1987
|
+
nodeIds: normalized.nodeIds,
|
|
1988
|
+
name,
|
|
1989
|
+
expectedRevision: input.expectedRevision,
|
|
1990
|
+
});
|
|
1991
|
+
let committed: PersistedCanvasState | undefined;
|
|
1992
|
+
const perform = () => {
|
|
1993
|
+
const previous = db
|
|
1994
|
+
.query<{ fingerprint: string; result: string }, [string]>(
|
|
1995
|
+
'SELECT fingerprint, result FROM board_moves WHERE request_id = ?',
|
|
1996
|
+
)
|
|
1997
|
+
.get(input.requestId);
|
|
1998
|
+
if (previous && !input.preview) {
|
|
1999
|
+
if (previous.fingerprint !== fingerprint)
|
|
2000
|
+
throw new OperationError('requestId was already used for a different move.', 409);
|
|
2001
|
+
return { result: JSON.parse(previous.result) as BoardMoveResult, replayed: true };
|
|
2002
|
+
}
|
|
2003
|
+
if (input.sourceBoardId !== this._activeBoardId)
|
|
2004
|
+
throw new OperationError('The source board is no longer open. Preview the move again.', 409);
|
|
2005
|
+
assertBoardStorageVersion(db, input.sourceBoardId, this._storageVersion);
|
|
2006
|
+
const library = new Map<string, MoveBoard>();
|
|
2007
|
+
for (const board of listBoardsFromDB(db)) {
|
|
2008
|
+
const live = this.liveBoards.get(board.id);
|
|
2009
|
+
const state =
|
|
2010
|
+
live && (board.id === input.sourceBoardId || live.saveTimer || live.persistenceError)
|
|
2011
|
+
? this.boardExecution.run(live, () => this.persistedState(false))
|
|
2012
|
+
: loadStateFromDB(db, board.id)!;
|
|
2013
|
+
state.nodes = state.nodes.map((node) => this.resolveNodeDataBlobs(node));
|
|
2014
|
+
library.set(board.id, { board, state });
|
|
2015
|
+
}
|
|
2016
|
+
const source = library.get(input.sourceBoardId)!;
|
|
2017
|
+
const destinationId = `board-${randomUUID()}`;
|
|
2018
|
+
const portalId = `board-portal-${randomUUID()}`;
|
|
2019
|
+
const at = new Date().toISOString();
|
|
2020
|
+
const plan = prepareBoardMove(
|
|
2021
|
+
normalized,
|
|
2022
|
+
library,
|
|
2023
|
+
destinationId,
|
|
2024
|
+
portalId,
|
|
2025
|
+
listImports(db, input.sourceBoardId),
|
|
2026
|
+
this.moveRuntimeBlockers(),
|
|
2027
|
+
currentActor(),
|
|
2028
|
+
at,
|
|
2029
|
+
);
|
|
2030
|
+
for (const id of plan.preview.affectedBoardIds) {
|
|
2031
|
+
const policy = library.get(id)!.state.ax?.policy;
|
|
2032
|
+
if (policy && (policy.scope || Object.keys(policy.agentScopes ?? {}).length))
|
|
2033
|
+
plan.preview.blockers.push(`${id}: clear the scope fence before relocating board-owned cards/references`);
|
|
2034
|
+
}
|
|
2035
|
+
for (const [id, state] of plan.states) {
|
|
2036
|
+
const original = library.get(id)?.state;
|
|
2037
|
+
if (!original) continue;
|
|
2038
|
+
const locked = humanPresence.lockedNodes(Date.now(), id);
|
|
2039
|
+
for (const node of original.nodes)
|
|
2040
|
+
if (
|
|
2041
|
+
locked.has(node.id) &&
|
|
2042
|
+
JSON.stringify(node) !== JSON.stringify(state.nodes.find((next) => next.id === node.id))
|
|
2043
|
+
)
|
|
2044
|
+
plan.preview.blockers.push(`${node.id}: being edited by ${locked.get(node.id)}`);
|
|
2045
|
+
}
|
|
2046
|
+
if (input.preview) return { preview: plan.preview };
|
|
2047
|
+
if (plan.preview.blockers.length)
|
|
2048
|
+
throw new OperationError('Move refused. Resolve the listed bindings, then preview again.', 409, {
|
|
2049
|
+
blockers: plan.preview.blockers,
|
|
2050
|
+
});
|
|
2051
|
+
db.run(
|
|
2052
|
+
'INSERT INTO boards (id, name, folder, created_at, relation_vocabulary, readme_node_id) VALUES (?, ?, ?, ?, ?, ?)',
|
|
2053
|
+
[
|
|
2054
|
+
destinationId,
|
|
2055
|
+
name,
|
|
2056
|
+
source.board.folder,
|
|
2057
|
+
at,
|
|
2058
|
+
JSON.stringify(source.board.relationVocabulary ?? null),
|
|
2059
|
+
plan.preview.readmeNodeId,
|
|
2060
|
+
],
|
|
2061
|
+
);
|
|
2062
|
+
for (const [id, state] of plan.states) {
|
|
2063
|
+
state.theme = this._theme;
|
|
2064
|
+
saveStateToDB(db, id, this.externalizePersistedStateBlobs(state));
|
|
2065
|
+
db.run(
|
|
2066
|
+
'INSERT INTO board_move_boundaries VALUES (?, ?) ON CONFLICT(board_id) DO UPDATE SET request_id = excluded.request_id',
|
|
2067
|
+
[id, input.requestId],
|
|
2068
|
+
);
|
|
2069
|
+
}
|
|
2070
|
+
if (plan.preview.readmeNodeId) updateBoardInDB(db, input.sourceBoardId, { readmeNodeId: null });
|
|
2071
|
+
for (const attachment of plan.preview.attachmentIds) {
|
|
2072
|
+
db.run('UPDATE attachments SET board_id = ? WHERE id = ? AND board_id = ?', [
|
|
2073
|
+
destinationId,
|
|
2074
|
+
attachment,
|
|
2075
|
+
input.sourceBoardId,
|
|
2076
|
+
]);
|
|
2077
|
+
db.run('UPDATE document_imports SET board_id = ? WHERE attachment_id = ? AND board_id = ?', [
|
|
2078
|
+
destinationId,
|
|
2079
|
+
attachment,
|
|
2080
|
+
input.sourceBoardId,
|
|
2081
|
+
]);
|
|
2082
|
+
}
|
|
2083
|
+
recordCardRelocation(db, input.requestId, input.sourceBoardId, destinationId, plan.preview.nodeIds);
|
|
2084
|
+
const result: BoardMoveResult = {
|
|
2085
|
+
sourceBoardId: input.sourceBoardId,
|
|
2086
|
+
destinationBoardId: destinationId,
|
|
2087
|
+
portalId,
|
|
2088
|
+
nodeIds: plan.preview.nodeIds,
|
|
2089
|
+
affectedBoardIds: [...plan.preview.affectedBoardIds, destinationId],
|
|
2090
|
+
};
|
|
2091
|
+
db.run('INSERT INTO board_moves VALUES (?, ?, ?, ?)', [input.requestId, fingerprint, JSON.stringify(result), at]);
|
|
2092
|
+
committed = loadStateFromDB(db, input.sourceBoardId)!;
|
|
2093
|
+
return { result, replayed: false };
|
|
2094
|
+
};
|
|
2095
|
+
const transaction = db.transaction(perform);
|
|
2096
|
+
const result = input.preview ? transaction() : transaction.immediate();
|
|
2097
|
+
if (committed) {
|
|
2098
|
+
// This is not a board activation: retain the camera, runtime partition and
|
|
2099
|
+
// mounted viewers. Only the committed nodes, relations and pins changed.
|
|
2100
|
+
const retained = new Set(committed.nodes.map((node) => node.id));
|
|
2101
|
+
const removed = [...this.nodes.keys()].filter((id) => !retained.has(id));
|
|
2102
|
+
this.nodes.clear();
|
|
2103
|
+
for (const node of committed.nodes) this.nodes.set(node.id, this.resolveNodeDataBlobs(node));
|
|
2104
|
+
this.edges.clear();
|
|
2105
|
+
for (const edge of committed.edges) this.edges.set(edge.id, edge);
|
|
2106
|
+
this._contextPinnedNodeIds = new Set(committed.contextPins);
|
|
2107
|
+
this._contextPinMeta = new Map(Object.entries(committed.contextPinMeta ?? {}));
|
|
2108
|
+
this._storageVersion = committed.storageVersion!;
|
|
2109
|
+
this._contentRevision = committed.revisionState!.revision;
|
|
2110
|
+
this._revisionFloor = committed.revisionState!.floor;
|
|
2111
|
+
this._deletionTombstones = [];
|
|
2112
|
+
for (const id of removed) this.ax.revalidateAfterNodeRemoval(id);
|
|
2113
|
+
mutationHistory.reset();
|
|
2114
|
+
this._lastPersistenceError = null;
|
|
2115
|
+
this.notifyChange('nodes');
|
|
2116
|
+
this.notifyChange('pins');
|
|
2117
|
+
this.notifyChange('ax');
|
|
2118
|
+
if (result.result) this.reconcileStoredBoards(result.result.affectedBoardIds);
|
|
2119
|
+
}
|
|
2120
|
+
return result;
|
|
2121
|
+
}
|
|
2122
|
+
|
|
1575
2123
|
updateBoard(
|
|
1576
2124
|
id: string,
|
|
1577
2125
|
patch: Pick<Parameters<typeof updateBoardInDB>[2], 'name' | 'folder' | 'mapPosition' | 'relationVocabulary'>,
|
|
@@ -1664,6 +2212,9 @@ class CanvasStateManager {
|
|
|
1664
2212
|
delete data.axStep;
|
|
1665
2213
|
delete data.axWorkStatus;
|
|
1666
2214
|
delete data.workItemId;
|
|
2215
|
+
delete data.workItemBoardId;
|
|
2216
|
+
delete data.liveWorkAvailable;
|
|
2217
|
+
delete data.liveWorkSource;
|
|
1667
2218
|
delete data.approvalGateId;
|
|
1668
2219
|
delete data.parentGroup;
|
|
1669
2220
|
if (typeof node.data.parentGroup === 'string' && ids.has(node.data.parentGroup)) {
|
|
@@ -1770,35 +2321,35 @@ class CanvasStateManager {
|
|
|
1770
2321
|
/** Deletes a board and its snapshots; deleting the open board returns to Home first. */
|
|
1771
2322
|
deleteBoard(id: string): boolean {
|
|
1772
2323
|
if (!this._db) return false;
|
|
1773
|
-
if (id === this.
|
|
2324
|
+
if (id === this.visibleBoard.id && !this.withVisibleBoard(() => this.switchBoard(null))) return false;
|
|
1774
2325
|
// Reverse relations must include outgoing links still debounced on the open board.
|
|
1775
|
-
|
|
1776
|
-
|
|
2326
|
+
this.flushToDisk();
|
|
2327
|
+
const removed = deleteBoardFromDB(this._db, id, (boardId, nodeId) =>
|
|
2328
|
+
this.invalidateRelationTarget(boardId, nodeId),
|
|
2329
|
+
);
|
|
2330
|
+
if (removed) {
|
|
2331
|
+
const board = this.liveBoards.get(id);
|
|
2332
|
+
if (board) {
|
|
2333
|
+
board.disposed = true;
|
|
2334
|
+
this.disposeBoardRuntime(board);
|
|
2335
|
+
}
|
|
2336
|
+
this.liveBoards.delete(id);
|
|
2337
|
+
}
|
|
2338
|
+
return removed;
|
|
1777
2339
|
}
|
|
1778
2340
|
|
|
1779
2341
|
/**
|
|
1780
|
-
* Save
|
|
1781
|
-
*
|
|
1782
|
-
* sessions, watchers, SSE) — see `openCanvasBoard` in canvas-operations.
|
|
2342
|
+
* Save pending boards, then select the visible owner. Existing partitions
|
|
2343
|
+
* remain live; canvas-operations starts the selected board's runtime/SSE.
|
|
1783
2344
|
*/
|
|
1784
2345
|
switchBoard(id: string | null): boolean {
|
|
1785
2346
|
if (!this._db) return false;
|
|
1786
|
-
if (this.
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
}
|
|
1793
|
-
let next = this.emptyPersistedState();
|
|
1794
|
-
if (id) {
|
|
1795
|
-
const state = loadStateFromDB(this._db, id);
|
|
1796
|
-
if (!state) return false;
|
|
1797
|
-
this.initializePersistedWikiLinks(state, id);
|
|
1798
|
-
next = state;
|
|
1799
|
-
}
|
|
1800
|
-
this.applyPersistedState(next);
|
|
1801
|
-
this._activeBoardId = id;
|
|
2347
|
+
if (this.boardExecution.getStore())
|
|
2348
|
+
throw new OperationError('Board-scoped operations cannot change navigation.', 409);
|
|
2349
|
+
if (id && !getBoardFromDB(this._db, id)) return false;
|
|
2350
|
+
this.flushToDisk();
|
|
2351
|
+
this.visibleBoard = id ? this.loadBoardState(id) : this.createBoardState();
|
|
2352
|
+
this.navigationGeneration = ++this.nextGeneration;
|
|
1802
2353
|
setActiveBoardIdInDB(this._db, id);
|
|
1803
2354
|
this.notifyChange('nodes');
|
|
1804
2355
|
this.notifyChange('pins');
|
|
@@ -1836,20 +2387,27 @@ class CanvasStateManager {
|
|
|
1836
2387
|
private scheduleSave(): void {
|
|
1837
2388
|
if (!this._db) return;
|
|
1838
2389
|
if (this._saveTimer) clearTimeout(this._saveTimer);
|
|
1839
|
-
this._saveTimer = setTimeout(
|
|
1840
|
-
this.
|
|
1841
|
-
|
|
1842
|
-
|
|
2390
|
+
this._saveTimer = setTimeout(
|
|
2391
|
+
this.bindBoard(() => {
|
|
2392
|
+
this._saveTimer = null;
|
|
2393
|
+
this.saveToDisk();
|
|
2394
|
+
}),
|
|
2395
|
+
SAVE_DEBOUNCE_MS,
|
|
2396
|
+
);
|
|
1843
2397
|
}
|
|
1844
2398
|
|
|
1845
2399
|
flushToDisk(): void {
|
|
1846
2400
|
if (!this._db) return;
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
2401
|
+
const current = this.boardState;
|
|
2402
|
+
for (const board of new Set([this.visibleBoard, ...this.liveBoards.values()])) {
|
|
2403
|
+
if (board !== current && !board.saveTimer && !board.persistenceError) continue;
|
|
2404
|
+
this.boardExecution.run(board, () => {
|
|
2405
|
+
if (this._saveTimer) {
|
|
2406
|
+
clearTimeout(this._saveTimer);
|
|
2407
|
+
this._saveTimer = null;
|
|
2408
|
+
}
|
|
2409
|
+
if (!this.saveToDisk()) throw new Error(this._lastPersistenceError?.message ?? 'Failed to save canvas state.');
|
|
2410
|
+
});
|
|
1853
2411
|
}
|
|
1854
2412
|
if (this._db) {
|
|
1855
2413
|
try {
|
|
@@ -1861,8 +2419,9 @@ class CanvasStateManager {
|
|
|
1861
2419
|
}
|
|
1862
2420
|
}
|
|
1863
2421
|
|
|
1864
|
-
private persistedState(): PersistedCanvasState {
|
|
1865
|
-
|
|
2422
|
+
private persistedState(externalize = true): PersistedCanvasState {
|
|
2423
|
+
const state: PersistedCanvasState = {
|
|
2424
|
+
storageVersion: this._storageVersion,
|
|
1866
2425
|
version: 1,
|
|
1867
2426
|
theme: this._theme,
|
|
1868
2427
|
tour: this._tour,
|
|
@@ -1874,7 +2433,8 @@ class CanvasStateManager {
|
|
|
1874
2433
|
contextPinMeta: this.pinMetaRecord(),
|
|
1875
2434
|
ax: this.getAxState(),
|
|
1876
2435
|
revisionState: this.revisionState(),
|
|
1877
|
-
}
|
|
2436
|
+
};
|
|
2437
|
+
return externalize ? this.externalizePersistedStateBlobs(state) : state;
|
|
1878
2438
|
}
|
|
1879
2439
|
|
|
1880
2440
|
/** Write current state to SQLite immediately. */
|
|
@@ -1888,7 +2448,7 @@ class CanvasStateManager {
|
|
|
1888
2448
|
}
|
|
1889
2449
|
const boardId = this.ensureActiveBoard();
|
|
1890
2450
|
if (!boardId) return false;
|
|
1891
|
-
saveStateToDB(this._db, boardId, this.persistedState());
|
|
2451
|
+
this._storageVersion = saveStateToDB(this._db, boardId, this.persistedState());
|
|
1892
2452
|
this._lastPersistenceError = null;
|
|
1893
2453
|
return true;
|
|
1894
2454
|
} catch (error) {
|
|
@@ -1904,18 +2464,20 @@ class CanvasStateManager {
|
|
|
1904
2464
|
}
|
|
1905
2465
|
}
|
|
1906
2466
|
|
|
1907
|
-
private _lastPersistenceError: { message: string; at: string } | null = null;
|
|
1908
|
-
|
|
1909
2467
|
/** Health view of state persistence: ok until a save fails, ok again after the next success. */
|
|
1910
2468
|
get persistenceHealth(): { ok: boolean; lastError: { message: string; at: string } | null } {
|
|
1911
|
-
|
|
2469
|
+
const lastError =
|
|
2470
|
+
[this.visibleBoard, ...this.liveBoards.values()].find((board) => board.persistenceError)?.persistenceError ??
|
|
2471
|
+
null;
|
|
2472
|
+
return { ok: lastError === null, lastError };
|
|
1912
2473
|
}
|
|
1913
2474
|
|
|
1914
2475
|
/** Close the SQLite database cleanly. Call on server shutdown. */
|
|
1915
2476
|
close(): void {
|
|
1916
2477
|
// A failed debounce has no timer left, but its edits still need saving.
|
|
1917
2478
|
// Leave the database and memory intact if the retry fails.
|
|
1918
|
-
if (this.
|
|
2479
|
+
if ([this.visibleBoard, ...this.liveBoards.values()].some((board) => board.saveTimer || board.persistenceError))
|
|
2480
|
+
this.flushToDisk();
|
|
1919
2481
|
if (this._db) {
|
|
1920
2482
|
try {
|
|
1921
2483
|
finalizeCanvasDbForClose(this._db);
|
|
@@ -1929,8 +2491,14 @@ class CanvasStateManager {
|
|
|
1929
2491
|
}
|
|
1930
2492
|
this._db = null;
|
|
1931
2493
|
}
|
|
1932
|
-
this.
|
|
1933
|
-
|
|
2494
|
+
for (const board of new Set([this.visibleBoard, ...this.liveBoards.values()])) {
|
|
2495
|
+
board.disposed = true;
|
|
2496
|
+
this.disposeBoardRuntime(board);
|
|
2497
|
+
}
|
|
2498
|
+
this.liveBoards.clear();
|
|
2499
|
+
this.hostState.capability = null;
|
|
2500
|
+
this.visibleBoard = this.createBoardState();
|
|
2501
|
+
this.navigationGeneration = ++this.nextGeneration;
|
|
1934
2502
|
}
|
|
1935
2503
|
|
|
1936
2504
|
// ── Snapshots ───────────────────────────────────────────────
|
|
@@ -1940,12 +2508,17 @@ class CanvasStateManager {
|
|
|
1940
2508
|
return join(this._workspaceRoot, PMX_CANVAS_DIR, SNAPSHOTS_SUBDIR);
|
|
1941
2509
|
}
|
|
1942
2510
|
|
|
1943
|
-
private applyPersistedState(state: PersistedCanvasState): void {
|
|
2511
|
+
private applyPersistedState(state: PersistedCanvasState, replaced = true): void {
|
|
1944
2512
|
// Validate the complete replacement before discarding any live board state.
|
|
2513
|
+
// Keep the cached task fields as the revision comparison baseline. Display
|
|
2514
|
+
// reads project fresh fields; explicit reconciliation stamps and saves them.
|
|
1945
2515
|
const nodes = (Array.isArray(state.nodes) ? state.nodes : [])
|
|
1946
2516
|
.filter((node) => node?.id)
|
|
1947
|
-
.map((node) => structuredClone(this.normalizeNode(node)));
|
|
1948
|
-
|
|
2517
|
+
.map((node) => structuredClone(this.normalizeNode(node, undefined, { projectWork: false })));
|
|
2518
|
+
if (replaced) {
|
|
2519
|
+
this._boardGeneration = ++this.nextGeneration;
|
|
2520
|
+
if (this.boardState === this.visibleBoard) this.navigationGeneration = ++this.nextGeneration;
|
|
2521
|
+
}
|
|
1949
2522
|
this._tour = state.tour ? structuredClone(state.tour) : undefined;
|
|
1950
2523
|
this.nodes.clear();
|
|
1951
2524
|
this.edges.clear();
|
|
@@ -1953,6 +2526,7 @@ class CanvasStateManager {
|
|
|
1953
2526
|
this._contextPinnedNodeIds.clear();
|
|
1954
2527
|
this._contextPinMeta.clear();
|
|
1955
2528
|
this.ax.resetCanvasBound();
|
|
2529
|
+
this._storageVersion = state.storageVersion ?? 0;
|
|
1956
2530
|
this._contentRevision = state.revisionState?.revision ?? 0;
|
|
1957
2531
|
this._revisionFloor = state.revisionState?.floor ?? 0;
|
|
1958
2532
|
this._deletionTombstones = structuredClone(state.revisionState?.tombstones ?? []);
|
|
@@ -2079,6 +2653,7 @@ class CanvasStateManager {
|
|
|
2079
2653
|
|
|
2080
2654
|
try {
|
|
2081
2655
|
const payload = this.externalizePersistedStateBlobs({
|
|
2656
|
+
storageVersion: this._storageVersion,
|
|
2082
2657
|
version: 1,
|
|
2083
2658
|
theme: this._theme,
|
|
2084
2659
|
viewport: this._viewport,
|
|
@@ -2174,6 +2749,7 @@ class CanvasStateManager {
|
|
|
2174
2749
|
restoreSnapshot(idOrName: string): boolean {
|
|
2175
2750
|
const resolved = this.readResolvedSnapshot(idOrName);
|
|
2176
2751
|
if (!resolved || resolved.state.version !== 1) return false;
|
|
2752
|
+
this.assertSnapshotRestorable(idOrName);
|
|
2177
2753
|
|
|
2178
2754
|
const previousState: PersistedCanvasState = this.externalizePersistedStateBlobs({
|
|
2179
2755
|
version: 1,
|
|
@@ -2214,7 +2790,11 @@ class CanvasStateManager {
|
|
|
2214
2790
|
|
|
2215
2791
|
try {
|
|
2216
2792
|
const revisionBeforeRestore = this._contentRevision;
|
|
2217
|
-
this.applyPersistedState({
|
|
2793
|
+
this.applyPersistedState({
|
|
2794
|
+
...nextState,
|
|
2795
|
+
storageVersion: this._storageVersion,
|
|
2796
|
+
revisionState: this.revisionState(),
|
|
2797
|
+
});
|
|
2218
2798
|
this.restampRestoredContent(previousState.nodes, previousState.edges, revisionBeforeRestore);
|
|
2219
2799
|
this.scheduleSave();
|
|
2220
2800
|
this.notifyChange('nodes');
|
|
@@ -2227,7 +2807,11 @@ class CanvasStateManager {
|
|
|
2227
2807
|
const before = Array.from(this.nodes.values(), (node) => structuredClone(node));
|
|
2228
2808
|
const edgesBefore = this.getEdges();
|
|
2229
2809
|
const revisionBeforeReplay = this._contentRevision;
|
|
2230
|
-
this.applyPersistedState({
|
|
2810
|
+
this.applyPersistedState({
|
|
2811
|
+
...nextState,
|
|
2812
|
+
storageVersion: this._storageVersion,
|
|
2813
|
+
revisionState: this.revisionState(),
|
|
2814
|
+
});
|
|
2231
2815
|
this.restampRestoredContent(before, edgesBefore, revisionBeforeReplay);
|
|
2232
2816
|
this.scheduleSave();
|
|
2233
2817
|
this.notifyChange('nodes');
|
|
@@ -2238,7 +2822,11 @@ class CanvasStateManager {
|
|
|
2238
2822
|
const before = Array.from(this.nodes.values(), (node) => structuredClone(node));
|
|
2239
2823
|
const edgesBefore = this.getEdges();
|
|
2240
2824
|
const revisionBeforeReplay = this._contentRevision;
|
|
2241
|
-
this.applyPersistedState({
|
|
2825
|
+
this.applyPersistedState({
|
|
2826
|
+
...previousState,
|
|
2827
|
+
storageVersion: this._storageVersion,
|
|
2828
|
+
revisionState: this.revisionState(),
|
|
2829
|
+
});
|
|
2242
2830
|
this.restampRestoredContent(before, edgesBefore, revisionBeforeReplay);
|
|
2243
2831
|
this.scheduleSave();
|
|
2244
2832
|
this.notifyChange('nodes');
|
|
@@ -2257,6 +2845,19 @@ class CanvasStateManager {
|
|
|
2257
2845
|
}
|
|
2258
2846
|
}
|
|
2259
2847
|
|
|
2848
|
+
/** Old snapshots stay readable, but neither whole-board nor one-card restore crosses a move. */
|
|
2849
|
+
assertSnapshotRestorable(idOrName: string): void {
|
|
2850
|
+
if (!this._db || !this._activeBoardId) return;
|
|
2851
|
+
const resolved = this.readResolvedSnapshot(idOrName);
|
|
2852
|
+
if (!resolved) throw new OperationError('Snapshot not found.', 404);
|
|
2853
|
+
assertBoardStorageVersion(this._db, this._activeBoardId, this._storageVersion);
|
|
2854
|
+
if ((resolved.state.moveBoundary ?? null) !== boardMoveBoundary(this._db, this._activeBoardId))
|
|
2855
|
+
throw new OperationError(
|
|
2856
|
+
'This snapshot predates a move. Inspect it and reapply desired content to the current card, or restore a whole-library backup.',
|
|
2857
|
+
409,
|
|
2858
|
+
);
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2260
2861
|
/** Read a snapshot's data without restoring it (for diff). Resolves by ID or name. */
|
|
2261
2862
|
getSnapshotData(idOrName: string): {
|
|
2262
2863
|
name: string;
|
|
@@ -2453,18 +3054,22 @@ class CanvasStateManager {
|
|
|
2453
3054
|
: (loadStateFromDB(this._db!, board.id)?.nodes ?? [])
|
|
2454
3055
|
).map((node) => ({ id: node.id, title: typeof node.data.title === 'string' ? node.data.title : node.id })),
|
|
2455
3056
|
}));
|
|
2456
|
-
this._db
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
const
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
3057
|
+
state.storageVersion = this._db
|
|
3058
|
+
.transaction(() => {
|
|
3059
|
+
assertBoardStorageVersion(this._db!, boardId, state.storageVersion ?? 0);
|
|
3060
|
+
for (const node of legacy) {
|
|
3061
|
+
const resolved = this.resolveNodeDataBlobs(node);
|
|
3062
|
+
const content = typeof resolved.data.content === 'string' ? resolved.data.content : '';
|
|
3063
|
+
node.data = { ...node.data, wikiLinks: resolveWikiLinks(content, [], catalog) };
|
|
3064
|
+
this._db!.run('UPDATE nodes SET data = ? WHERE board_id = ? AND id = ?', [
|
|
3065
|
+
JSON.stringify(node.data),
|
|
3066
|
+
boardId,
|
|
3067
|
+
node.id,
|
|
3068
|
+
]);
|
|
3069
|
+
}
|
|
3070
|
+
return boardStorageVersion(this._db!, boardId);
|
|
3071
|
+
})
|
|
3072
|
+
.immediate();
|
|
2468
3073
|
}
|
|
2469
3074
|
|
|
2470
3075
|
private bindWikiLinks(
|
|
@@ -2588,7 +3193,7 @@ class CanvasStateManager {
|
|
|
2588
3193
|
description: `Added ${cloned.type} node "${(cloned.data.title as string) ?? cloned.id}"`,
|
|
2589
3194
|
forward: this.suppressed(() => {
|
|
2590
3195
|
this.nodes.set(cloned.id, {
|
|
2591
|
-
...structuredClone(cloned),
|
|
3196
|
+
...this.normalizeNode(structuredClone(cloned)),
|
|
2592
3197
|
contentRevision: this.nextContentRevision(),
|
|
2593
3198
|
contentChangedAt: new Date().toISOString(),
|
|
2594
3199
|
lastEditedBy: currentActor(),
|
|
@@ -2639,6 +3244,14 @@ class CanvasStateManager {
|
|
|
2639
3244
|
updateNode(id: string, patch: Partial<CanvasNodeState>, savedWikiLinks?: WikiLink[] | null): void {
|
|
2640
3245
|
const existing = this.nodes.get(id);
|
|
2641
3246
|
if (!existing) return;
|
|
3247
|
+
if (
|
|
3248
|
+
existing.type === 'status' &&
|
|
3249
|
+
typeof existing.data.workItemId === 'string' &&
|
|
3250
|
+
((patch.type !== undefined && patch.type !== existing.type) ||
|
|
3251
|
+
(patch.data && patch.data.workItemId !== existing.data.workItemId))
|
|
3252
|
+
) {
|
|
3253
|
+
throw new OperationError('Live work card references are immutable.');
|
|
3254
|
+
}
|
|
2642
3255
|
if (patch.data) {
|
|
2643
3256
|
if (patch.data.contentOrigin !== existing.data.contentOrigin) {
|
|
2644
3257
|
throw new OperationError('Content origin is immutable after creation.');
|
|
@@ -2914,6 +3527,10 @@ class CanvasStateManager {
|
|
|
2914
3527
|
}
|
|
2915
3528
|
|
|
2916
3529
|
private invalidateRelationTarget(boardId: string, nodeId: string): void {
|
|
3530
|
+
if (boardId !== this._activeBoardId && this.liveBoards.has(boardId)) {
|
|
3531
|
+
this.withBoard(boardId, () => this.invalidateRelationTarget(boardId, nodeId));
|
|
3532
|
+
return;
|
|
3533
|
+
}
|
|
2917
3534
|
if (boardId === this._activeBoardId) {
|
|
2918
3535
|
const node = this.nodes.get(nodeId);
|
|
2919
3536
|
if (node) {
|
|
@@ -3356,6 +3973,7 @@ class CanvasStateManager {
|
|
|
3356
3973
|
*/
|
|
3357
3974
|
applyPersistedAxState(ax: unknown): void {
|
|
3358
3975
|
this.ax.applyPersistedAx(ax);
|
|
3976
|
+
this.refreshLiveWorkCards();
|
|
3359
3977
|
}
|
|
3360
3978
|
|
|
3361
3979
|
getAxFocus(): PmxAxFocusState {
|
|
@@ -3375,6 +3993,14 @@ class CanvasStateManager {
|
|
|
3375
3993
|
return this.ax.getWorkItems();
|
|
3376
3994
|
}
|
|
3377
3995
|
|
|
3996
|
+
getWorkItemsOnBoard(boardId: string): PmxAxWorkItem[] {
|
|
3997
|
+
if (boardId === this._activeBoardId) return this.getWorkItems();
|
|
3998
|
+
const live = this.liveBoards.get(boardId);
|
|
3999
|
+
if (live && (live.saveTimer || live.persistenceError))
|
|
4000
|
+
return this.boardExecution.run(live, () => live.ax.getWorkItems());
|
|
4001
|
+
return this._db ? (loadStateFromDB(this._db, boardId)?.ax?.workItems ?? []) : [];
|
|
4002
|
+
}
|
|
4003
|
+
|
|
3378
4004
|
addWorkItem(
|
|
3379
4005
|
input: {
|
|
3380
4006
|
title: string;
|
|
@@ -3420,6 +4046,7 @@ class CanvasStateManager {
|
|
|
3420
4046
|
for (const nodeId of nodeIds) {
|
|
3421
4047
|
const node = this.nodes.get(nodeId);
|
|
3422
4048
|
if (!node) continue;
|
|
4049
|
+
if (node.type === 'status' && typeof node.data.workItemId === 'string') continue;
|
|
3423
4050
|
// No-op guard: a title-/agentId-only work-item patch must not push
|
|
3424
4051
|
// history entries and SSE churn for chips that aren't changing.
|
|
3425
4052
|
if (node.data.axWorkStatus === status) continue;
|
|
@@ -3661,11 +4288,15 @@ class CanvasStateManager {
|
|
|
3661
4288
|
return this.ax.getAxSteering(q);
|
|
3662
4289
|
}
|
|
3663
4290
|
|
|
3664
|
-
getPendingSteering(
|
|
4291
|
+
getPendingSteering(
|
|
4292
|
+
options: { consumer?: string; limit?: number; addressedOnly?: boolean; offset?: number } = {},
|
|
4293
|
+
): PmxAxSteeringMessage[] {
|
|
3665
4294
|
return this.ax.getPendingSteering(options);
|
|
3666
4295
|
}
|
|
3667
4296
|
|
|
3668
|
-
getPendingSteeringForContext(
|
|
4297
|
+
getPendingSteeringForContext(
|
|
4298
|
+
options: { consumer?: string; limit?: number; addressedOnly?: boolean; offset?: number } = {},
|
|
4299
|
+
): PmxAxSteeringMessage[] {
|
|
3669
4300
|
return this.ax.getPendingSteeringForContext(options);
|
|
3670
4301
|
}
|
|
3671
4302
|
|
|
@@ -3689,6 +4320,8 @@ class CanvasStateManager {
|
|
|
3689
4320
|
this._contextReadListener?.(boardId);
|
|
3690
4321
|
return read;
|
|
3691
4322
|
} catch (error) {
|
|
4323
|
+
// Explicit receipts need a distinguishable outcome, not best-effort diagnostics.
|
|
4324
|
+
if (input.deliveryId) throw error;
|
|
3692
4325
|
logCanvasStateWarning('record context read failed', error);
|
|
3693
4326
|
return null;
|
|
3694
4327
|
}
|