@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/kit.js
CHANGED
|
@@ -3,88 +3,156 @@ import { coreKitSource } from "./kit-source.js";
|
|
|
3
3
|
import { compiles, snippet } from "./snippet.js";
|
|
4
4
|
import { loadAdapters } from "./adapters.js";
|
|
5
5
|
import { loadOptionalConfig } from "./instance/config.js";
|
|
6
|
-
export const coreKit = {
|
|
6
|
+
export const coreKit = {
|
|
7
|
+
include: "@kit",
|
|
8
|
+
source: coreKitSource,
|
|
9
|
+
recipes: [
|
|
10
|
+
{
|
|
11
|
+
name: "hello-card",
|
|
12
|
+
description: "A neutral text-in-board example, with explicit token styles",
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
};
|
|
7
16
|
/** Plain JavaScript to concatenate into a reviewed plugin body; defines the pp namespace. */
|
|
8
17
|
export const kit = () => coreKitSource;
|
|
9
18
|
export function checkKits(kits) {
|
|
10
|
-
const problems = []
|
|
19
|
+
const problems = [];
|
|
20
|
+
const names = new Set();
|
|
21
|
+
const recipeNames = new Set();
|
|
11
22
|
const sources = kits.map((value, index) => {
|
|
12
|
-
if (!value ||
|
|
23
|
+
if (!value ||
|
|
24
|
+
typeof value !== "object" ||
|
|
25
|
+
typeof value.include !== "string" ||
|
|
26
|
+
typeof value.source !== "string") {
|
|
13
27
|
problems.push("Malformed kit at index " + index);
|
|
14
|
-
return {
|
|
28
|
+
return {
|
|
29
|
+
include: "invalid",
|
|
30
|
+
bytes: 0,
|
|
31
|
+
sha256: "",
|
|
32
|
+
};
|
|
15
33
|
}
|
|
16
|
-
if (!/^@[a-z][a-z0-9-]*$/.test(value.include) || names.has(value.include))
|
|
34
|
+
if (!/^@[a-z][a-z0-9-]*$/.test(value.include) || names.has(value.include)) {
|
|
17
35
|
problems.push("Invalid or duplicate kit include: " + value.include);
|
|
18
|
-
|
|
36
|
+
}
|
|
37
|
+
if (value.requires !== undefined &&
|
|
38
|
+
(!Array.isArray(value.requires) || value.requires.some((name) => typeof name !== "string"))) {
|
|
19
39
|
problems.push("Invalid kit dependencies: " + value.include);
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
for (const dependency of value.requires ?? []) {
|
|
43
|
+
if (!names.has(dependency)) {
|
|
23
44
|
problems.push(`${value.include} requires earlier include ${dependency}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
24
48
|
names.add(value.include);
|
|
25
|
-
if (!value.source.startsWith("// penpair: read\n"))
|
|
49
|
+
if (!value.source.startsWith("// penpair: read\n")) {
|
|
26
50
|
problems.push(`${value.include} must declare read-only top-level inclusion`);
|
|
51
|
+
}
|
|
27
52
|
const bytes = Buffer.byteLength(value.source);
|
|
28
|
-
if (bytes > 32768)
|
|
53
|
+
if (bytes > 32768) {
|
|
29
54
|
problems.push(`${value.include} exceeds the 32 KiB source budget`);
|
|
55
|
+
}
|
|
30
56
|
const error = compiles(value.source);
|
|
31
|
-
if (error)
|
|
57
|
+
if (error) {
|
|
32
58
|
problems.push(`${value.include}: ${error}`);
|
|
33
|
-
|
|
59
|
+
}
|
|
60
|
+
if (!Array.isArray(value.recipes)) {
|
|
34
61
|
problems.push(`${value.include} has invalid recipe metadata`);
|
|
35
|
-
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
36
64
|
for (const recipe of value.recipes) {
|
|
37
|
-
if (!recipe ||
|
|
65
|
+
if (!recipe ||
|
|
66
|
+
typeof recipe.name !== "string" ||
|
|
67
|
+
!/^[a-z][a-z0-9-]*$/.test(recipe.name) ||
|
|
68
|
+
typeof recipe.description !== "string") {
|
|
38
69
|
problems.push(`${value.include} has invalid recipe metadata`);
|
|
39
|
-
|
|
70
|
+
}
|
|
71
|
+
else if (recipeNames.has(recipe.name)) {
|
|
40
72
|
problems.push("Duplicate recipe: " + recipe.name);
|
|
41
|
-
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
42
75
|
recipeNames.add(recipe.name);
|
|
76
|
+
}
|
|
43
77
|
}
|
|
44
|
-
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
include: value.include,
|
|
81
|
+
bytes,
|
|
82
|
+
sha256: createHash("sha256").update(value.source).digest("hex"),
|
|
83
|
+
};
|
|
45
84
|
});
|
|
46
85
|
let bytes = sources.reduce((sum, source) => sum + source.bytes, 0);
|
|
47
86
|
try {
|
|
48
|
-
const combined = snippet("kit-check", kits
|
|
87
|
+
const combined = snippet("kit-check", kits
|
|
88
|
+
.filter((value) => value && typeof value.source === "string")
|
|
89
|
+
.map((value) => value.source)
|
|
90
|
+
.join("\n") + "\nreturn {ok:true};");
|
|
49
91
|
bytes = Buffer.byteLength(combined.code);
|
|
50
92
|
const error = compiles(combined.code);
|
|
51
|
-
if (error)
|
|
93
|
+
if (error) {
|
|
52
94
|
problems.push(error);
|
|
95
|
+
}
|
|
53
96
|
}
|
|
54
97
|
catch (error) {
|
|
55
98
|
problems.push(String(error.message));
|
|
56
99
|
}
|
|
57
|
-
if (bytes > 49152)
|
|
100
|
+
if (bytes > 49152) {
|
|
58
101
|
problems.push("Combined kit exceeds the 48 KiB budget reserved before project code");
|
|
59
|
-
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
valid: problems.length === 0,
|
|
105
|
+
bytes,
|
|
106
|
+
sources,
|
|
107
|
+
problems,
|
|
108
|
+
};
|
|
60
109
|
}
|
|
61
110
|
/** Adapter imports happen only when an explicitly requested alias cannot be supplied by the core. */
|
|
62
|
-
export async function loadKits(root, includes = [
|
|
111
|
+
export async function loadKits(root, includes = [
|
|
112
|
+
"@kit",
|
|
113
|
+
]) {
|
|
63
114
|
const config = await loadOptionalConfig(root);
|
|
64
|
-
if (includes.length > 20 || new Set(includes).size !== includes.length)
|
|
115
|
+
if (includes.length > 20 || new Set(includes).size !== includes.length) {
|
|
65
116
|
throw new Error("Use at most 20 distinct kit includes");
|
|
66
|
-
|
|
67
|
-
|
|
117
|
+
}
|
|
118
|
+
const available = new Map([
|
|
119
|
+
[
|
|
120
|
+
"@kit",
|
|
121
|
+
coreKit,
|
|
122
|
+
],
|
|
123
|
+
]);
|
|
124
|
+
if (includes.some((name) => name !== "@kit")) {
|
|
68
125
|
const loaded = await loadAdapters(root, config?.adapters);
|
|
69
126
|
for (const adapter of loaded.adapters) {
|
|
70
|
-
if (adapter.kits !== undefined && !Array.isArray(adapter.kits))
|
|
127
|
+
if (adapter.kits !== undefined && !Array.isArray(adapter.kits)) {
|
|
71
128
|
throw new Error("Adapter kits must be an array: " + adapter.name);
|
|
129
|
+
}
|
|
72
130
|
for (const value of adapter.kits ?? []) {
|
|
73
|
-
if (!value || typeof value.include !== "string")
|
|
131
|
+
if (!value || typeof value.include !== "string") {
|
|
74
132
|
throw new Error("Malformed adapter kit: " + adapter.name);
|
|
75
|
-
|
|
133
|
+
}
|
|
134
|
+
if (available.has(value.include)) {
|
|
76
135
|
throw new Error("Ambiguous kit alias: " + value.include);
|
|
136
|
+
}
|
|
77
137
|
available.set(value.include, value);
|
|
78
138
|
}
|
|
79
139
|
}
|
|
80
|
-
for (const name of includes)
|
|
81
|
-
if (!available.has(name))
|
|
140
|
+
for (const name of includes) {
|
|
141
|
+
if (!available.has(name)) {
|
|
82
142
|
throw new Error(`Unknown kit ${name}${loaded.problems.length ? ": " + loaded.problems.join("; ") : "; configure an adapter that supplies it"}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
83
145
|
}
|
|
84
|
-
const selected = includes.map(name => {
|
|
85
|
-
|
|
146
|
+
const selected = includes.map((name) => {
|
|
147
|
+
const value = available.get(name);
|
|
148
|
+
if (!value) {
|
|
149
|
+
throw new Error("Unknown kit: " + name);
|
|
150
|
+
}
|
|
151
|
+
return value;
|
|
152
|
+
});
|
|
86
153
|
const report = checkKits(selected);
|
|
87
|
-
if (!report.valid)
|
|
154
|
+
if (!report.valid) {
|
|
88
155
|
throw new Error(report.problems.join("; "));
|
|
156
|
+
}
|
|
89
157
|
return selected;
|
|
90
158
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/** Trusted, dependency-free sandbox policy shared by the host observer and shipped kit. */
|
|
2
|
+
export const layoutPolicySource = String.raw `
|
|
3
|
+
const observeLayout = async (penpot, root, options) => {
|
|
4
|
+
if (!Number.isInteger(options.maxNodes === undefined ? 10000 : options.maxNodes) || (options.maxNodes !== undefined && (options.maxNodes < 1 || options.maxNodes > 10000))) throw new Error("Invalid settling node budget");
|
|
5
|
+
const fileId = penpot.currentFile.id, pageId = penpot.currentPage.id;
|
|
6
|
+
const validTarget = () => {
|
|
7
|
+
if (penpot.currentFile.id !== fileId || penpot.currentPage.id !== pageId) throw new Error("Layout target changed while waiting");
|
|
8
|
+
if (root.id !== penpot.currentPage.root.id && typeof penpot.currentPage.getShapeById === "function" && !penpot.currentPage.getShapeById(root.id)) throw new Error("Layout target no longer exists");
|
|
9
|
+
};
|
|
10
|
+
if (!root) throw new Error("layout target does not exist");
|
|
11
|
+
let nativeOutcome = "skipped", nativeError;
|
|
12
|
+
const target = typeof root.waitForLayoutUpdate === "function" ? root : penpot;
|
|
13
|
+
if (options.nativeMs > 0) {
|
|
14
|
+
nativeOutcome = "unsupported";
|
|
15
|
+
if (typeof target.waitForLayoutUpdate === "function") {
|
|
16
|
+
let timer;
|
|
17
|
+
try {
|
|
18
|
+
await Promise.race([target.waitForLayoutUpdate(options.nativeMs), new Promise((_, reject) => { timer = setTimeout(() => reject(new Error("Native layout attempt timed out")), options.nativeMs); })]);
|
|
19
|
+
nativeOutcome = "completed";
|
|
20
|
+
} catch (error) { nativeOutcome = "failed"; nativeError = String(error && error.message || error); }
|
|
21
|
+
finally { clearTimeout(timer); }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
validTarget();
|
|
25
|
+
// Keep hierarchy and sibling order as well as geometry; repeated IDs include cycles.
|
|
26
|
+
const geometry = [], stack = [[root, null, 0]], seen = new Set();
|
|
27
|
+
while (stack.length) {
|
|
28
|
+
if (geometry.length >= (options.maxNodes || 10000)) throw new Error("Layout sample exceeded node budget");
|
|
29
|
+
const [shape, parent, index] = stack.pop();
|
|
30
|
+
if (!shape || typeof shape.id !== "string" || !shape.id || seen.has(shape.id) || ![shape.x, shape.y, shape.width, shape.height].every(Number.isFinite)) throw new Error("Malformed layout geometry or duplicate IDs");
|
|
31
|
+
seen.add(shape.id);
|
|
32
|
+
geometry.push([shape.id, shape.x, shape.y, shape.width, shape.height, parent, index]);
|
|
33
|
+
const children = shape.children || [];
|
|
34
|
+
if (!Array.isArray(children)) throw new Error("Malformed layout hierarchy");
|
|
35
|
+
if (geometry.length + stack.length + children.length > (options.maxNodes || 10000)) throw new Error("Layout sample exceeded node budget");
|
|
36
|
+
for (let index = children.length - 1; index >= 0; index--) stack.push([children[index], shape.id, index]);
|
|
37
|
+
}
|
|
38
|
+
validTarget();
|
|
39
|
+
return { ok: true, fileId, pageId, native: nativeOutcome === "completed", nativeOutcome, nativeError, geometry, truncated: false };
|
|
40
|
+
};
|
|
41
|
+
const settleLayout = async (observe, options) => {
|
|
42
|
+
const { timeoutMs, intervalMs, strategy = "auto", sleep } = options;
|
|
43
|
+
if (!["auto", "native", "geometry"].includes(strategy) || !Number.isFinite(timeoutMs) || timeoutMs < 1 || timeoutMs > 30000 || !Number.isFinite(intervalMs) || intervalMs <= 0 || Math.ceil(timeoutMs / intervalMs) + 1 > 1000) throw new Error("Invalid layout strategy or settling budgets");
|
|
44
|
+
const started = Date.now(), deadline = started + timeoutMs;
|
|
45
|
+
let samples = 0, nativeOutcome = "skipped", fallbackReason, identity, previous, equal = 0;
|
|
46
|
+
const result = (stable, method, reason) => ({ stable, method, samples, strategy, nativeOutcome, ...(fallbackReason ? { fallbackReason } : {}), elapsedMs: Date.now() - started, ...(reason ? { reason } : {}) });
|
|
47
|
+
for (let count = 0; count < Math.ceil(timeoutMs / intervalMs) + 1; count++) {
|
|
48
|
+
const remaining = deadline - Date.now();
|
|
49
|
+
if (remaining <= 0) return result(false, strategy === "native" ? "native" : "geometry", "Layout observation exceeded its deadline");
|
|
50
|
+
const nativeMs = count === 0 && strategy !== "geometry" ? strategy === "native" ? remaining : Math.max(1, Math.min(500, Math.floor(remaining / 4))) : 0;
|
|
51
|
+
const state = await observe(nativeMs, remaining);
|
|
52
|
+
samples++;
|
|
53
|
+
if (!state || state.ok !== true || typeof state.fileId !== "string" || !state.fileId || typeof state.pageId !== "string" || !state.pageId) throw new Error("Layout returned no target identity evidence");
|
|
54
|
+
if (typeof state.native !== "boolean" || typeof state.truncated !== "boolean" || state.nativeOutcome !== undefined && !["skipped", "unsupported", "completed", "failed"].includes(state.nativeOutcome)) throw new Error("Malformed layout attempt evidence");
|
|
55
|
+
const nextIdentity = JSON.stringify([state.fileId, state.pageId, state.geometry?.[0]?.[0]]);
|
|
56
|
+
if (identity && identity !== nextIdentity) throw new Error("Layout target changed between samples");
|
|
57
|
+
identity = nextIdentity;
|
|
58
|
+
if (state.truncated || !Array.isArray(state.geometry) || !state.geometry.length || state.geometry.length > 10000) throw new Error("Incomplete layout geometry evidence");
|
|
59
|
+
const ids = new Set(), childIndices = new Map();
|
|
60
|
+
for (const row of state.geometry) {
|
|
61
|
+
if (!Array.isArray(row) || row.length !== 7 || typeof row[0] !== "string" || !row[0] || ids.has(row[0]) || !row.slice(1,5).every(value => typeof value === "number" && Number.isFinite(value)) || ((row[5] !== null && !ids.has(row[5])) || !Number.isInteger(row[6]) || row[6] < 0)) throw new Error("Layout API returned no geometry evidence or malformed hierarchy");
|
|
62
|
+
if (!ids.size ? row[5] !== null || row[6] !== 0 : row[5] === null || typeof row[5] !== "string") throw new Error("Malformed layout root hierarchy");
|
|
63
|
+
if (row[5] !== null) {
|
|
64
|
+
const expectedIndex = childIndices.get(row[5]) || 0;
|
|
65
|
+
if (row[6] !== expectedIndex) throw new Error("Malformed layout sibling order");
|
|
66
|
+
childIndices.set(row[5], expectedIndex + 1);
|
|
67
|
+
}
|
|
68
|
+
ids.add(row[0]);
|
|
69
|
+
}
|
|
70
|
+
if (count === 0) {
|
|
71
|
+
nativeOutcome = state.nativeOutcome || (state.native ? state.error ? "failed" : "completed" : strategy === "geometry" ? "skipped" : "unsupported");
|
|
72
|
+
if (Date.now() >= deadline) return result(false, strategy === "native" || state.native ? "native" : "geometry", "Layout observation exceeded its deadline");
|
|
73
|
+
if (nativeOutcome === "completed") return result(true, "native");
|
|
74
|
+
if (strategy === "native") return result(false, "native", state.nativeError || state.error || "Native layout API unavailable");
|
|
75
|
+
if (strategy === "auto") fallbackReason = state.nativeError || state.error || "Native layout API unavailable";
|
|
76
|
+
}
|
|
77
|
+
if (Date.now() >= deadline) return result(false, "geometry", "Layout observation exceeded its deadline");
|
|
78
|
+
const current = JSON.stringify(state.geometry);
|
|
79
|
+
equal = current === previous ? equal + 1 : 1;
|
|
80
|
+
if (equal >= 3) return result(true, "geometry");
|
|
81
|
+
previous = current;
|
|
82
|
+
await sleep(Math.min(intervalMs, Math.max(0, deadline - Date.now())));
|
|
83
|
+
}
|
|
84
|
+
return result(false, "geometry", "Layout did not settle within its bounded samples");
|
|
85
|
+
};
|
|
86
|
+
`;
|
package/dist/layout.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { PenpotToolClient } from "./client.js";
|
|
2
|
+
export type LayoutStrategy = "auto" | "native" | "geometry";
|
|
2
3
|
export interface LayoutOptions {
|
|
3
4
|
shapeId?: string;
|
|
5
|
+
strategy?: LayoutStrategy;
|
|
4
6
|
timeoutMs?: number;
|
|
5
7
|
intervalMs?: number;
|
|
6
8
|
sleep?: (ms: number) => Promise<void>;
|
|
@@ -10,6 +12,10 @@ export interface LayoutResult {
|
|
|
10
12
|
method: "native" | "geometry" | "unavailable";
|
|
11
13
|
samples: number;
|
|
12
14
|
reason?: string;
|
|
15
|
+
strategy?: LayoutStrategy;
|
|
16
|
+
nativeOutcome?: "skipped" | "unsupported" | "completed" | "failed";
|
|
17
|
+
fallbackReason?: string;
|
|
18
|
+
elapsedMs?: number;
|
|
13
19
|
}
|
|
14
|
-
/**
|
|
20
|
+
/** Layout evidence is independent of render equality and visual approval. Transport failures propagate without fallback. */
|
|
15
21
|
export declare function waitForStableLayout(client: PenpotToolClient, options?: LayoutOptions): Promise<LayoutResult>;
|
package/dist/layout.js
CHANGED
|
@@ -1,79 +1,36 @@
|
|
|
1
|
-
import { commandSleep } from "./command-result.js";
|
|
2
1
|
import { PenpairError } from "./errors.js";
|
|
2
|
+
import { commandSleep } from "./command-result.js";
|
|
3
3
|
import { runSnippet, snippet } from "./snippet.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
const target = CFG.shapeId && typeof shape.waitForLayoutUpdate === "function" ? shape : penpot;
|
|
9
|
-
let native = false;
|
|
10
|
-
if (CFG.native && typeof target.waitForLayoutUpdate === "function") {
|
|
11
|
-
try { await target.waitForLayoutUpdate(CFG.timeout); native = true; }
|
|
12
|
-
catch (error) { return { ok: true, fileId, pageId, native: true, error: String(error && error.message || error) }; }
|
|
13
|
-
}
|
|
14
|
-
if (penpot.currentFile.id !== fileId || penpot.currentPage.id !== pageId) return { ok: false, message: "layout target changed while waiting" };
|
|
15
|
-
const geometry = [], stack = [shape];
|
|
16
|
-
while (stack.length && geometry.length < 10000) {
|
|
17
|
-
const s = stack.pop(); geometry.push([s.id, s.x, s.y, s.width, s.height]);
|
|
18
|
-
for (const child of s.children || []) stack.push(child);
|
|
19
|
-
}
|
|
20
|
-
return { ok: true, fileId, pageId, native, geometry, truncated: stack.length > 0 };`;
|
|
21
|
-
/** Native completion is preferred. Older APIs fall back to bounded geometry samples; pixel verification stays separate. */
|
|
4
|
+
import { layoutPolicySource } from "./layout-policy.js";
|
|
5
|
+
// This evaluates only the static policy above, never project or transport content.
|
|
6
|
+
const settle = new Function(`${layoutPolicySource}\nreturn settleLayout;`)();
|
|
7
|
+
/** Layout evidence is independent of render equality and visual approval. Transport failures propagate without fallback. */
|
|
22
8
|
export async function waitForStableLayout(client, options = {}) {
|
|
23
|
-
const { timeoutMs =
|
|
24
|
-
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > 30_000 || !Number.isFinite(intervalMs) || intervalMs <= 0)
|
|
25
|
-
throw new Error("Layout timeout must be in (0, 30000] ms and the interval must be positive");
|
|
26
|
-
if (Math.ceil(timeoutMs / intervalMs) + 1 > 1000)
|
|
27
|
-
throw new Error("Layout polling must stay within 1000 samples");
|
|
28
|
-
const deadline = Date.now() + timeoutMs;
|
|
29
|
-
const sample = (native) => runSnippet(client, snippet("layout-state", layoutState, { shapeId, native, timeout: Math.max(1, deadline - Date.now()) }, "read"), { retries: 0, timeoutMs: Math.max(1, deadline - Date.now()) });
|
|
30
|
-
const validGeometry = (value) => Array.isArray(value) && value.length > 0 && value.every((row) => Array.isArray(row) && row.length === 5 && typeof row[0] === "string" && !!row[0] && row.slice(1).every((entry) => typeof entry === "number" && Number.isFinite(entry)));
|
|
31
|
-
let first;
|
|
9
|
+
const { timeoutMs = 30000, intervalMs = 200, strategy = "auto", sleep = commandSleep, shapeId, } = options;
|
|
32
10
|
try {
|
|
33
|
-
|
|
11
|
+
return await settle(async (nativeMs, remaining) => runSnippet(client, snippet("layout-state", `${layoutPolicySource}\nconst shape = CFG.shapeId ? penpot.currentPage.getShapeById(CFG.shapeId) : penpot.currentPage.root;\nreturn await observeLayout(penpot, shape, CFG);`, {
|
|
12
|
+
shapeId,
|
|
13
|
+
nativeMs,
|
|
14
|
+
}, "read"), {
|
|
15
|
+
retries: 0,
|
|
16
|
+
timeoutMs: remaining,
|
|
17
|
+
}), {
|
|
18
|
+
timeoutMs,
|
|
19
|
+
intervalMs,
|
|
20
|
+
strategy,
|
|
21
|
+
sleep,
|
|
22
|
+
});
|
|
34
23
|
}
|
|
35
24
|
catch (error) {
|
|
36
|
-
if (error instanceof PenpairError && error.code === "mcp-unavailable")
|
|
37
|
-
return {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (Date.now() > deadline)
|
|
45
|
-
return { stable: false, method: first.native ? "native" : "geometry", samples: 1, reason: "Layout observation exceeded its deadline" };
|
|
46
|
-
if (first.truncated)
|
|
47
|
-
return { stable: false, method: first.native ? "native" : "geometry", samples: 1, reason: "Layout sample exceeded 10000 nodes" };
|
|
48
|
-
if (typeof first.native !== "boolean" || typeof first.truncated !== "boolean" || !validGeometry(first.geometry))
|
|
49
|
-
throw new Error("Layout API returned no geometry evidence");
|
|
50
|
-
if (first.native)
|
|
51
|
-
return { stable: true, method: "native", samples: 1 };
|
|
52
|
-
let previous = JSON.stringify(first.geometry);
|
|
53
|
-
const maxSamples = Math.ceil(timeoutMs / intervalMs) + 1;
|
|
54
|
-
let samples = 1;
|
|
55
|
-
for (let count = 2; count <= maxSamples && Date.now() <= deadline; count++) {
|
|
56
|
-
await sleep(Math.min(intervalMs, Math.max(0, deadline - Date.now())));
|
|
57
|
-
let next;
|
|
58
|
-
try {
|
|
59
|
-
next = await sample(false);
|
|
25
|
+
if (error instanceof PenpairError && error.code === "mcp-unavailable") {
|
|
26
|
+
return {
|
|
27
|
+
stable: false,
|
|
28
|
+
method: "unavailable",
|
|
29
|
+
samples: 0,
|
|
30
|
+
strategy,
|
|
31
|
+
reason: "Layout observation did not complete within its request budget",
|
|
32
|
+
};
|
|
60
33
|
}
|
|
61
|
-
|
|
62
|
-
if (error instanceof PenpairError && error.code === "mcp-unavailable")
|
|
63
|
-
return { stable: false, method: "geometry", samples, reason: "Layout did not settle within its request budget" };
|
|
64
|
-
throw error;
|
|
65
|
-
}
|
|
66
|
-
samples = count;
|
|
67
|
-
if (Date.now() > deadline)
|
|
68
|
-
return { stable: false, method: "geometry", samples, reason: "Layout did not settle before its deadline" };
|
|
69
|
-
if (next.fileId !== first.fileId || next.pageId !== first.pageId)
|
|
70
|
-
throw new Error("Layout target changed between samples");
|
|
71
|
-
if (next.ok !== true || !validGeometry(next.geometry) || next.truncated)
|
|
72
|
-
return { stable: false, method: "geometry", samples: count, reason: "Incomplete layout sample" };
|
|
73
|
-
const current = JSON.stringify(next.geometry);
|
|
74
|
-
if (current === previous)
|
|
75
|
-
return { stable: true, method: "geometry", samples: count };
|
|
76
|
-
previous = current;
|
|
34
|
+
throw error;
|
|
77
35
|
}
|
|
78
|
-
return { stable: false, method: "geometry", samples, reason: "Layout did not settle within its bounded samples" };
|
|
79
36
|
}
|
package/dist/live-smoke.js
CHANGED
|
@@ -3,12 +3,21 @@ import { runDeepCheck } from "./deep-check.js";
|
|
|
3
3
|
import { guardFile } from "./file-guard.js";
|
|
4
4
|
/** A live write probe is never scheduled blindly: the caller explicitly identifies a disposable file and tested version. */
|
|
5
5
|
export async function runLiveSmoke(client, options) {
|
|
6
|
-
if (!options.confirmedScratch ||
|
|
6
|
+
if (!options.confirmedScratch ||
|
|
7
|
+
!options.scratchFileId.trim() ||
|
|
8
|
+
!/^\d+\.\d+\.\d+$/.test(options.expectedVersion)) {
|
|
7
9
|
throw new Error("Live smoke requires an explicitly confirmed scratch file ID and expected Penpot version");
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
+
}
|
|
11
|
+
const guarded = guardFile(client, {
|
|
12
|
+
id: options.scratchFileId,
|
|
13
|
+
});
|
|
14
|
+
const { result } = await executeCode(guarded, "return { version: penpot.version, fileId: penpot.currentFile.id };", {
|
|
15
|
+
effect: "read",
|
|
16
|
+
retries: 0,
|
|
17
|
+
});
|
|
10
18
|
const actual = result;
|
|
11
|
-
if (actual?.version !== options.expectedVersion || actual?.fileId !== options.scratchFileId)
|
|
19
|
+
if (actual?.version !== options.expectedVersion || actual?.fileId !== options.scratchFileId) {
|
|
12
20
|
throw new Error("Live smoke target/version differs from the pinned expectation. No probe was applied.");
|
|
21
|
+
}
|
|
13
22
|
return runDeepCheck(guarded, options);
|
|
14
23
|
}
|
package/dist/local-profile.js
CHANGED
|
@@ -2,7 +2,8 @@ import { ConfigError } from "./instance/config.js";
|
|
|
2
2
|
/** The commands that run Penpot on this computer exist only when a package provides a profile for them. */
|
|
3
3
|
export const needsLocalProfile = "Running Penpot on this computer is not part of this install. It comes with `@penpair/docker`: install it with `npm i -D @penpair/docker` and run `npx penpair …` (or, once, `npx @penpair/docker <command>`). Penpot on the web needs nothing extra.";
|
|
4
4
|
export function requireLocalProfile(local) {
|
|
5
|
-
if (!local)
|
|
5
|
+
if (!local) {
|
|
6
6
|
throw new ConfigError(needsLocalProfile);
|
|
7
|
+
}
|
|
7
8
|
return local;
|
|
8
9
|
}
|