@riddledc/riddle-proof 0.8.77 → 0.8.79
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/README.md +65 -3
- package/dist/change-proof.cjs +744 -85
- package/dist/change-proof.d.cts +100 -8
- package/dist/change-proof.d.ts +100 -8
- package/dist/change-proof.js +15 -1
- package/dist/{chunk-B2DP2LET.js → chunk-5IFZSUPF.js} +52 -13
- package/dist/chunk-6VFS2JFR.js +886 -0
- package/dist/{chunk-IY4W6STC.js → chunk-7N6X54WG.js} +116 -17
- package/dist/{chunk-GG2D3MFZ.js → chunk-FCSJZBC5.js} +26 -1
- package/dist/chunk-MEVVL4TI.js +258 -0
- package/dist/{chunk-H25IDX76.js → chunk-NEXWITV4.js} +221 -35
- package/dist/{chunk-UE4I7RTI.js → chunk-RQPCKRKT.js} +1 -1
- package/dist/cli/index.js +7 -6
- package/dist/cli.cjs +2026 -1060
- package/dist/cli.js +7 -6
- package/dist/index.cjs +932 -122
- package/dist/index.d.cts +4 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.js +42 -14
- package/dist/pr-comment.cjs +416 -17
- package/dist/pr-comment.d.cts +6 -1
- package/dist/pr-comment.d.ts +6 -1
- package/dist/pr-comment.js +6 -1
- package/dist/profile/index.cjs +26 -1
- package/dist/profile/index.js +1 -1
- package/dist/profile-suggestions.js +2 -2
- package/dist/profile.cjs +26 -1
- package/dist/profile.d.cts +7 -0
- package/dist/profile.d.ts +7 -0
- package/dist/profile.js +1 -1
- package/dist/receipts.cjs +286 -0
- package/dist/receipts.d.cts +115 -0
- package/dist/receipts.d.ts +115 -0
- package/dist/receipts.js +15 -0
- package/dist/riddle-client.cjs +98 -13
- package/dist/riddle-client.d.cts +18 -5
- package/dist/riddle-client.d.ts +18 -5
- package/dist/riddle-client.js +4 -1
- package/dist/runtime/index.cjs +98 -13
- package/dist/runtime/index.d.cts +5 -1
- package/dist/runtime/index.d.ts +5 -1
- package/dist/runtime/index.js +4 -1
- package/dist/runtime/riddle-client.cjs +98 -13
- package/dist/runtime/riddle-client.d.cts +5 -1
- package/dist/runtime/riddle-client.d.ts +5 -1
- package/dist/runtime/riddle-client.js +4 -1
- package/package.json +7 -2
- package/dist/chunk-BILL3UC2.js +0 -488
package/dist/change-proof.cjs
CHANGED
|
@@ -21,17 +21,288 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var change_proof_exports = {};
|
|
22
22
|
__export(change_proof_exports, {
|
|
23
23
|
RIDDLE_PROOF_CHANGE_CONTRACT_VERSION: () => RIDDLE_PROOF_CHANGE_CONTRACT_VERSION,
|
|
24
|
+
RIDDLE_PROOF_CHANGE_RECEIPT_V1_VERSION: () => RIDDLE_PROOF_CHANGE_RECEIPT_V1_VERSION,
|
|
24
25
|
RIDDLE_PROOF_CHANGE_RECEIPT_VERSION: () => RIDDLE_PROOF_CHANGE_RECEIPT_VERSION,
|
|
25
26
|
RIDDLE_PROOF_CHANGE_RESULT_VERSION: () => RIDDLE_PROOF_CHANGE_RESULT_VERSION,
|
|
27
|
+
RIDDLE_PROOF_HANDOFF_RECEIPT_VERSION: () => RIDDLE_PROOF_HANDOFF_RECEIPT_VERSION,
|
|
26
28
|
assessRiddleProofChange: () => assessRiddleProofChange,
|
|
27
29
|
createRiddleProofChangeReceipt: () => createRiddleProofChangeReceipt,
|
|
30
|
+
createRiddleProofHandoffReceipt: () => createRiddleProofHandoffReceipt,
|
|
31
|
+
migrateRiddleProofChangeReceipt: () => migrateRiddleProofChangeReceipt,
|
|
32
|
+
parseRiddleProofChangeReceipt: () => parseRiddleProofChangeReceipt,
|
|
33
|
+
parseRiddleProofHandoffReceipt: () => parseRiddleProofHandoffReceipt,
|
|
28
34
|
riddleProofChangeReceiptHtml: () => riddleProofChangeReceiptHtml,
|
|
29
35
|
riddleProofChangeReceiptMarkdown: () => riddleProofChangeReceiptMarkdown
|
|
30
36
|
});
|
|
31
37
|
module.exports = __toCommonJS(change_proof_exports);
|
|
38
|
+
|
|
39
|
+
// src/profile.ts
|
|
40
|
+
var RIDDLE_PROOF_PROFILE_STATUSES = [
|
|
41
|
+
"passed",
|
|
42
|
+
"product_regression",
|
|
43
|
+
"proof_insufficient",
|
|
44
|
+
"environment_blocked",
|
|
45
|
+
"configuration_error",
|
|
46
|
+
"needs_human_review"
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
// src/receipts.ts
|
|
50
|
+
var RIDDLE_PREVIEW_RECEIPT_VERSION = "riddle.preview-receipt.v1";
|
|
51
|
+
var RIDDLE_PROOF_OBSERVATION_RECEIPT_VERSION = "riddle-proof.observation-receipt.v1";
|
|
52
|
+
function isRecord(value) {
|
|
53
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
54
|
+
}
|
|
55
|
+
function requiredString(record, key, context) {
|
|
56
|
+
const value = record[key];
|
|
57
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
58
|
+
throw new Error(`${context}.${key} must be a non-empty string.`);
|
|
59
|
+
}
|
|
60
|
+
return value.trim();
|
|
61
|
+
}
|
|
62
|
+
function normalizedSourceIdentity(value) {
|
|
63
|
+
if (!isRecord(value)) return {};
|
|
64
|
+
return {
|
|
65
|
+
git_revision: typeof value.git_revision === "string" && value.git_revision.trim() ? value.git_revision.trim() : void 0,
|
|
66
|
+
repository: typeof value.repository === "string" && value.repository.trim() ? value.repository.trim() : void 0,
|
|
67
|
+
dirty: typeof value.dirty === "boolean" ? value.dirty : void 0,
|
|
68
|
+
label: typeof value.label === "string" && value.label.trim() ? value.label.trim() : void 0
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function parseRiddlePreviewReceipt(value) {
|
|
72
|
+
if (!isRecord(value)) throw new Error("Preview receipt must be an object.");
|
|
73
|
+
if (value.version !== RIDDLE_PREVIEW_RECEIPT_VERSION) {
|
|
74
|
+
throw new Error(`Unsupported Preview receipt version ${String(value.version || "missing")}.`);
|
|
75
|
+
}
|
|
76
|
+
if (!isRecord(value.source)) throw new Error("Preview receipt source must be an object.");
|
|
77
|
+
const expiresAt = requiredString(value, "expires_at", "preview receipt");
|
|
78
|
+
const publishedAt = requiredString(value, "published_at", "preview receipt");
|
|
79
|
+
const contentDigest = requiredString(value, "content_digest", "preview receipt");
|
|
80
|
+
if (!contentDigest.startsWith("sha256:") || contentDigest.length <= "sha256:".length) {
|
|
81
|
+
throw new Error("preview receipt.content_digest must be a sha256 digest.");
|
|
82
|
+
}
|
|
83
|
+
if (!Number.isFinite(Date.parse(expiresAt)) || !Number.isFinite(Date.parse(publishedAt))) {
|
|
84
|
+
throw new Error("Preview receipt timestamps must be valid ISO date strings.");
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
version: RIDDLE_PREVIEW_RECEIPT_VERSION,
|
|
88
|
+
preview_id: requiredString(value, "preview_id", "preview receipt"),
|
|
89
|
+
url: requiredString(value, "url", "preview receipt"),
|
|
90
|
+
expires_at: expiresAt,
|
|
91
|
+
content_digest: contentDigest,
|
|
92
|
+
source: normalizedSourceIdentity(value.source),
|
|
93
|
+
published_at: publishedAt
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function comparableArtifactName(value) {
|
|
97
|
+
return (value || "").split(/[/?#]/).pop()?.toLowerCase().replace(/\.(png|jpe?g|gif|webp|avif|svg)$/u, "").replace(/[^a-z0-9_-]+/gu, "-").replace(/^-+|-+$/g, "") || "";
|
|
98
|
+
}
|
|
99
|
+
function artifactMatchesLabel(artifact, label) {
|
|
100
|
+
const expected = comparableArtifactName(label);
|
|
101
|
+
if (!expected) return false;
|
|
102
|
+
return [artifact.name, artifact.url, artifact.path].map(comparableArtifactName).some((name) => name === expected || name.endsWith(`-${expected}`));
|
|
103
|
+
}
|
|
104
|
+
function artifactLooksLikeScreenshot(ref) {
|
|
105
|
+
const text = [ref.name, ref.url, ref.path, ref.kind, ref.content_type].filter(Boolean).join(" ").toLowerCase();
|
|
106
|
+
return /screenshot|\bimage\b/.test(text) || /\.(png|jpe?g|gif|webp|avif|svg)(\?|#|$)/.test(text);
|
|
107
|
+
}
|
|
108
|
+
function finalScreenshotLabels(result) {
|
|
109
|
+
const explicit = result.artifacts.canonical_screenshots || [];
|
|
110
|
+
if (explicit.length) return explicit;
|
|
111
|
+
const fromEvidence = (result.evidence?.viewports || []).map((viewport) => viewport.screenshot_label).filter((label) => typeof label === "string" && Boolean(label.trim()));
|
|
112
|
+
if (fromEvidence.length) return fromEvidence;
|
|
113
|
+
return (result.artifacts.screenshots || []).slice(0, 1);
|
|
114
|
+
}
|
|
115
|
+
function setupScreenshotLabels(result) {
|
|
116
|
+
const explicit = result.artifacts.setup_screenshots || [];
|
|
117
|
+
if (explicit.length) return explicit;
|
|
118
|
+
return (result.evidence?.viewports || []).flatMap(
|
|
119
|
+
(viewport) => (viewport.setup_action_results || []).filter((action) => action.action === "screenshot" && action.ok !== false && typeof action.screenshot_label === "string").map((action) => String(action.screenshot_label))
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
function observationArtifactFromRef(ref, canonicalLabels, setupLabels) {
|
|
123
|
+
const base = {
|
|
124
|
+
name: ref.name,
|
|
125
|
+
role: ref.role || "artifact",
|
|
126
|
+
url: ref.url,
|
|
127
|
+
path: ref.path,
|
|
128
|
+
kind: ref.kind,
|
|
129
|
+
content_type: ref.content_type,
|
|
130
|
+
source: ref.source
|
|
131
|
+
};
|
|
132
|
+
if (!artifactLooksLikeScreenshot(ref)) return base;
|
|
133
|
+
if (canonicalLabels.some((label) => artifactMatchesLabel(base, label))) {
|
|
134
|
+
return { ...base, role: "canonical_screenshot" };
|
|
135
|
+
}
|
|
136
|
+
if (setupLabels.some((label) => artifactMatchesLabel(base, label))) {
|
|
137
|
+
return { ...base, role: "setup_screenshot" };
|
|
138
|
+
}
|
|
139
|
+
return { ...base, role: "diagnostic" };
|
|
140
|
+
}
|
|
141
|
+
function profileObservationArtifacts(result) {
|
|
142
|
+
const canonicalLabels = finalScreenshotLabels(result);
|
|
143
|
+
const setupLabels = setupScreenshotLabels(result);
|
|
144
|
+
const artifacts = [];
|
|
145
|
+
const seen = /* @__PURE__ */ new Set();
|
|
146
|
+
const add = (artifact) => {
|
|
147
|
+
const key = artifact.url || artifact.path || `${artifact.role}:${artifact.name}`;
|
|
148
|
+
if (!artifact.name || seen.has(key)) return;
|
|
149
|
+
seen.add(key);
|
|
150
|
+
artifacts.push(artifact);
|
|
151
|
+
};
|
|
152
|
+
for (const ref of result.artifacts.riddle_artifacts || []) {
|
|
153
|
+
add(observationArtifactFromRef(ref, canonicalLabels, setupLabels));
|
|
154
|
+
}
|
|
155
|
+
for (const label of canonicalLabels) {
|
|
156
|
+
if (!artifacts.some((artifact) => artifactMatchesLabel(artifact, label))) {
|
|
157
|
+
add({ name: label, path: label, role: "canonical_screenshot", kind: "image" });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
for (const label of setupLabels) {
|
|
161
|
+
if (!artifacts.some((artifact) => artifactMatchesLabel(artifact, label))) {
|
|
162
|
+
add({ name: label, path: label, role: "setup_screenshot", kind: "image" });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
for (const label of result.artifacts.screenshots || []) {
|
|
166
|
+
if (!artifacts.some((artifact) => artifactMatchesLabel(artifact, label))) {
|
|
167
|
+
add({ name: label, path: label, role: "diagnostic", kind: "image" });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return artifacts;
|
|
171
|
+
}
|
|
172
|
+
function mergeObservationArtifacts(profileArtifacts, suppliedArtifacts) {
|
|
173
|
+
const artifacts = [...profileArtifacts];
|
|
174
|
+
for (const supplied of suppliedArtifacts) {
|
|
175
|
+
const suppliedNames = [supplied.name, supplied.url, supplied.path].map(comparableArtifactName).filter(Boolean);
|
|
176
|
+
const existingIndex = artifacts.findIndex((artifact) => {
|
|
177
|
+
if (supplied.url && artifact.url === supplied.url) return true;
|
|
178
|
+
if (supplied.path && artifact.path === supplied.path) return true;
|
|
179
|
+
return [artifact.name, artifact.url, artifact.path].map(comparableArtifactName).some((name) => name && suppliedNames.includes(name));
|
|
180
|
+
});
|
|
181
|
+
if (existingIndex < 0) {
|
|
182
|
+
artifacts.push(supplied);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const existing = artifacts[existingIndex];
|
|
186
|
+
const role = supplied.role === "canonical_screenshot" || supplied.role === "setup_screenshot" ? supplied.role : existing.role === "canonical_screenshot" || existing.role === "setup_screenshot" ? existing.role : supplied.role === "artifact" ? existing.role : supplied.role;
|
|
187
|
+
artifacts[existingIndex] = {
|
|
188
|
+
...existing,
|
|
189
|
+
...supplied,
|
|
190
|
+
role
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
return artifacts;
|
|
194
|
+
}
|
|
195
|
+
function defaultObservationId(role, capturedAt) {
|
|
196
|
+
return `obs_${role}_${capturedAt.replace(/[^0-9]/g, "").slice(0, 17) || "unknown"}`;
|
|
197
|
+
}
|
|
198
|
+
function profileCheckCounts(result) {
|
|
199
|
+
const counts = { total: result.checks.length, passed: 0, failed: 0, skipped: 0, needs_human_review: 0 };
|
|
200
|
+
for (const check of result.checks) {
|
|
201
|
+
if (check.status === "passed") counts.passed += 1;
|
|
202
|
+
if (check.status === "failed") counts.failed += 1;
|
|
203
|
+
if (check.status === "skipped") counts.skipped += 1;
|
|
204
|
+
if (check.status === "needs_human_review") counts.needs_human_review += 1;
|
|
205
|
+
}
|
|
206
|
+
return counts;
|
|
207
|
+
}
|
|
208
|
+
function createRiddleProofObservationReceipt(input) {
|
|
209
|
+
const capturedAt = input.captured_at || input.profile_result?.captured_at || (/* @__PURE__ */ new Date()).toISOString();
|
|
210
|
+
const profileArtifacts = input.profile_result ? profileObservationArtifacts(input.profile_result) : [];
|
|
211
|
+
const requestedCanonical = input.canonical_screenshot ? { ...input.canonical_screenshot, role: "canonical_screenshot" } : void 0;
|
|
212
|
+
const artifacts = mergeObservationArtifacts(
|
|
213
|
+
profileArtifacts,
|
|
214
|
+
[...input.artifacts || [], ...requestedCanonical ? [requestedCanonical] : []]
|
|
215
|
+
);
|
|
216
|
+
const canonicalScreenshot = requestedCanonical ? artifacts.find((artifact) => artifact.role === "canonical_screenshot" && artifactMatchesLabel(artifact, requestedCanonical.name)) || requestedCanonical : artifacts.find((artifact) => artifact.role === "canonical_screenshot");
|
|
217
|
+
const target = input.target.kind === "preview" && input.target.preview ? { ...input.target, preview: parseRiddlePreviewReceipt(input.target.preview) } : input.target;
|
|
218
|
+
return {
|
|
219
|
+
version: RIDDLE_PROOF_OBSERVATION_RECEIPT_VERSION,
|
|
220
|
+
observation_id: input.observation_id || defaultObservationId(input.comparison_role, capturedAt),
|
|
221
|
+
comparison_role: input.comparison_role,
|
|
222
|
+
executor: input.executor,
|
|
223
|
+
target,
|
|
224
|
+
source: normalizedSourceIdentity(input.source || input.target.preview?.source),
|
|
225
|
+
captured_at: capturedAt,
|
|
226
|
+
artifacts,
|
|
227
|
+
canonical_screenshot: canonicalScreenshot,
|
|
228
|
+
profile_summary: input.profile_result ? {
|
|
229
|
+
profile_name: input.profile_result.profile_name,
|
|
230
|
+
status: input.profile_result.status,
|
|
231
|
+
summary: input.profile_result.summary,
|
|
232
|
+
route: input.profile_result.route,
|
|
233
|
+
checks: profileCheckCounts(input.profile_result)
|
|
234
|
+
} : void 0,
|
|
235
|
+
proof: input.profile_result ? {
|
|
236
|
+
profile_name: input.profile_result.profile_name,
|
|
237
|
+
result: input.profile_result
|
|
238
|
+
} : void 0,
|
|
239
|
+
publication: input.publication,
|
|
240
|
+
execution: input.execution,
|
|
241
|
+
metadata: input.metadata
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
function parseRiddleProofObservationReceipt(value) {
|
|
245
|
+
if (!isRecord(value)) throw new Error("Observation receipt must be an object.");
|
|
246
|
+
if (value.version !== RIDDLE_PROOF_OBSERVATION_RECEIPT_VERSION) {
|
|
247
|
+
throw new Error(`Unsupported Observation receipt version ${String(value.version || "missing")}.`);
|
|
248
|
+
}
|
|
249
|
+
if (!isRecord(value.executor)) throw new Error("Observation receipt executor must be an object.");
|
|
250
|
+
if (!isRecord(value.target)) throw new Error("Observation receipt target must be an object.");
|
|
251
|
+
if (!isRecord(value.source)) throw new Error("Observation receipt source must be an object.");
|
|
252
|
+
requiredString(value, "observation_id", "observation receipt");
|
|
253
|
+
requiredString(value, "captured_at", "observation receipt");
|
|
254
|
+
const executorKind = requiredString(value.executor, "kind", "observation receipt executor");
|
|
255
|
+
if (!"local_playwright,riddle_hosted,browser_api,other".split(",").includes(executorKind)) {
|
|
256
|
+
throw new Error(`Unsupported observation executor kind ${executorKind}.`);
|
|
257
|
+
}
|
|
258
|
+
if (value.executor.runner !== void 0) {
|
|
259
|
+
requiredString(value.executor, "runner", "observation receipt executor");
|
|
260
|
+
}
|
|
261
|
+
const role = requiredString(value, "comparison_role", "observation receipt");
|
|
262
|
+
if (!["before", "after", "standalone"].includes(role)) {
|
|
263
|
+
throw new Error(`Unsupported observation comparison role ${role}.`);
|
|
264
|
+
}
|
|
265
|
+
const targetKind = requiredString(value.target, "kind", "observation receipt target");
|
|
266
|
+
if (targetKind !== "url" && targetKind !== "preview") {
|
|
267
|
+
throw new Error(`Unsupported observation target kind ${targetKind}.`);
|
|
268
|
+
}
|
|
269
|
+
requiredString(value.target, "url", "observation receipt target");
|
|
270
|
+
if (targetKind === "preview") {
|
|
271
|
+
if (value.target.preview === void 0) {
|
|
272
|
+
throw new Error("Preview Observation target must include its Preview receipt.");
|
|
273
|
+
}
|
|
274
|
+
parseRiddlePreviewReceipt(value.target.preview);
|
|
275
|
+
}
|
|
276
|
+
if (!Array.isArray(value.artifacts)) throw new Error("Observation receipt artifacts must be an array.");
|
|
277
|
+
const artifactRoles = /* @__PURE__ */ new Set(["canonical_screenshot", "setup_screenshot", "data", "diagnostic", "artifact"]);
|
|
278
|
+
const artifacts = value.artifacts.map((artifact, index) => {
|
|
279
|
+
if (!isRecord(artifact)) throw new Error(`Observation receipt artifact ${index} must be an object.`);
|
|
280
|
+
requiredString(artifact, "name", `observation receipt artifact ${index}`);
|
|
281
|
+
const artifactRole = requiredString(artifact, "role", `observation receipt artifact ${index}`);
|
|
282
|
+
if (!artifactRoles.has(artifactRole)) {
|
|
283
|
+
throw new Error(`Unsupported observation artifact role ${artifactRole}.`);
|
|
284
|
+
}
|
|
285
|
+
return artifact;
|
|
286
|
+
});
|
|
287
|
+
if (value.canonical_screenshot !== void 0) {
|
|
288
|
+
if (!isRecord(value.canonical_screenshot) || value.canonical_screenshot.role !== "canonical_screenshot") {
|
|
289
|
+
throw new Error("Observation canonical_screenshot must have the canonical_screenshot role.");
|
|
290
|
+
}
|
|
291
|
+
const canonical = value.canonical_screenshot;
|
|
292
|
+
const included = artifacts.some((artifact) => artifact.role === "canonical_screenshot" && artifact.name === canonical.name && artifact.url === canonical.url && artifact.path === canonical.path);
|
|
293
|
+
if (!included) {
|
|
294
|
+
throw new Error("Observation canonical_screenshot must reference an artifact in the receipt.");
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return value;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// src/change-proof.ts
|
|
32
301
|
var RIDDLE_PROOF_CHANGE_CONTRACT_VERSION = "riddle-proof.change-contract.v1";
|
|
33
302
|
var RIDDLE_PROOF_CHANGE_RESULT_VERSION = "riddle-proof.change-result.v1";
|
|
34
|
-
var
|
|
303
|
+
var RIDDLE_PROOF_CHANGE_RECEIPT_V1_VERSION = "riddle-proof.change-receipt.v1";
|
|
304
|
+
var RIDDLE_PROOF_CHANGE_RECEIPT_VERSION = "riddle-proof.change-receipt.v2";
|
|
305
|
+
var RIDDLE_PROOF_HANDOFF_RECEIPT_VERSION = "riddle-proof.handoff-receipt.v1";
|
|
35
306
|
var DEFAULT_BEFORE_STATUSES = ["passed", "product_regression"];
|
|
36
307
|
var DEFAULT_AFTER_STATUSES = ["passed"];
|
|
37
308
|
var DEFAULT_PROFILE_STATUS_TRANSITION_BEFORE = ["product_regression"];
|
|
@@ -153,13 +424,163 @@ function assessDelta(delta, before, after) {
|
|
|
153
424
|
}
|
|
154
425
|
return checkStatusTransitionResult(delta, before, after);
|
|
155
426
|
}
|
|
156
|
-
function
|
|
427
|
+
function profileResultFromObservation(observation) {
|
|
428
|
+
return observation?.proof?.result;
|
|
429
|
+
}
|
|
430
|
+
function previewReceiptCoversTarget(previewUrl, targetUrl) {
|
|
431
|
+
try {
|
|
432
|
+
const preview = new URL(previewUrl);
|
|
433
|
+
const target = new URL(targetUrl);
|
|
434
|
+
const previewPath = preview.pathname.replace(/\/+$/u, "");
|
|
435
|
+
return preview.origin === target.origin && (target.pathname === previewPath || target.pathname.startsWith(`${previewPath}/`));
|
|
436
|
+
} catch {
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
function sourceBindingResult(side, requirement, observation, expectedRevision, evaluatedAt) {
|
|
441
|
+
const expected = expectedRevision || requirement?.expected_git_revision;
|
|
442
|
+
const required = Boolean(
|
|
443
|
+
requirement?.preview_receipt_required || requirement?.require_clean_source || requirement?.require_content_digest || requirement?.expected_git_revision || expected
|
|
444
|
+
);
|
|
445
|
+
if (!required) return { side, required: false, ok: true, status: "not_required" };
|
|
446
|
+
if (!observation) {
|
|
447
|
+
return {
|
|
448
|
+
side,
|
|
449
|
+
required: true,
|
|
450
|
+
ok: false,
|
|
451
|
+
status: "missing",
|
|
452
|
+
expected_git_revision: expected,
|
|
453
|
+
message: `${side} source binding requires an Observation receipt.`
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
const preview = observation.target.preview;
|
|
457
|
+
if (requirement?.preview_receipt_required && !preview) {
|
|
458
|
+
return {
|
|
459
|
+
side,
|
|
460
|
+
required: true,
|
|
461
|
+
ok: false,
|
|
462
|
+
status: "missing",
|
|
463
|
+
expected_git_revision: expected,
|
|
464
|
+
message: `${side} source binding requires a Preview receipt.`
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
if (preview && !previewReceiptCoversTarget(preview.url, observation.target.url)) {
|
|
468
|
+
return {
|
|
469
|
+
side,
|
|
470
|
+
required: true,
|
|
471
|
+
ok: false,
|
|
472
|
+
status: "mismatched",
|
|
473
|
+
expected_git_revision: expected,
|
|
474
|
+
observed_git_revision: preview.source.git_revision || observation.source.git_revision,
|
|
475
|
+
content_digest: preview.content_digest,
|
|
476
|
+
preview_id: preview.preview_id,
|
|
477
|
+
preview_url: preview.url,
|
|
478
|
+
target_url: observation.target.url,
|
|
479
|
+
message: `${side} observation target is not contained by Preview ${preview.preview_id}.`
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
const observed = preview?.source.git_revision || observation.source.git_revision;
|
|
483
|
+
const digestRequired = requirement?.require_content_digest === true || requirement?.preview_receipt_required === true;
|
|
484
|
+
if (digestRequired && !preview?.content_digest) {
|
|
485
|
+
return {
|
|
486
|
+
side,
|
|
487
|
+
required: true,
|
|
488
|
+
ok: false,
|
|
489
|
+
status: "missing",
|
|
490
|
+
expected_git_revision: expected,
|
|
491
|
+
observed_git_revision: observed,
|
|
492
|
+
preview_id: preview?.preview_id,
|
|
493
|
+
preview_url: preview?.url,
|
|
494
|
+
target_url: observation.target.url,
|
|
495
|
+
message: `${side} Preview receipt is missing a content digest.`
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
if (expected && !observed) {
|
|
499
|
+
return {
|
|
500
|
+
side,
|
|
501
|
+
required: true,
|
|
502
|
+
ok: false,
|
|
503
|
+
status: "missing",
|
|
504
|
+
expected_git_revision: expected,
|
|
505
|
+
content_digest: preview?.content_digest,
|
|
506
|
+
preview_id: preview?.preview_id,
|
|
507
|
+
preview_url: preview?.url,
|
|
508
|
+
target_url: observation.target.url,
|
|
509
|
+
message: `${side} evidence is missing its source Git revision.`
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
if (expected && observed !== expected) {
|
|
513
|
+
return {
|
|
514
|
+
side,
|
|
515
|
+
required: true,
|
|
516
|
+
ok: false,
|
|
517
|
+
status: "mismatched",
|
|
518
|
+
expected_git_revision: expected,
|
|
519
|
+
observed_git_revision: observed,
|
|
520
|
+
content_digest: preview?.content_digest,
|
|
521
|
+
preview_id: preview?.preview_id,
|
|
522
|
+
preview_url: preview?.url,
|
|
523
|
+
target_url: observation.target.url,
|
|
524
|
+
message: `${side} evidence came from Git revision ${observed}, not expected revision ${expected}.`
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
const cleanRequired = requirement?.require_clean_source === true || requirement?.preview_receipt_required === true;
|
|
528
|
+
const dirty = preview?.source.dirty ?? observation.source.dirty;
|
|
529
|
+
if (cleanRequired && dirty !== false) {
|
|
530
|
+
return {
|
|
531
|
+
side,
|
|
532
|
+
required: true,
|
|
533
|
+
ok: false,
|
|
534
|
+
status: dirty === true ? "mismatched" : "missing",
|
|
535
|
+
expected_git_revision: expected,
|
|
536
|
+
observed_git_revision: observed,
|
|
537
|
+
content_digest: preview?.content_digest,
|
|
538
|
+
preview_id: preview?.preview_id,
|
|
539
|
+
preview_url: preview?.url,
|
|
540
|
+
target_url: observation.target.url,
|
|
541
|
+
message: dirty === true ? `${side} evidence was built from a dirty worktree and is not bound only to its Git revision.` : `${side} evidence does not record whether its source worktree was clean.`
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
const evaluatedMs = Date.parse(evaluatedAt);
|
|
545
|
+
const expiresMs = preview?.expires_at ? Date.parse(preview.expires_at) : Number.NaN;
|
|
546
|
+
if (preview && Number.isFinite(evaluatedMs) && Number.isFinite(expiresMs) && expiresMs <= evaluatedMs) {
|
|
547
|
+
return {
|
|
548
|
+
side,
|
|
549
|
+
required: true,
|
|
550
|
+
ok: false,
|
|
551
|
+
status: "stale",
|
|
552
|
+
expected_git_revision: expected,
|
|
553
|
+
observed_git_revision: observed,
|
|
554
|
+
content_digest: preview.content_digest,
|
|
555
|
+
preview_id: preview.preview_id,
|
|
556
|
+
preview_url: preview.url,
|
|
557
|
+
target_url: observation.target.url,
|
|
558
|
+
expires_at: preview.expires_at,
|
|
559
|
+
message: `${side} Preview receipt expired at ${preview.expires_at}.`
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
return {
|
|
563
|
+
side,
|
|
564
|
+
required: true,
|
|
565
|
+
ok: true,
|
|
566
|
+
status: "matched",
|
|
567
|
+
expected_git_revision: expected,
|
|
568
|
+
observed_git_revision: observed,
|
|
569
|
+
content_digest: preview?.content_digest,
|
|
570
|
+
preview_id: preview?.preview_id,
|
|
571
|
+
preview_url: preview?.url,
|
|
572
|
+
target_url: observation.target.url,
|
|
573
|
+
expires_at: preview?.expires_at
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
function collapsedChangeStatus(groups, deltas, sourceBindings) {
|
|
157
577
|
const groupResults = [groups.before, groups.after];
|
|
158
578
|
if (groupResults.some((group) => group.status === "environment_blocked")) return "environment_blocked";
|
|
159
579
|
if (groupResults.some((group) => group.status === "configuration_error")) return "configuration_error";
|
|
160
580
|
if (deltas.some((delta) => delta.status === "configuration_error")) return "configuration_error";
|
|
161
581
|
if (groupResults.some((group) => group.status === "needs_human_review")) return "needs_human_review";
|
|
162
582
|
if (groupResults.some((group) => group.status === "missing" || group.status === "proof_insufficient")) return "proof_insufficient";
|
|
583
|
+
if ([sourceBindings.before, sourceBindings.after].some((binding) => binding.required && !binding.ok)) return "proof_insufficient";
|
|
163
584
|
if (!deltas.length || deltas.some((delta) => delta.status === "proof_insufficient")) return "proof_insufficient";
|
|
164
585
|
if (groupResults.some((group) => !group.ok)) return "product_regression";
|
|
165
586
|
if (deltas.some((delta) => delta.status === "failed")) return "product_regression";
|
|
@@ -174,18 +595,38 @@ function changeSummary(name, status, deltas) {
|
|
|
174
595
|
return `${name} failed ${deltas.filter((delta) => delta.status === "failed").length} change delta(s).`;
|
|
175
596
|
}
|
|
176
597
|
function assessRiddleProofChange(contract, input) {
|
|
598
|
+
const beforeResult = input.before_result || profileResultFromObservation(input.before_observation);
|
|
599
|
+
const afterResult = input.after_result || profileResultFromObservation(input.after_observation);
|
|
177
600
|
const groups = {
|
|
178
|
-
before: groupResult("before", contract.before,
|
|
179
|
-
after: groupResult("after", contract.after,
|
|
601
|
+
before: groupResult("before", contract.before, beforeResult),
|
|
602
|
+
after: groupResult("after", contract.after, afterResult)
|
|
180
603
|
};
|
|
181
|
-
const deltas = (contract.deltas || []).map((delta) => assessDelta(delta,
|
|
182
|
-
const
|
|
604
|
+
const deltas = (contract.deltas || []).map((delta) => assessDelta(delta, beforeResult, afterResult));
|
|
605
|
+
const evaluatedAt = input.evaluated_at || (/* @__PURE__ */ new Date()).toISOString();
|
|
606
|
+
const sourceBindings = {
|
|
607
|
+
before: sourceBindingResult(
|
|
608
|
+
"before",
|
|
609
|
+
contract.source_binding?.before,
|
|
610
|
+
input.before_observation,
|
|
611
|
+
input.expected_source_revisions?.before,
|
|
612
|
+
evaluatedAt
|
|
613
|
+
),
|
|
614
|
+
after: sourceBindingResult(
|
|
615
|
+
"after",
|
|
616
|
+
contract.source_binding?.after,
|
|
617
|
+
input.after_observation,
|
|
618
|
+
input.expected_source_revisions?.after,
|
|
619
|
+
evaluatedAt
|
|
620
|
+
)
|
|
621
|
+
};
|
|
622
|
+
const status = collapsedChangeStatus(groups, deltas, sourceBindings);
|
|
183
623
|
return {
|
|
184
624
|
version: RIDDLE_PROOF_CHANGE_RESULT_VERSION,
|
|
185
625
|
contract_name: contract.name,
|
|
186
626
|
status,
|
|
187
627
|
groups,
|
|
188
628
|
deltas,
|
|
629
|
+
source_bindings: sourceBindings,
|
|
189
630
|
summary: changeSummary(contract.name, status, deltas),
|
|
190
631
|
metadata: contract.metadata
|
|
191
632
|
};
|
|
@@ -198,7 +639,7 @@ function changeReceiptVerdict(status) {
|
|
|
198
639
|
if (status === "proof_insufficient") return "proof_insufficient";
|
|
199
640
|
return "not_mergeable";
|
|
200
641
|
}
|
|
201
|
-
function
|
|
642
|
+
function profileCheckCounts2(result) {
|
|
202
643
|
const counts = {
|
|
203
644
|
total: result.checks.length,
|
|
204
645
|
passed: 0,
|
|
@@ -214,86 +655,90 @@ function profileCheckCounts(result) {
|
|
|
214
655
|
}
|
|
215
656
|
return counts;
|
|
216
657
|
}
|
|
217
|
-
function
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if (/\bjson\b|\btext\b|\bmarkdown\b|\bhtml\b|\blog\b/.test(text) || /\.(json|md|txt|html|log|har)(\?|#|$)/.test(text)) return "data";
|
|
221
|
-
return "artifact";
|
|
658
|
+
function artifactIsScreenshot(artifact) {
|
|
659
|
+
if (artifact.kind !== "image") return false;
|
|
660
|
+
return /screenshot|\.png|\.jpe?g|\.webp|\.gif|\.avif|\.svg/i.test([artifact.name, artifact.url, artifact.path].filter(Boolean).join(" "));
|
|
222
661
|
}
|
|
223
|
-
function
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
kind: artifactKind(ref),
|
|
228
|
-
url: ref.url,
|
|
229
|
-
path: ref.path,
|
|
230
|
-
source: ref.source
|
|
231
|
-
};
|
|
662
|
+
function metadataStringList(metadata, key) {
|
|
663
|
+
const value = metadata?.[key];
|
|
664
|
+
if (!Array.isArray(value)) return [];
|
|
665
|
+
return value.filter((item) => typeof item === "string" && item.trim().length > 0).map((item) => item.trim());
|
|
232
666
|
}
|
|
233
|
-
function
|
|
234
|
-
|
|
667
|
+
function changeRecommendation(verdict) {
|
|
668
|
+
if (verdict === "mergeable") {
|
|
669
|
+
return {
|
|
670
|
+
merge_recommended: true,
|
|
671
|
+
verdict,
|
|
672
|
+
label: "Merge recommended",
|
|
673
|
+
reason: "The declared before/after delta contract passed with sufficient bound evidence."
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
const reason = verdict === "not_mergeable" ? "The declared change delta did not pass." : verdict === "environment_blocked" ? "The environment blocked reliable evidence collection." : verdict === "needs_human_review" ? "The evidence requires human review." : verdict === "configuration_error" ? "The change contract is invalid." : "The change proof did not produce sufficient bound evidence.";
|
|
677
|
+
return { merge_recommended: false, verdict, label: "Merge not recommended", reason };
|
|
235
678
|
}
|
|
236
|
-
function
|
|
237
|
-
|
|
238
|
-
if (!screenshotName) return false;
|
|
239
|
-
return [artifact.name, artifact.url, artifact.path].map(comparableArtifactName).some((name) => name === screenshotName || name.endsWith(`-${screenshotName}`));
|
|
679
|
+
function noShippingAuthorization() {
|
|
680
|
+
return { status: "not_granted", authorized: false, source: "none" };
|
|
240
681
|
}
|
|
241
|
-
function
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
artifacts.push(artifact);
|
|
249
|
-
};
|
|
250
|
-
for (const ref of result.artifacts.riddle_artifacts || []) {
|
|
251
|
-
add(receiptArtifactFromRef(side, ref));
|
|
682
|
+
function recommendationMatches(actual, expected) {
|
|
683
|
+
return actual?.merge_recommended === expected.merge_recommended && actual.verdict === expected.verdict && actual.label === expected.label && actual.reason === expected.reason;
|
|
684
|
+
}
|
|
685
|
+
function assertShippingAuthorizationConsistent(authorization, receiptName) {
|
|
686
|
+
if (!authorization) throw new Error(`${receiptName} shipping authorization is required.`);
|
|
687
|
+
if (authorization.status !== "not_granted" && authorization.status !== "granted") {
|
|
688
|
+
throw new Error(`${receiptName} shipping authorization status is invalid.`);
|
|
252
689
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
690
|
+
if (authorization.source !== "none" && authorization.source !== "human" && authorization.source !== "automation") {
|
|
691
|
+
throw new Error(`${receiptName} shipping authorization source is invalid.`);
|
|
692
|
+
}
|
|
693
|
+
const granted = authorization.status === "granted";
|
|
694
|
+
if (authorization.authorized !== granted) {
|
|
695
|
+
throw new Error(`${receiptName} shipping authorization status and authorized flag must agree.`);
|
|
696
|
+
}
|
|
697
|
+
if (authorization.source === "none" === granted) {
|
|
698
|
+
throw new Error(`${receiptName} shipping authorization source must identify an authorizer only when granted.`);
|
|
260
699
|
}
|
|
261
|
-
return artifacts;
|
|
262
700
|
}
|
|
263
|
-
function
|
|
264
|
-
if (
|
|
265
|
-
return
|
|
701
|
+
function observationArtifactMatches(actual, expected) {
|
|
702
|
+
if (!actual || !expected) return actual === expected;
|
|
703
|
+
return actual.name === expected.name && actual.role === expected.role && actual.url === expected.url && actual.path === expected.path && actual.kind === expected.kind && actual.content_type === expected.content_type && actual.source === expected.source;
|
|
266
704
|
}
|
|
267
|
-
function
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
profile_name: result.profile_name,
|
|
273
|
-
status: result.status,
|
|
274
|
-
summary: result.summary,
|
|
275
|
-
route: result.route,
|
|
276
|
-
captured_at: result.captured_at,
|
|
277
|
-
checks: profileCheckCounts(result),
|
|
278
|
-
screenshots: artifacts.filter(artifactIsScreenshot),
|
|
279
|
-
artifacts
|
|
705
|
+
function observationExecutor(result) {
|
|
706
|
+
return result.runner === "local-playwright" ? { kind: "local_playwright", runner: result.runner } : {
|
|
707
|
+
kind: "riddle_hosted",
|
|
708
|
+
runner: result.runner,
|
|
709
|
+
job_id: result.riddle?.job_id
|
|
280
710
|
};
|
|
281
711
|
}
|
|
282
|
-
function
|
|
283
|
-
|
|
284
|
-
if (!
|
|
285
|
-
return
|
|
712
|
+
function observationForReceiptSide(side, result, source, sourceIdentity) {
|
|
713
|
+
if (!result) throw new Error(`${side}_result or ${side}_observation is required.`);
|
|
714
|
+
if (!source) throw new Error(`${side}_source or ${side}_observation is required.`);
|
|
715
|
+
return createRiddleProofObservationReceipt({
|
|
716
|
+
comparison_role: side,
|
|
717
|
+
executor: observationExecutor(result),
|
|
718
|
+
target: { kind: "url", url: source },
|
|
719
|
+
source: sourceIdentity,
|
|
720
|
+
profile_result: result,
|
|
721
|
+
execution: result.riddle?.execution,
|
|
722
|
+
publication: source.startsWith("http") ? { kind: result.runner === "local-playwright" ? "other" : "riddle_cdn", url: source } : { kind: "local", path: source }
|
|
723
|
+
});
|
|
286
724
|
}
|
|
287
725
|
function createRiddleProofChangeReceipt(input) {
|
|
726
|
+
const before = input.before_observation ? parseRiddleProofObservationReceipt(input.before_observation) : observationForReceiptSide("before", input.before_result, input.before_source, input.before_source_identity);
|
|
727
|
+
const after = input.after_observation ? parseRiddleProofObservationReceipt(input.after_observation) : observationForReceiptSide("after", input.after_result, input.after_source, input.after_source_identity);
|
|
728
|
+
const verdict = changeReceiptVerdict(input.result.status);
|
|
729
|
+
const shippingAuthorization = input.shipping_authorization || noShippingAuthorization();
|
|
730
|
+
assertShippingAuthorizationConsistent(shippingAuthorization, "Change receipt");
|
|
288
731
|
return {
|
|
289
732
|
version: RIDDLE_PROOF_CHANGE_RECEIPT_VERSION,
|
|
290
733
|
contract_name: input.result.contract_name,
|
|
291
734
|
profile_name: input.profile_name,
|
|
292
735
|
status: input.result.status,
|
|
293
|
-
verdict
|
|
736
|
+
verdict,
|
|
294
737
|
summary: input.result.summary,
|
|
295
|
-
before
|
|
296
|
-
after
|
|
738
|
+
before,
|
|
739
|
+
after,
|
|
740
|
+
groups: input.result.groups,
|
|
741
|
+
source_bindings: input.result.source_bindings,
|
|
297
742
|
deltas: input.result.deltas.map((delta) => ({
|
|
298
743
|
type: delta.type,
|
|
299
744
|
label: delta.label,
|
|
@@ -302,17 +747,201 @@ function createRiddleProofChangeReceipt(input) {
|
|
|
302
747
|
after_observed: delta.after_observed,
|
|
303
748
|
message: delta.message
|
|
304
749
|
})),
|
|
750
|
+
recommendation: changeRecommendation(verdict),
|
|
751
|
+
shipping_authorization: shippingAuthorization,
|
|
305
752
|
proves: metadataStringList(input.contract.metadata, "required_receipts"),
|
|
306
753
|
does_not_prove: metadataStringList(input.contract.metadata, "does_not_prove"),
|
|
307
754
|
metadata: input.result.metadata
|
|
308
755
|
};
|
|
309
756
|
}
|
|
757
|
+
function isRecord2(value) {
|
|
758
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
759
|
+
}
|
|
760
|
+
function migratedObservationFromLegacySide(side, legacy) {
|
|
761
|
+
const legacyArtifacts = legacy.artifacts.map((artifact) => ({
|
|
762
|
+
name: artifact.name,
|
|
763
|
+
url: artifact.url,
|
|
764
|
+
path: artifact.path,
|
|
765
|
+
source: artifact.source,
|
|
766
|
+
kind: artifact.kind,
|
|
767
|
+
role: legacy.screenshots[0] && artifactTarget(artifact) === artifactTarget(legacy.screenshots[0]) ? "canonical_screenshot" : artifact.kind === "image" ? "diagnostic" : artifact.kind === "data" ? "data" : "artifact"
|
|
768
|
+
}));
|
|
769
|
+
const canonicalScreenshot = legacyArtifacts.find((artifact) => artifact.role === "canonical_screenshot");
|
|
770
|
+
return {
|
|
771
|
+
version: "riddle-proof.observation-receipt.v1",
|
|
772
|
+
observation_id: `obs_${side}_migrated`,
|
|
773
|
+
comparison_role: side,
|
|
774
|
+
executor: { kind: "other", runner: "legacy-change-receipt" },
|
|
775
|
+
target: { kind: "url", url: legacy.source },
|
|
776
|
+
source: {},
|
|
777
|
+
captured_at: legacy.captured_at || (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
778
|
+
artifacts: legacyArtifacts,
|
|
779
|
+
canonical_screenshot: canonicalScreenshot,
|
|
780
|
+
profile_summary: {
|
|
781
|
+
profile_name: legacy.profile_name,
|
|
782
|
+
status: legacy.status,
|
|
783
|
+
summary: legacy.summary,
|
|
784
|
+
route: legacy.route,
|
|
785
|
+
checks: legacy.checks
|
|
786
|
+
},
|
|
787
|
+
metadata: { migrated_from: RIDDLE_PROOF_CHANGE_RECEIPT_V1_VERSION }
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
function migrateRiddleProofChangeReceipt(legacy) {
|
|
791
|
+
const recommendation = changeRecommendation(legacy.verdict);
|
|
792
|
+
return {
|
|
793
|
+
version: RIDDLE_PROOF_CHANGE_RECEIPT_VERSION,
|
|
794
|
+
contract_name: legacy.contract_name,
|
|
795
|
+
profile_name: legacy.profile_name,
|
|
796
|
+
status: legacy.status,
|
|
797
|
+
verdict: legacy.verdict,
|
|
798
|
+
summary: legacy.summary,
|
|
799
|
+
before: migratedObservationFromLegacySide("before", legacy.before),
|
|
800
|
+
after: migratedObservationFromLegacySide("after", legacy.after),
|
|
801
|
+
groups: {
|
|
802
|
+
before: {
|
|
803
|
+
side: "before",
|
|
804
|
+
label: "before",
|
|
805
|
+
ok: legacy.before.status === "passed" || legacy.before.status === "product_regression",
|
|
806
|
+
profile_name: legacy.before.profile_name,
|
|
807
|
+
status: legacy.before.status,
|
|
808
|
+
required_status: DEFAULT_BEFORE_STATUSES,
|
|
809
|
+
summary: legacy.before.summary
|
|
810
|
+
},
|
|
811
|
+
after: {
|
|
812
|
+
side: "after",
|
|
813
|
+
label: "after",
|
|
814
|
+
ok: legacy.after.status === "passed",
|
|
815
|
+
profile_name: legacy.after.profile_name,
|
|
816
|
+
status: legacy.after.status,
|
|
817
|
+
required_status: DEFAULT_AFTER_STATUSES,
|
|
818
|
+
summary: legacy.after.summary
|
|
819
|
+
}
|
|
820
|
+
},
|
|
821
|
+
source_bindings: {
|
|
822
|
+
before: { side: "before", required: false, ok: true, status: "not_required" },
|
|
823
|
+
after: { side: "after", required: false, ok: true, status: "not_required" }
|
|
824
|
+
},
|
|
825
|
+
deltas: legacy.deltas,
|
|
826
|
+
recommendation,
|
|
827
|
+
shipping_authorization: noShippingAuthorization(),
|
|
828
|
+
proves: legacy.proves,
|
|
829
|
+
does_not_prove: legacy.does_not_prove,
|
|
830
|
+
metadata: legacy.metadata
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
function parseRiddleProofChangeReceipt(value) {
|
|
834
|
+
if (!isRecord2(value)) throw new Error("Change receipt must be an object.");
|
|
835
|
+
if (value.version === RIDDLE_PROOF_CHANGE_RECEIPT_V1_VERSION) {
|
|
836
|
+
return migrateRiddleProofChangeReceipt(value);
|
|
837
|
+
}
|
|
838
|
+
if (value.version !== RIDDLE_PROOF_CHANGE_RECEIPT_VERSION) {
|
|
839
|
+
throw new Error(`Unsupported Change receipt version ${String(value.version || "missing")}.`);
|
|
840
|
+
}
|
|
841
|
+
const receipt = value;
|
|
842
|
+
if (!RIDDLE_PROOF_PROFILE_STATUSES.includes(receipt.status)) {
|
|
843
|
+
throw new Error(`Unsupported Change receipt status ${String(receipt.status || "missing")}.`);
|
|
844
|
+
}
|
|
845
|
+
parseRiddleProofObservationReceipt(receipt.before);
|
|
846
|
+
parseRiddleProofObservationReceipt(receipt.after);
|
|
847
|
+
if (receipt.before.comparison_role !== "before" || receipt.after.comparison_role !== "after") {
|
|
848
|
+
throw new Error("Change receipt observations must preserve their before and after comparison roles.");
|
|
849
|
+
}
|
|
850
|
+
const expectedVerdict = changeReceiptVerdict(receipt.status);
|
|
851
|
+
if (receipt.verdict !== expectedVerdict) {
|
|
852
|
+
throw new Error("Change receipt verdict must match its evaluated status.");
|
|
853
|
+
}
|
|
854
|
+
if (!recommendationMatches(receipt.recommendation, changeRecommendation(expectedVerdict))) {
|
|
855
|
+
throw new Error("Change receipt recommendation must be derived from the Change receipt verdict.");
|
|
856
|
+
}
|
|
857
|
+
assertShippingAuthorizationConsistent(receipt.shipping_authorization, "Change receipt");
|
|
858
|
+
return receipt;
|
|
859
|
+
}
|
|
860
|
+
function createRiddleProofHandoffReceipt(changeReceipt, options = {}) {
|
|
861
|
+
const change = parseRiddleProofChangeReceipt(changeReceipt);
|
|
862
|
+
const authorization = options.shipping_authorization || change.shipping_authorization;
|
|
863
|
+
assertShippingAuthorizationConsistent(authorization, "Handoff receipt");
|
|
864
|
+
return {
|
|
865
|
+
version: RIDDLE_PROOF_HANDOFF_RECEIPT_VERSION,
|
|
866
|
+
change,
|
|
867
|
+
verdict: change.verdict,
|
|
868
|
+
recommendation: change.recommendation,
|
|
869
|
+
shipping_authorization: authorization,
|
|
870
|
+
canonical_pair: {
|
|
871
|
+
before: change.before.canonical_screenshot,
|
|
872
|
+
after: change.after.canonical_screenshot
|
|
873
|
+
},
|
|
874
|
+
created_at: options.created_at || (/* @__PURE__ */ new Date()).toISOString()
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
function parseRiddleProofHandoffReceipt(value) {
|
|
878
|
+
if (!isRecord2(value) || value.version !== RIDDLE_PROOF_HANDOFF_RECEIPT_VERSION) {
|
|
879
|
+
throw new Error(`Unsupported Handoff receipt version ${String(isRecord2(value) ? value.version || "missing" : "missing")}.`);
|
|
880
|
+
}
|
|
881
|
+
const receipt = value;
|
|
882
|
+
if (!Number.isFinite(Date.parse(receipt.created_at))) {
|
|
883
|
+
throw new Error("Handoff receipt created_at must be a valid timestamp.");
|
|
884
|
+
}
|
|
885
|
+
const change = parseRiddleProofChangeReceipt(receipt.change);
|
|
886
|
+
if (receipt.verdict !== change.verdict || !recommendationMatches(receipt.recommendation, change.recommendation)) {
|
|
887
|
+
throw new Error("Handoff receipt must preserve the Change receipt verdict and recommendation.");
|
|
888
|
+
}
|
|
889
|
+
if (!observationArtifactMatches(receipt.canonical_pair?.before, change.before.canonical_screenshot) || !observationArtifactMatches(receipt.canonical_pair?.after, change.after.canonical_screenshot)) {
|
|
890
|
+
throw new Error("Handoff receipt canonical pair must project the Change receipt observations.");
|
|
891
|
+
}
|
|
892
|
+
assertShippingAuthorizationConsistent(receipt.shipping_authorization, "Handoff receipt");
|
|
893
|
+
return receipt;
|
|
894
|
+
}
|
|
310
895
|
function markdownTableCell(value) {
|
|
311
896
|
return String(value ?? "").replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
|
|
312
897
|
}
|
|
313
898
|
function artifactTarget(artifact) {
|
|
314
899
|
return artifact.url || artifact.path;
|
|
315
900
|
}
|
|
901
|
+
function observationArtifactKind(artifact) {
|
|
902
|
+
if (artifact.role === "canonical_screenshot" || artifact.role === "setup_screenshot" || artifact.kind === "image") return "image";
|
|
903
|
+
if (artifact.role === "data" || /\.(json|md|txt|html|log|har)(\?|#|$)/i.test(artifact.name)) return "data";
|
|
904
|
+
return "artifact";
|
|
905
|
+
}
|
|
906
|
+
function receiptSideView(side, observation) {
|
|
907
|
+
const artifacts = observation.artifacts.map((artifact) => ({
|
|
908
|
+
side,
|
|
909
|
+
name: artifact.name,
|
|
910
|
+
kind: observationArtifactKind(artifact),
|
|
911
|
+
url: artifact.url,
|
|
912
|
+
path: artifact.path,
|
|
913
|
+
source: artifact.source
|
|
914
|
+
}));
|
|
915
|
+
const summary = observation.profile_summary;
|
|
916
|
+
const canonical = observation.canonical_screenshot;
|
|
917
|
+
const screenshots = artifacts.filter(artifactIsScreenshot);
|
|
918
|
+
if (canonical) {
|
|
919
|
+
const canonicalTarget = canonical.url || canonical.path;
|
|
920
|
+
screenshots.sort((left, right) => {
|
|
921
|
+
const leftCanonical = (left.url || left.path) === canonicalTarget || left.name === canonical.name;
|
|
922
|
+
const rightCanonical = (right.url || right.path) === canonicalTarget || right.name === canonical.name;
|
|
923
|
+
return Number(rightCanonical) - Number(leftCanonical);
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
return {
|
|
927
|
+
side,
|
|
928
|
+
source: observation.target.url,
|
|
929
|
+
profile_name: summary?.profile_name || observation.proof?.profile_name || "profile",
|
|
930
|
+
status: summary?.status || observation.proof?.result.status || "proof_insufficient",
|
|
931
|
+
summary: summary?.summary || observation.proof?.result.summary || "No profile summary recorded.",
|
|
932
|
+
route: summary?.route,
|
|
933
|
+
captured_at: observation.captured_at,
|
|
934
|
+
checks: summary?.checks || (observation.proof ? profileCheckCounts2(observation.proof.result) : {
|
|
935
|
+
total: 0,
|
|
936
|
+
passed: 0,
|
|
937
|
+
failed: 0,
|
|
938
|
+
skipped: 0,
|
|
939
|
+
needs_human_review: 0
|
|
940
|
+
}),
|
|
941
|
+
screenshots,
|
|
942
|
+
artifacts
|
|
943
|
+
};
|
|
944
|
+
}
|
|
316
945
|
function markdownLink(label, target) {
|
|
317
946
|
return `[${label.replace(/\]/g, "\\]")}](${target})`;
|
|
318
947
|
}
|
|
@@ -336,6 +965,8 @@ function appendMarkdownArtifacts(lines, title, artifacts) {
|
|
|
336
965
|
lines.push("");
|
|
337
966
|
}
|
|
338
967
|
function riddleProofChangeReceiptMarkdown(receipt) {
|
|
968
|
+
const before = receiptSideView("before", receipt.before);
|
|
969
|
+
const after = receiptSideView("after", receipt.after);
|
|
339
970
|
const lines = [
|
|
340
971
|
"# Riddle Proof Change Receipt",
|
|
341
972
|
"",
|
|
@@ -343,6 +974,8 @@ function riddleProofChangeReceiptMarkdown(receipt) {
|
|
|
343
974
|
`**Status:** ${receipt.status}`,
|
|
344
975
|
`**Contract:** ${receipt.contract_name}`,
|
|
345
976
|
receipt.profile_name ? `**Profile:** ${receipt.profile_name}` : "",
|
|
977
|
+
`**Recommendation:** ${receipt.recommendation.label}`,
|
|
978
|
+
`**Shipping authorization:** ${receipt.shipping_authorization.status}`,
|
|
346
979
|
"",
|
|
347
980
|
receipt.summary,
|
|
348
981
|
"",
|
|
@@ -350,8 +983,15 @@ function riddleProofChangeReceiptMarkdown(receipt) {
|
|
|
350
983
|
"",
|
|
351
984
|
"| Side | Source | Status | Checks |",
|
|
352
985
|
"| --- | --- | --- | --- |",
|
|
353
|
-
`| Before | ${markdownTableCell(
|
|
354
|
-
`| After | ${markdownTableCell(
|
|
986
|
+
`| Before | ${markdownTableCell(before.source)} | ${before.status}${receipt.groups.before.ok ? " (matched baseline)" : " (baseline mismatch)"} | ${before.checks.passed} passed / ${before.checks.failed} failed |`,
|
|
987
|
+
`| After | ${markdownTableCell(after.source)} | ${after.status} | ${after.checks.passed} passed / ${after.checks.failed} failed |`,
|
|
988
|
+
"",
|
|
989
|
+
"## Source Binding",
|
|
990
|
+
"",
|
|
991
|
+
"| Side | Status | Git revision | Preview | Digest |",
|
|
992
|
+
"| --- | --- | --- | --- | --- |",
|
|
993
|
+
`| Before | ${receipt.source_bindings.before.status} | ${markdownTableCell(receipt.source_bindings.before.observed_git_revision || "not recorded")} | ${markdownTableCell(receipt.source_bindings.before.preview_id || "not required")} | ${markdownTableCell(receipt.source_bindings.before.content_digest || "not required")} |`,
|
|
994
|
+
`| After | ${receipt.source_bindings.after.status} | ${markdownTableCell(receipt.source_bindings.after.observed_git_revision || "not recorded")} | ${markdownTableCell(receipt.source_bindings.after.preview_id || "not required")} | ${markdownTableCell(receipt.source_bindings.after.content_digest || "not required")} |`,
|
|
355
995
|
"",
|
|
356
996
|
"## Delta Checks",
|
|
357
997
|
"",
|
|
@@ -363,8 +1003,8 @@ function riddleProofChangeReceiptMarkdown(receipt) {
|
|
|
363
1003
|
if (delta.message) lines.push(`| ${markdownTableCell(`${delta.label} message`)} | ${markdownTableCell(delta.message)} | | |`);
|
|
364
1004
|
}
|
|
365
1005
|
lines.push("");
|
|
366
|
-
appendMarkdownArtifacts(lines, "Before Screenshot",
|
|
367
|
-
appendMarkdownArtifacts(lines, "After Screenshot",
|
|
1006
|
+
appendMarkdownArtifacts(lines, "Before Canonical Screenshot", before.screenshots.slice(0, 1));
|
|
1007
|
+
appendMarkdownArtifacts(lines, "After Canonical Screenshot", after.screenshots.slice(0, 1));
|
|
368
1008
|
if (receipt.proves.length) {
|
|
369
1009
|
lines.push("## What This Proves", "");
|
|
370
1010
|
for (const claim of receipt.proves) lines.push(`- ${claim}`);
|
|
@@ -375,7 +1015,7 @@ function riddleProofChangeReceiptMarkdown(receipt) {
|
|
|
375
1015
|
for (const claim of receipt.does_not_prove) lines.push(`- ${claim}`);
|
|
376
1016
|
lines.push("");
|
|
377
1017
|
}
|
|
378
|
-
const linkedArtifacts = [...
|
|
1018
|
+
const linkedArtifacts = [...before.artifacts, ...after.artifacts].filter((artifact) => artifactTarget(artifact)).slice(0, 24);
|
|
379
1019
|
if (linkedArtifacts.length) {
|
|
380
1020
|
lines.push("## Artifacts", "");
|
|
381
1021
|
for (const artifact of linkedArtifacts) {
|
|
@@ -407,7 +1047,9 @@ function htmlList(items, emptyText) {
|
|
|
407
1047
|
return `<ul>${items.map((item) => `<li>${escapeHtml(item)}</li>`).join("")}</ul>`;
|
|
408
1048
|
}
|
|
409
1049
|
function riddleProofChangeReceiptHtml(receipt) {
|
|
410
|
-
const
|
|
1050
|
+
const before = receiptSideView("before", receipt.before);
|
|
1051
|
+
const after = receiptSideView("after", receipt.after);
|
|
1052
|
+
const artifactLinks = [...before.artifacts, ...after.artifacts].filter((artifact) => artifactTarget(artifact)).slice(0, 32);
|
|
411
1053
|
const deltaRows = receipt.deltas.map((delta) => `
|
|
412
1054
|
<tr>
|
|
413
1055
|
<td>${escapeHtml(delta.label)}</td>
|
|
@@ -455,26 +1097,37 @@ function riddleProofChangeReceiptHtml(receipt) {
|
|
|
455
1097
|
<h1>${escapeHtml(receipt.contract_name)}</h1>
|
|
456
1098
|
<p>${escapeHtml(receipt.summary)}</p>
|
|
457
1099
|
<p class="muted">Status: ${escapeHtml(receipt.status)}${receipt.profile_name ? ` | Profile: ${escapeHtml(receipt.profile_name)}` : ""}</p>
|
|
1100
|
+
<p>${escapeHtml(receipt.recommendation.label)}. Shipping authorization: ${escapeHtml(receipt.shipping_authorization.status)}.</p>
|
|
458
1101
|
</header>
|
|
459
1102
|
<section>
|
|
460
1103
|
<h2>Evidence Pair</h2>
|
|
461
1104
|
<div class="grid">
|
|
462
1105
|
<div class="side">
|
|
463
1106
|
<h3>Before</h3>
|
|
464
|
-
<p><code>${escapeHtml(
|
|
465
|
-
<p>Status: <strong class="${escapeHtml(
|
|
466
|
-
<p>${escapeHtml(
|
|
467
|
-
<p class="muted">${
|
|
1107
|
+
<p><code>${escapeHtml(before.source)}</code></p>
|
|
1108
|
+
<p>Status: <strong class="${escapeHtml(before.status)}">${escapeHtml(before.status)}</strong>${receipt.groups.before.ok ? " (matched baseline)" : " (baseline mismatch)"}</p>
|
|
1109
|
+
<p>${escapeHtml(before.summary)}</p>
|
|
1110
|
+
<p class="muted">${before.checks.passed} passed / ${before.checks.failed} failed checks</p>
|
|
468
1111
|
</div>
|
|
469
1112
|
<div class="side">
|
|
470
1113
|
<h3>After</h3>
|
|
471
|
-
<p><code>${escapeHtml(
|
|
472
|
-
<p>Status: <strong class="${escapeHtml(
|
|
473
|
-
<p>${escapeHtml(
|
|
474
|
-
<p class="muted">${
|
|
1114
|
+
<p><code>${escapeHtml(after.source)}</code></p>
|
|
1115
|
+
<p>Status: <strong class="${escapeHtml(after.status)}">${escapeHtml(after.status)}</strong></p>
|
|
1116
|
+
<p>${escapeHtml(after.summary)}</p>
|
|
1117
|
+
<p class="muted">${after.checks.passed} passed / ${after.checks.failed} failed checks</p>
|
|
475
1118
|
</div>
|
|
476
1119
|
</div>
|
|
477
1120
|
</section>
|
|
1121
|
+
<section>
|
|
1122
|
+
<h2>Source Binding</h2>
|
|
1123
|
+
<table>
|
|
1124
|
+
<thead><tr><th>Side</th><th>Status</th><th>Git revision</th><th>Preview</th><th>Digest</th></tr></thead>
|
|
1125
|
+
<tbody>
|
|
1126
|
+
<tr><td>Before</td><td>${escapeHtml(receipt.source_bindings.before.status)}</td><td>${escapeHtml(receipt.source_bindings.before.observed_git_revision || "not recorded")}</td><td>${escapeHtml(receipt.source_bindings.before.preview_id || "not required")}</td><td>${escapeHtml(receipt.source_bindings.before.content_digest || "not required")}</td></tr>
|
|
1127
|
+
<tr><td>After</td><td>${escapeHtml(receipt.source_bindings.after.status)}</td><td>${escapeHtml(receipt.source_bindings.after.observed_git_revision || "not recorded")}</td><td>${escapeHtml(receipt.source_bindings.after.preview_id || "not required")}</td><td>${escapeHtml(receipt.source_bindings.after.content_digest || "not required")}</td></tr>
|
|
1128
|
+
</tbody>
|
|
1129
|
+
</table>
|
|
1130
|
+
</section>
|
|
478
1131
|
<section>
|
|
479
1132
|
<h2>Delta Checks</h2>
|
|
480
1133
|
<table>
|
|
@@ -485,8 +1138,8 @@ function riddleProofChangeReceiptHtml(receipt) {
|
|
|
485
1138
|
<section>
|
|
486
1139
|
<h2>Screenshots</h2>
|
|
487
1140
|
<div class="grid">
|
|
488
|
-
<div>${
|
|
489
|
-
<div>${
|
|
1141
|
+
<div>${before.screenshots.slice(0, 1).map(htmlArtifactCard).join("") || `<p class="muted">No canonical before screenshot recorded.</p>`}</div>
|
|
1142
|
+
<div>${after.screenshots.slice(0, 1).map(htmlArtifactCard).join("") || `<p class="muted">No canonical after screenshot recorded.</p>`}</div>
|
|
490
1143
|
</div>
|
|
491
1144
|
</section>
|
|
492
1145
|
<section>
|
|
@@ -509,10 +1162,16 @@ function riddleProofChangeReceiptHtml(receipt) {
|
|
|
509
1162
|
// Annotate the CommonJS export names for ESM import in node:
|
|
510
1163
|
0 && (module.exports = {
|
|
511
1164
|
RIDDLE_PROOF_CHANGE_CONTRACT_VERSION,
|
|
1165
|
+
RIDDLE_PROOF_CHANGE_RECEIPT_V1_VERSION,
|
|
512
1166
|
RIDDLE_PROOF_CHANGE_RECEIPT_VERSION,
|
|
513
1167
|
RIDDLE_PROOF_CHANGE_RESULT_VERSION,
|
|
1168
|
+
RIDDLE_PROOF_HANDOFF_RECEIPT_VERSION,
|
|
514
1169
|
assessRiddleProofChange,
|
|
515
1170
|
createRiddleProofChangeReceipt,
|
|
1171
|
+
createRiddleProofHandoffReceipt,
|
|
1172
|
+
migrateRiddleProofChangeReceipt,
|
|
1173
|
+
parseRiddleProofChangeReceipt,
|
|
1174
|
+
parseRiddleProofHandoffReceipt,
|
|
516
1175
|
riddleProofChangeReceiptHtml,
|
|
517
1176
|
riddleProofChangeReceiptMarkdown
|
|
518
1177
|
});
|