@penpair/core 0.3.6 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/dist/adapters.d.ts +1 -1
  2. package/dist/adapters.js +3 -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/cli-main.js +64 -3
  13. package/dist/design-commands/context.d.ts +9 -0
  14. package/dist/design-commands/context.js +1 -0
  15. package/dist/design-commands/index.d.ts +3 -0
  16. package/dist/design-commands/index.js +7 -0
  17. package/dist/design-commands/map-command.d.ts +4 -0
  18. package/dist/design-commands/map-command.js +685 -0
  19. package/dist/design-commands/release-command.d.ts +3 -0
  20. package/dist/design-commands/release-command.js +239 -0
  21. package/dist/design-releases/artifact-paths.d.ts +6 -0
  22. package/dist/design-releases/artifact-paths.js +82 -0
  23. package/dist/design-releases/assemble.d.ts +3 -0
  24. package/dist/design-releases/assemble.js +176 -0
  25. package/dist/design-releases/check.d.ts +16 -0
  26. package/dist/design-releases/check.js +323 -0
  27. package/dist/design-releases/context.d.ts +2 -0
  28. package/dist/design-releases/context.js +119 -0
  29. package/dist/design-releases/contract-graph.d.ts +13 -0
  30. package/dist/design-releases/contract-graph.js +75 -0
  31. package/dist/design-releases/fidelity.d.ts +23 -0
  32. package/dist/design-releases/fidelity.js +328 -0
  33. package/dist/design-releases/finalize.d.ts +4 -0
  34. package/dist/design-releases/finalize.js +201 -0
  35. package/dist/design-releases/index.d.ts +8 -0
  36. package/dist/design-releases/index.js +7 -0
  37. package/dist/design-releases/live.d.ts +22 -0
  38. package/dist/design-releases/live.js +97 -0
  39. package/dist/design-releases/snippets.d.ts +5 -0
  40. package/dist/design-releases/snippets.js +89 -0
  41. package/dist/design-releases/store.d.ts +10 -0
  42. package/dist/design-releases/store.js +205 -0
  43. package/dist/design-releases/types.d.ts +161 -0
  44. package/dist/design-releases/types.js +1 -0
  45. package/dist/design-releases/validation.d.ts +6 -0
  46. package/dist/design-releases/validation.js +384 -0
  47. package/dist/design-releases/verify.d.ts +3 -0
  48. package/dist/design-releases/verify.js +148 -0
  49. package/dist/image-codec.d.ts +3 -0
  50. package/dist/image-codec.js +78 -0
  51. package/dist/image-evidence.d.ts +15 -0
  52. package/dist/image-evidence.js +121 -0
  53. package/dist/index.d.ts +5 -0
  54. package/dist/index.js +5 -0
  55. package/dist/inspection.js +13 -5
  56. package/dist/instance/config-schema.d.ts +4 -0
  57. package/dist/instance/config-schema.js +7 -0
  58. package/dist/map-penpot/content-validation.d.ts +3 -0
  59. package/dist/map-penpot/content-validation.js +57 -0
  60. package/dist/map-penpot/content.d.ts +22 -0
  61. package/dist/map-penpot/content.js +173 -0
  62. package/dist/map-penpot/details.d.ts +4 -0
  63. package/dist/map-penpot/details.js +84 -0
  64. package/dist/map-penpot/evidence-error.d.ts +5 -0
  65. package/dist/map-penpot/evidence-error.js +9 -0
  66. package/dist/map-penpot/icons.d.ts +5 -0
  67. package/dist/map-penpot/icons.js +79 -0
  68. package/dist/map-penpot/import-content.d.ts +2 -0
  69. package/dist/map-penpot/import-content.js +150 -0
  70. package/dist/map-penpot/import.d.ts +5 -0
  71. package/dist/map-penpot/import.js +255 -0
  72. package/dist/map-penpot/index.d.ts +11 -0
  73. package/dist/map-penpot/index.js +10 -0
  74. package/dist/map-penpot/path-geometry.d.ts +5 -0
  75. package/dist/map-penpot/path-geometry.js +51 -0
  76. package/dist/map-penpot/presentation.d.ts +20 -0
  77. package/dist/map-penpot/presentation.js +205 -0
  78. package/dist/map-penpot/projection.d.ts +8 -0
  79. package/dist/map-penpot/projection.js +176 -0
  80. package/dist/map-penpot/publish.d.ts +4 -0
  81. package/dist/map-penpot/publish.js +301 -0
  82. package/dist/map-penpot/release-maps.d.ts +21 -0
  83. package/dist/map-penpot/release-maps.js +104 -0
  84. package/dist/map-penpot/snippets.d.ts +4 -0
  85. package/dist/map-penpot/snippets.js +164 -0
  86. package/dist/map-penpot/status-labels.d.ts +7 -0
  87. package/dist/map-penpot/status-labels.js +7 -0
  88. package/dist/map-penpot/types.d.ts +161 -0
  89. package/dist/map-penpot/types.js +1 -0
  90. package/dist/maps/annotation-layout.d.ts +24 -0
  91. package/dist/maps/annotation-layout.js +85 -0
  92. package/dist/maps/context.d.ts +13 -0
  93. package/dist/maps/context.js +207 -0
  94. package/dist/maps/index.d.ts +9 -0
  95. package/dist/maps/index.js +9 -0
  96. package/dist/maps/layout.d.ts +35 -0
  97. package/dist/maps/layout.js +364 -0
  98. package/dist/maps/proposal.d.ts +5 -0
  99. package/dist/maps/proposal.js +67 -0
  100. package/dist/maps/render.d.ts +4 -0
  101. package/dist/maps/render.js +331 -0
  102. package/dist/maps/review.d.ts +16 -0
  103. package/dist/maps/review.js +90 -0
  104. package/dist/maps/store.d.ts +21 -0
  105. package/dist/maps/store.js +222 -0
  106. package/dist/maps/types.d.ts +206 -0
  107. package/dist/maps/types.js +1 -0
  108. package/dist/maps/validation.d.ts +10 -0
  109. package/dist/maps/validation.js +375 -0
  110. package/dist/skill-routing.js +4 -0
  111. package/dist/version.d.ts +1 -1
  112. package/dist/version.js +1 -1
  113. package/package.json +2 -1
  114. package/skills/penpair-design-release/SKILL.md +55 -0
  115. package/skills/penpair-design-release/agents/openai.yaml +6 -0
  116. package/skills/penpair-design-release/references/workflow.md +55 -0
  117. package/skills/penpair-diagrams/SKILL.md +52 -0
  118. package/skills/penpair-diagrams/agents/openai.yaml +6 -0
  119. package/skills/penpair-diagrams/references/workflow.md +84 -0
  120. package/skills/penpair-handoff/SKILL.md +7 -0
  121. package/skills/penpair-handoff/references/handoff-flow.md +7 -1
  122. package/skills/penpair-integration-map/SKILL.md +44 -0
  123. package/skills/penpair-integration-map/agents/openai.yaml +6 -0
  124. package/skills/penpair-integration-map/references/workflow.md +33 -0
  125. package/skills/penpair-journey-map/SKILL.md +42 -0
  126. package/skills/penpair-journey-map/agents/openai.yaml +6 -0
  127. package/skills/penpair-journey-map/references/workflow.md +35 -0
  128. package/skills/penpair-verify/SKILL.md +7 -0
  129. package/skills/penpair-verify/references/browser-evidence.md +11 -1
