@riddledc/riddle-proof 0.7.149 → 0.7.151

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/dist/cli.cjs CHANGED
@@ -6975,6 +6975,7 @@ var RIDDLE_PROOF_PROFILE_CHECK_TYPES = [
6975
6975
  "selector_text_visible",
6976
6976
  "selector_text_absent",
6977
6977
  "selector_text_order",
6978
+ "observe_within",
6978
6979
  "frame_text_visible",
6979
6980
  "frame_url_equals",
6980
6981
  "frame_url_matches",
@@ -8297,7 +8298,7 @@ function normalizeCheck(input, index) {
8297
8298
  throw new Error(`checks[${index}].type ${type} is not supported. Supported checks: ${RIDDLE_PROOF_PROFILE_CHECK_TYPES.join(", ")}`);
8298
8299
  }
8299
8300
  const isDialogCountCheck = isDialogCountCheckType(type);
8300
- if ((type === "selector_visible" || type === "selector_absent" || type === "selector_count_at_least" || type === "selector_count_equals" || type === "selector_count_equal" || type === "selector_count_eq" || type === "selector_text_visible" || type === "selector_text_absent") && !stringValue2(input.selector)) {
8301
+ if ((type === "selector_visible" || type === "selector_absent" || type === "selector_count_at_least" || type === "selector_count_equals" || type === "selector_count_equal" || type === "selector_count_eq" || type === "selector_text_visible" || type === "selector_text_absent" || type === "observe_within" && !stringValue2(input.text) && !stringValue2(input.pattern)) && !stringValue2(input.selector)) {
8301
8302
  throw new Error(`checks[${index}] ${type} requires selector.`);
8302
8303
  }
8303
8304
  if ((type === "frame_text_visible" || type === "frame_url_equals" || type === "frame_url_matches" || type === "frame_no_horizontal_overflow") && !stringValue2(input.selector)) {
@@ -8438,7 +8439,7 @@ function normalizeCheck(input, index) {
8438
8439
  allowed_content_types: allowedContentTypes,
8439
8440
  allow_get_fallback: isLinkStatusCheck ? input.allow_get_fallback === false || input.allowGetFallback === false ? false : true : void 0,
8440
8441
  max_overflow_px: numberValue(input.max_overflow_px),
8441
- timeout_ms: numberValue(input.timeout_ms) ?? numberValue(input.timeoutMs),
8442
+ timeout_ms: numberValue(input.timeout_ms) ?? numberValue(input.timeoutMs) ?? numberValue(input.within_ms) ?? numberValue(input.withinMs),
8442
8443
  run_direct_routes: input.run_direct_routes === false || input.runDirectRoutes === false ? false : true,
8443
8444
  run_clickthroughs: input.run_clickthroughs === false || input.runClickthroughs === false ? false : true,
8444
8445
  run_all_viewports: input.run_all_viewports === true || input.runAllViewports === true,
@@ -8956,6 +8957,16 @@ function summarizeLinkStatusEvidence(viewport, check) {
8956
8957
  function textKey(check) {
8957
8958
  return check.pattern ? `pattern:${check.pattern}/${check.flags || ""}` : `text:${check.text || ""}`;
8958
8959
  }
8960
+ function observeWithinTimeoutMs(check) {
8961
+ const raw = check.timeout_ms;
8962
+ if (typeof raw === "number" && Number.isFinite(raw) && raw > 0) return Math.min(Math.round(raw), 6e4);
8963
+ return 2e3;
8964
+ }
8965
+ function observeWithinKey(check) {
8966
+ const target = check.selector ? `selector:${check.selector}` : "page";
8967
+ const expectation = check.pattern ? `pattern:${check.pattern}/${check.flags || ""}` : check.text ? `text:${check.text}` : "visible";
8968
+ return `${target}|${expectation}|within:${observeWithinTimeoutMs(check)}`;
8969
+ }
8959
8970
  function textSequenceForCheck(viewport, check) {
8960
8971
  const key = selectorKey(check);
8961
8972
  const sequence = viewport.text_sequences?.[key];
@@ -9462,6 +9473,40 @@ function assessCheckFromEvidence(check, evidence) {
9462
9473
  message: failed ? `Selector ${key} text order failed in ${failed} viewport(s).` : void 0
9463
9474
  };
9464
9475
  }
9476
+ if (check.type === "observe_within") {
9477
+ const key = observeWithinKey(check);
9478
+ const timeoutMs = observeWithinTimeoutMs(check);
9479
+ const results = viewports.map((viewport) => {
9480
+ const observation = viewport.observations?.[key];
9481
+ const matched = observation?.matched === true;
9482
+ return {
9483
+ viewport: viewport.name,
9484
+ matched,
9485
+ elapsed_ms: numberValue(observation?.elapsed_ms) ?? null,
9486
+ timeout_ms: numberValue(observation?.timeout_ms) ?? timeoutMs,
9487
+ attempts: numberValue(observation?.attempts) ?? null,
9488
+ selector_count: numberValue(observation?.selector_count) ?? null,
9489
+ visible_count: numberValue(observation?.visible_count) ?? null,
9490
+ matched_count: numberValue(observation?.matched_count) ?? null,
9491
+ sample: stringValue2(observation?.sample) ?? null,
9492
+ error: stringValue2(observation?.error) ?? null
9493
+ };
9494
+ });
9495
+ const failed = results.filter((result) => !result.matched).length;
9496
+ return {
9497
+ type: check.type,
9498
+ label: checkLabel(check),
9499
+ status: failed ? "failed" : "passed",
9500
+ evidence: {
9501
+ selector: check.selector || null,
9502
+ text: check.text || null,
9503
+ pattern: check.pattern || null,
9504
+ timeout_ms: timeoutMs,
9505
+ viewports: results.map((result) => toJsonValue(result))
9506
+ },
9507
+ message: failed ? `Observation did not match within ${timeoutMs}ms in ${failed} viewport(s).` : void 0
9508
+ };
9509
+ }
9465
9510
  if (check.type === "frame_text_visible") {
9466
9511
  const key = selectorKey(check);
9467
9512
  const results = viewports.map((viewport) => {
@@ -11355,6 +11400,36 @@ function assessProfile(profile, evidence) {
11355
11400
  });
11356
11401
  continue;
11357
11402
  }
11403
+ if (check.type === "observe_within") {
11404
+ const key = observeWithinKey(check);
11405
+ const timeoutMs = observeWithinTimeoutMs(check);
11406
+ const results = checkViewports.map((viewport) => {
11407
+ const observation = viewport.observations && viewport.observations[key] && typeof viewport.observations[key] === "object"
11408
+ ? viewport.observations[key]
11409
+ : {};
11410
+ return {
11411
+ viewport: viewport.name,
11412
+ matched: observation.matched === true,
11413
+ elapsed_ms: typeof observation.elapsed_ms === "number" && Number.isFinite(observation.elapsed_ms) ? observation.elapsed_ms : null,
11414
+ timeout_ms: typeof observation.timeout_ms === "number" && Number.isFinite(observation.timeout_ms) ? observation.timeout_ms : timeoutMs,
11415
+ attempts: typeof observation.attempts === "number" && Number.isFinite(observation.attempts) ? observation.attempts : null,
11416
+ selector_count: typeof observation.selector_count === "number" && Number.isFinite(observation.selector_count) ? observation.selector_count : null,
11417
+ visible_count: typeof observation.visible_count === "number" && Number.isFinite(observation.visible_count) ? observation.visible_count : null,
11418
+ matched_count: typeof observation.matched_count === "number" && Number.isFinite(observation.matched_count) ? observation.matched_count : null,
11419
+ sample: typeof observation.sample === "string" && observation.sample.trim() ? observation.sample.trim() : null,
11420
+ error: typeof observation.error === "string" && observation.error.trim() ? observation.error.trim() : null,
11421
+ };
11422
+ });
11423
+ const failed = results.filter((result) => !result.matched).length;
11424
+ checks.push({
11425
+ type: check.type,
11426
+ label: check.label || check.type,
11427
+ status: failed ? "failed" : "passed",
11428
+ evidence: { selector: check.selector || null, text: check.text || null, pattern: check.pattern || null, timeout_ms: timeoutMs, viewports: results },
11429
+ message: failed ? "Observation did not match within " + timeoutMs + "ms in " + failed + " viewport(s)." : undefined,
11430
+ });
11431
+ continue;
11432
+ }
11358
11433
  if (check.type === "frame_text_visible") {
11359
11434
  const selector = check.selector || "";
11360
11435
  const results = checkViewports.map((viewport) => {
@@ -11787,6 +11862,19 @@ function ensureDialogHandler() {
11787
11862
  function textKey(check) {
11788
11863
  return check.pattern ? "pattern:" + check.pattern + "/" + (check.flags || "") : "text:" + (check.text || "");
11789
11864
  }
11865
+ function observeWithinTimeoutMs(check) {
11866
+ const raw = Number(check && check.timeout_ms);
11867
+ return Number.isFinite(raw) && raw > 0 ? Math.min(Math.round(raw), 60000) : 2000;
11868
+ }
11869
+ function observeWithinKey(check) {
11870
+ const target = check && check.selector ? "selector:" + check.selector : "page";
11871
+ const expectation = check && check.pattern
11872
+ ? "pattern:" + check.pattern + "/" + (check.flags || "")
11873
+ : check && check.text
11874
+ ? "text:" + check.text
11875
+ : "visible";
11876
+ return target + "|" + expectation + "|within:" + observeWithinTimeoutMs(check);
11877
+ }
11790
11878
  function textMatches(sample, check) {
11791
11879
  if (check.pattern) {
11792
11880
  try { return new RegExp(check.pattern, check.flags || "").test(sample || ""); } catch { return false; }
@@ -13102,6 +13190,104 @@ async function selectorTextSequence(selector) {
13102
13190
  };
13103
13191
  }).catch((error) => ({ count: 0, visible_count: 0, texts: [], visible_texts: [], match_texts: [], visible_match_texts: [], error: String(error && error.message ? error.message : error).slice(0, 500) }));
13104
13192
  }
13193
+ async function observeWithinSnapshot(check) {
13194
+ const payload = {
13195
+ selector: check.selector || "",
13196
+ text: check.text || "",
13197
+ pattern: check.pattern || "",
13198
+ flags: check.flags || "",
13199
+ wants_text: Boolean(check.text || check.pattern),
13200
+ };
13201
+ if (payload.selector) {
13202
+ return page.locator(payload.selector).evaluateAll((elements, input) => {
13203
+ const compact = (value) => String(value || "").replace(/\s+/g, " ").trim();
13204
+ const matchText = (value) => {
13205
+ const source = compact(value);
13206
+ if (input.pattern) {
13207
+ try { return new RegExp(input.pattern, input.flags || "").test(source); } catch { return false; }
13208
+ }
13209
+ return source.includes(input.text || "");
13210
+ };
13211
+ const isVisible = (element) => {
13212
+ const style = window.getComputedStyle(element);
13213
+ const rect = element.getBoundingClientRect();
13214
+ return style && style.visibility !== "hidden" && style.display !== "none" && rect.width > 0 && rect.height > 0;
13215
+ };
13216
+ const rows = elements.map((element, index) => {
13217
+ const text = compact(element.innerText || element.textContent || "");
13218
+ const visible = isVisible(element);
13219
+ return { index, text, visible, matched: input.wants_text ? matchText(text) : visible };
13220
+ });
13221
+ const visibleRows = rows.filter((row) => row.visible);
13222
+ const matches = input.wants_text ? visibleRows.filter((row) => row.matched) : visibleRows;
13223
+ const sampleRow = matches[0] || visibleRows[0] || rows[0] || null;
13224
+ return {
13225
+ selector: input.selector,
13226
+ text: input.text || null,
13227
+ pattern: input.pattern || null,
13228
+ selector_count: rows.length,
13229
+ visible_count: visibleRows.length,
13230
+ matched_count: matches.length,
13231
+ matched: matches.length > 0,
13232
+ sample: sampleRow && sampleRow.text ? sampleRow.text.slice(0, 240) : null,
13233
+ };
13234
+ }, payload).catch((error) => ({
13235
+ selector: payload.selector,
13236
+ text: payload.text || null,
13237
+ pattern: payload.pattern || null,
13238
+ selector_count: 0,
13239
+ visible_count: 0,
13240
+ matched_count: 0,
13241
+ matched: false,
13242
+ sample: null,
13243
+ error: String(error && error.message ? error.message : error).slice(0, 500),
13244
+ }));
13245
+ }
13246
+ return page.evaluate((input) => {
13247
+ const compact = (value) => String(value || "").replace(/\s+/g, " ").trim();
13248
+ const sample = compact(document.body ? document.body.innerText || document.body.textContent || "" : "");
13249
+ let matched = false;
13250
+ if (input.pattern) {
13251
+ try { matched = new RegExp(input.pattern, input.flags || "").test(sample); } catch { matched = false; }
13252
+ } else {
13253
+ matched = sample.includes(input.text || "");
13254
+ }
13255
+ return {
13256
+ selector: null,
13257
+ text: input.text || null,
13258
+ pattern: input.pattern || null,
13259
+ matched,
13260
+ matched_count: matched ? 1 : 0,
13261
+ sample: sample.slice(0, 240),
13262
+ };
13263
+ }, payload).catch((error) => ({
13264
+ selector: null,
13265
+ text: payload.text || null,
13266
+ pattern: payload.pattern || null,
13267
+ matched: false,
13268
+ matched_count: 0,
13269
+ sample: null,
13270
+ error: String(error && error.message ? error.message : error).slice(0, 500),
13271
+ }));
13272
+ }
13273
+ async function observeWithin(check) {
13274
+ const timeoutMs = observeWithinTimeoutMs(check);
13275
+ const startedAt = Date.now();
13276
+ let attempts = 0;
13277
+ let last = null;
13278
+ while (true) {
13279
+ attempts += 1;
13280
+ last = await observeWithinSnapshot(check);
13281
+ const elapsedMs = Date.now() - startedAt;
13282
+ if (last && last.matched === true) {
13283
+ return { ...last, timeout_ms: timeoutMs, elapsed_ms: elapsedMs, attempts };
13284
+ }
13285
+ if (elapsedMs >= timeoutMs) {
13286
+ return { ...(last || {}), matched: false, timeout_ms: timeoutMs, elapsed_ms: elapsedMs, attempts };
13287
+ }
13288
+ await page.waitForTimeout(Math.min(100, Math.max(25, timeoutMs - elapsedMs)));
13289
+ }
13290
+ }
13105
13291
  function linkProbeMaxLinks(check) {
13106
13292
  const value = Number(check.max_links || check.maxLinks || check.limit || 100);
13107
13293
  return Number.isInteger(value) && value > 0 ? Math.min(value, 500) : 100;
@@ -14154,6 +14340,7 @@ async function captureViewport(viewport) {
14154
14340
  const text_matches = {};
14155
14341
  const text_match_samples = {};
14156
14342
  const text_case_insensitive_samples = {};
14343
+ const observations = {};
14157
14344
  const http_statuses = {};
14158
14345
  const link_statuses = {};
14159
14346
  for (const check of profile.checks || []) {
@@ -14174,6 +14361,10 @@ async function captureViewport(viewport) {
14174
14361
  selectors[check.selector] = selectors[check.selector] || await selectorStats(check.selector);
14175
14362
  text_sequences[check.selector] = await selectorTextSequence(check.selector);
14176
14363
  }
14364
+ if (check.type === "observe_within") {
14365
+ const key = observeWithinKey(check);
14366
+ observations[key] = observations[key] || await observeWithin(check);
14367
+ }
14177
14368
  if ((check.type === "text_visible" || check.type === "text_absent") && (check.text || check.pattern)) {
14178
14369
  const key = textKey(check);
14179
14370
  const sample = dom.body_text || dom.body_text_sample || "";
@@ -14267,6 +14458,7 @@ async function captureViewport(viewport) {
14267
14458
  text_matches,
14268
14459
  text_match_samples,
14269
14460
  text_case_insensitive_samples,
14461
+ observations,
14270
14462
  http_statuses,
14271
14463
  link_statuses,
14272
14464
  route_inventory: routeInventory,
@@ -14872,6 +15064,7 @@ function profileRiddleJobMarkdown(result) {
14872
15064
  const attempts = cliFiniteNumber(riddle.attempts);
14873
15065
  const submittedAt = cliString(riddle.submitted_at);
14874
15066
  const completedAt = cliString(riddle.completed_at);
15067
+ const artifactRecovery = riddle.artifact_recovery === true;
14875
15068
  const parts = [
14876
15069
  mode ? `mode ${markdownInlineCode(mode)}` : "",
14877
15070
  jobCount === void 0 ? "" : `jobs ${jobCount}`,
@@ -14888,6 +15081,9 @@ function profileRiddleJobMarkdown(result) {
14888
15081
  if (submittedAt || completedAt) {
14889
15082
  lines.push(`- timing:${submittedAt ? ` submitted ${markdownInlineCode(submittedAt)}` : ""}${completedAt ? ` completed ${markdownInlineCode(completedAt)}` : ""}`);
14890
15083
  }
15084
+ if (artifactRecovery) {
15085
+ lines.push("- artifact recovery: used artifacts endpoint after non-terminal poll");
15086
+ }
14891
15087
  const splitJobs = Array.isArray(riddle.split_jobs) ? riddle.split_jobs.map(cliRecord).filter((job) => Boolean(job)) : [];
14892
15088
  for (const job of splitJobs.slice(0, 12)) {
14893
15089
  const viewport = cliString(job.viewport) || "viewport";
@@ -14896,13 +15092,15 @@ function profileRiddleJobMarkdown(result) {
14896
15092
  const splitTerminal = typeof job.terminal === "boolean" ? job.terminal : void 0;
14897
15093
  const splitElapsedMs = cliFiniteNumber(job.elapsed_ms);
14898
15094
  const splitPreSubmissionElapsedMs = cliFiniteNumber(job.pre_submission_elapsed_ms);
15095
+ const splitArtifactRecovery = job.artifact_recovery === true;
14899
15096
  lines.push(
14900
15097
  `- ${viewport}: ${[
14901
15098
  splitJobId ? `job ${markdownInlineCode(splitJobId)}` : "",
14902
15099
  splitStatus ? `status ${markdownInlineCode(splitStatus)}` : "",
14903
15100
  splitTerminal === void 0 ? "" : `terminal ${splitTerminal ? "true" : "false"}`,
14904
15101
  splitElapsedMs === void 0 ? "" : `elapsed ${formatPollDuration(splitElapsedMs)}`,
14905
- splitPreSubmissionElapsedMs === void 0 || splitPreSubmissionElapsedMs < 1e3 ? "" : `pre-submit ${formatPollDuration(splitPreSubmissionElapsedMs)}`
15102
+ splitPreSubmissionElapsedMs === void 0 || splitPreSubmissionElapsedMs < 1e3 ? "" : `pre-submit ${formatPollDuration(splitPreSubmissionElapsedMs)}`,
15103
+ splitArtifactRecovery ? "artifact recovery" : ""
14906
15104
  ].filter(Boolean).join(", ") || "job metadata unavailable"}`
14907
15105
  );
14908
15106
  }
@@ -14949,6 +15147,14 @@ function profileCheckMarkdownTarget(check) {
14949
15147
  if (check.type === "selector_text_order") {
14950
15148
  return selector ? `${markdownInlineCode(selector)} text order` : void 0;
14951
15149
  }
15150
+ if (check.type === "observe_within") {
15151
+ const textTarget = profileCheckTextTarget(evidence);
15152
+ const timeoutMs = cliFiniteNumber(evidence.timeout_ms);
15153
+ const withinLabel = timeoutMs === void 0 ? "within timeout" : `within ${timeoutMs}ms`;
15154
+ if (selector && textTarget) return `${markdownInlineCode(selector)} observes ${textTarget} ${withinLabel}`;
15155
+ if (selector) return `${markdownInlineCode(selector)} visible ${withinLabel}`;
15156
+ return textTarget ? `${textTarget} ${withinLabel}` : withinLabel;
15157
+ }
14952
15158
  if (check.type === "text_visible" || check.type === "text_absent") {
14953
15159
  return profileCheckTextTarget(evidence);
14954
15160
  }
@@ -15534,7 +15740,8 @@ function withRiddleMetadata(result, input) {
15534
15740
  elapsed_ms: poll?.elapsed_ms ?? result.riddle?.elapsed_ms,
15535
15741
  attempt: poll?.attempt ?? result.riddle?.attempt,
15536
15742
  attempts: poll?.attempts ?? result.riddle?.attempts,
15537
- timed_out: poll?.timed_out ?? result.riddle?.timed_out
15743
+ timed_out: poll?.timed_out ?? result.riddle?.timed_out,
15744
+ artifact_recovery: input.artifactRecovery ?? result.riddle?.artifact_recovery
15538
15745
  },
15539
15746
  artifacts: {
15540
15747
  ...result.artifacts,
@@ -15542,6 +15749,52 @@ function withRiddleMetadata(result, input) {
15542
15749
  }
15543
15750
  };
15544
15751
  }
15752
+ function riddleArtifactsPayloadStatus(payload) {
15753
+ const record = cliRecord(payload);
15754
+ return cliString(record?.status) ?? cliString(cliRecord(record?.job)?.status);
15755
+ }
15756
+ async function recoverProfileResultFromRiddleArtifacts(profile, input) {
15757
+ if (input.poll.poll?.timed_out !== true) return void 0;
15758
+ let artifactPayload;
15759
+ try {
15760
+ artifactPayload = await input.client.requestJson(`/v1/jobs/${input.jobId}/artifacts`);
15761
+ } catch {
15762
+ return void 0;
15763
+ }
15764
+ const artifacts = collectRiddleProfileArtifactRefs(artifactPayload);
15765
+ if (!artifacts.length) return void 0;
15766
+ const artifactStatus = riddleArtifactsPayloadStatus(artifactPayload);
15767
+ const terminal = artifactStatus ? isTerminalRiddleJobStatus(artifactStatus) : true;
15768
+ const recoveredPoll = input.poll.poll ? {
15769
+ ...input.poll.poll,
15770
+ status: artifactStatus ?? input.poll.poll.status,
15771
+ terminal
15772
+ } : void 0;
15773
+ const artifactResult = await profileResultFromRiddleArtifacts(profile, artifacts, [artifactPayload, input.poll.job]);
15774
+ if (artifactResult) {
15775
+ return withRiddleMetadata(artifactResult, {
15776
+ job_id: input.jobId,
15777
+ status: artifactStatus ?? input.poll.status,
15778
+ terminal,
15779
+ poll: recoveredPoll,
15780
+ artifacts,
15781
+ artifactRecovery: true
15782
+ });
15783
+ }
15784
+ if (!terminal) return void 0;
15785
+ return createRiddleProofProfileInsufficientResult({
15786
+ profile,
15787
+ runner: input.runner,
15788
+ error: `Riddle job ${input.jobId} timed out in status ${input.poll.status || "unknown"}, but artifacts were recovered without a proof result.`,
15789
+ riddle: {
15790
+ ...riddleMetadataFromPoll(input.jobId, input.poll),
15791
+ status: artifactStatus ?? input.poll.status,
15792
+ terminal,
15793
+ artifact_recovery: true
15794
+ },
15795
+ artifacts
15796
+ });
15797
+ }
15545
15798
  function riddleMetadataFromPoll(jobId, poll) {
15546
15799
  return {
15547
15800
  job_id: jobId,
@@ -15604,13 +15857,15 @@ function splitViewportRiddleMetadata(childRuns) {
15604
15857
  elapsed_ms: result.riddle?.elapsed_ms,
15605
15858
  attempt: result.riddle?.attempt,
15606
15859
  attempts: result.riddle?.attempts,
15607
- timed_out: result.riddle?.timed_out
15860
+ timed_out: result.riddle?.timed_out,
15861
+ artifact_recovery: result.riddle?.artifact_recovery
15608
15862
  }));
15609
15863
  return {
15610
15864
  mode: "split-viewports",
15611
15865
  job_count: childRuns.length,
15612
15866
  status: "split-viewports",
15613
15867
  terminal: childRuns.every(({ result }) => result.riddle?.terminal !== false),
15868
+ artifact_recovery: childRuns.some(({ result }) => result.riddle?.artifact_recovery === true),
15614
15869
  queue_elapsed_ms: sumDefinedNumbers(splitJobs.map((job) => job.queue_elapsed_ms)),
15615
15870
  pre_submission_elapsed_ms: sumDefinedNumbers(splitJobs.map((job) => job.pre_submission_elapsed_ms)),
15616
15871
  elapsed_ms: sumDefinedNumbers(splitJobs.map((job) => job.elapsed_ms)),
@@ -15733,6 +15988,13 @@ async function runSingleRiddleProfileForCli(profile, options, input) {
15733
15988
  });
15734
15989
  const artifacts = collectRiddleProfileArtifactRefs(poll.artifacts);
15735
15990
  if (!poll.ok || !poll.terminal) {
15991
+ const recoveredResult = await recoverProfileResultFromRiddleArtifacts(profile, {
15992
+ client,
15993
+ runner,
15994
+ jobId,
15995
+ poll
15996
+ });
15997
+ if (recoveredResult) return recoveredResult;
15736
15998
  return createRiddleProofProfileEnvironmentBlockedResult({
15737
15999
  profile,
15738
16000
  runner,
package/dist/cli.js CHANGED
@@ -13,9 +13,10 @@ import {
13
13
  profileStatusExitCode,
14
14
  resolveRiddleProofProfileTargetUrl,
15
15
  resolveRiddleProofProfileTimeoutSec
16
- } from "./chunk-QXQCG3WB.js";
16
+ } from "./chunk-QJJ3ISMK.js";
17
17
  import {
18
18
  createRiddleApiClient,
19
+ isTerminalRiddleJobStatus,
19
20
  parseRiddleViewport
20
21
  } from "./chunk-M3ZTY6PQ.js";
21
22
  import {
@@ -430,6 +431,7 @@ function profileRiddleJobMarkdown(result) {
430
431
  const attempts = cliFiniteNumber(riddle.attempts);
431
432
  const submittedAt = cliString(riddle.submitted_at);
432
433
  const completedAt = cliString(riddle.completed_at);
434
+ const artifactRecovery = riddle.artifact_recovery === true;
433
435
  const parts = [
434
436
  mode ? `mode ${markdownInlineCode(mode)}` : "",
435
437
  jobCount === void 0 ? "" : `jobs ${jobCount}`,
@@ -446,6 +448,9 @@ function profileRiddleJobMarkdown(result) {
446
448
  if (submittedAt || completedAt) {
447
449
  lines.push(`- timing:${submittedAt ? ` submitted ${markdownInlineCode(submittedAt)}` : ""}${completedAt ? ` completed ${markdownInlineCode(completedAt)}` : ""}`);
448
450
  }
451
+ if (artifactRecovery) {
452
+ lines.push("- artifact recovery: used artifacts endpoint after non-terminal poll");
453
+ }
449
454
  const splitJobs = Array.isArray(riddle.split_jobs) ? riddle.split_jobs.map(cliRecord).filter((job) => Boolean(job)) : [];
450
455
  for (const job of splitJobs.slice(0, 12)) {
451
456
  const viewport = cliString(job.viewport) || "viewport";
@@ -454,13 +459,15 @@ function profileRiddleJobMarkdown(result) {
454
459
  const splitTerminal = typeof job.terminal === "boolean" ? job.terminal : void 0;
455
460
  const splitElapsedMs = cliFiniteNumber(job.elapsed_ms);
456
461
  const splitPreSubmissionElapsedMs = cliFiniteNumber(job.pre_submission_elapsed_ms);
462
+ const splitArtifactRecovery = job.artifact_recovery === true;
457
463
  lines.push(
458
464
  `- ${viewport}: ${[
459
465
  splitJobId ? `job ${markdownInlineCode(splitJobId)}` : "",
460
466
  splitStatus ? `status ${markdownInlineCode(splitStatus)}` : "",
461
467
  splitTerminal === void 0 ? "" : `terminal ${splitTerminal ? "true" : "false"}`,
462
468
  splitElapsedMs === void 0 ? "" : `elapsed ${formatPollDuration(splitElapsedMs)}`,
463
- splitPreSubmissionElapsedMs === void 0 || splitPreSubmissionElapsedMs < 1e3 ? "" : `pre-submit ${formatPollDuration(splitPreSubmissionElapsedMs)}`
469
+ splitPreSubmissionElapsedMs === void 0 || splitPreSubmissionElapsedMs < 1e3 ? "" : `pre-submit ${formatPollDuration(splitPreSubmissionElapsedMs)}`,
470
+ splitArtifactRecovery ? "artifact recovery" : ""
464
471
  ].filter(Boolean).join(", ") || "job metadata unavailable"}`
465
472
  );
466
473
  }
@@ -507,6 +514,14 @@ function profileCheckMarkdownTarget(check) {
507
514
  if (check.type === "selector_text_order") {
508
515
  return selector ? `${markdownInlineCode(selector)} text order` : void 0;
509
516
  }
517
+ if (check.type === "observe_within") {
518
+ const textTarget = profileCheckTextTarget(evidence);
519
+ const timeoutMs = cliFiniteNumber(evidence.timeout_ms);
520
+ const withinLabel = timeoutMs === void 0 ? "within timeout" : `within ${timeoutMs}ms`;
521
+ if (selector && textTarget) return `${markdownInlineCode(selector)} observes ${textTarget} ${withinLabel}`;
522
+ if (selector) return `${markdownInlineCode(selector)} visible ${withinLabel}`;
523
+ return textTarget ? `${textTarget} ${withinLabel}` : withinLabel;
524
+ }
510
525
  if (check.type === "text_visible" || check.type === "text_absent") {
511
526
  return profileCheckTextTarget(evidence);
512
527
  }
@@ -1092,7 +1107,8 @@ function withRiddleMetadata(result, input) {
1092
1107
  elapsed_ms: poll?.elapsed_ms ?? result.riddle?.elapsed_ms,
1093
1108
  attempt: poll?.attempt ?? result.riddle?.attempt,
1094
1109
  attempts: poll?.attempts ?? result.riddle?.attempts,
1095
- timed_out: poll?.timed_out ?? result.riddle?.timed_out
1110
+ timed_out: poll?.timed_out ?? result.riddle?.timed_out,
1111
+ artifact_recovery: input.artifactRecovery ?? result.riddle?.artifact_recovery
1096
1112
  },
1097
1113
  artifacts: {
1098
1114
  ...result.artifacts,
@@ -1100,6 +1116,52 @@ function withRiddleMetadata(result, input) {
1100
1116
  }
1101
1117
  };
1102
1118
  }
1119
+ function riddleArtifactsPayloadStatus(payload) {
1120
+ const record = cliRecord(payload);
1121
+ return cliString(record?.status) ?? cliString(cliRecord(record?.job)?.status);
1122
+ }
1123
+ async function recoverProfileResultFromRiddleArtifacts(profile, input) {
1124
+ if (input.poll.poll?.timed_out !== true) return void 0;
1125
+ let artifactPayload;
1126
+ try {
1127
+ artifactPayload = await input.client.requestJson(`/v1/jobs/${input.jobId}/artifacts`);
1128
+ } catch {
1129
+ return void 0;
1130
+ }
1131
+ const artifacts = collectRiddleProfileArtifactRefs(artifactPayload);
1132
+ if (!artifacts.length) return void 0;
1133
+ const artifactStatus = riddleArtifactsPayloadStatus(artifactPayload);
1134
+ const terminal = artifactStatus ? isTerminalRiddleJobStatus(artifactStatus) : true;
1135
+ const recoveredPoll = input.poll.poll ? {
1136
+ ...input.poll.poll,
1137
+ status: artifactStatus ?? input.poll.poll.status,
1138
+ terminal
1139
+ } : void 0;
1140
+ const artifactResult = await profileResultFromRiddleArtifacts(profile, artifacts, [artifactPayload, input.poll.job]);
1141
+ if (artifactResult) {
1142
+ return withRiddleMetadata(artifactResult, {
1143
+ job_id: input.jobId,
1144
+ status: artifactStatus ?? input.poll.status,
1145
+ terminal,
1146
+ poll: recoveredPoll,
1147
+ artifacts,
1148
+ artifactRecovery: true
1149
+ });
1150
+ }
1151
+ if (!terminal) return void 0;
1152
+ return createRiddleProofProfileInsufficientResult({
1153
+ profile,
1154
+ runner: input.runner,
1155
+ error: `Riddle job ${input.jobId} timed out in status ${input.poll.status || "unknown"}, but artifacts were recovered without a proof result.`,
1156
+ riddle: {
1157
+ ...riddleMetadataFromPoll(input.jobId, input.poll),
1158
+ status: artifactStatus ?? input.poll.status,
1159
+ terminal,
1160
+ artifact_recovery: true
1161
+ },
1162
+ artifacts
1163
+ });
1164
+ }
1103
1165
  function riddleMetadataFromPoll(jobId, poll) {
1104
1166
  return {
1105
1167
  job_id: jobId,
@@ -1162,13 +1224,15 @@ function splitViewportRiddleMetadata(childRuns) {
1162
1224
  elapsed_ms: result.riddle?.elapsed_ms,
1163
1225
  attempt: result.riddle?.attempt,
1164
1226
  attempts: result.riddle?.attempts,
1165
- timed_out: result.riddle?.timed_out
1227
+ timed_out: result.riddle?.timed_out,
1228
+ artifact_recovery: result.riddle?.artifact_recovery
1166
1229
  }));
1167
1230
  return {
1168
1231
  mode: "split-viewports",
1169
1232
  job_count: childRuns.length,
1170
1233
  status: "split-viewports",
1171
1234
  terminal: childRuns.every(({ result }) => result.riddle?.terminal !== false),
1235
+ artifact_recovery: childRuns.some(({ result }) => result.riddle?.artifact_recovery === true),
1172
1236
  queue_elapsed_ms: sumDefinedNumbers(splitJobs.map((job) => job.queue_elapsed_ms)),
1173
1237
  pre_submission_elapsed_ms: sumDefinedNumbers(splitJobs.map((job) => job.pre_submission_elapsed_ms)),
1174
1238
  elapsed_ms: sumDefinedNumbers(splitJobs.map((job) => job.elapsed_ms)),
@@ -1291,6 +1355,13 @@ async function runSingleRiddleProfileForCli(profile, options, input) {
1291
1355
  });
1292
1356
  const artifacts = collectRiddleProfileArtifactRefs(poll.artifacts);
1293
1357
  if (!poll.ok || !poll.terminal) {
1358
+ const recoveredResult = await recoverProfileResultFromRiddleArtifacts(profile, {
1359
+ client,
1360
+ runner,
1361
+ jobId,
1362
+ poll
1363
+ });
1364
+ if (recoveredResult) return recoveredResult;
1294
1365
  return createRiddleProofProfileEnvironmentBlockedResult({
1295
1366
  profile,
1296
1367
  runner,