@riddledc/riddle-proof 0.7.148 → 0.7.150

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 CHANGED
@@ -605,6 +605,26 @@ The check records visible text for the selector in each viewport and reports
605
605
  matched counts plus short samples, which makes generated-command and evidence
606
606
  card audits easier to diagnose than global `text_visible` checks.
607
607
 
608
+ Use `observe_within` when the proof needs to catch a short-lived user-visible
609
+ state after setup actions, such as a combo badge, damage flash, transient
610
+ particle count, toast, or canvas-adjacent HUD update:
611
+
612
+ ```json
613
+ {
614
+ "type": "observe_within",
615
+ "selector": ".result-state",
616
+ "pattern": "Photon.*active",
617
+ "timeout_ms": 1500
618
+ }
619
+ ```
620
+
621
+ With `selector` plus `text` or `pattern`, the runner polls visible selector
622
+ text until it matches. With only `selector`, it polls for a visible matching
623
+ element. With only `text` or `pattern`, it polls the rendered page body. The
624
+ proof evidence records per-viewport match status, elapsed time, attempts,
625
+ selector counts when applicable, and a compact sample. `within_ms` is accepted
626
+ as an alias for `timeout_ms`; the default timeout is `2000`.
627
+
608
628
  Use `http_status` when the contract belongs to the fetched response itself:
609
629
  status code, content type, byte size, or raw body fragments from a markdown,
610
630
  JSON, YAML, robots, sitemap, or other machine-readable endpoint:
@@ -26,6 +26,7 @@ var RIDDLE_PROOF_PROFILE_CHECK_TYPES = [
26
26
  "selector_text_visible",
27
27
  "selector_text_absent",
28
28
  "selector_text_order",
29
+ "observe_within",
29
30
  "frame_text_visible",
30
31
  "frame_url_equals",
31
32
  "frame_url_matches",
@@ -497,6 +498,22 @@ function profileSetupWindowEvalReceipts(results) {
497
498
  return receipt;
498
499
  });
499
500
  }
