@penpair/core 0.3.7 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters.d.ts +1 -1
- package/dist/adapters.js +3 -0
- package/dist/approvals.d.ts +4 -1
- package/dist/approvals.js +7 -4
- package/dist/baseline-coverage.js +3 -3
- package/dist/baselines-config.d.ts +3 -0
- package/dist/baselines-config.js +3 -0
- package/dist/baselines.d.ts +7 -0
- package/dist/baselines.js +90 -21
- package/dist/bounded-file.d.ts +2 -0
- package/dist/bounded-file.js +38 -0
- package/dist/cli-main.js +64 -3
- package/dist/design-commands/context.d.ts +9 -0
- package/dist/design-commands/context.js +1 -0
- package/dist/design-commands/index.d.ts +3 -0
- package/dist/design-commands/index.js +7 -0
- package/dist/design-commands/map-command.d.ts +4 -0
- package/dist/design-commands/map-command.js +685 -0
- package/dist/design-commands/release-command.d.ts +3 -0
- package/dist/design-commands/release-command.js +239 -0
- package/dist/design-releases/artifact-paths.d.ts +6 -0
- package/dist/design-releases/artifact-paths.js +82 -0
- package/dist/design-releases/assemble.d.ts +3 -0
- package/dist/design-releases/assemble.js +176 -0
- package/dist/design-releases/check.d.ts +16 -0
- package/dist/design-releases/check.js +323 -0
- package/dist/design-releases/context.d.ts +2 -0
- package/dist/design-releases/context.js +119 -0
- package/dist/design-releases/contract-graph.d.ts +13 -0
- package/dist/design-releases/contract-graph.js +75 -0
- package/dist/design-releases/fidelity.d.ts +23 -0
- package/dist/design-releases/fidelity.js +328 -0
- package/dist/design-releases/finalize.d.ts +4 -0
- package/dist/design-releases/finalize.js +201 -0
- package/dist/design-releases/index.d.ts +8 -0
- package/dist/design-releases/index.js +7 -0
- package/dist/design-releases/live.d.ts +22 -0
- package/dist/design-releases/live.js +97 -0
- package/dist/design-releases/snippets.d.ts +5 -0
- package/dist/design-releases/snippets.js +89 -0
- package/dist/design-releases/store.d.ts +10 -0
- package/dist/design-releases/store.js +205 -0
- package/dist/design-releases/types.d.ts +161 -0
- package/dist/design-releases/types.js +1 -0
- package/dist/design-releases/validation.d.ts +6 -0
- package/dist/design-releases/validation.js +384 -0
- package/dist/design-releases/verify.d.ts +3 -0
- package/dist/design-releases/verify.js +148 -0
- package/dist/image-codec.d.ts +3 -0
- package/dist/image-codec.js +78 -0
- package/dist/image-evidence.d.ts +15 -0
- package/dist/image-evidence.js +121 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/inspection.js +13 -5
- package/dist/instance/config-schema.d.ts +4 -0
- package/dist/instance/config-schema.js +7 -0
- package/dist/map-penpot/content-validation.d.ts +3 -0
- package/dist/map-penpot/content-validation.js +57 -0
- package/dist/map-penpot/content.d.ts +22 -0
- package/dist/map-penpot/content.js +173 -0
- package/dist/map-penpot/details.d.ts +4 -0
- package/dist/map-penpot/details.js +84 -0
- package/dist/map-penpot/evidence-error.d.ts +5 -0
- package/dist/map-penpot/evidence-error.js +9 -0
- package/dist/map-penpot/icons.d.ts +5 -0
- package/dist/map-penpot/icons.js +79 -0
- package/dist/map-penpot/import-content.d.ts +2 -0
- package/dist/map-penpot/import-content.js +150 -0
- package/dist/map-penpot/import.d.ts +5 -0
- package/dist/map-penpot/import.js +255 -0
- package/dist/map-penpot/index.d.ts +11 -0
- package/dist/map-penpot/index.js +10 -0
- package/dist/map-penpot/path-geometry.d.ts +5 -0
- package/dist/map-penpot/path-geometry.js +51 -0
- package/dist/map-penpot/presentation.d.ts +20 -0
- package/dist/map-penpot/presentation.js +205 -0
- package/dist/map-penpot/projection.d.ts +8 -0
- package/dist/map-penpot/projection.js +176 -0
- package/dist/map-penpot/publish.d.ts +4 -0
- package/dist/map-penpot/publish.js +301 -0
- package/dist/map-penpot/release-maps.d.ts +21 -0
- package/dist/map-penpot/release-maps.js +104 -0
- package/dist/map-penpot/snippets.d.ts +4 -0
- package/dist/map-penpot/snippets.js +164 -0
- package/dist/map-penpot/status-labels.d.ts +7 -0
- package/dist/map-penpot/status-labels.js +7 -0
- package/dist/map-penpot/types.d.ts +161 -0
- package/dist/map-penpot/types.js +1 -0
- package/dist/maps/annotation-layout.d.ts +24 -0
- package/dist/maps/annotation-layout.js +85 -0
- package/dist/maps/context.d.ts +13 -0
- package/dist/maps/context.js +207 -0
- package/dist/maps/index.d.ts +9 -0
- package/dist/maps/index.js +9 -0
- package/dist/maps/layout.d.ts +35 -0
- package/dist/maps/layout.js +364 -0
- package/dist/maps/proposal.d.ts +5 -0
- package/dist/maps/proposal.js +67 -0
- package/dist/maps/render.d.ts +4 -0
- package/dist/maps/render.js +331 -0
- package/dist/maps/review.d.ts +16 -0
- package/dist/maps/review.js +90 -0
- package/dist/maps/store.d.ts +21 -0
- package/dist/maps/store.js +222 -0
- package/dist/maps/types.d.ts +206 -0
- package/dist/maps/types.js +1 -0
- package/dist/maps/validation.d.ts +10 -0
- package/dist/maps/validation.js +375 -0
- package/dist/skill-routing.js +4 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -1
- package/skills/penpair-design-release/SKILL.md +55 -0
- package/skills/penpair-design-release/agents/openai.yaml +6 -0
- package/skills/penpair-design-release/references/workflow.md +55 -0
- package/skills/penpair-diagrams/SKILL.md +52 -0
- package/skills/penpair-diagrams/agents/openai.yaml +6 -0
- package/skills/penpair-diagrams/references/workflow.md +84 -0
- package/skills/penpair-handoff/SKILL.md +7 -0
- package/skills/penpair-handoff/references/handoff-flow.md +7 -1
- package/skills/penpair-integration-map/SKILL.md +44 -0
- package/skills/penpair-integration-map/agents/openai.yaml +6 -0
- package/skills/penpair-integration-map/references/workflow.md +33 -0
- package/skills/penpair-journey-map/SKILL.md +42 -0
- package/skills/penpair-journey-map/agents/openai.yaml +6 -0
- package/skills/penpair-journey-map/references/workflow.md +35 -0
- package/skills/penpair-verify/SKILL.md +7 -0
- package/skills/penpair-verify/references/browser-evidence.md +10 -0
package/dist/adapters.d.ts
CHANGED
|
@@ -51,7 +51,7 @@ export interface Adapter {
|
|
|
51
51
|
/** Declares an adapter. Returns the definition unchanged, so a module can `export default defineAdapter({...})` with types checked. */
|
|
52
52
|
export declare const defineAdapter: <T extends Adapter>(adapter: T) => T;
|
|
53
53
|
/** The commands of the core itself; an adapter cannot take one of these names. */
|
|
54
|
-
export declare const reservedCommands: readonly ["formats", "snapshot", "approve", "mcp", "spec", "inspect", "probe", "pages", "tree", "audit", "tokens", "export", "shots", "archive", "snippets", "run", "run-file", "kit", "recipes", "skills", "init", "setup", "connect", "up", "down", "open", "status", "doctor", "backup", "restore", "baselines", "config", "runs", "onboard", "help"];
|
|
54
|
+
export declare const reservedCommands: readonly ["journey-map", "integration-map", "design-release", "formats", "snapshot", "approve", "mcp", "spec", "inspect", "probe", "pages", "tree", "audit", "tokens", "export", "shots", "archive", "snippets", "run", "run-file", "kit", "recipes", "skills", "init", "setup", "connect", "up", "down", "open", "status", "doctor", "backup", "restore", "baselines", "config", "runs", "onboard", "help"];
|
|
55
55
|
export interface LoadedAdapters {
|
|
56
56
|
adapters: Adapter[];
|
|
57
57
|
problems: string[];
|
package/dist/adapters.js
CHANGED
|
@@ -7,6 +7,9 @@ import { readProjectConfig } from "./instance/config.js";
|
|
|
7
7
|
export const defineAdapter = (adapter) => adapter;
|
|
8
8
|
/** The commands of the core itself; an adapter cannot take one of these names. */
|
|
9
9
|
export const reservedCommands = [
|
|
10
|
+
"journey-map",
|
|
11
|
+
"integration-map",
|
|
12
|
+
"design-release",
|
|
10
13
|
"formats",
|
|
11
14
|
"snapshot",
|
|
12
15
|
"approve",
|
package/dist/approvals.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CaptureIdentity, ExportResult } from "./export.js";
|
|
2
2
|
import { type BaselineCase } from "./baselines-config.js";
|
|
3
|
+
import type { BaselineImageFormat } from "./image-evidence.js";
|
|
3
4
|
export interface ApprovalRecord {
|
|
4
5
|
schemaVersion: 1;
|
|
5
6
|
id: string;
|
|
@@ -28,5 +29,7 @@ export declare function readApprovals(root: string): Promise<ApprovalFile>;
|
|
|
28
29
|
export declare function recordApproval(root: string, exported: ExportResult, words: {
|
|
29
30
|
note: string;
|
|
30
31
|
by: string;
|
|
31
|
-
}, intent?: BaselineCase, redact?: (text: string) => string
|
|
32
|
+
}, intent?: BaselineCase, redact?: (text: string) => string, options?: {
|
|
33
|
+
format?: BaselineImageFormat;
|
|
34
|
+
}): Promise<ApprovalRecord>;
|
|
32
35
|
export declare function matchingApproval(root: string, capture: CaptureIdentity, sha256: string, intent?: BaselineCase): Promise<ApprovalRecord | undefined>;
|
package/dist/approvals.js
CHANGED
|
@@ -7,6 +7,7 @@ import { observationHash } from "./journal.js";
|
|
|
7
7
|
import { resolveProjectInput, resolveProjectOutput } from "./project-input.js";
|
|
8
8
|
import { validateBaselineCase } from "./baselines-config.js";
|
|
9
9
|
import { pngSize } from "./png.js";
|
|
10
|
+
import { encodeBaselineImage } from "./image-codec.js";
|
|
10
11
|
export const approvalFile = "design/approvals.json";
|
|
11
12
|
export const artifactSha = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
12
13
|
export function approvalFingerprint(capture, sha256, intent) {
|
|
@@ -91,7 +92,7 @@ export async function readApprovals(root) {
|
|
|
91
92
|
}
|
|
92
93
|
}
|
|
93
94
|
/** Records supplied owner words; hashes detect accidental changes, not identity or consent. */
|
|
94
|
-
export async function recordApproval(root, exported, words, intent, redact) {
|
|
95
|
+
export async function recordApproval(root, exported, words, intent, redact, options = {}) {
|
|
95
96
|
exported = {
|
|
96
97
|
...exported,
|
|
97
98
|
capture: structuredClone(exported.capture),
|
|
@@ -113,7 +114,9 @@ export async function recordApproval(root, exported, words, intent, redact) {
|
|
|
113
114
|
words.by.length > 200) {
|
|
114
115
|
throw new Error("Supply the owner's exact nonempty words and identity; an agent cannot invent approval");
|
|
115
116
|
}
|
|
116
|
-
const
|
|
117
|
+
const format = options.format ?? "png";
|
|
118
|
+
const imageBytes = await encodeBaselineImage(exported.png, format);
|
|
119
|
+
const sha256 = artifactSha(imageBytes);
|
|
117
120
|
const fingerprint = approvalFingerprint(exported.capture, sha256, intent);
|
|
118
121
|
if (intent &&
|
|
119
122
|
(!matchesDesignViewport(exported.capture.designSize, intent.viewport) ||
|
|
@@ -142,7 +145,7 @@ export async function recordApproval(root, exported, words, intent, redact) {
|
|
|
142
145
|
}
|
|
143
146
|
: {}),
|
|
144
147
|
sha256,
|
|
145
|
-
image: `design/approvals/${sha256}
|
|
148
|
+
image: `design/approvals/${sha256}.${format}`,
|
|
146
149
|
fingerprint,
|
|
147
150
|
};
|
|
148
151
|
const record = {
|
|
@@ -168,7 +171,7 @@ export async function recordApproval(root, exported, words, intent, redact) {
|
|
|
168
171
|
recursive: true,
|
|
169
172
|
});
|
|
170
173
|
try {
|
|
171
|
-
await atomicWriteFile(image,
|
|
174
|
+
await atomicWriteFile(image, imageBytes);
|
|
172
175
|
}
|
|
173
176
|
catch (error) {
|
|
174
177
|
if (error.code !== "EEXIST" ||
|
|
@@ -2,7 +2,7 @@ import { open } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { resolveProjectOutput } from "./project-input.js";
|
|
5
|
-
import {
|
|
5
|
+
import { inspectImageEvidence } from "./image-evidence.js";
|
|
6
6
|
import { matchesDesignViewport } from "./design-viewport.js";
|
|
7
7
|
import { validateBaselines, } from "./baselines-config.js";
|
|
8
8
|
import { observationHash } from "./journal.js";
|
|
@@ -228,13 +228,13 @@ export async function readBaselineCoverage(root, config, expectedFileId) {
|
|
|
228
228
|
}
|
|
229
229
|
const target = await resolveProjectOutput(root, path.join(config.root, result.file));
|
|
230
230
|
const bytes = await boundedRead(target, 64 * 1024 * 1024);
|
|
231
|
-
const size =
|
|
231
|
+
const size = inspectImageEvidence(bytes);
|
|
232
232
|
if (!size ||
|
|
233
233
|
size.width < 1 ||
|
|
234
234
|
size.height < 1 ||
|
|
235
235
|
createHash("sha256").update(bytes).digest("hex") !== result.approved.sha256 ||
|
|
236
236
|
result.sha256 !== result.approved.sha256) {
|
|
237
|
-
throw new Error("Approved
|
|
237
|
+
throw new Error("Approved image bytes do not match their manifest");
|
|
238
238
|
}
|
|
239
239
|
result.imageWidth = size.width;
|
|
240
240
|
result.imageHeight = size.height;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { BaselineImageFormat } from "./image-evidence.js";
|
|
1
2
|
/** Declared design intent. It does not switch themes or establish browser behavior. */
|
|
2
3
|
export interface BaselineCase {
|
|
3
4
|
viewport: {
|
|
@@ -34,6 +35,8 @@ export interface BaselineFrame {
|
|
|
34
35
|
}
|
|
35
36
|
/** What `penpair baselines` exports: the frames of one page, as images in a folder of the project. */
|
|
36
37
|
export interface BaselinesConfig {
|
|
38
|
+
/** Encoding for new references; existing approved formats require explicit migration. */
|
|
39
|
+
format?: BaselineImageFormat;
|
|
37
40
|
page: string;
|
|
38
41
|
/** Folder for the images, relative to the project. */
|
|
39
42
|
root: string;
|
package/dist/baselines-config.js
CHANGED
|
@@ -6,6 +6,9 @@ export function validateBaselines(value, fail) {
|
|
|
6
6
|
return fail("baselines must be an object");
|
|
7
7
|
}
|
|
8
8
|
const v = value;
|
|
9
|
+
if (v.format !== undefined && v.format !== "png" && v.format !== "webp") {
|
|
10
|
+
fail("baselines.format must be png or webp");
|
|
11
|
+
}
|
|
9
12
|
if (typeof v.page !== "string" || v.page.length === 0) {
|
|
10
13
|
fail("baselines.page must be the name of a page");
|
|
11
14
|
}
|
package/dist/baselines.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type BaselineImageFormat } from "./image-evidence.js";
|
|
1
2
|
export { pngSize } from "./png.js";
|
|
2
3
|
import type { PenpotToolClient } from "./client.js";
|
|
3
4
|
import { type BaselineCoverage } from "./baseline-coverage.js";
|
|
@@ -70,6 +71,10 @@ export interface BaselinesReport {
|
|
|
70
71
|
wrote: string[];
|
|
71
72
|
}
|
|
72
73
|
export interface BaselinesOptions extends ExportOptions {
|
|
74
|
+
/** Encoding for new references, or the explicit migration target. */
|
|
75
|
+
format?: BaselineImageFormat;
|
|
76
|
+
/** Change an existing reference's encoding; apply also requires update and exact-artifact approval. */
|
|
77
|
+
migrateFormat?: boolean;
|
|
73
78
|
signal?: AbortSignal;
|
|
74
79
|
/** Write images that are new. Without it nothing is written, whatever is found. */
|
|
75
80
|
apply?: boolean;
|
|
@@ -78,6 +83,8 @@ export interface BaselinesOptions extends ExportOptions {
|
|
|
78
83
|
/** Explicit waiver for a changed baseline; retained as a forced reference, never an owner approval. */
|
|
79
84
|
forceReason?: string;
|
|
80
85
|
}
|
|
86
|
+
/** Resolve the exact encoding used by both approval and baseline capture. */
|
|
87
|
+
export declare function resolveBaselineImageFormat(projectRoot: string, config: BaselinesConfig, baselineId: string, options?: Pick<BaselinesOptions, "format" | "migrateFormat">): Promise<BaselineImageFormat>;
|
|
81
88
|
/**
|
|
82
89
|
* Exports the configured frames of a page as images into the project's folder: the approved design that an implementation is
|
|
83
90
|
* checked against. Looks first; writes only with `apply`, and replaces an image that differs only with `update`, so a change of
|
package/dist/baselines.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { matchingApproval } from "./approvals.js";
|
|
2
2
|
import { matchesDesignViewport } from "./design-viewport.js";
|
|
3
3
|
import { pngSize } from "./png.js";
|
|
4
|
+
import { encodeBaselineImage } from "./image-codec.js";
|
|
5
|
+
import { inspectImageEvidence } from "./image-evidence.js";
|
|
4
6
|
export { pngSize } from "./png.js";
|
|
5
7
|
import { createHash, randomUUID } from "node:crypto";
|
|
6
|
-
import { mkdir, readFile, writeFile, rename, rm } from "node:fs/promises";
|
|
8
|
+
import { mkdir, readFile, writeFile, rename, rm, stat } from "node:fs/promises";
|
|
7
9
|
import path from "node:path";
|
|
8
10
|
import { baselineCoverage } from "./baseline-coverage.js";
|
|
9
11
|
import { resolveProjectOutput } from "./project-input.js";
|
|
@@ -16,9 +18,16 @@ import { runOnPage } from "./operation.js";
|
|
|
16
18
|
import { frames } from "./snippets/read.js";
|
|
17
19
|
export * from "./baselines-config.js";
|
|
18
20
|
const sha = (buffer) => createHash("sha256").update(buffer).digest("hex");
|
|
19
|
-
async function readIfExists(file) {
|
|
21
|
+
async function readIfExists(file, maximum = 64 * 1024 * 1024) {
|
|
20
22
|
try {
|
|
21
|
-
|
|
23
|
+
if ((await stat(file)).size > maximum) {
|
|
24
|
+
throw new Error("Baseline artifact exceeds its read budget");
|
|
25
|
+
}
|
|
26
|
+
const bytes = await readFile(file);
|
|
27
|
+
if (bytes.length > maximum) {
|
|
28
|
+
throw new Error("Baseline artifact exceeds its read budget");
|
|
29
|
+
}
|
|
30
|
+
return bytes;
|
|
22
31
|
}
|
|
23
32
|
catch (error) {
|
|
24
33
|
if (error.code === "ENOENT") {
|
|
@@ -27,12 +36,72 @@ async function readIfExists(file) {
|
|
|
27
36
|
throw error;
|
|
28
37
|
}
|
|
29
38
|
}
|
|
39
|
+
async function existingBaseline(projectRoot, config, id, old) {
|
|
40
|
+
const candidates = old
|
|
41
|
+
? [
|
|
42
|
+
old.file,
|
|
43
|
+
]
|
|
44
|
+
: [
|
|
45
|
+
`${id}.png`,
|
|
46
|
+
`${id}.webp`,
|
|
47
|
+
];
|
|
48
|
+
let found = {};
|
|
49
|
+
for (const file of candidates) {
|
|
50
|
+
if (typeof file !== "string" || path.isAbsolute(file) || file.split(/[\\/]/).includes("..")) {
|
|
51
|
+
throw new Error("Existing baseline path leaves its folder");
|
|
52
|
+
}
|
|
53
|
+
const bytes = await readIfExists(await resolveProjectOutput(projectRoot, path.join(config.root, file)));
|
|
54
|
+
if (!bytes) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (found.bytes) {
|
|
58
|
+
throw new Error("Existing baseline candidates are ambiguous; inspect before capture");
|
|
59
|
+
}
|
|
60
|
+
found = {
|
|
61
|
+
file,
|
|
62
|
+
bytes,
|
|
63
|
+
format: inspectImageEvidence(bytes).format,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
if (old &&
|
|
67
|
+
(!found.bytes ||
|
|
68
|
+
old.sha256 !== sha(found.bytes) ||
|
|
69
|
+
(old.approved && old.approved.sha256 !== old.sha256))) {
|
|
70
|
+
throw new Error("Approved baseline bytes no longer match their manifest; inspect before replacing");
|
|
71
|
+
}
|
|
72
|
+
return found;
|
|
73
|
+
}
|
|
74
|
+
/** Resolve the exact encoding used by both approval and baseline capture. */
|
|
75
|
+
export async function resolveBaselineImageFormat(projectRoot, config, baselineId, options = {}) {
|
|
76
|
+
const target = options.format ?? config.format ?? "webp";
|
|
77
|
+
if (target !== "png" && target !== "webp") {
|
|
78
|
+
throw new Error("Baseline format must be png or webp");
|
|
79
|
+
}
|
|
80
|
+
const manifestPath = await resolveProjectOutput(projectRoot, path.join(config.root, "penpair-baselines.json"));
|
|
81
|
+
const bytes = await readIfExists(manifestPath, 16 * 1024 * 1024);
|
|
82
|
+
const previous = bytes ? JSON.parse(bytes.toString("utf8")) : undefined;
|
|
83
|
+
if (previous &&
|
|
84
|
+
((previous.schemaVersion !== undefined && previous.schemaVersion !== 2) ||
|
|
85
|
+
!Array.isArray(previous.frames) ||
|
|
86
|
+
previous.frames.length > 1000)) {
|
|
87
|
+
throw new Error("Existing baseline manifest is invalid; preserve and inspect it");
|
|
88
|
+
}
|
|
89
|
+
const old = previous?.frames?.find((entry) => entry.id === baselineId);
|
|
90
|
+
const existing = await existingBaseline(projectRoot, config, baselineId, old);
|
|
91
|
+
return existing.format && !options.migrateFormat ? existing.format : target;
|
|
92
|
+
}
|
|
30
93
|
/**
|
|
31
94
|
* Exports the configured frames of a page as images into the project's folder: the approved design that an implementation is
|
|
32
95
|
* checked against. Looks first; writes only with `apply`, and replaces an image that differs only with `update`, so a change of
|
|
33
96
|
* the design never silently becomes the new reference.
|
|
34
97
|
*/
|
|
35
98
|
export async function exportBaselines(client, projectRoot, config, options = {}) {
|
|
99
|
+
if (options.format !== undefined && options.format !== "png" && options.format !== "webp") {
|
|
100
|
+
throw new Error("Baseline format must be png or webp");
|
|
101
|
+
}
|
|
102
|
+
if (options.migrateFormat && options.apply && !options.update) {
|
|
103
|
+
throw new Error("Applying a baseline format migration requires update");
|
|
104
|
+
}
|
|
36
105
|
if (options.forceReason !== undefined &&
|
|
37
106
|
(!options.update ||
|
|
38
107
|
!options.apply ||
|
|
@@ -65,7 +134,7 @@ export async function exportBaselines(client, projectRoot, config, options = {})
|
|
|
65
134
|
const wrote = [];
|
|
66
135
|
const folder = await resolveProjectOutput(projectRoot, config.root);
|
|
67
136
|
const manifestPath = await resolveProjectOutput(projectRoot, path.join(config.root, "penpair-baselines.json"));
|
|
68
|
-
const previousBytes = await readIfExists(manifestPath);
|
|
137
|
+
const previousBytes = await readIfExists(manifestPath, 16 * 1024 * 1024);
|
|
69
138
|
const previous = previousBytes
|
|
70
139
|
? JSON.parse(previousBytes.toString("utf8"))
|
|
71
140
|
: undefined;
|
|
@@ -80,7 +149,13 @@ export async function exportBaselines(client, projectRoot, config, options = {})
|
|
|
80
149
|
for (const wanted of config.frames) {
|
|
81
150
|
options.signal?.throwIfAborted();
|
|
82
151
|
const old = previous?.frames?.find((entry) => entry.id === wanted.id);
|
|
83
|
-
const
|
|
152
|
+
const existingEvidence = await existingBaseline(projectRoot, config, wanted.id, old);
|
|
153
|
+
const format = existingEvidence.format && !options.migrateFormat
|
|
154
|
+
? existingEvidence.format
|
|
155
|
+
: (options.format ?? config.format ?? "webp");
|
|
156
|
+
const migrating = Boolean(existingEvidence.format && existingEvidence.format !== format);
|
|
157
|
+
const existing = existingEvidence.bytes;
|
|
158
|
+
const file = existingEvidence.file ?? `${wanted.id}.${format}`;
|
|
84
159
|
if (path.isAbsolute(file) || file.split(/[\\/]/).includes("..")) {
|
|
85
160
|
throw new Error("Existing baseline path leaves its folder");
|
|
86
161
|
}
|
|
@@ -157,13 +232,7 @@ export async function exportBaselines(client, projectRoot, config, options = {})
|
|
|
157
232
|
if (!pngSize(exported.png)) {
|
|
158
233
|
throw new Error("Baseline export is not a PNG with dimensions");
|
|
159
234
|
}
|
|
160
|
-
const
|
|
161
|
-
if (old &&
|
|
162
|
-
(!existing ||
|
|
163
|
-
old.sha256 !== sha(existing) ||
|
|
164
|
-
(old.approved && old.approved.sha256 !== old.sha256))) {
|
|
165
|
-
throw new Error("Approved baseline bytes no longer match their manifest; inspect before replacing");
|
|
166
|
-
}
|
|
235
|
+
const encoded = await encodeBaselineImage(exported.png, format);
|
|
167
236
|
const captured = {
|
|
168
237
|
...(wanted.case
|
|
169
238
|
? {
|
|
@@ -172,10 +241,10 @@ export async function exportBaselines(client, projectRoot, config, options = {})
|
|
|
172
241
|
: {}),
|
|
173
242
|
layout: exported.layout,
|
|
174
243
|
...exported.capture,
|
|
175
|
-
sha256: sha(
|
|
244
|
+
sha256: sha(encoded),
|
|
176
245
|
capturedAt: new Date().toISOString(),
|
|
177
246
|
toolVersion: version,
|
|
178
|
-
file: `${generation}/${wanted.id}
|
|
247
|
+
file: `${generation}/${wanted.id}.${format}`,
|
|
179
248
|
};
|
|
180
249
|
const caseProblems = [];
|
|
181
250
|
if (wanted.case) {
|
|
@@ -237,7 +306,7 @@ export async function exportBaselines(client, projectRoot, config, options = {})
|
|
|
237
306
|
recordHash: record.hash,
|
|
238
307
|
};
|
|
239
308
|
}
|
|
240
|
-
else if (options.forceReason) {
|
|
309
|
+
else if (options.forceReason && !migrating) {
|
|
241
310
|
captured.approval = {
|
|
242
311
|
kind: "forced-reference",
|
|
243
312
|
reason: options.forceReason,
|
|
@@ -256,8 +325,8 @@ export async function exportBaselines(client, projectRoot, config, options = {})
|
|
|
256
325
|
legacy: true,
|
|
257
326
|
sha256: sha(existing),
|
|
258
327
|
});
|
|
259
|
-
const image = approve ?
|
|
260
|
-
const size =
|
|
328
|
+
const image = approve ? encoded : existing;
|
|
329
|
+
const size = inspectImageEvidence(image);
|
|
261
330
|
results.push({
|
|
262
331
|
...base,
|
|
263
332
|
file: approve && options.apply ? captured.file : file,
|
|
@@ -270,7 +339,7 @@ export async function exportBaselines(client, projectRoot, config, options = {})
|
|
|
270
339
|
imageWidth: size?.width,
|
|
271
340
|
imageHeight: size?.height,
|
|
272
341
|
});
|
|
273
|
-
captures.set(wanted.id
|
|
342
|
+
captures.set(`${wanted.id}.${format}`, encoded);
|
|
274
343
|
}
|
|
275
344
|
const after = await runOnPage(client, frames({
|
|
276
345
|
page: config.page,
|
|
@@ -311,10 +380,10 @@ export async function exportBaselines(client, projectRoot, config, options = {})
|
|
|
311
380
|
const staged = path.join(folder, `.stage-${runId}`);
|
|
312
381
|
await mkdir(staged);
|
|
313
382
|
try {
|
|
314
|
-
for (const [
|
|
383
|
+
for (const [file, bytes] of captures) {
|
|
315
384
|
options.signal?.throwIfAborted();
|
|
316
|
-
await atomicWriteFile(path.join(staged,
|
|
317
|
-
options.onStep?.(`staging ${
|
|
385
|
+
await atomicWriteFile(path.join(staged, file), bytes);
|
|
386
|
+
options.onStep?.(`staging ${path.parse(file).name}`);
|
|
318
387
|
}
|
|
319
388
|
const cases = reflectionCases(config, results);
|
|
320
389
|
await writeFile(path.join(staged, "reflection-cases.json"), `${JSON.stringify({
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { lstat, open } from "node:fs/promises";
|
|
3
|
+
import { resolveProjectInput } from "./project-input.js";
|
|
4
|
+
/** Read a regular project file with an allocation bound even if another process grows it. */
|
|
5
|
+
export async function readProjectBytes(root, file, maxBytes) {
|
|
6
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 1) {
|
|
7
|
+
throw new Error("A positive file byte budget is required");
|
|
8
|
+
}
|
|
9
|
+
const input = await resolveProjectInput(root, file);
|
|
10
|
+
if (!(await lstat(input)).isFile()) {
|
|
11
|
+
throw new Error("Artifact input must be a regular file");
|
|
12
|
+
}
|
|
13
|
+
const handle = await open(input, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
|
|
14
|
+
try {
|
|
15
|
+
const info = await handle.stat();
|
|
16
|
+
if (!info.isFile() || info.size > maxBytes) {
|
|
17
|
+
throw new Error("Artifact input is not a regular file within its byte budget");
|
|
18
|
+
}
|
|
19
|
+
const chunks = [];
|
|
20
|
+
let total = 0;
|
|
21
|
+
while (total <= maxBytes) {
|
|
22
|
+
const chunk = Buffer.alloc(Math.min(64 * 1024, maxBytes + 1 - total));
|
|
23
|
+
const { bytesRead } = await handle.read(chunk, 0, chunk.length, total);
|
|
24
|
+
if (bytesRead === 0) {
|
|
25
|
+
return Buffer.concat(chunks, total);
|
|
26
|
+
}
|
|
27
|
+
total += bytesRead;
|
|
28
|
+
if (total > maxBytes) {
|
|
29
|
+
throw new Error("Artifact input exceeds its byte budget");
|
|
30
|
+
}
|
|
31
|
+
chunks.push(chunk.subarray(0, bytesRead));
|
|
32
|
+
}
|
|
33
|
+
throw new Error("Artifact input exceeds its byte budget");
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
await handle.close();
|
|
37
|
+
}
|
|
38
|
+
}
|
package/dist/cli-main.js
CHANGED
|
@@ -23,7 +23,8 @@ import { resolveProjectOutput } from "./project-input.js";
|
|
|
23
23
|
import { mkdir } from "node:fs/promises";
|
|
24
24
|
import { captureShots, exportShapeByName, findShapesByName } from "./shots.js";
|
|
25
25
|
import { exportShapePng } from "./export.js";
|
|
26
|
-
import { exportBaselines } from "./baselines.js";
|
|
26
|
+
import { exportBaselines, resolveBaselineImageFormat } from "./baselines.js";
|
|
27
|
+
import { encodeBaselineImage } from "./image-codec.js";
|
|
27
28
|
import { archiveOperation, runOnPage, runOperation } from "./operation.js";
|
|
28
29
|
import { formatDeepReport, runDeepCheck } from "./deep-check.js";
|
|
29
30
|
import { probe } from "./probe.js";
|
|
@@ -54,6 +55,11 @@ import { backupNoteCheck } from "./backup-note.js";
|
|
|
54
55
|
import { createCheckpoint } from "./native-api.js";
|
|
55
56
|
import { listJournals, readJournal } from "./journal.js";
|
|
56
57
|
const usageHead = `penpair ${version}
|
|
58
|
+
Design agreements and release pages:
|
|
59
|
+
penpair journey-map check|render|publish|import|accept|approve|context --from FILE
|
|
60
|
+
penpair integration-map check|render|publish|import|accept|approve|context --from FILE
|
|
61
|
+
penpair design-release plan|assemble|verify|finalize|check|context --from FILE
|
|
62
|
+
(publication previews by default; --apply writes Penpot, --write records local acceptance/review)
|
|
57
63
|
Start here (guided, plain language; safe to run again):
|
|
58
64
|
penpair setup asks where your Penpot is (web is the default), sets everything up and checks it
|
|
59
65
|
penpair setup --web [--external <address>] Penpot on the web: design.penpot.app, or your team's address; no questions about it
|
|
@@ -202,6 +208,12 @@ async function main(argv, cli) {
|
|
|
202
208
|
case: {
|
|
203
209
|
type: "string",
|
|
204
210
|
},
|
|
211
|
+
format: {
|
|
212
|
+
type: "string",
|
|
213
|
+
},
|
|
214
|
+
"migrate-format": {
|
|
215
|
+
type: "boolean",
|
|
216
|
+
},
|
|
205
217
|
"force-reason": {
|
|
206
218
|
type: "string",
|
|
207
219
|
},
|
|
@@ -436,6 +448,7 @@ async function main(argv, cli) {
|
|
|
436
448
|
"by",
|
|
437
449
|
"case",
|
|
438
450
|
"write",
|
|
451
|
+
"format",
|
|
439
452
|
],
|
|
440
453
|
spec: [
|
|
441
454
|
"from",
|
|
@@ -539,6 +552,8 @@ async function main(argv, cli) {
|
|
|
539
552
|
"apply",
|
|
540
553
|
"update",
|
|
541
554
|
"force-reason",
|
|
555
|
+
"format",
|
|
556
|
+
"migrate-format",
|
|
542
557
|
],
|
|
543
558
|
probe: [
|
|
544
559
|
"deep",
|
|
@@ -629,6 +644,12 @@ async function main(argv, cli) {
|
|
|
629
644
|
throw new Error(`--${key} is not supported by ${command || "this command"}`);
|
|
630
645
|
}
|
|
631
646
|
}
|
|
647
|
+
if (values.format && values.format !== "png" && values.format !== "webp") {
|
|
648
|
+
throw new CommandArgumentError("--format must be png or webp");
|
|
649
|
+
}
|
|
650
|
+
if (values["migrate-format"] && (!values.format || (values.apply && !values.update))) {
|
|
651
|
+
throw new CommandArgumentError("--migrate-format requires --format; applying it requires --update");
|
|
652
|
+
}
|
|
632
653
|
for (const [key, min, max, integer] of [
|
|
633
654
|
[
|
|
634
655
|
"depth",
|
|
@@ -1786,6 +1807,8 @@ async function main(argv, cli) {
|
|
|
1786
1807
|
apply: values.apply === true,
|
|
1787
1808
|
update: values.update === true,
|
|
1788
1809
|
forceReason: values["force-reason"],
|
|
1810
|
+
format: values.format,
|
|
1811
|
+
migrateFormat: values["migrate-format"],
|
|
1789
1812
|
});
|
|
1790
1813
|
output.result(report);
|
|
1791
1814
|
for (const r of report.results) {
|
|
@@ -1908,6 +1931,7 @@ async function main(argv, cli) {
|
|
|
1908
1931
|
throw new CommandArgumentError("approve --page PAGE --frame ID [--case CASE_ID] previews evidence; --write requires --note exact-owner-words and --by identity");
|
|
1909
1932
|
}
|
|
1910
1933
|
let intent;
|
|
1934
|
+
let imageFormat = values.format ?? "webp";
|
|
1911
1935
|
if (values.case) {
|
|
1912
1936
|
const baseline = projectConfig?.baselines;
|
|
1913
1937
|
const wanted = baseline?.frames.find((f) => f.id === values.case);
|
|
@@ -1926,6 +1950,9 @@ async function main(argv, cli) {
|
|
|
1926
1950
|
}
|
|
1927
1951
|
}
|
|
1928
1952
|
intent = wanted.case;
|
|
1953
|
+
imageFormat = values.format
|
|
1954
|
+
? values.format
|
|
1955
|
+
: await resolveBaselineImageFormat(root, baseline, wanted.id);
|
|
1929
1956
|
}
|
|
1930
1957
|
const captured = await exportShapePng(client, values.frame, {
|
|
1931
1958
|
...options,
|
|
@@ -1935,17 +1962,21 @@ async function main(argv, cli) {
|
|
|
1935
1962
|
if (!captured.stable) {
|
|
1936
1963
|
throw new Error("Approval export did not stabilize");
|
|
1937
1964
|
}
|
|
1938
|
-
const
|
|
1965
|
+
const approvalImage = await encodeBaselineImage(captured.png, imageFormat);
|
|
1966
|
+
const sha256 = artifactSha(approvalImage);
|
|
1939
1967
|
const report = values.write
|
|
1940
1968
|
? await recordApproval(root, captured, {
|
|
1941
1969
|
note: values.note,
|
|
1942
1970
|
by: values.by,
|
|
1943
|
-
}, intent, endpointRedactor(endpoint)
|
|
1971
|
+
}, intent, endpointRedactor(endpoint), {
|
|
1972
|
+
format: imageFormat,
|
|
1973
|
+
})
|
|
1944
1974
|
: {
|
|
1945
1975
|
schemaVersion: 1,
|
|
1946
1976
|
kind: "approval-candidate",
|
|
1947
1977
|
capture: captured.capture,
|
|
1948
1978
|
sha256,
|
|
1979
|
+
imageFormat,
|
|
1949
1980
|
fingerprint: approvalFingerprint(captured.capture, sha256, intent),
|
|
1950
1981
|
recorded: false,
|
|
1951
1982
|
};
|
|
@@ -2148,6 +2179,36 @@ async function runAdapterCommand(command, root, args) {
|
|
|
2148
2179
|
async function runCliInternal(argv, options = {}) {
|
|
2149
2180
|
try {
|
|
2150
2181
|
const root = projectRoot();
|
|
2182
|
+
if (argv[0] === "journey-map" ||
|
|
2183
|
+
argv[0] === "integration-map" ||
|
|
2184
|
+
argv[0] === "design-release") {
|
|
2185
|
+
const { runDesignCommand } = await import("./design-commands/index.js");
|
|
2186
|
+
let designRedactor = (text) => redactEndpoint(text);
|
|
2187
|
+
return await runDesignCommand(argv[0], argv.slice(1), {
|
|
2188
|
+
root,
|
|
2189
|
+
output,
|
|
2190
|
+
redact: (text) => designRedactor(text),
|
|
2191
|
+
async withPenpot(work) {
|
|
2192
|
+
const config = await loadCommandConfig(root);
|
|
2193
|
+
if (!config.file?.id) {
|
|
2194
|
+
throw new Error("Live design commands require an explicitly pinned Penpot file");
|
|
2195
|
+
}
|
|
2196
|
+
const endpoint = await projectEndpoint(root);
|
|
2197
|
+
designRedactor = endpointRedactor(endpoint);
|
|
2198
|
+
commandRedactor(endpointRedactor(endpoint));
|
|
2199
|
+
assertEndpointIsInstance(endpoint.description, config);
|
|
2200
|
+
const client = guardFile(await connect(endpoint), config.file, {
|
|
2201
|
+
mode: "strict",
|
|
2202
|
+
});
|
|
2203
|
+
try {
|
|
2204
|
+
return await work(client);
|
|
2205
|
+
}
|
|
2206
|
+
finally {
|
|
2207
|
+
await client.close();
|
|
2208
|
+
}
|
|
2209
|
+
},
|
|
2210
|
+
});
|
|
2211
|
+
}
|
|
2151
2212
|
if (argv[0] === "onboard") {
|
|
2152
2213
|
if (argv.slice(1).some((arg) => arg !== "--json")) {
|
|
2153
2214
|
throw new Error("usage: penpair onboard [--json]");
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CommandOutput } from "../command-result.js";
|
|
2
|
+
import type { PenpotToolClient } from "../client.js";
|
|
3
|
+
/** Project-owned artifact commands can work offline; a live operation opens a strictly pinned connection. */
|
|
4
|
+
export interface DesignCommandContext {
|
|
5
|
+
root: string;
|
|
6
|
+
output: CommandOutput;
|
|
7
|
+
redact(value: string): string;
|
|
8
|
+
withPenpot<T>(work: (client: PenpotToolClient) => Promise<T>): Promise<T>;
|
|
9
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { DesignCommandContext } from "./context.js";
|
|
2
|
+
export type { DesignCommandContext } from "./context.js";
|
|
3
|
+
export declare function runDesignCommand(family: "journey-map" | "integration-map" | "design-release", args: string[], context: DesignCommandContext): Promise<number>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { runMapCommand } from "./map-command.js";
|
|
2
|
+
import { runDesignReleaseCommand } from "./release-command.js";
|
|
3
|
+
export async function runDesignCommand(family, args, context) {
|
|
4
|
+
return family === "design-release"
|
|
5
|
+
? runDesignReleaseCommand(args, context)
|
|
6
|
+
: runMapCommand(family, args, context);
|
|
7
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { MapKind } from "../maps/types.js";
|
|
2
|
+
import type { DesignCommandContext } from "./context.js";
|
|
3
|
+
/** Explicit local artifact actions remain offline; native publication is preview-only until --apply. */
|
|
4
|
+
export declare function runMapCommand(kind: MapKind, args: string[], context: DesignCommandContext): Promise<number>;
|