@penpair/marwes-ui 0.2.0 → 0.3.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,5 +1,5 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
- import { atomicWriteFile, commandSleep, resolveProjectOutput, exportShapePng, guardFile, guardRevision, observationHash, readJournal, readJournalTarget, runJournaledSteps, runSnippet, snippet, waitForStableLayout, withCleanupDeadline, writeCoordinator } from "@penpair/core";
2
+ import { atomicWriteFile, commandSleep, resolveProjectOutput, exportShapePng, guardFile, guardRevision, observationHash, readJournal, readJournalTarget, runJournaledSteps, runSnippet, snippet, waitForStableLayout, withCleanupDeadline, writeCoordinator, } from "@penpair/core";
3
3
  import { mkdir, rename, rm } from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { canonical } from "./tokens.js";
@@ -16,23 +16,52 @@ for(const set of c.sets)for(const token of set.tokens){if(!("resolvedValue" in t
16
16
  if(JSON.stringify(resolved).length>500000)throw new Error("Resolved output exceeds bounded size");
17
17
  return {ok:true,readOnly:true,fileId:f.id,revision:f.revn,pageId:penpot.currentPage.id,pageName:penpot.currentPage.name,source,selection,supported,resolved,penpotApiVersion:typeof penpot.version==="string"?penpot.version:null};`;
18
18
  async function readMode(client, timeoutMs = 10000) {
19
- const result = await runSnippet(client, snippet("modes-read", readBody, {}, "read"), { effect: "read", retries: 0, timeoutMs });
20
- if (result.ok !== true || typeof result.fileId !== "string" || !Number.isInteger(result.revision) || !result.source || !result.selection || !Array.isArray(result.resolved))
19
+ const result = await runSnippet(client, snippet("modes-read", readBody, {}, "read"), {
20
+ effect: "read",
21
+ retries: 0,
22
+ timeoutMs,
23
+ });
24
+ if (result.ok !== true ||
25
+ typeof result.fileId !== "string" ||
26
+ !Number.isInteger(result.revision) ||
27
+ !result.source ||
28
+ !result.selection ||
29
+ !Array.isArray(result.resolved)) {
21
30
  throw new Error("Incomplete mode observation");
31
+ }
22
32
  const state = result;
23
- const ids = [...state.source.sets, ...state.source.themes].map(item => item.id);
24
- if (ids.some(id => typeof id !== "string" || !id) || new Set(ids).size !== ids.length)
33
+ const ids = [
34
+ ...state.source.sets,
35
+ ...state.source.themes,
36
+ ].map((item) => item.id);
37
+ if (ids.some((id) => typeof id !== "string" || !id) || new Set(ids).size !== ids.length) {
25
38
  throw new Error("Stable token-set and theme IDs are required for mode trials");
26
- if (new Set(state.source.sets.map(set => set.name)).size !== state.source.sets.length || new Set(state.source.themes.map(theme => canonical([theme.group, theme.name]))).size !== state.source.themes.length)
39
+ }
40
+ if (new Set(state.source.sets.map((set) => set.name)).size !== state.source.sets.length ||
41
+ new Set(state.source.themes.map((theme) => canonical([
42
+ theme.group,
43
+ theme.name,
44
+ ]))).size !== state.source.themes.length) {
27
45
  throw new Error("Ambiguous mode identity");
46
+ }
28
47
  state.sourceHash = observationHash(state.source);
29
48
  return state;
30
49
  }
31
50
  function desiredSelection(state, group, name) {
32
- const theme = state.source.themes.find(theme => theme.group === group && theme.name === name);
33
- if (!theme)
51
+ const theme = state.source.themes.find((theme) => theme.group === group && theme.name === name);
52
+ if (!theme) {
34
53
  throw new Error(`Missing mode ${group}/${name}`);
35
- return { sets: state.source.sets.map(set => ({ id: set.id, active: theme.sets.includes(set.name) })), themes: state.source.themes.map(item => ({ id: item.id, active: item.id === theme.id })) };
54
+ }
55
+ return {
56
+ sets: state.source.sets.map((set) => ({
57
+ id: set.id,
58
+ active: theme.sets.includes(set.name),
59
+ })),
60
+ themes: state.source.themes.map((item) => ({
61
+ id: item.id,
62
+ active: item.id === theme.id,
63
+ })),
64
+ };
36
65
  }
37
66
  function changeSelection(state, desired) {
38
67
  return snippet("modes-select", `
@@ -44,69 +73,167 @@ for(const theme of c.themes)if(theme.active)theme.toggleActive();
44
73
  for(const set of c.sets)if(set.active)set.toggleActive();
45
74
  for(const theme of c.themes)if(CFG.desired.themes.some(t=>t.id===theme.id&&t.active))theme.toggleActive();
46
75
  for(const set of c.sets){const wanted=CFG.desired.sets.find(s=>s.id===set.id);if(!wanted)throw new Error("Set identity changed");if(!!set.active!==wanted.active)set.toggleActive();}
47
- return {ok:true};`, { source: JSON.stringify(state.source), before: JSON.stringify(state.selection), desired }, "write");
76
+ return {ok:true};`, {
77
+ source: JSON.stringify(state.source),
78
+ before: JSON.stringify(state.selection),
79
+ desired,
80
+ }, "write");
48
81
  }
49
82
  function compareMode(snapshot, state, theme) {
50
- const activeSets = state.source.sets.filter(set => state.selection.sets.find(item => item.id === set.id)?.active).map(set => set.name);
51
- const expectedSets = snapshot.sets.filter(set => theme.sets.includes(set.name)).map(set => set.name);
52
- const actual = new Map(state.resolved.map(token => [token.name, token]));
53
- const values = expectedResolved(snapshot, theme).map(token => {
83
+ const activeSets = state.source.sets
84
+ .filter((set) => state.selection.sets.find((item) => item.id === set.id)?.active)
85
+ .map((set) => set.name);
86
+ const expectedSets = snapshot.sets
87
+ .filter((set) => theme.sets.includes(set.name))
88
+ .map((set) => set.name);
89
+ const actual = new Map(state.resolved.map((token) => [
90
+ token.name,
91
+ token,
92
+ ]));
93
+ const values = expectedResolved(snapshot, theme).map((token) => {
54
94
  const observed = actual.get(token.name);
55
- const comparison = observed?.unknown ? { unknown: observed.unknown } : !observed ? { unknown: "No active resolved token" } : observed.type !== token.type ? { equal: false } : compareResolved(token, observed.value);
56
- return { name: token.name, type: token.type, expected: token.value ?? null, actual: observed?.value ?? null, ...comparison };
95
+ const comparison = observed?.unknown
96
+ ? {
97
+ unknown: observed.unknown,
98
+ }
99
+ : !observed
100
+ ? {
101
+ unknown: "No active resolved token",
102
+ }
103
+ : observed.type !== token.type
104
+ ? {
105
+ equal: false,
106
+ }
107
+ : compareResolved(token, observed.value);
108
+ return {
109
+ name: token.name,
110
+ type: token.type,
111
+ expected: token.value ?? null,
112
+ actual: observed?.value ?? null,
113
+ ...comparison,
114
+ };
57
115
  });
58
116
  const orderMatches = canonical(activeSets) === canonical(expectedSets);
59
- return { group: theme.group, name: theme.name, context: "isolated-theme", activeSets, expectedSets, orderMatches, values, complete: values.length > 0 && values.every(value => !value.unknown), matches: orderMatches && values.length > 0 && values.every(value => value.equal === true), sourceHash: state.sourceHash };
117
+ return {
118
+ group: theme.group,
119
+ name: theme.name,
120
+ context: "isolated-theme",
121
+ activeSets,
122
+ expectedSets,
123
+ orderMatches,
124
+ values,
125
+ complete: values.length > 0 && values.every((value) => !value.unknown),
126
+ matches: orderMatches && values.length > 0 && values.every((value) => value.equal === true),
127
+ sourceHash: state.sourceHash,
128
+ };
60
129
  }
61
130
  export async function runModeTrial(client, snapshot, options) {
62
131
  const target = await readJournalTarget(client);
63
- client = guardFile(client, { id: target.fileId });
132
+ client = guardFile(client, {
133
+ id: target.fileId,
134
+ });
64
135
  const initial = await readMode(client);
65
- if (!initial.supported)
136
+ if (!initial.supported) {
66
137
  throw new Error("resolvedValue API unavailable; no theme was changed");
67
- if (snapshot.themes.length < 1 || snapshot.themes.length > 20)
138
+ }
139
+ if (snapshot.themes.length < 1 || snapshot.themes.length > 20) {
68
140
  throw new Error("Mode trials require 1–20 declared themes");
141
+ }
69
142
  let original = initial;
70
143
  const runId = options.resume ?? randomUUID();
71
144
  if (options.resume) {
72
145
  const record = await readJournal(options.root, runId);
73
146
  const payload = record.payload;
74
- if (record.operation !== "tokens-modes" || !payload?.original || record.target.fileId !== target.fileId)
147
+ if (record.operation !== "tokens-modes" ||
148
+ !payload?.original ||
149
+ record.target.fileId !== target.fileId) {
75
150
  throw new Error("No matching original mode state in this journal");
151
+ }
76
152
  original = payload.original;
77
153
  }
78
- if (original.sourceHash !== initial.sourceHash)
154
+ if (original.sourceHash !== initial.sourceHash) {
79
155
  throw new Error("Token declarations changed since the trial began");
80
- if (options.frame && (initial.pageId !== original.pageId || initial.pageName !== options.frame.page))
156
+ }
157
+ if (options.frame &&
158
+ (initial.pageId !== original.pageId || initial.pageName !== options.frame.page)) {
81
159
  throw new Error("Open the requested frame page before inspecting a mode trial");
160
+ }
82
161
  if (options.frame) {
83
- const frame = await runSnippet(client, snippet("modes-frame", "if(penpot.currentPage.id!==CFG.pageId)throw new Error('Frame page changed');const shape=penpot.currentPage.getShapeById(CFG.id);if(!shape||typeof shape.export!=='function')throw new Error('Requested exportable frame is unavailable');return {ok:true,frameId:shape.id};", { id: options.frame.id, pageId: initial.pageId }, "read"), { effect: "read", retries: 0 });
84
- if (frame.ok !== true || frame.frameId !== options.frame.id)
162
+ const frame = await runSnippet(client, snippet("modes-frame", "if(penpot.currentPage.id!==CFG.pageId)throw new Error('Frame page changed');const shape=penpot.currentPage.getShapeById(CFG.id);if(!shape||typeof shape.export!=='function')throw new Error('Requested exportable frame is unavailable');return {ok:true,frameId:shape.id};", {
163
+ id: options.frame.id,
164
+ pageId: initial.pageId,
165
+ }, "read"), {
166
+ effect: "read",
167
+ retries: 0,
168
+ });
169
+ if (frame.ok !== true || frame.frameId !== options.frame.id) {
85
170
  throw new Error("Frame identity was not observed");
171
+ }
86
172
  }
87
- const selections = snapshot.themes.map(theme => desiredSelection(original, theme.group, theme.name));
88
- const base = { schemaVersion: 1, kind: "mode-trial", fileId: target.fileId, snapshotHash: snapshot.hash, sourceHash: original.sourceHash, capturedAt: new Date().toISOString(), penpotApiVersion: initial.penpotApiVersion, phase: "inspected", modes: [], plannedThemes: snapshot.themes, original: original.selection, plannedStates: snapshot.themes.map((theme, index) => ({ group: theme.group, name: theme.name, activeSets: original.source.sets.filter(set => selections[index].sets.find(item => item.id === set.id)?.active).map(set => set.name) })), restoration: { verified: false }, complete: true, verified: false, uncertain: false };
89
- if (!options.apply)
173
+ const selections = snapshot.themes.map((theme) => desiredSelection(original, theme.group, theme.name));
174
+ const base = {
175
+ schemaVersion: 1,
176
+ kind: "mode-trial",
177
+ fileId: target.fileId,
178
+ snapshotHash: snapshot.hash,
179
+ sourceHash: original.sourceHash,
180
+ capturedAt: new Date().toISOString(),
181
+ penpotApiVersion: initial.penpotApiVersion,
182
+ phase: "inspected",
183
+ modes: [],
184
+ plannedThemes: snapshot.themes,
185
+ original: original.selection,
186
+ plannedStates: snapshot.themes.map((theme, index) => ({
187
+ group: theme.group,
188
+ name: theme.name,
189
+ activeSets: original.source.sets
190
+ .filter((set) => selections[index].sets.find((item) => item.id === set.id)?.active)
191
+ .map((set) => set.name),
192
+ })),
193
+ restoration: {
194
+ verified: false,
195
+ },
196
+ complete: true,
197
+ verified: false,
198
+ uncertain: false,
199
+ };
200
+ if (!options.apply) {
90
201
  return base;
202
+ }
91
203
  let report = base;
92
- const settle = async () => { if (options.layout !== false) {
93
- const result = await waitForStableLayout(client, { timeoutMs: 10000 });
94
- if (!result.stable)
95
- throw new Error("Layout did not settle for mode evidence");
96
- } };
97
- const inspect = async () => { await settle(); return readMode(client); };
204
+ const settle = async () => {
205
+ if (options.layout !== false) {
206
+ const result = await waitForStableLayout(client, {
207
+ timeoutMs: 10000,
208
+ });
209
+ if (!result.stable) {
210
+ throw new Error("Layout did not settle for mode evidence");
211
+ }
212
+ }
213
+ };
214
+ const inspect = async () => {
215
+ await settle();
216
+ return readMode(client);
217
+ };
98
218
  const stableMode = async () => {
99
219
  let previous = await inspect();
100
220
  const deadline = Date.now() + 5000;
101
221
  for (let samples = 2; samples <= 20 && Date.now() < deadline; samples++) {
102
222
  await (options.sleep ?? commandSleep)(100);
103
223
  const current = await readMode(client, Math.max(1, deadline - Date.now()));
104
- if (Date.now() > deadline)
224
+ if (Date.now() > deadline) {
105
225
  throw new Error("Resolved observation exceeded its deadline");
106
- if (current.sourceHash !== previous.sourceHash || canonical(current.selection) !== canonical(previous.selection))
226
+ }
227
+ if (current.sourceHash !== previous.sourceHash ||
228
+ canonical(current.selection) !== canonical(previous.selection)) {
107
229
  throw new Error("Source or selection changed while observing resolution");
108
- if (canonical(current.resolved) === canonical(previous.resolved))
109
- return { state: current, samples };
230
+ }
231
+ if (canonical(current.resolved) === canonical(previous.resolved)) {
232
+ return {
233
+ state: current,
234
+ samples,
235
+ };
236
+ }
110
237
  previous = current;
111
238
  }
112
239
  throw new Error("Resolved values did not stabilize within the observation budget");
@@ -114,44 +241,87 @@ export async function runModeTrial(client, snapshot, options) {
114
241
  const sourceOkay = (state) => state.sourceHash === original.sourceHash;
115
242
  const originalOkay = (state) => sourceOkay(state) && canonical(state.selection) === canonical(original.selection);
116
243
  const select = async (state, desired) => {
117
- if (!sourceOkay(state))
244
+ if (!sourceOkay(state)) {
118
245
  throw new Error("Token declarations changed during trial");
119
- if (canonical(state.selection) === canonical(desired))
246
+ }
247
+ if (canonical(state.selection) === canonical(desired)) {
120
248
  return;
249
+ }
121
250
  const current = await readJournalTarget(client);
122
- await runSnippet(guardRevision(client, current), changeSelection(state, desired), { effect: "write", timeoutMs: 10000 });
251
+ await runSnippet(guardRevision(client, current), changeSelection(state, desired), {
252
+ effect: "write",
253
+ timeoutMs: 10000,
254
+ });
123
255
  };
124
256
  const pendingImages = [];
125
257
  try {
126
- const record = await runJournaledSteps([{ key: "trial", label: "measure isolated modes and restore full selection", initial,
127
- signature: [readBody, changeSelection.toString(), compareMode.toString(), compareResolved.toString(), expectedResolved.toString(), runModeTrial.toString()].join("\n"),
258
+ const record = await runJournaledSteps([
259
+ {
260
+ key: "trial",
261
+ label: "measure isolated modes and restore full selection",
262
+ initial,
263
+ signature: [
264
+ readBody,
265
+ changeSelection.toString(),
266
+ compareMode.toString(),
267
+ compareResolved.toString(),
268
+ expectedResolved.toString(),
269
+ runModeTrial.toString(),
270
+ ].join("\n"),
128
271
  inspect,
129
272
  apply: async () => {
130
- report = { ...base, phase: "measured", journal: { runId, state: "running" } };
273
+ report = {
274
+ ...base,
275
+ phase: "measured",
276
+ journal: {
277
+ runId,
278
+ state: "running",
279
+ },
280
+ };
131
281
  try {
132
282
  for (let index = 0; index < snapshot.themes.length; index++) {
133
283
  options.onStep?.(`measure ${snapshot.themes[index].group}/${snapshot.themes[index].name}`);
134
284
  await select(await inspect(), selections[index]);
135
- const evidence = await stableMode(), observed = evidence.state;
136
- if (!sourceOkay(observed) || canonical(observed.selection) !== canonical(selections[index]))
285
+ const evidence = await stableMode();
286
+ const observed = evidence.state;
287
+ if (!sourceOkay(observed) ||
288
+ canonical(observed.selection) !== canonical(selections[index])) {
137
289
  throw new Error("Theme activation did not establish the isolated requested selection");
290
+ }
138
291
  const mode = compareMode(snapshot, observed, snapshot.themes[index]);
139
292
  mode.resolutionSamples = evidence.samples;
140
293
  report.modes.push(mode);
141
294
  if (options.frame) {
142
- const image = await exportShapePng(client, options.frame.id, { maxExports: 4, attempts: 1, sleep: options.sleep });
295
+ const image = await exportShapePng(client, options.frame.id, {
296
+ maxExports: 4,
297
+ attempts: 1,
298
+ sleep: options.sleep,
299
+ });
143
300
  const after = await readMode(client);
144
- if (!image.stable || !sourceOkay(after) || after.pageId !== original.pageId || canonical(after.selection) !== canonical(selections[index]) || canonical(after.resolved) !== canonical(observed.resolved))
301
+ if (!image.stable ||
302
+ !sourceOkay(after) ||
303
+ after.pageId !== original.pageId ||
304
+ canonical(after.selection) !== canonical(selections[index]) ||
305
+ canonical(after.resolved) !== canonical(observed.resolved)) {
145
306
  throw new Error("Mode changed during export");
307
+ }
146
308
  const name = `${index + 1}.png`;
147
- pendingImages.push({ name, bytes: image.png });
148
- mode.image = { file: name, sha256: createHash("sha256").update(image.png).digest("hex"), capture: image.capture };
309
+ pendingImages.push({
310
+ name,
311
+ bytes: image.png,
312
+ });
313
+ mode.image = {
314
+ file: name,
315
+ sha256: createHash("sha256").update(image.png).digest("hex"),
316
+ capture: image.capture,
317
+ };
149
318
  }
150
319
  }
151
320
  }
152
321
  catch (error) {
153
322
  report.error = error instanceof Error ? error.message : String(error);
154
- report.uncertain = error.code === "write-uncertain";
323
+ report.uncertain =
324
+ error.code === "write-uncertain";
155
325
  }
156
326
  finally {
157
327
  try {
@@ -159,26 +329,77 @@ export async function runModeTrial(client, snapshot, options) {
159
329
  const current = await readMode(client);
160
330
  await select(current, original.selection);
161
331
  const restored = await inspect();
162
- report.restoration = { verified: originalOkay(restored) };
163
- if (!report.restoration.verified)
164
- report.restoration.error = "Independent readback differs from the original complete selection";
332
+ report.restoration = {
333
+ verified: originalOkay(restored),
334
+ };
335
+ if (!report.restoration.verified) {
336
+ report.restoration.error =
337
+ "Independent readback differs from the original complete selection";
338
+ }
165
339
  }, 10000);
166
340
  }
167
341
  catch (error) {
168
- report.restoration = { verified: false, error: error instanceof Error ? error.message : String(error) };
342
+ report.restoration = {
343
+ verified: false,
344
+ error: error instanceof Error ? error.message : String(error),
345
+ };
169
346
  }
170
347
  }
171
- report.complete = !report.error && report.modes.length === snapshot.themes.length && report.modes.every(mode => mode.complete) && report.restoration.verified;
172
- report.verified = report.complete && report.modes.every(mode => mode.matches);
348
+ report.complete =
349
+ !report.error &&
350
+ report.modes.length === snapshot.themes.length &&
351
+ report.modes.every((mode) => mode.complete) &&
352
+ report.restoration.verified;
353
+ report.verified = report.complete && report.modes.every((mode) => mode.matches);
173
354
  return report;
174
355
  },
175
- verify: (current, _before, result) => originalOkay(current) && result?.restoration.verified === true,
176
- reconcile: (_before, current, result) => !sourceOkay(current) ? "conflict" : originalOkay(current) && result?.restoration.verified === true ? "complete" : [original.selection, ...selections].some(selection => canonical(selection) === canonical(current.selection)) ? "pending" : "conflict",
177
- }], { root: options.root, runId, resume: Boolean(options.resume), operation: "tokens-modes", input: { snapshot: observationHash(snapshot), frame: options.frame ?? null, layout: options.layout ?? {} }, payload: { original }, target: () => readJournalTarget(client), redact: options.redact, coordinate: writeCoordinator(client, { root: options.root, operation: "tokens-modes", forceLock: options.forceLock, warn: options.onStep }), checkpoint: options.checkpoint, requireCheckpoint: options.requireCheckpoint, onStep: options.onStep });
356
+ verify: (current, _before, result) => originalOkay(current) &&
357
+ result?.restoration.verified === true,
358
+ reconcile: (_before, current, result) => !sourceOkay(current)
359
+ ? "conflict"
360
+ : originalOkay(current) &&
361
+ result?.restoration.verified === true
362
+ ? "complete"
363
+ : [
364
+ original.selection,
365
+ ...selections,
366
+ ].some((selection) => canonical(selection) === canonical(current.selection))
367
+ ? "pending"
368
+ : "conflict",
369
+ },
370
+ ], {
371
+ root: options.root,
372
+ runId,
373
+ resume: Boolean(options.resume),
374
+ operation: "tokens-modes",
375
+ input: {
376
+ snapshot: observationHash(snapshot),
377
+ frame: options.frame ?? null,
378
+ layout: options.layout ?? {},
379
+ },
380
+ payload: {
381
+ original,
382
+ },
383
+ target: () => readJournalTarget(client),
384
+ redact: options.redact,
385
+ coordinate: writeCoordinator(client, {
386
+ root: options.root,
387
+ operation: "tokens-modes",
388
+ forceLock: options.forceLock,
389
+ warn: options.onStep,
390
+ }),
391
+ checkpoint: options.checkpoint,
392
+ requireCheckpoint: options.requireCheckpoint,
393
+ onStep: options.onStep,
394
+ });
178
395
  report = record.steps[0].result;
179
- if (!report)
396
+ if (!report) {
180
397
  throw new Error("Mode journal lacks measured results");
181
- report.journal = { runId, state: record.state };
398
+ }
399
+ report.journal = {
400
+ runId,
401
+ state: record.state,
402
+ };
182
403
  if (record.redacted) {
183
404
  report.redacted = true;
184
405
  report.complete = false;
@@ -190,40 +411,69 @@ export async function runModeTrial(client, snapshot, options) {
190
411
  try {
191
412
  const record = await readJournal(options.root, runId);
192
413
  report = record.steps[0]?.result ?? report;
193
- report.journal = { runId, state: record.state };
194
- if (record.redacted)
414
+ report.journal = {
415
+ runId,
416
+ state: record.state,
417
+ };
418
+ if (record.redacted) {
195
419
  report.redacted = true;
420
+ }
421
+ }
422
+ catch {
423
+ /* Preserve the actual failure. */
196
424
  }
197
- catch { /* Preserve the actual failure. */ }
198
425
  report.complete = false;
199
426
  report.verified = false;
200
427
  report.error ??= error instanceof Error ? error.message : String(error);
201
- report.uncertain ||= error.outcome === "uncertain";
428
+ report.uncertain ||=
429
+ error.outcome === "uncertain";
202
430
  }
203
431
  if (options.frame && report.complete && pendingImages.length !== snapshot.themes.length) {
204
432
  report.complete = false;
205
433
  report.verified = false;
206
- report.error = "Recovered measurement lacks image bytes; start a fresh trial to publish exports";
434
+ report.error =
435
+ "Recovered measurement lacks image bytes; start a fresh trial to publish exports";
207
436
  }
208
- if (options.frame && report.complete && pendingImages.length === snapshot.themes.length)
437
+ if (options.frame && report.complete && pendingImages.length === snapshot.themes.length) {
209
438
  await publishModes(options.root, options.frame.out, report, pendingImages, options.redact);
439
+ }
210
440
  return report;
211
441
  }
212
442
  async function publishModes(root, out, report, images, redact) {
213
- const destination = await resolveProjectOutput(root, out), generation = randomUUID(), stage = path.join(destination, "." + generation), final = path.join(destination, generation);
214
- report.artifacts = { manifest: path.join(destination, "manifest.json"), generation };
443
+ const destination = await resolveProjectOutput(root, out);
444
+ const generation = randomUUID();
445
+ const stage = path.join(destination, "." + generation);
446
+ const final = path.join(destination, generation);
447
+ report.artifacts = {
448
+ manifest: path.join(destination, "manifest.json"),
449
+ generation,
450
+ };
215
451
  const serialized = JSON.stringify(report, null, 2) + "\n";
216
- if (redact && redact(serialized) !== serialized)
452
+ if (redact && redact(serialized) !== serialized) {
217
453
  throw new Error("Mode evidence requires credential redaction; previous output preserved");
218
- await mkdir(stage, { recursive: true });
454
+ }
455
+ await mkdir(stage, {
456
+ recursive: true,
457
+ });
219
458
  try {
220
- for (const image of images)
459
+ for (const image of images) {
221
460
  await atomicWriteFile(path.join(stage, image.name), image.bytes);
461
+ }
222
462
  await atomicWriteFile(path.join(stage, "manifest.json"), serialized);
223
463
  await rename(stage, final);
224
- await atomicWriteFile(path.join(destination, "manifest.json"), JSON.stringify({ schemaVersion: 1, generation, manifest: `${generation}/manifest.json`, sha256: createHash("sha256").update(serialized).digest("hex") }, null, 2) + "\n", { replace: true });
464
+ await atomicWriteFile(path.join(destination, "manifest.json"), JSON.stringify({
465
+ schemaVersion: 1,
466
+ generation,
467
+ manifest: `${generation}/manifest.json`,
468
+ sha256: createHash("sha256").update(serialized).digest("hex"),
469
+ }, null, 2) + "\n", {
470
+ replace: true,
471
+ });
225
472
  }
226
473
  finally {
227
- await rm(stage, { recursive: true, force: true });
474
+ await rm(stage, {
475
+ recursive: true,
476
+ force: true,
477
+ });
228
478
  }
229
479
  }