konpeki 0.3.0 → 0.4.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.md +21 -28
- package/AUTHORING.md +68 -229
- package/CONTRIBUTING.md +10 -23
- package/README.md +96 -87
- package/SETUP.md +50 -114
- package/docs/development.md +43 -248
- package/docs/workflow.md +6 -108
- package/html/README.md +140 -0
- package/html/browser.ts +103 -0
- package/html/document.ts +38 -0
- package/html/floor.ts +34 -0
- package/html/index.html +5 -0
- package/html/inspect.ts +313 -0
- package/html/preview.css +62 -0
- package/html/preview.tsx +463 -0
- package/html/review-hints.ts +63 -0
- package/html/server.ts +99 -0
- package/html/source.ts +104 -0
- package/html/starter.ts +6 -0
- package/html/theme-authoring.md +176 -0
- package/html/theme.md +61 -0
- package/index.html +9 -9
- package/package.json +21 -35
- package/plugin.json +1 -1
- package/public/og.png +0 -0
- package/runtime/browser-B-29TH1a.mjs +595 -0
- package/runtime/floor-Cmk7G3pU.mjs +41 -0
- package/runtime/konpeki.mjs +90 -1410
- package/runtime/server-BAqC_5WD.mjs +2 -0
- package/runtime/server-DfRpfcY9.mjs +205 -0
- package/runtime/source-B_Ui9tMp.mjs +141 -0
- package/runtime/source-CZQq9GUO.mjs +2 -0
- package/skills/konpeki/SKILL.md +88 -172
- package/skills/konpeki/assets/blank.html +17 -0
- package/skills/konpeki/floor.md +76 -0
- package/skills/konpeki/references/cover.md +27 -0
- package/skills/konpeki/references/long-document.md +40 -0
- package/skills/konpeki/references/one-pager.md +27 -0
- package/skills/konpeki/references/patterns.md +118 -0
- package/skills/konpeki/references/resume.md +28 -0
- package/skills/konpeki/references/slides.md +28 -0
- package/skills/konpeki/scripts/ensure-runtime.mjs +10 -31
- package/skills/konpeki/scripts/prepare-document.mjs +25 -14
- package/src/components/PageBoard.tsx +156 -0
- package/src/lib/alignment.ts +21 -0
- package/src/lib/page-board.ts +25 -0
- package/src/lib/review-position.ts +19 -0
- package/src/styles/base.css +4 -6
- package/src/styles/feedback.css +97 -61
- package/src/styles/shell.css +95 -323
- package/theme-base.css +90 -0
- package/theme.css +56 -0
- package/vite.config.ts +2 -5
- package/composition/README.md +0 -156
- package/composition/compile.ts +0 -227
- package/composition/document.ts +0 -600
- package/composition/schema.json +0 -3001
- package/composition/schema.ts +0 -437
- package/composition/theme-tokens.ts +0 -16
- package/composition/types.ts +0 -269
- package/composition/validate.ts +0 -226
- package/composition/vector.ts +0 -143
- package/composition/visualizations.ts +0 -319
- package/design/README.md +0 -17
- package/design/palettes/README.md +0 -14
- package/design/palettes/base.ts +0 -14
- package/design/palettes/candidates.ts +0 -19
- package/design/palettes/index.ts +0 -78
- package/design/review/color-theme.md +0 -44
- package/design/review/layout.md +0 -16
- package/design/review/text.md +0 -18
- package/design/review/typography.md +0 -15
- package/design/review/visuals.md +0 -31
- package/design/semantic-patterns.md +0 -43
- package/design/themes/README.md +0 -40
- package/design/themes/index.ts +0 -24
- package/design/visual-languages/technical-product.md +0 -17
- package/design/visual-review.md +0 -88
- package/lib/assets.d.ts +0 -8
- package/lib/charts.ts +0 -18
- package/lib/contrast.ts +0 -16
- package/lib/layouts.ts +0 -50
- package/lib/slide.tsx +0 -42
- package/lib/taste.ts +0 -17
- package/lib/text.tsx +0 -89
- package/lib/typeface.ts +0 -44
- package/scripts/migrate-react-page.ts +0 -120
- package/skills/konpeki/assets/blank.json +0 -23
- package/slides/README.md +0 -153
- package/slides/architecture/PROMPT.md +0 -31
- package/slides/architecture/index.tsx +0 -102
- package/slides/article-brief/PROMPT.md +0 -35
- package/slides/article-brief/index.tsx +0 -71
- package/slides/bar-chart/PROMPT.md +0 -39
- package/slides/bar-chart/index.tsx +0 -97
- package/slides/comparison/PROMPT.md +0 -29
- package/slides/comparison/index.tsx +0 -95
- package/slides/decision-memo/PROMPT.md +0 -34
- package/slides/decision-memo/index.tsx +0 -85
- package/slides/delivery-plan/PROMPT.md +0 -45
- package/slides/delivery-plan/index.tsx +0 -105
- package/slides/experiment/PROMPT.md +0 -44
- package/slides/experiment/index.tsx +0 -127
- package/slides/incident-workflow/PROMPT.md +0 -57
- package/slides/incident-workflow/index.tsx +0 -78
- package/slides/introducing-konpeki/PROMPT.md +0 -40
- package/slides/introducing-konpeki/README.md +0 -76
- package/slides/introducing-konpeki/SOURCE.md +0 -26
- package/slides/introducing-konpeki/author.ts +0 -165
- package/slides/introducing-konpeki/composition.json +0 -3270
- package/slides/line-chart/PROMPT.md +0 -40
- package/slides/line-chart/index.tsx +0 -72
- package/slides/migration/PROMPT.md +0 -38
- package/slides/migration/index.tsx +0 -89
- package/slides/og-images/PROMPT.md +0 -21
- package/slides/og-images/index.tsx +0 -76
- package/slides/product-introduction/PROMPT.md +0 -24
- package/slides/product-introduction/index.tsx +0 -105
- package/slides/research-brief/PROMPT.md +0 -40
- package/slides/research-brief/index.tsx +0 -104
- package/slides/results-explanation/PROMPT.md +0 -32
- package/slides/results-explanation/index.tsx +0 -96
- package/slides/retrospective/PROMPT.md +0 -43
- package/slides/retrospective/index.tsx +0 -105
- package/slides/sankey/PROMPT.md +0 -11
- package/slides/sankey/index.tsx +0 -93
- package/slides/teaching/PROMPT.md +0 -45
- package/slides/teaching/index.tsx +0 -124
- package/slides/vertical-bar-charts/PROMPT.md +0 -13
- package/slides/vertical-bar-charts/index.tsx +0 -97
- package/src/app/App.tsx +0 -820
- package/src/components/BuildOrb.tsx +0 -40
- package/src/components/Canvas.tsx +0 -1185
- package/src/components/DiagramTypeIcon.tsx +0 -78
- package/src/components/InspectorPanel.tsx +0 -773
- package/src/components/LeftPanel.tsx +0 -120
- package/src/components/PageSizePicker.tsx +0 -30
- package/src/components/Presentation.tsx +0 -105
- package/src/components/RevisionNotes.tsx +0 -56
- package/src/components/RightPanel.tsx +0 -201
- package/src/components/VectorOverflowWarning.tsx +0 -46
- package/src/components/WorkspaceChrome.tsx +0 -288
- package/src/components/ui.tsx +0 -53
- package/src/lib/examples/react-page-migration.json +0 -1295
- package/src/lib/examples.ts +0 -42
- package/src/lib/export-png.ts +0 -104
- package/src/lib/file-session.ts +0 -87
- package/src/lib/history.ts +0 -53
- package/src/lib/model.ts +0 -188
- package/src/lib/page-size.ts +0 -24
- package/src/lib/presentation.ts +0 -17
- package/src/lib/review.ts +0 -26
- package/src/lib/storage.ts +0 -71
- package/src/lib/theme.ts +0 -25
- package/src/lib/use-file-session.ts +0 -227
- package/src/main.tsx +0 -29
- package/src/styles/canvas.css +0 -299
- package/src/styles/chrome.css +0 -384
- package/src/styles/component-previews.css +0 -386
- package/src/styles/left-panel.css +0 -166
- package/src/styles/presentation.css +0 -72
- package/src/styles/right-panel.css +0 -1215
package/runtime/konpeki.mjs
CHANGED
|
@@ -1,1434 +1,114 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { writeFile } from "node:fs/promises";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { createServer } from "vite";
|
|
6
|
-
import { readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
7
|
-
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
8
|
-
import "@xmldom/xmldom";
|
|
9
|
-
import lockfile from "proper-lockfile";
|
|
10
|
-
//#region composition/types.ts
|
|
11
|
-
var compositionSchema = "konpeki-composition/v1";
|
|
12
|
-
var canvasSize = {
|
|
13
|
-
width: 1920,
|
|
14
|
-
height: 1080
|
|
15
|
-
};
|
|
16
|
-
var canvasPadding = {
|
|
17
|
-
top: 72,
|
|
18
|
-
right: 112,
|
|
19
|
-
bottom: 0,
|
|
20
|
-
left: 112
|
|
21
|
-
};
|
|
22
|
-
var authoringModes = ["default", "dynamic"];
|
|
23
|
-
var themeIds = [
|
|
24
|
-
"plex",
|
|
25
|
-
"precision",
|
|
26
|
-
"editorial",
|
|
27
|
-
"blue-cyan",
|
|
28
|
-
"orange-coral",
|
|
29
|
-
"yellow",
|
|
30
|
-
"green",
|
|
31
|
-
"graphite"
|
|
32
|
-
];
|
|
33
|
-
var themeModes = ["paper", "night"];
|
|
34
|
-
var typographyIds = [
|
|
35
|
-
"plex-sans",
|
|
36
|
-
"noto-sans",
|
|
37
|
-
"plex-serif",
|
|
38
|
-
"hanken-grotesk"
|
|
39
|
-
];
|
|
40
|
-
var vectorElementKinds = [
|
|
41
|
-
"g",
|
|
42
|
-
"rect",
|
|
43
|
-
"circle",
|
|
44
|
-
"ellipse",
|
|
45
|
-
"line",
|
|
46
|
-
"polyline",
|
|
47
|
-
"polygon",
|
|
48
|
-
"path",
|
|
49
|
-
"text",
|
|
50
|
-
"tspan"
|
|
51
|
-
];
|
|
52
|
-
var componentKinds = [
|
|
53
|
-
"text-block",
|
|
54
|
-
"chart",
|
|
55
|
-
"diagram",
|
|
56
|
-
"image",
|
|
57
|
-
"table"
|
|
58
|
-
];
|
|
59
|
-
//#endregion
|
|
60
|
-
//#region composition/visualizations.ts
|
|
61
|
-
var diagramDefinitions = {
|
|
62
|
-
architecture: {
|
|
63
|
-
label: "Architecture",
|
|
64
|
-
category: "Systems & technical structure",
|
|
65
|
-
layout: "hub-and-spoke",
|
|
66
|
-
slotRole: "entity",
|
|
67
|
-
expression: "Show system boundaries, responsibilities, interfaces, and directional communication without implying physical deployment."
|
|
68
|
-
},
|
|
69
|
-
"it-current-state": {
|
|
70
|
-
label: "IT current state",
|
|
71
|
-
category: "Systems & technical structure",
|
|
72
|
-
layout: "hub-and-spoke",
|
|
73
|
-
slotRole: "entity",
|
|
74
|
-
expression: "Show the systems that exist today, their ownership, integration paths, friction, duplication, and known constraints."
|
|
75
|
-
},
|
|
76
|
-
"high-level": {
|
|
77
|
-
label: "High-level overview",
|
|
78
|
-
category: "Systems & technical structure",
|
|
79
|
-
layout: "layered",
|
|
80
|
-
slotRole: "entity",
|
|
81
|
-
expression: "Reduce the system to a small number of named capabilities and major relationships; suppress implementation detail."
|
|
82
|
-
},
|
|
83
|
-
deployment: {
|
|
84
|
-
label: "Deployment",
|
|
85
|
-
category: "Systems & technical structure",
|
|
86
|
-
layout: "layered",
|
|
87
|
-
slotRole: "entity",
|
|
88
|
-
expression: "Place software artifacts inside the environments, hosts, zones, or runtimes where they execute and show cross-zone communication."
|
|
89
|
-
},
|
|
90
|
-
flowchart: {
|
|
91
|
-
label: "Flowchart",
|
|
92
|
-
category: "Flow, interaction & behavior",
|
|
93
|
-
layout: "branching",
|
|
94
|
-
slotRole: "process-step",
|
|
95
|
-
expression: "Distinguish actions, decisions, merge points, and start or end states; label decision branches explicitly."
|
|
96
|
-
},
|
|
97
|
-
sequence: {
|
|
98
|
-
label: "Sequence",
|
|
99
|
-
category: "Flow, interaction & behavior",
|
|
100
|
-
layout: "request-flow",
|
|
101
|
-
slotRole: "entity",
|
|
102
|
-
expression: "Order actors horizontally and messages vertically through time; distinguish calls, returns, asynchronous messages, and branches."
|
|
103
|
-
},
|
|
104
|
-
"state-machine": {
|
|
105
|
-
label: "State machine",
|
|
106
|
-
category: "Flow, interaction & behavior",
|
|
107
|
-
layout: "branching",
|
|
108
|
-
slotRole: "entity",
|
|
109
|
-
expression: "Treat nodes as legal states and edges as guarded transitions; preserve initial, terminal, retry, and invalid transition semantics."
|
|
110
|
-
},
|
|
111
|
-
swimlane: {
|
|
112
|
-
label: "Swimlane",
|
|
113
|
-
category: "Flow, interaction & behavior",
|
|
114
|
-
layout: "layered",
|
|
115
|
-
slotRole: "process-step",
|
|
116
|
-
expression: "Partition steps by responsible actor or team and make every cross-lane handoff visible."
|
|
117
|
-
},
|
|
118
|
-
process: {
|
|
119
|
-
label: "Process",
|
|
120
|
-
category: "Flow, interaction & behavior",
|
|
121
|
-
layout: "linear",
|
|
122
|
-
slotRole: "process-step",
|
|
123
|
-
expression: "Show ordered work from trigger to outcome, including branches, loops, owners, and exceptional paths when supplied."
|
|
124
|
-
},
|
|
125
|
-
"data-flow": {
|
|
126
|
-
label: "Data flow",
|
|
127
|
-
category: "Flow, interaction & behavior",
|
|
128
|
-
layout: "hub-and-spoke",
|
|
129
|
-
slotRole: "entity",
|
|
130
|
-
expression: "Show data producers, transformations, stores, and consumers; label payloads and direction independently from control flow."
|
|
131
|
-
},
|
|
132
|
-
"dp-integration": {
|
|
133
|
-
label: "Data-product integration",
|
|
134
|
-
category: "Flow, interaction & behavior",
|
|
135
|
-
layout: "request-flow",
|
|
136
|
-
slotRole: "entity",
|
|
137
|
-
expression: "Show producer and consumer responsibilities, contracts, integration boundaries, and exchange direction for each data product."
|
|
138
|
-
},
|
|
139
|
-
"er-data-model": {
|
|
140
|
-
label: "ER / data model",
|
|
141
|
-
category: "Data & software modeling",
|
|
142
|
-
layout: "hub-and-spoke",
|
|
143
|
-
slotRole: "entity",
|
|
144
|
-
expression: "Represent entities with key attributes and label relationship cardinality and optionality at both endpoints."
|
|
145
|
-
},
|
|
146
|
-
medallion: {
|
|
147
|
-
label: "Medallion",
|
|
148
|
-
category: "Data & software modeling",
|
|
149
|
-
layout: "layered",
|
|
150
|
-
slotRole: "entity",
|
|
151
|
-
expression: "Arrange data products by progressive quality tier and show transformations from raw through refined to consumption-ready data."
|
|
152
|
-
},
|
|
153
|
-
"dp-security-matrix": {
|
|
154
|
-
label: "Data-product security matrix",
|
|
155
|
-
category: "Data & software modeling",
|
|
156
|
-
layout: "layered",
|
|
157
|
-
slotRole: "entity",
|
|
158
|
-
expression: "Use rows and columns as explicit subject and resource axes; encode permissions or controls at their intersections."
|
|
159
|
-
},
|
|
160
|
-
"dependency-graph": {
|
|
161
|
-
label: "Dependency graph",
|
|
162
|
-
category: "Data & software modeling",
|
|
163
|
-
layout: "branching",
|
|
164
|
-
slotRole: "entity",
|
|
165
|
-
expression: "Preserve many-parent fan-in, shared dependencies, and cycles; orient every dependency edge consistently."
|
|
166
|
-
},
|
|
167
|
-
"uml-class": {
|
|
168
|
-
label: "UML class",
|
|
169
|
-
category: "Data & software modeling",
|
|
170
|
-
layout: "hub-and-spoke",
|
|
171
|
-
slotRole: "entity",
|
|
172
|
-
expression: "Keep class names, attributes, and operations in separate compartments and preserve inheritance, aggregation, and composition notation."
|
|
173
|
-
},
|
|
174
|
-
"database-schema": {
|
|
175
|
-
label: "Database schema",
|
|
176
|
-
category: "Data & software modeling",
|
|
177
|
-
layout: "hub-and-spoke",
|
|
178
|
-
slotRole: "entity",
|
|
179
|
-
expression: "Show tables with typed columns, keys, constraints, and column-level foreign-key endpoints."
|
|
180
|
-
},
|
|
181
|
-
nested: {
|
|
182
|
-
label: "Nested containment",
|
|
183
|
-
category: "Hierarchy, containment & organization",
|
|
184
|
-
layout: "layered",
|
|
185
|
-
slotRole: "entity",
|
|
186
|
-
expression: "Use enclosure to express scope and ownership; containment must not be replaced by proximity alone."
|
|
187
|
-
},
|
|
188
|
-
tree: {
|
|
189
|
-
label: "Tree",
|
|
190
|
-
category: "Hierarchy, containment & organization",
|
|
191
|
-
layout: "branching",
|
|
192
|
-
slotRole: "entity",
|
|
193
|
-
expression: "Show a strict one-parent hierarchy with consistent depth and sibling ordering."
|
|
194
|
-
},
|
|
195
|
-
"org-chart": {
|
|
196
|
-
label: "Org chart",
|
|
197
|
-
category: "Hierarchy, containment & organization",
|
|
198
|
-
layout: "branching",
|
|
199
|
-
slotRole: "entity",
|
|
200
|
-
expression: "Show reporting structure, role ownership, and escalation paths; do not imply technical dependency."
|
|
201
|
-
},
|
|
202
|
-
"layer-stack": {
|
|
203
|
-
label: "Layer stack",
|
|
204
|
-
category: "Hierarchy, containment & organization",
|
|
205
|
-
layout: "layered",
|
|
206
|
-
slotRole: "entity",
|
|
207
|
-
expression: "Order abstraction or responsibility layers consistently and show only legitimate cross-layer dependencies."
|
|
208
|
-
},
|
|
209
|
-
"pyramid-funnel": {
|
|
210
|
-
label: "Pyramid / funnel",
|
|
211
|
-
category: "Hierarchy, containment & organization",
|
|
212
|
-
layout: "layered",
|
|
213
|
-
slotRole: "entity",
|
|
214
|
-
expression: "Use width and vertical order to encode rank, concentration, or drop-off; state which meaning applies."
|
|
215
|
-
},
|
|
216
|
-
timeline: {
|
|
217
|
-
label: "Timeline",
|
|
218
|
-
category: "Time & work",
|
|
219
|
-
layout: "linear",
|
|
220
|
-
slotRole: "process-step",
|
|
221
|
-
expression: "Place dated events on an honest temporal axis and preserve meaningful interval differences."
|
|
222
|
-
},
|
|
223
|
-
gantt: {
|
|
224
|
-
label: "Gantt",
|
|
225
|
-
category: "Time & work",
|
|
226
|
-
layout: "layered",
|
|
227
|
-
slotRole: "process-step",
|
|
228
|
-
expression: "Encode task start, duration, overlap, milestones, dependencies, and ownership against a shared time scale."
|
|
229
|
-
},
|
|
230
|
-
kanban: {
|
|
231
|
-
label: "Kanban",
|
|
232
|
-
category: "Time & work",
|
|
233
|
-
layout: "layered",
|
|
234
|
-
slotRole: "entity",
|
|
235
|
-
expression: "Use columns as workflow states, preserve card ownership and priority, and show WIP limits when supplied."
|
|
236
|
-
},
|
|
237
|
-
"user-journey": {
|
|
238
|
-
label: "User journey",
|
|
239
|
-
category: "Time & work",
|
|
240
|
-
layout: "linear",
|
|
241
|
-
slotRole: "process-step",
|
|
242
|
-
expression: "Order experience stages and align actions, touchpoints, pain points, and sentiment to each stage."
|
|
243
|
-
},
|
|
244
|
-
"story-map": {
|
|
245
|
-
label: "Story map",
|
|
246
|
-
category: "Time & work",
|
|
247
|
-
layout: "layered",
|
|
248
|
-
slotRole: "entity",
|
|
249
|
-
expression: "Use columns for narrative activity order and rows for release slices; preserve the release-cut line."
|
|
250
|
-
},
|
|
251
|
-
quadrant: {
|
|
252
|
-
label: "Quadrant",
|
|
253
|
-
category: "Quantitative & analytical",
|
|
254
|
-
layout: "layered",
|
|
255
|
-
slotRole: "entity",
|
|
256
|
-
expression: "Position items against two named axes with meaningful scales and make quadrant interpretation explicit."
|
|
257
|
-
},
|
|
258
|
-
"loop-flywheel": {
|
|
259
|
-
label: "Loop / flywheel",
|
|
260
|
-
category: "Conceptual & strategic",
|
|
261
|
-
layout: "cyclical",
|
|
262
|
-
slotRole: "process-step",
|
|
263
|
-
expression: "Show a reinforcing cycle with explicit direction and explain how each stage increases momentum or feeds the next."
|
|
264
|
-
},
|
|
265
|
-
venn: {
|
|
266
|
-
label: "Venn",
|
|
267
|
-
category: "Conceptual & strategic",
|
|
268
|
-
layout: "hub-and-spoke",
|
|
269
|
-
slotRole: "entity",
|
|
270
|
-
expression: "Use overlapping regions to encode set membership and label every meaningful intersection."
|
|
271
|
-
},
|
|
272
|
-
fishbone: {
|
|
273
|
-
label: "Fishbone",
|
|
274
|
-
category: "Conceptual & strategic",
|
|
275
|
-
layout: "branching",
|
|
276
|
-
slotRole: "entity",
|
|
277
|
-
expression: "Group candidate causes by category along branches that converge on one clearly stated effect."
|
|
278
|
-
},
|
|
279
|
-
"wardley-map": {
|
|
280
|
-
label: "Wardley map",
|
|
281
|
-
category: "Conceptual & strategic",
|
|
282
|
-
layout: "layered",
|
|
283
|
-
slotRole: "entity",
|
|
284
|
-
expression: "Position components by user visibility and evolution, then show value-chain dependencies and movement without treating the map as runtime architecture."
|
|
285
|
-
}
|
|
286
|
-
};
|
|
287
|
-
var diagramTypes = Object.keys(diagramDefinitions);
|
|
288
|
-
var chartTemplates = Object.keys({
|
|
289
|
-
"grouped-bar": {
|
|
290
|
-
label: "Grouped bar",
|
|
291
|
-
expression: "Compare quantitative values across categories with a shared zero baseline; use grouped series only when each category has comparable values.",
|
|
292
|
-
source: "diagram-design"
|
|
293
|
-
},
|
|
294
|
-
line: {
|
|
295
|
-
label: "Line",
|
|
296
|
-
expression: "Encode values on a shared scale and connect observations in meaningful order, usually time; preserve intervals and do not imply missing measurements.",
|
|
297
|
-
source: "diagram-design"
|
|
298
|
-
},
|
|
299
|
-
pie: {
|
|
300
|
-
label: "Pie",
|
|
301
|
-
expression: "Show a small set of mutually exclusive parts of one whole; preserve the total and label values directly when comparison by angle is difficult.",
|
|
302
|
-
source: "konpeki"
|
|
303
|
-
},
|
|
304
|
-
"annotated-detail": {
|
|
305
|
-
label: "Annotated detail",
|
|
306
|
-
expression: "Focus on one visual detail and connect concise annotations to exact evidence without obscuring the underlying image or chart.",
|
|
307
|
-
source: "konpeki"
|
|
308
|
-
},
|
|
309
|
-
radar: {
|
|
310
|
-
label: "Radar",
|
|
311
|
-
expression: "Compare multivariate profiles on consistently normalized radial axes; preserve axis order, scale, and direction across every series.",
|
|
312
|
-
source: "diagram-design"
|
|
313
|
-
},
|
|
314
|
-
treemap: {
|
|
315
|
-
label: "Treemap",
|
|
316
|
-
expression: "Encode part-to-whole magnitude by rectangle area within explicit hierarchy; preserve parent totals and avoid implying rank from position.",
|
|
317
|
-
source: "diagram-design"
|
|
318
|
-
},
|
|
319
|
-
scatter: {
|
|
320
|
-
label: "Scatter",
|
|
321
|
-
expression: "Position observations by two measured quantitative variables; preserve axis scales, units, outliers, and uncertainty about correlation.",
|
|
322
|
-
source: "diagram-design"
|
|
323
|
-
},
|
|
324
|
-
sankey: {
|
|
325
|
-
label: "Sankey",
|
|
326
|
-
expression: "Encode conserved quantity through splits and merges using proportional ribbon width; preserve node totals, flow direction, and units.",
|
|
327
|
-
source: "diagram-design"
|
|
328
|
-
}
|
|
329
|
-
});
|
|
330
|
-
Array.from(new Set(Object.values(diagramDefinitions).map((definition) => definition.category)));
|
|
331
|
-
//#endregion
|
|
332
|
-
//#region composition/theme-tokens.ts
|
|
333
|
-
/** Explicit opt-in bindings. Literal SVG attributes always remain fixed. */
|
|
334
|
-
var colorTokens = [
|
|
335
|
-
"ink",
|
|
336
|
-
"muted",
|
|
337
|
-
"background",
|
|
338
|
-
"surface",
|
|
339
|
-
"divider",
|
|
340
|
-
"accent",
|
|
341
|
-
"on-accent",
|
|
342
|
-
"wash"
|
|
343
|
-
];
|
|
344
|
-
var fontTokens = ["heading-font", "body-font"];
|
|
345
|
-
function tokensForAttribute(name) {
|
|
346
|
-
return name === "font-family" ? fontTokens : [
|
|
347
|
-
"fill",
|
|
348
|
-
"stroke",
|
|
349
|
-
"color"
|
|
350
|
-
].includes(name) ? colorTokens : [];
|
|
351
|
-
}
|
|
352
|
-
function validThemeBinding(name, value) {
|
|
353
|
-
return typeof value !== "string" || !value.startsWith("theme:") || tokensForAttribute(name).some((token) => value === `theme:${token}`);
|
|
354
|
-
}
|
|
355
|
-
//#endregion
|
|
356
|
-
//#region composition/schema.ts
|
|
357
|
-
var literalVectorAttribute = { oneOf: [{
|
|
358
|
-
type: "string",
|
|
359
|
-
maxLength: 1e4,
|
|
360
|
-
pattern: "^(?!theme:)"
|
|
361
|
-
}, { type: "number" }] };
|
|
362
|
-
var alignment = [
|
|
363
|
-
"start",
|
|
364
|
-
"center",
|
|
365
|
-
"end"
|
|
366
|
-
];
|
|
367
|
-
var titleStyle = ["plain", "prominent"];
|
|
368
|
-
var border = [
|
|
369
|
-
"none",
|
|
370
|
-
"outline",
|
|
371
|
-
"filled"
|
|
372
|
-
];
|
|
373
|
-
var colorScheme = [
|
|
374
|
-
"monochrome",
|
|
375
|
-
"accent-with-muted-context",
|
|
376
|
-
"categorical",
|
|
377
|
-
"sequential",
|
|
378
|
-
"status"
|
|
379
|
-
];
|
|
380
|
-
var emphasis = [
|
|
381
|
-
"none",
|
|
382
|
-
"primary",
|
|
383
|
-
"latest-series",
|
|
384
|
-
"exception"
|
|
385
|
-
];
|
|
386
|
-
var density = ["sparse", "standard"];
|
|
387
|
-
var appearanceOptions = {
|
|
388
|
-
"text-block": {
|
|
389
|
-
role: [
|
|
390
|
-
"title",
|
|
391
|
-
"subtitle",
|
|
392
|
-
"body",
|
|
393
|
-
"caption",
|
|
394
|
-
"footnote"
|
|
395
|
-
],
|
|
396
|
-
alignment,
|
|
397
|
-
border,
|
|
398
|
-
layout: [
|
|
399
|
-
"single",
|
|
400
|
-
"two-column",
|
|
401
|
-
"three-column",
|
|
402
|
-
"two-plus-two",
|
|
403
|
-
"four-column",
|
|
404
|
-
"one-plus-three",
|
|
405
|
-
"three-plus-one"
|
|
406
|
-
],
|
|
407
|
-
orientation: ["horizontal", "vertical"],
|
|
408
|
-
purpose: [
|
|
409
|
-
"narrative",
|
|
410
|
-
"comparison",
|
|
411
|
-
"emphasis"
|
|
412
|
-
],
|
|
413
|
-
treatment: [
|
|
414
|
-
"plain",
|
|
415
|
-
"subtle",
|
|
416
|
-
"strong"
|
|
417
|
-
],
|
|
418
|
-
logicalOrder: [
|
|
419
|
-
"none",
|
|
420
|
-
"parallel",
|
|
421
|
-
"progressive",
|
|
422
|
-
"cyclical",
|
|
423
|
-
"general-to-specific",
|
|
424
|
-
"hierarchical"
|
|
425
|
-
],
|
|
426
|
-
titleStyle,
|
|
427
|
-
rule: [
|
|
428
|
-
"none",
|
|
429
|
-
"top",
|
|
430
|
-
"bottom"
|
|
431
|
-
]
|
|
432
|
-
},
|
|
433
|
-
chart: {
|
|
434
|
-
template: chartTemplates,
|
|
435
|
-
selection: ["auto", "explicit"],
|
|
436
|
-
border,
|
|
437
|
-
colorScheme,
|
|
438
|
-
orientation: ["horizontal", "vertical"],
|
|
439
|
-
emphasis,
|
|
440
|
-
density,
|
|
441
|
-
legend: [
|
|
442
|
-
"none",
|
|
443
|
-
"top",
|
|
444
|
-
"right",
|
|
445
|
-
"bottom"
|
|
446
|
-
]
|
|
447
|
-
},
|
|
448
|
-
diagram: {
|
|
449
|
-
type: diagramTypes,
|
|
450
|
-
selection: ["auto", "explicit"],
|
|
451
|
-
border,
|
|
452
|
-
colorScheme,
|
|
453
|
-
emphasis,
|
|
454
|
-
density
|
|
455
|
-
},
|
|
456
|
-
image: {
|
|
457
|
-
border,
|
|
458
|
-
fit: ["cover", "contain"]
|
|
459
|
-
},
|
|
460
|
-
table: {
|
|
461
|
-
header: [
|
|
462
|
-
"row",
|
|
463
|
-
"none",
|
|
464
|
-
"column",
|
|
465
|
-
"both"
|
|
466
|
-
],
|
|
467
|
-
grid: [
|
|
468
|
-
"none",
|
|
469
|
-
"rows",
|
|
470
|
-
"all"
|
|
471
|
-
],
|
|
472
|
-
border,
|
|
473
|
-
colorScheme,
|
|
474
|
-
density
|
|
475
|
-
}
|
|
476
|
-
};
|
|
477
|
-
var text = {
|
|
478
|
-
type: "string",
|
|
479
|
-
minLength: 1,
|
|
480
|
-
pattern: "\\S"
|
|
481
|
-
};
|
|
482
|
-
var string = { type: "string" };
|
|
483
|
-
var enumeration = (values) => ({
|
|
484
|
-
type: "string",
|
|
485
|
-
enum: values
|
|
486
|
-
});
|
|
487
|
-
var object = (properties, required = Object.keys(properties)) => ({
|
|
488
|
-
type: "object",
|
|
489
|
-
properties,
|
|
490
|
-
required,
|
|
491
|
-
additionalProperties: false
|
|
492
|
-
});
|
|
493
|
-
var array = (items, minItems = 0) => ({
|
|
494
|
-
type: "array",
|
|
495
|
-
items,
|
|
496
|
-
minItems
|
|
497
|
-
});
|
|
498
|
-
var ids = {
|
|
499
|
-
...array(text, 1),
|
|
500
|
-
uniqueItems: true
|
|
501
|
-
};
|
|
502
|
-
var endpoint = object({
|
|
503
|
-
nodeId: text,
|
|
504
|
-
slotId: text
|
|
505
|
-
}, ["nodeId"]);
|
|
506
|
-
var rect = object({
|
|
507
|
-
x: {
|
|
508
|
-
type: "number",
|
|
509
|
-
minimum: 0,
|
|
510
|
-
maximum: 4096
|
|
511
|
-
},
|
|
512
|
-
y: {
|
|
513
|
-
type: "number",
|
|
514
|
-
minimum: 0,
|
|
515
|
-
maximum: 4096
|
|
516
|
-
},
|
|
517
|
-
width: {
|
|
518
|
-
type: "number",
|
|
519
|
-
exclusiveMinimum: 0,
|
|
520
|
-
maximum: 4096
|
|
521
|
-
},
|
|
522
|
-
height: {
|
|
523
|
-
type: "number",
|
|
524
|
-
exclusiveMinimum: 0,
|
|
525
|
-
maximum: 4096
|
|
526
|
-
}
|
|
527
|
-
});
|
|
528
|
-
var visualBase = {
|
|
529
|
-
viewBox: object({
|
|
530
|
-
x: { type: "number" },
|
|
531
|
-
y: { type: "number" },
|
|
532
|
-
width: {
|
|
533
|
-
type: "number",
|
|
534
|
-
exclusiveMinimum: 0,
|
|
535
|
-
maximum: 1e5
|
|
536
|
-
},
|
|
537
|
-
height: {
|
|
538
|
-
type: "number",
|
|
539
|
-
exclusiveMinimum: 0,
|
|
540
|
-
maximum: 1e5
|
|
541
|
-
}
|
|
542
|
-
}),
|
|
543
|
-
description: {
|
|
544
|
-
...text,
|
|
545
|
-
maxLength: 1e3
|
|
546
|
-
},
|
|
547
|
-
fit: enumeration([
|
|
548
|
-
"contain",
|
|
549
|
-
"cover",
|
|
550
|
-
"stretch"
|
|
551
|
-
])
|
|
552
|
-
};
|
|
553
|
-
var customVisual = { oneOf: [object({
|
|
554
|
-
format: { const: "vector" },
|
|
555
|
-
elements: {
|
|
556
|
-
...array(object({
|
|
557
|
-
id: text,
|
|
558
|
-
kind: enumeration(vectorElementKinds),
|
|
559
|
-
parentId: text,
|
|
560
|
-
attributes: {
|
|
561
|
-
type: "object",
|
|
562
|
-
maxProperties: 40,
|
|
563
|
-
properties: Object.fromEntries([
|
|
564
|
-
"fill",
|
|
565
|
-
"stroke",
|
|
566
|
-
"color",
|
|
567
|
-
"font-family"
|
|
568
|
-
].map((name) => [name, { anyOf: [literalVectorAttribute, { enum: tokensForAttribute(name).map((token) => `theme:${token}`) }] }])),
|
|
569
|
-
additionalProperties: literalVectorAttribute
|
|
570
|
-
},
|
|
571
|
-
text: {
|
|
572
|
-
type: "string",
|
|
573
|
-
maxLength: 1e4
|
|
574
|
-
}
|
|
575
|
-
}, [
|
|
576
|
-
"id",
|
|
577
|
-
"kind",
|
|
578
|
-
"attributes"
|
|
579
|
-
]), 0),
|
|
580
|
-
maxItems: 2e3
|
|
581
|
-
},
|
|
582
|
-
...visualBase
|
|
583
|
-
}, [
|
|
584
|
-
"format",
|
|
585
|
-
"elements",
|
|
586
|
-
"viewBox",
|
|
587
|
-
"description"
|
|
588
|
-
]), object({
|
|
589
|
-
format: { const: "svg" },
|
|
590
|
-
source: {
|
|
591
|
-
type: "string",
|
|
592
|
-
minLength: 1,
|
|
593
|
-
maxLength: 5e5,
|
|
594
|
-
pattern: "^\\s*<svg(?:\\s|>)"
|
|
595
|
-
},
|
|
596
|
-
...visualBase
|
|
597
|
-
}, [
|
|
598
|
-
"format",
|
|
599
|
-
"source",
|
|
600
|
-
"viewBox",
|
|
601
|
-
"description"
|
|
602
|
-
])] };
|
|
603
|
-
var topology = object({
|
|
604
|
-
kind: { const: "explicit" },
|
|
605
|
-
nodes: {
|
|
606
|
-
...array(object({
|
|
607
|
-
id: text,
|
|
608
|
-
slotId: text,
|
|
609
|
-
visible: { type: "boolean" },
|
|
610
|
-
preferredRect: rect,
|
|
611
|
-
primitive: { oneOf: [object({
|
|
612
|
-
kind: { const: "shape" },
|
|
613
|
-
shape: enumeration([
|
|
614
|
-
"rectangle",
|
|
615
|
-
"rounded-rectangle",
|
|
616
|
-
"circle",
|
|
617
|
-
"line"
|
|
618
|
-
]),
|
|
619
|
-
color: enumeration([
|
|
620
|
-
"ink",
|
|
621
|
-
"muted",
|
|
622
|
-
"accent"
|
|
623
|
-
]),
|
|
624
|
-
fill: enumeration([
|
|
625
|
-
"none",
|
|
626
|
-
"solid",
|
|
627
|
-
"wash"
|
|
628
|
-
])
|
|
629
|
-
}, ["kind", "shape"]), object({
|
|
630
|
-
kind: { const: "icon" },
|
|
631
|
-
name: text,
|
|
632
|
-
style: enumeration(["outline", "filled"]),
|
|
633
|
-
color: enumeration([
|
|
634
|
-
"ink",
|
|
635
|
-
"muted",
|
|
636
|
-
"accent"
|
|
637
|
-
])
|
|
638
|
-
}, ["kind"])] }
|
|
639
|
-
}, ["id", "slotId"]), 1),
|
|
640
|
-
maxItems: 24
|
|
641
|
-
},
|
|
642
|
-
edges: {
|
|
643
|
-
...array(object({
|
|
644
|
-
from: text,
|
|
645
|
-
to: text,
|
|
646
|
-
label: {
|
|
647
|
-
...text,
|
|
648
|
-
maxLength: 120
|
|
649
|
-
}
|
|
650
|
-
}, ["from", "to"]), 0),
|
|
651
|
-
maxItems: 48
|
|
652
|
-
}
|
|
653
|
-
});
|
|
654
|
-
var components = componentKinds.map((kind) => {
|
|
655
|
-
const options = appearanceOptions[kind];
|
|
656
|
-
const requiredAppearance = [
|
|
657
|
-
...kind === "chart" ? ["template"] : [],
|
|
658
|
-
...kind === "text-block" ? ["role"] : [],
|
|
659
|
-
...kind === "diagram" ? ["type"] : []
|
|
660
|
-
];
|
|
661
|
-
const component = object({
|
|
662
|
-
id: text,
|
|
663
|
-
kind: { const: kind },
|
|
664
|
-
preferredRect: rect,
|
|
665
|
-
slotIds: ids,
|
|
666
|
-
intent: string,
|
|
667
|
-
customVisual,
|
|
668
|
-
...kind === "text-block" ? {
|
|
669
|
-
content: {
|
|
670
|
-
type: "string",
|
|
671
|
-
maxLength: 5e4
|
|
672
|
-
},
|
|
673
|
-
textStyle: object({
|
|
674
|
-
size: {
|
|
675
|
-
type: "number",
|
|
676
|
-
minimum: 8,
|
|
677
|
-
maximum: 240
|
|
678
|
-
},
|
|
679
|
-
weight: { enum: [
|
|
680
|
-
400,
|
|
681
|
-
500,
|
|
682
|
-
600
|
|
683
|
-
] },
|
|
684
|
-
lineHeight: {
|
|
685
|
-
type: "number",
|
|
686
|
-
minimum: 1,
|
|
687
|
-
maximum: 3
|
|
688
|
-
},
|
|
689
|
-
color: enumeration([
|
|
690
|
-
"ink",
|
|
691
|
-
"muted",
|
|
692
|
-
"accent"
|
|
693
|
-
]),
|
|
694
|
-
font: enumeration(["heading", "body"])
|
|
695
|
-
}, [])
|
|
696
|
-
} : {},
|
|
697
|
-
appearance: object(Object.fromEntries(Object.entries(options).map(([key, values]) => [key, enumeration(values)])), requiredAppearance),
|
|
698
|
-
...kind === "diagram" || kind === "chart" ? { topology } : {}
|
|
699
|
-
}, [
|
|
700
|
-
"id",
|
|
701
|
-
"kind",
|
|
702
|
-
"preferredRect",
|
|
703
|
-
"slotIds",
|
|
704
|
-
...requiredAppearance.length ? ["appearance"] : []
|
|
705
|
-
]);
|
|
706
|
-
return kind === "chart" ? {
|
|
707
|
-
...component,
|
|
708
|
-
allOf: [{
|
|
709
|
-
if: { required: ["topology"] },
|
|
710
|
-
then: { properties: { appearance: { properties: { template: { const: "sankey" } } } } }
|
|
711
|
-
}]
|
|
712
|
-
} : component;
|
|
713
|
-
});
|
|
714
|
-
var schema = {
|
|
715
|
-
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
716
|
-
$id: "https://vcfgdev.github.io/konpeki/composition/v1/schema.json",
|
|
717
|
-
title: compositionSchema,
|
|
718
|
-
...object({
|
|
719
|
-
schema: { const: compositionSchema },
|
|
720
|
-
title: text,
|
|
721
|
-
authoringMode: enumeration(authoringModes),
|
|
722
|
-
theme: object({
|
|
723
|
-
id: enumeration(themeIds),
|
|
724
|
-
mode: enumeration(themeModes),
|
|
725
|
-
typography: enumeration(typographyIds)
|
|
726
|
-
}, ["id", "mode"]),
|
|
727
|
-
slides: array(object({
|
|
728
|
-
id: text,
|
|
729
|
-
name: text,
|
|
730
|
-
canvas: object({
|
|
731
|
-
width: {
|
|
732
|
-
type: "integer",
|
|
733
|
-
minimum: 256,
|
|
734
|
-
maximum: 4096
|
|
735
|
-
},
|
|
736
|
-
height: {
|
|
737
|
-
type: "integer",
|
|
738
|
-
minimum: 256,
|
|
739
|
-
maximum: 4096
|
|
740
|
-
}
|
|
741
|
-
}),
|
|
742
|
-
innerPadding: object({
|
|
743
|
-
top: {
|
|
744
|
-
type: "number",
|
|
745
|
-
minimum: 0
|
|
746
|
-
},
|
|
747
|
-
right: {
|
|
748
|
-
type: "number",
|
|
749
|
-
minimum: 0
|
|
750
|
-
},
|
|
751
|
-
bottom: {
|
|
752
|
-
type: "number",
|
|
753
|
-
minimum: 0
|
|
754
|
-
},
|
|
755
|
-
left: {
|
|
756
|
-
type: "number",
|
|
757
|
-
minimum: 0
|
|
758
|
-
}
|
|
759
|
-
}),
|
|
760
|
-
pageNumber: object({
|
|
761
|
-
style: enumeration([
|
|
762
|
-
"none",
|
|
763
|
-
"01",
|
|
764
|
-
"01/02"
|
|
765
|
-
]),
|
|
766
|
-
color: enumeration([
|
|
767
|
-
"ink",
|
|
768
|
-
"muted",
|
|
769
|
-
"accent"
|
|
770
|
-
])
|
|
771
|
-
}),
|
|
772
|
-
audience: text,
|
|
773
|
-
question: text,
|
|
774
|
-
intendedViewingSize: enumeration([
|
|
775
|
-
"presentation",
|
|
776
|
-
"social",
|
|
777
|
-
"article",
|
|
778
|
-
"custom"
|
|
779
|
-
]),
|
|
780
|
-
contentSlots: array({ oneOf: [object({
|
|
781
|
-
id: text,
|
|
782
|
-
label: text,
|
|
783
|
-
required: { type: "boolean" },
|
|
784
|
-
instruction: text,
|
|
785
|
-
role: enumeration([
|
|
786
|
-
"takeaway",
|
|
787
|
-
"body",
|
|
788
|
-
"evidence",
|
|
789
|
-
"comparison-item",
|
|
790
|
-
"process-step",
|
|
791
|
-
"entity",
|
|
792
|
-
"image",
|
|
793
|
-
"table"
|
|
794
|
-
])
|
|
795
|
-
}), object({
|
|
796
|
-
id: text,
|
|
797
|
-
label: text,
|
|
798
|
-
required: { type: "boolean" },
|
|
799
|
-
instruction: text,
|
|
800
|
-
role: enumeration(["qualification", "source"]),
|
|
801
|
-
targets: ids
|
|
802
|
-
})] }, 0),
|
|
803
|
-
components: array({ oneOf: components }, 0),
|
|
804
|
-
groups: array(object({
|
|
805
|
-
id: text,
|
|
806
|
-
label: string,
|
|
807
|
-
childIds: ids
|
|
808
|
-
}, ["id", "childIds"])),
|
|
809
|
-
readingOrder: {
|
|
810
|
-
...array(object({
|
|
811
|
-
kind: enumeration(["component", "group"]),
|
|
812
|
-
id: text
|
|
813
|
-
}), 0),
|
|
814
|
-
uniqueItems: true
|
|
815
|
-
},
|
|
816
|
-
paintOrder: {
|
|
817
|
-
...array(text, 0),
|
|
818
|
-
uniqueItems: true
|
|
819
|
-
},
|
|
820
|
-
relationships: array(object({
|
|
821
|
-
id: text,
|
|
822
|
-
kind: enumeration([
|
|
823
|
-
"flows-to",
|
|
824
|
-
"qualifies",
|
|
825
|
-
"compares-with",
|
|
826
|
-
"depends-on",
|
|
827
|
-
"connects-to"
|
|
828
|
-
]),
|
|
829
|
-
direction: enumeration([
|
|
830
|
-
"forward",
|
|
831
|
-
"bidirectional",
|
|
832
|
-
"undirected"
|
|
833
|
-
]),
|
|
834
|
-
from: endpoint,
|
|
835
|
-
to: endpoint,
|
|
836
|
-
label: string
|
|
837
|
-
}, [
|
|
838
|
-
"id",
|
|
839
|
-
"kind",
|
|
840
|
-
"direction",
|
|
841
|
-
"from",
|
|
842
|
-
"to"
|
|
843
|
-
]))
|
|
844
|
-
}, [
|
|
845
|
-
"id",
|
|
846
|
-
"name",
|
|
847
|
-
"canvas",
|
|
848
|
-
"audience",
|
|
849
|
-
"question",
|
|
850
|
-
"intendedViewingSize",
|
|
851
|
-
"contentSlots",
|
|
852
|
-
"components",
|
|
853
|
-
"groups",
|
|
854
|
-
"readingOrder",
|
|
855
|
-
"paintOrder",
|
|
856
|
-
"relationships"
|
|
857
|
-
]), 1)
|
|
858
|
-
}, [
|
|
859
|
-
"schema",
|
|
860
|
-
"title",
|
|
861
|
-
"slides"
|
|
862
|
-
])
|
|
863
|
-
};
|
|
864
|
-
//#endregion
|
|
865
|
-
//#region composition/vector.ts
|
|
866
|
-
var vectorAttributeNames = /* @__PURE__ */ new Set([
|
|
867
|
-
"x",
|
|
868
|
-
"y",
|
|
869
|
-
"dx",
|
|
870
|
-
"dy",
|
|
871
|
-
"x1",
|
|
872
|
-
"y1",
|
|
873
|
-
"x2",
|
|
874
|
-
"y2",
|
|
875
|
-
"width",
|
|
876
|
-
"height",
|
|
877
|
-
"rx",
|
|
878
|
-
"ry",
|
|
879
|
-
"cx",
|
|
880
|
-
"cy",
|
|
881
|
-
"r",
|
|
882
|
-
"d",
|
|
883
|
-
"points",
|
|
884
|
-
"transform",
|
|
885
|
-
"fill",
|
|
886
|
-
"fill-opacity",
|
|
887
|
-
"stroke",
|
|
888
|
-
"stroke-width",
|
|
889
|
-
"stroke-opacity",
|
|
890
|
-
"stroke-linecap",
|
|
891
|
-
"stroke-linejoin",
|
|
892
|
-
"stroke-dasharray",
|
|
893
|
-
"opacity",
|
|
894
|
-
"font-family",
|
|
895
|
-
"font-size",
|
|
896
|
-
"font-weight",
|
|
897
|
-
"font-style",
|
|
898
|
-
"text-anchor",
|
|
899
|
-
"dominant-baseline",
|
|
900
|
-
"vector-effect"
|
|
901
|
-
]);
|
|
902
|
-
//#endregion
|
|
903
|
-
//#region composition/validate.ts
|
|
904
|
-
var structural = new Ajv2020({
|
|
905
|
-
allErrors: true,
|
|
906
|
-
strict: false
|
|
907
|
-
}).compile(schema);
|
|
908
|
-
function validateComposition(input) {
|
|
909
|
-
if (!structural(input)) return {
|
|
910
|
-
ok: false,
|
|
911
|
-
issues: (structural.errors ?? []).map((error) => ({
|
|
912
|
-
path: error.instancePath,
|
|
913
|
-
message: error.message ?? "Invalid value"
|
|
914
|
-
}))
|
|
915
|
-
};
|
|
916
|
-
const candidate = input;
|
|
917
|
-
const issues = [];
|
|
918
|
-
const fail = (path, message) => issues.push({
|
|
919
|
-
path,
|
|
920
|
-
message
|
|
921
|
-
});
|
|
922
|
-
const unique = (ids, path) => {
|
|
923
|
-
if (new Set(ids).size !== ids.length) fail(path, "IDs must be unique");
|
|
924
|
-
};
|
|
925
|
-
unique(candidate.slides.map((slide) => slide.id), "/slides");
|
|
926
|
-
candidate.slides.forEach((slide, slideIndex) => {
|
|
927
|
-
const base = `/slides/${slideIndex}`;
|
|
928
|
-
const padding = slide.innerPadding;
|
|
929
|
-
if (padding && (padding.left + padding.right >= slide.canvas.width || padding.top + padding.bottom >= slide.canvas.height)) fail(`${base}/innerPadding`, "Padding leaves no page content area");
|
|
930
|
-
const components = new Map(slide.components.map((item) => [item.id, item]));
|
|
931
|
-
const slots = new Set(slide.contentSlots.map((item) => item.id));
|
|
932
|
-
const groups = new Map(slide.groups.map((item) => [item.id, item]));
|
|
933
|
-
unique([...components.keys(), ...groups.keys()], `${base}/groups`);
|
|
934
|
-
unique(slide.components.map((item) => item.id), `${base}/components`);
|
|
935
|
-
unique(slide.contentSlots.map((item) => item.id), `${base}/contentSlots`);
|
|
936
|
-
unique(slide.groups.map((item) => item.id), `${base}/groups`);
|
|
937
|
-
unique(slide.relationships.map((item) => item.id), `${base}/relationships`);
|
|
938
|
-
const assigned = [];
|
|
939
|
-
for (const component of slide.components) {
|
|
940
|
-
const path = `${base}/components/${component.id}`;
|
|
941
|
-
const r = component.preferredRect;
|
|
942
|
-
if (r.x + r.width > slide.canvas.width || r.y + r.height > slide.canvas.height) fail(path, "Rectangle exceeds canvas");
|
|
943
|
-
assigned.push(...component.slotIds);
|
|
944
|
-
for (const id of component.slotIds) if (!slots.has(id)) fail(path, `Unknown slot: ${id}`);
|
|
945
|
-
if (component.customVisual) {
|
|
946
|
-
if (component.customVisual.format === "svg") {
|
|
947
|
-
const source = component.customVisual.source;
|
|
948
|
-
if (/<script\b|<foreignObject\b|<\?xml-stylesheet\b|<!DOCTYPE\b|\son[a-z]+\s*=|(?:href|src)\s*=\s*["'](?!data:|#)|url\(\s*["']?(?!data:|#)/i.test(source)) fail(path, "Custom SVG must be self-contained and cannot include scripts, foreign objects, event handlers, document declarations, or external resources");
|
|
949
|
-
const sourceViewBox = source.match(/<svg\b[^>]*\bviewBox\s*=\s*(["'])([^"']+)\1/i)?.[2]?.trim().split(/[\s,]+/).map(Number);
|
|
950
|
-
const declaredViewBox = component.customVisual.viewBox;
|
|
951
|
-
if (sourceViewBox?.length !== 4 || sourceViewBox.some((value) => !Number.isFinite(value)) || sourceViewBox[0] !== declaredViewBox.x || sourceViewBox[1] !== declaredViewBox.y || sourceViewBox[2] !== declaredViewBox.width || sourceViewBox[3] !== declaredViewBox.height) fail(path, "Custom SVG root viewBox must match the declared local viewport");
|
|
952
|
-
} else {
|
|
953
|
-
const known = /* @__PURE__ */ new Set();
|
|
954
|
-
const kinds = new Map(component.customVisual.elements.map((element) => [element.id, element.kind]));
|
|
955
|
-
for (const element of component.customVisual.elements) {
|
|
956
|
-
if (known.has(element.id)) fail(path, `Duplicate vector element ID: ${element.id}`);
|
|
957
|
-
if (element.parentId && !known.has(element.parentId)) fail(path, `Vector parent must precede its child: ${element.parentId}`);
|
|
958
|
-
const parentKind = element.parentId ? kinds.get(element.parentId) : void 0;
|
|
959
|
-
if (element.parentId && parentKind !== "g" && !(element.kind === "tspan" && (parentKind === "text" || parentKind === "tspan"))) fail(path, "Vector children require a group or a text/tspan parent for tspans");
|
|
960
|
-
if (element.kind === "tspan" && parentKind !== "text" && parentKind !== "tspan") fail(path, "Vector tspans require a text or tspan parent");
|
|
961
|
-
if (element.text !== void 0 && element.kind !== "text" && element.kind !== "tspan") fail(path, "Only vector text and tspans may contain text");
|
|
962
|
-
known.add(element.id);
|
|
963
|
-
for (const [name, value] of Object.entries(element.attributes)) {
|
|
964
|
-
if (!validThemeBinding(name, value)) fail(path, `Invalid theme binding for vector attribute: ${name}`);
|
|
965
|
-
if (!vectorAttributeNames.has(name)) fail(path, `Unsupported vector attribute: ${name}`);
|
|
966
|
-
if (typeof value === "string" && /url\s*\(|javascript:|data:/i.test(value)) fail(path, `Unsafe vector attribute value: ${name}`);
|
|
967
|
-
}
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
}
|
|
971
|
-
if (component.kind === "chart" && component.topology && component.appearance.template !== "sankey") fail(path, "Only Sankey charts may define topology");
|
|
972
|
-
if ((component.kind === "diagram" || component.kind === "chart") && component.topology) {
|
|
973
|
-
const { nodes, edges } = component.topology;
|
|
974
|
-
unique(nodes.map((node) => node.id), path);
|
|
975
|
-
unique(nodes.map((node) => node.slotId), path);
|
|
976
|
-
unique(edges.map((edge) => JSON.stringify([
|
|
977
|
-
edge.from,
|
|
978
|
-
edge.to,
|
|
979
|
-
edge.label ?? ""
|
|
980
|
-
])), path);
|
|
981
|
-
const known = new Set(nodes.map((node) => node.id));
|
|
982
|
-
if (nodes.length !== component.slotIds.length) fail(path, "Topology must represent every component slot");
|
|
983
|
-
for (const node of nodes) {
|
|
984
|
-
if (!component.slotIds.includes(node.slotId)) fail(path, "Topology slot must belong to its component");
|
|
985
|
-
const nodeRect = node.preferredRect;
|
|
986
|
-
if (nodeRect && (nodeRect.x < r.x || nodeRect.y < r.y || nodeRect.x + nodeRect.width > r.x + r.width || nodeRect.y + nodeRect.height > r.y + r.height)) fail(path, "Topology node rectangle must stay within its component");
|
|
987
|
-
}
|
|
988
|
-
for (const edge of edges) {
|
|
989
|
-
if (!known.has(edge.from) || !known.has(edge.to)) fail(path, "Unknown topology endpoint");
|
|
990
|
-
if (edge.from === edge.to) fail(path, "Topology self-edges are not supported");
|
|
991
|
-
}
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
for (const id of slots) if (!assigned.includes(id)) fail(`${base}/contentSlots`, `Unassigned content slot: ${id}`);
|
|
995
|
-
const grouped = [];
|
|
996
|
-
for (const group of slide.groups) for (const id of group.childIds) {
|
|
997
|
-
grouped.push(id);
|
|
998
|
-
if (!components.has(id)) fail(`${base}/groups`, "Groups may contain only components");
|
|
999
|
-
}
|
|
1000
|
-
unique(grouped, `${base}/groups`);
|
|
1001
|
-
const reading = slide.readingOrder.flatMap((entry) => {
|
|
1002
|
-
if (entry.kind === "component") {
|
|
1003
|
-
if (!components.has(entry.id)) fail(`${base}/readingOrder`, "Unknown component");
|
|
1004
|
-
return [entry.id];
|
|
1005
|
-
}
|
|
1006
|
-
const group = groups.get(entry.id);
|
|
1007
|
-
if (!group) fail(`${base}/readingOrder`, "Unknown group");
|
|
1008
|
-
return group?.childIds ?? [];
|
|
1009
|
-
});
|
|
1010
|
-
for (const [path, order] of [["readingOrder", reading], ["paintOrder", slide.paintOrder]]) if (order.length !== components.size || new Set(order).size !== components.size || order.some((id) => !components.has(id))) fail(`${base}/${path}`, "Must contain every component exactly once");
|
|
1011
|
-
for (const slot of slide.contentSlots) if ("targets" in slot) {
|
|
1012
|
-
for (const id of slot.targets) if (!components.has(id) && !slots.has(id)) fail(`${base}/contentSlots`, "Unknown qualification/source target");
|
|
1013
|
-
}
|
|
1014
|
-
for (const relationship of slide.relationships) for (const endpoint of [relationship.from, relationship.to]) {
|
|
1015
|
-
const component = components.get(endpoint.nodeId);
|
|
1016
|
-
if (!component) fail(`${base}/relationships`, "Unknown relationship node");
|
|
1017
|
-
if (endpoint.slotId && !component?.slotIds.includes(endpoint.slotId)) fail(`${base}/relationships`, "Relationship slot must belong to its endpoint");
|
|
1018
|
-
}
|
|
1019
|
-
});
|
|
1020
|
-
return issues.length ? {
|
|
1021
|
-
ok: false,
|
|
1022
|
-
issues
|
|
1023
|
-
} : {
|
|
1024
|
-
ok: true,
|
|
1025
|
-
document: input
|
|
1026
|
-
};
|
|
1027
|
-
}
|
|
1028
|
-
//#endregion
|
|
1029
|
-
//#region composition/compile.ts
|
|
1030
|
-
function canonicalJSON(input) {
|
|
1031
|
-
const sort = (value) => Array.isArray(value) ? value.map(sort) : value && typeof value === "object" ? Object.fromEntries(Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, item]) => [key, sort(item)])) : value;
|
|
1032
|
-
return JSON.stringify(sort(input), null, 2);
|
|
1033
|
-
}
|
|
1034
|
-
canvasPadding.left, canvasPadding.left, canvasPadding.left, canvasSize.width - canvasPadding.left - canvasPadding.right, canvasPadding.left, canvasPadding.left;
|
|
1035
|
-
function parseCompositionJSON(raw) {
|
|
1036
|
-
try {
|
|
1037
|
-
const result = validateComposition(JSON.parse(raw));
|
|
1038
|
-
if (result.ok) return {
|
|
1039
|
-
ok: true,
|
|
1040
|
-
document: result.document
|
|
1041
|
-
};
|
|
1042
|
-
const issue = result.issues[0];
|
|
1043
|
-
return {
|
|
1044
|
-
ok: false,
|
|
1045
|
-
message: `${issue?.path || "Document"}: ${issue?.message || "Invalid composition"}`
|
|
1046
|
-
};
|
|
1047
|
-
} catch {
|
|
1048
|
-
return {
|
|
1049
|
-
ok: false,
|
|
1050
|
-
message: "The selected file is not valid JSON."
|
|
1051
|
-
};
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
//#endregion
|
|
1055
|
-
//#region bin/session-store.ts
|
|
1056
|
-
function revisionFor(value) {
|
|
1057
|
-
return createHash("sha256").update(value).digest("hex");
|
|
1058
|
-
}
|
|
1059
|
-
async function readCompositionFile(compositionPath) {
|
|
1060
|
-
const raw = await readFile(compositionPath, "utf8");
|
|
1061
|
-
const parsed = parseCompositionJSON(raw);
|
|
1062
|
-
if (!parsed.ok) throw new Error(parsed.message);
|
|
1063
|
-
return {
|
|
1064
|
-
document: parsed.document,
|
|
1065
|
-
revision: revisionFor(raw),
|
|
1066
|
-
name: basename(compositionPath)
|
|
1067
|
-
};
|
|
1068
|
-
}
|
|
1069
|
-
async function saveCompositionFile(compositionPath, expectedRevision, document) {
|
|
1070
|
-
const validation = validateComposition(document);
|
|
1071
|
-
if (!validation.ok) {
|
|
1072
|
-
const issue = validation.issues[0];
|
|
1073
|
-
throw Object.assign(/* @__PURE__ */ new Error(`${issue?.path || "document"} ${issue?.message || "is invalid"}`), { code: "INVALID_COMPOSITION" });
|
|
1074
|
-
}
|
|
1075
|
-
const currentRevision = revisionFor(await readFile(compositionPath, "utf8"));
|
|
1076
|
-
if (currentRevision !== expectedRevision) throw Object.assign(/* @__PURE__ */ new Error("The composition changed outside this browser."), {
|
|
1077
|
-
code: "REVISION_CONFLICT",
|
|
1078
|
-
revision: currentRevision
|
|
1079
|
-
});
|
|
1080
|
-
const value = `${canonicalJSON(document)}\n`;
|
|
1081
|
-
const temporary = join(dirname(compositionPath), `.${basename(compositionPath)}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`);
|
|
1082
|
-
await writeFile(temporary, value, {
|
|
1083
|
-
encoding: "utf8",
|
|
1084
|
-
flag: "wx"
|
|
1085
|
-
});
|
|
1086
|
-
try {
|
|
1087
|
-
await rename(temporary, compositionPath);
|
|
1088
|
-
} catch (error) {
|
|
1089
|
-
await rm(temporary, { force: true });
|
|
1090
|
-
throw error;
|
|
1091
|
-
}
|
|
1092
|
-
return { revision: revisionFor(value) };
|
|
1093
|
-
}
|
|
1094
|
-
//#endregion
|
|
1095
|
-
//#region src/lib/review.ts
|
|
1096
|
-
var emptyReview = () => ({
|
|
1097
|
-
schema: "konpeki-review/v1",
|
|
1098
|
-
version: 0,
|
|
1099
|
-
notes: []
|
|
1100
|
-
});
|
|
1101
|
-
var activeRequest = (request) => request?.status === "submitted" || request?.status === "working";
|
|
1102
|
-
//#endregion
|
|
1103
|
-
//#region bin/review-store.ts
|
|
1104
|
-
var reviewFileFor = (path) => `${resolve(path)}.review.json`;
|
|
1105
|
-
var conflict = (message) => Object.assign(new Error(message), { code: "REVISION_CONFLICT" });
|
|
1106
|
-
var invalid = (message) => Object.assign(new Error(message), { code: "INVALID_REVIEW" });
|
|
1107
|
-
async function readReview(path) {
|
|
1108
|
-
try {
|
|
1109
|
-
const state = JSON.parse(await readFile(reviewFileFor(path), "utf8"));
|
|
1110
|
-
if (state.schema !== "konpeki-review/v1") throw invalid("Unsupported revision-note format.");
|
|
1111
|
-
return state;
|
|
1112
|
-
} catch (error) {
|
|
1113
|
-
if (error.code === "ENOENT") return emptyReview();
|
|
1114
|
-
throw error;
|
|
1115
|
-
}
|
|
1116
|
-
}
|
|
1117
|
-
async function mutate(path, change) {
|
|
1118
|
-
const file = reviewFileFor(path);
|
|
1119
|
-
const release = await lockfile.lock(file, {
|
|
1120
|
-
realpath: false,
|
|
1121
|
-
retries: {
|
|
1122
|
-
retries: 60,
|
|
1123
|
-
minTimeout: 100,
|
|
1124
|
-
maxTimeout: 250
|
|
1125
|
-
}
|
|
1126
|
-
});
|
|
1127
|
-
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
1128
|
-
try {
|
|
1129
|
-
const state = await readReview(path);
|
|
1130
|
-
await change(state);
|
|
1131
|
-
state.version++;
|
|
1132
|
-
await writeFile(temporary, `${JSON.stringify(state, null, 2)}\n`);
|
|
1133
|
-
await rename(temporary, file);
|
|
1134
|
-
return state;
|
|
1135
|
-
} finally {
|
|
1136
|
-
await rm(temporary, { force: true });
|
|
1137
|
-
await release();
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
function validateTarget(document, target) {
|
|
1141
|
-
const slide = document.slides.find((s) => s.id === target.slideId);
|
|
1142
|
-
if (!slide) throw invalid("The note's page no longer exists. Remove the note and select a new target.");
|
|
1143
|
-
const component = slide.components.find((c) => c.id === target.componentId);
|
|
1144
|
-
if (target.componentId !== void 0 && !component) throw invalid("The note's component no longer exists. Remove the note and select a new target.");
|
|
1145
|
-
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.");
|
|
1146
|
-
}
|
|
1147
|
-
async function addRevisionNote(path, target, text) {
|
|
1148
|
-
if (typeof text !== "string" || !text.trim() || text.length > 4e3) throw invalid("Write a revision note of 1–4000 characters.");
|
|
1149
|
-
return mutate(path, async (state) => {
|
|
1150
|
-
if (activeRequest(state.request)) throw conflict("Wait for the current request before adding notes.");
|
|
1151
|
-
validateTarget((await readCompositionFile(path)).document, target);
|
|
1152
|
-
state.notes.push({
|
|
1153
|
-
id: randomUUID(),
|
|
1154
|
-
slideId: target.slideId,
|
|
1155
|
-
...target.componentId ? { componentId: target.componentId } : {},
|
|
1156
|
-
...target.elementId ? { elementId: target.elementId } : {},
|
|
1157
|
-
text: text.trim(),
|
|
1158
|
-
resolved: false
|
|
1159
|
-
});
|
|
1160
|
-
});
|
|
1161
|
-
}
|
|
1162
|
-
async function removeRevisionNote(path, id) {
|
|
1163
|
-
return mutate(path, (state) => {
|
|
1164
|
-
if (activeRequest(state.request)) throw conflict("Wait for the current request before removing notes.");
|
|
1165
|
-
state.notes = state.notes.filter((note) => note.id !== id);
|
|
1166
|
-
});
|
|
1167
|
-
}
|
|
1168
|
-
async function writeBuildRequest(path, request) {
|
|
1169
|
-
return mutate(path, async (state) => {
|
|
1170
|
-
if (activeRequest(state.request)) throw conflict("A build request is already waiting or working.");
|
|
1171
|
-
const current = await readCompositionFile(path);
|
|
1172
|
-
if (current.revision !== request.revision) throw conflict("Save the latest composition before requesting a build.");
|
|
1173
|
-
validateTarget(current.document, request);
|
|
1174
|
-
const notes = state.notes.filter((note) => !note.resolved);
|
|
1175
|
-
for (const note of notes) validateTarget(current.document, note);
|
|
1176
|
-
if (typeof request.instruction !== "string" || !request.instruction.trim()) throw invalid("Describe what the agent should build.");
|
|
1177
|
-
state.request = {
|
|
1178
|
-
schema: "konpeki-build-request/v2",
|
|
1179
|
-
id: randomUUID(),
|
|
1180
|
-
compositionPath: resolve(path),
|
|
1181
|
-
revision: current.revision,
|
|
1182
|
-
instruction: request.instruction.trim(),
|
|
1183
|
-
slideId: request.slideId,
|
|
1184
|
-
...request.componentId ? { componentId: request.componentId } : {},
|
|
1185
|
-
...request.elementId ? { elementId: request.elementId } : {},
|
|
1186
|
-
notes,
|
|
1187
|
-
status: "submitted"
|
|
1188
|
-
};
|
|
1189
|
-
});
|
|
1190
|
-
}
|
|
1191
|
-
async function claimBuildRequest(path) {
|
|
1192
|
-
if ((await readReview(path)).request?.status !== "submitted") return;
|
|
1193
|
-
let claimed;
|
|
1194
|
-
await mutate(path, (state) => {
|
|
1195
|
-
if (state.request?.status !== "submitted") return;
|
|
1196
|
-
state.request.status = "working";
|
|
1197
|
-
claimed = state.request;
|
|
1198
|
-
});
|
|
1199
|
-
return claimed;
|
|
1200
|
-
}
|
|
1201
|
-
async function finishBuildRequest(path, id, status, message) {
|
|
1202
|
-
if (![
|
|
1203
|
-
"done",
|
|
1204
|
-
"needs-clarification",
|
|
1205
|
-
"failed"
|
|
1206
|
-
].includes(status)) throw invalid("Status must be done, needs-clarification or failed.");
|
|
1207
|
-
if (status !== "done" && !message?.trim()) throw invalid("Explain what failed or needs clarification.");
|
|
1208
|
-
return mutate(path, async (state) => {
|
|
1209
|
-
if (state.request?.id !== id || !activeRequest(state.request)) throw conflict("This request is no longer active.");
|
|
1210
|
-
if (status === "done" && state.request.status !== "working") throw conflict("Claim the request with konpeki wait before completing it.");
|
|
1211
|
-
if (status === "done") {
|
|
1212
|
-
state.request.resultRevision = (await readCompositionFile(path)).revision;
|
|
1213
|
-
const completed = new Set(state.request.notes.map((note) => note.id));
|
|
1214
|
-
state.notes = state.notes.map((note) => completed.has(note.id) ? {
|
|
1215
|
-
...note,
|
|
1216
|
-
resolved: true
|
|
1217
|
-
} : note);
|
|
1218
|
-
}
|
|
1219
|
-
state.request.status = status;
|
|
1220
|
-
state.request.message = message?.trim();
|
|
1221
|
-
});
|
|
1222
|
-
}
|
|
1223
|
-
//#endregion
|
|
1224
|
-
//#region bin/session-plugin.ts
|
|
1225
|
-
var maximumBodyBytes = 16777216;
|
|
1226
|
-
var route = "/__konpeki/session";
|
|
1227
|
-
function sendJSON(response, status, value) {
|
|
1228
|
-
response.writeHead(status, {
|
|
1229
|
-
"content-type": "application/json; charset=utf-8",
|
|
1230
|
-
"cache-control": "no-store"
|
|
1231
|
-
});
|
|
1232
|
-
response.end(JSON.stringify(value));
|
|
1233
|
-
}
|
|
1234
|
-
async function readJSON(request) {
|
|
1235
|
-
let size = 0;
|
|
1236
|
-
const chunks = [];
|
|
1237
|
-
for await (const chunk of request) {
|
|
1238
|
-
size += chunk.length;
|
|
1239
|
-
if (size > maximumBodyBytes) throw Object.assign(/* @__PURE__ */ new Error("Request body is too large."), { code: "BODY_TOO_LARGE" });
|
|
1240
|
-
chunks.push(chunk);
|
|
1241
|
-
}
|
|
1242
|
-
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
1243
|
-
}
|
|
1244
|
-
function errorResponse(error) {
|
|
1245
|
-
const code = error instanceof Error && "code" in error ? error.code : void 0;
|
|
1246
|
-
return {
|
|
1247
|
-
status: code === "REVISION_CONFLICT" ? 409 : code === "INVALID_COMPOSITION" || code === "INVALID_REVIEW" ? 422 : code === "BODY_TOO_LARGE" ? 413 : error instanceof SyntaxError ? 400 : 500,
|
|
1248
|
-
body: {
|
|
1249
|
-
error: error instanceof Error ? error.message : "File-session request failed.",
|
|
1250
|
-
...error && typeof error === "object" && "revision" in error ? { revision: error.revision } : {}
|
|
1251
|
-
}
|
|
1252
|
-
};
|
|
1253
|
-
}
|
|
1254
|
-
function fileSessionPlugin({ compositionPath, token, onBuildRequest }) {
|
|
1255
|
-
return {
|
|
1256
|
-
name: "konpeki-file-session",
|
|
1257
|
-
enforce: "pre",
|
|
1258
|
-
config(config) {
|
|
1259
|
-
return { server: { fs: { deny: [
|
|
1260
|
-
".env",
|
|
1261
|
-
".env.*",
|
|
1262
|
-
"*.{crt,pem,key,p12,pfx,cer,der}",
|
|
1263
|
-
".npmrc",
|
|
1264
|
-
".yarnrc.yml",
|
|
1265
|
-
"**/.git/**",
|
|
1266
|
-
...config.server?.fs?.deny ?? [],
|
|
1267
|
-
"**/*.review.json",
|
|
1268
|
-
"**/*.review.json.*"
|
|
1269
|
-
] } } };
|
|
1270
|
-
},
|
|
1271
|
-
configureServer(server) {
|
|
1272
|
-
server.middlewares.use(async (request, response, next) => {
|
|
1273
|
-
const url = new URL(request.url || "/", "http://localhost");
|
|
1274
|
-
if (!url.pathname.startsWith(route)) return next();
|
|
1275
|
-
if (request.headers["x-konpeki-session"] !== token) {
|
|
1276
|
-
sendJSON(response, 403, { error: "Invalid session token." });
|
|
1277
|
-
return;
|
|
1278
|
-
}
|
|
1279
|
-
try {
|
|
1280
|
-
if (url.pathname === route && request.method === "GET") {
|
|
1281
|
-
const review = await readReview(compositionPath);
|
|
1282
|
-
sendJSON(response, 200, {
|
|
1283
|
-
...await readCompositionFile(compositionPath),
|
|
1284
|
-
review
|
|
1285
|
-
});
|
|
1286
|
-
return;
|
|
1287
|
-
}
|
|
1288
|
-
if (url.pathname === route && request.method === "PUT") {
|
|
1289
|
-
const body = await readJSON(request);
|
|
1290
|
-
sendJSON(response, 200, await saveCompositionFile(compositionPath, body.revision, body.document));
|
|
1291
|
-
return;
|
|
1292
|
-
}
|
|
1293
|
-
if (url.pathname === `${route}/build` && request.method === "POST") {
|
|
1294
|
-
const body = await readJSON(request);
|
|
1295
|
-
if (typeof body.instruction !== "string" || !body.instruction.trim()) {
|
|
1296
|
-
sendJSON(response, 400, { error: "Describe what the agent should build." });
|
|
1297
|
-
return;
|
|
1298
|
-
}
|
|
1299
|
-
const result = await writeBuildRequest(compositionPath, body);
|
|
1300
|
-
onBuildRequest?.(result);
|
|
1301
|
-
sendJSON(response, 201, result);
|
|
1302
|
-
return;
|
|
1303
|
-
}
|
|
1304
|
-
if (url.pathname === `${route}/notes` && request.method === "POST") {
|
|
1305
|
-
const body = await readJSON(request);
|
|
1306
|
-
sendJSON(response, 201, await addRevisionNote(compositionPath, body, body.text));
|
|
1307
|
-
return;
|
|
1308
|
-
}
|
|
1309
|
-
if (url.pathname === `${route}/notes` && request.method === "DELETE") {
|
|
1310
|
-
sendJSON(response, 200, await removeRevisionNote(compositionPath, (await readJSON(request)).id));
|
|
1311
|
-
return;
|
|
1312
|
-
}
|
|
1313
|
-
if (url.pathname === `${route}/cancel` && request.method === "POST") {
|
|
1314
|
-
sendJSON(response, 200, await finishBuildRequest(compositionPath, (await readJSON(request)).id, "failed", "Cancelled by user. Stop the agent before starting another request."));
|
|
1315
|
-
return;
|
|
1316
|
-
}
|
|
1317
|
-
sendJSON(response, 404, { error: "Unknown file-session endpoint." });
|
|
1318
|
-
} catch (error) {
|
|
1319
|
-
const responseError = errorResponse(error);
|
|
1320
|
-
sendJSON(response, responseError.status, responseError.body);
|
|
1321
|
-
}
|
|
1322
|
-
});
|
|
1323
|
-
}
|
|
1324
|
-
};
|
|
1325
|
-
}
|
|
1326
|
-
//#endregion
|
|
1327
6
|
//#region bin/konpeki.mjs
|
|
1328
7
|
var root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
1329
8
|
function usage() {
|
|
1330
9
|
console.error(`Usage:
|
|
1331
|
-
konpeki
|
|
1332
|
-
konpeki
|
|
1333
|
-
konpeki
|
|
1334
|
-
konpeki
|
|
1335
|
-
konpeki
|
|
10
|
+
konpeki browser install
|
|
11
|
+
konpeki preview <document.html> [--host <host>] [--port <port>] [--json]
|
|
12
|
+
konpeki validate <document.html>
|
|
13
|
+
konpeki check <document.html>
|
|
14
|
+
konpeki inspect <document.html> [--page N] [--details]
|
|
15
|
+
konpeki render <document.html> [--page N] [--format png|pdf] [--scale 2] [--output file]
|
|
16
|
+
|
|
17
|
+
Pages are one-based. Inspect and PDF include all pages unless --page is supplied.
|
|
18
|
+
Scale affects PNG only. Outputs must not already exist.`);
|
|
1336
19
|
}
|
|
1337
20
|
function option(name, fallback) {
|
|
1338
21
|
const index = process.argv.indexOf(name);
|
|
1339
22
|
return index === -1 ? fallback : process.argv[index + 1];
|
|
1340
23
|
}
|
|
1341
|
-
async function
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
port,
|
|
1356
|
-
strictPort: process.argv.includes("--port"),
|
|
1357
|
-
fs: { allow: [root, ...[
|
|
1358
|
-
"ibm-plex-sans",
|
|
1359
|
-
"ibm-plex-serif",
|
|
1360
|
-
"noto-sans",
|
|
1361
|
-
"hanken-grotesk"
|
|
1362
|
-
].map((font) => dirname(fileURLToPath(import.meta.resolve(`@fontsource/${font}/package.json`))))] }
|
|
1363
|
-
},
|
|
1364
|
-
plugins: [fileSessionPlugin({
|
|
1365
|
-
compositionPath,
|
|
1366
|
-
token,
|
|
1367
|
-
onBuildRequest: ({ request }) => {
|
|
1368
|
-
if (!request) return;
|
|
1369
|
-
console.error(`\nBuild requested for ${request.compositionPath}`);
|
|
1370
|
-
console.error(`Revision: ${request.revision}`);
|
|
1371
|
-
console.error(`Instruction: ${request.instruction}`);
|
|
1372
|
-
console.error(`Run: konpeki wait ${JSON.stringify(request.compositionPath)}\n`);
|
|
1373
|
-
}
|
|
1374
|
-
})]
|
|
1375
|
-
});
|
|
1376
|
-
try {
|
|
1377
|
-
await server.listen();
|
|
1378
|
-
} catch (error) {
|
|
1379
|
-
await server.close();
|
|
1380
|
-
throw error;
|
|
24
|
+
async function html(command, input) {
|
|
25
|
+
if (!/\.html?$/i.test(input)) throw new Error("Konpeki accepts HTML documents only.");
|
|
26
|
+
if (command === "preview") {
|
|
27
|
+
const port = Number(option("--port", "4318"));
|
|
28
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) throw new Error("Port must be an integer from 0 to 65535.");
|
|
29
|
+
const { previewHTML } = await import("./server-BAqC_5WD.mjs");
|
|
30
|
+
const documentPath = resolve(input);
|
|
31
|
+
const { url } = await previewHTML(documentPath, option("--host", "127.0.0.1"), port, root, process.argv.includes("--port"));
|
|
32
|
+
console.log(process.argv.includes("--json") ? JSON.stringify({
|
|
33
|
+
type: "ready",
|
|
34
|
+
documentPath,
|
|
35
|
+
url
|
|
36
|
+
}) : `Konpeki is reviewing ${documentPath}\n${url}\nPress Ctrl+C to stop.`);
|
|
37
|
+
return;
|
|
1381
38
|
}
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
type: "ready",
|
|
1388
|
-
compositionPath,
|
|
1389
|
-
url
|
|
1390
|
-
}));
|
|
1391
|
-
else {
|
|
1392
|
-
console.log(`Konpeki is editing ${compositionPath}`);
|
|
1393
|
-
console.log(url);
|
|
1394
|
-
console.log(`Waiting for edits and Build it requests. Press Ctrl+C to stop.`);
|
|
39
|
+
if (command === "validate") {
|
|
40
|
+
const { fileSource } = await import("./source-CZQq9GUO.mjs");
|
|
41
|
+
const snapshot = await fileSource(resolve(input)).read();
|
|
42
|
+
console.log(`${snapshot.name}: valid (${snapshot.revision})`);
|
|
43
|
+
return;
|
|
1395
44
|
}
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
45
|
+
if (![
|
|
46
|
+
"inspect",
|
|
47
|
+
"check",
|
|
48
|
+
"render"
|
|
49
|
+
].includes(command)) throw new Error(`Unknown command: ${command}`);
|
|
50
|
+
const { browserDocument } = await import("./browser-B-29TH1a.mjs");
|
|
51
|
+
const page = option("--page"), format = option("--format", "png");
|
|
52
|
+
const { bytes, report } = await browserDocument(resolve(input), {
|
|
53
|
+
page: page === void 0 ? void 0 : Number(page),
|
|
54
|
+
details: process.argv.includes("--details"),
|
|
55
|
+
...command === "render" ? {
|
|
56
|
+
format,
|
|
57
|
+
scale: Number(option("--scale", "2"))
|
|
58
|
+
} : {}
|
|
59
|
+
});
|
|
60
|
+
if (command === "render") {
|
|
61
|
+
for (const diagnostic of report.diagnostics) console.error(`${diagnostic.severity} ${diagnostic.code}: ${diagnostic.page}/${diagnostic.target}: ${diagnostic.message}`);
|
|
62
|
+
if (!report.ok) throw new Error("Render refused because the document has errors.");
|
|
63
|
+
const output = resolve(option("--output", input.replace(/\.html?$/i, "") + (page ? `-page-${page}` : "") + `.${format}`));
|
|
64
|
+
await writeFile(output, bytes, { flag: "wx" });
|
|
65
|
+
console.log(output);
|
|
66
|
+
const { reviewChecklist } = await import("./floor-Cmk7G3pU.mjs");
|
|
67
|
+
console.error(`Review ${output} at its viewing size and enlarged, against skills/konpeki/floor.md:\n${reviewChecklist().map((rule) => `- ${rule}`).join("\n")}`);
|
|
68
|
+
} else {
|
|
69
|
+
console.log(JSON.stringify(command === "check" ? {
|
|
70
|
+
ok: report.ok,
|
|
71
|
+
diagnostics: report.diagnostics
|
|
72
|
+
} : report, null, 2));
|
|
73
|
+
if (!report.ok) process.exitCode = 1;
|
|
1410
74
|
}
|
|
1411
75
|
}
|
|
1412
76
|
var command = process.argv[2];
|
|
1413
77
|
var input = process.argv[3];
|
|
1414
|
-
if (
|
|
78
|
+
if (command === "browser" && input === "install") {
|
|
79
|
+
const cli = resolve(dirname(fileURLToPath(import.meta.resolve("playwright/package.json"))), "cli.js");
|
|
80
|
+
const result = spawnSync(process.execPath, [
|
|
81
|
+
cli,
|
|
82
|
+
"install",
|
|
83
|
+
"chromium"
|
|
84
|
+
], { stdio: "inherit" });
|
|
85
|
+
if (result.error) throw result.error;
|
|
86
|
+
process.exitCode = result.status ?? 1;
|
|
87
|
+
} else if (!command || !input) {
|
|
1415
88
|
usage();
|
|
1416
89
|
process.exitCode = 1;
|
|
1417
90
|
} else try {
|
|
1418
|
-
|
|
1419
|
-
else if (command === "validate") await validate(input);
|
|
1420
|
-
else if (command === "wait") await waitForRequest(input);
|
|
1421
|
-
else if (command === "request") console.log(JSON.stringify((await readReview(input)).request ?? null, null, 2));
|
|
1422
|
-
else if (command === "finish") {
|
|
1423
|
-
if (!process.argv[4]) throw new Error("Provide the request ID returned by konpeki wait.");
|
|
1424
|
-
const result = await finishBuildRequest(input, process.argv[4], option("--status", "done"), option("--message"));
|
|
1425
|
-
console.log(JSON.stringify(result.request, null, 2));
|
|
1426
|
-
} else {
|
|
1427
|
-
usage();
|
|
1428
|
-
process.exitCode = 1;
|
|
1429
|
-
}
|
|
91
|
+
await html(command, input);
|
|
1430
92
|
} catch (error) {
|
|
1431
|
-
|
|
93
|
+
if (command === "check") console.log(JSON.stringify({
|
|
94
|
+
ok: false,
|
|
95
|
+
diagnostics: [{
|
|
96
|
+
code: "invalid-document",
|
|
97
|
+
severity: "error",
|
|
98
|
+
message: error instanceof Error ? error.message : String(error)
|
|
99
|
+
}]
|
|
100
|
+
}, null, 2));
|
|
101
|
+
else if (command === "inspect") console.log(JSON.stringify({
|
|
102
|
+
schema: "konpeki-html-inspection/v1",
|
|
103
|
+
ok: false,
|
|
104
|
+
pages: [],
|
|
105
|
+
diagnostics: [{
|
|
106
|
+
code: "inspection-failed",
|
|
107
|
+
severity: "error",
|
|
108
|
+
message: error instanceof Error ? error.message : String(error)
|
|
109
|
+
}]
|
|
110
|
+
}, null, 2));
|
|
111
|
+
else console.error(error instanceof Error ? error.message : error);
|
|
1432
112
|
process.exitCode = 1;
|
|
1433
113
|
}
|
|
1434
114
|
//#endregion
|