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.
Files changed (162) hide show
  1. package/AGENTS.md +21 -28
  2. package/AUTHORING.md +68 -229
  3. package/CONTRIBUTING.md +10 -23
  4. package/README.md +96 -87
  5. package/SETUP.md +50 -114
  6. package/docs/development.md +43 -248
  7. package/docs/workflow.md +6 -108
  8. package/html/README.md +140 -0
  9. package/html/browser.ts +103 -0
  10. package/html/document.ts +38 -0
  11. package/html/floor.ts +34 -0
  12. package/html/index.html +5 -0
  13. package/html/inspect.ts +313 -0
  14. package/html/preview.css +62 -0
  15. package/html/preview.tsx +463 -0
  16. package/html/review-hints.ts +63 -0
  17. package/html/server.ts +99 -0
  18. package/html/source.ts +104 -0
  19. package/html/starter.ts +6 -0
  20. package/html/theme-authoring.md +176 -0
  21. package/html/theme.md +61 -0
  22. package/index.html +9 -9
  23. package/package.json +21 -35
  24. package/plugin.json +1 -1
  25. package/public/og.png +0 -0
  26. package/runtime/browser-B-29TH1a.mjs +595 -0
  27. package/runtime/floor-Cmk7G3pU.mjs +41 -0
  28. package/runtime/konpeki.mjs +90 -1410
  29. package/runtime/server-BAqC_5WD.mjs +2 -0
  30. package/runtime/server-DfRpfcY9.mjs +205 -0
  31. package/runtime/source-B_Ui9tMp.mjs +141 -0
  32. package/runtime/source-CZQq9GUO.mjs +2 -0
  33. package/skills/konpeki/SKILL.md +88 -172
  34. package/skills/konpeki/assets/blank.html +17 -0
  35. package/skills/konpeki/floor.md +76 -0
  36. package/skills/konpeki/references/cover.md +27 -0
  37. package/skills/konpeki/references/long-document.md +40 -0
  38. package/skills/konpeki/references/one-pager.md +27 -0
  39. package/skills/konpeki/references/patterns.md +118 -0
  40. package/skills/konpeki/references/resume.md +28 -0
  41. package/skills/konpeki/references/slides.md +28 -0
  42. package/skills/konpeki/scripts/ensure-runtime.mjs +10 -31
  43. package/skills/konpeki/scripts/prepare-document.mjs +25 -14
  44. package/src/components/PageBoard.tsx +156 -0
  45. package/src/lib/alignment.ts +21 -0
  46. package/src/lib/page-board.ts +25 -0
  47. package/src/lib/review-position.ts +19 -0
  48. package/src/styles/base.css +4 -6
  49. package/src/styles/feedback.css +97 -61
  50. package/src/styles/shell.css +95 -323
  51. package/theme-base.css +90 -0
  52. package/theme.css +56 -0
  53. package/vite.config.ts +2 -5
  54. package/composition/README.md +0 -156
  55. package/composition/compile.ts +0 -227
  56. package/composition/document.ts +0 -600
  57. package/composition/schema.json +0 -3001
  58. package/composition/schema.ts +0 -437
  59. package/composition/theme-tokens.ts +0 -16
  60. package/composition/types.ts +0 -269
  61. package/composition/validate.ts +0 -226
  62. package/composition/vector.ts +0 -143
  63. package/composition/visualizations.ts +0 -319
  64. package/design/README.md +0 -17
  65. package/design/palettes/README.md +0 -14
  66. package/design/palettes/base.ts +0 -14
  67. package/design/palettes/candidates.ts +0 -19
  68. package/design/palettes/index.ts +0 -78
  69. package/design/review/color-theme.md +0 -44
  70. package/design/review/layout.md +0 -16
  71. package/design/review/text.md +0 -18
  72. package/design/review/typography.md +0 -15
  73. package/design/review/visuals.md +0 -31
  74. package/design/semantic-patterns.md +0 -43
  75. package/design/themes/README.md +0 -40
  76. package/design/themes/index.ts +0 -24
  77. package/design/visual-languages/technical-product.md +0 -17
  78. package/design/visual-review.md +0 -88
  79. package/lib/assets.d.ts +0 -8
  80. package/lib/charts.ts +0 -18
  81. package/lib/contrast.ts +0 -16
  82. package/lib/layouts.ts +0 -50
  83. package/lib/slide.tsx +0 -42
  84. package/lib/taste.ts +0 -17
  85. package/lib/text.tsx +0 -89
  86. package/lib/typeface.ts +0 -44
  87. package/scripts/migrate-react-page.ts +0 -120
  88. package/skills/konpeki/assets/blank.json +0 -23
  89. package/slides/README.md +0 -153
  90. package/slides/architecture/PROMPT.md +0 -31
  91. package/slides/architecture/index.tsx +0 -102
  92. package/slides/article-brief/PROMPT.md +0 -35
  93. package/slides/article-brief/index.tsx +0 -71
  94. package/slides/bar-chart/PROMPT.md +0 -39
  95. package/slides/bar-chart/index.tsx +0 -97
  96. package/slides/comparison/PROMPT.md +0 -29
  97. package/slides/comparison/index.tsx +0 -95
  98. package/slides/decision-memo/PROMPT.md +0 -34
  99. package/slides/decision-memo/index.tsx +0 -85
  100. package/slides/delivery-plan/PROMPT.md +0 -45
  101. package/slides/delivery-plan/index.tsx +0 -105
  102. package/slides/experiment/PROMPT.md +0 -44
  103. package/slides/experiment/index.tsx +0 -127
  104. package/slides/incident-workflow/PROMPT.md +0 -57
  105. package/slides/incident-workflow/index.tsx +0 -78
  106. package/slides/introducing-konpeki/PROMPT.md +0 -40
  107. package/slides/introducing-konpeki/README.md +0 -76
  108. package/slides/introducing-konpeki/SOURCE.md +0 -26
  109. package/slides/introducing-konpeki/author.ts +0 -165
  110. package/slides/introducing-konpeki/composition.json +0 -3270
  111. package/slides/line-chart/PROMPT.md +0 -40
  112. package/slides/line-chart/index.tsx +0 -72
  113. package/slides/migration/PROMPT.md +0 -38
  114. package/slides/migration/index.tsx +0 -89
  115. package/slides/og-images/PROMPT.md +0 -21
  116. package/slides/og-images/index.tsx +0 -76
  117. package/slides/product-introduction/PROMPT.md +0 -24
  118. package/slides/product-introduction/index.tsx +0 -105
  119. package/slides/research-brief/PROMPT.md +0 -40
  120. package/slides/research-brief/index.tsx +0 -104
  121. package/slides/results-explanation/PROMPT.md +0 -32
  122. package/slides/results-explanation/index.tsx +0 -96
  123. package/slides/retrospective/PROMPT.md +0 -43
  124. package/slides/retrospective/index.tsx +0 -105
  125. package/slides/sankey/PROMPT.md +0 -11
  126. package/slides/sankey/index.tsx +0 -93
  127. package/slides/teaching/PROMPT.md +0 -45
  128. package/slides/teaching/index.tsx +0 -124
  129. package/slides/vertical-bar-charts/PROMPT.md +0 -13
  130. package/slides/vertical-bar-charts/index.tsx +0 -97
  131. package/src/app/App.tsx +0 -820
  132. package/src/components/BuildOrb.tsx +0 -40
  133. package/src/components/Canvas.tsx +0 -1185
  134. package/src/components/DiagramTypeIcon.tsx +0 -78
  135. package/src/components/InspectorPanel.tsx +0 -773
  136. package/src/components/LeftPanel.tsx +0 -120
  137. package/src/components/PageSizePicker.tsx +0 -30
  138. package/src/components/Presentation.tsx +0 -105
  139. package/src/components/RevisionNotes.tsx +0 -56
  140. package/src/components/RightPanel.tsx +0 -201
  141. package/src/components/VectorOverflowWarning.tsx +0 -46
  142. package/src/components/WorkspaceChrome.tsx +0 -288
  143. package/src/components/ui.tsx +0 -53
  144. package/src/lib/examples/react-page-migration.json +0 -1295
  145. package/src/lib/examples.ts +0 -42
  146. package/src/lib/export-png.ts +0 -104
  147. package/src/lib/file-session.ts +0 -87
  148. package/src/lib/history.ts +0 -53
  149. package/src/lib/model.ts +0 -188
  150. package/src/lib/page-size.ts +0 -24
  151. package/src/lib/presentation.ts +0 -17
  152. package/src/lib/review.ts +0 -26
  153. package/src/lib/storage.ts +0 -71
  154. package/src/lib/theme.ts +0 -25
  155. package/src/lib/use-file-session.ts +0 -227
  156. package/src/main.tsx +0 -29
  157. package/src/styles/canvas.css +0 -299
  158. package/src/styles/chrome.css +0 -384
  159. package/src/styles/component-previews.css +0 -386
  160. package/src/styles/left-panel.css +0 -166
  161. package/src/styles/presentation.css +0 -72
  162. package/src/styles/right-panel.css +0 -1215
