konpeki 0.1.0 → 0.2.0

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