@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.
Files changed (125) hide show
  1. package/dist/adapters.js +58 -11
  2. package/dist/approvals.js +143 -29
  3. package/dist/atomic-output.js +9 -4
  4. package/dist/audit-rules.js +345 -79
  5. package/dist/backup-note.js +29 -8
  6. package/dist/baseline-coverage.js +169 -35
  7. package/dist/baselines-config.js +119 -29
  8. package/dist/baselines.js +280 -57
  9. package/dist/cli-arguments.js +18 -6
  10. package/dist/cli-main.js +1645 -392
  11. package/dist/client.d.ts +5 -2
  12. package/dist/client.js +127 -28
  13. package/dist/command-result.js +341 -52
  14. package/dist/coordination.js +91 -29
  15. package/dist/credentials.js +57 -13
  16. package/dist/deep-check.js +56 -12
  17. package/dist/design-spec.js +189 -49
  18. package/dist/endpoint.js +102 -34
  19. package/dist/export.js +86 -18
  20. package/dist/file-archive.js +33 -7
  21. package/dist/file-guard.js +29 -11
  22. package/dist/formats.js +215 -14
  23. package/dist/index.d.ts +8 -7
  24. package/dist/index.js +8 -7
  25. package/dist/inspection.d.ts +1 -1
  26. package/dist/inspection.js +378 -65
  27. package/dist/instance/config-schema.js +400 -41
  28. package/dist/instance/config.js +101 -30
  29. package/dist/instance/connect.js +151 -44
  30. package/dist/instance/connection.d.ts +26 -5
  31. package/dist/instance/connection.js +194 -21
  32. package/dist/instance/gitignore.js +5 -2
  33. package/dist/instance/init.js +32 -7
  34. package/dist/instance/runner.js +32 -10
  35. package/dist/instance/setup.js +46 -13
  36. package/dist/instance/tty.js +26 -8
  37. package/dist/journal.d.ts +10 -0
  38. package/dist/journal.js +212 -49
  39. package/dist/kit-smoke.js +82 -20
  40. package/dist/kit-source.js +10 -40
  41. package/dist/kit.js +102 -34
  42. package/dist/layout-policy.d.ts +2 -0
  43. package/dist/layout-policy.js +86 -0
  44. package/dist/layout.d.ts +7 -1
  45. package/dist/layout.js +27 -70
  46. package/dist/live-smoke.js +13 -4
  47. package/dist/local-profile.js +2 -1
  48. package/dist/mcp-server.js +360 -47
  49. package/dist/native-api.js +47 -11
  50. package/dist/onboard.js +33 -7
  51. package/dist/operation.d.ts +2 -2
  52. package/dist/operation.js +279 -69
  53. package/dist/page-session.d.ts +23 -0
  54. package/dist/page-session.js +142 -0
  55. package/dist/png.js +18 -3
  56. package/dist/portable-tokens.js +254 -57
  57. package/dist/probe.d.ts +1 -1
  58. package/dist/probe.js +48 -11
  59. package/dist/project-input.js +24 -10
  60. package/dist/project-snippets.js +18 -6
  61. package/dist/project-status.js +133 -24
  62. package/dist/registry.js +217 -60
  63. package/dist/run-file.js +129 -30
  64. package/dist/shots.js +108 -26
  65. package/dist/skill-check-entry.d.ts +17 -0
  66. package/dist/skill-check-entry.js +89 -0
  67. package/dist/skill-check-instructions.d.ts +7 -0
  68. package/dist/skill-check-instructions.js +39 -0
  69. package/dist/skill-check.d.ts +15 -0
  70. package/dist/skill-check.js +36 -0
  71. package/dist/skill-file.d.ts +2 -0
  72. package/dist/skill-file.js +31 -0
  73. package/dist/skill-instructions.d.ts +4 -0
  74. package/dist/skill-instructions.js +97 -22
  75. package/dist/skill-metadata.d.ts +8 -0
  76. package/dist/skill-metadata.js +52 -0
  77. package/dist/skill-routing.d.ts +2 -0
  78. package/dist/skill-routing.js +36 -0
  79. package/dist/skills.js +131 -41
  80. package/dist/snippet.js +27 -7
  81. package/dist/snippets/read.js +8 -2
  82. package/dist/structure-snapshot.d.ts +12 -0
  83. package/dist/structure-snapshot.js +439 -85
  84. package/dist/token-edit.js +219 -64
  85. package/dist/token-projection.d.ts +56 -0
  86. package/dist/token-projection.js +286 -0
  87. package/dist/tokens-diff.js +125 -32
  88. package/dist/version.d.ts +1 -1
  89. package/dist/version.js +1 -1
  90. package/package.json +3 -2
  91. package/skills/design-judgment/SKILL.md +1 -1
  92. package/skills/design-judgment/agents/openai.yaml +6 -0
  93. package/skills/penpair-design/SKILL.md +14 -1
  94. package/skills/penpair-design/agents/openai.yaml +6 -0
  95. package/skills/penpair-design/references/penpot-plugin-gotchas.md +2 -2
  96. package/skills/penpair-design/references/project-facts.md +2 -0
  97. package/skills/penpair-handoff/SKILL.md +14 -1
  98. package/skills/penpair-handoff/agents/openai.yaml +6 -0
  99. package/skills/penpair-handoff/references/comparison-companion.md +16 -0
  100. package/skills/penpair-handoff/references/handoff-flow.md +2 -0
  101. package/skills/penpair-recipes/SKILL.md +13 -1
  102. package/skills/penpair-recipes/agents/openai.yaml +6 -0
  103. package/skills/penpair-recipes/assets/visual-checkpoint.md +48 -0
  104. package/skills/penpair-recipes/references/visual-checkpoint.md +37 -0
  105. package/skills/penpair-setup/SKILL.md +9 -3
  106. package/skills/penpair-setup/agents/openai.yaml +6 -0
  107. package/skills/penpair-verify/SKILL.md +49 -0
  108. package/skills/penpair-verify/agents/openai.yaml +6 -0
  109. package/skills/penpair-verify/references/browser-evidence.md +33 -0
  110. package/dist/cli.d.ts +0 -2
  111. package/dist/cli.js +0 -3
  112. package/dist/docker/lifecycle.d.ts +0 -96
  113. package/dist/docker/lifecycle.js +0 -273
  114. package/dist/docker/secrets.d.ts +0 -11
  115. package/dist/docker/secrets.js +0 -38
  116. package/dist/docker/template.d.ts +0 -8
  117. package/dist/docker/template.js +0 -158
  118. package/dist/docker-profile.d.ts +0 -114
  119. package/dist/docker-profile.js +0 -18
  120. package/dist/instance/lifecycle.d.ts +0 -129
  121. package/dist/instance/lifecycle.js +0 -336
  122. package/dist/instance/secrets.d.ts +0 -11
  123. package/dist/instance/secrets.js +0 -38
  124. package/dist/instance/template.d.ts +0 -8
  125. package/dist/instance/template.js +0 -158
