@penpair/marwes-ui 0.1.3 → 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.d.ts +3 -0
- package/dist/build-kit.js +93 -0
- package/dist/commands.d.ts +4 -0
- package/dist/commands.js +633 -92
- package/dist/components.d.ts +95 -0
- package/dist/components.js +497 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.js +80 -7
- package/dist/index.d.ts +13 -0
- package/dist/index.js +30 -1
- package/dist/marwes.js +45 -12
- package/dist/mode-trial.d.ts +95 -0
- package/dist/mode-trial.js +479 -0
- package/dist/ownership.d.ts +26 -0
- package/dist/ownership.js +222 -0
- package/dist/penpot.d.ts +21 -3
- package/dist/penpot.js +519 -87
- package/dist/reconciliation.d.ts +6 -0
- package/dist/reconciliation.js +179 -0
- package/dist/resolved-values.d.ts +11 -0
- package/dist/resolved-values.js +124 -0
- package/dist/snapshot-file.d.ts +45 -0
- package/dist/snapshot-file.js +282 -40
- package/dist/theme-input.js +18 -7
- package/dist/theme-lint.d.ts +31 -0
- package/dist/theme-lint.js +145 -0
- package/dist/tokens.d.ts +6 -2
- package/dist/tokens.js +221 -45
- package/package.json +2 -2
|
@@ -0,0 +1,479 @@
|
|
|
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";
|
|
3
|
+
import { mkdir, rename, rm } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { canonical } from "./tokens.js";
|
|
6
|
+
import { compareResolved, expectedResolved } from "./resolved-values.js";
|
|
7
|
+
const readBody = `
|
|
8
|
+
const f=penpot.currentFile,c=penpot.library.local.tokens;
|
|
9
|
+
if(!Array.isArray(c.sets)||!Array.isArray(c.themes))throw new Error("Token API unavailable");
|
|
10
|
+
if(c.sets.reduce((n,s)=>n+s.tokens.length,0)>5000)throw new Error("Mode trial exceeds 5000 tokens");
|
|
11
|
+
const source={sets:c.sets.map(s=>({id:s.id,name:s.name,tokens:s.tokens.map(t=>({name:t.name,type:t.type,value:t.value,description:t.description||""}))})),themes:c.themes.map(t=>({id:t.id,group:t.group,name:t.name,sets:(t.activeSets||[]).map(s=>typeof s==="string"?s:s.name)}))};
|
|
12
|
+
if(JSON.stringify(source).length>500000)throw new Error("Mode source exceeds bounded size");
|
|
13
|
+
const selection={sets:c.sets.map(s=>({id:s.id,active:!!s.active})),themes:c.themes.map(t=>({id:t.id,active:!!t.active}))};
|
|
14
|
+
let supported=c.sets.every(set=>typeof set.toggleActive==="function")&&c.themes.every(theme=>typeof theme.toggleActive==="function");const resolved=[];
|
|
15
|
+
for(const set of c.sets)for(const token of set.tokens){if(!("resolvedValue" in token))supported=false;if(!set.active)continue;let value=null,unknown;try{const raw=token.resolvedValue;if(raw===undefined)unknown="API resolvedValue is unavailable";else value=JSON.parse(JSON.stringify(raw));}catch{unknown="Unreadable resolvedValue";}resolved.push({name:token.name,type:token.type,set:set.name,value,...unknown?{unknown}:{}});}
|
|
16
|
+
if(JSON.stringify(resolved).length>500000)throw new Error("Resolved output exceeds bounded size");
|
|
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
|
+
async function readMode(client, timeoutMs = 10000) {
|
|
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)) {
|
|
30
|
+
throw new Error("Incomplete mode observation");
|
|
31
|
+
}
|
|
32
|
+
const state = result;
|
|
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) {
|
|
38
|
+
throw new Error("Stable token-set and theme IDs are required for mode trials");
|
|
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) {
|
|
45
|
+
throw new Error("Ambiguous mode identity");
|
|
46
|
+
}
|
|
47
|
+
state.sourceHash = observationHash(state.source);
|
|
48
|
+
return state;
|
|
49
|
+
}
|
|
50
|
+
function desiredSelection(state, group, name) {
|
|
51
|
+
const theme = state.source.themes.find((theme) => theme.group === group && theme.name === name);
|
|
52
|
+
if (!theme) {
|
|
53
|
+
throw new Error(`Missing mode ${group}/${name}`);
|
|
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
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function changeSelection(state, desired) {
|
|
67
|
+
return snippet("modes-select", `
|
|
68
|
+
const observed=await(async()=>{${readBody}})();
|
|
69
|
+
if(JSON.stringify(observed.source)!==CFG.source||JSON.stringify(observed.selection)!==CFG.before)throw new Error("Mode source or selection changed before mutation");
|
|
70
|
+
const c=penpot.library.local.tokens;
|
|
71
|
+
// Direct set changes disable themes; establish flags in a documented order.
|
|
72
|
+
for(const theme of c.themes)if(theme.active)theme.toggleActive();
|
|
73
|
+
for(const set of c.sets)if(set.active)set.toggleActive();
|
|
74
|
+
for(const theme of c.themes)if(CFG.desired.themes.some(t=>t.id===theme.id&&t.active))theme.toggleActive();
|
|
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();}
|
|
76
|
+
return {ok:true};`, {
|
|
77
|
+
source: JSON.stringify(state.source),
|
|
78
|
+
before: JSON.stringify(state.selection),
|
|
79
|
+
desired,
|
|
80
|
+
}, "write");
|
|
81
|
+
}
|
|
82
|
+
function compareMode(snapshot, state, theme) {
|
|
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) => {
|
|
94
|
+
const observed = actual.get(token.name);
|
|
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
|
+
};
|
|
115
|
+
});
|
|
116
|
+
const orderMatches = canonical(activeSets) === canonical(expectedSets);
|
|
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
|
+
};
|
|
129
|
+
}
|
|
130
|
+
export async function runModeTrial(client, snapshot, options) {
|
|
131
|
+
const target = await readJournalTarget(client);
|
|
132
|
+
client = guardFile(client, {
|
|
133
|
+
id: target.fileId,
|
|
134
|
+
});
|
|
135
|
+
const initial = await readMode(client);
|
|
136
|
+
if (!initial.supported) {
|
|
137
|
+
throw new Error("resolvedValue API unavailable; no theme was changed");
|
|
138
|
+
}
|
|
139
|
+
if (snapshot.themes.length < 1 || snapshot.themes.length > 20) {
|
|
140
|
+
throw new Error("Mode trials require 1–20 declared themes");
|
|
141
|
+
}
|
|
142
|
+
let original = initial;
|
|
143
|
+
const runId = options.resume ?? randomUUID();
|
|
144
|
+
if (options.resume) {
|
|
145
|
+
const record = await readJournal(options.root, runId);
|
|
146
|
+
const payload = record.payload;
|
|
147
|
+
if (record.operation !== "tokens-modes" ||
|
|
148
|
+
!payload?.original ||
|
|
149
|
+
record.target.fileId !== target.fileId) {
|
|
150
|
+
throw new Error("No matching original mode state in this journal");
|
|
151
|
+
}
|
|
152
|
+
original = payload.original;
|
|
153
|
+
}
|
|
154
|
+
if (original.sourceHash !== initial.sourceHash) {
|
|
155
|
+
throw new Error("Token declarations changed since the trial began");
|
|
156
|
+
}
|
|
157
|
+
if (options.frame &&
|
|
158
|
+
(initial.pageId !== original.pageId || initial.pageName !== options.frame.page)) {
|
|
159
|
+
throw new Error("Open the requested frame page before inspecting a mode trial");
|
|
160
|
+
}
|
|
161
|
+
if (options.frame) {
|
|
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) {
|
|
170
|
+
throw new Error("Frame identity was not observed");
|
|
171
|
+
}
|
|
172
|
+
}
|
|
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) {
|
|
201
|
+
return base;
|
|
202
|
+
}
|
|
203
|
+
let report = base;
|
|
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
|
+
};
|
|
218
|
+
const stableMode = async () => {
|
|
219
|
+
let previous = await inspect();
|
|
220
|
+
const deadline = Date.now() + 5000;
|
|
221
|
+
for (let samples = 2; samples <= 20 && Date.now() < deadline; samples++) {
|
|
222
|
+
await (options.sleep ?? commandSleep)(100);
|
|
223
|
+
const current = await readMode(client, Math.max(1, deadline - Date.now()));
|
|
224
|
+
if (Date.now() > deadline) {
|
|
225
|
+
throw new Error("Resolved observation exceeded its deadline");
|
|
226
|
+
}
|
|
227
|
+
if (current.sourceHash !== previous.sourceHash ||
|
|
228
|
+
canonical(current.selection) !== canonical(previous.selection)) {
|
|
229
|
+
throw new Error("Source or selection changed while observing resolution");
|
|
230
|
+
}
|
|
231
|
+
if (canonical(current.resolved) === canonical(previous.resolved)) {
|
|
232
|
+
return {
|
|
233
|
+
state: current,
|
|
234
|
+
samples,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
previous = current;
|
|
238
|
+
}
|
|
239
|
+
throw new Error("Resolved values did not stabilize within the observation budget");
|
|
240
|
+
};
|
|
241
|
+
const sourceOkay = (state) => state.sourceHash === original.sourceHash;
|
|
242
|
+
const originalOkay = (state) => sourceOkay(state) && canonical(state.selection) === canonical(original.selection);
|
|
243
|
+
const select = async (state, desired) => {
|
|
244
|
+
if (!sourceOkay(state)) {
|
|
245
|
+
throw new Error("Token declarations changed during trial");
|
|
246
|
+
}
|
|
247
|
+
if (canonical(state.selection) === canonical(desired)) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const current = await readJournalTarget(client);
|
|
251
|
+
await runSnippet(guardRevision(client, current), changeSelection(state, desired), {
|
|
252
|
+
effect: "write",
|
|
253
|
+
timeoutMs: 10000,
|
|
254
|
+
});
|
|
255
|
+
};
|
|
256
|
+
const pendingImages = [];
|
|
257
|
+
try {
|
|
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"),
|
|
271
|
+
inspect,
|
|
272
|
+
apply: async () => {
|
|
273
|
+
report = {
|
|
274
|
+
...base,
|
|
275
|
+
phase: "measured",
|
|
276
|
+
journal: {
|
|
277
|
+
runId,
|
|
278
|
+
state: "running",
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
try {
|
|
282
|
+
for (let index = 0; index < snapshot.themes.length; index++) {
|
|
283
|
+
options.onStep?.(`measure ${snapshot.themes[index].group}/${snapshot.themes[index].name}`);
|
|
284
|
+
await select(await inspect(), selections[index]);
|
|
285
|
+
const evidence = await stableMode();
|
|
286
|
+
const observed = evidence.state;
|
|
287
|
+
if (!sourceOkay(observed) ||
|
|
288
|
+
canonical(observed.selection) !== canonical(selections[index])) {
|
|
289
|
+
throw new Error("Theme activation did not establish the isolated requested selection");
|
|
290
|
+
}
|
|
291
|
+
const mode = compareMode(snapshot, observed, snapshot.themes[index]);
|
|
292
|
+
mode.resolutionSamples = evidence.samples;
|
|
293
|
+
report.modes.push(mode);
|
|
294
|
+
if (options.frame) {
|
|
295
|
+
const image = await exportShapePng(client, options.frame.id, {
|
|
296
|
+
maxExports: 4,
|
|
297
|
+
attempts: 1,
|
|
298
|
+
sleep: options.sleep,
|
|
299
|
+
});
|
|
300
|
+
const after = await readMode(client);
|
|
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)) {
|
|
306
|
+
throw new Error("Mode changed during export");
|
|
307
|
+
}
|
|
308
|
+
const name = `${index + 1}.png`;
|
|
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
|
+
};
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
report.error = error instanceof Error ? error.message : String(error);
|
|
323
|
+
report.uncertain =
|
|
324
|
+
error.code === "write-uncertain";
|
|
325
|
+
}
|
|
326
|
+
finally {
|
|
327
|
+
try {
|
|
328
|
+
await withCleanupDeadline(async () => {
|
|
329
|
+
const current = await readMode(client);
|
|
330
|
+
await select(current, original.selection);
|
|
331
|
+
const restored = await inspect();
|
|
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
|
+
}
|
|
339
|
+
}, 10000);
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
report.restoration = {
|
|
343
|
+
verified: false,
|
|
344
|
+
error: error instanceof Error ? error.message : String(error),
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
}
|
|
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);
|
|
354
|
+
return report;
|
|
355
|
+
},
|
|
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
|
+
});
|
|
395
|
+
report = record.steps[0].result;
|
|
396
|
+
if (!report) {
|
|
397
|
+
throw new Error("Mode journal lacks measured results");
|
|
398
|
+
}
|
|
399
|
+
report.journal = {
|
|
400
|
+
runId,
|
|
401
|
+
state: record.state,
|
|
402
|
+
};
|
|
403
|
+
if (record.redacted) {
|
|
404
|
+
report.redacted = true;
|
|
405
|
+
report.complete = false;
|
|
406
|
+
report.verified = false;
|
|
407
|
+
report.error = "Journal required credential redaction; evidence is incomplete";
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
try {
|
|
412
|
+
const record = await readJournal(options.root, runId);
|
|
413
|
+
report = record.steps[0]?.result ?? report;
|
|
414
|
+
report.journal = {
|
|
415
|
+
runId,
|
|
416
|
+
state: record.state,
|
|
417
|
+
};
|
|
418
|
+
if (record.redacted) {
|
|
419
|
+
report.redacted = true;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
/* Preserve the actual failure. */
|
|
424
|
+
}
|
|
425
|
+
report.complete = false;
|
|
426
|
+
report.verified = false;
|
|
427
|
+
report.error ??= error instanceof Error ? error.message : String(error);
|
|
428
|
+
report.uncertain ||=
|
|
429
|
+
error.outcome === "uncertain";
|
|
430
|
+
}
|
|
431
|
+
if (options.frame && report.complete && pendingImages.length !== snapshot.themes.length) {
|
|
432
|
+
report.complete = false;
|
|
433
|
+
report.verified = false;
|
|
434
|
+
report.error =
|
|
435
|
+
"Recovered measurement lacks image bytes; start a fresh trial to publish exports";
|
|
436
|
+
}
|
|
437
|
+
if (options.frame && report.complete && pendingImages.length === snapshot.themes.length) {
|
|
438
|
+
await publishModes(options.root, options.frame.out, report, pendingImages, options.redact);
|
|
439
|
+
}
|
|
440
|
+
return report;
|
|
441
|
+
}
|
|
442
|
+
async function publishModes(root, out, report, images, redact) {
|
|
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
|
+
};
|
|
451
|
+
const serialized = JSON.stringify(report, null, 2) + "\n";
|
|
452
|
+
if (redact && redact(serialized) !== serialized) {
|
|
453
|
+
throw new Error("Mode evidence requires credential redaction; previous output preserved");
|
|
454
|
+
}
|
|
455
|
+
await mkdir(stage, {
|
|
456
|
+
recursive: true,
|
|
457
|
+
});
|
|
458
|
+
try {
|
|
459
|
+
for (const image of images) {
|
|
460
|
+
await atomicWriteFile(path.join(stage, image.name), image.bytes);
|
|
461
|
+
}
|
|
462
|
+
await atomicWriteFile(path.join(stage, "manifest.json"), serialized);
|
|
463
|
+
await rename(stage, final);
|
|
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
|
+
});
|
|
472
|
+
}
|
|
473
|
+
finally {
|
|
474
|
+
await rm(stage, {
|
|
475
|
+
recursive: true,
|
|
476
|
+
force: true,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Action, LiveTokens } from "./penpot.js";
|
|
2
|
+
import { type TokenSnapshot } from "./tokens.js";
|
|
3
|
+
export interface OwnershipChange {
|
|
4
|
+
set: string;
|
|
5
|
+
name: string;
|
|
6
|
+
property: string;
|
|
7
|
+
kind: "upstream" | "designer" | "conflict" | "untracked";
|
|
8
|
+
ancestor: unknown;
|
|
9
|
+
desired: unknown;
|
|
10
|
+
live: unknown;
|
|
11
|
+
}
|
|
12
|
+
export interface OwnershipReport {
|
|
13
|
+
changes: OwnershipChange[];
|
|
14
|
+
blockers: OwnershipChange[];
|
|
15
|
+
unmanagedSets: string[];
|
|
16
|
+
unmanagedTokens: {
|
|
17
|
+
set: string;
|
|
18
|
+
name: string;
|
|
19
|
+
}[];
|
|
20
|
+
}
|
|
21
|
+
/** Report each changed property against a verified ancestor; never guess a rename. */
|
|
22
|
+
export declare function ownershipReport(desired: TokenSnapshot, live: LiveTokens, ancestor?: TokenSnapshot): OwnershipReport;
|
|
23
|
+
/** Destruction is conservative across all sets, including those outside adapter ownership. */
|
|
24
|
+
export declare function assertSafeDestruction(actions: Action[], live: LiveTokens, bindingCounts?: Record<string, number>): void;
|
|
25
|
+
export declare function readSyncAncestor(root: string, fileId: string): Promise<TokenSnapshot | undefined>;
|
|
26
|
+
export declare function saveSyncAncestor(root: string, fileId: string, snapshot: TokenSnapshot, expectedAncestor?: TokenSnapshot): Promise<void>;
|