@penpair/core 0.2.0 → 0.3.1
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/adapters.js +58 -11
- package/dist/approvals.js +143 -29
- package/dist/atomic-output.js +9 -4
- package/dist/audit-rules.js +345 -79
- package/dist/backup-note.js +29 -8
- package/dist/baseline-coverage.js +169 -35
- package/dist/baselines-config.js +119 -29
- package/dist/baselines.js +280 -57
- package/dist/cli-arguments.js +18 -6
- package/dist/cli-main.js +1645 -392
- package/dist/client.d.ts +5 -2
- package/dist/client.js +127 -28
- package/dist/command-result.js +341 -52
- package/dist/coordination.js +91 -29
- package/dist/credentials.js +57 -13
- package/dist/deep-check.js +56 -12
- package/dist/design-spec.js +189 -49
- package/dist/endpoint.js +102 -34
- package/dist/export.js +86 -18
- package/dist/file-archive.js +33 -7
- package/dist/file-guard.js +29 -11
- package/dist/formats.js +215 -14
- package/dist/index.d.ts +8 -7
- package/dist/index.js +8 -7
- package/dist/inspection.d.ts +1 -1
- package/dist/inspection.js +378 -65
- package/dist/instance/config-schema.js +400 -41
- package/dist/instance/config.js +101 -30
- package/dist/instance/connect.js +151 -44
- package/dist/instance/connection.d.ts +26 -5
- package/dist/instance/connection.js +194 -21
- package/dist/instance/gitignore.js +5 -2
- package/dist/instance/init.js +32 -7
- package/dist/instance/runner.js +32 -10
- package/dist/instance/setup.js +46 -13
- package/dist/instance/tty.js +26 -8
- package/dist/journal.d.ts +10 -0
- package/dist/journal.js +212 -49
- package/dist/kit-smoke.js +82 -20
- package/dist/kit-source.js +10 -40
- package/dist/kit.js +102 -34
- package/dist/layout-policy.d.ts +2 -0
- package/dist/layout-policy.js +86 -0
- package/dist/layout.d.ts +7 -1
- package/dist/layout.js +27 -70
- package/dist/live-smoke.js +13 -4
- package/dist/local-profile.js +2 -1
- package/dist/mcp-server.js +360 -47
- package/dist/native-api.js +47 -11
- package/dist/onboard.js +33 -7
- package/dist/operation.d.ts +2 -2
- package/dist/operation.js +279 -69
- package/dist/page-session.d.ts +23 -0
- package/dist/page-session.js +142 -0
- package/dist/png.js +18 -3
- package/dist/portable-tokens.js +254 -57
- package/dist/probe.d.ts +1 -1
- package/dist/probe.js +48 -11
- package/dist/project-input.js +24 -10
- package/dist/project-snippets.js +18 -6
- package/dist/project-status.js +133 -24
- package/dist/registry.js +217 -60
- package/dist/run-file.js +129 -30
- package/dist/shots.js +108 -26
- package/dist/skill-check-entry.d.ts +17 -0
- package/dist/skill-check-entry.js +89 -0
- package/dist/skill-check-instructions.d.ts +7 -0
- package/dist/skill-check-instructions.js +39 -0
- package/dist/skill-check.d.ts +15 -0
- package/dist/skill-check.js +36 -0
- package/dist/skill-file.d.ts +2 -0
- package/dist/skill-file.js +31 -0
- package/dist/skill-instructions.d.ts +4 -0
- package/dist/skill-instructions.js +97 -22
- package/dist/skill-metadata.d.ts +8 -0
- package/dist/skill-metadata.js +52 -0
- package/dist/skill-routing.d.ts +2 -0
- package/dist/skill-routing.js +36 -0
- package/dist/skills.js +131 -41
- package/dist/snippet.js +27 -7
- package/dist/snippets/read.js +8 -2
- package/dist/structure-snapshot.d.ts +12 -0
- package/dist/structure-snapshot.js +439 -85
- package/dist/token-edit.js +219 -64
- package/dist/token-projection.d.ts +56 -0
- package/dist/token-projection.js +286 -0
- package/dist/tokens-diff.js +125 -32
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -2
- package/skills/design-judgment/SKILL.md +1 -1
- package/skills/design-judgment/agents/openai.yaml +6 -0
- package/skills/penpair-design/SKILL.md +14 -1
- package/skills/penpair-design/agents/openai.yaml +6 -0
- package/skills/penpair-design/references/penpot-plugin-gotchas.md +2 -2
- package/skills/penpair-design/references/project-facts.md +2 -0
- package/skills/penpair-handoff/SKILL.md +14 -1
- package/skills/penpair-handoff/agents/openai.yaml +6 -0
- package/skills/penpair-handoff/references/comparison-companion.md +16 -0
- package/skills/penpair-handoff/references/handoff-flow.md +2 -0
- package/skills/penpair-recipes/SKILL.md +13 -1
- package/skills/penpair-recipes/agents/openai.yaml +6 -0
- package/skills/penpair-recipes/assets/visual-checkpoint.md +48 -0
- package/skills/penpair-recipes/references/visual-checkpoint.md +37 -0
- package/skills/penpair-setup/SKILL.md +9 -3
- package/skills/penpair-setup/agents/openai.yaml +6 -0
- package/skills/penpair-verify/SKILL.md +49 -0
- package/skills/penpair-verify/agents/openai.yaml +6 -0
- package/skills/penpair-verify/references/browser-evidence.md +33 -0
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +0 -3
- package/dist/docker/lifecycle.d.ts +0 -96
- package/dist/docker/lifecycle.js +0 -273
- package/dist/docker/secrets.d.ts +0 -11
- package/dist/docker/secrets.js +0 -38
- package/dist/docker/template.d.ts +0 -8
- package/dist/docker/template.js +0 -158
- package/dist/docker-profile.d.ts +0 -114
- package/dist/docker-profile.js +0 -18
- package/dist/instance/lifecycle.d.ts +0 -129
- package/dist/instance/lifecycle.js +0 -336
- package/dist/instance/secrets.d.ts +0 -11
- package/dist/instance/secrets.js +0 -38
- package/dist/instance/template.d.ts +0 -8
- package/dist/instance/template.js +0 -158
package/dist/baselines.js
CHANGED
|
@@ -20,8 +20,9 @@ async function readIfExists(file) {
|
|
|
20
20
|
return await readFile(file);
|
|
21
21
|
}
|
|
22
22
|
catch (error) {
|
|
23
|
-
if (error.code === "ENOENT")
|
|
23
|
+
if (error.code === "ENOENT") {
|
|
24
24
|
return undefined;
|
|
25
|
+
}
|
|
25
26
|
throw error;
|
|
26
27
|
}
|
|
27
28
|
}
|
|
@@ -31,22 +32,43 @@ async function readIfExists(file) {
|
|
|
31
32
|
* the design never silently becomes the new reference.
|
|
32
33
|
*/
|
|
33
34
|
export async function exportBaselines(client, projectRoot, config, options = {}) {
|
|
34
|
-
if (options.forceReason !== undefined &&
|
|
35
|
+
if (options.forceReason !== undefined &&
|
|
36
|
+
(!options.update ||
|
|
37
|
+
!options.apply ||
|
|
38
|
+
!options.forceReason.trim() ||
|
|
39
|
+
options.forceReason.length > 2000)) {
|
|
35
40
|
throw new Error("A nonempty force reason requires apply and update");
|
|
41
|
+
}
|
|
36
42
|
config = structuredClone(config);
|
|
37
|
-
validateBaselines(config, message => {
|
|
38
|
-
|
|
39
|
-
|
|
43
|
+
validateBaselines(config, (message) => {
|
|
44
|
+
throw new Error(message);
|
|
45
|
+
});
|
|
46
|
+
const listing = await runOnPage(client, frames({
|
|
47
|
+
page: config.page,
|
|
48
|
+
}), config.page, options);
|
|
49
|
+
if (listing.ok !== true ||
|
|
50
|
+
typeof listing.fileId !== "string" ||
|
|
51
|
+
!listing.fileId ||
|
|
52
|
+
typeof listing.pageId !== "string" ||
|
|
53
|
+
!listing.pageId ||
|
|
54
|
+
!Number.isInteger(listing.revn) ||
|
|
55
|
+
!Array.isArray(listing.frames)) {
|
|
40
56
|
throw new Error("Baseline listing lacks file/page/revision evidence");
|
|
57
|
+
}
|
|
41
58
|
const found = listing.frames;
|
|
42
59
|
const results = [];
|
|
43
60
|
const wrote = [];
|
|
44
61
|
const folder = await resolveProjectOutput(projectRoot, config.root);
|
|
45
62
|
const manifestPath = await resolveProjectOutput(projectRoot, path.join(config.root, "penpair-baselines.json"));
|
|
46
63
|
const previousBytes = await readIfExists(manifestPath);
|
|
47
|
-
const previous = previousBytes
|
|
48
|
-
|
|
64
|
+
const previous = previousBytes
|
|
65
|
+
? JSON.parse(previousBytes.toString("utf8"))
|
|
66
|
+
: undefined;
|
|
67
|
+
if (previous &&
|
|
68
|
+
((previous.schemaVersion !== undefined && previous.schemaVersion !== 2) ||
|
|
69
|
+
!Array.isArray(previous.frames))) {
|
|
49
70
|
throw new Error("Existing baseline manifest is invalid; preserve and inspect it");
|
|
71
|
+
}
|
|
50
72
|
const runId = randomUUID();
|
|
51
73
|
const generation = `captures/${runId}`;
|
|
52
74
|
const captures = new Map();
|
|
@@ -54,79 +76,213 @@ export async function exportBaselines(client, projectRoot, config, options = {})
|
|
|
54
76
|
options.signal?.throwIfAborted();
|
|
55
77
|
const old = previous?.frames?.find((entry) => entry.id === wanted.id);
|
|
56
78
|
const file = old?.file ?? `${wanted.id}.png`;
|
|
57
|
-
if (path.isAbsolute(file) || file.split(/[\\/]/).includes(".."))
|
|
79
|
+
if (path.isAbsolute(file) || file.split(/[\\/]/).includes("..")) {
|
|
58
80
|
throw new Error("Existing baseline path leaves its folder");
|
|
81
|
+
}
|
|
59
82
|
const matches = found.filter((f) => f.name === wanted.frame);
|
|
60
83
|
if (!matches.length) {
|
|
61
|
-
results.push({
|
|
84
|
+
results.push({
|
|
85
|
+
...(wanted.case
|
|
86
|
+
? {
|
|
87
|
+
case: wanted.case,
|
|
88
|
+
}
|
|
89
|
+
: {}),
|
|
90
|
+
id: wanted.id,
|
|
91
|
+
frame: wanted.frame,
|
|
92
|
+
file,
|
|
93
|
+
status: "missing",
|
|
94
|
+
written: false,
|
|
95
|
+
});
|
|
62
96
|
continue;
|
|
63
97
|
}
|
|
64
|
-
if (matches.length > 1)
|
|
98
|
+
if (matches.length > 1) {
|
|
65
99
|
throw new Error(`more than one top-level frame on ${config.page} is named ${JSON.stringify(wanted.frame)}; rename one so the design reference is unambiguous`);
|
|
100
|
+
}
|
|
66
101
|
const frame = matches[0];
|
|
67
102
|
options.onStep?.(`exporting ${wanted.frame}`);
|
|
68
|
-
const exported = await exportShapePng(client, frame.id, {
|
|
69
|
-
|
|
70
|
-
|
|
103
|
+
const exported = await exportShapePng(client, frame.id, {
|
|
104
|
+
...options,
|
|
105
|
+
page: config.page,
|
|
106
|
+
stable: true,
|
|
107
|
+
});
|
|
108
|
+
const base = {
|
|
109
|
+
...(wanted.case
|
|
110
|
+
? {
|
|
111
|
+
case: wanted.case,
|
|
112
|
+
}
|
|
113
|
+
: {}),
|
|
114
|
+
id: wanted.id,
|
|
115
|
+
frame: wanted.frame,
|
|
116
|
+
frameId: frame.id,
|
|
117
|
+
width: frame.width,
|
|
118
|
+
height: frame.height,
|
|
119
|
+
file,
|
|
120
|
+
};
|
|
121
|
+
if (exported.capture.fileId !== listing.fileId ||
|
|
122
|
+
exported.capture.pageId !== listing.pageId ||
|
|
123
|
+
exported.capture.revision !== listing.revn) {
|
|
71
124
|
throw new Error("Baseline source changed during capture; nothing was published");
|
|
125
|
+
}
|
|
72
126
|
if (!exported.stable) {
|
|
73
|
-
results.push({
|
|
127
|
+
results.push({
|
|
128
|
+
...base,
|
|
129
|
+
status: "unstable",
|
|
130
|
+
written: false,
|
|
131
|
+
});
|
|
74
132
|
continue;
|
|
75
133
|
}
|
|
76
|
-
if (!pngSize(exported.png))
|
|
134
|
+
if (!pngSize(exported.png)) {
|
|
77
135
|
throw new Error("Baseline export is not a PNG with dimensions");
|
|
136
|
+
}
|
|
78
137
|
const existing = await readIfExists(await resolveProjectOutput(projectRoot, path.join(config.root, file)));
|
|
79
|
-
if (old &&
|
|
138
|
+
if (old &&
|
|
139
|
+
(!existing ||
|
|
140
|
+
old.sha256 !== sha(existing) ||
|
|
141
|
+
(old.approved && old.approved.sha256 !== old.sha256))) {
|
|
80
142
|
throw new Error("Approved baseline bytes no longer match their manifest; inspect before replacing");
|
|
81
|
-
|
|
143
|
+
}
|
|
144
|
+
const captured = {
|
|
145
|
+
...(wanted.case
|
|
146
|
+
? {
|
|
147
|
+
case: wanted.case,
|
|
148
|
+
}
|
|
149
|
+
: {}),
|
|
150
|
+
layout: exported.layout,
|
|
151
|
+
...exported.capture,
|
|
152
|
+
sha256: sha(exported.png),
|
|
153
|
+
capturedAt: new Date().toISOString(),
|
|
154
|
+
toolVersion: version,
|
|
155
|
+
file: `${generation}/${wanted.id}.png`,
|
|
156
|
+
};
|
|
82
157
|
const caseProblems = [];
|
|
83
158
|
if (wanted.case) {
|
|
84
|
-
const viewport = wanted.case.viewport
|
|
85
|
-
|
|
159
|
+
const viewport = wanted.case.viewport;
|
|
160
|
+
const size = pngSize(exported.png);
|
|
161
|
+
if (!exported.capture.designSize ||
|
|
162
|
+
exported.capture.designSize.width !== viewport.width ||
|
|
163
|
+
exported.capture.designSize.height !== viewport.height) {
|
|
86
164
|
caseProblems.push("Observed frame dimensions do not match the declared viewport");
|
|
87
|
-
|
|
165
|
+
}
|
|
166
|
+
if (size.width !== Math.round(viewport.width * exported.capture.scale) ||
|
|
167
|
+
size.height !== Math.round(viewport.height * exported.capture.scale)) {
|
|
88
168
|
caseProblems.push("PNG dimensions do not match declared viewport and capture scale");
|
|
89
|
-
|
|
169
|
+
}
|
|
170
|
+
if (wanted.case.activeThemeIds &&
|
|
171
|
+
(!exported.capture.activeThemeIds ||
|
|
172
|
+
observationHash([
|
|
173
|
+
...wanted.case.activeThemeIds,
|
|
174
|
+
].sort()) !==
|
|
175
|
+
observationHash([
|
|
176
|
+
...exported.capture.activeThemeIds,
|
|
177
|
+
].sort()))) {
|
|
90
178
|
caseProblems.push("Active native theme IDs do not match the capture assertion");
|
|
179
|
+
}
|
|
91
180
|
}
|
|
92
181
|
if (caseProblems.length) {
|
|
93
|
-
results.push({
|
|
182
|
+
results.push({
|
|
183
|
+
...base,
|
|
184
|
+
status: "case-mismatch",
|
|
185
|
+
written: false,
|
|
186
|
+
captured,
|
|
187
|
+
approved: old?.approved,
|
|
188
|
+
sha256: old?.sha256,
|
|
189
|
+
problems: caseProblems,
|
|
190
|
+
});
|
|
94
191
|
continue;
|
|
95
192
|
}
|
|
96
193
|
const oldCase = old?.approved && "case" in old.approved ? old.approved.case : undefined;
|
|
97
|
-
const status = !existing
|
|
98
|
-
|
|
194
|
+
const status = !existing
|
|
195
|
+
? "new"
|
|
196
|
+
: sha(existing) === captured.sha256 &&
|
|
197
|
+
observationHash(oldCase ?? null) === observationHash(wanted.case ?? null) &&
|
|
198
|
+
(!old?.approved ||
|
|
199
|
+
"legacy" in old.approved ||
|
|
200
|
+
(old.approved.fileId === captured.fileId &&
|
|
201
|
+
old.approved.pageId === captured.pageId &&
|
|
202
|
+
old.approved.frameId === captured.frameId))
|
|
203
|
+
? "same"
|
|
204
|
+
: "changed";
|
|
205
|
+
const promoteWaiver = status === "same" &&
|
|
206
|
+
old?.approved &&
|
|
207
|
+
"approval" in old.approved &&
|
|
208
|
+
old.approved.approval?.kind === "forced-reference" &&
|
|
209
|
+
options.update === true;
|
|
99
210
|
if ((status === "changed" || promoteWaiver) && options.update && options.apply) {
|
|
100
211
|
const record = await matchingApproval(projectRoot, exported.capture, captured.sha256, wanted.case);
|
|
101
|
-
if (record)
|
|
102
|
-
captured.approval = {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
212
|
+
if (record) {
|
|
213
|
+
captured.approval = {
|
|
214
|
+
kind: "owner-words",
|
|
215
|
+
id: record.id,
|
|
216
|
+
recordHash: record.hash,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
else if (options.forceReason) {
|
|
220
|
+
captured.approval = {
|
|
221
|
+
kind: "forced-reference",
|
|
222
|
+
reason: options.forceReason,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
106
226
|
throw new Error("Changed baseline requires matching exact-artifact owner words (penpair approve), or an explicit --force-reason waiver; previous artifacts preserved");
|
|
227
|
+
}
|
|
107
228
|
}
|
|
108
|
-
const approve = status === "new" ||
|
|
109
|
-
|
|
229
|
+
const approve = status === "new" ||
|
|
230
|
+
(status === "changed" && options.update === true) ||
|
|
231
|
+
Boolean(promoteWaiver);
|
|
232
|
+
const approved = approve
|
|
233
|
+
? captured
|
|
234
|
+
: (old?.approved ?? {
|
|
235
|
+
legacy: true,
|
|
236
|
+
sha256: sha(existing),
|
|
237
|
+
});
|
|
110
238
|
const image = approve ? exported.png : existing;
|
|
111
239
|
const size = pngSize(image);
|
|
112
|
-
results.push({
|
|
113
|
-
|
|
240
|
+
results.push({
|
|
241
|
+
...base,
|
|
242
|
+
file: approve && options.apply ? captured.file : file,
|
|
243
|
+
status,
|
|
244
|
+
written: false,
|
|
245
|
+
captured,
|
|
246
|
+
approved: options.apply || !approve ? approved : undefined,
|
|
247
|
+
sha256: sha(image),
|
|
248
|
+
bytes: image.length,
|
|
249
|
+
imageWidth: size?.width,
|
|
250
|
+
imageHeight: size?.height,
|
|
251
|
+
});
|
|
114
252
|
captures.set(wanted.id, exported.png);
|
|
115
253
|
}
|
|
116
|
-
const after = await runOnPage(client, frames({
|
|
117
|
-
|
|
118
|
-
|
|
254
|
+
const after = await runOnPage(client, frames({
|
|
255
|
+
page: config.page,
|
|
256
|
+
}), config.page, options);
|
|
257
|
+
const source = (value) => ({
|
|
258
|
+
fileId: value.fileId,
|
|
259
|
+
pageId: value.pageId,
|
|
260
|
+
revision: value.revn,
|
|
261
|
+
frames: value.frames,
|
|
262
|
+
});
|
|
263
|
+
if (observationHash(source(listing)) !== observationHash(source(after))) {
|
|
119
264
|
throw new Error("Baseline source changed before publication; previous artifacts were preserved");
|
|
120
|
-
|
|
265
|
+
}
|
|
266
|
+
const complete = results.length === config.frames.length &&
|
|
267
|
+
results.every((result) => result.status !== "missing" &&
|
|
268
|
+
result.status !== "unstable" &&
|
|
269
|
+
result.status !== "case-mismatch");
|
|
121
270
|
if (options.apply && complete) {
|
|
122
|
-
const evidence = JSON.stringify({
|
|
123
|
-
|
|
271
|
+
const evidence = JSON.stringify({
|
|
272
|
+
config,
|
|
273
|
+
results,
|
|
274
|
+
forceReason: options.forceReason,
|
|
275
|
+
});
|
|
276
|
+
if (options.redact && options.redact(evidence) !== evidence) {
|
|
124
277
|
throw new Error("Baseline evidence requires credential redaction; previous artifacts preserved");
|
|
278
|
+
}
|
|
125
279
|
options.signal?.throwIfAborted();
|
|
126
280
|
options.onStep?.("publishing complete capture");
|
|
127
281
|
options.signal?.throwIfAborted();
|
|
128
282
|
const captureFolder = await resolveProjectOutput(projectRoot, path.join(config.root, "captures"));
|
|
129
|
-
await mkdir(captureFolder, {
|
|
283
|
+
await mkdir(captureFolder, {
|
|
284
|
+
recursive: true,
|
|
285
|
+
});
|
|
130
286
|
const staged = path.join(folder, `.stage-${runId}`);
|
|
131
287
|
await mkdir(staged);
|
|
132
288
|
try {
|
|
@@ -136,29 +292,72 @@ export async function exportBaselines(client, projectRoot, config, options = {})
|
|
|
136
292
|
options.onStep?.(`staging ${id}`);
|
|
137
293
|
}
|
|
138
294
|
const cases = reflectionCases(config, results);
|
|
139
|
-
await writeFile(path.join(staged, "reflection-cases.json"), `${JSON.stringify({
|
|
295
|
+
await writeFile(path.join(staged, "reflection-cases.json"), `${JSON.stringify({
|
|
296
|
+
...cases,
|
|
297
|
+
captureId: runId,
|
|
298
|
+
}, null, 2)}\n`, {
|
|
299
|
+
flag: "wx",
|
|
300
|
+
});
|
|
140
301
|
options.signal?.throwIfAborted();
|
|
141
302
|
await rename(staged, path.join(captureFolder, runId));
|
|
142
303
|
// Immutable generation first. The canonical manifest is the commit point and is published last.
|
|
143
|
-
await atomicWriteFile(path.join(folder, "reflection-cases.json"), `${JSON.stringify({
|
|
304
|
+
await atomicWriteFile(path.join(folder, "reflection-cases.json"), `${JSON.stringify({
|
|
305
|
+
...cases,
|
|
306
|
+
captureId: runId,
|
|
307
|
+
}, null, 2)}\n`, {
|
|
308
|
+
replace: true,
|
|
309
|
+
});
|
|
144
310
|
options.signal?.throwIfAborted();
|
|
145
|
-
for (const result of results)
|
|
146
|
-
result.written = result.file === result.captured?.file && (result.status === "new" || options.update === true);
|
|
147
|
-
const manifest = { schemaVersion: 2, complete: true, captureId: runId, penpotFile: listing.file, fileId: listing.fileId, page: config.page, pageId: listing.pageId,
|
|
148
|
-
coverage: baselineCoverage(config, results), capturedRevision: listing.revn, casesFile: `${generation}/reflection-cases.json`, note: "Capture provenance and approved provenance are separate. Changed references require exact-artifact owner words or a recorded forced-reference waiver.", frames: results };
|
|
149
|
-
await atomicWriteFile(path.join(folder, "penpair-baselines.json"), `${JSON.stringify(manifest, null, 2)}\n`, { replace: true });
|
|
150
311
|
for (const result of results) {
|
|
151
|
-
result.written =
|
|
152
|
-
|
|
312
|
+
result.written =
|
|
313
|
+
result.file === result.captured?.file &&
|
|
314
|
+
(result.status === "new" || options.update === true);
|
|
315
|
+
}
|
|
316
|
+
const manifest = {
|
|
317
|
+
schemaVersion: 2,
|
|
318
|
+
complete: true,
|
|
319
|
+
captureId: runId,
|
|
320
|
+
penpotFile: listing.file,
|
|
321
|
+
fileId: listing.fileId,
|
|
322
|
+
page: config.page,
|
|
323
|
+
pageId: listing.pageId,
|
|
324
|
+
coverage: baselineCoverage(config, results),
|
|
325
|
+
capturedRevision: listing.revn,
|
|
326
|
+
casesFile: `${generation}/reflection-cases.json`,
|
|
327
|
+
note: "Capture provenance and approved provenance are separate. Changed references require exact-artifact owner words or a recorded forced-reference waiver.",
|
|
328
|
+
frames: results,
|
|
329
|
+
};
|
|
330
|
+
await atomicWriteFile(path.join(folder, "penpair-baselines.json"), `${JSON.stringify(manifest, null, 2)}\n`, {
|
|
331
|
+
replace: true,
|
|
332
|
+
});
|
|
333
|
+
for (const result of results) {
|
|
334
|
+
result.written =
|
|
335
|
+
result.file === result.captured?.file &&
|
|
336
|
+
(result.status === "new" || options.update === true);
|
|
337
|
+
if (result.written) {
|
|
153
338
|
wrote.push(path.join(config.root, result.file));
|
|
339
|
+
}
|
|
154
340
|
}
|
|
155
341
|
wrote.push(path.join(config.root, "reflection-cases.json"), path.join(config.root, "penpair-baselines.json"));
|
|
156
342
|
}
|
|
157
343
|
finally {
|
|
158
|
-
await rm(staged, {
|
|
344
|
+
await rm(staged, {
|
|
345
|
+
recursive: true,
|
|
346
|
+
force: true,
|
|
347
|
+
});
|
|
159
348
|
}
|
|
160
349
|
}
|
|
161
|
-
return {
|
|
350
|
+
return {
|
|
351
|
+
coverage: baselineCoverage(config, results),
|
|
352
|
+
complete,
|
|
353
|
+
fileId: listing.fileId,
|
|
354
|
+
pageId: listing.pageId,
|
|
355
|
+
page: config.page,
|
|
356
|
+
penpotFile: String(listing.file),
|
|
357
|
+
revn: Number(listing.revn),
|
|
358
|
+
results,
|
|
359
|
+
wrote,
|
|
360
|
+
};
|
|
162
361
|
}
|
|
163
362
|
/**
|
|
164
363
|
* Visual cases for Reflection, one per frame that names a route: the image's exact size is the viewport (so the capture and the
|
|
@@ -171,24 +370,48 @@ export function reflectionCases(config, results) {
|
|
|
171
370
|
const pendingCases = [];
|
|
172
371
|
for (const wanted of config.frames) {
|
|
173
372
|
const result = results.find((r) => r.id === wanted.id);
|
|
174
|
-
if (!wanted.route ||
|
|
373
|
+
if (!wanted.route ||
|
|
374
|
+
!result ||
|
|
375
|
+
result.imageWidth === undefined ||
|
|
376
|
+
result.imageHeight === undefined) {
|
|
175
377
|
continue;
|
|
378
|
+
}
|
|
176
379
|
if (wanted.case) {
|
|
177
380
|
const approvedCase = result.approved && "case" in result.approved ? result.approved.case : undefined;
|
|
178
|
-
pendingCases.push({
|
|
381
|
+
pendingCases.push({
|
|
382
|
+
id: wanted.id,
|
|
383
|
+
route: wanted.route,
|
|
384
|
+
case: approvedCase ?? wanted.case,
|
|
385
|
+
baseline: result.file,
|
|
386
|
+
ref: `penpair:${wanted.id}`,
|
|
387
|
+
approvedProvenance: result.approved ?? null,
|
|
388
|
+
captured: result.captured ?? null,
|
|
389
|
+
approvedMetadata: Boolean(approvedCase && observationHash(approvedCase) === observationHash(wanted.case)),
|
|
390
|
+
reason: "Explicit theme/state/fixture/font setup requires an implementation provider mapping",
|
|
391
|
+
browserVerification: "not-run",
|
|
392
|
+
});
|
|
179
393
|
continue;
|
|
180
394
|
}
|
|
181
|
-
const viewport = {
|
|
182
|
-
|
|
183
|
-
|
|
395
|
+
const viewport = {
|
|
396
|
+
name: `design-${result.imageWidth}x${result.imageHeight}`,
|
|
397
|
+
width: result.imageWidth,
|
|
398
|
+
height: result.imageHeight,
|
|
399
|
+
};
|
|
400
|
+
const entry = (routes[wanted.route] ??= {
|
|
401
|
+
viewports: [],
|
|
402
|
+
});
|
|
403
|
+
if (!entry.viewports.some((v) => v.name === viewport.name)) {
|
|
184
404
|
entry.viewports.push(viewport);
|
|
405
|
+
}
|
|
185
406
|
visualSmoke.push({
|
|
186
407
|
id: wanted.id,
|
|
187
408
|
route: wanted.route,
|
|
188
409
|
viewport: viewport.name,
|
|
189
410
|
baselineRoot: config.root,
|
|
190
411
|
baseline: result.file,
|
|
191
|
-
threshold: {
|
|
412
|
+
threshold: {
|
|
413
|
+
maxDiffPixelRatio: config.threshold ?? 0.05,
|
|
414
|
+
},
|
|
192
415
|
strict: false,
|
|
193
416
|
ref: `penpair:${wanted.id}`,
|
|
194
417
|
});
|
package/dist/cli-arguments.js
CHANGED
|
@@ -4,11 +4,16 @@ export class CommandArgumentError extends Error {
|
|
|
4
4
|
remedy = "Use the command's supported flags and required values; see penpair --help.";
|
|
5
5
|
}
|
|
6
6
|
function distance(a, b) {
|
|
7
|
-
let previous = Array.from({
|
|
7
|
+
let previous = Array.from({
|
|
8
|
+
length: b.length + 1,
|
|
9
|
+
}, (_, index) => index);
|
|
8
10
|
for (let i = 1; i <= a.length; i++) {
|
|
9
|
-
const next = [
|
|
10
|
-
|
|
11
|
+
const next = [
|
|
12
|
+
i,
|
|
13
|
+
];
|
|
14
|
+
for (let j = 1; j <= b.length; j++) {
|
|
11
15
|
next[j] = Math.min(next[j - 1] + 1, previous[j] + 1, previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
16
|
+
}
|
|
12
17
|
previous = next;
|
|
13
18
|
}
|
|
14
19
|
return previous[b.length];
|
|
@@ -20,15 +25,22 @@ export function parseCommandArgs(config) {
|
|
|
20
25
|
}
|
|
21
26
|
catch (error) {
|
|
22
27
|
const value = error;
|
|
23
|
-
if (!value.code?.startsWith("ERR_PARSE_ARGS"))
|
|
28
|
+
if (!value.code?.startsWith("ERR_PARSE_ARGS")) {
|
|
24
29
|
throw error;
|
|
30
|
+
}
|
|
25
31
|
const unknown = /Unknown option ['"]--([^'"]+)['"]/.exec(value.message ?? "")?.[1];
|
|
26
32
|
let hint = "";
|
|
27
33
|
if (unknown && unknown.length <= 100) {
|
|
28
|
-
const candidates = Object.keys(config.options ?? {})
|
|
34
|
+
const candidates = Object.keys(config.options ?? {})
|
|
35
|
+
.map((name) => ({
|
|
36
|
+
name,
|
|
37
|
+
distance: distance(unknown.toLowerCase(), name.toLowerCase()),
|
|
38
|
+
}))
|
|
39
|
+
.sort((a, b) => a.distance - b.distance || a.name.localeCompare(b.name));
|
|
29
40
|
const best = candidates[0];
|
|
30
|
-
if (best && best.distance <= Math.max(2, Math.ceil(unknown.length / 2)))
|
|
41
|
+
if (best && best.distance <= Math.max(2, Math.ceil(unknown.length / 2))) {
|
|
31
42
|
hint = ` Did you mean --${best.name}?`;
|
|
43
|
+
}
|
|
32
44
|
}
|
|
33
45
|
throw new CommandArgumentError((value.message ?? "Invalid command arguments") + hint);
|
|
34
46
|
}
|