@penpair/core 0.1.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/dist/adapters.d.ts +16 -1
  2. package/dist/adapters.js +62 -19
  3. package/dist/approvals.d.ts +32 -0
  4. package/dist/approvals.js +213 -0
  5. package/dist/atomic-output.d.ts +5 -0
  6. package/dist/atomic-output.js +33 -0
  7. package/dist/audit-rules.d.ts +60 -0
  8. package/dist/audit-rules.js +443 -0
  9. package/dist/backup-note.d.ts +4 -0
  10. package/dist/backup-note.js +49 -0
  11. package/dist/baseline-coverage.d.ts +25 -0
  12. package/dist/baseline-coverage.js +248 -0
  13. package/dist/baselines-config.d.ts +25 -0
  14. package/dist/baselines-config.js +150 -11
  15. package/dist/baselines.d.ts +37 -8
  16. package/dist/baselines.js +362 -59
  17. package/dist/cli-arguments.d.ts +7 -0
  18. package/dist/cli-arguments.js +47 -0
  19. package/dist/cli-main.d.ts +13 -1
  20. package/dist/cli-main.js +2107 -247
  21. package/dist/client.d.ts +30 -6
  22. package/dist/client.js +160 -35
  23. package/dist/command-result.d.ts +159 -0
  24. package/dist/command-result.js +468 -0
  25. package/dist/coordination.d.ts +15 -0
  26. package/dist/coordination.js +184 -0
  27. package/dist/credentials.d.ts +21 -0
  28. package/dist/credentials.js +105 -0
  29. package/dist/deep-check.js +57 -9
  30. package/dist/design-spec.d.ts +55 -0
  31. package/dist/design-spec.js +275 -0
  32. package/dist/endpoint.d.ts +8 -1
  33. package/dist/endpoint.js +133 -21
  34. package/dist/errors.d.ts +2 -1
  35. package/dist/errors.js +8 -4
  36. package/dist/export.d.ts +18 -0
  37. package/dist/export.js +113 -10
  38. package/dist/file-archive.d.ts +16 -0
  39. package/dist/file-archive.js +58 -0
  40. package/dist/file-guard.d.ts +8 -0
  41. package/dist/file-guard.js +58 -0
  42. package/dist/formats.d.ts +4 -0
  43. package/dist/formats.js +229 -0
  44. package/dist/index.d.ts +34 -1
  45. package/dist/index.js +34 -1
  46. package/dist/inspection.d.ts +106 -0
  47. package/dist/inspection.js +657 -0
  48. package/dist/instance/config-schema.d.ts +422 -0
  49. package/dist/instance/config-schema.js +435 -0
  50. package/dist/instance/config.d.ts +29 -0
  51. package/dist/instance/config.js +191 -29
  52. package/dist/instance/connect.d.ts +10 -1
  53. package/dist/instance/connect.js +178 -30
  54. package/dist/instance/connection.d.ts +39 -4
  55. package/dist/instance/connection.js +198 -14
  56. package/dist/instance/gitignore.js +5 -2
  57. package/dist/instance/init.js +32 -7
  58. package/dist/instance/runner.js +32 -10
  59. package/dist/instance/setup.js +46 -13
  60. package/dist/instance/tty.js +28 -8
  61. package/dist/journal.d.ts +87 -0
  62. package/dist/journal.js +423 -0
  63. package/dist/kit-smoke.d.ts +31 -0
  64. package/dist/kit-smoke.js +145 -0
  65. package/dist/kit-source.d.ts +2 -0
  66. package/dist/kit-source.js +279 -0
  67. package/dist/kit.d.ts +25 -0
  68. package/dist/kit.js +158 -0
  69. package/dist/layout-policy.d.ts +2 -0
  70. package/dist/layout-policy.js +86 -0
  71. package/dist/layout.d.ts +21 -0
  72. package/dist/layout.js +36 -0
  73. package/dist/live-smoke.d.ts +8 -0
  74. package/dist/live-smoke.js +23 -0
  75. package/dist/local-profile.js +2 -1
  76. package/dist/mcp-server.d.ts +12 -0
  77. package/dist/mcp-server.js +415 -0
  78. package/dist/native-api.d.ts +15 -0
  79. package/dist/native-api.js +78 -0
  80. package/dist/onboard.d.ts +10 -0
  81. package/dist/onboard.js +41 -0
  82. package/dist/operation.d.ts +22 -3
  83. package/dist/operation.js +341 -31
  84. package/dist/page-session.d.ts +23 -0
  85. package/dist/page-session.js +142 -0
  86. package/dist/png.d.ts +5 -0
  87. package/dist/png.js +22 -0
  88. package/dist/portable-tokens.d.ts +22 -0
  89. package/dist/portable-tokens.js +331 -0
  90. package/dist/probe.d.ts +6 -2
  91. package/dist/probe.js +64 -11
  92. package/dist/project-input.d.ts +6 -0
  93. package/dist/project-input.js +65 -0
  94. package/dist/project-snippets.js +29 -12
  95. package/dist/project-status.d.ts +24 -0
  96. package/dist/project-status.js +188 -0
  97. package/dist/registry.d.ts +2 -1
  98. package/dist/registry.js +224 -53
  99. package/dist/run-file.d.ts +26 -0
  100. package/dist/run-file.js +199 -0
  101. package/dist/shots.d.ts +43 -0
  102. package/dist/shots.js +172 -0
  103. package/dist/skill-instructions.d.ts +13 -0
  104. package/dist/skill-instructions.js +144 -0
  105. package/dist/skills.d.ts +7 -0
  106. package/dist/skills.js +163 -46
  107. package/dist/snippet.d.ts +3 -2
  108. package/dist/snippet.js +33 -9
  109. package/dist/snippets/archive.d.ts +9 -0
  110. package/dist/snippets/archive.js +10 -4
  111. package/dist/snippets/read.d.ts +1 -1
  112. package/dist/snippets/read.js +78 -41
  113. package/dist/structure-snapshot.d.ts +85 -0
  114. package/dist/structure-snapshot.js +525 -0
  115. package/dist/token-edit.d.ts +53 -0
  116. package/dist/token-edit.js +352 -0
  117. package/dist/token-projection.d.ts +56 -0
  118. package/dist/token-projection.js +286 -0
  119. package/dist/tokens-diff.d.ts +19 -6
  120. package/dist/tokens-diff.js +182 -18
  121. package/dist/version.d.ts +1 -1
  122. package/dist/version.js +1 -1
  123. package/package.json +1 -1
  124. package/skills/penpair-design/SKILL.md +20 -45
  125. package/skills/penpair-design/references/penpot-plugin-gotchas.md +58 -2
  126. package/skills/penpair-design/references/project-facts.md +3 -1
  127. package/skills/penpair-design/references/write-protocol.md +15 -3
  128. package/skills/penpair-handoff/SKILL.md +16 -0
  129. package/skills/penpair-handoff/references/comparison-companion.md +13 -0
  130. package/skills/penpair-handoff/references/handoff-flow.md +13 -0
  131. package/skills/penpair-recipes/SKILL.md +21 -0
  132. package/skills/penpair-recipes/references/build-flow.md +11 -0
  133. package/skills/penpair-setup/SKILL.md +8 -2
  134. package/dist/cli.d.ts +0 -2
  135. package/dist/cli.js +0 -3
  136. package/dist/docker/lifecycle.d.ts +0 -96
  137. package/dist/docker/lifecycle.js +0 -273
  138. package/dist/docker/secrets.d.ts +0 -11
  139. package/dist/docker/secrets.js +0 -38
  140. package/dist/docker/template.d.ts +0 -8
  141. package/dist/docker/template.js +0 -158
  142. package/dist/docker-profile.d.ts +0 -114
  143. package/dist/docker-profile.js +0 -18
  144. package/dist/instance/lifecycle.d.ts +0 -129
  145. package/dist/instance/lifecycle.js +0 -336
  146. package/dist/instance/secrets.d.ts +0 -11
  147. package/dist/instance/secrets.js +0 -38
  148. package/dist/instance/template.d.ts +0 -8
  149. package/dist/instance/template.js +0 -158
