@penpair/core 0.1.2 → 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.
- package/dist/adapters.d.ts +16 -1
- package/dist/adapters.js +8 -12
- package/dist/approvals.d.ts +32 -0
- package/dist/approvals.js +99 -0
- package/dist/atomic-output.d.ts +5 -0
- package/dist/atomic-output.js +28 -0
- package/dist/audit-rules.d.ts +60 -0
- package/dist/audit-rules.js +177 -0
- package/dist/backup-note.d.ts +4 -0
- package/dist/backup-note.js +28 -0
- package/dist/baseline-coverage.d.ts +25 -0
- package/dist/baseline-coverage.js +114 -0
- package/dist/baselines-config.d.ts +25 -0
- package/dist/baselines-config.js +52 -3
- package/dist/baselines.d.ts +37 -8
- package/dist/baselines.js +132 -52
- package/dist/cli-arguments.d.ts +7 -0
- package/dist/cli-arguments.js +35 -0
- package/dist/cli-main.d.ts +13 -1
- package/dist/cli-main.js +770 -139
- package/dist/client.d.ts +26 -5
- package/dist/client.js +46 -20
- package/dist/command-result.d.ts +159 -0
- package/dist/command-result.js +179 -0
- package/dist/coordination.d.ts +15 -0
- package/dist/coordination.js +122 -0
- package/dist/credentials.d.ts +21 -0
- package/dist/credentials.js +61 -0
- package/dist/deep-check.js +9 -5
- package/dist/design-spec.d.ts +55 -0
- package/dist/design-spec.js +135 -0
- package/dist/endpoint.d.ts +8 -1
- package/dist/endpoint.js +48 -4
- package/dist/errors.d.ts +2 -1
- package/dist/errors.js +8 -4
- package/dist/export.d.ts +18 -0
- package/dist/export.js +42 -7
- package/dist/file-archive.d.ts +16 -0
- package/dist/file-archive.js +32 -0
- package/dist/file-guard.d.ts +8 -0
- package/dist/file-guard.js +40 -0
- package/dist/formats.d.ts +4 -0
- package/dist/formats.js +28 -0
- package/dist/index.d.ts +33 -1
- package/dist/index.js +33 -1
- package/dist/inspection.d.ts +106 -0
- package/dist/inspection.js +344 -0
- package/dist/instance/config-schema.d.ts +422 -0
- package/dist/instance/config-schema.js +76 -0
- package/dist/instance/config.d.ts +29 -0
- package/dist/instance/config.js +102 -11
- package/dist/instance/connect.d.ts +10 -1
- package/dist/instance/connect.js +53 -12
- package/dist/instance/connection.d.ts +14 -0
- package/dist/instance/connection.js +13 -2
- package/dist/instance/init.js +1 -1
- package/dist/instance/setup.js +1 -1
- package/dist/instance/tty.js +3 -1
- package/dist/journal.d.ts +77 -0
- package/dist/journal.js +260 -0
- package/dist/kit-smoke.d.ts +31 -0
- package/dist/kit-smoke.js +83 -0
- package/dist/kit-source.d.ts +2 -0
- package/dist/kit-source.js +309 -0
- package/dist/kit.d.ts +25 -0
- package/dist/kit.js +90 -0
- package/dist/layout.d.ts +15 -0
- package/dist/layout.js +79 -0
- package/dist/live-smoke.d.ts +8 -0
- package/dist/live-smoke.js +14 -0
- package/dist/mcp-server.d.ts +12 -0
- package/dist/mcp-server.js +102 -0
- package/dist/native-api.d.ts +15 -0
- package/dist/native-api.js +42 -0
- package/dist/onboard.d.ts +10 -0
- package/dist/onboard.js +15 -0
- package/dist/operation.d.ts +20 -1
- package/dist/operation.js +113 -13
- package/dist/png.d.ts +5 -0
- package/dist/png.js +7 -0
- package/dist/portable-tokens.d.ts +22 -0
- package/dist/portable-tokens.js +134 -0
- package/dist/probe.d.ts +6 -2
- package/dist/probe.js +19 -3
- package/dist/project-input.d.ts +6 -0
- package/dist/project-input.js +51 -0
- package/dist/project-snippets.js +12 -7
- package/dist/project-status.d.ts +24 -0
- package/dist/project-status.js +79 -0
- package/dist/registry.d.ts +2 -1
- package/dist/registry.js +17 -3
- package/dist/run-file.d.ts +26 -0
- package/dist/run-file.js +100 -0
- package/dist/shots.d.ts +43 -0
- package/dist/shots.js +90 -0
- package/dist/skill-instructions.d.ts +13 -0
- package/dist/skill-instructions.js +75 -0
- package/dist/skills.d.ts +11 -0
- package/dist/skills.js +71 -33
- package/dist/snippet.d.ts +3 -2
- package/dist/snippet.js +12 -8
- package/dist/snippets/archive.d.ts +9 -0
- package/dist/snippets/archive.js +10 -4
- package/dist/snippets/read.d.ts +1 -1
- package/dist/snippets/read.js +71 -40
- package/dist/structure-snapshot.d.ts +73 -0
- package/dist/structure-snapshot.js +171 -0
- package/dist/token-edit.d.ts +53 -0
- package/dist/token-edit.js +197 -0
- package/dist/tokens-diff.d.ts +19 -6
- package/dist/tokens-diff.js +89 -18
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skills/penpair-design/SKILL.md +12 -40
- package/skills/penpair-design/references/penpot-plugin-gotchas.md +56 -0
- package/skills/penpair-design/references/project-facts.md +1 -1
- package/skills/penpair-design/references/write-protocol.md +15 -3
- package/skills/penpair-handoff/SKILL.md +14 -0
- package/skills/penpair-handoff/references/handoff-flow.md +11 -0
- package/skills/penpair-recipes/SKILL.md +16 -0
- package/skills/penpair-recipes/references/build-flow.md +11 -0
- package/skills/penpair-setup/SKILL.md +6 -2
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { mkdir, open, readFile, stat, unlink } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { captureInspection } from "./inspection.js";
|
|
4
|
+
import { validateDesignInspection } from "./design-spec.js";
|
|
5
|
+
import { observationHash } from "./journal.js";
|
|
6
|
+
import { atomicWriteFile } from "./atomic-output.js";
|
|
7
|
+
import { designDataBlock, resolveProjectInput, resolveProjectOutput } from "./project-input.js";
|
|
8
|
+
import { runSnippet, snippet } from "./snippet.js";
|
|
9
|
+
import { version } from "./version.js";
|
|
10
|
+
function ordered(value) { if (Array.isArray(value))
|
|
11
|
+
return value.map(ordered); if (!value || typeof value !== "object")
|
|
12
|
+
return value; return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => [k, ordered(v)])); }
|
|
13
|
+
export const structuralJson = (value) => JSON.stringify(ordered(value), null, 2) + "\n";
|
|
14
|
+
export function structuralPage(raw, name) {
|
|
15
|
+
const capture = validateDesignInspection(raw);
|
|
16
|
+
return { schemaVersion: 1, kind: "page-structure", fileId: capture.fileId, pageId: capture.pageId, revision: capture.revision, name, nodes: structuredClone(capture.nodes).sort((a, b) => String(a.id) < String(b.id) ? -1 : String(a.id) > String(b.id) ? 1 : 0) };
|
|
17
|
+
}
|
|
18
|
+
export function assembleStructuralSnapshot(pages, rawTokens) {
|
|
19
|
+
if (rawTokens.schemaVersion !== 1 || rawTokens.kind !== "token-inspection" || rawTokens.complete !== true || rawTokens.redacted === true || rawTokens.reasons.length || !Array.isArray(rawTokens.sets) || !Array.isArray(rawTokens.themes))
|
|
20
|
+
throw new Error("Complete unredacted token inspection required");
|
|
21
|
+
if (!pages.length || pages.length > 100 || new Set(pages.map(p => p.pageId)).size !== pages.length || pages.some(p => p.fileId !== rawTokens.fileId || p.revision !== rawTokens.revision) || pages.reduce((n, p) => n + p.nodes.length, 0) > 50000)
|
|
22
|
+
throw new Error("Snapshot identity/revision/page/node budget mismatch");
|
|
23
|
+
for (const page of pages) {
|
|
24
|
+
validateDesignInspection({ schemaVersion: 1, kind: "shape-inspection", fileId: page.fileId, pageId: page.pageId, revision: page.revision, complete: true, counts: {}, reasons: [], producer: { name: "penpair", version }, nodes: page.nodes });
|
|
25
|
+
if (Buffer.byteLength(structuralJson(page)) > 8 * 1024 * 1024)
|
|
26
|
+
throw new Error("Page snapshot exceeds 8 MiB; no partial snapshot was published");
|
|
27
|
+
}
|
|
28
|
+
const snapshot = { schemaVersion: 1, kind: "structural-snapshot", fileId: rawTokens.fileId, revision: rawTokens.revision, pages: structuredClone(pages).sort((a, b) => a.pageId < b.pageId ? -1 : a.pageId > b.pageId ? 1 : 0), tokens: { schemaVersion: 1, kind: "token-structure", fileId: rawTokens.fileId, revision: rawTokens.revision, sets: structuredClone(rawTokens.sets), themes: structuredClone(rawTokens.themes) }, fidelity: "review-only" };
|
|
29
|
+
if (Buffer.byteLength(structuralJson(snapshot.tokens)) > 8 * 1024 * 1024 || Buffer.byteLength(structuralJson(snapshot)) > 128 * 1024 * 1024)
|
|
30
|
+
throw new Error("Snapshot token/total byte budget exceeded");
|
|
31
|
+
return snapshot;
|
|
32
|
+
}
|
|
33
|
+
const listingSnippet = () => snippet("snapshot-pages", `
|
|
34
|
+
if (penpot.currentFile.pages.length > 100) return {ok:false,message:"Snapshot supports at most 100 pages"};
|
|
35
|
+
return {ok:true,fileId:penpot.currentFile.id,revision:penpot.currentFile.revn,pages:penpot.currentFile.pages.map(p=>({id:p.id,name:p.name}))};`, {}, "read");
|
|
36
|
+
export async function captureStructuralSnapshot(client, options = {}) {
|
|
37
|
+
const before = await runSnippet(client, listingSnippet(), options);
|
|
38
|
+
if (!Array.isArray(before.pages) || !before.pages.length || typeof before.fileId !== "string" || !Number.isInteger(before.revision))
|
|
39
|
+
throw new Error("Snapshot listing identity missing");
|
|
40
|
+
const pages = before.pages;
|
|
41
|
+
if (pages.some(p => typeof p.id !== "string" || typeof p.name !== "string") || new Set(pages.map(p => p.id)).size !== pages.length || new Set(pages.map(p => p.name)).size !== pages.length)
|
|
42
|
+
throw new Error("Ambiguous page identity/name; page selection must be explicit and unique");
|
|
43
|
+
const result = [];
|
|
44
|
+
let nodes = 0;
|
|
45
|
+
const started = Date.now();
|
|
46
|
+
const budget = () => { const remaining = 300000 - (Date.now() - started); if (remaining < 1)
|
|
47
|
+
throw new Error("Snapshot collection exceeded five-minute budget"); return { maxTotalBytes: 8 * 1024 * 1024, timeoutMs: Math.min(60000, remaining) }; };
|
|
48
|
+
for (const page of pages) {
|
|
49
|
+
options.signal?.throwIfAborted();
|
|
50
|
+
const capture = await captureInspection(client, { kind: "shape", shapeId: "@page-root", page: page.name, depth: 10000, maxNodes: 50000, documentBytes: 8 * 1024 * 1024 }, budget());
|
|
51
|
+
if (!capture.complete || capture.kind !== "shape-inspection" || capture.pageId !== page.id)
|
|
52
|
+
throw new Error("Snapshot page inspection incomplete or selected identity changed");
|
|
53
|
+
const data = structuralPage(capture, page.name);
|
|
54
|
+
nodes += data.nodes.length;
|
|
55
|
+
if (nodes > 50000)
|
|
56
|
+
throw new Error("Snapshot file exceeds 50000 nodes");
|
|
57
|
+
result.push(data);
|
|
58
|
+
}
|
|
59
|
+
const tokens = await captureInspection(client, { kind: "tokens", documentBytes: 8 * 1024 * 1024 }, budget());
|
|
60
|
+
const after = await runSnippet(client, listingSnippet(), options);
|
|
61
|
+
if (!tokens.complete || tokens.kind !== "token-inspection" || observationHash(before, true) !== observationHash(after, true))
|
|
62
|
+
throw new Error("Snapshot source changed or token inspection incomplete");
|
|
63
|
+
const snapshot = assembleStructuralSnapshot(result, tokens);
|
|
64
|
+
if (snapshot.fileId !== before.fileId || snapshot.revision !== before.revision)
|
|
65
|
+
throw new Error("Snapshot source changed during collection");
|
|
66
|
+
return snapshot;
|
|
67
|
+
}
|
|
68
|
+
async function readBounded(file, maximum) { if ((await stat(file)).size > maximum)
|
|
69
|
+
throw new Error("Structural artifact exceeds byte budget"); return JSON.parse(await readFile(file, "utf8")); }
|
|
70
|
+
export async function writeStructuralSnapshot(root, out, snapshot, redact) {
|
|
71
|
+
snapshot = assembleStructuralSnapshot(snapshot.pages, { ...snapshot.tokens, kind: "token-inspection", pageId: null, complete: true, reasons: [], counts: {}, producer: { name: "penpair", version } });
|
|
72
|
+
const pointer = await resolveProjectOutput(root, path.join(out, "snapshot.json"));
|
|
73
|
+
await mkdir(path.dirname(pointer), { recursive: true });
|
|
74
|
+
const lock = await open(pointer + ".lock", "wx", 0o600);
|
|
75
|
+
try {
|
|
76
|
+
return await publishStructuralSnapshot(root, out, snapshot, redact);
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
await lock.close();
|
|
80
|
+
await unlink(pointer + ".lock");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async function publishStructuralSnapshot(root, out, snapshot, redact) {
|
|
84
|
+
const artifacts = new Map();
|
|
85
|
+
const pages = snapshot.pages.map(page => { const file = `page-${observationHash(page.pageId).slice(0, 16)}.json`; artifacts.set(file, page); return { id: page.pageId, name: page.name, file, hash: observationHash(page) }; });
|
|
86
|
+
artifacts.set("tokens.json", snapshot.tokens);
|
|
87
|
+
const body = { schemaVersion: 1, kind: "structural-snapshot-manifest", fileId: snapshot.fileId, revision: snapshot.revision, producer: `penpair@${version}`, fidelity: "review-only", pages, tokens: { file: "tokens.json", hash: observationHash(snapshot.tokens) } };
|
|
88
|
+
const manifest = { ...body, hash: observationHash(body) };
|
|
89
|
+
const encoded = structuralJson({ manifest, artifacts: [...artifacts] });
|
|
90
|
+
if (redact && redact(encoded) !== encoded)
|
|
91
|
+
throw new Error("Snapshot evidence requires redaction; previous artifacts preserved");
|
|
92
|
+
const pointer = await resolveProjectOutput(root, path.join(out, "snapshot.json"));
|
|
93
|
+
try {
|
|
94
|
+
const old = await readStructuralSnapshot(root, out);
|
|
95
|
+
if (old.fileId !== snapshot.fileId || old.revision > snapshot.revision)
|
|
96
|
+
throw new Error("Refusing to replace another file's or newer structural snapshot");
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
if (error.code !== "ENOENT")
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
for (const [file, data] of artifacts) {
|
|
103
|
+
const target = await resolveProjectOutput(root, path.join(out, "generations", manifest.hash, file));
|
|
104
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
105
|
+
try {
|
|
106
|
+
await atomicWriteFile(target, structuralJson(data));
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
if (error.code !== "EEXIST" || observationHash(await readBounded(target, 8 * 1024 * 1024)) !== observationHash(data))
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
await atomicWriteFile(pointer, structuralJson(manifest), { replace: true });
|
|
114
|
+
return manifest;
|
|
115
|
+
}
|
|
116
|
+
export async function readStructuralSnapshot(root, folder) {
|
|
117
|
+
const manifest = await readBounded(await resolveProjectInput(root, path.join(folder, "snapshot.json")), 1024 * 1024);
|
|
118
|
+
const { hash, ...body } = manifest;
|
|
119
|
+
if (manifest.schemaVersion !== 1 || manifest.kind !== "structural-snapshot-manifest" || manifest.fidelity !== "review-only" || !/^[a-f0-9]{64}$/.test(hash) || hash !== observationHash(body) || !Array.isArray(manifest.pages) || !manifest.pages.length || manifest.pages.length > 100 || manifest.tokens.file !== "tokens.json")
|
|
120
|
+
throw new Error("Unsupported or altered structural manifest");
|
|
121
|
+
const read = async (file, expected) => { const data = await readBounded(await resolveProjectInput(root, path.join(folder, "generations", hash, file)), 8 * 1024 * 1024); if (observationHash(data) !== expected)
|
|
122
|
+
throw new Error("Structural artifact hash mismatch"); return data; };
|
|
123
|
+
const pages = [];
|
|
124
|
+
for (const entry of manifest.pages) {
|
|
125
|
+
if (!/^page-[a-f0-9]{16}\.json$/.test(entry.file))
|
|
126
|
+
throw new Error("Invalid structural page artifact path");
|
|
127
|
+
const page = await read(entry.file, entry.hash);
|
|
128
|
+
if (page.kind !== "page-structure" || page.pageId !== entry.id || page.name !== entry.name || page.fileId !== manifest.fileId || page.revision !== manifest.revision)
|
|
129
|
+
throw new Error("Structural page identity mismatch");
|
|
130
|
+
pages.push(page);
|
|
131
|
+
}
|
|
132
|
+
const tokens = await read("tokens.json", manifest.tokens.hash);
|
|
133
|
+
if (tokens.kind !== "token-structure" || tokens.fileId !== manifest.fileId || tokens.revision !== manifest.revision)
|
|
134
|
+
throw new Error("Structural token identity mismatch");
|
|
135
|
+
return assembleStructuralSnapshot(pages, { ...tokens, kind: "token-inspection", pageId: null, complete: true, reasons: [], counts: {}, producer: { name: "penpair", version } });
|
|
136
|
+
}
|
|
137
|
+
export function diffStructuralSnapshots(before, after, limit = 1000) {
|
|
138
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 10000)
|
|
139
|
+
throw new Error("Structural diff limit must be 1–10000");
|
|
140
|
+
if (before.fileId !== after.fileId)
|
|
141
|
+
throw new Error("Structural snapshots belong to different files; identities are not comparable");
|
|
142
|
+
const changes = [];
|
|
143
|
+
let totalChanges = 0;
|
|
144
|
+
const add = (change) => { totalChanges++; if (changes.length < limit)
|
|
145
|
+
changes.push(change); };
|
|
146
|
+
const oldPages = new Map(before.pages.map(p => [p.pageId, p])), newPages = new Map(after.pages.map(p => [p.pageId, p]));
|
|
147
|
+
for (const id of [...new Set([...oldPages.keys(), ...newPages.keys()])].sort()) {
|
|
148
|
+
const a = oldPages.get(id), b = newPages.get(id);
|
|
149
|
+
if (!a || !b) {
|
|
150
|
+
add({ kind: a ? "page-removed" : "page-added", pageId: id, before: a ?? null, after: b ?? null });
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (a.name !== b.name)
|
|
154
|
+
add({ kind: "page-name", pageId: id, before: a.name, after: b.name });
|
|
155
|
+
const oldNodes = new Map(a.nodes.map(n => [n.id, n])), newNodes = new Map(b.nodes.map(n => [n.id, n]));
|
|
156
|
+
for (const nodeId of [...new Set([...oldNodes.keys(), ...newNodes.keys()])].sort()) {
|
|
157
|
+
const x = oldNodes.get(nodeId), y = newNodes.get(nodeId);
|
|
158
|
+
if (!x || !y) {
|
|
159
|
+
add({ kind: x ? "node-removed" : "node-added", pageId: id, nodeId: String(nodeId), before: x ?? null, after: y ?? null });
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
for (const key of [...new Set([...Object.keys(x), ...Object.keys(y)])].sort())
|
|
163
|
+
if (observationHash(x[key] ?? null) !== observationHash(y[key] ?? null))
|
|
164
|
+
add({ kind: key, pageId: id, nodeId: String(nodeId), before: x[key] ?? null, after: y[key] ?? null });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (observationHash(before.tokens.sets) !== observationHash(after.tokens.sets) || observationHash(before.tokens.themes) !== observationHash(after.tokens.themes))
|
|
168
|
+
add({ kind: "tokens", before: before.tokens, after: after.tokens });
|
|
169
|
+
return { schemaVersion: 1, kind: "structural-diff", fileId: before.fileId, beforeRevision: before.revision, afterRevision: after.revision, totalChanges, complete: totalChanges <= limit, changes, fidelity: "review-only" };
|
|
170
|
+
}
|
|
171
|
+
export const renderStructuralDiff = (diff) => "# Structural design diff\n\nReview evidence; not a full-fidelity backup or restore archive.\n\n" + designDataBlock(diff);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Operation } from "./operation.js";
|
|
2
|
+
export declare const tokenTypes: readonly ["borderRadius", "shadow", "color", "dimension", "fontFamilies", "fontSizes", "fontWeights", "letterSpacing", "number", "opacity", "rotation", "sizing", "spacing", "borderWidth", "textCase", "textDecoration", "typography"];
|
|
3
|
+
export interface EditableToken {
|
|
4
|
+
name: string;
|
|
5
|
+
type: string;
|
|
6
|
+
value: string | Record<string, string>;
|
|
7
|
+
description: string;
|
|
8
|
+
}
|
|
9
|
+
export type TokenEdit = {
|
|
10
|
+
kind: "add-set";
|
|
11
|
+
set: string;
|
|
12
|
+
} | {
|
|
13
|
+
kind: "set";
|
|
14
|
+
set: string;
|
|
15
|
+
token: EditableToken;
|
|
16
|
+
} | {
|
|
17
|
+
kind: "remove";
|
|
18
|
+
set: string;
|
|
19
|
+
name: string;
|
|
20
|
+
} | {
|
|
21
|
+
kind: "promote";
|
|
22
|
+
set: string;
|
|
23
|
+
to: string;
|
|
24
|
+
};
|
|
25
|
+
export interface TokenEditState {
|
|
26
|
+
sets: {
|
|
27
|
+
name: string;
|
|
28
|
+
active: boolean;
|
|
29
|
+
tokens: EditableToken[];
|
|
30
|
+
}[];
|
|
31
|
+
themes: {
|
|
32
|
+
group: string;
|
|
33
|
+
name: string;
|
|
34
|
+
active: boolean;
|
|
35
|
+
sets: string[];
|
|
36
|
+
}[];
|
|
37
|
+
bindings: {
|
|
38
|
+
id: string;
|
|
39
|
+
page: string;
|
|
40
|
+
tokens: Record<string, string>;
|
|
41
|
+
}[];
|
|
42
|
+
}
|
|
43
|
+
export interface TokenEditPlan {
|
|
44
|
+
before: TokenEditState;
|
|
45
|
+
after: TokenEditState;
|
|
46
|
+
changed: boolean;
|
|
47
|
+
impact: string[];
|
|
48
|
+
}
|
|
49
|
+
export declare function validateEditableToken(value: unknown): EditableToken;
|
|
50
|
+
export declare function validateTokenEdit(edit: TokenEdit): TokenEdit;
|
|
51
|
+
/** Pure desired-state planner. Kept self-contained so the delivered mutation rechecks the same facts. */
|
|
52
|
+
export declare function planTokenEdit(before: TokenEditState, edit: TokenEdit): TokenEditPlan;
|
|
53
|
+
export declare const tokenEditOperation: Operation<TokenEdit>;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { snippet } from "./snippet.js";
|
|
2
|
+
export const tokenTypes = ["borderRadius", "shadow", "color", "dimension", "fontFamilies", "fontSizes", "fontWeights", "letterSpacing", "number", "opacity", "rotation", "sizing", "spacing", "borderWidth", "textCase", "textDecoration", "typography"];
|
|
3
|
+
export function validateEditableToken(value) {
|
|
4
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
5
|
+
throw new Error("Token must be an object");
|
|
6
|
+
const token = value;
|
|
7
|
+
if (Object.keys(token).some(key => !["name", "type", "value", "description"].includes(key)))
|
|
8
|
+
throw new Error("Unknown token field");
|
|
9
|
+
if (typeof token.name !== "string" || !token.name.trim() || token.name.length > 200 || /[{}\x00-\x1f]/.test(token.name))
|
|
10
|
+
throw new Error("Token name must be nonempty, at most 200 characters and contain no braces or control characters");
|
|
11
|
+
if (!tokenTypes.includes(token.type))
|
|
12
|
+
throw new Error("Unsupported token type");
|
|
13
|
+
if (!(typeof token.value === "string" && token.value.length > 0 || token.value && typeof token.value === "object" && !Array.isArray(token.value) && Object.keys(token.value).length > 0 && Object.values(token.value).every(v => typeof v === "string")))
|
|
14
|
+
throw new Error("Token value must be a nonempty string or composite string-valued object");
|
|
15
|
+
if (token.description !== undefined && typeof token.description !== "string")
|
|
16
|
+
throw new Error("Token description must be a string");
|
|
17
|
+
if (Buffer.byteLength(JSON.stringify(token)) > 16384)
|
|
18
|
+
throw new Error("Token exceeds 16 KiB");
|
|
19
|
+
return { name: token.name, type: token.type, value: token.value, description: token.description ?? "" };
|
|
20
|
+
}
|
|
21
|
+
export function validateTokenEdit(edit) {
|
|
22
|
+
if (!["add-set", "set", "remove", "promote"].includes(edit.kind))
|
|
23
|
+
throw new Error("Unknown token edit");
|
|
24
|
+
for (const name of [edit.set, ...(edit.kind === "promote" ? [edit.to] : [])])
|
|
25
|
+
if (typeof name !== "string" || !name.trim() || name.length > 200 || /[\x00-\x1f]/.test(name))
|
|
26
|
+
throw new Error("Set names must be nonempty and at most 200 characters without control characters");
|
|
27
|
+
if (edit.kind === "set")
|
|
28
|
+
return { ...edit, token: validateEditableToken(edit.token) };
|
|
29
|
+
if (edit.kind === "remove" && (typeof edit.name !== "string" || !edit.name.trim()))
|
|
30
|
+
throw new Error("Remove requires a token name");
|
|
31
|
+
if (edit.kind === "promote" && edit.set === edit.to)
|
|
32
|
+
throw new Error("Promotion requires distinct source and destination sets");
|
|
33
|
+
return edit;
|
|
34
|
+
}
|
|
35
|
+
/** Pure desired-state planner. Kept self-contained so the delivered mutation rechecks the same facts. */
|
|
36
|
+
export function planTokenEdit(before, edit) {
|
|
37
|
+
const canonical = (value) => {
|
|
38
|
+
if (Array.isArray(value))
|
|
39
|
+
return "[" + value.map(canonical).join(",") + "]";
|
|
40
|
+
if (value && typeof value === "object")
|
|
41
|
+
return "{" + Object.keys(value).sort().map(key => JSON.stringify(key) + ":" + canonical(value[key])).join(",") + "}";
|
|
42
|
+
return JSON.stringify(value);
|
|
43
|
+
};
|
|
44
|
+
const after = JSON.parse(JSON.stringify(before));
|
|
45
|
+
if (!Array.isArray(after.sets) || !Array.isArray(after.themes) || !Array.isArray(after.bindings))
|
|
46
|
+
throw new Error("Missing token observations");
|
|
47
|
+
const names = new Set();
|
|
48
|
+
for (const set of after.sets) {
|
|
49
|
+
if (names.has(set.name))
|
|
50
|
+
throw new Error("Ambiguous set: " + set.name);
|
|
51
|
+
names.add(set.name);
|
|
52
|
+
const tokens = new Set();
|
|
53
|
+
for (const token of set.tokens) {
|
|
54
|
+
if (tokens.has(token.name))
|
|
55
|
+
throw new Error("Ambiguous token: " + token.name);
|
|
56
|
+
tokens.add(token.name);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const impact = [];
|
|
60
|
+
const set = after.sets.find(s => s.name === edit.set);
|
|
61
|
+
if (edit.kind === "add-set") {
|
|
62
|
+
if (!set)
|
|
63
|
+
after.sets.push({ name: edit.set, active: false, tokens: [] });
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
if (!set)
|
|
67
|
+
throw new Error("Missing set: " + edit.set);
|
|
68
|
+
if (edit.kind === "set") {
|
|
69
|
+
const token = set.tokens.find(t => t.name === edit.token.name);
|
|
70
|
+
if (token && token.type !== edit.token.type)
|
|
71
|
+
throw new Error("Type replacement requires a separate reviewed removal");
|
|
72
|
+
if (token)
|
|
73
|
+
Object.assign(token, edit.token);
|
|
74
|
+
else
|
|
75
|
+
set.tokens.push(edit.token);
|
|
76
|
+
for (const row of before.bindings)
|
|
77
|
+
if (Object.values(row.tokens).includes(edit.token.name))
|
|
78
|
+
impact.push(row.page + "/" + row.id);
|
|
79
|
+
}
|
|
80
|
+
else if (edit.kind === "remove") {
|
|
81
|
+
// Conservatively refuse even when another active set might supply the same name.
|
|
82
|
+
if (set.tokens.some(t => t.name === edit.name)) {
|
|
83
|
+
const pattern = "{" + edit.name + "}";
|
|
84
|
+
for (const source of after.sets)
|
|
85
|
+
for (const token of source.tokens)
|
|
86
|
+
if (!(source.name === set.name && token.name === edit.name) && JSON.stringify(token.value).includes(pattern))
|
|
87
|
+
throw new Error("Retained reference from " + source.name + "/" + token.name);
|
|
88
|
+
if (before.bindings.some(row => Object.values(row.tokens).includes(edit.name)))
|
|
89
|
+
throw new Error("Token is bound to a retained shape");
|
|
90
|
+
set.tokens = set.tokens.filter(t => t.name !== edit.name);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
const target = after.sets.find(s => s.name === edit.to);
|
|
95
|
+
if (!target)
|
|
96
|
+
throw new Error("Missing promotion destination: " + edit.to);
|
|
97
|
+
for (const token of set.tokens) {
|
|
98
|
+
const existing = target.tokens.find(t => t.name === token.name);
|
|
99
|
+
if (existing && canonical(existing) !== canonical(token))
|
|
100
|
+
throw new Error("Promotion conflicts with destination token: " + token.name);
|
|
101
|
+
if (!existing)
|
|
102
|
+
target.tokens.push(token);
|
|
103
|
+
}
|
|
104
|
+
impact.push("Copies tokens; source, themes and active selection are preserved");
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
for (const set of after.sets)
|
|
108
|
+
set.tokens.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
109
|
+
return { before, after, changed: canonical(before) !== canonical(after), impact };
|
|
110
|
+
}
|
|
111
|
+
// Snapshot getter-backed API fields explicitly; refuse partial/oversized observations before any edit.
|
|
112
|
+
const observe = `
|
|
113
|
+
const observe = () => {
|
|
114
|
+
const catalog=penpot.library.local.tokens;
|
|
115
|
+
if (!catalog || !Array.isArray(catalog.sets) || !Array.isArray(catalog.themes)) throw new Error("Token API unavailable");
|
|
116
|
+
let count=0;
|
|
117
|
+
const sets=catalog.sets.map(s=>({name:s.name,active:!!s.active,tokens:s.tokens.map(t=>{
|
|
118
|
+
if(++count>10000)throw new Error("Token edit inspection exceeds 10000 tokens");
|
|
119
|
+
if(typeof t.name!=="string"||typeof t.type!=="string"||t.value===undefined)throw new Error("Incomplete token observation");
|
|
120
|
+
return {name:t.name,type:t.type,value:t.value,description:t.description||""};
|
|
121
|
+
}).sort((a,b)=>a.name<b.name?-1:a.name>b.name?1:0)}));
|
|
122
|
+
const themes=catalog.themes.map(t=>({group:t.group,name:t.name,active:!!t.active,sets:(t.activeSets||[]).map(s=>typeof s==="string"?s:s.name)}));
|
|
123
|
+
const bindings=[],stack=[],seen=new Set();
|
|
124
|
+
for(const page of penpot.currentFile.pages)for(const shape of page.root.children||[])stack.push({page:page.id,shape});
|
|
125
|
+
let visited=0;
|
|
126
|
+
while(stack.length){if(++visited>20000)throw new Error("Token edit binding inspection exceeds 20000 shapes");const {page,shape}=stack.pop();if(!shape.id||seen.has(shape.id))throw new Error("Invalid shape identity");seen.add(shape.id);
|
|
127
|
+
const tokens=shape.tokens||{};if(Object.values(tokens).some(v=>typeof v!=="string"))throw new Error("Unknown shape binding");
|
|
128
|
+
if(Object.keys(tokens).length)bindings.push({page,id:shape.id,tokens});for(const child of shape.children||[])stack.push({page,shape:child});}
|
|
129
|
+
const state=JSON.parse(JSON.stringify({sets,themes,bindings}));
|
|
130
|
+
if(JSON.stringify(state).length>500000)throw new Error("Token edit observation exceeds bounded document size; narrow using a reviewed project operation");
|
|
131
|
+
return state;
|
|
132
|
+
};`;
|
|
133
|
+
const canonical = (value) => {
|
|
134
|
+
if (Array.isArray(value))
|
|
135
|
+
return "[" + value.map(canonical).join(",") + "]";
|
|
136
|
+
if (value && typeof value === "object")
|
|
137
|
+
return "{" + Object.keys(value).sort().map(key => JSON.stringify(key) + ":" + canonical(value[key])).join(",") + "}";
|
|
138
|
+
return JSON.stringify(value);
|
|
139
|
+
};
|
|
140
|
+
function comparable(state) {
|
|
141
|
+
return { ...state, sets: state.sets.map(set => ({ ...set, tokens: set.tokens.map(token => {
|
|
142
|
+
let value = token.value;
|
|
143
|
+
if (token.type === "fontFamilies")
|
|
144
|
+
value = Array.isArray(value) ? value : [value];
|
|
145
|
+
if (token.type === "typography" && value && typeof value === "object" && !Array.isArray(value)) {
|
|
146
|
+
const { fontFamilies, fontFamily, ...rest } = value;
|
|
147
|
+
const family = fontFamily ?? fontFamilies;
|
|
148
|
+
value = { ...rest, ...(family === undefined ? {} : { fontFamily: Array.isArray(family) ? family : [family] }) };
|
|
149
|
+
}
|
|
150
|
+
return { ...token, value };
|
|
151
|
+
}) })) };
|
|
152
|
+
}
|
|
153
|
+
const stateFrom = (result) => {
|
|
154
|
+
if (result.ok !== true || !result.state)
|
|
155
|
+
throw new Error("No independent token-state evidence");
|
|
156
|
+
return result.state;
|
|
157
|
+
};
|
|
158
|
+
export const tokenEditOperation = {
|
|
159
|
+
name: "tokens-edit", page: () => undefined,
|
|
160
|
+
inspect(edit) { edit = validateTokenEdit(edit); return snippet("tokens-edit-inspect", `${observe}\nconst state=observe();const plan=(${planTokenEdit.toString()})(state,CFG.edit);return {ok:true,readOnly:true,state,plan};`, { edit }); },
|
|
161
|
+
apply(edit, inspected) {
|
|
162
|
+
edit = validateTokenEdit(edit);
|
|
163
|
+
return snippet("tokens-edit-apply", `${observe}
|
|
164
|
+
const canonical=${canonical.toString()}, before=observe();
|
|
165
|
+
if(!CFG.before)throw new Error("Missing pinned inspection");
|
|
166
|
+
if(canonical(before)!==canonical(CFG.before))throw new Error("Token source changed after inspection");
|
|
167
|
+
const plan=(${planTokenEdit.toString()})(before,CFG.edit),catalog=penpot.library.local.tokens;
|
|
168
|
+
if(plan.changed){
|
|
169
|
+
if(CFG.edit.kind==="add-set")catalog.addSet({name:CFG.edit.set});
|
|
170
|
+
else {
|
|
171
|
+
const set=catalog.sets.find(s=>s.name===CFG.edit.set);
|
|
172
|
+
if(CFG.edit.kind==="remove"){const token=set.tokens.find(t=>t.name===CFG.edit.name);if(token)token.remove();}
|
|
173
|
+
else if(CFG.edit.kind==="set"){
|
|
174
|
+
const token=set.tokens.find(t=>t.name===CFG.edit.token.name);
|
|
175
|
+
if(token){token.value=CFG.edit.token.value;token.description=CFG.edit.token.description;}
|
|
176
|
+
else{const added=set.addToken(CFG.edit.token);added.description=CFG.edit.token.description;}
|
|
177
|
+
}else{
|
|
178
|
+
const target=catalog.sets.find(s=>s.name===CFG.edit.to);
|
|
179
|
+
for(const token of set.tokens)if(!target.tokens.some(t=>t.name===token.name)){const added=target.addToken({name:token.name,type:token.type,value:token.value});added.description=token.description||"";}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return {ok:true,changed:plan.changed};`, { edit, before: inspected?.state ?? null });
|
|
184
|
+
},
|
|
185
|
+
readback: () => snippet("tokens-edit-readback", `${observe}\nreturn {ok:true,readOnly:true,state:observe()};`),
|
|
186
|
+
verify({ inspect, readback }) {
|
|
187
|
+
try {
|
|
188
|
+
const plan = inspect.plan;
|
|
189
|
+
if (!plan?.after)
|
|
190
|
+
return ["Missing original desired-state plan"];
|
|
191
|
+
return canonical(comparable(stateFrom(readback))) === canonical(comparable(plan.after)) ? [] : ["Independent token state differs from the inspected plan"];
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
return [String(error)];
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
};
|
package/dist/tokens-diff.d.ts
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
|
+
export interface TypedToken {
|
|
2
|
+
name: string;
|
|
3
|
+
type?: string | null;
|
|
4
|
+
value: unknown;
|
|
5
|
+
description?: string | null;
|
|
6
|
+
resolvedValue?: unknown;
|
|
7
|
+
unknowns?: string[];
|
|
8
|
+
}
|
|
1
9
|
export interface TokenDump {
|
|
10
|
+
schemaVersion?: 1;
|
|
11
|
+
complete?: boolean;
|
|
2
12
|
sets: {
|
|
3
13
|
order: number;
|
|
4
14
|
name: string;
|
|
5
15
|
active: boolean;
|
|
6
|
-
tokens: string[];
|
|
16
|
+
tokens: (string | TypedToken)[];
|
|
7
17
|
}[];
|
|
8
18
|
themes: {
|
|
9
19
|
group: string;
|
|
@@ -12,18 +22,21 @@ export interface TokenDump {
|
|
|
12
22
|
sets: string[];
|
|
13
23
|
}[];
|
|
14
24
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
export declare function effectiveValues(dump: TokenDump, themeName: string): Map<string, string>;
|
|
25
|
+
export declare function parseTokenDump(value: unknown): TokenDump;
|
|
26
|
+
/** Declared values under theme membership/order; this does not measure Penpot's resolvedValue or activate a theme. */
|
|
27
|
+
export declare function effectiveValues(input: TokenDump, themeName: string): Map<string, string>;
|
|
19
28
|
export interface EffectiveDiff {
|
|
29
|
+
complete: boolean;
|
|
30
|
+
unknown: string[];
|
|
20
31
|
added: string[];
|
|
21
32
|
removed: string[];
|
|
22
33
|
changed: {
|
|
23
34
|
name: string;
|
|
24
35
|
before: string;
|
|
25
36
|
after: string;
|
|
37
|
+
beforeType?: string;
|
|
38
|
+
afterType?: string;
|
|
26
39
|
}[];
|
|
27
40
|
}
|
|
28
|
-
/**
|
|
41
|
+
/** Composite values compare structurally; legacy flattened objects remain explicitly unknown. */
|
|
29
42
|
export declare function diffEffective(before: Map<string, string>, after: Map<string, string>): EffectiveDiff;
|
package/dist/tokens-diff.js
CHANGED
|
@@ -1,27 +1,98 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
import { observationHash } from "./journal.js";
|
|
2
|
+
import { PenpairError } from "./errors.js";
|
|
3
|
+
const evidence = new WeakMap();
|
|
4
|
+
export function parseTokenDump(value) {
|
|
5
|
+
let input = value;
|
|
6
|
+
if (input && typeof input === "object" && "outcome" in input && "data" in input) {
|
|
7
|
+
if (input.redacted === true)
|
|
8
|
+
throw new PenpairError("incomplete-output", "Token dump evidence was redacted; use an unmodified private source");
|
|
9
|
+
input = input.data;
|
|
10
|
+
}
|
|
11
|
+
if (!input || typeof input !== "object" || input.schemaVersion !== undefined && input.schemaVersion !== 1 || !Array.isArray(input.sets) || !Array.isArray(input.themes))
|
|
12
|
+
throw new Error("Unsupported or malformed token dump; export a fresh schema-1 dump");
|
|
13
|
+
if (input.complete !== undefined && typeof input.complete !== "boolean")
|
|
14
|
+
throw new Error("Malformed token dump completeness");
|
|
15
|
+
if (input.complete === false)
|
|
16
|
+
throw new PenpairError("incomplete-output", "Token dump is incomplete; capture it again before comparing");
|
|
17
|
+
const seen = new Set();
|
|
18
|
+
for (const raw of input.sets) {
|
|
19
|
+
const set = raw;
|
|
20
|
+
if (!set || typeof set.name !== "string" || seen.has(set.name) || !Number.isInteger(set.order) || !Array.isArray(set.tokens))
|
|
21
|
+
throw new Error("Token sets need unique names, numeric order and token arrays");
|
|
22
|
+
seen.add(set.name);
|
|
23
|
+
const names = new Set();
|
|
24
|
+
for (const token of set.tokens) {
|
|
25
|
+
const name = typeof token === "string" ? token.slice(0, token.indexOf("=")) : token?.name;
|
|
26
|
+
if (typeof name !== "string" || !name || names.has(name) || typeof token === "string" && token.indexOf("=") < 1 || typeof token !== "string" && !("value" in token))
|
|
27
|
+
throw new Error("Token dump contains ambiguous or malformed entries");
|
|
28
|
+
names.add(name);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
for (const theme of input.themes)
|
|
32
|
+
if (!theme || typeof theme.group !== "string" || typeof theme.name !== "string" || !Array.isArray(theme.sets) || theme.sets.some(name => typeof name !== "string"))
|
|
33
|
+
throw new Error("Token dump has malformed theme membership");
|
|
34
|
+
return input;
|
|
35
|
+
}
|
|
36
|
+
const display = (value) => { const text = typeof value === "string" ? value : JSON.stringify(value); if (text === undefined)
|
|
37
|
+
throw new Error("Token values must be JSON data"); return text; };
|
|
38
|
+
/** Declared values under theme membership/order; this does not measure Penpot's resolvedValue or activate a theme. */
|
|
39
|
+
export function effectiveValues(input, themeName) {
|
|
40
|
+
const dump = parseTokenDump(input), matches = dump.themes.filter(theme => theme.name === themeName);
|
|
41
|
+
if (!matches.length)
|
|
8
42
|
throw new Error(`no theme named ${themeName}`);
|
|
9
|
-
|
|
43
|
+
if (matches.length !== 1)
|
|
44
|
+
throw new Error(`theme name ${themeName} is ambiguous; use an unambiguous theme dump`);
|
|
45
|
+
const effective = new Map(), details = { unknown: new Set(), fingerprints: new Map(), displayed: new Map(), types: new Map() };
|
|
10
46
|
for (const set of [...dump.sets].sort((a, b) => a.order - b.order)) {
|
|
11
|
-
if (!
|
|
47
|
+
if (!matches[0].sets.includes(set.name))
|
|
12
48
|
continue;
|
|
13
|
-
for (const
|
|
49
|
+
for (const token of set.tokens) {
|
|
50
|
+
const legacy = typeof token === "string", name = legacy ? token.slice(0, token.indexOf("=")) : token.name;
|
|
51
|
+
const value = legacy ? token.slice(token.indexOf("=") + 1) : display(token.value);
|
|
14
52
|
effective.set(name, value);
|
|
53
|
+
details.unknown.delete(name);
|
|
54
|
+
details.fingerprints.delete(name);
|
|
55
|
+
details.types.delete(name);
|
|
56
|
+
if (legacy) {
|
|
57
|
+
if (value.includes("[object"))
|
|
58
|
+
details.unknown.add(name);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
if (token.type)
|
|
62
|
+
details.types.set(name, token.type);
|
|
63
|
+
details.fingerprints.set(name, observationHash({ type: token.type, value: token.value }));
|
|
64
|
+
if (!token.type || token.unknowns?.some(field => field === "value" || field === "type"))
|
|
65
|
+
details.unknown.add(name);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
15
68
|
}
|
|
69
|
+
details.displayed = new Map(effective);
|
|
70
|
+
evidence.set(effective, details);
|
|
16
71
|
return effective;
|
|
17
72
|
}
|
|
18
|
-
/**
|
|
73
|
+
/** Composite values compare structurally; legacy flattened objects remain explicitly unknown. */
|
|
19
74
|
export function diffEffective(before, after) {
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
75
|
+
const details = (map) => {
|
|
76
|
+
const saved = evidence.get(map), unknown = new Set(), fingerprints = new Map(), types = new Map();
|
|
77
|
+
for (const [name, value] of map) {
|
|
78
|
+
if (saved?.displayed.get(name) === value) {
|
|
79
|
+
if (saved.unknown.has(name))
|
|
80
|
+
unknown.add(name);
|
|
81
|
+
const hash = saved.fingerprints.get(name);
|
|
82
|
+
if (hash)
|
|
83
|
+
fingerprints.set(name, hash);
|
|
84
|
+
const type = saved.types.get(name);
|
|
85
|
+
if (type)
|
|
86
|
+
types.set(name, type);
|
|
87
|
+
}
|
|
88
|
+
else if (value.includes("[object"))
|
|
89
|
+
unknown.add(name);
|
|
90
|
+
}
|
|
91
|
+
return { unknown, fingerprints, displayed: new Map(map), types };
|
|
92
|
+
};
|
|
93
|
+
const a = details(before), b = details(after), unknown = [...new Set([...a.unknown, ...b.unknown])].sort();
|
|
94
|
+
const changed = [...after.keys()].filter(name => before.has(name) && !unknown.includes(name) &&
|
|
95
|
+
(a.fingerprints.has(name) && b.fingerprints.has(name) ? a.fingerprints.get(name) !== b.fingerprints.get(name) : before.get(name) !== after.get(name)))
|
|
96
|
+
.map(name => ({ name, before: before.get(name), after: after.get(name), ...(a.types.has(name) && b.types.has(name) ? { beforeType: a.types.get(name), afterType: b.types.get(name) } : {}) }));
|
|
97
|
+
return { complete: unknown.length === 0, unknown, added: [...after.keys()].filter(name => !before.has(name)), removed: [...before.keys()].filter(name => !after.has(name)), changed };
|
|
27
98
|
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Kept in its own file so that the command line and the library entry can both read it without importing each other. Equals package.json (a test checks). */
|
|
2
|
-
export declare const version = "0.
|
|
2
|
+
export declare const version = "0.2.0";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Kept in its own file so that the command line and the library entry can both read it without importing each other. Equals package.json (a test checks). */
|
|
2
|
-
export const version = "0.
|
|
2
|
+
export const version = "0.2.0";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@penpair/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Design together. Build together. The library and engine behind penpair: connect AI agents and people to a Penpot design file, safely. Install @penpair/web or @penpair/docker for the command.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|