@@ -0,0 +1,323 @@
1
+ import { exportShapePng } from "../export.js";
2
+ import { observationHash } from "../journal.js";
3
+ import { projection as validateProjection } from "../map-penpot/projection.js";
4
+ import { validateMapContract, hashMapContract } from "../maps/validation.js";
5
+ import { releaseContractGraph, nativeContractId } from "./contract-graph.js";
6
+ import { readProjection } from "../map-penpot/index.js";
7
+ import { findArtifactReview, validateArtifactReview } from "../maps/review.js";
8
+ import { withPageSession, selectPage } from "../page-session.js";
9
+ import { readArtifact } from "./artifact-paths.js";
10
+ import { compareNativeCopy, decodedPixels, inspectionContentHash, deriveNodeMap, } from "./fidelity.js";
11
+ import { assertSourceUnchanged, captureReleaseScreen, observeRelease, releaseClient, } from "./live.js";
12
+ import { assertReleaseContracts } from "./store.js";
13
+ import { hashDesignRelease, validateDesignRelease } from "./validation.js";
14
+ export function releaseReferences(release) {
15
+ const references = [
16
+ ...release.sources.flatMap((source) => [
17
+ source.inspection,
18
+ ...(source.render
19
+ ? [
20
+ source.render,
21
+ ]
22
+ : []),
23
+ ...(source.dependencies
24
+ ? [
25
+ source.dependencies,
26
+ ]
27
+ : []),
28
+ ]),
29
+ ...release.contracts.flatMap((contract) => [
30
+ {
31
+ path: contract.path,
32
+ sha256: contract.sha256,
33
+ },
34
+ ...(contract.review?.artifacts ?? []),
35
+ ]),
36
+ ...release.receipts.flatMap((receipt) => [
37
+ receipt.sourceInspection,
38
+ receipt.targetInspection,
39
+ receipt.originalRender,
40
+ receipt.targetRender,
41
+ ]),
42
+ ...release.maps.flatMap((map) => [
43
+ map.projection,
44
+ map.render,
45
+ ]),
46
+ ...(release.overview
47
+ ? [
48
+ release.overview.render,
49
+ ]
50
+ : []),
51
+ ];
52
+ return [
53
+ ...new Map(references.map((reference) => [
54
+ `${reference.path}:${reference.sha256}`,
55
+ reference,
56
+ ])).values(),
57
+ ];
58
+ }
59
+ export async function readReleaseBundle(root, release) {
60
+ if (!release.finalized) {
61
+ throw new Error("Finalized bundle pointer missing");
62
+ }
63
+ const bundle = JSON.parse((await readArtifact(root, release.finalized.bundle)).toString("utf8"));
64
+ if (bundle.schemaVersion !== 1 ||
65
+ bundle.kind !== "approved-design-release" ||
66
+ bundle.designHash !== hashDesignRelease(release) ||
67
+ hashDesignRelease(bundle.release) !== bundle.designHash ||
68
+ !Array.isArray(bundle.artifacts) ||
69
+ bundle.artifacts.length > 2000) {
70
+ throw new Error("Immutable release bundle does not match its final pointer");
71
+ }
72
+ const keys = new Set();
73
+ for (const item of bundle.artifacts) {
74
+ if (!item ||
75
+ !item.original ||
76
+ !item.snapshot ||
77
+ item.original.sha256 !== item.snapshot.sha256) {
78
+ throw new Error("Immutable artifact snapshot differs from its original approved hash");
79
+ }
80
+ const key = `${item.original.path}:${item.original.sha256}`;
81
+ if (keys.has(key)) {
82
+ throw new Error("Duplicate immutable artifact mapping");
83
+ }
84
+ keys.add(key);
85
+ }
86
+ for (const reference of releaseReferences(release)) {
87
+ if (!keys.has(`${reference.path}:${reference.sha256}`)) {
88
+ throw new Error("Immutable bundle omits required release evidence");
89
+ }
90
+ }
91
+ const review = validateArtifactReview(bundle.review);
92
+ for (const reference of review.artifacts ?? []) {
93
+ if (!keys.has(`${reference.path}:${reference.sha256}`)) {
94
+ throw new Error("Immutable bundle omits owner review evidence");
95
+ }
96
+ }
97
+ if (!findArtifactReview([
98
+ review,
99
+ ], "design-release", release.id, bundle.designHash) ||
100
+ review.hash !== release.finalized?.reviewHash ||
101
+ release.finalized.markerHash !== release.finalized.bundle.sha256) {
102
+ throw new Error("Finalized release owner review or marker binding changed");
103
+ }
104
+ return bundle;
105
+ }
106
+ export async function checkRelease(client, root, raw, options = {}) {
107
+ const release = validateDesignRelease(raw);
108
+ const reasons = [];
109
+ const designHash = hashDesignRelease(release);
110
+ let bundle;
111
+ const artifact = async (reference) => {
112
+ if (!bundle) {
113
+ return readArtifact(root, reference, 64 * 1024 * 1024);
114
+ }
115
+ const saved = bundle.artifacts.find((item) => item.original.path === reference.path && item.original.sha256 === reference.sha256);
116
+ if (!saved) {
117
+ throw new Error("Immutable bundle omits required release evidence");
118
+ }
119
+ return readArtifact(root, saved.snapshot, 64 * 1024 * 1024);
120
+ };
121
+ try {
122
+ if (release.status === "final") {
123
+ bundle = await readReleaseBundle(root, release);
124
+ for (const item of bundle.artifacts) {
125
+ await readArtifact(root, item.snapshot, 64 * 1024 * 1024);
126
+ }
127
+ }
128
+ await assertReleaseContracts(root, release.contracts, true, artifact, release.fileId);
129
+ for (const reference of releaseReferences(release)) {
130
+ await artifact(reference);
131
+ }
132
+ if (!release.fidelity ||
133
+ !release.overview ||
134
+ release.maps.length !== release.contracts.length) {
135
+ reasons.push("Complete verified native screen, map and overview evidence is missing");
136
+ }
137
+ const entries = await Promise.all(release.contracts.map(async (reference) => ({
138
+ reference,
139
+ map: validateMapContract(JSON.parse((await artifact(reference)).toString("utf8"))),
140
+ })));
141
+ const graph = releaseContractGraph(entries, release.fileId);
142
+ for (const map of release.maps) {
143
+ const entry = entries.find((candidate) => candidate.map.kind === map.kind && candidate.map.id === map.id);
144
+ if (!entry || map.contractHash !== hashMapContract(entry.map)) {
145
+ throw new Error("Release map evidence does not match its exact explicit contract");
146
+ }
147
+ const raw = JSON.parse((await artifact(map.projection)).toString("utf8"));
148
+ const observed = validateProjection({
149
+ fileId: release.fileId,
150
+ pageId: release.target.pageId,
151
+ boardId: map.boardId,
152
+ }, nativeContractId(entry.map), raw);
153
+ if (raw.schemaVersion !== 1 ||
154
+ raw.kind !== "native-map-projection" ||
155
+ raw.fileId !== release.fileId ||
156
+ raw.pageId !== release.target.pageId ||
157
+ raw.boardId !== map.boardId ||
158
+ raw.mapId !== nativeContractId(entry.map) ||
159
+ observed.fingerprint !== raw.fingerprint ||
160
+ observed.contractHash !== map.contractHash ||
161
+ observed.conflicts.length) {
162
+ throw new Error("Stored native map projection provenance changed");
163
+ }
164
+ const details = graph.details.get(nativeContractId(entry.map)) ?? {};
165
+ for (const node of entry.map.nodes.filter((node) => node.kind === "detail-link")) {
166
+ const child = details[node.sourceIds[0]];
167
+ const target = release.maps.find((candidate) => candidate.kind === child.kind && candidate.id === child.id);
168
+ if (!target || target.contractHash !== hashMapContract(child)) {
169
+ throw new Error("Release detail target lacks an independently pinned child board");
170
+ }
171
+ const clickables = observed.shapes.filter((shape) => shape.logicalId === node.id &&
172
+ [
173
+ "node",
174
+ "label",
175
+ ].includes(shape.role));
176
+ if (clickables.length !== 2 ||
177
+ clickables.some((shape) => !Array.isArray(shape.interactions) ||
178
+ shape.interactions.length !== 1 ||
179
+ shape.interactions[0]?.trigger !== "click" ||
180
+ shape.interactions[0]?.type !== "navigate-to" ||
181
+ shape.interactions[0]?.destinationId !== target.boardId ||
182
+ shape.interactions[0]?.destinationPageId !== release.target.pageId ||
183
+ shape.interactions[0]?.destinationMapHash !== target.contractHash ||
184
+ shape.interactions[0]?.destinationMapId !== nativeContractId(child))) {
185
+ throw new Error("Release detail interactions must target the exact reviewed release child board");
186
+ }
187
+ }
188
+ }
189
+ const completeNodeMap = Object.assign({}, ...release.receipts.map((receipt) => receipt.nodeMap));
190
+ for (const receipt of release.receipts) {
191
+ const source = release.sources.find((item) => item.id === receipt.sourceId);
192
+ const original = JSON.parse((await artifact(receipt.sourceInspection)).toString("utf8"));
193
+ const copied = JSON.parse((await artifact(receipt.targetInspection)).toString("utf8"));
194
+ const result = compareNativeCopy(original, copied, completeNodeMap);
195
+ if (result.sourceHash !== source.contentHash ||
196
+ result.sourceHash !== receipt.sourceContentHash ||
197
+ result.targetHash !== receipt.targetContentHash) {
198
+ throw new Error("Stored receipt native fingerprints changed");
199
+ }
200
+ if (observationHash(receipt.nodeMap) !== observationHash(deriveNodeMap(original, copied))) {
201
+ throw new Error("Stored receipt descendant mapping changed");
202
+ }
203
+ if (original.fileId !== release.fileId ||
204
+ original.pageId !== source.pageId ||
205
+ original.nodes?.[0]?.id !== source.frameId ||
206
+ copied.fileId !== release.fileId ||
207
+ copied.pageId !== release.target.pageId ||
208
+ copied.nodes?.[0]?.id !== receipt.targetFrameId) {
209
+ throw new Error("Stored receipt frame provenance changed");
210
+ }
211
+ if (!source.render) {
212
+ throw new Error("Pinned source render missing from release evidence");
213
+ }
214
+ const frozen = await decodedPixels(await artifact(source.render));
215
+ const sourcePixels = await decodedPixels(await artifact(receipt.originalRender));
216
+ const targetPixels = await decodedPixels(await artifact(receipt.targetRender));
217
+ if ([
218
+ frozen,
219
+ sourcePixels,
220
+ targetPixels,
221
+ ].some((pixels) => pixels.hash !== receipt.decodedPixelsHash ||
222
+ pixels.width !== receipt.width ||
223
+ pixels.height !== receipt.height)) {
224
+ throw new Error("Stored exact pixel fidelity evidence differs from its receipt");
225
+ }
226
+ }
227
+ if (!client) {
228
+ return {
229
+ valid: reasons.length === 0,
230
+ finalized: release.status === "final",
231
+ live: false,
232
+ designHash,
233
+ reasons,
234
+ implementation: release.implementation.status,
235
+ };
236
+ }
237
+ client = releaseClient(client, release);
238
+ await withPageSession(client, async () => {
239
+ const observed = await observeRelease(client, release);
240
+ if (!observed.pageId ||
241
+ observed.pageId !== release.target.pageId ||
242
+ observed.overviewFrameId !== release.target.overviewFrameId) {
243
+ throw new Error("Live release page/overview ownership changed");
244
+ }
245
+ if (release.status === "final" &&
246
+ (observed.marker?.status !== "released" ||
247
+ observed.marker.bundleHash !== release.finalized?.bundle.sha256 ||
248
+ observed.marker.reviewHash !== release.finalized?.reviewHash)) {
249
+ throw new Error("Live finalized release marker changed");
250
+ }
251
+ if (release.status !== "final") {
252
+ for (const source of release.sources) {
253
+ await assertSourceUnchanged(client, root, release, source);
254
+ }
255
+ }
256
+ for (const receipt of release.receipts) {
257
+ const original = JSON.parse((await artifact(receipt.sourceInspection)).toString("utf8"));
258
+ const target = await captureReleaseScreen(client, release.fileId, receipt.targetPageId, receipt.targetFrameId);
259
+ compareNativeCopy(original, target, completeNodeMap);
260
+ const image = await exportShapePng(client, receipt.targetFrameId, {
261
+ ...options.exportOptions,
262
+ stable: true,
263
+ });
264
+ const pixels = await decodedPixels(image.png);
265
+ if (!image.stable ||
266
+ pixels.hash !== receipt.decodedPixelsHash ||
267
+ pixels.width !== receipt.width ||
268
+ pixels.height !== receipt.height) {
269
+ throw new Error("Finalized release screen render drifted");
270
+ }
271
+ }
272
+ for (const map of release.maps) {
273
+ const stored = JSON.parse((await artifact(map.projection)).toString("utf8"));
274
+ const projection = await readProjection(client, {
275
+ fileId: release.fileId,
276
+ pageId: release.target.pageId,
277
+ boardId: map.boardId,
278
+ }, `${map.kind}:${map.id}`);
279
+ if (projection.conflicts.length ||
280
+ projection.fingerprint !== stored.fingerprint ||
281
+ projection.contractHash !== map.contractHash) {
282
+ throw new Error("Release native map drifted from reviewed contract projection");
283
+ }
284
+ const image = await exportShapePng(client, map.boardId, {
285
+ ...options.exportOptions,
286
+ stable: true,
287
+ });
288
+ if (!image.stable ||
289
+ (await decodedPixels(image.png)).hash !==
290
+ (await decodedPixels(await artifact(map.render))).hash) {
291
+ throw new Error("Release native map render drifted");
292
+ }
293
+ }
294
+ if (release.overview && release.target.overviewFrameId) {
295
+ const overview = await captureReleaseScreen(client, release.fileId, release.target.pageId, release.target.overviewFrameId);
296
+ const image = await exportShapePng(client, release.target.overviewFrameId, {
297
+ ...options.exportOptions,
298
+ stable: true,
299
+ });
300
+ if (inspectionContentHash(overview) !== release.overview.contentHash ||
301
+ !image.stable ||
302
+ (await decodedPixels(image.png)).hash !== release.overview.decodedPixelsHash) {
303
+ throw new Error("Release overview content drifted");
304
+ }
305
+ }
306
+ await selectPage(client, {
307
+ id: release.target.pageId,
308
+ });
309
+ });
310
+ }
311
+ catch (error) {
312
+ reasons.push(options.redact?.(error instanceof Error ? error.message : String(error)) ??
313
+ (error instanceof Error ? error.message : String(error)));
314
+ }
315
+ return {
316
+ valid: reasons.length === 0,
317
+ finalized: release.status === "final",
318
+ live: Boolean(client),
319
+ designHash,
320
+ reasons,
321
+ implementation: release.implementation.status,
322
+ };
323
+ }
@@ -0,0 +1,2 @@
1
+ import type { DesignRelease, ReleaseContext } from "./types.js";
2
+ export declare function releaseContext(root: string, raw: DesignRelease): Promise<ReleaseContext>;
@@ -0,0 +1,119 @@
1
+ import { releaseContractContexts } from "./store.js";
2
+ import { observationHash } from "../journal.js";
3
+ import { readArtifact } from "./artifact-paths.js";
4
+ import { checkRelease, readReleaseBundle } from "./check.js";
5
+ import { hashDesignRelease, validateDesignRelease } from "./validation.js";
6
+ export async function releaseContext(root, raw) {
7
+ const release = validateDesignRelease(raw);
8
+ const checked = await checkRelease(undefined, root, release);
9
+ const bundle = release.status === "final" && checked.valid
10
+ ? await readReleaseBundle(root, release)
11
+ : undefined;
12
+ const artifact = async (reference) => {
13
+ if (!bundle) {
14
+ return readArtifact(root, reference, 64 * 1024 * 1024);
15
+ }
16
+ const frozen = bundle.artifacts.find((item) => item.original.path === reference.path && item.original.sha256 === reference.sha256);
17
+ if (!frozen) {
18
+ throw new Error("Approved context is missing frozen evidence");
19
+ }
20
+ return readArtifact(root, frozen.snapshot, 64 * 1024 * 1024);
21
+ };
22
+ const mapContexts = [];
23
+ const nextActions = [];
24
+ if (checked.valid) {
25
+ mapContexts.push(...(await releaseContractContexts(root, release.contracts, artifact, release.fileId)));
26
+ for (const map of mapContexts) {
27
+ for (const finding of map.findings) {
28
+ if (finding.status === "open") {
29
+ nextActions.push(`${map.mapKind}/${map.mapId}: ${finding.message}`);
30
+ }
31
+ }
32
+ }
33
+ }
34
+ if (release.implementation.status !== "verified") {
35
+ nextActions.push(`Implementation verification is ${release.implementation.status}; implement the approved requirements and collect independent implementation evidence.`);
36
+ }
37
+ const context = {
38
+ schemaVersion: 1,
39
+ kind: "release-context",
40
+ id: release.id,
41
+ productVersion: release.productVersion,
42
+ designHash: hashDesignRelease(release),
43
+ finalized: release.status === "final",
44
+ ready: checked.valid &&
45
+ release.status === "final" &&
46
+ mapContexts.every((map) => map.ready && !map.truncated),
47
+ reasons: [
48
+ ...checked.reasons,
49
+ ...(release.status !== "final"
50
+ ? [
51
+ "Design release is not finalized",
52
+ ]
53
+ : []),
54
+ ],
55
+ implementation: release.implementation,
56
+ mapContexts,
57
+ mapTargets: release.maps.map((map) => ({
58
+ kind: map.kind,
59
+ id: map.id,
60
+ fileId: release.fileId,
61
+ pageId: release.target.pageId,
62
+ frameId: map.boardId,
63
+ contractHash: map.contractHash,
64
+ })),
65
+ nextActions,
66
+ screens: release.sources.map((source) => ({
67
+ id: source.id,
68
+ label: source.label,
69
+ sourceFrameId: source.frameId,
70
+ targetFrameId: release.receipts.find((receipt) => receipt.sourceId === source.id)?.targetFrameId ?? null,
71
+ })),
72
+ contracts: release.contracts,
73
+ proposedBaselines: {
74
+ page: release.target.name,
75
+ root: `design/releases/${release.productVersion}/baselines`,
76
+ format: "webp",
77
+ frames: release.receipts.map((receipt) => ({
78
+ id: `screen-${observationHash(receipt.sourceId).slice(0, 24)}`,
79
+ frameId: receipt.targetFrameId,
80
+ })),
81
+ },
82
+ truncated: mapContexts.some((map) => map.truncated),
83
+ };
84
+ const collections = [
85
+ "screens",
86
+ "contracts",
87
+ "mapContexts",
88
+ "mapTargets",
89
+ "nextActions",
90
+ ];
91
+ while (Buffer.byteLength(JSON.stringify(context)) > 127 * 1024) {
92
+ context.truncated = true;
93
+ context.ready = false;
94
+ const largest = collections.reduce((before, key) => JSON.stringify(context[key]).length > JSON.stringify(context[before]).length ? key : before);
95
+ if (context[largest].length) {
96
+ context[largest].splice(Math.floor(context[largest].length / 2));
97
+ }
98
+ else if (context.proposedBaselines.frames.length) {
99
+ context.proposedBaselines.frames.splice(Math.floor(context.proposedBaselines.frames.length / 2));
100
+ }
101
+ else if (context.implementation.evidence.length) {
102
+ context.implementation = {
103
+ status: context.implementation.status,
104
+ evidence: [],
105
+ };
106
+ }
107
+ else {
108
+ context.reasons = [
109
+ "Release context exceeded byte budget; inspect canonical release artifacts",
110
+ ];
111
+ break;
112
+ }
113
+ }
114
+ if (context.truncated) {
115
+ context.ready = false;
116
+ context.reasons.push("Context omits evidence; canonical release artifacts are required");
117
+ }
118
+ return context;
119
+ }
@@ -0,0 +1,13 @@
1
+ import type { MapContract } from "../maps/types.js";
2
+ import type { ReleaseContract } from "./types.js";
3
+ export interface ReleaseContractEntry {
4
+ reference: ReleaseContract;
5
+ map: MapContract;
6
+ }
7
+ export interface ReleaseContractGraph {
8
+ ordered: ReleaseContractEntry[];
9
+ details: Map<string, Record<string, MapContract>>;
10
+ }
11
+ export declare function nativeContractId(map: MapContract): string;
12
+ /** Detail traversal stays inside the explicit release family; no implicit recursive file loading. */
13
+ export declare function releaseContractGraph(entries: ReleaseContractEntry[], fileId?: string): ReleaseContractGraph;
@@ -0,0 +1,75 @@
1
+ import { validateMapDetail } from "../maps/validation.js";
2
+ export function nativeContractId(map) {
3
+ return `${map.kind}:${map.id}`;
4
+ }
5
+ /** Detail traversal stays inside the explicit release family; no implicit recursive file loading. */
6
+ export function releaseContractGraph(entries, fileId) {
7
+ if (entries.length > 100) {
8
+ throw new Error("Release contract graph exceeds 100 explicit members");
9
+ }
10
+ const byId = new Map(entries.map((entry) => [
11
+ nativeContractId(entry.map),
12
+ entry,
13
+ ]));
14
+ if (byId.size !== entries.length) {
15
+ throw new Error("Release contract identities must be unique");
16
+ }
17
+ const children = new Map();
18
+ const details = new Map();
19
+ for (const entry of entries) {
20
+ const key = nativeContractId(entry.map);
21
+ const links = entry.map.nodes.filter((node) => node.kind === "detail-link");
22
+ if (links.length > 64) {
23
+ throw new Error("Release map exceeds 64 direct detail links");
24
+ }
25
+ const resolved = {};
26
+ const linked = new Set();
27
+ for (const node of links) {
28
+ const source = entry.map.sources.find((item) => item.id === node.sourceIds?.[0]);
29
+ if (!source ||
30
+ source.kind !== "contract" ||
31
+ !source.path ||
32
+ !source.fileId ||
33
+ !source.pageId ||
34
+ !source.frameId) {
35
+ throw new Error("Release detail link is missing its explicit pinned contract target");
36
+ }
37
+ if (fileId && source.fileId !== fileId) {
38
+ throw new Error("Release detail link belongs to another pinned file");
39
+ }
40
+ const child = entries.find((candidate) => candidate.reference.path === source.path && candidate.reference.sha256 === source.sha256);
41
+ if (!child) {
42
+ throw new Error("Every referenced detail contract must be an explicit exact-hash release member");
43
+ }
44
+ resolved[source.id] = validateMapDetail(entry.map, node, child.map);
45
+ linked.add(nativeContractId(child.map));
46
+ }
47
+ details.set(key, resolved);
48
+ children.set(key, linked);
49
+ }
50
+ const visited = new Set();
51
+ const active = new Set();
52
+ const ordered = [];
53
+ const visit = (id) => {
54
+ if (active.has(id)) {
55
+ throw new Error("Cyclic detail contracts cannot be assembled or finalized");
56
+ }
57
+ if (visited.has(id)) {
58
+ return;
59
+ }
60
+ active.add(id);
61
+ for (const child of children.get(id) ?? []) {
62
+ visit(child);
63
+ }
64
+ active.delete(id);
65
+ visited.add(id);
66
+ ordered.push(byId.get(id));
67
+ };
68
+ for (const entry of entries) {
69
+ visit(nativeContractId(entry.map));
70
+ }
71
+ return {
72
+ ordered,
73
+ details,
74
+ };
75
+ }
@@ -0,0 +1,23 @@
1
+ import type { InspectionCapture } from "../inspection.js";
2
+ interface NativeTree {
3
+ root: Record<string, unknown>;
4
+ ordered: Array<{
5
+ path: string;
6
+ node: Record<string, unknown>;
7
+ }>;
8
+ paths: Map<string, string>;
9
+ }
10
+ export declare function nativeTree(inspection: InspectionCapture): NativeTree;
11
+ export declare function semanticInspection(inspection: InspectionCapture, remap?: Record<string, string>): unknown;
12
+ export declare function inspectionContentHash(inspection: InspectionCapture): string;
13
+ export declare function deriveNodeMap(source: InspectionCapture, target: InspectionCapture): Record<string, string>;
14
+ export declare function compareNativeCopy(source: InspectionCapture, target: InspectionCapture, fullNodeMap: Record<string, string>): {
15
+ sourceHash: string;
16
+ targetHash: string;
17
+ };
18
+ export declare function decodedPixels(png: Buffer): Promise<{
19
+ hash: string;
20
+ width: number;
21
+ height: number;
22
+ }>;
23
+ export {};