pmx-canvas 0.8.2 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/extensions/pmx-canvas/runtime.json +1 -1
- package/.github/extensions/pmx-canvas/tools.json +601 -22
- package/CHANGELOG.md +59 -0
- package/Readme.md +131 -532
- package/dist/canvas/global.css +2724 -476
- package/dist/canvas/index.js +168 -107
- package/dist/canvas/mcp-app.html +2899 -596
- package/dist/json-render/index.js +20 -20
- package/dist/types/client/canvas/BarHint.d.ts +12 -2
- package/dist/types/client/canvas/BoardLinkDialog.d.ts +3 -1
- package/dist/types/client/canvas/BoardPinButton.d.ts +10 -0
- package/dist/types/client/canvas/BoardSwitcher.d.ts +1 -1
- package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
- package/dist/types/client/canvas/CommandBar.d.ts +3 -4
- package/dist/types/client/canvas/ConnectionBanner.d.ts +2 -1
- package/dist/types/client/canvas/ContextChip.d.ts +3 -0
- package/dist/types/client/canvas/CrossBoardReferences.d.ts +5 -0
- package/dist/types/client/canvas/EdgeLayer.d.ts +14 -3
- package/dist/types/client/canvas/EmptyState.d.ts +4 -4
- package/dist/types/client/canvas/HomeGraphView.d.ts +2 -0
- package/dist/types/client/canvas/HomeMapView.d.ts +1 -0
- package/dist/types/client/canvas/HomeReviewView.d.ts +6 -0
- package/dist/types/client/canvas/HomeView.d.ts +9 -7
- package/dist/types/client/canvas/KnowledgeDialog.d.ts +4 -0
- package/dist/types/client/canvas/LifecycleChrome.d.ts +5 -0
- package/dist/types/client/canvas/LifecycleFields.d.ts +7 -0
- package/dist/types/client/canvas/LifecyclePanel.d.ts +1 -0
- package/dist/types/client/canvas/MemoryInspector.d.ts +7 -0
- package/dist/types/client/canvas/NearPinLayer.d.ts +14 -0
- package/dist/types/client/canvas/NodeContextMark.d.ts +40 -7
- package/dist/types/client/canvas/RelationPresetsDialog.d.ts +4 -0
- package/dist/types/client/canvas/RelationsPanel.d.ts +4 -0
- package/dist/types/client/canvas/SessionPanel.d.ts +2 -0
- package/dist/types/client/canvas/canvas-area.d.ts +1 -1
- package/dist/types/client/canvas/graph-geometry.d.ts +27 -0
- package/dist/types/client/canvas/graph-model.d.ts +37 -0
- package/dist/types/client/canvas/memory-layout.d.ts +23 -0
- package/dist/types/client/canvas/use-chrome-height.d.ts +2 -0
- package/dist/types/client/canvas/use-focus-trap.d.ts +1 -0
- package/dist/types/client/canvas/use-memory-graph.d.ts +8 -0
- package/dist/types/client/canvas/use-pan-zoom.d.ts +4 -1
- package/dist/types/client/icons.d.ts +0 -10
- package/dist/types/client/nodes/AxStepControls.d.ts +1 -1
- package/dist/types/client/nodes/InlineMarkdownEditor.d.ts +4 -4
- package/dist/types/client/nodes/MarkdownNode.d.ts +5 -1
- package/dist/types/client/nodes/McpAppNode.d.ts +4 -4
- package/dist/types/client/nodes/viewer-runtime-state.d.ts +1 -1
- package/dist/types/client/state/boards-store.d.ts +81 -9
- package/dist/types/client/state/canvas-store.d.ts +14 -1
- package/dist/types/client/state/context-chip-store.d.ts +22 -0
- package/dist/types/client/state/context-status-store.d.ts +35 -0
- package/dist/types/client/state/human-store.d.ts +2 -1
- package/dist/types/client/state/intent-bridge.d.ts +11 -8
- package/dist/types/client/state/knowledge-store.d.ts +22 -0
- package/dist/types/client/state/near-pin-store.d.ts +44 -1
- package/dist/types/client/state/presence-store.d.ts +4 -4
- package/dist/types/client/state/presentation.d.ts +28 -0
- package/dist/types/client/state/relation-store.d.ts +10 -0
- package/dist/types/client/state/session-store.d.ts +41 -2
- package/dist/types/client/state/workspace-settings.d.ts +7 -0
- package/dist/types/client/types.d.ts +13 -1
- package/dist/types/client/utils/keyboard.d.ts +2 -0
- package/dist/types/client/utils/time-ago.d.ts +4 -0
- package/dist/types/mcp/context-uri-template.d.ts +3 -1
- package/dist/types/server/agent-presence.d.ts +10 -3
- package/dist/types/server/ax-context.d.ts +1 -1
- package/dist/types/server/canvas-db.d.ts +30 -7
- package/dist/types/server/canvas-operations.d.ts +24 -2
- package/dist/types/server/canvas-provenance.d.ts +22 -2
- package/dist/types/server/canvas-serialization.d.ts +2 -0
- package/dist/types/server/canvas-state.d.ts +93 -15
- package/dist/types/server/canvas-validation.d.ts +1 -1
- package/dist/types/server/context-brief.d.ts +94 -6
- package/dist/types/server/context-reads.d.ts +33 -7
- package/dist/types/server/edit-summary.d.ts +20 -0
- package/dist/types/server/index.d.ts +71 -10
- package/dist/types/server/knowledge-lifecycle.d.ts +221 -0
- package/dist/types/server/lifecycle-status.d.ts +10 -0
- package/dist/types/server/memory-graph.d.ts +9 -0
- package/dist/types/server/mutation-history.d.ts +2 -0
- package/dist/types/server/operations/ops/ax-shared.d.ts +1 -1
- package/dist/types/server/operations/ops/boards.d.ts +2 -1
- package/dist/types/server/operations/ops/lifecycle.d.ts +2 -0
- package/dist/types/server/operations/ops/memory-graph.d.ts +2 -0
- package/dist/types/server/operations/ops/nodes.d.ts +5 -0
- package/dist/types/server/operations/ops/query.d.ts +23 -0
- package/dist/types/server/operations/registry.d.ts +1 -0
- package/dist/types/server/placement.d.ts +7 -0
- package/dist/types/server/scope-fence.d.ts +2 -1
- package/dist/types/server/spatial-analysis.d.ts +6 -3
- package/dist/types/server/tour-control.d.ts +32 -0
- package/dist/types/server/wiki-links.d.ts +14 -0
- package/dist/types/server/workbench-auth.d.ts +2 -0
- package/dist/types/shared/agent-context.d.ts +2 -0
- package/dist/types/shared/agent-presence.d.ts +15 -12
- package/dist/types/shared/auto-arrange.d.ts +1 -0
- package/dist/types/shared/boards.d.ts +4 -4
- package/dist/types/shared/knowledge-lifecycle.d.ts +41 -0
- package/dist/types/shared/knowledge-presets.d.ts +6 -0
- package/dist/types/shared/lifecycle-status.d.ts +24 -0
- package/dist/types/shared/memory-graph.d.ts +53 -0
- package/dist/types/shared/relations.d.ts +68 -0
- package/dist/types/shared/semantic-attention.d.ts +2 -0
- package/dist/types/shared/tour.d.ts +38 -0
- package/dist/types/shared/workspace-settings.d.ts +11 -0
- package/docs/RELEASE.md +21 -3
- package/docs/adr-001-bun-only-runtime.md +7 -1
- package/docs/adr-002-shared-product-separate-runtimes.md +93 -0
- package/docs/api-reference.md +109 -0
- package/docs/ax-host-adapter-contract.md +1 -1
- package/docs/ax-state-contract.md +5 -4
- package/docs/cli.md +153 -15
- package/docs/demos/pmx-canvas-demo.mp4 +0 -0
- package/docs/design/AgentContext.dc.html +3 -3
- package/docs/design/BoardPins.dc.html +37 -22
- package/docs/design/Categories.dc.html +208 -0
- package/docs/design/CategoryEditor.dc.html +33 -0
- package/docs/design/Chrome.dc.html +52 -9
- package/docs/design/ContextChip.dc.html +224 -0
- package/docs/design/ContextChip600.dc.html +136 -0
- package/docs/design/EmptyState.dc.html +45 -0
- package/docs/design/Flows.dc.html +23 -1
- package/docs/design/HeaderMarks.dc.html +183 -0
- package/docs/design/Home.dc.html +19 -1
- package/docs/design/HomeGraph.dc.html +86 -202
- package/docs/design/HomeMap.dc.html +23 -1
- package/docs/design/HomeTeam.dc.html +32 -14
- package/docs/design/Lifecycle.dc.html +22 -0
- package/docs/design/Lifecycle600.dc.html +18 -0
- package/docs/design/LifecycleVariants.dc.html +157 -0
- package/docs/design/LinkAuthorship.dc.html +139 -0
- package/docs/design/LinksChanged.dc.html +203 -0
- package/docs/design/LinksOptions.dc.html +137 -0
- package/docs/design/LinksOptions600.dc.html +134 -0
- package/docs/design/NearPin.dc.html +9 -10
- package/docs/design/Pane600.dc.html +4 -3
- package/docs/design/Relations.dc.html +3 -2
- package/docs/design/Restore.dc.html +152 -0
- package/docs/design/SeeChange.dc.html +267 -0
- package/docs/design/Uses.dc.html +12 -6
- package/docs/design/canvas.json +146 -19
- package/docs/design.md +449 -16
- package/docs/environment.md +0 -1
- package/docs/evals/curation-effect.md +128 -0
- package/docs/http-api.md +459 -37
- package/docs/mcp.md +146 -18
- package/docs/node-types.md +58 -3
- package/docs/plans/design-gaps-2026-10-08.md +53 -0
- package/docs/plans/plan-009-tech-debt-backlog.md +379 -0
- package/docs/plans/plan-016-context-made-visible-wave-1.md +150 -5
- package/docs/plans/plan-017-knowledge-categories-and-lifecycle.md +175 -0
- package/docs/plans/plan-018-connected-memory-wave-2.md +594 -0
- package/docs/plans/plan-019-post-0.9-roadmap.md +198 -0
- package/docs/product-context-vision-2026-09.md +17 -1
- package/docs/product-vision-2026-09.md +1082 -43
- package/docs/product-vision-review-2026-09.md +13 -1
- package/docs/screenshots/workbench-live-dark.png +0 -0
- package/docs/screenshots/workbench-live-light.png +0 -0
- package/docs/sdk.md +62 -2
- package/package.json +1 -1
- package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.json +1 -1
- package/plugins/pmx-canvas/plugin.json +1 -1
- package/plugins/pmx-canvas/runtime.json +1 -1
- package/plugins/pmx-canvas/tools.json +601 -22
- package/skills/pmx-canvas/SKILL.md +216 -42
- package/skills/pmx-canvas/evals/evals.json +224 -0
- package/skills/pmx-canvas/references/full-reference.md +120 -21
- package/skills/pmx-canvas/references/github-copilot-app-adapter.md +2 -2
- package/skills/pmx-canvas/references/importing-context.md +132 -0
- package/skills/pmx-canvas/references/installing-pmx-canvas.md +6 -6
- package/skills/pmx-canvas/references/tours-and-recording.md +16 -1
- package/skills/pmx-canvas-testing/SKILL.md +20 -0
- package/src/cli/agent.ts +2 -0
- package/src/cli/commands/ax.ts +12 -0
- package/src/cli/commands/boards.ts +88 -11
- package/src/cli/commands/edges.ts +28 -0
- package/src/cli/commands/groups.ts +7 -0
- package/src/cli/commands/nodes.ts +1 -0
- package/src/cli/commands/query.ts +34 -2
- package/src/cli/commands/record.ts +199 -48
- package/src/cli/index.ts +6 -1
- package/src/client/App.tsx +177 -70
- package/src/client/canvas/AttentionHistory.tsx +0 -2
- package/src/client/canvas/BarHint.tsx +40 -2
- package/src/client/canvas/BoardCopyDialog.tsx +1 -1
- package/src/client/canvas/BoardFolderDialog.tsx +7 -7
- package/src/client/canvas/BoardLinkDialog.tsx +138 -33
- package/src/client/canvas/BoardPinButton.tsx +33 -0
- package/src/client/canvas/BoardSwitcher.tsx +26 -40
- package/src/client/canvas/CanvasNode.tsx +360 -237
- package/src/client/canvas/CanvasViewport.tsx +45 -7
- package/src/client/canvas/CommandBar.tsx +29 -23
- package/src/client/canvas/CommandPalette.tsx +5 -12
- package/src/client/canvas/ConnectionBanner.tsx +3 -3
- package/src/client/canvas/ContextChip.tsx +296 -0
- package/src/client/canvas/ContextMenu.tsx +63 -5
- package/src/client/canvas/CrossBoardReferences.tsx +60 -0
- package/src/client/canvas/EdgeLayer.tsx +339 -76
- package/src/client/canvas/EmptyState.tsx +33 -18
- package/src/client/canvas/ExpandedNodeOverlay.tsx +72 -40
- package/src/client/canvas/ExternalWriters.tsx +57 -42
- package/src/client/canvas/HomeGraphView.tsx +942 -0
- package/src/client/canvas/HomeMapView.tsx +641 -0
- package/src/client/canvas/HomeReviewView.tsx +167 -0
- package/src/client/canvas/HomeView.tsx +687 -160
- package/src/client/canvas/IntentLayer.tsx +19 -12
- package/src/client/canvas/KnowledgeDialog.tsx +488 -0
- package/src/client/canvas/LifecycleChrome.tsx +133 -0
- package/src/client/canvas/LifecycleFields.tsx +91 -0
- package/src/client/canvas/LifecyclePanel.tsx +543 -0
- package/src/client/canvas/MemoryInspector.tsx +104 -0
- package/src/client/canvas/Minimap.tsx +3 -3
- package/src/client/canvas/NearPinLayer.tsx +187 -0
- package/src/client/canvas/NodeContextMark.tsx +175 -97
- package/src/client/canvas/Presentation.tsx +23 -57
- package/src/client/canvas/RelationPresetsDialog.tsx +413 -0
- package/src/client/canvas/RelationsPanel.tsx +82 -0
- package/src/client/canvas/SelectionBar.tsx +3 -6
- package/src/client/canvas/SessionPanel.tsx +90 -50
- package/src/client/canvas/SessionReceipt.tsx +311 -39
- package/src/client/canvas/ShortcutOverlay.tsx +1 -1
- package/src/client/canvas/SnapshotPanel.tsx +27 -36
- package/src/client/canvas/ToolRail.tsx +367 -192
- package/src/client/canvas/TopBar.tsx +354 -124
- package/src/client/canvas/canvas-area.ts +10 -3
- package/src/client/canvas/graph-geometry.ts +195 -0
- package/src/client/canvas/graph-model.ts +254 -0
- package/src/client/canvas/memory-layout.ts +106 -0
- package/src/client/canvas/use-chrome-height.ts +20 -0
- package/src/client/canvas/use-focus-trap.ts +31 -5
- package/src/client/canvas/use-memory-graph.ts +58 -0
- package/src/client/canvas/use-node-drag.ts +2 -1
- package/src/client/canvas/use-pan-zoom.ts +64 -7
- package/src/client/icons.tsx +0 -63
- package/src/client/mcp-app/style.css +0 -6
- package/src/client/nodes/AxStepControls.tsx +74 -50
- package/src/client/nodes/BoardNode.tsx +5 -1
- package/src/client/nodes/ContextNode.tsx +18 -11
- package/src/client/nodes/ExtAppFrame.tsx +6 -3
- package/src/client/nodes/FileNode.tsx +46 -42
- package/src/client/nodes/HtmlNode.tsx +32 -0
- package/src/client/nodes/ImageNode.tsx +1 -1
- package/src/client/nodes/InlineFormatBar.tsx +6 -9
- package/src/client/nodes/InlineMarkdownEditor.tsx +56 -17
- package/src/client/nodes/MarkdownNode.tsx +377 -109
- package/src/client/nodes/McpAppNode.tsx +50 -20
- package/src/client/nodes/MdFormatBar.tsx +21 -18
- package/src/client/nodes/PromptNode.tsx +1 -1
- package/src/client/nodes/ResponseNode.tsx +1 -1
- package/src/client/nodes/viewer-runtime-state.ts +54 -22
- package/src/client/state/attention-bridge.ts +64 -15
- package/src/client/state/boards-store.ts +193 -39
- package/src/client/state/canvas-store.ts +99 -12
- package/src/client/state/context-chip-store.ts +70 -0
- package/src/client/state/context-status-store.ts +53 -0
- package/src/client/state/human-store.ts +13 -7
- package/src/client/state/intent-bridge.ts +51 -30
- package/src/client/state/knowledge-store.ts +43 -0
- package/src/client/state/near-pin-store.ts +115 -6
- package/src/client/state/presence-store.ts +7 -10
- package/src/client/state/presentation.ts +91 -0
- package/src/client/state/relation-store.ts +81 -0
- package/src/client/state/session-store.ts +144 -18
- package/src/client/state/sse-bridge.ts +235 -32
- package/src/client/state/workspace-settings.ts +11 -0
- package/src/client/theme/global.css +2724 -476
- package/src/client/types.ts +12 -0
- package/src/client/utils/keyboard.ts +10 -0
- package/src/client/utils/time-ago.ts +14 -0
- package/src/json-render/renderer/index.tsx +31 -0
- package/src/mcp/canvas-app.ts +12 -2
- package/src/mcp/context-uri-template.ts +7 -4
- package/src/mcp/server.ts +63 -41
- package/src/mcp/workbench-app.ts +39 -6
- package/src/server/agent-presence.ts +128 -28
- package/src/server/ax-context.ts +27 -13
- package/src/server/board-export.ts +12 -1
- package/src/server/canvas-db.ts +331 -42
- package/src/server/canvas-operations.ts +195 -83
- package/src/server/canvas-provenance.ts +53 -22
- package/src/server/canvas-serialization.ts +2 -0
- package/src/server/canvas-state.ts +950 -122
- package/src/server/canvas-validation.ts +9 -2
- package/src/server/context-brief.ts +345 -38
- package/src/server/context-reads.ts +178 -30
- package/src/server/demo-state.json +355 -24
- package/src/server/edit-summary.ts +74 -0
- package/src/server/html-surface.ts +8 -1
- package/src/server/index.ts +181 -11
- package/src/server/knowledge-lifecycle.ts +304 -0
- package/src/server/lifecycle-status.ts +232 -0
- package/src/server/memory-graph.ts +166 -0
- package/src/server/mutation-history.ts +18 -6
- package/src/server/operations/composites.ts +32 -11
- package/src/server/operations/http.ts +7 -2
- package/src/server/operations/index.ts +4 -0
- package/src/server/operations/mcp.ts +26 -1
- package/src/server/operations/ops/ax-presence.ts +1 -1
- package/src/server/operations/ops/ax-read.ts +57 -6
- package/src/server/operations/ops/ax-timeline.ts +28 -2
- package/src/server/operations/ops/backup.ts +5 -3
- package/src/server/operations/ops/batch.ts +35 -4
- package/src/server/operations/ops/boards.ts +188 -47
- package/src/server/operations/ops/canvas-wire.ts +31 -1
- package/src/server/operations/ops/edges.ts +36 -2
- package/src/server/operations/ops/groups.ts +6 -1
- package/src/server/operations/ops/lifecycle.ts +262 -0
- package/src/server/operations/ops/memory-graph.ts +45 -0
- package/src/server/operations/ops/nodes.ts +107 -24
- package/src/server/operations/ops/query.ts +281 -68
- package/src/server/operations/ops/snapshots.ts +73 -1
- package/src/server/operations/ops/validate.ts +5 -1
- package/src/server/operations/ops/viewport.ts +97 -3
- package/src/server/operations/registry.ts +74 -25
- package/src/server/placement.ts +29 -0
- package/src/server/scope-fence.ts +78 -6
- package/src/server/server.ts +115 -16
- package/src/server/spatial-analysis.ts +48 -23
- package/src/server/tour-control.ts +82 -0
- package/src/server/wiki-links.ts +137 -0
- package/src/server/workbench-auth.ts +11 -0
- package/src/shared/agent-context.ts +3 -0
- package/src/shared/agent-presence.ts +15 -15
- package/src/shared/auto-arrange.ts +2 -0
- package/src/shared/boards.ts +4 -4
- package/src/shared/knowledge-lifecycle.ts +22 -0
- package/src/shared/knowledge-presets.ts +80 -0
- package/src/shared/lifecycle-status.ts +26 -0
- package/src/shared/memory-graph.ts +74 -0
- package/src/shared/relations.ts +98 -0
- package/src/shared/semantic-attention.ts +15 -1
- package/src/shared/tour.ts +23 -5
- package/src/shared/workspace-settings.ts +13 -0
- package/dist/types/client/canvas/ContextPinBar.d.ts +0 -1
- package/dist/types/client/canvas/FocusFieldLayer.d.ts +0 -1
- package/dist/types/client/canvas/kind-colors.d.ts +0 -3
- package/dist/types/server/agent-context.d.ts +0 -22
- 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', {
|
|
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
|
|
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
|
|
|
@@ -412,28 +425,32 @@ export async function executeOperation(
|
|
|
412
425
|
? withoutExternalMcp(run)
|
|
413
426
|
: run());
|
|
414
427
|
if (recordRead) {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
+
}
|
|
429
446
|
}
|
|
430
447
|
return result;
|
|
431
448
|
},
|
|
432
449
|
);
|
|
433
450
|
}
|
|
434
451
|
|
|
435
|
-
/**
|
|
436
|
-
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']);
|
|
437
454
|
const TRUSTED_HUMAN_ONLY_OPS = new Set(['board.delete', 'import.request', 'import.cancel', 'import.commit']);
|
|
438
455
|
|
|
439
456
|
/**
|
|
@@ -463,6 +480,9 @@ function needsOpenBoard(op: Operation): boolean {
|
|
|
463
480
|
|
|
464
481
|
async function executeOperationInner(name: string, rawInput: unknown, meta: ExecuteOperationMeta): Promise<unknown> {
|
|
465
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);
|
|
466
486
|
if (HUMAN_ONLY_OPS.has(name) && !meta.fromWorkbench) {
|
|
467
487
|
throw new OperationError('This action requires the human in the workbench.', 403);
|
|
468
488
|
}
|
|
@@ -483,7 +503,7 @@ async function executeOperationInner(name: string, rawInput: unknown, meta: Exec
|
|
|
483
503
|
if (name === 'node.add' && typeof capturedBoard === 'string' && capturedBoard !== canvasState.activeBoardId) {
|
|
484
504
|
throw new OperationError('The board changed while the file was loading.', 409);
|
|
485
505
|
}
|
|
486
|
-
if (!canvasState.activeBoardId && needsOpenBoard(op)) {
|
|
506
|
+
if (!canvasState.activeBoardId && needsOpenBoard(op) && !storedCreation) {
|
|
487
507
|
openBoardForWrite();
|
|
488
508
|
if (canvasState.activeBoardId) emitOperationEvent('boards-changed', boardsPayload());
|
|
489
509
|
}
|
|
@@ -495,11 +515,19 @@ async function executeOperationInner(name: string, rawInput: unknown, meta: Exec
|
|
|
495
515
|
// carries one, else its transport/source label.
|
|
496
516
|
const rawAgentId = asRecord(rawInput).agentId;
|
|
497
517
|
const writerKey = typeof rawAgentId === 'string' && rawAgentId.trim() ? rawAgentId.trim() : (meta.source ?? 'api');
|
|
498
|
-
const refusal =
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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;
|
|
503
531
|
if (refusal) throw new OperationError(`Outside the agent scope: ${refusal}`, 403);
|
|
504
532
|
// User wins (item 6): a node a human is holding right now is not the
|
|
505
533
|
// agent's to change — refuse with 409 so the agent requeues the edit.
|
|
@@ -513,6 +541,16 @@ async function executeOperationInner(name: string, rawInput: unknown, meta: Exec
|
|
|
513
541
|
}
|
|
514
542
|
}
|
|
515
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
|
+
}
|
|
516
554
|
const linkedId = linkedIntentId(rawInput);
|
|
517
555
|
// A ghost that expired QUIETLY (no veto) does not block its mutation — the
|
|
518
556
|
// write proceeds unlinked (and takes the auto-ghost path below). Vetoed and
|
|
@@ -580,8 +618,19 @@ async function executeOperationInner(name: string, rawInput: unknown, meta: Exec
|
|
|
580
618
|
}
|
|
581
619
|
}
|
|
582
620
|
|
|
621
|
+
const beforeBoard = canvasState.activeBoardId;
|
|
622
|
+
const beforeRevision =
|
|
623
|
+
name === 'board.create-from' || name === 'board.delete' ? canvasState.getContentRevision().revision : null;
|
|
583
624
|
const result = await op.execute(rawInput, operationContext);
|
|
584
|
-
|
|
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() });
|
|
585
634
|
notePresence(name, rawInput, meta, op, result);
|
|
586
635
|
return result;
|
|
587
636
|
}
|
package/src/server/placement.ts
CHANGED
|
@@ -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(
|
|
59
|
-
|
|
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([
|
|
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 {
|
|
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
|
-
|
|
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
|
/**
|