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/runtime/konpeki.mjs
CHANGED
|
@@ -1,33 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
-
import { access, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
4
3
|
import { basename, dirname, join, resolve } from "node:path";
|
|
5
4
|
import { fileURLToPath } from "node:url";
|
|
6
5
|
import { createServer } from "vite";
|
|
7
|
-
import {
|
|
6
|
+
import { readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
8
7
|
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
9
8
|
import "@xmldom/xmldom";
|
|
9
|
+
import lockfile from "proper-lockfile";
|
|
10
10
|
//#region composition/types.ts
|
|
11
|
-
var compositionSchema = "konpeki-composition/
|
|
12
|
-
var legacyCompositionSchemas = [
|
|
13
|
-
"konpeki-composition/v1",
|
|
14
|
-
"konpeki-composition/v2",
|
|
15
|
-
"konpeki-composition/v3",
|
|
16
|
-
"konpeki-composition/v4",
|
|
17
|
-
"konpeki-composition/v5",
|
|
18
|
-
"konpeki-composition/v6",
|
|
19
|
-
"konpeki-composition/v7",
|
|
20
|
-
"konpeki-composition/v8",
|
|
21
|
-
"konpeki-composition/v9",
|
|
22
|
-
"konpeki-composition/v10",
|
|
23
|
-
"konpeki-composition/v11",
|
|
24
|
-
"konpeki-composition/v12",
|
|
25
|
-
"konpeki-composition/v13",
|
|
26
|
-
"konpeki-composition/v14",
|
|
27
|
-
"konpeki-composition/v15",
|
|
28
|
-
"konpeki-composition/v16",
|
|
29
|
-
"konpeki-composition/v17"
|
|
30
|
-
];
|
|
11
|
+
var compositionSchema = "konpeki-composition/v1";
|
|
31
12
|
var canvasSize = {
|
|
32
13
|
width: 1920,
|
|
33
14
|
height: 1080
|
|
@@ -341,17 +322,6 @@ var chartTemplates = Object.keys({
|
|
|
341
322
|
}
|
|
342
323
|
});
|
|
343
324
|
Array.from(new Set(Object.values(diagramDefinitions).map((definition) => definition.category)));
|
|
344
|
-
function migrateLegacyDiagramType(type, layout) {
|
|
345
|
-
if (typeof type !== "string" || typeof layout !== "string") return void 0;
|
|
346
|
-
return {
|
|
347
|
-
"process/linear": "process",
|
|
348
|
-
"process/branching": "flowchart",
|
|
349
|
-
"process/cyclical": "loop-flywheel",
|
|
350
|
-
"system/layered": "layer-stack",
|
|
351
|
-
"system/request-flow": "sequence",
|
|
352
|
-
"system/hub-and-spoke": "architecture"
|
|
353
|
-
}[`${type}/${layout}`];
|
|
354
|
-
}
|
|
355
325
|
//#endregion
|
|
356
326
|
//#region composition/theme-tokens.ts
|
|
357
327
|
/** Explicit opt-in bindings. Literal SVG attributes always remain fixed. */
|
|
@@ -737,7 +707,7 @@ var components = componentKinds.map((kind) => {
|
|
|
737
707
|
});
|
|
738
708
|
var schema = {
|
|
739
709
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
740
|
-
$id: "https://vcfgdev.github.io/konpeki/composition/
|
|
710
|
+
$id: "https://vcfgdev.github.io/konpeki/composition/v1/schema.json",
|
|
741
711
|
title: compositionSchema,
|
|
742
712
|
...object({
|
|
743
713
|
schema: { const: compositionSchema },
|
|
@@ -928,388 +898,15 @@ var structural = new Ajv2020({
|
|
|
928
898
|
allErrors: true,
|
|
929
899
|
strict: false
|
|
930
900
|
}).compile(schema);
|
|
931
|
-
function migratePreV5Document(input) {
|
|
932
|
-
const slide = input.slide;
|
|
933
|
-
const components = Array.isArray(slide?.components) ? slide.components.map((value) => {
|
|
934
|
-
if (!value || typeof value !== "object") return value;
|
|
935
|
-
const component = value;
|
|
936
|
-
if (component.kind !== "callout" && component.kind !== "comparison" && component.kind !== "text-block") return component;
|
|
937
|
-
const previous = component.appearance && typeof component.appearance === "object" ? component.appearance : {};
|
|
938
|
-
const common = Object.fromEntries([
|
|
939
|
-
"alignment",
|
|
940
|
-
"border",
|
|
941
|
-
"titleStyle",
|
|
942
|
-
"rule"
|
|
943
|
-
].flatMap((key) => key in previous ? [[key, previous[key]]] : []));
|
|
944
|
-
const appearance = component.kind === "callout" ? {
|
|
945
|
-
...common,
|
|
946
|
-
layout: "single",
|
|
947
|
-
orientation: "horizontal",
|
|
948
|
-
purpose: "emphasis",
|
|
949
|
-
treatment: "strong"
|
|
950
|
-
} : component.kind === "comparison" ? {
|
|
951
|
-
...common,
|
|
952
|
-
layout: "two-column",
|
|
953
|
-
orientation: "horizontal",
|
|
954
|
-
purpose: "comparison",
|
|
955
|
-
treatment: "plain"
|
|
956
|
-
} : {
|
|
957
|
-
...previous,
|
|
958
|
-
purpose: previous.purpose ?? "narrative",
|
|
959
|
-
treatment: previous.treatment ?? "plain"
|
|
960
|
-
};
|
|
961
|
-
return {
|
|
962
|
-
...component,
|
|
963
|
-
kind: "text-block",
|
|
964
|
-
appearance
|
|
965
|
-
};
|
|
966
|
-
}) : slide?.components;
|
|
967
|
-
return {
|
|
968
|
-
...Object.fromEntries(Object.entries(input).filter(([key]) => key !== "target" && key !== "slide")),
|
|
969
|
-
schema: "konpeki-composition/v5",
|
|
970
|
-
...slide ? { slides: [{
|
|
971
|
-
...slide,
|
|
972
|
-
name: "Slide 01",
|
|
973
|
-
components
|
|
974
|
-
}] } : {}
|
|
975
|
-
};
|
|
976
|
-
}
|
|
977
|
-
function migrateV5Document(input) {
|
|
978
|
-
const slides = Array.isArray(input.slides) ? input.slides.map((value) => {
|
|
979
|
-
if (!value || typeof value !== "object") return value;
|
|
980
|
-
const slide = value;
|
|
981
|
-
const previousComponents = Array.isArray(slide.components) ? slide.components : [];
|
|
982
|
-
const pageNumber = previousComponents.find((component) => component.kind === "page-number");
|
|
983
|
-
const pageAppearance = pageNumber?.appearance && typeof pageNumber.appearance === "object" ? pageNumber.appearance : {};
|
|
984
|
-
const removedIds = new Set(previousComponents.filter((component) => component.kind === "page-number").map((component) => String(component.id)));
|
|
985
|
-
const removedSlots = new Set(previousComponents.filter((component) => component.kind === "page-number").flatMap((component) => Array.isArray(component.slotIds) ? component.slotIds.map(String) : []));
|
|
986
|
-
const components = previousComponents.flatMap((component) => {
|
|
987
|
-
const appearance = component.appearance && typeof component.appearance === "object" ? component.appearance : {};
|
|
988
|
-
if (component.kind === "page-number") return [];
|
|
989
|
-
if (component.kind === "evidence") return [{
|
|
990
|
-
...component,
|
|
991
|
-
kind: "visual"
|
|
992
|
-
}];
|
|
993
|
-
if (component.kind === "process") return [{
|
|
994
|
-
...component,
|
|
995
|
-
kind: "diagram",
|
|
996
|
-
appearance: {
|
|
997
|
-
...appearance,
|
|
998
|
-
type: "process"
|
|
999
|
-
}
|
|
1000
|
-
}];
|
|
1001
|
-
if (component.kind === "system-map") return [{
|
|
1002
|
-
...component,
|
|
1003
|
-
kind: "diagram",
|
|
1004
|
-
appearance: {
|
|
1005
|
-
...appearance,
|
|
1006
|
-
type: "system"
|
|
1007
|
-
}
|
|
1008
|
-
}];
|
|
1009
|
-
return [component];
|
|
1010
|
-
});
|
|
1011
|
-
const componentIds = new Set(components.map((component) => String(component.id)));
|
|
1012
|
-
const groups = Array.isArray(slide.groups) ? slide.groups.map((group) => ({
|
|
1013
|
-
...group,
|
|
1014
|
-
childIds: Array.isArray(group.childIds) ? group.childIds.filter((id) => !removedIds.has(String(id))) : group.childIds
|
|
1015
|
-
})).filter((group) => !Array.isArray(group.childIds) || group.childIds.length > 0) : slide.groups;
|
|
1016
|
-
const groupIds = new Set(Array.isArray(groups) ? groups.map((group) => String(group.id)) : []);
|
|
1017
|
-
const contentSlots = Array.isArray(slide.contentSlots) ? slide.contentSlots.filter((slot) => !removedSlots.has(String(slot.id))).map((slot) => {
|
|
1018
|
-
if (!Array.isArray(slot.targets)) return slot;
|
|
1019
|
-
const targets = slot.targets.filter((target) => !removedIds.has(String(target)) && !removedSlots.has(String(target)));
|
|
1020
|
-
return {
|
|
1021
|
-
...slot,
|
|
1022
|
-
targets: targets.length ? targets : [String(components[0]?.id ?? "")]
|
|
1023
|
-
};
|
|
1024
|
-
}) : slide.contentSlots;
|
|
1025
|
-
return {
|
|
1026
|
-
...slide,
|
|
1027
|
-
pageNumber: {
|
|
1028
|
-
style: pageNumber ? pageAppearance.style ?? "01" : "none",
|
|
1029
|
-
color: pageAppearance.color ?? "muted"
|
|
1030
|
-
},
|
|
1031
|
-
components,
|
|
1032
|
-
contentSlots,
|
|
1033
|
-
groups,
|
|
1034
|
-
readingOrder: Array.isArray(slide.readingOrder) ? slide.readingOrder.filter((entry) => entry.kind === "group" ? groupIds.has(String(entry.id)) : componentIds.has(String(entry.id))) : slide.readingOrder,
|
|
1035
|
-
paintOrder: Array.isArray(slide.paintOrder) ? slide.paintOrder.filter((id) => !removedIds.has(String(id))) : slide.paintOrder,
|
|
1036
|
-
relationships: Array.isArray(slide.relationships) ? slide.relationships.filter((relationship) => {
|
|
1037
|
-
const from = relationship.from;
|
|
1038
|
-
const to = relationship.to;
|
|
1039
|
-
return !removedIds.has(String(from?.nodeId)) && !removedIds.has(String(to?.nodeId));
|
|
1040
|
-
}) : slide.relationships
|
|
1041
|
-
};
|
|
1042
|
-
}) : input.slides;
|
|
1043
|
-
return {
|
|
1044
|
-
...input,
|
|
1045
|
-
schema: compositionSchema,
|
|
1046
|
-
slides
|
|
1047
|
-
};
|
|
1048
|
-
}
|
|
1049
|
-
function migrateV7Document(input) {
|
|
1050
|
-
const slides = Array.isArray(input.slides) ? input.slides.map((value) => {
|
|
1051
|
-
if (!value || typeof value !== "object") return value;
|
|
1052
|
-
const slide = value;
|
|
1053
|
-
const components = Array.isArray(slide.components) ? slide.components.map((value) => {
|
|
1054
|
-
if (!value || typeof value !== "object") return value;
|
|
1055
|
-
const component = value;
|
|
1056
|
-
const appearance = component.appearance && typeof component.appearance === "object" ? component.appearance : {};
|
|
1057
|
-
if (component.kind !== "visual" || appearance.template !== "image") return component;
|
|
1058
|
-
return {
|
|
1059
|
-
...component,
|
|
1060
|
-
kind: "image",
|
|
1061
|
-
appearance: {
|
|
1062
|
-
...appearance.border ? { border: appearance.border } : {},
|
|
1063
|
-
fit: "contain"
|
|
1064
|
-
}
|
|
1065
|
-
};
|
|
1066
|
-
}) : slide.components;
|
|
1067
|
-
return {
|
|
1068
|
-
...slide,
|
|
1069
|
-
components
|
|
1070
|
-
};
|
|
1071
|
-
}) : input.slides;
|
|
1072
|
-
return {
|
|
1073
|
-
...input,
|
|
1074
|
-
schema: compositionSchema,
|
|
1075
|
-
slides
|
|
1076
|
-
};
|
|
1077
|
-
}
|
|
1078
|
-
function migrateV10Document(input) {
|
|
1079
|
-
const slides = Array.isArray(input.slides) ? input.slides.map((value) => {
|
|
1080
|
-
if (!value || typeof value !== "object") return value;
|
|
1081
|
-
const slide = value;
|
|
1082
|
-
const components = Array.isArray(slide.components) ? slide.components.map((value) => {
|
|
1083
|
-
if (!value || typeof value !== "object") return value;
|
|
1084
|
-
const component = value;
|
|
1085
|
-
const appearance = component.appearance && typeof component.appearance === "object" ? component.appearance : {};
|
|
1086
|
-
if (component.kind === "headline") return {
|
|
1087
|
-
...component,
|
|
1088
|
-
kind: "text-block",
|
|
1089
|
-
appearance: {
|
|
1090
|
-
...appearance,
|
|
1091
|
-
role: "title",
|
|
1092
|
-
layout: "single",
|
|
1093
|
-
orientation: "horizontal",
|
|
1094
|
-
purpose: "narrative",
|
|
1095
|
-
treatment: "plain",
|
|
1096
|
-
logicalOrder: "none"
|
|
1097
|
-
}
|
|
1098
|
-
};
|
|
1099
|
-
if (component.kind === "footnote") return {
|
|
1100
|
-
...component,
|
|
1101
|
-
kind: "text-block",
|
|
1102
|
-
appearance: {
|
|
1103
|
-
...appearance,
|
|
1104
|
-
role: "footnote",
|
|
1105
|
-
layout: "single",
|
|
1106
|
-
orientation: "horizontal",
|
|
1107
|
-
purpose: "narrative",
|
|
1108
|
-
treatment: "plain",
|
|
1109
|
-
logicalOrder: "none",
|
|
1110
|
-
titleStyle: "plain"
|
|
1111
|
-
}
|
|
1112
|
-
};
|
|
1113
|
-
if (component.kind === "text-block") return {
|
|
1114
|
-
...component,
|
|
1115
|
-
appearance: {
|
|
1116
|
-
role: "body",
|
|
1117
|
-
...appearance
|
|
1118
|
-
}
|
|
1119
|
-
};
|
|
1120
|
-
if (component.kind === "visual") return {
|
|
1121
|
-
...component,
|
|
1122
|
-
kind: "chart"
|
|
1123
|
-
};
|
|
1124
|
-
if (component.kind === "diagram") {
|
|
1125
|
-
const { template, ...rest } = appearance;
|
|
1126
|
-
return {
|
|
1127
|
-
...component,
|
|
1128
|
-
appearance: {
|
|
1129
|
-
...rest,
|
|
1130
|
-
layout: template ?? appearance.layout ?? "linear"
|
|
1131
|
-
}
|
|
1132
|
-
};
|
|
1133
|
-
}
|
|
1134
|
-
if (component.kind === "icon" || component.kind === "shape") {
|
|
1135
|
-
const slotIds = Array.isArray(component.slotIds) ? component.slotIds.map(String) : [];
|
|
1136
|
-
const slotId = slotIds[0] ?? `${String(component.id)}-content`;
|
|
1137
|
-
const primitive = component.kind === "icon" ? {
|
|
1138
|
-
kind: "icon",
|
|
1139
|
-
...typeof component.intent === "string" && component.intent.trim() ? { name: component.intent.trim() } : {},
|
|
1140
|
-
...appearance.style ? { style: appearance.style } : {},
|
|
1141
|
-
...appearance.color ? { color: appearance.color } : {}
|
|
1142
|
-
} : {
|
|
1143
|
-
kind: "shape",
|
|
1144
|
-
shape: appearance.shape ?? "rectangle",
|
|
1145
|
-
...appearance.color ? { color: appearance.color } : {},
|
|
1146
|
-
...appearance.fill ? { fill: appearance.fill } : {}
|
|
1147
|
-
};
|
|
1148
|
-
return {
|
|
1149
|
-
...component,
|
|
1150
|
-
kind: "diagram",
|
|
1151
|
-
slotIds: slotIds.length ? slotIds : [slotId],
|
|
1152
|
-
appearance: {
|
|
1153
|
-
type: "system",
|
|
1154
|
-
layout: "hub-and-spoke",
|
|
1155
|
-
border: appearance.border ?? "none",
|
|
1156
|
-
colorScheme: "accent-with-muted-context",
|
|
1157
|
-
emphasis: "primary",
|
|
1158
|
-
density: "sparse"
|
|
1159
|
-
},
|
|
1160
|
-
topology: {
|
|
1161
|
-
kind: "explicit",
|
|
1162
|
-
nodes: (slotIds.length ? slotIds : [slotId]).map((currentSlotId, index) => ({
|
|
1163
|
-
id: index ? `${String(component.id)}-slot-${index + 1}` : String(component.id),
|
|
1164
|
-
slotId: currentSlotId,
|
|
1165
|
-
...index ? { visible: false } : { primitive },
|
|
1166
|
-
...component.preferredRect && typeof component.preferredRect === "object" ? { preferredRect: component.preferredRect } : {}
|
|
1167
|
-
})),
|
|
1168
|
-
edges: []
|
|
1169
|
-
}
|
|
1170
|
-
};
|
|
1171
|
-
}
|
|
1172
|
-
return component;
|
|
1173
|
-
}) : slide.components;
|
|
1174
|
-
const contentSlots = Array.isArray(slide.contentSlots) ? slide.contentSlots.map((value) => {
|
|
1175
|
-
if (!value || typeof value !== "object") return value;
|
|
1176
|
-
const slot = value;
|
|
1177
|
-
return slot.role === "icon" || slot.role === "shape" ? {
|
|
1178
|
-
...slot,
|
|
1179
|
-
role: "entity"
|
|
1180
|
-
} : slot;
|
|
1181
|
-
}) : slide.contentSlots;
|
|
1182
|
-
return {
|
|
1183
|
-
...slide,
|
|
1184
|
-
components,
|
|
1185
|
-
contentSlots
|
|
1186
|
-
};
|
|
1187
|
-
}) : input.slides;
|
|
1188
|
-
return {
|
|
1189
|
-
...input,
|
|
1190
|
-
schema: "konpeki-composition/v11",
|
|
1191
|
-
slides
|
|
1192
|
-
};
|
|
1193
|
-
}
|
|
1194
|
-
function migrateV11Document(input) {
|
|
1195
|
-
const slides = Array.isArray(input.slides) ? input.slides.map((value) => {
|
|
1196
|
-
if (!value || typeof value !== "object") return value;
|
|
1197
|
-
const slide = value;
|
|
1198
|
-
const components = Array.isArray(slide.components) ? slide.components.map((value) => {
|
|
1199
|
-
if (!value || typeof value !== "object") return value;
|
|
1200
|
-
const component = value;
|
|
1201
|
-
if (component.kind !== "diagram") return component;
|
|
1202
|
-
const appearance = component.appearance && typeof component.appearance === "object" ? component.appearance : {};
|
|
1203
|
-
const migratedType = migrateLegacyDiagramType(appearance.type, appearance.layout);
|
|
1204
|
-
const { layout, ...rest } = appearance;
|
|
1205
|
-
return {
|
|
1206
|
-
...component,
|
|
1207
|
-
appearance: {
|
|
1208
|
-
...rest,
|
|
1209
|
-
type: migratedType ?? null
|
|
1210
|
-
}
|
|
1211
|
-
};
|
|
1212
|
-
}) : slide.components;
|
|
1213
|
-
return {
|
|
1214
|
-
...slide,
|
|
1215
|
-
components
|
|
1216
|
-
};
|
|
1217
|
-
}) : input.slides;
|
|
1218
|
-
return {
|
|
1219
|
-
...input,
|
|
1220
|
-
schema: compositionSchema,
|
|
1221
|
-
slides
|
|
1222
|
-
};
|
|
1223
|
-
}
|
|
1224
|
-
function migrateInterimV12Sankey(input) {
|
|
1225
|
-
if (!input || typeof input !== "object") return input;
|
|
1226
|
-
const document = input;
|
|
1227
|
-
if (document.schema !== "konpeki-composition/v18" || !Array.isArray(document.slides)) return input;
|
|
1228
|
-
const slides = document.slides.map((value) => {
|
|
1229
|
-
if (!value || typeof value !== "object") return value;
|
|
1230
|
-
const slide = value;
|
|
1231
|
-
if (!Array.isArray(slide.components)) return value;
|
|
1232
|
-
const components = slide.components.map((componentValue) => {
|
|
1233
|
-
if (!componentValue || typeof componentValue !== "object") return componentValue;
|
|
1234
|
-
const component = componentValue;
|
|
1235
|
-
const appearance = component.appearance && typeof component.appearance === "object" ? component.appearance : {};
|
|
1236
|
-
if (component.kind !== "diagram" || appearance.type !== "sankey") return componentValue;
|
|
1237
|
-
const { type: _type, ...sharedAppearance } = appearance;
|
|
1238
|
-
return {
|
|
1239
|
-
...component,
|
|
1240
|
-
kind: "chart",
|
|
1241
|
-
appearance: {
|
|
1242
|
-
...sharedAppearance,
|
|
1243
|
-
template: "sankey"
|
|
1244
|
-
}
|
|
1245
|
-
};
|
|
1246
|
-
});
|
|
1247
|
-
return {
|
|
1248
|
-
...slide,
|
|
1249
|
-
components
|
|
1250
|
-
};
|
|
1251
|
-
});
|
|
1252
|
-
return {
|
|
1253
|
-
...document,
|
|
1254
|
-
slides
|
|
1255
|
-
};
|
|
1256
|
-
}
|
|
1257
|
-
function preserveLegacyVisualizationChoices(input) {
|
|
1258
|
-
if (!input || typeof input !== "object") return input;
|
|
1259
|
-
const document = input;
|
|
1260
|
-
if (!Array.isArray(document.slides)) return input;
|
|
1261
|
-
return {
|
|
1262
|
-
...document,
|
|
1263
|
-
slides: document.slides.map((value) => {
|
|
1264
|
-
if (!value || typeof value !== "object") return value;
|
|
1265
|
-
const slide = value;
|
|
1266
|
-
if (!Array.isArray(slide.components)) return value;
|
|
1267
|
-
return {
|
|
1268
|
-
...slide,
|
|
1269
|
-
components: slide.components.map((value) => {
|
|
1270
|
-
if (!value || typeof value !== "object") return value;
|
|
1271
|
-
const component = value;
|
|
1272
|
-
if (component.kind !== "diagram" && component.kind !== "chart" || !component.appearance || typeof component.appearance !== "object") return value;
|
|
1273
|
-
return {
|
|
1274
|
-
...component,
|
|
1275
|
-
appearance: {
|
|
1276
|
-
...component.appearance,
|
|
1277
|
-
selection: "explicit"
|
|
1278
|
-
}
|
|
1279
|
-
};
|
|
1280
|
-
})
|
|
1281
|
-
};
|
|
1282
|
-
})
|
|
1283
|
-
};
|
|
1284
|
-
}
|
|
1285
901
|
function validateComposition(input) {
|
|
1286
|
-
|
|
1287
|
-
const legacyInput = input;
|
|
1288
|
-
const normalized = migrateInterimV12Sankey(legacy ? [
|
|
1289
|
-
"konpeki-composition/v12",
|
|
1290
|
-
"konpeki-composition/v13",
|
|
1291
|
-
"konpeki-composition/v14",
|
|
1292
|
-
"konpeki-composition/v15",
|
|
1293
|
-
"konpeki-composition/v16",
|
|
1294
|
-
"konpeki-composition/v17"
|
|
1295
|
-
].includes(String(legacyInput.schema)) ? {
|
|
1296
|
-
...legacyInput,
|
|
1297
|
-
schema: compositionSchema
|
|
1298
|
-
} : migrateV11Document(legacyInput.schema === "konpeki-composition/v11" ? legacyInput : migrateV10Document([
|
|
1299
|
-
"konpeki-composition/v6",
|
|
1300
|
-
"konpeki-composition/v7",
|
|
1301
|
-
"konpeki-composition/v8",
|
|
1302
|
-
"konpeki-composition/v9",
|
|
1303
|
-
"konpeki-composition/v10"
|
|
1304
|
-
].includes(String(legacyInput.schema)) ? migrateV7Document(legacyInput) : migrateV7Document(migrateV5Document(legacyInput.schema === "konpeki-composition/v5" ? legacyInput : migratePreV5Document(legacyInput))))) : input);
|
|
1305
|
-
const candidate = legacy ? preserveLegacyVisualizationChoices(normalized) : normalized;
|
|
1306
|
-
if (!structural(candidate)) return {
|
|
902
|
+
if (!structural(input)) return {
|
|
1307
903
|
ok: false,
|
|
1308
904
|
issues: (structural.errors ?? []).map((error) => ({
|
|
1309
905
|
path: error.instancePath,
|
|
1310
906
|
message: error.message ?? "Invalid value"
|
|
1311
907
|
}))
|
|
1312
908
|
};
|
|
909
|
+
const candidate = input;
|
|
1313
910
|
const issues = [];
|
|
1314
911
|
const fail = (path, message) => issues.push({
|
|
1315
912
|
path,
|
|
@@ -1418,16 +1015,7 @@ function validateComposition(input) {
|
|
|
1418
1015
|
issues
|
|
1419
1016
|
} : {
|
|
1420
1017
|
ok: true,
|
|
1421
|
-
document:
|
|
1422
|
-
...candidate,
|
|
1423
|
-
slides: candidate.slides.map((slide) => ({
|
|
1424
|
-
...slide,
|
|
1425
|
-
components: slide.components.map((component) => component.kind === "text-block" && !component.customVisual && component.content === void 0 ? {
|
|
1426
|
-
...component,
|
|
1427
|
-
content: component.intent ?? ""
|
|
1428
|
-
} : component)
|
|
1429
|
-
}))
|
|
1430
|
-
} : candidate
|
|
1018
|
+
document: input
|
|
1431
1019
|
};
|
|
1432
1020
|
}
|
|
1433
1021
|
//#endregion
|
|
@@ -1461,10 +1049,6 @@ function parseCompositionJSON(raw) {
|
|
|
1461
1049
|
function revisionFor(value) {
|
|
1462
1050
|
return createHash("sha256").update(value).digest("hex");
|
|
1463
1051
|
}
|
|
1464
|
-
function requestFileFor(compositionPath) {
|
|
1465
|
-
const key = revisionFor(resolve(compositionPath));
|
|
1466
|
-
return join(tmpdir(), "konpeki", `${key}.request.json`);
|
|
1467
|
-
}
|
|
1468
1052
|
async function readCompositionFile(compositionPath) {
|
|
1469
1053
|
const raw = await readFile(compositionPath, "utf8");
|
|
1470
1054
|
const parsed = parseCompositionJSON(raw);
|
|
@@ -1500,28 +1084,134 @@ async function saveCompositionFile(compositionPath, expectedRevision, document)
|
|
|
1500
1084
|
}
|
|
1501
1085
|
return { revision: revisionFor(value) };
|
|
1502
1086
|
}
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1087
|
+
//#endregion
|
|
1088
|
+
//#region src/lib/review.ts
|
|
1089
|
+
var emptyReview = () => ({
|
|
1090
|
+
schema: "konpeki-review/v1",
|
|
1091
|
+
version: 0,
|
|
1092
|
+
notes: []
|
|
1093
|
+
});
|
|
1094
|
+
var activeRequest = (request) => request?.status === "submitted" || request?.status === "working";
|
|
1095
|
+
//#endregion
|
|
1096
|
+
//#region bin/review-store.ts
|
|
1097
|
+
var reviewFileFor = (path) => `${resolve(path)}.review.json`;
|
|
1098
|
+
var conflict = (message) => Object.assign(new Error(message), { code: "REVISION_CONFLICT" });
|
|
1099
|
+
var invalid = (message) => Object.assign(new Error(message), { code: "INVALID_REVIEW" });
|
|
1100
|
+
async function readReview(path) {
|
|
1101
|
+
try {
|
|
1102
|
+
const state = JSON.parse(await readFile(reviewFileFor(path), "utf8"));
|
|
1103
|
+
if (state.schema !== "konpeki-review/v1") throw invalid("Unsupported revision-note format.");
|
|
1104
|
+
return state;
|
|
1105
|
+
} catch (error) {
|
|
1106
|
+
if (error.code === "ENOENT") return emptyReview();
|
|
1107
|
+
throw error;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
async function mutate(path, change) {
|
|
1111
|
+
const file = reviewFileFor(path);
|
|
1112
|
+
const release = await lockfile.lock(file, {
|
|
1113
|
+
realpath: false,
|
|
1114
|
+
retries: {
|
|
1115
|
+
retries: 60,
|
|
1116
|
+
minTimeout: 100,
|
|
1117
|
+
maxTimeout: 250
|
|
1118
|
+
}
|
|
1119
|
+
});
|
|
1120
|
+
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
1121
|
+
try {
|
|
1122
|
+
const state = await readReview(path);
|
|
1123
|
+
await change(state);
|
|
1124
|
+
state.version++;
|
|
1125
|
+
await writeFile(temporary, `${JSON.stringify(state, null, 2)}\n`);
|
|
1126
|
+
await rename(temporary, file);
|
|
1127
|
+
return state;
|
|
1128
|
+
} finally {
|
|
1129
|
+
await rm(temporary, { force: true });
|
|
1130
|
+
await release();
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
function validateTarget(document, target) {
|
|
1134
|
+
const slide = document.slides.find((s) => s.id === target.slideId);
|
|
1135
|
+
if (!slide) throw invalid("The note's page no longer exists. Remove the note and select a new target.");
|
|
1136
|
+
const component = slide.components.find((c) => c.id === target.componentId);
|
|
1137
|
+
if (target.componentId !== void 0 && !component) throw invalid("The note's component no longer exists. Remove the note and select a new target.");
|
|
1138
|
+
if (target.elementId !== void 0 && !(component?.customVisual?.format === "vector" && component.customVisual.elements.some((e) => e.id === target.elementId))) throw invalid("The note's vector element no longer exists. Remove the note and select a new target.");
|
|
1139
|
+
}
|
|
1140
|
+
async function addRevisionNote(path, target, text) {
|
|
1141
|
+
if (typeof text !== "string" || !text.trim() || text.length > 4e3) throw invalid("Write a revision note of 1–4000 characters.");
|
|
1142
|
+
return mutate(path, async (state) => {
|
|
1143
|
+
if (activeRequest(state.request)) throw conflict("Wait for the current request before adding notes.");
|
|
1144
|
+
validateTarget((await readCompositionFile(path)).document, target);
|
|
1145
|
+
state.notes.push({
|
|
1146
|
+
id: randomUUID(),
|
|
1147
|
+
slideId: target.slideId,
|
|
1148
|
+
...target.componentId ? { componentId: target.componentId } : {},
|
|
1149
|
+
...target.elementId ? { elementId: target.elementId } : {},
|
|
1150
|
+
text: text.trim(),
|
|
1151
|
+
resolved: false
|
|
1152
|
+
});
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
async function removeRevisionNote(path, id) {
|
|
1156
|
+
return mutate(path, (state) => {
|
|
1157
|
+
if (activeRequest(state.request)) throw conflict("Wait for the current request before removing notes.");
|
|
1158
|
+
state.notes = state.notes.filter((note) => note.id !== id);
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
async function writeBuildRequest(path, request) {
|
|
1162
|
+
return mutate(path, async (state) => {
|
|
1163
|
+
if (activeRequest(state.request)) throw conflict("A build request is already waiting or working.");
|
|
1164
|
+
const current = await readCompositionFile(path);
|
|
1165
|
+
if (current.revision !== request.revision) throw conflict("Save the latest composition before requesting a build.");
|
|
1166
|
+
validateTarget(current.document, request);
|
|
1167
|
+
const notes = state.notes.filter((note) => !note.resolved);
|
|
1168
|
+
for (const note of notes) validateTarget(current.document, note);
|
|
1169
|
+
if (typeof request.instruction !== "string" || !request.instruction.trim()) throw invalid("Describe what the agent should build.");
|
|
1170
|
+
state.request = {
|
|
1171
|
+
schema: "konpeki-build-request/v2",
|
|
1172
|
+
id: randomUUID(),
|
|
1173
|
+
compositionPath: resolve(path),
|
|
1174
|
+
revision: current.revision,
|
|
1175
|
+
instruction: request.instruction.trim(),
|
|
1176
|
+
slideId: request.slideId,
|
|
1177
|
+
...request.componentId ? { componentId: request.componentId } : {},
|
|
1178
|
+
...request.elementId ? { elementId: request.elementId } : {},
|
|
1179
|
+
notes,
|
|
1180
|
+
status: "submitted"
|
|
1181
|
+
};
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
async function claimBuildRequest(path) {
|
|
1185
|
+
if ((await readReview(path)).request?.status !== "submitted") return;
|
|
1186
|
+
let claimed;
|
|
1187
|
+
await mutate(path, (state) => {
|
|
1188
|
+
if (state.request?.status !== "submitted") return;
|
|
1189
|
+
state.request.status = "working";
|
|
1190
|
+
claimed = state.request;
|
|
1191
|
+
});
|
|
1192
|
+
return claimed;
|
|
1193
|
+
}
|
|
1194
|
+
async function finishBuildRequest(path, id, status, message) {
|
|
1195
|
+
if (![
|
|
1196
|
+
"done",
|
|
1197
|
+
"needs-clarification",
|
|
1198
|
+
"failed"
|
|
1199
|
+
].includes(status)) throw invalid("Status must be done, needs-clarification or failed.");
|
|
1200
|
+
if (status !== "done" && !message?.trim()) throw invalid("Explain what failed or needs clarification.");
|
|
1201
|
+
return mutate(path, async (state) => {
|
|
1202
|
+
if (state.request?.id !== id || !activeRequest(state.request)) throw conflict("This request is no longer active.");
|
|
1203
|
+
if (status === "done" && state.request.status !== "working") throw conflict("Claim the request with konpeki wait before completing it.");
|
|
1204
|
+
if (status === "done") {
|
|
1205
|
+
state.request.resultRevision = (await readCompositionFile(path)).revision;
|
|
1206
|
+
const completed = new Set(state.request.notes.map((note) => note.id));
|
|
1207
|
+
state.notes = state.notes.map((note) => completed.has(note.id) ? {
|
|
1208
|
+
...note,
|
|
1209
|
+
resolved: true
|
|
1210
|
+
} : note);
|
|
1211
|
+
}
|
|
1212
|
+
state.request.status = status;
|
|
1213
|
+
state.request.message = message?.trim();
|
|
1508
1214
|
});
|
|
1509
|
-
const output = requestFileFor(compositionPath);
|
|
1510
|
-
await mkdir(dirname(output), { recursive: true });
|
|
1511
|
-
const value = {
|
|
1512
|
-
schema: "konpeki-build-request/v1",
|
|
1513
|
-
compositionPath: resolve(compositionPath),
|
|
1514
|
-
revision: request.revision,
|
|
1515
|
-
instruction: request.instruction.trim(),
|
|
1516
|
-
slideId: request.slideId,
|
|
1517
|
-
...request.componentId ? { componentId: request.componentId } : {},
|
|
1518
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1519
|
-
};
|
|
1520
|
-
await writeFile(output, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
1521
|
-
return {
|
|
1522
|
-
output,
|
|
1523
|
-
request: value
|
|
1524
|
-
};
|
|
1525
1215
|
}
|
|
1526
1216
|
//#endregion
|
|
1527
1217
|
//#region bin/session-plugin.ts
|
|
@@ -1547,7 +1237,7 @@ async function readJSON(request) {
|
|
|
1547
1237
|
function errorResponse(error) {
|
|
1548
1238
|
const code = error instanceof Error && "code" in error ? error.code : void 0;
|
|
1549
1239
|
return {
|
|
1550
|
-
status: code === "REVISION_CONFLICT" ? 409 : code === "INVALID_COMPOSITION" ? 422 : code === "BODY_TOO_LARGE" ? 413 : error instanceof SyntaxError ? 400 : 500,
|
|
1240
|
+
status: code === "REVISION_CONFLICT" ? 409 : code === "INVALID_COMPOSITION" || code === "INVALID_REVIEW" ? 422 : code === "BODY_TOO_LARGE" ? 413 : error instanceof SyntaxError ? 400 : 500,
|
|
1551
1241
|
body: {
|
|
1552
1242
|
error: error instanceof Error ? error.message : "File-session request failed.",
|
|
1553
1243
|
...error && typeof error === "object" && "revision" in error ? { revision: error.revision } : {}
|
|
@@ -1558,6 +1248,19 @@ function fileSessionPlugin({ compositionPath, token, onBuildRequest }) {
|
|
|
1558
1248
|
return {
|
|
1559
1249
|
name: "konpeki-file-session",
|
|
1560
1250
|
enforce: "pre",
|
|
1251
|
+
config(config) {
|
|
1252
|
+
return { server: { fs: { deny: [
|
|
1253
|
+
".env",
|
|
1254
|
+
".env.*",
|
|
1255
|
+
"*.{crt,pem,key,p12,pfx,cer,der}",
|
|
1256
|
+
".npmrc",
|
|
1257
|
+
".yarnrc.yml",
|
|
1258
|
+
"**/.git/**",
|
|
1259
|
+
...config.server?.fs?.deny ?? [],
|
|
1260
|
+
"**/*.review.json",
|
|
1261
|
+
"**/*.review.json.*"
|
|
1262
|
+
] } } };
|
|
1263
|
+
},
|
|
1561
1264
|
configureServer(server) {
|
|
1562
1265
|
server.middlewares.use(async (request, response, next) => {
|
|
1563
1266
|
const url = new URL(request.url || "/", "http://localhost");
|
|
@@ -1568,7 +1271,11 @@ function fileSessionPlugin({ compositionPath, token, onBuildRequest }) {
|
|
|
1568
1271
|
}
|
|
1569
1272
|
try {
|
|
1570
1273
|
if (url.pathname === route && request.method === "GET") {
|
|
1571
|
-
|
|
1274
|
+
const review = await readReview(compositionPath);
|
|
1275
|
+
sendJSON(response, 200, {
|
|
1276
|
+
...await readCompositionFile(compositionPath),
|
|
1277
|
+
review
|
|
1278
|
+
});
|
|
1572
1279
|
return;
|
|
1573
1280
|
}
|
|
1574
1281
|
if (url.pathname === route && request.method === "PUT") {
|
|
@@ -1584,7 +1291,20 @@ function fileSessionPlugin({ compositionPath, token, onBuildRequest }) {
|
|
|
1584
1291
|
}
|
|
1585
1292
|
const result = await writeBuildRequest(compositionPath, body);
|
|
1586
1293
|
onBuildRequest?.(result);
|
|
1587
|
-
sendJSON(response, 201,
|
|
1294
|
+
sendJSON(response, 201, result);
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
if (url.pathname === `${route}/notes` && request.method === "POST") {
|
|
1298
|
+
const body = await readJSON(request);
|
|
1299
|
+
sendJSON(response, 201, await addRevisionNote(compositionPath, body, body.text));
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
if (url.pathname === `${route}/notes` && request.method === "DELETE") {
|
|
1303
|
+
sendJSON(response, 200, await removeRevisionNote(compositionPath, (await readJSON(request)).id));
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
if (url.pathname === `${route}/cancel` && request.method === "POST") {
|
|
1307
|
+
sendJSON(response, 200, await finishBuildRequest(compositionPath, (await readJSON(request)).id, "failed", "Cancelled by user. Stop the agent before starting another request."));
|
|
1588
1308
|
return;
|
|
1589
1309
|
}
|
|
1590
1310
|
sendJSON(response, 404, { error: "Unknown file-session endpoint." });
|
|
@@ -1603,7 +1323,9 @@ function usage() {
|
|
|
1603
1323
|
console.error(`Usage:
|
|
1604
1324
|
konpeki preview <composition.json> [--host <host>] [--port <port>]
|
|
1605
1325
|
konpeki validate <composition.json>
|
|
1606
|
-
konpeki wait <composition.json
|
|
1326
|
+
konpeki wait <composition.json>
|
|
1327
|
+
konpeki request <composition.json>
|
|
1328
|
+
konpeki finish <composition.json> <request-id> [--status done|needs-clarification|failed] [--message <text>]`);
|
|
1607
1329
|
}
|
|
1608
1330
|
function option(name, fallback) {
|
|
1609
1331
|
const index = process.argv.indexOf(name);
|
|
@@ -1616,7 +1338,6 @@ async function preview(input) {
|
|
|
1616
1338
|
const host = option("--host", "127.0.0.1");
|
|
1617
1339
|
const port = Number(option("--port", "4318"));
|
|
1618
1340
|
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("Port must be an integer from 1 to 65535.");
|
|
1619
|
-
await rm(requestFileFor(compositionPath), { force: true });
|
|
1620
1341
|
const server = await createServer({
|
|
1621
1342
|
root,
|
|
1622
1343
|
configFile: resolve(root, "vite.config.ts"),
|
|
@@ -1629,6 +1350,7 @@ async function preview(input) {
|
|
|
1629
1350
|
compositionPath,
|
|
1630
1351
|
token,
|
|
1631
1352
|
onBuildRequest: ({ request }) => {
|
|
1353
|
+
if (!request) return;
|
|
1632
1354
|
console.log(`\nBuild requested for ${request.compositionPath}`);
|
|
1633
1355
|
console.log(`Revision: ${request.revision}`);
|
|
1634
1356
|
console.log(`Instruction: ${request.instruction}`);
|
|
@@ -1649,17 +1371,12 @@ async function validate(input) {
|
|
|
1649
1371
|
console.log(`${result.name}: valid (${result.revision})`);
|
|
1650
1372
|
}
|
|
1651
1373
|
async function waitForRequest(input) {
|
|
1652
|
-
|
|
1653
|
-
console.log(`Waiting for a Build it request for ${resolve(input)}…`);
|
|
1374
|
+
console.error(`Waiting for a Build it request for ${resolve(input)}…`);
|
|
1654
1375
|
for (;;) {
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
await rm(requestPath, { force: true });
|
|
1659
|
-
process.stdout.write(value);
|
|
1376
|
+
const request = await claimBuildRequest(resolve(input));
|
|
1377
|
+
if (request) {
|
|
1378
|
+
console.log(JSON.stringify(request, null, 2));
|
|
1660
1379
|
return;
|
|
1661
|
-
} catch (error) {
|
|
1662
|
-
if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") throw error;
|
|
1663
1380
|
}
|
|
1664
1381
|
await new Promise((resolveWait) => setTimeout(resolveWait, 250));
|
|
1665
1382
|
}
|
|
@@ -1673,7 +1390,12 @@ if (!command || !input) {
|
|
|
1673
1390
|
if (command === "preview") await preview(input);
|
|
1674
1391
|
else if (command === "validate") await validate(input);
|
|
1675
1392
|
else if (command === "wait") await waitForRequest(input);
|
|
1676
|
-
else
|
|
1393
|
+
else if (command === "request") console.log(JSON.stringify((await readReview(input)).request ?? null, null, 2));
|
|
1394
|
+
else if (command === "finish") {
|
|
1395
|
+
if (!process.argv[4]) throw new Error("Provide the request ID returned by konpeki wait.");
|
|
1396
|
+
const result = await finishBuildRequest(input, process.argv[4], option("--status", "done"), option("--message"));
|
|
1397
|
+
console.log(JSON.stringify(result.request, null, 2));
|
|
1398
|
+
} else {
|
|
1677
1399
|
usage();
|
|
1678
1400
|
process.exitCode = 1;
|
|
1679
1401
|
}
|