@penpair/core 0.1.3 → 0.2.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.
Files changed (123) hide show
  1. package/dist/adapters.d.ts +16 -1
  2. package/dist/adapters.js +8 -12
  3. package/dist/approvals.d.ts +32 -0
  4. package/dist/approvals.js +99 -0
  5. package/dist/atomic-output.d.ts +5 -0
  6. package/dist/atomic-output.js +28 -0
  7. package/dist/audit-rules.d.ts +60 -0
  8. package/dist/audit-rules.js +177 -0
  9. package/dist/backup-note.d.ts +4 -0
  10. package/dist/backup-note.js +28 -0
  11. package/dist/baseline-coverage.d.ts +25 -0
  12. package/dist/baseline-coverage.js +114 -0
  13. package/dist/baselines-config.d.ts +25 -0
  14. package/dist/baselines-config.js +52 -3
  15. package/dist/baselines.d.ts +37 -8
  16. package/dist/baselines.js +132 -52
  17. package/dist/cli-arguments.d.ts +7 -0
  18. package/dist/cli-arguments.js +35 -0
  19. package/dist/cli-main.d.ts +13 -1
  20. package/dist/cli-main.js +770 -139
  21. package/dist/client.d.ts +26 -5
  22. package/dist/client.js +46 -20
  23. package/dist/command-result.d.ts +159 -0
  24. package/dist/command-result.js +179 -0
  25. package/dist/coordination.d.ts +15 -0
  26. package/dist/coordination.js +122 -0
  27. package/dist/credentials.d.ts +21 -0
  28. package/dist/credentials.js +61 -0
  29. package/dist/deep-check.js +9 -5
  30. package/dist/design-spec.d.ts +55 -0
  31. package/dist/design-spec.js +135 -0
  32. package/dist/endpoint.d.ts +8 -1
  33. package/dist/endpoint.js +48 -4
  34. package/dist/errors.d.ts +2 -1
  35. package/dist/errors.js +8 -4
  36. package/dist/export.d.ts +18 -0
  37. package/dist/export.js +42 -7
  38. package/dist/file-archive.d.ts +16 -0
  39. package/dist/file-archive.js +32 -0
  40. package/dist/file-guard.d.ts +8 -0
  41. package/dist/file-guard.js +40 -0
  42. package/dist/formats.d.ts +4 -0
  43. package/dist/formats.js +28 -0
  44. package/dist/index.d.ts +33 -1
  45. package/dist/index.js +33 -1
  46. package/dist/inspection.d.ts +106 -0
  47. package/dist/inspection.js +344 -0
  48. package/dist/instance/config-schema.d.ts +422 -0
  49. package/dist/instance/config-schema.js +76 -0
  50. package/dist/instance/config.d.ts +29 -0
  51. package/dist/instance/config.js +102 -11
  52. package/dist/instance/connect.d.ts +10 -1
  53. package/dist/instance/connect.js +53 -12
  54. package/dist/instance/connection.d.ts +14 -0
  55. package/dist/instance/connection.js +13 -2
  56. package/dist/instance/init.js +1 -1
  57. package/dist/instance/setup.js +1 -1
  58. package/dist/instance/tty.js +3 -1
  59. package/dist/journal.d.ts +77 -0
  60. package/dist/journal.js +260 -0
  61. package/dist/kit-smoke.d.ts +31 -0
  62. package/dist/kit-smoke.js +83 -0
  63. package/dist/kit-source.d.ts +2 -0
  64. package/dist/kit-source.js +309 -0
  65. package/dist/kit.d.ts +25 -0
  66. package/dist/kit.js +90 -0
  67. package/dist/layout.d.ts +15 -0
  68. package/dist/layout.js +79 -0
  69. package/dist/live-smoke.d.ts +8 -0
  70. package/dist/live-smoke.js +14 -0
  71. package/dist/mcp-server.d.ts +12 -0
  72. package/dist/mcp-server.js +102 -0
  73. package/dist/native-api.d.ts +15 -0
  74. package/dist/native-api.js +42 -0
  75. package/dist/onboard.d.ts +10 -0
  76. package/dist/onboard.js +15 -0
  77. package/dist/operation.d.ts +20 -1
  78. package/dist/operation.js +113 -13
  79. package/dist/png.d.ts +5 -0
  80. package/dist/png.js +7 -0
  81. package/dist/portable-tokens.d.ts +22 -0
  82. package/dist/portable-tokens.js +134 -0
  83. package/dist/probe.d.ts +6 -2
  84. package/dist/probe.js +19 -3
  85. package/dist/project-input.d.ts +6 -0
  86. package/dist/project-input.js +51 -0
  87. package/dist/project-snippets.js +12 -7
  88. package/dist/project-status.d.ts +24 -0
  89. package/dist/project-status.js +79 -0
  90. package/dist/registry.d.ts +2 -1
  91. package/dist/registry.js +17 -3
  92. package/dist/run-file.d.ts +26 -0
  93. package/dist/run-file.js +100 -0
  94. package/dist/shots.d.ts +43 -0
  95. package/dist/shots.js +90 -0
  96. package/dist/skill-instructions.d.ts +13 -0
  97. package/dist/skill-instructions.js +75 -0
  98. package/dist/skills.d.ts +7 -0
  99. package/dist/skills.js +59 -32
  100. package/dist/snippet.d.ts +3 -2
  101. package/dist/snippet.js +12 -8
  102. package/dist/snippets/archive.d.ts +9 -0
  103. package/dist/snippets/archive.js +10 -4
  104. package/dist/snippets/read.d.ts +1 -1
  105. package/dist/snippets/read.js +71 -40
  106. package/dist/structure-snapshot.d.ts +73 -0
  107. package/dist/structure-snapshot.js +171 -0
  108. package/dist/token-edit.d.ts +53 -0
  109. package/dist/token-edit.js +197 -0
  110. package/dist/tokens-diff.d.ts +19 -6
  111. package/dist/tokens-diff.js +89 -18
  112. package/dist/version.d.ts +1 -1
  113. package/dist/version.js +1 -1
  114. package/package.json +1 -1
  115. package/skills/penpair-design/SKILL.md +12 -40
  116. package/skills/penpair-design/references/penpot-plugin-gotchas.md +56 -0
  117. package/skills/penpair-design/references/project-facts.md +1 -1
  118. package/skills/penpair-design/references/write-protocol.md +15 -3
  119. package/skills/penpair-handoff/SKILL.md +14 -0
  120. package/skills/penpair-handoff/references/handoff-flow.md +11 -0
  121. package/skills/penpair-recipes/SKILL.md +16 -0
  122. package/skills/penpair-recipes/references/build-flow.md +11 -0
  123. package/skills/penpair-setup/SKILL.md +6 -2