501
+ function profileSetupRangeValueReceipts(results) {
502
+ return results.filter((result) => profileSetupResultAction(result) === "set_range_value").map((result) => ({
503
+ ordinal: result.ordinal ?? null,
504
+ ok: result.ok !== false,
505
+ selector: result.selector ?? null,
506
+ frame_selector: result.frame_selector ?? null,
507
+ requested_value: result.requested_value ?? null,
508
+ actual_value: result.actual_value ?? null,
509
+ before_value: result.before_value ?? null,
510
+ value_as_number: result.value_as_number ?? null,
511
+ min: result.min ?? null,
512
+ max: result.max ?? null,
513
+ step: result.step ?? null,
514
+ reason: result.reason ?? result.error ?? null
515
+ }));
516
+ }
500
517
  function sampleProfileSetupSummaryItems(items, limit) {
501
518
  if (items.length <= limit) return items;
502
519
  const firstCount = Math.floor(limit / 2);
@@ -539,6 +556,8 @@ function profileSetupSummary(viewports, actionCount, expectedActionCountByViewpo
539
556
  const windowEvalStoredTotal = windowEvalReceipts.filter((result) => typeof result.return_stored_to === "string" && result.return_stored_to.trim()).length;
540
557
  const windowEvalCapturedTotal = windowEvalReceipts.filter((result) => result.return_captured === true).length;
541
558
  const sampledWindowEvalReceipts = sampleProfileSetupSummaryItems(windowEvalReceipts, 8);
559
+ const rangeValueReceipts = profileSetupRangeValueReceipts(results);
560
+ const sampledRangeValueReceipts = sampleProfileSetupSummaryItems(rangeValueReceipts, 8);
542
561
  const clickedItems = results.filter((result) => profileSetupResultAction(result) === "click" && result.ok !== false).map((result) => {
543
562
  const clickCount = typeof result.click_count === "number" && Number.isFinite(result.click_count) && result.click_count > 1 ? result.click_count : void 0;
544
563
  return {
@@ -587,6 +606,9 @@ function profileSetupSummary(viewports, actionCount, expectedActionCountByViewpo
587
606
  window_eval_captured_total: windowEvalCapturedTotal,
588
607
  window_eval_truncated: windowEvalReceipts.length > sampledWindowEvalReceipts.length,
589
608
  window_eval: sampledWindowEvalReceipts,
609
+ set_range_value_total: rangeValueReceipts.length,
610
+ set_range_value_truncated: rangeValueReceipts.length > sampledRangeValueReceipts.length,
611
+ set_range_value: sampledRangeValueReceipts,
590
612
  clicked,
591
613
  text_samples,
592
614
  failed: failed.map((result) => ({
@@ -1327,7 +1349,7 @@ function normalizeCheck(input, index) {
1327
1349
  throw new Error(`checks[${index}].type ${type} is not supported. Supported checks: ${RIDDLE_PROOF_PROFILE_CHECK_TYPES.join(", ")}`);
1328
1350
  }
1329
1351
  const isDialogCountCheck = isDialogCountCheckType(type);
1330
- 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") && !stringValue(input.selector)) {
1352
+ 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" && !stringValue(input.text) && !stringValue(input.pattern)) && !stringValue(input.selector)) {
1331
1353
  throw new Error(`checks[${index}] ${type} requires selector.`);
1332
1354
  }
1333
1355
  if ((type === "frame_text_visible" || type === "frame_url_equals" || type === "frame_url_matches" || type === "frame_no_horizontal_overflow") && !stringValue(input.selector)) {
@@ -1468,7 +1490,7 @@ function normalizeCheck(input, index) {
1468
1490
  allowed_content_types: allowedContentTypes,
1469
1491
  allow_get_fallback: isLinkStatusCheck ? input.allow_get_fallback === false || input.allowGetFallback === false ? false : true : void 0,
1470
1492
  max_overflow_px: numberValue(input.max_overflow_px),
1471
- timeout_ms: numberValue(input.timeout_ms) ?? numberValue(input.timeoutMs),
1493
+ timeout_ms: numberValue(input.timeout_ms) ?? numberValue(input.timeoutMs) ?? numberValue(input.within_ms) ?? numberValue(input.withinMs),
1472
1494
  run_direct_routes: input.run_direct_routes === false || input.runDirectRoutes === false ? false : true,
1473
1495
  run_clickthroughs: input.run_clickthroughs === false || input.runClickthroughs === false ? false : true,
1474
1496
  run_all_viewports: input.run_all_viewports === true || input.runAllViewports === true,
@@ -1986,6 +2008,16 @@ function summarizeLinkStatusEvidence(viewport, check) {
1986
2008
  function textKey(check) {
1987
2009
  return check.pattern ? `pattern:${check.pattern}/${check.flags || ""}` : `text:${check.text || ""}`;
1988
2010
  }
2011
+ function observeWithinTimeoutMs(check) {
2012
+ const raw = check.timeout_ms;
2013
+ if (typeof raw === "number" && Number.isFinite(raw) && raw > 0) return Math.min(Math.round(raw), 6e4);
2014
+ return 2e3;
2015
+ }
2016
+ function observeWithinKey(check) {
2017
+ const target = check.selector ? `selector:${check.selector}` : "page";
2018
+ const expectation = check.pattern ? `pattern:${check.pattern}/${check.flags || ""}` : check.text ? `text:${check.text}` : "visible";
2019
+ return `${target}|${expectation}|within:${observeWithinTimeoutMs(check)}`;
2020
+ }
1989
2021
  function textSequenceForCheck(viewport, check) {
1990
2022
  const key = selectorKey(check);
1991
2023
  const sequence = viewport.text_sequences?.[key];
@@ -2492,6 +2524,40 @@ function assessCheckFromEvidence(check, evidence) {
2492
2524
  message: failed ? `Selector ${key} text order failed in ${failed} viewport(s).` : void 0
2493
2525
  };
2494
2526
  }
2527
+ if (check.type === "observe_within") {
2528
+ const key = observeWithinKey(check);
2529
+ const timeoutMs = observeWithinTimeoutMs(check);
2530
+ const results = viewports.map((viewport) => {
2531
+ const observation = viewport.observations?.[key];
2532
+ const matched = observation?.matched === true;
2533
+ return {
2534
+ viewport: viewport.name,
2535
+ matched,
2536
+ elapsed_ms: numberValue(observation?.elapsed_ms) ?? null,
2537
+ timeout_ms: numberValue(observation?.timeout_ms) ?? timeoutMs,
2538
+ attempts: numberValue(observation?.attempts) ?? null,
2539
+ selector_count: numberValue(observation?.selector_count) ?? null,
2540
+ visible_count: numberValue(observation?.visible_count) ?? null,
2541
+ matched_count: numberValue(observation?.matched_count) ?? null,
2542
+ sample: stringValue(observation?.sample) ?? null,
2543
+ error: stringValue(observation?.error) ?? null
2544
+ };
2545
+ });
2546
+ const failed = results.filter((result) => !result.matched).length;
2547
+ return {
2548
+ type: check.type,
2549
+ label: checkLabel(check),
2550
+ status: failed ? "failed" : "passed",
2551
+ evidence: {
2552
+ selector: check.selector || null,
2553
+ text: check.text || null,
2554
+ pattern: check.pattern || null,
2555
+ timeout_ms: timeoutMs,
2556
+ viewports: results.map((result) => toJsonValue(result))
2557
+ },
2558
+ message: failed ? `Observation did not match within ${timeoutMs}ms in ${failed} viewport(s).` : void 0
2559
+ };
2560
+ }
2495
2561
  if (check.type === "frame_text_visible") {
2496
2562
  const key = selectorKey(check);
2497
2563
  const results = viewports.map((viewport) => {
@@ -3896,6 +3962,24 @@ function profileSetupWindowEvalReceipts(results) {
3896
3962
  return receipt;
3897
3963
  });
3898
3964
  }
3965
+ function profileSetupRangeValueReceipts(results) {
3966
+ return (results || [])
3967
+ .filter((result) => result && profileSetupResultAction(result) === "set_range_value")
3968
+ .map((result) => ({
3969
+ ordinal: result.ordinal ?? null,
3970
+ ok: result.ok !== false,
3971
+ selector: result.selector ?? null,
3972
+ frame_selector: result.frame_selector ?? null,
3973
+ requested_value: result.requested_value ?? null,
3974
+ actual_value: result.actual_value ?? null,
3975
+ before_value: result.before_value ?? null,
3976
+ value_as_number: result.value_as_number ?? null,
3977
+ min: result.min ?? null,
3978
+ max: result.max ?? null,
3979
+ step: result.step ?? null,
3980
+ reason: result.reason || result.error || null,
3981
+ }));
3982
+ }
3899
3983
  function sampleProfileSetupSummaryItems(items, limit) {
3900
3984
  if ((items || []).length <= limit) return items || [];
3901
3985
  const firstCount = Math.floor(limit / 2);
@@ -3952,6 +4036,8 @@ function profileSetupSummary(viewports, actionCount, expectedActionCountsByViewp
3952
4036
  const windowEvalStoredTotal = windowEvalReceipts.filter((result) => typeof result.return_stored_to === "string" && result.return_stored_to.trim()).length;
3953
4037
  const windowEvalCapturedTotal = windowEvalReceipts.filter((result) => result.return_captured === true).length;
3954
4038
  const sampledWindowEvalReceipts = sampleProfileSetupSummaryItems(windowEvalReceipts, 8);
4039
+ const rangeValueReceipts = profileSetupRangeValueReceipts(results);
4040
+ const sampledRangeValueReceipts = sampleProfileSetupSummaryItems(rangeValueReceipts, 8);
3955
4041
  const clickedItems = results
3956
4042
  .filter((result) => result && profileSetupResultAction(result) === "click" && result.ok !== false)
3957
4043
  .map((result) => {
@@ -4010,6 +4096,9 @@ function profileSetupSummary(viewports, actionCount, expectedActionCountsByViewp
4010
4096
  window_eval_captured_total: windowEvalCapturedTotal,
4011
4097
  window_eval_truncated: windowEvalReceipts.length > sampledWindowEvalReceipts.length,
4012
4098
  window_eval: sampledWindowEvalReceipts,
4099
+ set_range_value_total: rangeValueReceipts.length,
4100
+ set_range_value_truncated: rangeValueReceipts.length > sampledRangeValueReceipts.length,
4101
+ set_range_value: sampledRangeValueReceipts,
4013
4102
  clicked,
4014
4103
  text_samples: textSamples,
4015
4104
  failed: failed.map((result) => ({
@@ -4378,6 +4467,36 @@ function assessProfile(profile, evidence) {
4378
4467
  });
4379
4468
  continue;
4380
4469
  }
4470
+ if (check.type === "observe_within") {
4471
+ const key = observeWithinKey(check);
4472
+ const timeoutMs = observeWithinTimeoutMs(check);
4473
+ const results = checkViewports.map((viewport) => {
4474
+ const observation = viewport.observations && viewport.observations[key] && typeof viewport.observations[key] === "object"
4475
+ ? viewport.observations[key]
4476
+ : {};
4477
+ return {
4478
+ viewport: viewport.name,
4479
+ matched: observation.matched === true,
4480
+ elapsed_ms: typeof observation.elapsed_ms === "number" && Number.isFinite(observation.elapsed_ms) ? observation.elapsed_ms : null,
4481
+ timeout_ms: typeof observation.timeout_ms === "number" && Number.isFinite(observation.timeout_ms) ? observation.timeout_ms : timeoutMs,
4482
+ attempts: typeof observation.attempts === "number" && Number.isFinite(observation.attempts) ? observation.attempts : null,
4483
+ selector_count: typeof observation.selector_count === "number" && Number.isFinite(observation.selector_count) ? observation.selector_count : null,
4484
+ visible_count: typeof observation.visible_count === "number" && Number.isFinite(observation.visible_count) ? observation.visible_count : null,
4485
+ matched_count: typeof observation.matched_count === "number" && Number.isFinite(observation.matched_count) ? observation.matched_count : null,
4486
+ sample: typeof observation.sample === "string" && observation.sample.trim() ? observation.sample.trim() : null,
4487
+ error: typeof observation.error === "string" && observation.error.trim() ? observation.error.trim() : null,
4488
+ };
4489
+ });
4490
+ const failed = results.filter((result) => !result.matched).length;
4491
+ checks.push({
4492
+ type: check.type,
4493
+ label: check.label || check.type,
4494
+ status: failed ? "failed" : "passed",
4495
+ evidence: { selector: check.selector || null, text: check.text || null, pattern: check.pattern || null, timeout_ms: timeoutMs, viewports: results },
4496
+ message: failed ? "Observation did not match within " + timeoutMs + "ms in " + failed + " viewport(s)." : undefined,
4497
+ });
4498
+ continue;
4499
+ }
4381
4500
  if (check.type === "frame_text_visible") {
4382
4501
  const selector = check.selector || "";
4383
4502
  const results = checkViewports.map((viewport) => {
@@ -4810,6 +4929,19 @@ function ensureDialogHandler() {
4810
4929
  function textKey(check) {
4811
4930
  return check.pattern ? "pattern:" + check.pattern + "/" + (check.flags || "") : "text:" + (check.text || "");
4812
4931
  }
4932
+ function observeWithinTimeoutMs(check) {
4933
+ const raw = Number(check && check.timeout_ms);
4934
+ return Number.isFinite(raw) && raw > 0 ? Math.min(Math.round(raw), 60000) : 2000;
4935
+ }
4936
+ function observeWithinKey(check) {
4937
+ const target = check && check.selector ? "selector:" + check.selector : "page";
4938
+ const expectation = check && check.pattern
4939
+ ? "pattern:" + check.pattern + "/" + (check.flags || "")
4940
+ : check && check.text
4941
+ ? "text:" + check.text
4942
+ : "visible";
4943
+ return target + "|" + expectation + "|within:" + observeWithinTimeoutMs(check);
4944
+ }
4813
4945
  function textMatches(sample, check) {
4814
4946
  if (check.pattern) {
4815
4947
  try { return new RegExp(check.pattern, check.flags || "").test(sample || ""); } catch { return false; }
@@ -6125,6 +6257,104 @@ async function selectorTextSequence(selector) {
6125
6257
  };
6126
6258
  }).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) }));
6127
6259
  }
6260
+ async function observeWithinSnapshot(check) {
6261
+ const payload = {
6262
+ selector: check.selector || "",
6263
+ text: check.text || "",
6264
+ pattern: check.pattern || "",
6265
+ flags: check.flags || "",
6266
+ wants_text: Boolean(check.text || check.pattern),
6267
+ };
6268
+ if (payload.selector) {
6269
+ return page.locator(payload.selector).evaluateAll((elements, input) => {
6270
+ const compact = (value) => String(value || "").replace(/\s+/g, " ").trim();
6271
+ const matchText = (value) => {
6272
+ const source = compact(value);
6273
+ if (input.pattern) {
6274
+ try { return new RegExp(input.pattern, input.flags || "").test(source); } catch { return false; }
6275
+ }
6276
+ return source.includes(input.text || "");
6277
+ };
6278
+ const isVisible = (element) => {
6279
+ const style = window.getComputedStyle(element);
6280
+ const rect = element.getBoundingClientRect();
6281
+ return style && style.visibility !== "hidden" && style.display !== "none" && rect.width > 0 && rect.height > 0;
6282
+ };
6283
+ const rows = elements.map((element, index) => {
6284
+ const text = compact(element.innerText || element.textContent || "");
6285
+ const visible = isVisible(element);
6286
+ return { index, text, visible, matched: input.wants_text ? matchText(text) : visible };
6287
+ });
6288
+ const visibleRows = rows.filter((row) => row.visible);
6289
+ const matches = input.wants_text ? visibleRows.filter((row) => row.matched) : visibleRows;
6290
+ const sampleRow = matches[0] || visibleRows[0] || rows[0] || null;
6291
+ return {
6292
+ selector: input.selector,
6293
+ text: input.text || null,
6294
+ pattern: input.pattern || null,
6295
+ selector_count: rows.length,
6296
+ visible_count: visibleRows.length,
6297
+ matched_count: matches.length,
6298
+ matched: matches.length > 0,
6299
+ sample: sampleRow && sampleRow.text ? sampleRow.text.slice(0, 240) : null,
6300
+ };
6301
+ }, payload).catch((error) => ({
6302
+ selector: payload.selector,
6303
+ text: payload.text || null,
6304
+ pattern: payload.pattern || null,
6305
+ selector_count: 0,
6306
+ visible_count: 0,
6307
+ matched_count: 0,
6308
+ matched: false,
6309
+ sample: null,
6310
+ error: String(error && error.message ? error.message : error).slice(0, 500),
6311
+ }));
6312
+ }
6313
+ return page.evaluate((input) => {
6314
+ const compact = (value) => String(value || "").replace(/\s+/g, " ").trim();
6315
+ const sample = compact(document.body ? document.body.innerText || document.body.textContent || "" : "");
6316
+ let matched = false;
6317
+ if (input.pattern) {
6318
+ try { matched = new RegExp(input.pattern, input.flags || "").test(sample); } catch { matched = false; }
6319
+ } else {
6320
+ matched = sample.includes(input.text || "");
6321
+ }
6322
+ return {
6323
+ selector: null,
6324
+ text: input.text || null,
6325
+ pattern: input.pattern || null,
6326
+ matched,
6327
+ matched_count: matched ? 1 : 0,
6328
+ sample: sample.slice(0, 240),
6329
+ };
6330
+ }, payload).catch((error) => ({
6331
+ selector: null,
6332
+ text: payload.text || null,
6333
+ pattern: payload.pattern || null,
6334
+ matched: false,
6335
+ matched_count: 0,
6336
+ sample: null,
6337
+ error: String(error && error.message ? error.message : error).slice(0, 500),
6338
+ }));
6339
+ }
6340
+ async function observeWithin(check) {
6341
+ const timeoutMs = observeWithinTimeoutMs(check);
6342
+ const startedAt = Date.now();
6343
+ let attempts = 0;
6344
+ let last = null;
6345
+ while (true) {
6346
+ attempts += 1;
6347
+ last = await observeWithinSnapshot(check);
6348
+ const elapsedMs = Date.now() - startedAt;
6349
+ if (last && last.matched === true) {
6350
+ return { ...last, timeout_ms: timeoutMs, elapsed_ms: elapsedMs, attempts };
6351
+ }
6352
+ if (elapsedMs >= timeoutMs) {
6353
+ return { ...(last || {}), matched: false, timeout_ms: timeoutMs, elapsed_ms: elapsedMs, attempts };
6354
+ }
6355
+ await page.waitForTimeout(Math.min(100, Math.max(25, timeoutMs - elapsedMs)));
6356
+ }
6357
+ }
6128
6358
  function linkProbeMaxLinks(check) {
6129
6359
  const value = Number(check.max_links || check.maxLinks || check.limit || 100);
6130
6360
  return Number.isInteger(value) && value > 0 ? Math.min(value, 500) : 100;
@@ -7177,6 +7407,7 @@ async function captureViewport(viewport) {
7177
7407
  const text_matches = {};
7178
7408
  const text_match_samples = {};
7179
7409
  const text_case_insensitive_samples = {};
7410
+ const observations = {};
7180
7411
  const http_statuses = {};
7181
7412
  const link_statuses = {};
7182
7413
  for (const check of profile.checks || []) {
@@ -7197,6 +7428,10 @@ async function captureViewport(viewport) {
7197
7428
  selectors[check.selector] = selectors[check.selector] || await selectorStats(check.selector);
7198
7429
  text_sequences[check.selector] = await selectorTextSequence(check.selector);
7199
7430
  }
7431
+ if (check.type === "observe_within") {
7432
+ const key = observeWithinKey(check);
7433
+ observations[key] = observations[key] || await observeWithin(check);
7434
+ }
7200
7435
  if ((check.type === "text_visible" || check.type === "text_absent") && (check.text || check.pattern)) {
7201
7436
  const key = textKey(check);
7202
7437
  const sample = dom.body_text || dom.body_text_sample || "";
@@ -7290,6 +7525,7 @@ async function captureViewport(viewport) {
7290
7525
  text_matches,
7291
7526
  text_match_samples,
7292
7527
  text_case_insensitive_samples,
7528
+ observations,
7293
7529
  http_statuses,
7294
7530
  link_statuses,
7295
7531
  route_inventory: routeInventory,