@penpair/core 0.3.7 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/dist/adapters.d.ts +1 -1
  2. package/dist/adapters.js +4 -0
  3. package/dist/approvals.d.ts +4 -1
  4. package/dist/approvals.js +7 -4
  5. package/dist/baseline-coverage.js +3 -3
  6. package/dist/baselines-config.d.ts +3 -0
  7. package/dist/baselines-config.js +3 -0
  8. package/dist/baselines.d.ts +7 -0
  9. package/dist/baselines.js +90 -21
  10. package/dist/bounded-file.d.ts +2 -0
  11. package/dist/bounded-file.js +38 -0
  12. package/dist/browser-wake.d.ts +22 -0
  13. package/dist/browser-wake.js +241 -0
  14. package/dist/cli-main.js +193 -18
  15. package/dist/design-commands/context.d.ts +9 -0
  16. package/dist/design-commands/context.js +1 -0
  17. package/dist/design-commands/index.d.ts +3 -0
  18. package/dist/design-commands/index.js +7 -0
  19. package/dist/design-commands/map-command.d.ts +4 -0
  20. package/dist/design-commands/map-command.js +685 -0
  21. package/dist/design-commands/release-command.d.ts +3 -0
  22. package/dist/design-commands/release-command.js +239 -0
  23. package/dist/design-releases/artifact-paths.d.ts +6 -0
  24. package/dist/design-releases/artifact-paths.js +82 -0
  25. package/dist/design-releases/assemble.d.ts +3 -0
  26. package/dist/design-releases/assemble.js +176 -0
  27. package/dist/design-releases/check.d.ts +16 -0
  28. package/dist/design-releases/check.js +323 -0
  29. package/dist/design-releases/context.d.ts +2 -0
  30. package/dist/design-releases/context.js +119 -0
  31. package/dist/design-releases/contract-graph.d.ts +13 -0
  32. package/dist/design-releases/contract-graph.js +75 -0
  33. package/dist/design-releases/fidelity.d.ts +23 -0
  34. package/dist/design-releases/fidelity.js +328 -0
  35. package/dist/design-releases/finalize.d.ts +4 -0
  36. package/dist/design-releases/finalize.js +201 -0
  37. package/dist/design-releases/index.d.ts +8 -0
  38. package/dist/design-releases/index.js +7 -0
  39. package/dist/design-releases/live.d.ts +22 -0
  40. package/dist/design-releases/live.js +97 -0
  41. package/dist/design-releases/snippets.d.ts +5 -0
  42. package/dist/design-releases/snippets.js +89 -0
  43. package/dist/design-releases/store.d.ts +10 -0
  44. package/dist/design-releases/store.js +205 -0
  45. package/dist/design-releases/types.d.ts +161 -0
  46. package/dist/design-releases/types.js +1 -0
  47. package/dist/design-releases/validation.d.ts +6 -0
  48. package/dist/design-releases/validation.js +384 -0
  49. package/dist/design-releases/verify.d.ts +3 -0
  50. package/dist/design-releases/verify.js +148 -0
  51. package/dist/errors.js +1 -1
  52. package/dist/image-codec.d.ts +3 -0
  53. package/dist/image-codec.js +78 -0
  54. package/dist/image-evidence.d.ts +15 -0
  55. package/dist/image-evidence.js +121 -0
  56. package/dist/index.d.ts +5 -0
  57. package/dist/index.js +5 -0
  58. package/dist/inspection.js +13 -5
  59. package/dist/instance/config-schema.d.ts +4 -0
  60. package/dist/instance/config-schema.js +7 -0
  61. package/dist/instance/connection.d.ts +2 -0
  62. package/dist/instance/connection.js +15 -3
  63. package/dist/kit-source.js +1 -1
  64. package/dist/layout-policy.js +21 -11
  65. package/dist/layout-settling.d.ts +1 -0
  66. package/dist/layout-settling.js +51 -17
  67. package/dist/layout.d.ts +7 -1
  68. package/dist/layout.js +19 -3
  69. package/dist/map-penpot/content-validation.d.ts +3 -0
  70. package/dist/map-penpot/content-validation.js +57 -0
  71. package/dist/map-penpot/content.d.ts +22 -0
  72. package/dist/map-penpot/content.js +173 -0
  73. package/dist/map-penpot/details.d.ts +4 -0
  74. package/dist/map-penpot/details.js +84 -0
  75. package/dist/map-penpot/evidence-error.d.ts +5 -0
  76. package/dist/map-penpot/evidence-error.js +9 -0
  77. package/dist/map-penpot/icons.d.ts +5 -0
  78. package/dist/map-penpot/icons.js +79 -0
  79. package/dist/map-penpot/import-content.d.ts +2 -0
  80. package/dist/map-penpot/import-content.js +150 -0
  81. package/dist/map-penpot/import.d.ts +5 -0
  82. package/dist/map-penpot/import.js +255 -0
  83. package/dist/map-penpot/index.d.ts +11 -0
  84. package/dist/map-penpot/index.js +10 -0
  85. package/dist/map-penpot/path-geometry.d.ts +5 -0
  86. package/dist/map-penpot/path-geometry.js +51 -0
  87. package/dist/map-penpot/presentation.d.ts +20 -0
  88. package/dist/map-penpot/presentation.js +205 -0
  89. package/dist/map-penpot/projection.d.ts +8 -0
  90. package/dist/map-penpot/projection.js +176 -0
  91. package/dist/map-penpot/publish.d.ts +4 -0
  92. package/dist/map-penpot/publish.js +301 -0
  93. package/dist/map-penpot/release-maps.d.ts +21 -0
  94. package/dist/map-penpot/release-maps.js +104 -0
  95. package/dist/map-penpot/snippets.d.ts +4 -0
  96. package/dist/map-penpot/snippets.js +164 -0
  97. package/dist/map-penpot/status-labels.d.ts +7 -0
  98. package/dist/map-penpot/status-labels.js +7 -0
  99. package/dist/map-penpot/types.d.ts +161 -0
  100. package/dist/map-penpot/types.js +1 -0
  101. package/dist/maps/annotation-layout.d.ts +24 -0
  102. package/dist/maps/annotation-layout.js +85 -0
  103. package/dist/maps/context.d.ts +13 -0
  104. package/dist/maps/context.js +207 -0
  105. package/dist/maps/index.d.ts +9 -0
  106. package/dist/maps/index.js +9 -0
  107. package/dist/maps/layout.d.ts +35 -0
  108. package/dist/maps/layout.js +364 -0
  109. package/dist/maps/proposal.d.ts +5 -0
  110. package/dist/maps/proposal.js +67 -0
  111. package/dist/maps/render.d.ts +4 -0
  112. package/dist/maps/render.js +331 -0
  113. package/dist/maps/review.d.ts +16 -0
  114. package/dist/maps/review.js +90 -0
  115. package/dist/maps/store.d.ts +21 -0
  116. package/dist/maps/store.js +222 -0
  117. package/dist/maps/types.d.ts +206 -0
  118. package/dist/maps/types.js +1 -0
  119. package/dist/maps/validation.d.ts +10 -0
  120. package/dist/maps/validation.js +375 -0
  121. package/dist/plugin-health.d.ts +13 -0
  122. package/dist/plugin-health.js +39 -0
  123. package/dist/skill-routing.js +4 -0
  124. package/dist/version.d.ts +1 -1
  125. package/dist/version.js +1 -1
  126. package/package.json +2 -1
  127. package/skills/penpair-design-release/SKILL.md +55 -0
  128. package/skills/penpair-design-release/agents/openai.yaml +6 -0
  129. package/skills/penpair-design-release/references/workflow.md +55 -0
  130. package/skills/penpair-diagrams/SKILL.md +52 -0
  131. package/skills/penpair-diagrams/agents/openai.yaml +6 -0
  132. package/skills/penpair-diagrams/references/workflow.md +84 -0
  133. package/skills/penpair-handoff/SKILL.md +7 -0
  134. package/skills/penpair-handoff/references/handoff-flow.md +7 -1
  135. package/skills/penpair-integration-map/SKILL.md +44 -0
  136. package/skills/penpair-integration-map/agents/openai.yaml +6 -0
  137. package/skills/penpair-integration-map/references/workflow.md +33 -0
  138. package/skills/penpair-journey-map/SKILL.md +42 -0
  139. package/skills/penpair-journey-map/agents/openai.yaml +6 -0
  140. package/skills/penpair-journey-map/references/workflow.md +35 -0
  141. package/skills/penpair-verify/SKILL.md +7 -0
  142. package/skills/penpair-verify/references/browser-evidence.md +10 -0
