konpeki 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/authoring-visuals/SKILL.md +12 -1
- package/README.md +4 -4
- package/composition/README.md +26 -49
- package/composition/compile.ts +1 -1
- package/composition/document.ts +2 -111
- package/composition/schema.json +3 -3
- package/composition/schema.ts +1 -1
- package/composition/types.ts +1 -20
- package/composition/validate.ts +3 -461
- package/composition/visualizations.ts +0 -16
- package/docs/development.md +4 -0
- package/docs/workflow.md +49 -7
- package/package.json +3 -1
- package/runtime/konpeki.mjs +182 -460
- package/slides/introducing-konpeki/composition.json +1740 -1735
- package/src/app/App.tsx +92 -18
- package/src/components/BuildOrb.tsx +40 -0
- package/src/components/Canvas.tsx +17 -0
- package/src/components/InspectorPanel.tsx +37 -3
- package/src/components/LeftPanel.tsx +16 -3
- package/src/components/RevisionNotes.tsx +56 -0
- package/src/components/WorkspaceChrome.tsx +6 -6
- package/src/lib/examples/react-page-migration.json +1 -1
- package/src/lib/export-png.ts +4 -1
- package/src/lib/file-session.ts +14 -1
- package/src/lib/review.ts +26 -0
- package/src/lib/use-file-session.ts +49 -13
- package/src/styles/base.css +16 -0
- package/src/styles/canvas.css +43 -12
- package/src/styles/chrome.css +78 -28
- package/src/styles/feedback.css +19 -1
- package/src/styles/left-panel.css +48 -7
- package/src/styles/right-panel.css +62 -20
- package/src/styles/shell.css +41 -26
package/composition/validate.ts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
2
2
|
import { schema } from "./schema.ts";
|
|
3
|
-
import { migrateLegacyDiagramType } from "./visualizations.ts";
|
|
4
3
|
import {
|
|
5
|
-
compositionSchema,
|
|
6
|
-
legacyCompositionSchemas,
|
|
7
4
|
type CompositionDocument,
|
|
8
5
|
} from "./types.ts";
|
|
9
6
|
import { vectorAttributeNames } from "./vector.ts";
|
|
@@ -17,461 +14,8 @@ const structural = new Ajv2020({
|
|
|
17
14
|
allErrors: true,
|
|
18
15
|
strict: false,
|
|
19
16
|
}).compile<CompositionDocument>(schema);
|
|
20
|
-
function migratePreV5Document(input: Record<string, unknown>) {
|
|
21
|
-
const slide = input.slide as Record<string, unknown> | undefined;
|
|
22
|
-
const components = Array.isArray(slide?.components)
|
|
23
|
-
? slide.components.map((value: unknown) => {
|
|
24
|
-
if (!value || typeof value !== "object") return value;
|
|
25
|
-
const component = value as Record<string, unknown>;
|
|
26
|
-
if (
|
|
27
|
-
component.kind !== "callout" &&
|
|
28
|
-
component.kind !== "comparison" &&
|
|
29
|
-
component.kind !== "text-block"
|
|
30
|
-
)
|
|
31
|
-
return component;
|
|
32
|
-
const previous =
|
|
33
|
-
component.appearance && typeof component.appearance === "object"
|
|
34
|
-
? (component.appearance as Record<string, unknown>)
|
|
35
|
-
: {};
|
|
36
|
-
const common = Object.fromEntries(
|
|
37
|
-
["alignment", "border", "titleStyle", "rule"].flatMap((key) =>
|
|
38
|
-
key in previous ? [[key, previous[key]]] : [],
|
|
39
|
-
),
|
|
40
|
-
);
|
|
41
|
-
const appearance =
|
|
42
|
-
component.kind === "callout"
|
|
43
|
-
? {
|
|
44
|
-
...common,
|
|
45
|
-
layout: "single",
|
|
46
|
-
orientation: "horizontal",
|
|
47
|
-
purpose: "emphasis",
|
|
48
|
-
treatment: "strong",
|
|
49
|
-
}
|
|
50
|
-
: component.kind === "comparison"
|
|
51
|
-
? {
|
|
52
|
-
...common,
|
|
53
|
-
layout: "two-column",
|
|
54
|
-
orientation: "horizontal",
|
|
55
|
-
purpose: "comparison",
|
|
56
|
-
treatment: "plain",
|
|
57
|
-
}
|
|
58
|
-
: {
|
|
59
|
-
...previous,
|
|
60
|
-
purpose: previous.purpose ?? "narrative",
|
|
61
|
-
treatment: previous.treatment ?? "plain",
|
|
62
|
-
};
|
|
63
|
-
return { ...component, kind: "text-block", appearance };
|
|
64
|
-
})
|
|
65
|
-
: slide?.components;
|
|
66
|
-
return {
|
|
67
|
-
...Object.fromEntries(
|
|
68
|
-
Object.entries(input).filter(
|
|
69
|
-
([key]) => key !== "target" && key !== "slide",
|
|
70
|
-
),
|
|
71
|
-
),
|
|
72
|
-
schema: "konpeki-composition/v5",
|
|
73
|
-
...(slide
|
|
74
|
-
? { slides: [{ ...slide, name: "Slide 01", components }] }
|
|
75
|
-
: {}),
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
function migrateV5Document(input: Record<string, unknown>) {
|
|
79
|
-
const slides = Array.isArray(input.slides)
|
|
80
|
-
? input.slides.map((value: unknown) => {
|
|
81
|
-
if (!value || typeof value !== "object") return value;
|
|
82
|
-
const slide = value as Record<string, unknown>;
|
|
83
|
-
const previousComponents = Array.isArray(slide.components)
|
|
84
|
-
? (slide.components as Record<string, unknown>[])
|
|
85
|
-
: [];
|
|
86
|
-
const pageNumber = previousComponents.find(
|
|
87
|
-
(component) => component.kind === "page-number",
|
|
88
|
-
);
|
|
89
|
-
const pageAppearance =
|
|
90
|
-
pageNumber?.appearance && typeof pageNumber.appearance === "object"
|
|
91
|
-
? (pageNumber.appearance as Record<string, unknown>)
|
|
92
|
-
: {};
|
|
93
|
-
const removedIds = new Set(
|
|
94
|
-
previousComponents
|
|
95
|
-
.filter((component) => component.kind === "page-number")
|
|
96
|
-
.map((component) => String(component.id)),
|
|
97
|
-
);
|
|
98
|
-
const removedSlots = new Set(
|
|
99
|
-
previousComponents
|
|
100
|
-
.filter((component) => component.kind === "page-number")
|
|
101
|
-
.flatMap((component) =>
|
|
102
|
-
Array.isArray(component.slotIds)
|
|
103
|
-
? component.slotIds.map(String)
|
|
104
|
-
: [],
|
|
105
|
-
),
|
|
106
|
-
);
|
|
107
|
-
const components = previousComponents.flatMap((component) => {
|
|
108
|
-
const appearance =
|
|
109
|
-
component.appearance && typeof component.appearance === "object"
|
|
110
|
-
? (component.appearance as Record<string, unknown>)
|
|
111
|
-
: {};
|
|
112
|
-
if (component.kind === "page-number") return [];
|
|
113
|
-
if (component.kind === "evidence")
|
|
114
|
-
return [{ ...component, kind: "visual" }];
|
|
115
|
-
if (component.kind === "process")
|
|
116
|
-
return [
|
|
117
|
-
{
|
|
118
|
-
...component,
|
|
119
|
-
kind: "diagram",
|
|
120
|
-
appearance: { ...appearance, type: "process" },
|
|
121
|
-
},
|
|
122
|
-
];
|
|
123
|
-
if (component.kind === "system-map")
|
|
124
|
-
return [
|
|
125
|
-
{
|
|
126
|
-
...component,
|
|
127
|
-
kind: "diagram",
|
|
128
|
-
appearance: { ...appearance, type: "system" },
|
|
129
|
-
},
|
|
130
|
-
];
|
|
131
|
-
return [component];
|
|
132
|
-
});
|
|
133
|
-
const componentIds = new Set(components.map((component) => String(component.id)));
|
|
134
|
-
const groups = Array.isArray(slide.groups)
|
|
135
|
-
? (slide.groups as Record<string, unknown>[])
|
|
136
|
-
.map((group) => ({
|
|
137
|
-
...group,
|
|
138
|
-
childIds: Array.isArray(group.childIds)
|
|
139
|
-
? group.childIds.filter((id) => !removedIds.has(String(id)))
|
|
140
|
-
: group.childIds,
|
|
141
|
-
}))
|
|
142
|
-
.filter(
|
|
143
|
-
(group) =>
|
|
144
|
-
!Array.isArray(group.childIds) || group.childIds.length > 0,
|
|
145
|
-
)
|
|
146
|
-
: slide.groups;
|
|
147
|
-
const groupIds = new Set(
|
|
148
|
-
Array.isArray(groups)
|
|
149
|
-
? groups.map((group) => String(group.id))
|
|
150
|
-
: [],
|
|
151
|
-
);
|
|
152
|
-
const contentSlots = Array.isArray(slide.contentSlots)
|
|
153
|
-
? (slide.contentSlots as Record<string, unknown>[])
|
|
154
|
-
.filter((slot) => !removedSlots.has(String(slot.id)))
|
|
155
|
-
.map((slot) => {
|
|
156
|
-
if (!Array.isArray(slot.targets)) return slot;
|
|
157
|
-
const targets = slot.targets.filter(
|
|
158
|
-
(target) =>
|
|
159
|
-
!removedIds.has(String(target)) &&
|
|
160
|
-
!removedSlots.has(String(target)),
|
|
161
|
-
);
|
|
162
|
-
return {
|
|
163
|
-
...slot,
|
|
164
|
-
targets: targets.length
|
|
165
|
-
? targets
|
|
166
|
-
: [String(components[0]?.id ?? "")],
|
|
167
|
-
};
|
|
168
|
-
})
|
|
169
|
-
: slide.contentSlots;
|
|
170
|
-
return {
|
|
171
|
-
...slide,
|
|
172
|
-
pageNumber: {
|
|
173
|
-
style: pageNumber ? (pageAppearance.style ?? "01") : "none",
|
|
174
|
-
color: pageAppearance.color ?? "muted",
|
|
175
|
-
},
|
|
176
|
-
components,
|
|
177
|
-
contentSlots,
|
|
178
|
-
groups,
|
|
179
|
-
readingOrder: Array.isArray(slide.readingOrder)
|
|
180
|
-
? (slide.readingOrder as Record<string, unknown>[]).filter((entry) =>
|
|
181
|
-
entry.kind === "group"
|
|
182
|
-
? groupIds.has(String(entry.id))
|
|
183
|
-
: componentIds.has(String(entry.id)),
|
|
184
|
-
)
|
|
185
|
-
: slide.readingOrder,
|
|
186
|
-
paintOrder: Array.isArray(slide.paintOrder)
|
|
187
|
-
? slide.paintOrder.filter((id) => !removedIds.has(String(id)))
|
|
188
|
-
: slide.paintOrder,
|
|
189
|
-
relationships: Array.isArray(slide.relationships)
|
|
190
|
-
? (slide.relationships as Record<string, unknown>[]).filter(
|
|
191
|
-
(relationship) => {
|
|
192
|
-
const from = relationship.from as Record<string, unknown> | undefined;
|
|
193
|
-
const to = relationship.to as Record<string, unknown> | undefined;
|
|
194
|
-
return (
|
|
195
|
-
!removedIds.has(String(from?.nodeId)) &&
|
|
196
|
-
!removedIds.has(String(to?.nodeId))
|
|
197
|
-
);
|
|
198
|
-
},
|
|
199
|
-
)
|
|
200
|
-
: slide.relationships,
|
|
201
|
-
};
|
|
202
|
-
})
|
|
203
|
-
: input.slides;
|
|
204
|
-
return { ...input, schema: compositionSchema, slides };
|
|
205
|
-
}
|
|
206
|
-
function migrateV7Document(input: Record<string, unknown>) {
|
|
207
|
-
const slides = Array.isArray(input.slides)
|
|
208
|
-
? input.slides.map((value: unknown) => {
|
|
209
|
-
if (!value || typeof value !== "object") return value;
|
|
210
|
-
const slide = value as Record<string, unknown>;
|
|
211
|
-
const components = Array.isArray(slide.components)
|
|
212
|
-
? slide.components.map((value: unknown) => {
|
|
213
|
-
if (!value || typeof value !== "object") return value;
|
|
214
|
-
const component = value as Record<string, unknown>;
|
|
215
|
-
const appearance =
|
|
216
|
-
component.appearance && typeof component.appearance === "object"
|
|
217
|
-
? (component.appearance as Record<string, unknown>)
|
|
218
|
-
: {};
|
|
219
|
-
if (component.kind !== "visual" || appearance.template !== "image")
|
|
220
|
-
return component;
|
|
221
|
-
return {
|
|
222
|
-
...component,
|
|
223
|
-
kind: "image",
|
|
224
|
-
appearance: {
|
|
225
|
-
...(appearance.border ? { border: appearance.border } : {}),
|
|
226
|
-
fit: "contain",
|
|
227
|
-
},
|
|
228
|
-
};
|
|
229
|
-
})
|
|
230
|
-
: slide.components;
|
|
231
|
-
return { ...slide, components };
|
|
232
|
-
})
|
|
233
|
-
: input.slides;
|
|
234
|
-
return { ...input, schema: compositionSchema, slides };
|
|
235
|
-
}
|
|
236
|
-
function migrateV10Document(input: Record<string, unknown>) {
|
|
237
|
-
const slides = Array.isArray(input.slides)
|
|
238
|
-
? input.slides.map((value: unknown) => {
|
|
239
|
-
if (!value || typeof value !== "object") return value;
|
|
240
|
-
const slide = value as Record<string, unknown>;
|
|
241
|
-
const components = Array.isArray(slide.components)
|
|
242
|
-
? slide.components.map((value: unknown) => {
|
|
243
|
-
if (!value || typeof value !== "object") return value;
|
|
244
|
-
const component = value as Record<string, unknown>;
|
|
245
|
-
const appearance =
|
|
246
|
-
component.appearance && typeof component.appearance === "object"
|
|
247
|
-
? (component.appearance as Record<string, unknown>)
|
|
248
|
-
: {};
|
|
249
|
-
if (component.kind === "headline")
|
|
250
|
-
return {
|
|
251
|
-
...component,
|
|
252
|
-
kind: "text-block",
|
|
253
|
-
appearance: {
|
|
254
|
-
...appearance,
|
|
255
|
-
role: "title",
|
|
256
|
-
layout: "single",
|
|
257
|
-
orientation: "horizontal",
|
|
258
|
-
purpose: "narrative",
|
|
259
|
-
treatment: "plain",
|
|
260
|
-
logicalOrder: "none",
|
|
261
|
-
},
|
|
262
|
-
};
|
|
263
|
-
if (component.kind === "footnote")
|
|
264
|
-
return {
|
|
265
|
-
...component,
|
|
266
|
-
kind: "text-block",
|
|
267
|
-
appearance: {
|
|
268
|
-
...appearance,
|
|
269
|
-
role: "footnote",
|
|
270
|
-
layout: "single",
|
|
271
|
-
orientation: "horizontal",
|
|
272
|
-
purpose: "narrative",
|
|
273
|
-
treatment: "plain",
|
|
274
|
-
logicalOrder: "none",
|
|
275
|
-
titleStyle: "plain",
|
|
276
|
-
},
|
|
277
|
-
};
|
|
278
|
-
if (component.kind === "text-block")
|
|
279
|
-
return {
|
|
280
|
-
...component,
|
|
281
|
-
appearance: { role: "body", ...appearance },
|
|
282
|
-
};
|
|
283
|
-
if (component.kind === "visual")
|
|
284
|
-
return { ...component, kind: "chart" };
|
|
285
|
-
if (component.kind === "diagram") {
|
|
286
|
-
const { template, ...rest } = appearance;
|
|
287
|
-
return {
|
|
288
|
-
...component,
|
|
289
|
-
appearance: {
|
|
290
|
-
...rest,
|
|
291
|
-
layout: template ?? appearance.layout ?? "linear",
|
|
292
|
-
},
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
if (component.kind === "icon" || component.kind === "shape") {
|
|
296
|
-
const slotIds = Array.isArray(component.slotIds)
|
|
297
|
-
? component.slotIds.map(String)
|
|
298
|
-
: [];
|
|
299
|
-
const slotId = slotIds[0] ?? `${String(component.id)}-content`;
|
|
300
|
-
const primitive = component.kind === "icon"
|
|
301
|
-
? {
|
|
302
|
-
kind: "icon" as const,
|
|
303
|
-
...(typeof component.intent === "string" && component.intent.trim()
|
|
304
|
-
? { name: component.intent.trim() }
|
|
305
|
-
: {}),
|
|
306
|
-
...(appearance.style ? { style: appearance.style } : {}),
|
|
307
|
-
...(appearance.color ? { color: appearance.color } : {}),
|
|
308
|
-
}
|
|
309
|
-
: {
|
|
310
|
-
kind: "shape" as const,
|
|
311
|
-
shape: appearance.shape ?? "rectangle",
|
|
312
|
-
...(appearance.color ? { color: appearance.color } : {}),
|
|
313
|
-
...(appearance.fill ? { fill: appearance.fill } : {}),
|
|
314
|
-
};
|
|
315
|
-
return {
|
|
316
|
-
...component,
|
|
317
|
-
kind: "diagram",
|
|
318
|
-
slotIds: slotIds.length ? slotIds : [slotId],
|
|
319
|
-
appearance: {
|
|
320
|
-
type: "system",
|
|
321
|
-
layout: "hub-and-spoke",
|
|
322
|
-
border: appearance.border ?? "none",
|
|
323
|
-
colorScheme: "accent-with-muted-context",
|
|
324
|
-
emphasis: "primary",
|
|
325
|
-
density: "sparse",
|
|
326
|
-
},
|
|
327
|
-
topology: {
|
|
328
|
-
kind: "explicit",
|
|
329
|
-
nodes: (slotIds.length ? slotIds : [slotId]).map(
|
|
330
|
-
(currentSlotId, index) => ({
|
|
331
|
-
id: index
|
|
332
|
-
? `${String(component.id)}-slot-${index + 1}`
|
|
333
|
-
: String(component.id),
|
|
334
|
-
slotId: currentSlotId,
|
|
335
|
-
...(index ? { visible: false } : { primitive }),
|
|
336
|
-
...(component.preferredRect && typeof component.preferredRect === "object"
|
|
337
|
-
? { preferredRect: component.preferredRect }
|
|
338
|
-
: {}),
|
|
339
|
-
}),
|
|
340
|
-
),
|
|
341
|
-
edges: [],
|
|
342
|
-
},
|
|
343
|
-
};
|
|
344
|
-
}
|
|
345
|
-
return component;
|
|
346
|
-
})
|
|
347
|
-
: slide.components;
|
|
348
|
-
const contentSlots = Array.isArray(slide.contentSlots)
|
|
349
|
-
? slide.contentSlots.map((value: unknown) => {
|
|
350
|
-
if (!value || typeof value !== "object") return value;
|
|
351
|
-
const slot = value as Record<string, unknown>;
|
|
352
|
-
return slot.role === "icon" || slot.role === "shape"
|
|
353
|
-
? { ...slot, role: "entity" }
|
|
354
|
-
: slot;
|
|
355
|
-
})
|
|
356
|
-
: slide.contentSlots;
|
|
357
|
-
return { ...slide, components, contentSlots };
|
|
358
|
-
})
|
|
359
|
-
: input.slides;
|
|
360
|
-
return { ...input, schema: "konpeki-composition/v11", slides };
|
|
361
|
-
}
|
|
362
|
-
function migrateV11Document(input: Record<string, unknown>) {
|
|
363
|
-
const slides = Array.isArray(input.slides)
|
|
364
|
-
? input.slides.map((value: unknown) => {
|
|
365
|
-
if (!value || typeof value !== "object") return value;
|
|
366
|
-
const slide = value as Record<string, unknown>;
|
|
367
|
-
const components = Array.isArray(slide.components)
|
|
368
|
-
? slide.components.map((value: unknown) => {
|
|
369
|
-
if (!value || typeof value !== "object") return value;
|
|
370
|
-
const component = value as Record<string, unknown>;
|
|
371
|
-
if (component.kind !== "diagram") return component;
|
|
372
|
-
const appearance =
|
|
373
|
-
component.appearance && typeof component.appearance === "object"
|
|
374
|
-
? (component.appearance as Record<string, unknown>)
|
|
375
|
-
: {};
|
|
376
|
-
const migratedType = migrateLegacyDiagramType(
|
|
377
|
-
appearance.type,
|
|
378
|
-
appearance.layout,
|
|
379
|
-
);
|
|
380
|
-
const { layout, ...rest } = appearance;
|
|
381
|
-
return {
|
|
382
|
-
...component,
|
|
383
|
-
appearance: {
|
|
384
|
-
...rest,
|
|
385
|
-
type: migratedType ?? null,
|
|
386
|
-
},
|
|
387
|
-
};
|
|
388
|
-
})
|
|
389
|
-
: slide.components;
|
|
390
|
-
return { ...slide, components };
|
|
391
|
-
})
|
|
392
|
-
: input.slides;
|
|
393
|
-
return { ...input, schema: compositionSchema, slides };
|
|
394
|
-
}
|
|
395
|
-
function migrateInterimV12Sankey(input: unknown) {
|
|
396
|
-
if (!input || typeof input !== "object") return input;
|
|
397
|
-
const document = input as Record<string, unknown>;
|
|
398
|
-
if (document.schema !== compositionSchema || !Array.isArray(document.slides))
|
|
399
|
-
return input;
|
|
400
|
-
const slides = document.slides.map((value: unknown) => {
|
|
401
|
-
if (!value || typeof value !== "object") return value;
|
|
402
|
-
const slide = value as Record<string, unknown>;
|
|
403
|
-
if (!Array.isArray(slide.components)) return value;
|
|
404
|
-
const components = slide.components.map((componentValue: unknown) => {
|
|
405
|
-
if (!componentValue || typeof componentValue !== "object") return componentValue;
|
|
406
|
-
const component = componentValue as Record<string, unknown>;
|
|
407
|
-
const appearance = component.appearance && typeof component.appearance === "object"
|
|
408
|
-
? component.appearance as Record<string, unknown>
|
|
409
|
-
: {};
|
|
410
|
-
if (component.kind !== "diagram" || appearance.type !== "sankey")
|
|
411
|
-
return componentValue;
|
|
412
|
-
const { type: _type, ...sharedAppearance } = appearance;
|
|
413
|
-
return {
|
|
414
|
-
...component,
|
|
415
|
-
kind: "chart",
|
|
416
|
-
appearance: { ...sharedAppearance, template: "sankey" },
|
|
417
|
-
};
|
|
418
|
-
});
|
|
419
|
-
return { ...slide, components };
|
|
420
|
-
});
|
|
421
|
-
return { ...document, slides };
|
|
422
|
-
}
|
|
423
|
-
function preserveLegacyVisualizationChoices(input: unknown) {
|
|
424
|
-
if (!input || typeof input !== "object") return input;
|
|
425
|
-
const document = input as Record<string, unknown>;
|
|
426
|
-
if (!Array.isArray(document.slides)) return input;
|
|
427
|
-
return { ...document, slides: document.slides.map((value: unknown) => {
|
|
428
|
-
if (!value || typeof value !== "object") return value;
|
|
429
|
-
const slide = value as Record<string, unknown>;
|
|
430
|
-
if (!Array.isArray(slide.components)) return value;
|
|
431
|
-
return { ...slide, components: slide.components.map((value: unknown) => {
|
|
432
|
-
if (!value || typeof value !== "object") return value;
|
|
433
|
-
const component = value as Record<string, unknown>;
|
|
434
|
-
if ((component.kind !== "diagram" && component.kind !== "chart") || !component.appearance || typeof component.appearance !== "object") return value;
|
|
435
|
-
return { ...component, appearance: { ...component.appearance, selection: "explicit" } };
|
|
436
|
-
}) };
|
|
437
|
-
}) };
|
|
438
|
-
}
|
|
439
17
|
export function validateComposition(input: unknown): ValidationResult {
|
|
440
|
-
|
|
441
|
-
input &&
|
|
442
|
-
typeof input === "object" &&
|
|
443
|
-
"schema" in input &&
|
|
444
|
-
legacyCompositionSchemas.includes(
|
|
445
|
-
input.schema as (typeof legacyCompositionSchemas)[number],
|
|
446
|
-
),
|
|
447
|
-
);
|
|
448
|
-
const legacyInput = input as Record<string, unknown>;
|
|
449
|
-
const migrated = legacy
|
|
450
|
-
? ["konpeki-composition/v12", "konpeki-composition/v13", "konpeki-composition/v14", "konpeki-composition/v15", "konpeki-composition/v16", "konpeki-composition/v17"].includes(
|
|
451
|
-
String(legacyInput.schema),
|
|
452
|
-
)
|
|
453
|
-
? { ...legacyInput, schema: compositionSchema }
|
|
454
|
-
: migrateV11Document(
|
|
455
|
-
legacyInput.schema === "konpeki-composition/v11"
|
|
456
|
-
? legacyInput
|
|
457
|
-
: migrateV10Document(
|
|
458
|
-
["konpeki-composition/v6", "konpeki-composition/v7", "konpeki-composition/v8", "konpeki-composition/v9", "konpeki-composition/v10"].includes(
|
|
459
|
-
String(legacyInput.schema),
|
|
460
|
-
)
|
|
461
|
-
? migrateV7Document(legacyInput)
|
|
462
|
-
: migrateV7Document(
|
|
463
|
-
migrateV5Document(
|
|
464
|
-
legacyInput.schema === "konpeki-composition/v5"
|
|
465
|
-
? legacyInput
|
|
466
|
-
: migratePreV5Document(legacyInput),
|
|
467
|
-
),
|
|
468
|
-
),
|
|
469
|
-
),
|
|
470
|
-
)
|
|
471
|
-
: input;
|
|
472
|
-
const normalized = migrateInterimV12Sankey(migrated);
|
|
473
|
-
const candidate = legacy ? preserveLegacyVisualizationChoices(normalized) : normalized;
|
|
474
|
-
if (!structural(candidate))
|
|
18
|
+
if (!structural(input))
|
|
475
19
|
return {
|
|
476
20
|
ok: false,
|
|
477
21
|
issues: (structural.errors ?? []).map((error) => ({
|
|
@@ -479,6 +23,7 @@ export function validateComposition(input: unknown): ValidationResult {
|
|
|
479
23
|
message: error.message ?? "Invalid value",
|
|
480
24
|
})),
|
|
481
25
|
};
|
|
26
|
+
const candidate = input;
|
|
482
27
|
const issues: ValidationIssue[] = [];
|
|
483
28
|
const fail = (path: string, message: string) =>
|
|
484
29
|
issues.push({ path, message });
|
|
@@ -667,10 +212,7 @@ export function validateComposition(input: unknown): ValidationResult {
|
|
|
667
212
|
});
|
|
668
213
|
return issues.length
|
|
669
214
|
? { ok: false, issues }
|
|
670
|
-
: { ok: true, document:
|
|
671
|
-
...slide, components: slide.components.map((component) => component.kind === "text-block" && !component.customVisual && component.content === undefined
|
|
672
|
-
? { ...component, content: component.intent ?? "" } : component),
|
|
673
|
-
})) } : candidate };
|
|
215
|
+
: { ok: true, document: input };
|
|
674
216
|
}
|
|
675
217
|
export function assertComposition(input: unknown): CompositionDocument {
|
|
676
218
|
const result = validateComposition(input);
|
|
@@ -317,19 +317,3 @@ export const diagramCategories = Array.from(
|
|
|
317
317
|
export function diagramDefinition(type: DiagramType): DiagramDefinition {
|
|
318
318
|
return diagramDefinitions[type];
|
|
319
319
|
}
|
|
320
|
-
|
|
321
|
-
export function migrateLegacyDiagramType(
|
|
322
|
-
type: unknown,
|
|
323
|
-
layout: unknown,
|
|
324
|
-
): DiagramType | undefined {
|
|
325
|
-
if (typeof type !== "string" || typeof layout !== "string") return undefined;
|
|
326
|
-
const legacyTypes: Record<string, DiagramType> = {
|
|
327
|
-
"process/linear": "process",
|
|
328
|
-
"process/branching": "flowchart",
|
|
329
|
-
"process/cyclical": "loop-flywheel",
|
|
330
|
-
"system/layered": "layer-stack",
|
|
331
|
-
"system/request-flow": "sequence",
|
|
332
|
-
"system/hub-and-spoke": "architecture",
|
|
333
|
-
};
|
|
334
|
-
return legacyTypes[`${type}/${layout}`];
|
|
335
|
-
}
|
package/docs/development.md
CHANGED
|
@@ -170,6 +170,10 @@ disposable file session. It checks pending requests, agent refresh, failure
|
|
|
170
170
|
recovery and reduced motion, and captures the affected states. Add an output
|
|
171
171
|
directory and `--record` to also record the animation.
|
|
172
172
|
|
|
173
|
+
`node scripts/check-notes.mjs` checks page/vector note scopes, draft isolation,
|
|
174
|
+
reload persistence, CLI claim/finish, clarification and cancellation in a disposable
|
|
175
|
+
file-backed browser session. It accepts a screenshot directory as its first argument.
|
|
176
|
+
|
|
173
177
|
These checks exercise all five component kinds, empty slides, JSON round trips,
|
|
174
178
|
vector editing/history and fitted line dragging. They capture editor and
|
|
175
179
|
presentation states at two sizes; inspect the images because assertions alone
|
package/docs/workflow.md
CHANGED
|
@@ -49,13 +49,55 @@ An agent waiting for a person's revision request runs:
|
|
|
49
49
|
npm exec --no -- konpeki wait slides/my-visual/composition.json
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
In the file-backed canvas,
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
52
|
+
In the file-backed canvas, select a component or an inner vector element and
|
|
53
|
+
write a **Revision note** in the left panel's **Notes** tab. With nothing selected, the note targets
|
|
54
|
+
the page. **Add note** saves feedback without starting a build; add notes to several
|
|
55
|
+
targets, then choose **Build it** to submit them together. Numbered canvas pins
|
|
56
|
+
return to their note and selection. Pins and notes never appear in presentations
|
|
57
|
+
or PNG exports. Direct text, geometry and attribute edits still save normally.
|
|
58
|
+
|
|
59
|
+
**Build it** saves the composition and submits its exact revision, selected page,
|
|
60
|
+
component and optional vector-element ID, plus all unresolved notes. Without notes,
|
|
61
|
+
it requests a build from the saved composition and selected scope as before.
|
|
62
|
+
`wait` atomically claims one submitted request, marks it working, prints the
|
|
63
|
+
machine-readable v2 request and exits. It does not launch or wake an agent.
|
|
64
|
+
Until claimed, the canvas asks you to contact your coding agent and offers
|
|
65
|
+
**Copy prompt** for the handoff. “Agent working” means claimed, not a live
|
|
66
|
+
agent heartbeat. Only one request per document can be active. Connection errors
|
|
67
|
+
keep an active request locked until completion or cancellation.
|
|
68
|
+
|
|
69
|
+
The agent must reread the named composition and preserve newer human edits.
|
|
70
|
+
Follow each note's target IDs, not just the active page; never silently retarget
|
|
71
|
+
a deleted element. After validating, rendering and inspecting all requested
|
|
72
|
+
changes, explicitly finish using the ID returned by `wait`:
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
npm exec --no -- konpeki finish slides/my-visual/composition.json <request-id> --message "Updated and checked"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Only this acknowledgement resolves the submitted notes and unlocks editing.
|
|
79
|
+
File changes alone do not complete the request; explicit no-op completion is
|
|
80
|
+
valid when the requested result is already present. If blocked, finish with
|
|
81
|
+
`--status needs-clarification --message "What needs clarification?"` or
|
|
82
|
+
`--status failed --message "What failed"`. Notes stay unresolved for a retry.
|
|
83
|
+
Do not mark a partly applied batch done. **Cancel request** unlocks the editor
|
|
84
|
+
and keeps notes, but cannot stop an agent process; stop that agent before retrying.
|
|
85
|
+
|
|
86
|
+
Feedback is stored next to the document as `composition.json.review.json`, separate
|
|
87
|
+
from artwork and exports. Retain that file with the composition when moving work.
|
|
88
|
+
Browser reloads and preview restarts preserve notes and request status. To inspect
|
|
89
|
+
or recover an already claimed request after an interruption, run:
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
npm exec --no -- konpeki request slides/my-visual/composition.json
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Coordinate with the previous worker before resuming; `wait` does not claim an
|
|
96
|
+
already working request a second time. Do not edit the sidecar manually while a
|
|
97
|
+
preview or agent is updating it. Existing v1 temporary requests are not migrated;
|
|
98
|
+
finish them before upgrading, then resubmit if needed. Browser-local drafts and
|
|
99
|
+
hosted examples do not expose revision notes or **Build it** because no local
|
|
100
|
+
agent owns their files.
|
|
59
101
|
|
|
60
102
|
Browser screenshots do not establish PDF/PPTX editability, font embedding or
|
|
61
103
|
cross-application fidelity. Inspect every requested export separately.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "konpeki",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "A shared editable canvas for agent-made visuals",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -72,12 +72,14 @@
|
|
|
72
72
|
"@nivo/sankey": "0.99.0",
|
|
73
73
|
"@xmldom/xmldom": "0.9.12",
|
|
74
74
|
"ajv": "^8.20.0",
|
|
75
|
+
"proper-lockfile": "4.1.2",
|
|
75
76
|
"react": "19.2.8",
|
|
76
77
|
"react-dom": "19.2.8",
|
|
77
78
|
"vite": "^8.3.0"
|
|
78
79
|
},
|
|
79
80
|
"devDependencies": {
|
|
80
81
|
"@types/node": "^24.0.0",
|
|
82
|
+
"@types/proper-lockfile": "4.1.4",
|
|
81
83
|
"@types/react": "^19.2.18",
|
|
82
84
|
"@types/react-dom": "^19.2.5",
|
|
83
85
|
"typescript": "^5.9.3"
|