pmx-canvas 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/extensions/pmx-canvas/runtime.json +1 -1
- package/.github/extensions/pmx-canvas/tools.json +601 -22
- package/CHANGELOG.md +66 -0
- package/Readme.md +131 -532
- package/dist/canvas/global.css +2759 -510
- package/dist/canvas/index.js +168 -107
- package/dist/canvas/mcp-app.html +2936 -632
- package/dist/json-render/index.css +81 -1
- 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/HtmlNode.d.ts +1 -0
- 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/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 +55 -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 +454 -18
- package/docs/environment.md +0 -1
- package/docs/evals/curation-effect.md +128 -0
- package/docs/http-api.md +465 -38
- package/docs/mcp.md +150 -19
- 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 +234 -43
- package/skills/pmx-canvas/evals/evals.json +62 -0
- package/skills/pmx-canvas/references/ax-html-control-surface.md +6 -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 +87 -11
- package/src/cli/commands/edges.ts +28 -0
- package/src/cli/commands/groups.ts +7 -0
- package/src/cli/commands/nodes.ts +4 -2
- package/src/cli/commands/query.ts +34 -2
- package/src/cli/commands/record.ts +199 -48
- package/src/cli/index.ts +7 -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 +533 -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 +0 -1
- package/src/client/nodes/FileNode.tsx +46 -42
- package/src/client/nodes/HtmlNode.tsx +44 -2
- 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/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 +206 -29
- package/src/client/state/workspace-settings.ts +11 -0
- package/src/client/theme/global.css +2759 -510
- 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.css +6 -1
- package/src/json-render/renderer/index.tsx +31 -1
- 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 +196 -84
- 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 +179 -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 +203 -13
- 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 +32 -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/json-render.ts +5 -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 +77 -26
- 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
package/src/server/canvas-db.ts
CHANGED
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
import { Database } from 'bun:sqlite';
|
|
9
9
|
import type { ActorAttribution } from './attribution.js';
|
|
10
10
|
import { CONTEXT_READS_SCHEMA_SQL } from './context-reads.js';
|
|
11
|
-
import {
|
|
11
|
+
import { MIGRATED_SNAPSHOT_FOLDER, normalizeBoardFolder } from '../shared/boards.js';
|
|
12
|
+
import { wikiLinksFromData } from '../shared/memory-graph.js';
|
|
13
|
+
import { relationVocabularySchema, type RelationDefinition } from '../shared/relations.js';
|
|
12
14
|
import { tourSchema, type Tour } from '../shared/tour.js';
|
|
13
15
|
import { existsSync, mkdirSync } from 'node:fs';
|
|
14
16
|
import { dirname } from 'node:path';
|
|
@@ -54,6 +56,16 @@ export function normalizeCanvasTheme(value: unknown, fallback: CanvasTheme = 'ha
|
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
const SCHEMA_SQL = `
|
|
59
|
+
CREATE TABLE IF NOT EXISTS node_creation_requests (
|
|
60
|
+
request_id TEXT PRIMARY KEY, board_id TEXT NOT NULL, fingerprint TEXT NOT NULL, node TEXT NOT NULL
|
|
61
|
+
);
|
|
62
|
+
CREATE TABLE IF NOT EXISTS knowledge_categories (id TEXT PRIMARY KEY, value TEXT NOT NULL);
|
|
63
|
+
CREATE TABLE IF NOT EXISTS folder_properties (path TEXT PRIMARY KEY, value TEXT NOT NULL);
|
|
64
|
+
CREATE TABLE IF NOT EXISTS review_records (
|
|
65
|
+
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
66
|
+
board_id TEXT NOT NULL, node_id TEXT NOT NULL, value TEXT NOT NULL
|
|
67
|
+
);
|
|
68
|
+
CREATE INDEX IF NOT EXISTS review_records_target ON review_records (board_id, node_id, sequence);
|
|
57
69
|
CREATE TABLE IF NOT EXISTS meta (
|
|
58
70
|
key TEXT PRIMARY KEY,
|
|
59
71
|
value TEXT NOT NULL
|
|
@@ -75,7 +87,7 @@ const SCHEMA_SQL = `
|
|
|
75
87
|
viewport_y REAL NOT NULL DEFAULT 0,
|
|
76
88
|
viewport_scale REAL NOT NULL DEFAULT 1,
|
|
77
89
|
tour TEXT NOT NULL DEFAULT 'null',
|
|
78
|
-
|
|
90
|
+
folder TEXT,
|
|
79
91
|
readme_node_id TEXT
|
|
80
92
|
);
|
|
81
93
|
|
|
@@ -120,6 +132,13 @@ const SCHEMA_SQL = `
|
|
|
120
132
|
PRIMARY KEY (board_id, id)
|
|
121
133
|
);
|
|
122
134
|
|
|
135
|
+
-- Board pins (vision move 0a): the workspace's working set across boards. Not part of
|
|
136
|
+
-- any board's state, so snapshots and restores never touch them.
|
|
137
|
+
CREATE TABLE IF NOT EXISTS board_pins (
|
|
138
|
+
board_id TEXT PRIMARY KEY,
|
|
139
|
+
meta TEXT NOT NULL
|
|
140
|
+
);
|
|
141
|
+
|
|
123
142
|
CREATE TABLE IF NOT EXISTS context_pins (
|
|
124
143
|
board_id TEXT NOT NULL,
|
|
125
144
|
node_id TEXT NOT NULL,
|
|
@@ -291,6 +310,14 @@ const SCHEMA_SQL = `
|
|
|
291
310
|
);
|
|
292
311
|
`;
|
|
293
312
|
|
|
313
|
+
type EdgeAttribution = Pick<CanvasEdge, 'changedBy' | 'changedAt' | 'createdBy' | 'createdAt' | 'sourceRevision'>;
|
|
314
|
+
|
|
315
|
+
/** A link's authorship (LinkAuthorship.dc.html) as one JSON column, like a node's attribution. */
|
|
316
|
+
function edgeAttribution(edge: CanvasEdge): string {
|
|
317
|
+
const { changedBy, changedAt, createdBy, createdAt, sourceRevision } = edge;
|
|
318
|
+
return JSON.stringify({ changedBy, changedAt, createdBy, createdAt, sourceRevision });
|
|
319
|
+
}
|
|
320
|
+
|
|
294
321
|
/** Idempotently add a column to an existing table (no prior-migrations system in this repo). */
|
|
295
322
|
function ensureColumn(db: Database, table: string, column: string, ddl: string): void {
|
|
296
323
|
interface ColumnInfoRow {
|
|
@@ -404,7 +431,29 @@ export function openCanvasDb(dbPath: string, options: OpenCanvasDbOptions = {}):
|
|
|
404
431
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
405
432
|
|
|
406
433
|
const db = new Database(dbPath);
|
|
407
|
-
|
|
434
|
+
// Journal negotiation can return SQLITE_BUSY without invoking SQLite's busy
|
|
435
|
+
// handler (including WAL cleanup by a closing connection). Retry only this
|
|
436
|
+
// startup step within the same five-second budget, not arbitrary writes.
|
|
437
|
+
const journalDeadline = performance.now() + 5000;
|
|
438
|
+
while (true) {
|
|
439
|
+
try {
|
|
440
|
+
db.exec(`PRAGMA busy_timeout=${Math.max(1, Math.ceil(journalDeadline - performance.now()))}`);
|
|
441
|
+
db.exec('PRAGMA journal_mode=WAL');
|
|
442
|
+
break;
|
|
443
|
+
} catch (error) {
|
|
444
|
+
if (
|
|
445
|
+
!(error instanceof Error) ||
|
|
446
|
+
!('code' in error) ||
|
|
447
|
+
error.code !== 'SQLITE_BUSY' ||
|
|
448
|
+
performance.now() >= journalDeadline
|
|
449
|
+
) {
|
|
450
|
+
db.close();
|
|
451
|
+
throw error;
|
|
452
|
+
}
|
|
453
|
+
Bun.sleepSync(10);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
db.exec('PRAGMA busy_timeout=5000');
|
|
408
457
|
// FULL, not NORMAL: under WAL, NORMAL lets a host/OS-level crash lose the most
|
|
409
458
|
// recent commits (a clean process exit loses nothing either way). The reporter of
|
|
410
459
|
// issue #22 declined FULL specifically because a save rewrote the whole board, so
|
|
@@ -412,7 +461,6 @@ export function openCanvasDb(dbPath: string, options: OpenCanvasDbOptions = {}):
|
|
|
412
461
|
// typical save is a row or two — so that objection is gone and the durability is
|
|
413
462
|
// worth the fsync.
|
|
414
463
|
db.exec('PRAGMA synchronous=FULL');
|
|
415
|
-
db.exec('PRAGMA busy_timeout=5000');
|
|
416
464
|
if (needsBoardsMigration(db)) migrateToBoards(db, dbPath, options.migratedBoardName ?? 'Board');
|
|
417
465
|
db.exec(SCHEMA_SQL);
|
|
418
466
|
db.exec(CONTEXT_READS_SCHEMA_SQL);
|
|
@@ -423,14 +471,47 @@ export function openCanvasDb(dbPath: string, options: OpenCanvasDbOptions = {}):
|
|
|
423
471
|
ensureColumn(db, 'ax_steering', 'target', 'target TEXT');
|
|
424
472
|
ensureColumn(db, 'context_reads', 'board_id', 'board_id TEXT');
|
|
425
473
|
ensureColumn(db, 'context_reads', 'read_nodes', "read_nodes TEXT NOT NULL DEFAULT '{}'");
|
|
474
|
+
ensureColumn(db, 'context_reads', 'seen_links', "seen_links TEXT NOT NULL DEFAULT '{}'");
|
|
426
475
|
ensureColumn(db, 'context_pins', 'meta', 'meta TEXT');
|
|
427
476
|
ensureColumn(db, 'snapshot_pins', 'meta', 'meta TEXT');
|
|
428
|
-
|
|
477
|
+
const boardColumns = db
|
|
478
|
+
.query<{ name: string }, []>('PRAGMA table_info(boards)')
|
|
479
|
+
.all()
|
|
480
|
+
.map((column) => column.name);
|
|
481
|
+
if (!boardColumns.includes('folder') || boardColumns.includes('category')) {
|
|
482
|
+
// Recheck under the writer lock; already-current databases need no write transaction.
|
|
483
|
+
db.transaction(() => {
|
|
484
|
+
if (
|
|
485
|
+
db
|
|
486
|
+
.query<{ name: string }, []>('PRAGMA table_info(boards)')
|
|
487
|
+
.all()
|
|
488
|
+
.some((column) => column.name === 'category')
|
|
489
|
+
) {
|
|
490
|
+
db.exec('ALTER TABLE boards RENAME COLUMN category TO folder');
|
|
491
|
+
}
|
|
492
|
+
ensureColumn(db, 'boards', 'folder', 'folder TEXT');
|
|
493
|
+
}).immediate();
|
|
494
|
+
}
|
|
429
495
|
ensureColumn(db, 'boards', 'readme_node_id', 'readme_node_id TEXT');
|
|
496
|
+
ensureColumn(db, 'boards', 'map_position', 'map_position TEXT');
|
|
497
|
+
ensureColumn(db, 'boards', 'previous_board_id', 'previous_board_id TEXT');
|
|
498
|
+
ensureColumn(db, 'edges', 'to_board_id', 'to_board_id TEXT');
|
|
499
|
+
ensureColumn(db, 'snapshot_edges', 'to_board_id', 'to_board_id TEXT');
|
|
500
|
+
for (const table of ['edges', 'snapshot_edges']) {
|
|
501
|
+
ensureColumn(db, table, 'kind', 'kind TEXT');
|
|
502
|
+
ensureColumn(db, table, 'reason', 'reason TEXT');
|
|
503
|
+
}
|
|
504
|
+
ensureColumn(db, 'boards', 'relation_vocabulary', 'relation_vocabulary TEXT');
|
|
430
505
|
ensureColumn(db, 'nodes', 'attribution', "attribution TEXT NOT NULL DEFAULT '{}' ");
|
|
431
506
|
ensureColumn(db, 'nodes', 'content_revision', 'content_revision INTEGER NOT NULL DEFAULT 0');
|
|
432
507
|
ensureColumn(db, 'snapshot_nodes', 'attribution', "attribution TEXT NOT NULL DEFAULT '{}' ");
|
|
433
508
|
ensureColumn(db, 'snapshot_nodes', 'content_revision', 'content_revision INTEGER NOT NULL DEFAULT 0');
|
|
509
|
+
ensureColumn(db, 'nodes', 'links_revision', 'links_revision INTEGER NOT NULL DEFAULT 0');
|
|
510
|
+
ensureColumn(db, 'snapshot_nodes', 'links_revision', 'links_revision INTEGER NOT NULL DEFAULT 0');
|
|
511
|
+
ensureColumn(db, 'edges', 'revision', 'revision INTEGER NOT NULL DEFAULT 0');
|
|
512
|
+
ensureColumn(db, 'edges', 'attribution', "attribution TEXT NOT NULL DEFAULT '{}'");
|
|
513
|
+
ensureColumn(db, 'snapshot_edges', 'revision', 'revision INTEGER NOT NULL DEFAULT 0');
|
|
514
|
+
ensureColumn(db, 'snapshot_edges', 'attribution', "attribution TEXT NOT NULL DEFAULT '{}'");
|
|
434
515
|
ensureColumn(db, 'document_imports', 'committed_node_ids', 'committed_node_ids TEXT');
|
|
435
516
|
|
|
436
517
|
// Set schema version if not present
|
|
@@ -551,11 +632,11 @@ function snapshotsToBoards(db: Database): void {
|
|
|
551
632
|
);
|
|
552
633
|
const boardId = createBoardId();
|
|
553
634
|
db.run(
|
|
554
|
-
'INSERT INTO boards (id, name,
|
|
635
|
+
'INSERT INTO boards (id, name, folder, created_at, viewport_x, viewport_y, viewport_scale, tour) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
|
555
636
|
[
|
|
556
637
|
boardId,
|
|
557
638
|
snapshot.name,
|
|
558
|
-
|
|
639
|
+
MIGRATED_SNAPSHOT_FOLDER,
|
|
559
640
|
snapshot.created_at,
|
|
560
641
|
Number(meta.get('viewport_x') ?? 0),
|
|
561
642
|
Number(meta.get('viewport_y') ?? 0),
|
|
@@ -713,19 +794,21 @@ export function saveStateToDB(db: Database, boardId: string, state: PersistedCan
|
|
|
713
794
|
|
|
714
795
|
// Save nodes
|
|
715
796
|
const upsertNode = db.prepare(
|
|
716
|
-
`INSERT INTO nodes (board_id, id, type, pos_x, pos_y, width, height, z_index, collapsed, pinned, data, attribution, content_revision)
|
|
717
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
797
|
+
`INSERT INTO nodes (board_id, id, type, pos_x, pos_y, width, height, z_index, collapsed, pinned, data, attribution, content_revision, links_revision)
|
|
798
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
718
799
|
ON CONFLICT(board_id, id) DO UPDATE SET
|
|
719
800
|
type = excluded.type, pos_x = excluded.pos_x, pos_y = excluded.pos_y,
|
|
720
801
|
width = excluded.width, height = excluded.height, z_index = excluded.z_index,
|
|
721
802
|
collapsed = excluded.collapsed, pinned = excluded.pinned, data = excluded.data,
|
|
722
|
-
attribution = excluded.attribution, content_revision = excluded.content_revision
|
|
803
|
+
attribution = excluded.attribution, content_revision = excluded.content_revision,
|
|
804
|
+
links_revision = excluded.links_revision
|
|
723
805
|
WHERE type IS NOT excluded.type OR pos_x IS NOT excluded.pos_x
|
|
724
806
|
OR pos_y IS NOT excluded.pos_y OR width IS NOT excluded.width
|
|
725
807
|
OR height IS NOT excluded.height OR z_index IS NOT excluded.z_index
|
|
726
808
|
OR collapsed IS NOT excluded.collapsed OR pinned IS NOT excluded.pinned
|
|
727
809
|
OR data IS NOT excluded.data OR attribution IS NOT excluded.attribution
|
|
728
|
-
OR content_revision IS NOT excluded.content_revision
|
|
810
|
+
OR content_revision IS NOT excluded.content_revision
|
|
811
|
+
OR links_revision IS NOT excluded.links_revision`,
|
|
729
812
|
);
|
|
730
813
|
for (const node of state.nodes) {
|
|
731
814
|
upsertNode.run(
|
|
@@ -740,22 +823,32 @@ export function saveStateToDB(db: Database, boardId: string, state: PersistedCan
|
|
|
740
823
|
node.collapsed ? 1 : 0,
|
|
741
824
|
node.pinned ? 1 : 0,
|
|
742
825
|
JSON.stringify(node.data),
|
|
743
|
-
JSON.stringify({
|
|
826
|
+
JSON.stringify({
|
|
827
|
+
createdBy: node.createdBy,
|
|
828
|
+
lastEditedBy: node.lastEditedBy,
|
|
829
|
+
contentChangedAt: node.contentChangedAt,
|
|
830
|
+
freshnessStartedAt: node.freshnessStartedAt,
|
|
831
|
+
}),
|
|
744
832
|
node.contentRevision ?? 0,
|
|
833
|
+
node.linksRevision ?? 0,
|
|
745
834
|
);
|
|
746
835
|
}
|
|
747
836
|
deleteMissingRows(db, 'nodes', 'id', boardId, new Set(state.nodes.map((node) => node.id)));
|
|
748
837
|
|
|
749
838
|
// Save edges
|
|
750
839
|
const upsertEdge = db.prepare(
|
|
751
|
-
`INSERT INTO edges (board_id, id, from_node, to_node, type, label, style, animated)
|
|
752
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
840
|
+
`INSERT INTO edges (board_id, id, from_node, to_node, type, label, style, animated, revision, attribution, to_board_id, kind, reason)
|
|
841
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
753
842
|
ON CONFLICT(board_id, id) DO UPDATE SET
|
|
754
843
|
from_node = excluded.from_node, to_node = excluded.to_node, type = excluded.type,
|
|
755
|
-
label = excluded.label, style = excluded.style, animated = excluded.animated
|
|
844
|
+
label = excluded.label, style = excluded.style, animated = excluded.animated,
|
|
845
|
+
revision = excluded.revision, attribution = excluded.attribution, to_board_id = excluded.to_board_id,
|
|
846
|
+
kind = excluded.kind, reason = excluded.reason
|
|
756
847
|
WHERE from_node IS NOT excluded.from_node OR to_node IS NOT excluded.to_node
|
|
757
848
|
OR type IS NOT excluded.type OR label IS NOT excluded.label
|
|
758
|
-
OR style IS NOT excluded.style OR animated IS NOT excluded.animated
|
|
849
|
+
OR style IS NOT excluded.style OR animated IS NOT excluded.animated
|
|
850
|
+
OR revision IS NOT excluded.revision OR attribution IS NOT excluded.attribution
|
|
851
|
+
OR to_board_id IS NOT excluded.to_board_id OR kind IS NOT excluded.kind OR reason IS NOT excluded.reason`,
|
|
759
852
|
);
|
|
760
853
|
for (const edge of state.edges) {
|
|
761
854
|
upsertEdge.run(
|
|
@@ -767,6 +860,11 @@ export function saveStateToDB(db: Database, boardId: string, state: PersistedCan
|
|
|
767
860
|
edge.label ?? null,
|
|
768
861
|
edge.style ?? null,
|
|
769
862
|
edge.animated ? 1 : 0,
|
|
863
|
+
edge.revision ?? 0,
|
|
864
|
+
edgeAttribution(edge),
|
|
865
|
+
edge.toBoardId ?? null,
|
|
866
|
+
edge.kind ?? null,
|
|
867
|
+
edge.reason ?? null,
|
|
770
868
|
);
|
|
771
869
|
}
|
|
772
870
|
deleteMissingRows(db, 'edges', 'id', boardId, new Set(state.edges.map((edge) => edge.id)));
|
|
@@ -863,11 +961,12 @@ export function loadStateFromDB(db: Database, boardId?: string): PersistedCanvas
|
|
|
863
961
|
data: string;
|
|
864
962
|
attribution: string;
|
|
865
963
|
content_revision: number;
|
|
964
|
+
links_revision: number;
|
|
866
965
|
}
|
|
867
966
|
const nodeRows = db.query<NodeRow, [string]>('SELECT * FROM nodes WHERE board_id = ?').all(id);
|
|
868
967
|
const nodes: CanvasNodeState[] = nodeRows.map((row) => {
|
|
869
968
|
const attribution = JSON.parse(row.attribution || '{}') as Partial<
|
|
870
|
-
Pick<CanvasNodeState, 'createdBy' | 'lastEditedBy'>
|
|
969
|
+
Pick<CanvasNodeState, 'createdBy' | 'lastEditedBy' | 'contentChangedAt' | 'freshnessStartedAt'>
|
|
871
970
|
>;
|
|
872
971
|
return {
|
|
873
972
|
id: row.id,
|
|
@@ -880,7 +979,10 @@ export function loadStateFromDB(db: Database, boardId?: string): PersistedCanvas
|
|
|
880
979
|
data: JSON.parse(row.data) as Record<string, unknown>,
|
|
881
980
|
createdBy: attribution.createdBy,
|
|
882
981
|
lastEditedBy: attribution.lastEditedBy,
|
|
982
|
+
contentChangedAt: attribution.contentChangedAt,
|
|
983
|
+
freshnessStartedAt: attribution.freshnessStartedAt,
|
|
883
984
|
contentRevision: row.content_revision,
|
|
985
|
+
linksRevision: row.links_revision,
|
|
884
986
|
};
|
|
885
987
|
});
|
|
886
988
|
|
|
@@ -889,20 +991,30 @@ export function loadStateFromDB(db: Database, boardId?: string): PersistedCanvas
|
|
|
889
991
|
id: string;
|
|
890
992
|
from_node: string;
|
|
891
993
|
to_node: string;
|
|
994
|
+
to_board_id: string | null;
|
|
892
995
|
type: string;
|
|
996
|
+
kind: string | null;
|
|
997
|
+
reason: string | null;
|
|
893
998
|
label: string | null;
|
|
894
999
|
style: string | null;
|
|
895
1000
|
animated: number;
|
|
1001
|
+
revision: number;
|
|
1002
|
+
attribution: string;
|
|
896
1003
|
}
|
|
897
1004
|
const edgeRows = db.query<EdgeRow, [string]>('SELECT * FROM edges WHERE board_id = ?').all(id);
|
|
898
1005
|
const edges: CanvasEdge[] = edgeRows.map((row) => ({
|
|
899
1006
|
id: row.id,
|
|
900
1007
|
from: row.from_node,
|
|
901
1008
|
to: row.to_node,
|
|
1009
|
+
...(row.to_board_id ? { toBoardId: row.to_board_id } : {}),
|
|
902
1010
|
type: row.type as CanvasEdge['type'],
|
|
1011
|
+
...(row.kind ? { kind: row.kind } : {}),
|
|
1012
|
+
...(row.reason ? { reason: row.reason } : {}),
|
|
903
1013
|
...(row.label ? { label: row.label } : {}),
|
|
904
1014
|
...(row.style ? { style: row.style as CanvasEdge['style'] } : {}),
|
|
905
1015
|
...(row.animated ? { animated: true } : {}),
|
|
1016
|
+
...(row.revision ? { revision: row.revision } : {}),
|
|
1017
|
+
...(JSON.parse(row.attribution || '{}') as EdgeAttribution),
|
|
906
1018
|
}));
|
|
907
1019
|
|
|
908
1020
|
// Load annotations
|
|
@@ -966,38 +1078,53 @@ export function loadStateFromDB(db: Database, boardId?: string): PersistedCanvas
|
|
|
966
1078
|
export interface CanvasBoard {
|
|
967
1079
|
id: string;
|
|
968
1080
|
name: string;
|
|
969
|
-
/** A human-chosen
|
|
970
|
-
|
|
1081
|
+
/** A human-chosen folder on Home ("Planning", "Research"); null is unfiled. */
|
|
1082
|
+
folder: string | null;
|
|
971
1083
|
createdAt: string;
|
|
972
1084
|
lastOpenedAt: string | null;
|
|
973
1085
|
nodeCount: number;
|
|
974
1086
|
/** Markdown node used as this board's introduction. */
|
|
975
1087
|
readmeNodeId: string | null;
|
|
1088
|
+
/** Human-dragged position in Home Map; null uses the generated folder layout. */
|
|
1089
|
+
mapPosition?: { x: number; y: number } | null;
|
|
1090
|
+
/** The source of a recurring/template board, separate from ordinary portals. */
|
|
1091
|
+
previousBoardId?: string | null;
|
|
1092
|
+
relationVocabulary?: RelationDefinition[];
|
|
1093
|
+
/** Pinned into the agent's working set (vision move 0a); null when not pinned. */
|
|
1094
|
+
pin: ContextPinMeta | null;
|
|
976
1095
|
}
|
|
977
1096
|
|
|
978
1097
|
interface BoardListRow {
|
|
979
1098
|
id: string;
|
|
980
1099
|
name: string;
|
|
981
|
-
|
|
1100
|
+
folder: string | null;
|
|
982
1101
|
created_at: string;
|
|
983
1102
|
last_opened_at: string | null;
|
|
984
1103
|
node_count: number;
|
|
985
1104
|
readme_node_id: string | null;
|
|
1105
|
+
map_position: string | null;
|
|
1106
|
+
previous_board_id: string | null;
|
|
1107
|
+
relation_vocabulary: string | null;
|
|
1108
|
+
pin_meta: string | null;
|
|
986
1109
|
}
|
|
987
1110
|
|
|
988
|
-
const BOARD_LIST_SQL = `SELECT b.id, b.name, b.
|
|
989
|
-
(SELECT COUNT(*) FROM nodes n WHERE n.board_id = b.id) AS node_count
|
|
990
|
-
FROM boards b`;
|
|
1111
|
+
const BOARD_LIST_SQL = `SELECT b.id, b.name, b.folder, b.created_at, b.last_opened_at, b.readme_node_id, b.map_position, b.previous_board_id, b.relation_vocabulary,
|
|
1112
|
+
(SELECT COUNT(*) FROM nodes n WHERE n.board_id = b.id) AS node_count, p.meta AS pin_meta
|
|
1113
|
+
FROM boards b LEFT JOIN board_pins p ON p.board_id = b.id`;
|
|
991
1114
|
|
|
992
1115
|
function rowToBoard(row: BoardListRow): CanvasBoard {
|
|
993
1116
|
return {
|
|
994
1117
|
id: row.id,
|
|
995
1118
|
name: row.name,
|
|
996
|
-
|
|
1119
|
+
folder: row.folder,
|
|
997
1120
|
createdAt: row.created_at,
|
|
998
1121
|
lastOpenedAt: row.last_opened_at,
|
|
999
1122
|
nodeCount: row.node_count,
|
|
1000
1123
|
readmeNodeId: row.readme_node_id,
|
|
1124
|
+
mapPosition: row.map_position ? (JSON.parse(row.map_position) as { x: number; y: number }) : null,
|
|
1125
|
+
previousBoardId: row.previous_board_id,
|
|
1126
|
+
relationVocabulary: row.relation_vocabulary ? JSON.parse(row.relation_vocabulary) : [],
|
|
1127
|
+
pin: row.pin_meta ? (JSON.parse(row.pin_meta) as ContextPinMeta) : null,
|
|
1001
1128
|
};
|
|
1002
1129
|
}
|
|
1003
1130
|
|
|
@@ -1014,27 +1141,53 @@ export function getBoardFromDB(db: Database, id: string): CanvasBoard | null {
|
|
|
1014
1141
|
return row ? rowToBoard(row) : null;
|
|
1015
1142
|
}
|
|
1016
1143
|
|
|
1017
|
-
export function createBoardInDB(db: Database, name: string,
|
|
1018
|
-
|
|
1144
|
+
export function createBoardInDB(db: Database, name: string, folder: string | null = null): CanvasBoard {
|
|
1145
|
+
folder = normalizeBoardFolder(folder);
|
|
1019
1146
|
const id = createBoardId();
|
|
1020
1147
|
const createdAt = new Date(Date.now()).toISOString();
|
|
1021
|
-
db.run('INSERT INTO boards (id, name,
|
|
1022
|
-
return { id, name,
|
|
1148
|
+
db.run('INSERT INTO boards (id, name, folder, created_at) VALUES (?, ?, ?, ?)', [id, name, folder, createdAt]);
|
|
1149
|
+
return { id, name, folder, createdAt, lastOpenedAt: null, nodeCount: 0, readmeNodeId: null, pin: null };
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
/** Pin a board into the working set, or unpin it with null. False when the board does not exist. */
|
|
1153
|
+
export function setBoardPinInDB(db: Database, id: string, meta: ContextPinMeta | null): boolean {
|
|
1154
|
+
if (!db.query<{ id: string }, [string]>('SELECT id FROM boards WHERE id = ?').get(id)) return false;
|
|
1155
|
+
if (meta) db.run('INSERT OR REPLACE INTO board_pins (board_id, meta) VALUES (?, ?)', [id, JSON.stringify(meta)]);
|
|
1156
|
+
else db.run('DELETE FROM board_pins WHERE board_id = ?', [id]);
|
|
1157
|
+
return true;
|
|
1023
1158
|
}
|
|
1024
1159
|
|
|
1025
|
-
/** Rename and/or re-shelve a board; `
|
|
1160
|
+
/** Rename and/or re-shelve a board; `folder: null` unfiles it. */
|
|
1026
1161
|
export function updateBoardInDB(
|
|
1027
1162
|
db: Database,
|
|
1028
1163
|
id: string,
|
|
1029
|
-
patch: {
|
|
1164
|
+
patch: {
|
|
1165
|
+
name?: string;
|
|
1166
|
+
folder?: string | null;
|
|
1167
|
+
readmeNodeId?: string | null;
|
|
1168
|
+
mapPosition?: { x: number; y: number } | null;
|
|
1169
|
+
previousBoardId?: string | null;
|
|
1170
|
+
relationVocabulary?: RelationDefinition[];
|
|
1171
|
+
},
|
|
1030
1172
|
): boolean {
|
|
1031
|
-
const
|
|
1173
|
+
const folder = patch.folder === undefined ? undefined : normalizeBoardFolder(patch.folder);
|
|
1174
|
+
const vocabulary =
|
|
1175
|
+
patch.relationVocabulary === undefined ? undefined : relationVocabularySchema.parse(patch.relationVocabulary);
|
|
1032
1176
|
if (!db.query<{ id: string }, [string]>('SELECT id FROM boards WHERE id = ?').get(id)) return false;
|
|
1177
|
+
if (vocabulary !== undefined)
|
|
1178
|
+
db.run('UPDATE boards SET relation_vocabulary = ? WHERE id = ?', [JSON.stringify(vocabulary), id]);
|
|
1033
1179
|
if (patch.name !== undefined) db.run('UPDATE boards SET name = ? WHERE id = ?', [patch.name, id]);
|
|
1034
|
-
if (
|
|
1180
|
+
if (folder !== undefined) db.run('UPDATE boards SET folder = ? WHERE id = ?', [folder, id]);
|
|
1035
1181
|
if (patch.readmeNodeId !== undefined) {
|
|
1036
1182
|
db.run('UPDATE boards SET readme_node_id = ? WHERE id = ?', [patch.readmeNodeId, id]);
|
|
1037
1183
|
}
|
|
1184
|
+
if (patch.mapPosition !== undefined)
|
|
1185
|
+
db.run('UPDATE boards SET map_position = ? WHERE id = ?', [
|
|
1186
|
+
patch.mapPosition ? JSON.stringify(patch.mapPosition) : null,
|
|
1187
|
+
id,
|
|
1188
|
+
]);
|
|
1189
|
+
if (patch.previousBoardId !== undefined)
|
|
1190
|
+
db.run('UPDATE boards SET previous_board_id = ? WHERE id = ?', [patch.previousBoardId, id]);
|
|
1038
1191
|
return true;
|
|
1039
1192
|
}
|
|
1040
1193
|
|
|
@@ -1042,22 +1195,133 @@ export function updateBoardInDB(
|
|
|
1042
1195
|
export function createBoardWithStateInDB(
|
|
1043
1196
|
db: Database,
|
|
1044
1197
|
name: string,
|
|
1045
|
-
|
|
1198
|
+
folder: string | null,
|
|
1046
1199
|
state: PersistedCanvasState,
|
|
1047
1200
|
readmeNodeId: string | null,
|
|
1201
|
+
invalidateTarget?: (boardId: string, nodeId: string) => void,
|
|
1048
1202
|
): CanvasBoard {
|
|
1049
1203
|
return db.transaction(() => {
|
|
1050
|
-
const board = createBoardInDB(db, name,
|
|
1204
|
+
const board = createBoardInDB(db, name, folder);
|
|
1051
1205
|
saveStateToDB(db, board.id, state);
|
|
1206
|
+
for (const edge of state.edges) {
|
|
1207
|
+
if (edge.toBoardId && !edge.id.startsWith('codegraph-') && !edge.id.startsWith('boardmap-'))
|
|
1208
|
+
(invalidateTarget ?? ((boardId, nodeId) => invalidateLinksInDB(db, boardId, nodeId)))(edge.toBoardId, edge.to);
|
|
1209
|
+
}
|
|
1210
|
+
for (const node of state.nodes) {
|
|
1211
|
+
for (const { target } of node.type === 'markdown' ? wikiLinksFromData(node.data) : []) {
|
|
1212
|
+
if (target?.cardId && target.boardId !== board.id)
|
|
1213
|
+
(invalidateTarget ?? ((boardId, nodeId) => invalidateLinksInDB(db, boardId, nodeId)))(
|
|
1214
|
+
target.boardId,
|
|
1215
|
+
target.cardId,
|
|
1216
|
+
);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1052
1219
|
updateBoardInDB(db, board.id, { readmeNodeId });
|
|
1053
1220
|
return { ...board, nodeCount: state.nodes.length, readmeNodeId };
|
|
1054
1221
|
})();
|
|
1055
1222
|
}
|
|
1056
1223
|
|
|
1224
|
+
/** Derived incoming-link metadata only: the target owns its clock and its body. */
|
|
1225
|
+
export function invalidateLinksInDB(db: Database, boardId: string, nodeId: string): number | undefined {
|
|
1226
|
+
return db.transaction(() => {
|
|
1227
|
+
const node = db
|
|
1228
|
+
.query<{ links_revision: number; content_revision: number }, [string, string]>(
|
|
1229
|
+
'SELECT links_revision, content_revision FROM nodes WHERE board_id = ? AND id = ?',
|
|
1230
|
+
)
|
|
1231
|
+
.get(boardId, nodeId);
|
|
1232
|
+
if (!node || !getBoardFromDB(db, boardId)) return;
|
|
1233
|
+
const raw = db
|
|
1234
|
+
.query<{ value: string }, [string, string]>('SELECT value FROM ax_state WHERE board_id = ? AND key = ?')
|
|
1235
|
+
.get(boardId, 'revision')?.value;
|
|
1236
|
+
const state: PersistedRevisionState = raw ? JSON.parse(raw) : { revision: 0, floor: 0, tombstones: [] };
|
|
1237
|
+
state.revision = Math.max(state.revision, node.links_revision, node.content_revision) + 1;
|
|
1238
|
+
db.run('UPDATE nodes SET links_revision = ? WHERE board_id = ? AND id = ?', [state.revision, boardId, nodeId]);
|
|
1239
|
+
db.run(
|
|
1240
|
+
'INSERT INTO ax_state (board_id, key, value) VALUES (?, ?, ?) ON CONFLICT(board_id, key) DO UPDATE SET value = excluded.value',
|
|
1241
|
+
[boardId, 'revision', JSON.stringify(state)],
|
|
1242
|
+
);
|
|
1243
|
+
return state.revision;
|
|
1244
|
+
})();
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
/** A target's appearance/disappearance changes its incoming saved relations on source cards. */
|
|
1248
|
+
export function invalidateIncomingLinksInDB(
|
|
1249
|
+
db: Database,
|
|
1250
|
+
boardId: string,
|
|
1251
|
+
nodeId: string | null,
|
|
1252
|
+
invalidate: (boardId: string, nodeId: string) => void,
|
|
1253
|
+
): void {
|
|
1254
|
+
const sources = new Set<string>();
|
|
1255
|
+
for (const edge of db
|
|
1256
|
+
.query<{ id: string; board_id: string; from_node: string }, [string, string, string | null, string | null]>(
|
|
1257
|
+
'SELECT id, board_id, from_node FROM edges WHERE to_board_id = ? AND board_id <> ? AND (? IS NULL OR to_node = ?)',
|
|
1258
|
+
)
|
|
1259
|
+
.all(boardId, boardId, nodeId, nodeId)) {
|
|
1260
|
+
if (edge.id.startsWith('codegraph-') || edge.id.startsWith('boardmap-')) continue;
|
|
1261
|
+
const key = JSON.stringify([edge.board_id, edge.from_node]);
|
|
1262
|
+
if (sources.has(key)) continue;
|
|
1263
|
+
sources.add(key);
|
|
1264
|
+
invalidate(edge.board_id, edge.from_node);
|
|
1265
|
+
}
|
|
1266
|
+
for (const source of db
|
|
1267
|
+
.query<{ board_id: string; id: string }, [string, string, string | null, string | null]>(
|
|
1268
|
+
`SELECT DISTINCT n.board_id, n.id FROM nodes n, json_each(n.data, '$.wikiLinks') link
|
|
1269
|
+
WHERE n.type = 'markdown' AND n.board_id <> ?
|
|
1270
|
+
AND json_extract(link.value, '$.target.boardId') = ?
|
|
1271
|
+
AND (? IS NULL OR json_extract(link.value, '$.target.cardId') = ?)`,
|
|
1272
|
+
)
|
|
1273
|
+
.all(boardId, boardId, nodeId, nodeId)) {
|
|
1274
|
+
const key = JSON.stringify([source.board_id, source.id]);
|
|
1275
|
+
if (sources.has(key)) continue;
|
|
1276
|
+
sources.add(key);
|
|
1277
|
+
invalidate(source.board_id, source.id);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1057
1281
|
/** Deletes a board with its rows and its snapshots. */
|
|
1058
|
-
export function deleteBoardFromDB(
|
|
1282
|
+
export function deleteBoardFromDB(
|
|
1283
|
+
db: Database,
|
|
1284
|
+
id: string,
|
|
1285
|
+
invalidateTarget?: (boardId: string, nodeId: string) => void,
|
|
1286
|
+
): boolean {
|
|
1059
1287
|
const transaction = db.transaction(() => {
|
|
1060
|
-
|
|
1288
|
+
invalidateIncomingLinksInDB(
|
|
1289
|
+
db,
|
|
1290
|
+
id,
|
|
1291
|
+
null,
|
|
1292
|
+
invalidateTarget ?? ((boardId, nodeId) => invalidateLinksInDB(db, boardId, nodeId)),
|
|
1293
|
+
);
|
|
1294
|
+
for (const edge of db
|
|
1295
|
+
.query<{ id: string; to_board_id: string; to_node: string }, [string]>(
|
|
1296
|
+
'SELECT id, to_board_id, to_node FROM edges WHERE board_id = ? AND to_board_id IS NOT NULL',
|
|
1297
|
+
)
|
|
1298
|
+
.all(id)) {
|
|
1299
|
+
if (edge.to_board_id !== id && !edge.id.startsWith('codegraph-') && !edge.id.startsWith('boardmap-'))
|
|
1300
|
+
(invalidateTarget ?? ((boardId, nodeId) => invalidateLinksInDB(db, boardId, nodeId)))(
|
|
1301
|
+
edge.to_board_id,
|
|
1302
|
+
edge.to_node,
|
|
1303
|
+
);
|
|
1304
|
+
}
|
|
1305
|
+
for (const node of db
|
|
1306
|
+
.query<{ data: string }, [string]>("SELECT data FROM nodes WHERE board_id = ? AND type = 'markdown'")
|
|
1307
|
+
.all(id)) {
|
|
1308
|
+
for (const { target } of wikiLinksFromData(JSON.parse(node.data))) {
|
|
1309
|
+
if (target?.cardId && target.boardId !== id)
|
|
1310
|
+
(invalidateTarget ?? ((boardId, nodeId) => invalidateLinksInDB(db, boardId, nodeId)))(
|
|
1311
|
+
target.boardId,
|
|
1312
|
+
target.cardId,
|
|
1313
|
+
);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
for (const table of [
|
|
1317
|
+
'nodes',
|
|
1318
|
+
'edges',
|
|
1319
|
+
'annotations',
|
|
1320
|
+
'context_pins',
|
|
1321
|
+
'ax_state',
|
|
1322
|
+
'board_pins',
|
|
1323
|
+
'node_creation_requests',
|
|
1324
|
+
]) {
|
|
1061
1325
|
db.run(`DELETE FROM ${table} WHERE board_id = ?`, [id]);
|
|
1062
1326
|
}
|
|
1063
1327
|
const snapshotIds = db
|
|
@@ -1162,8 +1426,8 @@ export function saveSnapshotToDB(
|
|
|
1162
1426
|
|
|
1163
1427
|
// Insert snapshot nodes
|
|
1164
1428
|
const insertNode = db.prepare(
|
|
1165
|
-
`INSERT INTO snapshot_nodes (snapshot_id, id, type, pos_x, pos_y, width, height, z_index, collapsed, pinned, data, attribution, content_revision)
|
|
1166
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1429
|
+
`INSERT INTO snapshot_nodes (snapshot_id, id, type, pos_x, pos_y, width, height, z_index, collapsed, pinned, data, attribution, content_revision, links_revision)
|
|
1430
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1167
1431
|
);
|
|
1168
1432
|
for (const node of state.nodes) {
|
|
1169
1433
|
insertNode.run(
|
|
@@ -1178,15 +1442,21 @@ export function saveSnapshotToDB(
|
|
|
1178
1442
|
node.collapsed ? 1 : 0,
|
|
1179
1443
|
node.pinned ? 1 : 0,
|
|
1180
1444
|
JSON.stringify(node.data),
|
|
1181
|
-
JSON.stringify({
|
|
1445
|
+
JSON.stringify({
|
|
1446
|
+
createdBy: node.createdBy,
|
|
1447
|
+
lastEditedBy: node.lastEditedBy,
|
|
1448
|
+
contentChangedAt: node.contentChangedAt,
|
|
1449
|
+
freshnessStartedAt: node.freshnessStartedAt,
|
|
1450
|
+
}),
|
|
1182
1451
|
node.contentRevision ?? 0,
|
|
1452
|
+
node.linksRevision ?? 0,
|
|
1183
1453
|
);
|
|
1184
1454
|
}
|
|
1185
1455
|
|
|
1186
1456
|
// Insert snapshot edges
|
|
1187
1457
|
const insertEdge = db.prepare(
|
|
1188
|
-
`INSERT INTO snapshot_edges (snapshot_id, id, from_node, to_node, type, label, style, animated)
|
|
1189
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1458
|
+
`INSERT INTO snapshot_edges (snapshot_id, id, from_node, to_node, type, label, style, animated, revision, attribution, to_board_id, kind, reason)
|
|
1459
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1190
1460
|
);
|
|
1191
1461
|
for (const edge of state.edges) {
|
|
1192
1462
|
insertEdge.run(
|
|
@@ -1198,6 +1468,11 @@ export function saveSnapshotToDB(
|
|
|
1198
1468
|
edge.label ?? null,
|
|
1199
1469
|
edge.style ?? null,
|
|
1200
1470
|
edge.animated ? 1 : 0,
|
|
1471
|
+
edge.revision ?? 0,
|
|
1472
|
+
edgeAttribution(edge),
|
|
1473
|
+
edge.toBoardId ?? null,
|
|
1474
|
+
edge.kind ?? null,
|
|
1475
|
+
edge.reason ?? null,
|
|
1201
1476
|
);
|
|
1202
1477
|
}
|
|
1203
1478
|
|
|
@@ -1303,13 +1578,14 @@ export function loadSnapshotFromDB(
|
|
|
1303
1578
|
data: string;
|
|
1304
1579
|
attribution: string;
|
|
1305
1580
|
content_revision: number;
|
|
1581
|
+
links_revision: number;
|
|
1306
1582
|
}
|
|
1307
1583
|
const nodeRows = db
|
|
1308
1584
|
.query<NodeRow, [string]>('SELECT * FROM snapshot_nodes WHERE snapshot_id = ?')
|
|
1309
1585
|
.all(snapshotRow.id);
|
|
1310
1586
|
const nodes: CanvasNodeState[] = nodeRows.map((row) => {
|
|
1311
1587
|
const attribution = JSON.parse(row.attribution || '{}') as Partial<
|
|
1312
|
-
Pick<CanvasNodeState, 'createdBy' | 'lastEditedBy'>
|
|
1588
|
+
Pick<CanvasNodeState, 'createdBy' | 'lastEditedBy' | 'contentChangedAt' | 'freshnessStartedAt'>
|
|
1313
1589
|
>;
|
|
1314
1590
|
return {
|
|
1315
1591
|
id: row.id,
|
|
@@ -1322,7 +1598,10 @@ export function loadSnapshotFromDB(
|
|
|
1322
1598
|
data: JSON.parse(row.data) as Record<string, unknown>,
|
|
1323
1599
|
createdBy: attribution.createdBy,
|
|
1324
1600
|
lastEditedBy: attribution.lastEditedBy,
|
|
1601
|
+
contentChangedAt: attribution.contentChangedAt,
|
|
1602
|
+
freshnessStartedAt: attribution.freshnessStartedAt,
|
|
1325
1603
|
contentRevision: row.content_revision,
|
|
1604
|
+
linksRevision: row.links_revision,
|
|
1326
1605
|
};
|
|
1327
1606
|
});
|
|
1328
1607
|
|
|
@@ -1331,10 +1610,15 @@ export function loadSnapshotFromDB(
|
|
|
1331
1610
|
id: string;
|
|
1332
1611
|
from_node: string;
|
|
1333
1612
|
to_node: string;
|
|
1613
|
+
to_board_id: string | null;
|
|
1334
1614
|
type: string;
|
|
1615
|
+
kind: string | null;
|
|
1616
|
+
reason: string | null;
|
|
1335
1617
|
label: string | null;
|
|
1336
1618
|
style: string | null;
|
|
1337
1619
|
animated: number;
|
|
1620
|
+
revision: number;
|
|
1621
|
+
attribution: string;
|
|
1338
1622
|
}
|
|
1339
1623
|
const edgeRows = db
|
|
1340
1624
|
.query<EdgeRow, [string]>('SELECT * FROM snapshot_edges WHERE snapshot_id = ?')
|
|
@@ -1343,10 +1627,15 @@ export function loadSnapshotFromDB(
|
|
|
1343
1627
|
id: row.id,
|
|
1344
1628
|
from: row.from_node,
|
|
1345
1629
|
to: row.to_node,
|
|
1630
|
+
...(row.to_board_id ? { toBoardId: row.to_board_id } : {}),
|
|
1346
1631
|
type: row.type as CanvasEdge['type'],
|
|
1632
|
+
...(row.kind ? { kind: row.kind } : {}),
|
|
1633
|
+
...(row.reason ? { reason: row.reason } : {}),
|
|
1347
1634
|
...(row.label ? { label: row.label } : {}),
|
|
1348
1635
|
...(row.style ? { style: row.style as CanvasEdge['style'] } : {}),
|
|
1349
1636
|
...(row.animated ? { animated: true } : {}),
|
|
1637
|
+
...(row.revision ? { revision: row.revision } : {}),
|
|
1638
|
+
...(JSON.parse(row.attribution || '{}') as EdgeAttribution),
|
|
1350
1639
|
}));
|
|
1351
1640
|
|
|
1352
1641
|
// Load snapshot annotations
|