@@ -0,0 +1,206 @@
1
+ export type MapKind = "journey-map" | "integration-map";
2
+ export type MapOrigin = "observed" | "proposed" | "owner-decision" | "unknown";
3
+ export type MapIcon = "screen" | "action" | "decision" | "data" | "service" | "code" | "check" | "link";
4
+ export interface MapSource {
5
+ id: string;
6
+ kind: "inspection" | "baseline" | "code" | "contract" | "other";
7
+ sha256: string;
8
+ path?: string;
9
+ fileId?: string;
10
+ pageId?: string;
11
+ frameId?: string;
12
+ revision?: number;
13
+ }
14
+ export interface MapNode {
15
+ id: string;
16
+ label: string;
17
+ summary?: string;
18
+ description?: string;
19
+ icon?: MapIcon;
20
+ kind: string;
21
+ origin: MapOrigin;
22
+ requirementIds?: string[];
23
+ sourceIds?: string[];
24
+ }
25
+ export interface MapEdge {
26
+ id: string;
27
+ from: string;
28
+ to: string;
29
+ label: string;
30
+ origin: MapOrigin;
31
+ requirementIds?: string[];
32
+ }
33
+ export interface MapRequirement {
34
+ id: string;
35
+ text: string;
36
+ origin: MapOrigin;
37
+ sourceIds?: string[];
38
+ }
39
+ export interface MapFinding {
40
+ id: string;
41
+ message: string;
42
+ severity: "blocking" | "warning" | "info";
43
+ status: "open" | "resolved";
44
+ resolution?: string;
45
+ }
46
+ export interface MapContract {
47
+ schemaVersion: 1;
48
+ kind: MapKind;
49
+ id: string;
50
+ title: string;
51
+ goal: string;
52
+ summary?: string;
53
+ description?: string;
54
+ sources: MapSource[];
55
+ nodes: MapNode[];
56
+ edges: MapEdge[];
57
+ requirements: MapRequirement[];
58
+ findings: MapFinding[];
59
+ journey?: {
60
+ path: string;
61
+ sha256: string;
62
+ };
63
+ presentation?: {
64
+ fontFamily: string;
65
+ };
66
+ }
67
+ export interface JourneyMap extends MapContract {
68
+ kind: "journey-map";
69
+ }
70
+ export interface IntegrationMap extends MapContract {
71
+ kind: "integration-map";
72
+ }
73
+ export interface ArtifactReview {
74
+ schemaVersion: 1;
75
+ kind: "artifact-review";
76
+ id: string;
77
+ artifactKind: MapKind | "design-release" | "design-evidence";
78
+ artifactId: string;
79
+ artifactHash: string;
80
+ by: string;
81
+ note: string;
82
+ recordedAt: string;
83
+ artifacts?: Array<{
84
+ path: string;
85
+ sha256: string;
86
+ }>;
87
+ hash: string;
88
+ }
89
+ export interface MapProposal {
90
+ schemaVersion: 1;
91
+ kind: "map-proposal";
92
+ baseHash: string;
93
+ proposedHash: string;
94
+ proposedMap: MapContract;
95
+ reason: string;
96
+ hash: string;
97
+ }
98
+ export interface MapDrawingNode extends MapNode {
99
+ cardTitle: string;
100
+ cardSummary?: string;
101
+ annotationId?: string;
102
+ detail?: MapDetailTarget;
103
+ x: number;
104
+ y: number;
105
+ width: number;
106
+ height: number;
107
+ }
108
+ export interface MapDrawingEdge extends MapEdge {
109
+ labelBox: {
110
+ x: number;
111
+ y: number;
112
+ width: number;
113
+ height: number;
114
+ };
115
+ points: Array<{
116
+ x: number;
117
+ y: number;
118
+ }>;
119
+ }
120
+ export interface MapDetailTarget {
121
+ sourceId: string;
122
+ path: string;
123
+ sha256: string;
124
+ fileId: string;
125
+ pageId: string;
126
+ frameId: string;
127
+ mapId: string;
128
+ contractHash: string;
129
+ }
130
+ export interface MapDetailContext {
131
+ nodeId: string;
132
+ sourceId: string;
133
+ path: string;
134
+ sha256: string;
135
+ fileId: string;
136
+ pageId: string;
137
+ frameId: string;
138
+ mapId?: string;
139
+ mapHash?: string;
140
+ ready: boolean;
141
+ review: "separate-artifact";
142
+ contents: "not-inlined";
143
+ }
144
+ export interface MapDrawingPlan {
145
+ schemaVersion: 1;
146
+ kind: "map-drawing-plan";
147
+ mapKind: MapKind;
148
+ mapId: string;
149
+ mapHash: string;
150
+ title: string;
151
+ goal: string;
152
+ summary?: string;
153
+ description?: string;
154
+ headerSummary?: string;
155
+ header: {
156
+ titleBox: MapDrawingBox;
157
+ summaryBox?: MapDrawingBox;
158
+ legendY: number;
159
+ height: number;
160
+ };
161
+ annotations: MapDrawingAnnotation[];
162
+ fontFamily: string;
163
+ width: number;
164
+ height: number;
165
+ nodes: MapDrawingNode[];
166
+ edges: MapDrawingEdge[];
167
+ complete: boolean;
168
+ reasons: string[];
169
+ }
170
+ export interface MapDrawingBox {
171
+ x: number;
172
+ y: number;
173
+ width: number;
174
+ height: number;
175
+ }
176
+ export interface MapDrawingAnnotation extends MapDrawingBox {
177
+ id: string;
178
+ nodeId?: string;
179
+ title: string;
180
+ goal?: string;
181
+ summary?: string;
182
+ description?: string;
183
+ }
184
+ export interface MapContext {
185
+ schemaVersion: 1;
186
+ kind: "map-context";
187
+ mapKind: MapKind;
188
+ mapId: string;
189
+ mapHash: string;
190
+ title: string;
191
+ goal: string;
192
+ summary?: string;
193
+ description?: string;
194
+ reviewed: boolean;
195
+ ready: boolean;
196
+ freshness: "matches-supplied-inputs" | "stale" | "unassessed";
197
+ reasons: string[];
198
+ nodes: MapNode[];
199
+ edges: MapEdge[];
200
+ requirements: MapRequirement[];
201
+ findings: MapFinding[];
202
+ sources: MapSource[];
203
+ truncated: boolean;
204
+ details: MapDetailContext[];
205
+ implementation: "not-assessed";
206
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,10 @@
1
+ import type { MapContract, MapNode } from "./types.js";
2
+ export declare const maxMapBytes: number;
3
+ export declare function mapObject(value: unknown, label: string, keys: string[]): Record<string, unknown>;
4
+ export declare function mapText(value: unknown, label: string, maxLength?: number): string;
5
+ export declare function mapId(value: unknown, label: string): string;
6
+ export declare function mapHash(value: unknown, label: string): string;
7
+ export declare function mapPath(value: unknown, label: string): string;
8
+ export declare function validateMapContract(raw: unknown): MapContract;
9
+ export declare function hashMapContract(raw: unknown): string;
10
+ export declare function validateMapDetail(parent: Pick<MapContract, "kind" | "id" | "sources">, node: Pick<MapNode, "id" | "sourceIds" | "requirementIds">, raw: unknown): MapContract;
@@ -0,0 +1,375 @@
1
+ import path from "node:path";
2
+ import { observationHash } from "../journal.js";
3
+ export const maxMapBytes = 16 * 1024 * 1024;
4
+ export function mapObject(value, label, keys) {
5
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
6
+ throw new Error(`${label} must be an object`);
7
+ }
8
+ const object = value;
9
+ if (Object.keys(object).some((key) => !keys.includes(key))) {
10
+ throw new Error(`${label} contains an unsupported field`);
11
+ }
12
+ return object;
13
+ }
14
+ export function mapText(value, label, maxLength = 2000) {
15
+ if (typeof value !== "string" ||
16
+ !value.trim() ||
17
+ value.length > maxLength ||
18
+ value.includes("\0")) {
19
+ throw new Error(`${label} requires nonempty text of at most ${maxLength} characters`);
20
+ }
21
+ return value;
22
+ }
23
+ export function mapId(value, label) {
24
+ if (typeof value !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$/.test(value)) {
25
+ throw new Error(`${label} requires a stable logical ID`);
26
+ }
27
+ return value;
28
+ }
29
+ export function mapHash(value, label) {
30
+ if (typeof value !== "string" || !/^[a-f0-9]{64}$/.test(value)) {
31
+ throw new Error(`${label} requires a SHA-256 hash`);
32
+ }
33
+ return value;
34
+ }
35
+ export function mapPath(value, label) {
36
+ const entry = mapText(value, label, 2000);
37
+ if (path.isAbsolute(entry) || /^[a-zA-Z]:/.test(entry) || entry.split(/[\\/]/).includes("..")) {
38
+ throw new Error(`${label} must stay inside the project`);
39
+ }
40
+ return entry;
41
+ }
42
+ function mapOrigin(value) {
43
+ if (value !== "observed" &&
44
+ value !== "proposed" &&
45
+ value !== "owner-decision" &&
46
+ value !== "unknown") {
47
+ throw new Error("Map origin must distinguish observed, proposed, owner-decision or unknown");
48
+ }
49
+ return value;
50
+ }
51
+ function mapList(value, label, maximum) {
52
+ if (!Array.isArray(value) || value.length > maximum) {
53
+ throw new Error(`${label} must contain at most ${maximum} entries`);
54
+ }
55
+ return value;
56
+ }
57
+ function references(value, label) {
58
+ if (value === undefined) {
59
+ return undefined;
60
+ }
61
+ const entries = mapList(value, label, 1000).map((entry) => mapId(entry, label));
62
+ if (new Set(entries).size !== entries.length) {
63
+ throw new Error(`${label} contains duplicate references`);
64
+ }
65
+ return entries.sort();
66
+ }
67
+ function optionalReferences(object, key) {
68
+ const value = references(object[key], key);
69
+ return value === undefined
70
+ ? {}
71
+ : {
72
+ [key]: value,
73
+ };
74
+ }
75
+ function validateSource(raw) {
76
+ const source = mapObject(raw, "Map source", [
77
+ "id",
78
+ "kind",
79
+ "sha256",
80
+ "path",
81
+ "fileId",
82
+ "pageId",
83
+ "frameId",
84
+ "revision",
85
+ ]);
86
+ if (typeof source.kind !== "string" ||
87
+ ![
88
+ "inspection",
89
+ "baseline",
90
+ "code",
91
+ "contract",
92
+ "other",
93
+ ].includes(source.kind)) {
94
+ throw new Error("Unsupported map source kind");
95
+ }
96
+ const result = {
97
+ id: mapId(source.id, "Source ID"),
98
+ kind: source.kind,
99
+ sha256: mapHash(source.sha256, "Source hash"),
100
+ };
101
+ if (source.path !== undefined) {
102
+ result.path = mapPath(source.path, "Source path");
103
+ }
104
+ for (const key of [
105
+ "fileId",
106
+ "pageId",
107
+ "frameId",
108
+ ]) {
109
+ if (source[key] !== undefined) {
110
+ result[key] = mapText(source[key], key, 200);
111
+ }
112
+ }
113
+ if (source.revision !== undefined) {
114
+ if (!Number.isSafeInteger(source.revision) || Number(source.revision) < 0) {
115
+ throw new Error("Source revision must be a nonnegative safe integer");
116
+ }
117
+ result.revision = Number(source.revision);
118
+ }
119
+ return result;
120
+ }
121
+ function validateNode(raw) {
122
+ const node = mapObject(raw, "Map node", [
123
+ "id",
124
+ "label",
125
+ "summary",
126
+ "description",
127
+ "icon",
128
+ "kind",
129
+ "origin",
130
+ "requirementIds",
131
+ "sourceIds",
132
+ ]);
133
+ const result = {
134
+ id: mapId(node.id, "Node ID"),
135
+ label: mapText(node.label, "Node label"),
136
+ kind: mapId(node.kind, "Node kind"),
137
+ origin: mapOrigin(node.origin),
138
+ ...optionalReferences(node, "requirementIds"),
139
+ ...optionalReferences(node, "sourceIds"),
140
+ };
141
+ if (node.summary !== undefined) {
142
+ result.summary = mapText(node.summary, "Node summary", 240);
143
+ }
144
+ if (node.description !== undefined) {
145
+ result.description = mapText(node.description, "Node description", 4000);
146
+ }
147
+ if (node.icon !== undefined) {
148
+ if (typeof node.icon !== "string" ||
149
+ ![
150
+ "screen",
151
+ "action",
152
+ "decision",
153
+ "data",
154
+ "service",
155
+ "code",
156
+ "check",
157
+ "link",
158
+ ].includes(node.icon)) {
159
+ throw new Error("Node icon requires a supported semantic icon");
160
+ }
161
+ result.icon = node.icon;
162
+ }
163
+ return result;
164
+ }
165
+ function validateEdge(raw) {
166
+ const edge = mapObject(raw, "Map edge", [
167
+ "id",
168
+ "from",
169
+ "to",
170
+ "label",
171
+ "origin",
172
+ "requirementIds",
173
+ ]);
174
+ return {
175
+ id: mapId(edge.id, "Edge ID"),
176
+ from: mapId(edge.from, "Edge source"),
177
+ to: mapId(edge.to, "Edge target"),
178
+ label: mapText(edge.label, "Edge label"),
179
+ origin: mapOrigin(edge.origin),
180
+ ...optionalReferences(edge, "requirementIds"),
181
+ };
182
+ }
183
+ function validateRequirement(raw) {
184
+ const requirement = mapObject(raw, "Map requirement", [
185
+ "id",
186
+ "text",
187
+ "origin",
188
+ "sourceIds",
189
+ ]);
190
+ return {
191
+ id: mapId(requirement.id, "Requirement ID"),
192
+ text: mapText(requirement.text, "Requirement text", 8000),
193
+ origin: mapOrigin(requirement.origin),
194
+ ...optionalReferences(requirement, "sourceIds"),
195
+ };
196
+ }
197
+ function validateFinding(raw) {
198
+ const finding = mapObject(raw, "Map finding", [
199
+ "id",
200
+ "message",
201
+ "severity",
202
+ "status",
203
+ "resolution",
204
+ ]);
205
+ if (typeof finding.severity !== "string" ||
206
+ ![
207
+ "blocking",
208
+ "warning",
209
+ "info",
210
+ ].includes(finding.severity) ||
211
+ (finding.status !== "open" && finding.status !== "resolved")) {
212
+ throw new Error("Map findings require explicit severity and open/resolved status");
213
+ }
214
+ const result = {
215
+ id: mapId(finding.id, "Finding ID"),
216
+ message: mapText(finding.message, "Finding message", 8000),
217
+ severity: finding.severity,
218
+ status: finding.status,
219
+ };
220
+ if (finding.resolution !== undefined) {
221
+ result.resolution = mapText(finding.resolution, "Finding resolution", 8000);
222
+ }
223
+ if (result.status === "resolved" && !result.resolution) {
224
+ throw new Error("A resolved finding requires an explicit resolution");
225
+ }
226
+ return result;
227
+ }
228
+ function uniqueSorted(entries, label) {
229
+ if (new Set(entries.map((entry) => entry.id)).size !== entries.length) {
230
+ throw new Error(`Duplicate ${label} ID`);
231
+ }
232
+ return entries.sort((left, right) => (left.id < right.id ? -1 : left.id > right.id ? 1 : 0));
233
+ }
234
+ function validateReferences(contract) {
235
+ const nodeIds = new Set(contract.nodes.map((node) => node.id));
236
+ const sourceIds = new Set(contract.sources.map((source) => source.id));
237
+ const requirementIds = new Set(contract.requirements.map((requirement) => requirement.id));
238
+ for (const edge of contract.edges) {
239
+ if (!nodeIds.has(edge.from) || !nodeIds.has(edge.to)) {
240
+ throw new Error(`Edge ${edge.id} refers to a missing node`);
241
+ }
242
+ }
243
+ for (const item of [
244
+ ...contract.nodes,
245
+ ...contract.edges,
246
+ ]) {
247
+ if ((item.requirementIds ?? []).some((id) => !requirementIds.has(id))) {
248
+ throw new Error(`${item.id} refers to a missing requirement`);
249
+ }
250
+ }
251
+ for (const item of [
252
+ ...contract.nodes,
253
+ ...contract.requirements,
254
+ ]) {
255
+ if ((item.sourceIds ?? []).some((id) => !sourceIds.has(id))) {
256
+ throw new Error(`${item.id} refers to a missing source`);
257
+ }
258
+ if (item.origin === "observed" && !item.sourceIds?.length) {
259
+ throw new Error(`Observed ${item.id} requires an explicit source reference`);
260
+ }
261
+ }
262
+ const details = contract.nodes.filter((node) => node.kind === "detail-link");
263
+ if (details.length > 64) {
264
+ throw new Error("A map supports at most 64 explicit direct detail links");
265
+ }
266
+ for (const node of details) {
267
+ const source = contract.sources.find((entry) => entry.id === node.sourceIds?.[0]);
268
+ if (node.sourceIds?.length !== 1 ||
269
+ !node.requirementIds?.length ||
270
+ !source ||
271
+ source.kind !== "contract" ||
272
+ !source.path ||
273
+ !source.fileId ||
274
+ !source.pageId ||
275
+ !source.frameId) {
276
+ throw new Error("Detail links require one contract source with path, raw SHA, file/page/frame IDs and explicit mapped requirements");
277
+ }
278
+ }
279
+ if (details.length &&
280
+ new Set(contract.sources.flatMap((source) => source.fileId
281
+ ? [
282
+ source.fileId,
283
+ ]
284
+ : [])).size > 1) {
285
+ throw new Error("Linked native detail boards must belong to the same pinned file");
286
+ }
287
+ }
288
+ export function validateMapContract(raw) {
289
+ const encoded = JSON.stringify(raw);
290
+ if (!encoded || Buffer.byteLength(encoded) > maxMapBytes) {
291
+ throw new Error("Map contract exceeds the 16 MiB byte budget");
292
+ }
293
+ const object = mapObject(raw, "Map contract", [
294
+ "schemaVersion",
295
+ "kind",
296
+ "id",
297
+ "title",
298
+ "goal",
299
+ "summary",
300
+ "description",
301
+ "sources",
302
+ "nodes",
303
+ "edges",
304
+ "requirements",
305
+ "findings",
306
+ "journey",
307
+ "presentation",
308
+ ]);
309
+ if (object.schemaVersion !== 1 ||
310
+ (object.kind !== "journey-map" && object.kind !== "integration-map")) {
311
+ throw new Error("Unsupported map schema or kind");
312
+ }
313
+ const contract = {
314
+ schemaVersion: 1,
315
+ kind: object.kind,
316
+ id: mapId(object.id, "Map ID"),
317
+ title: mapText(object.title, "Map title", 200),
318
+ goal: mapText(object.goal, "Map goal", 8000),
319
+ sources: uniqueSorted(mapList(object.sources, "Map sources", 1000).map(validateSource), "source"),
320
+ nodes: uniqueSorted(mapList(object.nodes, "Map nodes", 1000).map(validateNode), "node"),
321
+ edges: uniqueSorted(mapList(object.edges, "Map edges", 2000).map(validateEdge), "edge"),
322
+ requirements: uniqueSorted(mapList(object.requirements, "Map requirements", 1000).map(validateRequirement), "requirement"),
323
+ findings: uniqueSorted(mapList(object.findings, "Map findings", 2000).map(validateFinding), "finding"),
324
+ };
325
+ if (!contract.nodes.length) {
326
+ throw new Error("A map requires at least one explicit node");
327
+ }
328
+ if (object.summary !== undefined) {
329
+ contract.summary = mapText(object.summary, "Map summary", 240);
330
+ }
331
+ if (object.description !== undefined) {
332
+ contract.description = mapText(object.description, "Map description", 8000);
333
+ }
334
+ if (object.journey !== undefined) {
335
+ if (contract.kind !== "integration-map") {
336
+ throw new Error("Only an integration map may reference a journey contract");
337
+ }
338
+ const journey = mapObject(object.journey, "Journey reference", [
339
+ "path",
340
+ "sha256",
341
+ ]);
342
+ contract.journey = {
343
+ path: mapPath(journey.path, "Journey path"),
344
+ sha256: mapHash(journey.sha256, "Journey hash"),
345
+ };
346
+ }
347
+ if (object.presentation !== undefined) {
348
+ const presentation = mapObject(object.presentation, "Map presentation", [
349
+ "fontFamily",
350
+ ]);
351
+ contract.presentation = {
352
+ fontFamily: mapText(presentation.fontFamily, "Map font", 200),
353
+ };
354
+ }
355
+ validateReferences(contract);
356
+ return contract;
357
+ }
358
+ export function hashMapContract(raw) {
359
+ return observationHash(validateMapContract(raw));
360
+ }
361
+ export function validateMapDetail(parent, node, raw) {
362
+ const detail = validateMapContract(raw);
363
+ if (detail.kind !== parent.kind || detail.id === parent.id) {
364
+ throw new Error("A detail must reference a different contract of the same map kind");
365
+ }
366
+ const required = new Set(detail.requirements.map((requirement) => requirement.id));
367
+ if ((node.requirementIds ?? []).some((id) => !required.has(id))) {
368
+ throw new Error(`Linked detail omits a requirement assigned to ${node.id}`);
369
+ }
370
+ const source = parent.sources.find((entry) => entry.id === node.sourceIds?.[0]);
371
+ if (source && detail.sources.some((entry) => entry.fileId && entry.fileId !== source.fileId)) {
372
+ throw new Error("Linked detail sources belong to a different pinned native file");
373
+ }
374
+ return detail;
375
+ }
@@ -0,0 +1,13 @@
1
+ export interface PluginHealth {
2
+ state: "responsive" | "suspended" | "unknown";
3
+ heartbeatAgeMs: number | null;
4
+ heartbeatSource: "upstream-error" | "unavailable";
5
+ keepAlive: {
6
+ method: "unknown";
7
+ verified: false;
8
+ };
9
+ }
10
+ /** Fresh execution proves responsiveness, not the plugin's heartbeat time or browser policy. */
11
+ export declare function responsivePluginHealth(): PluginHealth;
12
+ /** Upstream error text is the only heartbeat telemetry exposed by the current connection. */
13
+ export declare function pluginHealthFromError(error: unknown): PluginHealth;
@@ -0,0 +1,39 @@
1
+ function health(state, heartbeatAgeMs) {
2
+ return {
3
+ state,
4
+ heartbeatAgeMs,
5
+ heartbeatSource: heartbeatAgeMs === null ? "unavailable" : "upstream-error",
6
+ keepAlive: {
7
+ method: "unknown",
8
+ verified: false,
9
+ },
10
+ };
11
+ }
12
+ /** Fresh execution proves responsiveness, not the plugin's heartbeat time or browser policy. */
13
+ export function responsivePluginHealth() {
14
+ return health("responsive", null);
15
+ }
16
+ /** Upstream error text is the only heartbeat telemetry exposed by the current connection. */
17
+ export function pluginHealthFromError(error) {
18
+ const message = error instanceof Error
19
+ ? error.message.slice(0, 8192)
20
+ : typeof error === "string"
21
+ ? error.slice(0, 8192)
22
+ : "";
23
+ const code = typeof error === "object" && error !== null && "code" in error ? error.code : undefined;
24
+ const suspended = code === "plugin-suspended" || /\bsuspended\b|\bno heartbeat\b/i.test(message);
25
+ if (!suspended) {
26
+ return health("unknown", null);
27
+ }
28
+ const duration = /\bno heartbeat\s+for\s+([^\s,)]+?)\s*(milliseconds?|ms|seconds?|s)\b/i.exec(message);
29
+ if (!duration || !/^\d+(?:\.\d+)?$/.test(duration[1])) {
30
+ return health("suspended", null);
31
+ }
32
+ const value = Number(duration[1]);
33
+ const heartbeatAgeMs = value * (/^(?:ms|milliseconds?)$/i.test(duration[2]) ? 1 : 1000);
34
+ return health("suspended", Number.isFinite(heartbeatAgeMs) &&
35
+ heartbeatAgeMs >= 0 &&
36
+ heartbeatAgeMs <= Number.MAX_SAFE_INTEGER
37
+ ? heartbeatAgeMs
38
+ : null);
39
+ }
@@ -1,4 +1,8 @@
1
1
  const tasksBySkill = {
2
+ "penpair-journey-map": "Map user journeys, decisions and recovery paths in Penpot",
3
+ "penpair-integration-map": "Connect reviewed journeys to routes, components and data responsibilities",
4
+ "penpair-diagrams": "Draw or reconcile editable native Penpot diagrams",
5
+ "penpair-design-release": "Assemble and review a release/<version> design page",
2
6
  "penpair-setup": "Install Penpair or repair a Penpot connection",
3
7
  "penpair-design": "Inspect, edit, audit or export a Penpot file",
4
8
  "penpair-recipes": "Build a Penpot screen or derive a state variant from an approved frame",
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.3.7";
2
+ export declare const version = "0.4.1";
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.3.7";
2
+ export const version = "0.4.1";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@penpair/core",
3
- "version": "0.3.7",
3
+ "version": "0.4.1",
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": {
@@ -45,6 +45,7 @@
45
45
  "dependencies": {
46
46
  "@modelcontextprotocol/sdk": "1.32.0",
47
47
  "acorn": "8.19.0",
48
+ "sharp": "0.35.5",
48
49
  "yaml": "2.9.1"
49
50
  },
50
51
  "scripts": {