pmx-canvas 0.1.26 → 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 +74 -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 +114 -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 +50 -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 +496 -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 +234 -2
- package/src/server/mutation-history.ts +6 -0
- package/src/server/server.ts +419 -2
package/src/server/ax-state.ts
CHANGED
|
@@ -10,9 +10,137 @@ export interface PmxAxFocusState {
|
|
|
10
10
|
source: PmxAxSource | null;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
// ── New enums ──────────────────────────────────────────────────────
|
|
14
|
+
export type PmxAxEventKind =
|
|
15
|
+
| 'prompt' | 'assistant-message' | 'tool-start' | 'tool-result'
|
|
16
|
+
| 'failure' | 'approval' | 'steering';
|
|
17
|
+
export type PmxAxEvidenceKind =
|
|
18
|
+
| 'logs' | 'tool-result' | 'screenshot' | 'file' | 'diff' | 'test-output';
|
|
19
|
+
export type PmxAxWorkItemStatus = 'todo' | 'in-progress' | 'blocked' | 'done' | 'cancelled';
|
|
20
|
+
export type PmxAxApprovalStatus = 'pending' | 'approved' | 'rejected';
|
|
21
|
+
export type PmxAxReviewKind = 'comment' | 'finding';
|
|
22
|
+
export type PmxAxReviewSeverity = 'info' | 'warning' | 'error';
|
|
23
|
+
export type PmxAxReviewStatus = 'open' | 'resolved' | 'dismissed';
|
|
24
|
+
export type PmxAxReviewAnchorType = 'node' | 'file' | 'region';
|
|
25
|
+
|
|
26
|
+
// ── Canvas-bound records (live inside PmxAxState; snapshotted) ──────
|
|
27
|
+
export interface PmxAxWorkItem {
|
|
28
|
+
id: string;
|
|
29
|
+
title: string;
|
|
30
|
+
status: PmxAxWorkItemStatus;
|
|
31
|
+
detail: string | null;
|
|
32
|
+
nodeIds: string[];
|
|
33
|
+
createdAt: string;
|
|
34
|
+
updatedAt: string;
|
|
35
|
+
source: PmxAxSource | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface PmxAxApprovalGate {
|
|
39
|
+
id: string;
|
|
40
|
+
title: string;
|
|
41
|
+
detail: string | null;
|
|
42
|
+
action: string | null;
|
|
43
|
+
status: PmxAxApprovalStatus;
|
|
44
|
+
nodeIds: string[];
|
|
45
|
+
createdAt: string;
|
|
46
|
+
resolvedAt: string | null;
|
|
47
|
+
resolution: string | null;
|
|
48
|
+
source: PmxAxSource | null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface PmxAxReviewRegion {
|
|
52
|
+
line?: number;
|
|
53
|
+
endLine?: number;
|
|
54
|
+
label?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface PmxAxReviewAnnotation {
|
|
58
|
+
id: string;
|
|
59
|
+
kind: PmxAxReviewKind;
|
|
60
|
+
body: string;
|
|
61
|
+
severity: PmxAxReviewSeverity;
|
|
62
|
+
status: PmxAxReviewStatus;
|
|
63
|
+
anchorType: PmxAxReviewAnchorType;
|
|
64
|
+
nodeId: string | null;
|
|
65
|
+
file: string | null;
|
|
66
|
+
region: PmxAxReviewRegion | null;
|
|
67
|
+
author: string | null;
|
|
68
|
+
createdAt: string;
|
|
69
|
+
updatedAt: string;
|
|
70
|
+
source: PmxAxSource | null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── Timeline records (separate DB tables; NOT in PmxAxState) ────────
|
|
74
|
+
export interface PmxAxEvent {
|
|
75
|
+
id: string;
|
|
76
|
+
seq: number;
|
|
77
|
+
kind: PmxAxEventKind;
|
|
78
|
+
summary: string;
|
|
79
|
+
detail: string | null;
|
|
80
|
+
nodeIds: string[];
|
|
81
|
+
data: Record<string, unknown> | null;
|
|
82
|
+
createdAt: string;
|
|
83
|
+
source: PmxAxSource | null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface PmxAxEvidence {
|
|
87
|
+
id: string;
|
|
88
|
+
seq: number;
|
|
89
|
+
kind: PmxAxEvidenceKind;
|
|
90
|
+
title: string;
|
|
91
|
+
body: string | null;
|
|
92
|
+
ref: string | null;
|
|
93
|
+
nodeIds: string[];
|
|
94
|
+
data: Record<string, unknown> | null;
|
|
95
|
+
createdAt: string;
|
|
96
|
+
source: PmxAxSource | null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface PmxAxSteeringMessage {
|
|
100
|
+
id: string;
|
|
101
|
+
seq: number;
|
|
102
|
+
message: string;
|
|
103
|
+
delivered: boolean;
|
|
104
|
+
createdAt: string;
|
|
105
|
+
source: PmxAxSource | null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── Host/session capability (own table; reported by adapters) ──────
|
|
109
|
+
export interface PmxAxHostCapability {
|
|
110
|
+
host: string | null;
|
|
111
|
+
canvas: boolean;
|
|
112
|
+
hooks: boolean;
|
|
113
|
+
tools: boolean;
|
|
114
|
+
sessionMessaging: boolean;
|
|
115
|
+
permissions: boolean;
|
|
116
|
+
files: boolean;
|
|
117
|
+
uiPrompts: boolean;
|
|
118
|
+
reportedAt: string | null;
|
|
119
|
+
raw: Record<string, unknown> | null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ── Bounded timeline summary for context export ────────────────────
|
|
123
|
+
export interface PmxAxTimelineSummary {
|
|
124
|
+
recentEvents: PmxAxEvent[];
|
|
125
|
+
recentEvidence: PmxAxEvidence[];
|
|
126
|
+
pendingSteering: PmxAxSteeringMessage[];
|
|
127
|
+
counts: { events: number; evidence: number; steering: number };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── Constants ──────────────────────────────────────────────────────
|
|
131
|
+
export const AX_TIMELINE_RETENTION = 500;
|
|
132
|
+
export const AX_TIMELINE_DEFAULT_LIMIT = 50;
|
|
133
|
+
export const AX_TIMELINE_MAX_LIMIT = 200;
|
|
134
|
+
export const AX_CONTEXT_EVENT_LIMIT = 20;
|
|
135
|
+
export const AX_CONTEXT_EVIDENCE_LIMIT = 10;
|
|
136
|
+
export const AX_CONTEXT_STEERING_LIMIT = 10;
|
|
137
|
+
|
|
13
138
|
export interface PmxAxState {
|
|
14
139
|
version: 1;
|
|
15
140
|
focus: PmxAxFocusState;
|
|
141
|
+
workItems: PmxAxWorkItem[];
|
|
142
|
+
approvalGates: PmxAxApprovalGate[];
|
|
143
|
+
reviewAnnotations: PmxAxReviewAnnotation[];
|
|
16
144
|
}
|
|
17
145
|
|
|
18
146
|
export interface PmxAxPinnedContext {
|
|
@@ -35,6 +163,11 @@ export interface PmxAxContext {
|
|
|
35
163
|
};
|
|
36
164
|
pinned: PmxAxPinnedContext;
|
|
37
165
|
focus: PmxAxFocusContext;
|
|
166
|
+
workItems: PmxAxWorkItem[];
|
|
167
|
+
approvalGates: PmxAxApprovalGate[];
|
|
168
|
+
reviewAnnotations: PmxAxReviewAnnotation[];
|
|
169
|
+
timeline: PmxAxTimelineSummary;
|
|
170
|
+
host: PmxAxHostCapability | null;
|
|
38
171
|
}
|
|
39
172
|
|
|
40
173
|
const AX_SOURCES = new Set<PmxAxSource>(['agent', 'api', 'browser', 'cli', 'codex', 'copilot', 'mcp', 'sdk', 'system']);
|
|
@@ -66,6 +199,39 @@ function normalizeNodeIds(value: unknown, validNodeIds?: Set<string>): string[]
|
|
|
66
199
|
return ids;
|
|
67
200
|
}
|
|
68
201
|
|
|
202
|
+
const AX_EVENT_KINDS = new Set<PmxAxEventKind>(['prompt', 'assistant-message', 'tool-start', 'tool-result', 'failure', 'approval', 'steering']);
|
|
203
|
+
const AX_EVIDENCE_KINDS = new Set<PmxAxEvidenceKind>(['logs', 'tool-result', 'screenshot', 'file', 'diff', 'test-output']);
|
|
204
|
+
const AX_WORK_STATUSES = new Set<PmxAxWorkItemStatus>(['todo', 'in-progress', 'blocked', 'done', 'cancelled']);
|
|
205
|
+
const AX_APPROVAL_STATUSES = new Set<PmxAxApprovalStatus>(['pending', 'approved', 'rejected']);
|
|
206
|
+
const AX_REVIEW_KINDS = new Set<PmxAxReviewKind>(['comment', 'finding']);
|
|
207
|
+
const AX_REVIEW_SEVERITIES = new Set<PmxAxReviewSeverity>(['info', 'warning', 'error']);
|
|
208
|
+
const AX_REVIEW_STATUSES = new Set<PmxAxReviewStatus>(['open', 'resolved', 'dismissed']);
|
|
209
|
+
const AX_REVIEW_ANCHORS = new Set<PmxAxReviewAnchorType>(['node', 'file', 'region']);
|
|
210
|
+
|
|
211
|
+
export function isAxEventKind(value: unknown): value is PmxAxEventKind {
|
|
212
|
+
return typeof value === 'string' && AX_EVENT_KINDS.has(value as PmxAxEventKind);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function isAxEvidenceKind(value: unknown): value is PmxAxEvidenceKind {
|
|
216
|
+
return typeof value === 'string' && AX_EVIDENCE_KINDS.has(value as PmxAxEvidenceKind);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function nowIso(): string {
|
|
220
|
+
return new Date().toISOString();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function optionalString(value: unknown): string | null {
|
|
224
|
+
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function boundedRecord(value: unknown): Record<string, unknown> | null {
|
|
228
|
+
return isRecord(value) ? value : null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function axId(prefix: string): string {
|
|
232
|
+
return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
233
|
+
}
|
|
234
|
+
|
|
69
235
|
export function createEmptyAxFocusState(): PmxAxFocusState {
|
|
70
236
|
return {
|
|
71
237
|
nodeIds: [],
|
|
@@ -79,6 +245,24 @@ export function createEmptyAxState(): PmxAxState {
|
|
|
79
245
|
return {
|
|
80
246
|
version: 1,
|
|
81
247
|
focus: createEmptyAxFocusState(),
|
|
248
|
+
workItems: [],
|
|
249
|
+
approvalGates: [],
|
|
250
|
+
reviewAnnotations: [],
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function createEmptyAxHostCapability(): PmxAxHostCapability {
|
|
255
|
+
return {
|
|
256
|
+
host: null,
|
|
257
|
+
canvas: false,
|
|
258
|
+
hooks: false,
|
|
259
|
+
tools: false,
|
|
260
|
+
sessionMessaging: false,
|
|
261
|
+
permissions: false,
|
|
262
|
+
files: false,
|
|
263
|
+
uiPrompts: false,
|
|
264
|
+
reportedAt: null,
|
|
265
|
+
raw: null,
|
|
82
266
|
};
|
|
83
267
|
}
|
|
84
268
|
|
|
@@ -96,11 +280,264 @@ export function normalizeAxFocusState(input: unknown, validNodeIds?: Set<string>
|
|
|
96
280
|
};
|
|
97
281
|
}
|
|
98
282
|
|
|
283
|
+
// ── Canvas-bound normalizers (drop on missing id) ──────────────────
|
|
284
|
+
export function normalizeAxWorkItem(input: unknown, validNodeIds?: Set<string>): PmxAxWorkItem | null {
|
|
285
|
+
if (!isRecord(input) || typeof input.id !== 'string') return null;
|
|
286
|
+
const createdAt = normalizeTimestamp(input.createdAt) ?? nowIso();
|
|
287
|
+
return {
|
|
288
|
+
id: input.id,
|
|
289
|
+
title: typeof input.title === 'string' ? input.title : '(untitled)',
|
|
290
|
+
status: AX_WORK_STATUSES.has(input.status as PmxAxWorkItemStatus) ? input.status as PmxAxWorkItemStatus : 'todo',
|
|
291
|
+
detail: optionalString(input.detail),
|
|
292
|
+
nodeIds: normalizeNodeIds(input.nodeIds, validNodeIds),
|
|
293
|
+
createdAt,
|
|
294
|
+
updatedAt: normalizeTimestamp(input.updatedAt) ?? createdAt,
|
|
295
|
+
source: normalizeSource(input.source),
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function normalizeAxApprovalGate(input: unknown, validNodeIds?: Set<string>): PmxAxApprovalGate | null {
|
|
300
|
+
if (!isRecord(input) || typeof input.id !== 'string') return null;
|
|
301
|
+
return {
|
|
302
|
+
id: input.id,
|
|
303
|
+
title: typeof input.title === 'string' ? input.title : '(approval)',
|
|
304
|
+
detail: optionalString(input.detail),
|
|
305
|
+
action: optionalString(input.action),
|
|
306
|
+
status: AX_APPROVAL_STATUSES.has(input.status as PmxAxApprovalStatus) ? input.status as PmxAxApprovalStatus : 'pending',
|
|
307
|
+
nodeIds: normalizeNodeIds(input.nodeIds, validNodeIds),
|
|
308
|
+
createdAt: normalizeTimestamp(input.createdAt) ?? nowIso(),
|
|
309
|
+
resolvedAt: normalizeTimestamp(input.resolvedAt),
|
|
310
|
+
resolution: optionalString(input.resolution),
|
|
311
|
+
source: normalizeSource(input.source),
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function normalizeAxReviewAnnotation(input: unknown, validNodeIds?: Set<string>): PmxAxReviewAnnotation | null {
|
|
316
|
+
if (!isRecord(input) || typeof input.id !== 'string') return null;
|
|
317
|
+
const createdAt = normalizeTimestamp(input.createdAt) ?? nowIso();
|
|
318
|
+
const anchorType = AX_REVIEW_ANCHORS.has(input.anchorType as PmxAxReviewAnchorType) ? input.anchorType as PmxAxReviewAnchorType : 'node';
|
|
319
|
+
// node anchor pruned to a valid node; if invalid, drop the whole annotation (mirrors focus pruning)
|
|
320
|
+
let nodeId: string | null = null;
|
|
321
|
+
if (anchorType === 'node') {
|
|
322
|
+
if (typeof input.nodeId !== 'string') return null;
|
|
323
|
+
if (validNodeIds && !validNodeIds.has(input.nodeId)) return null;
|
|
324
|
+
nodeId = input.nodeId;
|
|
325
|
+
}
|
|
326
|
+
const region = isRecord(input.region) ? {
|
|
327
|
+
...(typeof input.region.line === 'number' ? { line: input.region.line } : {}),
|
|
328
|
+
...(typeof input.region.endLine === 'number' ? { endLine: input.region.endLine } : {}),
|
|
329
|
+
...(typeof input.region.label === 'string' ? { label: input.region.label } : {}),
|
|
330
|
+
} : null;
|
|
331
|
+
return {
|
|
332
|
+
id: input.id,
|
|
333
|
+
kind: AX_REVIEW_KINDS.has(input.kind as PmxAxReviewKind) ? input.kind as PmxAxReviewKind : 'comment',
|
|
334
|
+
body: typeof input.body === 'string' ? input.body : '',
|
|
335
|
+
severity: AX_REVIEW_SEVERITIES.has(input.severity as PmxAxReviewSeverity) ? input.severity as PmxAxReviewSeverity : 'info',
|
|
336
|
+
status: AX_REVIEW_STATUSES.has(input.status as PmxAxReviewStatus) ? input.status as PmxAxReviewStatus : 'open',
|
|
337
|
+
anchorType,
|
|
338
|
+
nodeId,
|
|
339
|
+
file: anchorType === 'file' ? optionalString(input.file) : null,
|
|
340
|
+
region: anchorType === 'region' ? region : null,
|
|
341
|
+
author: optionalString(input.author),
|
|
342
|
+
createdAt,
|
|
343
|
+
updatedAt: normalizeTimestamp(input.updatedAt) ?? createdAt,
|
|
344
|
+
source: normalizeSource(input.source),
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export function normalizeAxHostCapability(input: unknown): PmxAxHostCapability | null {
|
|
349
|
+
if (!isRecord(input)) return null;
|
|
350
|
+
const caps = isRecord(input.capabilities) ? input.capabilities : input;
|
|
351
|
+
const flag = (v: unknown): boolean => v === true;
|
|
352
|
+
return {
|
|
353
|
+
host: optionalString(input.host),
|
|
354
|
+
canvas: flag(caps.canvas),
|
|
355
|
+
hooks: flag(caps.hooks),
|
|
356
|
+
tools: flag(caps.tools),
|
|
357
|
+
sessionMessaging: flag(caps.sessionMessaging),
|
|
358
|
+
permissions: flag(caps.permissions),
|
|
359
|
+
files: flag(caps.files),
|
|
360
|
+
uiPrompts: flag(caps.uiPrompts),
|
|
361
|
+
reportedAt: normalizeTimestamp(input.reportedAt),
|
|
362
|
+
raw: boundedRecord(input.raw),
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ── Timeline normalizers (DB row parse; node ids kept as recorded) ──
|
|
367
|
+
export function normalizeAxEvent(input: unknown): PmxAxEvent | null {
|
|
368
|
+
if (!isRecord(input) || typeof input.id !== 'string') return null;
|
|
369
|
+
const kind = AX_EVENT_KINDS.has(input.kind as PmxAxEventKind) ? input.kind as PmxAxEventKind : null;
|
|
370
|
+
if (!kind) return null;
|
|
371
|
+
return {
|
|
372
|
+
id: input.id,
|
|
373
|
+
seq: typeof input.seq === 'number' ? input.seq : 0,
|
|
374
|
+
kind,
|
|
375
|
+
summary: typeof input.summary === 'string' ? input.summary : '',
|
|
376
|
+
detail: optionalString(input.detail),
|
|
377
|
+
nodeIds: normalizeNodeIds(input.nodeIds),
|
|
378
|
+
data: boundedRecord(input.data),
|
|
379
|
+
createdAt: normalizeTimestamp(input.createdAt) ?? nowIso(),
|
|
380
|
+
source: normalizeSource(input.source),
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export function normalizeAxEvidence(input: unknown): PmxAxEvidence | null {
|
|
385
|
+
if (!isRecord(input) || typeof input.id !== 'string') return null;
|
|
386
|
+
const kind = AX_EVIDENCE_KINDS.has(input.kind as PmxAxEvidenceKind) ? input.kind as PmxAxEvidenceKind : null;
|
|
387
|
+
if (!kind) return null;
|
|
388
|
+
return {
|
|
389
|
+
id: input.id,
|
|
390
|
+
seq: typeof input.seq === 'number' ? input.seq : 0,
|
|
391
|
+
kind,
|
|
392
|
+
title: typeof input.title === 'string' ? input.title : '',
|
|
393
|
+
body: typeof input.body === 'string' ? input.body : null,
|
|
394
|
+
ref: optionalString(input.ref),
|
|
395
|
+
nodeIds: normalizeNodeIds(input.nodeIds),
|
|
396
|
+
data: boundedRecord(input.data),
|
|
397
|
+
createdAt: normalizeTimestamp(input.createdAt) ?? nowIso(),
|
|
398
|
+
source: normalizeSource(input.source),
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export function normalizeAxSteeringMessage(input: unknown): PmxAxSteeringMessage | null {
|
|
403
|
+
if (!isRecord(input) || typeof input.id !== 'string' || typeof input.message !== 'string') return null;
|
|
404
|
+
return {
|
|
405
|
+
id: input.id,
|
|
406
|
+
seq: typeof input.seq === 'number' ? input.seq : 0,
|
|
407
|
+
message: input.message,
|
|
408
|
+
delivered: input.delivered === true,
|
|
409
|
+
createdAt: normalizeTimestamp(input.createdAt) ?? nowIso(),
|
|
410
|
+
source: normalizeSource(input.source),
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// ── Factories ──────────────────────────────────────────────────────
|
|
415
|
+
export function createAxWorkItem(
|
|
416
|
+
input: { title: string; status?: PmxAxWorkItemStatus; detail?: string | null; nodeIds?: string[] },
|
|
417
|
+
source: PmxAxSource | null,
|
|
418
|
+
validNodeIds?: Set<string>,
|
|
419
|
+
): PmxAxWorkItem {
|
|
420
|
+
const now = nowIso();
|
|
421
|
+
return {
|
|
422
|
+
id: axId('work'),
|
|
423
|
+
title: input.title,
|
|
424
|
+
status: input.status ?? 'todo',
|
|
425
|
+
detail: input.detail ?? null,
|
|
426
|
+
nodeIds: normalizeNodeIds(input.nodeIds, validNodeIds),
|
|
427
|
+
createdAt: now,
|
|
428
|
+
updatedAt: now,
|
|
429
|
+
source,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export function createAxApprovalGate(
|
|
434
|
+
input: { title: string; detail?: string | null; action?: string | null; nodeIds?: string[] },
|
|
435
|
+
source: PmxAxSource | null,
|
|
436
|
+
validNodeIds?: Set<string>,
|
|
437
|
+
): PmxAxApprovalGate {
|
|
438
|
+
return {
|
|
439
|
+
id: axId('appr'),
|
|
440
|
+
title: input.title,
|
|
441
|
+
detail: input.detail ?? null,
|
|
442
|
+
action: input.action ?? null,
|
|
443
|
+
status: 'pending',
|
|
444
|
+
nodeIds: normalizeNodeIds(input.nodeIds, validNodeIds),
|
|
445
|
+
createdAt: nowIso(),
|
|
446
|
+
resolvedAt: null,
|
|
447
|
+
resolution: null,
|
|
448
|
+
source,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export function createAxReviewAnnotation(
|
|
453
|
+
input: {
|
|
454
|
+
body: string;
|
|
455
|
+
kind?: PmxAxReviewKind;
|
|
456
|
+
severity?: PmxAxReviewSeverity;
|
|
457
|
+
anchorType?: PmxAxReviewAnchorType;
|
|
458
|
+
nodeId?: string | null;
|
|
459
|
+
file?: string | null;
|
|
460
|
+
region?: PmxAxReviewRegion | null;
|
|
461
|
+
author?: string | null;
|
|
462
|
+
},
|
|
463
|
+
source: PmxAxSource | null,
|
|
464
|
+
): PmxAxReviewAnnotation {
|
|
465
|
+
const now = nowIso();
|
|
466
|
+
const anchorType = input.anchorType ?? 'node';
|
|
467
|
+
return {
|
|
468
|
+
id: axId('rev'),
|
|
469
|
+
kind: input.kind ?? 'comment',
|
|
470
|
+
body: input.body,
|
|
471
|
+
severity: input.severity ?? 'info',
|
|
472
|
+
status: 'open',
|
|
473
|
+
anchorType,
|
|
474
|
+
nodeId: anchorType === 'node' ? (input.nodeId ?? null) : null,
|
|
475
|
+
file: anchorType === 'file' ? (input.file ?? null) : null,
|
|
476
|
+
region: anchorType === 'region' ? (input.region ?? null) : null,
|
|
477
|
+
author: input.author ?? null,
|
|
478
|
+
createdAt: now,
|
|
479
|
+
updatedAt: now,
|
|
480
|
+
source,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
export function createAxEvent(
|
|
485
|
+
input: { kind: PmxAxEventKind; summary: string; detail?: string | null; nodeIds?: string[]; data?: Record<string, unknown> | null },
|
|
486
|
+
source: PmxAxSource | null,
|
|
487
|
+
): Omit<PmxAxEvent, 'seq'> {
|
|
488
|
+
return {
|
|
489
|
+
id: axId('evt'),
|
|
490
|
+
kind: input.kind,
|
|
491
|
+
summary: input.summary,
|
|
492
|
+
detail: input.detail ?? null,
|
|
493
|
+
nodeIds: normalizeNodeIds(input.nodeIds),
|
|
494
|
+
data: input.data ?? null,
|
|
495
|
+
createdAt: nowIso(),
|
|
496
|
+
source,
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
export function createAxEvidence(
|
|
501
|
+
input: { kind: PmxAxEvidenceKind; title: string; body?: string | null; ref?: string | null; nodeIds?: string[]; data?: Record<string, unknown> | null },
|
|
502
|
+
source: PmxAxSource | null,
|
|
503
|
+
): Omit<PmxAxEvidence, 'seq'> {
|
|
504
|
+
return {
|
|
505
|
+
id: axId('evd'),
|
|
506
|
+
kind: input.kind,
|
|
507
|
+
title: input.title,
|
|
508
|
+
body: input.body ?? null,
|
|
509
|
+
ref: input.ref ?? null,
|
|
510
|
+
nodeIds: normalizeNodeIds(input.nodeIds),
|
|
511
|
+
data: input.data ?? null,
|
|
512
|
+
createdAt: nowIso(),
|
|
513
|
+
source,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
export function createAxSteeringMessage(message: string, source: PmxAxSource | null): Omit<PmxAxSteeringMessage, 'seq'> {
|
|
518
|
+
return {
|
|
519
|
+
id: axId('steer'),
|
|
520
|
+
message,
|
|
521
|
+
delivered: false,
|
|
522
|
+
createdAt: nowIso(),
|
|
523
|
+
source,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
99
527
|
export function normalizeAxState(input: unknown, validNodeIds?: Set<string>): PmxAxState {
|
|
100
528
|
if (!isRecord(input)) return createEmptyAxState();
|
|
101
529
|
return {
|
|
102
530
|
version: 1,
|
|
103
531
|
focus: normalizeAxFocusState(input.focus, validNodeIds),
|
|
532
|
+
workItems: Array.isArray(input.workItems)
|
|
533
|
+
? input.workItems.map((w) => normalizeAxWorkItem(w, validNodeIds)).filter((w): w is PmxAxWorkItem => w !== null)
|
|
534
|
+
: [],
|
|
535
|
+
approvalGates: Array.isArray(input.approvalGates)
|
|
536
|
+
? input.approvalGates.map((g) => normalizeAxApprovalGate(g, validNodeIds)).filter((g): g is PmxAxApprovalGate => g !== null)
|
|
537
|
+
: [],
|
|
538
|
+
reviewAnnotations: Array.isArray(input.reviewAnnotations)
|
|
539
|
+
? input.reviewAnnotations.map((r) => normalizeAxReviewAnnotation(r, validNodeIds)).filter((r): r is PmxAxReviewAnnotation => r !== null)
|
|
540
|
+
: [],
|
|
104
541
|
};
|
|
105
542
|
}
|
|
106
543
|
|
|
@@ -109,6 +546,11 @@ export function buildAxContext(input: {
|
|
|
109
546
|
pinned: PmxAxPinnedContext;
|
|
110
547
|
focus: PmxAxFocusState;
|
|
111
548
|
focusNodes: AgentContextNode[];
|
|
549
|
+
workItems: PmxAxWorkItem[];
|
|
550
|
+
approvalGates: PmxAxApprovalGate[];
|
|
551
|
+
reviewAnnotations: PmxAxReviewAnnotation[];
|
|
552
|
+
timeline: PmxAxTimelineSummary;
|
|
553
|
+
host: PmxAxHostCapability | null;
|
|
112
554
|
}): PmxAxContext {
|
|
113
555
|
return {
|
|
114
556
|
version: 1,
|
|
@@ -122,6 +564,11 @@ export function buildAxContext(input: {
|
|
|
122
564
|
...input.focus,
|
|
123
565
|
nodes: input.focusNodes,
|
|
124
566
|
},
|
|
567
|
+
workItems: input.workItems,
|
|
568
|
+
approvalGates: input.approvalGates,
|
|
569
|
+
reviewAnnotations: input.reviewAnnotations,
|
|
570
|
+
timeline: input.timeline,
|
|
571
|
+
host: input.host,
|
|
125
572
|
};
|
|
126
573
|
}
|
|
127
574
|
|
package/src/server/canvas-db.ts
CHANGED
|
@@ -17,7 +17,26 @@ import type {
|
|
|
17
17
|
CanvasSnapshotListOptions,
|
|
18
18
|
ViewportState,
|
|
19
19
|
} from './canvas-state.js';
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
createEmptyAxState,
|
|
22
|
+
normalizeAxState,
|
|
23
|
+
normalizeAxEvent,
|
|
24
|
+
normalizeAxEvidence,
|
|
25
|
+
normalizeAxSteeringMessage,
|
|
26
|
+
normalizeAxHostCapability,
|
|
27
|
+
AX_TIMELINE_RETENTION,
|
|
28
|
+
AX_TIMELINE_DEFAULT_LIMIT,
|
|
29
|
+
AX_TIMELINE_MAX_LIMIT,
|
|
30
|
+
AX_CONTEXT_EVENT_LIMIT,
|
|
31
|
+
AX_CONTEXT_EVIDENCE_LIMIT,
|
|
32
|
+
AX_CONTEXT_STEERING_LIMIT,
|
|
33
|
+
type PmxAxState,
|
|
34
|
+
type PmxAxEvent,
|
|
35
|
+
type PmxAxEvidence,
|
|
36
|
+
type PmxAxSteeringMessage,
|
|
37
|
+
type PmxAxHostCapability,
|
|
38
|
+
type PmxAxTimelineSummary,
|
|
39
|
+
} from './ax-state.js';
|
|
21
40
|
|
|
22
41
|
// ── Schema ──────────────────────────────────────────────────────
|
|
23
42
|
|
|
@@ -148,6 +167,49 @@ const SCHEMA_SQL = `
|
|
|
148
167
|
data BLOB NOT NULL,
|
|
149
168
|
json_bytes INTEGER NOT NULL
|
|
150
169
|
);
|
|
170
|
+
|
|
171
|
+
CREATE TABLE IF NOT EXISTS ax_events (
|
|
172
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
173
|
+
id TEXT NOT NULL UNIQUE,
|
|
174
|
+
kind TEXT NOT NULL,
|
|
175
|
+
summary TEXT NOT NULL,
|
|
176
|
+
detail TEXT,
|
|
177
|
+
node_ids TEXT NOT NULL DEFAULT '[]',
|
|
178
|
+
data TEXT,
|
|
179
|
+
created_at TEXT NOT NULL,
|
|
180
|
+
source TEXT
|
|
181
|
+
);
|
|
182
|
+
CREATE INDEX IF NOT EXISTS idx_ax_events_seq ON ax_events (seq);
|
|
183
|
+
|
|
184
|
+
CREATE TABLE IF NOT EXISTS ax_evidence (
|
|
185
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
186
|
+
id TEXT NOT NULL UNIQUE,
|
|
187
|
+
kind TEXT NOT NULL,
|
|
188
|
+
title TEXT NOT NULL,
|
|
189
|
+
body TEXT,
|
|
190
|
+
ref TEXT,
|
|
191
|
+
node_ids TEXT NOT NULL DEFAULT '[]',
|
|
192
|
+
data TEXT,
|
|
193
|
+
created_at TEXT NOT NULL,
|
|
194
|
+
source TEXT
|
|
195
|
+
);
|
|
196
|
+
CREATE INDEX IF NOT EXISTS idx_ax_evidence_seq ON ax_evidence (seq);
|
|
197
|
+
|
|
198
|
+
CREATE TABLE IF NOT EXISTS ax_steering (
|
|
199
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
200
|
+
id TEXT NOT NULL UNIQUE,
|
|
201
|
+
message TEXT NOT NULL,
|
|
202
|
+
delivered INTEGER NOT NULL DEFAULT 0,
|
|
203
|
+
created_at TEXT NOT NULL,
|
|
204
|
+
source TEXT
|
|
205
|
+
);
|
|
206
|
+
CREATE INDEX IF NOT EXISTS idx_ax_steering_seq ON ax_steering (seq);
|
|
207
|
+
|
|
208
|
+
CREATE TABLE IF NOT EXISTS ax_host_capabilities (
|
|
209
|
+
host TEXT PRIMARY KEY,
|
|
210
|
+
reported_at TEXT NOT NULL,
|
|
211
|
+
payload TEXT NOT NULL
|
|
212
|
+
);
|
|
151
213
|
`;
|
|
152
214
|
|
|
153
215
|
function normalizePositiveInteger(value: number | undefined): number | undefined {
|
|
@@ -743,3 +805,124 @@ export function hasBlobInDB(db: Database, sha256: string): boolean {
|
|
|
743
805
|
).get(sha256);
|
|
744
806
|
return (row?.c ?? 0) > 0;
|
|
745
807
|
}
|
|
808
|
+
|
|
809
|
+
// ── AX Timeline Persistence (NOT snapshotted; bounded by retention) ──
|
|
810
|
+
|
|
811
|
+
function safeParseJson(raw: string | null | undefined): unknown {
|
|
812
|
+
if (!raw) return null;
|
|
813
|
+
try {
|
|
814
|
+
return JSON.parse(raw);
|
|
815
|
+
} catch {
|
|
816
|
+
return null;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
export interface AxTimelineQuery {
|
|
821
|
+
limit?: number;
|
|
822
|
+
sessionId?: string;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function clampTimelineLimit(limit: number | undefined): number {
|
|
826
|
+
if (typeof limit !== 'number' || !Number.isFinite(limit) || limit <= 0) return AX_TIMELINE_DEFAULT_LIMIT;
|
|
827
|
+
return Math.min(Math.floor(limit), AX_TIMELINE_MAX_LIMIT);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function trimAxTable(db: Database, table: 'ax_events' | 'ax_evidence' | 'ax_steering'): void {
|
|
831
|
+
db.run(
|
|
832
|
+
`DELETE FROM ${table} WHERE seq <= (SELECT seq FROM ${table} ORDER BY seq DESC LIMIT 1 OFFSET ?)`,
|
|
833
|
+
[AX_TIMELINE_RETENTION],
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function readLastSeq(db: Database, table: 'ax_events' | 'ax_evidence' | 'ax_steering'): number {
|
|
838
|
+
const row = db.query<{ seq: number }, []>(`SELECT seq FROM ${table} ORDER BY seq DESC LIMIT 1`).get();
|
|
839
|
+
return row ? Number(row.seq) : 0;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
export function appendAxEventToDB(db: Database, ev: Omit<PmxAxEvent, 'seq'>): PmxAxEvent {
|
|
843
|
+
db.run(
|
|
844
|
+
'INSERT INTO ax_events (id, kind, summary, detail, node_ids, data, created_at, source) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
|
845
|
+
[ev.id, ev.kind, ev.summary, ev.detail, JSON.stringify(ev.nodeIds), ev.data ? JSON.stringify(ev.data) : null, ev.createdAt, ev.source],
|
|
846
|
+
);
|
|
847
|
+
const seq = readLastSeq(db, 'ax_events');
|
|
848
|
+
trimAxTable(db, 'ax_events');
|
|
849
|
+
return { ...ev, seq };
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
export function appendAxEvidenceToDB(db: Database, ev: Omit<PmxAxEvidence, 'seq'>): PmxAxEvidence {
|
|
853
|
+
db.run(
|
|
854
|
+
'INSERT INTO ax_evidence (id, kind, title, body, ref, node_ids, data, created_at, source) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
|
855
|
+
[ev.id, ev.kind, ev.title, ev.body, ev.ref, JSON.stringify(ev.nodeIds), ev.data ? JSON.stringify(ev.data) : null, ev.createdAt, ev.source],
|
|
856
|
+
);
|
|
857
|
+
const seq = readLastSeq(db, 'ax_evidence');
|
|
858
|
+
trimAxTable(db, 'ax_evidence');
|
|
859
|
+
return { ...ev, seq };
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
export function appendAxSteeringToDB(db: Database, s: Omit<PmxAxSteeringMessage, 'seq'>): PmxAxSteeringMessage {
|
|
863
|
+
db.run(
|
|
864
|
+
'INSERT INTO ax_steering (id, message, delivered, created_at, source) VALUES (?, ?, ?, ?, ?)',
|
|
865
|
+
[s.id, s.message, s.delivered ? 1 : 0, s.createdAt, s.source],
|
|
866
|
+
);
|
|
867
|
+
const seq = readLastSeq(db, 'ax_steering');
|
|
868
|
+
trimAxTable(db, 'ax_steering');
|
|
869
|
+
return { ...s, seq };
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
export function markAxSteeringDeliveredInDB(db: Database, id: string): boolean {
|
|
873
|
+
const r = db.run('UPDATE ax_steering SET delivered = 1 WHERE id = ?', [id]);
|
|
874
|
+
return r.changes > 0;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
export function loadAxEventsFromDB(db: Database, q: AxTimelineQuery = {}): PmxAxEvent[] {
|
|
878
|
+
interface Row { seq: number; id: string; kind: string; summary: string; detail: string | null; node_ids: string; data: string | null; created_at: string; source: string | null }
|
|
879
|
+
const rows = db.query<Row, [number]>('SELECT * FROM ax_events ORDER BY seq DESC LIMIT ?').all(clampTimelineLimit(q.limit));
|
|
880
|
+
return rows
|
|
881
|
+
.map((r) => normalizeAxEvent({ ...r, createdAt: r.created_at, nodeIds: safeParseJson(r.node_ids), data: safeParseJson(r.data) }))
|
|
882
|
+
.filter((e): e is PmxAxEvent => e !== null);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
export function loadAxEvidenceFromDB(db: Database, q: AxTimelineQuery = {}): PmxAxEvidence[] {
|
|
886
|
+
interface Row { seq: number; id: string; kind: string; title: string; body: string | null; ref: string | null; node_ids: string; data: string | null; created_at: string; source: string | null }
|
|
887
|
+
const rows = db.query<Row, [number]>('SELECT * FROM ax_evidence ORDER BY seq DESC LIMIT ?').all(clampTimelineLimit(q.limit));
|
|
888
|
+
return rows
|
|
889
|
+
.map((r) => normalizeAxEvidence({ ...r, createdAt: r.created_at, nodeIds: safeParseJson(r.node_ids), data: safeParseJson(r.data) }))
|
|
890
|
+
.filter((e): e is PmxAxEvidence => e !== null);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
export function loadAxSteeringFromDB(db: Database, q: AxTimelineQuery & { onlyPending?: boolean } = {}): PmxAxSteeringMessage[] {
|
|
894
|
+
interface Row { seq: number; id: string; message: string; delivered: number; created_at: string; source: string | null }
|
|
895
|
+
const sql = q.onlyPending
|
|
896
|
+
? 'SELECT * FROM ax_steering WHERE delivered = 0 ORDER BY seq DESC LIMIT ?'
|
|
897
|
+
: 'SELECT * FROM ax_steering ORDER BY seq DESC LIMIT ?';
|
|
898
|
+
const rows = db.query<Row, [number]>(sql).all(clampTimelineLimit(q.limit));
|
|
899
|
+
return rows
|
|
900
|
+
.map((r) => normalizeAxSteeringMessage({ ...r, createdAt: r.created_at, delivered: r.delivered === 1 }))
|
|
901
|
+
.filter((s): s is PmxAxSteeringMessage => s !== null);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
function countRows(db: Database, table: 'ax_events' | 'ax_evidence' | 'ax_steering'): number {
|
|
905
|
+
return Number(db.query<{ n: number }, []>(`SELECT COUNT(*) AS n FROM ${table}`).get()?.n ?? 0);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
export function loadAxTimelineSummaryFromDB(db: Database): PmxAxTimelineSummary {
|
|
909
|
+
return {
|
|
910
|
+
recentEvents: loadAxEventsFromDB(db, { limit: AX_CONTEXT_EVENT_LIMIT }),
|
|
911
|
+
recentEvidence: loadAxEvidenceFromDB(db, { limit: AX_CONTEXT_EVIDENCE_LIMIT }),
|
|
912
|
+
pendingSteering: loadAxSteeringFromDB(db, { onlyPending: true, limit: AX_CONTEXT_STEERING_LIMIT }),
|
|
913
|
+
counts: { events: countRows(db, 'ax_events'), evidence: countRows(db, 'ax_evidence'), steering: countRows(db, 'ax_steering') },
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
export function upsertAxHostCapabilityToDB(db: Database, cap: PmxAxHostCapability): void {
|
|
918
|
+
const host = cap.host ?? 'default';
|
|
919
|
+
db.run(
|
|
920
|
+
'INSERT OR REPLACE INTO ax_host_capabilities (host, reported_at, payload) VALUES (?, ?, ?)',
|
|
921
|
+
[host, cap.reportedAt ?? new Date().toISOString(), JSON.stringify(cap)],
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
export function loadAxHostCapabilityFromDB(db: Database): PmxAxHostCapability | null {
|
|
926
|
+
const row = db.query<{ payload: string }, []>('SELECT payload FROM ax_host_capabilities ORDER BY reported_at DESC LIMIT 1').get();
|
|
927
|
+
return row ? normalizeAxHostCapability(safeParseJson(row.payload)) : null;
|
|
928
|
+
}
|