@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.
- package/dist/build-kit.js +24 -6
- package/dist/commands.d.ts +4 -0
- package/dist/commands.js +500 -94
- package/dist/components.js +375 -74
- package/dist/config.js +53 -16
- package/dist/index.js +21 -3
- package/dist/marwes.js +45 -12
- package/dist/mode-trial.js +325 -75
- package/dist/ownership.js +162 -37
- package/dist/penpot.js +447 -114
- package/dist/reconciliation.js +112 -37
- package/dist/resolved-values.d.ts +3 -8
- package/dist/resolved-values.js +99 -103
- package/dist/snapshot-file.js +255 -50
- package/dist/theme-input.js +12 -4
- package/dist/theme-lint.js +113 -27
- package/dist/tokens.d.ts +1 -1
- package/dist/tokens.js +207 -52
- package/package.json +2 -2
package/dist/mode-trial.js
CHANGED
|
@@ -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"), {
|
|
20
|
-
|
|
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 = [
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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};`, {
|
|
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
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const
|
|
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
|
|
56
|
-
|
|
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 {
|
|
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, {
|
|
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
|
-
|
|
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" ||
|
|
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
|
-
|
|
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};", {
|
|
84
|
-
|
|
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 = {
|
|
89
|
-
|
|
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 () => {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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), {
|
|
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([
|
|
127
|
-
|
|
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 = {
|
|
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()
|
|
136
|
-
|
|
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, {
|
|
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 ||
|
|
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({
|
|
148
|
-
|
|
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 =
|
|
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 = {
|
|
163
|
-
|
|
164
|
-
|
|
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 = {
|
|
342
|
+
report.restoration = {
|
|
343
|
+
verified: false,
|
|
344
|
+
error: error instanceof Error ? error.message : String(error),
|
|
345
|
+
};
|
|
169
346
|
}
|
|
170
347
|
}
|
|
171
|
-
report.complete =
|
|
172
|
-
|
|
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) &&
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
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 = {
|
|
194
|
-
|
|
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 ||=
|
|
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 =
|
|
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)
|
|
214
|
-
|
|
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
|
-
|
|
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({
|
|
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, {
|
|
474
|
+
await rm(stage, {
|
|
475
|
+
recursive: true,
|
|
476
|
+
force: true,
|
|
477
|
+
});
|
|
228
478
|
}
|
|
229
479
|
}
|