pmx-canvas 0.9.2 → 0.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/extensions/pmx-canvas/extension.mjs +9 -3
- package/.github/extensions/pmx-canvas/runtime.json +1 -1
- package/CHANGELOG.md +12 -0
- package/dist/canvas/global.css +12 -17
- package/dist/canvas/index.js +58 -58
- package/dist/canvas/mcp-app.html +75 -80
- package/dist/types/client/canvas/submit-without-navigation.d.ts +3 -0
- package/dist/types/client/state/intent-bridge.d.ts +0 -3
- package/dist/types/client/state/presence-store.d.ts +3 -3
- package/dist/types/client/types.d.ts +1 -1
- package/dist/types/server/canvas-state.d.ts +2 -0
- package/dist/types/server/index.d.ts +2 -2
- package/dist/types/server/knowledge-lifecycle.d.ts +11 -2
- package/dist/types/server/lifecycle-status.d.ts +3 -1
- package/dist/types/server/tour-control.d.ts +6 -3
- package/docs/cli.md +10 -1
- package/docs/design/Chrome.dc.html +1 -0
- package/docs/design.md +3 -1
- package/docs/mcp.md +5 -1
- package/docs/node-types.md +3 -0
- package/docs/plans/plan-019-post-0.9-roadmap.md +68 -9
- package/docs/sdk.md +7 -0
- package/package.json +1 -1
- package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/extension.mjs +9 -3
- 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/skills/pmx-canvas/SKILL.md +4 -4
- package/skills/pmx-canvas/references/github-copilot-app-adapter.md +2 -2
- package/skills/pmx-canvas/references/installing-pmx-canvas.md +6 -6
- package/src/client/canvas/BoardCopyDialog.tsx +2 -1
- package/src/client/canvas/KnowledgeDialog.tsx +3 -0
- package/src/client/canvas/LifecyclePanel.tsx +4 -0
- package/src/client/canvas/RelationPresetsDialog.tsx +2 -0
- package/src/client/canvas/submit-without-navigation.ts +14 -0
- package/src/client/state/canvas-store.ts +12 -24
- package/src/client/state/intent-bridge.ts +0 -6
- package/src/client/state/presence-store.ts +3 -3
- package/src/client/state/sse-bridge.ts +11 -6
- package/src/client/theme/global.css +12 -17
- package/src/server/canvas-state.ts +10 -0
- package/src/server/index.ts +10 -30
- package/src/server/knowledge-lifecycle.ts +66 -0
- package/src/server/lifecycle-status.ts +79 -7
- package/src/server/operations/ops/lifecycle.ts +3 -7
- package/src/server/operations/ops/nodes.ts +2 -5
- package/src/server/operations/ops/viewport.ts +3 -7
- package/src/server/server.ts +13 -10
- package/src/server/tour-control.ts +17 -8
|
@@ -768,31 +768,19 @@ export function animateViewport(
|
|
|
768
768
|
duration = 300,
|
|
769
769
|
options: { recordHistory?: boolean } = {},
|
|
770
770
|
): void {
|
|
771
|
-
if (animationId !== null) cancelAnimationFrame(animationId);
|
|
772
|
-
|
|
773
771
|
const from = { ...viewport.value };
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
if (t < 1) {
|
|
788
|
-
animationId = requestAnimationFrame(tick);
|
|
789
|
-
} else {
|
|
790
|
-
animationId = null;
|
|
791
|
-
commitViewport(target, options);
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
animationId = requestAnimationFrame(tick);
|
|
772
|
+
animateCameraPath(
|
|
773
|
+
(t) => {
|
|
774
|
+
const e = easeOutCubic(t);
|
|
775
|
+
return {
|
|
776
|
+
x: from.x + (target.x - from.x) * e,
|
|
777
|
+
y: from.y + (target.y - from.y) * e,
|
|
778
|
+
scale: from.scale + (target.scale - from.scale) * e,
|
|
779
|
+
};
|
|
780
|
+
},
|
|
781
|
+
duration,
|
|
782
|
+
() => commitViewport(target, options),
|
|
783
|
+
);
|
|
796
784
|
}
|
|
797
785
|
|
|
798
786
|
/**
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { canvasArea, canvasFitInsets } from '../canvas/canvas-area';
|
|
2
2
|
import { showToast } from './attention-bridge';
|
|
3
3
|
import { isHostedWorkbench, workbenchFetch } from './workbench-transport';
|
|
4
|
-
import type { AgentPresenceSnapshot } from '../../shared/agent-presence.js';
|
|
5
4
|
import type { CanvasEdge, CanvasNodeState } from '../types';
|
|
6
5
|
import { addEdge, canvasInteractionEpoch, nodes } from './canvas-store';
|
|
7
6
|
import { activeBoardId, boardsLoaded } from './boards-store';
|
|
@@ -401,11 +400,6 @@ export async function fetchAxSurfaceState(): Promise<unknown> {
|
|
|
401
400
|
return requestJson<unknown>('fetchAxSurfaceState', '/api/canvas/ax/surface-snapshot', null);
|
|
402
401
|
}
|
|
403
402
|
|
|
404
|
-
/** Connect-time agent-presence read; live updates arrive as `agent-presence` SSE frames. */
|
|
405
|
-
export async function fetchAgentPresence(): Promise<Partial<AgentPresenceSnapshot> | null> {
|
|
406
|
-
return requestJson<Partial<AgentPresenceSnapshot> | null>('fetchAgentPresence', '/api/canvas/ax/presence', null);
|
|
407
|
-
}
|
|
408
|
-
|
|
409
403
|
/** Ask the server to open a node's surface in the system browser. */
|
|
410
404
|
export async function openNodeInSystemBrowserRequest(
|
|
411
405
|
nodeId: string,
|
|
@@ -12,9 +12,9 @@ import {
|
|
|
12
12
|
} from '../../shared/agent-presence.js';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* Agent presence (rail-chrome-v2 phase 2). Fed by the server's
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* Agent presence (rail-chrome-v2 phase 2). Fed by the server's ordered
|
|
16
|
+
* `agent-presence` frames: a connect snapshot and every change, including TTL
|
|
17
|
+
* expiry, over SSE or polling. This store never runs its own expiry ticker.
|
|
18
18
|
*
|
|
19
19
|
* `sessionActive` is the ONE selector every agent surface reads — the session
|
|
20
20
|
* panel, command bar, presence layer, and top-bar chip all mount on it.
|
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
axSurfaceState,
|
|
15
15
|
bringToFront,
|
|
16
16
|
cancelViewportAnimation,
|
|
17
|
-
commitViewport,
|
|
18
17
|
canvasTheme,
|
|
19
18
|
connectionStatus,
|
|
20
19
|
contextPinsRevision,
|
|
@@ -50,12 +49,12 @@ import {
|
|
|
50
49
|
import { applyContextStatus, refreshContextStatus } from './context-status-store';
|
|
51
50
|
import { refreshContextChip } from './context-chip-store';
|
|
52
51
|
import {
|
|
53
|
-
fetchAgentPresence,
|
|
54
52
|
fetchAxSurfaceState,
|
|
55
53
|
fetchCanvasState,
|
|
56
54
|
reportClientViewportSize,
|
|
57
55
|
requestJson,
|
|
58
56
|
tabAgentKey,
|
|
57
|
+
updateViewportFromClient,
|
|
59
58
|
} from './intent-bridge';
|
|
60
59
|
import { agentActivity, applyPresenceSnapshot, sessionActive } from './presence-store';
|
|
61
60
|
import { applyHumanSnapshot, humanClientId, startHumanPresence, stopHumanPresence } from './human-store';
|
|
@@ -577,9 +576,8 @@ function handleConnected(data: Record<string, unknown>): void {
|
|
|
577
576
|
// Reconnect marker for holders of server-minted URLs (Finding S).
|
|
578
577
|
workbenchConnectionEpoch.value += 1;
|
|
579
578
|
reloadIfServerUpgraded(data.version, data.bundleStamp);
|
|
580
|
-
//
|
|
581
|
-
//
|
|
582
|
-
void fetchAgentPresence().then(applyPresenceSnapshot);
|
|
579
|
+
// Both transports send an ordered `agent-presence` connect snapshot, then
|
|
580
|
+
// live frames. A separate HTTP read can arrive late and overwrite newer state.
|
|
583
581
|
void refreshContextStatus();
|
|
584
582
|
refreshContextChip();
|
|
585
583
|
// Human collaborators (phase 8): announce this tab and read who else is here.
|
|
@@ -1123,7 +1121,14 @@ function handleCanvasCameraMove(data: Record<string, unknown>): void {
|
|
|
1123
1121
|
try {
|
|
1124
1122
|
// The presentation camera is local; otherwise the end camera becomes the
|
|
1125
1123
|
// board viewport — unrecorded, so a camera move is never an undo step.
|
|
1126
|
-
playStop(
|
|
1124
|
+
playStop(
|
|
1125
|
+
stop.data,
|
|
1126
|
+
presenting.value
|
|
1127
|
+
? undefined
|
|
1128
|
+
: (end) => {
|
|
1129
|
+
void updateViewportFromClient(end, { recordHistory: false });
|
|
1130
|
+
},
|
|
1131
|
+
);
|
|
1127
1132
|
} catch {
|
|
1128
1133
|
// Target not in this tab's layout yet: take the server's resolved camera.
|
|
1129
1134
|
const fallback = cameraSchema.safeParse(data.viewport);
|
|
@@ -4050,8 +4050,14 @@ kbd.toolbar-menu-hint {
|
|
|
4050
4050
|
}
|
|
4051
4051
|
|
|
4052
4052
|
/* ── Markdown reader mode (expanded view) ──────────────────── */
|
|
4053
|
+
.md-expanded-document {
|
|
4054
|
+
display: flex;
|
|
4055
|
+
flex-direction: column;
|
|
4056
|
+
}
|
|
4057
|
+
|
|
4053
4058
|
.md-reader {
|
|
4054
|
-
|
|
4059
|
+
flex: 1;
|
|
4060
|
+
min-height: 0;
|
|
4055
4061
|
overflow-y: auto;
|
|
4056
4062
|
display: flex;
|
|
4057
4063
|
justify-content: center;
|
|
@@ -4205,11 +4211,11 @@ kbd.toolbar-menu-hint {
|
|
|
4205
4211
|
background: var(--c-surface-subtle);
|
|
4206
4212
|
}
|
|
4207
4213
|
|
|
4208
|
-
/* Source
|
|
4214
|
+
/* Source stays below the scroll viewport, never over the document text. */
|
|
4209
4215
|
.md-edit-fab {
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4216
|
+
align-self: flex-end;
|
|
4217
|
+
flex-shrink: 0;
|
|
4218
|
+
margin: 16px;
|
|
4213
4219
|
padding: 8px 18px;
|
|
4214
4220
|
font-size: 13px;
|
|
4215
4221
|
font-weight: 600;
|
|
@@ -5991,21 +5997,10 @@ html.is-node-resizing .ext-app-preview-catcher {
|
|
|
5991
5997
|
/* Its sticky/backdrop-filter context must paint hints above the Source FAB. */
|
|
5992
5998
|
z-index: 2;
|
|
5993
5999
|
}
|
|
5994
|
-
.expanded-overlay-panel:has(.ax-step-dock) .md-expanded-document {
|
|
5995
|
-
display: flex;
|
|
5996
|
-
flex-direction: column;
|
|
5997
|
-
}
|
|
5998
|
-
.expanded-overlay-panel:has(.ax-step-dock) .md-reader {
|
|
5999
|
-
flex: 1;
|
|
6000
|
-
min-height: 0;
|
|
6001
|
-
}
|
|
6002
6000
|
.expanded-overlay-panel:has(.ax-step-dock) .md-edit-fab {
|
|
6003
6001
|
/* Keep Source and the document out of the bottom dock's hint area. */
|
|
6004
|
-
position: static;
|
|
6005
6002
|
order: -1;
|
|
6006
|
-
|
|
6007
|
-
flex-shrink: 0;
|
|
6008
|
-
margin-bottom: 16px;
|
|
6003
|
+
margin: 0 0 16px;
|
|
6009
6004
|
}
|
|
6010
6005
|
.ax-step-dock:has(.toolbar-tooltip-anchor:hover) .toolbar-tooltip-anchor:not(:hover) > :focus-visible + .toolbar-tooltip {
|
|
6011
6006
|
/* Pointer exploration temporarily takes precedence without moving keyboard focus. */
|
|
@@ -2863,6 +2863,16 @@ class CanvasStateManager {
|
|
|
2863
2863
|
return node ? structuredClone(this.nodeForRead(node)) : undefined;
|
|
2864
2864
|
}
|
|
2865
2865
|
|
|
2866
|
+
/** Camera targets need geometry, not hydrated documents, frames or image blobs. */
|
|
2867
|
+
nodeGeometry(): Array<Pick<CanvasNodeState, 'id' | 'type' | 'position' | 'size'>> {
|
|
2868
|
+
return Array.from(this.nodes.values(), ({ id, type, position, size }) => ({
|
|
2869
|
+
id,
|
|
2870
|
+
type,
|
|
2871
|
+
position: { ...position },
|
|
2872
|
+
size: { ...size },
|
|
2873
|
+
}));
|
|
2874
|
+
}
|
|
2875
|
+
|
|
2866
2876
|
/** Who made and last edited each card, and its content revision — no content, no copies. */
|
|
2867
2877
|
nodeStamps(): Array<Pick<CanvasNodeState, 'id' | 'createdBy' | 'lastEditedBy' | 'contentRevision'>> {
|
|
2868
2878
|
return [...this.nodes.values()].map(({ id, createdBy, lastEditedBy, contentRevision }) => ({
|
package/src/server/index.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
|
-
import {
|
|
3
|
-
import { exitTour, goToTourStop, moveCamera, type TourStep, type TourStepResult } from './tour-control.js';
|
|
2
|
+
import type { Camera, Tour, TourStop } from '../shared/tour.js';
|
|
3
|
+
import { currentTour, exitTour, goToTourStop, moveCamera, type TourStep, type TourStepResult } from './tour-control.js';
|
|
4
4
|
export type { Tour, TourStop } from '../shared/tour.js';
|
|
5
5
|
export type { TourStep, TourStepResult } from './tour-control.js';
|
|
6
6
|
import { agentPresence } from './agent-presence.js';
|
|
7
7
|
import { checkFenceTarget, type FenceTarget } from './scope-fence.js';
|
|
8
8
|
import type { AgentPhase, AgentPresence, AgentPresenceSnapshot } from '../shared/agent-presence.js';
|
|
9
|
-
import { canvasState
|
|
10
|
-
import type { CanvasAnnotation, CanvasNodeState, CanvasEdge, CanvasLayout
|
|
9
|
+
import { canvasState } from './canvas-state.js';
|
|
10
|
+
import type { CanvasAnnotation, CanvasNodeState, CanvasEdge, CanvasLayout } from './canvas-state.js';
|
|
11
11
|
import { buildCanvasAxContext } from './ax-context.js';
|
|
12
12
|
import { applyAxInteraction, type AxInteractionInput, type AxInteractionPublicResult } from './ax-interaction.js';
|
|
13
13
|
import { intentRegistry } from './intent-registry.js';
|
|
@@ -46,12 +46,12 @@ import { workbenchToken } from './workbench-auth.js';
|
|
|
46
46
|
import type { BackupFile, BackupStatus } from './backup.js';
|
|
47
47
|
import type { ExportManifest } from './board-export.js';
|
|
48
48
|
import { onFileNodeChanged } from './file-watcher.js';
|
|
49
|
-
import { findOpenCanvasPosition
|
|
49
|
+
import { findOpenCanvasPosition } from './placement.js';
|
|
50
50
|
import { overlapsAny } from '../shared/placement.js';
|
|
51
51
|
import { buildSpatialContext } from './spatial-analysis.js';
|
|
52
52
|
import { searchCanvasNodes } from './operations/ops/query.js';
|
|
53
53
|
import { mutationHistory, diffLayouts, formatDiff } from './mutation-history.js';
|
|
54
|
-
import {
|
|
54
|
+
import { buildCodeGraphSummary, formatCodeGraph } from './code-graph.js';
|
|
55
55
|
import {
|
|
56
56
|
addCanvasNode,
|
|
57
57
|
addCanvasEdge,
|
|
@@ -90,12 +90,7 @@ import { validateCanvasLayout } from './canvas-validation.js';
|
|
|
90
90
|
import { describeCanvasSchema, validateStructuredCanvasPayload } from './canvas-schema.js';
|
|
91
91
|
import { serializeCanvasNode, type SerializedCanvasNode } from './canvas-serialization.js';
|
|
92
92
|
import { contextReadFromPayload } from './context-reads.js';
|
|
93
|
-
import {
|
|
94
|
-
buildHtmlPrimitive,
|
|
95
|
-
getHtmlPrimitiveSemanticMetadata,
|
|
96
|
-
isHtmlPrimitiveKind,
|
|
97
|
-
listHtmlPrimitiveDescriptors,
|
|
98
|
-
} from './html-primitives.js';
|
|
93
|
+
import { buildHtmlPrimitive, getHtmlPrimitiveSemanticMetadata } from './html-primitives.js';
|
|
99
94
|
import type { HtmlPrimitiveKind } from './html-primitives.js';
|
|
100
95
|
import {
|
|
101
96
|
buildWebArtifactOnCanvas,
|
|
@@ -104,15 +99,7 @@ import {
|
|
|
104
99
|
} from './web-artifacts.js';
|
|
105
100
|
import { closeMcpAppSession, type ExternalMcpTransportConfig } from './mcp-app-runtime.js';
|
|
106
101
|
import type { DiagramPresetOpenInput } from './diagram-presets.js';
|
|
107
|
-
import {
|
|
108
|
-
buildGraphSpec,
|
|
109
|
-
GRAPH_NODE_SIZE,
|
|
110
|
-
JSON_RENDER_NODE_SIZE,
|
|
111
|
-
normalizeAndValidateJsonRenderSpec,
|
|
112
|
-
type GraphNodeInput,
|
|
113
|
-
type JsonRenderNodeInput,
|
|
114
|
-
type JsonRenderSpec,
|
|
115
|
-
} from '../json-render/server.js';
|
|
102
|
+
import type { GraphNodeInput, JsonRenderNodeInput, JsonRenderSpec } from '../json-render/server.js';
|
|
116
103
|
import {
|
|
117
104
|
startCanvasServer,
|
|
118
105
|
stopCanvasServer,
|
|
@@ -126,15 +113,8 @@ import {
|
|
|
126
113
|
screenshotCanvasAutomationWebView,
|
|
127
114
|
emitPrimaryWorkbenchEvent,
|
|
128
115
|
setPrimaryWorkbenchCanvasPromptHandler,
|
|
129
|
-
setPrimaryWorkbenchAutoOpenEnabled,
|
|
130
|
-
consumePrimaryWorkbenchIntents,
|
|
131
|
-
} from './server.js';
|
|
132
|
-
import type {
|
|
133
|
-
CanvasAutomationWebViewOptions,
|
|
134
|
-
CanvasAutomationWebViewStatus,
|
|
135
|
-
PrimaryWorkbenchCanvasPromptRequest,
|
|
136
|
-
PrimaryWorkbenchIntent,
|
|
137
116
|
} from './server.js';
|
|
117
|
+
import type { CanvasAutomationWebViewOptions, CanvasAutomationWebViewStatus } from './server.js';
|
|
138
118
|
|
|
139
119
|
/**
|
|
140
120
|
* Node object returned by the SDK's create/get methods. It is the fully
|
|
@@ -633,7 +613,7 @@ export class PmxCanvas extends EventEmitter {
|
|
|
633
613
|
}
|
|
634
614
|
|
|
635
615
|
getTour(): Tour {
|
|
636
|
-
return
|
|
616
|
+
return currentTour().tour;
|
|
637
617
|
}
|
|
638
618
|
|
|
639
619
|
setTour(tour: Tour | null): void {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Database } from 'bun:sqlite';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { normalizeBoardFolder } from '../shared/boards.js';
|
|
4
|
+
import { memoryKey, type MemoryTarget } from '../shared/memory-graph.js';
|
|
4
5
|
import { currentActor, type ActorAttribution } from './attribution.js';
|
|
5
6
|
import { OperationError } from './operations/types.js';
|
|
6
7
|
import type { LifecycleQualification } from '../shared/lifecycle-status.js';
|
|
@@ -47,6 +48,16 @@ export interface ReviewRecord {
|
|
|
47
48
|
sourceRevisions?: Record<string, number | null>;
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
// SQLite trim defaults to ASCII space; match JavaScript evidence.trim(), including Unicode whitespace.
|
|
52
|
+
const evidenceWhitespace =
|
|
53
|
+
'\u0009\u000a\u000b\u000c\u000d\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000\ufeff';
|
|
54
|
+
const validationPredicate = `json_extract(value, '$.applied') = 1
|
|
55
|
+
AND json_extract(value, '$.outcome') IN ('retain', 'revise', 'externalize')
|
|
56
|
+
AND trim(json_extract(value, '$.evidence'), '${evidenceWhitespace}') <> ''`;
|
|
57
|
+
const retirementPredicate = `(json_extract(value, '$.outcome') = 'restore'
|
|
58
|
+
OR (json_extract(value, '$.applied') = 1 AND json_extract(value, '$.outcome') = 'retire'))`;
|
|
59
|
+
const indexedDatabases = new WeakSet<Database>();
|
|
60
|
+
|
|
50
61
|
export type KnowledgeInput =
|
|
51
62
|
| { action: 'list' }
|
|
52
63
|
| { action: 'category-set'; category: KnowledgeCategory }
|
|
@@ -240,6 +251,61 @@ export class KnowledgeLifecycle {
|
|
|
240
251
|
return resolved;
|
|
241
252
|
}
|
|
242
253
|
|
|
254
|
+
private indexedDb(): Database {
|
|
255
|
+
const db = this.db();
|
|
256
|
+
if (!indexedDatabases.has(db)) {
|
|
257
|
+
db.exec(`CREATE INDEX IF NOT EXISTS review_latest_validation ON review_records
|
|
258
|
+
(board_id, node_id, sequence DESC) WHERE ${validationPredicate};
|
|
259
|
+
CREATE INDEX IF NOT EXISTS review_latest_retirement ON review_records
|
|
260
|
+
(board_id, node_id, sequence DESC) WHERE ${retirementPredicate};
|
|
261
|
+
CREATE INDEX IF NOT EXISTS lifecycle_incoming_sources ON edges (to_board_id, to_node)
|
|
262
|
+
WHERE COALESCE(kind, type) IN ('derived-from', 'contradicts', 'supersedes');`);
|
|
263
|
+
indexedDatabases.add(db);
|
|
264
|
+
}
|
|
265
|
+
return db;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Stored owners of semantic sources; the caller overlays the active board's unsaved edges. */
|
|
269
|
+
incomingSourceBoards(target: MemoryTarget, activeBoardId: string | null): string[] {
|
|
270
|
+
return this.indexedDb()
|
|
271
|
+
.query<{ board_id: string }, [string, string | null, string | null]>(
|
|
272
|
+
`SELECT DISTINCT board_id FROM edges WHERE to_board_id = ? AND to_node = ?
|
|
273
|
+
AND board_id IS NOT ? AND COALESCE(kind, type) IN ('derived-from', 'contradicts', 'supersedes')`,
|
|
274
|
+
)
|
|
275
|
+
.all(target.boardId, target.cardId ?? null, activeBoardId)
|
|
276
|
+
.map((row) => row.board_id);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** One indexed latest validation and retirement per card, never its full audit history. */
|
|
280
|
+
reviewSummaries(targets: MemoryTarget[]): Map<string, { lastValidation: ReviewRecord | null; retired: boolean }> {
|
|
281
|
+
if (!targets.length) return new Map();
|
|
282
|
+
const rows = this.indexedDb()
|
|
283
|
+
.query<{ boardId: string; nodeId: string; validation: string | null; retirement: string | null }, [string]>(
|
|
284
|
+
`WITH targets AS (
|
|
285
|
+
SELECT json_extract(value, '$.boardId') AS boardId, json_extract(value, '$.cardId') AS nodeId
|
|
286
|
+
FROM json_each(?)
|
|
287
|
+
) SELECT boardId, nodeId,
|
|
288
|
+
(SELECT value FROM review_records WHERE board_id = boardId AND node_id = nodeId
|
|
289
|
+
AND ${validationPredicate} ORDER BY sequence DESC LIMIT 1) AS validation,
|
|
290
|
+
(SELECT json_extract(value, '$.outcome') FROM review_records WHERE board_id = boardId AND node_id = nodeId
|
|
291
|
+
AND ${retirementPredicate} ORDER BY sequence DESC LIMIT 1) AS retirement
|
|
292
|
+
FROM targets`,
|
|
293
|
+
)
|
|
294
|
+
.all(JSON.stringify(targets));
|
|
295
|
+
return new Map(
|
|
296
|
+
rows.map((row) => {
|
|
297
|
+
const record: ReviewRecord | null = row.validation ? JSON.parse(row.validation) : null;
|
|
298
|
+
return [
|
|
299
|
+
memoryKey({ boardId: row.boardId, cardId: row.nodeId }),
|
|
300
|
+
{
|
|
301
|
+
lastValidation: record ? { ...record, contentRevision: record.contentRevision ?? null } : null,
|
|
302
|
+
retired: row.retirement === 'retire',
|
|
303
|
+
},
|
|
304
|
+
];
|
|
305
|
+
}),
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
243
309
|
reviews(boardId: string, nodeId: string) {
|
|
244
310
|
const records = (
|
|
245
311
|
this.db()
|
|
@@ -25,8 +25,70 @@ export function lifecycleQualifications(
|
|
|
25
25
|
return lifecycleSnapshot(now, atUse).cards;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/** A card being inspected is in use, including its transitive sources and portal contents. */
|
|
29
|
+
export function lifecycleQualification(target: MemoryTarget, now = Date.now()): LifecycleQualification | undefined {
|
|
30
|
+
return lifecycleSnapshot(now, [target], target).cards[memoryKey(target)];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Read closure boards once; never activate them or flush the active board to discover edges. */
|
|
34
|
+
function dependencyClosure(target: MemoryTarget) {
|
|
35
|
+
const reads = new Map<string, ReturnType<typeof canvasState.readBoard>>();
|
|
36
|
+
const nodes = new Map<string, CanvasNodeState>();
|
|
37
|
+
const dependencies = new Map<string, MemoryTarget[]>();
|
|
38
|
+
const selected = new Set<string>();
|
|
39
|
+
const queue: MemoryTarget[] = [target];
|
|
40
|
+
const read = (boardId: string) => {
|
|
41
|
+
if (!reads.has(boardId)) {
|
|
42
|
+
const board = canvasState.readBoard(boardId, false);
|
|
43
|
+
reads.set(boardId, board);
|
|
44
|
+
for (const node of board?.layout.nodes ?? []) nodes.set(memoryKey({ boardId, cardId: node.id }), node);
|
|
45
|
+
for (const edge of board?.layout.edges ?? []) {
|
|
46
|
+
const end = relationSourceEnd(edge);
|
|
47
|
+
if (!end) continue;
|
|
48
|
+
const from = { boardId, cardId: edge.from };
|
|
49
|
+
const to = { boardId: edge.toBoardId ?? boardId, cardId: edge.to };
|
|
50
|
+
const key = memoryKey(end === 'from' ? to : from);
|
|
51
|
+
const sources = dependencies.get(key) ?? [];
|
|
52
|
+
sources.push(end === 'from' ? from : to);
|
|
53
|
+
dependencies.set(key, sources);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return reads.get(boardId);
|
|
57
|
+
};
|
|
58
|
+
const activeBoardId = canvasState.activeBoardId;
|
|
59
|
+
const incomingActive = new Set(
|
|
60
|
+
canvasState
|
|
61
|
+
.getEdges()
|
|
62
|
+
.flatMap((edge) =>
|
|
63
|
+
edge.toBoardId && relationSourceEnd(edge) === 'from'
|
|
64
|
+
? [memoryKey({ boardId: edge.toBoardId, cardId: edge.to })]
|
|
65
|
+
: [],
|
|
66
|
+
),
|
|
67
|
+
);
|
|
68
|
+
for (const current of queue) {
|
|
69
|
+
const key = memoryKey(current);
|
|
70
|
+
if (selected.has(key)) continue;
|
|
71
|
+
selected.add(key);
|
|
72
|
+
const board = read(current.boardId);
|
|
73
|
+
if (!current.cardId) {
|
|
74
|
+
for (const node of board?.layout.nodes ?? []) queue.push({ boardId: current.boardId, cardId: node.id });
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const node = nodes.get(key);
|
|
78
|
+
if (!node) continue;
|
|
79
|
+
const provenance = normalizeCanvasNodeData(node.type, node.data).provenance as CanvasNodeProvenance | undefined;
|
|
80
|
+
queue.push(...(provenance?.sources ?? []).map(({ boardId, cardId }) => ({ boardId, cardId })));
|
|
81
|
+
if (node.type === 'board' && typeof node.data.boardId === 'string') queue.push({ boardId: node.data.boardId });
|
|
82
|
+
const owners = new Set([current.boardId, ...canvasState.knowledge.incomingSourceBoards(current, activeBoardId)]);
|
|
83
|
+
if (activeBoardId && incomingActive.has(key)) owners.add(activeBoardId);
|
|
84
|
+
for (const owner of owners) read(owner);
|
|
85
|
+
queue.push(...(dependencies.get(key) ?? []));
|
|
86
|
+
}
|
|
87
|
+
return { reads, selected };
|
|
88
|
+
}
|
|
89
|
+
|
|
28
90
|
/** Compact queue metadata and qualifications come from the same authoritative read. */
|
|
29
|
-
export function lifecycleSnapshot(now = Date.now(), atUse: MemoryTarget[] = []) {
|
|
91
|
+
export function lifecycleSnapshot(now = Date.now(), atUse: MemoryTarget[] = [], only?: MemoryTarget) {
|
|
30
92
|
const result: Record<string, LifecycleQualification> = {};
|
|
31
93
|
const cards = new Map<string, CanvasNodeState>();
|
|
32
94
|
const validations = new Map<string, ReviewRecord | null>();
|
|
@@ -34,26 +96,36 @@ export function lifecycleSnapshot(now = Date.now(), atUse: MemoryTarget[] = [])
|
|
|
34
96
|
const portals = new Map<string, string>();
|
|
35
97
|
const policies = new Map<string, Lifecycle | null>();
|
|
36
98
|
const used = new Set<string>();
|
|
37
|
-
const
|
|
99
|
+
const boards = canvasState.listBoards();
|
|
100
|
+
const closure = only ? dependencyClosure(only) : null;
|
|
101
|
+
const reads = boards
|
|
102
|
+
.filter((board) => !closure || closure.reads.has(board.id))
|
|
103
|
+
.map((board) => ({
|
|
104
|
+
board,
|
|
105
|
+
read: closure ? closure.reads.get(board.id) : canvasState.readBoard(board.id, false),
|
|
106
|
+
}));
|
|
107
|
+
const targets = reads
|
|
108
|
+
.flatMap(({ board, read }) => (read?.layout.nodes ?? []).map((node) => ({ boardId: board.id, cardId: node.id })))
|
|
109
|
+
.filter((target) => !closure || closure.selected.has(memoryKey(target)));
|
|
110
|
+
const summaries = canvasState.databasePath ? canvasState.knowledge.reviewSummaries(targets) : null;
|
|
38
111
|
const catalog = canvasState.databasePath
|
|
39
112
|
? canvasState.knowledge.list()
|
|
40
113
|
: { categories: [], folders: [], relationPresets: [] };
|
|
41
114
|
const session =
|
|
42
115
|
agentPresence.reviewSessionStartedAt() ??
|
|
43
116
|
Date.parse(
|
|
44
|
-
|
|
117
|
+
boards.find((board) => board.id === canvasState.activeBoardId)?.lastOpenedAt ??
|
|
45
118
|
new Date(processStartedAt).toISOString(),
|
|
46
119
|
);
|
|
47
120
|
for (const { board, read } of reads) {
|
|
48
121
|
const seen = new Map(canvasState.getNodeReadStatus(board.id).map((status) => [status.nodeId, status]));
|
|
49
122
|
for (const node of read?.layout.nodes ?? []) {
|
|
50
123
|
const key = memoryKey({ boardId: board.id, cardId: node.id });
|
|
124
|
+
if (closure && !closure.selected.has(key)) continue;
|
|
51
125
|
cards.set(key, node);
|
|
52
126
|
const { lifecycle } = canvasState.knowledge.resolveCard(board.folder, node, catalog);
|
|
53
127
|
policies.set(key, lifecycle);
|
|
54
|
-
const { lastValidation, retired } =
|
|
55
|
-
? canvasState.knowledge.reviews(board.id, node.id)
|
|
56
|
-
: { lastValidation: null, retired: false };
|
|
128
|
+
const { lastValidation, retired } = summaries?.get(key) ?? { lastValidation: null, retired: false };
|
|
57
129
|
validations.set(key, lastValidation);
|
|
58
130
|
const lastRead = seen.get(node.id);
|
|
59
131
|
const pinned = read!.state.contextPins.includes(node.id);
|
|
@@ -218,7 +290,7 @@ export function lifecycleSnapshot(now = Date.now(), atUse: MemoryTarget[] = [])
|
|
|
218
290
|
for (const { board, read } of reads) {
|
|
219
291
|
for (const node of read?.layout.nodes ?? []) {
|
|
220
292
|
const status = result[memoryKey({ boardId: board.id, cardId: node.id })];
|
|
221
|
-
if (status.retired || !(status.due || status.sources.length)) continue;
|
|
293
|
+
if (!status || status.retired || !(status.due || status.sources.length)) continue;
|
|
222
294
|
reviewCards.push({
|
|
223
295
|
boardId: board.id,
|
|
224
296
|
cardId: node.id,
|
|
@@ -3,8 +3,7 @@ import { canvasState } from '../../canvas-state.js';
|
|
|
3
3
|
import { categorySchema, folderPropertiesSchema, lifecycleSchema } from '../../knowledge-lifecycle.js';
|
|
4
4
|
import { defineOperation, OperationError, type Operation } from '../types.js';
|
|
5
5
|
import { readTargetBoard } from './boards.js';
|
|
6
|
-
import {
|
|
7
|
-
import { memoryKey } from '../../../shared/memory-graph.js';
|
|
6
|
+
import { lifecycleQualification, lifecycleSnapshot } from '../../lifecycle-status.js';
|
|
8
7
|
import { folderAncestors } from '../../../shared/boards.js';
|
|
9
8
|
import { relationPresetSchema } from '../../../shared/relations.js';
|
|
10
9
|
|
|
@@ -155,9 +154,7 @@ export const lifecycleOperations: Operation[] = [
|
|
|
155
154
|
contentRevision: node.contentRevision ?? 0,
|
|
156
155
|
...canvasState.knowledge.resolveCard(folder, node),
|
|
157
156
|
...canvasState.knowledge.reviews(read.boardId, input.id),
|
|
158
|
-
qualification:
|
|
159
|
-
memoryKey({ boardId: read.boardId, cardId: input.id })
|
|
160
|
-
],
|
|
157
|
+
qualification: lifecycleQualification({ boardId: read.boardId, cardId: input.id }),
|
|
161
158
|
};
|
|
162
159
|
},
|
|
163
160
|
}),
|
|
@@ -216,8 +213,7 @@ export const lifecycleOperations: Operation[] = [
|
|
|
216
213
|
if (!canvasState.getNode(input.id) || !canvasState.activeBoardId)
|
|
217
214
|
throw new OperationError('Card not found.', 404);
|
|
218
215
|
const observations =
|
|
219
|
-
|
|
220
|
-
{};
|
|
216
|
+
lifecycleQualification({ boardId: canvasState.activeBoardId, cardId: input.id })?.observations ?? {};
|
|
221
217
|
const validates =
|
|
222
218
|
input.applied && input.evidence.trim() && ['retain', 'revise', 'externalize'].includes(input.outcome);
|
|
223
219
|
if (validates) {
|
|
@@ -55,8 +55,7 @@ import {
|
|
|
55
55
|
import { WEBPAGE_NODE_DEFAULT_SIZE, normalizeWebpageUrl } from '../../webpage-node.js';
|
|
56
56
|
import { defineOperation, OperationError, type Operation, type OperationContext } from '../types.js';
|
|
57
57
|
import { readTargetBoard } from './boards.js';
|
|
58
|
-
import {
|
|
59
|
-
import { memoryKey } from '../../../shared/memory-graph.js';
|
|
58
|
+
import { lifecycleQualification } from '../../lifecycle-status.js';
|
|
60
59
|
|
|
61
60
|
// ── Node types ────────────────────────────────────────────────
|
|
62
61
|
// Single definition site for the basic node-type list: drives the HTTP type
|
|
@@ -1005,9 +1004,7 @@ const nodeGetOperation = defineOperation<z.infer<typeof nodeGetSchema>, Serializ
|
|
|
1005
1004
|
const full = includeBlobs === true || includeBlobs === 'true';
|
|
1006
1005
|
const qualify = (node: CanvasNodeState, boardId: string | null) => ({
|
|
1007
1006
|
...(full ? serializeCanvasNode(node) : serializeCanvasNodeWithBlobSummaries(node)),
|
|
1008
|
-
qualification: boardId
|
|
1009
|
-
? lifecycleQualifications(Date.now(), [{ boardId, cardId: id }])[memoryKey({ boardId, cardId: id })]
|
|
1010
|
-
: undefined,
|
|
1007
|
+
qualification: boardId ? lifecycleQualification({ boardId, cardId: id }) : undefined,
|
|
1011
1008
|
});
|
|
1012
1009
|
if (typeof board === 'string' && board.trim()) {
|
|
1013
1010
|
const target = readTargetBoard(board, full);
|
|
@@ -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,
|
|
16
|
+
import { tourSchema, rectSchema, cameraSchema, tourStopSchema } from '../../../shared/tour.js';
|
|
17
17
|
import { canvasState } from '../../canvas-state.js';
|
|
18
18
|
import {
|
|
19
19
|
arrangeCanvasNodes,
|
|
@@ -23,7 +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
|
|
26
|
+
import { currentTour, exitTour, goToTourStop, moveCamera } from '../../tour-control.js';
|
|
27
27
|
import { defineOperation, OperationError, type Operation } from '../types.js';
|
|
28
28
|
import { closeNodeAppSession, isRecord } from './nodes.js';
|
|
29
29
|
|
|
@@ -329,11 +329,7 @@ const tourGetOperation = defineOperation({
|
|
|
329
329
|
toolName: 'canvas_get_tour',
|
|
330
330
|
description: 'Read the board tour, deriving groups in reading order when absent.',
|
|
331
331
|
},
|
|
332
|
-
handler: () => (
|
|
333
|
-
tour: canvasState.getTour() ?? derivedTour(canvasState.getLayout().nodes),
|
|
334
|
-
derived: !canvasState.getTour(),
|
|
335
|
-
position: tourPosition(),
|
|
336
|
-
}),
|
|
332
|
+
handler: () => currentTour(),
|
|
337
333
|
});
|
|
338
334
|
|
|
339
335
|
// ── tour.go / tour.exit / camera.move ────────────────────────
|
package/src/server/server.ts
CHANGED
|
@@ -2483,16 +2483,19 @@ async function handleRender(req: Request): Promise<Response> {
|
|
|
2483
2483
|
// The editor sends its draft through this same endpoint. Only changed/unsaved
|
|
2484
2484
|
// text may resolve titles; persisted text must replay even unresolved bindings.
|
|
2485
2485
|
const persisted = node?.type === 'markdown' && node.data.content === markdown;
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2486
|
+
let links = previous;
|
|
2487
|
+
if (!persisted) {
|
|
2488
|
+
const initializedRefs =
|
|
2489
|
+
node?.type === 'markdown' && node.data.wikiLinks !== undefined && typeof node.data.content === 'string'
|
|
2490
|
+
? wikiReferences(node.data.content)
|
|
2491
|
+
: [];
|
|
2492
|
+
const refs = wikiReferences(markdown).filter(
|
|
2493
|
+
(ref) => !initializedRefs.includes(ref) || previous.some((link) => link.ref === ref),
|
|
2494
|
+
);
|
|
2495
|
+
const needsResolution = refs.some((ref) => !previous.some((link) => link.ref === ref && link.target));
|
|
2496
|
+
const catalog = needsResolution ? canvasState.wikiCatalog() : [];
|
|
2497
|
+
links = refs.map((ref) => resolveWikiLinks(`[[${ref}]]`, previous, catalog)[0]);
|
|
2498
|
+
}
|
|
2496
2499
|
const targets = new Map<string, ReturnType<typeof canvasState.readBoard>>();
|
|
2497
2500
|
const html = renderWikiMarkdown(normalizeMarkdownExternalUrls(markdown), links, (target) => {
|
|
2498
2501
|
if (!targets.has(target.boardId)) targets.set(target.boardId, canvasState.readBoard(target.boardId, false));
|