@riddledc/riddle-proof 0.8.76 → 0.8.78
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 +74 -5
- package/dist/advanced/index.d.cts +1 -1
- package/dist/advanced/index.d.ts +1 -1
- package/dist/advanced/proof-run-engine.d.cts +1 -1
- package/dist/advanced/proof-run-engine.d.ts +1 -1
- package/dist/change-proof.cjs +991 -7
- package/dist/change-proof.d.cts +157 -1
- package/dist/change-proof.d.ts +157 -1
- package/dist/change-proof.js +25 -3
- package/dist/{chunk-B2DP2LET.js → chunk-5IFZSUPF.js} +52 -13
- package/dist/chunk-6VFS2JFR.js +886 -0
- package/dist/{chunk-CWRIXP5H.js → chunk-7N6X54WG.js} +181 -17
- package/dist/{chunk-GG2D3MFZ.js → chunk-FCSJZBC5.js} +26 -1
- package/dist/{chunk-AK2EPEVO.js → chunk-HSGZV2NK.js} +221 -63
- package/dist/chunk-MEVVL4TI.js +258 -0
- package/dist/{chunk-UE4I7RTI.js → chunk-RQPCKRKT.js} +1 -1
- package/dist/cli/index.js +7 -6
- package/dist/cli.cjs +1678 -361
- package/dist/cli.js +7 -6
- package/dist/index.cjs +1242 -42
- package/dist/index.d.cts +4 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.js +46 -10
- package/dist/pr-comment.cjs +481 -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/{proof-run-engine-MiKZt9oY.d.ts → proof-run-engine-CsQshTUX.d.ts} +3 -3
- package/dist/{proof-run-engine-Baiv6l3A.d.cts → proof-run-engine-DmUqh7Cw.d.cts} +3 -3
- package/dist/proof-run-engine.d.cts +1 -1
- package/dist/proof-run-engine.d.ts +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-6S7DZWVC.js +0 -167
|
@@ -0,0 +1,886 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createRiddleProofObservationReceipt,
|
|
3
|
+
parseRiddleProofObservationReceipt
|
|
4
|
+
} from "./chunk-MEVVL4TI.js";
|
|
5
|
+
import {
|
|
6
|
+
RIDDLE_PROOF_PROFILE_STATUSES
|
|
7
|
+
} from "./chunk-FCSJZBC5.js";
|
|
8
|
+
|
|
9
|
+
// src/change-proof.ts
|
|
10
|
+
var RIDDLE_PROOF_CHANGE_CONTRACT_VERSION = "riddle-proof.change-contract.v1";
|
|
11
|
+
var RIDDLE_PROOF_CHANGE_RESULT_VERSION = "riddle-proof.change-result.v1";
|
|
12
|
+
var RIDDLE_PROOF_CHANGE_RECEIPT_V1_VERSION = "riddle-proof.change-receipt.v1";
|
|
13
|
+
var RIDDLE_PROOF_CHANGE_RECEIPT_VERSION = "riddle-proof.change-receipt.v2";
|
|
14
|
+
var RIDDLE_PROOF_HANDOFF_RECEIPT_VERSION = "riddle-proof.handoff-receipt.v1";
|
|
15
|
+
var DEFAULT_BEFORE_STATUSES = ["passed", "product_regression"];
|
|
16
|
+
var DEFAULT_AFTER_STATUSES = ["passed"];
|
|
17
|
+
var DEFAULT_PROFILE_STATUS_TRANSITION_BEFORE = ["product_regression"];
|
|
18
|
+
var DEFAULT_PROFILE_STATUS_TRANSITION_AFTER = ["passed"];
|
|
19
|
+
var DEFAULT_CHECK_STATUS_TRANSITION_BEFORE = ["failed"];
|
|
20
|
+
var DEFAULT_CHECK_STATUS_TRANSITION_AFTER = ["passed"];
|
|
21
|
+
function listValue(value, fallback) {
|
|
22
|
+
if (Array.isArray(value)) return value;
|
|
23
|
+
if (value === void 0) return fallback;
|
|
24
|
+
return [value];
|
|
25
|
+
}
|
|
26
|
+
function includesValue(allowed, observed) {
|
|
27
|
+
return observed !== void 0 && allowed.includes(observed);
|
|
28
|
+
}
|
|
29
|
+
function groupResult(side, contract, result) {
|
|
30
|
+
const fallback = side === "before" ? DEFAULT_BEFORE_STATUSES : DEFAULT_AFTER_STATUSES;
|
|
31
|
+
const requiredStatus = listValue(contract?.required_status, fallback);
|
|
32
|
+
const label = contract?.label || side;
|
|
33
|
+
if (!result) {
|
|
34
|
+
return {
|
|
35
|
+
side,
|
|
36
|
+
label,
|
|
37
|
+
ok: false,
|
|
38
|
+
status: "missing",
|
|
39
|
+
required_status: requiredStatus,
|
|
40
|
+
message: `${label} profile result is missing.`
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const ok = includesValue(requiredStatus, result.status);
|
|
44
|
+
return {
|
|
45
|
+
side,
|
|
46
|
+
label,
|
|
47
|
+
ok,
|
|
48
|
+
profile_name: result.profile_name,
|
|
49
|
+
status: result.status,
|
|
50
|
+
required_status: requiredStatus,
|
|
51
|
+
summary: result.summary,
|
|
52
|
+
message: ok ? void 0 : `${label} profile status ${result.status} did not match required status ${requiredStatus.join(", ")}.`
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function findCheck(result, delta) {
|
|
56
|
+
return result.checks.find((check) => {
|
|
57
|
+
if (delta.check_label && check.label !== delta.check_label) return false;
|
|
58
|
+
if (delta.check_type && check.type !== delta.check_type) return false;
|
|
59
|
+
return Boolean(delta.check_label || delta.check_type);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function profileStatusTransitionResult(delta, before, after) {
|
|
63
|
+
const label = delta.label || "profile-status-transition";
|
|
64
|
+
if (!before || !after) {
|
|
65
|
+
return {
|
|
66
|
+
type: delta.type,
|
|
67
|
+
label,
|
|
68
|
+
status: "proof_insufficient",
|
|
69
|
+
before_observed: before?.status,
|
|
70
|
+
after_observed: after?.status,
|
|
71
|
+
message: `${label} needs both before and after profile results.`
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const beforeAllowed = listValue(delta.before_status, DEFAULT_PROFILE_STATUS_TRANSITION_BEFORE);
|
|
75
|
+
const afterAllowed = listValue(delta.after_status, DEFAULT_PROFILE_STATUS_TRANSITION_AFTER);
|
|
76
|
+
const passed = includesValue(beforeAllowed, before.status) && includesValue(afterAllowed, after.status);
|
|
77
|
+
return {
|
|
78
|
+
type: delta.type,
|
|
79
|
+
label,
|
|
80
|
+
status: passed ? "passed" : "failed",
|
|
81
|
+
before_observed: before.status,
|
|
82
|
+
after_observed: after.status,
|
|
83
|
+
message: passed ? void 0 : `${label} expected before ${beforeAllowed.join(", ")} and after ${afterAllowed.join(", ")}, got ${before.status} -> ${after.status}.`
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function checkStatusTransitionResult(delta, before, after) {
|
|
87
|
+
const label = delta.label || delta.check_label || delta.check_type || "check-status-transition";
|
|
88
|
+
if (!delta.check_label && !delta.check_type) {
|
|
89
|
+
return {
|
|
90
|
+
type: delta.type,
|
|
91
|
+
label,
|
|
92
|
+
status: "configuration_error",
|
|
93
|
+
message: `${label} needs check_label or check_type.`
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
if (!before || !after) {
|
|
97
|
+
return {
|
|
98
|
+
type: delta.type,
|
|
99
|
+
label,
|
|
100
|
+
status: "proof_insufficient",
|
|
101
|
+
before_observed: before?.status,
|
|
102
|
+
after_observed: after?.status,
|
|
103
|
+
message: `${label} needs both before and after profile results.`
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const beforeCheck = findCheck(before, delta);
|
|
107
|
+
const afterCheck = findCheck(after, delta);
|
|
108
|
+
if (!beforeCheck || !afterCheck) {
|
|
109
|
+
return {
|
|
110
|
+
type: delta.type,
|
|
111
|
+
label,
|
|
112
|
+
status: "proof_insufficient",
|
|
113
|
+
before_observed: beforeCheck?.status,
|
|
114
|
+
after_observed: afterCheck?.status,
|
|
115
|
+
message: `${label} was not present in ${!beforeCheck && !afterCheck ? "before or after" : !beforeCheck ? "before" : "after"} profile checks.`
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
const beforeAllowed = listValue(delta.before_status, DEFAULT_CHECK_STATUS_TRANSITION_BEFORE);
|
|
119
|
+
const afterAllowed = listValue(delta.after_status, DEFAULT_CHECK_STATUS_TRANSITION_AFTER);
|
|
120
|
+
const passed = includesValue(beforeAllowed, beforeCheck.status) && includesValue(afterAllowed, afterCheck.status);
|
|
121
|
+
return {
|
|
122
|
+
type: delta.type,
|
|
123
|
+
label,
|
|
124
|
+
status: passed ? "passed" : "failed",
|
|
125
|
+
before_observed: beforeCheck.status,
|
|
126
|
+
after_observed: afterCheck.status,
|
|
127
|
+
message: passed ? void 0 : `${label} expected before ${beforeAllowed.join(", ")} and after ${afterAllowed.join(", ")}, got ${beforeCheck.status} -> ${afterCheck.status}.`
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function assessDelta(delta, before, after) {
|
|
131
|
+
if (delta.type === "profile_status_transition") {
|
|
132
|
+
return profileStatusTransitionResult(delta, before, after);
|
|
133
|
+
}
|
|
134
|
+
return checkStatusTransitionResult(delta, before, after);
|
|
135
|
+
}
|
|
136
|
+
function profileResultFromObservation(observation) {
|
|
137
|
+
return observation?.proof?.result;
|
|
138
|
+
}
|
|
139
|
+
function previewReceiptCoversTarget(previewUrl, targetUrl) {
|
|
140
|
+
try {
|
|
141
|
+
const preview = new URL(previewUrl);
|
|
142
|
+
const target = new URL(targetUrl);
|
|
143
|
+
const previewPath = preview.pathname.replace(/\/+$/u, "");
|
|
144
|
+
return preview.origin === target.origin && (target.pathname === previewPath || target.pathname.startsWith(`${previewPath}/`));
|
|
145
|
+
} catch {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function sourceBindingResult(side, requirement, observation, expectedRevision, evaluatedAt) {
|
|
150
|
+
const expected = expectedRevision || requirement?.expected_git_revision;
|
|
151
|
+
const required = Boolean(
|
|
152
|
+
requirement?.preview_receipt_required || requirement?.require_clean_source || requirement?.require_content_digest || requirement?.expected_git_revision || expected
|
|
153
|
+
);
|
|
154
|
+
if (!required) return { side, required: false, ok: true, status: "not_required" };
|
|
155
|
+
if (!observation) {
|
|
156
|
+
return {
|
|
157
|
+
side,
|
|
158
|
+
required: true,
|
|
159
|
+
ok: false,
|
|
160
|
+
status: "missing",
|
|
161
|
+
expected_git_revision: expected,
|
|
162
|
+
message: `${side} source binding requires an Observation receipt.`
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
const preview = observation.target.preview;
|
|
166
|
+
if (requirement?.preview_receipt_required && !preview) {
|
|
167
|
+
return {
|
|
168
|
+
side,
|
|
169
|
+
required: true,
|
|
170
|
+
ok: false,
|
|
171
|
+
status: "missing",
|
|
172
|
+
expected_git_revision: expected,
|
|
173
|
+
message: `${side} source binding requires a Preview receipt.`
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
if (preview && !previewReceiptCoversTarget(preview.url, observation.target.url)) {
|
|
177
|
+
return {
|
|
178
|
+
side,
|
|
179
|
+
required: true,
|
|
180
|
+
ok: false,
|
|
181
|
+
status: "mismatched",
|
|
182
|
+
expected_git_revision: expected,
|
|
183
|
+
observed_git_revision: preview.source.git_revision || observation.source.git_revision,
|
|
184
|
+
content_digest: preview.content_digest,
|
|
185
|
+
preview_id: preview.preview_id,
|
|
186
|
+
preview_url: preview.url,
|
|
187
|
+
target_url: observation.target.url,
|
|
188
|
+
message: `${side} observation target is not contained by Preview ${preview.preview_id}.`
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
const observed = preview?.source.git_revision || observation.source.git_revision;
|
|
192
|
+
const digestRequired = requirement?.require_content_digest === true || requirement?.preview_receipt_required === true;
|
|
193
|
+
if (digestRequired && !preview?.content_digest) {
|
|
194
|
+
return {
|
|
195
|
+
side,
|
|
196
|
+
required: true,
|
|
197
|
+
ok: false,
|
|
198
|
+
status: "missing",
|
|
199
|
+
expected_git_revision: expected,
|
|
200
|
+
observed_git_revision: observed,
|
|
201
|
+
preview_id: preview?.preview_id,
|
|
202
|
+
preview_url: preview?.url,
|
|
203
|
+
target_url: observation.target.url,
|
|
204
|
+
message: `${side} Preview receipt is missing a content digest.`
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
if (expected && !observed) {
|
|
208
|
+
return {
|
|
209
|
+
side,
|
|
210
|
+
required: true,
|
|
211
|
+
ok: false,
|
|
212
|
+
status: "missing",
|
|
213
|
+
expected_git_revision: expected,
|
|
214
|
+
content_digest: preview?.content_digest,
|
|
215
|
+
preview_id: preview?.preview_id,
|
|
216
|
+
preview_url: preview?.url,
|
|
217
|
+
target_url: observation.target.url,
|
|
218
|
+
message: `${side} evidence is missing its source Git revision.`
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
if (expected && observed !== expected) {
|
|
222
|
+
return {
|
|
223
|
+
side,
|
|
224
|
+
required: true,
|
|
225
|
+
ok: false,
|
|
226
|
+
status: "mismatched",
|
|
227
|
+
expected_git_revision: expected,
|
|
228
|
+
observed_git_revision: observed,
|
|
229
|
+
content_digest: preview?.content_digest,
|
|
230
|
+
preview_id: preview?.preview_id,
|
|
231
|
+
preview_url: preview?.url,
|
|
232
|
+
target_url: observation.target.url,
|
|
233
|
+
message: `${side} evidence came from Git revision ${observed}, not expected revision ${expected}.`
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
const cleanRequired = requirement?.require_clean_source === true || requirement?.preview_receipt_required === true;
|
|
237
|
+
const dirty = preview?.source.dirty ?? observation.source.dirty;
|
|
238
|
+
if (cleanRequired && dirty !== false) {
|
|
239
|
+
return {
|
|
240
|
+
side,
|
|
241
|
+
required: true,
|
|
242
|
+
ok: false,
|
|
243
|
+
status: dirty === true ? "mismatched" : "missing",
|
|
244
|
+
expected_git_revision: expected,
|
|
245
|
+
observed_git_revision: observed,
|
|
246
|
+
content_digest: preview?.content_digest,
|
|
247
|
+
preview_id: preview?.preview_id,
|
|
248
|
+
preview_url: preview?.url,
|
|
249
|
+
target_url: observation.target.url,
|
|
250
|
+
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.`
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
const evaluatedMs = Date.parse(evaluatedAt);
|
|
254
|
+
const expiresMs = preview?.expires_at ? Date.parse(preview.expires_at) : Number.NaN;
|
|
255
|
+
if (preview && Number.isFinite(evaluatedMs) && Number.isFinite(expiresMs) && expiresMs <= evaluatedMs) {
|
|
256
|
+
return {
|
|
257
|
+
side,
|
|
258
|
+
required: true,
|
|
259
|
+
ok: false,
|
|
260
|
+
status: "stale",
|
|
261
|
+
expected_git_revision: expected,
|
|
262
|
+
observed_git_revision: observed,
|
|
263
|
+
content_digest: preview.content_digest,
|
|
264
|
+
preview_id: preview.preview_id,
|
|
265
|
+
preview_url: preview.url,
|
|
266
|
+
target_url: observation.target.url,
|
|
267
|
+
expires_at: preview.expires_at,
|
|
268
|
+
message: `${side} Preview receipt expired at ${preview.expires_at}.`
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
side,
|
|
273
|
+
required: true,
|
|
274
|
+
ok: true,
|
|
275
|
+
status: "matched",
|
|
276
|
+
expected_git_revision: expected,
|
|
277
|
+
observed_git_revision: observed,
|
|
278
|
+
content_digest: preview?.content_digest,
|
|
279
|
+
preview_id: preview?.preview_id,
|
|
280
|
+
preview_url: preview?.url,
|
|
281
|
+
target_url: observation.target.url,
|
|
282
|
+
expires_at: preview?.expires_at
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
function collapsedChangeStatus(groups, deltas, sourceBindings) {
|
|
286
|
+
const groupResults = [groups.before, groups.after];
|
|
287
|
+
if (groupResults.some((group) => group.status === "environment_blocked")) return "environment_blocked";
|
|
288
|
+
if (groupResults.some((group) => group.status === "configuration_error")) return "configuration_error";
|
|
289
|
+
if (deltas.some((delta) => delta.status === "configuration_error")) return "configuration_error";
|
|
290
|
+
if (groupResults.some((group) => group.status === "needs_human_review")) return "needs_human_review";
|
|
291
|
+
if (groupResults.some((group) => group.status === "missing" || group.status === "proof_insufficient")) return "proof_insufficient";
|
|
292
|
+
if ([sourceBindings.before, sourceBindings.after].some((binding) => binding.required && !binding.ok)) return "proof_insufficient";
|
|
293
|
+
if (!deltas.length || deltas.some((delta) => delta.status === "proof_insufficient")) return "proof_insufficient";
|
|
294
|
+
if (groupResults.some((group) => !group.ok)) return "product_regression";
|
|
295
|
+
if (deltas.some((delta) => delta.status === "failed")) return "product_regression";
|
|
296
|
+
return "passed";
|
|
297
|
+
}
|
|
298
|
+
function changeSummary(name, status, deltas) {
|
|
299
|
+
if (status === "passed") return `${name} passed ${deltas.length} change delta(s).`;
|
|
300
|
+
if (status === "environment_blocked") return `${name} could not compare reliable evidence because an environment was blocked.`;
|
|
301
|
+
if (status === "configuration_error") return `${name} has an invalid change proof contract.`;
|
|
302
|
+
if (status === "needs_human_review") return `${name} needs human review before the change proof can pass.`;
|
|
303
|
+
if (status === "proof_insufficient") return `${name} did not produce enough before/after evidence for a change proof.`;
|
|
304
|
+
return `${name} failed ${deltas.filter((delta) => delta.status === "failed").length} change delta(s).`;
|
|
305
|
+
}
|
|
306
|
+
function assessRiddleProofChange(contract, input) {
|
|
307
|
+
const beforeResult = input.before_result || profileResultFromObservation(input.before_observation);
|
|
308
|
+
const afterResult = input.after_result || profileResultFromObservation(input.after_observation);
|
|
309
|
+
const groups = {
|
|
310
|
+
before: groupResult("before", contract.before, beforeResult),
|
|
311
|
+
after: groupResult("after", contract.after, afterResult)
|
|
312
|
+
};
|
|
313
|
+
const deltas = (contract.deltas || []).map((delta) => assessDelta(delta, beforeResult, afterResult));
|
|
314
|
+
const evaluatedAt = input.evaluated_at || (/* @__PURE__ */ new Date()).toISOString();
|
|
315
|
+
const sourceBindings = {
|
|
316
|
+
before: sourceBindingResult(
|
|
317
|
+
"before",
|
|
318
|
+
contract.source_binding?.before,
|
|
319
|
+
input.before_observation,
|
|
320
|
+
input.expected_source_revisions?.before,
|
|
321
|
+
evaluatedAt
|
|
322
|
+
),
|
|
323
|
+
after: sourceBindingResult(
|
|
324
|
+
"after",
|
|
325
|
+
contract.source_binding?.after,
|
|
326
|
+
input.after_observation,
|
|
327
|
+
input.expected_source_revisions?.after,
|
|
328
|
+
evaluatedAt
|
|
329
|
+
)
|
|
330
|
+
};
|
|
331
|
+
const status = collapsedChangeStatus(groups, deltas, sourceBindings);
|
|
332
|
+
return {
|
|
333
|
+
version: RIDDLE_PROOF_CHANGE_RESULT_VERSION,
|
|
334
|
+
contract_name: contract.name,
|
|
335
|
+
status,
|
|
336
|
+
groups,
|
|
337
|
+
deltas,
|
|
338
|
+
source_bindings: sourceBindings,
|
|
339
|
+
summary: changeSummary(contract.name, status, deltas),
|
|
340
|
+
metadata: contract.metadata
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
function changeReceiptVerdict(status) {
|
|
344
|
+
if (status === "passed") return "mergeable";
|
|
345
|
+
if (status === "environment_blocked") return "environment_blocked";
|
|
346
|
+
if (status === "configuration_error") return "configuration_error";
|
|
347
|
+
if (status === "needs_human_review") return "needs_human_review";
|
|
348
|
+
if (status === "proof_insufficient") return "proof_insufficient";
|
|
349
|
+
return "not_mergeable";
|
|
350
|
+
}
|
|
351
|
+
function profileCheckCounts(result) {
|
|
352
|
+
const counts = {
|
|
353
|
+
total: result.checks.length,
|
|
354
|
+
passed: 0,
|
|
355
|
+
failed: 0,
|
|
356
|
+
skipped: 0,
|
|
357
|
+
needs_human_review: 0
|
|
358
|
+
};
|
|
359
|
+
for (const check of result.checks) {
|
|
360
|
+
if (check.status === "passed") counts.passed += 1;
|
|
361
|
+
if (check.status === "failed") counts.failed += 1;
|
|
362
|
+
if (check.status === "skipped") counts.skipped += 1;
|
|
363
|
+
if (check.status === "needs_human_review") counts.needs_human_review += 1;
|
|
364
|
+
}
|
|
365
|
+
return counts;
|
|
366
|
+
}
|
|
367
|
+
function artifactIsScreenshot(artifact) {
|
|
368
|
+
if (artifact.kind !== "image") return false;
|
|
369
|
+
return /screenshot|\.png|\.jpe?g|\.webp|\.gif|\.avif|\.svg/i.test([artifact.name, artifact.url, artifact.path].filter(Boolean).join(" "));
|
|
370
|
+
}
|
|
371
|
+
function metadataStringList(metadata, key) {
|
|
372
|
+
const value = metadata?.[key];
|
|
373
|
+
if (!Array.isArray(value)) return [];
|
|
374
|
+
return value.filter((item) => typeof item === "string" && item.trim().length > 0).map((item) => item.trim());
|
|
375
|
+
}
|
|
376
|
+
function changeRecommendation(verdict) {
|
|
377
|
+
if (verdict === "mergeable") {
|
|
378
|
+
return {
|
|
379
|
+
merge_recommended: true,
|
|
380
|
+
verdict,
|
|
381
|
+
label: "Merge recommended",
|
|
382
|
+
reason: "The declared before/after delta contract passed with sufficient bound evidence."
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
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.";
|
|
386
|
+
return { merge_recommended: false, verdict, label: "Merge not recommended", reason };
|
|
387
|
+
}
|
|
388
|
+
function noShippingAuthorization() {
|
|
389
|
+
return { status: "not_granted", authorized: false, source: "none" };
|
|
390
|
+
}
|
|
391
|
+
function recommendationMatches(actual, expected) {
|
|
392
|
+
return actual?.merge_recommended === expected.merge_recommended && actual.verdict === expected.verdict && actual.label === expected.label && actual.reason === expected.reason;
|
|
393
|
+
}
|
|
394
|
+
function assertShippingAuthorizationConsistent(authorization, receiptName) {
|
|
395
|
+
if (!authorization) throw new Error(`${receiptName} shipping authorization is required.`);
|
|
396
|
+
if (authorization.status !== "not_granted" && authorization.status !== "granted") {
|
|
397
|
+
throw new Error(`${receiptName} shipping authorization status is invalid.`);
|
|
398
|
+
}
|
|
399
|
+
if (authorization.source !== "none" && authorization.source !== "human" && authorization.source !== "automation") {
|
|
400
|
+
throw new Error(`${receiptName} shipping authorization source is invalid.`);
|
|
401
|
+
}
|
|
402
|
+
const granted = authorization.status === "granted";
|
|
403
|
+
if (authorization.authorized !== granted) {
|
|
404
|
+
throw new Error(`${receiptName} shipping authorization status and authorized flag must agree.`);
|
|
405
|
+
}
|
|
406
|
+
if (authorization.source === "none" === granted) {
|
|
407
|
+
throw new Error(`${receiptName} shipping authorization source must identify an authorizer only when granted.`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
function observationArtifactMatches(actual, expected) {
|
|
411
|
+
if (!actual || !expected) return actual === expected;
|
|
412
|
+
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;
|
|
413
|
+
}
|
|
414
|
+
function observationExecutor(result) {
|
|
415
|
+
return result.runner === "local-playwright" ? { kind: "local_playwright", runner: result.runner } : {
|
|
416
|
+
kind: "riddle_hosted",
|
|
417
|
+
runner: result.runner,
|
|
418
|
+
job_id: result.riddle?.job_id
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
function observationForReceiptSide(side, result, source, sourceIdentity) {
|
|
422
|
+
if (!result) throw new Error(`${side}_result or ${side}_observation is required.`);
|
|
423
|
+
if (!source) throw new Error(`${side}_source or ${side}_observation is required.`);
|
|
424
|
+
return createRiddleProofObservationReceipt({
|
|
425
|
+
comparison_role: side,
|
|
426
|
+
executor: observationExecutor(result),
|
|
427
|
+
target: { kind: "url", url: source },
|
|
428
|
+
source: sourceIdentity,
|
|
429
|
+
profile_result: result,
|
|
430
|
+
execution: result.riddle?.execution,
|
|
431
|
+
publication: source.startsWith("http") ? { kind: result.runner === "local-playwright" ? "other" : "riddle_cdn", url: source } : { kind: "local", path: source }
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
function createRiddleProofChangeReceipt(input) {
|
|
435
|
+
const before = input.before_observation ? parseRiddleProofObservationReceipt(input.before_observation) : observationForReceiptSide("before", input.before_result, input.before_source, input.before_source_identity);
|
|
436
|
+
const after = input.after_observation ? parseRiddleProofObservationReceipt(input.after_observation) : observationForReceiptSide("after", input.after_result, input.after_source, input.after_source_identity);
|
|
437
|
+
const verdict = changeReceiptVerdict(input.result.status);
|
|
438
|
+
const shippingAuthorization = input.shipping_authorization || noShippingAuthorization();
|
|
439
|
+
assertShippingAuthorizationConsistent(shippingAuthorization, "Change receipt");
|
|
440
|
+
return {
|
|
441
|
+
version: RIDDLE_PROOF_CHANGE_RECEIPT_VERSION,
|
|
442
|
+
contract_name: input.result.contract_name,
|
|
443
|
+
profile_name: input.profile_name,
|
|
444
|
+
status: input.result.status,
|
|
445
|
+
verdict,
|
|
446
|
+
summary: input.result.summary,
|
|
447
|
+
before,
|
|
448
|
+
after,
|
|
449
|
+
groups: input.result.groups,
|
|
450
|
+
source_bindings: input.result.source_bindings,
|
|
451
|
+
deltas: input.result.deltas.map((delta) => ({
|
|
452
|
+
type: delta.type,
|
|
453
|
+
label: delta.label,
|
|
454
|
+
status: delta.status,
|
|
455
|
+
before_observed: delta.before_observed,
|
|
456
|
+
after_observed: delta.after_observed,
|
|
457
|
+
message: delta.message
|
|
458
|
+
})),
|
|
459
|
+
recommendation: changeRecommendation(verdict),
|
|
460
|
+
shipping_authorization: shippingAuthorization,
|
|
461
|
+
proves: metadataStringList(input.contract.metadata, "required_receipts"),
|
|
462
|
+
does_not_prove: metadataStringList(input.contract.metadata, "does_not_prove"),
|
|
463
|
+
metadata: input.result.metadata
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
function isRecord(value) {
|
|
467
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
468
|
+
}
|
|
469
|
+
function migratedObservationFromLegacySide(side, legacy) {
|
|
470
|
+
const legacyArtifacts = legacy.artifacts.map((artifact) => ({
|
|
471
|
+
name: artifact.name,
|
|
472
|
+
url: artifact.url,
|
|
473
|
+
path: artifact.path,
|
|
474
|
+
source: artifact.source,
|
|
475
|
+
kind: artifact.kind,
|
|
476
|
+
role: legacy.screenshots[0] && artifactTarget(artifact) === artifactTarget(legacy.screenshots[0]) ? "canonical_screenshot" : artifact.kind === "image" ? "diagnostic" : artifact.kind === "data" ? "data" : "artifact"
|
|
477
|
+
}));
|
|
478
|
+
const canonicalScreenshot = legacyArtifacts.find((artifact) => artifact.role === "canonical_screenshot");
|
|
479
|
+
return {
|
|
480
|
+
version: "riddle-proof.observation-receipt.v1",
|
|
481
|
+
observation_id: `obs_${side}_migrated`,
|
|
482
|
+
comparison_role: side,
|
|
483
|
+
executor: { kind: "other", runner: "legacy-change-receipt" },
|
|
484
|
+
target: { kind: "url", url: legacy.source },
|
|
485
|
+
source: {},
|
|
486
|
+
captured_at: legacy.captured_at || (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
487
|
+
artifacts: legacyArtifacts,
|
|
488
|
+
canonical_screenshot: canonicalScreenshot,
|
|
489
|
+
profile_summary: {
|
|
490
|
+
profile_name: legacy.profile_name,
|
|
491
|
+
status: legacy.status,
|
|
492
|
+
summary: legacy.summary,
|
|
493
|
+
route: legacy.route,
|
|
494
|
+
checks: legacy.checks
|
|
495
|
+
},
|
|
496
|
+
metadata: { migrated_from: RIDDLE_PROOF_CHANGE_RECEIPT_V1_VERSION }
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
function migrateRiddleProofChangeReceipt(legacy) {
|
|
500
|
+
const recommendation = changeRecommendation(legacy.verdict);
|
|
501
|
+
return {
|
|
502
|
+
version: RIDDLE_PROOF_CHANGE_RECEIPT_VERSION,
|
|
503
|
+
contract_name: legacy.contract_name,
|
|
504
|
+
profile_name: legacy.profile_name,
|
|
505
|
+
status: legacy.status,
|
|
506
|
+
verdict: legacy.verdict,
|
|
507
|
+
summary: legacy.summary,
|
|
508
|
+
before: migratedObservationFromLegacySide("before", legacy.before),
|
|
509
|
+
after: migratedObservationFromLegacySide("after", legacy.after),
|
|
510
|
+
groups: {
|
|
511
|
+
before: {
|
|
512
|
+
side: "before",
|
|
513
|
+
label: "before",
|
|
514
|
+
ok: legacy.before.status === "passed" || legacy.before.status === "product_regression",
|
|
515
|
+
profile_name: legacy.before.profile_name,
|
|
516
|
+
status: legacy.before.status,
|
|
517
|
+
required_status: DEFAULT_BEFORE_STATUSES,
|
|
518
|
+
summary: legacy.before.summary
|
|
519
|
+
},
|
|
520
|
+
after: {
|
|
521
|
+
side: "after",
|
|
522
|
+
label: "after",
|
|
523
|
+
ok: legacy.after.status === "passed",
|
|
524
|
+
profile_name: legacy.after.profile_name,
|
|
525
|
+
status: legacy.after.status,
|
|
526
|
+
required_status: DEFAULT_AFTER_STATUSES,
|
|
527
|
+
summary: legacy.after.summary
|
|
528
|
+
}
|
|
529
|
+
},
|
|
530
|
+
source_bindings: {
|
|
531
|
+
before: { side: "before", required: false, ok: true, status: "not_required" },
|
|
532
|
+
after: { side: "after", required: false, ok: true, status: "not_required" }
|
|
533
|
+
},
|
|
534
|
+
deltas: legacy.deltas,
|
|
535
|
+
recommendation,
|
|
536
|
+
shipping_authorization: noShippingAuthorization(),
|
|
537
|
+
proves: legacy.proves,
|
|
538
|
+
does_not_prove: legacy.does_not_prove,
|
|
539
|
+
metadata: legacy.metadata
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
function parseRiddleProofChangeReceipt(value) {
|
|
543
|
+
if (!isRecord(value)) throw new Error("Change receipt must be an object.");
|
|
544
|
+
if (value.version === RIDDLE_PROOF_CHANGE_RECEIPT_V1_VERSION) {
|
|
545
|
+
return migrateRiddleProofChangeReceipt(value);
|
|
546
|
+
}
|
|
547
|
+
if (value.version !== RIDDLE_PROOF_CHANGE_RECEIPT_VERSION) {
|
|
548
|
+
throw new Error(`Unsupported Change receipt version ${String(value.version || "missing")}.`);
|
|
549
|
+
}
|
|
550
|
+
const receipt = value;
|
|
551
|
+
if (!RIDDLE_PROOF_PROFILE_STATUSES.includes(receipt.status)) {
|
|
552
|
+
throw new Error(`Unsupported Change receipt status ${String(receipt.status || "missing")}.`);
|
|
553
|
+
}
|
|
554
|
+
parseRiddleProofObservationReceipt(receipt.before);
|
|
555
|
+
parseRiddleProofObservationReceipt(receipt.after);
|
|
556
|
+
if (receipt.before.comparison_role !== "before" || receipt.after.comparison_role !== "after") {
|
|
557
|
+
throw new Error("Change receipt observations must preserve their before and after comparison roles.");
|
|
558
|
+
}
|
|
559
|
+
const expectedVerdict = changeReceiptVerdict(receipt.status);
|
|
560
|
+
if (receipt.verdict !== expectedVerdict) {
|
|
561
|
+
throw new Error("Change receipt verdict must match its evaluated status.");
|
|
562
|
+
}
|
|
563
|
+
if (!recommendationMatches(receipt.recommendation, changeRecommendation(expectedVerdict))) {
|
|
564
|
+
throw new Error("Change receipt recommendation must be derived from the Change receipt verdict.");
|
|
565
|
+
}
|
|
566
|
+
assertShippingAuthorizationConsistent(receipt.shipping_authorization, "Change receipt");
|
|
567
|
+
return receipt;
|
|
568
|
+
}
|
|
569
|
+
function createRiddleProofHandoffReceipt(changeReceipt, options = {}) {
|
|
570
|
+
const change = parseRiddleProofChangeReceipt(changeReceipt);
|
|
571
|
+
const authorization = options.shipping_authorization || change.shipping_authorization;
|
|
572
|
+
assertShippingAuthorizationConsistent(authorization, "Handoff receipt");
|
|
573
|
+
return {
|
|
574
|
+
version: RIDDLE_PROOF_HANDOFF_RECEIPT_VERSION,
|
|
575
|
+
change,
|
|
576
|
+
verdict: change.verdict,
|
|
577
|
+
recommendation: change.recommendation,
|
|
578
|
+
shipping_authorization: authorization,
|
|
579
|
+
canonical_pair: {
|
|
580
|
+
before: change.before.canonical_screenshot,
|
|
581
|
+
after: change.after.canonical_screenshot
|
|
582
|
+
},
|
|
583
|
+
created_at: options.created_at || (/* @__PURE__ */ new Date()).toISOString()
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
function parseRiddleProofHandoffReceipt(value) {
|
|
587
|
+
if (!isRecord(value) || value.version !== RIDDLE_PROOF_HANDOFF_RECEIPT_VERSION) {
|
|
588
|
+
throw new Error(`Unsupported Handoff receipt version ${String(isRecord(value) ? value.version || "missing" : "missing")}.`);
|
|
589
|
+
}
|
|
590
|
+
const receipt = value;
|
|
591
|
+
if (!Number.isFinite(Date.parse(receipt.created_at))) {
|
|
592
|
+
throw new Error("Handoff receipt created_at must be a valid timestamp.");
|
|
593
|
+
}
|
|
594
|
+
const change = parseRiddleProofChangeReceipt(receipt.change);
|
|
595
|
+
if (receipt.verdict !== change.verdict || !recommendationMatches(receipt.recommendation, change.recommendation)) {
|
|
596
|
+
throw new Error("Handoff receipt must preserve the Change receipt verdict and recommendation.");
|
|
597
|
+
}
|
|
598
|
+
if (!observationArtifactMatches(receipt.canonical_pair?.before, change.before.canonical_screenshot) || !observationArtifactMatches(receipt.canonical_pair?.after, change.after.canonical_screenshot)) {
|
|
599
|
+
throw new Error("Handoff receipt canonical pair must project the Change receipt observations.");
|
|
600
|
+
}
|
|
601
|
+
assertShippingAuthorizationConsistent(receipt.shipping_authorization, "Handoff receipt");
|
|
602
|
+
return receipt;
|
|
603
|
+
}
|
|
604
|
+
function markdownTableCell(value) {
|
|
605
|
+
return String(value ?? "").replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
|
|
606
|
+
}
|
|
607
|
+
function artifactTarget(artifact) {
|
|
608
|
+
return artifact.url || artifact.path;
|
|
609
|
+
}
|
|
610
|
+
function observationArtifactKind(artifact) {
|
|
611
|
+
if (artifact.role === "canonical_screenshot" || artifact.role === "setup_screenshot" || artifact.kind === "image") return "image";
|
|
612
|
+
if (artifact.role === "data" || /\.(json|md|txt|html|log|har)(\?|#|$)/i.test(artifact.name)) return "data";
|
|
613
|
+
return "artifact";
|
|
614
|
+
}
|
|
615
|
+
function receiptSideView(side, observation) {
|
|
616
|
+
const artifacts = observation.artifacts.map((artifact) => ({
|
|
617
|
+
side,
|
|
618
|
+
name: artifact.name,
|
|
619
|
+
kind: observationArtifactKind(artifact),
|
|
620
|
+
url: artifact.url,
|
|
621
|
+
path: artifact.path,
|
|
622
|
+
source: artifact.source
|
|
623
|
+
}));
|
|
624
|
+
const summary = observation.profile_summary;
|
|
625
|
+
const canonical = observation.canonical_screenshot;
|
|
626
|
+
const screenshots = artifacts.filter(artifactIsScreenshot);
|
|
627
|
+
if (canonical) {
|
|
628
|
+
const canonicalTarget = canonical.url || canonical.path;
|
|
629
|
+
screenshots.sort((left, right) => {
|
|
630
|
+
const leftCanonical = (left.url || left.path) === canonicalTarget || left.name === canonical.name;
|
|
631
|
+
const rightCanonical = (right.url || right.path) === canonicalTarget || right.name === canonical.name;
|
|
632
|
+
return Number(rightCanonical) - Number(leftCanonical);
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
return {
|
|
636
|
+
side,
|
|
637
|
+
source: observation.target.url,
|
|
638
|
+
profile_name: summary?.profile_name || observation.proof?.profile_name || "profile",
|
|
639
|
+
status: summary?.status || observation.proof?.result.status || "proof_insufficient",
|
|
640
|
+
summary: summary?.summary || observation.proof?.result.summary || "No profile summary recorded.",
|
|
641
|
+
route: summary?.route,
|
|
642
|
+
captured_at: observation.captured_at,
|
|
643
|
+
checks: summary?.checks || (observation.proof ? profileCheckCounts(observation.proof.result) : {
|
|
644
|
+
total: 0,
|
|
645
|
+
passed: 0,
|
|
646
|
+
failed: 0,
|
|
647
|
+
skipped: 0,
|
|
648
|
+
needs_human_review: 0
|
|
649
|
+
}),
|
|
650
|
+
screenshots,
|
|
651
|
+
artifacts
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
function markdownLink(label, target) {
|
|
655
|
+
return `[${label.replace(/\]/g, "\\]")}](${target})`;
|
|
656
|
+
}
|
|
657
|
+
function appendMarkdownArtifacts(lines, title, artifacts) {
|
|
658
|
+
lines.push(`### ${title}`, "");
|
|
659
|
+
if (!artifacts.length) {
|
|
660
|
+
lines.push("- No screenshot artifacts recorded.", "");
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
for (const artifact of artifacts.slice(0, 6)) {
|
|
664
|
+
const target = artifactTarget(artifact);
|
|
665
|
+
if (target && artifact.kind === "image") {
|
|
666
|
+
lines.push(`![${artifact.name.replace(/\]/g, "\\]")}](${target})`);
|
|
667
|
+
} else if (target) {
|
|
668
|
+
lines.push(`- ${markdownLink(artifact.name, target)}`);
|
|
669
|
+
} else {
|
|
670
|
+
lines.push(`- ${artifact.name}`);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
if (artifacts.length > 6) lines.push(`- ${artifacts.length - 6} more artifact(s) omitted`);
|
|
674
|
+
lines.push("");
|
|
675
|
+
}
|
|
676
|
+
function riddleProofChangeReceiptMarkdown(receipt) {
|
|
677
|
+
const before = receiptSideView("before", receipt.before);
|
|
678
|
+
const after = receiptSideView("after", receipt.after);
|
|
679
|
+
const lines = [
|
|
680
|
+
"# Riddle Proof Change Receipt",
|
|
681
|
+
"",
|
|
682
|
+
`**Verdict:** ${receipt.verdict}`,
|
|
683
|
+
`**Status:** ${receipt.status}`,
|
|
684
|
+
`**Contract:** ${receipt.contract_name}`,
|
|
685
|
+
receipt.profile_name ? `**Profile:** ${receipt.profile_name}` : "",
|
|
686
|
+
`**Recommendation:** ${receipt.recommendation.label}`,
|
|
687
|
+
`**Shipping authorization:** ${receipt.shipping_authorization.status}`,
|
|
688
|
+
"",
|
|
689
|
+
receipt.summary,
|
|
690
|
+
"",
|
|
691
|
+
"## Evidence Pair",
|
|
692
|
+
"",
|
|
693
|
+
"| Side | Source | Status | Checks |",
|
|
694
|
+
"| --- | --- | --- | --- |",
|
|
695
|
+
`| Before | ${markdownTableCell(before.source)} | ${before.status}${receipt.groups.before.ok ? " (matched baseline)" : " (baseline mismatch)"} | ${before.checks.passed} passed / ${before.checks.failed} failed |`,
|
|
696
|
+
`| After | ${markdownTableCell(after.source)} | ${after.status} | ${after.checks.passed} passed / ${after.checks.failed} failed |`,
|
|
697
|
+
"",
|
|
698
|
+
"## Source Binding",
|
|
699
|
+
"",
|
|
700
|
+
"| Side | Status | Git revision | Preview | Digest |",
|
|
701
|
+
"| --- | --- | --- | --- | --- |",
|
|
702
|
+
`| 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")} |`,
|
|
703
|
+
`| 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")} |`,
|
|
704
|
+
"",
|
|
705
|
+
"## Delta Checks",
|
|
706
|
+
"",
|
|
707
|
+
"| Delta | Status | Before | After |",
|
|
708
|
+
"| --- | --- | --- | --- |"
|
|
709
|
+
].filter(Boolean);
|
|
710
|
+
for (const delta of receipt.deltas) {
|
|
711
|
+
lines.push(`| ${markdownTableCell(delta.label)} | ${delta.status} | ${markdownTableCell(delta.before_observed)} | ${markdownTableCell(delta.after_observed)} |`);
|
|
712
|
+
if (delta.message) lines.push(`| ${markdownTableCell(`${delta.label} message`)} | ${markdownTableCell(delta.message)} | | |`);
|
|
713
|
+
}
|
|
714
|
+
lines.push("");
|
|
715
|
+
appendMarkdownArtifacts(lines, "Before Canonical Screenshot", before.screenshots.slice(0, 1));
|
|
716
|
+
appendMarkdownArtifacts(lines, "After Canonical Screenshot", after.screenshots.slice(0, 1));
|
|
717
|
+
if (receipt.proves.length) {
|
|
718
|
+
lines.push("## What This Proves", "");
|
|
719
|
+
for (const claim of receipt.proves) lines.push(`- ${claim}`);
|
|
720
|
+
lines.push("");
|
|
721
|
+
}
|
|
722
|
+
if (receipt.does_not_prove.length) {
|
|
723
|
+
lines.push("## What This Does Not Prove", "");
|
|
724
|
+
for (const claim of receipt.does_not_prove) lines.push(`- ${claim}`);
|
|
725
|
+
lines.push("");
|
|
726
|
+
}
|
|
727
|
+
const linkedArtifacts = [...before.artifacts, ...after.artifacts].filter((artifact) => artifactTarget(artifact)).slice(0, 24);
|
|
728
|
+
if (linkedArtifacts.length) {
|
|
729
|
+
lines.push("## Artifacts", "");
|
|
730
|
+
for (const artifact of linkedArtifacts) {
|
|
731
|
+
lines.push(`- ${artifact.side}: ${markdownLink(artifact.name, artifactTarget(artifact) || "")}`);
|
|
732
|
+
}
|
|
733
|
+
lines.push("");
|
|
734
|
+
}
|
|
735
|
+
return `${lines.join("\n").trim()}
|
|
736
|
+
`;
|
|
737
|
+
}
|
|
738
|
+
function escapeHtml(value) {
|
|
739
|
+
return String(value ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
740
|
+
}
|
|
741
|
+
function htmlArtifactCard(artifact) {
|
|
742
|
+
const target = artifactTarget(artifact);
|
|
743
|
+
if (!target) return `<p>${escapeHtml(artifact.name)}</p>`;
|
|
744
|
+
if (artifact.kind === "image") {
|
|
745
|
+
return `<figure><img src="${escapeHtml(target)}" alt="${escapeHtml(artifact.name)}"><figcaption>${escapeHtml(artifact.name)}</figcaption></figure>`;
|
|
746
|
+
}
|
|
747
|
+
return `<p><a href="${escapeHtml(target)}">${escapeHtml(artifact.name)}</a></p>`;
|
|
748
|
+
}
|
|
749
|
+
function htmlArtifactLink(artifact) {
|
|
750
|
+
const target = artifactTarget(artifact);
|
|
751
|
+
if (!target) return escapeHtml(artifact.name);
|
|
752
|
+
return `<a href="${escapeHtml(target)}">${escapeHtml(artifact.name)}</a>`;
|
|
753
|
+
}
|
|
754
|
+
function htmlList(items, emptyText) {
|
|
755
|
+
if (!items.length) return `<p class="muted">${escapeHtml(emptyText)}</p>`;
|
|
756
|
+
return `<ul>${items.map((item) => `<li>${escapeHtml(item)}</li>`).join("")}</ul>`;
|
|
757
|
+
}
|
|
758
|
+
function riddleProofChangeReceiptHtml(receipt) {
|
|
759
|
+
const before = receiptSideView("before", receipt.before);
|
|
760
|
+
const after = receiptSideView("after", receipt.after);
|
|
761
|
+
const artifactLinks = [...before.artifacts, ...after.artifacts].filter((artifact) => artifactTarget(artifact)).slice(0, 32);
|
|
762
|
+
const deltaRows = receipt.deltas.map((delta) => `
|
|
763
|
+
<tr>
|
|
764
|
+
<td>${escapeHtml(delta.label)}</td>
|
|
765
|
+
<td><span class="status ${escapeHtml(delta.status)}">${escapeHtml(delta.status)}</span></td>
|
|
766
|
+
<td>${escapeHtml(delta.before_observed)}</td>
|
|
767
|
+
<td>${escapeHtml(delta.after_observed)}</td>
|
|
768
|
+
</tr>
|
|
769
|
+
${delta.message ? `<tr><td class="muted">${escapeHtml(delta.label)} message</td><td colspan="3">${escapeHtml(delta.message)}</td></tr>` : ""}`).join("");
|
|
770
|
+
return `<!doctype html>
|
|
771
|
+
<html lang="en">
|
|
772
|
+
<head>
|
|
773
|
+
<meta charset="utf-8">
|
|
774
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
775
|
+
<title>${escapeHtml(receipt.contract_name)} - Riddle Proof Change Receipt</title>
|
|
776
|
+
<style>
|
|
777
|
+
:root { color-scheme: light dark; --bg: #0f141b; --panel: #171f29; --text: #eef4f8; --muted: #aebbc8; --border: #314152; --ok: #69d089; --bad: #ff7a7a; --warn: #ffd166; }
|
|
778
|
+
body { margin: 0; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: var(--bg); color: var(--text); }
|
|
779
|
+
main { max-width: 1120px; margin: 0 auto; padding: 32px 20px 48px; }
|
|
780
|
+
header, section { border: 1px solid var(--border); background: var(--panel); border-radius: 8px; padding: 20px; margin: 0 0 18px; }
|
|
781
|
+
h1, h2, h3, p { margin-top: 0; }
|
|
782
|
+
h1 { font-size: clamp(1.6rem, 3vw, 2.4rem); }
|
|
783
|
+
h2 { font-size: 1.1rem; margin-bottom: 14px; }
|
|
784
|
+
.muted { color: var(--muted); }
|
|
785
|
+
.verdict { display: inline-flex; align-items: center; gap: 8px; padding: 6px 10px; border-radius: 999px; border: 1px solid var(--border); font-weight: 700; }
|
|
786
|
+
.mergeable, .passed { color: var(--ok); }
|
|
787
|
+
.not_mergeable, .failed, .product_regression { color: var(--bad); }
|
|
788
|
+
.proof_insufficient, .environment_blocked, .needs_human_review, .configuration_error { color: var(--warn); }
|
|
789
|
+
.grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
|
|
790
|
+
.side { border: 1px solid var(--border); border-radius: 8px; padding: 14px; min-width: 0; }
|
|
791
|
+
code { overflow-wrap: anywhere; color: var(--muted); }
|
|
792
|
+
table { width: 100%; border-collapse: collapse; }
|
|
793
|
+
th, td { border-bottom: 1px solid var(--border); padding: 10px 8px; text-align: left; vertical-align: top; }
|
|
794
|
+
figure { margin: 0; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; background: #0b1017; }
|
|
795
|
+
img { display: block; width: 100%; height: auto; }
|
|
796
|
+
figcaption { padding: 8px 10px; color: var(--muted); font-size: 0.9rem; }
|
|
797
|
+
ul { margin-bottom: 0; }
|
|
798
|
+
a { color: #8bc7ff; }
|
|
799
|
+
@media (max-width: 760px) { .grid { grid-template-columns: 1fr; } main { padding: 18px 12px 32px; } }
|
|
800
|
+
</style>
|
|
801
|
+
</head>
|
|
802
|
+
<body>
|
|
803
|
+
<main>
|
|
804
|
+
<header>
|
|
805
|
+
<p class="verdict ${escapeHtml(receipt.verdict)}">${escapeHtml(receipt.verdict)}</p>
|
|
806
|
+
<h1>${escapeHtml(receipt.contract_name)}</h1>
|
|
807
|
+
<p>${escapeHtml(receipt.summary)}</p>
|
|
808
|
+
<p class="muted">Status: ${escapeHtml(receipt.status)}${receipt.profile_name ? ` | Profile: ${escapeHtml(receipt.profile_name)}` : ""}</p>
|
|
809
|
+
<p>${escapeHtml(receipt.recommendation.label)}. Shipping authorization: ${escapeHtml(receipt.shipping_authorization.status)}.</p>
|
|
810
|
+
</header>
|
|
811
|
+
<section>
|
|
812
|
+
<h2>Evidence Pair</h2>
|
|
813
|
+
<div class="grid">
|
|
814
|
+
<div class="side">
|
|
815
|
+
<h3>Before</h3>
|
|
816
|
+
<p><code>${escapeHtml(before.source)}</code></p>
|
|
817
|
+
<p>Status: <strong class="${escapeHtml(before.status)}">${escapeHtml(before.status)}</strong>${receipt.groups.before.ok ? " (matched baseline)" : " (baseline mismatch)"}</p>
|
|
818
|
+
<p>${escapeHtml(before.summary)}</p>
|
|
819
|
+
<p class="muted">${before.checks.passed} passed / ${before.checks.failed} failed checks</p>
|
|
820
|
+
</div>
|
|
821
|
+
<div class="side">
|
|
822
|
+
<h3>After</h3>
|
|
823
|
+
<p><code>${escapeHtml(after.source)}</code></p>
|
|
824
|
+
<p>Status: <strong class="${escapeHtml(after.status)}">${escapeHtml(after.status)}</strong></p>
|
|
825
|
+
<p>${escapeHtml(after.summary)}</p>
|
|
826
|
+
<p class="muted">${after.checks.passed} passed / ${after.checks.failed} failed checks</p>
|
|
827
|
+
</div>
|
|
828
|
+
</div>
|
|
829
|
+
</section>
|
|
830
|
+
<section>
|
|
831
|
+
<h2>Source Binding</h2>
|
|
832
|
+
<table>
|
|
833
|
+
<thead><tr><th>Side</th><th>Status</th><th>Git revision</th><th>Preview</th><th>Digest</th></tr></thead>
|
|
834
|
+
<tbody>
|
|
835
|
+
<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>
|
|
836
|
+
<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>
|
|
837
|
+
</tbody>
|
|
838
|
+
</table>
|
|
839
|
+
</section>
|
|
840
|
+
<section>
|
|
841
|
+
<h2>Delta Checks</h2>
|
|
842
|
+
<table>
|
|
843
|
+
<thead><tr><th>Delta</th><th>Status</th><th>Before</th><th>After</th></tr></thead>
|
|
844
|
+
<tbody>${deltaRows}</tbody>
|
|
845
|
+
</table>
|
|
846
|
+
</section>
|
|
847
|
+
<section>
|
|
848
|
+
<h2>Screenshots</h2>
|
|
849
|
+
<div class="grid">
|
|
850
|
+
<div>${before.screenshots.slice(0, 1).map(htmlArtifactCard).join("") || `<p class="muted">No canonical before screenshot recorded.</p>`}</div>
|
|
851
|
+
<div>${after.screenshots.slice(0, 1).map(htmlArtifactCard).join("") || `<p class="muted">No canonical after screenshot recorded.</p>`}</div>
|
|
852
|
+
</div>
|
|
853
|
+
</section>
|
|
854
|
+
<section>
|
|
855
|
+
<h2>What This Proves</h2>
|
|
856
|
+
${htmlList(receipt.proves, "No explicit proof claims were recorded in contract metadata.")}
|
|
857
|
+
</section>
|
|
858
|
+
<section>
|
|
859
|
+
<h2>What This Does Not Prove</h2>
|
|
860
|
+
${htmlList(receipt.does_not_prove, "No explicit limits were recorded in contract metadata.")}
|
|
861
|
+
</section>
|
|
862
|
+
<section>
|
|
863
|
+
<h2>Artifacts</h2>
|
|
864
|
+
${artifactLinks.length ? `<ul>${artifactLinks.map((artifact) => `<li>${escapeHtml(artifact.side)}: ${htmlArtifactLink(artifact)}</li>`).join("")}</ul>` : `<p class="muted">No linked artifacts recorded.</p>`}
|
|
865
|
+
</section>
|
|
866
|
+
</main>
|
|
867
|
+
</body>
|
|
868
|
+
</html>
|
|
869
|
+
`;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
export {
|
|
873
|
+
RIDDLE_PROOF_CHANGE_CONTRACT_VERSION,
|
|
874
|
+
RIDDLE_PROOF_CHANGE_RESULT_VERSION,
|
|
875
|
+
RIDDLE_PROOF_CHANGE_RECEIPT_V1_VERSION,
|
|
876
|
+
RIDDLE_PROOF_CHANGE_RECEIPT_VERSION,
|
|
877
|
+
RIDDLE_PROOF_HANDOFF_RECEIPT_VERSION,
|
|
878
|
+
assessRiddleProofChange,
|
|
879
|
+
createRiddleProofChangeReceipt,
|
|
880
|
+
migrateRiddleProofChangeReceipt,
|
|
881
|
+
parseRiddleProofChangeReceipt,
|
|
882
|
+
createRiddleProofHandoffReceipt,
|
|
883
|
+
parseRiddleProofHandoffReceipt,
|
|
884
|
+
riddleProofChangeReceiptMarkdown,
|
|
885
|
+
riddleProofChangeReceiptHtml
|
|
886
|
+
};
|