@@ -1,1434 +1,114 @@
1
1
  #!/usr/bin/env node
2
- import { createHash, randomBytes, randomUUID } from "node:crypto";
3
- import { basename, dirname, join, resolve } from "node:path";
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 preview <composition.json> [--host <host>] [--port <port>] [--json]
1332
- konpeki validate <composition.json>
1333
- konpeki wait <composition.json>
1334
- konpeki request <composition.json>
1335
- konpeki finish <composition.json> <request-id> [--status done|needs-clarification|failed] [--message <text>]`);
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 preview(input) {
1342
- const compositionPath = resolve(input);
1343
- await readCompositionFile(compositionPath);
1344
- const token = randomBytes(24).toString("base64url");
1345
- const host = option("--host", "127.0.0.1");
1346
- const port = Number(option("--port", "4318"));
1347
- const json = process.argv.includes("--json");
1348
- if (!Number.isInteger(port) || port < 0 || port > 65535) throw new Error("Port must be an integer from 0 to 65535 (0 chooses a free port).");
1349
- const server = await createServer({
1350
- root,
1351
- configFile: resolve(root, "vite.config.ts"),
1352
- logLevel: json ? "silent" : "info",
1353
- server: {
1354
- host,
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
- const address = server.httpServer?.address();
1383
- const actualPort = address && typeof address === "object" ? address.port : port;
1384
- const displayHost = host === "0.0.0.0" || host === "::" ? "localhost" : host;
1385
- const url = `http://${displayHost.includes(":") ? `[${displayHost}]` : displayHost}:${actualPort}/?session=${encodeURIComponent(token)}`;
1386
- if (json) console.log(JSON.stringify({
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
- async function validate(input) {
1398
- const result = await readCompositionFile(resolve(input));
1399
- console.log(`${result.name}: valid (${result.revision})`);
1400
- }
1401
- async function waitForRequest(input) {
1402
- console.error(`Waiting for a Build it request for ${resolve(input)}…`);
1403
- for (;;) {
1404
- const request = await claimBuildRequest(resolve(input));
1405
- if (request) {
1406
- console.log(JSON.stringify(request, null, 2));
1407
- return;
1408
- }
1409
- await new Promise((resolveWait) => setTimeout(resolveWait, 250));
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 (!command || !input) {
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
- if (command === "preview") await preview(input);
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
- console.error(error instanceof Error ? error.message : error);
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