@@ -1,3 +1,4 @@
1
+ import { inheritPageSession } from "./page-session.js";
1
2
  import { createHash, randomUUID } from "node:crypto";
2
3
  import { hostname, userInfo } from "node:os";
3
4
  import { lstat, mkdir, open, readFile, unlink } from "node:fs/promises";
@@ -8,22 +9,43 @@ import { maxSnippetBytes } from "./client.js";
8
9
  import { PenpairError } from "./errors.js";
9
10
  /** Exclusive only within this checkout. Shared file markers below are advisory, not compare-and-swap. */
10
11
  async function localLease(root, fileId, runId, force) {
11
- await ensureGitignoreEntry(root, ".penpair", { directory: true, comment: "penpair: private local operation evidence" });
12
+ await ensureGitignoreEntry(root, ".penpair", {
13
+ directory: true,
14
+ comment: "penpair: private local operation evidence",
15
+ });
12
16
  const parent = path.join(root, ".penpair");
13
- await mkdir(parent, { mode: 0o700 }).catch((error) => { if (error.code !== "EEXIST")
14
- throw error; });
17
+ await mkdir(parent, {
18
+ mode: 0o700,
19
+ }).catch((error) => {
20
+ if (error.code !== "EEXIST") {
21
+ throw error;
22
+ }
23
+ });
15
24
  const parentInfo = await lstat(parent);
16
- if (!parentInfo.isDirectory() || parentInfo.isSymbolicLink())
25
+ if (!parentInfo.isDirectory() || parentInfo.isSymbolicLink()) {
17
26
  throw new Error("Write locks require a real local directory");
27
+ }
18
28
  const directory = path.join(parent, "locks");
19
- await mkdir(directory, { mode: 0o700 }).catch((error) => { if (error.code !== "EEXIST")
20
- throw error; });
29
+ await mkdir(directory, {
30
+ mode: 0o700,
31
+ }).catch((error) => {
32
+ if (error.code !== "EEXIST") {
33
+ throw error;
34
+ }
35
+ });
21
36
  const info = await lstat(directory);
22
- if (!info.isDirectory() || info.isSymbolicLink())
37
+ if (!info.isDirectory() || info.isSymbolicLink()) {
23
38
  throw new Error("Write locks require a real local directory");
39
+ }
24
40
  const destination = path.join(directory, `${createHash("sha256").update(fileId).digest("hex")}.json`);
25
41
  const token = randomUUID();
26
- const entry = { token, runId, pid: process.pid, host: hostname(), createdAt: new Date().toISOString() };
42
+ const entry = {
43
+ token,
44
+ runId,
45
+ pid: process.pid,
46
+ host: hostname(),
47
+ createdAt: new Date().toISOString(),
48
+ };
27
49
  const create = async () => {
28
50
  const handle = await open(destination, "wx", 0o600);
29
51
  try {
@@ -37,8 +59,9 @@ async function localLease(root, fileId, runId, force) {
37
59
  await create();
38
60
  }
39
61
  catch (error) {
40
- if (error.code !== "EEXIST")
62
+ if (error.code !== "EEXIST") {
41
63
  throw error;
64
+ }
42
65
  const previous = JSON.parse(await readFile(destination, "utf8"));
43
66
  let running = false;
44
67
  if (previous.host === hostname() && Number.isInteger(previous.pid) && previous.pid > 0) {
@@ -47,20 +70,23 @@ async function localLease(root, fileId, runId, force) {
47
70
  running = true;
48
71
  }
49
72
  catch (failure) {
50
- if (failure.code !== "ESRCH")
73
+ if (failure.code !== "ESRCH") {
51
74
  running = true;
75
+ }
52
76
  }
53
77
  }
54
- if (!force || running)
78
+ if (!force || running) {
55
79
  throw new Error(`Local writer ${previous.runId} holds this file. ${running ? "Its process is still running; wait for it to finish." : "Inspect its journal, then use --force-lock to remove the abandoned local lock."}`);
80
+ }
56
81
  // Explicit owner override only. Simultaneous force operations are not an exclusion guarantee.
57
82
  await unlink(destination);
58
83
  await create();
59
84
  }
60
85
  return async () => {
61
86
  const current = JSON.parse(await readFile(destination, "utf8"));
62
- if (current.token === token)
87
+ if (current.token === token) {
63
88
  await unlink(destination);
89
+ }
64
90
  };
65
91
  }
66
92
  const readMarker = `const f = penpot.currentFile;
@@ -72,36 +98,62 @@ if (raw) { try { marker = JSON.parse(raw); } catch { return { ok: false, message
72
98
  export function writeCoordinator(client, options) {
73
99
  return async (runId, target) => {
74
100
  const releaseLocal = await localLease(options.root, target.fileId, runId, options.forceLock === true);
75
- const marker = { schemaVersion: 1, token: randomUUID(), runId, operation: options.operation, owner: userInfo().username, createdAt: Date.now(), expiresAt: Date.now() + 15 * 60_000 };
76
- const params = { marker, fileId: target.fileId, force: options.forceLock === true };
101
+ const marker = {
102
+ schemaVersion: 1,
103
+ token: randomUUID(),
104
+ runId,
105
+ operation: options.operation,
106
+ owner: userInfo().username,
107
+ createdAt: Date.now(),
108
+ expiresAt: Date.now() + 15 * 60_000,
109
+ };
110
+ const params = {
111
+ marker,
112
+ fileId: target.fileId,
113
+ force: options.forceLock === true,
114
+ };
77
115
  const targetGuard = 'if (penpot.currentFile.id !== CFG.fileId) return { ok:false, message:"Writer marker target changed" };';
78
116
  try {
79
- const before = await runSnippet(client, snippet("writer-inspect", targetGuard + readMarker + 'return { ok:true, supported:true, marker };', params, "read"));
117
+ const before = await runSnippet(client, snippet("writer-inspect", targetGuard + readMarker + "return { ok:true, supported:true, marker };", params, "read"));
80
118
  if (before.supported === false) {
81
119
  options.warn?.("Shared writer markers unavailable; local checkout lock only. Coordinate with colleagues before writing.");
82
- return { check: async () => undefined, release: releaseLocal };
120
+ return {
121
+ check: async () => undefined,
122
+ release: releaseLocal,
123
+ };
83
124
  }
84
- const acquired = await runSnippet(client, snippet("writer-acquire", targetGuard + readMarker + `
125
+ const acquired = await runSnippet(client, snippet("writer-acquire", targetGuard +
126
+ readMarker +
127
+ `
85
128
  if (marker && marker.token !== CFG.marker.token && (!Number.isFinite(marker.expiresAt) || marker.expiresAt > Date.now()) && !CFG.force)
86
129
  return { ok:false, message:"Another writer is running " + String(marker.operation) + " (run " + String(marker.runId) + "). Coordinate before --force-lock." };
87
130
  f.setSharedPluginData("penpair", "writer", JSON.stringify(CFG.marker));
88
131
  return { ok:true, supported:true };`, params, "write"));
89
- if (acquired.supported !== true)
132
+ if (acquired.supported !== true) {
90
133
  throw new Error("Writer marker capability changed during acquisition");
134
+ }
91
135
  const check = async () => {
92
- const observed = await runSnippet(client, snippet("writer-check", targetGuard + readMarker + 'return { ok:true, supported:true, owned: !!marker && marker.token === CFG.marker.token && marker.expiresAt > Date.now() };', params, "read"));
93
- if (observed.supported !== true || observed.owned !== true)
136
+ const observed = await runSnippet(client, snippet("writer-check", targetGuard +
137
+ readMarker +
138
+ "return { ok:true, supported:true, owned: !!marker && marker.token === CFG.marker.token && marker.expiresAt > Date.now() };", params, "read"));
139
+ if (observed.supported !== true || observed.owned !== true) {
94
140
  throw new Error("Shared writer marker was lost or expired; stop and coordinate before resuming");
141
+ }
95
142
  };
96
143
  await check();
97
- return { check, release: async () => {
144
+ return {
145
+ check,
146
+ release: async () => {
98
147
  try {
99
- await runSnippet(client, snippet("writer-release", targetGuard + readMarker + 'if (marker && marker.token === CFG.marker.token) f.setSharedPluginData("penpair", "writer", ""); return { ok:true };', params, "write"));
148
+ await runSnippet(client, snippet("writer-release", targetGuard +
149
+ readMarker +
150
+ 'if (marker && marker.token === CFG.marker.token) f.setSharedPluginData("penpair", "writer", ""); return { ok:true };', params, "write"));
100
151
  }
101
152
  finally {
102
153
  await releaseLocal();
103
154
  }
104
- } };
155
+ },
156
+ };
105
157
  }
106
158
  catch (error) {
107
159
  await releaseLocal();
@@ -111,12 +163,22 @@ return { ok:true, supported:true };`, params, "write"));
111
163
  }
112
164
  /** Revision precondition is checked inside the delivered mutation, after all guard calls. It is not a server CAS. */
113
165
  export function guardRevision(client, target) {
114
- return { ...client, close: () => client.close(), async callTool(name, args, options) {
115
- if (name !== "execute_code" || typeof args.code !== "string")
166
+ return inheritPageSession({
167
+ ...client,
168
+ close: () => client.close(),
169
+ async callTool(name, args, options) {
170
+ if (name !== "execute_code" || typeof args.code !== "string") {
116
171
  throw new Error("Revision guards require execute_code");
117
- const code = `{ if (penpot.currentFile.id !== ${JSON.stringify(target.fileId)} || penpot.currentFile.revn !== ${JSON.stringify(target.revision)}) return {ok:false, message:"File revision changed before mutation; inspect again"}; }\n` + args.code;
118
- if (Buffer.byteLength(code) > maxSnippetBytes)
172
+ }
173
+ const code = `{ if (penpot.currentFile.id !== ${JSON.stringify(target.fileId)} || penpot.currentFile.revn !== ${JSON.stringify(target.revision)}) return {ok:false, message:"File revision changed before mutation; inspect again"}; }\n` +
174
+ args.code;
175
+ if (Buffer.byteLength(code) > maxSnippetBytes) {
119
176
  throw new PenpairError("snippet-too-large", "Snippet including revision guard exceeds 90 KB");
120
- return client.callTool(name, { ...args, code }, options);
121
- } };
177
+ }
178
+ return client.callTool(name, {
179
+ ...args,
180
+ code,
181
+ }, options);
182
+ },
183
+ }, client);
122
184
  }
@@ -4,36 +4,74 @@ import { open } from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { PenpairError } from "./errors.js";
6
6
  export function validateCredentialReference(raw) {
7
- if (!raw || typeof raw !== "object" || Array.isArray(raw))
7
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
8
8
  throw new Error("Credential reference must be a file or keychain object");
9
+ }
9
10
  const v = raw;
10
11
  if (v.type === "file") {
11
- if (Object.keys(v).some(k => !["type", "path"].includes(k)) || typeof v.path !== "string" || !v.path.trim() || v.path.length > 4096 || /[\x00-\x1f]/.test(v.path))
12
+ if (Object.keys(v).some((k) => ![
13
+ "type",
14
+ "path",
15
+ ].includes(k)) ||
16
+ typeof v.path !== "string" ||
17
+ !v.path.trim() ||
18
+ v.path.length > 4096 ||
19
+ /[\x00-\x1f]/.test(v.path)) {
12
20
  throw new Error("Invalid credential file reference");
21
+ }
13
22
  }
14
23
  else if (v.type === "keychain") {
15
- if (Object.keys(v).some(k => !["type", "service", "account"].includes(k)) || [v.service, v.account].some(s => typeof s !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9._ /:@-]{0,199}$/.test(s)))
24
+ if (Object.keys(v).some((k) => ![
25
+ "type",
26
+ "service",
27
+ "account",
28
+ ].includes(k)) ||
29
+ [
30
+ v.service,
31
+ v.account,
32
+ ].some((s) => typeof s !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9._ /:@-]{0,199}$/.test(s))) {
16
33
  throw new Error("Invalid keychain service/account reference");
34
+ }
17
35
  }
18
- else
36
+ else {
19
37
  throw new Error("Only file and macOS keychain credential references are supported; arbitrary command providers are deferred");
38
+ }
20
39
  return structuredClone(v);
21
40
  }
22
- const keychainRead = (command, args, options) => new Promise((resolve, reject) => execFile(command, args, { ...options, killSignal: "SIGKILL" }, (error, stdout) => error ? reject(new Error("Credential provider unavailable or access denied")) : resolve(stdout)));
41
+ const keychainRead = (command, args, options) => new Promise((resolve, reject) => execFile(command, args, {
42
+ ...options,
43
+ killSignal: "SIGKILL",
44
+ }, (error, stdout) => error
45
+ ? reject(new Error("Credential provider unavailable or access denied"))
46
+ : resolve(stdout)));
23
47
  const normalize = (raw) => {
24
48
  const value = raw.trim();
25
- if (!value || Buffer.byteLength(value) > 8192 || /[\r\n\x00]/.test(value))
49
+ if (!value || Buffer.byteLength(value) > 8192 || /[\r\n\x00]/.test(value)) {
26
50
  throw new PenpairError("endpoint-invalid", "Credential provider must return one bounded MCP URL line");
51
+ }
27
52
  return value;
28
53
  };
29
54
  /** Resolve only at connection time. No URL, key, child stderr or command error object enters diagnostics. */
30
55
  export async function readCredential(reference, options) {
31
- const ref = validateCredentialReference(reference), platform = options.platform ?? process.platform;
56
+ const ref = validateCredentialReference(reference);
57
+ const platform = options.platform ?? process.platform;
32
58
  if (ref.type === "keychain") {
33
- if (platform !== "darwin")
59
+ if (platform !== "darwin") {
34
60
  throw new PenpairError("endpoint-unconfigured", "macOS Keychain references require macOS; configure a private credential file on this platform");
61
+ }
35
62
  try {
36
- return normalize(await (options.keychainRead ?? keychainRead)("/usr/bin/security", ["find-generic-password", "-s", ref.service, "-a", ref.account, "-w"], { timeout: 5000, maxBuffer: 16384, encoding: "utf8" }));
63
+ return normalize(await (options.keychainRead ?? keychainRead)("/usr/bin/security", [
64
+ "find-generic-password",
65
+ "-s",
66
+ ref.service,
67
+ "-a",
68
+ ref.account,
69
+ "-w",
70
+ ], {
71
+ timeout: 5000,
72
+ maxBuffer: 16384,
73
+ encoding: "utf8",
74
+ }));
37
75
  }
38
76
  catch {
39
77
  throw new PenpairError("endpoint-unconfigured", "OS credential lookup failed, timed out, or returned invalid output; inspect access in the OS credential UI");
@@ -43,13 +81,19 @@ export async function readCredential(reference, options) {
43
81
  try {
44
82
  handle = await open(path.resolve(options.root, ref.path), constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
45
83
  const info = await handle.stat();
46
- if (!info.isFile() || info.size > 16384)
84
+ if (!info.isFile() || info.size > 16384) {
47
85
  throw new Error("not a bounded regular file");
48
- if (platform !== "win32" && ((info.mode & 0o077) !== 0 || typeof process.getuid === "function" && info.uid !== process.getuid()))
86
+ }
87
+ if (platform !== "win32" &&
88
+ ((info.mode & 0o077) !== 0 ||
89
+ (typeof process.getuid === "function" && info.uid !== process.getuid()))) {
49
90
  throw new Error("not private to current owner");
50
- const buffer = Buffer.alloc(16385), read = await handle.read(buffer, 0, buffer.length, 0);
51
- if (read.bytesRead > 16384)
91
+ }
92
+ const buffer = Buffer.alloc(16385);
93
+ const read = await handle.read(buffer, 0, buffer.length, 0);
94
+ if (read.bytesRead > 16384) {
52
95
  throw new Error("credential file byte budget exceeded");
96
+ }
53
97
  return normalize(buffer.subarray(0, read.bytesRead).toString("utf8"));
54
98
  }
55
99
  catch {
@@ -68,30 +68,74 @@ for (const c of penpot.library.local.components) if (c.name.startsWith(tag)) lef
68
68
  return { ok: true, readOnly: true, left };`;
69
69
  export async function runDeepCheck(client, options = {}) {
70
70
  const tag = `penpair-deep-${randomBytes(3).toString("hex")}`;
71
- const first = (await executeCode(client, wrap(deepCheckBody, { tag }), { ...options, effect: "write" })).result;
72
- if (first.ok === false)
71
+ const first = (await executeCode(client, wrap(deepCheckBody, {
72
+ tag,
73
+ }), {
74
+ ...options,
75
+ effect: "write",
76
+ })).result;
77
+ if (first.ok === false) {
73
78
  throw new Error(`the deep check could not run: ${first.message ?? "unknown error"}`);
79
+ }
74
80
  await new Promise((resolve) => setTimeout(resolve, options.settleMs ?? 3_000));
75
- const second = (await executeCode(client, wrap(deepLeftoversBody, { tag }), { ...options, effect: "read" })).result;
76
- const steps = Array.isArray(first.steps) ? [...first.steps] : [];
77
- const required = ["create board", "flex layout and children", "text content", "token set, token and binding", "component and swap", "clone", "export png"];
81
+ const second = (await executeCode(client, wrap(deepLeftoversBody, {
82
+ tag,
83
+ }), {
84
+ ...options,
85
+ effect: "read",
86
+ })).result;
87
+ const steps = Array.isArray(first.steps)
88
+ ? [
89
+ ...first.steps,
90
+ ]
91
+ : [];
92
+ const required = [
93
+ "create board",
94
+ "flex layout and children",
95
+ "text content",
96
+ "token set, token and binding",
97
+ "component and swap",
98
+ "clone",
99
+ "export png",
100
+ ];
78
101
  const complete = required.every((name) => steps.some((step) => step.step === name && step.ok === true));
79
- if (!complete)
80
- steps.push({ step: "complete evidence", ok: false, ms: 0, detail: "Required deep-check observations are missing or failed" });
81
- const leftovers = Array.isArray(second.left) ? second.left : ["cleanup readback was missing; inspect the scratch file"];
102
+ if (!complete) {
103
+ steps.push({
104
+ step: "complete evidence",
105
+ ok: false,
106
+ ms: 0,
107
+ detail: "Required deep-check observations are missing or failed",
108
+ });
109
+ }
110
+ const leftovers = Array.isArray(second.left)
111
+ ? second.left
112
+ : [
113
+ "cleanup readback was missing; inspect the scratch file",
114
+ ];
82
115
  return {
83
116
  tag,
84
- ...(first.penpotVersion ? { penpotVersion: first.penpotVersion } : {}),
117
+ ...(first.penpotVersion
118
+ ? {
119
+ penpotVersion: first.penpotVersion,
120
+ }
121
+ : {}),
85
122
  steps,
86
123
  cleanup: first.cleanup ?? [],
87
124
  leftovers,
88
- ok: first.ok === true && second.ok === true && complete && steps.every((s) => s.ok === true) && leftovers.length === 0,
125
+ ok: first.ok === true &&
126
+ second.ok === true &&
127
+ complete &&
128
+ steps.every((s) => s.ok === true) &&
129
+ leftovers.length === 0,
89
130
  };
90
131
  }
91
132
  export function formatDeepReport(report) {
92
133
  const lines = report.steps.map((s) => `${s.ok ? "ok " : "FAIL"} ${s.step} (${s.ms} ms)${s.detail ? `: ${s.detail}` : ""}`);
93
- lines.push(report.leftovers.length === 0 ? "ok clean-up: nothing left in the file" : `FAIL clean-up: left in the file, remove by hand: ${report.leftovers.join(", ")}`);
94
- if (report.penpotVersion)
134
+ lines.push(report.leftovers.length === 0
135
+ ? "ok clean-up: nothing left in the file"
136
+ : `FAIL clean-up: left in the file, remove by hand: ${report.leftovers.join(", ")}`);
137
+ if (report.penpotVersion) {
95
138
  lines.push(`Penpot ${report.penpotVersion}`);
139
+ }
96
140
  return lines.join("\n");
97
141
  }