pmx-canvas 0.1.25 → 0.1.27
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 +191 -0
- package/CHANGELOG.md +116 -0
- package/Readme.md +74 -27
- package/dist/canvas/index.js +82 -82
- package/dist/json-render/index.css +1 -1
- package/dist/json-render/index.js +944 -164
- package/dist/types/json-render/catalog.d.ts +195 -20
- package/dist/types/json-render/charts/components.d.ts +7 -0
- package/dist/types/json-render/charts/definitions.d.ts +13 -1
- package/dist/types/json-render/charts/tufte-components.d.ts +65 -0
- package/dist/types/json-render/charts/tufte-definitions.d.ts +164 -0
- package/dist/types/json-render/directives.d.ts +23 -0
- package/dist/types/json-render/renderer/index.d.ts +1 -0
- package/dist/types/json-render/server.d.ts +32 -1
- package/dist/types/mcp/canvas-access.d.ts +62 -0
- package/dist/types/server/ax-state.d.ts +170 -0
- package/dist/types/server/canvas-db.d.ts +17 -1
- package/dist/types/server/canvas-operations.d.ts +45 -0
- package/dist/types/server/canvas-schema.d.ts +5 -1
- package/dist/types/server/canvas-state.d.ts +95 -4
- package/dist/types/server/index.d.ts +118 -2
- package/dist/types/server/mutation-history.d.ts +1 -1
- package/docs/cli.md +42 -0
- package/docs/http-api.md +64 -0
- package/docs/mcp.md +23 -5
- package/docs/node-types.md +1 -1
- package/docs/screenshots/codex-app.png +0 -0
- package/docs/screenshots/github-copilot-app.png +0 -0
- package/docs/sdk.md +19 -1
- package/package.json +10 -7
- package/skills/control-session-orchestrator/SKILL.md +359 -0
- package/skills/control-session-orchestrator/evals/evals.json +75 -0
- package/skills/data-analysis/SKILL.md +6 -0
- package/skills/pmx-canvas/SKILL.md +63 -4
- package/skills/pmx-canvas/references/github-copilot-app-adapter.md +6 -0
- package/skills/tufte-viz/SKILL.md +157 -0
- package/skills/tufte-viz/references/analytical-design.md +217 -0
- package/skills/tufte-viz/references/tufte-principles.md +147 -0
- package/src/cli/agent.ts +280 -2
- package/src/cli/index.ts +2 -1
- package/src/client/nodes/ExtAppFrame.tsx +23 -1
- package/src/client/nodes/McpAppNode.tsx +6 -2
- package/src/json-render/catalog.ts +22 -1
- package/src/json-render/charts/components.tsx +97 -10
- package/src/json-render/charts/definitions.ts +19 -2
- package/src/json-render/charts/extra-components.tsx +5 -4
- package/src/json-render/charts/tufte-components.tsx +383 -0
- package/src/json-render/charts/tufte-definitions.ts +128 -0
- package/src/json-render/directives.ts +29 -0
- package/src/json-render/renderer/index.css +101 -0
- package/src/json-render/renderer/index.tsx +33 -0
- package/src/json-render/server.ts +257 -5
- package/src/mcp/canvas-access.ts +261 -0
- package/src/mcp/server.ts +500 -7
- package/src/server/ax-context.ts +8 -3
- package/src/server/ax-state.ts +447 -0
- package/src/server/canvas-db.ts +184 -1
- package/src/server/canvas-operations.ts +107 -0
- package/src/server/canvas-schema.ts +26 -3
- package/src/server/canvas-state.ts +349 -2
- package/src/server/index.ts +250 -2
- package/src/server/mutation-history.ts +6 -0
- package/src/server/server.ts +428 -2
package/src/server/server.ts
CHANGED
|
@@ -77,7 +77,15 @@ import {
|
|
|
77
77
|
import { buildCodeGraphSummary, formatCodeGraph } from './code-graph.js';
|
|
78
78
|
import { buildAgentContextPreamble, serializeNodeForAgentContext } from './agent-context.js';
|
|
79
79
|
import { buildCanvasAxContext } from './ax-context.js';
|
|
80
|
-
import
|
|
80
|
+
import { isAxEventKind, isAxEvidenceKind } from './ax-state.js';
|
|
81
|
+
import type {
|
|
82
|
+
PmxAxReviewAnchorType,
|
|
83
|
+
PmxAxReviewKind,
|
|
84
|
+
PmxAxReviewRegion,
|
|
85
|
+
PmxAxReviewSeverity,
|
|
86
|
+
PmxAxReviewStatus,
|
|
87
|
+
PmxAxSource,
|
|
88
|
+
} from './ax-state.js';
|
|
81
89
|
import { normalizeCanvasTheme, type CanvasTheme } from './canvas-db.js';
|
|
82
90
|
import { validateLocalImageFile } from './image-source.js';
|
|
83
91
|
import {
|
|
@@ -86,12 +94,14 @@ import {
|
|
|
86
94
|
MARKDOWN_NODE_DEFAULT_SIZE,
|
|
87
95
|
MCP_APP_NODE_DEFAULT_SIZE,
|
|
88
96
|
applyCanvasNodeUpdates,
|
|
97
|
+
appendCanvasJsonRenderStream,
|
|
89
98
|
buildStructuredNodeUpdate,
|
|
90
99
|
arrangeCanvasNodes,
|
|
91
100
|
clearCanvas,
|
|
92
101
|
createCanvasGraphNode,
|
|
93
102
|
createCanvasGroup,
|
|
94
103
|
createCanvasJsonRenderNode,
|
|
104
|
+
createCanvasStreamingJsonRenderNode,
|
|
95
105
|
deleteCanvasSnapshot,
|
|
96
106
|
executeCanvasBatch,
|
|
97
107
|
fitCanvasView,
|
|
@@ -101,6 +111,7 @@ import {
|
|
|
101
111
|
refreshCanvasWebpageNode,
|
|
102
112
|
removeCanvasNode,
|
|
103
113
|
removeCanvasEdge,
|
|
114
|
+
resolveHtmlContent,
|
|
104
115
|
restoreCanvasSnapshot,
|
|
105
116
|
saveCanvasSnapshot,
|
|
106
117
|
scheduleCodeGraphRecompute,
|
|
@@ -1656,7 +1667,7 @@ async function handleCanvasAddNode(req: Request): Promise<Response> {
|
|
|
1656
1667
|
const htmlMergedData = type === 'html'
|
|
1657
1668
|
? {
|
|
1658
1669
|
...(extraData ?? {}),
|
|
1659
|
-
...(typeof body.html === 'string' ? { html: body.html } : {}),
|
|
1670
|
+
...(typeof body.html === 'string' ? { html: resolveHtmlContent(body.html) } : {}),
|
|
1660
1671
|
...(typeof body.summary === 'string' ? { summary: body.summary } : {}),
|
|
1661
1672
|
...(typeof body.agentSummary === 'string' ? { agentSummary: body.agentSummary } : {}),
|
|
1662
1673
|
...(typeof body.description === 'string' ? { description: body.description } : {}),
|
|
@@ -2214,6 +2225,34 @@ async function handleCanvasAddJsonRender(req: Request): Promise<Response> {
|
|
|
2214
2225
|
}
|
|
2215
2226
|
}
|
|
2216
2227
|
|
|
2228
|
+
async function handleJsonRenderStream(req: Request): Promise<Response> {
|
|
2229
|
+
const body = await readJson(req);
|
|
2230
|
+
const patches = Array.isArray(body.patches) ? body.patches : [];
|
|
2231
|
+
const done = body.done === true;
|
|
2232
|
+
const geometry = resolveCreateGeometry(body);
|
|
2233
|
+
try {
|
|
2234
|
+
let nodeId = typeof body.nodeId === 'string' && body.nodeId ? body.nodeId : undefined;
|
|
2235
|
+
let url = '';
|
|
2236
|
+
if (!nodeId) {
|
|
2237
|
+
const created = createCanvasStreamingJsonRenderNode({
|
|
2238
|
+
...(typeof body.title === 'string' ? { title: body.title } : {}),
|
|
2239
|
+
...(body.strictSize === true ? { strictSize: true } : {}),
|
|
2240
|
+
...geometry,
|
|
2241
|
+
});
|
|
2242
|
+
nodeId = created.id;
|
|
2243
|
+
url = created.url;
|
|
2244
|
+
}
|
|
2245
|
+
const result = appendCanvasJsonRenderStream(nodeId, patches, done);
|
|
2246
|
+
if (!result.ok) return responseJson({ ok: false, error: result.error }, 400);
|
|
2247
|
+
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
2248
|
+
const node = canvasState.getNode(nodeId);
|
|
2249
|
+
return responseJson({ id: nodeId, url: url || String(node?.data.url ?? ''), ...result });
|
|
2250
|
+
} catch (error) {
|
|
2251
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2252
|
+
return responseJson({ ok: false, error: message }, 400);
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2217
2256
|
async function handleCanvasAddGraph(req: Request): Promise<Response> {
|
|
2218
2257
|
const body = await readJson(req);
|
|
2219
2258
|
const title = typeof body.title === 'string' && body.title.trim() ? body.title.trim() : 'Graph';
|
|
@@ -2242,6 +2281,16 @@ async function handleCanvasAddGraph(req: Request): Promise<Response> {
|
|
|
2242
2281
|
const nodeHeight = pickPositiveNumber(body, 'nodeHeight') ?? (size ? pickPositiveNumber(size, 'height') : undefined);
|
|
2243
2282
|
const showLegend = typeof body.showLegend === 'boolean' ? body.showLegend : undefined;
|
|
2244
2283
|
const showLabels = typeof body.showLabels === 'boolean' ? body.showLabels : undefined;
|
|
2284
|
+
const colorBy =
|
|
2285
|
+
body.colorBy === 'series' || body.colorBy === 'category' || body.colorBy === 'value' || body.colorBy === 'none'
|
|
2286
|
+
? body.colorBy
|
|
2287
|
+
: undefined;
|
|
2288
|
+
const highlight =
|
|
2289
|
+
typeof body.highlight === 'number' || body.highlight === 'max' || body.highlight === 'min' || body.highlight === null
|
|
2290
|
+
? body.highlight
|
|
2291
|
+
: undefined;
|
|
2292
|
+
const sort =
|
|
2293
|
+
body.sort === 'asc' || body.sort === 'desc' || body.sort === 'none' ? body.sort : undefined;
|
|
2245
2294
|
const result = createCanvasGraphNode({
|
|
2246
2295
|
title,
|
|
2247
2296
|
graphType,
|
|
@@ -2258,8 +2307,23 @@ async function handleCanvasAddGraph(req: Request): Promise<Response> {
|
|
|
2258
2307
|
...(typeof body.lineKey === 'string' ? { lineKey: body.lineKey } : {}),
|
|
2259
2308
|
...(aggregate ? { aggregate } : {}),
|
|
2260
2309
|
...(typeof body.color === 'string' ? { color: body.color } : {}),
|
|
2310
|
+
...(colorBy ? { colorBy } : {}),
|
|
2311
|
+
...(highlight !== undefined ? { highlight } : {}),
|
|
2261
2312
|
...(typeof body.barColor === 'string' ? { barColor: body.barColor } : {}),
|
|
2262
2313
|
...(typeof body.lineColor === 'string' ? { lineColor: body.lineColor } : {}),
|
|
2314
|
+
...(typeof body.labelKey === 'string' ? { labelKey: body.labelKey } : {}),
|
|
2315
|
+
...(typeof body.targetKey === 'string' ? { targetKey: body.targetKey } : {}),
|
|
2316
|
+
...(typeof body.rangesKey === 'string' ? { rangesKey: body.rangesKey } : {}),
|
|
2317
|
+
...(typeof body.beforeKey === 'string' ? { beforeKey: body.beforeKey } : {}),
|
|
2318
|
+
...(typeof body.afterKey === 'string' ? { afterKey: body.afterKey } : {}),
|
|
2319
|
+
...(typeof body.beforeLabel === 'string' ? { beforeLabel: body.beforeLabel } : {}),
|
|
2320
|
+
...(typeof body.afterLabel === 'string' ? { afterLabel: body.afterLabel } : {}),
|
|
2321
|
+
...(sort ? { sort } : {}),
|
|
2322
|
+
...(typeof body.fill === 'boolean' ? { fill: body.fill } : {}),
|
|
2323
|
+
...(typeof body.showEndDot === 'boolean' ? { showEndDot: body.showEndDot } : {}),
|
|
2324
|
+
...(typeof body.showMinMax === 'boolean' ? { showMinMax: body.showMinMax } : {}),
|
|
2325
|
+
...(typeof body.showValue === 'boolean' ? { showValue: body.showValue } : {}),
|
|
2326
|
+
...(typeof body.colorByDirection === 'boolean' ? { colorByDirection: body.colorByDirection } : {}),
|
|
2263
2327
|
...(typeof body.height === 'number' ? { height: body.height } : {}),
|
|
2264
2328
|
...(showLegend !== undefined ? { showLegend } : {}),
|
|
2265
2329
|
...(showLabels !== undefined ? { showLabels } : {}),
|
|
@@ -2334,11 +2398,17 @@ async function handleJsonRenderView(url: URL): Promise<Response> {
|
|
|
2334
2398
|
? themeValue
|
|
2335
2399
|
: undefined;
|
|
2336
2400
|
const title = (node.data.title as string) || node.id;
|
|
2401
|
+
// Devtools panel is double-gated: the operator must opt in via the env flag
|
|
2402
|
+
// AND the request must carry ?devtools=1. Off by default in all normal runs.
|
|
2403
|
+
const devtoolsEnabled =
|
|
2404
|
+
process.env.PMX_CANVAS_JSON_RENDER_DEVTOOLS === '1' &&
|
|
2405
|
+
url.searchParams.get('devtools') === '1';
|
|
2337
2406
|
const html = await buildJsonRenderViewerHtml({
|
|
2338
2407
|
title,
|
|
2339
2408
|
spec,
|
|
2340
2409
|
...(theme ? { theme } : {}),
|
|
2341
2410
|
...(url.searchParams.get('display') === 'expanded' ? { display: 'expanded' as const } : {}),
|
|
2411
|
+
...(devtoolsEnabled ? { devtools: true } : {}),
|
|
2342
2412
|
});
|
|
2343
2413
|
return new Response(html, {
|
|
2344
2414
|
headers: {
|
|
@@ -3653,6 +3723,284 @@ async function handleAxStatePatch(req: Request): Promise<Response> {
|
|
|
3653
3723
|
return responseJson({ ok: true, state: canvasState.getAxState() });
|
|
3654
3724
|
}
|
|
3655
3725
|
|
|
3726
|
+
async function handleAxEventAdd(req: Request): Promise<Response> {
|
|
3727
|
+
const body = await readJson(req);
|
|
3728
|
+
if (!isAxEventKind(body.kind) || typeof body.summary !== 'string') {
|
|
3729
|
+
return responseJson({ ok: false, error: 'event requires kind and summary.' }, 400);
|
|
3730
|
+
}
|
|
3731
|
+
const event = canvasState.recordAxEvent(
|
|
3732
|
+
{
|
|
3733
|
+
kind: body.kind,
|
|
3734
|
+
summary: body.summary,
|
|
3735
|
+
detail: typeof body.detail === 'string' ? body.detail : null,
|
|
3736
|
+
nodeIds: normalizeAxNodeIds(body.nodeIds),
|
|
3737
|
+
data: isRecord(body.data) ? body.data : null,
|
|
3738
|
+
},
|
|
3739
|
+
{ source: normalizeAxSource(body.source, 'api') },
|
|
3740
|
+
);
|
|
3741
|
+
broadcastWorkbenchEvent('ax-event-created', {
|
|
3742
|
+
event,
|
|
3743
|
+
sessionId: primaryWorkbenchSessionId,
|
|
3744
|
+
timestamp: new Date().toISOString(),
|
|
3745
|
+
});
|
|
3746
|
+
return responseJson({ ok: true, event });
|
|
3747
|
+
}
|
|
3748
|
+
|
|
3749
|
+
async function handleAxSteer(req: Request): Promise<Response> {
|
|
3750
|
+
const body = await readJson(req);
|
|
3751
|
+
if (typeof body.message !== 'string' || !body.message.trim()) {
|
|
3752
|
+
return responseJson({ ok: false, error: 'steer requires a non-empty message.' }, 400);
|
|
3753
|
+
}
|
|
3754
|
+
const steering = canvasState.recordSteeringMessage(body.message, {
|
|
3755
|
+
source: normalizeAxSource(body.source, 'api'),
|
|
3756
|
+
});
|
|
3757
|
+
broadcastWorkbenchEvent('ax-event-created', {
|
|
3758
|
+
steering,
|
|
3759
|
+
sessionId: primaryWorkbenchSessionId,
|
|
3760
|
+
timestamp: new Date().toISOString(),
|
|
3761
|
+
});
|
|
3762
|
+
return responseJson({ ok: true, steering });
|
|
3763
|
+
}
|
|
3764
|
+
|
|
3765
|
+
function handleAxTimelineGet(url: URL): Response {
|
|
3766
|
+
const limit = Number(url.searchParams.get('limit') ?? '');
|
|
3767
|
+
return responseJson({
|
|
3768
|
+
ok: true,
|
|
3769
|
+
...canvasState.getAxTimeline(Number.isFinite(limit) && limit > 0 ? { limit } : {}),
|
|
3770
|
+
});
|
|
3771
|
+
}
|
|
3772
|
+
|
|
3773
|
+
const AX_WORK_STATUSES = new Set(['todo', 'in-progress', 'blocked', 'done', 'cancelled']);
|
|
3774
|
+
|
|
3775
|
+
function normalizeAxWorkItemStatus(value: unknown): 'todo' | 'in-progress' | 'blocked' | 'done' | 'cancelled' | undefined {
|
|
3776
|
+
return typeof value === 'string' && AX_WORK_STATUSES.has(value)
|
|
3777
|
+
? value as 'todo' | 'in-progress' | 'blocked' | 'done' | 'cancelled'
|
|
3778
|
+
: undefined;
|
|
3779
|
+
}
|
|
3780
|
+
|
|
3781
|
+
function handleAxWorkList(): Response {
|
|
3782
|
+
return responseJson({ ok: true, workItems: canvasState.getWorkItems() });
|
|
3783
|
+
}
|
|
3784
|
+
|
|
3785
|
+
async function handleAxWorkAdd(req: Request): Promise<Response> {
|
|
3786
|
+
const body = await readJson(req);
|
|
3787
|
+
if (typeof body.title !== 'string' || !body.title.trim()) {
|
|
3788
|
+
return responseJson({ ok: false, error: 'work item requires a title.' }, 400);
|
|
3789
|
+
}
|
|
3790
|
+
const status = normalizeAxWorkItemStatus(body.status);
|
|
3791
|
+
const workItem = canvasState.addWorkItem(
|
|
3792
|
+
{
|
|
3793
|
+
title: body.title,
|
|
3794
|
+
...(status ? { status } : {}),
|
|
3795
|
+
...(typeof body.detail === 'string' ? { detail: body.detail } : {}),
|
|
3796
|
+
...(Array.isArray(body.nodeIds) ? { nodeIds: normalizeAxNodeIds(body.nodeIds) } : {}),
|
|
3797
|
+
},
|
|
3798
|
+
{ source: normalizeAxSource(body.source, 'api') },
|
|
3799
|
+
);
|
|
3800
|
+
broadcastWorkbenchEvent('ax-state-changed', {
|
|
3801
|
+
workItem,
|
|
3802
|
+
sessionId: primaryWorkbenchSessionId,
|
|
3803
|
+
timestamp: new Date().toISOString(),
|
|
3804
|
+
});
|
|
3805
|
+
return responseJson({ ok: true, workItem });
|
|
3806
|
+
}
|
|
3807
|
+
|
|
3808
|
+
async function handleAxWorkUpdate(req: Request, id: string): Promise<Response> {
|
|
3809
|
+
const body = await readJson(req);
|
|
3810
|
+
const status = normalizeAxWorkItemStatus(body.status);
|
|
3811
|
+
const workItem = canvasState.updateWorkItem(
|
|
3812
|
+
id,
|
|
3813
|
+
{
|
|
3814
|
+
...(typeof body.title === 'string' ? { title: body.title } : {}),
|
|
3815
|
+
...(status ? { status } : {}),
|
|
3816
|
+
...(typeof body.detail === 'string' || body.detail === null ? { detail: body.detail as string | null } : {}),
|
|
3817
|
+
...(Array.isArray(body.nodeIds) ? { nodeIds: normalizeAxNodeIds(body.nodeIds) } : {}),
|
|
3818
|
+
},
|
|
3819
|
+
{ source: normalizeAxSource(body.source, 'api') },
|
|
3820
|
+
);
|
|
3821
|
+
if (!workItem) return responseJson({ ok: false, error: 'work item not found.' }, 404);
|
|
3822
|
+
broadcastWorkbenchEvent('ax-state-changed', {
|
|
3823
|
+
workItem,
|
|
3824
|
+
sessionId: primaryWorkbenchSessionId,
|
|
3825
|
+
timestamp: new Date().toISOString(),
|
|
3826
|
+
});
|
|
3827
|
+
return responseJson({ ok: true, workItem });
|
|
3828
|
+
}
|
|
3829
|
+
|
|
3830
|
+
function handleAxApprovalList(): Response {
|
|
3831
|
+
return responseJson({ ok: true, approvalGates: canvasState.getApprovalGates() });
|
|
3832
|
+
}
|
|
3833
|
+
|
|
3834
|
+
async function handleAxApprovalRequest(req: Request): Promise<Response> {
|
|
3835
|
+
const body = await readJson(req);
|
|
3836
|
+
if (typeof body.title !== 'string' || !body.title.trim()) {
|
|
3837
|
+
return responseJson({ ok: false, error: 'approval request requires a title.' }, 400);
|
|
3838
|
+
}
|
|
3839
|
+
const approvalGate = canvasState.requestApproval(
|
|
3840
|
+
{
|
|
3841
|
+
title: body.title,
|
|
3842
|
+
...(typeof body.detail === 'string' ? { detail: body.detail } : {}),
|
|
3843
|
+
...(typeof body.action === 'string' ? { action: body.action } : {}),
|
|
3844
|
+
...(Array.isArray(body.nodeIds) ? { nodeIds: normalizeAxNodeIds(body.nodeIds) } : {}),
|
|
3845
|
+
},
|
|
3846
|
+
{ source: normalizeAxSource(body.source, 'api') },
|
|
3847
|
+
);
|
|
3848
|
+
broadcastWorkbenchEvent('ax-state-changed', {
|
|
3849
|
+
approvalGate,
|
|
3850
|
+
sessionId: primaryWorkbenchSessionId,
|
|
3851
|
+
timestamp: new Date().toISOString(),
|
|
3852
|
+
});
|
|
3853
|
+
return responseJson({ ok: true, approvalGate });
|
|
3854
|
+
}
|
|
3855
|
+
|
|
3856
|
+
async function handleAxApprovalResolve(req: Request, id: string): Promise<Response> {
|
|
3857
|
+
const body = await readJson(req);
|
|
3858
|
+
if (body.decision !== 'approved' && body.decision !== 'rejected') {
|
|
3859
|
+
return responseJson({ ok: false, error: 'resolve requires decision approved or rejected.' }, 400);
|
|
3860
|
+
}
|
|
3861
|
+
const approvalGate = canvasState.resolveApproval(
|
|
3862
|
+
id,
|
|
3863
|
+
body.decision,
|
|
3864
|
+
{
|
|
3865
|
+
...(typeof body.resolution === 'string' ? { resolution: body.resolution } : {}),
|
|
3866
|
+
source: normalizeAxSource(body.source, 'api'),
|
|
3867
|
+
},
|
|
3868
|
+
);
|
|
3869
|
+
if (!approvalGate) return responseJson({ ok: false, error: 'approval gate not found or already resolved.' }, 404);
|
|
3870
|
+
broadcastWorkbenchEvent('ax-state-changed', {
|
|
3871
|
+
approvalGate,
|
|
3872
|
+
sessionId: primaryWorkbenchSessionId,
|
|
3873
|
+
timestamp: new Date().toISOString(),
|
|
3874
|
+
});
|
|
3875
|
+
return responseJson({ ok: true, approvalGate });
|
|
3876
|
+
}
|
|
3877
|
+
|
|
3878
|
+
async function handleAxEvidenceAdd(req: Request): Promise<Response> {
|
|
3879
|
+
const body = await readJson(req);
|
|
3880
|
+
if (!isAxEvidenceKind(body.kind) || typeof body.title !== 'string' || !body.title.trim()) {
|
|
3881
|
+
return responseJson({ ok: false, error: 'evidence requires kind and title.' }, 400);
|
|
3882
|
+
}
|
|
3883
|
+
const evidence = canvasState.addEvidence(
|
|
3884
|
+
{
|
|
3885
|
+
kind: body.kind,
|
|
3886
|
+
title: body.title,
|
|
3887
|
+
body: typeof body.body === 'string' ? body.body : null,
|
|
3888
|
+
ref: typeof body.ref === 'string' ? body.ref : null,
|
|
3889
|
+
nodeIds: normalizeAxNodeIds(body.nodeIds),
|
|
3890
|
+
data: isRecord(body.data) ? body.data : null,
|
|
3891
|
+
},
|
|
3892
|
+
{ source: normalizeAxSource(body.source, 'api') },
|
|
3893
|
+
);
|
|
3894
|
+
broadcastWorkbenchEvent('ax-event-created', {
|
|
3895
|
+
evidence,
|
|
3896
|
+
sessionId: primaryWorkbenchSessionId,
|
|
3897
|
+
timestamp: new Date().toISOString(),
|
|
3898
|
+
});
|
|
3899
|
+
return responseJson({ ok: true, evidence });
|
|
3900
|
+
}
|
|
3901
|
+
|
|
3902
|
+
const AX_REVIEW_KINDS = new Set(['comment', 'finding']);
|
|
3903
|
+
const AX_REVIEW_SEVERITIES = new Set(['info', 'warning', 'error']);
|
|
3904
|
+
const AX_REVIEW_STATUSES = new Set(['open', 'resolved', 'dismissed']);
|
|
3905
|
+
const AX_REVIEW_ANCHORS = new Set(['node', 'file', 'region']);
|
|
3906
|
+
|
|
3907
|
+
function normalizeAxReviewKind(value: unknown): PmxAxReviewKind | undefined {
|
|
3908
|
+
return typeof value === 'string' && AX_REVIEW_KINDS.has(value) ? value as PmxAxReviewKind : undefined;
|
|
3909
|
+
}
|
|
3910
|
+
function normalizeAxReviewSeverity(value: unknown): PmxAxReviewSeverity | undefined {
|
|
3911
|
+
return typeof value === 'string' && AX_REVIEW_SEVERITIES.has(value) ? value as PmxAxReviewSeverity : undefined;
|
|
3912
|
+
}
|
|
3913
|
+
function normalizeAxReviewStatus(value: unknown): PmxAxReviewStatus | undefined {
|
|
3914
|
+
return typeof value === 'string' && AX_REVIEW_STATUSES.has(value) ? value as PmxAxReviewStatus : undefined;
|
|
3915
|
+
}
|
|
3916
|
+
function normalizeAxReviewAnchor(value: unknown): PmxAxReviewAnchorType | undefined {
|
|
3917
|
+
return typeof value === 'string' && AX_REVIEW_ANCHORS.has(value) ? value as PmxAxReviewAnchorType : undefined;
|
|
3918
|
+
}
|
|
3919
|
+
function normalizeAxReviewRegion(value: unknown): PmxAxReviewRegion | undefined {
|
|
3920
|
+
if (!isRecord(value)) return undefined;
|
|
3921
|
+
return {
|
|
3922
|
+
...(typeof value.line === 'number' ? { line: value.line } : {}),
|
|
3923
|
+
...(typeof value.endLine === 'number' ? { endLine: value.endLine } : {}),
|
|
3924
|
+
...(typeof value.label === 'string' ? { label: value.label } : {}),
|
|
3925
|
+
};
|
|
3926
|
+
}
|
|
3927
|
+
|
|
3928
|
+
function handleAxReviewList(): Response {
|
|
3929
|
+
return responseJson({ ok: true, reviewAnnotations: canvasState.getReviewAnnotations() });
|
|
3930
|
+
}
|
|
3931
|
+
|
|
3932
|
+
async function handleAxReviewAdd(req: Request): Promise<Response> {
|
|
3933
|
+
const body = await readJson(req);
|
|
3934
|
+
if (typeof body.body !== 'string' || !body.body.trim()) {
|
|
3935
|
+
return responseJson({ ok: false, error: 'review annotation requires a body.' }, 400);
|
|
3936
|
+
}
|
|
3937
|
+
const kind = normalizeAxReviewKind(body.kind);
|
|
3938
|
+
const severity = normalizeAxReviewSeverity(body.severity);
|
|
3939
|
+
const anchorType = normalizeAxReviewAnchor(body.anchorType);
|
|
3940
|
+
const region = normalizeAxReviewRegion(body.region);
|
|
3941
|
+
const reviewAnnotation = canvasState.addReviewAnnotation(
|
|
3942
|
+
{
|
|
3943
|
+
body: body.body,
|
|
3944
|
+
...(kind ? { kind } : {}),
|
|
3945
|
+
...(severity ? { severity } : {}),
|
|
3946
|
+
...(anchorType ? { anchorType } : {}),
|
|
3947
|
+
...(typeof body.nodeId === 'string' ? { nodeId: body.nodeId } : {}),
|
|
3948
|
+
...(typeof body.file === 'string' ? { file: body.file } : {}),
|
|
3949
|
+
...(region ? { region } : {}),
|
|
3950
|
+
...(typeof body.author === 'string' ? { author: body.author } : {}),
|
|
3951
|
+
},
|
|
3952
|
+
{ source: normalizeAxSource(body.source, 'api') },
|
|
3953
|
+
);
|
|
3954
|
+
if (!reviewAnnotation) {
|
|
3955
|
+
return responseJson({ ok: false, error: 'node-anchored review annotation requires a nodeId that exists on the canvas.' }, 400);
|
|
3956
|
+
}
|
|
3957
|
+
broadcastWorkbenchEvent('ax-state-changed', {
|
|
3958
|
+
reviewAnnotation,
|
|
3959
|
+
sessionId: primaryWorkbenchSessionId,
|
|
3960
|
+
timestamp: new Date().toISOString(),
|
|
3961
|
+
});
|
|
3962
|
+
return responseJson({ ok: true, reviewAnnotation });
|
|
3963
|
+
}
|
|
3964
|
+
|
|
3965
|
+
async function handleAxReviewUpdate(req: Request, id: string): Promise<Response> {
|
|
3966
|
+
const body = await readJson(req);
|
|
3967
|
+
const status = normalizeAxReviewStatus(body.status);
|
|
3968
|
+
const severity = normalizeAxReviewSeverity(body.severity);
|
|
3969
|
+
const kind = normalizeAxReviewKind(body.kind);
|
|
3970
|
+
const reviewAnnotation = canvasState.updateReviewAnnotation(
|
|
3971
|
+
id,
|
|
3972
|
+
{
|
|
3973
|
+
...(typeof body.body === 'string' ? { body: body.body } : {}),
|
|
3974
|
+
...(status ? { status } : {}),
|
|
3975
|
+
...(severity ? { severity } : {}),
|
|
3976
|
+
...(kind ? { kind } : {}),
|
|
3977
|
+
},
|
|
3978
|
+
{ source: normalizeAxSource(body.source, 'api') },
|
|
3979
|
+
);
|
|
3980
|
+
if (!reviewAnnotation) return responseJson({ ok: false, error: 'review annotation not found.' }, 404);
|
|
3981
|
+
broadcastWorkbenchEvent('ax-state-changed', {
|
|
3982
|
+
reviewAnnotation,
|
|
3983
|
+
sessionId: primaryWorkbenchSessionId,
|
|
3984
|
+
timestamp: new Date().toISOString(),
|
|
3985
|
+
});
|
|
3986
|
+
return responseJson({ ok: true, reviewAnnotation });
|
|
3987
|
+
}
|
|
3988
|
+
|
|
3989
|
+
function handleAxHostCapabilityGet(): Response {
|
|
3990
|
+
return responseJson({ ok: true, host: canvasState.getHostCapability() });
|
|
3991
|
+
}
|
|
3992
|
+
|
|
3993
|
+
async function handleAxHostCapabilityReport(req: Request): Promise<Response> {
|
|
3994
|
+
const body = await readJson(req);
|
|
3995
|
+
const host = canvasState.setHostCapability(body, { source: normalizeAxSource(body.source, 'api') });
|
|
3996
|
+
broadcastWorkbenchEvent('ax-state-changed', {
|
|
3997
|
+
host,
|
|
3998
|
+
sessionId: primaryWorkbenchSessionId,
|
|
3999
|
+
timestamp: new Date().toISOString(),
|
|
4000
|
+
});
|
|
4001
|
+
return responseJson({ ok: true, host });
|
|
4002
|
+
}
|
|
4003
|
+
|
|
3656
4004
|
// ── Port resolution ───────────────────────────────────────────
|
|
3657
4005
|
|
|
3658
4006
|
function buildPortCandidates(preferredPort: number): number[] {
|
|
@@ -4584,6 +4932,15 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
4584
4932
|
return handleSnapshotGc(req);
|
|
4585
4933
|
}
|
|
4586
4934
|
|
|
4935
|
+
if (url.pathname === '/api/canvas/snapshots/diff' && req.method === 'GET') {
|
|
4936
|
+
const name = url.searchParams.get('name') ?? url.searchParams.get('id') ?? '';
|
|
4937
|
+
if (!name.trim()) return responseJson({ ok: false, error: 'Missing snapshot name or id.' }, 400);
|
|
4938
|
+
const snapshot = canvasState.getSnapshotData(name);
|
|
4939
|
+
if (!snapshot) return responseJson({ ok: false, error: `Snapshot "${name}" not found.` }, 404);
|
|
4940
|
+
const diff = diffLayouts(snapshot.name, snapshot, canvasState.getLayout());
|
|
4941
|
+
return responseJson({ ok: true, text: formatDiff(diff), diff });
|
|
4942
|
+
}
|
|
4943
|
+
|
|
4587
4944
|
if (url.pathname.startsWith('/api/canvas/snapshots/') && url.pathname.endsWith('/diff') && req.method === 'GET') {
|
|
4588
4945
|
const id = decodeURIComponent(url.pathname.slice('/api/canvas/snapshots/'.length, -'/diff'.length));
|
|
4589
4946
|
const snapshot = canvasState.getSnapshotData(id);
|
|
@@ -4632,6 +4989,71 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
4632
4989
|
return handleAxFocusUpdate(req);
|
|
4633
4990
|
}
|
|
4634
4991
|
|
|
4992
|
+
if (url.pathname === '/api/canvas/ax/event' && req.method === 'POST') {
|
|
4993
|
+
return handleAxEventAdd(req);
|
|
4994
|
+
}
|
|
4995
|
+
|
|
4996
|
+
if (url.pathname === '/api/canvas/ax/steer' && req.method === 'POST') {
|
|
4997
|
+
return handleAxSteer(req);
|
|
4998
|
+
}
|
|
4999
|
+
|
|
5000
|
+
if (url.pathname === '/api/canvas/ax/timeline' && req.method === 'GET') {
|
|
5001
|
+
return handleAxTimelineGet(url);
|
|
5002
|
+
}
|
|
5003
|
+
|
|
5004
|
+
if (url.pathname === '/api/canvas/ax/work' && req.method === 'GET') {
|
|
5005
|
+
return handleAxWorkList();
|
|
5006
|
+
}
|
|
5007
|
+
|
|
5008
|
+
if (url.pathname === '/api/canvas/ax/work' && req.method === 'POST') {
|
|
5009
|
+
return handleAxWorkAdd(req);
|
|
5010
|
+
}
|
|
5011
|
+
|
|
5012
|
+
if (url.pathname.startsWith('/api/canvas/ax/work/') && req.method === 'PATCH') {
|
|
5013
|
+
const workItemId = decodeURIComponent(url.pathname.slice('/api/canvas/ax/work/'.length));
|
|
5014
|
+
return handleAxWorkUpdate(req, workItemId);
|
|
5015
|
+
}
|
|
5016
|
+
|
|
5017
|
+
if (url.pathname === '/api/canvas/ax/approval' && req.method === 'GET') {
|
|
5018
|
+
return handleAxApprovalList();
|
|
5019
|
+
}
|
|
5020
|
+
|
|
5021
|
+
if (url.pathname === '/api/canvas/ax/approval' && req.method === 'POST') {
|
|
5022
|
+
return handleAxApprovalRequest(req);
|
|
5023
|
+
}
|
|
5024
|
+
|
|
5025
|
+
if (url.pathname.startsWith('/api/canvas/ax/approval/') && url.pathname.endsWith('/resolve') && req.method === 'POST') {
|
|
5026
|
+
const approvalId = decodeURIComponent(
|
|
5027
|
+
url.pathname.slice('/api/canvas/ax/approval/'.length, -'/resolve'.length),
|
|
5028
|
+
);
|
|
5029
|
+
return handleAxApprovalResolve(req, approvalId);
|
|
5030
|
+
}
|
|
5031
|
+
|
|
5032
|
+
if (url.pathname === '/api/canvas/ax/evidence' && req.method === 'POST') {
|
|
5033
|
+
return handleAxEvidenceAdd(req);
|
|
5034
|
+
}
|
|
5035
|
+
|
|
5036
|
+
if (url.pathname === '/api/canvas/ax/review' && req.method === 'GET') {
|
|
5037
|
+
return handleAxReviewList();
|
|
5038
|
+
}
|
|
5039
|
+
|
|
5040
|
+
if (url.pathname === '/api/canvas/ax/review' && req.method === 'POST') {
|
|
5041
|
+
return handleAxReviewAdd(req);
|
|
5042
|
+
}
|
|
5043
|
+
|
|
5044
|
+
if (url.pathname.startsWith('/api/canvas/ax/review/') && req.method === 'PATCH') {
|
|
5045
|
+
const reviewId = decodeURIComponent(url.pathname.slice('/api/canvas/ax/review/'.length));
|
|
5046
|
+
return handleAxReviewUpdate(req, reviewId);
|
|
5047
|
+
}
|
|
5048
|
+
|
|
5049
|
+
if (url.pathname === '/api/canvas/ax/host-capability' && req.method === 'GET') {
|
|
5050
|
+
return handleAxHostCapabilityGet();
|
|
5051
|
+
}
|
|
5052
|
+
|
|
5053
|
+
if (url.pathname === '/api/canvas/ax/host-capability' && req.method === 'PUT') {
|
|
5054
|
+
return handleAxHostCapabilityReport(req);
|
|
5055
|
+
}
|
|
5056
|
+
|
|
4635
5057
|
// Spatial context API
|
|
4636
5058
|
if (url.pathname === '/api/canvas/spatial-context' && req.method === 'GET') {
|
|
4637
5059
|
const layout = canvasState.getLayout();
|
|
@@ -4694,6 +5116,10 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
4694
5116
|
return handleCanvasAddJsonRender(req);
|
|
4695
5117
|
}
|
|
4696
5118
|
|
|
5119
|
+
if (url.pathname === '/api/canvas/json-render/stream' && req.method === 'POST') {
|
|
5120
|
+
return handleJsonRenderStream(req);
|
|
5121
|
+
}
|
|
5122
|
+
|
|
4697
5123
|
if (url.pathname === '/api/canvas/graph' && req.method === 'POST') {
|
|
4698
5124
|
return handleCanvasAddGraph(req);
|
|
4699
5125
|
}
|