konpeki 0.1.0 → 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 +26 -25
- package/SETUP.md +1 -1
- 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 +57 -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 +112 -54
- package/src/components/BuildOrb.tsx +40 -0
- package/src/components/Canvas.tsx +17 -0
- package/src/components/InspectorPanel.tsx +41 -8
- package/src/components/LeftPanel.tsx +16 -3
- package/src/components/RevisionNotes.tsx +56 -0
- package/src/components/WorkspaceChrome.tsx +11 -9
- 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 +84 -19
- package/src/styles/base.css +16 -0
- package/src/styles/canvas.css +43 -12
- package/src/styles/chrome.css +106 -8
- package/src/styles/feedback.css +45 -25
- package/src/styles/left-panel.css +48 -7
- package/src/styles/right-panel.css +62 -20
- package/src/styles/shell.css +70 -16
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
|
@@ -101,6 +101,54 @@ commands against a composition outside the installed package. Do not publish
|
|
|
101
101
|
until that isolated preview works. Publish the tested tarball rather than
|
|
102
102
|
rebuilding during publication. Packing locally does not publish anything.
|
|
103
103
|
|
|
104
|
+
## Tag releases
|
|
105
|
+
|
|
106
|
+
`.github/workflows/publish.yml` stages releases on bare version tags such as `0.1.1`
|
|
107
|
+
(no `v` prefix). The tag must equal `package.json`'s version.
|
|
108
|
+
The workflow installs the mise toolchain and frozen dependencies, runs typecheck,
|
|
109
|
+
tests, build and package checks, then installs a tarball in an isolated directory
|
|
110
|
+
to validate a composition and build the packaged canvas. It stages that same
|
|
111
|
+
tarball for maintainer approval; it does not publish directly. Browser review
|
|
112
|
+
remains a pre-release responsibility.
|
|
113
|
+
|
|
114
|
+
Before the first tag release, configure **Trusted publishing → GitHub Actions**
|
|
115
|
+
in the `konpeki` package settings on npmjs.com:
|
|
116
|
+
|
|
117
|
+
- Organization or user: `vcfgdev`
|
|
118
|
+
- Repository: `konpeki`
|
|
119
|
+
- Workflow filename: `publish.yml`
|
|
120
|
+
- Environment name: leave empty
|
|
121
|
+
- Leave **Allow npm publish** unchecked (staged publishing only)
|
|
122
|
+
|
|
123
|
+
The workflow uses GitHub-hosted runners and OIDC (`id-token: write`); no npm
|
|
124
|
+
token secret is needed. Staged publishing requires npm 11.15.0 or newer and
|
|
125
|
+
Node 22.14.0 or newer; the pinned toolchain meets both requirements.
|
|
126
|
+
npm generates provenance automatically for public repositories;
|
|
127
|
+
private repositories do not receive provenance.
|
|
128
|
+
|
|
129
|
+
After updating the package version, completing release checks and pushing the
|
|
130
|
+
release commit, explicitly create and push its matching tag:
|
|
131
|
+
|
|
132
|
+
```sh
|
|
133
|
+
git tag 0.1.1
|
|
134
|
+
git push origin 0.1.1
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Replace `0.1.1` with the new version. `0.1.0` is already published and cannot be
|
|
138
|
+
republished. Pushing a matching tag submits the tested package to npm's staging
|
|
139
|
+
area. After the workflow succeeds, review the release in npmjs.com's **Staged
|
|
140
|
+
Packages** tab and click **Approve**, completing 2FA to publish it. Alternatively,
|
|
141
|
+
use an authenticated local CLI:
|
|
142
|
+
|
|
143
|
+
```sh
|
|
144
|
+
mise exec -- npm stage list konpeki
|
|
145
|
+
mise exec -- npm stage view <stage-id>
|
|
146
|
+
mise exec -- npm stage approve <stage-id>
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Approval makes the version public. Reject an incorrect staged release instead
|
|
150
|
+
of approving it (`npm stage reject <stage-id>`).
|
|
151
|
+
|
|
104
152
|
## Verification
|
|
105
153
|
|
|
106
154
|
```sh
|
|
@@ -117,6 +165,15 @@ node scripts/check-canvas.mjs http://localhost:4318 .amp/in/artifacts
|
|
|
117
165
|
node scripts/check-pages.mjs http://localhost:4318 .amp/in/artifacts/pages
|
|
118
166
|
```
|
|
119
167
|
|
|
168
|
+
For the Build it lifecycle, `node scripts/check-build.mjs` starts its own
|
|
169
|
+
disposable file session. It checks pending requests, agent refresh, failure
|
|
170
|
+
recovery and reduced motion, and captures the affected states. Add an output
|
|
171
|
+
directory and `--record` to also record the animation.
|
|
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
|
+
|
|
120
177
|
These checks exercise all five component kinds, empty slides, JSON round trips,
|
|
121
178
|
vector editing/history and fitted line dragging. They capture editor and
|
|
122
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"
|