@@ -0,0 +1,260 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { link, lstat, mkdir, readFile, readdir, rename, unlink, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { redactEndpoint } from "./endpoint.js";
5
+ import { ensureGitignoreEntry } from "./instance/gitignore.js";
6
+ import { version } from "./version.js";
7
+ import { runSnippet, snippet } from "./snippet.js";
8
+ export async function readJournalTarget(client) {
9
+ const result = await runSnippet(client, snippet("journal-target", "return { ok: true, fileId: penpot.currentFile.id, revision: penpot.currentFile.revn };", {}, "read"));
10
+ if (result.ok !== true || typeof result.fileId !== "string" || !result.fileId || !Number.isInteger(result.revision))
11
+ throw new Error("Journal target identity/revision was not observed");
12
+ return { fileId: result.fileId, revision: Number(result.revision) };
13
+ }
14
+ /** Stable JSON hashing; observation comparisons may explicitly ignore transport execution metadata. */
15
+ export function observationHash(value, ignoreExecution = false) {
16
+ const canonical = (entry) => {
17
+ if (Array.isArray(entry))
18
+ return entry.map(canonical);
19
+ if (typeof entry !== "object" || entry === null)
20
+ return entry;
21
+ return Object.fromEntries(Object.entries(entry).filter(([key]) => !ignoreExecution || key !== "execution").sort(([a], [b]) => a.localeCompare(b)).map(([key, child]) => [key, canonical(child)]));
22
+ };
23
+ return createHash("sha256").update(JSON.stringify(canonical(value)) ?? "null").digest("hex");
24
+ }
25
+ function validateRunId(runId) {
26
+ if (!/^[a-zA-Z0-9-]{1,100}$/.test(runId))
27
+ throw new Error("Run ID must contain only letters, digits and hyphens (at most 100)");
28
+ }
29
+ async function folder(root, create) {
30
+ if (create)
31
+ await ensureGitignoreEntry(root, ".penpair", { directory: true, comment: "penpair: private local operations and recovery evidence" });
32
+ let current = root;
33
+ for (const segment of [".penpair", "runs"]) {
34
+ current = path.join(current, segment);
35
+ try {
36
+ const info = await lstat(current);
37
+ if (!info.isDirectory() || info.isSymbolicLink())
38
+ throw new Error("Operation journal directories must be real directories inside this project");
39
+ }
40
+ catch (error) {
41
+ if (error.code !== "ENOENT")
42
+ throw error;
43
+ if (create)
44
+ await mkdir(current, { mode: 0o700 });
45
+ }
46
+ }
47
+ return current;
48
+ }
49
+ export async function readJournal(root, runId) {
50
+ validateRunId(runId);
51
+ const file = path.join(await folder(root, false), `${runId}.json`);
52
+ const info = await lstat(file);
53
+ if (!info.isFile() || info.isSymbolicLink())
54
+ throw new Error("Operation journal must be a regular local file");
55
+ const record = JSON.parse(await readFile(file, "utf8"));
56
+ if (record.schemaVersion !== 1 || record.runId !== runId || typeof record.operation !== "string" || !Array.isArray(record.steps) || !record.target || typeof record.target.fileId !== "string")
57
+ throw new Error("Operation journal is invalid or uses an unsupported schema");
58
+ return record;
59
+ }
60
+ export async function listJournals(root) {
61
+ let files;
62
+ try {
63
+ files = await readdir(await folder(root, false));
64
+ }
65
+ catch (error) {
66
+ if (error.code === "ENOENT")
67
+ return [];
68
+ throw error;
69
+ }
70
+ const records = await Promise.all(files.filter((name) => /^[a-zA-Z0-9-]+\.json$/.test(name)).map((name) => readJournal(root, name.slice(0, -5))));
71
+ return records.map(({ runId, operation, state, target, updatedAt }) => ({ runId, operation, state, target, updatedAt })).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
72
+ }
73
+ export class JournalRunError extends Error {
74
+ runId;
75
+ outcome;
76
+ constructor(runId, outcome, message) {
77
+ super(`Run ${runId} is ${outcome}: ${message}. Inspect \`penpair runs ${runId}\` before explicitly resuming.`);
78
+ this.runId = runId;
79
+ this.outcome = outcome;
80
+ }
81
+ }
82
+ /** Each started phase is persisted before its write. Resume must inspect and reconcile actual state, never replay blindly. */
83
+ export async function runJournaledSteps(steps, options) {
84
+ if (options.requireCheckpoint && !options.checkpoint)
85
+ throw new Error("A required checkpoint needs a checkpoint provider");
86
+ const runId = options.runId ?? randomUUID();
87
+ validateRunId(runId);
88
+ const redact = (text) => redactEndpoint(options.redact?.(text) ?? text);
89
+ const directory = await folder(options.root, true);
90
+ const destination = path.join(directory, `${runId}.json`);
91
+ const target = await options.target();
92
+ if (!target || typeof target.fileId !== "string" || !target.fileId)
93
+ throw new Error("Operation journal requires an observed file ID");
94
+ const inputHash = observationHash({ input: options.input, requireCheckpoint: options.requireCheckpoint === true, checkpointRequested: Boolean(options.checkpoint) });
95
+ const planHash = observationHash({ steps: steps.map(({ key, signature }) => ({ key, signature })), finalVerifier: options.finalVerify?.toString() });
96
+ if (new Set(steps.map((step) => step.key)).size !== steps.length)
97
+ throw new Error("Journal step keys must be unique");
98
+ let record;
99
+ if (options.resume) {
100
+ if (!options.runId)
101
+ throw new Error("Resume requires the original run ID");
102
+ record = await readJournal(options.root, runId);
103
+ if (record.redacted)
104
+ throw new Error("This journal required redaction and cannot be automatically resumed; inspect and make a fresh plan");
105
+ if (record.engineVersion !== version || record.operation !== options.operation || record.inputHash !== inputHash || record.planHash !== planHash || record.target.fileId !== target.fileId || record.steps.length !== steps.length)
106
+ throw new Error("Resume target, inputs, step implementation or engine version changed; no write was replayed");
107
+ }
108
+ else {
109
+ const before = [];
110
+ for (const step of steps)
111
+ before.push(step.initial === undefined ? await step.inspect() : step.initial);
112
+ const observed = await options.target();
113
+ if (observed.fileId !== target.fileId || (target.revision !== undefined && observed.revision !== target.revision))
114
+ throw new Error("File changed while planning; inspect again before starting a run");
115
+ const now = new Date().toISOString();
116
+ record = { schemaVersion: 1, engineVersion: version, runId, operation: options.operation, inputHash, planHash, payload: options.payload, target, state: "planned", createdAt: now, updatedAt: now, redacted: false, steps: steps.map((step, index) => ({ key: step.key, label: step.label, state: "pending", before: before[index] })) };
117
+ }
118
+ const persist = async (create = false) => {
119
+ record.updatedAt = new Date().toISOString();
120
+ const sanitize = (value) => {
121
+ if (typeof value === "string")
122
+ return redact(value);
123
+ if (Array.isArray(value))
124
+ return value.map(sanitize);
125
+ if (value && typeof value === "object")
126
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [redact(key), sanitize(entry)]));
127
+ return value;
128
+ };
129
+ const serialize = () => JSON.stringify(sanitize(record), null, 2);
130
+ if (serialize() !== JSON.stringify(record, null, 2))
131
+ record.redacted = true;
132
+ const temporary = path.join(directory, `.${runId}-${randomUUID()}.tmp`);
133
+ await writeFile(temporary, `${serialize()}\n`, { flag: "wx", mode: 0o600 });
134
+ try {
135
+ if (create)
136
+ await link(temporary, destination);
137
+ else
138
+ await rename(temporary, destination);
139
+ }
140
+ finally {
141
+ await unlink(temporary).catch((error) => { if (error.code !== "ENOENT")
142
+ throw error; });
143
+ }
144
+ };
145
+ if (!options.resume)
146
+ await persist(true);
147
+ options.onStep?.(`operation run ${runId}`);
148
+ const assertTarget = async () => {
149
+ const actual = await options.target();
150
+ if (actual.fileId !== record.target.fileId)
151
+ throw new Error("The operation target file changed");
152
+ return actual;
153
+ };
154
+ let lease;
155
+ try {
156
+ lease = await options.coordinate?.(runId, target);
157
+ if (options.checkpoint && record.steps.every((step) => step.state === "pending") && record.checkpoint === undefined) {
158
+ record.state = "running";
159
+ await persist();
160
+ await assertTarget();
161
+ const checkpoint = await options.checkpoint(runId);
162
+ if (checkpoint.supported !== false && checkpoint.report?.verified !== true)
163
+ throw new Error("Checkpoint did not provide verified recovery evidence");
164
+ if (!checkpoint.supported && options.requireCheckpoint) {
165
+ record.state = "failed";
166
+ throw new Error("Required checkpoint API is unavailable; no operation step was applied");
167
+ }
168
+ record.checkpoint = checkpoint;
169
+ await persist();
170
+ }
171
+ for (let index = 0; index < steps.length; index++) {
172
+ const step = steps[index];
173
+ const saved = record.steps[index];
174
+ await lease?.check();
175
+ const beforeInspection = await assertTarget();
176
+ const current = await step.inspect();
177
+ const afterInspection = await assertTarget();
178
+ if (beforeInspection.revision !== afterInspection.revision) {
179
+ record.state = "conflict";
180
+ throw new Error("File revision changed during step inspection");
181
+ }
182
+ if (saved.state === "completed") {
183
+ if (!step.verify(current, saved.before, saved.result)) {
184
+ record.state = "conflict";
185
+ throw new Error(`Previously completed step ${step.label} no longer matches`);
186
+ }
187
+ continue;
188
+ }
189
+ const reconciliation = step.reconcile(saved.before, current, saved.result);
190
+ if (reconciliation === "conflict") {
191
+ record.state = "conflict";
192
+ throw new Error(`External or unexplained changes conflict with ${step.label}`);
193
+ }
194
+ if (reconciliation === "complete") {
195
+ if (!step.verify(current, saved.before, saved.result)) {
196
+ record.state = "conflict";
197
+ throw new Error(`Reconciliation lacks verification for ${step.label}`);
198
+ }
199
+ saved.state = "completed";
200
+ saved.after = current;
201
+ await persist();
202
+ continue;
203
+ }
204
+ saved.state = "started";
205
+ saved.targetBefore = afterInspection;
206
+ record.state = "running";
207
+ await persist();
208
+ options.onStep?.(step.label);
209
+ try {
210
+ const beforeApply = await assertTarget();
211
+ if (beforeApply.revision !== afterInspection.revision) {
212
+ saved.state = "pending";
213
+ record.state = "conflict";
214
+ throw new Error("File revision changed after inspection; no step was replayed");
215
+ }
216
+ saved.result = await step.apply(current, beforeApply);
217
+ const after = await step.inspect();
218
+ saved.after = after;
219
+ if (!step.verify(after, saved.before, saved.result)) {
220
+ saved.state = "failed";
221
+ record.state = "failed";
222
+ throw new Error(`Independent verification failed for ${step.label}`);
223
+ }
224
+ saved.targetAfter = await assertTarget();
225
+ saved.state = "completed";
226
+ await persist();
227
+ }
228
+ catch (error) {
229
+ if (saved.state === "started") {
230
+ saved.state = "uncertain";
231
+ record.state = "uncertain";
232
+ }
233
+ saved.error = redact(error instanceof Error ? error.message : String(error));
234
+ throw error;
235
+ }
236
+ }
237
+ if (options.finalVerify && !await options.finalVerify()) {
238
+ record.state = "failed";
239
+ throw new Error("Final operation verification failed");
240
+ }
241
+ record.state = "verified";
242
+ delete record.error;
243
+ await persist();
244
+ return readJournal(options.root, runId);
245
+ }
246
+ catch (error) {
247
+ record.error = redact(error instanceof Error ? error.message : String(error));
248
+ if (record.state === "planned" || record.state === "running")
249
+ record.state = "uncertain";
250
+ try {
251
+ await persist();
252
+ }
253
+ catch { /* The pre-write started marker remains the recovery authority when a later local save fails. */ }
254
+ throw new JournalRunError(runId, record.state, redact(error instanceof Error ? error.message : String(error)));
255
+ }
256
+ finally {
257
+ if (lease)
258
+ await lease.release().catch((error) => options.onStep?.(`Writer marker release needs inspection: ${redact(error instanceof Error ? error.message : String(error))}`));
259
+ }
260
+ }
@@ -0,0 +1,31 @@
1
+ import type { PenpotToolClient } from "./client.js";
2
+ import { type Operation, type OperationOptions } from "./operation.js";
3
+ interface KitProbeParams {
4
+ tag: string;
5
+ }
6
+ export declare const kitSmokeOperation: Operation<KitProbeParams>;
7
+ /** Opt-in scratch operation; uses existing active typed tokens and removes only its directly owned temporary root. */
8
+ export declare function runKitSmoke(client: PenpotToolClient, root: string, options?: OperationOptions): Promise<import("./operation.js").OperationReport | {
9
+ preview: {
10
+ path: string;
11
+ bytes: number;
12
+ sha256: string;
13
+ visualApproval: boolean;
14
+ };
15
+ complete?: boolean;
16
+ redacted?: boolean;
17
+ layout?: import("./layout.js").LayoutResult;
18
+ journal?: {
19
+ runId: string;
20
+ state: string;
21
+ };
22
+ outcome: "inspected" | "verified" | "failed";
23
+ operation: string;
24
+ phase: "inspected" | "applied";
25
+ inspect: import("./snippet.js").SnippetResult;
26
+ apply?: import("./snippet.js").SnippetResult;
27
+ readback?: import("./snippet.js").SnippetResult;
28
+ problems: string[];
29
+ verified: boolean;
30
+ }>;
31
+ export {};
@@ -0,0 +1,83 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { mkdir, readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { kit } from "./kit.js";
5
+ import { runOperation } from "./operation.js";
6
+ import { snippet } from "./snippet.js";
7
+ import { observationHash } from "./journal.js";
8
+ import { atomicWriteFile } from "./atomic-output.js";
9
+ import { resolveProjectOutput } from "./project-input.js";
10
+ const snapshotBody = `
11
+ const state=()=>{
12
+ const f=penpot.currentFile,p=penpot.currentPage,c=penpot.library.local.tokens,structure=[],stack=[p.root],seen=new Set();
13
+ while(stack.length){if(structure.length>=10000)throw new Error("Kit probe exceeds 10000 shapes");const shape=stack.pop();if(!shape.id||seen.has(shape.id)||![shape.x,shape.y,shape.width,shape.height].every(Number.isFinite))throw new Error("Incomplete kit-probe geometry");seen.add(shape.id);structure.push([shape.id,shape.type,shape.name||"",shape.x,shape.y,shape.width,shape.height,shape.tokens||{},shape.type==="text"?shape.characters:null]);for(const child of shape.children||[])stack.push(child);}
14
+ let count=0;const tokens=c.sets.map(set=>({id:set.id,name:set.name,active:!!set.active,tokens:set.active?set.tokens.map(token=>{if(++count>10000)throw new Error("Kit-probe token budget exceeded");return {name:token.name,type:token.type,value:token.value};}):[]}));
15
+ const themes=c.themes.map(theme=>({id:theme.id,group:theme.group,name:theme.name,active:!!theme.active}));
16
+ const result={fileId:f.id,pageId:p.id,structure,tokens,themes};if(JSON.stringify(result).length>30000)throw new Error("Kit probe needs a smaller scratch page/token context");return result;
17
+ };`;
18
+ export const kitSmokeOperation = {
19
+ name: "kit-smoke", page: () => undefined,
20
+ inspect: params => snippet("kit-smoke-inspect", `${snapshotBody}
21
+ const current=state(),effective=new Map();for(const set of penpot.library.local.tokens.sets)if(set.active)for(const token of set.tokens)effective.set(token.name,token);
22
+ const pick=type=>{const names=[...effective.values()].filter(token=>token.type===type).map(token=>token.name).sort();if(!names.length)throw new Error("Kit probe needs an active "+type+" token on its scratch file");return names[0];};
23
+ if(penpot.currentPage.root.children.some(shape=>shape.name===CFG.tag))throw new Error("A prior kit-probe root needs inspection before another run");
24
+ return {ok:true,readOnly:true,state:current,selected:{color:pick("color"),typo:pick("typography"),space:pick("spacing"),radius:pick("borderRadius")}};`, params, "read"),
25
+ apply: (params, inspected) => snippet("kit-smoke-apply", `${kit()}
26
+ ${snapshotBody}
27
+ if(!CFG.expected||JSON.stringify(state())!==JSON.stringify(CFG.expected))throw new Error("Kit-probe source changed before mutation");
28
+ let root=null,result={ok:true,built:false};
29
+ try{
30
+ root=penpot.createBoard();root.name=CFG.tag;root.fills=[];root.x=200000;root.resize(300,180);
31
+ const card=pp.recipe("hello-card",root,{box:{name:"Hello card",w:240,gap:CFG.selected.space,pad:[CFG.selected.space,CFG.selected.space,CFG.selected.space,CFG.selected.space],stroke:CFG.selected.color,radius:CFG.selected.radius},text:{typo:CFG.selected.typo,color:CFG.selected.color},label:"Hello, card"});
32
+ const settled=await pp.flush({root});if(!settled.layout.stable)throw new Error("Kit did not settle");
33
+ const label=card.children.find(shape=>shape.type==="text");if(!label)throw new Error("Kit text was not observed");
34
+ const actual={text:label.characters,typography:(label.tokens||{}).typography,color:(label.tokens||{}).fill,space:(card.tokens||{}).rowGap,radius:(card.tokens||{}).borderRadiusTopLeft};
35
+ const first=await root.export({type:"png",scale:1});const layout=await pp.settle(root);if(!layout.stable)throw new Error("Kit export layout did not settle");const second=await root.export({type:"png",scale:1});
36
+ if(!first||!second||!second.length||second.length>2097152||first.length!==second.length||Array.from(first).some((value,index)=>value!==second[index]))throw new Error("Kit preview did not produce bounded matching renders");
37
+ let binary="";for(let index=0;index<second.length;index+=8192)binary+=String.fromCharCode.apply(null,second.subarray(index,index+8192));
38
+ result={ok:true,built:true,actual,geometry:{width:card.width,height:card.height},previewBase64:btoa(binary),previewLength:second.length,capture:{fileId:penpot.currentFile.id,pageId:penpot.currentPage.id,frameId:root.id,revision:penpot.currentFile.revn,penpotApiVersion:typeof penpot.version==="string"?penpot.version:null},layout:settled.layout};
39
+ }catch(error){result.error=String(error&&error.message||error);}
40
+ finally{if(root)try{root.remove();}catch(error){result.cleanupError=String(error&&error.message||error);}}
41
+ return result;`, { ...params, expected: inspected?.state ?? null, selected: inspected?.selected ?? null }, "write"),
42
+ readback: () => snippet("kit-smoke-readback", `${snapshotBody}\nreturn {ok:true,readOnly:true,state:state()};`, {}, "read"),
43
+ verify({ inspect, apply, readback }) {
44
+ const selected = inspect.selected;
45
+ const problems = [];
46
+ if (!selected || apply.built !== true || apply.error || apply.cleanupError)
47
+ problems.push(`Kit example failed: ${apply.error ?? apply.cleanupError ?? "no observed build"}`);
48
+ if (!selected || observationHash(apply.actual) !== observationHash({ text: "Hello, card", typography: selected.typo, color: selected.color, space: selected.space, radius: selected.radius }))
49
+ problems.push("Kit example lacks matching independent text/binding observations");
50
+ const geometry = apply.geometry;
51
+ if (!geometry || typeof geometry.width !== "number" || !Number.isFinite(geometry.width) || geometry.width <= 0 || typeof geometry.height !== "number" || !Number.isFinite(geometry.height) || geometry.height <= 0)
52
+ problems.push("Kit example has no finite positive geometry");
53
+ if (!inspect.state || !readback.state || observationHash(inspect.state) !== observationHash(readback.state))
54
+ problems.push("Kit cleanup did not independently preserve the original page and token selection");
55
+ if (typeof apply.previewBase64 !== "string" || !Number.isInteger(apply.previewLength) || Number(apply.previewLength) < 1)
56
+ problems.push("Kit example has no stable preview evidence");
57
+ return problems;
58
+ },
59
+ };
60
+ /** Opt-in scratch operation; uses existing active typed tokens and removes only its directly owned temporary root. */
61
+ export async function runKitSmoke(client, root, options = {}) {
62
+ const runId = options.journal?.runId ?? randomUUID();
63
+ const report = await runOperation(client, kitSmokeOperation, { tag: `_penpair-kit-${runId}` }, { ...options, journal: options.apply ? { ...options.journal, root, runId } : undefined });
64
+ const encoded = report.apply?.previewBase64;
65
+ if (report.apply)
66
+ delete report.apply.previewBase64;
67
+ if (!report.verified || !report.apply)
68
+ return report;
69
+ if (typeof encoded !== "string" || !/^[A-Za-z0-9+/]*={0,2}$/.test(encoded))
70
+ throw new Error("Kit preview encoding is invalid");
71
+ const bytes = Buffer.from(encoded, "base64");
72
+ if (bytes.length !== report.apply.previewLength || bytes.length > 2097152)
73
+ throw new Error("Kit preview byte evidence is invalid");
74
+ const destination = await resolveProjectOutput(root, `.penpair/probes/kit-${runId}.png`);
75
+ await mkdir(path.dirname(destination), { recursive: true, mode: 0o700 });
76
+ const existing = await readFile(destination).catch((error) => { if (error.code !== "ENOENT")
77
+ throw error; return undefined; });
78
+ if (existing && !existing.equals(bytes))
79
+ throw new Error("Existing kit preview differs; previous evidence was preserved");
80
+ if (!existing)
81
+ await atomicWriteFile(destination, bytes, { mode: 0o600 });
82
+ return { ...report, preview: { path: destination, bytes: bytes.length, sha256: createHash("sha256").update(bytes).digest("hex"), visualApproval: false } };
83
+ }
@@ -0,0 +1,2 @@
1
+ /** Plain plugin-sandbox JavaScript. Inclusion defines helpers without touching the document. */
2
+ export declare const coreKitSource: string;