@@ -0,0 +1,105 @@
1
+ import { execFile } from "node:child_process";
2
+ import { constants } from "node:fs";
3
+ import { open } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { PenpairError } from "./errors.js";
6
+ export function validateCredentialReference(raw) {
7
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
8
+ throw new Error("Credential reference must be a file or keychain object");
9
+ }
10
+ const v = raw;
11
+ if (v.type === "file") {
12
+ if (Object.keys(v).some((k) => ![
13
+ "type",
14
+ "path",
15
+ ].includes(k)) ||
16
+ typeof v.path !== "string" ||
17
+ !v.path.trim() ||
18
+ v.path.length > 4096 ||
19
+ /[\x00-\x1f]/.test(v.path)) {
20
+ throw new Error("Invalid credential file reference");
21
+ }
22
+ }
23
+ else if (v.type === "keychain") {
24
+ if (Object.keys(v).some((k) => ![
25
+ "type",
26
+ "service",
27
+ "account",
28
+ ].includes(k)) ||
29
+ [
30
+ v.service,
31
+ v.account,
32
+ ].some((s) => typeof s !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9._ /:@-]{0,199}$/.test(s))) {
33
+ throw new Error("Invalid keychain service/account reference");
34
+ }
35
+ }
36
+ else {
37
+ throw new Error("Only file and macOS keychain credential references are supported; arbitrary command providers are deferred");
38
+ }
39
+ return structuredClone(v);
40
+ }
41
+ const keychainRead = (command, args, options) => new Promise((resolve, reject) => execFile(command, args, {
42
+ ...options,
43
+ killSignal: "SIGKILL",
44
+ }, (error, stdout) => error
45
+ ? reject(new Error("Credential provider unavailable or access denied"))
46
+ : resolve(stdout)));
47
+ const normalize = (raw) => {
48
+ const value = raw.trim();
49
+ if (!value || Buffer.byteLength(value) > 8192 || /[\r\n\x00]/.test(value)) {
50
+ throw new PenpairError("endpoint-invalid", "Credential provider must return one bounded MCP URL line");
51
+ }
52
+ return value;
53
+ };
54
+ /** Resolve only at connection time. No URL, key, child stderr or command error object enters diagnostics. */
55
+ export async function readCredential(reference, options) {
56
+ const ref = validateCredentialReference(reference);
57
+ const platform = options.platform ?? process.platform;
58
+ if (ref.type === "keychain") {
59
+ if (platform !== "darwin") {
60
+ throw new PenpairError("endpoint-unconfigured", "macOS Keychain references require macOS; configure a private credential file on this platform");
61
+ }
62
+ try {
63
+ return normalize(await (options.keychainRead ?? keychainRead)("/usr/bin/security", [
64
+ "find-generic-password",
65
+ "-s",
66
+ ref.service,
67
+ "-a",
68
+ ref.account,
69
+ "-w",
70
+ ], {
71
+ timeout: 5000,
72
+ maxBuffer: 16384,
73
+ encoding: "utf8",
74
+ }));
75
+ }
76
+ catch {
77
+ throw new PenpairError("endpoint-unconfigured", "OS credential lookup failed, timed out, or returned invalid output; inspect access in the OS credential UI");
78
+ }
79
+ }
80
+ let handle;
81
+ try {
82
+ handle = await open(path.resolve(options.root, ref.path), constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
83
+ const info = await handle.stat();
84
+ if (!info.isFile() || info.size > 16384) {
85
+ throw new Error("not a bounded regular file");
86
+ }
87
+ if (platform !== "win32" &&
88
+ ((info.mode & 0o077) !== 0 ||
89
+ (typeof process.getuid === "function" && info.uid !== process.getuid()))) {
90
+ throw new Error("not private to current owner");
91
+ }
92
+ const buffer = Buffer.alloc(16385);
93
+ const read = await handle.read(buffer, 0, buffer.length, 0);
94
+ if (read.bytesRead > 16384) {
95
+ throw new Error("credential file byte budget exceeded");
96
+ }
97
+ return normalize(buffer.subarray(0, read.bytesRead).toString("utf8"));
98
+ }
99
+ catch {
100
+ throw new PenpairError("endpoint-unconfigured", "Credential file must be a readable owner-private regular file (0600 on POSIX), not a symlink, with one bounded MCP URL line");
101
+ }
102
+ finally {
103
+ await handle?.close();
104
+ }
105
+ }
@@ -68,26 +68,74 @@ for (const c of penpot.library.local.components) if (c.name.startsWith(tag)) lef
68
68
  return { ok: true, readOnly: true, left };`;
69
69
  export async function runDeepCheck(client, options = {}) {
70
70
  const tag = `penpair-deep-${randomBytes(3).toString("hex")}`;
71
- const first = (await executeCode(client, wrap(deepCheckBody, { tag }), options)).result;
72
- if (first.ok === false)
71
+ const first = (await executeCode(client, wrap(deepCheckBody, {
72
+ tag,
73
+ }), {
74
+ ...options,
75
+ effect: "write",
76
+ })).result;
77
+ if (first.ok === false) {
73
78
  throw new Error(`the deep check could not run: ${first.message ?? "unknown error"}`);
79
+ }
74
80
  await new Promise((resolve) => setTimeout(resolve, options.settleMs ?? 3_000));
75
- const second = (await executeCode(client, wrap(deepLeftoversBody, { tag }), options)).result;
76
- const steps = first.steps ?? [];
77
- const leftovers = second.left ?? [];
81
+ const second = (await executeCode(client, wrap(deepLeftoversBody, {
82
+ tag,
83
+ }), {
84
+ ...options,
85
+ effect: "read",
86
+ })).result;
87
+ const steps = Array.isArray(first.steps)
88
+ ? [
89
+ ...first.steps,
90
+ ]
91
+ : [];
92
+ const required = [
93
+ "create board",
94
+ "flex layout and children",
95
+ "text content",
96
+ "token set, token and binding",
97
+ "component and swap",
98
+ "clone",
99
+ "export png",
100
+ ];
101
+ const complete = required.every((name) => steps.some((step) => step.step === name && step.ok === true));
102
+ if (!complete) {
103
+ steps.push({
104
+ step: "complete evidence",
105
+ ok: false,
106
+ ms: 0,
107
+ detail: "Required deep-check observations are missing or failed",
108
+ });
109
+ }
110
+ const leftovers = Array.isArray(second.left)
111
+ ? second.left
112
+ : [
113
+ "cleanup readback was missing; inspect the scratch file",
114
+ ];
78
115
  return {
79
116
  tag,
80
- ...(first.penpotVersion ? { penpotVersion: first.penpotVersion } : {}),
117
+ ...(first.penpotVersion
118
+ ? {
119
+ penpotVersion: first.penpotVersion,
120
+ }
121
+ : {}),
81
122
  steps,
82
123
  cleanup: first.cleanup ?? [],
83
124
  leftovers,
84
- ok: steps.every((s) => s.ok) && leftovers.length === 0,
125
+ ok: first.ok === true &&
126
+ second.ok === true &&
127
+ complete &&
128
+ steps.every((s) => s.ok === true) &&
129
+ leftovers.length === 0,
85
130
  };
86
131
  }
87
132
  export function formatDeepReport(report) {
88
133
  const lines = report.steps.map((s) => `${s.ok ? "ok " : "FAIL"} ${s.step} (${s.ms} ms)${s.detail ? `: ${s.detail}` : ""}`);
89
- lines.push(report.leftovers.length === 0 ? "ok clean-up: nothing left in the file" : `FAIL clean-up: left in the file, remove by hand: ${report.leftovers.join(", ")}`);
90
- if (report.penpotVersion)
134
+ lines.push(report.leftovers.length === 0
135
+ ? "ok clean-up: nothing left in the file"
136
+ : `FAIL clean-up: left in the file, remove by hand: ${report.leftovers.join(", ")}`);
137
+ if (report.penpotVersion) {
91
138
  lines.push(`Penpot ${report.penpotVersion}`);
139
+ }
92
140
  return lines.join("\n");
93
141
  }
@@ -0,0 +1,55 @@
1
+ import type { InspectionDocument } from "./inspection.js";
2
+ export interface ComponentMapEntry {
3
+ libraryId: string;
4
+ componentId: string;
5
+ codeComponent: string;
6
+ props?: Record<string, string | number | boolean | null>;
7
+ textProps?: Record<string, string>;
8
+ }
9
+ export interface ComponentMap {
10
+ schemaVersion: 1;
11
+ components: ComponentMapEntry[];
12
+ }
13
+ export interface DesignSpec {
14
+ schemaVersion: 1;
15
+ kind: "design-spec";
16
+ source: {
17
+ fileId: string;
18
+ pageId: string;
19
+ revision: number;
20
+ frameId: string;
21
+ sha256: string;
22
+ };
23
+ mappingHash: string;
24
+ nodes: Array<{
25
+ id: string;
26
+ evidence: Record<string, unknown>;
27
+ hint: {
28
+ codeComponent: string;
29
+ props: Record<string, unknown>;
30
+ } | null;
31
+ unknowns: string[];
32
+ }>;
33
+ unknowns: string[];
34
+ }
35
+ export declare const emptyComponentMap: ComponentMap;
36
+ export declare function readDesignJson(root: string, file: string, maxBytes?: number): Promise<unknown>;
37
+ export declare function validateComponentMap(raw: unknown): ComponentMap;
38
+ /** A complete single-root tree is required; no missing descendant may quietly disappear from the handoff. */
39
+ export declare function validateDesignInspection(raw: unknown): InspectionDocument & {
40
+ nodes: Record<string, unknown>[];
41
+ };
42
+ export declare function designSource(inspection: InspectionDocument): {
43
+ fileId: string;
44
+ pageId: string;
45
+ revision: number;
46
+ frameId: string;
47
+ sha256: string;
48
+ };
49
+ export declare function buildDesignSpec(raw: InspectionDocument, mapping?: ComponentMap): DesignSpec;
50
+ export declare function renderDesignSpec(spec: DesignSpec): string;
51
+ export declare function checkDesignSpec(markdown: string, inspection: InspectionDocument, mapping?: ComponentMap): {
52
+ current: boolean;
53
+ reason: string;
54
+ };
55
+ export declare function writeDesignSpec(root: string, out: string, markdown: string, redact?: (text: string) => string): Promise<void>;
@@ -0,0 +1,275 @@
1
+ import { mkdir, readFile, stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { designDataBlock, resolveProjectInput, resolveProjectOutput } from "./project-input.js";
5
+ import { atomicWriteFile } from "./atomic-output.js";
6
+ import { observationHash } from "./journal.js";
7
+ export const emptyComponentMap = {
8
+ schemaVersion: 1,
9
+ components: [],
10
+ };
11
+ export async function readDesignJson(root, file, maxBytes = 16 * 1024 * 1024) {
12
+ const input = await resolveProjectInput(root, file);
13
+ if ((await stat(input)).size > maxBytes) {
14
+ throw new Error("Design artifact exceeds the input byte budget");
15
+ }
16
+ return JSON.parse(await readFile(input, "utf8"));
17
+ }
18
+ export function validateComponentMap(raw) {
19
+ const v = raw;
20
+ if (!v || v.schemaVersion !== 1 || !Array.isArray(v.components) || v.components.length > 1000) {
21
+ throw new Error("Unsupported component-map schema");
22
+ }
23
+ const seen = new Set();
24
+ for (const c of v.components) {
25
+ if (!c ||
26
+ typeof c.libraryId !== "string" ||
27
+ !c.libraryId ||
28
+ typeof c.componentId !== "string" ||
29
+ !c.componentId ||
30
+ !/^[A-Z][A-Za-z0-9]*(?:\.[A-Z][A-Za-z0-9]*)*$/.test(c.codeComponent)) {
31
+ throw new Error("Mappings require explicit library/component IDs and an identifier codeComponent");
32
+ }
33
+ const id = JSON.stringify([
34
+ c.libraryId,
35
+ c.componentId,
36
+ ]);
37
+ if (seen.has(id)) {
38
+ throw new Error("Ambiguous component mapping");
39
+ }
40
+ seen.add(id);
41
+ for (const [key, value] of Object.entries(c.props ?? {})) {
42
+ if (!/^[a-zA-Z][a-zA-Z0-9]*$/.test(key) ||
43
+ (![
44
+ "string",
45
+ "number",
46
+ "boolean",
47
+ ].includes(typeof value) &&
48
+ value !== null) ||
49
+ (typeof value === "number" && !Number.isFinite(value))) {
50
+ throw new Error("Mapping props must be literal scalar data");
51
+ }
52
+ }
53
+ for (const [key, value] of Object.entries(c.textProps ?? {})) {
54
+ if (!/^[a-zA-Z][a-zA-Z0-9]*$/.test(key) || typeof value !== "string" || !value) {
55
+ throw new Error("textProps map prop names to explicit descendant node IDs");
56
+ }
57
+ }
58
+ }
59
+ return structuredClone(v);
60
+ }
61
+ /** A complete single-root tree is required; no missing descendant may quietly disappear from the handoff. */
62
+ export function validateDesignInspection(raw) {
63
+ const v = raw;
64
+ if (!v ||
65
+ v.schemaVersion !== 1 ||
66
+ v.kind !== "shape-inspection" ||
67
+ v.complete !== true ||
68
+ v.redacted === true ||
69
+ typeof v.fileId !== "string" ||
70
+ !v.fileId ||
71
+ typeof v.pageId !== "string" ||
72
+ !v.pageId ||
73
+ !Number.isInteger(v.revision) ||
74
+ !Array.isArray(v.nodes) ||
75
+ !v.nodes.length ||
76
+ v.nodes.length > 50000 ||
77
+ !Array.isArray(v.reasons) ||
78
+ v.reasons.length) {
79
+ throw new Error("A complete unredacted schema-1 shape inspection is required");
80
+ }
81
+ const nodes = v.nodes;
82
+ const byId = new Map(nodes.map((n) => [
83
+ n.id,
84
+ n,
85
+ ]));
86
+ if (byId.size !== nodes.length) {
87
+ throw new Error("Duplicate design node identity");
88
+ }
89
+ let roots = 0;
90
+ for (const n of nodes) {
91
+ if (typeof n.id !== "string" ||
92
+ !n.id ||
93
+ !Array.isArray(n.childIds) ||
94
+ n.childIds.length !== n.childrenTotal ||
95
+ !Number.isInteger(n.index) ||
96
+ Number(n.index) < 0) {
97
+ throw new Error("Invalid design node identity/hierarchy");
98
+ }
99
+ if (n.parentId === null) {
100
+ roots++;
101
+ }
102
+ else if (typeof n.parentId !== "string" || !byId.has(n.parentId)) {
103
+ throw new Error("Missing design parent");
104
+ }
105
+ for (const [index, childId] of n.childIds.entries()) {
106
+ const child = byId.get(childId);
107
+ if (!child || child.parentId !== n.id || child.index !== index) {
108
+ throw new Error("Incomplete or inconsistent design hierarchy");
109
+ }
110
+ }
111
+ if (n.parentId !== null &&
112
+ (byId.get(n.parentId)?.childIds)[Number(n.index)] !== n.id) {
113
+ throw new Error("Unlinked design node");
114
+ }
115
+ }
116
+ if (roots !== 1) {
117
+ throw new Error("Design capture must have one root");
118
+ }
119
+ const seen = new Set();
120
+ const stack = [
121
+ nodes.find((n) => n.parentId === null),
122
+ ];
123
+ while (stack.length) {
124
+ const n = stack.pop();
125
+ if (seen.has(n.id)) {
126
+ throw new Error("Design hierarchy cycle");
127
+ }
128
+ seen.add(n.id);
129
+ stack.push(...n.childIds.map((id) => byId.get(id)));
130
+ }
131
+ if (seen.size !== nodes.length) {
132
+ throw new Error("Disconnected design nodes");
133
+ }
134
+ return v;
135
+ }
136
+ export function designSource(inspection) {
137
+ const v = validateDesignInspection(inspection);
138
+ const nodes = [
139
+ ...v.nodes,
140
+ ].sort((a, b) => String(a.id).localeCompare(String(b.id), "en"));
141
+ return {
142
+ fileId: v.fileId,
143
+ pageId: v.pageId,
144
+ revision: v.revision,
145
+ frameId: String(nodes.find((n) => n.parentId === null).id),
146
+ sha256: observationHash({
147
+ fileId: v.fileId,
148
+ pageId: v.pageId,
149
+ revision: v.revision,
150
+ scope: v.scope ?? null,
151
+ nodes,
152
+ }),
153
+ };
154
+ }
155
+ export function buildDesignSpec(raw, mapping = emptyComponentMap) {
156
+ const capture = validateDesignInspection(raw);
157
+ const map = validateComponentMap(mapping);
158
+ let traversalBudget = 1_000_000;
159
+ const byId = new Map(capture.nodes.map((row) => [
160
+ row.id,
161
+ row,
162
+ ]));
163
+ const nodes = capture.nodes.map((n) => {
164
+ const source = n.component?.source;
165
+ const matches = map.components.filter((c) => c.libraryId === source?.libraryId && c.componentId === source?.id);
166
+ let hint = null;
167
+ const unknowns = Array.isArray(n.unknowns)
168
+ ? n.unknowns.map(String)
169
+ : [
170
+ "inspection-field-availability",
171
+ ];
172
+ for (const field of [
173
+ "geometry",
174
+ "text",
175
+ "layout",
176
+ "component",
177
+ "tokens",
178
+ "style",
179
+ "metadata",
180
+ ]) {
181
+ if (!(field in n)) {
182
+ unknowns.push(`uninspected:${field}`);
183
+ }
184
+ }
185
+ if (matches.length === 1) {
186
+ const match = matches[0];
187
+ const props = {
188
+ ...match.props,
189
+ };
190
+ // Restrict overrides to descendants of this instance, not similarly named text elsewhere in the page.
191
+ const descendants = new Set();
192
+ const pending = [
193
+ ...n.childIds,
194
+ ];
195
+ while (pending.length) {
196
+ if (--traversalBudget < 0) {
197
+ throw new Error("Component override traversal budget exceeded");
198
+ }
199
+ const id = pending.pop();
200
+ const row = byId.get(id);
201
+ descendants.add(id);
202
+ pending.push(...row.childIds);
203
+ }
204
+ for (const [prop, id] of Object.entries(match.textProps ?? {})) {
205
+ const text = byId.get(id)?.text;
206
+ if (!descendants.has(id) || typeof text?.characters !== "string") {
207
+ unknowns.push(`unresolved-text-prop:${prop}`);
208
+ }
209
+ else {
210
+ props[prop] = text.characters;
211
+ }
212
+ }
213
+ hint = {
214
+ codeComponent: match.codeComponent,
215
+ props,
216
+ };
217
+ }
218
+ else if (source) {
219
+ unknowns.push("unmapped-component");
220
+ }
221
+ return {
222
+ id: String(n.id),
223
+ evidence: structuredClone(n),
224
+ hint,
225
+ unknowns,
226
+ };
227
+ });
228
+ return {
229
+ schemaVersion: 1,
230
+ kind: "design-spec",
231
+ source: designSource(capture),
232
+ mappingHash: observationHash(map),
233
+ nodes,
234
+ unknowns: [
235
+ "Interaction behavior and application state require an explicit implementation contract.",
236
+ "Accessibility roles, focus order, keyboard behavior and responsive rules are not inferred from appearance.",
237
+ "Pixel dimensions describe this captured state only; implementation component hints are owner-declared data, not executable JSX.",
238
+ ],
239
+ };
240
+ }
241
+ const sha = (text) => createHash("sha256").update(text).digest("hex");
242
+ export function renderDesignSpec(spec) {
243
+ const body = "# Derived design handoff\n\nGenerated evidence; review unknowns with the design owner before implementation.\n\n" +
244
+ designDataBlock(spec);
245
+ const record = {
246
+ schemaVersion: 1,
247
+ kind: "design-spec-record",
248
+ source: spec.source,
249
+ mappingHash: spec.mappingHash,
250
+ bodyHash: sha(body),
251
+ };
252
+ return `<!-- penpair-spec:${JSON.stringify(record)} -->\n${body}`;
253
+ }
254
+ export function checkDesignSpec(markdown, inspection, mapping = emptyComponentMap) {
255
+ // Exact regeneration detects both changed provenance and manual edits, without executing any embedded design content.
256
+ const current = markdown === renderDesignSpec(buildDesignSpec(inspection, mapping));
257
+ return {
258
+ current,
259
+ reason: current
260
+ ? "Matches supplied inspection and explicit mappings; live freshness is not assessed offline"
261
+ : "Source, mapping or generated document changed",
262
+ };
263
+ }
264
+ export async function writeDesignSpec(root, out, markdown, redact) {
265
+ if (redact && redact(markdown) !== markdown) {
266
+ throw new Error("Design handoff requires redaction; prior output preserved");
267
+ }
268
+ const target = await resolveProjectOutput(root, out);
269
+ await mkdir(path.dirname(target), {
270
+ recursive: true,
271
+ });
272
+ await atomicWriteFile(target, markdown, {
273
+ replace: true,
274
+ });
275
+ }
@@ -1,4 +1,5 @@
1
- export type EndpointSource = "env" | "env-file" | "codex-config" | "claude-config";
1
+ import { type CredentialReference } from "./credentials.js";
2
+ export type EndpointSource = "credential-file" | "keychain" | "env" | "env-file" | "codex-config" | "claude-config";
2
3
  export interface EndpointDescription {
3
4
  source: EndpointSource;
4
5
  origin: string;
@@ -19,6 +20,10 @@ export interface ResolveOptions {
19
20
  variableNames?: string[];
20
21
  /** The MCP server name looked up in the Codex and Claude Code configuration. */
21
22
  serverName?: string;
23
+ /** Long-lived project tools may disable unrelated global client configuration fallback. */
24
+ globalConfig?: boolean;
25
+ credential?: CredentialReference;
26
+ credentialRoot?: string;
22
27
  }
23
28
  /** Reads one `NAME=value` line from env-file text: optional `export`, optional quotes, `#` comments; an empty value counts as unset. */
24
29
  export declare function parseEnvFileValue(text: string, name: string): string | undefined;
@@ -34,3 +39,5 @@ export declare function parseClaudeServerUrl(json: string, serverName?: string):
34
39
  export declare function resolveEndpoint(options?: ResolveOptions): Promise<Endpoint>;
35
40
  /** Removes the endpoint URL, its query string and any userToken parameter from text that may be persisted. */
36
41
  export declare function redactEndpoint(text: string, endpoint?: Endpoint): string;
42
+ /** Known endpoint redaction with optional boundary fragments for serialized inspection chunks. */
43
+ export declare function endpointRedactor(endpoint: Endpoint, fragments?: boolean): (text: string) => string;